ent.go 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. // Package repository 提供应用程序的基础设施层组件。
  2. // 包括数据库连接初始化、ORM 客户端管理、Redis 连接、数据库迁移等核心功能。
  3. package repository
  4. import (
  5. "context"
  6. "database/sql"
  7. "errors"
  8. "fmt"
  9. "log/slog"
  10. "strings"
  11. "time"
  12. "github.com/Wei-Shaw/sub2api/ent"
  13. "github.com/Wei-Shaw/sub2api/internal/config"
  14. "github.com/Wei-Shaw/sub2api/internal/pkg/timezone"
  15. "github.com/Wei-Shaw/sub2api/migrations"
  16. "entgo.io/ent/dialect"
  17. entsql "entgo.io/ent/dialect/sql"
  18. "github.com/lib/pq"
  19. )
  20. const (
  21. maxDatabaseInitializationRetries = 8
  22. databaseInitializationRetryBase = time.Second
  23. databaseInitializationRetryMax = 30 * time.Second
  24. )
  25. // initializeDatabaseWithRetry retries only errors that indicate PostgreSQL is
  26. // temporarily unavailable during startup. Permanent configuration, migration,
  27. // and data errors are returned immediately so they remain visible to operators.
  28. func initializeDatabaseWithRetry(ctx context.Context, initialize func(context.Context) error) error {
  29. return initializeDatabaseWithRetryWithWait(ctx, initialize, waitForDatabaseInitializationRetry)
  30. }
  31. func initializeDatabaseWithRetryWithWait(
  32. ctx context.Context,
  33. initialize func(context.Context) error,
  34. wait func(context.Context, time.Duration) error,
  35. ) error {
  36. for attempt := 1; ; attempt++ {
  37. if err := initialize(ctx); err == nil {
  38. return nil
  39. } else {
  40. if !isTransientDatabaseInitializationError(err) || attempt > maxDatabaseInitializationRetries {
  41. return err
  42. }
  43. delay := databaseInitializationRetryBase * time.Duration(1<<(attempt-1))
  44. if delay > databaseInitializationRetryMax {
  45. delay = databaseInitializationRetryMax
  46. }
  47. slog.Warn("database initialization temporarily unavailable; retrying",
  48. "retry", attempt,
  49. "max_retries", maxDatabaseInitializationRetries,
  50. "retry_in", delay,
  51. "error", err,
  52. )
  53. if err := wait(ctx, delay); err != nil {
  54. return err
  55. }
  56. }
  57. }
  58. }
  59. func waitForDatabaseInitializationRetry(ctx context.Context, delay time.Duration) error {
  60. timer := time.NewTimer(delay)
  61. defer timer.Stop()
  62. select {
  63. case <-ctx.Done():
  64. return ctx.Err()
  65. case <-timer.C:
  66. return nil
  67. }
  68. }
  69. func isTransientDatabaseInitializationError(err error) bool {
  70. var pqErr *pq.Error
  71. if !errors.As(err, &pqErr) {
  72. return false
  73. }
  74. code := string(pqErr.Code)
  75. return code == "57P03" || strings.HasPrefix(code, "08")
  76. }
  77. // InitEnt 初始化 Ent ORM 客户端并返回客户端实例和底层的 *sql.DB。
  78. //
  79. // 该函数执行以下操作:
  80. // 1. 初始化全局时区设置,确保时间处理一致性
  81. // 2. 建立 PostgreSQL 数据库连接
  82. // 3. 自动执行数据库迁移,确保 schema 与代码同步
  83. // 4. 创建并返回 Ent 客户端实例
  84. //
  85. // 重要提示:调用者必须负责关闭返回的 ent.Client(关闭时会自动关闭底层的 driver/db)。
  86. //
  87. // 参数:
  88. // - cfg: 应用程序配置,包含数据库连接信息和时区设置
  89. //
  90. // 返回:
  91. // - *ent.Client: Ent ORM 客户端,用于执行数据库操作
  92. // - *sql.DB: 底层的 SQL 数据库连接,可用于直接执行原生 SQL
  93. // - error: 初始化过程中的错误
  94. func InitEnt(cfg *config.Config) (*ent.Client, *sql.DB, error) {
  95. // 优先初始化时区设置,确保所有时间操作使用统一的时区。
  96. // 这对于跨时区部署和日志时间戳的一致性至关重要。
  97. if err := timezone.Init(cfg.Timezone); err != nil {
  98. return nil, nil, err
  99. }
  100. // 构建包含时区信息的数据库连接字符串 (DSN)。
  101. // 时区信息会传递给 PostgreSQL,确保数据库层面的时间处理正确。
  102. dsn := cfg.Database.DSNWithTimezone(cfg.Timezone)
  103. // 使用 Ent 的 SQL 驱动打开 PostgreSQL 连接。
  104. // dialect.Postgres 指定使用 PostgreSQL 方言进行 SQL 生成。
  105. var drv *entsql.Driver
  106. if cfg.Server.EnableServerTiming {
  107. connector, err := pq.NewConnector(dsn)
  108. if err != nil {
  109. return nil, nil, err
  110. }
  111. drv = entsql.OpenDB(dialect.Postgres, sql.OpenDB(newServerTimingConnector(connector)))
  112. } else {
  113. var err error
  114. drv, err = entsql.Open(dialect.Postgres, dsn)
  115. if err != nil {
  116. return nil, nil, err
  117. }
  118. }
  119. applyDBPoolSettings(drv.DB(), cfg)
  120. // 确保数据库 schema 已准备就绪。
  121. // SQL 迁移文件是 schema 的权威来源(source of truth)。
  122. // 这种方式比 Ent 的自动迁移更可控,支持复杂的迁移场景。
  123. migrationCtx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
  124. defer cancel()
  125. if err := initializeDatabaseWithRetry(migrationCtx, func(ctx context.Context) error {
  126. return applyMigrationsFS(ctx, drv.DB(), migrations.FS)
  127. }); err != nil {
  128. _ = drv.Close() // 迁移失败时关闭驱动,避免资源泄露
  129. return nil, nil, err
  130. }
  131. // 创建 Ent 客户端,绑定到已配置的数据库驱动。
  132. client := ent.NewClient(ent.Driver(drv))
  133. // 启动阶段:从配置或数据库中确保系统密钥可用。
  134. if err := ensureBootstrapSecrets(migrationCtx, client, cfg); err != nil {
  135. _ = client.Close()
  136. return nil, nil, err
  137. }
  138. // 在密钥补齐后执行完整配置校验,避免空 jwt.secret 导致服务运行时失败。
  139. if err := cfg.Validate(); err != nil {
  140. _ = client.Close()
  141. return nil, nil, fmt.Errorf("validate config after secret bootstrap: %w", err)
  142. }
  143. // SIMPLE 模式:启动时补齐各平台默认分组。
  144. // - anthropic/openai/gemini: 确保存在 <platform>-default
  145. // - antigravity: 仅要求存在 >=2 个未软删除分组(用于 claude/gemini 混合调度场景)
  146. if cfg.RunMode == config.RunModeSimple {
  147. seedCtx, seedCancel := context.WithTimeout(context.Background(), 30*time.Second)
  148. defer seedCancel()
  149. if err := ensureSimpleModeDefaultGroups(seedCtx, client); err != nil {
  150. _ = client.Close()
  151. return nil, nil, err
  152. }
  153. if err := ensureSimpleModeAdminConcurrency(seedCtx, client); err != nil {
  154. _ = client.Close()
  155. return nil, nil, err
  156. }
  157. }
  158. return client, drv.DB(), nil
  159. }