queen-mq 0.1.6 → 0.2.11
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 +166 -2078
- package/{src → client-js}/benchmark/consumer.js +5 -3
- package/{src → client-js}/benchmark/producer.js +9 -4
- package/client-js/client/client.js +1511 -0
- package/{src → client-js}/client/index.js +1 -4
- package/{src → client-js}/client/utils/http.js +3 -2
- package/{src → client-js}/client/utils/retry.js +7 -1
- package/client-js/test/advanced-client-tests.js +761 -0
- package/{src → client-js}/test/advanced-pattern-tests.js +2 -2
- package/{src → client-js}/test/bus-mode-tests.js +6 -6
- package/{src → client-js}/test/core-tests.js +8 -3
- package/{src → client-js}/test/edge-case-tests.js +4 -3
- package/client-js/test/human.js +162 -0
- package/client-js/test/qos0-tests.js +334 -0
- package/{src → client-js}/test/test-new.js +78 -1
- package/package.json +8 -29
- package/init-db.js +0 -20
- package/src/client/client.js +0 -586
- package/src/cluster-server.js +0 -242
- package/src/config.js +0 -229
- package/src/database/connection.js +0 -129
- package/src/database/poolManager.js +0 -199
- package/src/database/schema-v2.sql +0 -278
- package/src/managers/eventManager.js +0 -59
- package/src/managers/queueManagerOptimized.js +0 -1431
- package/src/managers/resourceCache.js +0 -96
- package/src/managers/systemEventManager.js +0 -132
- package/src/routes/ack.js +0 -26
- package/src/routes/configure.js +0 -46
- package/src/routes/messages.js +0 -368
- package/src/routes/pop.js +0 -69
- package/src/routes/push.js +0 -28
- package/src/routes/resources.js +0 -330
- package/src/routes/status.js +0 -1037
- package/src/server.js +0 -1528
- package/src/test/MIGRATION_ISSUES.md +0 -174
- package/src/test/test.js +0 -4524
- package/src/utils/streaming.js +0 -231
- package/src/webapp-dist/assets/Analytics-DJrE4Rz2.css +0 -1
- package/src/webapp-dist/assets/Analytics-HEMXKmfJ.js +0 -1
- package/src/webapp-dist/assets/ConfirmDialog-YEoCdE7x.js +0 -1
- package/src/webapp-dist/assets/ConsumerGroups-CC5VJywp.css +0 -1
- package/src/webapp-dist/assets/ConsumerGroups-D8Z20HhZ.js +0 -1
- package/src/webapp-dist/assets/Dashboard-BRFOz6it.css +0 -1
- package/src/webapp-dist/assets/Dashboard-Bpx9I32h.js +0 -1
- package/src/webapp-dist/assets/LoadingSpinner-BPttXcbs.js +0 -1
- package/src/webapp-dist/assets/Messages-BOfVHKma.css +0 -1
- package/src/webapp-dist/assets/Messages-EKURCbLl.js +0 -1
- package/src/webapp-dist/assets/QueueDetail-BXXA7xoZ.css +0 -1
- package/src/webapp-dist/assets/QueueDetail-Bmxg0KiA.js +0 -1
- package/src/webapp-dist/assets/Queues-CvrJkrZT.css +0 -1
- package/src/webapp-dist/assets/Queues-yUx9_3-9.js +0 -1
- package/src/webapp-dist/assets/StatusBadge-C1bBtXHh.css +0 -1
- package/src/webapp-dist/assets/StatusBadge-C6N84XmO.js +0 -1
- package/src/webapp-dist/assets/analytics-BapTekLM.js +0 -1
- package/src/webapp-dist/assets/colors-5z6Q_kUr.js +0 -18
- package/src/webapp-dist/assets/index-B7w2bKT6.css +0 -1
- package/src/webapp-dist/assets/index-CUSnwYTH.js +0 -31
- package/src/webapp-dist/assets/messages-CRV6E-xU.js +0 -1
- package/src/webapp-dist/assets/queen-logo-blue.svg +0 -210
- package/src/webapp-dist/assets/queen-logo-cyan.svg +0 -210
- package/src/webapp-dist/assets/queen-logo-indigo.svg +0 -210
- package/src/webapp-dist/assets/queen-logo-orange.svg +0 -210
- package/src/webapp-dist/assets/queen-logo-pink.svg +0 -210
- package/src/webapp-dist/assets/queen-logo-purple.svg +0 -210
- package/src/webapp-dist/assets/queen-logo-rose.svg +0 -239
- package/src/webapp-dist/assets/queen-logo.svg +0 -263
- package/src/webapp-dist/assets/queues-BdUbxfBC.js +0 -1
- package/src/webapp-dist/assets/resources-Bs9U-ceF.js +0 -1
- package/src/webapp-dist/index.html +0 -15
- package/src/websocket/wsServer.js +0 -228
- /package/{src → client-js}/benchmark/consumer_multi.js +0 -0
- /package/{src → client-js}/benchmark/producer_multi.js +0 -0
- /package/{src → client-js}/client/utils/loadBalancer.js +0 -0
- /package/{src → client-js}/services/encryptionService.js +0 -0
- /package/{src → client-js}/services/evictionService.js +0 -0
- /package/{src → client-js}/services/retentionService.js +0 -0
- /package/{src → client-js}/services/startupSync.js +0 -0
- /package/{src → client-js}/test/README.md +0 -0
- /package/{src → client-js}/test/enterprise-tests.js +0 -0
- /package/{src → client-js}/test/partition-locking-tests.js +0 -0
- /package/{src → client-js}/test/utils.js +0 -0
- /package/{src → client-js}/test/window-buffer-test.js +0 -0
- /package/{src → client-js}/utils/logger.js +0 -0
- /package/{src → client-js}/utils/uuid.js +0 -0
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Queen - PostgreSQL-backed Message Queue
|
|
1
|
+
# Queen MQ - PostgreSQL-backed C++ Message Queue
|
|
2
2
|
|
|
3
3
|
<div align="center">
|
|
4
4
|
|
|
@@ -7,2170 +7,258 @@
|
|
|
7
7
|
[](LICENSE.md)
|
|
8
8
|
[](https://nodejs.org/)
|
|
9
9
|
|
|
10
|
-
[Quick Start](
|
|
10
|
+
[Quick Start](#js-client-usage) • [Examples](#-examples) • [Webapp](#webapp) • [Server Setup](#install-server-and-configure-it) • [HTTP API](#raw-http-api)
|
|
11
11
|
|
|
12
12
|
<p align="center">
|
|
13
|
-
<img src="assets/queen-logo.svg" alt="Queen Logo" width="120" />
|
|
13
|
+
<img src="assets/queen-logo-rose.svg" alt="Queen Logo" width="120" />
|
|
14
14
|
</p>
|
|
15
15
|
|
|
16
16
|
</div>
|
|
17
17
|
|
|
18
18
|
---
|
|
19
19
|
|
|
20
|
-
##
|
|
20
|
+
## Introduction
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
QueenMQ is a queue system written in C++ and backed by Postgres. Supports queues and consumer groups.
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
## JS Client usage
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
- **Async iteration**: Process messages with familiar `for await` syntax
|
|
29
|
-
- **Batch processing**: Use `takeBatch()` for 250k+ msg/sec throughput on millions of messages
|
|
30
|
-
- **Smart addressing**: `orders/urgent@workers` - queue, partition, and consumer group in one
|
|
26
|
+
```js
|
|
27
|
+
import { Queen } from 'queen-mq'
|
|
31
28
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
- **Partition locking** prevents duplicate processing across consumers
|
|
37
|
-
- **Connection pooling** and optimized batch operations
|
|
38
|
-
|
|
39
|
-
**🏗️ Flexible Architecture**
|
|
40
|
-
- **Queue Mode**: Competitive consumption (traditional work queue)
|
|
41
|
-
- **Bus Mode**: Pub/sub with consumer groups (event streaming)
|
|
42
|
-
- **Mixed Mode**: Combine both patterns in the same system
|
|
43
|
-
- **Partitions**: FIFO ordering with parallel processing
|
|
44
|
-
|
|
45
|
-
**🔒 Enterprise Features**
|
|
46
|
-
- **AES-256-GCM Encryption**: Protect sensitive data at rest
|
|
47
|
-
- **Message Retention**: Automatic cleanup policies
|
|
48
|
-
- **Message Eviction**: SLA enforcement for time-sensitive tasks
|
|
49
|
-
- **Dead Letter Queue**: Handle failed messages gracefully
|
|
50
|
-
|
|
51
|
-
**📊 Built-in Observability**
|
|
52
|
-
- **Real-time Dashboard**: WebSocket-powered monitoring
|
|
53
|
-
- **Rich Analytics**: Throughput, lag, queue depth metrics
|
|
54
|
-
- **Message Browser**: Search, inspect, and retry messages
|
|
55
|
-
- **System Health**: Database, memory, and performance metrics
|
|
56
|
-
- **Cursor Tracking**: Monitor consumption progress per consumer group
|
|
57
|
-
|
|
58
|
-
### Use Cases
|
|
59
|
-
|
|
60
|
-
- **Task Queues**: Background jobs, email sending, data processing
|
|
61
|
-
- **Event Streaming**: Audit logs, analytics, multi-service event handling
|
|
62
|
-
- **Workflow Orchestration**: Multi-stage pipelines, saga patterns
|
|
63
|
-
- **Rate Limiting**: Throttle and batch time-sensitive operations
|
|
64
|
-
- **Priority Processing**: Handle urgent tasks before routine ones
|
|
65
|
-
|
|
66
|
-
---
|
|
67
|
-
|
|
68
|
-
## 📋 Table of Contents
|
|
69
|
-
|
|
70
|
-
- [First Queue](#-first-queue)
|
|
71
|
-
- [Quick Start](#-quick-start)
|
|
72
|
-
- [Client Examples](#-client-examples)
|
|
73
|
-
- [Server Setup](#-server-setup)
|
|
74
|
-
- [Core Concepts](#-core-concepts)
|
|
75
|
-
- [Cursor-Based Consumption Strategy](#-cursor-based-consumption-strategy)
|
|
76
|
-
- [HTTP API Reference](#-http-api-reference)
|
|
77
|
-
- [Dashboard](#-dashboard)
|
|
78
|
-
- [Configuration](#-configuration)
|
|
79
|
-
- [Full Examples](#-full-examples)
|
|
80
|
-
- [Testing](#-testing)
|
|
81
|
-
- [Contributing](#-contributing)
|
|
82
|
-
|
|
83
|
-
---
|
|
84
|
-
|
|
85
|
-
## First Queue
|
|
86
|
-
|
|
87
|
-
```javascript
|
|
88
|
-
import { Queen } from 'queen-mq';
|
|
89
|
-
|
|
90
|
-
const client = new Queen({
|
|
91
|
-
baseUrls: ['http://localhost:6632']
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
// Configure queue
|
|
95
|
-
await client.queue('tasks', {
|
|
96
|
-
leaseTime: 300,
|
|
97
|
-
retryLimit: 3
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
// Push a message
|
|
101
|
-
await client.push('tasks', {
|
|
102
|
-
action: 'send-email',
|
|
103
|
-
to: 'user@example.com'
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
// Process messages
|
|
107
|
-
for await (const message of client.take('tasks')) {
|
|
108
|
-
console.log('Processing:', message.data);
|
|
109
|
-
await client.ack(message);
|
|
110
|
-
}
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
That's it! You now have a working message queue system.
|
|
114
|
-
|
|
115
|
-
## 🏃 Quick Start
|
|
116
|
-
|
|
117
|
-
### Prerequisites
|
|
118
|
-
|
|
119
|
-
- **Node.js 22+**
|
|
120
|
-
- **PostgreSQL 12+**
|
|
121
|
-
|
|
122
|
-
### Installation
|
|
123
|
-
|
|
124
|
-
```bash
|
|
125
|
-
# Clone the repository
|
|
126
|
-
git clone https://github.com/smartpricing/queen
|
|
127
|
-
cd queen
|
|
128
|
-
|
|
129
|
-
# Install dependencies
|
|
130
|
-
nvm use 22
|
|
131
|
-
npm install
|
|
132
|
-
|
|
133
|
-
# Initialize database schema
|
|
134
|
-
node init-db.js
|
|
135
|
-
```
|
|
136
|
-
|
|
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
|
-
### Set Environment (Optional)
|
|
165
|
-
|
|
166
|
-
```bash
|
|
167
|
-
# Database connection
|
|
168
|
-
export PG_USER=postgres
|
|
169
|
-
export PG_HOST=localhost
|
|
170
|
-
export PG_DB=postgres
|
|
171
|
-
export PG_PASSWORD=postgres
|
|
172
|
-
export PG_PORT=5432
|
|
173
|
-
|
|
174
|
-
# Enable encryption (optional)
|
|
175
|
-
export QUEEN_ENCRYPTION_KEY=$(openssl rand -hex 32)
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
### Start the Server
|
|
179
|
-
|
|
180
|
-
```bash
|
|
181
|
-
npm start
|
|
182
|
-
# Server starts on http://localhost:6632
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
---
|
|
186
|
-
|
|
187
|
-
## 💻 Client Examples
|
|
188
|
-
|
|
189
|
-
The Queen client provides a minimalist API with just 4 methods that compose into any messaging pattern you need.
|
|
190
|
-
|
|
191
|
-
### Installation
|
|
192
|
-
|
|
193
|
-
```bash
|
|
194
|
-
npm install queen-mq
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
### Basic Usage
|
|
198
|
-
|
|
199
|
-
#### 1. Configure a Queue
|
|
200
|
-
|
|
201
|
-
```javascript
|
|
202
|
-
import { Queen } from 'queen-mq';
|
|
203
|
-
|
|
204
|
-
const client = new Queen({
|
|
205
|
-
baseUrls: ['http://localhost:6632'],
|
|
206
|
-
timeout: 30000,
|
|
207
|
-
retryAttempts: 3
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
// Configure with options
|
|
211
|
-
await client.queue('orders', {
|
|
212
|
-
priority: 10, // Higher priority queues processed first
|
|
213
|
-
leaseTime: 600, // 10 minutes to process each message
|
|
214
|
-
retryLimit: 3, // Retry up to 3 times
|
|
215
|
-
delayedProcessing: 0, // No delay (immediate processing)
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
// Configure with namespace and task for grouping
|
|
219
|
-
await client.queue('order-processing', {
|
|
220
|
-
priority: 10
|
|
221
|
-
}, {
|
|
222
|
-
namespace: 'ecommerce',
|
|
223
|
-
task: 'checkout'
|
|
224
|
-
});
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
#### 2. Push Messages
|
|
228
|
-
|
|
229
|
-
```javascript
|
|
230
|
-
// Single message
|
|
231
|
-
await client.push('orders', {
|
|
232
|
-
orderId: 12345,
|
|
233
|
-
amount: 99.99
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
// To a specific partition
|
|
237
|
-
await client.push('orders/urgent', {
|
|
238
|
-
orderId: 12346,
|
|
239
|
-
amount: 999.99,
|
|
240
|
-
priority: 'high'
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
// Batch messages
|
|
244
|
-
await client.push('orders', [
|
|
245
|
-
{ orderId: 12347, amount: 49.99 },
|
|
246
|
-
{ orderId: 12348, amount: 79.99 },
|
|
247
|
-
{ orderId: 12349, amount: 29.99 }
|
|
248
|
-
]);
|
|
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
|
-
```
|
|
259
|
-
|
|
260
|
-
#### 3. Take Messages (Async Iterator)
|
|
261
|
-
|
|
262
|
-
```javascript
|
|
263
|
-
// Process continuously with long polling
|
|
264
|
-
for await (const message of client.take('orders', {
|
|
265
|
-
wait: true, // Enable long polling
|
|
266
|
-
timeout: 30000, // 30 second timeout
|
|
267
|
-
batch: 10 // Fetch up to 10 at once
|
|
268
|
-
})) {
|
|
269
|
-
try {
|
|
270
|
-
await processOrder(message.data);
|
|
271
|
-
await client.ack(message); // Success
|
|
272
|
-
} catch (error) {
|
|
273
|
-
await client.ack(message, false, { error: error.message }); // Failure
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// Process limited messages
|
|
278
|
-
for await (const message of client.take('orders', { limit: 100 })) {
|
|
279
|
-
await processOrder(message.data);
|
|
280
|
-
await client.ack(message);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Take from specific partition
|
|
284
|
-
for await (const message of client.take('orders/urgent')) {
|
|
285
|
-
await processUrgentOrder(message.data);
|
|
286
|
-
await client.ack(message);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// Use takeBatch to get arrays of messages (higher throughput)
|
|
290
|
-
for await (const messages of client.takeBatch('orders', {
|
|
291
|
-
batch: 1000, // Fetch 1000 at a time
|
|
292
|
-
wait: true
|
|
293
|
-
})) {
|
|
294
|
-
// messages is an array of up to 1000 messages
|
|
295
|
-
console.log(`Processing batch of ${messages.length} messages`);
|
|
296
|
-
|
|
297
|
-
try {
|
|
298
|
-
// Process entire batch
|
|
299
|
-
await processBatch(messages.map(m => m.data));
|
|
300
|
-
|
|
301
|
-
// Acknowledge entire batch at once (efficient!)
|
|
302
|
-
await client.ack(messages); // Pass array for batch ack
|
|
303
|
-
} catch (error) {
|
|
304
|
-
// Mark entire batch as failed
|
|
305
|
-
await client.ack(messages, false, { error: error.message });
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
```
|
|
309
|
-
|
|
310
|
-
#### 4. Acknowledge Messages
|
|
311
|
-
|
|
312
|
-
```javascript
|
|
313
|
-
// Acknowledge success
|
|
314
|
-
await client.ack(message);
|
|
315
|
-
// or
|
|
316
|
-
await client.ack(message, true);
|
|
317
|
-
|
|
318
|
-
// Acknowledge failure (will retry based on retryLimit)
|
|
319
|
-
await client.ack(message, false);
|
|
320
|
-
|
|
321
|
-
// Acknowledge with error context
|
|
322
|
-
await client.ack(message, false, {
|
|
323
|
-
error: 'Payment gateway timeout'
|
|
29
|
+
const client = new Queen({
|
|
30
|
+
baseUrls: ['http://localhost:6632'],
|
|
31
|
+
timeout: 30000,
|
|
32
|
+
retryAttempts: 3
|
|
324
33
|
});
|
|
325
34
|
|
|
326
|
-
|
|
327
|
-
await client.ack('4dfb0478-655b-4c91-bcd9-b7acacf0400f', true);
|
|
328
|
-
|
|
329
|
-
// Request explicit retry
|
|
330
|
-
await client.ack(message, 'retry');
|
|
331
|
-
```
|
|
332
|
-
|
|
333
|
-
### Address Notation
|
|
334
|
-
|
|
335
|
-
Queen uses a simple addressing scheme that encodes queue, partition, and consumer group:
|
|
336
|
-
|
|
337
|
-
```javascript
|
|
338
|
-
// Basic addresses
|
|
339
|
-
'orders' // Queue (Default partition)
|
|
340
|
-
'orders/urgent' // Queue with specific partition
|
|
341
|
-
'orders@workers' // Queue with consumer group (bus mode)
|
|
342
|
-
'orders/urgent@workers' // Full address: queue + partition + group
|
|
343
|
-
|
|
344
|
-
// Namespace/task filtering (cross-queue consumption)
|
|
345
|
-
'namespace:ecommerce' // All queues in namespace
|
|
346
|
-
'task:checkout' // All queues with task
|
|
347
|
-
'namespace:ecommerce/task:checkout' // Combined filter
|
|
348
|
-
'namespace:ecommerce/task:checkout@audit' // With consumer group
|
|
349
|
-
```
|
|
350
|
-
|
|
351
|
-
### Consumer Patterns
|
|
352
|
-
|
|
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
|
-
#### Consumer Groups (Bus Mode)
|
|
420
|
-
|
|
421
|
-
```javascript
|
|
422
|
-
// Multiple services process the same messages independently
|
|
423
|
-
|
|
424
|
-
// Analytics service
|
|
425
|
-
for await (const event of client.take('events@analytics')) {
|
|
426
|
-
await updateAnalytics(event.data);
|
|
427
|
-
await client.ack(event, true, { group: 'analytics' });
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
// Monitoring service (gets same messages)
|
|
431
|
-
for await (const event of client.take('events@monitoring')) {
|
|
432
|
-
await checkThresholds(event.data);
|
|
433
|
-
await client.ack(event, true, { group: 'monitoring' });
|
|
434
|
-
}
|
|
35
|
+
const queue = 'html-processing'
|
|
435
36
|
|
|
436
|
-
//
|
|
437
|
-
|
|
438
|
-
await logToAudit(event.data);
|
|
439
|
-
await client.ack(event, true, { group: 'audit' });
|
|
440
|
-
}
|
|
441
|
-
```
|
|
442
|
-
|
|
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
|
-
}
|
|
37
|
+
// Create a queue
|
|
38
|
+
await client.queue(queue, { leaseTime: 30 });
|
|
453
39
|
|
|
454
|
-
//
|
|
455
|
-
|
|
456
|
-
subscriptionMode: 'new'
|
|
457
|
-
})) {
|
|
458
|
-
await processEvent(event.data);
|
|
459
|
-
await client.ack(event);
|
|
460
|
-
}
|
|
40
|
+
// Push some data, specifyng the partition
|
|
41
|
+
await client.push(`${queue}/customer-1828`, [ { id: 1 } ]);
|
|
461
42
|
|
|
462
|
-
//
|
|
463
|
-
for await (const
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
})) {
|
|
467
|
-
await processHistoricalEvent(event.data);
|
|
468
|
-
await client.ack(event);
|
|
43
|
+
// Consume data with iterators
|
|
44
|
+
for await (const msg of client.take(queue, { limit: 1 })) {
|
|
45
|
+
console.log(msg.data.id)
|
|
46
|
+
await client.ack(msg) // OR await client.ack(msg, false) for nack
|
|
469
47
|
}
|
|
470
|
-
```
|
|
471
48
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
//
|
|
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
|
-
}
|
|
49
|
+
// Consume data with iterators, getting the entire batch
|
|
50
|
+
for await (const messages of client.takeBatch(queue, { limit: 1, batch: 10, wait: true, timeout: 2000 })) {
|
|
51
|
+
const newMex = messages.map(x => x.data.id * 2)
|
|
52
|
+
await client.ack(messages) // OR await client.ack(messages, false) for nack
|
|
494
53
|
}
|
|
495
|
-
```
|
|
496
|
-
|
|
497
|
-
#### Graceful Shutdown
|
|
498
|
-
|
|
499
|
-
```javascript
|
|
500
|
-
let running = true;
|
|
501
|
-
|
|
502
|
-
process.on('SIGTERM', () => {
|
|
503
|
-
console.log('Shutting down gracefully...');
|
|
504
|
-
running = false;
|
|
505
|
-
});
|
|
506
54
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
await client.ack(message);
|
|
55
|
+
// Consume data with a consumer group
|
|
56
|
+
for await (const msg of client.take(`${queue}@analytics-data`, { limit: 2, batch: 2 })) {
|
|
57
|
+
// Do your computation and than ack with consumer group
|
|
58
|
+
await client.ack(msg, true, { group: 'analytics-data' });
|
|
512
59
|
}
|
|
513
60
|
|
|
514
|
-
|
|
515
|
-
|
|
61
|
+
// Consume data from any partition of the queue, continusly
|
|
62
|
+
// This "pipeline" is useful for doing exactly one processing
|
|
63
|
+
await client
|
|
64
|
+
.pipeline(queue)
|
|
65
|
+
.withAutoRenewal({
|
|
66
|
+
interval: 5000 // Renew lease every 5 seconds
|
|
67
|
+
})
|
|
68
|
+
.withConcurrency(5) // Five parallel promises
|
|
69
|
+
.take(10, {
|
|
70
|
+
wait: true, // Use long polling
|
|
71
|
+
timeout: 30000 // Long polling length in millisconds
|
|
72
|
+
})
|
|
73
|
+
.processBatch(async (messages) => {
|
|
74
|
+
return messages.map(x => x.data.id * 2);
|
|
75
|
+
})
|
|
76
|
+
.atomically((tx, originalMessages, processedMessages) => { // ack and push are transactional inside atomically
|
|
77
|
+
tx.ack(originalMessages);
|
|
78
|
+
tx.push('another-queue', processedMessages);
|
|
79
|
+
})
|
|
80
|
+
.repeat({ continuous: true })
|
|
81
|
+
.execute();
|
|
516
82
|
```
|
|
517
83
|
|
|
518
|
-
|
|
84
|
+
## 📚 Examples
|
|
519
85
|
|
|
520
|
-
|
|
86
|
+
### Basic Usage
|
|
87
|
+
- **[Basic Queue Operations](examples/01-basic-usage.js)** - Create queue, push, take, and ack messages
|
|
88
|
+
- **[Batch Operations](examples/02-batch-operations.js)** - Push and consume messages in batches
|
|
521
89
|
|
|
522
|
-
###
|
|
90
|
+
### Advanced Features
|
|
91
|
+
- **[Queue Configuration](examples/06-queue-configuration.js)** - Configure maxSize, windowBuffer, delay, retryLimit, and priority
|
|
92
|
+
- **[Delayed Processing](examples/04-delayed-processing.js)** - Process messages after a delay
|
|
93
|
+
- **[Window Buffer](examples/05-window-buffer.js)** - Delay message availability after push
|
|
523
94
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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
|
-
```
|
|
95
|
+
### Filtering & Routing
|
|
96
|
+
- **[Namespace & Task Filtering](examples/07-namespace-task-filtering.js)** - Route and filter messages by namespace and task
|
|
97
|
+
- **[Consumer Groups](examples/08-consumer-groups.js)** - Multiple consumer groups processing same messages
|
|
534
98
|
|
|
535
|
-
###
|
|
99
|
+
### Pipelines
|
|
100
|
+
- **[Transactional Pipelines](examples/03-transactional-pipeline.js)** - Atomic processing with ack and push in a transaction
|
|
536
101
|
|
|
537
|
-
|
|
102
|
+
### Event Streaming (QoS 0)
|
|
103
|
+
- **[Event Streaming](examples/09-event-streaming.js)** - At-most-once delivery with buffering and auto-ack
|
|
538
104
|
|
|
539
|
-
|
|
540
|
-
# Server 1
|
|
541
|
-
PORT=6632 WORKER_ID=server-1 npm start
|
|
105
|
+
## QoS 0: At-Most-Once Event Streaming
|
|
542
106
|
|
|
543
|
-
|
|
544
|
-
PORT=6633 WORKER_ID=server-2 npm start
|
|
107
|
+
For high-throughput event streams, Queen supports **at-most-once delivery** with server-side buffering and auto-acknowledgment.
|
|
545
108
|
|
|
546
|
-
|
|
547
|
-
PORT=6634 WORKER_ID=server-3 npm start
|
|
548
|
-
```
|
|
109
|
+
### Server-Side Buffering
|
|
549
110
|
|
|
550
|
-
|
|
111
|
+
Batch events on the server for 10-100x reduction in database writes:
|
|
551
112
|
|
|
552
113
|
```javascript
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
'http://localhost:6634'
|
|
558
|
-
],
|
|
559
|
-
loadBalancingStrategy: 'ROUND_ROBIN', // or 'RANDOM', 'LEAST_CONNECTIONS'
|
|
560
|
-
enableFailover: true
|
|
114
|
+
// Push with buffering (QoS 0)
|
|
115
|
+
await client.push('metrics', { cpu: 45, memory: 67 }, {
|
|
116
|
+
bufferMs: 100, // Server batches for 100ms
|
|
117
|
+
bufferMax: 100 // Or until 100 events
|
|
561
118
|
});
|
|
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
|
-
|
|
613
|
-
### Environment Variables
|
|
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
|
|
623
|
-
```
|
|
624
|
-
|
|
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
|
-
---
|
|
631
|
-
|
|
632
|
-
## 💡 Core Concepts
|
|
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
119
|
|
|
688
|
-
|
|
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
|
-
}
|
|
120
|
+
// Result: 1000 events = ~10 DB writes (instead of 1000)
|
|
713
121
|
```
|
|
714
122
|
|
|
715
|
-
###
|
|
123
|
+
### Auto-Acknowledgment
|
|
716
124
|
|
|
717
|
-
|
|
125
|
+
Skip manual ack for fire-and-forget consumption:
|
|
718
126
|
|
|
719
127
|
```javascript
|
|
720
|
-
//
|
|
721
|
-
await client.
|
|
722
|
-
|
|
723
|
-
|
|
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);
|
|
128
|
+
// Consume with auto-ack
|
|
129
|
+
for await (const msg of client.take('metrics', { autoAck: true })) {
|
|
130
|
+
updateDashboard(msg.data);
|
|
131
|
+
// No ack() needed - automatically acknowledged!
|
|
731
132
|
}
|
|
732
133
|
```
|
|
733
134
|
|
|
734
|
-
|
|
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
|
|
135
|
+
### Fan-Out Pattern (Consumer Groups)
|
|
738
136
|
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
Consumer groups enable **pub-sub messaging** where multiple independent consumers process the same messages:
|
|
137
|
+
Combine buffering + auto-ack + consumer groups for pub/sub:
|
|
742
138
|
|
|
743
139
|
```javascript
|
|
744
|
-
//
|
|
745
|
-
await client.push('events', {
|
|
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
|
-
}
|
|
140
|
+
// Publisher (buffered)
|
|
141
|
+
await client.push('events', { action: 'login' }, { bufferMs: 100 });
|
|
753
142
|
|
|
754
|
-
//
|
|
755
|
-
for await (const
|
|
756
|
-
|
|
757
|
-
await client.ack(event);
|
|
143
|
+
// Multiple subscribers (each group gets all messages)
|
|
144
|
+
for await (const e of client.take('events@dashboard', { autoAck: true })) {
|
|
145
|
+
updateUI(e.data);
|
|
758
146
|
}
|
|
759
147
|
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
await logEvent(event.data);
|
|
763
|
-
await client.ack(event);
|
|
148
|
+
for await (const e of client.take('events@analytics', { autoAck: true })) {
|
|
149
|
+
trackEvent(e.data);
|
|
764
150
|
}
|
|
765
151
|
```
|
|
766
152
|
|
|
767
|
-
|
|
768
|
-
- Independent message status tracking
|
|
769
|
-
- Separate partition leases
|
|
770
|
-
- Individual retry counters
|
|
771
|
-
- Isolated processing state
|
|
772
|
-
|
|
773
|
-
### Queue Mode vs Bus Mode
|
|
774
|
-
|
|
775
|
-
**Queue Mode** (default - competitive consumption):
|
|
776
|
-
```javascript
|
|
777
|
-
// Without consumer group - messages distributed
|
|
778
|
-
await client.push('tasks', { id: 1 });
|
|
779
|
-
await client.push('tasks', { id: 2 });
|
|
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
|
-
```
|
|
787
|
-
|
|
788
|
-
**Bus Mode** (pub-sub with consumer groups):
|
|
789
|
-
```javascript
|
|
790
|
-
// With consumer groups - all groups see all messages
|
|
791
|
-
await client.push('events', { id: 1 });
|
|
153
|
+
### PostgreSQL Failover
|
|
792
154
|
|
|
793
|
-
|
|
794
|
-
for await (const msg of client.take('events@group1')) { }
|
|
155
|
+
Queen automatically buffers messages to disk when PostgreSQL is unavailable - **zero message loss**:
|
|
795
156
|
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
157
|
+
- Normal pushes go directly to PostgreSQL (FIFO preserved)
|
|
158
|
+
- If PostgreSQL is down, messages buffered to file (macOS: `/tmp/queen`, Linux: `/var/lib/queen/buffers`)
|
|
159
|
+
- Automatic replay when PostgreSQL recovers
|
|
160
|
+
- Survives server crashes and restarts
|
|
161
|
+
- Directory auto-created on first run
|
|
799
162
|
|
|
800
|
-
**
|
|
801
|
-
```javascript
|
|
802
|
-
// Competitive workers process jobs
|
|
803
|
-
for await (const job of client.take('jobs')) {
|
|
804
|
-
await processJob(job.data);
|
|
805
|
-
await client.ack(job);
|
|
806
|
-
}
|
|
163
|
+
**No configuration needed** - failover is automatic!
|
|
807
164
|
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
await client.ack(job);
|
|
812
|
-
}
|
|
165
|
+
**Custom directory:**
|
|
166
|
+
```bash
|
|
167
|
+
FILE_BUFFER_DIR=/custom/path ./bin/queen-server
|
|
813
168
|
```
|
|
814
169
|
|
|
815
|
-
###
|
|
816
|
-
|
|
817
|
-
Configure priority at the **queue level**:
|
|
170
|
+
### When to Use
|
|
818
171
|
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
// Urgent orders processed first, then normal, then batch
|
|
825
|
-
```
|
|
172
|
+
| Feature | Use For | Don't Use For |
|
|
173
|
+
|---------|---------|---------------|
|
|
174
|
+
| **Buffering** | Metrics, logs, analytics, UI updates | Critical tasks, payments |
|
|
175
|
+
| **Auto-Ack** | Fire-and-forget events, notifications | Tasks requiring retry logic |
|
|
176
|
+
| **Failover** | Everything (automatic) | N/A - always beneficial |
|
|
826
177
|
|
|
827
|
-
|
|
178
|
+
## Webapp
|
|
828
179
|
|
|
829
|
-
|
|
180
|
+
A modern Vue 3 web interface for managing and monitoring Queen MQ.
|
|
830
181
|
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
182
|
+
**Features:**
|
|
183
|
+
- 📊 Real-time dashboard with system metrics
|
|
184
|
+
- 📈 Message throughput visualization
|
|
185
|
+
- 🔍 Queue management and monitoring
|
|
186
|
+
- 👥 Consumer group tracking
|
|
187
|
+
- 💬 Message browser
|
|
188
|
+
- 📉 Analytics and insights
|
|
189
|
+
- 🌓 Dark/light theme support
|
|
836
190
|
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
191
|
+
**Quick Start:**
|
|
192
|
+
```bash
|
|
193
|
+
cd webapp
|
|
194
|
+
npm install
|
|
195
|
+
npm run dev
|
|
841
196
|
```
|
|
842
197
|
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
Schedule messages for future processing:
|
|
198
|
+
The dashboard will be available at `http://localhost:4000`
|
|
846
199
|
|
|
847
|
-
|
|
848
|
-
await client.queue('scheduled-jobs', {
|
|
849
|
-
delayedProcessing: 3600 // 1 hour delay
|
|
850
|
-
});
|
|
851
|
-
|
|
852
|
-
await client.push('scheduled-jobs', {
|
|
853
|
-
reportType: 'daily-sales'
|
|
854
|
-
});
|
|
855
|
-
// Message won't be available for processing until 1 hour later
|
|
856
|
-
```
|
|
857
|
-
|
|
858
|
-
### Window Buffering
|
|
200
|
+
See [webapp/README.md](webapp/README.md) for more details.
|
|
859
201
|
|
|
860
|
-
|
|
202
|
+
## Install server and configure it
|
|
861
203
|
|
|
862
|
-
|
|
863
|
-
await client.queue('analytics', {
|
|
864
|
-
windowBuffer: 60 // Wait 60 seconds to accumulate messages
|
|
865
|
-
});
|
|
204
|
+
### Quick Start
|
|
866
205
|
|
|
867
|
-
|
|
206
|
+
```sh
|
|
207
|
+
cd server
|
|
208
|
+
make clean
|
|
209
|
+
make deps
|
|
210
|
+
make build-only
|
|
211
|
+
DB_POOL_SIZE=50 ./bin/queen-server
|
|
868
212
|
```
|
|
869
213
|
|
|
870
|
-
|
|
214
|
+
**📖 Complete Build & Tuning Guide:** [server/README.md](server/README.md)
|
|
871
215
|
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
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
|
-
```
|
|
216
|
+
Includes:
|
|
217
|
+
- Build instructions and optimization
|
|
218
|
+
- Performance tuning (worker threads, database pool)
|
|
219
|
+
- Production deployment (systemd, Docker, load balancing)
|
|
220
|
+
- Troubleshooting common issues
|
|
221
|
+
- Benchmarking guides
|
|
883
222
|
|
|
884
|
-
###
|
|
223
|
+
### Environment Variables
|
|
885
224
|
|
|
886
|
-
|
|
225
|
+
[The full list of environment variables is here](server/ENV_VARIABLES.md)
|
|
887
226
|
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
227
|
+
### With Docker
|
|
228
|
+
```sh
|
|
229
|
+
./build.sh
|
|
891
230
|
```
|
|
892
231
|
|
|
893
|
-
|
|
894
|
-
await client.queue('sensitive-data', {
|
|
895
|
-
encryptionEnabled: true
|
|
896
|
-
});
|
|
232
|
+
### Running on k8s
|
|
897
233
|
|
|
898
|
-
|
|
899
|
-
await client.push('sensitive-data', { ssn: '123-45-6789' });
|
|
900
|
-
```
|
|
234
|
+
[Running in k8s](server/k8s-example.yaml)
|
|
901
235
|
|
|
902
|
-
|
|
236
|
+
## 🔌 Raw HTTP API
|
|
903
237
|
|
|
904
|
-
|
|
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
|
-
```
|
|
238
|
+
You can use Queen directly from HTTP without the JS client.
|
|
911
239
|
|
|
912
|
-
|
|
240
|
+
[Here the complete list of API endpoints](API.md)
|
|
913
241
|
|
|
914
|
-
|
|
915
|
-
await client.queue('time-sensitive', {
|
|
916
|
-
maxWaitTimeSeconds: 60 // Evict messages older than 1 minute
|
|
917
|
-
});
|
|
918
|
-
```
|
|
242
|
+
## ⚠️ Known Issues & Roadmap
|
|
919
243
|
|
|
920
|
-
###
|
|
244
|
+
### Server Startup Timing (Critical)
|
|
245
|
+
**Issue:** Worker initialization timeout (30s → 3600s) now matches file buffer recovery timeout. This is a temporary fix.
|
|
921
246
|
|
|
922
|
-
**1.
|
|
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)
|
|
247
|
+
**Problem:** During startup, if the file buffer has many events to recover (e.g., after a long PostgreSQL outage), Worker 0 performs blocking recovery that can take up to 1 hour. The acceptor waits for all workers to initialize before starting to accept connections.
|
|
927
248
|
|
|
928
|
-
**
|
|
929
|
-
- Set lease time slightly longer than expected processing time
|
|
930
|
-
- Handle timeouts gracefully
|
|
931
|
-
- Acknowledge messages as soon as processing completes
|
|
249
|
+
**Current Fix:** Worker initialization timeout increased to 3600s to prevent premature timeout.
|
|
932
250
|
|
|
933
|
-
**
|
|
934
|
-
-
|
|
935
|
-
-
|
|
936
|
-
-
|
|
251
|
+
**Better Solution Needed:**
|
|
252
|
+
- Make recovery non-blocking while preserving FIFO ordering guarantees
|
|
253
|
+
- Implement progressive readiness with memory-buffered queue during recovery
|
|
254
|
+
- Add configurable recovery timeout with graceful degradation
|
|
255
|
+
- See: `server/src/services/file_buffer.cpp:212` (MAX_STARTUP_RECOVERY_SECONDS)
|
|
256
|
+
- See: `server/src/acceptor_server.cpp:1876` (worker initialization timeout)
|
|
937
257
|
|
|
938
|
-
|
|
939
|
-
-
|
|
940
|
-
-
|
|
941
|
-
-
|
|
942
|
-
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
## 🚀 Cursor-Based Consumption Strategy
|
|
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:**
|
|
955
|
-
|
|
956
|
-
Each partition maintains a cursor position per consumer group:
|
|
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
|
|
960
|
-
|
|
961
|
-
The cursor always moves **forward** in time, ensuring strict FIFO ordering.
|
|
962
|
-
|
|
963
|
-
### The takeBatch Method
|
|
964
|
-
|
|
965
|
-
Queen provides two consumption methods:
|
|
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>
|
|
258
|
+
### Other TODO Items
|
|
259
|
+
- retention jobs
|
|
260
|
+
- reconsume
|
|
261
|
+
- fix frontend
|
|
262
|
+
- pg async
|
|
263
|
+
- pg reconnect
|
|
264
|
+
- memory leak?
|