Skip to content

Khái niệm cốt lõi

Hướng dẫn này giải thích các thành phần nền tảng của Fountain: singleton của ứng dụng, vòng đời của các runnable cùng thứ tự chạy, các kiểu runnable mà bạn đăng ký, các core controller cùng observer pattern của chúng, hai mô hình tích hợp thư viện, và các quy ước mà framework tuân theo. Hiểu được những khái niệm này là nền tảng cho mọi thứ khác trong framework.

Singleton Fountain

Fountain được khởi động thông qua một struct Fountain dạng singleton. Bạn lấy nó bằng New(), đăng ký các thành phần muốn chạy bằng các builder method theo phong cách fluent, rồi khởi chạy vòng đời bằng Serving(). New() luôn trả về cùng một instance ở mọi lần gọi (nó được khởi tạo một lần duy nhất qua sync.Once), nên mọi cấu hình bạn tích lũy qua các builder method đều áp dụng cho một ứng dụng dùng chung.

Các builder method đăng ký bốn kiểu runnable và trả về *Fountain để có thể nối chuỗi lời gọi:

  • WithAppInstances(appInstances ...runnable.AppInstance) — các server chạy lâu dài.
  • WithInvokers(invokers ...runnable.Invoker) — các hàm khởi tạo chạy một lần.
  • WithJobs(jobs ...runnable.JobInstance) — các job chạy một lần.
  • WithCrons(crons ...runnable.JobInstance) — các job chạy theo lịch.

Serving() là điểm vào dạng blocking, chạy các thành phần đã đăng ký theo thứ tự và giữ tiến trình hoạt động cho đến khi nhận tín hiệu tắt.

go
fountain.New().
    WithAppInstances(httpServer, grpcServer).
    WithInvokers(initDB).
    WithCrons(dailyReport).
    Serving()

Vòng đời & thứ tự chạy

Khi bạn gọi Serving(), Fountain chạy các kiểu đã đăng ký theo một thứ tự cố định:

Invokers → Jobs → Crons → AppInstances → Auxiliary

  1. Invokers chạy đầu tiên. Chúng thực thi song song khi cờ concurrency bật, ngược lại thì chạy tuần tự.
  2. Jobs chạy kế tiếp, tuân theo cùng quy tắc concurrency như invokers.
  3. Crons chạy sau đó. Khi có crons, cả hai cờ concurrencyhang sẽ tự động được bật để chúng chạy song song và giữ ứng dụng hoạt động.
  4. AppInstances sau đó được khởi tạo và phục vụ. concurrencyhang cũng được bật ở đây để các server có thể chạy song song và giữ tiến trình hoạt động liên tục.
  5. Auxiliary server (metrics / governance) khởi chạy khi hang được bật, giữ ứng dụng hoạt động cho đến khi nhận được tín hiệu tắt (shutdownSignals, ví dụ SIGTERM / SIGINT).

Hai cờ chi phối hành vi này:

  • concurrency — ảnh hưởng tới invokers và jobs. Khi bật, các thành phần này được khởi chạy trong các goroutine (phối hợp bằng sync.WaitGroup) và chạy song song. Khi tắt, chúng chạy tuần tự: mỗi thành phần phải hoàn tất trước khi thành phần kế tiếp bắt đầu.
  • hang — giữ ứng dụng ở trạng thái chờ thay vì thoát ngay sau khi công việc hoàn tất. Nó thường đi kèm với concurrency để tiến trình chờ tín hiệu tắt từ hệ thống. Khi hang tắt, Serving() sẽ không chờ và thoát ngay khi công việc đã đăng ký hoàn thành — phù hợp cho các tác vụ ngắn và nhanh gọn.

Khi tắt, Fountain chạy các hàm beforeStopClean, dừng jobs và crons, hủy các instance, rồi chạy các hàm afterStopClean.

Các kiểu runnable

Các thành phần bạn đăng ký hiện thực các interface được định nghĩa trong package runnable.

AppInstance

AppInstance là một server chạy lâu dài (server HTTP, gRPC, TCP, UDP, hoặc QUIC). Fountain khởi tạo nó, chạy Serving() trong một goroutine, và hủy nó khi tắt. Interface như sau:

go
// AppInstance type;
type AppInstance interface {
    Initialize() error
    // Serving was called in a goroutine
    Serving()
    Destroy() error
    OnConfigChange()
    Info() ServerInstance
}

