scheduler_cache.go 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. package service
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "strconv"
  7. "strings"
  8. "time"
  9. )
  10. const (
  11. SchedulerModeSingle = "single"
  12. SchedulerModeMixed = "mixed"
  13. SchedulerModeForced = "forced"
  14. )
  15. var (
  16. ErrSchedulerBucketRetired = errors.New("scheduler bucket retired")
  17. ErrSchedulerBucketWriteFenced = errors.New("scheduler bucket write fenced")
  18. ErrSchedulerGroupLifecycleLeaseInvalid = errors.New("scheduler group lifecycle lease invalid")
  19. ErrSchedulerGroupLifecycleLeaseLost = errors.New("scheduler group lifecycle lease lost")
  20. )
  21. // SchedulerBucketWriteToken fences a snapshot writer to one bucket epoch.
  22. // Tokens must be captured before any database load or queued rebuild work.
  23. type SchedulerBucketWriteToken struct {
  24. Bucket SchedulerBucket
  25. Epoch int64
  26. }
  27. func (t SchedulerBucketWriteToken) ValidFor(bucket SchedulerBucket) bool {
  28. return t.Epoch > 0 && t.Bucket == bucket
  29. }
  30. // SchedulerGroupLifecycleLease identifies one owner of a group's short-lived
  31. // retirement/reopen critical section.
  32. type SchedulerGroupLifecycleLease struct {
  33. GroupID int64
  34. OwnerToken string
  35. }
  36. func (l SchedulerGroupLifecycleLease) ValidFor(groupID int64) bool {
  37. return groupID > 0 && l.GroupID == groupID && l.OwnerToken != ""
  38. }
  39. type SchedulerBucket struct {
  40. GroupID int64
  41. Platform string
  42. Mode string
  43. }
  44. func (b SchedulerBucket) String() string {
  45. return fmt.Sprintf("%d:%s:%s", b.GroupID, b.Platform, b.Mode)
  46. }
  47. func ParseSchedulerBucket(raw string) (SchedulerBucket, bool) {
  48. parts := strings.Split(raw, ":")
  49. if len(parts) != 3 {
  50. return SchedulerBucket{}, false
  51. }
  52. groupID, err := strconv.ParseInt(parts[0], 10, 64)
  53. if err != nil {
  54. return SchedulerBucket{}, false
  55. }
  56. if parts[1] == "" || parts[2] == "" {
  57. return SchedulerBucket{}, false
  58. }
  59. return SchedulerBucket{
  60. GroupID: groupID,
  61. Platform: parts[1],
  62. Mode: parts[2],
  63. }, true
  64. }
  65. // SchedulerCache 负责调度快照与账号快照的缓存读写。
  66. type SchedulerCache interface {
  67. // GetSnapshot 读取快照并返回命中与否(ready + active + 数据完整)。
  68. GetSnapshot(ctx context.Context, bucket SchedulerBucket) ([]*Account, bool, error)
  69. // CaptureBucketWriteToken captures the current open epoch without changing
  70. // retirement state. A tombstoned bucket returns ErrSchedulerBucketRetired.
  71. CaptureBucketWriteToken(ctx context.Context, bucket SchedulerBucket) (SchedulerBucketWriteToken, error)
  72. // SetSnapshot 写入快照并切换激活版本。token 必须在 DB load/任务排队前取得。
  73. SetSnapshot(ctx context.Context, bucket SchedulerBucket, token SchedulerBucketWriteToken, accounts []Account) error
  74. // RetireBucket persistently tombstones a bucket and fences every older writer.
  75. // Readers that captured the active version before retirement may finish; new
  76. // readers observe ready/active as absent.
  77. RetireBucket(ctx context.Context, bucket SchedulerBucket) error
  78. // ReopenBucket is the only operation allowed to clear a tombstone. It returns
  79. // the retirement generation established by RetireBucket; repeated calls for
  80. // the same generation are idempotent. Callers must serialize a fresh authority
  81. // check through ReopenBucket with RetireBucket under the same group lifecycle
  82. // lease; ordinary rebuild paths never call ReopenBucket.
  83. ReopenBucket(ctx context.Context, bucket SchedulerBucket) (SchedulerBucketWriteToken, error)
  84. // TryAcquireGroupLifecycleLease serializes authoritative retirement/reopen
  85. // decisions for one non-zero group across instances.
  86. TryAcquireGroupLifecycleLease(ctx context.Context, groupID int64, ttl time.Duration) (SchedulerGroupLifecycleLease, bool, error)
  87. // ReleaseGroupLifecycleLease releases the lease only if its owner token still
  88. // matches, so an expired holder cannot delete a successor's lease. Missing,
  89. // expired, mismatched, and already released leases return
  90. // ErrSchedulerGroupLifecycleLeaseLost.
  91. ReleaseGroupLifecycleLease(ctx context.Context, lease SchedulerGroupLifecycleLease) error
  92. // GetAccount 获取单账号快照。
  93. GetAccount(ctx context.Context, accountID int64) (*Account, error)
  94. // SetAccount 写入单账号快照(包含不可调度状态)。
  95. SetAccount(ctx context.Context, account *Account) error
  96. // DeleteAccount 删除单账号快照。
  97. DeleteAccount(ctx context.Context, accountID int64) error
  98. // UpdateLastUsed 批量更新账号的最后使用时间。
  99. UpdateLastUsed(ctx context.Context, updates map[int64]time.Time) error
  100. // TryLockBucket 尝试获取分桶重建锁。
  101. TryLockBucket(ctx context.Context, bucket SchedulerBucket, ttl time.Duration) (bool, error)
  102. // UnlockBucket 释放分桶重建锁。
  103. UnlockBucket(ctx context.Context, bucket SchedulerBucket) error
  104. // ListBuckets 返回已注册的分桶集合。
  105. ListBuckets(ctx context.Context) ([]SchedulerBucket, error)
  106. // GetOutboxWatermark 读取 outbox 水位。
  107. GetOutboxWatermark(ctx context.Context) (int64, error)
  108. // SetOutboxWatermark 保存 outbox 水位。
  109. SetOutboxWatermark(ctx context.Context, id int64) error
  110. }