2026-04-01 00:58:42 +08:00
|
|
|
package bootstrap
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"fmt"
|
2026-04-13 16:08:12 +08:00
|
|
|
"path/filepath"
|
2026-04-02 00:31:28 +08:00
|
|
|
"time"
|
2026-04-01 00:58:42 +08:00
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
|
goredis "github.com/redis/go-redis/v9"
|
|
|
|
|
"go.uber.org/zap"
|
|
|
|
|
|
2026-04-02 00:31:28 +08:00
|
|
|
"github.com/geo-platform/tenant-api/internal/shared/auditlog"
|
2026-04-01 00:58:42 +08:00
|
|
|
"github.com/geo-platform/tenant-api/internal/shared/auth"
|
2026-05-05 23:01:25 +08:00
|
|
|
sharedbootstrap "github.com/geo-platform/tenant-api/internal/shared/bootstrap"
|
2026-04-02 00:31:28 +08:00
|
|
|
"github.com/geo-platform/tenant-api/internal/shared/cache"
|
2026-04-01 00:58:42 +08:00
|
|
|
"github.com/geo-platform/tenant-api/internal/shared/config"
|
2026-05-13 15:59:39 +08:00
|
|
|
"github.com/geo-platform/tenant-api/internal/shared/ipregion"
|
2026-04-01 00:58:42 +08:00
|
|
|
"github.com/geo-platform/tenant-api/internal/shared/llm"
|
2026-04-12 09:56:18 +08:00
|
|
|
"github.com/geo-platform/tenant-api/internal/shared/messaging/rabbitmq"
|
2026-04-01 00:58:42 +08:00
|
|
|
"github.com/geo-platform/tenant-api/internal/shared/middleware"
|
2026-04-05 17:14:13 +08:00
|
|
|
"github.com/geo-platform/tenant-api/internal/shared/objectstorage"
|
2026-04-01 00:58:42 +08:00
|
|
|
"github.com/geo-platform/tenant-api/internal/shared/observability"
|
|
|
|
|
"github.com/geo-platform/tenant-api/internal/shared/repository/postgres"
|
|
|
|
|
"github.com/geo-platform/tenant-api/internal/shared/repository/redis"
|
|
|
|
|
"github.com/geo-platform/tenant-api/internal/shared/response"
|
2026-04-05 17:14:13 +08:00
|
|
|
"github.com/geo-platform/tenant-api/internal/shared/retrieval"
|
2026-04-01 00:58:42 +08:00
|
|
|
"github.com/geo-platform/tenant-api/internal/shared/stream"
|
2026-04-17 09:19:37 +08:00
|
|
|
tenantapp "github.com/geo-platform/tenant-api/internal/tenant/app"
|
2026-04-13 16:08:12 +08:00
|
|
|
"github.com/geo-platform/tenant-api/internal/tenant/prompts"
|
2026-04-17 09:02:33 +08:00
|
|
|
"github.com/geo-platform/tenant-api/internal/tenant/repository"
|
2026-04-01 00:58:42 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type App struct {
|
2026-05-01 16:01:23 +08:00
|
|
|
ConfigStore *config.Store
|
|
|
|
|
Logger *zap.Logger
|
|
|
|
|
DB *pgxpool.Pool
|
|
|
|
|
MonitoringDB *pgxpool.Pool
|
|
|
|
|
RabbitMQ *rabbitmq.Client
|
|
|
|
|
Redis *goredis.Client
|
|
|
|
|
Engine *gin.Engine
|
|
|
|
|
JWT *auth.Manager
|
|
|
|
|
Sessions *auth.SessionStore
|
|
|
|
|
LoginGuard *auth.LoginGuard
|
2026-05-14 11:05:20 +08:00
|
|
|
PasswordCipher *auth.PasswordCipher
|
2026-05-01 16:01:23 +08:00
|
|
|
LLM llm.Client
|
|
|
|
|
ReloadableLLM *llm.ReloadableClient
|
|
|
|
|
RetrievalProvider retrieval.Provider
|
|
|
|
|
ReloadableRetrieval *retrieval.ReloadableProvider
|
|
|
|
|
VectorStore retrieval.VectorStore
|
|
|
|
|
ReloadableVectorStore *retrieval.ReloadableVectorStore
|
|
|
|
|
ObjectStorage objectstorage.Client
|
|
|
|
|
ReloadableObjectStorage *objectstorage.ReloadableClient
|
|
|
|
|
AuditLogs *auditlog.AsyncWriter
|
|
|
|
|
GenerationStreams *stream.GenerationHub
|
|
|
|
|
DesktopTaskStreams *stream.DesktopTaskHub
|
|
|
|
|
DesktopDispatch *stream.DesktopDispatchHub
|
|
|
|
|
Cache cache.Cache
|
2026-05-13 15:59:39 +08:00
|
|
|
IPRegions *ipregion.Resolver
|
2026-05-01 16:01:23 +08:00
|
|
|
BrandService *tenantapp.BrandService
|
2026-05-12 21:53:36 +08:00
|
|
|
QuestionExpansion *tenantapp.QuestionExpansionService
|
2026-05-01 16:01:23 +08:00
|
|
|
MonitoringService *tenantapp.MonitoringService
|
2026-05-29 23:17:01 +08:00
|
|
|
MediaSupplyService *tenantapp.MediaSupplyService
|
2026-05-01 16:01:23 +08:00
|
|
|
KolProfiles repository.KolProfileRepository
|
|
|
|
|
KolPackages repository.KolPackageRepository
|
|
|
|
|
KolMarketplace repository.KolMarketplaceRepository
|
|
|
|
|
KolPrompts repository.KolPromptRepository
|
|
|
|
|
KolSubscriptions repository.KolSubscriptionRepository
|
|
|
|
|
KolAssists repository.KolAssistRepository
|
|
|
|
|
KolPromptAsset *tenantapp.KolPromptAsset
|
2026-04-01 00:58:42 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func New(configPath string) (*App, error) {
|
2026-05-01 16:01:23 +08:00
|
|
|
configStore, err := config.NewStore(configPath)
|
2026-04-01 00:58:42 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("load config: %w", err)
|
|
|
|
|
}
|
2026-05-01 16:01:23 +08:00
|
|
|
cfg := configStore.Current()
|
2026-04-13 16:08:12 +08:00
|
|
|
prompts.SetConfigFile(filepath.Join(filepath.Dir(configPath), "prompts.yml"))
|
2026-04-01 00:58:42 +08:00
|
|
|
|
|
|
|
|
logger, err := observability.NewLogger(cfg.Log.Level, cfg.Log.Format)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("init logger: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
|
|
pool, err := postgres.NewPool(ctx, cfg.Database)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("init postgres: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-18 20:56:05 +08:00
|
|
|
if err := syncMembershipPlans(ctx, pool, cfg.Membership); err != nil {
|
|
|
|
|
pool.Close()
|
|
|
|
|
return nil, fmt.Errorf("sync membership plans: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-12 09:56:18 +08:00
|
|
|
monitoringPool, err := postgres.NewPool(ctx, cfg.MonitoringDatabase)
|
|
|
|
|
if err != nil {
|
|
|
|
|
pool.Close()
|
|
|
|
|
return nil, fmt.Errorf("init monitoring postgres: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-01 00:58:42 +08:00
|
|
|
rdb, err := redis.NewClient(ctx, cfg.Redis)
|
|
|
|
|
if err != nil {
|
2026-04-30 01:06:08 +08:00
|
|
|
logger.Warn("redis unavailable during startup; continuing with degraded auth/cache fallback", zap.Error(err))
|
|
|
|
|
rdb = redis.NewLazyClient(cfg.Redis)
|
2026-04-01 00:58:42 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-12 09:56:18 +08:00
|
|
|
mqClient, err := rabbitmq.New(ctx, cfg.RabbitMQ)
|
|
|
|
|
if err != nil {
|
|
|
|
|
pool.Close()
|
|
|
|
|
monitoringPool.Close()
|
|
|
|
|
_ = rdb.Close()
|
|
|
|
|
return nil, fmt.Errorf("init rabbitmq: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-01 00:58:42 +08:00
|
|
|
jwtMgr := auth.NewManager(cfg.JWT.Secret, cfg.JWT.AccessTTL, cfg.JWT.RefreshTTL)
|
|
|
|
|
sessions := auth.NewSessionStore(rdb)
|
2026-05-14 11:05:20 +08:00
|
|
|
passwordCipher, err := buildPasswordCipher(cfg.Auth.PasswordCipher)
|
|
|
|
|
if err != nil {
|
|
|
|
|
pool.Close()
|
|
|
|
|
monitoringPool.Close()
|
|
|
|
|
_ = rdb.Close()
|
|
|
|
|
_ = mqClient.Close()
|
|
|
|
|
return nil, fmt.Errorf("init password cipher: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if passwordCipher == nil {
|
|
|
|
|
logger.Warn("auth password cipher disabled; login request payloads may expose plaintext passwords in browser developer tools")
|
|
|
|
|
}
|
2026-05-12 12:32:39 +08:00
|
|
|
loginGuardCfg := auth.DefaultLoginGuardConfig("tenant")
|
|
|
|
|
loginGuardCfg.Enabled = cfg.Auth.LoginGuard.Enabled
|
|
|
|
|
if !loginGuardCfg.Enabled {
|
|
|
|
|
logger.Warn("tenant login guard disabled by config; brute-force / rate-limit protection is off")
|
|
|
|
|
}
|
|
|
|
|
loginGuard := auth.NewLoginGuard(rdb, loginGuardCfg)
|
2026-05-01 16:01:23 +08:00
|
|
|
llmClient := llm.NewReloadableClient(llm.New(cfg.LLM))
|
|
|
|
|
retrievalProvider := retrieval.NewReloadableProvider(retrieval.NewProvider(cfg.Retrieval, logger))
|
|
|
|
|
vectorStore := retrieval.NewReloadableVectorStore(retrieval.NewQdrantStore(cfg.Qdrant, logger))
|
|
|
|
|
objectStorageClient := objectstorage.NewReloadableClient(objectstorage.New(cfg.ObjectStorage, logger))
|
2026-04-02 00:31:28 +08:00
|
|
|
auditLogs := auditlog.NewAsyncWriter(pool, logger)
|
2026-04-15 14:20:26 +08:00
|
|
|
generationStreams := stream.NewGenerationHub(mqClient)
|
2026-04-19 14:18:20 +08:00
|
|
|
desktopTaskStreams := stream.NewDesktopTaskHub(mqClient)
|
2026-04-20 19:46:59 +08:00
|
|
|
desktopDispatchBindings := []string{
|
|
|
|
|
cfg.RabbitMQ.DesktopDispatchPublishPrefix + ".*",
|
2026-04-22 00:24:21 +08:00
|
|
|
cfg.RabbitMQ.DesktopDispatchMonitorPrefix + ".*",
|
|
|
|
|
cfg.RabbitMQ.DesktopDispatchMonitorPrefix + ".*.*",
|
2026-04-20 19:46:59 +08:00
|
|
|
}
|
|
|
|
|
desktopDispatch := stream.NewDesktopDispatchHub(mqClient, logger, desktopDispatchBindings)
|
2026-05-03 02:02:39 +08:00
|
|
|
appCache := cache.NewWithOptions(cfg.Cache.Driver, rdb, cache.Options{
|
|
|
|
|
Namespace: cfg.Cache.Namespace,
|
|
|
|
|
JitterRatio: cfg.Cache.JitterRatio,
|
|
|
|
|
MetricsEnabled: cfg.Cache.MetricsEnabled,
|
|
|
|
|
AsyncFillEnabled: cfg.Cache.AsyncFillEnabled,
|
|
|
|
|
AsyncFillWorkers: cfg.Cache.AsyncFillWorkers,
|
|
|
|
|
AsyncFillBuffer: cfg.Cache.AsyncFillBuffer,
|
|
|
|
|
AsyncFillTimeout: cfg.Cache.AsyncFillTimeout,
|
|
|
|
|
L1Enabled: cfg.Cache.L1Enabled,
|
|
|
|
|
L1TTL: cfg.Cache.L1TTL,
|
|
|
|
|
DeleteScanCount: cfg.Cache.DeleteScanCount,
|
|
|
|
|
})
|
2026-05-13 15:59:39 +08:00
|
|
|
ipRegions, err := ipregion.NewResolver(cfg.IPRegion.V4XDBPath, cfg.IPRegion.V6XDBPath, logger)
|
|
|
|
|
if err != nil {
|
|
|
|
|
logger.Warn("ip region resolver unavailable; location-aware defaults will be degraded", zap.Error(err))
|
|
|
|
|
}
|
2026-05-01 16:01:23 +08:00
|
|
|
brandService := tenantapp.NewBrandService(pool, monitoringPool, auditLogs, cfg.BrandLibrary).WithCache(appCache)
|
2026-05-13 15:59:39 +08:00
|
|
|
questionExpansion := tenantapp.NewQuestionExpansionService(pool, llmClient, brandService).
|
|
|
|
|
WithCache(appCache).
|
|
|
|
|
WithIPRegionResolver(ipRegions)
|
2026-05-01 16:01:23 +08:00
|
|
|
monitoringService := tenantapp.NewMonitoringService(pool, monitoringPool, mqClient, cfg.MonitoringDispatch, cfg.BrandLibrary, logger).WithRedis(rdb)
|
2026-06-03 22:11:22 +08:00
|
|
|
mediaSupplyService := tenantapp.NewMediaSupplyService(pool, rdb, logger, configStore).WithObjectStorage(objectStorageClient)
|
2026-04-17 09:02:33 +08:00
|
|
|
kolProfiles := repository.NewKolProfileRepository(pool)
|
2026-04-17 09:19:37 +08:00
|
|
|
kolPackages := repository.NewKolPackageRepository(pool)
|
2026-04-17 14:23:32 +08:00
|
|
|
kolMarketplace := repository.NewKolMarketplaceRepository(pool)
|
2026-04-17 09:19:37 +08:00
|
|
|
kolPrompts := repository.NewKolPromptRepository(pool)
|
2026-04-17 14:23:32 +08:00
|
|
|
kolSubscriptions := repository.NewKolSubscriptionRepository(pool)
|
2026-04-17 13:52:01 +08:00
|
|
|
kolAssists := repository.NewKolAssistRepository(pool)
|
2026-04-17 09:19:37 +08:00
|
|
|
kolPromptAsset := tenantapp.NewKolPromptAsset(objectStorageClient)
|
2026-04-01 00:58:42 +08:00
|
|
|
|
2026-05-05 23:01:25 +08:00
|
|
|
engine, err := sharedbootstrap.NewEngine(cfg.Server)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("init http engine: %w", err)
|
2026-04-01 00:58:42 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
engine.Use(
|
|
|
|
|
middleware.Recovery(logger),
|
|
|
|
|
middleware.RequestID(),
|
2026-04-12 09:56:18 +08:00
|
|
|
middleware.Logger(logger),
|
2026-04-01 00:58:42 +08:00
|
|
|
)
|
2026-05-14 11:05:20 +08:00
|
|
|
if cfg.Server.SecurityHeaders.Enabled {
|
|
|
|
|
engine.Use(middleware.SecurityHeaders())
|
|
|
|
|
}
|
|
|
|
|
engine.Use(middleware.CORSWithConfig(middleware.CORSConfig{
|
|
|
|
|
AllowedOrigins: cfg.Server.AllowedOrigins,
|
|
|
|
|
AllowAll: len(cfg.Server.AllowedOrigins) == 0,
|
|
|
|
|
}))
|
2026-04-01 00:58:42 +08:00
|
|
|
|
|
|
|
|
engine.GET("/api/health/live", func(c *gin.Context) {
|
|
|
|
|
response.Success(c, gin.H{"status": "alive"})
|
|
|
|
|
})
|
|
|
|
|
engine.GET("/api/health/ready", func(c *gin.Context) {
|
|
|
|
|
if err := pool.Ping(c.Request.Context()); err != nil {
|
|
|
|
|
response.Error(c, response.ErrServiceUnavailable(50301, "postgres_unavailable", "database is not reachable"))
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-04-12 09:56:18 +08:00
|
|
|
if err := monitoringPool.Ping(c.Request.Context()); err != nil {
|
|
|
|
|
response.Error(c, response.ErrServiceUnavailable(50303, "monitoring_postgres_unavailable", "monitoring database is not reachable"))
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-04-01 00:58:42 +08:00
|
|
|
if err := rdb.Ping(c.Request.Context()).Err(); err != nil {
|
|
|
|
|
response.Error(c, response.ErrServiceUnavailable(50302, "redis_unavailable", "redis is not reachable"))
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-04-12 09:56:18 +08:00
|
|
|
if err := mqClient.Ping(c.Request.Context()); err != nil {
|
|
|
|
|
response.Error(c, response.ErrServiceUnavailable(50304, "rabbitmq_unavailable", "rabbitmq is not reachable"))
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-04-01 00:58:42 +08:00
|
|
|
response.Success(c, gin.H{"status": "ready"})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return &App{
|
2026-05-01 16:01:23 +08:00
|
|
|
ConfigStore: configStore,
|
|
|
|
|
Logger: logger,
|
|
|
|
|
DB: pool,
|
|
|
|
|
MonitoringDB: monitoringPool,
|
|
|
|
|
RabbitMQ: mqClient,
|
|
|
|
|
Redis: rdb,
|
|
|
|
|
Engine: engine,
|
|
|
|
|
JWT: jwtMgr,
|
|
|
|
|
Sessions: sessions,
|
|
|
|
|
LoginGuard: loginGuard,
|
2026-05-14 11:05:20 +08:00
|
|
|
PasswordCipher: passwordCipher,
|
2026-05-01 16:01:23 +08:00
|
|
|
LLM: llmClient,
|
|
|
|
|
ReloadableLLM: llmClient,
|
|
|
|
|
RetrievalProvider: retrievalProvider,
|
|
|
|
|
ReloadableRetrieval: retrievalProvider,
|
|
|
|
|
VectorStore: vectorStore,
|
|
|
|
|
ReloadableVectorStore: vectorStore,
|
|
|
|
|
ObjectStorage: objectStorageClient,
|
|
|
|
|
ReloadableObjectStorage: objectStorageClient,
|
|
|
|
|
AuditLogs: auditLogs,
|
|
|
|
|
GenerationStreams: generationStreams,
|
|
|
|
|
DesktopTaskStreams: desktopTaskStreams,
|
|
|
|
|
DesktopDispatch: desktopDispatch,
|
|
|
|
|
Cache: appCache,
|
2026-05-13 15:59:39 +08:00
|
|
|
IPRegions: ipRegions,
|
2026-05-01 16:01:23 +08:00
|
|
|
BrandService: brandService,
|
2026-05-12 21:53:36 +08:00
|
|
|
QuestionExpansion: questionExpansion,
|
2026-05-01 16:01:23 +08:00
|
|
|
MonitoringService: monitoringService,
|
2026-05-29 23:17:01 +08:00
|
|
|
MediaSupplyService: mediaSupplyService,
|
2026-05-01 16:01:23 +08:00
|
|
|
KolProfiles: kolProfiles,
|
|
|
|
|
KolPackages: kolPackages,
|
|
|
|
|
KolMarketplace: kolMarketplace,
|
|
|
|
|
KolPrompts: kolPrompts,
|
|
|
|
|
KolSubscriptions: kolSubscriptions,
|
|
|
|
|
KolAssists: kolAssists,
|
|
|
|
|
KolPromptAsset: kolPromptAsset,
|
2026-04-01 00:58:42 +08:00
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-14 11:05:20 +08:00
|
|
|
func buildPasswordCipher(cfg config.PasswordCipherConfig) (*auth.PasswordCipher, error) {
|
|
|
|
|
if !cfg.Enabled {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
if cfg.PrivateKeyPEM == "" {
|
|
|
|
|
return auth.NewEphemeralPasswordCipher(cfg.KeyID)
|
|
|
|
|
}
|
|
|
|
|
return auth.NewPasswordCipher(cfg.PrivateKeyPEM, cfg.KeyID)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-01 16:01:23 +08:00
|
|
|
func (a *App) Config() *config.Config {
|
|
|
|
|
if a == nil || a.ConfigStore == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return a.ConfigStore.Current()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *App) StartConfigWatcher(ctx context.Context) {
|
|
|
|
|
if a == nil || a.ConfigStore == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
go func() {
|
|
|
|
|
if err := a.ConfigStore.Watch(ctx, a.Logger, a.applyReloadedConfig); err != nil && a.Logger != nil {
|
|
|
|
|
a.Logger.Warn("config hot reload watcher stopped", zap.Error(err))
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *App) applyReloadedConfig(event config.ReloadEvent) {
|
|
|
|
|
if a == nil || event.Current == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
a.JWT.Update(event.Current.JWT.Secret, event.Current.JWT.AccessTTL, event.Current.JWT.RefreshTTL)
|
|
|
|
|
if a.ReloadableLLM != nil {
|
|
|
|
|
a.ReloadableLLM.Update(llm.New(event.Current.LLM))
|
|
|
|
|
}
|
|
|
|
|
if a.ReloadableRetrieval != nil {
|
|
|
|
|
a.ReloadableRetrieval.Update(retrieval.NewProvider(event.Current.Retrieval, a.Logger))
|
|
|
|
|
}
|
|
|
|
|
if a.ReloadableVectorStore != nil {
|
|
|
|
|
a.ReloadableVectorStore.Update(retrieval.NewQdrantStore(event.Current.Qdrant, a.Logger))
|
|
|
|
|
}
|
|
|
|
|
if a.ReloadableObjectStorage != nil {
|
|
|
|
|
a.ReloadableObjectStorage.Update(objectstorage.New(event.Current.ObjectStorage, a.Logger))
|
|
|
|
|
}
|
|
|
|
|
if a.BrandService != nil {
|
|
|
|
|
a.BrandService.UpdateConfig(event.Current.BrandLibrary)
|
|
|
|
|
}
|
|
|
|
|
if a.MonitoringService != nil {
|
|
|
|
|
a.MonitoringService.UpdateConfig(event.Current.MonitoringDispatch, event.Current.BrandLibrary)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := syncMembershipPlans(context.Background(), a.DB, event.Current.Membership); err != nil {
|
|
|
|
|
a.Logger.Warn("config reload membership plan sync failed", zap.Error(err))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if config.RestartRequired(event.Changes) && a.Logger != nil {
|
|
|
|
|
a.Logger.Warn("config reload changed connection or listener settings; restart process to apply those fields")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-18 20:56:05 +08:00
|
|
|
// syncMembershipPlans upserts all configured plans in a single transaction so
|
|
|
|
|
// that a partial failure (or concurrent startup) never leaves the `plans` table
|
|
|
|
|
// in a half-synced state.
|
|
|
|
|
func syncMembershipPlans(ctx context.Context, pool *pgxpool.Pool, membership config.MembershipConfig) error {
|
|
|
|
|
tx, err := pool.Begin(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
defer func() {
|
|
|
|
|
_ = tx.Rollback(ctx)
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
if err := repository.NewTenantPlanRepository(tx).UpsertConfiguredPlans(ctx, membership); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return tx.Commit(ctx)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-01 00:58:42 +08:00
|
|
|
func (a *App) Close() {
|
2026-05-01 16:01:23 +08:00
|
|
|
if a.ConfigStore != nil {
|
|
|
|
|
a.ConfigStore.Close()
|
|
|
|
|
}
|
2026-04-02 00:31:28 +08:00
|
|
|
if a.AuditLogs != nil {
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
|
|
|
|
if err := a.AuditLogs.Close(ctx); err != nil {
|
|
|
|
|
a.Logger.Warn("shutdown with pending audit logs", zap.Error(err))
|
|
|
|
|
}
|
|
|
|
|
cancel()
|
|
|
|
|
}
|
2026-05-13 15:59:39 +08:00
|
|
|
if a.IPRegions != nil {
|
|
|
|
|
a.IPRegions.Close()
|
|
|
|
|
}
|
2026-04-01 00:58:42 +08:00
|
|
|
a.DB.Close()
|
2026-04-12 09:56:18 +08:00
|
|
|
if a.MonitoringDB != nil {
|
|
|
|
|
a.MonitoringDB.Close()
|
|
|
|
|
}
|
|
|
|
|
if a.RabbitMQ != nil {
|
|
|
|
|
_ = a.RabbitMQ.Close()
|
|
|
|
|
}
|
2026-04-01 00:58:42 +08:00
|
|
|
_ = a.Redis.Close()
|
|
|
|
|
_ = a.Logger.Sync()
|
|
|
|
|
}
|