Skip to content

Commit 09fa568

Browse files
authored
Merge pull request #165 from Agencybuilds/feat/dead-letter-queue
feat: Implement Dead Letter Queue for Failed Message Processing
2 parents 5c00b39 + 7eea3c8 commit 09fa568

4 files changed

Lines changed: 147 additions & 0 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Dead Letter Queue Architecture
2+
3+
## Overview
4+
The Dead Letter Queue (DLQ) is designed to handle failed message processing across all services. It ensures that critical messages are not lost when downstream services are unavailable, or processing fails repeatedly.
5+
6+
## Technical Requirements
7+
- **Performance**: Operations target < 100ms P99 for critical paths.
8+
- **Availability**: 99.99% uptime via resilient in-memory queuing combined with persistent flushing (future scope).
9+
- **Scope**: System-wide implementation affecting core pools and net pipelines.
10+
11+
## Component Design
12+
`DeadLetterQueue` holds a bounded deque of `FailedMessage` records. Each record contains:
13+
- `message_id`
14+
- `payload`
15+
- `error_reason`
16+
- `timestamp`
17+
- `retry_count`
18+
19+
## Blue-Green Strategy & Canary Analysis
20+
The DLQ component will be initially deployed to canary nodes to monitor processing and dropped message rates. Upon stable metric emission, it will be rolled out via a blue-green deployment strategy.
21+
22+
## Monitoring & Alerting
23+
Built-in metrics track `total_enqueued`, `total_processed`, and `total_dropped`. Dashboards will monitor these metrics. An alert is triggered if `total_dropped` increases over time.

docs/runbooks/dlq-monitoring.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Runbook: Dead Letter Queue (DLQ) Monitoring and Alerting
2+
3+
## Overview
4+
This runbook covers procedures to follow when DLQ alerts are triggered.
5+
6+
## Alerts
7+
8+
### 1. High Dropped Message Rate
9+
**Condition**: `dlq.metrics.total_dropped` increases by more than 100 within a 5-minute window.
10+
**Impact**: Messages are being lost because the queue is full, possibly due to a stuck downstream processor or unusually high failure rates.
11+
**Actions**:
12+
1. Check the error reasons for the failed messages.
13+
2. Verify if downstream processing nodes are healthy.
14+
3. Temporarily increase the queue capacity if necessary and safe.
15+
16+
### 2. High Enqueue Rate
17+
**Condition**: Spike in `total_enqueued`.
18+
**Impact**: Elevated message processing failures.
19+
**Actions**:
20+
1. Identify the service producing the failed messages.
21+
2. Check network stability and dependency health.
22+
23+
## Dashboards
24+
- DLQ processing rate
25+
- DLQ depth
26+
- Drop rate over time

src/core/dlq.rs

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
use alloc::vec::Vec;
2+
use alloc::string::String;
3+
use alloc::collections::VecDeque;
4+
5+
#[derive(Clone, Debug, PartialEq)]
6+
pub struct FailedMessage {
7+
pub message_id: String,
8+
pub payload: Vec<u8>,
9+
pub error_reason: String,
10+
pub timestamp: u64,
11+
pub retry_count: u8,
12+
}
13+
14+
pub struct DeadLetterQueue {
15+
pub queue: VecDeque<FailedMessage>,
16+
pub max_size: usize,
17+
pub metrics: DlqMetrics,
18+
}
19+
20+
#[derive(Default, Clone, Debug, PartialEq)]
21+
pub struct DlqMetrics {
22+
pub total_enqueued: u64,
23+
pub total_processed: u64,
24+
pub total_dropped: u64,
25+
}
26+
27+
impl DeadLetterQueue {
28+
pub fn new(max_size: usize) -> Self {
29+
Self {
30+
queue: VecDeque::new(),
31+
max_size,
32+
metrics: DlqMetrics::default(),
33+
}
34+
}
35+
36+
pub fn enqueue(&mut self, message: FailedMessage) -> Result<(), &'static str> {
37+
if self.queue.len() >= self.max_size {
38+
self.metrics.total_dropped += 1;
39+
return Err("Queue is full");
40+
}
41+
self.queue.push_back(message);
42+
self.metrics.total_enqueued += 1;
43+
Ok(())
44+
}
45+
46+
pub fn dequeue(&mut self) -> Option<FailedMessage> {
47+
if let Some(message) = self.queue.pop_front() {
48+
self.metrics.total_processed += 1;
49+
Some(message)
50+
} else {
51+
None
52+
}
53+
}
54+
55+
pub fn get_metrics(&self) -> DlqMetrics {
56+
self.metrics.clone()
57+
}
58+
}
59+
60+
#[cfg(test)]
61+
mod tests {
62+
use super::*;
63+
use alloc::string::ToString;
64+
use alloc::vec;
65+
66+
#[test]
67+
fn test_enqueue_dequeue() {
68+
let mut dlq = DeadLetterQueue::new(10);
69+
let msg = FailedMessage {
70+
message_id: "1".to_string(),
71+
payload: vec![1, 2, 3],
72+
error_reason: "timeout".to_string(),
73+
timestamp: 123456789,
74+
retry_count: 0,
75+
};
76+
assert!(dlq.enqueue(msg.clone()).is_ok());
77+
let dequeued = dlq.dequeue();
78+
assert_eq!(dequeued, Some(msg));
79+
assert_eq!(dlq.metrics.total_enqueued, 1);
80+
assert_eq!(dlq.metrics.total_processed, 1);
81+
}
82+
83+
#[test]
84+
fn test_queue_full() {
85+
let mut dlq = DeadLetterQueue::new(1);
86+
let msg = FailedMessage {
87+
message_id: "1".to_string(),
88+
payload: vec![1, 2, 3],
89+
error_reason: "timeout".to_string(),
90+
timestamp: 123456789,
91+
retry_count: 0,
92+
};
93+
assert!(dlq.enqueue(msg.clone()).is_ok());
94+
assert!(dlq.enqueue(msg.clone()).is_err());
95+
assert_eq!(dlq.metrics.total_dropped, 1);
96+
}
97+
}

src/core/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
pub mod liveness;
22
pub mod poc;
33
pub mod pool;
4+
pub mod dlq;

0 commit comments

Comments
 (0)