mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-02 06:56:36 +08:00
feat(core): implement app profile lifecycle dispatcher and wire cli commands
This commit is contained in:
+5
-50
@@ -6,15 +6,9 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
gwrunner "github.com/Rain-kl/Wavelet/internal/apps/message_gateway/runner"
|
||||
"github.com/Rain-kl/Wavelet/internal/infra/task/scheduler"
|
||||
"github.com/Rain-kl/Wavelet/internal/infra/task/worker"
|
||||
"github.com/Rain-kl/Wavelet/internal/platform/bootstrap"
|
||||
"github.com/Rain-kl/Wavelet/internal/router"
|
||||
"github.com/Rain-kl/Wavelet/core"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -22,48 +16,9 @@ var allCmd = &cobra.Command{
|
||||
Use: "all",
|
||||
Short: "以融合模式同时启动 API、Worker 和 Scheduler",
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
log.Println("[All] 融合模式启动")
|
||||
bootstrap.RegisterAll()
|
||||
runBootstrap(bootstrap.Options{API: true})
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// 启动 API HTTP 服务
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
log.Println("[All] 启动 API 服务")
|
||||
router.Serve(func() {
|
||||
printStartupBanner(startupState{mode: "API + Worker + Scheduler", relationalDB: latestMigrationState.relationalDB, clickHouseDB: latestMigrationState.clickHouseDB, listensForHTTP: true})
|
||||
})
|
||||
}()
|
||||
|
||||
go func() {
|
||||
if err := gwrunner.Start(context.Background()); err != nil {
|
||||
log.Printf("[All] message gateway stopped: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// 启动 Asynq Worker 任务处理服务
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
log.Println("[All] 启动 Worker 服务")
|
||||
if err := worker.StartWorker(); err != nil {
|
||||
log.Printf("[All] Worker 启动失败: %v\n", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// 启动 Asynq 定时任务调度器
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
log.Println("[All] 启动 Scheduler 服务")
|
||||
if err := scheduler.StartScheduler(); err != nil {
|
||||
log.Printf("[All] Scheduler 启动失败: %v\n", err)
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
app := newWaveletApp(core.ProfileAll)
|
||||
if err := app.Run(); err != nil {
|
||||
log.Fatalf("[All] run failed: %v\n", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
+7
-7
@@ -5,8 +5,9 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/Rain-kl/Wavelet/internal/platform/bootstrap"
|
||||
"github.com/Rain-kl/Wavelet/internal/router"
|
||||
"log"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/core"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -14,10 +15,9 @@ var apiCmd = &cobra.Command{
|
||||
Use: "api",
|
||||
Short: "wavelet API",
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
bootstrap.RegisterAPI()
|
||||
runBootstrap(bootstrap.Options{API: true})
|
||||
router.Serve(func() {
|
||||
printStartupBanner(startupState{mode: "API", relationalDB: latestMigrationState.relationalDB, clickHouseDB: latestMigrationState.clickHouseDB, listensForHTTP: true})
|
||||
})
|
||||
app := newWaveletApp(core.ProfileAPI)
|
||||
if err := app.Run(); err != nil {
|
||||
log.Fatalf("[API] run failed: %v\n", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,308 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/core"
|
||||
"github.com/Rain-kl/Wavelet/core/extpoints"
|
||||
gwrunner "github.com/Rain-kl/Wavelet/internal/apps/message_gateway/runner"
|
||||
"github.com/Rain-kl/Wavelet/internal/infra/config"
|
||||
"github.com/Rain-kl/Wavelet/internal/infra/task/scheduler"
|
||||
"github.com/Rain-kl/Wavelet/internal/infra/task/worker"
|
||||
"github.com/Rain-kl/Wavelet/internal/platform/bootstrap"
|
||||
"github.com/Rain-kl/Wavelet/internal/router"
|
||||
"github.com/Rain-kl/Wavelet/pkg/util"
|
||||
"github.com/Rain-kl/Wavelet/plugins/infra/cache"
|
||||
"github.com/Rain-kl/Wavelet/plugins/infra/database"
|
||||
"github.com/Rain-kl/Wavelet/plugins/infra/logger"
|
||||
"github.com/Rain-kl/Wavelet/plugins/infra/storage"
|
||||
"github.com/hibiken/asynq"
|
||||
)
|
||||
|
||||
// newWaveletApp creates a core.App wired with Wavelet platform infrastructure and profile drivers.
|
||||
//
|
||||
//nolint:contextcheck
|
||||
func newWaveletApp(profile core.Profile) *core.App {
|
||||
app := core.NewApp(
|
||||
core.WithProfile(profile),
|
||||
core.WithShutdownTimeout(time.Duration(config.Config.App.GracefulShutdownTimeout)*time.Second),
|
||||
)
|
||||
|
||||
// Register standard infrastructure plugins
|
||||
app.Use(
|
||||
database.New(),
|
||||
cache.New(),
|
||||
logger.New(),
|
||||
storage.New(),
|
||||
)
|
||||
|
||||
// Bind Goose migration runner
|
||||
app.SetMigrationRunner(func(_ context.Context, _ []extpoints.MigrationEntry) error {
|
||||
runMigrations()
|
||||
return nil
|
||||
})
|
||||
|
||||
// Mount drivers for each aspect
|
||||
app.Use(
|
||||
newWaveletHTTPDriver(profile),
|
||||
newWaveletWorkerDriver(profile),
|
||||
newWaveletSchedulerDriver(profile),
|
||||
)
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
type waveletHTTPDriver struct {
|
||||
profile core.Profile
|
||||
server *http.Server
|
||||
mu sync.Mutex
|
||||
running bool
|
||||
}
|
||||
|
||||
func newWaveletHTTPDriver(profile core.Profile) *waveletHTTPDriver {
|
||||
return &waveletHTTPDriver{profile: profile}
|
||||
}
|
||||
|
||||
func (d *waveletHTTPDriver) Name() string {
|
||||
return "driver_wavelet_http"
|
||||
}
|
||||
|
||||
func (d *waveletHTTPDriver) Apply(ctx *core.Context) error {
|
||||
return ctx.RegisterDriver(d)
|
||||
}
|
||||
|
||||
func (d *waveletHTTPDriver) Type() core.DriverType {
|
||||
return core.DriverTypeHTTP
|
||||
}
|
||||
|
||||
//nolint:contextcheck
|
||||
func (d *waveletHTTPDriver) Start(ctx context.Context) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
if d.running {
|
||||
return nil
|
||||
}
|
||||
|
||||
bootstrap.RegisterAPI()
|
||||
runBootstrap(bootstrap.Options{API: true})
|
||||
|
||||
engine, err := router.BuildEngine()
|
||||
if err != nil {
|
||||
return fmt.Errorf("[API] build router engine failed: %w", err)
|
||||
}
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: config.Config.App.Addr,
|
||||
Handler: engine,
|
||||
ReadHeaderTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
listener, err := (&net.ListenConfig{}).Listen(ctx, "tcp", config.Config.App.Addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[API] listen on %s failed: %w", config.Config.App.Addr, err)
|
||||
}
|
||||
|
||||
mode := "API"
|
||||
if d.profile == core.ProfileAll {
|
||||
mode = "API + Worker + Scheduler"
|
||||
}
|
||||
printStartupBanner(startupState{
|
||||
mode: mode,
|
||||
relationalDB: latestMigrationState.relationalDB,
|
||||
clickHouseDB: latestMigrationState.clickHouseDB,
|
||||
listensForHTTP: true,
|
||||
})
|
||||
|
||||
d.server = srv
|
||||
d.running = true
|
||||
|
||||
util.Go(func() {
|
||||
log.Printf("[API] server listening on %s\n", config.Config.App.Addr)
|
||||
if serveErr := srv.Serve(listener); serveErr != nil && !errors.Is(serveErr, http.ErrServerClosed) {
|
||||
log.Fatalf("[API] server failed: %v\n", serveErr)
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *waveletHTTPDriver) Stop(ctx context.Context) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
if !d.running {
|
||||
return nil
|
||||
}
|
||||
d.running = false
|
||||
|
||||
var err error
|
||||
if d.server != nil {
|
||||
err = d.server.Shutdown(ctx)
|
||||
d.server = nil
|
||||
}
|
||||
bootstrap.Stop(ctx)
|
||||
log.Println("[API] server exited")
|
||||
return err
|
||||
}
|
||||
|
||||
type waveletWorkerDriver struct {
|
||||
profile core.Profile
|
||||
server *asynq.Server
|
||||
mu sync.Mutex
|
||||
running bool
|
||||
}
|
||||
|
||||
func newWaveletWorkerDriver(profile core.Profile) *waveletWorkerDriver {
|
||||
return &waveletWorkerDriver{profile: profile}
|
||||
}
|
||||
|
||||
func (d *waveletWorkerDriver) Name() string {
|
||||
return "driver_wavelet_worker"
|
||||
}
|
||||
|
||||
func (d *waveletWorkerDriver) Apply(ctx *core.Context) error {
|
||||
return ctx.RegisterDriver(d)
|
||||
}
|
||||
|
||||
func (d *waveletWorkerDriver) Type() core.DriverType {
|
||||
return core.DriverTypeWorker
|
||||
}
|
||||
|
||||
//nolint:contextcheck
|
||||
func (d *waveletWorkerDriver) Start(_ context.Context) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
if d.running {
|
||||
return nil
|
||||
}
|
||||
|
||||
if d.profile == core.ProfileAll {
|
||||
bootstrap.RegisterAll()
|
||||
} else {
|
||||
bootstrap.RegisterWorker()
|
||||
}
|
||||
runBootstrap(bootstrap.Options{})
|
||||
|
||||
if d.profile == core.ProfileWorker {
|
||||
printStartupBanner(startupState{
|
||||
mode: "Worker",
|
||||
relationalDB: latestMigrationState.relationalDB,
|
||||
clickHouseDB: latestMigrationState.clickHouseDB,
|
||||
})
|
||||
}
|
||||
|
||||
util.Go(func() {
|
||||
if err := gwrunner.Start(context.Background()); err != nil {
|
||||
log.Printf("[Worker] message gateway stopped: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
log.Println("[Worker] 启动任务处理服务")
|
||||
srv, err := worker.StartWorkerServer()
|
||||
if err != nil {
|
||||
return fmt.Errorf("[Worker] 启动失败: %w", err)
|
||||
}
|
||||
|
||||
d.server = srv
|
||||
d.running = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *waveletWorkerDriver) Stop(_ context.Context) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
if !d.running {
|
||||
return nil
|
||||
}
|
||||
d.running = false
|
||||
|
||||
if d.server != nil {
|
||||
d.server.Stop()
|
||||
d.server.Shutdown()
|
||||
d.server = nil
|
||||
}
|
||||
log.Println("[Worker] 任务处理服务已退出")
|
||||
return nil
|
||||
}
|
||||
|
||||
type waveletSchedulerDriver struct {
|
||||
profile core.Profile
|
||||
mu sync.Mutex
|
||||
running bool
|
||||
}
|
||||
|
||||
func newWaveletSchedulerDriver(profile core.Profile) *waveletSchedulerDriver {
|
||||
return &waveletSchedulerDriver{profile: profile}
|
||||
}
|
||||
|
||||
func (d *waveletSchedulerDriver) Name() string {
|
||||
return "driver_wavelet_scheduler"
|
||||
}
|
||||
|
||||
func (d *waveletSchedulerDriver) Apply(ctx *core.Context) error {
|
||||
return ctx.RegisterDriver(d)
|
||||
}
|
||||
|
||||
func (d *waveletSchedulerDriver) Type() core.DriverType {
|
||||
return core.DriverTypeScheduler
|
||||
}
|
||||
|
||||
//nolint:contextcheck
|
||||
func (d *waveletSchedulerDriver) Start(_ context.Context) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
if d.running {
|
||||
return nil
|
||||
}
|
||||
|
||||
if d.profile == core.ProfileAll {
|
||||
bootstrap.RegisterAll()
|
||||
} else {
|
||||
bootstrap.RegisterScheduler()
|
||||
}
|
||||
runBootstrap(bootstrap.Options{})
|
||||
|
||||
if d.profile == core.ProfileSchedule {
|
||||
printStartupBanner(startupState{
|
||||
mode: "Scheduler",
|
||||
relationalDB: latestMigrationState.relationalDB,
|
||||
clickHouseDB: latestMigrationState.clickHouseDB,
|
||||
})
|
||||
}
|
||||
|
||||
log.Println("[Scheduler] 启动定时任务调度服务")
|
||||
if err := scheduler.ReloadScheduler(); err != nil {
|
||||
return fmt.Errorf("[Scheduler] 启动失败: %w", err)
|
||||
}
|
||||
|
||||
d.running = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *waveletSchedulerDriver) Stop(_ context.Context) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
if !d.running {
|
||||
return nil
|
||||
}
|
||||
d.running = false
|
||||
|
||||
scheduler.StopScheduler()
|
||||
log.Println("[Scheduler] 定时任务调度服务已退出")
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/core"
|
||||
)
|
||||
|
||||
func TestNewWaveletAppProfiles(t *testing.T) {
|
||||
profiles := []core.Profile{
|
||||
core.ProfileAPI,
|
||||
core.ProfileWorker,
|
||||
core.ProfileSchedule,
|
||||
core.ProfileAll,
|
||||
}
|
||||
|
||||
for _, prof := range profiles {
|
||||
t.Run(string(prof), func(t *testing.T) {
|
||||
app := newWaveletApp(prof)
|
||||
require.NotNil(t, app)
|
||||
assert.Equal(t, prof, app.Profile())
|
||||
|
||||
// Verify 4 infra plugins + 3 driver plugins registered
|
||||
plugins := app.Plugins()
|
||||
assert.Len(t, plugins, 7)
|
||||
|
||||
// Verify each standard infra plugin is registered
|
||||
_, ok := app.Plugin("database")
|
||||
assert.True(t, ok, "database plugin missing")
|
||||
|
||||
_, ok = app.Plugin("cache")
|
||||
assert.True(t, ok, "cache plugin missing")
|
||||
|
||||
_, ok = app.Plugin("logger")
|
||||
assert.True(t, ok, "logger plugin missing")
|
||||
|
||||
_, ok = app.Plugin("storage")
|
||||
assert.True(t, ok, "storage plugin missing")
|
||||
|
||||
// Verify driver plugins
|
||||
_, ok = app.Plugin("driver_wavelet_http")
|
||||
assert.True(t, ok, "http driver missing")
|
||||
|
||||
_, ok = app.Plugin("driver_wavelet_worker")
|
||||
assert.True(t, ok, "worker driver missing")
|
||||
|
||||
_, ok = app.Plugin("driver_wavelet_scheduler")
|
||||
assert.True(t, ok, "scheduler driver missing")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,7 @@ package cmd
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/infra/task/scheduler"
|
||||
"github.com/Rain-kl/Wavelet/internal/platform/bootstrap"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/core"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -17,11 +15,9 @@ var schedulerCmd = &cobra.Command{
|
||||
Use: "scheduler",
|
||||
Short: "wavelet Scheduler",
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
runBootstrap(bootstrap.Options{})
|
||||
printStartupBanner(startupState{mode: "Scheduler", relationalDB: latestMigrationState.relationalDB, clickHouseDB: latestMigrationState.clickHouseDB})
|
||||
log.Println("[Scheduler] 启动定时任务调度服务")
|
||||
if err := scheduler.StartScheduler(); err != nil {
|
||||
log.Fatalf("[调度器] 启动失败: %v", err)
|
||||
app := newWaveletApp(core.ProfileSchedule)
|
||||
if err := app.Run(); err != nil {
|
||||
log.Fatalf("[Scheduler] run failed: %v\n", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
+4
-15
@@ -5,13 +5,9 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
gwrunner "github.com/Rain-kl/Wavelet/internal/apps/message_gateway/runner"
|
||||
"github.com/Rain-kl/Wavelet/internal/infra/task/worker"
|
||||
"github.com/Rain-kl/Wavelet/internal/platform/bootstrap"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/core"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -19,16 +15,9 @@ var workerCmd = &cobra.Command{
|
||||
Use: "worker",
|
||||
Short: "wavelet Worker",
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
runBootstrap(bootstrap.Options{})
|
||||
printStartupBanner(startupState{mode: "Worker", relationalDB: latestMigrationState.relationalDB, clickHouseDB: latestMigrationState.clickHouseDB})
|
||||
go func() {
|
||||
if err := gwrunner.Start(context.Background()); err != nil {
|
||||
log.Printf("[Worker] message gateway stopped: %v", err)
|
||||
}
|
||||
}()
|
||||
log.Println("[Worker] 启动任务处理服务")
|
||||
if err := worker.StartWorker(); err != nil {
|
||||
log.Fatalf("[工作器] 启动失败: %v", err)
|
||||
app := newWaveletApp(core.ProfileWorker)
|
||||
if err := app.Run(); err != nil {
|
||||
log.Fatalf("[Worker] run failed: %v\n", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user