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,185 @@
|
|
|
1
|
+
# Multi-Server Cache Consistency Solutions
|
|
2
|
+
|
|
3
|
+
## Problem
|
|
4
|
+
When running multiple Queen servers, each server maintains its own in-memory resource cache with a 1-minute TTL. Configuration changes on one server are not reflected on other servers until the cache expires.
|
|
5
|
+
|
|
6
|
+
## Solutions
|
|
7
|
+
|
|
8
|
+
### 1. **Disable Cache in Multi-Server Mode** (Quick Fix)
|
|
9
|
+
Set cache TTL to 0 or very short duration when running multiple servers.
|
|
10
|
+
|
|
11
|
+
```javascript
|
|
12
|
+
// In src/managers/resourceCache.js
|
|
13
|
+
const TTL = process.env.QUEEN_CACHE_TTL ? parseInt(process.env.QUEEN_CACHE_TTL) : 60000;
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Run servers with:
|
|
17
|
+
```bash
|
|
18
|
+
QUEEN_CACHE_TTL=0 PORT=6632 node src/server.js
|
|
19
|
+
QUEEN_CACHE_TTL=0 PORT=6633 node src/server.js
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### 2. **Cache Invalidation via Database Triggers** (Recommended)
|
|
23
|
+
Add a `cache_invalidation` table and use database triggers to notify servers of changes.
|
|
24
|
+
|
|
25
|
+
```sql
|
|
26
|
+
-- Add to schema
|
|
27
|
+
CREATE TABLE queen.cache_invalidations (
|
|
28
|
+
id SERIAL PRIMARY KEY,
|
|
29
|
+
resource_type VARCHAR(50) NOT NULL,
|
|
30
|
+
resource_key VARCHAR(255) NOT NULL,
|
|
31
|
+
invalidated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
CREATE INDEX idx_cache_invalidations_time ON queen.cache_invalidations(invalidated_at);
|
|
35
|
+
|
|
36
|
+
-- Trigger on queue updates
|
|
37
|
+
CREATE OR REPLACE FUNCTION queen.notify_cache_invalidation()
|
|
38
|
+
RETURNS TRIGGER AS $$
|
|
39
|
+
BEGIN
|
|
40
|
+
INSERT INTO queen.cache_invalidations (resource_type, resource_key)
|
|
41
|
+
VALUES ('queue', NEW.name);
|
|
42
|
+
|
|
43
|
+
PERFORM pg_notify('cache_invalidation', json_build_object(
|
|
44
|
+
'type', 'queue',
|
|
45
|
+
'key', NEW.name
|
|
46
|
+
)::text);
|
|
47
|
+
|
|
48
|
+
RETURN NEW;
|
|
49
|
+
END;
|
|
50
|
+
$$ LANGUAGE plpgsql;
|
|
51
|
+
|
|
52
|
+
CREATE TRIGGER queue_cache_invalidation
|
|
53
|
+
AFTER UPDATE ON queen.queues
|
|
54
|
+
FOR EACH ROW
|
|
55
|
+
EXECUTE FUNCTION queen.notify_cache_invalidation();
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 3. **Redis-Based Distributed Cache** (Best for Scale)
|
|
59
|
+
Replace in-memory cache with Redis for shared caching across servers.
|
|
60
|
+
|
|
61
|
+
```javascript
|
|
62
|
+
// src/managers/redisResourceCache.js
|
|
63
|
+
import Redis from 'ioredis';
|
|
64
|
+
|
|
65
|
+
export const createRedisResourceCache = (redisConfig) => {
|
|
66
|
+
const redis = new Redis(redisConfig);
|
|
67
|
+
const TTL = 60; // seconds
|
|
68
|
+
|
|
69
|
+
const getCacheKey = (queue, partition) =>
|
|
70
|
+
`queen:resource:${queue}:${partition || 'Default'}`;
|
|
71
|
+
|
|
72
|
+
const checkResource = async (queue, partition) => {
|
|
73
|
+
const key = getCacheKey(queue, partition);
|
|
74
|
+
const cached = await redis.get(key);
|
|
75
|
+
return cached ? JSON.parse(cached) : null;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const cacheResource = async (queue, partition, data) => {
|
|
79
|
+
const key = getCacheKey(queue, partition);
|
|
80
|
+
await redis.setex(key, TTL, JSON.stringify(data));
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const invalidate = async (queue, partition) => {
|
|
84
|
+
if (queue && partition) {
|
|
85
|
+
await redis.del(getCacheKey(queue, partition));
|
|
86
|
+
} else if (queue) {
|
|
87
|
+
const keys = await redis.keys(`queen:resource:${queue}:*`);
|
|
88
|
+
if (keys.length > 0) {
|
|
89
|
+
await redis.del(...keys);
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
const keys = await redis.keys('queen:resource:*');
|
|
93
|
+
if (keys.length > 0) {
|
|
94
|
+
await redis.del(...keys);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
checkResource,
|
|
101
|
+
cacheResource,
|
|
102
|
+
invalidate,
|
|
103
|
+
invalidateQueue: (queue) => invalidate(queue)
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### 4. **Polling-Based Cache Invalidation** (Simple)
|
|
109
|
+
Each server periodically checks for configuration changes.
|
|
110
|
+
|
|
111
|
+
```javascript
|
|
112
|
+
// Add to resourceCache.js
|
|
113
|
+
const pollForChanges = async (pool) => {
|
|
114
|
+
const result = await pool.query(`
|
|
115
|
+
SELECT name, updated_at
|
|
116
|
+
FROM queen.queues
|
|
117
|
+
WHERE updated_at > $1
|
|
118
|
+
`, [lastCheckTime]);
|
|
119
|
+
|
|
120
|
+
for (const row of result.rows) {
|
|
121
|
+
invalidateQueue(row.name);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
lastCheckTime = new Date();
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// Poll every 5 seconds
|
|
128
|
+
setInterval(() => pollForChanges(pool), 5000);
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### 5. **Event-Based Invalidation via Message Queue**
|
|
132
|
+
Use the Queen queue itself to propagate cache invalidation events.
|
|
133
|
+
|
|
134
|
+
```javascript
|
|
135
|
+
// When configuration changes on one server
|
|
136
|
+
await client.push({
|
|
137
|
+
items: [{
|
|
138
|
+
queue: '_system_cache_invalidation',
|
|
139
|
+
partition: 'Default',
|
|
140
|
+
payload: {
|
|
141
|
+
action: 'invalidate',
|
|
142
|
+
resourceType: 'queue',
|
|
143
|
+
resourceKey: queueName,
|
|
144
|
+
timestamp: Date.now()
|
|
145
|
+
}
|
|
146
|
+
}]
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// All servers consume from this queue
|
|
150
|
+
client.consume({
|
|
151
|
+
queue: '_system_cache_invalidation',
|
|
152
|
+
handler: async (message) => {
|
|
153
|
+
const { resourceType, resourceKey } = message.payload;
|
|
154
|
+
if (resourceType === 'queue') {
|
|
155
|
+
resourceCache.invalidateQueue(resourceKey);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Recommended Approach
|
|
162
|
+
|
|
163
|
+
For your immediate testing needs, use **Solution 1** (disable cache):
|
|
164
|
+
```bash
|
|
165
|
+
# Start servers without cache
|
|
166
|
+
QUEEN_CACHE_TTL=0 PORT=6632 node src/server.js
|
|
167
|
+
QUEEN_CACHE_TTL=0 PORT=6633 node src/server.js
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
For production multi-server deployment:
|
|
171
|
+
- **Small scale (2-3 servers)**: Solution 2 (Database triggers with LISTEN/NOTIFY)
|
|
172
|
+
- **Medium scale (4-10 servers)**: Solution 3 (Redis cache)
|
|
173
|
+
- **Large scale (10+ servers)**: Solution 3 (Redis) + Solution 5 (Event-based)
|
|
174
|
+
|
|
175
|
+
## Testing Cache Behavior
|
|
176
|
+
|
|
177
|
+
After implementing a solution, run:
|
|
178
|
+
```bash
|
|
179
|
+
node examples/test-cache-multi-server.js
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The test should show:
|
|
183
|
+
- Both servers returning the same messages ā
|
|
184
|
+
- Configuration changes immediately visible on both servers ā
|
|
185
|
+
- No cache inconsistency warnings ā
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# Performance Tuning Guide for Queen
|
|
2
|
+
|
|
3
|
+
## Database Connection Pool Optimization
|
|
4
|
+
|
|
5
|
+
When running Queen under high load, you may encounter database connection pool exhaustion errors:
|
|
6
|
+
```
|
|
7
|
+
Error: timeout exceeded when trying to connect
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
This happens when the rate of incoming requests exceeds the available database connections.
|
|
11
|
+
|
|
12
|
+
## Solution 1: Increase Connection Pool Size
|
|
13
|
+
|
|
14
|
+
Set these environment variables before starting the server:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# Increase the connection pool size (default is 20)
|
|
18
|
+
export DB_POOL_SIZE=50
|
|
19
|
+
|
|
20
|
+
# Increase connection timeout (default is 2000ms)
|
|
21
|
+
export DB_CONNECTION_TIMEOUT=5000
|
|
22
|
+
|
|
23
|
+
# Start the server
|
|
24
|
+
npm start
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Solution 2: Optimize PostgreSQL Settings
|
|
28
|
+
|
|
29
|
+
Edit your `postgresql.conf`:
|
|
30
|
+
|
|
31
|
+
```conf
|
|
32
|
+
# Increase maximum connections
|
|
33
|
+
max_connections = 200
|
|
34
|
+
|
|
35
|
+
# Connection pooling
|
|
36
|
+
shared_preload_libraries = 'pg_stat_statements'
|
|
37
|
+
|
|
38
|
+
# Memory settings for better performance
|
|
39
|
+
shared_buffers = 256MB
|
|
40
|
+
effective_cache_size = 1GB
|
|
41
|
+
work_mem = 4MB
|
|
42
|
+
maintenance_work_mem = 64MB
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Solution 3: Use Batch Inserts (Database Optimization)
|
|
46
|
+
|
|
47
|
+
The current implementation processes messages individually within a transaction. For better performance under high load, consider using batch inserts.
|
|
48
|
+
|
|
49
|
+
### Current Implementation (Sequential)
|
|
50
|
+
```javascript
|
|
51
|
+
// Process each item individually
|
|
52
|
+
for (const item of items) {
|
|
53
|
+
// Individual INSERT for each message
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Optimized Implementation (Batch)
|
|
58
|
+
```sql
|
|
59
|
+
-- Use single INSERT with multiple VALUES
|
|
60
|
+
INSERT INTO queen.messages (transaction_id, queue_id, payload, status)
|
|
61
|
+
VALUES
|
|
62
|
+
($1, $2, $3, 'pending'),
|
|
63
|
+
($4, $5, $6, 'pending'),
|
|
64
|
+
...
|
|
65
|
+
RETURNING id, transaction_id;
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Solution 4: Implement Connection Pooling with PgBouncer
|
|
69
|
+
|
|
70
|
+
For production environments with very high load, use PgBouncer as a connection pooler:
|
|
71
|
+
|
|
72
|
+
1. Install PgBouncer:
|
|
73
|
+
```bash
|
|
74
|
+
# macOS
|
|
75
|
+
brew install pgbouncer
|
|
76
|
+
|
|
77
|
+
# Linux
|
|
78
|
+
apt-get install pgbouncer
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
2. Configure PgBouncer (`/etc/pgbouncer/pgbouncer.ini`):
|
|
82
|
+
```ini
|
|
83
|
+
[databases]
|
|
84
|
+
queen = host=localhost port=5432 dbname=postgres
|
|
85
|
+
|
|
86
|
+
[pgbouncer]
|
|
87
|
+
listen_port = 6432
|
|
88
|
+
listen_addr = *
|
|
89
|
+
auth_type = md5
|
|
90
|
+
auth_file = /etc/pgbouncer/userlist.txt
|
|
91
|
+
pool_mode = transaction
|
|
92
|
+
max_client_conn = 1000
|
|
93
|
+
default_pool_size = 50
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
3. Update Queen to connect through PgBouncer:
|
|
97
|
+
```bash
|
|
98
|
+
export PG_PORT=6432 # PgBouncer port
|
|
99
|
+
npm start
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Recommended Production Settings
|
|
103
|
+
|
|
104
|
+
For a production environment handling high message throughput:
|
|
105
|
+
|
|
106
|
+
### Environment Variables
|
|
107
|
+
```bash
|
|
108
|
+
# Database
|
|
109
|
+
DB_POOL_SIZE=50
|
|
110
|
+
DB_CONNECTION_TIMEOUT=5000
|
|
111
|
+
DB_IDLE_TIMEOUT=30000
|
|
112
|
+
|
|
113
|
+
# Application
|
|
114
|
+
NODE_ENV=production
|
|
115
|
+
NODE_OPTIONS="--max-old-space-size=4096"
|
|
116
|
+
|
|
117
|
+
# Monitoring
|
|
118
|
+
LOG_LEVEL=warn
|
|
119
|
+
ENABLE_METRICS=true
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Example High-Load Configuration
|
|
123
|
+
|
|
124
|
+
For sustained load of 1000+ messages/second:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
# Start server with optimized settings
|
|
128
|
+
DB_POOL_SIZE=100 \
|
|
129
|
+
DB_CONNECTION_TIMEOUT=10000 \
|
|
130
|
+
NODE_OPTIONS="--max-old-space-size=8192" \
|
|
131
|
+
npm start
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Testing Under Load
|
|
135
|
+
|
|
136
|
+
### Reasonable Load Test
|
|
137
|
+
```bash
|
|
138
|
+
# 10 messages per batch, every 100ms = ~100 msg/sec
|
|
139
|
+
BATCH_SIZE=10 INTERVAL=100 node examples/continuous-producer.js
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Medium Load Test
|
|
143
|
+
```bash
|
|
144
|
+
# 50 messages per batch, every 500ms = ~100 msg/sec
|
|
145
|
+
BATCH_SIZE=50 INTERVAL=500 node examples/continuous-producer.js
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### High Load Test (requires tuning)
|
|
149
|
+
```bash
|
|
150
|
+
# First, increase DB pool
|
|
151
|
+
export DB_POOL_SIZE=100
|
|
152
|
+
|
|
153
|
+
# Then run high load producer
|
|
154
|
+
# 100 messages per batch, every 1000ms = ~100 msg/sec
|
|
155
|
+
BATCH_SIZE=100 INTERVAL=1000 node examples/continuous-producer.js
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Monitoring Performance
|
|
159
|
+
|
|
160
|
+
Monitor these metrics to identify bottlenecks:
|
|
161
|
+
|
|
162
|
+
1. **Database Connections**
|
|
163
|
+
```sql
|
|
164
|
+
-- Check active connections
|
|
165
|
+
SELECT count(*) FROM pg_stat_activity;
|
|
166
|
+
|
|
167
|
+
-- Check connections by state
|
|
168
|
+
SELECT state, count(*)
|
|
169
|
+
FROM pg_stat_activity
|
|
170
|
+
GROUP BY state;
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
2. **Connection Pool Status**
|
|
174
|
+
```javascript
|
|
175
|
+
// Add monitoring endpoint to server
|
|
176
|
+
app.get('/metrics/pool', (req, res) => {
|
|
177
|
+
res.json({
|
|
178
|
+
totalCount: pool.totalCount,
|
|
179
|
+
idleCount: pool.idleCount,
|
|
180
|
+
waitingCount: pool.waitingCount
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
3. **Message Throughput**
|
|
186
|
+
```bash
|
|
187
|
+
# Monitor message insertion rate
|
|
188
|
+
watch -n 1 "psql -c 'SELECT count(*) FROM queen.messages WHERE created_at > NOW() - INTERVAL '\''1 minute'\'';'"
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Best Practices
|
|
192
|
+
|
|
193
|
+
1. **Start Conservative**: Begin with lower message rates and gradually increase
|
|
194
|
+
2. **Monitor Resources**: Watch CPU, memory, and database connections
|
|
195
|
+
3. **Use Batching**: Process messages in reasonable batches (10-100 per batch)
|
|
196
|
+
4. **Implement Backpressure**: Slow down producers when consumers can't keep up
|
|
197
|
+
5. **Scale Horizontally**: Run multiple Queen instances behind a load balancer
|
|
198
|
+
6. **Use Read Replicas**: For analytics and read-heavy operations
|
|
199
|
+
|
|
200
|
+
## Troubleshooting
|
|
201
|
+
|
|
202
|
+
### "timeout exceeded when trying to connect"
|
|
203
|
+
- Increase `DB_POOL_SIZE`
|
|
204
|
+
- Increase `DB_CONNECTION_TIMEOUT`
|
|
205
|
+
- Check PostgreSQL `max_connections`
|
|
206
|
+
- Consider using PgBouncer
|
|
207
|
+
|
|
208
|
+
### "Push request aborted"
|
|
209
|
+
- Client timeout is too short
|
|
210
|
+
- Increase client timeout in producer
|
|
211
|
+
- Reduce batch size
|
|
212
|
+
|
|
213
|
+
### High Memory Usage
|
|
214
|
+
- Reduce batch sizes
|
|
215
|
+
- Increase Node.js heap size with `--max-old-space-size`
|
|
216
|
+
- Implement message pagination
|
|
217
|
+
|
|
218
|
+
### Slow Message Processing
|
|
219
|
+
- Add database indexes
|
|
220
|
+
- Use batch operations
|
|
221
|
+
- Implement caching for frequently accessed data
|
|
222
|
+
- Consider partitioning large tables
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Bus Mode Example for Queen V3
|
|
5
|
+
*
|
|
6
|
+
* This example demonstrates the new bus/pub-sub functionality where
|
|
7
|
+
* multiple consumer groups can receive ALL messages independently,
|
|
8
|
+
* similar to Kafka's consumer groups.
|
|
9
|
+
*
|
|
10
|
+
* Each consumer group maintains its own progress through the message stream.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { createQueenClient } from '../src/client/index.js';
|
|
14
|
+
|
|
15
|
+
async function main() {
|
|
16
|
+
const client = createQueenClient({
|
|
17
|
+
baseUrl: process.env.QUEEN_URL || 'http://localhost:6632',
|
|
18
|
+
timeout: 35000
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const queue = 'events';
|
|
22
|
+
|
|
23
|
+
console.log('š Starting Bus Mode Example');
|
|
24
|
+
console.log('š¦ Queue:', queue);
|
|
25
|
+
console.log('');
|
|
26
|
+
console.log('This example will:');
|
|
27
|
+
console.log('1. Push some test messages to the queue');
|
|
28
|
+
console.log('2. Start 3 different consumer groups');
|
|
29
|
+
console.log('3. Show how each group receives ALL messages independently');
|
|
30
|
+
console.log('');
|
|
31
|
+
|
|
32
|
+
// First, push some test messages
|
|
33
|
+
console.log('š¤ Pushing test messages...');
|
|
34
|
+
const testMessages = [];
|
|
35
|
+
for (let i = 1; i <= 5; i++) {
|
|
36
|
+
testMessages.push({
|
|
37
|
+
queue,
|
|
38
|
+
payload: {
|
|
39
|
+
id: i,
|
|
40
|
+
type: 'test-event',
|
|
41
|
+
timestamp: new Date().toISOString(),
|
|
42
|
+
data: `Message ${i}`
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
await client.push({ items: testMessages });
|
|
48
|
+
console.log(`ā
Pushed ${testMessages.length} messages\n`);
|
|
49
|
+
|
|
50
|
+
// Track messages received by each consumer group
|
|
51
|
+
const received = {
|
|
52
|
+
analytics: [],
|
|
53
|
+
notifications: [],
|
|
54
|
+
audit: []
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Consumer Group 1: Analytics Service
|
|
58
|
+
console.log('šµ Starting Analytics Consumer Group...');
|
|
59
|
+
const analyticsConsumer = client.consume({
|
|
60
|
+
queue,
|
|
61
|
+
consumerGroup: 'analytics-service',
|
|
62
|
+
handler: async (message) => {
|
|
63
|
+
console.log(` [Analytics] Received message ${message.data.id}: ${message.data.data}`);
|
|
64
|
+
received.analytics.push(message.data.id);
|
|
65
|
+
|
|
66
|
+
// Simulate processing
|
|
67
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
68
|
+
},
|
|
69
|
+
options: {
|
|
70
|
+
batch: 2,
|
|
71
|
+
wait: false // Don't wait for new messages in this example
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Consumer Group 2: Notification Service
|
|
76
|
+
console.log('š¢ Starting Notifications Consumer Group...');
|
|
77
|
+
const notificationConsumer = client.consume({
|
|
78
|
+
queue,
|
|
79
|
+
consumerGroup: 'notification-service',
|
|
80
|
+
handler: async (message) => {
|
|
81
|
+
console.log(` [Notifications] Received message ${message.data.id}: ${message.data.data}`);
|
|
82
|
+
received.notifications.push(message.data.id);
|
|
83
|
+
|
|
84
|
+
// Simulate processing
|
|
85
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
86
|
+
},
|
|
87
|
+
options: {
|
|
88
|
+
batch: 1,
|
|
89
|
+
wait: false
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Consumer Group 3: Audit Log Service
|
|
94
|
+
console.log('š” Starting Audit Log Consumer Group...');
|
|
95
|
+
const auditConsumer = client.consume({
|
|
96
|
+
queue,
|
|
97
|
+
consumerGroup: 'audit-log',
|
|
98
|
+
handler: async (message) => {
|
|
99
|
+
console.log(` [Audit] Received message ${message.data.id}: ${message.data.data}`);
|
|
100
|
+
received.audit.push(message.data.id);
|
|
101
|
+
|
|
102
|
+
// Simulate processing
|
|
103
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
104
|
+
},
|
|
105
|
+
options: {
|
|
106
|
+
batch: 3,
|
|
107
|
+
wait: false
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Wait for all messages to be processed
|
|
112
|
+
console.log('\nā³ Processing messages...\n');
|
|
113
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
114
|
+
|
|
115
|
+
// Stop all consumers
|
|
116
|
+
analyticsConsumer();
|
|
117
|
+
notificationConsumer();
|
|
118
|
+
auditConsumer();
|
|
119
|
+
|
|
120
|
+
// Display results
|
|
121
|
+
console.log('\nš Results:');
|
|
122
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
123
|
+
console.log(`Analytics Service received: [${received.analytics.sort().join(', ')}]`);
|
|
124
|
+
console.log(`Notification Service received: [${received.notifications.sort().join(', ')}]`);
|
|
125
|
+
console.log(`Audit Log Service received: [${received.audit.sort().join(', ')}]`);
|
|
126
|
+
console.log('');
|
|
127
|
+
|
|
128
|
+
// Verify all groups received all messages
|
|
129
|
+
const allReceived =
|
|
130
|
+
received.analytics.length === testMessages.length &&
|
|
131
|
+
received.notifications.length === testMessages.length &&
|
|
132
|
+
received.audit.length === testMessages.length;
|
|
133
|
+
|
|
134
|
+
if (allReceived) {
|
|
135
|
+
console.log('ā
SUCCESS: All consumer groups received ALL messages!');
|
|
136
|
+
console.log(' This demonstrates bus/pub-sub mode where each consumer group');
|
|
137
|
+
console.log(' gets its own copy of every message, independently.');
|
|
138
|
+
} else {
|
|
139
|
+
console.log('ā ļø Some messages were not received by all groups.');
|
|
140
|
+
console.log(' This might be due to timing. Try running again.');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
console.log('\nšÆ Key Differences from Queue Mode:');
|
|
144
|
+
console.log(' ⢠In Queue Mode: Messages are consumed by ONE worker');
|
|
145
|
+
console.log(' ⢠In Bus Mode: Messages are consumed by ALL consumer groups');
|
|
146
|
+
console.log(' ⢠Each group maintains its own progress/offset');
|
|
147
|
+
console.log(' ⢠Perfect for: analytics, notifications, audit logs, etc.');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Demonstrate subscription modes
|
|
151
|
+
async function demonstrateSubscriptionModes() {
|
|
152
|
+
const client = createQueenClient({
|
|
153
|
+
baseUrl: process.env.QUEEN_URL || 'http://localhost:6632'
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const queue = 'subscription-test';
|
|
157
|
+
|
|
158
|
+
console.log('\n\nš
Demonstrating Subscription Modes');
|
|
159
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
160
|
+
|
|
161
|
+
// Push some historical messages
|
|
162
|
+
console.log('š¤ Pushing historical messages...');
|
|
163
|
+
for (let i = 1; i <= 3; i++) {
|
|
164
|
+
await client.push({
|
|
165
|
+
items: [{
|
|
166
|
+
queue,
|
|
167
|
+
payload: { id: i, type: 'historical', data: `Historical message ${i}` }
|
|
168
|
+
}]
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
console.log('ā
Historical messages pushed\n');
|
|
173
|
+
|
|
174
|
+
// Consumer 1: Consume all messages (default)
|
|
175
|
+
console.log('šµ Consumer Group "all-messages" (default mode):');
|
|
176
|
+
const allMessages = [];
|
|
177
|
+
const consumer1 = client.consume({
|
|
178
|
+
queue,
|
|
179
|
+
consumerGroup: 'all-messages',
|
|
180
|
+
handler: async (msg) => {
|
|
181
|
+
console.log(` Received: ${msg.data.type} - ${msg.data.data}`);
|
|
182
|
+
allMessages.push(msg.data.id);
|
|
183
|
+
},
|
|
184
|
+
options: { wait: false }
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
188
|
+
consumer1();
|
|
189
|
+
|
|
190
|
+
// Consumer 2: Only new messages from subscription time
|
|
191
|
+
console.log('\nš¢ Consumer Group "new-only" (subscriptionMode: "new"):');
|
|
192
|
+
const newOnly = [];
|
|
193
|
+
const consumer2 = client.consume({
|
|
194
|
+
queue,
|
|
195
|
+
consumerGroup: 'new-only',
|
|
196
|
+
handler: async (msg) => {
|
|
197
|
+
console.log(` Received: ${msg.data.type} - ${msg.data.data}`);
|
|
198
|
+
newOnly.push(msg.data.id);
|
|
199
|
+
},
|
|
200
|
+
options: {
|
|
201
|
+
wait: false,
|
|
202
|
+
subscriptionMode: 'new'
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Push new messages after subscription
|
|
207
|
+
console.log('\nš¤ Pushing new messages...');
|
|
208
|
+
for (let i = 4; i <= 6; i++) {
|
|
209
|
+
await client.push({
|
|
210
|
+
items: [{
|
|
211
|
+
queue,
|
|
212
|
+
payload: { id: i, type: 'new', data: `New message ${i}` }
|
|
213
|
+
}]
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
218
|
+
consumer2();
|
|
219
|
+
|
|
220
|
+
// Display results
|
|
221
|
+
console.log('\nš Subscription Mode Results:');
|
|
222
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
223
|
+
console.log(`"all-messages" group received: [${allMessages.sort().join(', ')}] (all 6 messages)`);
|
|
224
|
+
console.log(`"new-only" group received: [${newOnly.sort().join(', ')}] (only new messages)`);
|
|
225
|
+
console.log('');
|
|
226
|
+
console.log('ā
This demonstrates how consumer groups can choose when to start consuming!');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Run the examples
|
|
230
|
+
main()
|
|
231
|
+
.then(() => demonstrateSubscriptionModes())
|
|
232
|
+
.then(() => {
|
|
233
|
+
console.log('\n⨠Bus mode examples completed!');
|
|
234
|
+
process.exit(0);
|
|
235
|
+
})
|
|
236
|
+
.catch(error => {
|
|
237
|
+
console.error('ā Error:', error);
|
|
238
|
+
process.exit(1);
|
|
239
|
+
});
|