优化结构
This commit is contained in:
@@ -446,6 +446,5 @@ func (d *Data) reloadConfig(ctx context.Context) error {
|
||||
if d.storage != nil {
|
||||
d.storage.Replace(candidateStorage)
|
||||
}
|
||||
d.notifyResources()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -46,7 +46,6 @@ func NewIntegrationRuntime(data *Data) *runtimeconfig.Store {
|
||||
type Data struct {
|
||||
initMu sync.Mutex
|
||||
configMu sync.Mutex
|
||||
resourceMu sync.RWMutex
|
||||
databaseReady atomic.Bool
|
||||
gormDB *reloadableDB
|
||||
redis *reloadableRedis
|
||||
@@ -65,7 +64,6 @@ type Data struct {
|
||||
appLogger *slog.Logger
|
||||
auditLog *dataScopeAuditWriter
|
||||
catalog module.Catalog
|
||||
resourceHook func()
|
||||
}
|
||||
|
||||
// DB exposes the active primary database to narrowly scoped data submodules.
|
||||
@@ -172,30 +170,6 @@ func (d *Data) NamedRedisClients() map[string]redis.UniversalClient {
|
||||
return result
|
||||
}
|
||||
|
||||
// SetResourceHook registers a composition-root callback invoked after active
|
||||
// database, Redis, or Mongo handles change. A callback keeps data independent
|
||||
// from the global package while allowing the root to refresh shared handles.
|
||||
func (d *Data) SetResourceHook(hook func()) {
|
||||
if d == nil {
|
||||
return
|
||||
}
|
||||
d.resourceMu.Lock()
|
||||
d.resourceHook = hook
|
||||
d.resourceMu.Unlock()
|
||||
}
|
||||
|
||||
func (d *Data) notifyResources() {
|
||||
if d == nil {
|
||||
return
|
||||
}
|
||||
d.resourceMu.RLock()
|
||||
hook := d.resourceHook
|
||||
d.resourceMu.RUnlock()
|
||||
if hook != nil {
|
||||
hook()
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Data) logger() *slog.Logger {
|
||||
if d != nil && d.appLogger != nil {
|
||||
return d.appLogger
|
||||
@@ -467,5 +441,4 @@ func (d *Data) activateDatabase(db *gorm.DB, config *config.Database) {
|
||||
d.gormDB.replace(db, d.enqueueDataScopeAudit)
|
||||
d.runtime.UpdateDatabase(config)
|
||||
d.databaseReady.Store(true)
|
||||
d.notifyResources()
|
||||
}
|
||||
|
||||
@@ -105,7 +105,6 @@ func (d *Data) PersistRuntimeConfig(ctx context.Context, value *configpkg.Config
|
||||
if d.storage != nil {
|
||||
d.storage.Replace(candidateStorage)
|
||||
}
|
||||
d.notifyResources()
|
||||
return nil
|
||||
}
|
||||
func (d *Data) ReloadConfig(ctx context.Context) error {
|
||||
@@ -213,6 +212,5 @@ func (d *Data) InitializeDatabase(ctx context.Context, input *system.DatabaseCon
|
||||
if d.integrations != nil {
|
||||
d.integrations.Replace(integrationConfigs)
|
||||
}
|
||||
d.notifyResources()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
package global
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"log/slog"
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"gorm.io/gorm"
|
||||
platformmq "kra/pkg/mq"
|
||||
platformws "kra/pkg/websocket"
|
||||
)
|
||||
|
||||
// FileStorage is the transport-neutral subset shared by every file storage
|
||||
// implementation. Domain operations that return business models remain in
|
||||
// the biz-owned FileStorage interface instead of making global depend on biz.
|
||||
type FileStorage interface {
|
||||
Open(context.Context, string) (io.ReadCloser, error)
|
||||
Delete(context.Context, string) error
|
||||
DeletePrefix(context.Context, string) error
|
||||
}
|
||||
|
||||
// Scheduler owns the lifecycle of the process-wide task scheduler. Scheduling
|
||||
// business operations remain behind the task domain interfaces.
|
||||
type Scheduler interface {
|
||||
Start(context.Context) error
|
||||
Stop(context.Context) error
|
||||
}
|
||||
|
||||
// Resources is an immutable snapshot from the registry's point of view.
|
||||
// Replacing a resource publishes a new snapshot; it does not close the old
|
||||
// resource because lifecycle ownership belongs to the composition root.
|
||||
type Resources struct {
|
||||
Logger *slog.Logger
|
||||
DB *gorm.DB
|
||||
NamedDBs map[string]*gorm.DB
|
||||
Redis redis.UniversalClient
|
||||
NamedRedis map[string]redis.UniversalClient
|
||||
Mongo *mongo.Client
|
||||
Storage FileStorage
|
||||
MQ platformmq.Registry
|
||||
WebSocket platformws.Hub
|
||||
Scheduler Scheduler
|
||||
}
|
||||
|
||||
// ResourceRegistry provides concurrency-safe access to process-wide runtime
|
||||
// resources. Prefer constructor injection for domain code; the registry is for
|
||||
// framework callbacks and integration points that cannot be wired directly.
|
||||
type ResourceRegistry struct {
|
||||
mu sync.RWMutex
|
||||
resources Resources
|
||||
}
|
||||
|
||||
func NewResourceRegistry() *ResourceRegistry {
|
||||
return &ResourceRegistry{resources: normalizeResources(Resources{})}
|
||||
}
|
||||
|
||||
func (r *ResourceRegistry) Snapshot() Resources {
|
||||
if r == nil {
|
||||
return normalizeResources(Resources{})
|
||||
}
|
||||
r.mu.RLock()
|
||||
resources := cloneResources(r.resources)
|
||||
r.mu.RUnlock()
|
||||
return resources
|
||||
}
|
||||
|
||||
func (r *ResourceRegistry) Replace(resources Resources) {
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
r.mu.Lock()
|
||||
r.resources = normalizeResources(resources)
|
||||
r.mu.Unlock()
|
||||
}
|
||||
|
||||
func normalizeResources(resources Resources) Resources {
|
||||
if resources.Logger == nil {
|
||||
resources.Logger = slog.Default()
|
||||
}
|
||||
resources.Redis = normalizeInterface(resources.Redis)
|
||||
resources.Storage = normalizeInterface(resources.Storage)
|
||||
resources.MQ = normalizeInterface(resources.MQ)
|
||||
resources.WebSocket = normalizeInterface(resources.WebSocket)
|
||||
resources.Scheduler = normalizeInterface(resources.Scheduler)
|
||||
resources.NamedDBs = cloneDBMap(resources.NamedDBs)
|
||||
resources.NamedRedis = cloneRedisMap(resources.NamedRedis)
|
||||
return resources
|
||||
}
|
||||
|
||||
func cloneResources(resources Resources) Resources {
|
||||
resources.NamedDBs = cloneDBMap(resources.NamedDBs)
|
||||
resources.NamedRedis = cloneRedisMap(resources.NamedRedis)
|
||||
return resources
|
||||
}
|
||||
|
||||
func cloneDBMap(source map[string]*gorm.DB) map[string]*gorm.DB {
|
||||
if len(source) == 0 {
|
||||
return nil
|
||||
}
|
||||
copyOf := make(map[string]*gorm.DB, len(source))
|
||||
for name, db := range source {
|
||||
if name != "" && db != nil {
|
||||
copyOf[name] = db
|
||||
}
|
||||
}
|
||||
if len(copyOf) == 0 {
|
||||
return nil
|
||||
}
|
||||
return copyOf
|
||||
}
|
||||
|
||||
func cloneRedisMap(source map[string]redis.UniversalClient) map[string]redis.UniversalClient {
|
||||
if len(source) == 0 {
|
||||
return nil
|
||||
}
|
||||
copyOf := make(map[string]redis.UniversalClient, len(source))
|
||||
for name, client := range source {
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
client = normalizeInterface(client)
|
||||
if client != nil {
|
||||
copyOf[name] = client
|
||||
}
|
||||
}
|
||||
if len(copyOf) == 0 {
|
||||
return nil
|
||||
}
|
||||
return copyOf
|
||||
}
|
||||
|
||||
func normalizeInterface[T any](value T) T {
|
||||
reflected := reflect.ValueOf(value)
|
||||
if reflected.IsValid() {
|
||||
switch reflected.Kind() {
|
||||
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice:
|
||||
if !reflected.IsNil() {
|
||||
return value
|
||||
}
|
||||
default:
|
||||
return value
|
||||
}
|
||||
}
|
||||
var zero T
|
||||
return zero
|
||||
}
|
||||
|
||||
var defaultResources = NewResourceRegistry()
|
||||
|
||||
// Install publishes resources assembled by the composition root and returns
|
||||
// the process-wide registry for lifecycle wiring.
|
||||
func Install(resources Resources) *ResourceRegistry {
|
||||
defaultResources.Replace(resources)
|
||||
return defaultResources
|
||||
}
|
||||
|
||||
func DefaultRegistry() *ResourceRegistry { return defaultResources }
|
||||
|
||||
func ResourceSnapshot() Resources { return defaultResources.Snapshot() }
|
||||
@@ -1,147 +0,0 @@
|
||||
package global
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"log/slog"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"gorm.io/gorm"
|
||||
platformmq "kra/pkg/mq"
|
||||
platformws "kra/pkg/websocket"
|
||||
)
|
||||
|
||||
type storageStub struct{}
|
||||
|
||||
func (*storageStub) Open(context.Context, string) (io.ReadCloser, error) { return nil, nil }
|
||||
func (*storageStub) Delete(context.Context, string) error { return nil }
|
||||
func (*storageStub) DeletePrefix(context.Context, string) error { return nil }
|
||||
|
||||
type schedulerStub struct{}
|
||||
|
||||
func (*schedulerStub) Start(context.Context) error { return nil }
|
||||
func (*schedulerStub) Stop(context.Context) error { return nil }
|
||||
|
||||
type mqStub struct{}
|
||||
|
||||
func (*mqStub) Register(platformmq.SubscriptionSet) error { return nil }
|
||||
func (*mqStub) Unregister(string) error { return nil }
|
||||
func (*mqStub) Client(string) platformmq.Client { return nil }
|
||||
func (*mqStub) PublishTo(context.Context, string, string, []byte, byte, bool) error {
|
||||
return nil
|
||||
}
|
||||
func (*mqStub) SubscribeTo(context.Context, string, string, byte, platformmq.Handler) error {
|
||||
return nil
|
||||
}
|
||||
func (*mqStub) UnsubscribeFrom(context.Context, string, ...string) error { return nil }
|
||||
func (*mqStub) ConnectedTo(string) bool { return true }
|
||||
|
||||
type webSocketStub struct{}
|
||||
|
||||
func (*webSocketStub) Enabled() bool { return true }
|
||||
func (*webSocketStub) Broadcast([]byte) error { return nil }
|
||||
func (*webSocketStub) BroadcastBinary([]byte) error { return nil }
|
||||
func (*webSocketStub) Sessions() ([]*platformws.Session, error) { return nil, nil }
|
||||
func (*webSocketStub) Len() int { return 0 }
|
||||
func (*webSocketStub) Send(*platformws.Session, []byte) error { return nil }
|
||||
func (*webSocketStub) SendBinary(*platformws.Session, []byte) error { return nil }
|
||||
func (*webSocketStub) OnMessage(func(*platformws.Session, []byte)) {}
|
||||
func (*webSocketStub) OnBinaryMessage(func(*platformws.Session, []byte)) {}
|
||||
func (*webSocketStub) OnConnect(func(*platformws.Session)) {}
|
||||
func (*webSocketStub) OnDisconnect(func(*platformws.Session)) {}
|
||||
|
||||
func TestResourceRegistryDefaults(t *testing.T) {
|
||||
registry := NewResourceRegistry()
|
||||
resources := registry.Snapshot()
|
||||
if resources.Logger == nil {
|
||||
t.Fatal("default logger is nil")
|
||||
}
|
||||
if resources.DB != nil || len(resources.NamedDBs) != 0 || resources.Redis != nil || len(resources.NamedRedis) != 0 || resources.Mongo != nil || resources.Storage != nil || resources.MQ != nil || resources.WebSocket != nil || resources.Scheduler != nil {
|
||||
t.Fatalf("unexpected initialized resource: %#v", resources)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceRegistryReplaceAndSnapshot(t *testing.T) {
|
||||
logger := slog.Default().With("test", true)
|
||||
db := &gorm.DB{}
|
||||
redisClient := redis.NewClient(&redis.Options{Addr: "127.0.0.1:6379"})
|
||||
t.Cleanup(func() { _ = redisClient.Close() })
|
||||
mongoClient := &mongo.Client{}
|
||||
storage := &storageStub{}
|
||||
mq := &mqStub{}
|
||||
webSocket := &webSocketStub{}
|
||||
scheduler := &schedulerStub{}
|
||||
|
||||
registry := NewResourceRegistry()
|
||||
inputDBs := map[string]*gorm.DB{"reporting": db}
|
||||
inputRedis := map[string]redis.UniversalClient{"cache": redisClient}
|
||||
registry.Replace(Resources{
|
||||
Logger: logger, DB: db, NamedDBs: inputDBs,
|
||||
Redis: redisClient, NamedRedis: inputRedis, Mongo: mongoClient,
|
||||
Storage: storage, MQ: mq, WebSocket: webSocket, Scheduler: scheduler,
|
||||
})
|
||||
resources := registry.Snapshot()
|
||||
if resources.Logger != logger || resources.DB != db || resources.Redis != redisClient || resources.Mongo != mongoClient || resources.Storage != storage || resources.MQ != mq || resources.WebSocket != webSocket || resources.Scheduler != scheduler {
|
||||
t.Fatalf("replacement snapshot does not match: %#v", resources)
|
||||
}
|
||||
if resources.NamedDBs["reporting"] != db || resources.NamedRedis["cache"] != redisClient {
|
||||
t.Fatal("named resource lookup returned the wrong resource")
|
||||
}
|
||||
|
||||
inputDBs["mutated"] = &gorm.DB{}
|
||||
inputRedis["mutated"] = redisClient
|
||||
if _, exists := registry.Snapshot().NamedDBs["mutated"]; exists {
|
||||
t.Fatal("named database map was not copied on replacement")
|
||||
}
|
||||
if _, exists := registry.Snapshot().NamedRedis["mutated"]; exists {
|
||||
t.Fatal("named redis map was not copied on replacement")
|
||||
}
|
||||
|
||||
resources.NamedDBs["mutated"] = db
|
||||
if _, exists := registry.Snapshot().NamedDBs["mutated"]; exists {
|
||||
t.Fatal("snapshot shares its named database map with the registry")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceRegistryNormalizesTypedNilInterfaces(t *testing.T) {
|
||||
registry := NewResourceRegistry()
|
||||
var redisClient *redis.Client
|
||||
var storage *storageStub
|
||||
var mq *mqStub
|
||||
var webSocket *webSocketStub
|
||||
var scheduler *schedulerStub
|
||||
registry.Replace(Resources{Redis: redisClient, Storage: storage, MQ: mq, WebSocket: webSocket, Scheduler: scheduler})
|
||||
resources := registry.Snapshot()
|
||||
if resources.Redis != nil || resources.Storage != nil || resources.MQ != nil || resources.WebSocket != nil || resources.Scheduler != nil {
|
||||
t.Fatalf("typed nil resource was retained: %#v", resources)
|
||||
}
|
||||
if resources.Logger == nil {
|
||||
t.Fatal("nil logger was not replaced with the default logger")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceRegistryConcurrentAccess(t *testing.T) {
|
||||
registry := NewResourceRegistry()
|
||||
const iterations = 1000
|
||||
var wait sync.WaitGroup
|
||||
for index := 0; index < 8; index++ {
|
||||
wait.Add(1)
|
||||
go func() {
|
||||
defer wait.Done()
|
||||
for step := 0; step < iterations; step++ {
|
||||
registry.Replace(Resources{DB: &gorm.DB{}})
|
||||
if registry.Snapshot().Logger == nil {
|
||||
t.Error("concurrent snapshot lost the default logger")
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
wait.Wait()
|
||||
if registry.Snapshot().DB == nil {
|
||||
t.Fatal("concurrent replacement lost the database")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user