Skip to content

fix: stop aggregate writes losing messages silently - #734

Open
Maelstromeous wants to merge 3 commits into
mainfrom
fix/aggregate-write-errors
Open

Maelstromeous wants to merge 3 commits into
mainfrom
fix/aggregate-write-errors

Conversation

@Maelstromeous

@Maelstromeous Maelstromeous commented Sep 28, 2026 •

Copy link
Copy Markdown
Member

Manual steps

No manual steps or intervention required.

What this does

Aggregate writes could lose data without any trace. This makes the failures visible and retries the recoverable ones:

  • Duplicate-key races. Each message's docs ran as an ordered bulk of updateMany upserts, and a duplicate-key error was caught and reported as success. When two consumers created the same new row at once, the loser's first doc failed and every increment after it in that message was dropped. Upserts now use updateOne; a duplicate-key error on one doc is retried from that doc, and the docs after it still apply. A second failure on the same doc is thrown.
  • Failed writes were acknowledged. Any other write error was logged (or not, for per-alert rows) and the message acked, so a Mongo blip lost increments for good. A failed write that provably wrote nothing is now requeued once; if it fails again on redelivery it is logged as an error and dropped, so a bad message cannot loop. A prolonged Mongo outage still ends in dropped messages after that one retry; a durable delayed retry belongs with the delivery work in the next PR.
  • Replays never double count. A failed upsert is only requeued when it provably wrote nothing: the first operation failed, or no Mongo server was reachable. A failure after some increments applied is logged as an error and acked, because replaying it would count those increments twice. Message ids with de-duplication, which make every replay safe, are the next PR.
  • Lookup and insert failures. A failed alert lookup in a global message used to be reported as "instance does not exist" and dropped; only a genuinely missing alert is dropped now, and a lookup failure is requeued once. Failed inserts (populations, facility control events) are requeued once; those collections have unique indexes and inserts are now unordered, so a replay cannot duplicate rows.
  • Double ack on victories. The victories cache bump ran inside the same try as the write. If Redis failed after the message was acked, the catch acked it again, and RabbitMQ answers a double ack by closing the channel, which stops aggregate consumption until a restart. The cache bump now runs after the message is settled.

Pairs with ps2alerts/aggregator#1024, which fixes the keys those writes use. Either can deploy first.

Validation done

tsc --noEmit and ESLint clean. 13 specs cover the retry, requeue and no-replay rules, all passing, alongside the pagination specs from #736, and run in the Tests workflow.

🤖 Generated with Claude Code

- Upserts write each doc with updateOne. A duplicate-key race on one doc
  is retried from that doc and the rest still apply; before, the error
  was swallowed and every increment after it in the message was lost.
- Failed aggregate writes are requeued once, then logged and dropped,
  instead of being acknowledged as if they had succeeded.
- The victories cache bump runs after the message is settled, so a Redis
  failure can no longer ack the message twice and close the channel.
- Adds jest and ts-jest, which the existing jest config needed, with
  specs for the retry and requeue rules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d insert failures

Review follow-ups:
- A failed upsert is only requeued when it provably wrote nothing (the
  first operation failed, or no server was reachable). Replaying one that
  half-applied its increments would count them twice, so it is logged and
  acked instead.
- A failed alert lookup in a global message is requeued once rather than
  treated as a deleted alert and dropped.
- Failed inserts (populations, facility control events) are requeued
  once; the collections have unique indexes and inserts are now unordered,
  so a replay cannot duplicate rows or stop at the first duplicate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Takes main's lockfile, which now carries the same jest dependencies (#736).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant