MotirBuilding in public
MOTIR · moooon
onMotir
You’re viewing a public project. Anyone can view it — no account needed. Sign in to submit, upvote, or comment on requests.View-only — you can’t edit work items
MOTIR-3730

(motir-core) A double-click of Replay RAISES P2002 out of the dashboard action — the engine's DLQ replay enqueues by INSERT and does not treat the dedup violation as "already enqueued"

Done
Description

replayDLQ (lib/jobs/dlq.ts) enqueues a replay as a fresh job_event + job_queue pair and calls jobQueueRepository.create unguarded. Replaying the SAME dead-lettered row twice derives the SAME key — {original}:replay:{dlqId}, by design, so a double-click cannot double-deliver — so the second insert hits the job_queue_job_idempotency_key partial unique index and Prisma raises P2002, which propagates out of the Server Action behind the operator dashboard's Replay button.

Why it was invisible until now

Until MOTIR-3418 there were two arms. email.send reached the VENDOR arm in every unit test, where the second re-emit was swallowed server-side by an expiring dedup window and the operator saw two success toasts. The engine arm has behaved this way for every routed job since MOTIR-3463, and the engine arm is the only arm since MOTIR-3418 — so this is a production behaviour today, not a regression the retirement introduced.

The inconsistency, and it is one file over

dispatchEventToEngine treats exactly this violation as "already enqueued": isUniqueViolation(err) → retry once, then report it in alreadyEnqueued. That is the contract the engine states for a duplicate enqueue everywhere else. The replay path does not apply it — not by decision, but because the arm was written for the case where the row had not already been replayed.

What is NOT wrong

The dedup itself is correct and wanted. The re-shape to {original}:replay:{dlqId} is deliberate (PRODECT_FINDINGS #40) and the index is what makes it enforceable. Nothing here argues for weakening either.

Reproduction

tests/jobs/dlq.test.ts"replaying the SAME row twice REFUSES the second" asserts the current behaviour with the reasoning in a comment, so the defect is pinned rather than latent. Its assertion is the thing to invert when this is fixed.

Acceptance criteria

  • A second replayDLQ of an already-replayed row does not throw. It resolves, having enqueued nothing new, and the caller can tell that from the return value rather than from a log line.
  • The dashboard's Replay button reports "already replayed" (or equivalent) rather than an unhandled error; the DLQ row's replayedAt is not re-stamped by the no-op.
  • tests/jobs/dlq.test.ts's double-click test asserts the new behaviour, and its comment naming this bug is removed.
  • The unique index and the {original}:replay:{dlqId} re-shape are unchanged — this card weakens neither.

Context refs

  • lib/jobs/dlq.tsreplayDLQ, the unguarded jobQueueRepository.create
  • lib/jobs/engine/dispatcher.tsisUniqueViolation and the alreadyEnqueued contract this should match
  • lib/services/jobsDashboardService.ts — the Replay caller whose error surface this reaches
  • prisma/migrations/20260825220000_job_queue_idempotency_key/migration.sql — the index