fix task center archive claim SQL

This commit is contained in:
hy001 2026-07-08 20:03:52 +08:00
parent 714c1c5c68
commit 90ab87393e
5 changed files with 133 additions and 19 deletions

View File

@ -439,7 +439,7 @@ func Load() Config {
SecretID: getEnvAny([]string{"CHATAPP_TASK_CENTER_ARCHIVE_COS_SECRET_ID", "TASK_CENTER_ARCHIVE_COS_SECRET_ID", "LIKEI_TENCENT_COS_SECRET_ID"}, ""), SecretID: getEnvAny([]string{"CHATAPP_TASK_CENTER_ARCHIVE_COS_SECRET_ID", "TASK_CENTER_ARCHIVE_COS_SECRET_ID", "LIKEI_TENCENT_COS_SECRET_ID"}, ""),
SecretKey: getEnvAny([]string{"CHATAPP_TASK_CENTER_ARCHIVE_COS_SECRET_KEY", "TASK_CENTER_ARCHIVE_COS_SECRET_KEY", "LIKEI_TENCENT_COS_SECRET_KEY"}, ""), SecretKey: getEnvAny([]string{"CHATAPP_TASK_CENTER_ARCHIVE_COS_SECRET_KEY", "TASK_CENTER_ARCHIVE_COS_SECRET_KEY", "LIKEI_TENCENT_COS_SECRET_KEY"}, ""),
Prefix: getEnvAny([]string{"CHATAPP_TASK_CENTER_ARCHIVE_COS_PREFIX", "TASK_CENTER_ARCHIVE_COS_PREFIX"}, "task-center-events/v1"), Prefix: getEnvAny([]string{"CHATAPP_TASK_CENTER_ARCHIVE_COS_PREFIX", "TASK_CENTER_ARCHIVE_COS_PREFIX"}, "task-center-events/v1"),
BatchSize: getEnvIntAny([]string{"CHATAPP_TASK_CENTER_ARCHIVE_BATCH_SIZE", "TASK_CENTER_ARCHIVE_BATCH_SIZE"}, 10000), BatchSize: getEnvIntAny([]string{"CHATAPP_TASK_CENTER_ARCHIVE_BATCH_SIZE", "TASK_CENTER_ARCHIVE_BATCH_SIZE"}, 500),
FlushSeconds: getEnvIntAny([]string{"CHATAPP_TASK_CENTER_ARCHIVE_FLUSH_SECONDS", "TASK_CENTER_ARCHIVE_FLUSH_SECONDS"}, 60), FlushSeconds: getEnvIntAny([]string{"CHATAPP_TASK_CENTER_ARCHIVE_FLUSH_SECONDS", "TASK_CENTER_ARCHIVE_FLUSH_SECONDS"}, 60),
Workers: getEnvIntAny([]string{"CHATAPP_TASK_CENTER_ARCHIVE_WORKERS", "TASK_CENTER_ARCHIVE_WORKERS"}, 2), Workers: getEnvIntAny([]string{"CHATAPP_TASK_CENTER_ARCHIVE_WORKERS", "TASK_CENTER_ARCHIVE_WORKERS"}, 2),
}, },

View File

