优化结构

This commit is contained in:
Yvan
2026-08-24 13:25:16 +08:00
parent 7a9a738c6a
commit 0b6b8ddf1a
72 changed files with 2475 additions and 1443 deletions
+283
View File
@@ -0,0 +1,283 @@
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 (r *ResourceRegistry) Logger() *slog.Logger { return r.Snapshot().Logger }
func (r *ResourceRegistry) SetLogger(logger *slog.Logger) {
r.update(func(resources *Resources) { resources.Logger = logger })
}
func (r *ResourceRegistry) DB() *gorm.DB { return r.Snapshot().DB }
func (r *ResourceRegistry) SetDB(db *gorm.DB) {
r.update(func(resources *Resources) { resources.DB = db })
}
// NamedDB returns a configured secondary database by alias. An empty alias
// resolves to the primary database.
func (r *ResourceRegistry) NamedDB(name string) *gorm.DB {
if name == "" {
return r.DB()
}
return r.Snapshot().NamedDBs[name]
}
// SetNamedDBs replaces the complete secondary-database alias set.
func (r *ResourceRegistry) SetNamedDBs(databases map[string]*gorm.DB) {
r.update(func(resources *Resources) { resources.NamedDBs = cloneDBMap(databases) })
}
func (r *ResourceRegistry) Redis() redis.UniversalClient { return r.Snapshot().Redis }
func (r *ResourceRegistry) SetRedis(client redis.UniversalClient) {
r.update(func(resources *Resources) { resources.Redis = client })
}
// NamedRedis returns a configured secondary Redis client by alias. An empty
// alias resolves to the primary Redis client.
func (r *ResourceRegistry) NamedRedis(name string) redis.UniversalClient {
if name == "" {
return r.Redis()
}
return r.Snapshot().NamedRedis[name]
}
// SetNamedRedis replaces the complete secondary-Redis alias set.
func (r *ResourceRegistry) SetNamedRedis(clients map[string]redis.UniversalClient) {
r.update(func(resources *Resources) { resources.NamedRedis = cloneRedisMap(clients) })
}
func (r *ResourceRegistry) Mongo() *mongo.Client { return r.Snapshot().Mongo }
func (r *ResourceRegistry) SetMongo(client *mongo.Client) {
r.update(func(resources *Resources) { resources.Mongo = client })
}
func (r *ResourceRegistry) Storage() FileStorage { return r.Snapshot().Storage }
func (r *ResourceRegistry) SetStorage(storage FileStorage) {
r.update(func(resources *Resources) { resources.Storage = storage })
}
func (r *ResourceRegistry) MQ() platformmq.Registry { return r.Snapshot().MQ }
func (r *ResourceRegistry) SetMQ(registry platformmq.Registry) {
r.update(func(resources *Resources) { resources.MQ = registry })
}
func (r *ResourceRegistry) WebSocket() platformws.Hub { return r.Snapshot().WebSocket }
func (r *ResourceRegistry) SetWebSocket(hub platformws.Hub) {
r.update(func(resources *Resources) { resources.WebSocket = hub })
}
func (r *ResourceRegistry) Scheduler() Scheduler { return r.Snapshot().Scheduler }
func (r *ResourceRegistry) SetScheduler(scheduler Scheduler) {
r.update(func(resources *Resources) { resources.Scheduler = scheduler })
}
func (r *ResourceRegistry) update(change func(*Resources)) {
if r == nil || change == nil {
return
}
r.mu.Lock()
resources := cloneResources(r.resources)
change(&resources)
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()
func ResourceSnapshot() Resources { return defaultResources.Snapshot() }
func ReplaceResources(resources Resources) { defaultResources.Replace(resources) }
func Logger() *slog.Logger { return defaultResources.Logger() }
func SetLogger(logger *slog.Logger) { defaultResources.SetLogger(logger) }
func DB() *gorm.DB { return defaultResources.DB() }
func SetDB(db *gorm.DB) { defaultResources.SetDB(db) }
func NamedDB(name string) *gorm.DB { return defaultResources.NamedDB(name) }
func SetNamedDBs(databases map[string]*gorm.DB) { defaultResources.SetNamedDBs(databases) }
func Redis() redis.UniversalClient { return defaultResources.Redis() }
func SetRedis(client redis.UniversalClient) { defaultResources.SetRedis(client) }
func NamedRedis(name string) redis.UniversalClient { return defaultResources.NamedRedis(name) }
func SetNamedRedis(clients map[string]redis.UniversalClient) { defaultResources.SetNamedRedis(clients) }
func Mongo() *mongo.Client { return defaultResources.Mongo() }
func SetMongo(client *mongo.Client) { defaultResources.SetMongo(client) }
func Storage() FileStorage { return defaultResources.Storage() }
func SetStorage(storage FileStorage) { defaultResources.SetStorage(storage) }
func MQ() platformmq.Registry { return defaultResources.MQ() }
func SetMQ(registry platformmq.Registry) { defaultResources.SetMQ(registry) }
func WebSocket() platformws.Hub { return defaultResources.WebSocket() }
func SetWebSocket(hub platformws.Hub) { defaultResources.SetWebSocket(hub) }
func TaskScheduler() Scheduler { return defaultResources.Scheduler() }
func SetTaskScheduler(scheduler Scheduler) { defaultResources.SetScheduler(scheduler) }
+153
View File
@@ -0,0 +1,153 @@
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()
if registry.Logger() == nil {
t.Fatal("default logger is nil")
}
resources := registry.Snapshot()
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 TestResourceRegistryReplaceAndSetters(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()
registry.Replace(Resources{
Logger: logger, DB: db, NamedDBs: map[string]*gorm.DB{"reporting": db},
Redis: redisClient, NamedRedis: map[string]redis.UniversalClient{"cache": redisClient}, 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)
}
nextDB := &gorm.DB{}
registry.SetDB(nextDB)
if registry.DB() != nextDB {
t.Fatal("database setter did not publish the new database")
}
if registry.Redis() != redisClient || registry.Storage() != storage {
t.Fatal("database setter changed unrelated resources")
}
if registry.NamedDB("reporting") != db || registry.NamedDB("") != nextDB {
t.Fatal("named database lookup returned the wrong resource")
}
if registry.NamedRedis("cache") != redisClient || registry.NamedRedis("") != redisClient {
t.Fatal("named redis lookup returned the wrong resource")
}
inputDBs := map[string]*gorm.DB{"analytics": db}
registry.SetNamedDBs(inputDBs)
inputDBs["mutated"] = nextDB
if registry.NamedDB("mutated") != nil || registry.NamedDB("analytics") != db {
t.Fatal("named database map was not copied")
}
inputRedis := map[string]redis.UniversalClient{"sessions": redisClient}
registry.SetNamedRedis(inputRedis)
inputRedis["mutated"] = redisClient
if registry.NamedRedis("mutated") != nil || registry.NamedRedis("sessions") != redisClient {
t.Fatal("named redis map was not copied")
}
}
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.SetDB(&gorm.DB{})
_ = registry.Snapshot()
_ = registry.Logger()
}
}()
}
wait.Wait()
if registry.DB() == nil {
t.Fatal("concurrent setters lost the database")
}
}