queen-mq 0.3.1 → 0.4.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 (39) hide show
  1. package/README.md +212 -144
  2. package/client-js/client-v2/Queen.js +28 -0
  3. package/client-js/client-v2/README.md +6 -2
  4. package/client-js/client-v2/builders/QueueBuilder.js +7 -1
  5. package/client-js/client-v2/builders/TransactionBuilder.js +20 -4
  6. package/client-js/client-v2/stream/StreamBuilder.js +72 -0
  7. package/client-js/client-v2/stream/StreamConsumer.js +156 -0
  8. package/client-js/client-v2/stream/Window.js +140 -0
  9. package/client-js/test-v2/MAINTENANCE_TEST.md +148 -0
  10. package/client-js/test-v2/consume.js +1 -0
  11. package/client-js/test-v2/maintenance.js +261 -0
  12. package/client-js/test-v2/retention.js +69 -0
  13. package/client-js/test-v2/run.js +7 -1
  14. package/package.json +1 -1
  15. package/client-js/client/client.js +0 -1536
  16. package/client-js/client/index.js +0 -4
  17. package/client-js/client/utils/http.js +0 -173
  18. package/client-js/client/utils/loadBalancer.js +0 -152
  19. package/client-js/client/utils/retry.js +0 -41
  20. package/client-js/services/encryptionService.js +0 -82
  21. package/client-js/services/evictionService.js +0 -160
  22. package/client-js/services/retentionService.js +0 -162
  23. package/client-js/services/startupSync.js +0 -35
  24. package/client-js/test/README.md +0 -224
  25. package/client-js/test/advanced-client-tests.js +0 -761
  26. package/client-js/test/advanced-pattern-tests.js +0 -1137
  27. package/client-js/test/bus-mode-tests.js +0 -361
  28. package/client-js/test/core-tests.js +0 -457
  29. package/client-js/test/edge-case-tests.js +0 -562
  30. package/client-js/test/enterprise-tests.js +0 -637
  31. package/client-js/test/human.js +0 -162
  32. package/client-js/test/partition-locking-tests.js +0 -545
  33. package/client-js/test/partition-transaction-tests.js +0 -482
  34. package/client-js/test/qos0-tests.js +0 -334
  35. package/client-js/test/test-new.js +0 -370
  36. package/client-js/test/utils.js +0 -169
  37. package/client-js/test/window-buffer-test.js +0 -114
  38. package/client-js/utils/logger.js +0 -44
  39. package/client-js/utils/uuid.js +0 -5