Initialize() chuẩn bị instance, Serving() chạy server (được gọi trong một goroutine, nên có thể blocking), Destroy() giải phóng tài nguyên khi tắt, OnConfigChange() là callback được kích hoạt khi cấu hình thay đổi, và Info() trả về metadata ServerInstance (tên, ID, protocol, address, v.v.) được dùng cho những việc như service discovery.

Invoker

Invoker là một hàm khởi tạo chạy một lần — kiểu runnable đơn giản nhất. Nó chỉ là một hàm trả về error:

go
type Invoker func() error

Invokers chạy đầu tiên trong vòng đời, khiến chúng là nơi phù hợp cho công việc thiết lập như kết nối tới database hoặc làm nóng (warm) cache.

JobInstance

JobInstance là một đơn vị công việc chạy một lần hoặc theo lịch. Cả WithJobs(...) (job chạy một lần) và WithCrons(...) (job theo lịch) đều nhận các giá trị JobInstance. Interface như sau:

go
// JobInstance type;
type JobInstance interface {
    Start() error
    GetName() string
    Stop() error
}

Start() chạy job, GetName() định danh nó, và Stop() được gọi trong quá trình tắt.

Core controller & observer pattern

Logic nghiệp vụ trong Fountain được tổ chức thành các core controller. Một controller hiện thực interface CoreController từ package core:

go
// CoreController interface
type CoreController interface {
    InstallController()
    RegisterCallback(cb any)
}

Bạn đăng ký các controller bằng core.RegisterCoreController(&MyController{}), rồi kết nối chúng bằng core.InstallCoreControllers() một khi các phụ thuộc của chúng (database, cache, v.v.) đã sẵn sàng. Trong quá trình cài đặt, Fountain gọi InstallController() trên từng controller, rồi gọi RegisterCallback(...) trên mỗi controller cho từng controller khác, cho phép các controller khám phá lẫn nhau.

go
core.RegisterCoreController(&UserController{})
core.RegisterCoreController(&OrderController{})

// after databases, caches, etc. are installed:
core.InstallCoreControllers()

Các controller giao tiếp thông qua một observer pattern. Một controller muốn nhận sự kiện sẽ hiện thực interface generic CoreControllerObserver[T] (một method Update(event EventType, params ...T)). Để phát một sự kiện tới tất cả các controller quan sát kiểu đó, hãy gọi core.NotifyObservers(...):

go
core.NotifyObservers[*User](EventUserCreated, user)

NotifyObservers duyệt qua các controller đã đăng ký và gọi Update(...) trên bất kỳ controller nào hiện thực interface observer khớp, giữ cho các controller tách rời (decoupled) mà vẫn có thể phản ứng với sự kiện của nhau.

Mô hình tích hợp

Các thư viện hạ tầng (server, database, cache, broker, client) được cài đặt vào instance Fountain thông qua một trong hai mô hình.

Mô hình config-key — gắn thành phần với một key lồng nhau trong config.yaml rồi cài đặt nó:

go
client := fedis.WithConfigKey("redis").InstallFountainInstance()

Mô hình functional-options — cấu hình thành phần ngay tại chỗ bằng các hàm option With*:

go
server := fgrpc.WithConfigKey("fgrpc").
    InstallFountainInstance(fgrpc.WithServerOptions(serverOptions...))

Hai mô hình có thể kết hợp: một dạng phổ biến là chọn config key bằng WithConfigKey(...) rồi truyền các functional option vào InstallFountainInstance(...). Cấu hình nằm trong config.yaml với các key lồng nhau, và biến môi trường dùng tiền tố FOUNTAIN_.

Quy ước

Fountain tuân theo một tập quy ước nhất quán trong toàn bộ codebase:

  • Đặt tên package — các package tiện ích/thư viện dùng tiền tố f*: flog, fedis, fhttp, fgrpc, v.v. Các thư mục protocol client dùng f<protocol>_client/.
  • Hằng số — các hằng số toàn cục dùng tiền tố K ở dạng UPPER_SNAKE_CASE (ví dụ KPackageName, KServerKindProvider).
  • Thứ tự import — nhóm các import theo: thư viện chuẩn → bên thứ ba → gitlab.soludian.com/soludian/fountain/... → các package local.
  • Logging — dùng flog, không dùng package log của thư viện chuẩn. Lấy logger bằng flog.NewFountainLoggerOnce().
  • Lỗi — dùng các helper trong libs/ferr/ để bao gói (wrap) và xử lý lỗi.