Skip to content

Commit b953829

Browse files
committed
Change: Disable DLQ setup for RabbitMqEventBus by default
1 parent 419d424 commit b953829

6 files changed

Lines changed: 228 additions & 60 deletions

File tree

src/rabbitmq/RabbitMqCommandBus.ts

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { RabbitMqGateway, type Subscription } from './RabbitMqGateway.ts';
55
import { type ConfigProvider, resolveProvider } from './utils/index.ts';
66

77
export type RabbitMqCommandBusConfig = Partial<Pick<Subscription,
8-
'exchange' | 'queueName' | 'ignoreOwn' | 'concurrentLimit' | 'handlerProcessTimeout' | 'queueExpires'>>;
8+
'exchange' | 'queueName' | 'ignoreOwn' | 'concurrentLimit' | 'handlerProcessTimeout' | 'queueExpires' | 'deadLetterQueue'>>;
99

1010
type ResolvedRabbitMqCommandBusConfig = RabbitMqCommandBusConfig
1111
& Required<Pick<RabbitMqCommandBusConfig, 'exchange' | 'queueName' | 'ignoreOwn'>>;
@@ -18,7 +18,8 @@ async function resolveConfig(provider?: ConfigProvider<RabbitMqCommandBusConfig>
1818
concurrentLimit,
1919
handlerProcessTimeout,
2020
queueName,
21-
queueExpires
21+
queueExpires,
22+
deadLetterQueue
2223
} = await resolveProvider(provider) ?? {};
2324

2425
assertString(exchange, 'rabbitMqCommandConfig.exchange');
@@ -30,8 +31,10 @@ async function resolveConfig(provider?: ConfigProvider<RabbitMqCommandBusConfig>
3031
assertNonNegativeInteger(handlerProcessTimeout, 'rabbitMqCommandConfig.handlerProcessTimeout');
3132
if (queueExpires !== undefined)
3233
assertNonNegativeInteger(queueExpires, 'rabbitMqCommandConfig.queueExpires');
34+
if (deadLetterQueue !== undefined)
35+
assertBoolean(deadLetterQueue, 'rabbitMqCommandConfig.deadLetterQueue');
3336

34-
return { exchange, queueName, ignoreOwn, concurrentLimit, handlerProcessTimeout, queueExpires };
37+
return { exchange, queueName, ignoreOwn, concurrentLimit, handlerProcessTimeout, queueExpires, deadLetterQueue };
3538
}
3639

3740
/**
@@ -99,18 +102,12 @@ export class RabbitMqCommandBus implements ICommandBus {
99102
* Only one consumer receives each message.
100103
*/
101104
async on(messageType: string, handler: IMessageHandler): Promise<void> {
102-
const { exchange, queueName, ignoreOwn, concurrentLimit, handlerProcessTimeout, queueExpires } =
103-
await this.#resolveConfig();
105+
const subscriptionParams = await this.#resolveConfig();
104106

105107
await this.#gateway.subscribe({
106-
exchange,
107-
queueName,
108+
...subscriptionParams,
108109
eventType: messageType,
109-
handler,
110-
ignoreOwn,
111-
concurrentLimit,
112-
handlerProcessTimeout,
113-
queueExpires
110+
handler
114111
});
115112
}
116113

src/rabbitmq/RabbitMqEventBus.ts

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { RabbitMqGateway, type Subscription, type SubscribeResult } from './Rabb
66
import { type ConfigProvider, resolveProvider } from './utils/index.ts';
77

88
export type RabbitMqEventBusConfig = Partial<Pick<Subscription,
9-
'exchange' | 'queueName' | 'ignoreOwn' | 'concurrentLimit' | 'handlerProcessTimeout' | 'queueExpires'>>;
9+
'exchange' | 'queueName' | 'ignoreOwn' | 'concurrentLimit' | 'handlerProcessTimeout' | 'queueExpires' | 'deadLetterQueue'>>;
1010

1111
type ResolvedRabbitMqEventBusConfig = RabbitMqEventBusConfig
1212
& Required<Pick<RabbitMqEventBusConfig, 'exchange' | 'ignoreOwn'>>;
@@ -19,7 +19,8 @@ async function resolveConfig(provider?: ConfigProvider<RabbitMqEventBusConfig>)
1919
concurrentLimit,
2020
handlerProcessTimeout,
2121
queueName,
22-
queueExpires
22+
queueExpires,
23+
deadLetterQueue = false
2324
} = await resolveProvider(provider) ?? {};
2425

2526
assertString(exchange, 'rabbitMqEventConfig.exchange');
@@ -32,8 +33,9 @@ async function resolveConfig(provider?: ConfigProvider<RabbitMqEventBusConfig>)
3233
assertString(queueName, 'rabbitMqEventConfig.queueName');
3334
if (queueExpires !== undefined)
3435
assertNonNegativeInteger(queueExpires, 'rabbitMqEventConfig.queueExpires');
36+
assertBoolean(deadLetterQueue, 'rabbitMqEventConfig.deadLetterQueue');
3537

