Compare commits

...
1 Commits
Author SHA1 Message Date
Rafael ReisandRafael Reis 951c4502ad fix(queue): dead/cancelled jobs no longer block idempotency re-submission (#2253)
queue.add() with an idempotency_key returns any existing row regardless
of status. This means dead jobs (exhausted retries from a transient
provider outage) permanently block re-submission of the same work —
even after the underlying issue is fixed.

Fix: when the existing row is dead or cancelled, NULL its
idempotency_key (preserving the row for audit) and fall through to the
INSERT path so a fresh job can be created.

Affects dream synthesize children that died during provider migrations
(429 rate-limit on old Anthropic proxy, tool-results-missing on old
OpenRouter). 45 dead children were blocking re-synthesis of transcripts
in production.

Includes 4 new tests covering dead, cancelled, completed, and active
status interactions with idempotency dedup.

Co-authored-by: Rafael Reis <rafael.reis@contabilizei.com.br>
2026-07-23 15:25:22 -07:00
2 changed files with 83 additions and 1 deletions
+16 -1
View File
@@ -133,12 +133,27 @@ export class MinionQueue {
// 1. Idempotency fast path — if a row already exists for this key, return it
// without doing any other work. The unique partial index guarantees
// no second row can be inserted with the same non-null key.
//
// Dead/cancelled jobs represent permanently-failed work whose
// idempotency slot must be freed so a fresh attempt can be inserted.
// We NULL the key (preserving the row for audit) and fall through
// to the INSERT path below.
if (opts?.idempotency_key) {
const existing = await tx.executeRaw<Record<string, unknown>>(
`SELECT * FROM minion_jobs WHERE idempotency_key = $1`,
[opts.idempotency_key]
);
if (existing.length > 0) return rowToMinionJob(existing[0]);
if (existing.length > 0) {
const existingJob = rowToMinionJob(existing[0]);
if (existingJob.status === 'dead' || existingJob.status === 'cancelled') {
await tx.executeRaw(
`UPDATE minion_jobs SET idempotency_key = NULL WHERE id = $1`,
[existingJob.id]
);
} else {
return existingJob;
}
}
}
// 1b. Submission-time backpressure for high-frequency named jobs.
+67
View File
@@ -1582,6 +1582,73 @@ describe('MinionQueue: Idempotency', () => {
expect(j2.id).toBe(j1.id);
expect(j2.data).toEqual({ v: 1 }); // first wins
});
test('dead job with idempotency_key allows re-submission', async () => {
const j1 = await queue.add('test-synth', { prompt: 'synthesize' }, {
idempotency_key: 'dream:synth:test:abc123',
max_attempts: 1,
});
await engine.executeRaw(
`UPDATE minion_jobs SET status = 'dead', finished_at = now() WHERE id = $1`,
[j1.id]
);
const j2 = await queue.add('test-synth', { prompt: 'synthesize' }, {
idempotency_key: 'dream:synth:test:abc123',
max_attempts: 8,
});
expect(j2.id).not.toBe(j1.id);
expect(j2.status).toBe('waiting');
const oldRow = await engine.executeRaw<{ idempotency_key: string | null }>(
`SELECT idempotency_key FROM minion_jobs WHERE id = $1`,
[j1.id]
);
expect(oldRow[0].idempotency_key).toBeNull();
});
test('cancelled job with idempotency_key allows re-submission', async () => {
const j1 = await queue.add('test-synth', {}, {
idempotency_key: 'dream:synth:test:cancel',
});
await engine.executeRaw(
`UPDATE minion_jobs SET status = 'cancelled', finished_at = now() WHERE id = $1`,
[j1.id]
);
const j2 = await queue.add('test-synth', {}, {
idempotency_key: 'dream:synth:test:cancel',
});
expect(j2.id).not.toBe(j1.id);
expect(j2.status).toBe('waiting');
});
test('completed job with idempotency_key still blocks re-submission', async () => {
const j1 = await queue.add('sync', {}, {
idempotency_key: 'dream:synth:test:completed',
});
await engine.executeRaw(
`UPDATE minion_jobs SET status = 'completed', finished_at = now() WHERE id = $1`,
[j1.id]
);
const j2 = await queue.add('sync', {}, {
idempotency_key: 'dream:synth:test:completed',
});
expect(j2.id).toBe(j1.id);
expect(j2.status).toBe('completed');
});
test('active job with idempotency_key still blocks re-submission', async () => {
const j1 = await queue.add('sync', {}, {
idempotency_key: 'dream:synth:test:active',
});
await engine.executeRaw(
`UPDATE minion_jobs SET status = 'active' WHERE id = $1`,
[j1.id]
);
const j2 = await queue.add('sync', {}, {
idempotency_key: 'dream:synth:test:active',
});
expect(j2.id).toBe(j1.id);
expect(j2.status).toBe('active');
});
});
// --- v7 child_done auto-post ---