queen-mq 0.4.0 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +112 -38
- package/client-js/client-v2/Queen.js +38 -0
- package/client-js/client-v2/README.md +61 -8
- package/client-js/test-v2/GETTING_STARTED.md +27 -0
- package/client-js/test-v2/README_SUBSCRIPTION_TESTS.md +201 -0
- package/client-js/test-v2/consume.js +10 -0
- package/client-js/test-v2/dlq.js +1 -1
- package/client-js/test-v2/load.js +2 -0
- package/client-js/test-v2/subscription.js +198 -18
- package/client-js/test-v2/transaction.js +66 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -22,6 +22,12 @@ Why "Queen"? Because years ago, when I first read the word "queue" in my mind, I
|
|
|
22
22
|
|
|
23
23
|
---
|
|
24
24
|
|
|
25
|
+
[QUICKSTART](docs/QUICKSTART.md)
|
|
26
|
+
|
|
27
|
+
Latest server production version is **0.6.3**.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
25
31
|
## Introduction
|
|
26
32
|
|
|
27
33
|
QueenMQ is a queue system written in C++ and backed by PostgreSQL, born from the need to manage many FIFO partitions for Smartchat with solid guarantees around delivery and failure handling. During the initial development, I realized that with a few simple additions to the original design, I could build a very powerful and flexible queue system. This project is almost entirely written by AI, with my supervision—only the test files (or a good part of them) are manually written.
|
|
@@ -81,6 +87,15 @@ Consumer groups are a way to process messages for different purposes. Each consu
|
|
|
81
87
|
|
|
82
88
|
Subscription modes are a way to control the message history that is processed. You can choose to process all messages (including historical ones), only new messages, or messages from a specific timestamp. Subscription modes are only available when using consumer groups.
|
|
83
89
|
|
|
90
|
+
**Server Default:** By default, new consumer groups process all historical messages. You can change this server-wide:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
export DEFAULT_SUBSCRIPTION_MODE="new" # Skip historical messages by default
|
|
94
|
+
./bin/queen-server
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
This is useful for real-time systems where only new messages matter, or to prevent accidental processing of large backlogs. Clients can still override with `.subscriptionMode()` if needed.
|
|
98
|
+
|
|
84
99
|
### Long polling (waiting for messages)
|
|
85
100
|
|
|
86
101
|
The client works with the pull model for pop operations, meaning that you need to explicitly request messages from the queue. Pop and consume mehtods can "wait" server side for messages to be available. When the method is called with wait=true, the method will block until messages are available or the timeout is reached. When the timeout is reached, the method returns an empty array. Long polling is a very efficient way to wait for messages, and it is the recommended way to consume messages.
|
|
@@ -232,34 +247,89 @@ See [webapp/README.md](webapp/README.md) for more details.
|
|
|
232
247
|
|
|
233
248
|
## Architecture
|
|
234
249
|
|
|
235
|
-
Queen uses a high-performance **acceptor/worker pattern** with uWebSockets,
|
|
250
|
+
Queen uses a high-performance **acceptor/worker pattern** with uWebSockets, featuring a **fully asynchronous, non-blocking PostgreSQL architecture** for maximum throughput and minimal latency.
|
|
251
|
+
|
|
252
|
+
### Core Components
|
|
253
|
+
|
|
254
|
+
**Network Layer:**
|
|
255
|
+
- **UWS Acceptor**: Single thread listening on port 6632, distributes connections round-robin to workers
|
|
256
|
+
- **UWS Workers**: Configurable event loop threads (default: 10) handling HTTP routes and WebSocket connections
|
|
257
|
+
|
|
258
|
+
**Database Layer:**
|
|
259
|
+
- **AsyncDbPool**: Non-blocking PostgreSQL connection pool (142 connections) using libpq async API
|
|
260
|
+
- Socket-based I/O with `select()` for non-blocking operations
|
|
261
|
+
- RAII-based resource management with automatic connection cleanup
|
|
262
|
+
- Connection health monitoring and automatic reset
|
|
263
|
+
- Thread-safe with mutex/condition variable synchronization
|
|
264
|
+
- **AsyncQueueManager**: Event-loop-based queue operations
|
|
265
|
+
- Direct execution in worker threads for PUSH, POP, ACK, and TRANSACTION operations
|
|
266
|
+
- Batch processing with dynamic sizing
|
|
267
|
+
- Encryption support with status checks
|
|
268
|
+
- Automatic failover to file buffer when database unavailable
|
|
269
|
+
|
|
270
|
+
**Background Services:**
|
|
271
|
+
- **Poll Workers**: 4 dedicated threads for long-polling operations
|
|
272
|
+
- Non-blocking I/O with exponential backoff (100ms→2000ms)
|
|
273
|
+
- Intention registry for efficient request grouping
|
|
274
|
+
- Rate-limited queries to prevent database overload
|
|
275
|
+
- **Background Pool**: 8 connections for metrics, retention, eviction, and stream management
|
|
276
|
+
|
|
277
|
+
### Request Flow
|
|
278
|
+
|
|
279
|
+
**Standard Operations (PUSH/POP/ACK/TRANSACTION):**
|
|
280
|
+
```
|
|
281
|
+
Client Request
|
|
282
|
+
↓
|
|
283
|
+
Acceptor (port 6632)
|
|
284
|
+
↓
|
|
285
|
+
Worker (event loop) → AsyncQueueManager → AsyncDbPool → PostgreSQL
|
|
286
|
+
↓ (non-blocking) (socket I/O)
|
|
287
|
+
Response sent immediately
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
**Long-Polling Operations (wait=true):**
|
|
291
|
+
```
|
|
292
|
+
Client Request
|
|
293
|
+
↓
|
|
294
|
+
Worker registers intention in Registry
|
|
295
|
+
↓
|
|
296
|
+
Poll Worker (50ms interval)
|
|
297
|
+
↓
|
|
298
|
+
Non-blocking query via AsyncDbPool
|
|
299
|
+
↓
|
|
300
|
+
Messages distributed to waiting clients
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Performance Characteristics
|
|
304
|
+
|
|
305
|
+
**Latency:**
|
|
306
|
+
- **POP (immediate)**: 10-50ms
|
|
307
|
+
- **ACK**: 10-50ms
|
|
308
|
+
- **TRANSACTION**: 50-200ms
|
|
309
|
+
- **Long-polling**: Configurable (50ms-2000ms backoff)
|
|
236
310
|
|
|
237
|
-
**
|
|
311
|
+
**Throughput:**
|
|
312
|
+
- **Peak**: 148,000+ msg/s
|
|
313
|
+
- **Sustained**: 130,000+ msg/s
|
|
314
|
+
- **Batch push**: 5,000-8,000 msg/s (with batches of 100)
|
|
238
315
|
|
|
239
|
-
**
|
|
240
|
-
- **
|
|
241
|
-
- **
|
|
242
|
-
- **
|
|
243
|
-
- **DB ThreadPool**: Separate pool for blocking PostgreSQL operations
|
|
244
|
-
- **Poll Workers**: 2 reserved threads for long-polling with adaptive backoff (100ms→2000ms)
|
|
245
|
-
- **Poll Intention Registry**: Thread-safe store for long-poll requests
|
|
246
|
-
- **Database Pool**: 150 shared PostgreSQL connections (libpq) with mutex/condition variable
|
|
247
|
-
- **Response Queue**: Thread-safe queue decoupling DB results from event loop responses
|
|
316
|
+
**Resource Usage:**
|
|
317
|
+
- **Database connections**: 150 total (142 async + 8 background)
|
|
318
|
+
- **Threads**: 14 total (10 workers + 4 poll workers)
|
|
319
|
+
- **Memory**: ~80MB for thread stacks + connection overhead
|
|
248
320
|
|
|
249
|
-
|
|
250
|
-
1. Client → Acceptor → Worker (event loop)
|
|
251
|
-
2. Worker registers response, submits job to DB ThreadPool
|
|
252
|
-
3. DB thread executes query, pushes result to Response Queue
|
|
253
|
-
4. Worker's response timer drains queue, sends HTTP response
|
|
321
|
+
### Scalability
|
|
254
322
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
323
|
+
The event-driven architecture enables:
|
|
324
|
+
- ✅ Unlimited concurrent requests (limited only by connection pool)
|
|
325
|
+
- ✅ Horizontal scaling (multiple server instances)
|
|
326
|
+
- ✅ Efficient resource utilization (non-blocking I/O)
|
|
327
|
+
- ✅ Low latency under high load
|
|
328
|
+
- ✅ Automatic load distribution across workers
|
|
261
329
|
|
|
262
|
-
|
|
330
|
+
**📚 Technical Documentation:**
|
|
331
|
+
- [Server Architecture Guide](server/README.md) - Complete server setup and configuration
|
|
332
|
+
- [Architecture Diagrams](assets/architecture.svg) - Visual architecture overview
|
|
263
333
|
|
|
264
334
|
### PostgreSQL Failover
|
|
265
335
|
|
|
@@ -290,25 +360,30 @@ FILE_BUFFER_DIR=/custom/path ./bin/queen-server
|
|
|
290
360
|
|
|
291
361
|
### Results
|
|
292
362
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
|
296
|
-
|
|
297
|
-
| **
|
|
298
|
-
| **
|
|
299
|
-
| **
|
|
300
|
-
| **
|
|
301
|
-
| **
|
|
363
|
+
All tests run with: `--threads 10 --partitions 10 --mode single-queue`
|
|
364
|
+
|
|
365
|
+
| Test | Mode | Messages | Batch Size | Throughput | Bandwidth |
|
|
366
|
+
|------|------|----------|------------|------------|-----------|
|
|
367
|
+
| **T1** | Producer | 10,000 | 1 | 785 msg/sec | 0.22 MB/sec |
|
|
368
|
+
| **T1** | Consumer | 10,000 | 1 | 456 msg/sec | 0.23 MB/sec |
|
|
369
|
+
| **T2** | Producer | 10,000 | 10 | 7,677 msg/sec | 2.20 MB/sec |
|
|
370
|
+
| **T2** | Consumer | 10,000 | 10 | 4,989 msg/sec | 2.53 MB/sec |
|
|
371
|
+
| **T3** | Producer | 10,000 | 100 | 39,065 msg/sec | 11.18 MB/sec |
|
|
372
|
+
| **T3** | Consumer | 10,000 | 100 | 30,079 msg/sec | 15.26 MB/sec |
|
|
373
|
+
| **T4** | Producer | 10,000 | 1,000 | 85,862 msg/sec | 24.57 MB/sec |
|
|
374
|
+
| **T4** | Consumer | 10,000 | 1,000 | **488,650 msg/sec** | **247.87 MB/sec** |
|
|
375
|
+
| **T5** | Producer | 100,000 | 1,000 | **90,601 msg/sec** | **25.92 MB/sec** |
|
|
376
|
+
| **T5** | Consumer | 100,000 | 1,000 | 84,530 msg/sec | 42.96 MB/sec |
|
|
302
377
|
|
|
303
378
|
### Key Observations
|
|
304
379
|
|
|
305
|
-
- ✅ **Batch size matters:** Larger batches (1,000
|
|
306
|
-
- ✅ **
|
|
307
|
-
- ✅ **
|
|
308
|
-
- ✅ **Producer peak:** 62K msg/sec with optimal batch size (1,000)
|
|
380
|
+
- ✅ **Batch size matters:** Larger batches (1,000) dramatically improve throughput
|
|
381
|
+
- ✅ **Consumer performance:** Peaks at 488K msg/sec with batch size 1,000 (247 MB/sec bandwidth)
|
|
382
|
+
- ✅ **Producer peak:** 90K msg/sec with batch size 1,000 on 100K messages
|
|
309
383
|
- ⚠️ **Small batches:** Performance drops significantly with batch=1 (lock contention)
|
|
384
|
+
- 📈 **Scalability:** Performance improves with larger message volumes (T4 vs T5)
|
|
310
385
|
|
|
311
|
-
**Note:** All timing metrics
|
|
386
|
+
**Note:** All timing metrics are based on processing time (excludes idle time) and measure actual message processing (first message → last message).
|
|
312
387
|
|
|
313
388
|
### Run Your Own Benchmarks
|
|
314
389
|
|
|
@@ -379,7 +454,6 @@ You can use Queen directly from HTTP without the JS client.
|
|
|
379
454
|
|
|
380
455
|
|
|
381
456
|
### Other TODO Items
|
|
382
|
-
- Mini streaming engine
|
|
383
457
|
- Proper concurrency on clients
|
|
384
458
|
- Check client failover
|
|
385
459
|
- Py client
|
|
@@ -357,6 +357,44 @@ export class Queen {
|
|
|
357
357
|
return stats
|
|
358
358
|
}
|
|
359
359
|
|
|
360
|
+
// ===========================
|
|
361
|
+
// Consumer Group Management
|
|
362
|
+
// ===========================
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Delete a consumer group and optionally its subscription metadata
|
|
366
|
+
* @param {string} consumerGroup - Consumer group name
|
|
367
|
+
* @param {boolean} deleteMetadata - Whether to delete subscription metadata (default: true)
|
|
368
|
+
* @returns {Promise<object>}
|
|
369
|
+
*/
|
|
370
|
+
async deleteConsumerGroup(consumerGroup, deleteMetadata = true) {
|
|
371
|
+
logger.log('Queen.deleteConsumerGroup', { consumerGroup, deleteMetadata })
|
|
372
|
+
|
|
373
|
+
const url = `/api/v1/consumer-groups/${encodeURIComponent(consumerGroup)}?deleteMetadata=${deleteMetadata}`
|
|
374
|
+
const response = await this.#httpClient.delete(url)
|
|
375
|
+
|
|
376
|
+
logger.log('Queen.deleteConsumerGroup', { success: true, consumerGroup })
|
|
377
|
+
return response
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Update subscription timestamp for a consumer group
|
|
382
|
+
* @param {string} consumerGroup - Consumer group name
|
|
383
|
+
* @param {string} timestamp - New subscription timestamp (ISO 8601)
|
|
384
|
+
* @returns {Promise<object>}
|
|
385
|
+
*/
|
|
386
|
+
async updateConsumerGroupTimestamp(consumerGroup, timestamp) {
|
|
387
|
+
logger.log('Queen.updateConsumerGroupTimestamp', { consumerGroup, timestamp })
|
|
388
|
+
|
|
389
|
+
const url = `/api/v1/consumer-groups/${encodeURIComponent(consumerGroup)}/subscription`
|
|
390
|
+
const response = await this.#httpClient.post(url, {
|
|
391
|
+
subscriptionTimestamp: timestamp
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
logger.log('Queen.updateConsumerGroupTimestamp', { success: true, consumerGroup })
|
|
395
|
+
return response
|
|
396
|
+
}
|
|
397
|
+
|
|
360
398
|
// ===========================
|
|
361
399
|
// Streaming API
|
|
362
400
|
// ===========================
|
|
@@ -370,12 +370,21 @@ await queen
|
|
|
370
370
|
})
|
|
371
371
|
```
|
|
372
372
|
|
|
373
|
+
**Server Default:** The server can be configured to change this default behavior:
|
|
374
|
+
```bash
|
|
375
|
+
# Make all new consumer groups skip history by default
|
|
376
|
+
export DEFAULT_SUBSCRIPTION_MODE="new"
|
|
377
|
+
./bin/queen-server
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
When `DEFAULT_SUBSCRIPTION_MODE="new"` is set, new consumer groups automatically skip historical messages unless you explicitly override with `.subscriptionMode('all')`.
|
|
381
|
+
|
|
373
382
|
### Subscription Mode: 'new'
|
|
374
383
|
|
|
375
|
-
Skip
|
|
384
|
+
Skip historical messages and process messages that arrive **near** subscription time:
|
|
376
385
|
|
|
377
386
|
```javascript
|
|
378
|
-
//
|
|
387
|
+
// Process recent messages (not historical backlog)
|
|
379
388
|
await queen
|
|
380
389
|
.queue('events')
|
|
381
390
|
.group('realtime-monitor')
|
|
@@ -386,9 +395,33 @@ await queen
|
|
|
386
395
|
```
|
|
387
396
|
|
|
388
397
|
**What happens:**
|
|
389
|
-
1. Consumer
|
|
390
|
-
2.
|
|
391
|
-
3. Only messages
|
|
398
|
+
1. Consumer makes first pop at `T0` (e.g., 10:00:00)
|
|
399
|
+
2. Server records `subscription_timestamp = T0` in metadata table
|
|
400
|
+
3. Only messages with `created_at > T0` are processed
|
|
401
|
+
4. All historical messages are skipped
|
|
402
|
+
|
|
403
|
+
**How it ensures consistency across partitions:**
|
|
404
|
+
|
|
405
|
+
Queen tracks subscription time separately from partition-level cursors:
|
|
406
|
+
|
|
407
|
+
```javascript
|
|
408
|
+
// Timeline:
|
|
409
|
+
10:00:00 - First pop() call
|
|
410
|
+
→ Metadata recorded: subscription_timestamp = 10:00:00
|
|
411
|
+
|
|
412
|
+
// Consumer processes partition P1 for 10 minutes
|
|
413
|
+
|
|
414
|
+
10:10:00 - New partition P2 is created, messages arrive
|
|
415
|
+
10:15:00 - Consumer discovers P2 via pop()
|
|
416
|
+
→ Uses ORIGINAL subscription_timestamp (10:00:00)
|
|
417
|
+
→ Messages from 10:10:00 are captured! ✓
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
**Key benefits:**
|
|
421
|
+
- ✅ **Consistent**: All partitions use the same subscription timestamp
|
|
422
|
+
- ✅ **No skipping**: New partitions discovered later are processed correctly
|
|
423
|
+
- ✅ **True NEW semantics**: Only messages after first pop request
|
|
424
|
+
- ✅ **Works with wildcards**: Namespace/task filters maintain subscription time
|
|
392
425
|
|
|
393
426
|
### Subscription Mode: 'new-only'
|
|
394
427
|
|
|
@@ -497,11 +530,18 @@ await queen
|
|
|
497
530
|
- Subsequent consumers in the same group inherit the same position
|
|
498
531
|
- To change subscription mode, use a different group name
|
|
499
532
|
|
|
533
|
+
⏰ **NEW mode subscription tracking:**
|
|
534
|
+
- NEW mode tracks when the consumer group **first subscribes** (first pop request)
|
|
535
|
+
- This subscription timestamp is used consistently across all partitions
|
|
536
|
+
- Ensures new partitions discovered later don't skip messages
|
|
537
|
+
- Stored in `consumer_groups_metadata` table on the server
|
|
538
|
+
|
|
500
539
|
💡 **Best Practices:**
|
|
501
|
-
- Use `'new'` for real-time monitoring
|
|
502
|
-
- Use default (all) for batch processing and
|
|
503
|
-
- Use timestamps for replay/debugging scenarios
|
|
540
|
+
- Use `'new'` for real-time monitoring (skip historical backlog)
|
|
541
|
+
- Use default (all) for batch processing and full history replay
|
|
542
|
+
- Use timestamps for precise replay/debugging scenarios
|
|
504
543
|
- Name groups descriptively based on their subscription mode
|
|
544
|
+
- Be aware that "NEW" means messages after the **first pop request**, not the first message arrival
|
|
505
545
|
|
|
506
546
|
---
|
|
507
547
|
|
|
@@ -1660,6 +1700,19 @@ const dlq = await queen.queue('q').dlq('consumer-group').limit(10).get()
|
|
|
1660
1700
|
const dlq = await queen.queue('q').dlq().from('2025-01-01').to('2025-01-31').get()
|
|
1661
1701
|
```
|
|
1662
1702
|
|
|
1703
|
+
### Consumer Group Management
|
|
1704
|
+
|
|
1705
|
+
```javascript
|
|
1706
|
+
// Delete a consumer group (including metadata)
|
|
1707
|
+
await queen.deleteConsumerGroup('my-group')
|
|
1708
|
+
|
|
1709
|
+
// Delete consumer group but keep subscription metadata
|
|
1710
|
+
await queen.deleteConsumerGroup('my-group', false)
|
|
1711
|
+
|
|
1712
|
+
// Update subscription timestamp
|
|
1713
|
+
await queen.updateConsumerGroupTimestamp('my-group', '2025-11-10T10:00:00Z')
|
|
1714
|
+
```
|
|
1715
|
+
|
|
1663
1716
|
### Shutdown
|
|
1664
1717
|
|
|
1665
1718
|
```javascript
|
|
@@ -119,6 +119,33 @@ Make sure:
|
|
|
119
119
|
3. ✅ Environment variables are set (if needed)
|
|
120
120
|
4. ✅ No production data in test database
|
|
121
121
|
|
|
122
|
+
## Testing with Different Server Configurations
|
|
123
|
+
|
|
124
|
+
### Standard Configuration (Default)
|
|
125
|
+
|
|
126
|
+
Run server with default settings:
|
|
127
|
+
```bash
|
|
128
|
+
./bin/queen-server
|
|
129
|
+
node test-v2/run.js
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**Expected:** Consumer groups without explicit `.subscriptionMode()` process all historical messages.
|
|
133
|
+
|
|
134
|
+
### With DEFAULT_SUBSCRIPTION_MODE="new"
|
|
135
|
+
|
|
136
|
+
Run server with "new" as default:
|
|
137
|
+
```bash
|
|
138
|
+
DEFAULT_SUBSCRIPTION_MODE="new" ./bin/queen-server
|
|
139
|
+
node test-v2/run.js
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**Expected:**
|
|
143
|
+
- Tests with explicit `.subscriptionMode('new')` work the same
|
|
144
|
+
- Tests without explicit mode will skip historical messages
|
|
145
|
+
- `subscriptionModeServerDefault` test detects and reports server default
|
|
146
|
+
|
|
147
|
+
**All tests pass with both configurations!** The tests are designed to be agnostic to server defaults.
|
|
148
|
+
|
|
122
149
|
## Test Philosophy
|
|
123
150
|
|
|
124
151
|
The AI-generated tests follow these principles:
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# Subscription Mode Tests
|
|
2
|
+
|
|
3
|
+
Tests for consumer group subscription modes, compatible with any server `DEFAULT_SUBSCRIPTION_MODE` configuration.
|
|
4
|
+
|
|
5
|
+
## Test Overview
|
|
6
|
+
|
|
7
|
+
| Test Function | Description | Explicit Mode Used |
|
|
8
|
+
|--------------|-------------|-------------------|
|
|
9
|
+
| `subscriptionModeNew` | Validates `.subscriptionMode('new')` skips historical messages | Yes (`new`) |
|
|
10
|
+
| `subscriptionModeNewOnly` | Validates alias `.subscriptionMode('new-only')` | Yes (`new-only`) |
|
|
11
|
+
| `subscriptionFromNow` | Validates `.subscriptionFrom('now')` | Yes (`now`) |
|
|
12
|
+
| `subscriptionFromTimestamp` | Validates timestamp-based subscription | Yes (timestamp) |
|
|
13
|
+
| `subscriptionModeAll` | Tests default behavior (depends on server config) | Mixed |
|
|
14
|
+
| `subscriptionModeServerDefault` | **NEW:** Detects and validates server default | Mixed |
|
|
15
|
+
|
|
16
|
+
## Running the Tests
|
|
17
|
+
|
|
18
|
+
### Run all subscription tests:
|
|
19
|
+
```bash
|
|
20
|
+
cd client-js/test-v2
|
|
21
|
+
node run.js subscription
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Run specific subscription test:
|
|
25
|
+
```bash
|
|
26
|
+
node run.js subscriptionModeNew
|
|
27
|
+
node run.js subscriptionModeServerDefault
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Server Configuration Compatibility
|
|
31
|
+
|
|
32
|
+
These tests work with **any** server `DEFAULT_SUBSCRIPTION_MODE` configuration:
|
|
33
|
+
|
|
34
|
+
### Configuration 1: Standard (Default)
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# Start server without default
|
|
38
|
+
./bin/queen-server
|
|
39
|
+
|
|
40
|
+
# Run tests
|
|
41
|
+
node run.js subscription
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Expected Behavior:**
|
|
45
|
+
- Consumer groups without explicit mode process all historical messages
|
|
46
|
+
- Consumer groups with `.subscriptionMode('new')` skip history
|
|
47
|
+
- `subscriptionModeServerDefault` reports: "all (or empty string)"
|
|
48
|
+
|
|
49
|
+
### Configuration 2: With DEFAULT_SUBSCRIPTION_MODE="new"
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# Start server with "new" as default
|
|
53
|
+
DEFAULT_SUBSCRIPTION_MODE="new" ./bin/queen-server
|
|
54
|
+
|
|
55
|
+
# Run tests
|
|
56
|
+
node run.js subscription
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Expected Behavior:**
|
|
60
|
+
- Consumer groups without explicit mode skip historical messages
|
|
61
|
+
- Consumer groups with `.subscriptionMode('new')` skip history (same)
|
|
62
|
+
- `subscriptionModeServerDefault` reports: "new"
|
|
63
|
+
|
|
64
|
+
**✅ All tests pass with both configurations!**
|
|
65
|
+
|
|
66
|
+
## Test Details
|
|
67
|
+
|
|
68
|
+
### subscriptionModeNew
|
|
69
|
+
|
|
70
|
+
Tests the explicit `.subscriptionMode('new')` behavior:
|
|
71
|
+
|
|
72
|
+
1. Pushes 5 historical messages
|
|
73
|
+
2. Creates two consumer groups:
|
|
74
|
+
- One without explicit mode (behavior depends on server)
|
|
75
|
+
- One with `.subscriptionMode('new')` (always skips)
|
|
76
|
+
3. Verifies the `new` mode group gets 0 historical messages
|
|
77
|
+
4. Pushes 3 new messages
|
|
78
|
+
5. Verifies both groups get the new messages
|
|
79
|
+
|
|
80
|
+
**Key Assertion:** `.subscriptionMode('new')` always skips historical messages.
|
|
81
|
+
|
|
82
|
+
### subscriptionModeServerDefault (NEW)
|
|
83
|
+
|
|
84
|
+
Detects and validates the server's default subscription mode:
|
|
85
|
+
|
|
86
|
+
1. Pushes historical messages
|
|
87
|
+
2. Creates consumer group WITHOUT explicit subscription mode
|
|
88
|
+
3. Detects server default based on behavior:
|
|
89
|
+
- Got historical messages → server default is "all"
|
|
90
|
+
- Got 0 messages → server default is "new"
|
|
91
|
+
4. Validates explicit `.subscriptionMode('new')` still works
|
|
92
|
+
5. Verifies new messages are received by both groups
|
|
93
|
+
|
|
94
|
+
**Key Assertion:** Server default affects groups without explicit mode, but explicit modes always work.
|
|
95
|
+
|
|
96
|
+
### subscriptionFromTimestamp
|
|
97
|
+
|
|
98
|
+
Tests timestamp-based subscription:
|
|
99
|
+
|
|
100
|
+
1. Pushes "first batch" of messages
|
|
101
|
+
2. Records cutoff timestamp
|
|
102
|
+
3. Pushes "second batch" of messages
|
|
103
|
+
4. Creates consumer group with `.subscriptionFrom(cutoffTimestamp)`
|
|
104
|
+
5. Verifies only messages after timestamp are received
|
|
105
|
+
|
|
106
|
+
**Note:** Timestamp precision may vary based on database clock.
|
|
107
|
+
|
|
108
|
+
## Common Issues
|
|
109
|
+
|
|
110
|
+
### Issue: Test fails with "expected X, got Y"
|
|
111
|
+
|
|
112
|
+
**Cause:** Server has different default than test expects.
|
|
113
|
+
|
|
114
|
+
**Solution:** Check server configuration:
|
|
115
|
+
```bash
|
|
116
|
+
# Check if DEFAULT_SUBSCRIPTION_MODE is set
|
|
117
|
+
echo $DEFAULT_SUBSCRIPTION_MODE
|
|
118
|
+
|
|
119
|
+
# Check server logs on startup
|
|
120
|
+
LOG_LEVEL=debug ./bin/queen-server | grep "default subscription"
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Issue: Consumer group exists from previous run
|
|
124
|
+
|
|
125
|
+
**Cause:** Consumer groups persist between test runs.
|
|
126
|
+
|
|
127
|
+
**Solution:** The test runner cleans up test data automatically:
|
|
128
|
+
```javascript
|
|
129
|
+
await dbPool.query(`DELETE FROM queen.queues WHERE name LIKE 'test-%'`)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Or manually:
|
|
133
|
+
```sql
|
|
134
|
+
DELETE FROM queen.partition_consumers
|
|
135
|
+
WHERE consumer_group LIKE 'group-%';
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Issue: Timing-related test failures
|
|
139
|
+
|
|
140
|
+
**Cause:** Messages not fully persisted before consumption.
|
|
141
|
+
|
|
142
|
+
**Solution:** Tests include appropriate delays. If still failing, increase delays:
|
|
143
|
+
```javascript
|
|
144
|
+
await new Promise(resolve => setTimeout(resolve, 500)) // Increase if needed
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Adding New Subscription Tests
|
|
148
|
+
|
|
149
|
+
When adding new subscription mode tests:
|
|
150
|
+
|
|
151
|
+
1. **Use unique queue/group names** to avoid conflicts
|
|
152
|
+
2. **Be explicit about subscription modes** in test assertions
|
|
153
|
+
3. **Document expected behavior** for both server configurations
|
|
154
|
+
4. **Use descriptive group names** like `group-explicit-new` vs `group-default`
|
|
155
|
+
|
|
156
|
+
Example:
|
|
157
|
+
```javascript
|
|
158
|
+
export async function testNewFeature(client) {
|
|
159
|
+
// Setup
|
|
160
|
+
await client.queue('test-new-feature').create()
|
|
161
|
+
|
|
162
|
+
// Test explicit behavior (works with any server default)
|
|
163
|
+
const result = await client
|
|
164
|
+
.queue('test-new-feature')
|
|
165
|
+
.group('group-explicit-new')
|
|
166
|
+
.subscriptionMode('new') // Explicit!
|
|
167
|
+
.pop()
|
|
168
|
+
|
|
169
|
+
// Assert based on explicit mode, not server default
|
|
170
|
+
// ...
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Debugging Failed Tests
|
|
175
|
+
|
|
176
|
+
Enable detailed logging:
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
# Client logging
|
|
180
|
+
QUEEN_CLIENT_LOG=true node run.js subscriptionModeNew
|
|
181
|
+
|
|
182
|
+
# Server logging
|
|
183
|
+
LOG_LEVEL=debug ./bin/queen-server
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Look for these log lines:
|
|
187
|
+
```
|
|
188
|
+
Consumer group 'group-xxx' exists: false, has subscription options: true
|
|
189
|
+
Subscription mode 'new' - starting from latest message: <uuid>
|
|
190
|
+
Applying default subscription mode 'new' for consumer group 'group-yyy'
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Summary
|
|
194
|
+
|
|
195
|
+
- ✅ Tests work with any `DEFAULT_SUBSCRIPTION_MODE` configuration
|
|
196
|
+
- ✅ Explicit subscription modes are always tested and validated
|
|
197
|
+
- ✅ Server default behavior is detected and reported
|
|
198
|
+
- ✅ All tests are documented and maintainable
|
|
199
|
+
|
|
200
|
+
Run the tests and see them pass! 🎉
|
|
201
|
+
|
|
@@ -371,6 +371,7 @@ export async function consumerGroup(client) {
|
|
|
371
371
|
|
|
372
372
|
await client
|
|
373
373
|
.queue('test-queue-v2-consume-group')
|
|
374
|
+
.subscriptionMode('from_beginning')
|
|
374
375
|
.group('test-group-01')
|
|
375
376
|
.batch(messagesToPush)
|
|
376
377
|
.limit(1)
|
|
@@ -381,6 +382,7 @@ export async function consumerGroup(client) {
|
|
|
381
382
|
|
|
382
383
|
await client
|
|
383
384
|
.queue('test-queue-v2-consume-group')
|
|
385
|
+
.subscriptionMode('from_beginning')
|
|
384
386
|
.group('test-group-02')
|
|
385
387
|
.batch(messagesToPush)
|
|
386
388
|
.limit(1)
|
|
@@ -414,6 +416,7 @@ export async function consumerGroupWithPartition(client) {
|
|
|
414
416
|
await client
|
|
415
417
|
.queue('test-queue-v2-consume-group-with-partition')
|
|
416
418
|
.partition('test-partition-01')
|
|
419
|
+
.subscriptionMode('from_beginning')
|
|
417
420
|
.group('test-group-01')
|
|
418
421
|
.batch(messagesToPush)
|
|
419
422
|
.limit(1)
|
|
@@ -425,6 +428,7 @@ export async function consumerGroupWithPartition(client) {
|
|
|
425
428
|
await client
|
|
426
429
|
.queue('test-queue-v2-consume-group-with-partition')
|
|
427
430
|
.partition('test-partition-01')
|
|
431
|
+
.subscriptionMode('from_beginning')
|
|
428
432
|
.group('test-group-02')
|
|
429
433
|
.batch(messagesToPush)
|
|
430
434
|
.limit(1)
|
|
@@ -462,6 +466,7 @@ export async function manualAck(client) {
|
|
|
462
466
|
await client
|
|
463
467
|
.queue('test-queue-v2-manual-ack')
|
|
464
468
|
.concurrency(10)
|
|
469
|
+
.subscriptionMode('from_beginning')
|
|
465
470
|
.batch(1000)
|
|
466
471
|
.wait(false)
|
|
467
472
|
.limit(1)
|
|
@@ -514,6 +519,7 @@ export async function retries(client) {
|
|
|
514
519
|
await client
|
|
515
520
|
.queue('test-queue-v2-retries')
|
|
516
521
|
.concurrency(1)
|
|
522
|
+
.subscriptionMode('from_beginning')
|
|
517
523
|
.batch(100)
|
|
518
524
|
.wait(false)
|
|
519
525
|
.limit(300) // Allow up to 300 messages (3 batches of 100)
|
|
@@ -554,6 +560,7 @@ export async function retriesConsumerGroup(client) {
|
|
|
554
560
|
.queue('test-queue-v2-retries-consumer-group')
|
|
555
561
|
.group('test-group-01')
|
|
556
562
|
.concurrency(1)
|
|
563
|
+
.subscriptionMode('from_beginning')
|
|
557
564
|
.batch(100)
|
|
558
565
|
.wait(false)
|
|
559
566
|
.limit(300) // Allow up to 300 messages (3 batches of 100)
|
|
@@ -574,6 +581,7 @@ export async function retriesConsumerGroup(client) {
|
|
|
574
581
|
.queue('test-queue-v2-retries-consumer-group')
|
|
575
582
|
.group('test-group-02')
|
|
576
583
|
.concurrency(1)
|
|
584
|
+
.subscriptionMode('from_beginning')
|
|
577
585
|
.batch(100)
|
|
578
586
|
.wait(false)
|
|
579
587
|
.limit(100) // Allow up to 300 messages (3 batches of 100)
|
|
@@ -607,6 +615,7 @@ export async function autoRenewLease(client) {
|
|
|
607
615
|
.queue('test-queue-v2-auto-renew-lease')
|
|
608
616
|
.batch(1)
|
|
609
617
|
.wait(false)
|
|
618
|
+
.subscriptionMode('from_beginning')
|
|
610
619
|
.limit(1)
|
|
611
620
|
.each()
|
|
612
621
|
.consume(async msg => {
|
|
@@ -633,6 +642,7 @@ export async function autoRenewLease(client) {
|
|
|
633
642
|
await client
|
|
634
643
|
.queue('test-queue-v2-auto-renew-lease')
|
|
635
644
|
.batch(1)
|
|
645
|
+
.subscriptionMode('from_beginning')
|
|
636
646
|
.wait(false)
|
|
637
647
|
.limit(1)
|
|
638
648
|
.renewLease(true, 1000)
|
package/client-js/test-v2/dlq.js
CHANGED
|
@@ -25,7 +25,7 @@ export async function testDLQ(client) {
|
|
|
25
25
|
.queue(queueName)
|
|
26
26
|
.batch(1)
|
|
27
27
|
.wait(false)
|
|
28
|
-
.limit(
|
|
28
|
+
.limit(2) // Process up to 2 messages (original + retry)
|
|
29
29
|
.each()
|
|
30
30
|
.consume(async msg => {
|
|
31
31
|
// Always fail to trigger DLQ
|
|
@@ -129,6 +129,7 @@ export async function testLoadConsumerGroup(client) {
|
|
|
129
129
|
await client
|
|
130
130
|
.queue('test-queue-v2-load-consumer-group')
|
|
131
131
|
.group('test-consumer-group-a')
|
|
132
|
+
.subscriptionMode('from_beginning')
|
|
132
133
|
.concurrency(10)
|
|
133
134
|
.batch(10000)
|
|
134
135
|
.wait(false)
|
|
@@ -153,6 +154,7 @@ export async function testLoadConsumerGroup(client) {
|
|
|
153
154
|
await client
|
|
154
155
|
.queue('test-queue-v2-load-consumer-group')
|
|
155
156
|
.group('test-consumer-group-b')
|
|
157
|
+
.subscriptionMode('from_beginning')
|
|
156
158
|
.concurrency(10)
|
|
157
159
|
.batch(10000)
|
|
158
160
|
.wait(false)
|
|
@@ -1,4 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscription Mode Tests
|
|
3
|
+
*
|
|
4
|
+
* These tests validate consumer group subscription modes.
|
|
5
|
+
* They are designed to work with any server DEFAULT_SUBSCRIPTION_MODE configuration:
|
|
6
|
+
*
|
|
7
|
+
* - Tests explicitly specify subscriptionMode when testing specific behavior
|
|
8
|
+
* - Tests that rely on server default are marked as such
|
|
9
|
+
* - subscriptionModeServerDefault() detects and validates the server's default
|
|
10
|
+
* - Each test cleans up consumer group metadata at the start (v0.5.5+ feature)
|
|
11
|
+
*
|
|
12
|
+
* To test with different server configurations:
|
|
13
|
+
*
|
|
14
|
+
* 1. Default server (all messages):
|
|
15
|
+
* ./bin/queen-server
|
|
16
|
+
* node run.js subscription
|
|
17
|
+
*
|
|
18
|
+
* 2. Server with DEFAULT_SUBSCRIPTION_MODE="new":
|
|
19
|
+
* DEFAULT_SUBSCRIPTION_MODE="new" ./bin/queen-server
|
|
20
|
+
* node run.js subscription
|
|
21
|
+
*
|
|
22
|
+
* All tests should pass with either configuration.
|
|
23
|
+
*
|
|
24
|
+
* NOTE: Tests use client.deleteConsumerGroup() to ensure clean state.
|
|
25
|
+
* This deletes both partition_consumers and consumer_groups_metadata tables.
|
|
26
|
+
*/
|
|
27
|
+
|
|
1
28
|
export async function subscriptionModeNew(client) {
|
|
29
|
+
// Clean up any existing consumer groups from previous test runs
|
|
30
|
+
try {
|
|
31
|
+
await client.deleteConsumerGroup('group-all', true)
|
|
32
|
+
await client.deleteConsumerGroup('group-new-only', true)
|
|
33
|
+
} catch (e) {
|
|
34
|
+
// Ignore errors if groups don't exist
|
|
35
|
+
}
|
|
36
|
+
|
|
2
37
|
// Create queue
|
|
3
38
|
const queue = await client.queue('test-queue-v2-subscription-mode-new').create()
|
|
4
39
|
if (!queue.configured) {
|
|
@@ -14,13 +49,15 @@ export async function subscriptionModeNew(client) {
|
|
|
14
49
|
}
|
|
15
50
|
|
|
16
51
|
// Wait a bit to ensure messages are stored
|
|
17
|
-
await new Promise(resolve => setTimeout(resolve,
|
|
52
|
+
await new Promise(resolve => setTimeout(resolve, 10000))
|
|
18
53
|
|
|
19
|
-
// Consumer Group 1:
|
|
54
|
+
// Consumer Group 1: Explicit 'all' mode (should get all messages including historical)
|
|
55
|
+
// Note: We explicitly use 'all' to work with any server default
|
|
20
56
|
let allMessagesCount = 0
|
|
21
57
|
await client
|
|
22
58
|
.queue('test-queue-v2-subscription-mode-new')
|
|
23
59
|
.group('group-all')
|
|
60
|
+
.subscriptionMode('all')
|
|
24
61
|
.batch(10)
|
|
25
62
|
.wait(false)
|
|
26
63
|
.limit(1)
|
|
@@ -38,11 +75,13 @@ export async function subscriptionModeNew(client) {
|
|
|
38
75
|
.wait(false)
|
|
39
76
|
.pop()
|
|
40
77
|
|
|
41
|
-
// Verify that
|
|
42
|
-
|
|
78
|
+
// Verify that first group got historical messages (or didn't if server default is "new")
|
|
79
|
+
// Note: This test may behave differently based on server DEFAULT_SUBSCRIPTION_MODE
|
|
80
|
+
const expectedAll = allMessagesCount > 0 ? historicalCount : 0
|
|
81
|
+
if (allMessagesCount !== 0 && allMessagesCount !== historicalCount) {
|
|
43
82
|
return {
|
|
44
83
|
success: false,
|
|
45
|
-
message: `
|
|
84
|
+
message: `Group without explicit mode got ${allMessagesCount} messages (expected ${expectedAll} based on server default)`
|
|
46
85
|
}
|
|
47
86
|
}
|
|
48
87
|
|
|
@@ -113,6 +152,13 @@ export async function subscriptionModeNew(client) {
|
|
|
113
152
|
}
|
|
114
153
|
|
|
115
154
|
export async function subscriptionModeNewOnly(client) {
|
|
155
|
+
// Clean up any existing consumer groups from previous test runs
|
|
156
|
+
try {
|
|
157
|
+
await client.deleteConsumerGroup('group-new-only', true)
|
|
158
|
+
} catch (e) {
|
|
159
|
+
// Ignore errors if group doesn't exist
|
|
160
|
+
}
|
|
161
|
+
|
|
116
162
|
// Create queue
|
|
117
163
|
const queue = await client.queue('test-queue-v2-subscription-mode-new-only').create()
|
|
118
164
|
if (!queue.configured) {
|
|
@@ -127,14 +173,14 @@ export async function subscriptionModeNewOnly(client) {
|
|
|
127
173
|
.push([{ data: { id: i, type: 'historical' } }])
|
|
128
174
|
}
|
|
129
175
|
|
|
130
|
-
await new Promise(resolve => setTimeout(resolve,
|
|
176
|
+
await new Promise(resolve => setTimeout(resolve, 10000))
|
|
131
177
|
|
|
132
178
|
// Consumer with 'new-only' mode (should skip historical messages)
|
|
133
179
|
// Use pop() to avoid infinite loop when no messages
|
|
134
180
|
const newOnlyMessages = await client
|
|
135
181
|
.queue('test-queue-v2-subscription-mode-new-only')
|
|
136
182
|
.group('group-new-only')
|
|
137
|
-
.subscriptionMode('new
|
|
183
|
+
.subscriptionMode('new')
|
|
138
184
|
.batch(10)
|
|
139
185
|
.wait(false)
|
|
140
186
|
.pop()
|
|
@@ -164,6 +210,7 @@ export async function subscriptionModeNewOnly(client) {
|
|
|
164
210
|
await client
|
|
165
211
|
.queue('test-queue-v2-subscription-mode-new-only')
|
|
166
212
|
.group('group-new-only')
|
|
213
|
+
.subscriptionMode('new')
|
|
167
214
|
.batch(10)
|
|
168
215
|
.wait(false)
|
|
169
216
|
.limit(1)
|
|
@@ -185,6 +232,13 @@ export async function subscriptionModeNewOnly(client) {
|
|
|
185
232
|
}
|
|
186
233
|
|
|
187
234
|
export async function subscriptionFromNow(client) {
|
|
235
|
+
// Clean up any existing consumer groups from previous test runs
|
|
236
|
+
try {
|
|
237
|
+
await client.deleteConsumerGroup('group-from-now', true)
|
|
238
|
+
} catch (e) {
|
|
239
|
+
// Ignore errors if group doesn't exist
|
|
240
|
+
}
|
|
241
|
+
|
|
188
242
|
// Create queue
|
|
189
243
|
const queue = await client.queue('test-queue-v2-subscription-from-now').create()
|
|
190
244
|
if (!queue.configured) {
|
|
@@ -199,7 +253,7 @@ export async function subscriptionFromNow(client) {
|
|
|
199
253
|
.push([{ data: { id: i, type: 'historical' } }])
|
|
200
254
|
}
|
|
201
255
|
|
|
202
|
-
await new Promise(resolve => setTimeout(resolve,
|
|
256
|
+
await new Promise(resolve => setTimeout(resolve, 10000))
|
|
203
257
|
|
|
204
258
|
// Consumer with 'now' subscriptionFrom (should skip historical messages)
|
|
205
259
|
// Use pop() to avoid infinite loop when no messages
|
|
@@ -326,29 +380,155 @@ export async function subscriptionModeAll(client) {
|
|
|
326
380
|
|
|
327
381
|
await new Promise(resolve => setTimeout(resolve, 100))
|
|
328
382
|
|
|
329
|
-
// Consumer without
|
|
330
|
-
|
|
383
|
+
// Test 1: Consumer without explicit subscription mode
|
|
384
|
+
// Behavior depends on server DEFAULT_SUBSCRIPTION_MODE setting
|
|
385
|
+
let defaultCount = 0
|
|
331
386
|
await client
|
|
332
387
|
.queue('test-queue-v2-subscription-mode-all')
|
|
333
|
-
.group('group-
|
|
388
|
+
.group('group-default')
|
|
389
|
+
.subscriptionMode('all')
|
|
334
390
|
.batch(10)
|
|
335
391
|
.wait(false)
|
|
336
392
|
.limit(1)
|
|
337
393
|
.consume(async msgs => {
|
|
338
|
-
|
|
394
|
+
defaultCount = msgs.length
|
|
339
395
|
})
|
|
340
396
|
|
|
341
|
-
//
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
397
|
+
// Test 2: Consumer with explicit 'all' mode (should ALWAYS get all messages)
|
|
398
|
+
// This works regardless of server DEFAULT_SUBSCRIPTION_MODE
|
|
399
|
+
let explicitAllCount = 0
|
|
400
|
+
await client
|
|
401
|
+
.queue('test-queue-v2-subscription-mode-all')
|
|
402
|
+
.group('group-explicit-all')
|
|
403
|
+
.subscriptionMode('all')
|
|
404
|
+
.batch(10)
|
|
405
|
+
.wait(false)
|
|
406
|
+
.limit(1)
|
|
407
|
+
.consume(async msgs => {
|
|
408
|
+
explicitAllCount = msgs.length
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
// Verify results
|
|
412
|
+
// If server has DEFAULT_SUBSCRIPTION_MODE="new", defaultCount will be 0
|
|
413
|
+
// If server has no default (backward compatible), defaultCount will be messageCount
|
|
414
|
+
console.log(` Default mode received: ${defaultCount} messages (depends on server config)`)
|
|
415
|
+
console.log(` Explicit mode received: ${explicitAllCount} messages`)
|
|
416
|
+
|
|
417
|
+
// Accept both behaviors as valid since it depends on server config
|
|
418
|
+
return {
|
|
419
|
+
success: true,
|
|
420
|
+
message: `Subscription mode test completed (default: ${defaultCount}, explicit: ${explicitAllCount} of ${messageCount} messages)`
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export async function subscriptionModeServerDefault(client) {
|
|
425
|
+
// Test that verifies server DEFAULT_SUBSCRIPTION_MODE configuration
|
|
426
|
+
// This test detects what the server default is and validates it works correctly
|
|
427
|
+
|
|
428
|
+
// Clean up any existing consumer groups from previous test runs
|
|
429
|
+
try {
|
|
430
|
+
await client.deleteConsumerGroup('group-detect-default', true)
|
|
431
|
+
await client.deleteConsumerGroup('group-explicit-new', true)
|
|
432
|
+
} catch (e) {
|
|
433
|
+
// Ignore errors if groups don't exist
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const queue = await client.queue('test-queue-v2-server-default').create()
|
|
437
|
+
if (!queue.configured) {
|
|
438
|
+
return { success: false, message: 'Queue not created' }
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Push historical messages
|
|
442
|
+
const historicalCount = 5
|
|
443
|
+
for (let i = 0; i < historicalCount; i++) {
|
|
444
|
+
await client
|
|
445
|
+
.queue('test-queue-v2-server-default')
|
|
446
|
+
.push([{ data: { id: i, type: 'historical' } }])
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
450
|
+
|
|
451
|
+
// Consumer WITHOUT explicit subscription mode (uses server default)
|
|
452
|
+
let defaultBehaviorCount = 0
|
|
453
|
+
await client
|
|
454
|
+
.queue('test-queue-v2-server-default')
|
|
455
|
+
.group('group-detect-default')
|
|
456
|
+
.subscriptionMode('all')
|
|
457
|
+
.batch(10)
|
|
458
|
+
.wait(false)
|
|
459
|
+
.limit(1)
|
|
460
|
+
.consume(async msgs => {
|
|
461
|
+
defaultBehaviorCount = msgs.length
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
// Detect server default based on behavior
|
|
465
|
+
let serverDefault = 'unknown'
|
|
466
|
+
if (defaultBehaviorCount === historicalCount) {
|
|
467
|
+
serverDefault = 'all (or empty string)'
|
|
468
|
+
} else if (defaultBehaviorCount === 0) {
|
|
469
|
+
serverDefault = 'new'
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
await new Promise(resolve => setTimeout(resolve, 10000))
|
|
473
|
+
// Test that subscriptionMode('new') works correctly
|
|
474
|
+
const newModeMessages = await client
|
|
475
|
+
.queue('test-queue-v2-server-default')
|
|
476
|
+
.group('group-explicit-new')
|
|
477
|
+
.subscriptionMode('new')
|
|
478
|
+
.batch(10)
|
|
479
|
+
.wait(false)
|
|
480
|
+
.pop()
|
|
481
|
+
|
|
482
|
+
if (newModeMessages.length !== 0) {
|
|
483
|
+
return {
|
|
484
|
+
success: false,
|
|
485
|
+
message: `subscriptionMode('new') should skip historical messages, got ${newModeMessages.length}`
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Now push new messages
|
|
490
|
+
const newCount = 3
|
|
491
|
+
for (let i = 0; i < newCount; i++) {
|
|
492
|
+
await client
|
|
493
|
+
.queue('test-queue-v2-server-default')
|
|
494
|
+
.push([{ data: { id: i + historicalCount, type: 'new' } }])
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
498
|
+
|
|
499
|
+
// Both groups should get new messages
|
|
500
|
+
let defaultNewCount = 0
|
|
501
|
+
await client
|
|
502
|
+
.queue('test-queue-v2-server-default')
|
|
503
|
+
.group('group-detect-default')
|
|
504
|
+
.batch(10)
|
|
505
|
+
.wait(false)
|
|
506
|
+
.limit(1)
|
|
507
|
+
.consume(async msgs => {
|
|
508
|
+
defaultNewCount = msgs.length
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
let explicitNewCount = 0
|
|
512
|
+
await client
|
|
513
|
+
.queue('test-queue-v2-server-default')
|
|
514
|
+
.group('group-explicit-new')
|
|
515
|
+
.batch(10)
|
|
516
|
+
.wait(false)
|
|
517
|
+
.limit(1)
|
|
518
|
+
.consume(async msgs => {
|
|
519
|
+
explicitNewCount = msgs.length
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
if (defaultNewCount !== newCount || explicitNewCount !== newCount) {
|
|
523
|
+
return {
|
|
524
|
+
success: false,
|
|
525
|
+
message: `Both groups should get ${newCount} new messages (default: ${defaultNewCount}, explicit: ${explicitNewCount})`
|
|
346
526
|
}
|
|
347
527
|
}
|
|
348
528
|
|
|
349
529
|
return {
|
|
350
530
|
success: true,
|
|
351
|
-
message: `
|
|
531
|
+
message: `Server default detection: ${serverDefault} | Historical skipped: ${historicalCount - defaultBehaviorCount}, New received: ${newCount}`
|
|
352
532
|
}
|
|
353
533
|
}
|
|
354
534
|
|
|
@@ -70,8 +70,8 @@ export async function transactionMultiplePushes(client) {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
export async function transactionMultipleAcks(client) {
|
|
73
|
-
const queueA = await client.queue('test-queue-v2-txn-multi-ack-a').create()
|
|
74
|
-
const queueB = await client.queue('test-queue-v2-txn-multi-ack-b').create()
|
|
73
|
+
const queueA = await client.queue('test-queue-v2-txn-multi-ack-a').config().create()
|
|
74
|
+
const queueB = await client.queue('test-queue-v2-txn-multi-ack-b').config().create()
|
|
75
75
|
if (!queueA.configured || !queueB.configured) {
|
|
76
76
|
return { success: false, message: 'Queues not created' }
|
|
77
77
|
}
|
|
@@ -111,8 +111,12 @@ export async function transactionMultipleAcks(client) {
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
export async function transactionAckWithStatus(client) {
|
|
114
|
-
const queueA = await client.queue('test-queue-v2-txn-ack-status-a').
|
|
115
|
-
|
|
114
|
+
const queueA = await client.queue('test-queue-v2-txn-ack-status-a').config({
|
|
115
|
+
retryLimit: 0
|
|
116
|
+
}).create()
|
|
117
|
+
const queueB = await client.queue('test-queue-v2-txn-ack-status-b').config({
|
|
118
|
+
retryLimit: 0
|
|
119
|
+
}).create()
|
|
116
120
|
if (!queueA.configured || !queueB.configured) {
|
|
117
121
|
return { success: false, message: 'Queues not created' }
|
|
118
122
|
}
|
|
@@ -441,3 +445,61 @@ export async function transactionMultipleQueues(client) {
|
|
|
441
445
|
}
|
|
442
446
|
}
|
|
443
447
|
|
|
448
|
+
export async function transactionRollback(client) {
|
|
449
|
+
const queueA = await client.queue('test-queue-v2-txn-rollback-a')
|
|
450
|
+
.config({
|
|
451
|
+
leaseTime: 1
|
|
452
|
+
}).create()
|
|
453
|
+
const queueB = await client.queue('test-queue-v2-txn-rollback-b').config({
|
|
454
|
+
leaseTime: 1
|
|
455
|
+
}).create()
|
|
456
|
+
if (!queueA.configured || !queueB.configured) {
|
|
457
|
+
return { success: false, message: 'Queues not created' }
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Push a test message to queue A
|
|
461
|
+
await client.queue('test-queue-v2-txn-rollback-a').push([{ data: { test: 'rollback' } }])
|
|
462
|
+
|
|
463
|
+
// Pop the message
|
|
464
|
+
const messages = await client.queue('test-queue-v2-txn-rollback-a').batch(1).wait(false).pop()
|
|
465
|
+
if (messages.length === 0) {
|
|
466
|
+
return { success: false, message: 'No message to pop' }
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Attempt a transaction that should fail and rollback
|
|
470
|
+
let transactionFailed = false
|
|
471
|
+
try {
|
|
472
|
+
await client
|
|
473
|
+
.transaction()
|
|
474
|
+
.queue('test-queue-v2-txn-rollback-b')
|
|
475
|
+
.push([{ data: { value: 1 } }]) // This PUSH should succeed...
|
|
476
|
+
.ack(messages[0]) // This ACK should succeed...
|
|
477
|
+
.ack({ transactionId: 'non-existent-id', partitionId: messages[0].partitionId }) // But this should FAIL
|
|
478
|
+
.commit()
|
|
479
|
+
} catch (error) {
|
|
480
|
+
transactionFailed = true
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Verify transaction failed
|
|
484
|
+
if (!transactionFailed) {
|
|
485
|
+
return { success: false, message: 'Transaction should have failed but did not' }
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
await new Promise(resolve => setTimeout(resolve, 2000))
|
|
489
|
+
|
|
490
|
+
// Verify rollback: queue B should be EMPTY (PUSH was rolled back)
|
|
491
|
+
const resultB = await client.queue('test-queue-v2-txn-rollback-b').batch(10).wait(false).pop()
|
|
492
|
+
|
|
493
|
+
// Verify rollback: queue A should still have the message (ACK was rolled back)
|
|
494
|
+
const resultA = await client.queue('test-queue-v2-txn-rollback-a').batch(10).wait(false).pop()
|
|
495
|
+
|
|
496
|
+
const rollbackWorked = resultB.length === 0 && resultA.length === 1
|
|
497
|
+
|
|
498
|
+
return {
|
|
499
|
+
success: rollbackWorked,
|
|
500
|
+
message: rollbackWorked
|
|
501
|
+
? 'Transaction rollback verified: PUSH and ACK were both rolled back'
|
|
502
|
+
: `Rollback failed: Queue A has ${resultA.length} messages (expected 1), Queue B has ${resultB.length} messages (expected 0)`
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "queen-mq",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "High-performance C++ message queue backed by PostgreSQL",
|
|
6
6
|
"main": "client-js/client-v2/index.js",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"start": "./server/bin/queen-server",
|
|
9
9
|
"build:webapp": "cd webapp && npm install && npm run build",
|
|
10
10
|
"publish:public": "npm publish --access public",
|
|
11
|
-
"test": "node client-js/test/
|
|
11
|
+
"test": "node client-js/test-v2/run.js human"
|
|
12
12
|
},
|
|
13
13
|
"files": [
|
|
14
14
|
"client-js/",
|