This commit is contained in:
2026-06-20 19:16:44 +08:00
parent db3d030169
commit dbb87823e8
26 changed files with 676 additions and 115 deletions
+50 -4
View File
@@ -125,7 +125,7 @@ func TestComplete_ClearsLease(t *testing.T) {
require.NoError(t, err)
leased, err := repo.Lease(ctx, "h")
require.NoError(t, err)
require.NoError(t, repo.Complete(ctx, leased.ID))
require.NoError(t, repo.Complete(ctx, leased.ID, "h"))
got, err := repo.Get(ctx, job.ID)
require.NoError(t, err)
assert.Equal(t, jobs.StatusCompleted, got.Status)
@@ -133,6 +133,49 @@ func TestComplete_ClearsLease(t *testing.T) {
assert.Nil(t, got.LeasedBy)
}
// TestComplete_LeaseLostIsNoOp verifies the fencing predicate: a write-back
// from a worker that no longer holds the lease (e.g. the reaper re-leased the
// job) matches 0 rows and returns ErrLeaseLost without clobbering the new run.
func TestComplete_LeaseLostIsNoOp(t *testing.T) {
t.Parallel()
ctx := context.Background()
repo, pool := setupRepo(t)
_, err := repo.Enqueue(ctx, jobs.TypeWebhookDeliver, json.RawMessage(`{}`), time.Now(), 5)
require.NoError(t, err)
// Worker A leases the job, then its handler stalls past the lease timeout.
leased, err := repo.Lease(ctx, "workerA")
require.NoError(t, err)
// Reaper resets it to pending; worker B re-leases it (now leased_by=workerB).
_, err = pool.Exec(ctx, `UPDATE jobs SET leased_at = now() - INTERVAL '10 minutes' WHERE id=$1`, leased.ID)
require.NoError(t, err)
ids, err := repo.ReapStuckRunningJobs(ctx, time.Now().Add(-5*time.Minute))
require.NoError(t, err)
require.Len(t, ids, 1)
reLeased, err := repo.Lease(ctx, "workerB")
require.NoError(t, err)
require.Equal(t, leased.ID, reLeased.ID)
// Stale worker A's terminal write-backs must be no-ops (ErrLeaseLost) and
// must not change the row that workerB now owns.
assert.ErrorIs(t, repo.Complete(ctx, leased.ID, "workerA"), jobs.ErrLeaseLost)
assert.ErrorIs(t, repo.FailWithRetry(ctx, leased.ID, "workerA", time.Now(), "stale"), jobs.ErrLeaseLost)
assert.ErrorIs(t, repo.DeadLetter(ctx, leased.ID, "workerA", "stale"), jobs.ErrLeaseLost)
got, err := repo.Get(ctx, leased.ID)
require.NoError(t, err)
assert.Equal(t, jobs.StatusRunning, got.Status, "row must still be owned by workerB's run")
require.NotNil(t, got.LeasedBy)
assert.Equal(t, "workerB", *got.LeasedBy)
assert.Nil(t, got.LastError, "stale write must not have set an error")
// Worker B (the true owner) completes successfully.
require.NoError(t, repo.Complete(ctx, leased.ID, "workerB"))
got, err = repo.Get(ctx, leased.ID)
require.NoError(t, err)
assert.Equal(t, jobs.StatusCompleted, got.Status)
}
func TestFailWithRetry_ReschedulesAndKeepsAttempts(t *testing.T) {
t.Parallel()
ctx := context.Background()
@@ -142,7 +185,7 @@ func TestFailWithRetry_ReschedulesAndKeepsAttempts(t *testing.T) {
leased, err := repo.Lease(ctx, "h")
require.NoError(t, err)
next := time.Now().Add(30 * time.Second)
require.NoError(t, repo.FailWithRetry(ctx, leased.ID, next, "boom"))
require.NoError(t, repo.FailWithRetry(ctx, leased.ID, "h", next, "boom"))
got, err := repo.Get(ctx, leased.ID)
require.NoError(t, err)
assert.Equal(t, jobs.StatusPending, got.Status)
@@ -160,7 +203,7 @@ func TestDeadLetter_FinalState(t *testing.T) {
require.NoError(t, err)
leased, err := repo.Lease(ctx, "h")
require.NoError(t, err)
require.NoError(t, repo.DeadLetter(ctx, leased.ID, "max attempts"))
require.NoError(t, repo.DeadLetter(ctx, leased.ID, "h", "max attempts"))
got, err := repo.Get(ctx, leased.ID)
require.NoError(t, err)
assert.Equal(t, jobs.StatusDead, got.Status)
@@ -204,7 +247,10 @@ func TestPurge_DeletesOldCompletedAndDead(t *testing.T) {
deadOld := mkOld(jobs.StatusDead)
freshComp, err := repo.Enqueue(ctx, jobs.TypeWebhookDeliver, json.RawMessage(`{}`), time.Now(), 5)
require.NoError(t, err)
require.NoError(t, repo.Complete(ctx, freshComp.ID))
leasedFresh, err := repo.Lease(ctx, "h")
require.NoError(t, err)
require.Equal(t, freshComp.ID, leasedFresh.ID)
require.NoError(t, repo.Complete(ctx, freshComp.ID, "h"))
rows, err := repo.PurgeCompleted(ctx, time.Now().Add(-7*24*time.Hour))
require.NoError(t, err)