queen-mq 1.3.0 → 2.0.2
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/README.md +99 -21
- package/client-v2/Queen.js +47 -15
- package/client-v2/README.md +34 -13
- package/client-v2/admin/Admin.js +30 -55
- package/client-v2/builders/QueueBuilder.js +106 -34
- package/client-v2/builders/TimerBuilder.js +2 -2
- package/client-v2/builders/TransactionBuilder.js +35 -9
- package/client-v2/consumer/ConsumerManager.js +79 -53
- package/client-v2/ephemeral/Ephemeral.js +47 -17
- package/client-v2/kv/Kv.js +4 -4
- package/client-v2/kv/expiry.js +1 -1
- package/client-v2/streams/Stream.js +8 -3
- package/client-v2/streams/helpers/rateLimiter.js +4 -4
- package/client-v2/streams/operators/GateOperator.js +9 -2
- package/client-v2/streams/operators/ReduceOperator.js +2 -2
- package/client-v2/streams/operators/WindowSessionOperator.js +3 -3
- package/client-v2/streams/runtime/Runner.js +111 -54
- package/client-v2/streams/runtime/cycle.js +4 -4
- package/client-v2/streams/runtime/register.js +1 -1
- package/client-v2/utils/conflation.js +0 -6
- package/client-v2/utils/consumerGroup.js +54 -0
- package/client-v2/utils/defaults.js +3 -2
- package/package.json +5 -8
- package/test-v2/_kvtimers.js +12 -13
- package/test-v2/ackwindow.js +12 -192
- package/test-v2/admin-unit/removedRoutes.test.js +63 -0
- package/test-v2/bootstrap.js +2 -2
- package/test-v2/conflation-unit/conflationWire.test.js +0 -12
- package/test-v2/consume.js +41 -1
- package/test-v2/consumer-unit/handlerError.test.js +161 -0
- package/test-v2/consumer-unit/nackScope.test.js +88 -0
- package/test-v2/docs.js +5 -4
- package/test-v2/ephemeral-unit/ephemeralWire.test.js +24 -1
- package/test-v2/http-unit/pushStatus.test.js +142 -0
- package/test-v2/http-unit/renew.test.js +96 -0
- package/test-v2/kv-unit/timerWire.test.js +1 -1
- package/test-v2/kv-unit/txnWire.test.js +101 -2
- package/test-v2/kv.js +6 -5
- package/test-v2/pop-unit/popDefaults.test.js +236 -0
- package/test-v2/pop.js +96 -1
- package/test-v2/run.js +50 -114
- package/test-v2/runner-unit/fatalExit.test.js +64 -0
- package/test-v2/semantics.js +20 -34
- package/test-v2/stream/_helpers.js +10 -19
- package/test-v2/stream/cron.js +1 -1
- package/test-v2/stream/gate.js +54 -0
- package/test-v2/stream/index.js +2 -0
- package/test-v2/stream/tumbling.js +3 -3
- package/test-v2/streams-unit/ack.test.js +61 -0
- package/test-v2/streams-unit/cycle.test.js +1 -1
- package/test-v2/streams-unit/e2e.test.js +6 -10
- package/test-v2/streams-unit/gate.test.js +189 -0
- package/test-v2/timers.js +6 -5
- package/test-v2/transaction.js +169 -0
- package/test-v2/watermark.js +38 -176
- package/test-v2/maintenance.js +0 -277
package/test-v2/watermark.js
CHANGED
|
@@ -1,90 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Seek / delete re-consumption tests
|
|
2
|
+
* Seek / delete re-consumption tests.
|
|
3
3
|
*
|
|
4
|
-
* These
|
|
4
|
+
* These pin the BEHAVIOUR a user cares about, and nothing else:
|
|
5
5
|
* 1. Seeking a consumer group backwards re-exposes the old messages, and the
|
|
6
6
|
* group re-consumes them from the start within a short bound.
|
|
7
7
|
* 2. Deleting a consumer group (all queues, or for one queue) lets a group of
|
|
8
8
|
* the same name consume every message again from the start.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* it to exercise the 2-minute buffer:
|
|
16
|
-
* pl.updated_at >= (v_watermark - interval '2 minutes')
|
|
17
|
-
*
|
|
18
|
-
* With the wildcard hot-list ON (QUEEN_HOTLIST=1, the default) empty polls are
|
|
19
|
-
* answered from the in-memory candidate ring and that bookkeeping row is NEVER
|
|
20
|
-
* written — by design, not a bug: the ring, not the watermark, is the discovery
|
|
21
|
-
* source. So asserting the row exists fails on the hot-list path even though
|
|
22
|
-
* re-consumption works perfectly.
|
|
23
|
-
*
|
|
24
|
-
* The behaviour is identical on both engines, so these tests now assert the
|
|
25
|
-
* behaviour directly and only exercise the watermark-age scenario on the LEGACY
|
|
26
|
-
* path — detected at runtime by whether the empty poll actually wrote the row
|
|
27
|
-
* (no env flag needed). On the hot-list path the broker's seek/delete handlers
|
|
28
|
-
* re-seed / invalidate the ring so the reconsume is immediate; the idle-bounded
|
|
29
|
-
* consume below (idleMillis 3000) is what proves "within a short bound".
|
|
10
|
+
* Each test first drains the queue and polls it empty, so the group has seen
|
|
11
|
+
* "nothing to deliver" before the seek or the delete. "Within a short bound"
|
|
12
|
+
* is the idle-bounded consume (idleMillis 3000) that follows: a seek or a
|
|
13
|
+
* delete that left stale per-group state behind makes that consume go idle
|
|
14
|
+
* before every message came back, and the count comes up short.
|
|
30
15
|
*
|
|
31
16
|
* Run: node run.js seekBackwardsAllowsReconsume
|
|
32
17
|
* node run.js deleteConsumerGroupAllowsReconsume
|
|
33
18
|
* node run.js deleteConsumerGroupForQueueAllowsReconsume
|
|
34
19
|
*/
|
|
35
20
|
|
|
36
|
-
import { dbPool } from './run.js'
|
|
37
|
-
|
|
38
21
|
const QUEUE_NAME_SEEK = 'test-queue-v2-watermark-seek'
|
|
39
22
|
const QUEUE_NAME_DELETE = 'test-queue-v2-watermark-delete'
|
|
40
23
|
|
|
41
|
-
/**
|
|
42
|
-
* Helper: Move the watermark FORWARD in time to simulate that partitions
|
|
43
|
-
* are "old" relative to the watermark.
|
|
44
|
-
*
|
|
45
|
-
* The filter is: pl.updated_at >= (v_watermark - interval '2 minutes')
|
|
46
|
-
*
|
|
47
|
-
* If watermark = NOW + 10 minutes, then:
|
|
48
|
-
* filter = pl.updated_at >= (NOW + 10 - 2) = pl.updated_at >= NOW + 8
|
|
49
|
-
*
|
|
50
|
-
* Partitions updated at NOW will fail this filter (NOW < NOW + 8)
|
|
51
|
-
*/
|
|
52
|
-
async function advanceWatermark(queueName, consumerGroup, minutesForward = 10) {
|
|
53
|
-
const result = await dbPool.query(`
|
|
54
|
-
UPDATE queen.consumer_watermarks w
|
|
55
|
-
SET last_empty_scan_at = NOW() + interval '${minutesForward} minutes',
|
|
56
|
-
updated_at = NOW() + interval '${minutesForward} minutes'
|
|
57
|
-
FROM queen.queues q
|
|
58
|
-
WHERE w.queue_id = q.id AND q.name = $1 AND w.consumer_group = $2
|
|
59
|
-
RETURNING w.*
|
|
60
|
-
`, [queueName, consumerGroup])
|
|
61
|
-
return result.rows[0]
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Helper: Check if watermark exists for a queue/consumer_group
|
|
66
|
-
*/
|
|
67
|
-
async function getWatermark(queueName, consumerGroup) {
|
|
68
|
-
const result = await dbPool.query(`
|
|
69
|
-
SELECT w.* FROM queen.consumer_watermarks w
|
|
70
|
-
JOIN queen.queues q ON q.id = w.queue_id
|
|
71
|
-
WHERE q.name = $1 AND w.consumer_group = $2
|
|
72
|
-
`, [queueName, consumerGroup])
|
|
73
|
-
return result.rows[0]
|
|
74
|
-
}
|
|
75
|
-
|
|
76
24
|
/**
|
|
77
25
|
* Test: Seeking backwards should allow re-consumption of messages
|
|
78
26
|
*
|
|
79
27
|
* Steps:
|
|
80
28
|
* 1. Push messages to queue
|
|
81
|
-
* 2. Consume all messages with a consumer group
|
|
82
|
-
* 3.
|
|
83
|
-
* 4.
|
|
84
|
-
* 5. Try to consume again - should get all messages
|
|
29
|
+
* 2. Consume all messages with a consumer group, then poll it empty
|
|
30
|
+
* 3. Seek the consumer group backwards to before the messages
|
|
31
|
+
* 4. Try to consume again - should get all messages
|
|
85
32
|
*
|
|
86
|
-
* Expected: All messages are re-consumed after seek
|
|
87
|
-
* Actual (bug): Watermark blocks re-consumption, 0 messages returned
|
|
33
|
+
* Expected: All messages are re-consumed after seek, within the idle window
|
|
88
34
|
*/
|
|
89
35
|
export async function seekBackwardsAllowsReconsume(client) {
|
|
90
36
|
const consumerGroup = 'watermark-seek-test-cg'
|
|
@@ -92,11 +38,6 @@ export async function seekBackwardsAllowsReconsume(client) {
|
|
|
92
38
|
// Cleanup
|
|
93
39
|
try {
|
|
94
40
|
await client.deleteConsumerGroup(consumerGroup, true)
|
|
95
|
-
// Also clean up any orphaned watermark from previous runs
|
|
96
|
-
await dbPool.query(
|
|
97
|
-
'DELETE FROM queen.consumer_watermarks WHERE consumer_group = $1',
|
|
98
|
-
[consumerGroup]
|
|
99
|
-
)
|
|
100
41
|
} catch (e) {
|
|
101
42
|
// Ignore
|
|
102
43
|
}
|
|
@@ -147,7 +88,7 @@ export async function seekBackwardsAllowsReconsume(client) {
|
|
|
147
88
|
}
|
|
148
89
|
}
|
|
149
90
|
|
|
150
|
-
// 5. Verify queue is empty for this consumer (
|
|
91
|
+
// 5. Verify queue is empty for this consumer (the group has now polled empty)
|
|
151
92
|
const emptyCheck = await client
|
|
152
93
|
.queue(QUEUE_NAME_SEEK)
|
|
153
94
|
.group(consumerGroup)
|
|
@@ -162,24 +103,9 @@ export async function seekBackwardsAllowsReconsume(client) {
|
|
|
162
103
|
}
|
|
163
104
|
}
|
|
164
105
|
|
|
165
|
-
console.log(` Verified queue is empty
|
|
166
|
-
|
|
167
|
-
// 6. Detect which discovery path we are on. On the LEGACY path the empty poll
|
|
168
|
-
// wrote a consumer_watermarks row; on the hot-list path it did not (empty
|
|
169
|
-
// polls are served from the ring — by design). We do NOT assert the row: it
|
|
170
|
-
// is a path-specific bookkeeping detail, not the behaviour under test.
|
|
171
|
-
const watermarkBefore = await getWatermark(QUEUE_NAME_SEEK, consumerGroup)
|
|
172
|
-
if (watermarkBefore) {
|
|
173
|
-
// Legacy path: age the watermark 10 minutes into the future so the old
|
|
174
|
-
// partitions fall outside the 2-minute buffer — the original repro that
|
|
175
|
-
// proves seek must reset the watermark.
|
|
176
|
-
const advancedWatermark = await advanceWatermark(QUEUE_NAME_SEEK, consumerGroup, 10)
|
|
177
|
-
console.log(` [legacy] Watermark row exists; advanced to ${advancedWatermark.last_empty_scan_at} (10m future)`)
|
|
178
|
-
} else {
|
|
179
|
-
console.log(` [hotlist] No watermark row (empty polls served from the ring) — skipping the watermark-age step`)
|
|
180
|
-
}
|
|
106
|
+
console.log(` Verified queue is empty`)
|
|
181
107
|
|
|
182
|
-
//
|
|
108
|
+
// 6. Seek backwards to BEFORE the messages were pushed
|
|
183
109
|
console.log(` Seeking to timestamp: ${timestampBeforeMessages}`)
|
|
184
110
|
|
|
185
111
|
const seekResult = await client.admin.seekConsumerGroup(
|
|
@@ -190,18 +116,10 @@ export async function seekBackwardsAllowsReconsume(client) {
|
|
|
190
116
|
|
|
191
117
|
console.log(` Seek result: ${JSON.stringify(seekResult)}`)
|
|
192
118
|
|
|
193
|
-
// 9. Check if watermark was reset (it should be, but currently isn't - that's the bug)
|
|
194
|
-
const watermarkAfterSeek = await getWatermark(QUEUE_NAME_SEEK, consumerGroup)
|
|
195
|
-
if (watermarkAfterSeek) {
|
|
196
|
-
console.log(` WARNING: Watermark still exists after seek: ${watermarkAfterSeek.last_empty_scan_at}`)
|
|
197
|
-
} else {
|
|
198
|
-
console.log(` Watermark was properly deleted after seek`)
|
|
199
|
-
}
|
|
200
|
-
|
|
201
119
|
// Small delay after seek
|
|
202
120
|
await new Promise(resolve => setTimeout(resolve, 500))
|
|
203
121
|
|
|
204
|
-
//
|
|
122
|
+
// 7. Try to consume again - should get all messages back
|
|
205
123
|
let secondConsumeCount = 0
|
|
206
124
|
await client
|
|
207
125
|
.queue(QUEUE_NAME_SEEK)
|
|
@@ -216,17 +134,13 @@ export async function seekBackwardsAllowsReconsume(client) {
|
|
|
216
134
|
|
|
217
135
|
console.log(` Second consume (after seek): ${secondConsumeCount} messages`)
|
|
218
136
|
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
// (still at ${watermarkAfterSeek?.last_empty_scan_at}); on the hot-list path
|
|
223
|
-
// the ring was not re-seeded, so reconsume would only resume at the ≤30s
|
|
224
|
-
// periodic floor instead of immediately.
|
|
137
|
+
// 8. Verify we got all messages back within the idle-bounded consume above.
|
|
138
|
+
// A shortfall means the backward seek did not re-expose the messages
|
|
139
|
+
// promptly.
|
|
225
140
|
if (secondConsumeCount !== messageCount) {
|
|
226
141
|
return {
|
|
227
142
|
success: false,
|
|
228
|
-
message: `After seek backwards, should re-consume ${messageCount} messages within the idle window, but got ${secondConsumeCount}
|
|
229
|
-
`(legacy watermark after seek: ${watermarkAfterSeek?.last_empty_scan_at})`
|
|
143
|
+
message: `After seek backwards, should re-consume ${messageCount} messages within the idle window, but got ${secondConsumeCount}`
|
|
230
144
|
}
|
|
231
145
|
}
|
|
232
146
|
|
|
@@ -241,14 +155,12 @@ export async function seekBackwardsAllowsReconsume(client) {
|
|
|
241
155
|
*
|
|
242
156
|
* Steps:
|
|
243
157
|
* 1. Push messages to queue
|
|
244
|
-
* 2. Consume all messages with a consumer group
|
|
245
|
-
* 3.
|
|
246
|
-
* 4.
|
|
247
|
-
* 5.
|
|
248
|
-
* 6. Try to consume - should get all messages
|
|
158
|
+
* 2. Consume all messages with a consumer group, then poll it empty
|
|
159
|
+
* 3. Delete the consumer group
|
|
160
|
+
* 4. Create a new consumer group with the SAME name
|
|
161
|
+
* 5. Try to consume - should get all messages
|
|
249
162
|
*
|
|
250
163
|
* Expected: All messages are consumed by the "new" consumer group
|
|
251
|
-
* Actual (bug): Orphaned watermark blocks consumption, 0 messages returned
|
|
252
164
|
*/
|
|
253
165
|
export async function deleteConsumerGroupAllowsReconsume(client) {
|
|
254
166
|
const consumerGroup = 'watermark-delete-test-cg'
|
|
@@ -256,11 +168,6 @@ export async function deleteConsumerGroupAllowsReconsume(client) {
|
|
|
256
168
|
// Cleanup
|
|
257
169
|
try {
|
|
258
170
|
await client.deleteConsumerGroup(consumerGroup, true)
|
|
259
|
-
// Also clean up any orphaned watermark from previous runs
|
|
260
|
-
await dbPool.query(
|
|
261
|
-
'DELETE FROM queen.consumer_watermarks WHERE consumer_group = $1',
|
|
262
|
-
[consumerGroup]
|
|
263
|
-
)
|
|
264
171
|
} catch (e) {
|
|
265
172
|
// Ignore
|
|
266
173
|
}
|
|
@@ -305,7 +212,7 @@ export async function deleteConsumerGroupAllowsReconsume(client) {
|
|
|
305
212
|
}
|
|
306
213
|
}
|
|
307
214
|
|
|
308
|
-
// 4. Verify queue is empty for this consumer (
|
|
215
|
+
// 4. Verify queue is empty for this consumer (the group has now polled empty)
|
|
309
216
|
const emptyCheck = await client
|
|
310
217
|
.queue(QUEUE_NAME_DELETE)
|
|
311
218
|
.group(consumerGroup)
|
|
@@ -320,40 +227,20 @@ export async function deleteConsumerGroupAllowsReconsume(client) {
|
|
|
320
227
|
}
|
|
321
228
|
}
|
|
322
229
|
|
|
323
|
-
console.log(` Verified queue is empty
|
|
324
|
-
|
|
325
|
-
// 5. Detect the discovery path (see seekBackwardsAllowsReconsume). The
|
|
326
|
-
// watermark row is a legacy-path artefact, not the behaviour under test, so
|
|
327
|
-
// we do not assert it — we only exercise the watermark-age scenario when the
|
|
328
|
-
// row was actually written (legacy path).
|
|
329
|
-
const watermarkBefore = await getWatermark(QUEUE_NAME_DELETE, consumerGroup)
|
|
330
|
-
if (watermarkBefore) {
|
|
331
|
-
const advancedWatermark = await advanceWatermark(QUEUE_NAME_DELETE, consumerGroup, 10)
|
|
332
|
-
console.log(` [legacy] Watermark row exists; advanced to ${advancedWatermark.last_empty_scan_at} (10m future)`)
|
|
333
|
-
} else {
|
|
334
|
-
console.log(` [hotlist] No watermark row (empty polls served from the ring) — skipping the watermark-age step`)
|
|
335
|
-
}
|
|
230
|
+
console.log(` Verified queue is empty`)
|
|
336
231
|
|
|
337
|
-
//
|
|
232
|
+
// 5. Delete the consumer group
|
|
338
233
|
console.log(` Deleting consumer group: ${consumerGroup}`)
|
|
339
234
|
|
|
340
235
|
const deleteResult = await client.deleteConsumerGroup(consumerGroup, true)
|
|
341
236
|
console.log(` Delete result: ${JSON.stringify(deleteResult)}`)
|
|
342
237
|
|
|
343
|
-
// 8. Check if watermark was deleted (it should be, but currently isn't - that's the bug)
|
|
344
|
-
const watermarkAfterDelete = await getWatermark(QUEUE_NAME_DELETE, consumerGroup)
|
|
345
|
-
if (watermarkAfterDelete) {
|
|
346
|
-
console.log(` WARNING: Orphaned watermark still exists: ${watermarkAfterDelete.last_empty_scan_at}`)
|
|
347
|
-
} else {
|
|
348
|
-
console.log(` Watermark was properly deleted`)
|
|
349
|
-
}
|
|
350
|
-
|
|
351
238
|
// Small delay after delete
|
|
352
239
|
await new Promise(resolve => setTimeout(resolve, 500))
|
|
353
240
|
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
//
|
|
241
|
+
// 6. Create a "new" consumer group with the same name and try to consume.
|
|
242
|
+
// The delete removed the group's cursors and metadata, so it must start
|
|
243
|
+
// fresh; any per-group state that survived would skip the messages.
|
|
357
244
|
let secondConsumeCount = 0
|
|
358
245
|
await client
|
|
359
246
|
.queue(QUEUE_NAME_DELETE)
|
|
@@ -369,16 +256,14 @@ export async function deleteConsumerGroupAllowsReconsume(client) {
|
|
|
369
256
|
|
|
370
257
|
console.log(` Second consume (after delete + recreate): ${secondConsumeCount} messages`)
|
|
371
258
|
|
|
372
|
-
//
|
|
373
|
-
//
|
|
374
|
-
//
|
|
375
|
-
// hot-list path, a surviving group ring / stale seeded-groups cache that
|
|
376
|
-
// suppressed the from-the-start reconsume.
|
|
259
|
+
// 7. Verify we got all messages within the idle-bounded consume above. A
|
|
260
|
+
// shortfall means the delete left stale per-group state behind that
|
|
261
|
+
// suppressed the from-the-start reconsume.
|
|
377
262
|
if (secondConsumeCount !== messageCount) {
|
|
378
263
|
return {
|
|
379
264
|
success: false,
|
|
380
265
|
message: `After deleting CG, new CG with same name should consume ${messageCount} messages within the idle window, ` +
|
|
381
|
-
`but got ${secondConsumeCount}
|
|
266
|
+
`but got ${secondConsumeCount}`
|
|
382
267
|
}
|
|
383
268
|
}
|
|
384
269
|
|
|
@@ -400,11 +285,6 @@ export async function deleteConsumerGroupForQueueAllowsReconsume(client) {
|
|
|
400
285
|
// Cleanup
|
|
401
286
|
try {
|
|
402
287
|
await client.admin.deleteConsumerGroupForQueue(consumerGroup, queueName, true)
|
|
403
|
-
// Also clean up any orphaned watermark from previous runs
|
|
404
|
-
await dbPool.query(
|
|
405
|
-
'DELETE FROM queen.consumer_watermarks w USING queen.queues q WHERE w.queue_id = q.id AND q.name = $1 AND w.consumer_group = $2',
|
|
406
|
-
[queueName, consumerGroup]
|
|
407
|
-
)
|
|
408
288
|
} catch (e) {
|
|
409
289
|
// Ignore
|
|
410
290
|
}
|
|
@@ -449,7 +329,7 @@ export async function deleteConsumerGroupForQueueAllowsReconsume(client) {
|
|
|
449
329
|
}
|
|
450
330
|
}
|
|
451
331
|
|
|
452
|
-
// 4. Verify queue is empty (
|
|
332
|
+
// 4. Verify queue is empty (the group has now polled empty)
|
|
453
333
|
const emptyCheck = await client
|
|
454
334
|
.queue(queueName)
|
|
455
335
|
.group(consumerGroup)
|
|
@@ -464,35 +344,17 @@ export async function deleteConsumerGroupForQueueAllowsReconsume(client) {
|
|
|
464
344
|
}
|
|
465
345
|
}
|
|
466
346
|
|
|
467
|
-
console.log(` Verified queue is empty
|
|
468
|
-
|
|
469
|
-
// 5. Detect the discovery path (see seekBackwardsAllowsReconsume). The
|
|
470
|
-
// watermark row is a legacy-path artefact, not the behaviour under test.
|
|
471
|
-
const watermarkBefore = await getWatermark(queueName, consumerGroup)
|
|
472
|
-
if (watermarkBefore) {
|
|
473
|
-
const advancedWatermark = await advanceWatermark(queueName, consumerGroup, 10)
|
|
474
|
-
console.log(` [legacy] Watermark row exists; advanced to ${advancedWatermark.last_empty_scan_at} (10m future)`)
|
|
475
|
-
} else {
|
|
476
|
-
console.log(` [hotlist] No watermark row (empty polls served from the ring) — skipping the watermark-age step`)
|
|
477
|
-
}
|
|
347
|
+
console.log(` Verified queue is empty`)
|
|
478
348
|
|
|
479
|
-
//
|
|
349
|
+
// 5. Delete consumer group for this specific queue
|
|
480
350
|
console.log(` Deleting consumer group for queue: ${consumerGroup} / ${queueName}`)
|
|
481
351
|
|
|
482
352
|
const deleteResult = await client.admin.deleteConsumerGroupForQueue(consumerGroup, queueName, true)
|
|
483
353
|
console.log(` Delete result: ${JSON.stringify(deleteResult)}`)
|
|
484
354
|
|
|
485
|
-
// 8. Check if watermark was deleted
|
|
486
|
-
const watermarkAfterDelete = await getWatermark(queueName, consumerGroup)
|
|
487
|
-
if (watermarkAfterDelete) {
|
|
488
|
-
console.log(` WARNING: Orphaned watermark still exists: ${watermarkAfterDelete.last_empty_scan_at}`)
|
|
489
|
-
} else {
|
|
490
|
-
console.log(` Watermark was properly deleted`)
|
|
491
|
-
}
|
|
492
|
-
|
|
493
355
|
await new Promise(resolve => setTimeout(resolve, 500))
|
|
494
356
|
|
|
495
|
-
//
|
|
357
|
+
// 6. Try to consume again
|
|
496
358
|
let secondConsumeCount = 0
|
|
497
359
|
await client
|
|
498
360
|
.queue(queueName)
|
|
@@ -512,7 +374,7 @@ export async function deleteConsumerGroupForQueueAllowsReconsume(client) {
|
|
|
512
374
|
return {
|
|
513
375
|
success: false,
|
|
514
376
|
message: `After deleting CG for queue, should consume ${messageCount} messages within the idle window, ` +
|
|
515
|
-
`but got ${secondConsumeCount}
|
|
377
|
+
`but got ${secondConsumeCount}`
|
|
516
378
|
}
|
|
517
379
|
}
|
|
518
380
|
|
package/test-v2/maintenance.js
DELETED
|
@@ -1,277 +0,0 @@
|
|
|
1
|
-
import { Queen } from '../client-v2/index.js';
|
|
2
|
-
import axios from 'axios';
|
|
3
|
-
|
|
4
|
-
export async function test_maintenance_mode(client = null) {
|
|
5
|
-
console.log('🧪 Testing Maintenance Mode with File Buffer\n');
|
|
6
|
-
|
|
7
|
-
const queen = client
|
|
8
|
-
|
|
9
|
-
// Default Queen port; override with QUEEN_SERVER_URL (same as run.js).
|
|
10
|
-
const API_URL = process.env.QUEEN_SERVER_URL || 'http://localhost:6632';
|
|
11
|
-
const QUEUE_NAME = 'test-maintenance-queue';
|
|
12
|
-
const MESSAGES_PER_SECOND = 10;
|
|
13
|
-
const MAINTENANCE_DURATION_SECONDS = 10;
|
|
14
|
-
|
|
15
|
-
let producedCount = 0;
|
|
16
|
-
let receivedCount = 0;
|
|
17
|
-
let consumerActive = false;
|
|
18
|
-
|
|
19
|
-
try {
|
|
20
|
-
// 0. Ensure maintenance mode is OFF before starting
|
|
21
|
-
console.log('🧹 Step 0: Ensuring clean state...');
|
|
22
|
-
await axios.post(`${API_URL}/api/v1/system/maintenance`, { enabled: false });
|
|
23
|
-
|
|
24
|
-
let initialStatus = await axios.get(`${API_URL}/api/v1/system/maintenance`);
|
|
25
|
-
console.log(` Initial status:`, initialStatus.data);
|
|
26
|
-
|
|
27
|
-
if (initialStatus.data.bufferedMessages > 0) {
|
|
28
|
-
console.log(` ⚠️ Found ${initialStatus.data.bufferedMessages} buffered messages from previous runs`);
|
|
29
|
-
console.log(' Waiting for buffer to drain (checking every 2 seconds)...');
|
|
30
|
-
|
|
31
|
-
// Wait for buffer to drain completely
|
|
32
|
-
let waitTime = 0;
|
|
33
|
-
const maxWait = 60; // Max 60 seconds
|
|
34
|
-
while (waitTime < maxWait) {
|
|
35
|
-
await sleep(2000);
|
|
36
|
-
waitTime += 2;
|
|
37
|
-
const status = await axios.get(`${API_URL}/api/v1/system/maintenance`);
|
|
38
|
-
console.log(` ${waitTime}s - Buffered: ${status.data.bufferedMessages}`);
|
|
39
|
-
|
|
40
|
-
if (status.data.bufferedMessages === 0) {
|
|
41
|
-
console.log(' ✅ Buffer drained completely');
|
|
42
|
-
break;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (waitTime >= maxWait) {
|
|
47
|
-
console.log(` ⚠️ Timeout waiting for buffer to drain. Continuing anyway...`);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
console.log('');
|
|
51
|
-
|
|
52
|
-
// 1. Configure queue
|
|
53
|
-
console.log(`📋 Step 1: Configuring queue "${QUEUE_NAME}"...`);
|
|
54
|
-
await queen.queue(QUEUE_NAME).config({
|
|
55
|
-
leaseTime: 60,
|
|
56
|
-
}).create();
|
|
57
|
-
console.log('✅ Queue configured\n');
|
|
58
|
-
|
|
59
|
-
// 2. Start consumer
|
|
60
|
-
console.log('📥 Step 2: Starting consumer...');
|
|
61
|
-
consumerActive = true;
|
|
62
|
-
|
|
63
|
-
const consumePromise = (async () => {
|
|
64
|
-
while (consumerActive) {
|
|
65
|
-
try {
|
|
66
|
-
// wait(true) long-polls on the server. This is important: pushes
|
|
67
|
-
// commit before partition_lookup is refreshed (PUSHPOPLOOKUPSOL
|
|
68
|
-
// is async), so a wait(false) pop can race past freshly-pushed
|
|
69
|
-
// messages and return empty. Long-poll re-runs the candidate
|
|
70
|
-
// scan internally and picks up messages once the lookup row
|
|
71
|
-
// commits — typically within milliseconds.
|
|
72
|
-
// Use a short timeout (1s) so the consumer notices
|
|
73
|
-
// consumerActive=false promptly when the test winds down.
|
|
74
|
-
const messages = await queen.queue(QUEUE_NAME)
|
|
75
|
-
.batch(10)
|
|
76
|
-
.wait(true)
|
|
77
|
-
.timeoutMillis(1000)
|
|
78
|
-
.limit(1)
|
|
79
|
-
.each()
|
|
80
|
-
.pop();
|
|
81
|
-
|
|
82
|
-
if (messages && messages.length > 0) {
|
|
83
|
-
receivedCount += messages.length;
|
|
84
|
-
console.log(` 📨 Received ${messages.length} messages (total: ${receivedCount})`);
|
|
85
|
-
|
|
86
|
-
// Acknowledge messages
|
|
87
|
-
await queen.ack(messages);
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
} catch (err) {
|
|
91
|
-
if (consumerActive) {
|
|
92
|
-
console.error(' ❌ Consumer error:', err.message);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
await sleep(10);
|
|
96
|
-
}
|
|
97
|
-
})();
|
|
98
|
-
|
|
99
|
-
console.log('✅ Consumer started\n');
|
|
100
|
-
|
|
101
|
-
// 3. Start producer (10 messages/second)
|
|
102
|
-
console.log(`📤 Step 3: Starting producer (${MESSAGES_PER_SECOND} msgs/sec)...`);
|
|
103
|
-
let producerRunning = true;
|
|
104
|
-
|
|
105
|
-
const producerLoop = async () => {
|
|
106
|
-
while (producerRunning) {
|
|
107
|
-
try {
|
|
108
|
-
// Push batch of 10 messages
|
|
109
|
-
const batch = [];
|
|
110
|
-
for (let i = 0; i < MESSAGES_PER_SECOND; i++) {
|
|
111
|
-
batch.push({
|
|
112
|
-
data: {
|
|
113
|
-
messageNumber: producedCount + i,
|
|
114
|
-
timestamp: Date.now()
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Push and wait for confirmation
|
|
120
|
-
await queen.queue(QUEUE_NAME).push(batch);
|
|
121
|
-
producedCount += batch.length; // Only increment after successful push
|
|
122
|
-
|
|
123
|
-
// Wait 1 second before next batch
|
|
124
|
-
await sleep(1000);
|
|
125
|
-
} catch (err) {
|
|
126
|
-
console.error(' ❌ Producer error:', err.message);
|
|
127
|
-
await sleep(1000); // Wait before retry
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
const producerPromise = producerLoop();
|
|
133
|
-
|
|
134
|
-
console.log('✅ Producer started\n');
|
|
135
|
-
|
|
136
|
-
// Wait 3 seconds for messages to flow normally
|
|
137
|
-
console.log('⏱️ Step 4: Waiting 3 seconds for normal message flow...');
|
|
138
|
-
await sleep(3000);
|
|
139
|
-
console.log(` Produced: ${producedCount}, Received: ${receivedCount}\n`);
|
|
140
|
-
|
|
141
|
-
const receivedBeforeMaintenance = receivedCount;
|
|
142
|
-
|
|
143
|
-
// 4. Enable maintenance mode
|
|
144
|
-
console.log('🔧 Step 5: Enabling MAINTENANCE MODE...');
|
|
145
|
-
const maintenanceResponse = await axios.post(`${API_URL}/api/v1/system/maintenance`, {
|
|
146
|
-
enabled: true
|
|
147
|
-
});
|
|
148
|
-
console.log(` Response:`, maintenanceResponse.data);
|
|
149
|
-
console.log('✅ Maintenance mode enabled\n');
|
|
150
|
-
|
|
151
|
-
// Wait 2 seconds and check that consumer stops receiving
|
|
152
|
-
console.log('⏱️ Step 6: Waiting 5 seconds - consumer should stop receiving...');
|
|
153
|
-
await sleep(5000);
|
|
154
|
-
const receivedDuringWait = receivedCount - receivedBeforeMaintenance;
|
|
155
|
-
console.log(` Messages received during maintenance: ${receivedDuringWait}`);
|
|
156
|
-
console.log(` Total: Produced=${producedCount}, Received=${receivedCount}\n`);
|
|
157
|
-
|
|
158
|
-
// 5. Keep producing during maintenance
|
|
159
|
-
console.log(`⏱️ Step 7: Producing during maintenance for ${MAINTENANCE_DURATION_SECONDS} seconds...`);
|
|
160
|
-
console.log(' (Messages should go to file buffer)\n');
|
|
161
|
-
await sleep(MAINTENANCE_DURATION_SECONDS * 1000);
|
|
162
|
-
|
|
163
|
-
const producedDuringMaintenance = producedCount;
|
|
164
|
-
const receivedAfterMaintenance = receivedCount;
|
|
165
|
-
const messagesBuffered = producedDuringMaintenance - receivedAfterMaintenance;
|
|
166
|
-
|
|
167
|
-
console.log(` Maintenance period complete:`);
|
|
168
|
-
console.log(` - Total produced: ${producedCount}`);
|
|
169
|
-
console.log(` - Total received: ${receivedCount}`);
|
|
170
|
-
console.log(` - Buffered (should be ~${MESSAGES_PER_SECOND * MAINTENANCE_DURATION_SECONDS}): ${messagesBuffered}\n`);
|
|
171
|
-
|
|
172
|
-
// 6. Check maintenance status
|
|
173
|
-
console.log('📊 Step 8: Checking maintenance status...');
|
|
174
|
-
const statusResponse = await axios.get(`${API_URL}/api/v1/system/maintenance`);
|
|
175
|
-
console.log(` Status:`, statusResponse.data);
|
|
176
|
-
console.log('');
|
|
177
|
-
|
|
178
|
-
// 7. Disable maintenance mode
|
|
179
|
-
console.log('✅ Step 9: Disabling MAINTENANCE MODE...');
|
|
180
|
-
const disableResponse = await axios.post(`${API_URL}/api/v1/system/maintenance`, {
|
|
181
|
-
enabled: false
|
|
182
|
-
});
|
|
183
|
-
console.log(` Response:`, disableResponse.data);
|
|
184
|
-
console.log(' (File buffer should start draining to database)\n');
|
|
185
|
-
|
|
186
|
-
// 8. Wait for messages to resume and stop producer
|
|
187
|
-
console.log('⏱️ Step 10: Waiting for messages to resume...');
|
|
188
|
-
await sleep(2000);
|
|
189
|
-
|
|
190
|
-
const receivedAfterResume = receivedCount;
|
|
191
|
-
const messagesResumed = receivedAfterResume - receivedAfterMaintenance;
|
|
192
|
-
console.log(` Messages received after resuming: ${messagesResumed}`);
|
|
193
|
-
console.log('✅ Messages are flowing again!\n');
|
|
194
|
-
|
|
195
|
-
// Stop producer
|
|
196
|
-
console.log('🛑 Step 11: Stopping producer...');
|
|
197
|
-
producerRunning = false;
|
|
198
|
-
await producerPromise; // Wait for producer to actually stop
|
|
199
|
-
|
|
200
|
-
const finalProducedCount = producedCount;
|
|
201
|
-
console.log(` Final produced count: ${finalProducedCount}\n`);
|
|
202
|
-
|
|
203
|
-
// 9. Wait for drain to complete
|
|
204
|
-
console.log('⏱️ Step 12: Waiting 10 seconds for file buffer to drain...');
|
|
205
|
-
for (let i = 0; i < 10; i++) {
|
|
206
|
-
await sleep(1000);
|
|
207
|
-
console.log(` ${i + 1}s - Received: ${receivedCount}/${finalProducedCount} (${((receivedCount/finalProducedCount)*100).toFixed(1)}%)`);
|
|
208
|
-
|
|
209
|
-
// Check if we've received all messages
|
|
210
|
-
if (receivedCount >= finalProducedCount) {
|
|
211
|
-
console.log(' ✅ All messages received!\n');
|
|
212
|
-
break;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// 10. Stop consumer
|
|
217
|
-
console.log('🛑 Step 13: Stopping consumer...');
|
|
218
|
-
consumerActive = false;
|
|
219
|
-
await consumePromise;
|
|
220
|
-
console.log('✅ Consumer stopped\n');
|
|
221
|
-
|
|
222
|
-
// 11. Final verification
|
|
223
|
-
console.log('📊 Final Verification:\n');
|
|
224
|
-
console.log(` Total Produced: ${finalProducedCount}`);
|
|
225
|
-
console.log(` Total Received: ${receivedCount}`);
|
|
226
|
-
console.log(` Difference: ${finalProducedCount - receivedCount}`);
|
|
227
|
-
|
|
228
|
-
if (receivedCount === finalProducedCount) {
|
|
229
|
-
console.log('\n✅ SUCCESS: All messages accounted for!');
|
|
230
|
-
console.log(' Maintenance mode works correctly with file buffer.\n');
|
|
231
|
-
|
|
232
|
-
return {
|
|
233
|
-
success: true,
|
|
234
|
-
message: `All ${finalProducedCount} messages accounted for during maintenance mode test`
|
|
235
|
-
};
|
|
236
|
-
} else if (receivedCount < finalProducedCount) {
|
|
237
|
-
const missing = finalProducedCount - receivedCount;
|
|
238
|
-
console.log(`\n⚠️ WARNING: Missing ${missing} messages`);
|
|
239
|
-
console.log(' They may still be in the file buffer or queue.');
|
|
240
|
-
console.log(' Check /var/lib/queen/buffers/ for remaining files.\n');
|
|
241
|
-
|
|
242
|
-
// Check buffer status
|
|
243
|
-
const finalStatus = await axios.get(`${API_URL}/api/v1/system/maintenance`);
|
|
244
|
-
console.log(' Buffer status:', finalStatus.data);
|
|
245
|
-
|
|
246
|
-
return {
|
|
247
|
-
success: false,
|
|
248
|
-
message: `Missing ${missing} messages (produced: ${finalProducedCount}, received: ${receivedCount})`
|
|
249
|
-
};
|
|
250
|
-
} else {
|
|
251
|
-
console.log(`\n❌ ERROR: Received more messages than produced! (${receivedCount} > ${finalProducedCount})`);
|
|
252
|
-
console.log(' This should not happen - possible duplicate issue.\n');
|
|
253
|
-
|
|
254
|
-
return {
|
|
255
|
-
success: false,
|
|
256
|
-
message: `Received more than produced (${receivedCount} > ${finalProducedCount})`
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
} catch (err) {
|
|
261
|
-
console.error('\n❌ Test failed:', err.message);
|
|
262
|
-
console.error(err.stack);
|
|
263
|
-
|
|
264
|
-
// Clean up
|
|
265
|
-
consumerActive = false;
|
|
266
|
-
|
|
267
|
-
return { success: false, message: `Test threw error: ${err.message}` };
|
|
268
|
-
} finally {
|
|
269
|
-
// Clean up
|
|
270
|
-
consumerActive = false;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function sleep(ms) {
|
|
275
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
276
|
-
}
|
|
277
|
-
|