@@ -1,482 +0,0 @@
1
- /**
2
- * Partition-Scoped Transaction ID Tests
3
- *
4
- * Tests that transaction_id uniqueness is properly scoped to partitions.
5
- * With the constraint UNIQUE(partition_id, transaction_id), the same transaction_id
6
- * can exist in multiple partitions. This tests that ACK operations correctly
7
- * identify and operate on the right message by using BOTH partition_id AND transaction_id.
8
- */
9
-
10
- import { startTest, passTest, failTest, sleep, dbPool, getMessageCount, log, TEST_CONFIG } from './utils.js';
11
-
12
- /**
13
- * Test: Duplicate transaction IDs across partitions with correct ACK targeting
14
- *
15
- * This is the critical test that verifies the fix for partition-scoped transaction_ids.
16
- * Without the fix, ACKing a message in one partition could accidentally ACK
17
- * a message with the same transaction_id in a different partition.
18
- */
19
- export async function testDuplicateTransactionIdsAcrossPartitions(client) {
20
- startTest('Duplicate Transaction IDs Across Partitions - ACK Targeting', 'edge');
21
-
22
- try {
23
- const queue = 'test-duplicate-txn-ids';
24
- const partition1 = 'partition-A';
25
- const partition2 = 'partition-B';
26
- const sharedTxnId = 'shared-txn-12345';
27
-
28
- // Configure queue
29
- await client.queue(queue, { leaseTime: 30 });
30
-
31
- // CRITICAL: Push messages with the SAME transaction_id to DIFFERENT partitions
32
- log(`Pushing message with txn=${sharedTxnId} to ${partition1}`, 'test');
33
- await client.push(`${queue}/${partition1}`,
34
- { data: 'Message in Partition A', partition: partition1 },
35
- { transactionId: sharedTxnId }
36
- );
37
-
38
- log(`Pushing message with txn=${sharedTxnId} to ${partition2}`, 'test');
39
- await client.push(`${queue}/${partition2}`,
40
- { data: 'Message in Partition B', partition: partition2 },
41
- { transactionId: sharedTxnId }
42
- );
43
-
44
- await sleep(100);
45
-
46
- // Verify both messages exist in database
47
- const msgCountA = await getMessageCount(queue, partition1);
48
- const msgCountB = await getMessageCount(queue, partition2);
49
-
50
- if (msgCountA !== 1) {
51
- throw new Error(`Expected 1 message in ${partition1}, found ${msgCountA}`);
52
- }
53
- if (msgCountB !== 1) {
54
- throw new Error(`Expected 1 message in ${partition2}, found ${msgCountB}`);
55
- }
56
-
57
- log('✓ Both messages with same transaction_id exist in different partitions', 'success');
58
-
59
- // Take message from partition A
60
- log(`Taking message from ${partition1}`, 'test');
61
- let messageA = null;
62
- for await (const msg of client.take(`${queue}/${partition1}`, { limit: 1 })) {
63
- messageA = msg;
64
- break;
65
- }
66
-
67
- if (!messageA) {
68
- throw new Error('Failed to take message from partition A');
69
- }
70
-
71
- if (messageA.transactionId !== sharedTxnId) {
72
- throw new Error(`Expected transaction_id ${sharedTxnId}, got ${messageA.transactionId}`);
73
- }
74
-
75
- if (messageA.data.partition !== partition1) {
76
- throw new Error(`Expected partition ${partition1}, got ${messageA.data.partition}`);
77
- }
78
-
79
- log(`✓ Received correct message from ${partition1}: ${JSON.stringify(messageA.data)}`, 'success');
80
-
81
- // ACK the message from partition A
82
- log(`ACKing message in ${partition1} with txn=${sharedTxnId}`, 'test');
83
- await client.ack(messageA, true);
84
-
85
- await sleep(200);
86
-
87
- // CRITICAL CHECK: Verify message in partition A is acknowledged
88
- const result = await dbPool.query(`
89
- SELECT
90
- m.id,
91
- m.transaction_id,
92
- p.name as partition_name,
93
- pc.last_consumed_id,
94
- CASE
95
- WHEN m.id <= pc.last_consumed_id AND DATE_TRUNC('milliseconds', m.created_at) <= DATE_TRUNC('milliseconds', pc.last_consumed_created_at)
96
- THEN 'completed'
97
- ELSE 'pending'
98
- END as status
99
- FROM queen.messages m
100
- JOIN queen.partitions p ON p.id = m.partition_id
101
- JOIN queen.queues q ON q.id = p.queue_id
102
- LEFT JOIN queen.partition_consumers pc ON pc.partition_id = p.id AND pc.consumer_group = '__QUEUE_MODE__'
103
- WHERE q.name = $1 AND m.transaction_id = $2
104
- ORDER BY p.name
105
- `, [queue, sharedTxnId]);
106
-
107
- if (result.rows.length !== 2) {
108
- throw new Error(`Expected 2 messages in database, found ${result.rows.length}`);
109
- }
110
-
111
- const msgA = result.rows.find(r => r.partition_name === partition1);
112
- const msgB = result.rows.find(r => r.partition_name === partition2);
113
-
114
- if (!msgA || !msgB) {
115
- throw new Error('Could not find both messages in query result');
116
- }
117
-
118
- log(`Message A (${partition1}): status=${msgA.status}`, 'test');
119
- log(`Message B (${partition2}): status=${msgB.status}`, 'test');
120
-
121
- // CRITICAL ASSERTION: Message in partition A should be completed
122
- if (msgA.status !== 'completed') {
123
- throw new Error(`Message in ${partition1} should be completed, but status is: ${msgA.status}`);
124
- }
125
-
126
- // CRITICAL ASSERTION: Message in partition B should still be pending
127
- if (msgB.status !== 'pending') {
128
- throw new Error(`Message in ${partition2} should still be pending, but status is: ${msgB.status}`);
129
- }
130
-
131
- log(`✓ ACK correctly targeted only ${partition1}, ${partition2} remains pending`, 'success');
132
-
133
- // Now take and ACK message from partition B
134
- log(`Taking message from ${partition2}`, 'test');
135
- let messageB = null;
136
- for await (const msg of client.take(`${queue}/${partition2}`, { limit: 1 })) {
137
- messageB = msg;
138
- break;
139
- }
140
-
141
- if (!messageB) {
142
- throw new Error('Failed to take message from partition B');
143
- }
144
-
145
- if (messageB.transactionId !== sharedTxnId) {
146
- throw new Error(`Expected transaction_id ${sharedTxnId}, got ${messageB.transactionId}`);
147
- }
148
-
149
- if (messageB.data.partition !== partition2) {
150
- throw new Error(`Expected partition ${partition2}, got ${messageB.data.partition}`);
151
- }
152
-
153
- log(`✓ Received correct message from ${partition2}: ${JSON.stringify(messageB.data)}`, 'success');
154
-
155
- // ACK the message from partition B
156
- log(`ACKing message in ${partition2} with txn=${sharedTxnId}`, 'test');
157
- await client.ack(messageB, true);
158
-
159
- await sleep(200);
160
-
161
- // Verify both messages are now completed
162
- const finalResult = await dbPool.query(`
163
- SELECT
164
- p.name as partition_name,
165
- CASE
166
- WHEN m.id <= pc.last_consumed_id AND DATE_TRUNC('milliseconds', m.created_at) <= DATE_TRUNC('milliseconds', pc.last_consumed_created_at)
167
- THEN 'completed'
168
- ELSE 'pending'
169
- END as status
170
- FROM queen.messages m
171
- JOIN queen.partitions p ON p.id = m.partition_id
172
- JOIN queen.queues q ON q.id = p.queue_id
173
- LEFT JOIN queen.partition_consumers pc ON pc.partition_id = p.id AND pc.consumer_group = '__QUEUE_MODE__'
174
- WHERE q.name = $1 AND m.transaction_id = $2
175
- ORDER BY p.name
176
- `, [queue, sharedTxnId]);
177
-
178
- const finalMsgA = finalResult.rows.find(r => r.partition_name === partition1);
179
- const finalMsgB = finalResult.rows.find(r => r.partition_name === partition2);
180
-
181
- if (finalMsgA.status !== 'completed' || finalMsgB.status !== 'completed') {
182
- throw new Error('Both messages should be completed after individual ACKs');
183
- }
184
-
185
- log('✓ Both messages correctly ACKed independently', 'success');
186
-
187
- passTest('Partition-scoped transaction_id ACK targeting works correctly');
188
- } catch (error) {
189
- failTest(error);
190
- }
191
- }
192
-
193
- /**
194
- * Test: Batch ACK with duplicate transaction IDs across partitions
195
- *
196
- * Tests that batch acknowledgment correctly scopes to partition_id
197
- */
198
- export async function testBatchAckWithDuplicateTransactionIds(client) {
199
- startTest('Batch ACK with Duplicate Transaction IDs', 'edge');
200
-
201
- try {
202
- const queue = 'test-batch-ack-duplicate';
203
- const partition1 = 'partition-X';
204
- const partition2 = 'partition-Y';
205
-
206
- await client.queue(queue, { leaseTime: 30 });
207
-
208
- // Push 3 messages to partition1 with specific transaction IDs
209
- const txnIds = ['batch-1', 'batch-2', 'batch-3'];
210
-
211
- for (const txnId of txnIds) {
212
- await client.push(`${queue}/${partition1}`,
213
- { data: `P1-${txnId}` },
214
- { transactionId: txnId }
215
- );
216
-
217
- // Push same transaction_id to partition2
218
- await client.push(`${queue}/${partition2}`,
219
- { data: `P2-${txnId}` },
220
- { transactionId: txnId }
221
- );
222
- }
223
-
224
- await sleep(500); // Give more time for messages to be ready
225
-
226
- // Take all messages from partition1 (batch size 10 to get all 3)
227
- const messagesP1 = [];
228
- for await (const msg of client.take(`${queue}/${partition1}`, { batch: 10, limit: 10 })) {
229
- messagesP1.push(msg);
230
- if (messagesP1.length >= 3) break;
231
- }
232
-
233
- if (messagesP1.length !== 3) {
234
- log(`DEBUG: Only got ${messagesP1.length} messages. Transaction IDs: ${messagesP1.map(m => m.transactionId).join(', ')}`, 'warning');
235
-
236
- // Check database to see what's there
237
- const dbCheck = await dbPool.query(`
238
- SELECT m.transaction_id, p.name as partition_name, m.created_at
239
- FROM queen.messages m
240
- JOIN queen.partitions p ON p.id = m.partition_id
241
- JOIN queen.queues q ON q.id = p.queue_id
242
- WHERE q.name = $1
243
- ORDER BY p.name, m.created_at
244
- `, [queue]);
245
-
246
- log(`DEBUG: Database has ${dbCheck.rows.length} messages: ${JSON.stringify(dbCheck.rows)}`, 'warning');
247
- throw new Error(`Expected 3 messages from partition1, got ${messagesP1.length}`);
248
- }
249
-
250
- log(`Received ${messagesP1.length} messages from ${partition1}`, 'test');
251
-
252
- // Batch ACK all messages from partition1
253
- for (const msg of messagesP1) {
254
- await client.ack(msg, true);
255
- }
256
-
257
- await sleep(200);
258
-
259
- // Verify partition1 messages are completed but partition2 messages are still pending
260
- for (const txnId of txnIds) {
261
- const result = await dbPool.query(`
262
- SELECT
263
- p.name as partition_name,
264
- CASE
265
- WHEN m.id <= pc.last_consumed_id AND DATE_TRUNC('milliseconds', m.created_at) <= DATE_TRUNC('milliseconds', pc.last_consumed_created_at)
266
- THEN 'completed'
267
- ELSE 'pending'
268
- END as status
269
- FROM queen.messages m
270
- JOIN queen.partitions p ON p.id = m.partition_id
271
- JOIN queen.queues q ON q.id = p.queue_id
272
- LEFT JOIN queen.partition_consumers pc ON pc.partition_id = p.id AND pc.consumer_group = '__QUEUE_MODE__'
273
- WHERE q.name = $1 AND m.transaction_id = $2
274
- ORDER BY p.name
275
- `, [queue, txnId]);
276
-
277
- const p1Status = result.rows.find(r => r.partition_name === partition1)?.status;
278
- const p2Status = result.rows.find(r => r.partition_name === partition2)?.status;
279
-
280
- if (p1Status !== 'completed') {
281
- throw new Error(`${partition1} message with txn=${txnId} should be completed, got ${p1Status}`);
282
- }
283
-
284
- if (p2Status !== 'pending') {
285
- throw new Error(`${partition2} message with txn=${txnId} should be pending, got ${p2Status}`);
286
- }
287
- }
288
-
289
- log(`✓ Batch ACK in ${partition1} did not affect ${partition2}`, 'success');
290
-
291
- passTest('Batch ACK correctly scopes by partition_id');
292
- } catch (error) {
293
- failTest(error);
294
- }
295
- }
296
-
297
- /**
298
- * Test: DLQ with duplicate transaction IDs
299
- *
300
- * Tests that moving messages to DLQ correctly scopes by partition_id
301
- */
302
- export async function testDLQWithDuplicateTransactionIds(client) {
303
- startTest('DLQ with Duplicate Transaction IDs', 'edge');
304
-
305
- try {
306
- const queue = 'test-dlq-duplicate';
307
- const partition1 = 'partition-DLQ-A';
308
- const partition2 = 'partition-DLQ-B';
309
- const txnId = 'dlq-shared-txn';
310
-
311
- await client.queue(queue, { retryLimit: 0 }); // Immediate DLQ
312
-
313
- // Push same transaction_id to both partitions
314
- await client.push(`${queue}/${partition1}`, { data: 'P1-data' }, { transactionId: txnId });
315
- await client.push(`${queue}/${partition2}`, { data: 'P2-data' }, { transactionId: txnId });
316
-
317
- await sleep(100);
318
-
319
- // Take from partition1 and fail it
320
- let msgP1 = null;
321
- for await (const msg of client.take(`${queue}/${partition1}`, { limit: 1 })) {
322
- msgP1 = msg;
323
- break;
324
- }
325
-
326
- if (!msgP1) {
327
- throw new Error('Failed to take message from partition1');
328
- }
329
-
330
- // ACK as failed - should go to DLQ
331
- await client.ack(msgP1, false, { error: 'Test error for P1' });
332
-
333
- await sleep(200);
334
-
335
- // Check DLQ - only partition1 message should be there
336
- const dlqResult = await dbPool.query(`
337
- SELECT
338
- p.name as partition_name,
339
- dlq.error_message
340
- FROM queen.dead_letter_queue dlq
341
- JOIN queen.messages m ON dlq.message_id = m.id
342
- JOIN queen.partitions p ON p.id = m.partition_id
343
- JOIN queen.queues q ON q.id = p.queue_id
344
- WHERE q.name = $1 AND m.transaction_id = $2
345
- `, [queue, txnId]);
346
-
347
- if (dlqResult.rows.length !== 1) {
348
- throw new Error(`Expected 1 message in DLQ, found ${dlqResult.rows.length}`);
349
- }
350
-
351
- if (dlqResult.rows[0].partition_name !== partition1) {
352
- throw new Error(`Expected DLQ message from ${partition1}, got ${dlqResult.rows[0].partition_name}`);
353
- }
354
-
355
- if (!dlqResult.rows[0].error_message.includes('Test error for P1')) {
356
- throw new Error(`Unexpected error message: ${dlqResult.rows[0].error_message}`);
357
- }
358
-
359
- log(`✓ DLQ correctly scoped to ${partition1}`, 'success');
360
-
361
- // Verify partition2 message is still pending
362
- let msgP2 = null;
363
- for await (const msg of client.take(`${queue}/${partition2}`, { limit: 1 })) {
364
- msgP2 = msg;
365
- break;
366
- }
367
-
368
- if (!msgP2) {
369
- throw new Error('Message in partition2 should still be available');
370
- }
371
-
372
- if (msgP2.transactionId !== txnId) {
373
- throw new Error(`Expected transaction_id ${txnId} in P2, got ${msgP2.transactionId}`);
374
- }
375
-
376
- log(`✓ ${partition2} message with same txn_id is still available`, 'success');
377
-
378
- passTest('DLQ correctly scopes by partition_id');
379
- } catch (error) {
380
- failTest(error);
381
- }
382
- }
383
-
384
- /**
385
- * Test: Analytics API with duplicate transaction IDs
386
- *
387
- * Tests that the GET /api/v1/messages/:partitionId/:transactionId endpoint
388
- * returns the correct message when the same transaction_id exists in multiple partitions
389
- */
390
- export async function testAnalyticsAPIWithDuplicateTransactionIds(client) {
391
- startTest('Analytics API with Duplicate Transaction IDs', 'edge');
392
-
393
- try {
394
- const queue = 'test-analytics-duplicate';
395
- const partition1 = 'analytics-P1';
396
- const partition2 = 'analytics-P2';
397
- const txnId = 'analytics-shared-txn';
398
-
399
- await client.queue(queue, {});
400
-
401
- // Push messages with same transaction_id to both partitions
402
- await client.push(`${queue}/${partition1}`,
403
- { unique: 'data-from-P1', timestamp: Date.now() },
404
- { transactionId: txnId }
405
- );
406
-
407
- await client.push(`${queue}/${partition2}`,
408
- { unique: 'data-from-P2', timestamp: Date.now() + 1000 },
409
- { transactionId: txnId }
410
- );
411
-
412
- await sleep(100);
413
-
414
- // Get partition IDs from database
415
- const partitionResult = await dbPool.query(`
416
- SELECT p.id, p.name
417
- FROM queen.partitions p
418
- JOIN queen.queues q ON q.id = p.queue_id
419
- WHERE q.name = $1
420
- ORDER BY p.name
421
- `, [queue]);
422
-
423
- const p1Id = partitionResult.rows.find(r => r.name === partition1)?.id;
424
- const p2Id = partitionResult.rows.find(r => r.name === partition2)?.id;
425
-
426
- if (!p1Id || !p2Id) {
427
- throw new Error('Failed to get partition IDs');
428
- }
429
-
430
- log(`Partition IDs: ${partition1}=${p1Id}, ${partition2}=${p2Id}`, 'test');
431
-
432
- // Fetch message using Analytics API (via HTTP)
433
- const baseUrl = TEST_CONFIG.baseUrls[0];
434
-
435
- // Fetch from partition1
436
- const response1 = await fetch(`${baseUrl}/api/v1/messages/${p1Id}/${txnId}`);
437
- if (!response1.ok) {
438
- throw new Error(`Failed to fetch message from partition1: ${response1.statusText}`);
439
- }
440
- const data1 = await response1.json();
441
-
442
- if (data1.transactionId !== txnId) {
443
- throw new Error(`Expected transactionId ${txnId}, got ${data1.transactionId}`);
444
- }
445
-
446
- if (data1.partition !== partition1) {
447
- throw new Error(`Expected partition ${partition1}, got ${data1.partition}`);
448
- }
449
-
450
- if (data1.payload.unique !== 'data-from-P1') {
451
- throw new Error(`Expected data from P1, got: ${JSON.stringify(data1.payload)}`);
452
- }
453
-
454
- log(`✓ Analytics API correctly returned message from ${partition1}`, 'success');
455
-
456
- // Fetch from partition2
457
- const response2 = await fetch(`${baseUrl}/api/v1/messages/${p2Id}/${txnId}`);
458
- if (!response2.ok) {
459
- throw new Error(`Failed to fetch message from partition2: ${response2.statusText}`);
460
- }
461
- const data2 = await response2.json();
462
-
463
- if (data2.transactionId !== txnId) {
464
- throw new Error(`Expected transactionId ${txnId}, got ${data2.transactionId}`);
465
- }
466
-
467
- if (data2.partition !== partition2) {
468
- throw new Error(`Expected partition ${partition2}, got ${data2.partition}`);
469
- }
470
-
471
- if (data2.payload.unique !== 'data-from-P2') {
472
- throw new Error(`Expected data from P2, got: ${JSON.stringify(data2.payload)}`);
473
- }
474
-
475
- log(`✓ Analytics API correctly returned message from ${partition2}`, 'success');
476
-
477
- passTest('Analytics API correctly distinguishes messages by partition_id');
478
- } catch (error) {
479
- failTest(error);
480
- }
481
- }
482
-