@ -116,19 +116,19 @@ func (TaskCenterEventDedup) TableName() string { return "task_center_event_dedup
// TaskCenterEventArchiveOutbox 保存待归档到 COS 的原始事件。 // TaskCenterEventArchiveOutbox 保存待归档到 COS 的原始事件。
type TaskCenterEventArchiveOutbox struct { type TaskCenterEventArchiveOutbox struct {
ID int64 `gorm:"column:id;primaryKey"` ID int64 `gorm:"column:id;primaryKey;index:idx_task_center_archive_claim_create,priority:3;index:idx_task_center_archive_claim_stale,priority:3"`
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_task_center_archive_event,priority:1"` SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_task_center_archive_event,priority:1"`
EventID string `gorm:"column:event_id;size:128;uniqueIndex:uk_task_center_archive_event,priority:2"` EventID string `gorm:"column:event_id;size:128;uniqueIndex:uk_task_center_archive_event,priority:2"`
EventType string `gorm:"column:event_type;size:64"` EventType string `gorm:"column:event_type;size:64"`
UserID int64 `gorm:"column:user_id"` UserID int64 `gorm:"column:user_id"`
OccurredAt time.Time `gorm:"column:occurred_at"` OccurredAt time.Time `gorm:"column:occurred_at"`
RawJSON string `gorm:"column:raw_json;type:text"` RawJSON string `gorm:"column:raw_json;type:text"`
Status string `gorm:"column:status;size:32;index:idx_task_center_archive_status_time,priority:1"` Status string `gorm:"column:status;size:32;index:idx_task_center_archive_status_time,priority:1;index:idx_task_center_archive_claim_create,priority:1;index:idx_task_center_archive_claim_stale,priority:1"`
RetryCount int `gorm:"column:retry_count"` RetryCount int `gorm:"column:retry_count"`
COSKey string `gorm:"column:cos_key;size:512"` COSKey string `gorm:"column:cos_key;size:512"`
FailureReason string `gorm:"column:failure_reason;size:512"` FailureReason string `gorm:"column:failure_reason;size:512"`
CreateTime time.Time `gorm:"column:create_time;index:idx_task_center_archive_status_time,priority:2"` CreateTime time.Time `gorm:"column:create_time;index:idx_task_center_archive_status_time,priority:2;index:idx_task_center_archive_claim_create,priority:2"`
UpdateTime time.Time `gorm:"column:update_time"` UpdateTime time.Time `gorm:"column:update_time;index:idx_task_center_archive_claim_stale,priority:2"`
ArchivedAt *time.Time `gorm:"column:archived_at"` ArchivedAt *time.Time `gorm:"column:archived_at"`
} }

View File

@ -20,6 +20,11 @@ import (
"gorm.io/gorm/clause" "gorm.io/gorm/clause"
) )
const (
defaultTaskCenterArchiveBatchSize = 500
maxTaskCenterArchiveBatchSize = 1000
)
// StartArchiveWorker 启动任务中心事件 COS 归档 worker。 // StartArchiveWorker 启动任务中心事件 COS 归档 worker。
func (s *Service) StartArchiveWorker(ctx context.Context) error { func (s *Service) StartArchiveWorker(ctx context.Context) error {
if !s.cfg.TaskCenter.Archive.Enabled { if !s.cfg.TaskCenter.Archive.Enabled {
@ -68,10 +73,7 @@ func (s *Service) runArchiveWorker(ctx context.Context, workerIndex int) {
} }
func (s *Service) archivePendingEvents(ctx context.Context) error { func (s *Service) archivePendingEvents(ctx context.Context) error {
batchSize := s.cfg.TaskCenter.Archive.BatchSize batchSize := normalizeTaskCenterArchiveBatchSize(s.cfg.TaskCenter.Archive.BatchSize)
if batchSize <= 0 {
batchSize = 10000
}
rows, err := s.claimArchiveRows(ctx, batchSize) rows, err := s.claimArchiveRows(ctx, batchSize)
if err != nil { if err != nil {
return err return err
@ -97,16 +99,30 @@ func (s *Service) claimArchiveRows(ctx context.Context, batchSize int) ([]model.
var rows []model.TaskCenterEventArchiveOutbox var rows []model.TaskCenterEventArchiveOutbox
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
staleProcessingBefore := time.Now().Add(-10 * time.Minute) staleProcessingBefore := time.Now().Add(-10 * time.Minute)
if err := tx.Clauses(clause.Locking{Strength: "UPDATE", Options: "SKIP LOCKED"}). staleLimit, _ := taskCenterArchiveClaimLimits(batchSize)
Where("status IN ? OR (status = ? AND update_time < ?)", if staleLimit > 0 {
[]string{ArchiveStatusPending, ArchiveStatusFailed}, var staleRows []model.TaskCenterEventArchiveOutbox
ArchiveStatusProcessing, // PROCESSING 超时行保留固定接管额度,避免 PENDING/FAILED 持续堆积时旧锁永久饥饿。
staleProcessingBefore, if err := tx.Clauses(clause.Locking{Strength: "UPDATE", Options: "SKIP LOCKED"}).
). Where("status = ? AND update_time < ?", ArchiveStatusProcessing, staleProcessingBefore).
Order("create_time asc, id asc"). Order("update_time asc, id asc").
Limit(batchSize). Limit(staleLimit).
Find(&rows).Error; err != nil { Find(&staleRows).Error; err != nil {
return err return err
}
rows = append(rows, staleRows...)
}
if remaining := batchSize - len(rows); remaining > 0 {
var retryableRows []model.TaskCenterEventArchiveOutbox
// PENDING/FAILED 只按状态和创建时间抢占,避免和 PROCESSING 超时接管揉成 OR 后放大扫描。
if err := tx.Clauses(clause.Locking{Strength: "UPDATE", Options: "SKIP LOCKED"}).
Where("status IN ?", []string{ArchiveStatusPending, ArchiveStatusFailed}).
Order("create_time asc, id asc").
Limit(remaining).
Find(&retryableRows).Error; err != nil {
return err
}
rows = append(rows, retryableRows...)
} }
if len(rows) == 0 { if len(rows) == 0 {
return nil return nil
@ -124,6 +140,30 @@ func (s *Service) claimArchiveRows(ctx context.Context, batchSize int) ([]model.
return rows, nil return rows, nil
} }
func normalizeTaskCenterArchiveBatchSize(batchSize int) int {
if batchSize <= 0 {
return defaultTaskCenterArchiveBatchSize
}
if batchSize > maxTaskCenterArchiveBatchSize {
return maxTaskCenterArchiveBatchSize
}
return batchSize
}
func taskCenterArchiveClaimLimits(batchSize int) (int, int) {
if batchSize <= 1 {
return batchSize, 0
}
staleLimit := batchSize / 10
if staleLimit < 1 {
staleLimit = 1
}
if staleLimit > 100 {
staleLimit = 100
}
return staleLimit, batchSize - staleLimit
}
func (s *Service) newCOSClient() (*cos.Client, error) { func (s *Service) newCOSClient() (*cos.Client, error) {
bucketURL, err := url.Parse(fmt.Sprintf("https://%s.cos.%s.myqcloud.com", bucketURL, err := url.Parse(fmt.Sprintf("https://%s.cos.%s.myqcloud.com",
strings.TrimSpace(s.cfg.TaskCenter.Archive.Bucket), strings.TrimSpace(s.cfg.TaskCenter.Archive.Bucket),

View File

@ -42,6 +42,51 @@ func newTestService(t *testing.T, java taskCenterJavaGateway) (*Service, *gorm.D
return service, db return service, db
} }
func TestNormalizeTaskCenterArchiveBatchSize(t *testing.T) {
tests := []struct {
name string
input int
want int
}{
{name: "default", input: 0, want: defaultTaskCenterArchiveBatchSize},
{name: "negative default", input: -1, want: defaultTaskCenterArchiveBatchSize},
{name: "keeps configured value", input: 200, want: 200},
{name: "caps large batch", input: 10000, want: maxTaskCenterArchiveBatchSize},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := normalizeTaskCenterArchiveBatchSize(tt.input); got != tt.want {
t.Fatalf("normalizeTaskCenterArchiveBatchSize(%d) = %d, want %d", tt.input, got, tt.want)
}
})
}
}
func TestTaskCenterArchiveClaimLimitsReserveStaleRows(t *testing.T) {
tests := []struct {
name string
batchSize int
wantStale int
wantRetryable int
}{
{name: "single row", batchSize: 1, wantStale: 1, wantRetryable: 0},
{name: "small batch keeps one stale slot", batchSize: 5, wantStale: 1, wantRetryable: 4},
{name: "normal batch uses ten percent", batchSize: 500, wantStale: 50, wantRetryable: 450},
{name: "large batch caps stale slot", batchSize: 1000, wantStale: 100, wantRetryable: 900},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotStale, gotRetryable := taskCenterArchiveClaimLimits(tt.batchSize)
if gotStale != tt.wantStale || gotRetryable != tt.wantRetryable {
t.Fatalf("taskCenterArchiveClaimLimits(%d) = (%d, %d), want (%d, %d)",
tt.batchSize, gotStale, gotRetryable, tt.wantStale, tt.wantRetryable)
}
})
}
}
func TestDailyTaskConfigEventListAndClaim(t *testing.T) { func TestDailyTaskConfigEventListAndClaim(t *testing.T) {
java := &taskCenterTestGateway{events: map[string]bool{}} java := &taskCenterTestGateway{events: map[string]bool{}}
service, db := newTestService(t, java) service, db := newTestService(t, java)

View File

@ -0,0 +1,29 @@
SET @current_schema := DATABASE();
SET @add_task_center_archive_claim_create_sql := IF(
NOT EXISTS (
SELECT 1 FROM INFORMATION_SCHEMA.STATISTICS
WHERE table_schema = @current_schema
AND table_name = 'task_center_event_archive_outbox'
AND index_name = 'idx_task_center_archive_claim_create'
),
'ALTER TABLE `task_center_event_archive_outbox` ADD INDEX `idx_task_center_archive_claim_create` (`status`, `create_time`, `id`)',
'SELECT 1'
);
PREPARE add_task_center_archive_claim_create_stmt FROM @add_task_center_archive_claim_create_sql;
EXECUTE add_task_center_archive_claim_create_stmt;
DEALLOCATE PREPARE add_task_center_archive_claim_create_stmt;
SET @add_task_center_archive_claim_stale_sql := IF(
NOT EXISTS (
SELECT 1 FROM INFORMATION_SCHEMA.STATISTICS
WHERE table_schema = @current_schema
AND table_name = 'task_center_event_archive_outbox'
AND index_name = 'idx_task_center_archive_claim_stale'
),
'ALTER TABLE `task_center_event_archive_outbox` ADD INDEX `idx_task_center_archive_claim_stale` (`status`, `update_time`, `id`)',
'SELECT 1'
);
PREPARE add_task_center_archive_claim_stale_stmt FROM @add_task_center_archive_claim_stale_sql;
EXECUTE add_task_center_archive_claim_stale_stmt;
DEALLOCATE PREPARE add_task_center_archive_claim_stale_stmt;