queen-mq 0.4.0 → 0.6.4
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 +520 -273
- package/{client-js/client-v2 → client-v2}/Queen.js +38 -0
- package/{client-js/client-v2 → client-v2}/README.md +61 -8
- package/package.json +5 -8
- package/{client-js/test-v2 → test-v2}/GETTING_STARTED.md +27 -0
- package/test-v2/README_SUBSCRIPTION_TESTS.md +201 -0
- package/{client-js/test-v2 → test-v2}/consume.js +10 -0
- package/{client-js/test-v2 → test-v2}/dlq.js +1 -1
- package/{client-js/test-v2 → test-v2}/load.js +2 -0
- package/{client-js/test-v2 → test-v2}/subscription.js +198 -18
- package/{client-js/test-v2 → test-v2}/transaction.js +66 -4
- package/LICENSE.md +0 -202
- package/client-js/benchmark/consumer.js +0 -209
- package/client-js/benchmark/consumer_multi.js +0 -216
- package/client-js/benchmark/producer.js +0 -80
- package/client-js/benchmark/producer_multi.js +0 -115
- /package/{client-js/client-v2 → client-v2}/LOGGING.md +0 -0
- /package/{client-js/client-v2 → client-v2}/buffer/BufferManager.js +0 -0
- /package/{client-js/client-v2 → client-v2}/buffer/MessageBuffer.js +0 -0
- /package/{client-js/client-v2 → client-v2}/builders/QueueBuilder.js +0 -0
- /package/{client-js/client-v2 → client-v2}/builders/TransactionBuilder.js +0 -0
- /package/{client-js/client-v2 → client-v2}/consumer/ConsumerManager.js +0 -0
- /package/{client-js/client-v2 → client-v2}/http/HttpClient.js +0 -0
- /package/{client-js/client-v2 → client-v2}/http/LoadBalancer.js +0 -0
- /package/{client-js/client-v2 → client-v2}/index.js +0 -0
- /package/{client-js/client-v2 → client-v2}/stream/StreamBuilder.js +0 -0
- /package/{client-js/client-v2 → client-v2}/stream/StreamConsumer.js +0 -0
- /package/{client-js/client-v2 → client-v2}/stream/Window.js +0 -0
- /package/{client-js/client-v2 → client-v2}/utils/defaults.js +0 -0
- /package/{client-js/client-v2 → client-v2}/utils/logger.js +0 -0
- /package/{client-js/client-v2 → client-v2}/utils/validation.js +0 -0
- /package/{client-js/test-v2 → test-v2}/AI_TEST_SUMMARY.md +0 -0
- /package/{client-js/test-v2 → test-v2}/MAINTENANCE_TEST.md +0 -0
- /package/{client-js/test-v2 → test-v2}/ai_buffering.js +0 -0
- /package/{client-js/test-v2 → test-v2}/ai_error_handling.js +0 -0
- /package/{client-js/test-v2 → test-v2}/ai_lease_renewal.js +0 -0
- /package/{client-js/test-v2 → test-v2}/ai_mixed_scenarios.js +0 -0
- /package/{client-js/test-v2 → test-v2}/ai_priority.js +0 -0
- /package/{client-js/test-v2 → test-v2}/ai_resources.js +0 -0
- /package/{client-js/test-v2 → test-v2}/ai_ttl_retention.js +0 -0
- /package/{client-js/test-v2 → test-v2}/complete.js +0 -0
- /package/{client-js/test-v2 → test-v2}/maintenance.js +0 -0
- /package/{client-js/test-v2 → test-v2}/pop.js +0 -0
- /package/{client-js/test-v2 → test-v2}/push.js +0 -0
- /package/{client-js/test-v2 → test-v2}/queue.js +0 -0
- /package/{client-js/test-v2 → test-v2}/retention.js +0 -0
- /package/{client-js/test-v2 → test-v2}/run.js +0 -0
package/README.md
CHANGED
|
@@ -1,385 +1,632 @@
|
|
|
1
|
-
# Queen MQ -
|
|
1
|
+
# Queen MQ - JavaScript Client
|
|
2
2
|
|
|
3
3
|
<div align="center">
|
|
4
4
|
|
|
5
|
-
**
|
|
5
|
+
**Modern, high-performance message queue client for Node.js**
|
|
6
6
|
|
|
7
|
+
[](https://www.npmjs.com/package/queen-mq)
|
|
7
8
|
[](LICENSE.md)
|
|
8
9
|
[](https://nodejs.org/)
|
|
9
|
-
[](https://en.cppreference.com/w/cpp/17)
|
|
10
10
|
|
|
11
|
-
[Quick Start](#
|
|
12
|
-
|
|
13
|
-
<p align="center">
|
|
14
|
-
<img src="assets/queen-logo.svg" alt="Queen Logo" width="120" />
|
|
15
|
-
</p>
|
|
11
|
+
[Quick Start](#quick-start) • [Complete Guide](client-v2/README.md) • [Examples](#examples) • [API Reference](#api-reference)
|
|
16
12
|
|
|
17
13
|
</div>
|
|
18
14
|
|
|
19
15
|
---
|
|
20
16
|
|
|
21
|
-
|
|
17
|
+
## What is Queen MQ?
|
|
18
|
+
|
|
19
|
+
Queen MQ is a PostgreSQL-backed message queue system with a powerful feature set:
|
|
20
|
+
|
|
21
|
+
- **FIFO Partitions** - Unlimited ordered partitions within queues
|
|
22
|
+
- **Consumer Groups** - Kafka-style consumer groups for scalability
|
|
23
|
+
- **Flexible Semantics** - Exactly-once, at-least-once, and at-most-once delivery
|
|
24
|
+
- **Transactions** - Atomic operations across push and ack
|
|
25
|
+
- **High Performance** - 200K+ messages/sec with proper batching
|
|
26
|
+
- **Subscription Modes** - Process from beginning, new messages only, or from timestamp
|
|
27
|
+
- **Dead Letter Queue** - Automatic failure handling and monitoring
|
|
28
|
+
- **Message Tracing** - Debug distributed workflows with trace timelines
|
|
29
|
+
- **Client-Side Buffering** - 10x-100x throughput boost for high-volume pushes
|
|
30
|
+
- **Real-time Streaming** - Windowed aggregation and processing
|
|
31
|
+
|
|
32
|
+
This client provides a fluent, promise-based API for Node.js applications.
|
|
22
33
|
|
|
23
34
|
---
|
|
24
35
|
|
|
25
|
-
##
|
|
26
|
-
|
|
27
|
-
QueenMQ is a queue system written in C++ and backed by PostgreSQL, born from the need to manage many FIFO partitions for Smartchat with solid guarantees around delivery and failure handling. During the initial development, I realized that with a few simple additions to the original design, I could build a very powerful and flexible queue system. This project is almost entirely written by AI, with my supervision—only the test files (or a good part of them) are manually written.
|
|
28
|
-
|
|
29
|
-
Here are the main features:
|
|
30
|
-
- Unlimited FIFO partitions within queues
|
|
31
|
-
- Queue semantics like RabbitMQ
|
|
32
|
-
- Consumer groups over queues like Kafka
|
|
33
|
-
- Allows for all the patterns you need, from simple queues to complex workflows and request/response patterns
|
|
34
|
-
- QoS levels: Exactly-once delivery (with transactionId), at-least-once delivery, and at-most-once delivery
|
|
35
|
-
- Subscription modes for replay (new messages only or from a specific timestamp) and message history control
|
|
36
|
-
- Transactions between operations (push and ack mainly) for atomicity
|
|
37
|
-
- Dead letter queue for failure handling
|
|
38
|
-
- Lease renewal for long-running tasks
|
|
39
|
-
- Message tracing for debugging workflows
|
|
40
|
-
- Encryption of messages at DB level
|
|
41
|
-
- Automatic message retention and cleanup - Configurable per-queue retention policies
|
|
42
|
-
- Streaming capabilities for real-time aggregation and processing of messages
|
|
43
|
-
- A nice webapp for monitoring and managing the system
|
|
44
|
-
- Maintenace mode that allows to continue pushing messages even when the database is down, and to drain the messages to the database when the maintenance mode is disabled
|
|
45
|
-
|
|
46
|
-
The system consists of a PostgreSQL database, a replicated server that can be scaled horizontally (though it won't be the bottleneck), and a client library for interacting with the server. All client-server communication happens over HTTP, and the client library are available for JavaScript and C++ (Other languages like Python are planned). There's also a modern Vue 3 web app for monitoring and managing the system.
|
|
47
|
-
|
|
48
|
-
With proper batching, the system can handle +200k **messages** per second (not req/s) on modest hardware.
|
|
49
|
-
|
|
50
|
-
Main documentation:
|
|
51
|
-
- [Client Guide JS](client-js/client-v2/README.md)
|
|
52
|
-
- [Client Guide C++](client-cpp/README.md)
|
|
53
|
-
- [Server Guide](server/README.md)
|
|
54
|
-
- [Streaming Guide](docs/STREAMING_USAGE.md)
|
|
55
|
-
- [API Reference](server/API.md)
|
|
56
|
-
- [Message Retention & Cleanup](docs/RETENTION.md)
|
|
57
|
-
- [Webapp](webapp/README.md)
|
|
58
|
-
- [Expose the Webapp behind a proxy](proxy/README.md)
|
|
59
|
-
|
|
60
|
-
## Concepts
|
|
61
|
-
|
|
62
|
-
Although the system is designed to be simple to use (not really), there are some concepts that are important to understand to use the system effectively.
|
|
36
|
+
## Installation
|
|
63
37
|
|
|
64
|
-
|
|
38
|
+
```bash
|
|
39
|
+
npm install queen-mq
|
|
40
|
+
```
|
|
65
41
|
|
|
66
|
-
|
|
42
|
+
**Requirements:** Node.js 22+
|
|
67
43
|
|
|
68
|
-
|
|
44
|
+
---
|
|
69
45
|
|
|
70
|
-
|
|
46
|
+
## Quick Start
|
|
71
47
|
|
|
72
|
-
|
|
48
|
+
```javascript
|
|
49
|
+
import { Queen } from 'queen-mq'
|
|
73
50
|
|
|
74
|
-
|
|
51
|
+
// Connect to Queen server
|
|
52
|
+
const queen = new Queen('http://localhost:6632')
|
|
75
53
|
|
|
76
|
-
|
|
54
|
+
// Create a queue
|
|
55
|
+
await queen.queue('tasks').create()
|
|
77
56
|
|
|
78
|
-
|
|
57
|
+
// Push messages
|
|
58
|
+
await queen.queue('tasks').push([
|
|
59
|
+
{ data: { task: 'send-email', to: 'alice@example.com' } }
|
|
60
|
+
])
|
|
79
61
|
|
|
80
|
-
|
|
62
|
+
// Consume messages
|
|
63
|
+
await queen.queue('tasks').consume(async (message) => {
|
|
64
|
+
console.log('Processing:', message.data)
|
|
65
|
+
// Auto-ack on success, auto-retry on error
|
|
66
|
+
})
|
|
67
|
+
```
|
|
81
68
|
|
|
82
|
-
|
|
69
|
+
---
|
|
83
70
|
|
|
84
|
-
|
|
71
|
+
## Core Concepts
|
|
85
72
|
|
|
86
|
-
|
|
73
|
+
### Queues
|
|
87
74
|
|
|
88
|
-
|
|
75
|
+
Logical containers for messages with configurable settings:
|
|
76
|
+
- **Lease time** - How long a consumer has to process a message
|
|
77
|
+
- **Retry limit** - Number of retry attempts before DLQ
|
|
78
|
+
- **Priority** - Queue priority for multi-queue consumers
|
|
79
|
+
- **Encryption** - Message payload encryption at rest
|
|
80
|
+
- **Retention** - Automatic cleanup policies
|
|
89
81
|
|
|
90
|
-
|
|
82
|
+
```javascript
|
|
83
|
+
await queen.queue('orders')
|
|
84
|
+
.config({
|
|
85
|
+
leaseTime: 300, // 5 minutes
|
|
86
|
+
retryLimit: 3,
|
|
87
|
+
priority: 5,
|
|
88
|
+
encryptionEnabled: false
|
|
89
|
+
})
|
|
90
|
+
.create()
|
|
91
|
+
```
|
|
91
92
|
|
|
92
|
-
###
|
|
93
|
+
### Partitions
|
|
93
94
|
|
|
94
|
-
|
|
95
|
+
Ordered lanes within a queue. Messages in the same partition are processed sequentially:
|
|
95
96
|
|
|
96
|
-
|
|
97
|
+
```javascript
|
|
98
|
+
// All messages for user-123 are processed in order
|
|
99
|
+
await queen.queue('user-events')
|
|
100
|
+
.partition('user-123')
|
|
101
|
+
.push([
|
|
102
|
+
{ data: { event: 'login' } },
|
|
103
|
+
{ data: { event: 'view-page' } },
|
|
104
|
+
{ data: { event: 'logout' } }
|
|
105
|
+
])
|
|
106
|
+
```
|
|
97
107
|
|
|
98
|
-
|
|
108
|
+
**Use cases:**
|
|
109
|
+
- Per-user ordering
|
|
110
|
+
- Per-tenant isolation
|
|
111
|
+
- Sharding for parallelism
|
|
99
112
|
|
|
100
|
-
###
|
|
113
|
+
### Consumer Groups
|
|
101
114
|
|
|
102
|
-
|
|
115
|
+
Multiple consumers sharing work, with independent progress tracking:
|
|
103
116
|
|
|
104
|
-
|
|
117
|
+
```javascript
|
|
118
|
+
// Worker 1 & 2 share the load
|
|
119
|
+
await queen.queue('emails')
|
|
120
|
+
.group('processors')
|
|
121
|
+
.consume(async (message) => {
|
|
122
|
+
await sendEmail(message.data)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
// Separate group processes same messages independently
|
|
126
|
+
await queen.queue('emails')
|
|
127
|
+
.group('analytics')
|
|
128
|
+
.consume(async (message) => {
|
|
129
|
+
await logMetrics(message.data)
|
|
130
|
+
})
|
|
131
|
+
```
|
|
105
132
|
|
|
106
|
-
|
|
133
|
+
### Subscription Modes
|
|
107
134
|
|
|
108
|
-
|
|
135
|
+
Control whether consumer groups process historical messages:
|
|
109
136
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
137
|
+
```javascript
|
|
138
|
+
// Default: Process ALL messages (including backlog)
|
|
139
|
+
await queen.queue('events')
|
|
140
|
+
.group('batch-analytics')
|
|
141
|
+
.consume(async (message) => { /* all messages */ })
|
|
142
|
+
|
|
143
|
+
// Skip history, only new messages
|
|
144
|
+
await queen.queue('events')
|
|
145
|
+
.group('realtime-monitor')
|
|
146
|
+
.subscriptionMode('new')
|
|
147
|
+
.consume(async (message) => { /* new only */ })
|
|
148
|
+
|
|
149
|
+
// Start from specific timestamp
|
|
150
|
+
await queen.queue('events')
|
|
151
|
+
.group('replay')
|
|
152
|
+
.subscriptionFrom('2025-10-28T10:00:00.000Z')
|
|
153
|
+
.consume(async (message) => { /* from timestamp */ })
|
|
154
|
+
```
|
|
120
155
|
|
|
156
|
+
---
|
|
121
157
|
|
|
122
|
-
##
|
|
158
|
+
## Connection Options
|
|
123
159
|
|
|
124
|
-
|
|
160
|
+
### Single Server
|
|
125
161
|
|
|
126
162
|
```javascript
|
|
127
|
-
import { Queen } from 'queen-mq'
|
|
128
|
-
|
|
129
|
-
// Connect to Queen
|
|
130
163
|
const queen = new Queen('http://localhost:6632')
|
|
164
|
+
```
|
|
131
165
|
|
|
132
|
-
|
|
133
|
-
await queen
|
|
134
|
-
.queue('critical-task')
|
|
135
|
-
.config({
|
|
136
|
-
leaseTime: 10, // 10 seconds to process the messages (seconds)
|
|
137
|
-
})
|
|
138
|
-
.create()
|
|
166
|
+
### Multiple Servers (High Availability)
|
|
139
167
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
.push([
|
|
145
|
-
{
|
|
146
|
-
transactionId: 'my-id', // This is autogenerated, but you can set your own. Unique per queue partition.
|
|
147
|
-
data: { id: 123, description: 'Critical task' } // The message payload
|
|
148
|
-
},
|
|
168
|
+
```javascript
|
|
169
|
+
const queen = new Queen([
|
|
170
|
+
'http://server1:6632',
|
|
171
|
+
'http://server2:6632'
|
|
149
172
|
])
|
|
150
|
-
|
|
151
|
-
console.log('Messages pushed successfully:', messages)
|
|
152
|
-
})
|
|
153
|
-
.onDuplicate(async (messages) => { // Not mandatory, triggered when a message with the same transactionId is pushed
|
|
154
|
-
console.warn('Duplicate transaction IDs detected')
|
|
155
|
-
})
|
|
156
|
-
.onError(async (messages, error) => { // Without callbacks, push throws an error if some messages are not pushed
|
|
157
|
-
console.error('Error pushing messages:', error)
|
|
158
|
-
})
|
|
173
|
+
```
|
|
159
174
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
//
|
|
168
|
-
|
|
169
|
-
// 10 messages per batch, prefetch them
|
|
170
|
-
.batch(10)
|
|
171
|
-
// Auto-renew the lease for the messages every 2 seconds
|
|
172
|
-
.renewLease(true, 2000)
|
|
173
|
-
// Process each message individually
|
|
174
|
-
.each()
|
|
175
|
-
// Do your work here
|
|
176
|
-
.consume(async (message) => {
|
|
177
|
-
console.log('Processing:', message.data)
|
|
178
|
-
})
|
|
179
|
-
// Ack the messages if you processed them successfully
|
|
180
|
-
.onSuccess(async (message) => {
|
|
181
|
-
await queen
|
|
182
|
-
.transaction()
|
|
183
|
-
.queue('critical-task-next')
|
|
184
|
-
.partition('XXX')
|
|
185
|
-
.push([{ data: { message: 'Final', count: 3 } }])
|
|
186
|
-
.ack(message)
|
|
187
|
-
.commit()
|
|
175
|
+
### Full Configuration
|
|
176
|
+
|
|
177
|
+
```javascript
|
|
178
|
+
const queen = new Queen({
|
|
179
|
+
urls: ['http://server1:6632', 'http://server2:6632'],
|
|
180
|
+
timeoutMillis: 30000,
|
|
181
|
+
retryAttempts: 3,
|
|
182
|
+
loadBalancingStrategy: 'round-robin', // or 'session'
|
|
183
|
+
enableFailover: true
|
|
188
184
|
})
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Basic Usage Patterns
|
|
190
|
+
|
|
191
|
+
### Push Messages
|
|
192
|
+
|
|
193
|
+
```javascript
|
|
194
|
+
// Simple push
|
|
195
|
+
await queen.queue('tasks').push([
|
|
196
|
+
{ data: { job: 'resize-image', imageId: 123 } }
|
|
197
|
+
])
|
|
198
|
+
|
|
199
|
+
// With partition
|
|
200
|
+
await queen.queue('tasks')
|
|
201
|
+
.partition('tenant-456')
|
|
202
|
+
.push([{ data: { action: 'process' } }])
|
|
203
|
+
|
|
204
|
+
// With custom transaction ID (for exactly-once)
|
|
205
|
+
await queen.queue('tasks').push([
|
|
206
|
+
{
|
|
207
|
+
transactionId: 'unique-id-123',
|
|
208
|
+
data: { value: 42 }
|
|
209
|
+
}
|
|
210
|
+
])
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Consume Messages (Long-Running Workers)
|
|
214
|
+
|
|
215
|
+
```javascript
|
|
216
|
+
// Runs forever, processes messages as they arrive
|
|
217
|
+
await queen.queue('tasks')
|
|
218
|
+
.concurrency(10) // 10 parallel workers
|
|
219
|
+
.batch(20) // Fetch 20 at a time
|
|
220
|
+
.consume(async (message) => {
|
|
221
|
+
await processTask(message.data)
|
|
222
|
+
// Auto-ack on success, auto-retry on error
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
// Process with limit and stop
|
|
226
|
+
await queen.queue('tasks')
|
|
227
|
+
.limit(100)
|
|
228
|
+
.consume(async (message) => {
|
|
229
|
+
await processTask(message.data)
|
|
230
|
+
})
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Pop Messages (On-Demand Processing)
|
|
234
|
+
|
|
235
|
+
```javascript
|
|
236
|
+
// Grab messages manually
|
|
237
|
+
const messages = await queen.queue('tasks')
|
|
238
|
+
.batch(10)
|
|
239
|
+
.wait(true) // Long polling
|
|
240
|
+
.pop()
|
|
241
|
+
|
|
242
|
+
// Manual acknowledgment
|
|
243
|
+
for (const message of messages) {
|
|
244
|
+
try {
|
|
245
|
+
await processMessage(message.data)
|
|
246
|
+
await queen.ack(message, true) // Success
|
|
247
|
+
} catch (error) {
|
|
248
|
+
await queen.ack(message, false) // Retry
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Transactions (Atomic Operations)
|
|
254
|
+
|
|
255
|
+
```javascript
|
|
256
|
+
// Pop from queue A
|
|
257
|
+
const messages = await queen.queue('input').pop()
|
|
258
|
+
|
|
259
|
+
// Atomically: ack input AND push output
|
|
260
|
+
await queen.transaction()
|
|
261
|
+
.ack(messages[0])
|
|
262
|
+
.queue('output')
|
|
263
|
+
.push([{ data: processedResult }])
|
|
264
|
+
.commit()
|
|
265
|
+
|
|
266
|
+
// If commit fails, nothing happens - message stays in input queue
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Client-Side Buffering (High Throughput)
|
|
270
|
+
|
|
271
|
+
```javascript
|
|
272
|
+
// Buffer messages locally, batch to server
|
|
273
|
+
for (let i = 0; i < 10000; i++) {
|
|
274
|
+
await queen.queue('events')
|
|
275
|
+
.buffer({ messageCount: 500, timeMillis: 1000 })
|
|
276
|
+
.push([{ data: { id: i } }])
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Flush remaining buffered messages
|
|
280
|
+
await queen.flushAllBuffers()
|
|
281
|
+
|
|
282
|
+
// Result: 10x-100x faster than individual pushes
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Dead Letter Queue
|
|
286
|
+
|
|
287
|
+
```javascript
|
|
288
|
+
// Enable DLQ on queue
|
|
289
|
+
await queen.queue('risky')
|
|
290
|
+
.config({ retryLimit: 3, dlqAfterMaxRetries: true })
|
|
291
|
+
.create()
|
|
292
|
+
|
|
293
|
+
// Query failed messages
|
|
294
|
+
const dlq = await queen.queue('risky')
|
|
295
|
+
.dlq()
|
|
296
|
+
.limit(10)
|
|
297
|
+
.get()
|
|
298
|
+
|
|
299
|
+
console.log(`Found ${dlq.total} failed messages`)
|
|
300
|
+
for (const msg of dlq.messages) {
|
|
301
|
+
console.log('Error:', msg.errorMessage)
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Message Tracing
|
|
306
|
+
|
|
307
|
+
```javascript
|
|
308
|
+
await queen.queue('orders').consume(async (msg) => {
|
|
309
|
+
const orderId = msg.data.orderId
|
|
310
|
+
|
|
311
|
+
// Record trace with name for cross-service correlation
|
|
312
|
+
await msg.trace({
|
|
313
|
+
traceName: `order-${orderId}`,
|
|
314
|
+
eventType: 'info',
|
|
315
|
+
data: { text: 'Order processing started' }
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
await processOrder(msg.data)
|
|
319
|
+
|
|
320
|
+
await msg.trace({
|
|
321
|
+
traceName: `order-${orderId}`,
|
|
322
|
+
eventType: 'processing',
|
|
323
|
+
data: {
|
|
324
|
+
text: 'Order completed',
|
|
325
|
+
total: msg.data.total
|
|
326
|
+
}
|
|
327
|
+
})
|
|
193
328
|
})
|
|
329
|
+
|
|
330
|
+
// View traces in webapp: Traces → Search "order-12345"
|
|
194
331
|
```
|
|
195
332
|
|
|
196
|
-
|
|
333
|
+
---
|
|
197
334
|
|
|
198
|
-
|
|
335
|
+
## Examples
|
|
199
336
|
|
|
200
|
-
|
|
337
|
+
### Complete Pipeline with Consumer Groups
|
|
201
338
|
|
|
202
|
-
|
|
339
|
+
```javascript
|
|
340
|
+
import { Queen } from 'queen-mq'
|
|
203
341
|
|
|
204
|
-
|
|
342
|
+
const queen = new Queen('http://localhost:6632')
|
|
205
343
|
|
|
206
|
-
|
|
344
|
+
// Stage 1: Ingest with buffering
|
|
345
|
+
async function ingestEvents() {
|
|
346
|
+
for (let i = 0; i < 10000; i++) {
|
|
347
|
+
await queen.queue('raw-events')
|
|
348
|
+
.partition(`user-${i % 100}`)
|
|
349
|
+
.buffer({ messageCount: 500, timeMillis: 1000 })
|
|
350
|
+
.push([{ data: { userId: i % 100, event: 'page_view' } }])
|
|
351
|
+
}
|
|
352
|
+
await queen.flushAllBuffers()
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Stage 2: Process with transactions
|
|
356
|
+
async function processEvents() {
|
|
357
|
+
await queen.queue('raw-events')
|
|
358
|
+
.group('processors')
|
|
359
|
+
.concurrency(5)
|
|
360
|
+
.batch(10)
|
|
361
|
+
.autoAck(false)
|
|
362
|
+
.consume(async (messages) => {
|
|
363
|
+
const results = messages.map(m => process(m.data))
|
|
364
|
+
|
|
365
|
+
// Atomic: ack all inputs, push all outputs
|
|
366
|
+
const txn = queen.transaction()
|
|
367
|
+
for (const msg of messages) txn.ack(msg)
|
|
368
|
+
txn.queue('processed-events').push(results.map(r => ({ data: r })))
|
|
369
|
+
await txn.commit()
|
|
370
|
+
})
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Stage 3: Separate analytics consumer (fan-out)
|
|
374
|
+
async function analytics() {
|
|
375
|
+
await queen.queue('raw-events')
|
|
376
|
+
.group('analytics')
|
|
377
|
+
.subscriptionMode('new') // Skip backlog
|
|
378
|
+
.consume(async (message) => {
|
|
379
|
+
await logMetrics(message.data)
|
|
380
|
+
})
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
await ingestEvents()
|
|
384
|
+
await Promise.all([processEvents(), analytics()])
|
|
385
|
+
```
|
|
207
386
|
|
|
208
|
-
|
|
387
|
+
### Long-Running Tasks with Lease Renewal
|
|
209
388
|
|
|
210
|
-
|
|
389
|
+
```javascript
|
|
390
|
+
await queen.queue('video-processing')
|
|
391
|
+
.renewLease(true, 60000) // Renew every 60 seconds
|
|
392
|
+
.consume(async (message) => {
|
|
393
|
+
// Can take hours - lease keeps renewing automatically
|
|
394
|
+
await processVideo(message.data)
|
|
395
|
+
})
|
|
396
|
+
```
|
|
211
397
|
|
|
212
|
-
|
|
213
|
-
- 📊 Real-time dashboard with system metrics
|
|
214
|
-
- 📈 Message throughput visualization
|
|
215
|
-
- 🔍 Queue management and monitoring
|
|
216
|
-
- 👥 Consumer group tracking
|
|
217
|
-
- 💬 Message browser with trace timeline
|
|
218
|
-
- 🔎 **Trace explorer for debugging distributed workflows**
|
|
219
|
-
- 📉 Analytics and insights
|
|
220
|
-
- 🌓 Dark/light theme support
|
|
398
|
+
### Error Handling with Callbacks
|
|
221
399
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
400
|
+
```javascript
|
|
401
|
+
await queen.queue('tasks')
|
|
402
|
+
.autoAck(false)
|
|
403
|
+
.consume(async (message) => {
|
|
404
|
+
return await riskyOperation(message.data)
|
|
405
|
+
})
|
|
406
|
+
.onSuccess(async (message, result) => {
|
|
407
|
+
console.log('Success:', result)
|
|
408
|
+
await queen.ack(message, true)
|
|
409
|
+
})
|
|
410
|
+
.onError(async (message, error) => {
|
|
411
|
+
console.error('Failed:', error.message)
|
|
412
|
+
|
|
413
|
+
// Custom retry logic
|
|
414
|
+
if (error.message.includes('temporary')) {
|
|
415
|
+
await queen.ack(message, false) // Retry
|
|
416
|
+
} else {
|
|
417
|
+
await queen.ack(message, 'failed', { error: error.message })
|
|
418
|
+
}
|
|
419
|
+
})
|
|
227
420
|
```
|
|
228
421
|
|
|
229
|
-
|
|
422
|
+
---
|
|
230
423
|
|
|
231
|
-
|
|
424
|
+
## API Reference
|
|
232
425
|
|
|
233
|
-
|
|
426
|
+
### Queue Operations
|
|
234
427
|
|
|
235
|
-
|
|
428
|
+
```javascript
|
|
429
|
+
// Create
|
|
430
|
+
await queen.queue('my-queue').create()
|
|
431
|
+
await queen.queue('my-queue').config({ priority: 5 }).create()
|
|
236
432
|
|
|
237
|
-
|
|
433
|
+
// Delete
|
|
434
|
+
await queen.queue('my-queue').delete()
|
|
238
435
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
- **Response Timers**: Per-worker timers (25ms tick) drain response queue back to clients
|
|
243
|
-
- **DB ThreadPool**: Separate pool for blocking PostgreSQL operations
|
|
244
|
-
- **Poll Workers**: 2 reserved threads for long-polling with adaptive backoff (100ms→2000ms)
|
|
245
|
-
- **Poll Intention Registry**: Thread-safe store for long-poll requests
|
|
246
|
-
- **Database Pool**: 150 shared PostgreSQL connections (libpq) with mutex/condition variable
|
|
247
|
-
- **Response Queue**: Thread-safe queue decoupling DB results from event loop responses
|
|
436
|
+
// Get info
|
|
437
|
+
const info = await queen.getQueueInfo('my-queue')
|
|
438
|
+
```
|
|
248
439
|
|
|
249
|
-
|
|
250
|
-
1. Client → Acceptor → Worker (event loop)
|
|
251
|
-
2. Worker registers response, submits job to DB ThreadPool
|
|
252
|
-
3. DB thread executes query, pushes result to Response Queue
|
|
253
|
-
4. Worker's response timer drains queue, sends HTTP response
|
|
440
|
+
### Push
|
|
254
441
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
5. Timeouts detected by Poll Workers, send 204 No Content
|
|
442
|
+
```javascript
|
|
443
|
+
await queen.queue('q').push([{ data: { value: 1 } }])
|
|
444
|
+
await queen.queue('q').partition('p1').push([{ data: { value: 1 } }])
|
|
445
|
+
await queen.queue('q').buffer({ messageCount: 100, timeMillis: 1000 }).push([...])
|
|
446
|
+
```
|
|
261
447
|
|
|
262
|
-
|
|
448
|
+
### Pop
|
|
263
449
|
|
|
264
|
-
|
|
450
|
+
```javascript
|
|
451
|
+
const msgs = await queen.queue('q').pop()
|
|
452
|
+
const msgs = await queen.queue('q').batch(10).pop()
|
|
453
|
+
const msgs = await queen.queue('q').batch(10).wait(true).pop()
|
|
454
|
+
```
|
|
265
455
|
|
|
266
|
-
|
|
456
|
+
### Consume
|
|
267
457
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
-
|
|
458
|
+
```javascript
|
|
459
|
+
await queen.queue('q').consume(async (msg) => { /* process */ })
|
|
460
|
+
await queen.queue('q').limit(10).consume(async (msg) => { /* process */ })
|
|
461
|
+
await queen.queue('q').concurrency(5).consume(async (msg) => { /* 5 workers */ })
|
|
462
|
+
await queen.queue('q').group('my-group').consume(async (msg) => { /* consumer group */ })
|
|
463
|
+
```
|
|
273
464
|
|
|
274
|
-
|
|
465
|
+
### Acknowledgment
|
|
275
466
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
467
|
+
```javascript
|
|
468
|
+
await queen.ack(message, true) // Success
|
|
469
|
+
await queen.ack(message, false) // Retry
|
|
470
|
+
await queen.ack(message, false, { error: 'reason' })
|
|
471
|
+
await queen.ack([msg1, msg2], true) // Batch ack
|
|
279
472
|
```
|
|
280
473
|
|
|
281
|
-
|
|
474
|
+
### Transactions
|
|
282
475
|
|
|
283
|
-
|
|
476
|
+
```javascript
|
|
477
|
+
await queen.transaction()
|
|
478
|
+
.ack(message)
|
|
479
|
+
.queue('output')
|
|
480
|
+
.push([{ data: { result: 'processed' } }])
|
|
481
|
+
.commit()
|
|
482
|
+
```
|
|
284
483
|
|
|
285
|
-
###
|
|
286
|
-
- **Hardware:** Apple M4 Air (all components on same machine, 10 processors available)
|
|
287
|
-
- **Server:** 1 server, 4 workers, 95 total DB connections/threads
|
|
288
|
-
- **Database:** PostgreSQL in Docker
|
|
289
|
-
- **Client:** C++ benchmark tool (`benchmark/bin/benchmark`)
|
|
484
|
+
### Lease Renewal
|
|
290
485
|
|
|
291
|
-
|
|
486
|
+
```javascript
|
|
487
|
+
await queen.renew(message)
|
|
488
|
+
await queen.renew([msg1, msg2, msg3])
|
|
489
|
+
await queen.queue('q').renewLease(true, 60000).consume(async (msg) => { /* auto-renew */ })
|
|
490
|
+
```
|
|
292
491
|
|
|
293
|
-
|
|
294
|
-
|------|---------|----------|------------|------------|------------|------------|-----------|
|
|
295
|
-
| **Producer** | 100 | 10K | 1 | 100 | single-queue | **1,431 msg/sec** | 0.41 MB/sec |
|
|
296
|
-
| **Producer** | 100 | 10K | 1 | 100 | multi-queue | 527 msg/sec | 0.15 MB/sec |
|
|
297
|
-
| **Producer** | 10 | 1M | 1,000 | 100 | single-queue | **62,927 msg/sec** | 18.00 MB/sec |
|
|
298
|
-
| **Producer** | 10 | 1M | 1,000 | 100 | multi-queue | 49,663 msg/sec | 14.21 MB/sec |
|
|
299
|
-
| **Producer** | 10 | 1M | 10,000 | 10 | single-queue | 30,351 msg/sec | 8.68 MB/sec |
|
|
300
|
-
| **Consumer** | 10 | 1M | 1,000 | 100 | single-queue | **31,922 msg/sec** | 16.25 MB/sec |
|
|
301
|
-
| **Consumer** | 10 | 1M | 10,000 | 10 | single-queue | **82,974 msg/sec** | 42.25 MB/sec |
|
|
492
|
+
### Buffering
|
|
302
493
|
|
|
303
|
-
|
|
494
|
+
```javascript
|
|
495
|
+
await queen.flushAllBuffers()
|
|
496
|
+
await queen.queue('q').flushBuffer()
|
|
497
|
+
const stats = queen.getBufferStats()
|
|
498
|
+
```
|
|
304
499
|
|
|
305
|
-
|
|
306
|
-
- ✅ **Single-queue mode faster:** Better partition-level parallelism than multi-queue
|
|
307
|
-
- ✅ **Consumer performance:** Scales well with batch size (82K msg/sec with 10K batches)
|
|
308
|
-
- ✅ **Producer peak:** 62K msg/sec with optimal batch size (1,000)
|
|
309
|
-
- ⚠️ **Small batches:** Performance drops significantly with batch=1 (lock contention)
|
|
500
|
+
### Dead Letter Queue
|
|
310
501
|
|
|
311
|
-
|
|
502
|
+
```javascript
|
|
503
|
+
const dlq = await queen.queue('q').dlq().limit(10).get()
|
|
504
|
+
const dlq = await queen.queue('q').dlq('consumer-group').limit(10).get()
|
|
505
|
+
const dlq = await queen.queue('q').dlq().from('2025-01-01').to('2025-01-31').get()
|
|
506
|
+
```
|
|
312
507
|
|
|
313
|
-
###
|
|
508
|
+
### Shutdown
|
|
314
509
|
|
|
315
|
-
```
|
|
316
|
-
|
|
317
|
-
|
|
510
|
+
```javascript
|
|
511
|
+
await queen.close() // Flush buffers and close connections
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
---
|
|
515
|
+
|
|
516
|
+
## Configuration Defaults
|
|
517
|
+
|
|
518
|
+
### Client Defaults
|
|
519
|
+
|
|
520
|
+
```javascript
|
|
521
|
+
{
|
|
522
|
+
timeoutMillis: 30000,
|
|
523
|
+
retryAttempts: 3,
|
|
524
|
+
retryDelayMillis: 1000,
|
|
525
|
+
loadBalancingStrategy: 'round-robin',
|
|
526
|
+
enableFailover: true
|
|
527
|
+
}
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
### Queue Defaults
|
|
531
|
+
|
|
532
|
+
```javascript
|
|
533
|
+
{
|
|
534
|
+
leaseTime: 300, // 5 minutes
|
|
535
|
+
retryLimit: 3,
|
|
536
|
+
priority: 0,
|
|
537
|
+
delayedProcessing: 0,
|
|
538
|
+
windowBuffer: 0,
|
|
539
|
+
maxSize: 0, // Unlimited
|
|
540
|
+
retentionSeconds: 0, // Keep forever
|
|
541
|
+
encryptionEnabled: false
|
|
542
|
+
}
|
|
543
|
+
```
|
|
318
544
|
|
|
319
|
-
|
|
320
|
-
./bin/benchmark producer --threads 10 --count 1000000 --batch 1000 --partitions 100 --mode single-queue
|
|
545
|
+
### Consume Defaults
|
|
321
546
|
|
|
322
|
-
|
|
323
|
-
|
|
547
|
+
```javascript
|
|
548
|
+
{
|
|
549
|
+
concurrency: 1,
|
|
550
|
+
batch: 1,
|
|
551
|
+
autoAck: true,
|
|
552
|
+
wait: true, // Long polling
|
|
553
|
+
timeoutMillis: 30000,
|
|
554
|
+
limit: null, // Run forever
|
|
555
|
+
renewLease: false
|
|
556
|
+
}
|
|
324
557
|
```
|
|
325
558
|
|
|
326
|
-
|
|
559
|
+
---
|
|
327
560
|
|
|
328
|
-
##
|
|
561
|
+
## Logging
|
|
329
562
|
|
|
330
|
-
|
|
563
|
+
Enable detailed logging for debugging:
|
|
331
564
|
|
|
332
|
-
```
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
make deps
|
|
336
|
-
make build-only
|
|
337
|
-
DB_POOL_SIZE=50 ./bin/queen-server
|
|
565
|
+
```bash
|
|
566
|
+
export QUEEN_CLIENT_LOG=true
|
|
567
|
+
node your-app.js
|
|
338
568
|
```
|
|
339
569
|
|
|
340
|
-
|
|
570
|
+
Example output:
|
|
571
|
+
```
|
|
572
|
+
[2025-10-28T10:30:45.123Z] [INFO] [Queen.constructor] {"status":"initialized","urls":1}
|
|
573
|
+
[2025-10-28T10:30:45.234Z] [INFO] [QueueBuilder.push] {"queue":"tasks","partition":"Default","count":5}
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
---
|
|
577
|
+
|
|
578
|
+
## Best Practices
|
|
579
|
+
|
|
580
|
+
1. ✅ **Use `consume()` for workers** - Simpler API, handles retries automatically
|
|
581
|
+
2. ✅ **Use `pop()` for control** - When you need precise control over acking
|
|
582
|
+
3. ✅ **Buffer for speed** - Always use buffering when pushing many messages
|
|
583
|
+
4. ✅ **Partitions for order** - Use partitions when message order matters
|
|
584
|
+
5. ✅ **Consumer groups for scale** - Run multiple workers in the same group
|
|
585
|
+
6. ✅ **Transactions for consistency** - Use transactions for atomic operations
|
|
586
|
+
7. ✅ **Enable DLQ** - Always enable DLQ in production
|
|
587
|
+
8. ✅ **Renew long leases** - Use auto-renewal for long-running tasks
|
|
588
|
+
9. ✅ **Graceful shutdown** - Always call `queen.close()` before exiting
|
|
589
|
+
10. ✅ **Monitor DLQ** - Regularly check for failed messages
|
|
590
|
+
|
|
591
|
+
---
|
|
592
|
+
|
|
593
|
+
## TypeScript Support
|
|
341
594
|
|
|
342
|
-
|
|
343
|
-
- Build instructions and optimization
|
|
344
|
-
- Performance tuning (worker threads, database pool)
|
|
345
|
-
- Production deployment (systemd, Docker, load balancing)
|
|
346
|
-
- Troubleshooting common issues
|
|
347
|
-
- Benchmarking guides
|
|
595
|
+
Full TypeScript definitions included:
|
|
348
596
|
|
|
349
|
-
|
|
597
|
+
```typescript
|
|
598
|
+
import { Queen, Message, QueueConfig } from 'queen-mq'
|
|
350
599
|
|
|
351
|
-
|
|
600
|
+
const queen: Queen = new Queen('http://localhost:6632')
|
|
352
601
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
602
|
+
interface OrderData {
|
|
603
|
+
orderId: number
|
|
604
|
+
amount: number
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const messages: Message<OrderData>[] = await queen.queue('orders').pop()
|
|
356
608
|
```
|
|
357
609
|
|
|
358
|
-
|
|
610
|
+
---
|
|
359
611
|
|
|
360
|
-
|
|
612
|
+
## Documentation
|
|
361
613
|
|
|
362
|
-
|
|
614
|
+
- **[Complete V2 Guide](client-v2/README.md)** - Full tutorial with all features (94 test examples)
|
|
615
|
+
- **[HTTP API Reference](https://github.com/smartpricing/queen/blob/master/server/API.md)** - Raw HTTP endpoints
|
|
616
|
+
- **[Server Guide](https://github.com/smartpricing/queen/blob/master/server/README.md)** - Server setup and configuration
|
|
617
|
+
- **[Architecture Guide](https://github.com/smartpricing/queen/blob/master/docs/ARCHITECTURE.md)** - Deep dive into internals
|
|
363
618
|
|
|
364
|
-
|
|
619
|
+
---
|
|
365
620
|
|
|
366
|
-
|
|
621
|
+
## Support
|
|
367
622
|
|
|
368
|
-
|
|
623
|
+
- **GitHub:** [smartpricing/queen](https://github.com/smartpricing/queen)
|
|
624
|
+
- **Issues:** [GitHub Issues](https://github.com/smartpricing/queen/issues)
|
|
625
|
+
- **LinkedIn:** [Smartness](https://www.linkedin.com/company/smartness-com/)
|
|
369
626
|
|
|
370
|
-
|
|
371
|
-
**Issue:** Worker initialization timeout (30s → 3600s) now matches file buffer recovery timeout. This is a temporary fix.
|
|
627
|
+
---
|
|
372
628
|
|
|
373
|
-
|
|
374
|
-
- Make recovery non-blocking while preserving FIFO ordering guarantees
|
|
375
|
-
- Implement progressive readiness with memory-buffered queue during recovery
|
|
376
|
-
- Add configurable recovery timeout with graceful degradation
|
|
377
|
-
- See: `server/src/services/file_buffer.cpp:212` (MAX_STARTUP_RECOVERY_SECONDS)
|
|
378
|
-
- See: `server/src/acceptor_server.cpp:1876` (worker initialization timeout)
|
|
629
|
+
## License
|
|
379
630
|
|
|
631
|
+
Apache 2.0 - See [LICENSE.md](../LICENSE.md)
|
|
380
632
|
|
|
381
|
-
### Other TODO Items
|
|
382
|
-
- Mini streaming engine
|
|
383
|
-
- Proper concurrency on clients
|
|
384
|
-
- Check client failover
|
|
385
|
-
- Py client
|