Skip to content

feat(api): idempotent incident creation via dedup_key#321

Open
juanjogarciatorres43 wants to merge 1 commit into
mainfrom
feat/incident-idempotency-dedup-key
Open

feat(api): idempotent incident creation via dedup_key#321
juanjogarciatorres43 wants to merge 1 commit into
mainfrom
feat/incident-idempotency-dedup-key

Conversation

@juanjogarciatorres43

Copy link
Copy Markdown
Collaborator

What

Adds server-side de-duplication for incident creation: POST /api/v2/firefighter/incidents now accepts an optional dedup_key, and at most one open incident can exist per key. Automated sources can safely retry/re-deliver without opening duplicate incidents.

This is the IMPACT half of QC-159 (Datadog → IMPACT auto-raise; ADR-0110). It is independently mergeable and fully backward-compatible — manual creation paths (Slack/web) send no dedup_key (NULL) and are unaffected.

Why

The Datadog→IMPACT mechanism re-delivers and the source monitors flap heavily, so without dedup each alert would open a new P3 (and a new Slack channel). Doing dedup at the source of truth (the incident store) is atomic, correct across the incident lifecycle, and reusable by every future adopter — strictly better than a per-receiver dedup store.

How

  • Incident.dedup_key — optional CharField (NULL for manual incidents).
  • Partial unique constraintUNIQUE(dedup_key) WHERE dedup_key IS NOT NULL AND _status <= Mitigated (40).
    • Atomic → no duplicate even under concurrent delivery (no TOCTOU).
    • NULL keys never collide.
    • Once an incident reaches Post-mortem/Closed it leaves the partial index, so a later recurrence opens a new incident (no permanent blocking).
  • API — serializer accepts write-only dedup_key; on conflict it returns the existing open incident with HTTP 200 (instead of 201) and does not re-trigger the Slack channel workflow.

Tests

  • Model: collision while open, Mitigated still blocks, key freed past the open window, NULL never collides.
  • Serializer: returns existing incident on conflict / re-raises otherwise.
  • ✅ Full Python CI green on the branch (tests on Postgres, mypy, ruff, build).

Notes for review (Pulse)

  • Migration 0034 adds a partial unique index on incidents_incident (small table → normal build is fine; switch to CREATE UNIQUE INDEX CONCURRENTLY if a zero-lock build is preferred).
  • "Open" window is _status <= 40 (Mitigated) — a recurrence during Post-mortem opens a new incident (intended).
  • except IntegrityError is broad but only swallowed when a matching open incident exists; otherwise re-raised.

Add server-side de-duplication for incident creation so automated sources
(e.g. the Datadog->IMPACT auto-raise mechanism) can safely retry/redeliver
without opening duplicate incidents.

- Incident.dedup_key: optional idempotency key (NULL for manual incidents).
- Partial unique constraint: at most one OPEN incident (status <= Mitigated)
  per dedup_key. NULL keys never collide; once an incident reaches
  Post-mortem/Closed the key is freed, so a later recurrence opens a new one.
- API: POST /incidents accepts an optional dedup_key (write-only). On conflict
  the existing open incident is returned with HTTP 200 (instead of 201), and
  the Slack channel workflow is NOT re-triggered.

Manual creation paths are unaffected (no dedup_key -> NULL -> no constraint).
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