36-
return { exchange, ignoreOwn, concurrentLimit, handlerProcessTimeout, queueName, queueExpires };
38+
return { exchange, ignoreOwn, concurrentLimit, handlerProcessTimeout, queueName, queueExpires, deadLetterQueue };
3739
}
3840

3941
/**
@@ -43,6 +45,10 @@ async function resolveConfig(provider?: ConfigProvider<RabbitMqEventBusConfig>)
4345
* By default uses an exclusive (non-durable) queue per connection.
4446
* Set `queueName` in config for a durable queue that survives restarts.
4547
*
48+
* Dead letter queue is disabled by default (`deadLetterQueue: false`).
49+
* When a handler fails or times out, the message is discarded.
50+
* Set `deadLetterQueue: true` to route rejected messages to a `.failed` queue.
51+
*
4652
* Optionally ignores messages published by this instance (default: true).
4753
*
4854
* Supports named durable queues via {@link queue} for single-consumer delivery.
@@ -92,18 +98,12 @@ export class RabbitMqEventBus implements IEventBus, IObservableQueueProvider {
9298
* (non-durable) queue that is deleted on disconnect.
9399
*/
94100
async on(eventType: string, handler: IMessageHandler): Promise<SubscribeResult> {
95-
const { exchange, queueName, ignoreOwn, concurrentLimit, handlerProcessTimeout, queueExpires } =
96-
await this.#resolveConfig();
101+
const subscriptionParams = await this.#resolveConfig();
97102

98103
return this.#gateway.subscribe({
99-
exchange,
100-
queueName,
104+
...subscriptionParams,
101105
eventType,
102106
handler,
103-
ignoreOwn,
104-
concurrentLimit,
105-
handlerProcessTimeout,
106-
queueExpires,
107107
singleActiveConsumer: true
108108
});
109109
}
@@ -133,15 +133,16 @@ export class RabbitMqEventBus implements IEventBus, IObservableQueueProvider {
133133
queue = new RabbitMqCommandBus({
134134
rabbitMqGateway: this.#gateway,
135135
rabbitMqCommandBusConfig: async () => {
136-
const { exchange, concurrentLimit, handlerProcessTimeout, queueExpires } =
136+
const { exchange, concurrentLimit, handlerProcessTimeout, queueExpires, deadLetterQueue } =
137137
await this.#resolveConfig();
138138

139139
return {
140140
exchange,
141141
queueName,
142142
concurrentLimit,
143143
handlerProcessTimeout,
144-
queueExpires
144+
queueExpires,
145+
deadLetterQueue
145146
};
146147
}
147148
});

src/rabbitmq/RabbitMqGateway.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,13 @@ export type Subscription = {
6565
* Enables RabbitMQ single active consumer mode (`x-single-active-consumer`).
6666
*/
6767
singleActiveConsumer?: boolean;
68+
69+
/**
70+
* Whether to create a dead letter queue for rejected or timed out messages.
71+
* Only applicable to durable queues (when `queueName` is set).
72+
* Defaults to `true` when `queueName` is set, `false` otherwise.
73+
*/
74+
deadLetterQueue?: boolean;
6875
};
6976

7077
export type SubscribeResult = {
@@ -442,9 +449,13 @@ export class RabbitMqGateway {
442449
eventType,
443450
queueExpires,
444451
handlerProcessTimeout,
445-
singleActiveConsumer
452+
singleActiveConsumer,
453+
deadLetterQueue = !!queueName
446454
} = subscription;
447455

456+
if (deadLetterQueue && !queueName)
457+
throw new Error('deadLetterQueue requires a durable queue (queueName must be set)');
458+
448459
const channel = await this.#assertQueueChannel(queueName);
449460

450461
let queueGivenName = queueName;
@@ -471,14 +482,17 @@ export class RabbitMqGateway {
471482
}
472483
else {
473484
// Handle durable queue case
474-
const deadLetterExchangeName = `${exchange}.failed`;
485+
let deadLetterExchangeName: string | undefined;
486+
if (deadLetterQueue) {
487+
deadLetterExchangeName = `${exchange}.failed`;
475488

476-
// Assert dead letter queue for rejected or timed out messages
477-
await this.#assetQueue(channel, deadLetterExchangeName, `${queueGivenName}.failed`);
489+
// Assert dead letter queue for rejected or timed out messages
490+
await this.#assetQueue(channel, deadLetterExchangeName, `${queueGivenName}.failed`);
491+
}
478492

479493
// Assert durable queue that will survive broker restart
480494
const reply = await this.#assetQueue(channel, exchange, queueGivenName, eventType, {
481-
deadLetterExchangeName,
495+
...deadLetterExchangeName && { deadLetterExchangeName },
482496
...singleActiveConsumer && { singleActiveConsumer },
483497
...queueExpires && { queueExpires },
484498
handlerProcessTimeout

tests/integration/rabbitmq/RabbitMqGateway.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,33 @@ describe('RabbitMqGateway', () => {
227227
await cn2.close();
228228
});
229229

230+
it('does not create DLQ when deadLetterQueue is false', async () => {
231+
await gateway1.subscribe({
232+
exchange,
233+
queueName,
234+
eventType: 'dlq.disabled',
235+
handler: _msg => {
236+
throw new Error('intentional failure');
237+
},
238+
deadLetterQueue: false
239+
});
240+
241+
const message: IMessage = {
242+
type: 'dlq.disabled',
243+
payload: { shouldFail: true }
244+
};
245+
246+
await gateway1.publish(exchange, message);
247+
await delay(50);
248+
249+
// Verify the .failed queue does not exist
250+
const res = await fetch(`http://localhost:15672/api/queues/%2F/${encodeURIComponent(`${queueName}.failed`)}`, {
251+
headers: { Authorization: `Basic ${btoa('guest:guest')}` }
252+
});
253+
254+
expect(res.status).toBe(404);
255+
});
256+
230257
it('does not ack a message after timeout and logs it while consumer keeps processing', async () => {
231258
const errorLogs: Array<{ message: string, meta?: { [key: string]: any } }> = [];
232259
const logger: ILogger = {

tests/unit/rabbitmq/RabbitMqBuses.test.ts

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ describe('RabbitMq bus config forwarding', () => {
3737
concurrentLimit: 2,
3838
handlerProcessTimeout: 1234,
3939
queueExpires: 5678,
40+
deadLetterQueue: false,
4041
singleActiveConsumer: true
4142
});
4243
});
@@ -61,6 +62,7 @@ describe('RabbitMq bus config forwarding', () => {
6162
concurrentLimit: undefined,
6263
handlerProcessTimeout: undefined,
6364
queueExpires: undefined,
65+
deadLetterQueue: false,
6466
singleActiveConsumer: true
6567
});
6668
});
@@ -88,7 +90,8 @@ describe('RabbitMq bus config forwarding', () => {
8890
ignoreOwn: false,
8991
concurrentLimit: 3,
9092
handlerProcessTimeout: 2345,
91-
queueExpires: 6789
93+
queueExpires: 6789,
94+
deadLetterQueue: undefined
9295
});
9396
});
9497

