queen-mq 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/API.md +1116 -0
- package/CACHE.md +519 -0
- package/DASHBOARD-V3.md +478 -0
- package/DASHBOARD.md +382 -0
- package/MOD_QUEUE.md +453 -0
- package/PARTITION_LOCKING_DESIGN.md +989 -0
- package/PLAN.md +707 -0
- package/QUERY_ANALSYS.md +72 -0
- package/QUEUE_BUS.md +334 -0
- package/README.md +1495 -0
- package/V2-PLAN.md +236 -0
- package/assets/dashboard.png +0 -0
- package/dashboard/.vscode/extensions.json +3 -0
- package/dashboard/README.md +5 -0
- package/dashboard/index.html +14 -0
- package/dashboard/package-lock.json +1458 -0
- package/dashboard/package.json +25 -0
- package/dashboard/public/vite.svg +1 -0
- package/dashboard/src/App.vue +29 -0
- package/dashboard/src/assets/styles/main.css +908 -0
- package/dashboard/src/assets/vue.svg +1 -0
- package/dashboard/src/components/cards/MetricCard.vue +298 -0
- package/dashboard/src/components/charts/QueueDepthChart.vue +276 -0
- package/dashboard/src/components/charts/QueueLagChart.vue +436 -0
- package/dashboard/src/components/charts/ThroughputChart.vue +302 -0
- package/dashboard/src/components/common/ActivityFeed.vue +251 -0
- package/dashboard/src/components/layout/AppHeader.vue +208 -0
- package/dashboard/src/components/layout/AppLayout.vue +88 -0
- package/dashboard/src/components/layout/AppSidebar.vue +261 -0
- package/dashboard/src/main.js +44 -0
- package/dashboard/src/router.js +54 -0
- package/dashboard/src/services/api.js +187 -0
- package/dashboard/src/services/websocket.js +167 -0
- package/dashboard/src/utils/constants.js +56 -0
- package/dashboard/src/utils/helpers.js +118 -0
- package/dashboard/src/views/Analytics.vue +912 -0
- package/dashboard/src/views/Dashboard.vue +906 -0
- package/dashboard/src/views/Messages.vue +437 -0
- package/dashboard/src/views/QueueDetail.vue +501 -0
- package/dashboard/src/views/Queues.vue +333 -0
- package/dashboard/vite.config.js +30 -0
- package/debug-namespace.js +110 -0
- package/docs/long-polling.md +159 -0
- package/docs/multi-server-cache-solutions.md +185 -0
- package/docs/performance-tuning.md +222 -0
- package/examples/bus-mode.js +239 -0
- package/examples/continuous-consumer-optimized.js +215 -0
- package/examples/continuous-consumer.js +159 -0
- package/examples/continuous-producer.js +343 -0
- package/examples/mixed-mode.js +277 -0
- package/examples/multi-server-test.js +305 -0
- package/examples/single.js +64 -0
- package/examples/smartchat-dealyed.js +42 -0
- package/examples/smartchat.js +52 -0
- package/examples/test-cache-invalidation.js +119 -0
- package/examples/test-cache-multi-server.js +245 -0
- package/examples/test-minimal-client.js +112 -0
- package/examples/test-queue-creation-policy.js +137 -0
- package/init-db.js +20 -0
- package/package.json +36 -0
- package/src/client/client.js +291 -0
- package/src/client/index.js +6 -0
- package/src/client/queenClient.js +513 -0
- package/src/client/utils/http.js +172 -0
- package/src/client/utils/loadBalancer.js +152 -0
- package/src/client/utils/retry.js +35 -0
- package/src/config.js +215 -0
- package/src/database/connection.js +103 -0
- package/src/database/poolManager.js +192 -0
- package/src/database/schema-v2.sql +214 -0
- package/src/managers/eventManager.js +59 -0
- package/src/managers/queueManagerOptimized.js +1512 -0
- package/src/managers/resourceCache.js +96 -0
- package/src/managers/systemEventManager.js +127 -0
- package/src/routes/ack.js +26 -0
- package/src/routes/analytics.js +812 -0
- package/src/routes/configure.js +46 -0
- package/src/routes/messages.js +298 -0
- package/src/routes/pop.js +85 -0
- package/src/routes/push.js +28 -0
- package/src/routes/resources.js +296 -0
- package/src/server.js +1286 -0
- package/src/services/encryptionService.js +82 -0
- package/src/services/evictionService.js +131 -0
- package/src/services/retentionService.js +129 -0
- package/src/services/startupSync.js +35 -0
- package/src/test/test.js +4521 -0
- package/src/utils/logger.js +44 -0
- package/src/utils/uuid.js +5 -0
- package/src/websocket/wsServer.js +221 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Continuous Producer Example for Queen V2
|
|
5
|
+
*
|
|
6
|
+
* This example demonstrates a continuous message producer that pushes
|
|
7
|
+
* messages to a Queen queue at regular intervals. It's designed to work
|
|
8
|
+
* with the continuous-consumer.js example to demonstrate the full
|
|
9
|
+
* message flow with long polling.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { createQueenClient } from '../src/client/index.js';
|
|
13
|
+
|
|
14
|
+
async function main() {
|
|
15
|
+
// Create client with configuration
|
|
16
|
+
const client = createQueenClient({
|
|
17
|
+
baseUrl: process.env.QUEEN_URL || 'http://localhost:6632',
|
|
18
|
+
timeout: 10000 // 10 second timeout for push operations
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// V2: Using queue and optional partition
|
|
22
|
+
const queue = process.env.QUEUE || 'email-translations';
|
|
23
|
+
const partition = process.env.PARTITION; // Optional: if not set, uses "Default"
|
|
24
|
+
const useMultiplePartitions = process.env.MULTI_PARTITION === 'true';
|
|
25
|
+
|
|
26
|
+
// Production rate configuration
|
|
27
|
+
const messagesPerBatch = parseInt(process.env.BATCH_SIZE) || 100;
|
|
28
|
+
const intervalMilliseconds = parseInt(process.env.INTERVAL) || 10000;
|
|
29
|
+
const burstMode = process.env.BURST === 'true';
|
|
30
|
+
const highPerformanceMode = process.env.HIGH_PERF === 'true';
|
|
31
|
+
|
|
32
|
+
console.log('š Starting continuous producer (V2)');
|
|
33
|
+
console.log(`š¦ Target Queue: ${queue}${partition ? '/' + partition : ''}`);
|
|
34
|
+
console.log(`š Configuration:`);
|
|
35
|
+
console.log(` - Messages per batch: ${messagesPerBatch}`);
|
|
36
|
+
console.log(` - Interval: ${intervalMilliseconds} milliseconds`);
|
|
37
|
+
console.log(` - Burst mode: ${burstMode ? 'enabled' : 'disabled'}`);
|
|
38
|
+
console.log(` - High performance: ${highPerformanceMode ? 'enabled' : 'disabled'}`);
|
|
39
|
+
console.log(` - Multiple partitions: ${useMultiplePartitions ? 'enabled' : 'disabled'}`);
|
|
40
|
+
|
|
41
|
+
if (highPerformanceMode) {
|
|
42
|
+
console.log('\nā” HIGH PERFORMANCE MODE ENABLED');
|
|
43
|
+
console.log(' Target: 10,000+ messages/second');
|
|
44
|
+
console.log(' Make sure server is running with: DB_POOL_SIZE=100 node src/server.js');
|
|
45
|
+
}
|
|
46
|
+
console.log('');
|
|
47
|
+
|
|
48
|
+
// Configure the queue
|
|
49
|
+
try {
|
|
50
|
+
await client.configure({
|
|
51
|
+
queue: queue,
|
|
52
|
+
// partition parameter removed - all config is queue-level now
|
|
53
|
+
options: {
|
|
54
|
+
retryLimit: 3,
|
|
55
|
+
retryDelay: 1000,
|
|
56
|
+
dlqAfterMaxRetries: true,
|
|
57
|
+
leaseTime: 300
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
console.log('ā
Queue configured successfully\n');
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.log('ā ļø Queue configuration failed (may already exist):', error.message, '\n');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let stats = {
|
|
66
|
+
batches: 0,
|
|
67
|
+
messages: 0,
|
|
68
|
+
errors: 0,
|
|
69
|
+
startTime: Date.now(),
|
|
70
|
+
lastBatchTime: null,
|
|
71
|
+
partitionCounts: {}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Partition selection for multi-partition mode
|
|
75
|
+
const partitions = useMultiplePartitions
|
|
76
|
+
? ['Default', 'urgent', 'bulk', 'priority', 'standard']
|
|
77
|
+
: [partition || 'Default'];
|
|
78
|
+
|
|
79
|
+
// Message generator function
|
|
80
|
+
function generateMessages(count) {
|
|
81
|
+
const messages = [];
|
|
82
|
+
const templates = [
|
|
83
|
+
{ type: 'welcome', subject: 'Welcome to our service!' },
|
|
84
|
+
{ type: 'notification', subject: 'New feature available' },
|
|
85
|
+
{ type: 'reminder', subject: 'Don\'t forget to check out...' },
|
|
86
|
+
{ type: 'newsletter', subject: 'Weekly updates' },
|
|
87
|
+
{ type: 'alert', subject: 'Important information' }
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
for (let i = 0; i < count; i++) {
|
|
91
|
+
const template = templates[Math.floor(Math.random() * templates.length)];
|
|
92
|
+
const messageId = `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
93
|
+
|
|
94
|
+
// Select partition based on message type or random
|
|
95
|
+
let selectedPartition;
|
|
96
|
+
if (useMultiplePartitions) {
|
|
97
|
+
// Route alerts to urgent, newsletters to bulk, etc.
|
|
98
|
+
if (template.type === 'alert') {
|
|
99
|
+
selectedPartition = 'urgent';
|
|
100
|
+
} else if (template.type === 'newsletter') {
|
|
101
|
+
selectedPartition = 'bulk';
|
|
102
|
+
} else if (Math.random() > 0.7) {
|
|
103
|
+
selectedPartition = 'priority';
|
|
104
|
+
} else {
|
|
105
|
+
selectedPartition = partitions[Math.floor(Math.random() * partitions.length)];
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
selectedPartition = partition || 'Default';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
messages.push({
|
|
112
|
+
queue: queue,
|
|
113
|
+
partition: selectedPartition,
|
|
114
|
+
payload: {
|
|
115
|
+
id: messageId,
|
|
116
|
+
type: template.type,
|
|
117
|
+
email: {
|
|
118
|
+
to: `user${Math.floor(Math.random() * 1000)}@example.com`,
|
|
119
|
+
subject: template.subject,
|
|
120
|
+
template: template.type,
|
|
121
|
+
variables: {
|
|
122
|
+
userName: `User${Math.floor(Math.random() * 1000)}`,
|
|
123
|
+
timestamp: new Date().toISOString(),
|
|
124
|
+
locale: ['en', 'es', 'fr', 'de'][Math.floor(Math.random() * 4)]
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
priority: Math.random() > 0.8 ? 'high' : 'normal',
|
|
128
|
+
createdAt: new Date().toISOString()
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Track partition usage
|
|
133
|
+
stats.partitionCounts[selectedPartition] = (stats.partitionCounts[selectedPartition] || 0) + 1;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return messages;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Producer function
|
|
140
|
+
async function produceMessages() {
|
|
141
|
+
const batchSize = burstMode
|
|
142
|
+
? messagesPerBatch * (1 + Math.floor(Math.random() * 3)) // 1x to 3x in burst mode
|
|
143
|
+
: messagesPerBatch;
|
|
144
|
+
|
|
145
|
+
const messages = generateMessages(batchSize);
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
console.log(`š¤ [${new Date().toISOString()}] Pushing ${messages.length} messages...`);
|
|
149
|
+
|
|
150
|
+
const result = await client.push({
|
|
151
|
+
items: messages
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
stats.batches++;
|
|
155
|
+
stats.messages += result.messages.length;
|
|
156
|
+
stats.lastBatchTime = Date.now();
|
|
157
|
+
|
|
158
|
+
console.log(`ā
Successfully pushed ${result.messages.length} messages`);
|
|
159
|
+
|
|
160
|
+
if (useMultiplePartitions) {
|
|
161
|
+
// Show partition distribution for this batch
|
|
162
|
+
const batchPartitions = {};
|
|
163
|
+
messages.forEach(m => {
|
|
164
|
+
batchPartitions[m.partition] = (batchPartitions[m.partition] || 0) + 1;
|
|
165
|
+
});
|
|
166
|
+
console.log(` Partition distribution:`, batchPartitions);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
console.log(` Sample IDs: ${result.messages.slice(0, 3).map(m => m.transactionId).join(', ')}${result.messages.length > 3 ? '...' : ''}`);
|
|
170
|
+
|
|
171
|
+
// Show sample message details occasionally
|
|
172
|
+
if (stats.batches % 5 === 0) {
|
|
173
|
+
console.log(` Sample payload:`, JSON.stringify(messages[0].payload, null, 2));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
} catch (error) {
|
|
177
|
+
stats.errors++;
|
|
178
|
+
console.error(`ā Failed to push messages:`, error.message);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Start producing messages
|
|
183
|
+
console.log('š Starting production cycle...\n');
|
|
184
|
+
|
|
185
|
+
if (highPerformanceMode) {
|
|
186
|
+
// High performance mode: Multiple concurrent producers
|
|
187
|
+
const concurrentProducers = 10;
|
|
188
|
+
const highPerfBatchSize = 1000;
|
|
189
|
+
const highPerfInterval = 100; // 10 producers * 1000 msgs * 10/sec = 100,000 msgs/sec theoretical max
|
|
190
|
+
|
|
191
|
+
console.log(`ā” Running ${concurrentProducers} concurrent producers`);
|
|
192
|
+
console.log(`ā” Each producing ${highPerfBatchSize} messages every ${highPerfInterval}ms`);
|
|
193
|
+
console.log(`ā” Theoretical max: ${(concurrentProducers * highPerfBatchSize * (1000/highPerfInterval)).toFixed(0)} messages/second\n`);
|
|
194
|
+
|
|
195
|
+
// Start multiple concurrent producers
|
|
196
|
+
const producers = [];
|
|
197
|
+
for (let i = 0; i < concurrentProducers; i++) {
|
|
198
|
+
const producerId = i;
|
|
199
|
+
|
|
200
|
+
// Stagger start times to avoid thundering herd
|
|
201
|
+
setTimeout(() => {
|
|
202
|
+
const interval = setInterval(async () => {
|
|
203
|
+
const messages = generateMessages(highPerfBatchSize);
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
const start = Date.now();
|
|
207
|
+
const result = await client.push({ items: messages });
|
|
208
|
+
const duration = Date.now() - start;
|
|
209
|
+
|
|
210
|
+
stats.batches++;
|
|
211
|
+
stats.messages += result.messages.length;
|
|
212
|
+
stats.lastBatchTime = Date.now();
|
|
213
|
+
|
|
214
|
+
if (stats.batches % 10 === 0) {
|
|
215
|
+
console.log(`ā” [Producer ${producerId}] Pushed ${result.messages.length} messages in ${duration}ms`);
|
|
216
|
+
}
|
|
217
|
+
} catch (error) {
|
|
218
|
+
stats.errors++;
|
|
219
|
+
console.error(`ā [Producer ${producerId}] Failed:`, error.message);
|
|
220
|
+
}
|
|
221
|
+
}, highPerfInterval);
|
|
222
|
+
|
|
223
|
+
producers.push(interval);
|
|
224
|
+
}, i * 10); // Stagger by 10ms
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Store intervals for cleanup
|
|
228
|
+
globalThis.highPerfProducers = producers;
|
|
229
|
+
} else {
|
|
230
|
+
// Normal mode: Single producer
|
|
231
|
+
// Initial batch
|
|
232
|
+
await produceMessages();
|
|
233
|
+
|
|
234
|
+
// Set up interval for continuous production
|
|
235
|
+
const productionInterval = setInterval(async () => {
|
|
236
|
+
await produceMessages();
|
|
237
|
+
}, intervalMilliseconds);
|
|
238
|
+
|
|
239
|
+
globalThis.productionInterval = productionInterval;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Statistics reporting
|
|
243
|
+
const statsInterval = setInterval(() => {
|
|
244
|
+
const uptime = Math.floor((Date.now() - stats.startTime) / 1000);
|
|
245
|
+
const rate = stats.messages / (uptime / 60);
|
|
246
|
+
|
|
247
|
+
console.log('\nš Production Statistics:');
|
|
248
|
+
console.log(` Uptime: ${formatUptime(uptime)}`);
|
|
249
|
+
console.log(` Batches sent: ${stats.batches}`);
|
|
250
|
+
console.log(` Messages sent: ${stats.messages}`);
|
|
251
|
+
console.log(` Production rate: ${rate.toFixed(2)} msg/min`);
|
|
252
|
+
console.log(` Errors: ${stats.errors}`);
|
|
253
|
+
|
|
254
|
+
if (useMultiplePartitions && Object.keys(stats.partitionCounts).length > 0) {
|
|
255
|
+
console.log(' Total partition distribution:');
|
|
256
|
+
for (const [part, count] of Object.entries(stats.partitionCounts)) {
|
|
257
|
+
const percentage = ((count / stats.messages) * 100).toFixed(1);
|
|
258
|
+
console.log(` ${part}: ${count} messages (${percentage}%)`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (stats.lastBatchTime) {
|
|
263
|
+
const timeSinceLastBatch = Math.floor((Date.now() - stats.lastBatchTime) / 1000);
|
|
264
|
+
console.log(` Time since last batch: ${timeSinceLastBatch}s`);
|
|
265
|
+
}
|
|
266
|
+
console.log('');
|
|
267
|
+
}, 60000);
|
|
268
|
+
|
|
269
|
+
// Handle shutdown
|
|
270
|
+
process.on('SIGINT', () => {
|
|
271
|
+
console.log('\n\nš Shutting down producer...');
|
|
272
|
+
|
|
273
|
+
// Clear intervals based on mode
|
|
274
|
+
if (globalThis.highPerfProducers) {
|
|
275
|
+
globalThis.highPerfProducers.forEach(interval => clearInterval(interval));
|
|
276
|
+
}
|
|
277
|
+
if (globalThis.productionInterval) {
|
|
278
|
+
clearInterval(globalThis.productionInterval);
|
|
279
|
+
}
|
|
280
|
+
clearInterval(statsInterval);
|
|
281
|
+
|
|
282
|
+
const uptime = Math.floor((Date.now() - stats.startTime) / 1000);
|
|
283
|
+
console.log('\nš Final Production Statistics:');
|
|
284
|
+
console.log(` Total runtime: ${formatUptime(uptime)}`);
|
|
285
|
+
console.log(` Total batches: ${stats.batches}`);
|
|
286
|
+
console.log(` Total messages: ${stats.messages}`);
|
|
287
|
+
console.log(` Average batch size: ${(stats.messages / stats.batches).toFixed(1)}`);
|
|
288
|
+
console.log(` Overall rate: ${(stats.messages / (uptime / 60)).toFixed(2)} msg/min`);
|
|
289
|
+
console.log(` Errors encountered: ${stats.errors}`);
|
|
290
|
+
console.log(` Success rate: ${((1 - stats.errors / stats.batches) * 100).toFixed(1)}%`);
|
|
291
|
+
|
|
292
|
+
if (useMultiplePartitions && Object.keys(stats.partitionCounts).length > 0) {
|
|
293
|
+
console.log('\n Final partition distribution:');
|
|
294
|
+
for (const [part, count] of Object.entries(stats.partitionCounts)) {
|
|
295
|
+
const percentage = ((count / stats.messages) * 100).toFixed(1);
|
|
296
|
+
console.log(` ${part}: ${count} messages (${percentage}%)`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
process.exit(0);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// Simulate varying load (optional - only in normal mode)
|
|
304
|
+
if (process.env.VARY_LOAD === 'true' && !highPerformanceMode) {
|
|
305
|
+
console.log('š Variable load mode enabled - production rate will vary\n');
|
|
306
|
+
|
|
307
|
+
setInterval(() => {
|
|
308
|
+
// Randomly adjust the production rate
|
|
309
|
+
const factor = 0.5 + Math.random() * 2; // 0.5x to 2.5x
|
|
310
|
+
const newInterval = Math.floor(intervalMilliseconds * factor);
|
|
311
|
+
|
|
312
|
+
if (globalThis.productionInterval) {
|
|
313
|
+
clearInterval(globalThis.productionInterval);
|
|
314
|
+
globalThis.productionInterval = setInterval(async () => {
|
|
315
|
+
await produceMessages();
|
|
316
|
+
}, newInterval);
|
|
317
|
+
|
|
318
|
+
console.log(`š Production rate adjusted: new interval ${newInterval}ms`);
|
|
319
|
+
}
|
|
320
|
+
}, 120000); // Adjust every 2 minutes
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
console.log('ā
Producer started. Press Ctrl+C to stop.\n');
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function formatUptime(seconds) {
|
|
327
|
+
const hours = Math.floor(seconds / 3600);
|
|
328
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
329
|
+
const secs = seconds % 60;
|
|
330
|
+
|
|
331
|
+
const parts = [];
|
|
332
|
+
if (hours > 0) parts.push(`${hours}h`);
|
|
333
|
+
if (minutes > 0) parts.push(`${minutes}m`);
|
|
334
|
+
parts.push(`${secs}s`);
|
|
335
|
+
|
|
336
|
+
return parts.join(' ');
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Run the producer
|
|
340
|
+
main().catch(error => {
|
|
341
|
+
console.error('ā Fatal error:', error);
|
|
342
|
+
process.exit(1);
|
|
343
|
+
});
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mixed Mode Example for Queen V3
|
|
5
|
+
*
|
|
6
|
+
* This example demonstrates how the SAME queue can serve both:
|
|
7
|
+
* 1. Competing consumers (Queue Mode) - workers compete for messages
|
|
8
|
+
* 2. Consumer groups (Bus Mode) - each group gets all messages
|
|
9
|
+
*
|
|
10
|
+
* This is a unique feature that makes Queen more flexible than traditional
|
|
11
|
+
* message queue systems.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { createQueenClient } from '../src/client/index.js';
|
|
15
|
+
|
|
16
|
+
async function main() {
|
|
17
|
+
const client = createQueenClient({
|
|
18
|
+
baseUrl: process.env.QUEEN_URL || 'http://localhost:6632',
|
|
19
|
+
timeout: 35000
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const queue = 'orders';
|
|
23
|
+
|
|
24
|
+
console.log('š Starting Mixed Mode Example');
|
|
25
|
+
console.log('š¦ Queue:', queue);
|
|
26
|
+
console.log('');
|
|
27
|
+
console.log('This example demonstrates the SAME queue serving:');
|
|
28
|
+
console.log(' ⢠3 Workers in Queue Mode (competing for messages)');
|
|
29
|
+
console.log(' ⢠2 Consumer Groups in Bus Mode (each gets all messages)');
|
|
30
|
+
console.log('');
|
|
31
|
+
|
|
32
|
+
// Track message processing
|
|
33
|
+
const processed = {
|
|
34
|
+
workers: {
|
|
35
|
+
worker1: [],
|
|
36
|
+
worker2: [],
|
|
37
|
+
worker3: []
|
|
38
|
+
},
|
|
39
|
+
groups: {
|
|
40
|
+
analytics: [],
|
|
41
|
+
audit: []
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Start 3 competing workers (Queue Mode - no consumer group)
|
|
46
|
+
console.log('š· Starting 3 competing workers (Queue Mode):');
|
|
47
|
+
|
|
48
|
+
const worker1 = client.consume({
|
|
49
|
+
queue,
|
|
50
|
+
// No consumerGroup = Queue Mode (competing consumers)
|
|
51
|
+
handler: async (message) => {
|
|
52
|
+
console.log(` [Worker 1] Processing order ${message.data.orderId}`);
|
|
53
|
+
processed.workers.worker1.push(message.data.orderId);
|
|
54
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
55
|
+
},
|
|
56
|
+
options: { batch: 1, wait: false }
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const worker2 = client.consume({
|
|
60
|
+
queue,
|
|
61
|
+
// No consumerGroup = Queue Mode
|
|
62
|
+
handler: async (message) => {
|
|
63
|
+
console.log(` [Worker 2] Processing order ${message.data.orderId}`);
|
|
64
|
+
processed.workers.worker2.push(message.data.orderId);
|
|
65
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
66
|
+
},
|
|
67
|
+
options: { batch: 1, wait: false }
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const worker3 = client.consume({
|
|
71
|
+
queue,
|
|
72
|
+
// No consumerGroup = Queue Mode
|
|
73
|
+
handler: async (message) => {
|
|
74
|
+
console.log(` [Worker 3] Processing order ${message.data.orderId}`);
|
|
75
|
+
processed.workers.worker3.push(message.data.orderId);
|
|
76
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
77
|
+
},
|
|
78
|
+
options: { batch: 1, wait: false }
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
console.log('ā
Workers started\n');
|
|
82
|
+
|
|
83
|
+
// Start 2 consumer groups (Bus Mode - each gets all messages)
|
|
84
|
+
console.log('š Starting 2 consumer groups (Bus Mode):');
|
|
85
|
+
|
|
86
|
+
const analyticsConsumer = client.consume({
|
|
87
|
+
queue,
|
|
88
|
+
consumerGroup: 'analytics', // With consumerGroup = Bus Mode
|
|
89
|
+
handler: async (message) => {
|
|
90
|
+
console.log(` [Analytics Group] Analyzing order ${message.data.orderId}`);
|
|
91
|
+
processed.groups.analytics.push(message.data.orderId);
|
|
92
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
93
|
+
},
|
|
94
|
+
options: { batch: 2, wait: false }
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const auditConsumer = client.consume({
|
|
98
|
+
queue,
|
|
99
|
+
consumerGroup: 'audit-log', // With consumerGroup = Bus Mode
|
|
100
|
+
handler: async (message) => {
|
|
101
|
+
console.log(` [Audit Group] Logging order ${message.data.orderId}`);
|
|
102
|
+
processed.groups.audit.push(message.data.orderId);
|
|
103
|
+
await new Promise(resolve => setTimeout(resolve, 30));
|
|
104
|
+
},
|
|
105
|
+
options: { batch: 3, wait: false }
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
console.log('ā
Consumer groups started\n');
|
|
109
|
+
|
|
110
|
+
// Push test orders
|
|
111
|
+
console.log('š¤ Pushing 10 test orders...\n');
|
|
112
|
+
const orders = [];
|
|
113
|
+
for (let i = 1; i <= 10; i++) {
|
|
114
|
+
orders.push({
|
|
115
|
+
queue,
|
|
116
|
+
payload: {
|
|
117
|
+
orderId: i,
|
|
118
|
+
amount: Math.floor(Math.random() * 1000) + 100,
|
|
119
|
+
customer: `customer-${i}`,
|
|
120
|
+
timestamp: new Date().toISOString()
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
await client.push({ items: orders });
|
|
126
|
+
|
|
127
|
+
// Wait for processing
|
|
128
|
+
console.log('ā³ Processing messages...\n');
|
|
129
|
+
await new Promise(resolve => setTimeout(resolve, 4000));
|
|
130
|
+
|
|
131
|
+
// Stop all consumers
|
|
132
|
+
worker1();
|
|
133
|
+
worker2();
|
|
134
|
+
worker3();
|
|
135
|
+
analyticsConsumer();
|
|
136
|
+
auditConsumer();
|
|
137
|
+
|
|
138
|
+
// Display results
|
|
139
|
+
console.log('\n\nš RESULTS');
|
|
140
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
141
|
+
|
|
142
|
+
console.log('\nš§ QUEUE MODE (Competing Workers):');
|
|
143
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
144
|
+
console.log(`Worker 1 processed: [${processed.workers.worker1.sort((a,b) => a-b).join(', ')}]`);
|
|
145
|
+
console.log(`Worker 2 processed: [${processed.workers.worker2.sort((a,b) => a-b).join(', ')}]`);
|
|
146
|
+
console.log(`Worker 3 processed: [${processed.workers.worker3.sort((a,b) => a-b).join(', ')}]`);
|
|
147
|
+
|
|
148
|
+
const totalWorkerMessages =
|
|
149
|
+
processed.workers.worker1.length +
|
|
150
|
+
processed.workers.worker2.length +
|
|
151
|
+
processed.workers.worker3.length;
|
|
152
|
+
|
|
153
|
+
console.log(`\nTotal messages processed by workers: ${totalWorkerMessages}`);
|
|
154
|
+
console.log('ā
Each message was processed by ONLY ONE worker (competing)');
|
|
155
|
+
|
|
156
|
+
console.log('\nš” BUS MODE (Consumer Groups):');
|
|
157
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
158
|
+
console.log(`Analytics group received: [${processed.groups.analytics.sort((a,b) => a-b).join(', ')}]`);
|
|
159
|
+
console.log(`Audit group received: [${processed.groups.audit.sort((a,b) => a-b).join(', ')}]`);
|
|
160
|
+
|
|
161
|
+
console.log(`\nTotal messages received by Analytics: ${processed.groups.analytics.length}`);
|
|
162
|
+
console.log(`Total messages received by Audit: ${processed.groups.audit.length}`);
|
|
163
|
+
console.log('ā
Each consumer group received ALL messages (broadcast)');
|
|
164
|
+
|
|
165
|
+
// Verify the behavior
|
|
166
|
+
console.log('\n\nšÆ KEY INSIGHTS:');
|
|
167
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
168
|
+
|
|
169
|
+
// Check if workers competed correctly
|
|
170
|
+
const workerOrderIds = new Set([
|
|
171
|
+
...processed.workers.worker1,
|
|
172
|
+
...processed.workers.worker2,
|
|
173
|
+
...processed.workers.worker3
|
|
174
|
+
]);
|
|
175
|
+
|
|
176
|
+
if (workerOrderIds.size === totalWorkerMessages && totalWorkerMessages === 10) {
|
|
177
|
+
console.log('ā
Workers competed correctly: Each order processed ONCE across all workers');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Check if consumer groups got all messages
|
|
181
|
+
if (processed.groups.analytics.length === 10 && processed.groups.audit.length === 10) {
|
|
182
|
+
console.log('ā
Consumer groups worked correctly: Each group got ALL orders');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
console.log('\nš Summary:');
|
|
186
|
+
console.log(' ⢠The SAME queue served both patterns simultaneously');
|
|
187
|
+
console.log(' ⢠Workers (no consumer group) ā competed for messages');
|
|
188
|
+
console.log(' ⢠Consumer groups ā each received all messages');
|
|
189
|
+
console.log(' ⢠This flexibility is unique to Queen!');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Demonstrate a real-world scenario
|
|
193
|
+
async function realWorldScenario() {
|
|
194
|
+
console.log('\n\nš¢ REAL-WORLD SCENARIO');
|
|
195
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
196
|
+
console.log('E-commerce Order Processing System:');
|
|
197
|
+
console.log(' ⢠Order Processing Workers (Queue Mode) - handle the actual order');
|
|
198
|
+
console.log(' ⢠Analytics Service (Bus Mode) - analyzes all orders');
|
|
199
|
+
console.log(' ⢠Notification Service (Bus Mode) - sends notifications for all orders');
|
|
200
|
+
console.log(' ⢠Audit Service (Bus Mode) - logs all orders for compliance');
|
|
201
|
+
console.log('');
|
|
202
|
+
|
|
203
|
+
const client = createQueenClient({
|
|
204
|
+
baseUrl: process.env.QUEEN_URL || 'http://localhost:6632'
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const queue = 'ecommerce-orders';
|
|
208
|
+
|
|
209
|
+
// Simulate order processing workers
|
|
210
|
+
console.log('Starting order processing workers...');
|
|
211
|
+
for (let i = 1; i <= 2; i++) {
|
|
212
|
+
client.consume({
|
|
213
|
+
queue,
|
|
214
|
+
// No consumer group - workers compete
|
|
215
|
+
handler: async (message) => {
|
|
216
|
+
console.log(` [Order Worker ${i}] Processing order #${message.data.orderId} ($${message.data.amount})`);
|
|
217
|
+
// Simulate order processing: inventory, payment, shipping
|
|
218
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
219
|
+
console.log(` [Order Worker ${i}] ā
Order #${message.data.orderId} completed`);
|
|
220
|
+
},
|
|
221
|
+
options: { batch: 1, wait: true }
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Analytics service (needs all orders)
|
|
226
|
+
console.log('Starting analytics service...');
|
|
227
|
+
client.consume({
|
|
228
|
+
queue,
|
|
229
|
+
consumerGroup: 'analytics-service',
|
|
230
|
+
handler: async (message) => {
|
|
231
|
+
console.log(` [Analytics] Recording metrics for order #${message.data.orderId}`);
|
|
232
|
+
// Update dashboards, calculate revenue, track trends
|
|
233
|
+
},
|
|
234
|
+
options: { batch: 5, wait: true }
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Notification service (needs all orders)
|
|
238
|
+
console.log('Starting notification service...');
|
|
239
|
+
client.consume({
|
|
240
|
+
queue,
|
|
241
|
+
consumerGroup: 'notification-service',
|
|
242
|
+
handler: async (message) => {
|
|
243
|
+
console.log(` [Notifications] Sending email for order #${message.data.orderId} to ${message.data.email}`);
|
|
244
|
+
// Send order confirmation emails, SMS, push notifications
|
|
245
|
+
},
|
|
246
|
+
options: { batch: 1, wait: true }
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Audit service (needs all orders for compliance)
|
|
250
|
+
console.log('Starting audit service...');
|
|
251
|
+
client.consume({
|
|
252
|
+
queue,
|
|
253
|
+
consumerGroup: 'audit-service',
|
|
254
|
+
handler: async (message) => {
|
|
255
|
+
console.log(` [Audit] Logging order #${message.data.orderId} for compliance`);
|
|
256
|
+
// Store in audit log, ensure regulatory compliance
|
|
257
|
+
},
|
|
258
|
+
options: { batch: 10, wait: true }
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
console.log('\nā
All services started!');
|
|
262
|
+
console.log('\nNow push some orders to see the system in action:');
|
|
263
|
+
console.log(' await client.push({ items: [{ queue: "ecommerce-orders", payload: {...} }] })');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Run the example
|
|
267
|
+
main()
|
|
268
|
+
.then(() => realWorldScenario())
|
|
269
|
+
.then(() => {
|
|
270
|
+
console.log('\n\n⨠Mixed mode example completed!');
|
|
271
|
+
console.log('Press Ctrl+C to exit.');
|
|
272
|
+
// Keep the real-world scenario running
|
|
273
|
+
})
|
|
274
|
+
.catch(error => {
|
|
275
|
+
console.error('ā Error:', error);
|
|
276
|
+
process.exit(1);
|
|
277
|
+
});
|