queen-mq 0.12.3 → 0.13.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.
- package/client-v2/LOGGING.md +63 -30
- package/client-v2/Queen.js +8 -7
- package/client-v2/buffer/BufferManager.js +12 -23
- package/client-v2/builders/QueueBuilder.js +0 -2
- package/client-v2/consumer/ConsumerManager.js +2 -3
- package/client-v2/http/HttpClient.js +0 -1
- package/client-v2/utils/defaults.js +2 -1
- package/client-v2/utils/logger.js +46 -14
- package/package.json +1 -1
- package/test-v2/auth.js +340 -0
- package/test-v2/logger.js +312 -0
- package/test-v2/push.js +275 -0
- package/test-v2/run.js +7 -1
- package/test-v2/watermark.js +507 -0
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Watermark Bug Tests
|
|
3
|
+
*
|
|
4
|
+
* These tests verify that consumer_watermarks are properly reset when:
|
|
5
|
+
* 1. Seeking a consumer group backwards to a timestamp
|
|
6
|
+
* 2. Deleting a consumer group (watermark should be cleaned up)
|
|
7
|
+
*
|
|
8
|
+
* The consumer_watermarks table optimizes POP by tracking when a consumer
|
|
9
|
+
* last found the queue empty. However, if the watermark isn't reset when
|
|
10
|
+
* the cursor moves backwards, old messages become invisible.
|
|
11
|
+
*
|
|
12
|
+
* BUG: Currently, seek and delete operations don't touch consumer_watermarks,
|
|
13
|
+
* causing re-consumption to fail silently.
|
|
14
|
+
*
|
|
15
|
+
* IMPORTANT: The watermark filter has a 2-minute buffer:
|
|
16
|
+
* pl.updated_at >= (v_watermark - interval '2 minutes')
|
|
17
|
+
*
|
|
18
|
+
* So the bug only manifests after 2+ minutes have passed. To test this without
|
|
19
|
+
* waiting, we directly manipulate the watermark timestamp in the database.
|
|
20
|
+
*
|
|
21
|
+
* Run: node run.js seekBackwardsAllowsReconsume
|
|
22
|
+
* node run.js deleteConsumerGroupAllowsReconsume
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { dbPool } from './run.js'
|
|
26
|
+
|
|
27
|
+
const QUEUE_NAME_SEEK = 'test-queue-v2-watermark-seek'
|
|
28
|
+
const QUEUE_NAME_DELETE = 'test-queue-v2-watermark-delete'
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Helper: Move the watermark FORWARD in time to simulate that partitions
|
|
32
|
+
* are "old" relative to the watermark.
|
|
33
|
+
*
|
|
34
|
+
* The filter is: pl.updated_at >= (v_watermark - interval '2 minutes')
|
|
35
|
+
*
|
|
36
|
+
* If watermark = NOW + 10 minutes, then:
|
|
37
|
+
* filter = pl.updated_at >= (NOW + 10 - 2) = pl.updated_at >= NOW + 8
|
|
38
|
+
*
|
|
39
|
+
* Partitions updated at NOW will fail this filter (NOW < NOW + 8)
|
|
40
|
+
*/
|
|
41
|
+
async function advanceWatermark(queueName, consumerGroup, minutesForward = 10) {
|
|
42
|
+
const result = await dbPool.query(`
|
|
43
|
+
UPDATE queen.consumer_watermarks
|
|
44
|
+
SET last_empty_scan_at = NOW() + interval '${minutesForward} minutes',
|
|
45
|
+
updated_at = NOW() + interval '${minutesForward} minutes'
|
|
46
|
+
WHERE queue_name = $1 AND consumer_group = $2
|
|
47
|
+
RETURNING *
|
|
48
|
+
`, [queueName, consumerGroup])
|
|
49
|
+
return result.rows[0]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Helper: Check if watermark exists for a queue/consumer_group
|
|
54
|
+
*/
|
|
55
|
+
async function getWatermark(queueName, consumerGroup) {
|
|
56
|
+
const result = await dbPool.query(`
|
|
57
|
+
SELECT * FROM queen.consumer_watermarks
|
|
58
|
+
WHERE queue_name = $1 AND consumer_group = $2
|
|
59
|
+
`, [queueName, consumerGroup])
|
|
60
|
+
return result.rows[0]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Test: Seeking backwards should allow re-consumption of messages
|
|
65
|
+
*
|
|
66
|
+
* Steps:
|
|
67
|
+
* 1. Push messages to queue
|
|
68
|
+
* 2. Consume all messages with a consumer group (watermark advances)
|
|
69
|
+
* 3. Artificially age the watermark (simulate 10 minutes passing)
|
|
70
|
+
* 4. Seek the consumer group backwards to before the messages
|
|
71
|
+
* 5. Try to consume again - should get all messages
|
|
72
|
+
*
|
|
73
|
+
* Expected: All messages are re-consumed after seek
|
|
74
|
+
* Actual (bug): Watermark blocks re-consumption, 0 messages returned
|
|
75
|
+
*/
|
|
76
|
+
export async function seekBackwardsAllowsReconsume(client) {
|
|
77
|
+
const consumerGroup = 'watermark-seek-test-cg'
|
|
78
|
+
|
|
79
|
+
// Cleanup
|
|
80
|
+
try {
|
|
81
|
+
await client.deleteConsumerGroup(consumerGroup, true)
|
|
82
|
+
// Also clean up any orphaned watermark from previous runs
|
|
83
|
+
await dbPool.query(
|
|
84
|
+
'DELETE FROM queen.consumer_watermarks WHERE consumer_group = $1',
|
|
85
|
+
[consumerGroup]
|
|
86
|
+
)
|
|
87
|
+
} catch (e) {
|
|
88
|
+
// Ignore
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 1. Create queue
|
|
92
|
+
const queue = await client.queue(QUEUE_NAME_SEEK).create()
|
|
93
|
+
if (!queue.configured) {
|
|
94
|
+
return { success: false, message: 'Queue not created' }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 2. Record timestamp BEFORE pushing messages
|
|
98
|
+
const timestampBeforeMessages = new Date().toISOString()
|
|
99
|
+
|
|
100
|
+
// Small delay to ensure timestamp ordering
|
|
101
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
102
|
+
|
|
103
|
+
// 3. Push messages
|
|
104
|
+
const messageCount = 10
|
|
105
|
+
for (let i = 0; i < messageCount; i++) {
|
|
106
|
+
await client
|
|
107
|
+
.queue(QUEUE_NAME_SEEK)
|
|
108
|
+
.partition(`p-${i}`)
|
|
109
|
+
.push([{ data: { id: i, batch: 'original' } }])
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
console.log(` Pushed ${messageCount} messages`)
|
|
113
|
+
|
|
114
|
+
// 4. Consume all messages with consumer group
|
|
115
|
+
let firstConsumeCount = 0
|
|
116
|
+
await client
|
|
117
|
+
.queue(QUEUE_NAME_SEEK)
|
|
118
|
+
.group(consumerGroup)
|
|
119
|
+
.subscriptionMode('all')
|
|
120
|
+
.concurrency(5)
|
|
121
|
+
.batch(1)
|
|
122
|
+
.wait(false)
|
|
123
|
+
.idleMillis(3000)
|
|
124
|
+
.consume(async msgs => {
|
|
125
|
+
firstConsumeCount += msgs.length
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
console.log(` First consume: ${firstConsumeCount} messages`)
|
|
129
|
+
|
|
130
|
+
if (firstConsumeCount !== messageCount) {
|
|
131
|
+
return {
|
|
132
|
+
success: false,
|
|
133
|
+
message: `First consume should get ${messageCount} messages, got ${firstConsumeCount}`
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 5. Verify queue is empty for this consumer (triggers watermark advancement)
|
|
138
|
+
const emptyCheck = await client
|
|
139
|
+
.queue(QUEUE_NAME_SEEK)
|
|
140
|
+
.group(consumerGroup)
|
|
141
|
+
.batch(10)
|
|
142
|
+
.wait(false)
|
|
143
|
+
.pop()
|
|
144
|
+
|
|
145
|
+
if (emptyCheck.length !== 0) {
|
|
146
|
+
return {
|
|
147
|
+
success: false,
|
|
148
|
+
message: `Queue should be empty after first consume, got ${emptyCheck.length} messages`
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
console.log(` Verified queue is empty (watermark should be set)`)
|
|
153
|
+
|
|
154
|
+
// 6. Check watermark was created
|
|
155
|
+
const watermarkBefore = await getWatermark(QUEUE_NAME_SEEK, consumerGroup)
|
|
156
|
+
if (!watermarkBefore) {
|
|
157
|
+
return {
|
|
158
|
+
success: false,
|
|
159
|
+
message: 'Watermark was not created after empty queue check'
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
console.log(` Watermark exists: last_empty_scan_at = ${watermarkBefore.last_empty_scan_at}`)
|
|
163
|
+
|
|
164
|
+
// 7. CRITICAL: Advance the watermark forward to simulate that partitions are "old"
|
|
165
|
+
// relative to the watermark. This triggers the bug where old partitions are filtered out.
|
|
166
|
+
const advancedWatermark = await advanceWatermark(QUEUE_NAME_SEEK, consumerGroup, 10)
|
|
167
|
+
console.log(` Advanced watermark to: ${advancedWatermark.last_empty_scan_at} (10 minutes in future)`)
|
|
168
|
+
|
|
169
|
+
// 8. Seek backwards to BEFORE the messages were pushed
|
|
170
|
+
console.log(` Seeking to timestamp: ${timestampBeforeMessages}`)
|
|
171
|
+
|
|
172
|
+
const seekResult = await client.admin.seekConsumerGroup(
|
|
173
|
+
consumerGroup,
|
|
174
|
+
QUEUE_NAME_SEEK,
|
|
175
|
+
{ timestamp: timestampBeforeMessages }
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
console.log(` Seek result: ${JSON.stringify(seekResult)}`)
|
|
179
|
+
|
|
180
|
+
// 9. Check if watermark was reset (it should be, but currently isn't - that's the bug)
|
|
181
|
+
const watermarkAfterSeek = await getWatermark(QUEUE_NAME_SEEK, consumerGroup)
|
|
182
|
+
if (watermarkAfterSeek) {
|
|
183
|
+
console.log(` WARNING: Watermark still exists after seek: ${watermarkAfterSeek.last_empty_scan_at}`)
|
|
184
|
+
} else {
|
|
185
|
+
console.log(` Watermark was properly deleted after seek`)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Small delay after seek
|
|
189
|
+
await new Promise(resolve => setTimeout(resolve, 500))
|
|
190
|
+
|
|
191
|
+
// 10. Try to consume again - should get all messages back
|
|
192
|
+
let secondConsumeCount = 0
|
|
193
|
+
await client
|
|
194
|
+
.queue(QUEUE_NAME_SEEK)
|
|
195
|
+
.group(consumerGroup)
|
|
196
|
+
.concurrency(5)
|
|
197
|
+
.batch(1)
|
|
198
|
+
.wait(false)
|
|
199
|
+
.idleMillis(3000)
|
|
200
|
+
.consume(async msgs => {
|
|
201
|
+
secondConsumeCount += msgs.length
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
console.log(` Second consume (after seek): ${secondConsumeCount} messages`)
|
|
205
|
+
|
|
206
|
+
// 11. Verify we got all messages back
|
|
207
|
+
if (secondConsumeCount !== messageCount) {
|
|
208
|
+
return {
|
|
209
|
+
success: false,
|
|
210
|
+
message: `After seek backwards, should re-consume ${messageCount} messages, but got ${secondConsumeCount}. ` +
|
|
211
|
+
`This is because consumer_watermarks was not reset on seek (watermark still at ${watermarkAfterSeek?.last_empty_scan_at}).`
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
success: true,
|
|
217
|
+
message: `Seek backwards allowed re-consumption of ${secondConsumeCount} messages`
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Test: Deleting consumer group should allow fresh consumption
|
|
223
|
+
*
|
|
224
|
+
* Steps:
|
|
225
|
+
* 1. Push messages to queue
|
|
226
|
+
* 2. Consume all messages with a consumer group (watermark advances)
|
|
227
|
+
* 3. Artificially age the watermark (simulate 10 minutes passing)
|
|
228
|
+
* 4. Delete the consumer group
|
|
229
|
+
* 5. Create a new consumer group with the SAME name
|
|
230
|
+
* 6. Try to consume - should get all messages
|
|
231
|
+
*
|
|
232
|
+
* Expected: All messages are consumed by the "new" consumer group
|
|
233
|
+
* Actual (bug): Orphaned watermark blocks consumption, 0 messages returned
|
|
234
|
+
*/
|
|
235
|
+
export async function deleteConsumerGroupAllowsReconsume(client) {
|
|
236
|
+
const consumerGroup = 'watermark-delete-test-cg'
|
|
237
|
+
|
|
238
|
+
// Cleanup
|
|
239
|
+
try {
|
|
240
|
+
await client.deleteConsumerGroup(consumerGroup, true)
|
|
241
|
+
// Also clean up any orphaned watermark from previous runs
|
|
242
|
+
await dbPool.query(
|
|
243
|
+
'DELETE FROM queen.consumer_watermarks WHERE consumer_group = $1',
|
|
244
|
+
[consumerGroup]
|
|
245
|
+
)
|
|
246
|
+
} catch (e) {
|
|
247
|
+
// Ignore
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// 1. Create queue
|
|
251
|
+
const queue = await client.queue(QUEUE_NAME_DELETE).create()
|
|
252
|
+
if (!queue.configured) {
|
|
253
|
+
return { success: false, message: 'Queue not created' }
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// 2. Push messages
|
|
257
|
+
const messageCount = 10
|
|
258
|
+
for (let i = 0; i < messageCount; i++) {
|
|
259
|
+
await client
|
|
260
|
+
.queue(QUEUE_NAME_DELETE)
|
|
261
|
+
.partition(`p-${i}`)
|
|
262
|
+
.push([{ data: { id: i, batch: 'original' } }])
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
console.log(` Pushed ${messageCount} messages`)
|
|
266
|
+
|
|
267
|
+
// 3. Consume all messages with consumer group
|
|
268
|
+
let firstConsumeCount = 0
|
|
269
|
+
await client
|
|
270
|
+
.queue(QUEUE_NAME_DELETE)
|
|
271
|
+
.group(consumerGroup)
|
|
272
|
+
.subscriptionMode('all')
|
|
273
|
+
.concurrency(5)
|
|
274
|
+
.batch(1)
|
|
275
|
+
.wait(false)
|
|
276
|
+
.idleMillis(3000)
|
|
277
|
+
.consume(async msgs => {
|
|
278
|
+
firstConsumeCount += msgs.length
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
console.log(` First consume: ${firstConsumeCount} messages`)
|
|
282
|
+
|
|
283
|
+
if (firstConsumeCount !== messageCount) {
|
|
284
|
+
return {
|
|
285
|
+
success: false,
|
|
286
|
+
message: `First consume should get ${messageCount} messages, got ${firstConsumeCount}`
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// 4. Verify queue is empty for this consumer (triggers watermark advancement)
|
|
291
|
+
const emptyCheck = await client
|
|
292
|
+
.queue(QUEUE_NAME_DELETE)
|
|
293
|
+
.group(consumerGroup)
|
|
294
|
+
.batch(10)
|
|
295
|
+
.wait(false)
|
|
296
|
+
.pop()
|
|
297
|
+
|
|
298
|
+
if (emptyCheck.length !== 0) {
|
|
299
|
+
return {
|
|
300
|
+
success: false,
|
|
301
|
+
message: `Queue should be empty after first consume, got ${emptyCheck.length} messages`
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
console.log(` Verified queue is empty (watermark should be set)`)
|
|
306
|
+
|
|
307
|
+
// 5. Check watermark was created
|
|
308
|
+
const watermarkBefore = await getWatermark(QUEUE_NAME_DELETE, consumerGroup)
|
|
309
|
+
if (!watermarkBefore) {
|
|
310
|
+
return {
|
|
311
|
+
success: false,
|
|
312
|
+
message: 'Watermark was not created after empty queue check'
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
console.log(` Watermark exists: last_empty_scan_at = ${watermarkBefore.last_empty_scan_at}`)
|
|
316
|
+
|
|
317
|
+
// 6. CRITICAL: Advance the watermark forward to simulate that partitions are "old"
|
|
318
|
+
const advancedWatermark = await advanceWatermark(QUEUE_NAME_DELETE, consumerGroup, 10)
|
|
319
|
+
console.log(` Advanced watermark to: ${advancedWatermark.last_empty_scan_at} (10 minutes in future)`)
|
|
320
|
+
|
|
321
|
+
// 7. Delete the consumer group
|
|
322
|
+
console.log(` Deleting consumer group: ${consumerGroup}`)
|
|
323
|
+
|
|
324
|
+
const deleteResult = await client.deleteConsumerGroup(consumerGroup, true)
|
|
325
|
+
console.log(` Delete result: ${JSON.stringify(deleteResult)}`)
|
|
326
|
+
|
|
327
|
+
// 8. Check if watermark was deleted (it should be, but currently isn't - that's the bug)
|
|
328
|
+
const watermarkAfterDelete = await getWatermark(QUEUE_NAME_DELETE, consumerGroup)
|
|
329
|
+
if (watermarkAfterDelete) {
|
|
330
|
+
console.log(` WARNING: Orphaned watermark still exists: ${watermarkAfterDelete.last_empty_scan_at}`)
|
|
331
|
+
} else {
|
|
332
|
+
console.log(` Watermark was properly deleted`)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Small delay after delete
|
|
336
|
+
await new Promise(resolve => setTimeout(resolve, 500))
|
|
337
|
+
|
|
338
|
+
// 9. Create a "new" consumer group with the same name and try to consume
|
|
339
|
+
// Since partition_consumers was deleted, this should start fresh
|
|
340
|
+
// But if consumer_watermarks wasn't deleted, it will skip all messages
|
|
341
|
+
let secondConsumeCount = 0
|
|
342
|
+
await client
|
|
343
|
+
.queue(QUEUE_NAME_DELETE)
|
|
344
|
+
.group(consumerGroup)
|
|
345
|
+
.subscriptionMode('all')
|
|
346
|
+
.concurrency(5)
|
|
347
|
+
.batch(1)
|
|
348
|
+
.wait(false)
|
|
349
|
+
.idleMillis(3000)
|
|
350
|
+
.consume(async msgs => {
|
|
351
|
+
secondConsumeCount += msgs.length
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
console.log(` Second consume (after delete + recreate): ${secondConsumeCount} messages`)
|
|
355
|
+
|
|
356
|
+
// 10. Verify we got all messages
|
|
357
|
+
if (secondConsumeCount !== messageCount) {
|
|
358
|
+
return {
|
|
359
|
+
success: false,
|
|
360
|
+
message: `After deleting CG, new CG with same name should consume ${messageCount} messages, ` +
|
|
361
|
+
`but got ${secondConsumeCount}. Orphaned watermark at ${watermarkAfterDelete?.last_empty_scan_at}.`
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
success: true,
|
|
367
|
+
message: `Delete + recreate allowed fresh consumption of ${secondConsumeCount} messages`
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Test: Deleting consumer group for specific queue should allow fresh consumption
|
|
373
|
+
*
|
|
374
|
+
* Similar to above but uses deleteConsumerGroupForQueue instead of deleteConsumerGroup
|
|
375
|
+
*/
|
|
376
|
+
export async function deleteConsumerGroupForQueueAllowsReconsume(client) {
|
|
377
|
+
const consumerGroup = 'watermark-delete-queue-test-cg'
|
|
378
|
+
const queueName = 'test-queue-v2-watermark-delete-queue'
|
|
379
|
+
|
|
380
|
+
// Cleanup
|
|
381
|
+
try {
|
|
382
|
+
await client.admin.deleteConsumerGroupForQueue(consumerGroup, queueName, true)
|
|
383
|
+
// Also clean up any orphaned watermark from previous runs
|
|
384
|
+
await dbPool.query(
|
|
385
|
+
'DELETE FROM queen.consumer_watermarks WHERE queue_name = $1 AND consumer_group = $2',
|
|
386
|
+
[queueName, consumerGroup]
|
|
387
|
+
)
|
|
388
|
+
} catch (e) {
|
|
389
|
+
// Ignore
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// 1. Create queue
|
|
393
|
+
const queue = await client.queue(queueName).create()
|
|
394
|
+
if (!queue.configured) {
|
|
395
|
+
return { success: false, message: 'Queue not created' }
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// 2. Push messages
|
|
399
|
+
const messageCount = 10
|
|
400
|
+
for (let i = 0; i < messageCount; i++) {
|
|
401
|
+
await client
|
|
402
|
+
.queue(queueName)
|
|
403
|
+
.partition(`p-${i}`)
|
|
404
|
+
.push([{ data: { id: i, batch: 'original' } }])
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
console.log(` Pushed ${messageCount} messages`)
|
|
408
|
+
|
|
409
|
+
// 3. Consume all messages with consumer group
|
|
410
|
+
let firstConsumeCount = 0
|
|
411
|
+
await client
|
|
412
|
+
.queue(queueName)
|
|
413
|
+
.group(consumerGroup)
|
|
414
|
+
.subscriptionMode('all')
|
|
415
|
+
.concurrency(5)
|
|
416
|
+
.batch(1)
|
|
417
|
+
.wait(false)
|
|
418
|
+
.idleMillis(3000)
|
|
419
|
+
.consume(async msgs => {
|
|
420
|
+
firstConsumeCount += msgs.length
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
console.log(` First consume: ${firstConsumeCount} messages`)
|
|
424
|
+
|
|
425
|
+
if (firstConsumeCount !== messageCount) {
|
|
426
|
+
return {
|
|
427
|
+
success: false,
|
|
428
|
+
message: `First consume should get ${messageCount} messages, got ${firstConsumeCount}`
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// 4. Verify queue is empty (triggers watermark)
|
|
433
|
+
const emptyCheck = await client
|
|
434
|
+
.queue(queueName)
|
|
435
|
+
.group(consumerGroup)
|
|
436
|
+
.batch(10)
|
|
437
|
+
.wait(false)
|
|
438
|
+
.pop()
|
|
439
|
+
|
|
440
|
+
if (emptyCheck.length !== 0) {
|
|
441
|
+
return {
|
|
442
|
+
success: false,
|
|
443
|
+
message: `Queue should be empty after first consume, got ${emptyCheck.length} messages`
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
console.log(` Verified queue is empty (watermark should be set)`)
|
|
448
|
+
|
|
449
|
+
// 5. Check watermark was created
|
|
450
|
+
const watermarkBefore = await getWatermark(queueName, consumerGroup)
|
|
451
|
+
if (!watermarkBefore) {
|
|
452
|
+
return {
|
|
453
|
+
success: false,
|
|
454
|
+
message: 'Watermark was not created after empty queue check'
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
console.log(` Watermark exists: last_empty_scan_at = ${watermarkBefore.last_empty_scan_at}`)
|
|
458
|
+
|
|
459
|
+
// 6. CRITICAL: Advance the watermark forward
|
|
460
|
+
const advancedWatermark = await advanceWatermark(queueName, consumerGroup, 10)
|
|
461
|
+
console.log(` Advanced watermark to: ${advancedWatermark.last_empty_scan_at} (10 minutes in future)`)
|
|
462
|
+
|
|
463
|
+
// 7. Delete consumer group for this specific queue
|
|
464
|
+
console.log(` Deleting consumer group for queue: ${consumerGroup} / ${queueName}`)
|
|
465
|
+
|
|
466
|
+
const deleteResult = await client.admin.deleteConsumerGroupForQueue(consumerGroup, queueName, true)
|
|
467
|
+
console.log(` Delete result: ${JSON.stringify(deleteResult)}`)
|
|
468
|
+
|
|
469
|
+
// 8. Check if watermark was deleted
|
|
470
|
+
const watermarkAfterDelete = await getWatermark(queueName, consumerGroup)
|
|
471
|
+
if (watermarkAfterDelete) {
|
|
472
|
+
console.log(` WARNING: Orphaned watermark still exists: ${watermarkAfterDelete.last_empty_scan_at}`)
|
|
473
|
+
} else {
|
|
474
|
+
console.log(` Watermark was properly deleted`)
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
await new Promise(resolve => setTimeout(resolve, 500))
|
|
478
|
+
|
|
479
|
+
// 9. Try to consume again
|
|
480
|
+
let secondConsumeCount = 0
|
|
481
|
+
await client
|
|
482
|
+
.queue(queueName)
|
|
483
|
+
.group(consumerGroup)
|
|
484
|
+
.subscriptionMode('all')
|
|
485
|
+
.concurrency(5)
|
|
486
|
+
.batch(1)
|
|
487
|
+
.wait(false)
|
|
488
|
+
.idleMillis(3000)
|
|
489
|
+
.consume(async msgs => {
|
|
490
|
+
secondConsumeCount += msgs.length
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
console.log(` Second consume (after delete): ${secondConsumeCount} messages`)
|
|
494
|
+
|
|
495
|
+
if (secondConsumeCount !== messageCount) {
|
|
496
|
+
return {
|
|
497
|
+
success: false,
|
|
498
|
+
message: `After deleting CG for queue, should consume ${messageCount} messages, ` +
|
|
499
|
+
`but got ${secondConsumeCount}. Orphaned watermark at ${watermarkAfterDelete?.last_empty_scan_at}.`
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return {
|
|
504
|
+
success: true,
|
|
505
|
+
message: `Delete CG for queue allowed fresh consumption of ${secondConsumeCount} messages`
|
|
506
|
+
}
|
|
507
|
+
}
|