You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(webhook): add dead letter queue for failed message processing (#148)
Permanently failed deliveries (max retries exhausted) and SSRF-blocked jobs
are now routed to a bounded dead letter queue instead of being silently
dropped, preventing silent data loss during downstream outages. Adds
inspection, single-entry redelivery, and removal endpoints under /deadletter,
Prometheus DLQ metrics, and architecture/runbook documentation.
Closes#124
-**Max Retries**: Defaulted to **5 attempts** before a webhook is classified as failed.
94
94
95
-
### 3.6 Distributed Job Scheduler with Lease-based Worker Claiming
95
+
### 3.6 Dead Letter Queue (`/deadletter`)
96
96
97
-
Queue processing runs on a **distributed job scheduler** (`jobScheduler.ts`) where multiple worker loops compete to claim due webhook jobs under short-lived **leases**:
97
+
Rather than silently dropping a message that can never be delivered, the service routes permanently failed jobs into a bounded **Dead Letter Queue (DLQ)** for inspection and operator-driven redelivery:
98
98
99
-
-**Claim protocol**: A job becomes claimable once its `runAt` time has elapsed. A worker acquires the job by claiming a lease from a shared lease registry (`LeaseStore`); while that lease is valid, no other worker can claim the same job, so concurrent replicas/workers can never double-deliver the same webhook.
100
-
-**Fencing across processes**: `LeaseStore.claim` is atomic (synchronous within the Node event loop and serialisable against a shared store such as Redis/etcd in production), which gives cross-process mutual exclusion. Each lease carries a monotonic fencing token.
101
-
-**Heartbeat / lease renewal**: while a job is executing, the owning worker renews its lease on a configurable interval, so a healthy long-running delivery is never stolen by a competing worker.
102
-
-**Crash recovery**: if a worker dies without renewing, its lease expires and another worker reclaims the job — exactly-once under normal operation, at-least-once on worker failure.
103
-
-**Retry via rescheduling**: a failed attempt with retries remaining calls `ctx.reschedule(nextAttemptTime)` (exponential backoff + jitter), returning the job to the claimable pool at a future time.
104
-
-**Horizontal scaling**: worker count is controlled by `WEBHOOK_WORKER_COUNT` (default `3`). Scaling replicas or raising the worker count increases delivery concurrency without risking duplicate deliveries.
99
+
-**What is dead-lettered**: Deliveries that exhaust their maximum retry budget (`MAX_ATTEMPTS_EXHAUSTED`) and jobs rejected by the SSRF shield (`SSRF_BLOCKED`).
100
+
-**Retention**: A bounded, in-memory store (default **1,000 entries**, FIFO). When full, the oldest dead letter is evicted and counted as discarded for alerting.
101
+
-**Inspection**: `GET /deadletter` lists entries (newest first); `GET /deadletter/:id` fetches a single entry including the failed payload, attempt history, reason, and last error.
102
+
-**Redelivery**: `POST /deadletter/:id/requeue` reconstructs a fresh delivery job from the stored entry (with a fresh retry budget) and pushes it back onto the active queue. Requeued deliveries are re-signed and pass through the full security + retry pipeline again.
103
+
-**Removal**: `DELETE /deadletter/:id` removes a single entry; `DELETE /deadletter?confirm=true` purges the whole queue. Purging requires an explicit confirmation query parameter to prevent accidental data loss.
104
+
105
+
The DLQ is intentionally in-memory to mirror the service's ingestion pipeline and keep inspection/redelivery on the fast path. For deployments requiring cross-restart durability, the out-of-process persistent queue pattern (Redis/RabbitMQ) noted in the runbook should be substituted.
105
106
106
107
---
107
108
@@ -112,9 +113,7 @@ The service registers Prometheus counters and histograms to measure health indic
112
113
-`webhook_delivery_duration_seconds`: Histogram of endpoint response latency.
113
114
-`webhook_queue_size_current`: Gauge representing current queue occupancy.
114
115
-`webhook_failures_total`: Total dropped or exhausted delivery alerts.
115
-
-`webhook_scheduler_workers_current`: Gauge of active worker loops.
116
-
-`webhook_scheduler_active_leases_current`: Gauge of jobs currently executing under a worker lease.
117
-
-`webhook_scheduler_jobs_submitted_total`: Counter of jobs submitted to the scheduler.
118
-
-`webhook_scheduler_jobs_processed_total`: Counter of jobs executed by workers.
119
-
-`webhook_scheduler_jobs_failed_total`: Counter of jobs whose execution threw.
120
-
-`webhook_scheduler_lease_reclaimed_total`: Counter of expired leases reclaimed by another worker (crash recovery events).
116
+
-`webhook_dead_letter_queue_size_current`: Gauge of the current DLQ occupancy.
117
+
-`webhook_dead_letter_enqueued_total`: Counter of messages entering the DLQ, labeled by `reason` (`MAX_ATTEMPTS_EXHAUSTED` | `SSRF_BLOCKED`).
118
+
-`webhook_dead_letter_requeued_total`: Counter of dead letters pushed back onto the active queue.
119
+
-`webhook_dead_letter_discarded_total`: Counter of dead letters evicted, purged, or manually removed.
Copy file name to clipboardExpand all lines: docs/WEBHOOK_RUNBOOK.md
+32-13Lines changed: 32 additions & 13 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -82,20 +82,39 @@ To ensure the safety of the off-chain system, the SSRF (Server-Side Request Forg
82
82
83
83
---
84
84
85
-
## 5. Scheduler & Worker Operations
85
+
## 5. Dead Letter Queue (DLQ) Operations
86
86
87
-
Queue processing runs on the distributed job scheduler, where workers claim jobs under short-lived leases. Diagnose scheduler health through the `webhook_scheduler_*` metrics on `/metrics` and the scheduler block on `/health`.
87
+
When a webhook permanently fails after exhausting its retry budget, or is rejected by the SSRF shield, it is moved to the **dead letter queue** instead of being dropped. `webhook_dead_letter_queue_size_current` climbing or `webhook_dead_letter_enqueued_total` increasing indicates persistent downstream failures.
88
88
89
-
### Health Indicators
90
-
-`webhook_scheduler_workers_current`**0** → worker loops are not running; the scheduler cannot drain the queue. Restart the service.
91
-
-`webhook_scheduler_active_leases_current` sustained at the worker count → all workers are blocked on slow deliveries; inspect downstream endpoint latency and consider scaling out.
92
-
-`webhook_scheduler_lease_reclaimed_total` climbing → workers are expiring mid-delivery (lease not renewed). Investigate event-loop blocking / GC pauses, or increase the lease duration.
1. Confirm workers are healthy: `curl -s http://webhook-service.internal/health` and check `scheduler.workers` is non-empty and `scheduler.pendingCount` is not climbing.
-Delivery concurrency per instance: `WEBHOOK_WORKER_COUNT` (default `3`).
101
-
-Lease duration and heartbeat are configurable in `jobScheduler.ts` (`leaseDurationMs`, `leaseRenewIntervalMs`, `pollIntervalMs`).
118
+
### Operational Notes
119
+
-**Bounded retention**: The DLQ holds up to 1,000 entries in memory; the oldest entry is evicted (and counted as `webhook_dead_letter_discarded_total`) when capacity is exceeded.
120
+
-**Durability**: The DLQ is in-memory. For transactions that must survive service restarts, deploy the out-of-process persistent queue pattern (Redis/RabbitMQ) as the backing store.
0 commit comments