fix(worker): 防止事务泄漏并恢复滞留队列
为 PostgreSQL 连接增加可配置的事务空闲与锁等待超时,并在请求取消或 Worker 退出后使用独立有界上下文回滚事务。\n\n恢复过期任务时按批次使用 SKIP LOCKED,周期重建缺失或终态 River Job;锁等待超时只在确认尚未提交上游时安全重排,避免重复执行和重复结算。\n\n验证:完整 Go 测试、go vet、生产 Kustomize 渲染、gofmt 与 git diff --check 均通过。
This commit is contained in:
@@ -543,6 +543,160 @@ SELECT
|
||||
t.Fatalf("recovered task left admissions=%d leases=%d", recoveredAdmissions, recoveredLeases)
|
||||
}
|
||||
|
||||
lockedRecoveryTask := createTask(true)
|
||||
unlockedRecoveryTask := createTask(true)
|
||||
if _, err := first.pool.Exec(ctx, `
|
||||
UPDATE gateway_tasks
|
||||
SET status = 'running',
|
||||
execution_token = gen_random_uuid(),
|
||||
execution_lease_expires_at = now() - interval '1 second'
|
||||
WHERE id = ANY($1::uuid[])`, []string{lockedRecoveryTask.ID, unlockedRecoveryTask.ID}); err != nil {
|
||||
t.Fatalf("prepare locked recovery tasks: %v", err)
|
||||
}
|
||||
lockTx, err := second.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("begin task row lock: %v", err)
|
||||
}
|
||||
var lockedTaskID string
|
||||
if err := lockTx.QueryRow(ctx, `
|
||||
SELECT id::text
|
||||
FROM gateway_tasks
|
||||
WHERE id = $1::uuid
|
||||
FOR UPDATE`, lockedRecoveryTask.ID).Scan(&lockedTaskID); err != nil {
|
||||
rollbackTransaction(lockTx)
|
||||
t.Fatalf("lock interrupted task row: %v", err)
|
||||
}
|
||||
recoveryCtx, recoveryCancel := context.WithTimeout(ctx, 3*time.Second)
|
||||
recovery, err = first.RecoverInterruptedRuntimeState(recoveryCtx)
|
||||
recoveryCancel()
|
||||
if err != nil {
|
||||
rollbackTransaction(lockTx)
|
||||
t.Fatalf("recover unlocked task while another row is locked: %v", err)
|
||||
}
|
||||
if recovery.RequeuedAsyncTasks < 1 {
|
||||
rollbackTransaction(lockTx)
|
||||
t.Fatalf("SKIP LOCKED recovery = %+v, want at least one requeued task", recovery)
|
||||
}
|
||||
var lockedStatus, unlockedStatus string
|
||||
if err := first.pool.QueryRow(ctx, `
|
||||
SELECT
|
||||
(SELECT status FROM gateway_tasks WHERE id = $1::uuid),
|
||||
(SELECT status FROM gateway_tasks WHERE id = $2::uuid)`,
|
||||
lockedRecoveryTask.ID,
|
||||
unlockedRecoveryTask.ID,
|
||||
).Scan(&lockedStatus, &unlockedStatus); err != nil {
|
||||
rollbackTransaction(lockTx)
|
||||
t.Fatalf("read SKIP LOCKED recovery states: %v", err)
|
||||
}
|
||||
if lockedStatus != "running" || unlockedStatus != "queued" {
|
||||
rollbackTransaction(lockTx)
|
||||
t.Fatalf("SKIP LOCKED recovery statuses = %s/%s, want running/queued", lockedStatus, unlockedStatus)
|
||||
}
|
||||
rollbackTransaction(lockTx)
|
||||
recovery, err = first.RecoverInterruptedRuntimeState(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("recover previously locked task: %v", err)
|
||||
}
|
||||
if recovery.RequeuedAsyncTasks < 1 {
|
||||
t.Fatalf("unlocked recovery = %+v, want requeued task", recovery)
|
||||
}
|
||||
if err := first.pool.QueryRow(ctx, `
|
||||
SELECT status
|
||||
FROM gateway_tasks
|
||||
WHERE id = $1::uuid`, lockedRecoveryTask.ID).Scan(&lockedStatus); err != nil {
|
||||
t.Fatalf("read unlocked recovery status: %v", err)
|
||||
}
|
||||
if lockedStatus != "queued" {
|
||||
t.Fatalf("unlocked recovery status = %s, want queued", lockedStatus)
|
||||
}
|
||||
|
||||
preSubmissionTask := createTask(true)
|
||||
preSubmissionToken := uuid.NewString()
|
||||
if _, err := first.pool.Exec(ctx, `
|
||||
UPDATE gateway_tasks
|
||||
SET status = 'running',
|
||||
execution_token = $2::uuid,
|
||||
execution_lease_expires_at = now() + interval '5 minutes'
|
||||
WHERE id = $1::uuid`,
|
||||
preSubmissionTask.ID,
|
||||
preSubmissionToken,
|
||||
); err != nil {
|
||||
t.Fatalf("prepare pre-submission lock timeout task: %v", err)
|
||||
}
|
||||
if _, err := first.pool.Exec(ctx, `
|
||||
INSERT INTO gateway_task_attempts (
|
||||
task_id, attempt_no, queue_key, status, upstream_submission_status
|
||||
)
|
||||
VALUES ($1::uuid, 1, 'integration-test', 'running', 'not_submitted')`,
|
||||
preSubmissionTask.ID,
|
||||
); err != nil {
|
||||
t.Fatalf("create pre-submission attempt: %v", err)
|
||||
}
|
||||
requeuedTask, changed, err := first.RequeueTaskBeforeUpstreamSubmission(
|
||||
ctx,
|
||||
preSubmissionTask.ID,
|
||||
preSubmissionToken,
|
||||
time.Second,
|
||||
)
|
||||
if err != nil || !changed || requeuedTask.Status != "queued" {
|
||||
t.Fatalf("pre-submission lock timeout requeue task=%+v changed=%v err=%v", requeuedTask, changed, err)
|
||||
}
|
||||
var preSubmissionAttempts int
|
||||
if err := first.pool.QueryRow(ctx, `
|
||||
SELECT count(*)
|
||||
FROM gateway_task_attempts
|
||||
WHERE task_id = $1::uuid`, preSubmissionTask.ID).Scan(&preSubmissionAttempts); err != nil {
|
||||
t.Fatalf("count cleaned pre-submission attempts: %v", err)
|
||||
}
|
||||
if preSubmissionAttempts != 0 {
|
||||
t.Fatalf("pre-submission lock timeout left %d attempts, want 0", preSubmissionAttempts)
|
||||
}
|
||||
|
||||
ambiguousTask := createTask(true)
|
||||
ambiguousToken := uuid.NewString()
|
||||
if _, err := first.pool.Exec(ctx, `
|
||||
UPDATE gateway_tasks
|
||||
SET status = 'running',
|
||||
execution_token = $2::uuid,
|
||||
execution_lease_expires_at = now() + interval '5 minutes'
|
||||
WHERE id = $1::uuid`,
|
||||
ambiguousTask.ID,
|
||||
ambiguousToken,
|
||||
); err != nil {
|
||||
t.Fatalf("prepare ambiguous submission task: %v", err)
|
||||
}
|
||||
if _, err := first.pool.Exec(ctx, `
|
||||
INSERT INTO gateway_task_attempts (
|
||||
task_id, attempt_no, queue_key, status, upstream_submission_status
|
||||
)
|
||||
VALUES ($1::uuid, 1, 'integration-test', 'running', 'submitting')`,
|
||||
ambiguousTask.ID,
|
||||
); err != nil {
|
||||
t.Fatalf("create ambiguous submission attempt: %v", err)
|
||||
}
|
||||
_, changed, err = first.RequeueTaskBeforeUpstreamSubmission(
|
||||
ctx,
|
||||
ambiguousTask.ID,
|
||||
ambiguousToken,
|
||||
time.Second,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("guard ambiguous submission task requeue: %v", err)
|
||||
}
|
||||
if changed {
|
||||
t.Fatal("ambiguous submission task was requeued")
|
||||
}
|
||||
var ambiguousStatus string
|
||||
if err := first.pool.QueryRow(ctx, `
|
||||
SELECT status
|
||||
FROM gateway_tasks
|
||||
WHERE id = $1::uuid`, ambiguousTask.ID).Scan(&ambiguousStatus); err != nil {
|
||||
t.Fatalf("read ambiguous submission task: %v", err)
|
||||
}
|
||||
if ambiguousStatus != "running" {
|
||||
t.Fatalf("ambiguous submission task status = %s, want running", ambiguousStatus)
|
||||
}
|
||||
|
||||
terminalResidue := createTask(true)
|
||||
result, err = first.TryTaskAdmission(ctx, inputFor(terminalResidue, 100, ""))
|
||||
if err != nil || !result.Admitted {
|
||||
|
||||
Reference in New Issue
Block a user