@zdavison/matador 4.0.1 → 4.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/fanout.d.ts +62 -0
- package/dist/core/fanout.d.ts.map +1 -1
- package/dist/core/fanout.js +217 -15
- package/dist/core/fanout.test.js +904 -6
- package/dist/core/matador.d.ts +33 -0
- package/dist/core/matador.d.ts.map +1 -1
- package/dist/core/matador.js +6 -1
- package/dist/core/matador.test.js +243 -7
- package/dist/errors/index.d.ts +1 -1
- package/dist/errors/index.d.ts.map +1 -1
- package/dist/errors/index.js +2 -2
- package/dist/errors/matador-errors.d.ts +10 -8
- package/dist/errors/matador-errors.d.ts.map +1 -1
- package/dist/errors/matador-errors.js +18 -14
- package/dist/index.cjs +345 -73
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/transport/local/local-transport.d.ts +6 -0
- package/dist/transport/local/local-transport.d.ts.map +1 -1
- package/dist/transport/local/local-transport.js +35 -18
- package/dist/transport/local/local-transport.test.js +40 -4
- package/dist/transport/multi/multi-transport.d.ts +6 -0
- package/dist/transport/multi/multi-transport.d.ts.map +1 -1
- package/dist/transport/multi/multi-transport.js +20 -0
- package/dist/transport/multi/multi-transport.test.js +82 -0
- package/dist/transport/rabbitmq/rabbitmq-transport-reconnection.test.js +45 -6
- package/dist/transport/rabbitmq/rabbitmq-transport.d.ts +6 -0
- package/dist/transport/rabbitmq/rabbitmq-transport.d.ts.map +1 -1
- package/dist/transport/rabbitmq/rabbitmq-transport.js +83 -40
- package/dist/transport/transport.d.ts +12 -0
- package/dist/transport/transport.d.ts.map +1 -1
- package/dist/types/event.d.ts +17 -0
- package/dist/types/event.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/core/fanout.d.ts
CHANGED
|
@@ -12,6 +12,35 @@ export interface FanoutConfig {
|
|
|
12
12
|
readonly hooks: SafeHooks;
|
|
13
13
|
readonly topology: Topology;
|
|
14
14
|
readonly defaultQueue: string;
|
|
15
|
+
readonly maxRetryBufferSize?: number | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* Maximum number of retry attempts for a buffered message before it's
|
|
18
|
+
* dropped and reported via onEnqueueError, instead of being retried forever.
|
|
19
|
+
*
|
|
20
|
+
* @default undefined (no limit — retries until the buffer flushes successfully)
|
|
21
|
+
*/
|
|
22
|
+
readonly maxRetryAttempts?: number | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* How often (in ms) to attempt flushing the retry buffer, independent of
|
|
25
|
+
* transport reconnect events. This covers the case where the transport
|
|
26
|
+
* stays connected but an individual publish keeps failing (e.g. broker
|
|
27
|
+
* nack, publish confirm timeout) — onConnected never fires again for
|
|
28
|
+
* that, so nothing would otherwise retry it.
|
|
29
|
+
*
|
|
30
|
+
* @default 30000. Pass 0 to disable the interval and rely on
|
|
31
|
+
* onConnected/manual flushes only.
|
|
32
|
+
*/
|
|
33
|
+
readonly retryIntervalMs?: number | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Number of consecutive flush failures tolerated within a single flush
|
|
36
|
+
* pass before bailing out early and re-buffering the untried remainder,
|
|
37
|
+
* instead of retrying every buffered message against a broker that's
|
|
38
|
+
* rejecting everything.
|
|
39
|
+
*
|
|
40
|
+
* @default 10. Pass 0 (or a negative number) to disable the breaker and
|
|
41
|
+
* always attempt every buffered message on every flush pass.
|
|
42
|
+
*/
|
|
43
|
+
readonly maxConsecutiveFlushFailures?: number | undefined;
|
|
15
44
|
}
|
|
16
45
|
/**
|
|
17
46
|
* Result of sending an event.
|
|
@@ -46,7 +75,15 @@ export declare class FanoutEngine {
|
|
|
46
75
|
private readonly topology;
|
|
47
76
|
private readonly defaultQueue;
|
|
48
77
|
private enqueuingCount;
|
|
78
|
+
private flushInFlightCount;
|
|
79
|
+
private readonly retryBuffer;
|
|
80
|
+
private readonly maxRetryBufferSize;
|
|
81
|
+
private readonly maxRetryAttempts;
|
|
82
|
+
private readonly maxConsecutiveFlushFailures;
|
|
83
|
+
private readonly disposeOnConnected;
|
|
84
|
+
private readonly retryTimer;
|
|
49
85
|
constructor(config: FanoutConfig);
|
|
86
|
+
dispose(): void;
|
|
50
87
|
/**
|
|
51
88
|
* Current count of events being enqueued.
|
|
52
89
|
*/
|
|
@@ -55,6 +92,31 @@ export declare class FanoutEngine {
|
|
|
55
92
|
* Sends an event to all registered subscribers.
|
|
56
93
|
*/
|
|
57
94
|
send<T>(eventClass: EventClass<T>, event: Event<T>, options?: EventOptions): Promise<SendResult>;
|
|
95
|
+
/**
|
|
96
|
+
* Attempts a single buffered item during a flush pass
|
|
97
|
+
* @returns true if the attempt succeeded; false on failure
|
|
98
|
+
*/
|
|
99
|
+
private attemptFlushItem;
|
|
100
|
+
/**
|
|
101
|
+
* Flushes the retry buffer
|
|
102
|
+
*
|
|
103
|
+
* This is called when the transport reconnects, and is used to retry any messages that were buffered while the transport was disconnected
|
|
104
|
+
*/
|
|
105
|
+
private flushRetryBuffer;
|
|
106
|
+
/**
|
|
107
|
+
* Re-buffers messages that were never attempted because a flush pass
|
|
108
|
+
* stopped out early. Respects maxRetryBufferSize: concurrent sends can have
|
|
109
|
+
* refilled the buffer while this flush was running, so anything beyond
|
|
110
|
+
* remaining capacity is dropped and reported instead of silently growing
|
|
111
|
+
* the buffer past its cap.
|
|
112
|
+
*/
|
|
113
|
+
private rebufferUntried;
|
|
114
|
+
/**
|
|
115
|
+
* Handles a failed flush attempt for a single buffered item: drops it once
|
|
116
|
+
* maxRetryAttempts is exceeded, otherwise re-buffers it (unless the buffer
|
|
117
|
+
* is full, in which case it's dropped too).
|
|
118
|
+
*/
|
|
119
|
+
private handleFlushFailure;
|
|
58
120
|
private isSubscriberEnabled;
|
|
59
121
|
}
|
|
60
122
|
//# sourceMappingURL=fanout.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fanout.d.ts","sourceRoot":"","sources":["../../src/core/fanout.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAErD,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"fanout.d.ts","sourceRoot":"","sources":["../../src/core/fanout.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAErD,OAAO,KAAK,EAAe,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACpE,OAAO,KAAK,EAGV,KAAK,EACL,UAAU,EACV,YAAY,EACb,MAAM,mBAAmB,CAAC;AAsB3B;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAEjD;;;;;OAKG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAE/C;;;;;;;;;OASG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAE9C;;;;;;;;OAQG;IACH,QAAQ,CAAC,2BAA2B,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC3D;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,CAAC;CACvC;AAED;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;CACvB;AAED;;;;;;;;GAQG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IACtC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiB;IACxC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAY;IAClC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAW;IACpC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,kBAAkB,CAAK;IAC/B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAsB;IAClD,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAS;IAC5C,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAqB;IACtD,OAAO,CAAC,QAAQ,CAAC,2BAA2B,CAAS;IACrD,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAA2B;IAC9D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA6C;gBAE5D,MAAM,EAAE,YAAY;IAkChC,OAAO,IAAI,IAAI;IAMf;;OAEG;IACH,IAAI,wBAAwB,IAAI,MAAM,CAErC;IAED;;OAEG;IACG,IAAI,CAAC,CAAC,EACV,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC,EACzB,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,EACf,OAAO,GAAE,YAAiB,GACzB,OAAO,CAAC,UAAU,CAAC;IAgItB;;;OAGG;YACW,gBAAgB;IAsB9B;;;;OAIG;YACW,gBAAgB;IA2D9B;;;;;;OAMG;YACW,eAAe;IAqC7B;;;;OAIG;YACW,kBAAkB;YAiClB,mBAAmB;CAelC"}
|
package/dist/core/fanout.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { TransportSendError } from '../errors/index.js';
|
|
2
2
|
import { resolveTargetQueueName } from '../topology/index.js';
|
|
3
3
|
import { createEnvelope } from '../types/index.js';
|
|
4
|
+
/**
|
|
5
|
+
* Default number of consecutive flush failures tolerated within a single
|
|
6
|
+
* flush pass before bailing out early and re-buffering the untried
|
|
7
|
+
* remainder.
|
|
8
|
+
*/
|
|
9
|
+
const DEFAULT_MAX_CONSECUTIVE_FLUSH_FAILURES = 10;
|
|
4
10
|
/**
|
|
5
11
|
* Engine for fanning out events to subscribers.
|
|
6
12
|
*
|
|
@@ -17,18 +23,53 @@ export class FanoutEngine {
|
|
|
17
23
|
topology;
|
|
18
24
|
defaultQueue;
|
|
19
25
|
enqueuingCount = 0;
|
|
26
|
+
flushInFlightCount = 0;
|
|
27
|
+
retryBuffer = [];
|
|
28
|
+
maxRetryBufferSize;
|
|
29
|
+
maxRetryAttempts;
|
|
30
|
+
maxConsecutiveFlushFailures;
|
|
31
|
+
disposeOnConnected;
|
|
32
|
+
retryTimer;
|
|
20
33
|
constructor(config) {
|
|
21
34
|
this.transport = config.transport;
|
|
22
35
|
this.schema = config.schema;
|
|
23
36
|
this.hooks = config.hooks;
|
|
24
37
|
this.topology = config.topology;
|
|
25
38
|
this.defaultQueue = config.defaultQueue;
|
|
39
|
+
this.maxRetryBufferSize = config.maxRetryBufferSize ?? 5000;
|
|
40
|
+
this.maxRetryAttempts = config.maxRetryAttempts;
|
|
41
|
+
const maxConsecutiveFlushFailures = config.maxConsecutiveFlushFailures ??
|
|
42
|
+
DEFAULT_MAX_CONSECUTIVE_FLUSH_FAILURES;
|
|
43
|
+
this.maxConsecutiveFlushFailures =
|
|
44
|
+
maxConsecutiveFlushFailures > 0
|
|
45
|
+
? maxConsecutiveFlushFailures
|
|
46
|
+
: Number.POSITIVE_INFINITY;
|
|
47
|
+
// If the transport supports it, add a callback to flush the retry buffer when the transport reconnects
|
|
48
|
+
// The returned value is a function to unsubscribe from the callback
|
|
49
|
+
this.disposeOnConnected = this.transport.onConnected?.(() => {
|
|
50
|
+
void this.flushRetryBuffer();
|
|
51
|
+
});
|
|
52
|
+
// Also flush periodically: onConnected only fires on connection-level
|
|
53
|
+
// reconnects, so it never fires when the transport stays connected but
|
|
54
|
+
// an individual publish keeps failing (e.g. broker nack, confirm timeout).
|
|
55
|
+
const retryIntervalMs = config.retryIntervalMs ?? 30_000;
|
|
56
|
+
if (retryIntervalMs > 0) {
|
|
57
|
+
this.retryTimer = setInterval(() => {
|
|
58
|
+
void this.flushRetryBuffer();
|
|
59
|
+
}, retryIntervalMs);
|
|
60
|
+
this.retryTimer.unref?.();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
dispose() {
|
|
64
|
+
// Unsubscribe from the callback (that flushes the retry buffer when the transport reconnects)
|
|
65
|
+
this.disposeOnConnected?.();
|
|
66
|
+
clearInterval(this.retryTimer);
|
|
26
67
|
}
|
|
27
68
|
/**
|
|
28
69
|
* Current count of events being enqueued.
|
|
29
70
|
*/
|
|
30
71
|
get eventsBeingEnqueuedCount() {
|
|
31
|
-
return this.enqueuingCount;
|
|
72
|
+
return this.enqueuingCount + this.flushInFlightCount;
|
|
32
73
|
}
|
|
33
74
|
/**
|
|
34
75
|
* Sends an event to all registered subscribers.
|
|
@@ -68,11 +109,10 @@ export class FanoutEngine {
|
|
|
68
109
|
delayMs: options.delayMs,
|
|
69
110
|
});
|
|
70
111
|
// Send to transport
|
|
112
|
+
const sendOptions = options.delayMs !== undefined ? { delay: options.delayMs } : undefined;
|
|
71
113
|
this.enqueuingCount++;
|
|
72
114
|
try {
|
|
73
|
-
const usedTransport = await this.transport.send(qualifiedQueue, envelope,
|
|
74
|
-
? { delay: options.delayMs }
|
|
75
|
-
: undefined);
|
|
115
|
+
const usedTransport = await this.transport.send(qualifiedQueue, envelope, sendOptions);
|
|
76
116
|
sent++;
|
|
77
117
|
await this.hooks.onEnqueueSuccess({
|
|
78
118
|
envelope,
|
|
@@ -82,17 +122,52 @@ export class FanoutEngine {
|
|
|
82
122
|
}
|
|
83
123
|
catch (error) {
|
|
84
124
|
const cause = error instanceof Error ? error : new Error(String(error));
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
125
|
+
const shouldBuffer = options.buffer !== false;
|
|
126
|
+
if (shouldBuffer && this.retryBuffer.length < this.maxRetryBufferSize) {
|
|
127
|
+
this.retryBuffer.push({
|
|
128
|
+
queue: qualifiedQueue,
|
|
129
|
+
envelope,
|
|
130
|
+
sendOptions,
|
|
131
|
+
subscriberName: subscriber.name,
|
|
132
|
+
attempts: 0,
|
|
133
|
+
});
|
|
134
|
+
this.hooks.logger.warn(`[Matador] 🟡 Message for '${subscriber.name}' buffered for retry on reconnect.`, {
|
|
135
|
+
queue: qualifiedQueue,
|
|
136
|
+
subscriberName: subscriber.name,
|
|
137
|
+
bufferSize: this.retryBuffer.length,
|
|
138
|
+
maxBufferSize: this.maxRetryBufferSize,
|
|
139
|
+
});
|
|
140
|
+
if (options.throwOnBufferedFailure) {
|
|
141
|
+
const err = new TransportSendError(qualifiedQueue, cause);
|
|
142
|
+
// By adding the error, the main Send in Matador will throw
|
|
143
|
+
errors.push({
|
|
144
|
+
subscriberName: subscriber.name,
|
|
145
|
+
queue: qualifiedQueue,
|
|
146
|
+
error: err,
|
|
147
|
+
});
|
|
148
|
+
await this.hooks.onEnqueueError({
|
|
149
|
+
envelope,
|
|
150
|
+
error: err,
|
|
151
|
+
transport: this.transport.name,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
if (shouldBuffer) {
|
|
157
|
+
this.hooks.logger.error(`[Matador] 🔴 Retry buffer full (${this.maxRetryBufferSize}). Message for '${subscriber.name}' dropped and will not be retried.`);
|
|
158
|
+
}
|
|
159
|
+
const err = new TransportSendError(qualifiedQueue, cause);
|
|
160
|
+
errors.push({
|
|
161
|
+
subscriberName: subscriber.name,
|
|
162
|
+
queue: qualifiedQueue,
|
|
163
|
+
error: err,
|
|
164
|
+
});
|
|
165
|
+
await this.hooks.onEnqueueError({
|
|
166
|
+
envelope,
|
|
167
|
+
error: err,
|
|
168
|
+
transport: this.transport.name,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
96
171
|
}
|
|
97
172
|
finally {
|
|
98
173
|
this.enqueuingCount--;
|
|
@@ -105,6 +180,133 @@ export class FanoutEngine {
|
|
|
105
180
|
errors,
|
|
106
181
|
};
|
|
107
182
|
}
|
|
183
|
+
/**
|
|
184
|
+
* Attempts a single buffered item during a flush pass
|
|
185
|
+
* @returns true if the attempt succeeded; false on failure
|
|
186
|
+
*/
|
|
187
|
+
async attemptFlushItem(item) {
|
|
188
|
+
this.enqueuingCount++;
|
|
189
|
+
try {
|
|
190
|
+
const usedTransport = await this.transport.send(item.queue, item.envelope, item.sendOptions);
|
|
191
|
+
await this.hooks.onEnqueueSuccess({
|
|
192
|
+
envelope: item.envelope,
|
|
193
|
+
queue: item.queue,
|
|
194
|
+
transport: usedTransport,
|
|
195
|
+
});
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
await this.handleFlushFailure(item, error);
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
finally {
|
|
203
|
+
this.enqueuingCount--;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Flushes the retry buffer
|
|
208
|
+
*
|
|
209
|
+
* This is called when the transport reconnects, and is used to retry any messages that were buffered while the transport was disconnected
|
|
210
|
+
*/
|
|
211
|
+
async flushRetryBuffer() {
|
|
212
|
+
if (this.retryBuffer.length === 0)
|
|
213
|
+
return;
|
|
214
|
+
this.hooks.logger.info(`[Matador] ⏳ Flushing ${this.retryBuffer.length} buffered message(s)...`);
|
|
215
|
+
// Drain the buffer atomically so concurrent flush calls don't double-send.
|
|
216
|
+
const toFlush = this.retryBuffer.splice(0);
|
|
217
|
+
// Tracked separately from enqueuingCount so shutdown doesn't see a it falsely idle while items are being flushed
|
|
218
|
+
this.flushInFlightCount += toFlush.length;
|
|
219
|
+
let consecutiveFailures = 0;
|
|
220
|
+
try {
|
|
221
|
+
for (let i = 0; i < toFlush.length; i++) {
|
|
222
|
+
const item = toFlush[i];
|
|
223
|
+
if (!item) {
|
|
224
|
+
this.flushInFlightCount--;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const succeeded = await this.attemptFlushItem(item);
|
|
228
|
+
this.flushInFlightCount--;
|
|
229
|
+
// Small sleep to avoid overwhelming the transport
|
|
230
|
+
// Random sleep to avoid thundering herd (between 0 and 10ms)
|
|
231
|
+
const randomMs = Math.random() * 10;
|
|
232
|
+
await new Promise((resolve) => setTimeout(resolve, randomMs));
|
|
233
|
+
// A success means the transport is actually working, so it resets the consecutive failures streak
|
|
234
|
+
consecutiveFailures = succeeded ? 0 : consecutiveFailures + 1;
|
|
235
|
+
if (consecutiveFailures >= this.maxConsecutiveFlushFailures) {
|
|
236
|
+
// A run of failures this long is more likely a connection-level issue (broker down, slow, rejecting everything)
|
|
237
|
+
// So we stop early to avoid retrying 5k messages for nothing
|
|
238
|
+
const untried = toFlush.slice(i + 1);
|
|
239
|
+
this.flushInFlightCount -= untried.length;
|
|
240
|
+
if (untried.length > 0) {
|
|
241
|
+
await this.rebufferUntried(untried, consecutiveFailures);
|
|
242
|
+
}
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
finally {
|
|
248
|
+
this.flushInFlightCount = 0;
|
|
249
|
+
}
|
|
250
|
+
const remaining = this.retryBuffer.length;
|
|
251
|
+
if (remaining > 0) {
|
|
252
|
+
this.hooks.logger.warn(`[Matador] 🟡 ${remaining} buffered message(s) could not be flushed; will retry later.`);
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
this.hooks.logger.info('[Matador] 🟢 All buffered messages flushed successfully.');
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Re-buffers messages that were never attempted because a flush pass
|
|
260
|
+
* stopped out early. Respects maxRetryBufferSize: concurrent sends can have
|
|
261
|
+
* refilled the buffer while this flush was running, so anything beyond
|
|
262
|
+
* remaining capacity is dropped and reported instead of silently growing
|
|
263
|
+
* the buffer past its cap.
|
|
264
|
+
*/
|
|
265
|
+
async rebufferUntried(untried, consecutiveFailures) {
|
|
266
|
+
const capacity = Math.max(0, this.maxRetryBufferSize - this.retryBuffer.length);
|
|
267
|
+
const toRebuffer = untried.slice(0, capacity);
|
|
268
|
+
const dropped = untried.slice(capacity);
|
|
269
|
+
if (toRebuffer.length > 0) {
|
|
270
|
+
this.retryBuffer.unshift(...toRebuffer);
|
|
271
|
+
}
|
|
272
|
+
const droppedSuffix = dropped.length > 0 ? `, ${dropped.length} dropped (buffer full).` : '.';
|
|
273
|
+
this.hooks.logger.warn(`[Matador] 🟡 Stopping this flush pass after ${consecutiveFailures} consecutive failures; ${toRebuffer.length} untried message(s) re-buffered for the next attempt${droppedSuffix}`);
|
|
274
|
+
for (const item of dropped) {
|
|
275
|
+
this.hooks.logger.error(`[Matador] 🔴 Retry buffer full (${this.maxRetryBufferSize}). Message for '${item.subscriberName}' dropped and will not be retried.`);
|
|
276
|
+
const err = new TransportSendError(item.queue, new Error('Retry buffer full'));
|
|
277
|
+
await this.hooks.onEnqueueError({
|
|
278
|
+
envelope: item.envelope,
|
|
279
|
+
error: err,
|
|
280
|
+
transport: this.transport.name,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Handles a failed flush attempt for a single buffered item: drops it once
|
|
286
|
+
* maxRetryAttempts is exceeded, otherwise re-buffers it (unless the buffer
|
|
287
|
+
* is full, in which case it's dropped too).
|
|
288
|
+
*/
|
|
289
|
+
async handleFlushFailure(item, error) {
|
|
290
|
+
item.attempts++;
|
|
291
|
+
const cause = error instanceof Error ? error : new Error(String(error));
|
|
292
|
+
const exceededAttempts = this.maxRetryAttempts !== undefined &&
|
|
293
|
+
item.attempts >= this.maxRetryAttempts;
|
|
294
|
+
if (!exceededAttempts &&
|
|
295
|
+
this.retryBuffer.length < this.maxRetryBufferSize) {
|
|
296
|
+
// Re-buffer on failure; it will be retried on the next reconnect or flush interval.
|
|
297
|
+
this.retryBuffer.push(item);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (exceededAttempts) {
|
|
301
|
+
this.hooks.logger.error(`[Matador] 🔴 Message for '${item.subscriberName}' exceeded max retry attempts (${this.maxRetryAttempts}) and will not be retried further.`);
|
|
302
|
+
}
|
|
303
|
+
const err = new TransportSendError(item.queue, cause);
|
|
304
|
+
await this.hooks.onEnqueueError({
|
|
305
|
+
envelope: item.envelope,
|
|
306
|
+
error: err,
|
|
307
|
+
transport: this.transport.name,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
108
310
|
async isSubscriberEnabled(subscriber) {
|
|
109
311
|
if (!subscriber.enabled) {
|
|
110
312
|
return true;
|