@@ -114,7 +117,61 @@ describe('RabbitMq bus config forwarding', () => {
114117
ignoreOwn: false,
115118
concurrentLimit: 4,
116119
handlerProcessTimeout: 3456,
117-
queueExpires: 7890
120+
queueExpires: 7890,
121+
deadLetterQueue: false
118122
});
119123
});
124+
125+
it('forwards deadLetterQueue: false from event bus config to subscribe()', async () => {
126+
const rabbitMqGateway = createGateway();
127+
const bus = new RabbitMqEventBus({
128+
rabbitMqGateway: rabbitMqGateway as any,
129+
rabbitMqEventBusConfig: {
130+
exchange: 'events',
131+
queueName: 'event-queue',
132+
deadLetterQueue: false
133+
}
134+
});
135+
136+
await bus.on('test.event', handler);
137+
138+
expect(rabbitMqGateway.subscribe).toHaveBeenCalledWith(
139+
expect.objectContaining({ deadLetterQueue: false })
140+
);
141+
});
142+
143+
it('forwards deadLetterQueue: false from command bus config to subscribe()', async () => {
144+
const rabbitMqGateway = createGateway();
145+
const bus = new RabbitMqCommandBus({
146+
rabbitMqGateway: rabbitMqGateway as any,
147+
rabbitMqCommandBusConfig: {
148+
exchange: 'commands',
149+
queueName: 'command-queue',
150+
deadLetterQueue: false
151+
}
152+
});
153+
154+
await bus.on('test.command', handler);
155+
156+
expect(rabbitMqGateway.subscribe).toHaveBeenCalledWith(
157+
expect.objectContaining({ deadLetterQueue: false })
158+
);
159+
});
160+
161+
it('forwards deadLetterQueue from event bus queue() config to subscribe()', async () => {
162+
const rabbitMqGateway = createGateway();
163+
const bus = new RabbitMqEventBus({
164+
rabbitMqGateway: rabbitMqGateway as any,
165+
rabbitMqEventBusConfig: {
166+
exchange: 'events',
167+
deadLetterQueue: false
168+
}
169+
});
170+
171+
await bus.queue('shared-queue').on('test.event', handler);
172+
173+
expect(rabbitMqGateway.subscribe).toHaveBeenCalledWith(
174+
expect.objectContaining({ deadLetterQueue: false })
175+
);
176+
});
120177
});

0 commit comments

Comments
 (0)