mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-02 23:06:36 +08:00
feat(core): implement cordis configuration extension and migrate all plugins
This commit is contained in:
+1
-9
@@ -6,7 +6,6 @@ package cmd
|
||||
|
||||
import (
|
||||
"Wavelet/core"
|
||||
"log"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -15,13 +14,6 @@ var allCmd = &cobra.Command{
|
||||
Use: "all",
|
||||
Short: "以融合模式同时启动 API、Worker 和 Scheduler",
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
printStartupBanner(startupState{
|
||||
mode: "all (API + Worker + Scheduler)",
|
||||
listensForHTTP: true,
|
||||
})
|
||||
app := newWaveletApp(core.ProfileAll)
|
||||
if err := app.Run(); err != nil {
|
||||
log.Fatalf("[All] run failed: %v\n", err)
|
||||
}
|
||||
runProfileApp(core.ProfileAll, "all (API + Worker + Scheduler)", true)
|
||||
},
|
||||
}
|
||||
|
||||
+1
-9
@@ -5,7 +5,6 @@ package cmd
|
||||
|
||||
import (
|
||||
"Wavelet/core"
|
||||
"log"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -14,13 +13,6 @@ var apiCmd = &cobra.Command{
|
||||
Use: "api",
|
||||
Short: "wavelet API",
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
printStartupBanner(startupState{
|
||||
mode: "api",
|
||||
listensForHTTP: true,
|
||||
})
|
||||
app := newWaveletApp(core.ProfileAPI)
|
||||
if err := app.Run(); err != nil {
|
||||
log.Fatalf("[API] run failed: %v\n", err)
|
||||
}
|
||||
runProfileApp(core.ProfileAPI, "api", true)
|
||||
},
|
||||
}
|
||||
|
||||
+54
-25
@@ -6,7 +6,6 @@ package cmd
|
||||
import (
|
||||
"Wavelet/core"
|
||||
"Wavelet/core/contracts"
|
||||
"Wavelet/pkg/config"
|
||||
"Wavelet/plugins/domain/admin"
|
||||
"Wavelet/plugins/domain/auth"
|
||||
"Wavelet/plugins/domain/cap"
|
||||
@@ -22,6 +21,7 @@ import (
|
||||
"Wavelet/plugins/drivers/driver_inproc_worker"
|
||||
"Wavelet/plugins/infra/cache"
|
||||
"Wavelet/plugins/infra/cache_memory"
|
||||
"Wavelet/plugins/infra/config"
|
||||
"Wavelet/plugins/infra/logger"
|
||||
"Wavelet/plugins/infra/storage"
|
||||
"context"
|
||||
@@ -38,14 +38,48 @@ import (
|
||||
infradb "Wavelet/plugins/infra/database"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultShutdownTimeout = 15 * time.Second
|
||||
defaultHTTPAddr = "127.0.0.1:3000"
|
||||
)
|
||||
|
||||
// runProfileApp prepares and runs the application for a given profile.
|
||||
func runProfileApp(profile core.Profile, mode string, listensForHTTP bool) {
|
||||
app := newWaveletApp(profile)
|
||||
if err := app.Prepare(); err != nil {
|
||||
log.Fatalf("[%s] prepare failed: %v\n", mode, err)
|
||||
}
|
||||
state := startupState{
|
||||
mode: mode,
|
||||
listensForHTTP: listensForHTTP,
|
||||
env: app.Context().Config().String("app.env", "production"),
|
||||
}
|
||||
if listensForHTTP {
|
||||
state.addr = app.Context().Config().String("app.addr", defaultHTTPAddr)
|
||||
}
|
||||
printStartupBanner(state)
|
||||
if err := app.Run(); err != nil {
|
||||
log.Fatalf("[%s] run failed: %v\n", mode, err)
|
||||
}
|
||||
}
|
||||
|
||||
// newWaveletApp creates a core.App wired with Wavelet platform infrastructure, domain plugins, and profile drivers.
|
||||
//
|
||||
//nolint:contextcheck
|
||||
func newWaveletApp(profile core.Profile) *core.App {
|
||||
app := core.NewApp(
|
||||
func newWaveletApp(profile core.Profile, opts ...core.AppOption) *core.App {
|
||||
src, err := config.NewSource()
|
||||
if err != nil {
|
||||
log.Fatalf("[App] load config source failed: %v\n", err)
|
||||
}
|
||||
|
||||
appOpts := []core.AppOption{
|
||||
core.WithProfile(profile),
|
||||
core.WithShutdownTimeout(time.Duration(config.Config.App.GracefulShutdownTimeout)*time.Second),
|
||||
)
|
||||
core.WithConfigSource(src),
|
||||
core.WithShutdownTimeout(defaultShutdownTimeout),
|
||||
}
|
||||
appOpts = append(appOpts, opts...)
|
||||
|
||||
app := core.NewApp(appOpts...)
|
||||
|
||||
// 1. Register standard infrastructure plugins
|
||||
app.Use(
|
||||
@@ -54,20 +88,15 @@ func newWaveletApp(profile core.Profile) *core.App {
|
||||
storage.New(),
|
||||
)
|
||||
|
||||
// 2. Register Cache and Async/Cron Drivers based on Redis configuration
|
||||
if config.Config.Redis.Enabled {
|
||||
app.Use(
|
||||
cache.New(),
|
||||
driver_asynq_worker.New(),
|
||||
driver_asynq_cron.New(),
|
||||
)
|
||||
} else {
|
||||
app.Use(
|
||||
cache_memory.New(),
|
||||
driver_inproc_worker.New(),
|
||||
driver_inproc_cron.New(),
|
||||
)
|
||||
}
|
||||
// 2. Register Cache and Async/Cron Drivers (both gated: cache vs cache_memory, asynq vs inproc)
|
||||
app.Use(
|
||||
cache.New(),
|
||||
cache_memory.New(),
|
||||
driver_asynq_worker.New(),
|
||||
driver_inproc_worker.New(),
|
||||
driver_asynq_cron.New(),
|
||||
driver_inproc_cron.New(),
|
||||
)
|
||||
|
||||
// 3. Register all 8 domain business plugins (admin first to ensure schema and base config tables exist)
|
||||
app.Use(
|
||||
@@ -86,7 +115,7 @@ func newWaveletApp(profile core.Profile) *core.App {
|
||||
|
||||
// 5. Mount HTTP runtime driver
|
||||
app.Use(
|
||||
driver_http.New(driver_http.WithAddr(config.Config.App.Addr)),
|
||||
driver_http.New(),
|
||||
)
|
||||
|
||||
return app
|
||||
@@ -236,7 +265,7 @@ func (e *gooseEngine) Migrate(ctx *core.Context, entries []core.MigrationEntry)
|
||||
return fmt.Errorf("migration: get underlying DB from GORM: %w", err)
|
||||
}
|
||||
|
||||
dialect := gooseDialect()
|
||||
dialect := gooseDialect(ctx)
|
||||
dialectStr := string(dialect)
|
||||
|
||||
for _, entry := range entries {
|
||||
@@ -267,11 +296,11 @@ func (e *gooseEngine) Migrate(ctx *core.Context, entries []core.MigrationEntry)
|
||||
}
|
||||
|
||||
// gooseDialect returns the goose dialect based on the configured database engine.
|
||||
func gooseDialect() goose.Dialect {
|
||||
if !config.Config.Database.Enabled {
|
||||
return goose.DialectSQLite3
|
||||
func gooseDialect(ctx *core.Context) goose.Dialect {
|
||||
if ctx != nil && ctx.Config() != nil && ctx.Config().Bool("database.enabled", false) {
|
||||
return goose.DialectPostgres
|
||||
}
|
||||
return goose.DialectPostgres
|
||||
return goose.DialectSQLite3
|
||||
}
|
||||
|
||||
func findMigrationFS(rootFS fs.FS, dialect goose.Dialect) fs.FS {
|
||||
|
||||
+57
-18
@@ -5,7 +5,6 @@ package cmd
|
||||
|
||||
import (
|
||||
"Wavelet/core"
|
||||
"Wavelet/pkg/config"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -22,13 +21,22 @@ func TestNewWaveletAppProfiles(t *testing.T) {
|
||||
|
||||
for _, prof := range profiles {
|
||||
t.Run(string(prof), func(t *testing.T) {
|
||||
app := newWaveletApp(prof)
|
||||
app := newWaveletApp(prof, core.WithConfigValues(map[string]any{
|
||||
"app": map[string]any{
|
||||
"addr": "127.0.0.1:0",
|
||||
},
|
||||
"redis": map[string]any{
|
||||
"enabled": false,
|
||||
},
|
||||
}))
|
||||
require.NotNil(t, app)
|
||||
assert.Equal(t, prof, app.Profile())
|
||||
|
||||
// 3 infra + (1 cache + 2 worker/cron) + 8 domain + 1 http driver = 15 plugins
|
||||
// 3 infra + 2 cache + 4 worker/cron + 8 domain + 1 http driver = 18 plugins
|
||||
plugins := app.Plugins()
|
||||
assert.Len(t, plugins, 15)
|
||||
assert.Len(t, plugins, 18)
|
||||
|
||||
require.NoError(t, app.Reconcile())
|
||||
|
||||
// Verify standard infra plugins
|
||||
_, ok := app.Plugin("database")
|
||||
@@ -41,14 +49,29 @@ func TestNewWaveletAppProfiles(t *testing.T) {
|
||||
assert.True(t, ok, "storage plugin missing")
|
||||
|
||||
// In zero-Redis mode (default in test)
|
||||
_, ok = app.Plugin("cache_memory")
|
||||
assert.True(t, ok, "cache_memory plugin missing")
|
||||
f, ok := app.Fiber("cache_memory")
|
||||
assert.True(t, ok, "cache_memory fiber missing")
|
||||
assert.Equal(t, core.FiberActive, f.State())
|
||||
|
||||
_, ok = app.Plugin("driver_inproc_worker")
|
||||
assert.True(t, ok, "inproc worker driver missing")
|
||||
f, ok = app.Fiber("cache")
|
||||
assert.True(t, ok, "cache fiber missing")
|
||||
assert.Equal(t, core.FiberSkipped, f.State())
|
||||
|
||||
_, ok = app.Plugin("driver_inproc_cron")
|
||||
assert.True(t, ok, "inproc scheduler driver missing")
|
||||
f, ok = app.Fiber("driver_inproc_worker")
|
||||
assert.True(t, ok, "inproc worker driver fiber missing")
|
||||
assert.Equal(t, core.FiberActive, f.State())
|
||||
|
||||
f, ok = app.Fiber("driver_asynq_worker")
|
||||
assert.True(t, ok, "asynq worker driver fiber missing")
|
||||
assert.Equal(t, core.FiberSkipped, f.State())
|
||||
|
||||
f, ok = app.Fiber("driver_inproc_cron")
|
||||
assert.True(t, ok, "inproc scheduler driver fiber missing")
|
||||
assert.Equal(t, core.FiberActive, f.State())
|
||||
|
||||
f, ok = app.Fiber("driver_asynq_cron")
|
||||
assert.True(t, ok, "asynq scheduler driver fiber missing")
|
||||
assert.Equal(t, core.FiberSkipped, f.State())
|
||||
|
||||
// Verify domain plugins
|
||||
_, ok = app.Plugin("auth")
|
||||
@@ -83,19 +106,35 @@ func TestNewWaveletAppProfiles(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNewWaveletAppWithRedisEnabled(t *testing.T) {
|
||||
orig := config.Config.Redis.Enabled
|
||||
config.Config.Redis.Enabled = true
|
||||
defer func() { config.Config.Redis.Enabled = orig }()
|
||||
|
||||
app := newWaveletApp(core.ProfileAll)
|
||||
app := newWaveletApp(core.ProfileAll, core.WithConfigValues(map[string]any{
|
||||
"redis": map[string]any{
|
||||
"enabled": true,
|
||||
},
|
||||
}))
|
||||
require.NotNil(t, app)
|
||||
require.NoError(t, app.Reconcile())
|
||||
|
||||
_, ok := app.Plugin("cache")
|
||||
f, ok := app.Fiber("cache")
|
||||
assert.True(t, ok, "cache plugin missing in Redis mode")
|
||||
assert.Equal(t, core.FiberActive, f.State())
|
||||
|
||||
_, ok = app.Plugin("driver_asynq_worker")
|
||||
f, ok = app.Fiber("driver_asynq_worker")
|
||||
assert.True(t, ok, "asynq worker driver missing in Redis mode")
|
||||
assert.Equal(t, core.FiberActive, f.State())
|
||||
|
||||
_, ok = app.Plugin("driver_asynq_cron")
|
||||
f, ok = app.Fiber("driver_asynq_cron")
|
||||
assert.True(t, ok, "asynq scheduler driver missing in Redis mode")
|
||||
assert.Equal(t, core.FiberActive, f.State())
|
||||
|
||||
f, ok = app.Fiber("cache_memory")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, core.FiberSkipped, f.State())
|
||||
|
||||
f, ok = app.Fiber("driver_inproc_worker")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, core.FiberSkipped, f.State())
|
||||
|
||||
f, ok = app.Fiber("driver_inproc_cron")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, core.FiberSkipped, f.State())
|
||||
}
|
||||
|
||||
+13
-7
@@ -1,23 +1,20 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package cmd provides CLI command entry points.
|
||||
//
|
||||
//nolint:unused
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"Wavelet/pkg/buildinfo"
|
||||
"Wavelet/pkg/config"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//nolint:unused // startup banner formatting utilities
|
||||
type startupState struct {
|
||||
mode string
|
||||
listensForHTTP bool
|
||||
env string
|
||||
addr string
|
||||
}
|
||||
|
||||
func printStartupBanner(state startupState) {
|
||||
@@ -25,6 +22,15 @@ func printStartupBanner(state startupState) {
|
||||
}
|
||||
|
||||
func formatStartupBanner(state startupState) string {
|
||||
env := state.env
|
||||
if env == "" {
|
||||
env = "production"
|
||||
}
|
||||
addr := state.addr
|
||||
if addr == "" {
|
||||
addr = "127.0.0.1:3000"
|
||||
}
|
||||
|
||||
lines := []string{
|
||||
"",
|
||||
"__ __ _ _ ",
|
||||
@@ -34,12 +40,12 @@ func formatStartupBanner(state startupState) string {
|
||||
" \\_/\\_/ \\__,_| \\_/ \\___|_|\\___|\\__|",
|
||||
fmt.Sprintf(" Wavelet %s", buildinfo.Version),
|
||||
"",
|
||||
fmt.Sprintf(" Environment: %s", config.Config.App.Env),
|
||||
fmt.Sprintf(" Environment: %s", env),
|
||||
fmt.Sprintf(" Runtime: %s/%s (%s)", runtime.GOOS, runtime.GOARCH, runtime.Version()),
|
||||
fmt.Sprintf(" Build time: %s", buildTime()),
|
||||
}
|
||||
if state.listensForHTTP {
|
||||
lines = append(lines, fmt.Sprintf(" Listening: http://%s", config.Config.App.Addr))
|
||||
lines = append(lines, fmt.Sprintf(" Listening: http://%s", addr))
|
||||
}
|
||||
lines = append(lines, fmt.Sprintf(" Mode: %s", state.mode), "")
|
||||
return strings.Join(lines, "\n")
|
||||
|
||||
@@ -5,7 +5,6 @@ package cmd
|
||||
|
||||
import (
|
||||
"Wavelet/pkg/buildinfo"
|
||||
"Wavelet/pkg/config"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -13,23 +12,19 @@ import (
|
||||
func TestFormatStartupBanner(t *testing.T) {
|
||||
previousVersion := buildinfo.Version
|
||||
previousBuildTime := buildinfo.BuildTime
|
||||
previousEnv := config.Config.App.Env
|
||||
previousAddr := config.Config.App.Addr
|
||||
t.Cleanup(func() {
|
||||
buildinfo.Version = previousVersion
|
||||
buildinfo.BuildTime = previousBuildTime
|
||||
config.Config.App.Env = previousEnv
|
||||
config.Config.App.Addr = previousAddr
|
||||
})
|
||||
|
||||
buildinfo.Version = "v3.2.1"
|
||||
buildinfo.BuildTime = "2026-07-13T08:00:00Z"
|
||||
config.Config.App.Env = "production"
|
||||
config.Config.App.Addr = ":3000"
|
||||
|
||||
banner := formatStartupBanner(startupState{
|
||||
mode: "API",
|
||||
listensForHTTP: true,
|
||||
env: "production",
|
||||
addr: ":3000",
|
||||
})
|
||||
|
||||
for _, want := range []string{
|
||||
|
||||
@@ -7,49 +7,56 @@ import (
|
||||
"Wavelet/core"
|
||||
"Wavelet/core/contracts"
|
||||
"Wavelet/core/extpoints"
|
||||
"Wavelet/pkg/config"
|
||||
"Wavelet/pkg/idgen"
|
||||
"context"
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRedisPluggability_Simulation(t *testing.T) {
|
||||
origRedisEnabled := config.Config.Redis.Enabled
|
||||
origAddr := config.Config.App.Addr
|
||||
config.Config.App.Addr = "127.0.0.1:0"
|
||||
defer func() {
|
||||
config.Config.Redis.Enabled = origRedisEnabled
|
||||
config.Config.App.Addr = origAddr
|
||||
}()
|
||||
_ = idgen.Init(1)
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// 场景 1: 拔出 Redis (Zero-Redis Monolith 模式)
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
t.Run("Scenario_Unplugged_ZeroRedis_Mode", func(t *testing.T) {
|
||||
config.Config.Redis.Enabled = false
|
||||
|
||||
app := newWaveletApp(core.ProfileAll)
|
||||
app := newWaveletApp(core.ProfileAll, core.WithConfigValues(map[string]any{
|
||||
"app": map[string]any{
|
||||
"addr": "127.0.0.1:0",
|
||||
},
|
||||
"redis": map[string]any{
|
||||
"enabled": false,
|
||||
},
|
||||
}))
|
||||
require.NotNil(t, app)
|
||||
require.NoError(t, app.Reconcile())
|
||||
|
||||
// 1. 验证插件挂载形态
|
||||
_, ok := app.Plugin("cache_memory")
|
||||
f, ok := app.Fiber("cache_memory")
|
||||
assert.True(t, ok, "cache_memory 必须挂载")
|
||||
_, ok = app.Plugin("driver_inproc_worker")
|
||||
assert.Equal(t, core.FiberActive, f.State())
|
||||
f, ok = app.Fiber("driver_inproc_worker")
|
||||
assert.True(t, ok, "driver_inproc_worker 必须挂载")
|
||||
_, ok = app.Plugin("driver_inproc_cron")
|
||||
assert.Equal(t, core.FiberActive, f.State())
|
||||
f, ok = app.Fiber("driver_inproc_cron")
|
||||
assert.True(t, ok, "driver_inproc_cron 必须挂载")
|
||||
assert.Equal(t, core.FiberActive, f.State())
|
||||
|
||||
_, ok = app.Plugin("cache")
|
||||
assert.False(t, ok, "分布式 cache 不得挂载")
|
||||
_, ok = app.Plugin("driver_asynq_worker")
|
||||
assert.False(t, ok, "asynq_worker 不得挂载")
|
||||
_, ok = app.Plugin("driver_asynq_cron")
|
||||
assert.False(t, ok, "asynq_cron 不得挂载")
|
||||
f, ok = app.Fiber("cache")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, core.FiberSkipped, f.State(), "分布式 cache 不得挂载")
|
||||
f, ok = app.Fiber("driver_asynq_worker")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, core.FiberSkipped, f.State(), "asynq_worker 不得挂载")
|
||||
f, ok = app.Fiber("driver_asynq_cron")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, core.FiberSkipped, f.State(), "asynq_cron 不得挂载")
|
||||
|
||||
// 2. 注册测试任务与 Cron 定时
|
||||
var taskExecuted atomic.Int32
|
||||
@@ -114,25 +121,42 @@ func TestRedisPluggability_Simulation(t *testing.T) {
|
||||
// 场景 2: 插入 Redis (Distributed Cluster 模式)
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
t.Run("Scenario_Plugged_Redis_Mode", func(t *testing.T) {
|
||||
config.Config.Redis.Enabled = true
|
||||
mr, err := miniredis.Run()
|
||||
require.NoError(t, err)
|
||||
defer mr.Close()
|
||||
|
||||
app := newWaveletApp(core.ProfileAll)
|
||||
app := newWaveletApp(core.ProfileAll, core.WithConfigValues(map[string]any{
|
||||
"app": map[string]any{
|
||||
"addr": "127.0.0.1:0",
|
||||
},
|
||||
"redis": map[string]any{
|
||||
"enabled": true,
|
||||
"addrs": []string{mr.Addr()},
|
||||
},
|
||||
}))
|
||||
require.NotNil(t, app)
|
||||
require.NoError(t, app.Reconcile())
|
||||
|
||||
// 1. 验证插件挂载形态
|
||||
_, ok := app.Plugin("cache")
|
||||
f, ok := app.Fiber("cache")
|
||||
assert.True(t, ok, "分布式 cache 必须挂载")
|
||||
_, ok = app.Plugin("driver_asynq_worker")
|
||||
assert.Equal(t, core.FiberActive, f.State())
|
||||
f, ok = app.Fiber("driver_asynq_worker")
|
||||
assert.True(t, ok, "driver_asynq_worker 必须挂载")
|
||||
_, ok = app.Plugin("driver_asynq_cron")
|
||||
assert.Equal(t, core.FiberActive, f.State())
|
||||
f, ok = app.Fiber("driver_asynq_cron")
|
||||
assert.True(t, ok, "driver_asynq_cron 必须挂载")
|
||||
assert.Equal(t, core.FiberActive, f.State())
|
||||
|
||||
_, ok = app.Plugin("cache_memory")
|
||||
assert.False(t, ok, "纯内存 cache 不得挂载")
|
||||
_, ok = app.Plugin("driver_inproc_worker")
|
||||
assert.False(t, ok, "inproc_worker 不得挂载")
|
||||
_, ok = app.Plugin("driver_inproc_cron")
|
||||
assert.False(t, ok, "inproc_cron 不得挂载")
|
||||
f, ok = app.Fiber("cache_memory")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, core.FiberSkipped, f.State(), "纯内存 cache 不得挂载")
|
||||
f, ok = app.Fiber("driver_inproc_worker")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, core.FiberSkipped, f.State(), "inproc_worker 不得挂载")
|
||||
f, ok = app.Fiber("driver_inproc_cron")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, core.FiberSkipped, f.State(), "inproc_cron 不得挂载")
|
||||
|
||||
// 2. 注册测试任务
|
||||
var asynqTaskExecuted atomic.Int32
|
||||
@@ -143,7 +167,7 @@ func TestRedisPluggability_Simulation(t *testing.T) {
|
||||
return nil
|
||||
}, extpoints.WithTaskTimeout(3*time.Second))
|
||||
|
||||
// 3. 启动应用 (连接真实运行中的 Redis 6379)
|
||||
// 3. 启动应用
|
||||
bootCtx, bootCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer bootCancel()
|
||||
require.NoError(t, app.Start(bootCtx))
|
||||
@@ -172,7 +196,7 @@ func TestRedisPluggability_Simulation(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, taskSvc)
|
||||
|
||||
taskID, err := taskSvc.Dispatch(reqCtx, "test:asynq_task", []byte("payload_plugged"), "unit_test")
|
||||
taskID, err := taskSvc.Dispatch(reqCtx, "test:asynq_task", []byte("payload_plugged"), "default")
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, taskID)
|
||||
|
||||
@@ -190,10 +214,20 @@ func TestRedisPluggability_Simulation(t *testing.T) {
|
||||
// 场景 3: 往复插拔连续切换 (拔出 → 插入 → 再拔出,验证时空可组合性与零残留)
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
t.Run("Scenario_Dynamic_Plug_Unplug_Sequence", func(t *testing.T) {
|
||||
mr, err := miniredis.Run()
|
||||
require.NoError(t, err)
|
||||
defer mr.Close()
|
||||
|
||||
for i := 1; i <= 2; i++ {
|
||||
// 1. 拔出 Redis 运行
|
||||
config.Config.Redis.Enabled = false
|
||||
appUnplugged := newWaveletApp(core.ProfileAll)
|
||||
appUnplugged := newWaveletApp(core.ProfileAll, core.WithConfigValues(map[string]any{
|
||||
"app": map[string]any{
|
||||
"addr": "127.0.0.1:0",
|
||||
},
|
||||
"redis": map[string]any{
|
||||
"enabled": false,
|
||||
},
|
||||
}))
|
||||
require.NoError(t, appUnplugged.Start(context.Background()))
|
||||
|
||||
cacheSvc1, err := core.Inject[contracts.CacheService](appUnplugged.Context())
|
||||
@@ -203,8 +237,15 @@ func TestRedisPluggability_Simulation(t *testing.T) {
|
||||
require.NoError(t, appUnplugged.Stop(context.Background()))
|
||||
|
||||
// 2. 插入 Redis 运行
|
||||
config.Config.Redis.Enabled = true
|
||||
appPlugged := newWaveletApp(core.ProfileAll)
|
||||
appPlugged := newWaveletApp(core.ProfileAll, core.WithConfigValues(map[string]any{
|
||||
"app": map[string]any{
|
||||
"addr": "127.0.0.1:0",
|
||||
},
|
||||
"redis": map[string]any{
|
||||
"enabled": true,
|
||||
"addrs": []string{mr.Addr()},
|
||||
},
|
||||
}))
|
||||
require.NoError(t, appPlugged.Start(context.Background()))
|
||||
|
||||
cacheSvc2, err := core.Inject[contracts.CacheService](appPlugged.Context())
|
||||
|
||||
+54
-12
@@ -4,10 +4,12 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"Wavelet/core/extpoints"
|
||||
"Wavelet/pkg/buildinfo"
|
||||
"Wavelet/pkg/config"
|
||||
"Wavelet/pkg/idgen"
|
||||
"Wavelet/pkg/logger"
|
||||
"Wavelet/pkg/trace"
|
||||
"Wavelet/plugins/infra/config"
|
||||
"context"
|
||||
"log"
|
||||
"time"
|
||||
@@ -17,23 +19,63 @@ import (
|
||||
|
||||
const traceShutdownTimeout = 10 * time.Second
|
||||
|
||||
type hostConfig struct {
|
||||
App struct {
|
||||
AppName string `config:"app_name" env:"APP_NAME" default:"Wavelet"`
|
||||
Env string `config:"env" env:"APP_ENV" default:"production"`
|
||||
NodeID int64 `config:"node_id" env:"APP_NODE_ID" default:"1"`
|
||||
Addr string `config:"addr" env:"APP_ADDR" default:"127.0.0.1:3000"`
|
||||
} `config:"app"`
|
||||
Log struct {
|
||||
Level string `config:"level" env:"LOG_LEVEL" default:"info"`
|
||||
Format string `config:"format" env:"LOG_FORMAT" default:"json"`
|
||||
Output string `config:"output" env:"LOG_OUTPUT" default:"stdout"`
|
||||
FilePath string `config:"file_path" env:"LOG_FILE_PATH" default:"./logs/app.log"`
|
||||
MaxSize int `config:"max_size" env:"LOG_MAX_SIZE" default:"100"`
|
||||
MaxAge int `config:"max_age" env:"LOG_MAX_AGE" default:"30"`
|
||||
MaxBackups int `config:"max_backups" env:"LOG_MAX_BACKUPS" default:"10"`
|
||||
Compress bool `config:"compress" env:"LOG_COMPRESS" default:"true"`
|
||||
} `config:"log"`
|
||||
OTel struct {
|
||||
SamplingRate float64 `config:"sampling_rate" env:"OTEL_SAMPLING_RATE" default:"1.0"`
|
||||
TracerName string `config:"tracer_name" env:"OTEL_TRACER_NAME" default:"github.com/Rain-kl/Wavelet"`
|
||||
} `config:"otel"`
|
||||
}
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "wavelet",
|
||||
PersistentPreRun: func(_ *cobra.Command, _ []string) {
|
||||
src, err := config.NewSource()
|
||||
if err != nil {
|
||||
log.Fatalf("[CMD] load config source failed: %v", err)
|
||||
}
|
||||
var cfg hostConfig
|
||||
reg := extpoints.NewConfigRegistry(src)
|
||||
_ = reg.Declare("host", extpoints.ConfigBinding{Target: &cfg})
|
||||
if err := reg.Resolve(); err != nil {
|
||||
log.Fatalf("[CMD] resolve host config failed: %v", err)
|
||||
}
|
||||
_ = reg.Bind("", &cfg)
|
||||
|
||||
// Initialize idgen snowflake generator
|
||||
if err := idgen.Init(cfg.App.NodeID); err != nil {
|
||||
log.Fatalf("[CMD] init idgen failed: %v", err)
|
||||
}
|
||||
|
||||
logger.Init(logger.Config{
|
||||
Level: config.Config.Log.Level,
|
||||
Format: config.Config.Log.Format,
|
||||
Output: config.Config.Log.Output,
|
||||
FilePath: config.Config.Log.FilePath,
|
||||
MaxSize: config.Config.Log.MaxSize,
|
||||
MaxAge: config.Config.Log.MaxAge,
|
||||
MaxBackups: config.Config.Log.MaxBackups,
|
||||
Compress: config.Config.Log.Compress,
|
||||
Level: cfg.Log.Level,
|
||||
Format: cfg.Log.Format,
|
||||
Output: cfg.Log.Output,
|
||||
FilePath: cfg.Log.FilePath,
|
||||
MaxSize: cfg.Log.MaxSize,
|
||||
MaxAge: cfg.Log.MaxAge,
|
||||
MaxBackups: cfg.Log.MaxBackups,
|
||||
Compress: cfg.Log.Compress,
|
||||
})
|
||||
trace.Init(trace.Config{
|
||||
AppName: config.Config.App.AppName,
|
||||
SamplingRate: config.Config.Otel.SamplingRate,
|
||||
TracerName: config.Config.Otel.TracerName,
|
||||
AppName: cfg.App.AppName,
|
||||
SamplingRate: cfg.OTel.SamplingRate,
|
||||
TracerName: cfg.OTel.TracerName,
|
||||
})
|
||||
},
|
||||
PersistentPostRun: func(_ *cobra.Command, _ []string) {
|
||||
|
||||
@@ -5,7 +5,6 @@ package cmd
|
||||
|
||||
import (
|
||||
"Wavelet/core"
|
||||
"log"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -14,13 +13,6 @@ var schedulerCmd = &cobra.Command{
|
||||
Use: "scheduler",
|
||||
Short: "wavelet Scheduler",
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
printStartupBanner(startupState{
|
||||
mode: "scheduler",
|
||||
listensForHTTP: false,
|
||||
})
|
||||
app := newWaveletApp(core.ProfileSchedule)
|
||||
if err := app.Run(); err != nil {
|
||||
log.Fatalf("[Scheduler] run failed: %v\n", err)
|
||||
}
|
||||
runProfileApp(core.ProfileSchedule, "scheduler", false)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ package cmd
|
||||
|
||||
import (
|
||||
"Wavelet/core"
|
||||
"log"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -14,13 +13,6 @@ var workerCmd = &cobra.Command{
|
||||
Use: "worker",
|
||||
Short: "wavelet Worker",
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
printStartupBanner(startupState{
|
||||
mode: "worker",
|
||||
listensForHTTP: false,
|
||||
})
|
||||
app := newWaveletApp(core.ProfileWorker)
|
||||
if err := app.Run(); err != nil {
|
||||
log.Fatalf("[Worker] run failed: %v\n", err)
|
||||
}
|
||||
runProfileApp(core.ProfileWorker, "worker", false)
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user