queen-mq 0.1.6 → 0.2.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/README.md +467 -1775
- package/package.json +2 -14
- package/src/client/client.js +664 -5
- package/src/managers/queueManagerOptimized.js +250 -47
- package/src/routes/ack.js +3 -3
- package/src/server.js +107 -2
- package/src/test/advanced-client-tests.js +761 -0
- package/src/test/advanced-pattern-tests.js +2 -2
- package/src/test/bus-mode-tests.js +6 -6
- package/src/test/core-tests.js +1 -1
- package/src/test/edge-case-tests.js +2 -2
- package/src/test/test-new.js +36 -1
package/README.md
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
### Why Queen?
|
|
25
25
|
|
|
26
26
|
**🚀 Developer-First API**
|
|
27
|
+
- **Pipeline & Transaction APIs**: High-level fluent interfaces for complex workflows
|
|
27
28
|
- **4 core methods, infinite patterns**: `queue()`, `push()`, `take()`, `ack()`
|
|
28
29
|
- **Async iteration**: Process messages with familiar `for await` syntax
|
|
29
30
|
- **Batch processing**: Use `takeBatch()` for 250k+ msg/sec throughput on millions of messages
|
|
@@ -35,18 +36,22 @@
|
|
|
35
36
|
- **Long polling** for event-driven, real-time message delivery
|
|
36
37
|
- **Partition locking** prevents duplicate processing across consumers
|
|
37
38
|
- **Connection pooling** and optimized batch operations
|
|
39
|
+
- **Parallel processing** across partitions with `withConcurrency()`
|
|
38
40
|
|
|
39
41
|
**🏗️ Flexible Architecture**
|
|
40
42
|
- **Queue Mode**: Competitive consumption (traditional work queue)
|
|
41
43
|
- **Bus Mode**: Pub/sub with consumer groups (event streaming)
|
|
42
44
|
- **Mixed Mode**: Combine both patterns in the same system
|
|
43
45
|
- **Partitions**: FIFO ordering with parallel processing
|
|
46
|
+
- **Exactly-Once Processing**: Lease-based locking with automatic validation
|
|
44
47
|
|
|
45
48
|
**🔒 Enterprise Features**
|
|
46
49
|
- **AES-256-GCM Encryption**: Protect sensitive data at rest
|
|
47
50
|
- **Message Retention**: Automatic cleanup policies
|
|
48
51
|
- **Message Eviction**: SLA enforcement for time-sensitive tasks
|
|
49
52
|
- **Dead Letter Queue**: Handle failed messages gracefully
|
|
53
|
+
- **Automatic Lease Renewal**: For long-running tasks
|
|
54
|
+
- **Atomic Transactions**: Multi-operation consistency
|
|
50
55
|
|
|
51
56
|
**📊 Built-in Observability**
|
|
52
57
|
- **Real-time Dashboard**: WebSocket-powered monitoring
|
|
@@ -67,12 +72,12 @@
|
|
|
67
72
|
|
|
68
73
|
## 📋 Table of Contents
|
|
69
74
|
|
|
70
|
-
- [First Queue](
|
|
75
|
+
- [First Queue](#first-queue)
|
|
71
76
|
- [Quick Start](#-quick-start)
|
|
72
|
-
- [Client
|
|
77
|
+
- [Advanced Client APIs](#advanced-client-apis)
|
|
78
|
+
- [Standard Client Examples](#-client-examples)
|
|
73
79
|
- [Server Setup](#-server-setup)
|
|
74
80
|
- [Core Concepts](#-core-concepts)
|
|
75
|
-
- [Cursor-Based Consumption Strategy](#-cursor-based-consumption-strategy)
|
|
76
81
|
- [HTTP API Reference](#-http-api-reference)
|
|
77
82
|
- [Dashboard](#-dashboard)
|
|
78
83
|
- [Configuration](#-configuration)
|
|
@@ -88,13 +93,17 @@
|
|
|
88
93
|
import { Queen } from 'queen-mq';
|
|
89
94
|
|
|
90
95
|
const client = new Queen({
|
|
91
|
-
|
|
96
|
+
baseUrl: 'http://localhost:6632' // Single server
|
|
97
|
+
// OR for multiple servers with load balancing:
|
|
98
|
+
// baseUrls: ['http://server1:6632', 'http://server2:6632'],
|
|
99
|
+
// loadBalancingStrategy: 'round-robin', // or 'random', 'least-connections'
|
|
100
|
+
// enableFailover: true
|
|
92
101
|
});
|
|
93
102
|
|
|
94
103
|
// Configure queue
|
|
95
104
|
await client.queue('tasks', {
|
|
96
|
-
leaseTime: 300,
|
|
97
|
-
retryLimit: 3
|
|
105
|
+
leaseTime: 300, // 5 minutes to process each message
|
|
106
|
+
retryLimit: 3 // Retry up to 3 times
|
|
98
107
|
});
|
|
99
108
|
|
|
100
109
|
// Push a message
|
|
@@ -134,33 +143,6 @@ npm install
|
|
|
134
143
|
node init-db.js
|
|
135
144
|
```
|
|
136
145
|
|
|
137
|
-
### Running the Server
|
|
138
|
-
|
|
139
|
-
**Option 1: Standalone Server (Traditional)**
|
|
140
|
-
|
|
141
|
-
```bash
|
|
142
|
-
npm start
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
**Option 2: Programmatic Server (New in v0.1.2)**
|
|
146
|
-
|
|
147
|
-
```javascript
|
|
148
|
-
import { QueenServer } from 'queen-mq';
|
|
149
|
-
|
|
150
|
-
// Start server with default config
|
|
151
|
-
const server = await QueenServer();
|
|
152
|
-
|
|
153
|
-
// Or with custom options
|
|
154
|
-
const server = await QueenServer({
|
|
155
|
-
port: 3000,
|
|
156
|
-
host: '127.0.0.1'
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
console.log(`Server running at http://${server.host}:${server.port}`);
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
This allows you to embed Queen MQ directly in your application, run multiple instances, or easily start/stop servers in tests. See [PROGRAMMATIC_SERVER.md](./PROGRAMMATIC_SERVER.md) for more details.
|
|
163
|
-
|
|
164
146
|
### Set Environment (Optional)
|
|
165
147
|
|
|
166
148
|
```bash
|
|
@@ -184,6 +166,193 @@ npm start
|
|
|
184
166
|
|
|
185
167
|
---
|
|
186
168
|
|
|
169
|
+
## Advanced Client APIs
|
|
170
|
+
|
|
171
|
+
### Pipeline API - Fluent Message Processing
|
|
172
|
+
|
|
173
|
+
The Pipeline API provides a chainable interface for complex message processing workflows:
|
|
174
|
+
|
|
175
|
+
```javascript
|
|
176
|
+
// Simple message processing (one at a time)
|
|
177
|
+
await client.pipeline('my-queue')
|
|
178
|
+
.take(100) // Take up to 100 messages
|
|
179
|
+
.process(async (message) => { // Process each message individually
|
|
180
|
+
console.log('Processing:', message.data);
|
|
181
|
+
return { processed: true };
|
|
182
|
+
})
|
|
183
|
+
.execute();
|
|
184
|
+
|
|
185
|
+
// Batch processing
|
|
186
|
+
await client.pipeline('my-queue')
|
|
187
|
+
.take(100)
|
|
188
|
+
.processBatch(async (messages) => { // Process messages as a batch
|
|
189
|
+
console.log(`Processing ${messages.length} messages`);
|
|
190
|
+
return messages.map(m => ({ processed: m.id }));
|
|
191
|
+
})
|
|
192
|
+
.execute();
|
|
193
|
+
|
|
194
|
+
// With automatic lease renewal for long-running tasks
|
|
195
|
+
await client.pipeline('my-queue')
|
|
196
|
+
.take(50)
|
|
197
|
+
.withAutoRenewal({ interval: 5000 }) // Renew lease every 5 seconds
|
|
198
|
+
.process(async (message) => {
|
|
199
|
+
// Long-running task - lease automatically renewed
|
|
200
|
+
// Without this, if task takes > leaseTime, message may be redelivered
|
|
201
|
+
await heavyComputation(message);
|
|
202
|
+
})
|
|
203
|
+
.execute();
|
|
204
|
+
|
|
205
|
+
// Parallel processing across partitions
|
|
206
|
+
await client.pipeline('my-queue')
|
|
207
|
+
.take(100)
|
|
208
|
+
.withConcurrency(4) // 4 parallel workers
|
|
209
|
+
.process(async (message) => {
|
|
210
|
+
await processMessage(message);
|
|
211
|
+
})
|
|
212
|
+
.repeat({ continuous: true }) // Keep running continuously (default)
|
|
213
|
+
.execute();
|
|
214
|
+
|
|
215
|
+
// With error handling
|
|
216
|
+
await client.pipeline('my-queue')
|
|
217
|
+
.take(100)
|
|
218
|
+
.process(async (message) => {
|
|
219
|
+
if (message.data.invalid) {
|
|
220
|
+
throw new Error('Invalid message format');
|
|
221
|
+
}
|
|
222
|
+
return await riskyOperation(message);
|
|
223
|
+
})
|
|
224
|
+
.onError(async (error, messages) => {
|
|
225
|
+
console.error('Processing failed:', error.message);
|
|
226
|
+
|
|
227
|
+
// Move failed messages to error queue
|
|
228
|
+
await client.push('error-queue', {
|
|
229
|
+
error: error.message,
|
|
230
|
+
messages: messages.map(m => m.data),
|
|
231
|
+
timestamp: Date.now()
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// ACK as failed (will retry based on retryLimit)
|
|
235
|
+
for (const msg of messages) {
|
|
236
|
+
await client.ack(msg, false, { error: error.message });
|
|
237
|
+
}
|
|
238
|
+
})
|
|
239
|
+
.execute();
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
#### Automatic Lease Renewal
|
|
243
|
+
|
|
244
|
+
By default, messages have a lease time (e.g., 5 minutes). If processing takes longer, the message may be redelivered to another consumer. Use `.withAutoRenewal()` to prevent this:
|
|
245
|
+
|
|
246
|
+
```javascript
|
|
247
|
+
// WITHOUT auto-renewal (default) - Risk of redelivery
|
|
248
|
+
await client.pipeline('video-processing')
|
|
249
|
+
.take(10)
|
|
250
|
+
.process(async (message) => {
|
|
251
|
+
// If this takes > leaseTime, message may be processed twice!
|
|
252
|
+
await longRunningTask(message);
|
|
253
|
+
})
|
|
254
|
+
.execute();
|
|
255
|
+
|
|
256
|
+
// WITH auto-renewal - Safe for long tasks
|
|
257
|
+
await client.pipeline('video-processing')
|
|
258
|
+
.take(10)
|
|
259
|
+
.withAutoRenewal({
|
|
260
|
+
interval: 30000 // Renew every 30 seconds (default)
|
|
261
|
+
})
|
|
262
|
+
.process(async (message) => {
|
|
263
|
+
// Lease automatically renewed while processing
|
|
264
|
+
await longRunningTask(message); // Safe even if takes hours
|
|
265
|
+
})
|
|
266
|
+
.execute();
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
**When to use:**
|
|
270
|
+
- Video/audio processing
|
|
271
|
+
- Large file operations
|
|
272
|
+
- Machine learning inference
|
|
273
|
+
- Any task that might exceed the lease time
|
|
274
|
+
|
|
275
|
+
**Important:**
|
|
276
|
+
- Auto-renewal stops when message is ACKed or process crashes
|
|
277
|
+
- Set interval < lease time (e.g., renew at 1/3 of lease time)
|
|
278
|
+
- Only works within pipeline API
|
|
279
|
+
|
|
280
|
+
#### Error Handling in Pipeline
|
|
281
|
+
|
|
282
|
+
The `onError` handler provides robust error recovery:
|
|
283
|
+
|
|
284
|
+
**Key Behaviors:**
|
|
285
|
+
- Catches errors from `process()` or `processBatch()`
|
|
286
|
+
- Receives the error and affected messages
|
|
287
|
+
- Pipeline continues after error handler (doesn't stop)
|
|
288
|
+
- Without `onError`, errors stop the pipeline
|
|
289
|
+
|
|
290
|
+
**Error Recovery Strategies:**
|
|
291
|
+
```javascript
|
|
292
|
+
// Strategy 1: Retry with backoff
|
|
293
|
+
.onError(async (error, messages) => {
|
|
294
|
+
// ACK as failed - will retry based on retryLimit
|
|
295
|
+
await client.ack(messages, false);
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
// Strategy 2: Move to Dead Letter Queue
|
|
299
|
+
.onError(async (error, messages) => {
|
|
300
|
+
for (const msg of messages) {
|
|
301
|
+
if (msg.retryCount >= 3) {
|
|
302
|
+
await client.push('dlq', { original: msg, error: error.message });
|
|
303
|
+
await client.ack(msg, true); // Remove from queue
|
|
304
|
+
} else {
|
|
305
|
+
await client.ack(msg, false); // Retry
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
// Strategy 3: Skip and continue
|
|
311
|
+
.onError(async (error, messages) => {
|
|
312
|
+
console.error(`Skipping ${messages.length} messages: ${error.message}`);
|
|
313
|
+
await client.ack(messages, true); // ACK as success to remove
|
|
314
|
+
})
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
**Important Notes:**
|
|
318
|
+
- With `atomically()`: atomic operations only run on success
|
|
319
|
+
- In `repeat()` mode: errors don't stop continuous processing
|
|
320
|
+
- Batch processing: error affects all messages in the batch
|
|
321
|
+
- Parallel workers: each worker has independent error handling
|
|
322
|
+
|
|
323
|
+
### Transaction API - Atomic Operations
|
|
324
|
+
|
|
325
|
+
Execute multiple operations atomically with lease validation:
|
|
326
|
+
|
|
327
|
+
```javascript
|
|
328
|
+
// Atomic ACK + PUSH
|
|
329
|
+
const messages = await client.takeSingleBatch('input-queue');
|
|
330
|
+
|
|
331
|
+
await client.transaction()
|
|
332
|
+
.ack(messages) // ACK input messages
|
|
333
|
+
.push('output-queue', processedResults) // Push to output
|
|
334
|
+
.extend(leaseId) // Extend another lease
|
|
335
|
+
.commit(); // Execute atomically
|
|
336
|
+
|
|
337
|
+
// Pipeline with custom atomic operations
|
|
338
|
+
await client.pipeline('my-queue')
|
|
339
|
+
.take(50)
|
|
340
|
+
.process(async (message) => {
|
|
341
|
+
return await transform(message);
|
|
342
|
+
})
|
|
343
|
+
.atomically((tx, originalMessages, processedMessages) => {
|
|
344
|
+
tx.ack(originalMessages)
|
|
345
|
+
.push('output-queue', processedMessages)
|
|
346
|
+
.push('audit-queue', {
|
|
347
|
+
timestamp: Date.now(),
|
|
348
|
+
count: processedMessages.length
|
|
349
|
+
});
|
|
350
|
+
})
|
|
351
|
+
.execute();
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
187
356
|
## 💻 Client Examples
|
|
188
357
|
|
|
189
358
|
The Queen client provides a minimalist API with just 4 methods that compose into any messaging pattern you need.
|
|
@@ -202,17 +371,21 @@ npm install queen-mq
|
|
|
202
371
|
import { Queen } from 'queen-mq';
|
|
203
372
|
|
|
204
373
|
const client = new Queen({
|
|
205
|
-
|
|
374
|
+
baseUrl: 'http://localhost:6632',
|
|
206
375
|
timeout: 30000,
|
|
207
376
|
retryAttempts: 3
|
|
208
377
|
});
|
|
209
378
|
|
|
210
379
|
// Configure with options
|
|
211
380
|
await client.queue('orders', {
|
|
212
|
-
priority: 10,
|
|
213
|
-
leaseTime:
|
|
214
|
-
retryLimit: 3,
|
|
215
|
-
|
|
381
|
+
priority: 10, // Higher priority queues processed first
|
|
382
|
+
leaseTime: 300, // 5 minutes to process each message
|
|
383
|
+
retryLimit: 3, // Retry up to 3 times
|
|
384
|
+
windowBuffer: 0, // No delay (immediate processing)
|
|
385
|
+
retentionSeconds: 86400, // Keep messages for 24 hours
|
|
386
|
+
completedRetentionSeconds: 3600, // Keep completed messages for 1 hour
|
|
387
|
+
partitions: 10, // Create 10 partitions for parallel processing
|
|
388
|
+
maxWaitTimeSeconds: 600 // Evict messages waiting > 10 minutes
|
|
216
389
|
});
|
|
217
390
|
|
|
218
391
|
// Configure with namespace and task for grouping
|
|
@@ -246,15 +419,6 @@ await client.push('orders', [
|
|
|
246
419
|
{ orderId: 12348, amount: 79.99 },
|
|
247
420
|
{ orderId: 12349, amount: 29.99 }
|
|
248
421
|
]);
|
|
249
|
-
|
|
250
|
-
// With message properties
|
|
251
|
-
await client.push('orders', {
|
|
252
|
-
orderId: 12350,
|
|
253
|
-
amount: 199.99
|
|
254
|
-
}, {
|
|
255
|
-
transactionId: 'txn-12350', // For idempotency
|
|
256
|
-
traceId: '550e8400-e29b-41d4-a716-446655440000' // Valid UUID for tracing
|
|
257
|
-
});
|
|
258
422
|
```
|
|
259
423
|
|
|
260
424
|
#### 3. Take Messages (Async Iterator)
|
|
@@ -305,6 +469,9 @@ for await (const messages of client.takeBatch('orders', {
|
|
|
305
469
|
await client.ack(messages, false, { error: error.message });
|
|
306
470
|
}
|
|
307
471
|
}
|
|
472
|
+
|
|
473
|
+
// Take single batch (convenience method)
|
|
474
|
+
const messages = await client.takeSingleBatch('orders', { batch: 100 });
|
|
308
475
|
```
|
|
309
476
|
|
|
310
477
|
#### 4. Acknowledge Messages
|
|
@@ -323,11 +490,44 @@ await client.ack(message, false, {
|
|
|
323
490
|
error: 'Payment gateway timeout'
|
|
324
491
|
});
|
|
325
492
|
|
|
326
|
-
//
|
|
327
|
-
await client.ack(
|
|
493
|
+
// Batch acknowledgment (with lease validation)
|
|
494
|
+
await client.ack(messages); // Pass array for batch ack
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
#### 5. Renew Leases (for long-running tasks)
|
|
498
|
+
|
|
499
|
+
```javascript
|
|
500
|
+
// Renew lease for a single message
|
|
501
|
+
const result = await client.renewLease(message);
|
|
502
|
+
console.log(`Lease renewed until: ${result.newExpiresAt}`);
|
|
503
|
+
|
|
504
|
+
// Renew by lease ID directly
|
|
505
|
+
await client.renewLease('lease-uuid-123');
|
|
328
506
|
|
|
329
|
-
//
|
|
330
|
-
await client.
|
|
507
|
+
// Renew multiple messages at once
|
|
508
|
+
const results = await client.renewLease(messages);
|
|
509
|
+
results.forEach(r => {
|
|
510
|
+
if (r.success) {
|
|
511
|
+
console.log(`Renewed ${r.leaseId} until ${r.newExpiresAt}`);
|
|
512
|
+
} else {
|
|
513
|
+
console.error(`Failed to renew ${r.leaseId}: ${r.error}`);
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
// Example: Manual renewal during long processing
|
|
518
|
+
for await (const message of client.take('my-queue')) {
|
|
519
|
+
// Set up periodic renewal
|
|
520
|
+
const renewalInterval = setInterval(async () => {
|
|
521
|
+
await client.renewLease(message);
|
|
522
|
+
}, 30000); // Renew every 30 seconds
|
|
523
|
+
|
|
524
|
+
try {
|
|
525
|
+
await longRunningTask(message);
|
|
526
|
+
await client.ack(message);
|
|
527
|
+
} finally {
|
|
528
|
+
clearInterval(renewalInterval);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
331
531
|
```
|
|
332
532
|
|
|
333
533
|
### Address Notation
|
|
@@ -350,72 +550,6 @@ Queen uses a simple addressing scheme that encodes queue, partition, and consume
|
|
|
350
550
|
|
|
351
551
|
### Consumer Patterns
|
|
352
552
|
|
|
353
|
-
#### Continuous Processing (Long Polling)
|
|
354
|
-
|
|
355
|
-
```javascript
|
|
356
|
-
// Efficient real-time processing
|
|
357
|
-
for await (const message of client.take('tasks', {
|
|
358
|
-
wait: true, // Long polling - waits for messages
|
|
359
|
-
timeout: 30000 // Server timeout
|
|
360
|
-
})) {
|
|
361
|
-
await processTask(message.data);
|
|
362
|
-
await client.ack(message);
|
|
363
|
-
}
|
|
364
|
-
```
|
|
365
|
-
|
|
366
|
-
#### Batch Processing
|
|
367
|
-
|
|
368
|
-
```javascript
|
|
369
|
-
// Method 1: Manual batching with take()
|
|
370
|
-
const batch = [];
|
|
371
|
-
for await (const message of client.take('analytics', { batch: 100 })) {
|
|
372
|
-
batch.push(message);
|
|
373
|
-
|
|
374
|
-
if (batch.length >= 100) {
|
|
375
|
-
await processBatch(batch.map(m => m.data));
|
|
376
|
-
|
|
377
|
-
// Acknowledge all
|
|
378
|
-
for (const msg of batch) {
|
|
379
|
-
await client.ack(msg);
|
|
380
|
-
}
|
|
381
|
-
batch.length = 0;
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// Method 2: Direct batch processing with takeBatch() (RECOMMENDED)
|
|
386
|
-
for await (const messages of client.takeBatch('analytics', { batch: 1000 })) {
|
|
387
|
-
// messages is already an array!
|
|
388
|
-
await processBatch(messages.map(m => m.data));
|
|
389
|
-
|
|
390
|
-
// Single batch acknowledgment (much faster!)
|
|
391
|
-
await client.ack(messages);
|
|
392
|
-
}
|
|
393
|
-
```
|
|
394
|
-
|
|
395
|
-
#### Parallel Processing with Partitions
|
|
396
|
-
|
|
397
|
-
```javascript
|
|
398
|
-
// Create workers for parallel processing
|
|
399
|
-
const partitions = ['worker-1', 'worker-2', 'worker-3', 'worker-4'];
|
|
400
|
-
|
|
401
|
-
// Distribute messages across partitions
|
|
402
|
-
for (let i = 0; i < messages.length; i++) {
|
|
403
|
-
const partition = partitions[i % partitions.length];
|
|
404
|
-
await client.push(`tasks/${partition}`, messages[i]);
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// Each worker processes its own partition (in parallel)
|
|
408
|
-
async function worker(partition) {
|
|
409
|
-
for await (const msg of client.take(`tasks/${partition}`)) {
|
|
410
|
-
await processTask(msg.data);
|
|
411
|
-
await client.ack(msg);
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// Start all workers
|
|
416
|
-
await Promise.all(partitions.map(p => worker(p)));
|
|
417
|
-
```
|
|
418
|
-
|
|
419
553
|
#### Consumer Groups (Bus Mode)
|
|
420
554
|
|
|
421
555
|
```javascript
|
|
@@ -440,60 +574,6 @@ for await (const event of client.take('events@audit')) {
|
|
|
440
574
|
}
|
|
441
575
|
```
|
|
442
576
|
|
|
443
|
-
#### Subscription Modes
|
|
444
|
-
|
|
445
|
-
```javascript
|
|
446
|
-
// Start from all existing messages (replay)
|
|
447
|
-
for await (const event of client.take('events@replay-service', {
|
|
448
|
-
subscriptionMode: 'all'
|
|
449
|
-
})) {
|
|
450
|
-
await replayEvent(event.data);
|
|
451
|
-
await client.ack(event);
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
// Start from new messages only (real-time)
|
|
455
|
-
for await (const event of client.take('events@realtime', {
|
|
456
|
-
subscriptionMode: 'new'
|
|
457
|
-
})) {
|
|
458
|
-
await processEvent(event.data);
|
|
459
|
-
await client.ack(event);
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
// Start from specific timestamp
|
|
463
|
-
for await (const event of client.take('events@historical', {
|
|
464
|
-
subscriptionMode: 'from',
|
|
465
|
-
subscriptionFrom: '2024-01-01T00:00:00Z'
|
|
466
|
-
})) {
|
|
467
|
-
await processHistoricalEvent(event.data);
|
|
468
|
-
await client.ack(event);
|
|
469
|
-
}
|
|
470
|
-
```
|
|
471
|
-
|
|
472
|
-
#### Error Handling
|
|
473
|
-
|
|
474
|
-
```javascript
|
|
475
|
-
// Robust error handling with retries
|
|
476
|
-
for await (const message of client.take('critical-tasks')) {
|
|
477
|
-
let retries = 3;
|
|
478
|
-
|
|
479
|
-
while (retries > 0) {
|
|
480
|
-
try {
|
|
481
|
-
await processTask(message.data);
|
|
482
|
-
await client.ack(message);
|
|
483
|
-
break;
|
|
484
|
-
} catch (error) {
|
|
485
|
-
retries--;
|
|
486
|
-
if (retries === 0) {
|
|
487
|
-
console.error('Task failed after retries:', error);
|
|
488
|
-
await client.ack(message, false, { error: error.message });
|
|
489
|
-
} else {
|
|
490
|
-
await new Promise(r => setTimeout(r, 1000 * (4 - retries)));
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
```
|
|
496
|
-
|
|
497
577
|
#### Graceful Shutdown
|
|
498
578
|
|
|
499
579
|
```javascript
|
|
@@ -511,7 +591,6 @@ for await (const message of client.take('orders')) {
|
|
|
511
591
|
await client.ack(message);
|
|
512
592
|
}
|
|
513
593
|
|
|
514
|
-
await client.close();
|
|
515
594
|
console.log('Shutdown complete');
|
|
516
595
|
```
|
|
517
596
|
|
|
@@ -519,1658 +598,271 @@ console.log('Shutdown complete');
|
|
|
519
598
|
|
|
520
599
|
## 🖥️ Server Setup
|
|
521
600
|
|
|
522
|
-
###
|
|
523
|
-
|
|
524
|
-
```bash
|
|
525
|
-
# Start the server
|
|
526
|
-
npm start
|
|
527
|
-
|
|
528
|
-
# Or with custom configuration
|
|
529
|
-
PORT=6632 \
|
|
530
|
-
DB_POOL_SIZE=20 \
|
|
531
|
-
QUEEN_ENCRYPTION_KEY=$(openssl rand -hex 32) \
|
|
532
|
-
npm start
|
|
533
|
-
```
|
|
534
|
-
|
|
535
|
-
### Multi-Server (Load Balanced)
|
|
536
|
-
|
|
537
|
-
Queen supports running multiple servers for high availability and load distribution:
|
|
601
|
+
### Environment Variables
|
|
538
602
|
|
|
539
603
|
```bash
|
|
540
|
-
# Server
|
|
541
|
-
PORT=6632
|
|
542
|
-
|
|
543
|
-
#
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
# Server 3
|
|
547
|
-
PORT=6634 WORKER_ID=server-3 npm start
|
|
548
|
-
```
|
|
604
|
+
# Server configuration
|
|
605
|
+
PORT=6632 # Server port
|
|
606
|
+
HOST=0.0.0.0 # Server host
|
|
607
|
+
WORKER_ID=srv-1 # Unique server instance ID
|
|
608
|
+
APP_NAME=queen-mq # Application name
|
|
549
609
|
|
|
550
|
-
|
|
610
|
+
# Database connection
|
|
611
|
+
PG_USER=postgres # PostgreSQL user
|
|
612
|
+
PG_HOST=localhost # PostgreSQL host
|
|
613
|
+
PG_DB=postgres # PostgreSQL database
|
|
614
|
+
PG_PASSWORD=postgres # PostgreSQL password
|
|
615
|
+
PG_PORT=5432 # PostgreSQL port
|
|
616
|
+
PG_USE_SSL=false # Enable SSL for PostgreSQL
|
|
617
|
+
DB_POOL_SIZE=150 # Connection pool size
|
|
618
|
+
DB_IDLE_TIMEOUT=30000 # Idle connection timeout (ms)
|
|
619
|
+
DB_CONNECTION_TIMEOUT=2000 # Connection timeout (ms)
|
|
620
|
+
DB_STATEMENT_TIMEOUT=30000 # Statement timeout (ms)
|
|
621
|
+
|
|
622
|
+
# Queue configuration
|
|
623
|
+
DEFAULT_TIMEOUT=30000 # Default pop timeout (ms)
|
|
624
|
+
MAX_TIMEOUT=60000 # Maximum pop timeout (ms)
|
|
625
|
+
DEFAULT_BATCH_SIZE=1 # Default batch size
|
|
626
|
+
BATCH_INSERT_SIZE=1000 # Batch insert size for push operations
|
|
627
|
+
DEFAULT_LEASE_TIME=300 # Default lease time (seconds)
|
|
628
|
+
DEFAULT_RETRY_LIMIT=3 # Default retry limit
|
|
629
|
+
DEFAULT_RETRY_DELAY=1000 # Default retry delay (ms)
|
|
630
|
+
DEFAULT_PRIORITY=0 # Default message priority
|
|
631
|
+
DEFAULT_WINDOW_BUFFER=0 # Default window buffer (seconds)
|
|
632
|
+
|
|
633
|
+
# Long polling configuration
|
|
634
|
+
QUEUE_POLL_INTERVAL=100 # Initial poll interval (ms)
|
|
635
|
+
QUEUE_MAX_POLL_INTERVAL=2000 # Max poll interval after backoff (ms)
|
|
636
|
+
QUEUE_BACKOFF_THRESHOLD=5 # Empty polls before backoff
|
|
637
|
+
QUEUE_BACKOFF_MULTIPLIER=2 # Exponential backoff multiplier
|
|
638
|
+
|
|
639
|
+
# Encryption
|
|
640
|
+
QUEEN_ENCRYPTION_KEY= # 32-byte hex key for AES-256-GCM encryption
|
|
641
|
+
|
|
642
|
+
# Retention & Eviction
|
|
643
|
+
DEFAULT_RETENTION_SECONDS=0 # Message retention (0 = disabled)
|
|
644
|
+
DEFAULT_COMPLETED_RETENTION_SECONDS=0 # Completed message retention
|
|
645
|
+
RETENTION_INTERVAL=300000 # Retention service interval (ms)
|
|
646
|
+
RETENTION_BATCH_SIZE=1000 # Retention batch size
|
|
647
|
+
EVICTION_INTERVAL=60000 # Eviction service interval (ms)
|
|
648
|
+
EVICTION_BATCH_SIZE=1000 # Eviction batch size
|
|
649
|
+
METRICS_RETENTION_DAYS=90 # Metrics retention period
|
|
650
|
+
|
|
651
|
+
# System Events
|
|
652
|
+
QUEEN_SYSTEM_EVENTS_ENABLED=false # Enable system event propagation
|
|
653
|
+
QUEEN_SYSTEM_EVENTS_BATCH_MS=10 # Event batching window (ms)
|
|
654
|
+
QUEEN_SYSTEM_EVENTS_SYNC_TIMEOUT=30000 # Startup sync timeout (ms)
|
|
655
|
+
|
|
656
|
+
# WebSocket configuration
|
|
657
|
+
WS_COMPRESSION=0 # WebSocket compression level
|
|
658
|
+
WS_MAX_PAYLOAD_LENGTH=16384 # Max WebSocket payload (bytes)
|
|
659
|
+
WS_IDLE_TIMEOUT=60 # WebSocket idle timeout (seconds)
|
|
660
|
+
WS_MAX_CONNECTIONS=1000 # Max WebSocket connections
|
|
661
|
+
WS_HEARTBEAT_INTERVAL=30000 # WebSocket heartbeat interval (ms)
|
|
662
|
+
|
|
663
|
+
# API configuration
|
|
664
|
+
MAX_BODY_SIZE=104857600 # Max request body size (100MB)
|
|
665
|
+
API_DEFAULT_LIMIT=100 # Default pagination limit
|
|
666
|
+
API_MAX_LIMIT=1000 # Max pagination limit
|
|
667
|
+
CORS_MAX_AGE=86400 # CORS max age (seconds)
|
|
668
|
+
CORS_ALLOWED_ORIGINS=* # CORS allowed origins
|
|
669
|
+
CORS_ALLOWED_METHODS=GET,POST,PUT,DELETE,OPTIONS
|
|
670
|
+
CORS_ALLOWED_HEADERS=Content-Type,Authorization
|
|
551
671
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
672
|
+
# Monitoring & Logging
|
|
673
|
+
ENABLE_REQUEST_COUNTING=true # Enable request metrics
|
|
674
|
+
ENABLE_MESSAGE_COUNTING=true # Enable message metrics
|
|
675
|
+
METRICS_ENDPOINT_ENABLED=true # Enable metrics endpoint
|
|
676
|
+
HEALTH_CHECK_ENABLED=true # Enable health check endpoint
|
|
677
|
+
ENABLE_LOGGING=true # Enable logging
|
|
678
|
+
LOG_LEVEL=info # Log level (debug|info|warn|error)
|
|
679
|
+
LOG_FORMAT=json # Log format (json|text)
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
### Programmatic Server
|
|
683
|
+
|
|
684
|
+
```javascript
|
|
685
|
+
import { createQueenServer } from 'queen-mq/server';
|
|
686
|
+
|
|
687
|
+
const server = await createQueenServer({
|
|
688
|
+
port: 6632,
|
|
689
|
+
database: {
|
|
690
|
+
connectionString: 'postgresql://...',
|
|
691
|
+
poolSize: 20
|
|
692
|
+
},
|
|
693
|
+
encryption: {
|
|
694
|
+
key: 'your-32-byte-hex-key'
|
|
695
|
+
},
|
|
696
|
+
features: {
|
|
697
|
+
retention: true,
|
|
698
|
+
eviction: true
|
|
699
|
+
}
|
|
561
700
|
});
|
|
562
|
-
```
|
|
563
|
-
|
|
564
|
-
### Docker Deployment
|
|
565
|
-
|
|
566
|
-
```dockerfile
|
|
567
|
-
FROM node:22-alpine
|
|
568
|
-
|
|
569
|
-
WORKDIR /app
|
|
570
|
-
COPY package*.json ./
|
|
571
|
-
RUN npm ci --production
|
|
572
|
-
|
|
573
|
-
COPY . .
|
|
574
|
-
|
|
575
|
-
EXPOSE 6632
|
|
576
|
-
CMD ["node", "src/server.js"]
|
|
577
|
-
```
|
|
578
|
-
|
|
579
|
-
```yaml
|
|
580
|
-
# docker-compose.yml
|
|
581
|
-
version: '3.8'
|
|
582
|
-
|
|
583
|
-
services:
|
|
584
|
-
postgres:
|
|
585
|
-
image: postgres:16
|
|
586
|
-
environment:
|
|
587
|
-
POSTGRES_DB: queen
|
|
588
|
-
POSTGRES_USER: queen
|
|
589
|
-
POSTGRES_PASSWORD: queen
|
|
590
|
-
volumes:
|
|
591
|
-
- postgres_data:/var/lib/postgresql/data
|
|
592
|
-
ports:
|
|
593
|
-
- "5432:5432"
|
|
594
|
-
|
|
595
|
-
queen:
|
|
596
|
-
build: .
|
|
597
|
-
ports:
|
|
598
|
-
- "6632:6632"
|
|
599
|
-
environment:
|
|
600
|
-
PG_HOST: postgres
|
|
601
|
-
PG_DB: queen
|
|
602
|
-
PG_USER: queen
|
|
603
|
-
PG_PASSWORD: queen
|
|
604
|
-
DB_POOL_SIZE: 20
|
|
605
|
-
QUEEN_ENCRYPTION_KEY: ${QUEEN_ENCRYPTION_KEY}
|
|
606
|
-
depends_on:
|
|
607
|
-
- postgres
|
|
608
|
-
|
|
609
|
-
volumes:
|
|
610
|
-
postgres_data:
|
|
611
|
-
```
|
|
612
701
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
See the [Configuration](#-configuration) section for a complete list of environment variables.
|
|
616
|
-
|
|
617
|
-
### Database Schema
|
|
618
|
-
|
|
619
|
-
The database schema is automatically created when you run:
|
|
620
|
-
|
|
621
|
-
```bash
|
|
622
|
-
node init-db.js
|
|
702
|
+
await server.start();
|
|
623
703
|
```
|
|
624
704
|
|
|
625
|
-
This creates:
|
|
626
|
-
- `queen.queues` - Top-level message containers
|
|
627
|
-
- `queen.partitions` - Subdivisions within queues (FIFO ordering)
|
|
628
|
-
- `queen.messages` - Individual messages with processing state
|
|
629
|
-
|
|
630
705
|
---
|
|
631
706
|
|
|
632
|
-
##
|
|
633
|
-
|
|
634
|
-
### Architecture
|
|
635
|
-
|
|
636
|
-
Queen uses a two-tier architecture:
|
|
637
|
-
|
|
638
|
-
```
|
|
639
|
-
Queues (optional namespace/task grouping)
|
|
640
|
-
└── Partitions (FIFO ordering, parallel processing)
|
|
641
|
-
└── Messages (lease-based processing)
|
|
642
|
-
```
|
|
643
|
-
|
|
644
|
-
**Key Principles:**
|
|
645
|
-
- **Configuration at queue level**: All settings (priority, lease time, retries) apply to the entire queue
|
|
646
|
-
- **FIFO within partitions**: Messages in the same partition are always processed in order
|
|
647
|
-
- **Partition locking**: Prevents duplicate processing across consumers
|
|
648
|
-
- **Lease-based processing**: Messages automatically return to pending if not acknowledged
|
|
649
|
-
|
|
650
|
-
### Queues and Partitions
|
|
651
|
-
|
|
652
|
-
**Queues** are top-level organizational units. Each queue automatically gets a "Default" partition, and you can create additional partitions for logical separation or parallel processing.
|
|
653
|
-
|
|
654
|
-
```javascript
|
|
655
|
-
// Messages go to "Default" partition
|
|
656
|
-
await client.push('orders', { orderId: 123 });
|
|
657
|
-
|
|
658
|
-
// Push to specific partition
|
|
659
|
-
await client.push('orders/high-priority', { orderId: 456 });
|
|
660
|
-
|
|
661
|
-
// Take from specific partition
|
|
662
|
-
for await (const order of client.take('orders/high-priority')) {
|
|
663
|
-
await processUrgentOrder(order.data);
|
|
664
|
-
await client.ack(order);
|
|
665
|
-
}
|
|
666
|
-
```
|
|
667
|
-
|
|
668
|
-
**Partitions enable:**
|
|
669
|
-
- **Parallel processing**: Different consumers can process different partitions simultaneously
|
|
670
|
-
- **Ordered processing**: FIFO guarantees within each partition
|
|
671
|
-
- **Logical separation**: Different priorities, teams, or workflow stages
|
|
672
|
-
- **Resource isolation**: Lock contention is per-partition
|
|
673
|
-
|
|
674
|
-
### Message Lifecycle
|
|
675
|
-
|
|
676
|
-
```
|
|
677
|
-
pending → processing → completed/failed → (retry) → dead_letter
|
|
678
|
-
```
|
|
679
|
-
|
|
680
|
-
1. **Pending**: Message queued, waiting to be processed
|
|
681
|
-
2. **Processing**: Leased to a worker (with timeout)
|
|
682
|
-
3. **Completed**: Successfully processed
|
|
683
|
-
4. **Failed**: Processing failed (may retry based on `retryLimit`)
|
|
684
|
-
5. **Dead Letter**: Exceeded retry limits
|
|
685
|
-
|
|
686
|
-
### Partition Locking
|
|
687
|
-
|
|
688
|
-
**Partition locking ensures message processing isolation** - when a consumer retrieves messages from a partition, that partition is locked to prevent other consumers from accessing it until:
|
|
689
|
-
|
|
690
|
-
- The consumer acknowledges all messages (releases lock)
|
|
691
|
-
- The lease expires (automatic release)
|
|
692
|
-
- The consumer explicitly releases the partition
|
|
693
|
-
|
|
694
|
-
**Lock Scope:**
|
|
695
|
-
- **Queue Mode**: Each consumer session is unique - locks prevent any other consumer from accessing the partition
|
|
696
|
-
- **Bus Mode**: Locks are per consumer group - different groups can process the same partition independently
|
|
697
|
-
|
|
698
|
-
```javascript
|
|
699
|
-
// Example: Partition locking in action
|
|
700
|
-
|
|
701
|
-
// Consumer 1 takes from partition A (locks it)
|
|
702
|
-
for await (const msg of client.take('orders', { limit: 5 })) {
|
|
703
|
-
// Processing partition A - no other consumer can access it
|
|
704
|
-
await client.ack(msg);
|
|
705
|
-
// Partition A unlocked after all 5 messages acknowledged
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
// Consumer 2 gets messages from partition B (A was locked)
|
|
709
|
-
for await (const msg of client.take('orders', { limit: 5 })) {
|
|
710
|
-
// Processing partition B instead
|
|
711
|
-
await client.ack(msg);
|
|
712
|
-
}
|
|
713
|
-
```
|
|
714
|
-
|
|
715
|
-
### FIFO Ordering
|
|
716
|
-
|
|
717
|
-
Queen provides **strong FIFO guarantees within each partition**:
|
|
718
|
-
|
|
719
|
-
```javascript
|
|
720
|
-
// These messages will be processed in order 1, 2, 3
|
|
721
|
-
await client.push('tasks/user-123', [
|
|
722
|
-
{ step: 1, action: 'create' },
|
|
723
|
-
{ step: 2, action: 'update' },
|
|
724
|
-
{ step: 3, action: 'complete' }
|
|
725
|
-
]);
|
|
726
|
-
|
|
727
|
-
// Consumer will always receive them in order
|
|
728
|
-
for await (const task of client.take('tasks/user-123')) {
|
|
729
|
-
console.log(task.data.step); // Prints: 1, then 2, then 3
|
|
730
|
-
await client.ack(task);
|
|
731
|
-
}
|
|
732
|
-
```
|
|
733
|
-
|
|
734
|
-
**Use cases:**
|
|
735
|
-
- **Per-user operations**: Use user ID as partition for ordered processing
|
|
736
|
-
- **Per-resource operations**: Use resource ID to maintain operation order
|
|
737
|
-
- **Workflow stages**: Use partition to represent different stages
|
|
738
|
-
|
|
739
|
-
### Consumer Groups (Bus Mode)
|
|
740
|
-
|
|
741
|
-
Consumer groups enable **pub-sub messaging** where multiple independent consumers process the same messages:
|
|
742
|
-
|
|
743
|
-
```javascript
|
|
744
|
-
// Push once
|
|
745
|
-
await client.push('events', { type: 'order.created', orderId: 123 });
|
|
746
|
-
|
|
747
|
-
// Multiple services consume independently
|
|
748
|
-
// Service 1: Analytics
|
|
749
|
-
for await (const event of client.take('events@analytics')) {
|
|
750
|
-
await updateAnalytics(event.data);
|
|
751
|
-
await client.ack(event);
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
// Service 2: Notification (gets same message)
|
|
755
|
-
for await (const event of client.take('events@notification')) {
|
|
756
|
-
await sendNotification(event.data);
|
|
757
|
-
await client.ack(event);
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
// Service 3: Audit (also gets same message)
|
|
761
|
-
for await (const event of client.take('events@audit')) {
|
|
762
|
-
await logEvent(event.data);
|
|
763
|
-
await client.ack(event);
|
|
764
|
-
}
|
|
765
|
-
```
|
|
766
|
-
|
|
767
|
-
**Each consumer group maintains:**
|
|
768
|
-
- Independent message status tracking
|
|
769
|
-
- Separate partition leases
|
|
770
|
-
- Individual retry counters
|
|
771
|
-
- Isolated processing state
|
|
707
|
+
## 🔑 Core Concepts
|
|
772
708
|
|
|
773
|
-
###
|
|
709
|
+
### Message Flow
|
|
774
710
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
// Worker 1 gets message 1
|
|
782
|
-
for await (const msg of client.take('tasks')) { }
|
|
783
|
-
|
|
784
|
-
// Worker 2 gets message 2 (different message)
|
|
785
|
-
for await (const msg of client.take('tasks')) { }
|
|
786
|
-
```
|
|
711
|
+
1. **Push**: Messages are encrypted and stored in PostgreSQL with metadata
|
|
712
|
+
2. **Pop/Take**: Messages are leased to consumers with automatic lock management
|
|
713
|
+
3. **Process**: Consumers process messages with automatic lease renewal
|
|
714
|
+
4. **ACK**: Messages are marked complete and leases are released
|
|
715
|
+
5. **Retry**: Failed messages are retried with exponential backoff
|
|
787
716
|
|
|
788
|
-
|
|
789
|
-
```javascript
|
|
790
|
-
// With consumer groups - all groups see all messages
|
|
791
|
-
await client.push('events', { id: 1 });
|
|
717
|
+
### Partition & Lease Management
|
|
792
718
|
|
|
793
|
-
|
|
794
|
-
|
|
719
|
+
- Each queue can have multiple partitions for parallel processing
|
|
720
|
+
- Consumers acquire exclusive leases on partitions
|
|
721
|
+
- Leases include unique IDs for validation and fencing
|
|
722
|
+
- Automatic lease renewal for long-running tasks
|
|
723
|
+
- Dead letter queue for exhausted retries
|
|
795
724
|
|
|
796
|
-
|
|
797
|
-
for await (const msg of client.take('events@group2')) { }
|
|
798
|
-
```
|
|
725
|
+
### Scalability
|
|
799
726
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
await client.ack(job);
|
|
806
|
-
}
|
|
727
|
+
- Horizontal scaling with multiple server instances
|
|
728
|
+
- Partition-based parallelism
|
|
729
|
+
- Connection pooling and query optimization
|
|
730
|
+
- WebSocket support for real-time updates
|
|
731
|
+
- Efficient batch operations
|
|
807
732
|
|
|
808
|
-
|
|
809
|
-
for await (const job of client.take('jobs@monitoring')) {
|
|
810
|
-
await monitorJob(job.data);
|
|
811
|
-
await client.ack(job);
|
|
812
|
-
}
|
|
813
|
-
```
|
|
733
|
+
---
|
|
814
734
|
|
|
815
|
-
|
|
735
|
+
## 📚 HTTP API Reference
|
|
816
736
|
|
|
817
|
-
|
|
737
|
+
### Queue Management
|
|
818
738
|
|
|
819
|
-
```
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
739
|
+
```bash
|
|
740
|
+
# Create/configure queue
|
|
741
|
+
curl -X POST http://localhost:6632/api/v1/configure \
|
|
742
|
+
-H "Content-Type: application/json" \
|
|
743
|
+
-d '{
|
|
744
|
+
"queue": "my-queue",
|
|
745
|
+
"options": {
|
|
746
|
+
"maxRetries": 3,
|
|
747
|
+
"visibilityTimeout": 30000
|
|
748
|
+
}
|
|
749
|
+
}'
|
|
823
750
|
|
|
824
|
-
|
|
751
|
+
# Delete queue
|
|
752
|
+
curl -X DELETE http://localhost:6632/api/v1/configure/my-queue
|
|
825
753
|
```
|
|
826
754
|
|
|
827
|
-
###
|
|
828
|
-
|
|
829
|
-
Messages are "leased" to workers for a specific duration. If not acknowledged within the lease time, they automatically return to pending status:
|
|
830
|
-
|
|
831
|
-
```javascript
|
|
832
|
-
// Configure lease time
|
|
833
|
-
await client.queue('long-tasks', {
|
|
834
|
-
leaseTime: 600 // 10 minutes to process
|
|
835
|
-
});
|
|
755
|
+
### Message Operations
|
|
836
756
|
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
757
|
+
```bash
|
|
758
|
+
# Push messages
|
|
759
|
+
curl -X POST http://localhost:6632/api/v1/push \
|
|
760
|
+
-H "Content-Type: application/json" \
|
|
761
|
+
-d '{
|
|
762
|
+
"queue": "my-queue",
|
|
763
|
+
"messages": [
|
|
764
|
+
{ "data": "message 1" },
|
|
765
|
+
{ "data": "message 2", "priority": 100 }
|
|
766
|
+
]
|
|
767
|
+
}'
|
|
768
|
+
|
|
769
|
+
# Pop messages (take)
|
|
770
|
+
curl -X POST http://localhost:6632/api/v1/pop \
|
|
771
|
+
-H "Content-Type: application/json" \
|
|
772
|
+
-d '{
|
|
773
|
+
"queue": "my-queue",
|
|
774
|
+
"batch": 10,
|
|
775
|
+
"visibilityTimeout": 30000
|
|
776
|
+
}'
|
|
777
|
+
|
|
778
|
+
# Acknowledge messages
|
|
779
|
+
curl -X POST http://localhost:6632/api/v1/ack \
|
|
780
|
+
-H "Content-Type: application/json" \
|
|
781
|
+
-d '{
|
|
782
|
+
"queue": "my-queue",
|
|
783
|
+
"transactionId": "msg-transaction-id",
|
|
784
|
+
"status": "completed",
|
|
785
|
+
"leaseId": "lease-uuid"
|
|
786
|
+
}'
|
|
841
787
|
```
|
|
842
788
|
|
|
843
|
-
###
|
|
789
|
+
### Advanced Operations
|
|
844
790
|
|
|
845
|
-
|
|
791
|
+
```bash
|
|
792
|
+
# Atomic transaction
|
|
793
|
+
curl -X POST http://localhost:6632/api/v1/transaction \
|
|
794
|
+
-H "Content-Type: application/json" \
|
|
795
|
+
-d '{
|
|
796
|
+
"operations": [
|
|
797
|
+
{
|
|
798
|
+
"type": "ack",
|
|
799
|
+
"queue": "input-queue",
|
|
800
|
+
"transactionId": "msg-id",
|
|
801
|
+
"status": "completed"
|
|
802
|
+
},
|
|
803
|
+
{
|
|
804
|
+
"type": "push",
|
|
805
|
+
"queue": "output-queue",
|
|
806
|
+
"messages": [{"data": "processed"}]
|
|
807
|
+
}
|
|
808
|
+
],
|
|
809
|
+
"requiredLeases": ["lease-uuid-1", "lease-uuid-2"]
|
|
810
|
+
}'
|
|
846
811
|
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
}
|
|
812
|
+
# Extend lease
|
|
813
|
+
curl -X POST http://localhost:6632/api/v1/lease/lease-uuid/extend \
|
|
814
|
+
-H "Content-Type: application/json" \
|
|
815
|
+
-d '{}'
|
|
851
816
|
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
});
|
|
855
|
-
// Message won't be available for processing until 1 hour later
|
|
817
|
+
# Get queue status
|
|
818
|
+
curl http://localhost:6632/api/v1/status/my-queue
|
|
856
819
|
```
|
|
857
820
|
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
Batch messages within a time window:
|
|
821
|
+
---
|
|
861
822
|
|
|
862
|
-
|
|
863
|
-
await client.queue('analytics', {
|
|
864
|
-
windowBuffer: 60 // Wait 60 seconds to accumulate messages
|
|
865
|
-
});
|
|
823
|
+
## 📊 Dashboard
|
|
866
824
|
|
|
867
|
-
|
|
868
|
-
```
|
|
825
|
+
Access the real-time dashboard at `http://localhost:6632/`
|
|
869
826
|
|
|
870
|
-
###
|
|
827
|
+
### Features
|
|
828
|
+
- Real-time queue metrics
|
|
829
|
+
- Message browser with search
|
|
830
|
+
- Consumer group monitoring
|
|
831
|
+
- System health indicators
|
|
832
|
+
- Performance graphs
|
|
871
833
|
|
|
834
|
+
### WebSocket Events
|
|
872
835
|
```javascript
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
836
|
+
const ws = new WebSocket('ws://localhost:6632/ws/dashboard');
|
|
837
|
+
ws.on('message', (data) => {
|
|
838
|
+
const event = JSON.parse(data);
|
|
839
|
+
console.log('Queue event:', event);
|
|
876
840
|
});
|
|
877
|
-
|
|
878
|
-
// Failed messages automatically retry
|
|
879
|
-
await client.ack(message, false); // Will retry if retries < 3
|
|
880
|
-
|
|
881
|
-
// After 3 failures, message moves to dead_letter status
|
|
882
841
|
```
|
|
883
842
|
|
|
884
|
-
|
|
843
|
+
---
|
|
885
844
|
|
|
886
|
-
|
|
845
|
+
## 🧪 Testing
|
|
887
846
|
|
|
888
847
|
```bash
|
|
889
|
-
#
|
|
890
|
-
|
|
891
|
-
```
|
|
892
|
-
|
|
893
|
-
```javascript
|
|
894
|
-
await client.queue('sensitive-data', {
|
|
895
|
-
encryptionEnabled: true
|
|
896
|
-
});
|
|
897
|
-
|
|
898
|
-
// Messages encrypted at rest in database
|
|
899
|
-
await client.push('sensitive-data', { ssn: '123-45-6789' });
|
|
900
|
-
```
|
|
901
|
-
|
|
902
|
-
#### 2. Message Retention
|
|
903
|
-
|
|
904
|
-
```javascript
|
|
905
|
-
await client.queue('temp-queue', {
|
|
906
|
-
retentionSeconds: 3600, // Delete pending after 1 hour
|
|
907
|
-
completedRetentionSeconds: 300, // Delete completed after 5 minutes
|
|
908
|
-
retentionEnabled: true
|
|
909
|
-
});
|
|
910
|
-
```
|
|
848
|
+
# Run test suite
|
|
849
|
+
npm test
|
|
911
850
|
|
|
912
|
-
|
|
851
|
+
# Run specific test
|
|
852
|
+
npm test -- --grep "Pipeline"
|
|
913
853
|
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
maxWaitTimeSeconds: 60 // Evict messages older than 1 minute
|
|
917
|
-
});
|
|
854
|
+
# Benchmark
|
|
855
|
+
npm run benchmark
|
|
918
856
|
```
|
|
919
857
|
|
|
920
|
-
### Best Practices
|
|
921
|
-
|
|
922
|
-
**1. Partition Strategy**
|
|
923
|
-
- Use user IDs for per-user ordering
|
|
924
|
-
- Use resource IDs for per-resource ordering
|
|
925
|
-
- Use round-robin for load distribution
|
|
926
|
-
- Keep partition counts manageable (10-100s, not 1000s)
|
|
927
|
-
|
|
928
|
-
**2. Lease Management**
|
|
929
|
-
- Set lease time slightly longer than expected processing time
|
|
930
|
-
- Handle timeouts gracefully
|
|
931
|
-
- Acknowledge messages as soon as processing completes
|
|
932
|
-
|
|
933
|
-
**3. Consumer Group Design**
|
|
934
|
-
- One clear purpose per consumer group
|
|
935
|
-
- Design groups to be independent
|
|
936
|
-
- Ensure operations are idempotent
|
|
937
|
-
|
|
938
|
-
**4. Error Handling**
|
|
939
|
-
- Always wrap processing in try-catch
|
|
940
|
-
- Provide meaningful error messages in ack
|
|
941
|
-
- Use retry limits appropriately
|
|
942
|
-
- Monitor dead letter queue
|
|
943
|
-
|
|
944
858
|
---
|
|
945
859
|
|
|
946
|
-
##
|
|
947
|
-
|
|
948
|
-
Queen uses a **cursor-based consumption model** for optimal performance at scale, providing O(batch_size) constant-time operations regardless of queue depth.
|
|
949
|
-
|
|
950
|
-
### How It Works
|
|
951
|
-
|
|
952
|
-
Traditional message queues scan through all messages to find pending ones, leading to performance degradation as messages accumulate. Queen's cursor-based approach maintains a position marker (cursor) for each consumer, allowing direct access to the next batch of messages.
|
|
953
|
-
|
|
954
|
-
**Partition Cursors:**
|
|
860
|
+
## 🤝 Contributing
|
|
955
861
|
|
|
956
|
-
|
|
957
|
-
- `last_consumed_created_at`: Timestamp of last consumed message
|
|
958
|
-
- `last_consumed_id`: UUID of last consumed message (tie-breaker for same timestamp)
|
|
959
|
-
- `total_messages_consumed`: Running count of consumed messages
|
|
862
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and guidelines.
|
|
960
863
|
|
|
961
|
-
|
|
864
|
+
---
|
|
962
865
|
|
|
963
|
-
|
|
866
|
+
## 📄 License
|
|
964
867
|
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
**1. `take()` - Individual message iterator:**
|
|
968
|
-
```javascript
|
|
969
|
-
// Processes messages one at a time
|
|
970
|
-
for await (const message of client.take('orders', { batch: 1000 })) {
|
|
971
|
-
await processOrder(message.data);
|
|
972
|
-
await client.ack(message);
|
|
973
|
-
}
|
|
974
|
-
```
|
|
975
|
-
|
|
976
|
-
**2. `takeBatch()` - Array iterator (HIGH PERFORMANCE):**
|
|
977
|
-
```javascript
|
|
978
|
-
// Yields arrays of messages - achieves 100k+ msg/s throughput
|
|
979
|
-
for await (const messages of client.takeBatch('orders', { batch: 1000 })) {
|
|
980
|
-
// messages is an array of up to 1000 message objects
|
|
981
|
-
await processBatch(messages.map(m => m.data));
|
|
982
|
-
|
|
983
|
-
// Batch acknowledge - single DB transaction for all messages
|
|
984
|
-
await client.ack(messages);
|
|
985
|
-
}
|
|
986
|
-
```
|
|
987
|
-
|
|
988
|
-
**Under the hood**, both methods use cursor-based batch retrieval:
|
|
989
|
-
|
|
990
|
-
```sql
|
|
991
|
-
-- Cursor-based query (simplified)
|
|
992
|
-
SELECT * FROM messages
|
|
993
|
-
WHERE partition_id = $1
|
|
994
|
-
AND id > $2::uuid -- Start after last cursor position
|
|
995
|
-
ORDER BY created_at ASC, id ASC
|
|
996
|
-
LIMIT $3 -- Batch size
|
|
997
|
-
FOR UPDATE SKIP LOCKED
|
|
998
|
-
```
|
|
999
|
-
|
|
1000
|
-
**Key characteristics:**
|
|
1001
|
-
1. **Constant-time**: Performance stays consistent whether you've consumed 0% or 99% of messages
|
|
1002
|
-
2. **FIFO guarantee**: Messages always returned in creation order
|
|
1003
|
-
3. **Lock-free scanning**: `SKIP LOCKED` prevents contention between consumers
|
|
1004
|
-
4. **Efficient**: No table scans - direct cursor-based access using UUIDv7 (time-ordered)
|
|
1005
|
-
|
|
1006
|
-
**Performance tip:** Use `takeBatch()` with large batch sizes (1,000-10,000) for maximum throughput. The server fetches messages in batches regardless, but `takeBatch()` gives you the array directly, allowing bulk processing and batch acknowledgment in a single operation.
|
|
1007
|
-
|
|
1008
|
-
### Batch Acknowledgment Semantics
|
|
1009
|
-
|
|
1010
|
-
Queen handles batch acknowledgments intelligently:
|
|
1011
|
-
|
|
1012
|
-
**Partial Success** (some messages succeed, some fail):
|
|
1013
|
-
```javascript
|
|
1014
|
-
// Batch: 10,000 messages
|
|
1015
|
-
// Success: 9,999 messages
|
|
1016
|
-
// Failed: 1 message
|
|
1017
|
-
|
|
1018
|
-
// Behavior:
|
|
1019
|
-
// ✅ Cursor advances past all 10,000 messages
|
|
1020
|
-
// ✅ Failed message moved to Dead Letter Queue
|
|
1021
|
-
// ✅ Next take() starts from message 10,001
|
|
1022
|
-
// ✅ FIFO maintained, no redelivery of successful messages
|
|
1023
|
-
```
|
|
1024
|
-
|
|
1025
|
-
**Total Batch Failure** (all messages fail):
|
|
1026
|
-
```javascript
|
|
1027
|
-
// Batch: 10,000 messages
|
|
1028
|
-
// Success: 0 messages
|
|
1029
|
-
// Failed: 10,000 messages
|
|
1030
|
-
|
|
1031
|
-
// Behavior:
|
|
1032
|
-
// ❌ Cursor DOES NOT advance
|
|
1033
|
-
// ❌ Messages NOT moved to DLQ
|
|
1034
|
-
// ✅ Lease released
|
|
1035
|
-
// ✅ Next take() gets SAME batch (retry)
|
|
1036
|
-
// ✅ Allows recovery from transient failures
|
|
1037
|
-
```
|
|
1038
|
-
|
|
1039
|
-
This design handles transient failures (network issues, service outages) gracefully while preventing poison messages from blocking the queue.
|
|
1040
|
-
|
|
1041
|
-
### Performance Comparison
|
|
1042
|
-
|
|
1043
|
-
| Operation | Traditional Approach | Cursor Approach | Improvement |
|
|
1044
|
-
|-----------|---------------------|-----------------|-------------|
|
|
1045
|
-
| Pop @ 0% consumed | O(partition_size) | O(batch_size) | Same |
|
|
1046
|
-
| Pop @ 50% consumed | O(partition_size) | O(batch_size) | **10-100x faster** |
|
|
1047
|
-
| Pop @ 99% consumed | O(partition_size) | O(batch_size) | **100-1000x faster** |
|
|
1048
|
-
|
|
1049
|
-
**Real-world benchmark** (1M messages):
|
|
1050
|
-
```
|
|
1051
|
-
Traditional:
|
|
1052
|
-
Early batches: 300ms per pop
|
|
1053
|
-
Late batches: 3500ms per pop (10x degradation)
|
|
1054
|
-
|
|
1055
|
-
Cursor-based:
|
|
1056
|
-
Early batches: 150ms per pop
|
|
1057
|
-
Late batches: 200ms per pop (constant!)
|
|
1058
|
-
```
|
|
1059
|
-
|
|
1060
|
-
### Dead Letter Queue
|
|
1061
|
-
|
|
1062
|
-
Individual message failures are moved to the Dead Letter Queue for inspection and manual intervention:
|
|
1063
|
-
|
|
1064
|
-
```javascript
|
|
1065
|
-
// Monitor DLQ
|
|
1066
|
-
const response = await fetch('http://localhost:6632/api/v1/analytics/dlq');
|
|
1067
|
-
const dlqMessages = await response.json();
|
|
1068
|
-
|
|
1069
|
-
// Inspect failed messages
|
|
1070
|
-
for (const msg of dlqMessages) {
|
|
1071
|
-
console.log(`Failed: ${msg.error_message}`);
|
|
1072
|
-
|
|
1073
|
-
// After fixing issue, can re-push if needed
|
|
1074
|
-
await client.push(msg.queue, fixedPayload);
|
|
1075
|
-
}
|
|
1076
|
-
```
|
|
1077
|
-
|
|
1078
|
-
**DLQ Query:**
|
|
1079
|
-
```sql
|
|
1080
|
-
SELECT * FROM queen.dead_letter_queue
|
|
1081
|
-
WHERE consumer_group = 'my-group'
|
|
1082
|
-
ORDER BY failed_at DESC
|
|
1083
|
-
LIMIT 100;
|
|
1084
|
-
```
|
|
1085
|
-
|
|
1086
|
-
### Batch Size Guidelines
|
|
1087
|
-
|
|
1088
|
-
Choose batch sizes based on your workload:
|
|
1089
|
-
|
|
1090
|
-
**Smaller batches (100-1,000):**
|
|
1091
|
-
- ✅ Faster individual batch processing
|
|
1092
|
-
- ✅ Less impact if entire batch fails
|
|
1093
|
-
- ✅ Lower memory footprint
|
|
1094
|
-
- ❌ More network round-trips
|
|
1095
|
-
|
|
1096
|
-
**Larger batches (5,000-10,000):**
|
|
1097
|
-
- ✅ Higher throughput (100,000+ msg/sec achievable)
|
|
1098
|
-
- ✅ Fewer network round-trips
|
|
1099
|
-
- ✅ Better database efficiency
|
|
1100
|
-
- ❌ More messages retry if entire batch fails
|
|
1101
|
-
- ❌ Higher memory usage
|
|
1102
|
-
|
|
1103
|
-
**Recommendation:** Start with 1,000-2,000 for balanced performance. Increase to 5,000-10,000 for maximum throughput with reliable processing.
|
|
1104
|
-
|
|
1105
|
-
### Monitoring Cursor Progress
|
|
1106
|
-
|
|
1107
|
-
Track consumption progress via SQL:
|
|
1108
|
-
|
|
1109
|
-
```sql
|
|
1110
|
-
-- View cursor positions
|
|
1111
|
-
SELECT
|
|
1112
|
-
p.name as partition,
|
|
1113
|
-
pc.consumer_group,
|
|
1114
|
-
pc.total_messages_consumed,
|
|
1115
|
-
pc.total_batches_consumed,
|
|
1116
|
-
pc.last_consumed_at,
|
|
1117
|
-
EXTRACT(EPOCH FROM (NOW() - pc.last_consumed_at)) as seconds_since_last_consume
|
|
1118
|
-
FROM queen.partition_cursors pc
|
|
1119
|
-
JOIN queen.partitions p ON p.id = pc.partition_id
|
|
1120
|
-
ORDER BY pc.last_consumed_at DESC;
|
|
1121
|
-
|
|
1122
|
-
-- Monitor DLQ
|
|
1123
|
-
SELECT COUNT(*) as failed_count, consumer_group
|
|
1124
|
-
FROM queen.dead_letter_queue
|
|
1125
|
-
GROUP BY consumer_group;
|
|
1126
|
-
```
|
|
1127
|
-
|
|
1128
|
-
---
|
|
1129
|
-
|
|
1130
|
-
## 🔌 HTTP API Reference
|
|
1131
|
-
|
|
1132
|
-
Base URL: `http://localhost:6632/api/v1`
|
|
1133
|
-
|
|
1134
|
-
### Push Messages
|
|
1135
|
-
|
|
1136
|
-
**Endpoint:** `POST /api/v1/push`
|
|
1137
|
-
|
|
1138
|
-
**Request:**
|
|
1139
|
-
```json
|
|
1140
|
-
{
|
|
1141
|
-
"items": [
|
|
1142
|
-
{
|
|
1143
|
-
"queue": "orders",
|
|
1144
|
-
"partition": "urgent",
|
|
1145
|
-
"payload": { "orderId": 123, "amount": 99.99 },
|
|
1146
|
-
"transactionId": "optional-idempotency-key",
|
|
1147
|
-
"traceId": "550e8400-e29b-41d4-a716-446655440000"
|
|
1148
|
-
}
|
|
1149
|
-
]
|
|
1150
|
-
}
|
|
1151
|
-
```
|
|
1152
|
-
|
|
1153
|
-
**Response:**
|
|
1154
|
-
```json
|
|
1155
|
-
{
|
|
1156
|
-
"messages": [
|
|
1157
|
-
{
|
|
1158
|
-
"id": "018e63b7-6165-453f-88ae-56effa177605",
|
|
1159
|
-
"transactionId": "4dfb0478-655b-4c91-bcd9-b7acacf0400f",
|
|
1160
|
-
"status": "queued"
|
|
1161
|
-
}
|
|
1162
|
-
]
|
|
1163
|
-
}
|
|
1164
|
-
```
|
|
1165
|
-
|
|
1166
|
-
### Pop Messages
|
|
1167
|
-
|
|
1168
|
-
**From specific partition:**
|
|
1169
|
-
```
|
|
1170
|
-
GET /api/v1/pop/queue/{queue}/partition/{partition}?wait=true&timeout=30000&batch=10
|
|
1171
|
-
```
|
|
1172
|
-
|
|
1173
|
-
**From any partition in queue:**
|
|
1174
|
-
```
|
|
1175
|
-
GET /api/v1/pop/queue/{queue}?wait=true&timeout=30000&batch=10
|
|
1176
|
-
```
|
|
1177
|
-
|
|
1178
|
-
**With namespace/task filter:**
|
|
1179
|
-
```
|
|
1180
|
-
GET /api/v1/pop?namespace=ecommerce&task=checkout&wait=true&timeout=30000&batch=10
|
|
1181
|
-
```
|
|
1182
|
-
|
|
1183
|
-
**With consumer group (bus mode):**
|
|
1184
|
-
```
|
|
1185
|
-
GET /api/v1/pop/queue/{queue}?consumerGroup=analytics&subscriptionMode=all&wait=true&timeout=30000
|
|
1186
|
-
```
|
|
1187
|
-
|
|
1188
|
-
**Response:**
|
|
1189
|
-
```json
|
|
1190
|
-
{
|
|
1191
|
-
"messages": [
|
|
1192
|
-
{
|
|
1193
|
-
"id": "018e63b7-6165-453f-88ae-56effa177605",
|
|
1194
|
-
"transactionId": "4dfb0478-655b-4c91-bcd9-b7acacf0400f",
|
|
1195
|
-
"queue": "orders",
|
|
1196
|
-
"partition": "urgent",
|
|
1197
|
-
"data": { "orderId": 123, "amount": 99.99 },
|
|
1198
|
-
"retryCount": 0,
|
|
1199
|
-
"priority": 10,
|
|
1200
|
-
"createdAt": "2024-10-08T12:00:00.000Z",
|
|
1201
|
-
"options": { "leaseTime": 300, "retryLimit": 3 }
|
|
1202
|
-
}
|
|
1203
|
-
]
|
|
1204
|
-
}
|
|
1205
|
-
```
|
|
1206
|
-
|
|
1207
|
-
### Acknowledge Messages
|
|
1208
|
-
|
|
1209
|
-
**Single:**
|
|
1210
|
-
```json
|
|
1211
|
-
POST /api/v1/ack
|
|
1212
|
-
{
|
|
1213
|
-
"transactionId": "4dfb0478-655b-4c91-bcd9-b7acacf0400f",
|
|
1214
|
-
"status": "completed",
|
|
1215
|
-
"consumerGroup": "analytics",
|
|
1216
|
-
"error": null
|
|
1217
|
-
}
|
|
1218
|
-
```
|
|
1219
|
-
|
|
1220
|
-
**Batch:**
|
|
1221
|
-
```json
|
|
1222
|
-
POST /api/v1/ack/batch
|
|
1223
|
-
{
|
|
1224
|
-
"acknowledgments": [
|
|
1225
|
-
{ "transactionId": "uuid-1", "status": "completed" },
|
|
1226
|
-
{ "transactionId": "uuid-2", "status": "failed", "error": "Processing error" }
|
|
1227
|
-
]
|
|
1228
|
-
}
|
|
1229
|
-
```
|
|
1230
|
-
|
|
1231
|
-
### Configure Queue
|
|
1232
|
-
|
|
1233
|
-
```json
|
|
1234
|
-
POST /api/v1/configure
|
|
1235
|
-
{
|
|
1236
|
-
"queue": "orders",
|
|
1237
|
-
"namespace": "ecommerce",
|
|
1238
|
-
"task": "checkout",
|
|
1239
|
-
"options": {
|
|
1240
|
-
"leaseTime": 600,
|
|
1241
|
-
"retryLimit": 5,
|
|
1242
|
-
"priority": 10,
|
|
1243
|
-
"maxSize": 10000,
|
|
1244
|
-
"ttl": 3600,
|
|
1245
|
-
"dlqAfterMaxRetries": true,
|
|
1246
|
-
"delayedProcessing": 0,
|
|
1247
|
-
"windowBuffer": 0,
|
|
1248
|
-
"retentionSeconds": 0,
|
|
1249
|
-
"completedRetentionSeconds": 0,
|
|
1250
|
-
"retentionEnabled": false,
|
|
1251
|
-
"encryptionEnabled": false,
|
|
1252
|
-
"maxWaitTimeSeconds": 0
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
```
|
|
1256
|
-
|
|
1257
|
-
### Analytics
|
|
1258
|
-
|
|
1259
|
-
**Queue statistics:**
|
|
1260
|
-
```
|
|
1261
|
-
GET /api/v1/analytics/queue/{queue}
|
|
1262
|
-
```
|
|
1263
|
-
|
|
1264
|
-
**All queues overview:**
|
|
1265
|
-
```
|
|
1266
|
-
GET /api/v1/analytics/queues
|
|
1267
|
-
```
|
|
1268
|
-
|
|
1269
|
-
**Queue depths:**
|
|
1270
|
-
```
|
|
1271
|
-
GET /api/v1/analytics/queue-depths
|
|
1272
|
-
```
|
|
1273
|
-
|
|
1274
|
-
**Throughput metrics:**
|
|
1275
|
-
```
|
|
1276
|
-
GET /api/v1/analytics/throughput
|
|
1277
|
-
```
|
|
1278
|
-
|
|
1279
|
-
**Queue lag analysis:**
|
|
1280
|
-
```
|
|
1281
|
-
GET /api/v1/analytics/queue-lag?queue=orders
|
|
1282
|
-
```
|
|
1283
|
-
|
|
1284
|
-
### Message Management
|
|
1285
|
-
|
|
1286
|
-
**List messages:**
|
|
1287
|
-
```
|
|
1288
|
-
GET /api/v1/messages?queue=orders&status=pending&limit=100
|
|
1289
|
-
```
|
|
1290
|
-
|
|
1291
|
-
**Get single message:**
|
|
1292
|
-
```
|
|
1293
|
-
GET /api/v1/messages/{transactionId}
|
|
1294
|
-
```
|
|
1295
|
-
|
|
1296
|
-
**Delete message:**
|
|
1297
|
-
```
|
|
1298
|
-
DELETE /api/v1/messages/{transactionId}
|
|
1299
|
-
```
|
|
1300
|
-
|
|
1301
|
-
**Retry failed message:**
|
|
1302
|
-
```
|
|
1303
|
-
POST /api/v1/messages/{transactionId}/retry
|
|
1304
|
-
```
|
|
1305
|
-
|
|
1306
|
-
**Move to dead letter queue:**
|
|
1307
|
-
```
|
|
1308
|
-
POST /api/v1/messages/{transactionId}/dlq
|
|
1309
|
-
```
|
|
1310
|
-
|
|
1311
|
-
**Clear queue:**
|
|
1312
|
-
```
|
|
1313
|
-
DELETE /api/v1/queues/{queue}/clear
|
|
1314
|
-
```
|
|
1315
|
-
|
|
1316
|
-
**Delete queue:**
|
|
1317
|
-
```
|
|
1318
|
-
DELETE /api/v1/resources/queues/{queue}
|
|
1319
|
-
```
|
|
1320
|
-
_Note: Deletes the queue and all its partitions, messages, and related data._
|
|
1321
|
-
|
|
1322
|
-
### System Health
|
|
1323
|
-
|
|
1324
|
-
**Health check:**
|
|
1325
|
-
```
|
|
1326
|
-
GET /health
|
|
1327
|
-
```
|
|
1328
|
-
|
|
1329
|
-
**Detailed metrics:**
|
|
1330
|
-
```
|
|
1331
|
-
GET /metrics
|
|
1332
|
-
```
|
|
1333
|
-
|
|
1334
|
-
### WebSocket (Real-time Updates)
|
|
1335
|
-
|
|
1336
|
-
**Connect:**
|
|
1337
|
-
```javascript
|
|
1338
|
-
const ws = new WebSocket('ws://localhost:6632/ws/dashboard');
|
|
1339
|
-
|
|
1340
|
-
ws.onmessage = (event) => {
|
|
1341
|
-
const { event: eventType, data } = JSON.parse(event.data);
|
|
1342
|
-
// Handle events: message.pushed, message.completed, queue.depth, etc.
|
|
1343
|
-
};
|
|
1344
|
-
```
|
|
1345
|
-
|
|
1346
|
-
**Events:**
|
|
1347
|
-
- `message.pushed` - New message added
|
|
1348
|
-
- `message.processing` - Message being processed
|
|
1349
|
-
- `message.completed` - Message completed
|
|
1350
|
-
- `message.failed` - Message failed
|
|
1351
|
-
- `queue.created` - New queue created
|
|
1352
|
-
- `queue.depth` - Queue depth update (every 5s)
|
|
1353
|
-
- `system.stats` - System statistics (every 10s)
|
|
1354
|
-
|
|
1355
|
-
See [API.md](API.md) for complete API documentation.
|
|
1356
|
-
|
|
1357
|
-
---
|
|
1358
|
-
|
|
1359
|
-
## 📊 Dashboard
|
|
1360
|
-
|
|
1361
|
-
Queen includes a comprehensive web dashboard for monitoring and management.
|
|
1362
|
-
|
|
1363
|
-
[Dashboard](/assets/dashboard-01.png)
|
|
1364
|
-
|
|
1365
|
-
### Access
|
|
1366
|
-
|
|
1367
|
-
1. Start the server: `npm start`
|
|
1368
|
-
2. Open browser: `http://localhost:4000`
|
|
1369
|
-
3. WebSocket connection provides real-time updates
|
|
1370
|
-
|
|
1371
|
-
### Features
|
|
1372
|
-
|
|
1373
|
-
**System Overview**
|
|
1374
|
-
- Real-time metrics: total messages, processing rate, system health
|
|
1375
|
-
- Queue summary with pending/processing/completed counts
|
|
1376
|
-
- Performance indicators: throughput, latency, error rates
|
|
1377
|
-
|
|
1378
|
-
**Queue Management**
|
|
1379
|
-
- Queue list with status and message counts
|
|
1380
|
-
- Partition view with priority indicators
|
|
1381
|
-
- Message browser with search and filter
|
|
1382
|
-
- Retry and DLQ management
|
|
1383
|
-
|
|
1384
|
-
**Real-time Monitoring**
|
|
1385
|
-
- Live updates via WebSocket
|
|
1386
|
-
- Throughput charts (messages per second over time)
|
|
1387
|
-
- Queue depth graphs with trend analysis
|
|
1388
|
-
- Lag monitoring (processing time and backlog)
|
|
1389
|
-
|
|
1390
|
-
**Analytics Dashboard**
|
|
1391
|
-
- Performance metrics per queue
|
|
1392
|
-
- Historical trends and patterns
|
|
1393
|
-
- System health monitoring
|
|
1394
|
-
- Database connections and memory usage
|
|
1395
|
-
|
|
1396
|
-
**Message Browser**
|
|
1397
|
-
- Search by queue, partition, status, time range
|
|
1398
|
-
- View full payload and metadata
|
|
1399
|
-
- Manually retry failed messages
|
|
1400
|
-
- Dead letter queue management
|
|
1401
|
-
|
|
1402
|
-
### Dashboard Development
|
|
1403
|
-
|
|
1404
|
-
The dashboard is built with Vue.js and located in the `dashboard/` directory:
|
|
1405
|
-
|
|
1406
|
-
```bash
|
|
1407
|
-
cd dashboard
|
|
1408
|
-
npm install
|
|
1409
|
-
npm run dev # Development mode
|
|
1410
|
-
npm run build # Production build
|
|
1411
|
-
```
|
|
1412
|
-
|
|
1413
|
-
---
|
|
1414
|
-
|
|
1415
|
-
## ⚙️ Configuration
|
|
1416
|
-
|
|
1417
|
-
All configuration uses environment variables with sensible defaults. Configuration is centralized in `src/config.js`.
|
|
1418
|
-
|
|
1419
|
-
### Server Configuration
|
|
1420
|
-
|
|
1421
|
-
```bash
|
|
1422
|
-
PORT=6632 # Server port (default: 6632)
|
|
1423
|
-
HOST=0.0.0.0 # Server host (default: 0.0.0.0)
|
|
1424
|
-
WORKER_ID=worker-1 # Worker identifier
|
|
1425
|
-
APP_NAME=queen-mq # Application name
|
|
1426
|
-
|
|
1427
|
-
# CORS
|
|
1428
|
-
CORS_MAX_AGE=86400
|
|
1429
|
-
CORS_ALLOWED_ORIGINS=*
|
|
1430
|
-
CORS_ALLOWED_METHODS=GET,POST,PUT,DELETE,OPTIONS
|
|
1431
|
-
CORS_ALLOWED_HEADERS=Content-Type,Authorization
|
|
1432
|
-
```
|
|
1433
|
-
|
|
1434
|
-
### Database Configuration
|
|
1435
|
-
|
|
1436
|
-
```bash
|
|
1437
|
-
# Connection
|
|
1438
|
-
PG_USER=postgres
|
|
1439
|
-
PG_HOST=localhost
|
|
1440
|
-
PG_DB=postgres
|
|
1441
|
-
PG_PASSWORD=postgres
|
|
1442
|
-
PG_PORT=5432
|
|
1443
|
-
|
|
1444
|
-
# Connection pool
|
|
1445
|
-
DB_POOL_SIZE=20 # Max connections
|
|
1446
|
-
DB_IDLE_TIMEOUT=30000 # Idle timeout (ms)
|
|
1447
|
-
DB_CONNECTION_TIMEOUT=2000 # Connection timeout (ms)
|
|
1448
|
-
DB_STATEMENT_TIMEOUT=30000 # Statement timeout (ms)
|
|
1449
|
-
DB_QUERY_TIMEOUT=30000 # Query timeout (ms)
|
|
1450
|
-
DB_MAX_RETRIES=3 # Max retry attempts
|
|
1451
|
-
```
|
|
1452
|
-
|
|
1453
|
-
### Queue Processing
|
|
1454
|
-
|
|
1455
|
-
```bash
|
|
1456
|
-
# Pop defaults
|
|
1457
|
-
DEFAULT_TIMEOUT=30000 # Default pop timeout (ms)
|
|
1458
|
-
MAX_TIMEOUT=60000 # Maximum pop timeout (ms)
|
|
1459
|
-
DEFAULT_BATCH_SIZE=1 # Default batch size
|
|
1460
|
-
BATCH_INSERT_SIZE=1000 # Batch size for bulk inserts
|
|
1461
|
-
|
|
1462
|
-
# Long polling
|
|
1463
|
-
QUEUE_POLL_INTERVAL=100 # Poll interval (ms)
|
|
1464
|
-
QUEUE_POLL_INTERVAL_FILTERED=1000 # Poll interval for filtered pops (ms)
|
|
1465
|
-
|
|
1466
|
-
# Queue defaults
|
|
1467
|
-
DEFAULT_LEASE_TIME=300 # Lease time (seconds)
|
|
1468
|
-
DEFAULT_RETRY_LIMIT=3 # Retry limit
|
|
1469
|
-
DEFAULT_RETRY_DELAY=1000 # Retry delay (ms)
|
|
1470
|
-
DEFAULT_MAX_SIZE=10000 # Max queue size
|
|
1471
|
-
DEFAULT_TTL=3600 # TTL (seconds)
|
|
1472
|
-
DEFAULT_PRIORITY=0 # Priority
|
|
1473
|
-
DEFAULT_DELAYED_PROCESSING=0 # Delayed processing (seconds)
|
|
1474
|
-
DEFAULT_WINDOW_BUFFER=0 # Window buffer (seconds)
|
|
1475
|
-
```
|
|
1476
|
-
|
|
1477
|
-
### Background Jobs
|
|
1478
|
-
|
|
1479
|
-
```bash
|
|
1480
|
-
LEASE_RECLAIM_INTERVAL=5000 # Lease reclamation (ms)
|
|
1481
|
-
RETENTION_INTERVAL=300000 # Retention checks (ms)
|
|
1482
|
-
RETENTION_BATCH_SIZE=1000 # Retention batch size
|
|
1483
|
-
PARTITION_CLEANUP_DAYS=7 # Days before cleaning empty partitions
|
|
1484
|
-
EVICTION_INTERVAL=60000 # Eviction checks (ms)
|
|
1485
|
-
EVICTION_BATCH_SIZE=1000 # Eviction batch size
|
|
1486
|
-
```
|
|
1487
|
-
|
|
1488
|
-
### WebSocket
|
|
1489
|
-
|
|
1490
|
-
```bash
|
|
1491
|
-
WS_COMPRESSION=0 # Compression level
|
|
1492
|
-
WS_MAX_PAYLOAD_LENGTH=16384 # Max payload (bytes)
|
|
1493
|
-
WS_IDLE_TIMEOUT=60 # Idle timeout (seconds)
|
|
1494
|
-
WS_MAX_CONNECTIONS=1000 # Max connections
|
|
1495
|
-
WS_HEARTBEAT_INTERVAL=30000 # Heartbeat (ms)
|
|
1496
|
-
```
|
|
1497
|
-
|
|
1498
|
-
### Encryption
|
|
1499
|
-
|
|
1500
|
-
```bash
|
|
1501
|
-
# Generate key: openssl rand -hex 32
|
|
1502
|
-
QUEEN_ENCRYPTION_KEY=<64-hex-chars> # AES-256-GCM encryption key
|
|
1503
|
-
```
|
|
1504
|
-
|
|
1505
|
-
### Client SDK
|
|
1506
|
-
|
|
1507
|
-
```bash
|
|
1508
|
-
QUEEN_BASE_URL=http://localhost:6632
|
|
1509
|
-
CLIENT_RETRY_ATTEMPTS=3
|
|
1510
|
-
CLIENT_RETRY_DELAY=1000
|
|
1511
|
-
CLIENT_RETRY_BACKOFF=2
|
|
1512
|
-
CLIENT_POOL_SIZE=10
|
|
1513
|
-
CLIENT_REQUEST_TIMEOUT=30000
|
|
1514
|
-
```
|
|
1515
|
-
|
|
1516
|
-
### Queue Options
|
|
1517
|
-
|
|
1518
|
-
```javascript
|
|
1519
|
-
{
|
|
1520
|
-
// Processing
|
|
1521
|
-
leaseTime: 300, // Seconds before lease expires
|
|
1522
|
-
retryLimit: 3, // Max retry attempts
|
|
1523
|
-
priority: 0, // Queue priority (higher = first)
|
|
1524
|
-
delayedProcessing: 0, // Delay in seconds
|
|
1525
|
-
windowBuffer: 0, // Buffer time for batching
|
|
1526
|
-
dlqAfterMaxRetries: true, // Move to DLQ after max retries
|
|
1527
|
-
|
|
1528
|
-
// Encryption (Queue-level)
|
|
1529
|
-
encryptionEnabled: false, // Enable AES-256-GCM encryption
|
|
1530
|
-
|
|
1531
|
-
// Retention (Partition-level)
|
|
1532
|
-
retentionSeconds: 0, // Delete pending messages after X seconds
|
|
1533
|
-
completedRetentionSeconds: 0, // Delete completed/failed after X seconds
|
|
1534
|
-
partitionRetentionSeconds: 0, // Delete empty partitions after X seconds
|
|
1535
|
-
retentionEnabled: false, // Enable retention
|
|
1536
|
-
|
|
1537
|
-
// Eviction (Queue-level)
|
|
1538
|
-
maxWaitTimeSeconds: 0 // Evict messages older than X seconds
|
|
1539
|
-
}
|
|
1540
|
-
```
|
|
1541
|
-
|
|
1542
|
-
---
|
|
1543
|
-
|
|
1544
|
-
## 📚 Full Examples
|
|
1545
|
-
|
|
1546
|
-
### Example 1: Email Queue with Priority
|
|
1547
|
-
|
|
1548
|
-
```javascript
|
|
1549
|
-
import { Queen } from 'queen-mq';
|
|
1550
|
-
|
|
1551
|
-
const client = new Queen({
|
|
1552
|
-
baseUrls: ['http://localhost:6632']
|
|
1553
|
-
});
|
|
1554
|
-
|
|
1555
|
-
// Configure queues with different priorities
|
|
1556
|
-
await client.queue('emails-urgent', {
|
|
1557
|
-
priority: 10,
|
|
1558
|
-
leaseTime: 300,
|
|
1559
|
-
retryLimit: 5
|
|
1560
|
-
});
|
|
1561
|
-
|
|
1562
|
-
await client.queue('emails-normal', {
|
|
1563
|
-
priority: 5,
|
|
1564
|
-
leaseTime: 300,
|
|
1565
|
-
retryLimit: 3
|
|
1566
|
-
});
|
|
1567
|
-
|
|
1568
|
-
// Producer: Send emails
|
|
1569
|
-
async function sendEmails() {
|
|
1570
|
-
// Urgent email
|
|
1571
|
-
await client.push('emails-urgent', {
|
|
1572
|
-
to: 'admin@company.com',
|
|
1573
|
-
subject: 'Critical Alert',
|
|
1574
|
-
body: 'System issue detected',
|
|
1575
|
-
timestamp: Date.now()
|
|
1576
|
-
});
|
|
1577
|
-
|
|
1578
|
-
// Normal email
|
|
1579
|
-
await client.push('emails-normal', {
|
|
1580
|
-
to: 'user@example.com',
|
|
1581
|
-
subject: 'Welcome',
|
|
1582
|
-
body: 'Thanks for signing up',
|
|
1583
|
-
timestamp: Date.now()
|
|
1584
|
-
});
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
|
-
// Consumer: Process emails
|
|
1588
|
-
async function processEmails() {
|
|
1589
|
-
// Urgent emails processed first (higher priority)
|
|
1590
|
-
for await (const email of client.take('emails-urgent', {
|
|
1591
|
-
wait: true,
|
|
1592
|
-
timeout: 30000
|
|
1593
|
-
})) {
|
|
1594
|
-
try {
|
|
1595
|
-
console.log('Sending urgent email:', email.data.to);
|
|
1596
|
-
await sendEmail(email.data);
|
|
1597
|
-
await client.ack(email);
|
|
1598
|
-
} catch (error) {
|
|
1599
|
-
console.error('Failed to send email:', error);
|
|
1600
|
-
await client.ack(email, false, { error: error.message });
|
|
1601
|
-
}
|
|
1602
|
-
}
|
|
1603
|
-
}
|
|
1604
|
-
|
|
1605
|
-
// Send batch of emails
|
|
1606
|
-
await sendEmails();
|
|
1607
|
-
|
|
1608
|
-
// Start processing
|
|
1609
|
-
processEmails().catch(console.error);
|
|
1610
|
-
```
|
|
1611
|
-
|
|
1612
|
-
### Example 2: Task Pipeline
|
|
1613
|
-
|
|
1614
|
-
```javascript
|
|
1615
|
-
import { Queen } from 'queen-mq';
|
|
1616
|
-
|
|
1617
|
-
const client = new Queen({
|
|
1618
|
-
baseUrls: ['http://localhost:6632']
|
|
1619
|
-
});
|
|
1620
|
-
|
|
1621
|
-
// Configure pipeline stages
|
|
1622
|
-
await client.queue('stage-1-validate', { priority: 10 });
|
|
1623
|
-
await client.queue('stage-2-process', { priority: 9 });
|
|
1624
|
-
await client.queue('stage-3-finalize', { priority: 8 });
|
|
1625
|
-
|
|
1626
|
-
// Stage 1: Validate
|
|
1627
|
-
async function validateStage() {
|
|
1628
|
-
for await (const msg of client.take('stage-1-validate', { wait: true })) {
|
|
1629
|
-
try {
|
|
1630
|
-
const validated = await validate(msg.data);
|
|
1631
|
-
await client.ack(msg);
|
|
1632
|
-
|
|
1633
|
-
// Pass to next stage
|
|
1634
|
-
await client.push('stage-2-process', validated);
|
|
1635
|
-
} catch (error) {
|
|
1636
|
-
await client.ack(msg, false, { error: error.message });
|
|
1637
|
-
}
|
|
1638
|
-
}
|
|
1639
|
-
}
|
|
1640
|
-
|
|
1641
|
-
// Stage 2: Process
|
|
1642
|
-
async function processStage() {
|
|
1643
|
-
for await (const msg of client.take('stage-2-process', { wait: true })) {
|
|
1644
|
-
try {
|
|
1645
|
-
const processed = await process(msg.data);
|
|
1646
|
-
await client.ack(msg);
|
|
1647
|
-
|
|
1648
|
-
// Pass to next stage
|
|
1649
|
-
await client.push('stage-3-finalize', processed);
|
|
1650
|
-
} catch (error) {
|
|
1651
|
-
await client.ack(msg, false, { error: error.message });
|
|
1652
|
-
}
|
|
1653
|
-
}
|
|
1654
|
-
}
|
|
1655
|
-
|
|
1656
|
-
// Stage 3: Finalize
|
|
1657
|
-
async function finalizeStage() {
|
|
1658
|
-
for await (const msg of client.take('stage-3-finalize', { wait: true })) {
|
|
1659
|
-
try {
|
|
1660
|
-
await finalize(msg.data);
|
|
1661
|
-
await client.ack(msg);
|
|
1662
|
-
console.log('Pipeline complete:', msg.data.id);
|
|
1663
|
-
} catch (error) {
|
|
1664
|
-
await client.ack(msg, false, { error: error.message });
|
|
1665
|
-
}
|
|
1666
|
-
}
|
|
1667
|
-
}
|
|
1668
|
-
|
|
1669
|
-
// Start pipeline
|
|
1670
|
-
Promise.all([
|
|
1671
|
-
validateStage(),
|
|
1672
|
-
processStage(),
|
|
1673
|
-
finalizeStage()
|
|
1674
|
-
]);
|
|
1675
|
-
|
|
1676
|
-
// Add work to pipeline
|
|
1677
|
-
await client.push('stage-1-validate', { id: 1, data: 'raw data' });
|
|
1678
|
-
```
|
|
1679
|
-
|
|
1680
|
-
### Example 3: Event Streaming (Bus Mode)
|
|
1681
|
-
|
|
1682
|
-
```javascript
|
|
1683
|
-
import { Queen } from 'queen-mq';
|
|
1684
|
-
|
|
1685
|
-
const client = new Queen({
|
|
1686
|
-
baseUrls: ['http://localhost:6632']
|
|
1687
|
-
});
|
|
1688
|
-
|
|
1689
|
-
// Configure event queue
|
|
1690
|
-
await client.queue('events', {
|
|
1691
|
-
priority: 10,
|
|
1692
|
-
leaseTime: 60
|
|
1693
|
-
});
|
|
1694
|
-
|
|
1695
|
-
// Producer: Emit events
|
|
1696
|
-
async function emitEvents() {
|
|
1697
|
-
await client.push('events', {
|
|
1698
|
-
type: 'order.created',
|
|
1699
|
-
orderId: 12345,
|
|
1700
|
-
userId: 789,
|
|
1701
|
-
amount: 99.99,
|
|
1702
|
-
timestamp: Date.now()
|
|
1703
|
-
});
|
|
1704
|
-
}
|
|
1705
|
-
|
|
1706
|
-
// Consumer 1: Analytics Service
|
|
1707
|
-
async function analyticsService() {
|
|
1708
|
-
for await (const event of client.take('events@analytics', {
|
|
1709
|
-
subscriptionMode: 'all', // Replay all messages
|
|
1710
|
-
wait: true
|
|
1711
|
-
})) {
|
|
1712
|
-
console.log('[Analytics] Processing event:', event.data.type);
|
|
1713
|
-
await updateAnalytics(event.data);
|
|
1714
|
-
await client.ack(event, true, { group: 'analytics' });
|
|
1715
|
-
}
|
|
1716
|
-
}
|
|
1717
|
-
|
|
1718
|
-
// Consumer 2: Notification Service
|
|
1719
|
-
async function notificationService() {
|
|
1720
|
-
for await (const event of client.take('events@notifications', {
|
|
1721
|
-
subscriptionMode: 'new', // Only new messages
|
|
1722
|
-
wait: true
|
|
1723
|
-
})) {
|
|
1724
|
-
console.log('[Notifications] Processing event:', event.data.type);
|
|
1725
|
-
await sendNotification(event.data);
|
|
1726
|
-
await client.ack(event, true, { group: 'notifications' });
|
|
1727
|
-
}
|
|
1728
|
-
}
|
|
1729
|
-
|
|
1730
|
-
// Consumer 3: Audit Service
|
|
1731
|
-
async function auditService() {
|
|
1732
|
-
for await (const event of client.take('events@audit', {
|
|
1733
|
-
subscriptionMode: 'all', // Log everything
|
|
1734
|
-
wait: true
|
|
1735
|
-
})) {
|
|
1736
|
-
console.log('[Audit] Logging event:', event.data.type);
|
|
1737
|
-
await logToAudit(event.data);
|
|
1738
|
-
await client.ack(event, true, { group: 'audit' });
|
|
1739
|
-
}
|
|
1740
|
-
}
|
|
1741
|
-
|
|
1742
|
-
// Start all services (they all see the same events)
|
|
1743
|
-
Promise.all([
|
|
1744
|
-
analyticsService(),
|
|
1745
|
-
notificationService(),
|
|
1746
|
-
auditService()
|
|
1747
|
-
]);
|
|
1748
|
-
|
|
1749
|
-
// Emit events
|
|
1750
|
-
await emitEvents();
|
|
1751
|
-
```
|
|
1752
|
-
|
|
1753
|
-
### Example 4: Batch Processing (High Throughput)
|
|
1754
|
-
|
|
1755
|
-
```javascript
|
|
1756
|
-
import { Queen } from 'queen-mq';
|
|
1757
|
-
|
|
1758
|
-
const client = new Queen({
|
|
1759
|
-
baseUrls: ['http://localhost:6632']
|
|
1760
|
-
});
|
|
1761
|
-
|
|
1762
|
-
// Configure for batch processing
|
|
1763
|
-
await client.queue('data-processing', {
|
|
1764
|
-
priority: 5,
|
|
1765
|
-
leaseTime: 600, // 10 minutes for batch
|
|
1766
|
-
windowBuffer: 30 // Buffer for 30 seconds
|
|
1767
|
-
});
|
|
1768
|
-
|
|
1769
|
-
// Producer: Send data
|
|
1770
|
-
async function sendData() {
|
|
1771
|
-
const records = [];
|
|
1772
|
-
for (let i = 0; i < 100000; i++) {
|
|
1773
|
-
records.push({ id: i, value: Math.random() });
|
|
1774
|
-
}
|
|
1775
|
-
|
|
1776
|
-
// Push in batches
|
|
1777
|
-
await client.push('data-processing/analytics', records);
|
|
1778
|
-
}
|
|
1779
|
-
|
|
1780
|
-
// Consumer: HIGH PERFORMANCE batch processor using takeBatch()
|
|
1781
|
-
async function batchProcessor() {
|
|
1782
|
-
const BATCH_SIZE = 5000; // Large batches for 100k+ msg/s throughput
|
|
1783
|
-
|
|
1784
|
-
// takeBatch() yields arrays directly - no manual batching needed!
|
|
1785
|
-
for await (const messages of client.takeBatch('data-processing/analytics', {
|
|
1786
|
-
batch: BATCH_SIZE,
|
|
1787
|
-
wait: true,
|
|
1788
|
-
timeout: 30000
|
|
1789
|
-
})) {
|
|
1790
|
-
try {
|
|
1791
|
-
console.log(`Processing batch of ${messages.length} records`);
|
|
1792
|
-
|
|
1793
|
-
// Extract data
|
|
1794
|
-
const records = messages.map(m => m.data);
|
|
1795
|
-
|
|
1796
|
-
// Bulk process (single DB operation)
|
|
1797
|
-
await bulkInsertToDatabase(records);
|
|
1798
|
-
|
|
1799
|
-
// Batch acknowledge (single DB transaction!)
|
|
1800
|
-
await client.ack(messages);
|
|
1801
|
-
|
|
1802
|
-
console.log(`✓ Batch complete in single transaction`);
|
|
1803
|
-
} catch (error) {
|
|
1804
|
-
console.error('Batch processing failed:', error);
|
|
1805
|
-
|
|
1806
|
-
// Mark entire batch as failed (single transaction)
|
|
1807
|
-
await client.ack(messages, false, { error: error.message });
|
|
1808
|
-
}
|
|
1809
|
-
}
|
|
1810
|
-
}
|
|
1811
|
-
|
|
1812
|
-
// Run
|
|
1813
|
-
await sendData();
|
|
1814
|
-
await batchProcessor();
|
|
1815
|
-
|
|
1816
|
-
// Performance characteristics:
|
|
1817
|
-
// - Batch size 5000: ~100,000 messages/second
|
|
1818
|
-
// - Single DB transaction per batch (fetch + ack)
|
|
1819
|
-
// - Constant memory usage
|
|
1820
|
-
// - No performance degradation as queue grows
|
|
1821
|
-
```
|
|
1822
|
-
|
|
1823
|
-
### Example 5: Scheduled Jobs
|
|
1824
|
-
|
|
1825
|
-
```javascript
|
|
1826
|
-
import { Queen } from 'queen-mq';
|
|
1827
|
-
|
|
1828
|
-
const client = new Queen({
|
|
1829
|
-
baseUrls: ['http://localhost:6632']
|
|
1830
|
-
});
|
|
1831
|
-
|
|
1832
|
-
// Configure with delayed processing
|
|
1833
|
-
await client.queue('scheduled-jobs', {
|
|
1834
|
-
delayedProcessing: 3600, // 1 hour delay
|
|
1835
|
-
priority: 5
|
|
1836
|
-
});
|
|
1837
|
-
|
|
1838
|
-
// Schedule a job
|
|
1839
|
-
async function scheduleReport() {
|
|
1840
|
-
await client.push('scheduled-jobs/daily-reports', {
|
|
1841
|
-
reportType: 'daily-sales',
|
|
1842
|
-
date: new Date().toISOString().split('T')[0],
|
|
1843
|
-
recipients: ['manager@company.com'],
|
|
1844
|
-
scheduledAt: Date.now()
|
|
1845
|
-
});
|
|
1846
|
-
|
|
1847
|
-
console.log('Report scheduled for processing in 1 hour');
|
|
1848
|
-
}
|
|
1849
|
-
|
|
1850
|
-
// Process scheduled jobs
|
|
1851
|
-
async function processScheduledJobs() {
|
|
1852
|
-
for await (const job of client.take('scheduled-jobs/daily-reports', {
|
|
1853
|
-
wait: true
|
|
1854
|
-
})) {
|
|
1855
|
-
try {
|
|
1856
|
-
console.log('Generating report:', job.data.reportType);
|
|
1857
|
-
await generateReport(job.data);
|
|
1858
|
-
await client.ack(job);
|
|
1859
|
-
} catch (error) {
|
|
1860
|
-
await client.ack(job, false, { error: error.message });
|
|
1861
|
-
}
|
|
1862
|
-
}
|
|
1863
|
-
}
|
|
1864
|
-
|
|
1865
|
-
await scheduleReport();
|
|
1866
|
-
processScheduledJobs().catch(console.error);
|
|
1867
|
-
```
|
|
1868
|
-
|
|
1869
|
-
### Example 6: Rate Limiting
|
|
1870
|
-
|
|
1871
|
-
```javascript
|
|
1872
|
-
import { Queen } from 'queen-mq';
|
|
1873
|
-
|
|
1874
|
-
const client = new Queen({
|
|
1875
|
-
baseUrls: ['http://localhost:6632']
|
|
1876
|
-
});
|
|
1877
|
-
|
|
1878
|
-
await client.queue('api-calls', {
|
|
1879
|
-
priority: 5,
|
|
1880
|
-
leaseTime: 60
|
|
1881
|
-
});
|
|
1882
|
-
|
|
1883
|
-
// Producer: Queue API calls
|
|
1884
|
-
async function queueApiCalls(calls) {
|
|
1885
|
-
await client.push('api-calls', calls);
|
|
1886
|
-
}
|
|
1887
|
-
|
|
1888
|
-
// Consumer: Rate-limited processor (10 calls per second max)
|
|
1889
|
-
async function rateLimitedProcessor() {
|
|
1890
|
-
const RATE_LIMIT = 10; // calls per second
|
|
1891
|
-
const INTERVAL = 1000; // 1 second
|
|
1892
|
-
|
|
1893
|
-
let callsThisInterval = 0;
|
|
1894
|
-
let intervalStart = Date.now();
|
|
1895
|
-
|
|
1896
|
-
for await (const call of client.take('api-calls', { wait: true })) {
|
|
1897
|
-
// Check if we need to wait
|
|
1898
|
-
if (callsThisInterval >= RATE_LIMIT) {
|
|
1899
|
-
const elapsed = Date.now() - intervalStart;
|
|
1900
|
-
if (elapsed < INTERVAL) {
|
|
1901
|
-
await new Promise(r => setTimeout(r, INTERVAL - elapsed));
|
|
1902
|
-
}
|
|
1903
|
-
callsThisInterval = 0;
|
|
1904
|
-
intervalStart = Date.now();
|
|
1905
|
-
}
|
|
1906
|
-
|
|
1907
|
-
try {
|
|
1908
|
-
await makeApiCall(call.data);
|
|
1909
|
-
await client.ack(call);
|
|
1910
|
-
callsThisInterval++;
|
|
1911
|
-
} catch (error) {
|
|
1912
|
-
await client.ack(call, false, { error: error.message });
|
|
1913
|
-
}
|
|
1914
|
-
}
|
|
1915
|
-
}
|
|
1916
|
-
|
|
1917
|
-
// Generate calls
|
|
1918
|
-
const calls = Array.from({ length: 100 }, (_, i) => ({
|
|
1919
|
-
id: i,
|
|
1920
|
-
endpoint: '/api/data',
|
|
1921
|
-
method: 'GET'
|
|
1922
|
-
}));
|
|
1923
|
-
|
|
1924
|
-
await queueApiCalls(calls);
|
|
1925
|
-
rateLimitedProcessor().catch(console.error);
|
|
1926
|
-
```
|
|
1927
|
-
|
|
1928
|
-
### Example 7: Enterprise Features
|
|
1929
|
-
|
|
1930
|
-
```javascript
|
|
1931
|
-
import { Queen } from 'queen-mq';
|
|
1932
|
-
|
|
1933
|
-
const client = new Queen({
|
|
1934
|
-
baseUrls: ['http://localhost:6632']
|
|
1935
|
-
});
|
|
1936
|
-
|
|
1937
|
-
// Configure with all enterprise features
|
|
1938
|
-
await client.queue('production-queue', {
|
|
1939
|
-
// Encryption
|
|
1940
|
-
encryptionEnabled: true,
|
|
1941
|
-
|
|
1942
|
-
// Retention
|
|
1943
|
-
retentionSeconds: 86400, // Delete pending after 24 hours
|
|
1944
|
-
completedRetentionSeconds: 3600, // Delete completed after 1 hour
|
|
1945
|
-
retentionEnabled: true,
|
|
1946
|
-
|
|
1947
|
-
// Eviction (SLA enforcement)
|
|
1948
|
-
maxWaitTimeSeconds: 600, // Evict messages older than 10 minutes
|
|
1949
|
-
|
|
1950
|
-
// Standard options
|
|
1951
|
-
priority: 10,
|
|
1952
|
-
leaseTime: 300,
|
|
1953
|
-
retryLimit: 3,
|
|
1954
|
-
dlqAfterMaxRetries: true
|
|
1955
|
-
});
|
|
1956
|
-
|
|
1957
|
-
// Push sensitive data (will be encrypted)
|
|
1958
|
-
await client.push('production-queue', {
|
|
1959
|
-
userId: 123,
|
|
1960
|
-
creditCard: '4111-1111-1111-1111',
|
|
1961
|
-
amount: 99.99,
|
|
1962
|
-
timestamp: Date.now()
|
|
1963
|
-
});
|
|
1964
|
-
|
|
1965
|
-
// Process (data decrypted automatically)
|
|
1966
|
-
for await (const message of client.take('production-queue', { wait: true })) {
|
|
1967
|
-
console.log('Processing encrypted data:', message.data.userId);
|
|
1968
|
-
await processPayment(message.data);
|
|
1969
|
-
await client.ack(message);
|
|
1970
|
-
}
|
|
1971
|
-
```
|
|
1972
|
-
|
|
1973
|
-
---
|
|
1974
|
-
|
|
1975
|
-
## 🧪 Testing
|
|
1976
|
-
|
|
1977
|
-
Queen includes a comprehensive test suite covering all features.
|
|
1978
|
-
|
|
1979
|
-
### Run Tests
|
|
1980
|
-
|
|
1981
|
-
```bash
|
|
1982
|
-
# Start the server first
|
|
1983
|
-
npm start
|
|
1984
|
-
|
|
1985
|
-
# Run all tests
|
|
1986
|
-
node src/test/test-new.js
|
|
1987
|
-
|
|
1988
|
-
# Run specific test categories
|
|
1989
|
-
node src/test/test-new.js core # Core features
|
|
1990
|
-
node src/test/test-new.js partition # Partition locking
|
|
1991
|
-
node src/test/test-new.js enterprise # Enterprise features
|
|
1992
|
-
node src/test/test-new.js bus # Bus mode
|
|
1993
|
-
node src/test/test-new.js edge # Edge cases
|
|
1994
|
-
node src/test/test-new.js advanced # Advanced patterns
|
|
1995
|
-
|
|
1996
|
-
# Show help
|
|
1997
|
-
node src/test/test-new.js help
|
|
1998
|
-
```
|
|
1999
|
-
|
|
2000
|
-
### Test Coverage
|
|
2001
|
-
|
|
2002
|
-
The test suite verifies:
|
|
2003
|
-
|
|
2004
|
-
**Core Features:**
|
|
2005
|
-
- Queue creation and configuration
|
|
2006
|
-
- Single and batch message push
|
|
2007
|
-
- Message take and acknowledgment
|
|
2008
|
-
- Delayed processing
|
|
2009
|
-
- Partition FIFO ordering
|
|
2010
|
-
|
|
2011
|
-
**Partition Locking:**
|
|
2012
|
-
- Lock acquisition and release
|
|
2013
|
-
- Bus mode partition locking
|
|
2014
|
-
- Specific partition locking
|
|
2015
|
-
- Namespace/task filtering with locking
|
|
2016
|
-
|
|
2017
|
-
**Enterprise Features:**
|
|
2018
|
-
- AES-256-GCM encryption
|
|
2019
|
-
- Message retention policies
|
|
2020
|
-
- Message eviction
|
|
2021
|
-
- Combined enterprise features
|
|
2022
|
-
|
|
2023
|
-
**Bus Mode:**
|
|
2024
|
-
- Consumer groups
|
|
2025
|
-
- Subscription modes (all, new, from)
|
|
2026
|
-
- Consumer group isolation
|
|
2027
|
-
- Mixed mode (queue + bus)
|
|
2028
|
-
|
|
2029
|
-
**Edge Cases:**
|
|
2030
|
-
- Empty and null payloads
|
|
2031
|
-
- Very large payloads
|
|
2032
|
-
- Concurrent operations
|
|
2033
|
-
- Lease expiration
|
|
2034
|
-
- SQL injection prevention
|
|
2035
|
-
- XSS prevention
|
|
2036
|
-
|
|
2037
|
-
**Advanced Patterns:**
|
|
2038
|
-
- Multi-stage pipelines
|
|
2039
|
-
- Fan-out/fan-in
|
|
2040
|
-
- Priority scenarios
|
|
2041
|
-
- Dead letter queue
|
|
2042
|
-
- Circuit breaker
|
|
2043
|
-
- Message deduplication
|
|
2044
|
-
- Event sourcing
|
|
2045
|
-
|
|
2046
|
-
### Test Results
|
|
2047
|
-
|
|
2048
|
-
Example output:
|
|
2049
|
-
```
|
|
2050
|
-
🚀 Starting Queen Message Queue Test Suite
|
|
2051
|
-
Using the new minimalist Queen client interface
|
|
2052
|
-
================================================================================
|
|
2053
|
-
|
|
2054
|
-
📦 CORE FEATURES
|
|
2055
|
-
----------------------------------------
|
|
2056
|
-
✅ Queue Creation Policy
|
|
2057
|
-
✅ Single Message Push
|
|
2058
|
-
✅ Batch Message Push
|
|
2059
|
-
✅ Queue Configuration
|
|
2060
|
-
✅ Take and Acknowledgment
|
|
2061
|
-
✅ Delayed Processing
|
|
2062
|
-
✅ Partition FIFO Ordering
|
|
2063
|
-
|
|
2064
|
-
🔒 PARTITION LOCKING
|
|
2065
|
-
----------------------------------------
|
|
2066
|
-
✅ Partition Locking
|
|
2067
|
-
✅ Bus Partition Locking
|
|
2068
|
-
✅ Specific Partition Locking
|
|
2069
|
-
✅ Namespace Task Filtering
|
|
2070
|
-
✅ Namespace Task Bus Mode
|
|
2071
|
-
|
|
2072
|
-
📈 Test Summary
|
|
2073
|
-
================================================================================
|
|
2074
|
-
Total: 42 | Passed: 42 | Failed: 0 | Duration: 45.2s
|
|
2075
|
-
```
|
|
2076
|
-
|
|
2077
|
-
---
|
|
2078
|
-
|
|
2079
|
-
## 🤝 Contributing
|
|
2080
|
-
|
|
2081
|
-
We welcome contributions! Here's how to get started:
|
|
2082
|
-
|
|
2083
|
-
1. **Fork the repository**
|
|
2084
|
-
2. **Create a feature branch**: `git checkout -b feature/amazing-feature`
|
|
2085
|
-
3. **Make your changes**
|
|
2086
|
-
4. **Run the test suite**: `node src/test/test-new.js`
|
|
2087
|
-
5. **Commit your changes**: `git commit -m 'Add amazing feature'`
|
|
2088
|
-
6. **Push to the branch**: `git push origin feature/amazing-feature`
|
|
2089
|
-
7. **Open a Pull Request**
|
|
2090
|
-
|
|
2091
|
-
### Development Setup
|
|
2092
|
-
|
|
2093
|
-
```bash
|
|
2094
|
-
# Clone your fork
|
|
2095
|
-
git clone https://github.com/your-username/queen
|
|
2096
|
-
cd queen
|
|
2097
|
-
|
|
2098
|
-
# Install dependencies
|
|
2099
|
-
nvm use 22
|
|
2100
|
-
npm install
|
|
2101
|
-
|
|
2102
|
-
# Initialize database
|
|
2103
|
-
node init-db.js
|
|
2104
|
-
|
|
2105
|
-
# Start server
|
|
2106
|
-
npm start
|
|
2107
|
-
|
|
2108
|
-
# Run tests
|
|
2109
|
-
node src/test/test-new.js
|
|
2110
|
-
```
|
|
2111
|
-
|
|
2112
|
-
### Code Style
|
|
2113
|
-
|
|
2114
|
-
- Use ES6+ features
|
|
2115
|
-
- Follow existing code style
|
|
2116
|
-
- Add comments for complex logic
|
|
2117
|
-
- Write tests for new features
|
|
2118
|
-
|
|
2119
|
-
---
|
|
2120
|
-
|
|
2121
|
-
## 📄 License
|
|
2122
|
-
|
|
2123
|
-
Apache License 2.0 - see [LICENSE.md](LICENSE.md) for details.
|
|
2124
|
-
|
|
2125
|
-
---
|
|
2126
|
-
|
|
2127
|
-
## 🔗 Links
|
|
2128
|
-
|
|
2129
|
-
- **Repository**: [github.com/smartpricing/queen](https://github.com/smartpricing/queen)
|
|
2130
|
-
- **Documentation**: See `docs/` directory
|
|
2131
|
-
- **Issues**: [GitHub Issues](https://github.com/smartpricing/queen/issues)
|
|
2132
|
-
- **API Reference**: [API.md](API.md)
|
|
2133
|
-
|
|
2134
|
-
---
|
|
2135
|
-
|
|
2136
|
-
## 📈 Performance
|
|
2137
|
-
|
|
2138
|
-
**Benchmarks** (PostgreSQL 16, Node.js 22, cursor-based consumption):
|
|
2139
|
-
- **Throughput**: 100,000+ messages/second with batch operations
|
|
2140
|
-
- **Latency**: < 10ms for immediate pop operations
|
|
2141
|
-
- **Constant-time consumption**: O(batch_size) regardless of queue depth
|
|
2142
|
-
- **Concurrent Connections**: 1,000+ long polling connections
|
|
2143
|
-
- **Database**: Optimized with proper indexing and connection pooling
|
|
2144
|
-
|
|
2145
|
-
**Cursor-Based Architecture Benefits:**
|
|
2146
|
-
- **No performance degradation**: Consistent speed whether queue has 1K or 1B messages
|
|
2147
|
-
- **Predictable latency**: 150-200ms per batch throughout entire queue lifecycle
|
|
2148
|
-
- **Efficient batch processing**: Direct cursor access eliminates table scans
|
|
2149
|
-
- **Scalable to billions**: UUIDv7-based cursor positioning
|
|
2150
|
-
|
|
2151
|
-
**Additional Optimization Features:**
|
|
2152
|
-
- Connection pooling with configurable size
|
|
2153
|
-
- Resource caching for queue/partition lookups
|
|
2154
|
-
- Batch operations for bulk inserts/updates (up to 10,000 messages per batch)
|
|
2155
|
-
- Optimized SQL queries with proper indexes
|
|
2156
|
-
- Event-driven architecture for minimal polling overhead
|
|
2157
|
-
- Long polling for real-time message delivery
|
|
2158
|
-
- SKIP LOCKED for lock-free concurrent consumption
|
|
2159
|
-
|
|
2160
|
-
---
|
|
2161
|
-
|
|
2162
|
-
## 🎯 Roadmap
|
|
2163
|
-
|
|
2164
|
-
- [ ] **Message Scheduling**: Cron-like scheduling for recurring jobs
|
|
2165
|
-
- [ ] **Client Libraries**: Python, Go, Java clients
|
|
2166
|
-
- [ ] **Kubernetes Operator**: Native K8s support
|
|
2167
|
-
|
|
2168
|
-
---
|
|
2169
|
-
|
|
2170
|
-
<div align="center">
|
|
2171
|
-
|
|
2172
|
-
**Queen Message Queue System** - Built for performance, reliability, and developer happiness 🚀
|
|
2173
|
-
|
|
2174
|
-
Made with ❤️ by [Smartness](https://github.com/smartpricing)
|
|
2175
|
-
|
|
2176
|
-
</div>
|
|
868
|
+
Apache 2.0 - see [LICENSE.md](LICENSE.md)
|