Skip to content

fix(ingestion): close lost-event race on persist failure (#62)#76

Merged
JumpTechCode merged 1 commit into
mainfrom
fix/ingestion-lost-event-race
Jun 16, 2026
Merged

fix(ingestion): close lost-event race on persist failure (#62)#76
JumpTechCode merged 1 commit into
mainfrom
fix/ingestion-lost-event-race

Conversation

@JumpTechCode

Copy link
Copy Markdown
Collaborator

Summary

Closes #62.

The webhook handler marked the Redis idempotency key (SET NX EX 24h) before persisting the incident, with no rollback if persistence failed. A transient ingest failure after the mark turned the sender's byte-identical retry into a silently-dropped event — the exact failure mode ADR 0001 ("never lose an event") forbids.

Fix

  • Add unmark(source, body) to the IdempotencyStore protocol + RedisIdempotencyStore (deletes the key; key derivation factored into _idempotency_key).
  • Wrap ingest in the handler: on failure → log, best-effort unmark (compensating delete), return INFRA_UNAVAILABLE (→ HTTP 503, retryable) so the sender re-delivers and the cleared key lets the retry re-process.
  • If unmark itself fails, still return 503 and let the key fall back to its 24h TTL.

Why this is safe: ingest commits in a single transaction — a failure rolls back atomically, so unmark can never erase a body that was actually persisted.

Tests

  • test_ingest_failure_unmarks_idempotency_key — asserts 503 + unmark awaited (fails if the compensating delete is removed).
  • test_ingest_failure_still_503_when_unmark_also_fails — nested-failure path stays retryable.
  • test_unmark_makes_body_processable_again / test_unmark_absent_key_is_noop — Redis round-trip + no-op.

All 492 unit tests pass; ruff + mypy --strict clean.

Failure mode (the mantra)

On persist failure: logged (ingest_failed_after_idempotency_mark), key cleared, breaker-agnostic 503 returned to the sender; if the unmark fails too it's logged (idempotency_unmark_failed) and the 24h TTL is the backstop.

The webhook handler marked the Redis idempotency key (SET NX EX 24h)
before persisting the incident. A transient failure in `ingest` after
the mark turned the sender's byte-identical retry into a silently
dropped event — the exact failure mode ADR 0001 ("never lose an event")
is meant to eliminate.

Wrap `ingest` in the handler: on failure, best-effort `unmark` the key
(new IdempotencyStore method) and return INFRA_UNAVAILABLE (503) so the
sender re-delivers and the now-cleared key lets the retry re-process.
If the unmark itself fails, still return 503 and let the key fall back
to its 24h TTL. The fix is safe because `ingest` is single-transaction:
a failure rolls back, so unmark never erases a persisted event.

Adds regression tests for ingest-fails-after-mark (with and without a
failing unmark) and a mark→unmark→processable-again round-trip.

Closes #62

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@JumpTechCode
JumpTechCode merged commit 2d7f058 into main Jun 16, 2026
6 checks passed
@JumpTechCode
JumpTechCode deleted the fix/ingestion-lost-event-race branch June 16, 2026 06:28
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.

Ingestion: lost-event race — idempotency key marked before the incident is persisted

1 participant