Files
gh-jezweb-claude-skills-ski…/templates/queues-dlq-pattern.ts
2025-11-30 08:24:26 +08:00

216 lines
5.5 KiB
TypeScript

/**
* Dead Letter Queue (DLQ) Consumer
*
* Handles messages that failed after max retries in the main queue.
*
* Use when: You need to:
* - Log permanently failed messages
* - Alert ops team about failures
* - Store failed messages for manual review
* - Implement custom retry logic
*
* Setup:
* 1. Create DLQ: npx wrangler queues create my-dlq
* 2. Configure main queue consumer with DLQ:
* "dead_letter_queue": "my-dlq" in wrangler.jsonc
* 3. Create consumer for DLQ (this file)
* 4. Deploy both consumers
*/
type Env = {
DB: D1Database;
ALERTS: KVNamespace;
MAIN_QUEUE: Queue; // Reference to main queue for retry
};
export default {
async queue(
batch: MessageBatch,
env: Env,
ctx: ExecutionContext
): Promise<void> {
console.log(`⚠️ Processing ${batch.messages.length} FAILED messages from DLQ`);
for (const message of batch.messages) {
try {
await handleFailedMessage(message, env);
// Acknowledge to remove from DLQ
message.ack();
} catch (error) {
console.error(`Failed to process DLQ message ${message.id}:`, error);
// Don't ack - will retry in DLQ
}
}
},
};
/**
* Handle permanently failed message
*/
async function handleFailedMessage(message: Message, env: Env) {
console.log(`💀 Dead Letter Message:`);
console.log(` ID: ${message.id}`);
console.log(` Attempts: ${message.attempts}`);
console.log(` Original Timestamp: ${message.timestamp}`);
console.log(` Body:`, message.body);
// 1. Store in database for manual review
await storeFailed Message(message, env);
// 2. Send alert to ops team
await sendAlert(message, env);
// 3. Optional: Implement custom retry logic
if (shouldRetryInMainQueue(message)) {
await retryInMainQueue(message, env);
}
}
/**
* Store failed message in database
*/
async function storeFailedMessage(message: Message, env: Env) {
await env.DB.prepare(`
INSERT INTO failed_messages (
id,
queue_name,
body,
attempts,
original_timestamp,
failed_at,
error_details
) VALUES (?, ?, ?, ?, ?, ?, ?)
`)
.bind(
message.id,
'my-queue', // Or get from message.body if you include it
JSON.stringify(message.body),
message.attempts,
message.timestamp.toISOString(),
new Date().toISOString(),
JSON.stringify({
reason: 'Max retries exceeded',
lastAttempt: message.attempts,
})
)
.run();
console.log(`Stored failed message in database: ${message.id}`);
}
/**
* Send alert to ops team
*/
async function sendAlert(message: Message, env: Env) {
// Send to monitoring service (e.g., PagerDuty, Slack, Email)
const alert = {
severity: 'high',
title: 'Queue Message Permanently Failed',
description: `Message ${message.id} failed after ${message.attempts} attempts`,
details: {
messageId: message.id,
attempts: message.attempts,
body: message.body,
timestamp: message.timestamp.toISOString(),
},
};
// Example: Store in KV for alert aggregation
const alertKey = `alert:${message.id}`;
await env.ALERTS.put(alertKey, JSON.stringify(alert), {
expirationTtl: 86400 * 7, // 7 days
});
// Example: Send webhook to Slack
await fetch(process.env.SLACK_WEBHOOK_URL || '', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: `🚨 Queue DLQ Alert: Message ${message.id} failed permanently`,
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*Message ID:* ${message.id}\n*Attempts:* ${message.attempts}\n*Type:* ${message.body.type}`,
},
},
],
}),
});
console.log(`Alert sent for message: ${message.id}`);
}
/**
* Determine if message should be retried in main queue
* (e.g., after fixing a bug, or for specific message types)
*/
function shouldRetryInMainQueue(message: Message): boolean {
// Example: Retry if it's a payment message and attempts < 10
if (message.body.type === 'charge-payment' && message.attempts < 10) {
return true;
}
// Example: Retry if it failed due to a specific error
if (message.body.retryable === true) {
return true;
}
return false;
}
/**
* Retry message in main queue (with delay)
*/
async function retryInMainQueue(message: Message, env: Env) {
console.log(`Retrying message ${message.id} in main queue`);
// Send back to main queue with exponential delay
const delaySeconds = Math.min(
3600 * Math.pow(2, message.attempts - 3), // Start from where DLQ picked up
43200 // Max 12 hours
);
await env.MAIN_QUEUE.send(
{
...message.body,
retriedFromDLQ: true,
originalMessageId: message.id,
dlqAttempts: message.attempts,
},
{ delaySeconds }
);
console.log(`Message ${message.id} re-queued with ${delaySeconds}s delay`);
}
/**
* Manual retry endpoint (call via REST API or dashboard)
*/
export async function manualRetry(messageId: string, env: Env) {
// Fetch failed message from database
const result = await env.DB.prepare(
'SELECT * FROM failed_messages WHERE id = ?'
)
.bind(messageId)
.first();
if (!result) {
throw new Error(`Message ${messageId} not found in DLQ`);
}
const body = JSON.parse(result.body as string);
// Send back to main queue
await env.MAIN_QUEUE.send({
...body,
manualRetry: true,
retriedBy: 'admin',
retriedAt: new Date().toISOString(),
});
console.log(`Manually retried message: ${messageId}`);
}