queen-mq 0.3.1 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +309 -167
- package/client-js/client-v2/Queen.js +66 -0
- package/client-js/client-v2/README.md +67 -10
- package/client-js/client-v2/builders/QueueBuilder.js +7 -1
- package/client-js/client-v2/builders/TransactionBuilder.js +20 -4
- package/client-js/client-v2/stream/StreamBuilder.js +72 -0
- package/client-js/client-v2/stream/StreamConsumer.js +156 -0
- package/client-js/client-v2/stream/Window.js +140 -0
- package/client-js/test-v2/GETTING_STARTED.md +27 -0
- package/client-js/test-v2/MAINTENANCE_TEST.md +148 -0
- package/client-js/test-v2/README_SUBSCRIPTION_TESTS.md +201 -0
- package/client-js/test-v2/consume.js +11 -0
- package/client-js/test-v2/dlq.js +1 -1
- package/client-js/test-v2/load.js +2 -0
- package/client-js/test-v2/maintenance.js +261 -0
- package/client-js/test-v2/retention.js +69 -0
- package/client-js/test-v2/run.js +7 -1
- package/client-js/test-v2/subscription.js +198 -18
- package/client-js/test-v2/transaction.js +66 -4
- package/package.json +2 -2
- package/client-js/client/client.js +0 -1536
- package/client-js/client/index.js +0 -4
- package/client-js/client/utils/http.js +0 -173
- package/client-js/client/utils/loadBalancer.js +0 -152
- package/client-js/client/utils/retry.js +0 -41
- package/client-js/services/encryptionService.js +0 -82
- package/client-js/services/evictionService.js +0 -160
- package/client-js/services/retentionService.js +0 -162
- package/client-js/services/startupSync.js +0 -35
- package/client-js/test/README.md +0 -224
- package/client-js/test/advanced-client-tests.js +0 -761
- package/client-js/test/advanced-pattern-tests.js +0 -1137
- package/client-js/test/bus-mode-tests.js +0 -361
- package/client-js/test/core-tests.js +0 -457
- package/client-js/test/edge-case-tests.js +0 -562
- package/client-js/test/enterprise-tests.js +0 -637
- package/client-js/test/human.js +0 -162
- package/client-js/test/partition-locking-tests.js +0 -545
- package/client-js/test/partition-transaction-tests.js +0 -482
- package/client-js/test/qos0-tests.js +0 -334
- package/client-js/test/test-new.js +0 -370
- package/client-js/test/utils.js +0 -169
- package/client-js/test/window-buffer-test.js +0 -114
- package/client-js/utils/logger.js +0 -44
- package/client-js/utils/uuid.js +0 -5
package/README.md
CHANGED
|
@@ -2,15 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
<div align="center">
|
|
4
4
|
|
|
5
|
-
**A modern,
|
|
5
|
+
**A modern, performant message queue system built on PostgreSQL**
|
|
6
6
|
|
|
7
7
|
[](LICENSE.md)
|
|
8
8
|
[](https://nodejs.org/)
|
|
9
|
+
[](https://en.cppreference.com/w/cpp/17)
|
|
9
10
|
|
|
10
|
-
[Quick Start](#
|
|
11
|
+
[Quick Start](#one-single-example) • [Complete V2 Guide](client-js/client-v2/README.md) • [Webapp](#webapp) • [Server Setup](#install-the-server-and-configure-it) • [HTTP API](#raw-http-api)
|
|
11
12
|
|
|
12
13
|
<p align="center">
|
|
13
|
-
<img src="assets/queen-logo
|
|
14
|
+
<img src="assets/queen-logo.svg" alt="Queen Logo" width="120" />
|
|
14
15
|
</p>
|
|
15
16
|
|
|
16
17
|
</div>
|
|
@@ -21,6 +22,12 @@ Why "Queen"? Because years ago, when I first read the word "queue" in my mind, I
|
|
|
21
22
|
|
|
22
23
|
---
|
|
23
24
|
|
|
25
|
+
[QUICKSTART](docs/QUICKSTART.md)
|
|
26
|
+
|
|
27
|
+
Latest server production version is **0.6.3**.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
24
31
|
## Introduction
|
|
25
32
|
|
|
26
33
|
QueenMQ is a queue system written in C++ and backed by PostgreSQL, born from the need to manage many FIFO partitions for Smartchat with solid guarantees around delivery and failure handling. During the initial development, I realized that with a few simple additions to the original design, I could build a very powerful and flexible queue system. This project is almost entirely written by AI, with my supervision—only the test files (or a good part of them) are manually written.
|
|
@@ -29,6 +36,7 @@ Here are the main features:
|
|
|
29
36
|
- Unlimited FIFO partitions within queues
|
|
30
37
|
- Queue semantics like RabbitMQ
|
|
31
38
|
- Consumer groups over queues like Kafka
|
|
39
|
+
- Allows for all the patterns you need, from simple queues to complex workflows and request/response patterns
|
|
32
40
|
- QoS levels: Exactly-once delivery (with transactionId), at-least-once delivery, and at-most-once delivery
|
|
33
41
|
- Subscription modes for replay (new messages only or from a specific timestamp) and message history control
|
|
34
42
|
- Transactions between operations (push and ack mainly) for atomicity
|
|
@@ -36,19 +44,97 @@ Here are the main features:
|
|
|
36
44
|
- Lease renewal for long-running tasks
|
|
37
45
|
- Message tracing for debugging workflows
|
|
38
46
|
- Encryption of messages at DB level
|
|
47
|
+
- Automatic message retention and cleanup - Configurable per-queue retention policies
|
|
48
|
+
- Streaming capabilities for real-time aggregation and processing of messages
|
|
49
|
+
- A nice webapp for monitoring and managing the system
|
|
50
|
+
- 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
|
|
39
51
|
|
|
40
|
-
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
|
|
52
|
+
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.
|
|
41
53
|
|
|
42
|
-
With proper batching, the system can handle +
|
|
54
|
+
With proper batching, the system can handle +200k **messages** per second (not req/s) on modest hardware.
|
|
43
55
|
|
|
44
56
|
Main documentation:
|
|
45
|
-
- [Client Guide](client-js/client-v2/README.md)
|
|
57
|
+
- [Client Guide JS](client-js/client-v2/README.md)
|
|
58
|
+
- [Client Guide C++](client-cpp/README.md)
|
|
46
59
|
- [Server Guide](server/README.md)
|
|
47
|
-
- [
|
|
60
|
+
- [Streaming Guide](docs/STREAMING_USAGE.md)
|
|
61
|
+
- [API Reference](server/API.md)
|
|
62
|
+
- [Message Retention & Cleanup](docs/RETENTION.md)
|
|
48
63
|
- [Webapp](webapp/README.md)
|
|
64
|
+
- [Expose the Webapp behind a proxy](proxy/README.md)
|
|
65
|
+
|
|
66
|
+
## Concepts
|
|
67
|
+
|
|
68
|
+
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.
|
|
69
|
+
|
|
70
|
+
### Queues
|
|
71
|
+
|
|
72
|
+
Queues are a way to organize your messages into logical groups. Each queue is like a container for messages. You can have as many queues as you want, and you can configure each queue with different settings, like the lease time, retry limit, encryption, retention, priority, delayed processing, window buffer and retention.
|
|
73
|
+
|
|
74
|
+
### Partitions
|
|
75
|
+
|
|
76
|
+
Partitions are a way to organize your messages into logical ordered groups. Each partition is like a separate queue inside a queue, and messages in the same partition are guaranteed to be processed in order. Only one consumer/client can acquire the lock for a partition at a time. You can have as many partitions as you want, and you can have as many consumers/clients as you want. If the lease in not acknowledged in time, the message is released and can be acquired by another consumer/client. Use renewLease() to renew the lease during long-running tasks. Each different consumer group can acquire the lock for a partition independently, so you can have as many consumer groups as you want.
|
|
77
|
+
|
|
78
|
+
### Default partition and consumer group
|
|
79
|
+
|
|
80
|
+
The default partition (if not specified) is `Default`. If you don't specify a consumer group, the messages are processed in queue mode, that is creating a default consumer group named `__QUEUE_MODE__`. Each queue can be used at the same time by multiple consumer groups, and each consumer group can process messages from multiple partitions.
|
|
81
|
+
|
|
82
|
+
### Consumer groups
|
|
83
|
+
|
|
84
|
+
Consumer groups are a way to process messages for different purposes. Each consumer group is like a separate queue, and messages in the same consumer group partition are guaranteed to be processed in order. Each consumer group tracks its own position in the queue. You can start a consumer group from the beginning of the queue, from a specific timestamp, or only new messages.
|
|
85
|
+
|
|
86
|
+
### Subscription modes
|
|
87
|
+
|
|
88
|
+
Subscription modes are a way to control the message history that is processed. You can choose to process all messages (including historical ones), only new messages, or messages from a specific timestamp. Subscription modes are only available when using consumer groups.
|
|
89
|
+
|
|
90
|
+
**Server Default:** By default, new consumer groups process all historical messages. You can change this server-wide:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
export DEFAULT_SUBSCRIPTION_MODE="new" # Skip historical messages by default
|
|
94
|
+
./bin/queen-server
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
This is useful for real-time systems where only new messages matter, or to prevent accidental processing of large backlogs. Clients can still override with `.subscriptionMode()` if needed.
|
|
98
|
+
|
|
99
|
+
### Long polling (waiting for messages)
|
|
100
|
+
|
|
101
|
+
The client works with the pull model for pop operations, meaning that you need to explicitly request messages from the queue. Pop and consume mehtods can "wait" server side for messages to be available. When the method is called with wait=true, the method will block until messages are available or the timeout is reached. When the timeout is reached, the method returns an empty array. Long polling is a very efficient way to wait for messages, and it is the recommended way to consume messages.
|
|
102
|
+
|
|
103
|
+
### Lease renewal
|
|
104
|
+
|
|
105
|
+
Lease renewal is a way to keep the lock for a partition or consumer group alive. You can use lease renewal to prevent the lock from expiring and being acquired by another consumer/client.
|
|
106
|
+
|
|
107
|
+
### Ack and Nack
|
|
108
|
+
|
|
109
|
+
Ack and Nack are the way to acknowledge or not a message. Ack means that the message has been processed successfully, and Nack means that the message has not been processed successfully. Ack/Nack requires the partitionId, the transactionId and the leaseId to be specified. If the ack is coming from a consumer group, the consumer group name is also required. This logic is handled automatically by the client library, you don't need to worry about it. If the message has already been acknowledged, the ack will be ignored. Based on the configuration, the message can be automatically acknowledged or manually acknowledged.
|
|
110
|
+
|
|
111
|
+
### Transactions
|
|
112
|
+
|
|
113
|
+
Transactions are a way to ensure that a group of operations are atomic. You can use transactions to ensure that a group of operations are executed together, and if one of the operations fails, the entire transaction is rolled back. Transactions are useful to achieve the exactly-once guarantee. You can process a message on a queue, and forward to the next queue only if the consumer lease is still valid, concatenating ack and push into a single transaction.
|
|
49
114
|
|
|
115
|
+
### Dead letter queue
|
|
50
116
|
|
|
51
|
-
|
|
117
|
+
The dead letter queue is a way to handle messages that are not processed successfully. When a message is not processed successfully, it is moved to the dead letter queue. Messages goes in the DLQ when they are NACK after the retry limit is reached. You configure the retry limit for each queue at queue config.
|
|
118
|
+
|
|
119
|
+
## Comparison with RabbitMQ, Kafka, and NATS
|
|
120
|
+
|
|
121
|
+
For users familiar with existing message queue systems, here's how Queen's semantics and usage patterns compare:
|
|
122
|
+
|
|
123
|
+
### Conceptual Mapping
|
|
124
|
+
|
|
125
|
+
| Queen Concept | RabbitMQ Equivalent | Kafka Equivalent | NATS Equivalent |
|
|
126
|
+
|---------------|---------------------|------------------|-----------------|
|
|
127
|
+
| Queue | Queue | Topic | Stream (JetStream) |
|
|
128
|
+
| Partition | N/A (queues are single-consumer by default) | Partition | N/A |
|
|
129
|
+
| Consumer Group | Competing Consumers pattern | Consumer Group | Queue Group |
|
|
130
|
+
| Queue Mode (no group) | Exclusive consumer | N/A (always uses groups) | Single subscriber |
|
|
131
|
+
| Lease | Message TTL / Visibility timeout | N/A (commit-based) | Ack wait / nak delay |
|
|
132
|
+
| Ack/Nack | Ack/Nack | Commit offset | Ack/Nak |
|
|
133
|
+
| Transaction | Publisher confirms + consumer acks | Transactional producer/consumer | N/A |
|
|
134
|
+
| Dead Letter Queue | Dead Letter Exchange | N/A (manual) | N/A (manual) |
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
## One single example
|
|
52
138
|
|
|
53
139
|
> 📖 **[Complete Guide: client-js/client-v2/README.md](client-js/client-v2/README.md)** - Full tutorial with all features!
|
|
54
140
|
|
|
@@ -59,162 +145,191 @@ import { Queen } from 'queen-mq'
|
|
|
59
145
|
const queen = new Queen('http://localhost:6632')
|
|
60
146
|
|
|
61
147
|
// Create a queue
|
|
62
|
-
await queen
|
|
148
|
+
await queen
|
|
149
|
+
.queue('critical-task')
|
|
150
|
+
.config({
|
|
151
|
+
leaseTime: 10, // 10 seconds to process the messages (seconds)
|
|
152
|
+
})
|
|
153
|
+
.create()
|
|
63
154
|
|
|
64
155
|
// Push messages
|
|
65
|
-
await queen
|
|
66
|
-
|
|
67
|
-
|
|
156
|
+
await queen
|
|
157
|
+
.queue('critical-task')
|
|
158
|
+
.partition('tenant-123')
|
|
159
|
+
.push([
|
|
160
|
+
{
|
|
161
|
+
transactionId: 'my-id', // This is autogenerated, but you can set your own. Unique per queue partition.
|
|
162
|
+
data: { id: 123, description: 'Critical task' } // The message payload
|
|
163
|
+
},
|
|
68
164
|
])
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
165
|
+
.onSuccess(async (messages) => { // Not mandatory
|
|
166
|
+
console.log('Messages pushed successfully:', messages)
|
|
167
|
+
})
|
|
168
|
+
.onDuplicate(async (messages) => { // Not mandatory, triggered when a message with the same transactionId is pushed
|
|
169
|
+
console.warn('Duplicate transaction IDs detected')
|
|
170
|
+
})
|
|
171
|
+
.onError(async (messages, error) => { // Without callbacks, push throws an error if some messages are not pushed
|
|
172
|
+
console.error('Error pushing messages:', error)
|
|
75
173
|
})
|
|
76
174
|
|
|
77
|
-
//
|
|
78
|
-
const messages = await queen.queue('tasks').batch(10).pop()
|
|
79
|
-
for (const msg of messages) {
|
|
80
|
-
try {
|
|
81
|
-
await processMessage(msg.data)
|
|
82
|
-
await queen.ack(msg, true) // Success
|
|
83
|
-
} catch (error) {
|
|
84
|
-
await queen.ack(msg, false) // Retry
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Partitions (for ordering)
|
|
89
|
-
await queen
|
|
90
|
-
.queue('user-events')
|
|
91
|
-
.partition('user-123')
|
|
92
|
-
.push([{ data: { event: 'login' } }])
|
|
93
|
-
|
|
94
|
-
// Consumer groups (for scaling)
|
|
95
|
-
await queen
|
|
96
|
-
.queue('emails')
|
|
97
|
-
.group('processors')
|
|
98
|
-
.concurrency(5)
|
|
99
|
-
.consume(async (message) => {
|
|
100
|
-
await sendEmail(message.data)
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
// Subscription modes (control message history)
|
|
104
|
-
// Skip historical messages, only process new ones
|
|
105
|
-
await queen
|
|
106
|
-
.queue('events')
|
|
107
|
-
.group('realtime-monitor')
|
|
108
|
-
.subscriptionMode('new')
|
|
109
|
-
.consume(async (message) => {
|
|
110
|
-
console.log('New event only:', message.data)
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
// Start from a specific timestamp
|
|
114
|
-
const timestamp = '2025-10-28T10:00:00.000Z'
|
|
115
|
-
await queen
|
|
116
|
-
.queue('events')
|
|
117
|
-
.group('replay-from-timestamp')
|
|
118
|
-
.subscriptionFrom(timestamp)
|
|
119
|
-
.consume(async (message) => {
|
|
120
|
-
console.log('Replaying from 10am:', message.data)
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
// Transactions (atomic ack + push)
|
|
124
|
-
const [msg] = await queen.queue('input').pop()
|
|
175
|
+
// Consume messages
|
|
125
176
|
await queen
|
|
177
|
+
.queue('critical-task')
|
|
178
|
+
// The consumer group name, without this, the messages are processed in queue mode
|
|
179
|
+
.group('processor-consumer-group')
|
|
180
|
+
// 10 parallel workers
|
|
181
|
+
.concurrency(10)
|
|
182
|
+
// I want to manually ack/nack messages
|
|
183
|
+
.autoAck(false)
|
|
184
|
+
// 10 messages per batch, prefetch them
|
|
185
|
+
.batch(10)
|
|
186
|
+
// Auto-renew the lease for the messages every 2 seconds
|
|
187
|
+
.renewLease(true, 2000)
|
|
188
|
+
// Process each message individually
|
|
189
|
+
.each()
|
|
190
|
+
// Do your work here
|
|
191
|
+
.consume(async (message) => {
|
|
192
|
+
console.log('Processing:', message.data)
|
|
193
|
+
})
|
|
194
|
+
// Ack the messages if you processed them successfully
|
|
195
|
+
.onSuccess(async (message) => {
|
|
196
|
+
await queen
|
|
126
197
|
.transaction()
|
|
127
|
-
.
|
|
128
|
-
.
|
|
129
|
-
.push([{ data: {
|
|
130
|
-
.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
.
|
|
136
|
-
.
|
|
137
|
-
|
|
138
|
-
// Message tracing (for debugging and monitoring)
|
|
139
|
-
await queen.queue('orders').consume(async (msg) => {
|
|
140
|
-
await msg.trace({
|
|
141
|
-
traceName: ['tenant-acme', 'order-flow-123'],
|
|
142
|
-
data: { text: 'Processing started', orderId: msg.data.id }
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
// Process order...
|
|
146
|
-
|
|
147
|
-
await msg.trace({
|
|
148
|
-
traceName: ['tenant-acme', 'order-flow-123'],
|
|
149
|
-
data: { text: 'Order completed' }
|
|
150
|
-
})
|
|
198
|
+
.queue('critical-task-next')
|
|
199
|
+
.partition('XXX')
|
|
200
|
+
.push([{ data: { message: 'Final', count: 3 } }])
|
|
201
|
+
.ack(message)
|
|
202
|
+
.commit()
|
|
203
|
+
})
|
|
204
|
+
// Nack the messages if you failed to process them
|
|
205
|
+
.onError(async (message, error) => {
|
|
206
|
+
console.error('Error processing messages:', error)
|
|
207
|
+
await queen.ack(message, false)
|
|
151
208
|
})
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Webapp
|
|
212
|
+
|
|
213
|
+
A modern Vue 3 web interface for managing and monitoring Queen MQ.
|
|
214
|
+
|
|
215
|
+

|
|
216
|
+
|
|
217
|
+

|
|
218
|
+
|
|
219
|
+

|
|
152
220
|
|
|
153
|
-
|
|
154
|
-
|
|
221
|
+

|
|
222
|
+
|
|
223
|
+

|
|
224
|
+
|
|
225
|
+

|
|
226
|
+
|
|
227
|
+
**Features:**
|
|
228
|
+
- 📊 Real-time dashboard with system metrics
|
|
229
|
+
- 📈 Message throughput visualization
|
|
230
|
+
- 🔍 Queue management and monitoring
|
|
231
|
+
- 👥 Consumer group tracking
|
|
232
|
+
- 💬 Message browser with trace timeline
|
|
233
|
+
- 🔎 **Trace explorer for debugging distributed workflows**
|
|
234
|
+
- 📉 Analytics and insights
|
|
235
|
+
- 🌓 Dark/light theme support
|
|
236
|
+
|
|
237
|
+
**Quick Start:**
|
|
238
|
+
```bash
|
|
239
|
+
cd webapp
|
|
240
|
+
npm install
|
|
241
|
+
npm run dev
|
|
155
242
|
```
|
|
156
243
|
|
|
157
|
-
|
|
158
|
-
- ✅ Fluent, chainable API
|
|
159
|
-
- ✅ Auto-acknowledgment (or manual control)
|
|
160
|
-
- ✅ Partitions for ordered processing
|
|
161
|
-
- ✅ Consumer groups for scaling
|
|
162
|
-
- ✅ **Subscription modes (new messages only or from timestamp)**
|
|
163
|
-
- ✅ Transactions for atomicity
|
|
164
|
-
- ✅ Client-side buffering for speed
|
|
165
|
-
- ✅ Dead letter queue for failures
|
|
166
|
-
- ✅ Lease renewal for long tasks
|
|
167
|
-
- ✅ **Message tracing for debugging workflows**
|
|
168
|
-
- ✅ Graceful shutdown with buffer flush
|
|
169
|
-
|
|
170
|
-
## 📚 Examples
|
|
171
|
-
|
|
172
|
-
### Client
|
|
173
|
-
|
|
174
|
-
See the **[Complete V2 Guide](client-js/client-v2/README.md)** with 14 parts covering everything from basics to advanced features:
|
|
175
|
-
- Queue creation, push, and consume
|
|
176
|
-
- Partitions and consumer groups
|
|
177
|
-
- **Subscription modes (new messages, timestamps)**
|
|
178
|
-
- Transactions and buffering
|
|
179
|
-
- Dead letter queues
|
|
180
|
-
- Lease renewal
|
|
181
|
-
- Message tracing
|
|
182
|
-
- Complete real-world pipeline example
|
|
183
|
-
- And much more!
|
|
184
|
-
|
|
185
|
-
**Test Files** (94 working examples): [client-js/test-v2/](client-js/test-v2/)
|
|
244
|
+
The dashboard will be available at `http://localhost:4000` or at `http://localhost:6632` directly from the server.
|
|
186
245
|
|
|
246
|
+
See [webapp/README.md](webapp/README.md) for more details.
|
|
187
247
|
|
|
188
248
|
## Architecture
|
|
189
249
|
|
|
190
|
-
Queen uses a high-performance **acceptor/worker pattern** with uWebSockets,
|
|
250
|
+
Queen uses a high-performance **acceptor/worker pattern** with uWebSockets, featuring a **fully asynchronous, non-blocking PostgreSQL architecture** for maximum throughput and minimal latency.
|
|
251
|
+
|
|
252
|
+
### Core Components
|
|
253
|
+
|
|
254
|
+
**Network Layer:**
|
|
255
|
+
- **UWS Acceptor**: Single thread listening on port 6632, distributes connections round-robin to workers
|
|
256
|
+
- **UWS Workers**: Configurable event loop threads (default: 10) handling HTTP routes and WebSocket connections
|
|
257
|
+
|
|
258
|
+
**Database Layer:**
|
|
259
|
+
- **AsyncDbPool**: Non-blocking PostgreSQL connection pool (142 connections) using libpq async API
|
|
260
|
+
- Socket-based I/O with `select()` for non-blocking operations
|
|
261
|
+
- RAII-based resource management with automatic connection cleanup
|
|
262
|
+
- Connection health monitoring and automatic reset
|
|
263
|
+
- Thread-safe with mutex/condition variable synchronization
|
|
264
|
+
- **AsyncQueueManager**: Event-loop-based queue operations
|
|
265
|
+
- Direct execution in worker threads for PUSH, POP, ACK, and TRANSACTION operations
|
|
266
|
+
- Batch processing with dynamic sizing
|
|
267
|
+
- Encryption support with status checks
|
|
268
|
+
- Automatic failover to file buffer when database unavailable
|
|
191
269
|
|
|
192
|
-
**
|
|
270
|
+
**Background Services:**
|
|
271
|
+
- **Poll Workers**: 4 dedicated threads for long-polling operations
|
|
272
|
+
- Non-blocking I/O with exponential backoff (100ms→2000ms)
|
|
273
|
+
- Intention registry for efficient request grouping
|
|
274
|
+
- Rate-limited queries to prevent database overload
|
|
275
|
+
- **Background Pool**: 8 connections for metrics, retention, eviction, and stream management
|
|
276
|
+
|
|
277
|
+
### Request Flow
|
|
278
|
+
|
|
279
|
+
**Standard Operations (PUSH/POP/ACK/TRANSACTION):**
|
|
280
|
+
```
|
|
281
|
+
Client Request
|
|
282
|
+
↓
|
|
283
|
+
Acceptor (port 6632)
|
|
284
|
+
↓
|
|
285
|
+
Worker (event loop) → AsyncQueueManager → AsyncDbPool → PostgreSQL
|
|
286
|
+
↓ (non-blocking) (socket I/O)
|
|
287
|
+
Response sent immediately
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
**Long-Polling Operations (wait=true):**
|
|
291
|
+
```
|
|
292
|
+
Client Request
|
|
293
|
+
↓
|
|
294
|
+
Worker registers intention in Registry
|
|
295
|
+
↓
|
|
296
|
+
Poll Worker (50ms interval)
|
|
297
|
+
↓
|
|
298
|
+
Non-blocking query via AsyncDbPool
|
|
299
|
+
↓
|
|
300
|
+
Messages distributed to waiting clients
|
|
301
|
+
```
|
|
193
302
|
|
|
194
|
-
|
|
195
|
-
- **UWS Acceptor**: Single thread listening on port 6632, round-robin distributes to workers
|
|
196
|
-
- **UWS Workers**: N event loop threads (default: 10) handling HTTP routes and WebSocket
|
|
197
|
-
- **Response Timers**: Per-worker timers (25ms tick) drain response queue back to clients
|
|
198
|
-
- **DB ThreadPool**: Separate pool for blocking PostgreSQL operations
|
|
199
|
-
- **Poll Workers**: 2 reserved threads for long-polling with adaptive backoff (100ms→2000ms)
|
|
200
|
-
- **Poll Intention Registry**: Thread-safe store for long-poll requests
|
|
201
|
-
- **Database Pool**: 150 shared PostgreSQL connections (libpq) with mutex/condition variable
|
|
202
|
-
- **Response Queue**: Thread-safe queue decoupling DB results from event loop responses
|
|
303
|
+
### Performance Characteristics
|
|
203
304
|
|
|
204
|
-
**
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
305
|
+
**Latency:**
|
|
306
|
+
- **POP (immediate)**: 10-50ms
|
|
307
|
+
- **ACK**: 10-50ms
|
|
308
|
+
- **TRANSACTION**: 50-200ms
|
|
309
|
+
- **Long-polling**: Configurable (50ms-2000ms backoff)
|
|
209
310
|
|
|
210
|
-
**
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
4. Messages distributed to waiting clients via Response Queue
|
|
215
|
-
5. Timeouts detected by Poll Workers, send 204 No Content
|
|
311
|
+
**Throughput:**
|
|
312
|
+
- **Peak**: 148,000+ msg/s
|
|
313
|
+
- **Sustained**: 130,000+ msg/s
|
|
314
|
+
- **Batch push**: 5,000-8,000 msg/s (with batches of 100)
|
|
216
315
|
|
|
217
|
-
|
|
316
|
+
**Resource Usage:**
|
|
317
|
+
- **Database connections**: 150 total (142 async + 8 background)
|
|
318
|
+
- **Threads**: 14 total (10 workers + 4 poll workers)
|
|
319
|
+
- **Memory**: ~80MB for thread stacks + connection overhead
|
|
320
|
+
|
|
321
|
+
### Scalability
|
|
322
|
+
|
|
323
|
+
The event-driven architecture enables:
|
|
324
|
+
- ✅ Unlimited concurrent requests (limited only by connection pool)
|
|
325
|
+
- ✅ Horizontal scaling (multiple server instances)
|
|
326
|
+
- ✅ Efficient resource utilization (non-blocking I/O)
|
|
327
|
+
- ✅ Low latency under high load
|
|
328
|
+
- ✅ Automatic load distribution across workers
|
|
329
|
+
|
|
330
|
+
**📚 Technical Documentation:**
|
|
331
|
+
- [Server Architecture Guide](server/README.md) - Complete server setup and configuration
|
|
332
|
+
- [Architecture Diagrams](assets/architecture.svg) - Visual architecture overview
|
|
218
333
|
|
|
219
334
|
### PostgreSQL Failover
|
|
220
335
|
|
|
@@ -233,32 +348,59 @@ Queen automatically buffers messages to disk when PostgreSQL is unavailable - **
|
|
|
233
348
|
FILE_BUFFER_DIR=/custom/path ./bin/queen-server
|
|
234
349
|
```
|
|
235
350
|
|
|
236
|
-
##
|
|
351
|
+
## Performance Benchmarks
|
|
237
352
|
|
|
238
|
-
|
|
353
|
+
**Preliminary results from C++ client benchmark** (detailed benchmarks on dedicated hardware coming soon)
|
|
239
354
|
|
|
240
|
-
|
|
241
|
-
-
|
|
242
|
-
-
|
|
243
|
-
-
|
|
244
|
-
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
355
|
+
### Test Environment
|
|
356
|
+
- **Hardware:** Apple M4 Air (all components on same machine, 10 processors available)
|
|
357
|
+
- **Server:** 1 server, 4 workers, 95 total DB connections/threads
|
|
358
|
+
- **Database:** PostgreSQL in Docker
|
|
359
|
+
- **Client:** C++ benchmark tool (`benchmark/bin/benchmark`)
|
|
360
|
+
|
|
361
|
+
### Results
|
|
362
|
+
|
|
363
|
+
All tests run with: `--threads 10 --partitions 10 --mode single-queue`
|
|
364
|
+
|
|
365
|
+
| Test | Mode | Messages | Batch Size | Throughput | Bandwidth |
|
|
366
|
+
|------|------|----------|------------|------------|-----------|
|
|
367
|
+
| **T1** | Producer | 10,000 | 1 | 785 msg/sec | 0.22 MB/sec |
|
|
368
|
+
| **T1** | Consumer | 10,000 | 1 | 456 msg/sec | 0.23 MB/sec |
|
|
369
|
+
| **T2** | Producer | 10,000 | 10 | 7,677 msg/sec | 2.20 MB/sec |
|
|
370
|
+
| **T2** | Consumer | 10,000 | 10 | 4,989 msg/sec | 2.53 MB/sec |
|
|
371
|
+
| **T3** | Producer | 10,000 | 100 | 39,065 msg/sec | 11.18 MB/sec |
|
|
372
|
+
| **T3** | Consumer | 10,000 | 100 | 30,079 msg/sec | 15.26 MB/sec |
|
|
373
|
+
| **T4** | Producer | 10,000 | 1,000 | 85,862 msg/sec | 24.57 MB/sec |
|
|
374
|
+
| **T4** | Consumer | 10,000 | 1,000 | **488,650 msg/sec** | **247.87 MB/sec** |
|
|
375
|
+
| **T5** | Producer | 100,000 | 1,000 | **90,601 msg/sec** | **25.92 MB/sec** |
|
|
376
|
+
| **T5** | Consumer | 100,000 | 1,000 | 84,530 msg/sec | 42.96 MB/sec |
|
|
377
|
+
|
|
378
|
+
### Key Observations
|
|
379
|
+
|
|
380
|
+
- ✅ **Batch size matters:** Larger batches (1,000) dramatically improve throughput
|
|
381
|
+
- ✅ **Consumer performance:** Peaks at 488K msg/sec with batch size 1,000 (247 MB/sec bandwidth)
|
|
382
|
+
- ✅ **Producer peak:** 90K msg/sec with batch size 1,000 on 100K messages
|
|
383
|
+
- ⚠️ **Small batches:** Performance drops significantly with batch=1 (lock contention)
|
|
384
|
+
- 📈 **Scalability:** Performance improves with larger message volumes (T4 vs T5)
|
|
385
|
+
|
|
386
|
+
**Note:** All timing metrics are based on processing time (excludes idle time) and measure actual message processing (first message → last message).
|
|
387
|
+
|
|
388
|
+
### Run Your Own Benchmarks
|
|
249
389
|
|
|
250
|
-
**Quick Start:**
|
|
251
390
|
```bash
|
|
252
|
-
cd
|
|
253
|
-
|
|
254
|
-
npm run dev
|
|
255
|
-
```
|
|
391
|
+
cd benchmark
|
|
392
|
+
make
|
|
256
393
|
|
|
257
|
-
|
|
394
|
+
# Producer
|
|
395
|
+
./bin/benchmark producer --threads 10 --count 1000000 --batch 1000 --partitions 100 --mode single-queue
|
|
258
396
|
|
|
259
|
-
|
|
397
|
+
# Consumer
|
|
398
|
+
./bin/benchmark consumer --threads 10 --batch 1000 --partitions 100 --mode single-queue
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
See [benchmark/README.md](benchmark/README.md) for detailed usage.
|
|
260
402
|
|
|
261
|
-
## Install server and configure it
|
|
403
|
+
## Install the server and configure it
|
|
262
404
|
|
|
263
405
|
### Quick Start
|
|
264
406
|
|
|
@@ -296,7 +438,7 @@ Includes:
|
|
|
296
438
|
|
|
297
439
|
You can use Queen directly from HTTP without the JS client.
|
|
298
440
|
|
|
299
|
-
[Here the complete list of API endpoints](API.md)
|
|
441
|
+
[Here the complete list of API endpoints](server/API.md)
|
|
300
442
|
|
|
301
443
|
## ⚠️ Known Issues & Roadmap
|
|
302
444
|
|
|
@@ -312,6 +454,6 @@ You can use Queen directly from HTTP without the JS client.
|
|
|
312
454
|
|
|
313
455
|
|
|
314
456
|
### Other TODO Items
|
|
315
|
-
-
|
|
316
|
-
-
|
|
317
|
-
-
|
|
457
|
+
- Proper concurrency on clients
|
|
458
|
+
- Check client failover
|
|
459
|
+
- Py client
|
|
@@ -8,6 +8,8 @@ import { LoadBalancer } from './http/LoadBalancer.js'
|
|
|
8
8
|
import { BufferManager } from './buffer/BufferManager.js'
|
|
9
9
|
import { QueueBuilder } from './builders/QueueBuilder.js'
|
|
10
10
|
import { TransactionBuilder } from './builders/TransactionBuilder.js'
|
|
11
|
+
import { StreamBuilder } from './stream/StreamBuilder.js'
|
|
12
|
+
import { StreamConsumer } from './stream/StreamConsumer.js'
|
|
11
13
|
import { CLIENT_DEFAULTS } from './utils/defaults.js'
|
|
12
14
|
import { validateUrl, validateUrls } from './utils/validation.js'
|
|
13
15
|
import * as logger from './utils/logger.js'
|
|
@@ -355,6 +357,70 @@ export class Queen {
|
|
|
355
357
|
return stats
|
|
356
358
|
}
|
|
357
359
|
|
|
360
|
+
// ===========================
|
|
361
|
+
// Consumer Group Management
|
|
362
|
+
// ===========================
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Delete a consumer group and optionally its subscription metadata
|
|
366
|
+
* @param {string} consumerGroup - Consumer group name
|
|
367
|
+
* @param {boolean} deleteMetadata - Whether to delete subscription metadata (default: true)
|
|
368
|
+
* @returns {Promise<object>}
|
|
369
|
+
*/
|
|
370
|
+
async deleteConsumerGroup(consumerGroup, deleteMetadata = true) {
|
|
371
|
+
logger.log('Queen.deleteConsumerGroup', { consumerGroup, deleteMetadata })
|
|
372
|
+
|
|
373
|
+
const url = `/api/v1/consumer-groups/${encodeURIComponent(consumerGroup)}?deleteMetadata=${deleteMetadata}`
|
|
374
|
+
const response = await this.#httpClient.delete(url)
|
|
375
|
+
|
|
376
|
+
logger.log('Queen.deleteConsumerGroup', { success: true, consumerGroup })
|
|
377
|
+
return response
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Update subscription timestamp for a consumer group
|
|
382
|
+
* @param {string} consumerGroup - Consumer group name
|
|
383
|
+
* @param {string} timestamp - New subscription timestamp (ISO 8601)
|
|
384
|
+
* @returns {Promise<object>}
|
|
385
|
+
*/
|
|
386
|
+
async updateConsumerGroupTimestamp(consumerGroup, timestamp) {
|
|
387
|
+
logger.log('Queen.updateConsumerGroupTimestamp', { consumerGroup, timestamp })
|
|
388
|
+
|
|
389
|
+
const url = `/api/v1/consumer-groups/${encodeURIComponent(consumerGroup)}/subscription`
|
|
390
|
+
const response = await this.#httpClient.post(url, {
|
|
391
|
+
subscriptionTimestamp: timestamp
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
logger.log('Queen.updateConsumerGroupTimestamp', { success: true, consumerGroup })
|
|
395
|
+
return response
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// ===========================
|
|
399
|
+
// Streaming API
|
|
400
|
+
// ===========================
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Define a stream for windowed processing
|
|
404
|
+
* @param {string} name - Stream name
|
|
405
|
+
* @param {string} namespace - Stream namespace
|
|
406
|
+
* @returns {StreamBuilder}
|
|
407
|
+
*/
|
|
408
|
+
stream(name, namespace) {
|
|
409
|
+
logger.log('Queen.stream', { name, namespace })
|
|
410
|
+
return new StreamBuilder(this.#httpClient, this, name, namespace)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Create a consumer for a stream
|
|
415
|
+
* @param {string} streamName - Stream name
|
|
416
|
+
* @param {string} consumerGroup - Consumer group
|
|
417
|
+
* @returns {StreamConsumer}
|
|
418
|
+
*/
|
|
419
|
+
consumer(streamName, consumerGroup) {
|
|
420
|
+
logger.log('Queen.consumer', { streamName, consumerGroup })
|
|
421
|
+
return new StreamConsumer(this.#httpClient, this, streamName, consumerGroup)
|
|
422
|
+
}
|
|
423
|
+
|
|
358
424
|
// ===========================
|
|
359
425
|
// Graceful Shutdown
|
|
360
426
|
// ===========================
|