queen-mq 1.2.0 → 2.0.0

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.
Files changed (52) hide show
  1. package/README.md +71 -18
  2. package/client-v2/Queen.js +45 -13
  3. package/client-v2/README.md +26 -11
  4. package/client-v2/admin/Admin.js +0 -47
  5. package/client-v2/builders/QueueBuilder.js +33 -10
  6. package/client-v2/builders/TimerBuilder.js +2 -2
  7. package/client-v2/builders/TransactionBuilder.js +35 -9
  8. package/client-v2/consumer/ConsumerManager.js +67 -46
  9. package/client-v2/ephemeral/Ephemeral.js +7 -9
  10. package/client-v2/kv/Kv.js +4 -4
  11. package/client-v2/kv/expiry.js +1 -1
  12. package/client-v2/streams/Stream.js +8 -3
  13. package/client-v2/streams/helpers/rateLimiter.js +4 -4
  14. package/client-v2/streams/operators/GateOperator.js +9 -2
  15. package/client-v2/streams/operators/ReduceOperator.js +2 -2
  16. package/client-v2/streams/operators/WindowSessionOperator.js +3 -3
  17. package/client-v2/streams/runtime/Runner.js +111 -54
  18. package/client-v2/streams/runtime/cycle.js +4 -4
  19. package/client-v2/streams/runtime/register.js +1 -1
  20. package/client-v2/utils/conflation.js +0 -6
  21. package/client-v2/utils/consumerGroup.js +54 -0
  22. package/package.json +5 -8
  23. package/test-v2/_kvtimers.js +12 -13
  24. package/test-v2/ackwindow.js +12 -192
  25. package/test-v2/bootstrap.js +2 -2
  26. package/test-v2/conflation-unit/conflationWire.test.js +0 -12
  27. package/test-v2/consume.js +41 -1
  28. package/test-v2/consumer-unit/handlerError.test.js +161 -0
  29. package/test-v2/docs.js +5 -4
  30. package/test-v2/http-unit/pushStatus.test.js +142 -0
  31. package/test-v2/http-unit/renew.test.js +96 -0
  32. package/test-v2/kv-unit/timerWire.test.js +1 -1
  33. package/test-v2/kv-unit/txnWire.test.js +101 -2
  34. package/test-v2/kv.js +6 -5
  35. package/test-v2/pop.js +28 -1
  36. package/test-v2/retention.js +28 -7
  37. package/test-v2/run.js +50 -114
  38. package/test-v2/runner-unit/fatalExit.test.js +64 -0
  39. package/test-v2/semantics.js +20 -34
  40. package/test-v2/stream/_helpers.js +10 -19
  41. package/test-v2/stream/cron.js +1 -1
  42. package/test-v2/stream/gate.js +54 -0
  43. package/test-v2/stream/index.js +2 -0
  44. package/test-v2/stream/tumbling.js +13 -9
  45. package/test-v2/streams-unit/ack.test.js +61 -0
  46. package/test-v2/streams-unit/cycle.test.js +1 -1
  47. package/test-v2/streams-unit/e2e.test.js +6 -10
  48. package/test-v2/streams-unit/gate.test.js +189 -0
  49. package/test-v2/timers.js +6 -5
  50. package/test-v2/transaction.js +169 -0
  51. package/test-v2/watermark.js +38 -176
  52. package/test-v2/maintenance.js +0 -277
@@ -1,90 +1,36 @@
1
1
  /**
2
- * Seek / delete re-consumption tests (behaviour, not bookkeeping)
2
+ * Seek / delete re-consumption tests.
3
3
  *
4
- * These tests verify the BEHAVIOUR a user cares about:
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
- * WHY THIS WAS REWRITTEN (2026-07-24)
11
- * -----------------------------------
12
- * The consumer_watermarks table is a LEGACY-path optimisation: it records when a
13
- * consumer last found a queue empty so an indexed empty-scan can be skipped. The
14
- * original tests asserted the *row* was created after an empty poll and then aged
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 (watermark advances)
82
- * 3. Artificially age the watermark (simulate 10 minutes passing)
83
- * 4. Seek the consumer group backwards to before the messages
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 (triggers watermark advancement)
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 (watermark should be set)`)
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
- // 8. Seek backwards to BEFORE the messages were pushed
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
- // 10. Try to consume again - should get all messages back
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
- // 11. Verify we got all messages back within the idle-bounded consume above.
220
- // A shortfall means the backward seek did not re-expose the messages
221
- // promptly: on the legacy path the consumer_watermarks row was not reset
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 (watermark advances)
245
- * 3. Artificially age the watermark (simulate 10 minutes passing)
246
- * 4. Delete the consumer group
247
- * 5. Create a new consumer group with the SAME name
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 (triggers watermark advancement)
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 (watermark should be set)`)
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
- // 7. Delete the consumer group
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
- // 9. Create a "new" consumer group with the same name and try to consume
355
- // Since partition_consumers was deleted, this should start fresh
356
- // But if consumer_watermarks wasn't deleted, it will skip all messages
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
- // 10. Verify we got all messages within the idle-bounded consume above. A
373
- // shortfall means the delete left stale discovery state: a legacy orphaned
374
- // watermark (at ${watermarkAfterDelete?.last_empty_scan_at}), or, on the
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}. (legacy watermark after delete: ${watermarkAfterDelete?.last_empty_scan_at})`
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 (triggers watermark)
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 (watermark should be set)`)
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
- // 7. Delete consumer group for this specific queue
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
- // 9. Try to consume again
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}. (legacy watermark after delete: ${watermarkAfterDelete?.last_empty_scan_at})`
377
+ `but got ${secondConsumeCount}`
516
378
  }
517
379
  }
518
380
 
@@ -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
-