Эх сурвалжийг харах

docs: pin webhook payload transaction and attempt counting

AI-Co-Authored-By: Grok
chendeben 1 сар өмнө
parent
commit
2d0d2118bd

+ 7 - 6
docs/superpowers/specs/2026-07-09-mailhub-webhooks-design.md

@@ -75,11 +75,12 @@ Transitions:
 
 1. Enqueue → `pending`, `attempt_count=0`, `next_attempt_at=now`.
 2. Worker **claims** due `pending` rows → `processing` (see claim protocol).
-3. HTTP 2xx → `success`.
-4. HTTP non-2xx / timeout / network error:
-   - If `attempt_count < 8` after increment → `pending` + backoff `next_attempt_at`.
+3. Complete HTTP attempt: always increment `attempt_count` (including 2xx).
+4. HTTP 2xx → `success`.
+5. HTTP non-2xx / timeout / network error:
+   - If `attempt_count < 8` → `pending` + backoff `next_attempt_at`.
    - Else → `dead`.
-5. Manual replay (from `success` or `dead`, or `pending` with errors) → `pending`, `attempt_count=0`, `next_attempt_at=now`, clear `error` (keep or clear last response preview; MVP may clear both).
+6. Manual replay (from `success` or `dead`, or `pending` with errors) → `pending`, `attempt_count=0`, `next_attempt_at=now`, clear `error` (MVP may clear response preview). **Reject or no-op if status is `processing`** (wait for lease expiry) to avoid racing an in-flight POST.
 
 ### Worker claim protocol
 
@@ -129,7 +130,7 @@ Indexes: `(user_id)`, `(user_id, domain_id)`.
 | id | INTEGER PK | Public id may be exposed as `whd_{id}` |
 | webhook_id | INTEGER NOT NULL | |
 | user_id | INTEGER NOT NULL | Denormalized for isolation queries |
-| send_event_id | INTEGER NOT NULL | |
+| send_event_id | INTEGER NOT NULL | Real event id, or `0` for synthetic tests; **no FK** to send_events |
 | event_type | TEXT NOT NULL | sent \| bounced \| failed |
 | payload_json | TEXT NOT NULL | Exact body bytes basis (JSON text) |
 | status | TEXT NOT NULL | pending \| processing \| success \| dead |
@@ -208,7 +209,7 @@ On webhook delete: CASCADE or mark deliveries orphaned—prefer **ON DELETE CASC
 
 ### Signature
 
-**Payload id construction:** Insert `webhook_deliveries` first to obtain `id`, then set `payload_json` with `"id": "whd_<id>"` (and stable `created_at`) before the first attempt. Signature always uses that stored `payload_json` as `raw_body`.
+**Payload id construction:** In **one SQLite transaction**, insert the delivery row (placeholder `payload_json` if needed), read `lastInsertRowid`, then set final `payload_json` with `"id": "whd_<id>"` and stable `created_at` **before commit**, so the worker never claims a row without the signed body. Signature always uses that stored `payload_json` as `raw_body`.
 
 Signed string: `{t}.{raw_body}` where `raw_body` is the exact stored JSON string POSTed.