|
@@ -75,11 +75,12 @@ Transitions:
|
|
|
|
|
|
|
|
1. Enqueue → `pending`, `attempt_count=0`, `next_attempt_at=now`.
|
|
1. Enqueue → `pending`, `attempt_count=0`, `next_attempt_at=now`.
|
|
|
2. Worker **claims** due `pending` rows → `processing` (see claim protocol).
|
|
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`.
|
|
- 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
|
|
### 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}` |
|
|
| id | INTEGER PK | Public id may be exposed as `whd_{id}` |
|
|
|
| webhook_id | INTEGER NOT NULL | |
|
|
| webhook_id | INTEGER NOT NULL | |
|
|
|
| user_id | INTEGER NOT NULL | Denormalized for isolation queries |
|
|
| 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 |
|
|
| event_type | TEXT NOT NULL | sent \| bounced \| failed |
|
|
|
| payload_json | TEXT NOT NULL | Exact body bytes basis (JSON text) |
|
|
| payload_json | TEXT NOT NULL | Exact body bytes basis (JSON text) |
|
|
|
| status | TEXT NOT NULL | pending \| processing \| success \| dead |
|
|
| 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
|
|
### 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.
|
|
Signed string: `{t}.{raw_body}` where `raw_body` is the exact stored JSON string POSTed.
|
|
|
|
|
|