-
Notifications
You must be signed in to change notification settings - Fork 0
Enhance error handling and retry mechanisms in message queue with worker-level retry policy #255
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 15 commits
9a1c3cb
8bd3099
6764c89
dddbd6c
d0a62e5
9e6a114
57fb099
0cc4a4a
f112847
2ef3ca5
1105510
c9eb937
2bab751
aeddf2a
aede30b
c64e724
b1c80f0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -38,4 +38,5 @@ export type { | |||
| QueueDefinition, | ||||
| InferPublisherNames, | ||||
| InferConsumerNames, | ||||
| RetryPolicy, | ||||
|
||||
| RetryPolicy, |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,8 @@ pnpm add @amqp-contract/worker | |
|
|
||
| - ✅ **Type-safe message consumption** — Handlers are fully typed based on your contract | ||
| - ✅ **Automatic validation** — Messages are validated before reaching your handlers | ||
| - ✅ **Retry policies** — Configurable retry limits with exponential backoff to prevent infinite loops | ||
| - ✅ **Dead letter exchange support** — Automatically route permanently failed messages to DLX | ||
| - ✅ **Prefetch configuration** — Control message flow with per-consumer prefetch settings | ||
| - ✅ **Batch processing** — Process multiple messages at once for better throughput | ||
| - ✅ **Automatic reconnection** — Built-in connection management with failover support | ||
|
|
@@ -75,6 +77,59 @@ You can define handlers outside of the worker creation using `defineHandler` and | |
|
|
||
| ## Error Handling | ||
|
|
||
| ### Retry Policies (Production-Ready) | ||
|
|
||
| **For production use, always configure a retry policy** to prevent infinite retry loops and handle permanently failed messages gracefully. | ||
|
|
||
| ```typescript | ||
| import { defineConsumer, defineQueue, defineMessage } from "@amqp-contract/contract"; | ||
| import { z } from "zod"; | ||
|
|
||
| const orderQueue = defineQueue("order-processing", { | ||
| durable: true, | ||
| deadLetter: { | ||
| exchange: dlxExchange, // Messages that exceed retry limit go here | ||
| routingKey: "order.failed", | ||
| }, | ||
| }); | ||
|
|
||
| const orderMessage = defineMessage( | ||
| z.object({ | ||
| orderId: z.string(), | ||
| amount: z.number(), | ||
| }), | ||
| ); | ||
|
|
||
| const processOrderConsumer = defineConsumer(orderQueue, orderMessage, { | ||
| retryPolicy: { | ||
| maxAttempts: 3, // Maximum 3 attempts (initial + 2 retries) | ||
| backoff: { | ||
| type: "exponential", // or "fixed" | ||
| initialInterval: 1000, // Start with 1 second | ||
| maxInterval: 60000, // Cap at 60 seconds | ||
| coefficient: 2, // Double interval each retry (1s, 2s, 4s, ...) | ||
| }, | ||
| }, | ||
| }); | ||
| ``` | ||
|
||
|
|
||
| **Retry Policy Options:** | ||
|
|
||
| - `maxAttempts`: Maximum number of attempts (initial + retries, set to `0` for fail-fast behavior) | ||
| - `backoff.type`: `"fixed"` (same interval) or `"exponential"` (increasing interval) | ||
| - `backoff.initialInterval`: Interval in milliseconds before first retry (default: 1000) | ||
| - `backoff.maxInterval`: Maximum interval for exponential backoff (default: 60000) | ||
| - `backoff.coefficient`: Multiplier for exponential backoff (default: 2) | ||
|
|
||
| **Behavior:** | ||
|
|
||
| - Messages are retried up to `maxAttempts` times with configurable backoff intervals | ||
| - Attempt count is tracked in message headers (`x-retry-count`) | ||
| - After exhausting attempts, messages are sent to the dead letter exchange (if configured) | ||
| - If no DLX is configured, messages are rejected without requeue | ||
|
|
||
| ### Basic Error Handling | ||
|
|
||
| Worker handlers use standard Promise-based async/await pattern: | ||
|
|
||
| ```typescript | ||
|
|
@@ -86,7 +141,7 @@ handlers: { | |
| // Message acknowledged automatically on success | ||
| } catch (error) { | ||
| // Exception automatically caught by worker | ||
| // Message is requeued for retry | ||
| // Message is retried according to retry policy | ||
| throw error; | ||
| } | ||
| }; | ||
|
|
@@ -102,6 +157,16 @@ Worker defines error classes for internal use: | |
|
|
||
| These errors are logged but **handlers don't need to use them** - just throw standard exceptions. | ||
|
|
||
| ### Migration from Legacy Behavior | ||
|
|
||
| If you have existing consumers without retry policies, they will continue to work with the legacy behavior (infinite retries). However, **this is not recommended for production** as it can lead to infinite retry loops. | ||
|
|
||
| To migrate: | ||
|
|
||
| 1. Add a dead letter exchange to your queue configuration (optional but recommended) | ||
| 2. Configure a retry policy on your consumer definition | ||
btravers marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 3. Test with your actual failure scenarios to tune the retry parameters | ||
|
|
||
| ## API | ||
|
|
||
| For complete API documentation, see the [Worker API Reference](https://btravers.github.io/amqp-contract/api/worker). | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| import type { ContractDefinition } from "@amqp-contract/contract"; | ||
| import { TypedAmqpWorker } from "../worker.js"; | ||
| import type { WorkerInferConsumerHandlers } from "../types.js"; | ||
| import { it as baseIt } from "@amqp-contract/testing/extension"; | ||
|
|
||
| export const it = baseIt.extend<{ | ||
| workerFactory: <TContract extends ContractDefinition>( | ||
| contract: TContract, | ||
| handlers: WorkerInferConsumerHandlers<TContract>, | ||
| ) => Promise<TypedAmqpWorker<TContract>>; | ||
| }>({ | ||
| workerFactory: async ({ amqpConnectionUrl }, use) => { | ||
| const workers: Array<TypedAmqpWorker<ContractDefinition>> = []; | ||
|
|
||
| try { | ||
| await use( | ||
| async <TContract extends ContractDefinition>( | ||
| contract: TContract, | ||
| handlers: WorkerInferConsumerHandlers<TContract>, | ||
| ) => { | ||
| const worker = await TypedAmqpWorker.create({ | ||
| contract, | ||
| handlers, | ||
| urls: [amqpConnectionUrl], | ||
| }).resultToPromise(); | ||
|
|
||
| workers.push(worker); | ||
| return worker; | ||
| }, | ||
| ); | ||
| } finally { | ||
| await Promise.all( | ||
| workers.map(async (worker) => { | ||
| try { | ||
| await worker.close().resultToPromise(); | ||
| } catch (error) { | ||
| // Swallow errors during cleanup | ||
| console.error("Failed to close worker during fixture cleanup:", error); | ||
| } | ||
| }), | ||
| ); | ||
| } | ||
| }, | ||
| }); |
Uh oh!
There was an error while loading. Please reload this page.