Why a scraping run could report 602 successes out of 600 tasks — and the one-line-of-thinking fix that stops it.
→ / space / click to advance · ← to go back
Production runs occasionally showed successful > total. Physically impossible — a run has 600 tasks, it can't succeed 602 times.
The number wasn't wrong data. It was the same task counted twice.
+1 on the run rowA bare, commutative ADD on DynamoDB. Fast, lock-free — and assumes each task settles exactly once.
If a scrape takes longer than the visibility timeout (slow target site), SQS assumes A died and hands the message to B. Now two workers hold the same task.
+1.pending · not terminal → proceedprocessing · "resuming" → proceedPutTask(successful) → ADD +1PutTask(successful) → ADD +1// HandleMessage — the "guard" that was supposed to dedupe switch task.Status { case Pending: // first delivery → proceed case Processing: // redelivery → proceed (!) case Successful, Failed: // already done → drop ✓ return nil } ▲ read here … … gateway call (slow) … ▼ write terminal + ADD here — nothing held in between
The terminal drop only catches a sequential redelivery (A fully done before B reads). Two concurrent deliveries both read "not terminal" and sail through.
Instead of "read status, then blindly overwrite", the terminal write becomes a conditional write: "set me to successful — but only if I'm still processing."
Evaluated server-side inside the single DynamoDB PutItem. No version field, no extra read.
PutTaskIfStatus(processing→successful) WINS → ADD +1PutTaskIfStatus(processing→successful) ErrConflict (status already successful)Both still make one gateway call — that's inherent to at-least-once. Only the counting is now exactly-once.
PutTaskIfStatus()
Stops a duplicate from reverting an already-terminal row back to processing — which would re-open the whole window.
PutTaskIfStatus()
Gates the ADD (and the retry re-enqueue) on winning. Exactly one of N duplicates counts.
status. Zero extra reads, zero new rows — the write we already do simply refuses to run twice.successful row back to processing and lose the result key.total = 1 completed = 5 successful = 5 → FAIL: 5 > 1
total = 1 completed = 1 successful = 1 → PASS ✓
go test -tags=integration -race -run DoubleCount · success + failure paths · full suite green
github.com/ZenRows/conveyor · PR #166 · branch fix/cor-stats-double-count