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
package/src/server.js
ADDED
|
@@ -0,0 +1,1286 @@
|
|
|
1
|
+
import uWS from 'uWebSockets.js';
|
|
2
|
+
import pg from 'pg';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import config from './config.js';
|
|
7
|
+
import { initDatabase } from './database/connection.js';
|
|
8
|
+
import { createOptimizedQueueManager } from './managers/queueManagerOptimized.js';
|
|
9
|
+
import { createResourceCache } from './managers/resourceCache.js';
|
|
10
|
+
import { createEventManager } from './managers/eventManager.js';
|
|
11
|
+
import { SystemEventManager, SYSTEM_QUEUE } from './managers/systemEventManager.js';
|
|
12
|
+
import { syncSystemEvents } from './services/startupSync.js';
|
|
13
|
+
import { createQueenClient } from './client/queenClient.js';
|
|
14
|
+
import { createPushRoute } from './routes/push.js';
|
|
15
|
+
import { createPopRoute } from './routes/pop.js';
|
|
16
|
+
import { createAckRoute } from './routes/ack.js';
|
|
17
|
+
import { createConfigureRoute } from './routes/configure.js';
|
|
18
|
+
import { createAnalyticsRoutes } from './routes/analytics.js';
|
|
19
|
+
import { createMessagesRoutes } from './routes/messages.js';
|
|
20
|
+
import { createResourcesRoutes } from './routes/resources.js';
|
|
21
|
+
import { createWebSocketServer } from './websocket/wsServer.js';
|
|
22
|
+
import { initEncryption } from './services/encryptionService.js';
|
|
23
|
+
import { startRetentionJob } from './services/retentionService.js';
|
|
24
|
+
import { startEvictionJob } from './services/evictionService.js';
|
|
25
|
+
import { log } from './utils/logger.js';
|
|
26
|
+
|
|
27
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
|
|
29
|
+
const PORT = config.SERVER.PORT;
|
|
30
|
+
const HOST = config.SERVER.HOST;
|
|
31
|
+
|
|
32
|
+
// Use WORKER_ID from config as the server instance ID for consumer groups
|
|
33
|
+
// This ensures the same server (identified by WORKER_ID) maintains its position in the event stream
|
|
34
|
+
const SERVER_INSTANCE_ID = config.SERVER.WORKER_ID;
|
|
35
|
+
|
|
36
|
+
// Performance monitoring
|
|
37
|
+
let requestCount = 0;
|
|
38
|
+
let messageCount = 0;
|
|
39
|
+
const startTime = Date.now();
|
|
40
|
+
|
|
41
|
+
// Create optimized connection pool
|
|
42
|
+
const { Pool } = pg;
|
|
43
|
+
const createOptimizedPool = () => {
|
|
44
|
+
return new Pool({
|
|
45
|
+
user: config.DATABASE.USER,
|
|
46
|
+
host: config.DATABASE.HOST,
|
|
47
|
+
database: config.DATABASE.DATABASE,
|
|
48
|
+
password: config.DATABASE.PASSWORD,
|
|
49
|
+
port: config.DATABASE.PORT,
|
|
50
|
+
max: config.DATABASE.POOL_SIZE,
|
|
51
|
+
idleTimeoutMillis: config.DATABASE.IDLE_TIMEOUT,
|
|
52
|
+
connectionTimeoutMillis: config.DATABASE.CONNECTION_TIMEOUT,
|
|
53
|
+
statement_timeout: config.DATABASE.STATEMENT_TIMEOUT,
|
|
54
|
+
query_timeout: config.DATABASE.QUERY_TIMEOUT,
|
|
55
|
+
application_name: config.SERVER.APPLICATION_NAME
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Initialize components
|
|
60
|
+
const pool = createOptimizedPool();
|
|
61
|
+
const resourceCache = createResourceCache();
|
|
62
|
+
const eventManager = createEventManager();
|
|
63
|
+
|
|
64
|
+
// Initialize system event manager
|
|
65
|
+
const systemEventManager = new SystemEventManager(pool, SERVER_INSTANCE_ID);
|
|
66
|
+
|
|
67
|
+
// Register cache event handlers
|
|
68
|
+
resourceCache.registerEventHandlers(systemEventManager);
|
|
69
|
+
|
|
70
|
+
// Always use optimized queue manager - it's better for all scenarios
|
|
71
|
+
const queueManager = createOptimizedQueueManager(pool, resourceCache, eventManager, systemEventManager);
|
|
72
|
+
const analyticsRoutes = createAnalyticsRoutes(queueManager);
|
|
73
|
+
const messagesRoutes = createMessagesRoutes(pool, queueManager);
|
|
74
|
+
const resourcesRoutes = createResourcesRoutes(pool);
|
|
75
|
+
|
|
76
|
+
// Initialize database with migrations
|
|
77
|
+
const initDatabaseWithMigrations = async () => {
|
|
78
|
+
// Run base schema
|
|
79
|
+
await initDatabase(pool);
|
|
80
|
+
|
|
81
|
+
// Run extension migrations
|
|
82
|
+
const migrationPath = path.join(__dirname, '..', 'migrations', '001-extend-features.sql');
|
|
83
|
+
if (fs.existsSync(migrationPath)) {
|
|
84
|
+
const migration = fs.readFileSync(migrationPath, 'utf8');
|
|
85
|
+
try {
|
|
86
|
+
await pool.query(migration);
|
|
87
|
+
log('ā
Extension features migration applied');
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (!error.message.includes('already exists')) {
|
|
90
|
+
log('Migration error:', error);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
await initDatabaseWithMigrations();
|
|
97
|
+
|
|
98
|
+
// Initialize system queue
|
|
99
|
+
async function initializeSystemQueue() {
|
|
100
|
+
try {
|
|
101
|
+
// Direct database operation to create system queue
|
|
102
|
+
await pool.query(`
|
|
103
|
+
INSERT INTO queen.queues (name, ttl, priority, max_queue_size)
|
|
104
|
+
VALUES ($1, 300, 100, 10000)
|
|
105
|
+
ON CONFLICT (name) DO UPDATE SET
|
|
106
|
+
ttl = EXCLUDED.ttl,
|
|
107
|
+
priority = EXCLUDED.priority,
|
|
108
|
+
max_queue_size = EXCLUDED.max_queue_size
|
|
109
|
+
`, [SYSTEM_QUEUE]);
|
|
110
|
+
|
|
111
|
+
await pool.query(`
|
|
112
|
+
INSERT INTO queen.partitions (queue_id, name)
|
|
113
|
+
SELECT id, 'Default' FROM queen.queues WHERE name = $1
|
|
114
|
+
ON CONFLICT (queue_id, name) DO NOTHING
|
|
115
|
+
`, [SYSTEM_QUEUE]);
|
|
116
|
+
|
|
117
|
+
log('ā
System event queue initialized');
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error('Failed to initialize system queue:', error);
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
await initializeSystemQueue();
|
|
125
|
+
|
|
126
|
+
// Initialize extended features
|
|
127
|
+
log('š Initializing Queen Services...');
|
|
128
|
+
|
|
129
|
+
// 1. Encryption service
|
|
130
|
+
const encryptionEnabled = initEncryption();
|
|
131
|
+
|
|
132
|
+
// 2. Retention service
|
|
133
|
+
const stopRetention = startRetentionJob(pool);
|
|
134
|
+
|
|
135
|
+
// 3. Eviction service
|
|
136
|
+
const stopEviction = startEvictionJob(pool, eventManager);
|
|
137
|
+
|
|
138
|
+
// Background job for lease reclamation
|
|
139
|
+
setInterval(async () => {
|
|
140
|
+
try {
|
|
141
|
+
const reclaimed = await queueManager.reclaimExpiredLeases();
|
|
142
|
+
if (reclaimed > 0) {
|
|
143
|
+
log(`Reclaimed ${reclaimed} expired leases`);
|
|
144
|
+
}
|
|
145
|
+
} catch (error) {
|
|
146
|
+
log('Error reclaiming leases:', error);
|
|
147
|
+
}
|
|
148
|
+
}, config.JOBS.LEASE_RECLAIM_INTERVAL);
|
|
149
|
+
|
|
150
|
+
// Create server
|
|
151
|
+
const app = uWS.App();
|
|
152
|
+
|
|
153
|
+
// Initialize WebSocket server
|
|
154
|
+
const wsServer = createWebSocketServer(app, eventManager);
|
|
155
|
+
|
|
156
|
+
// Background job for queue depth updates
|
|
157
|
+
setInterval(async () => {
|
|
158
|
+
await wsServer.updateQueueDepths(queueManager);
|
|
159
|
+
}, config.JOBS.QUEUE_DEPTH_UPDATE_INTERVAL);
|
|
160
|
+
|
|
161
|
+
// Background job for system stats
|
|
162
|
+
setInterval(async () => {
|
|
163
|
+
await wsServer.sendSystemStats(pool);
|
|
164
|
+
}, config.JOBS.SYSTEM_STATS_UPDATE_INTERVAL);
|
|
165
|
+
|
|
166
|
+
// CORS headers helper
|
|
167
|
+
const setCorsHeaders = (res) => {
|
|
168
|
+
res.writeHeader('Access-Control-Allow-Origin', config.API.CORS_ALLOWED_ORIGINS);
|
|
169
|
+
res.writeHeader('Access-Control-Allow-Methods', config.API.CORS_ALLOWED_METHODS);
|
|
170
|
+
res.writeHeader('Access-Control-Allow-Headers', config.API.CORS_ALLOWED_HEADERS);
|
|
171
|
+
res.writeHeader('Access-Control-Max-Age', config.API.CORS_MAX_AGE.toString());
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// Helper to read JSON body
|
|
175
|
+
const readJson = (res, cb, abortedRef) => {
|
|
176
|
+
let buffer;
|
|
177
|
+
const maxBodySize = config.API.MAX_BODY_SIZE; // 10MB max body size
|
|
178
|
+
|
|
179
|
+
res.onData((ab, isLast) => {
|
|
180
|
+
const chunk = Buffer.from(ab);
|
|
181
|
+
|
|
182
|
+
// Check size limit
|
|
183
|
+
const currentSize = (buffer ? buffer.length : 0) + chunk.length;
|
|
184
|
+
if (currentSize > maxBodySize) {
|
|
185
|
+
if (abortedRef && abortedRef.aborted) return;
|
|
186
|
+
res.writeStatus(config.HTTP_STATUS.BAD_REQUEST.toString()).end(JSON.stringify({ error: 'Request body too large' }));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (isLast) {
|
|
191
|
+
let json;
|
|
192
|
+
try {
|
|
193
|
+
let fullData;
|
|
194
|
+
if (buffer) {
|
|
195
|
+
fullData = Buffer.concat([buffer, chunk]);
|
|
196
|
+
} else {
|
|
197
|
+
fullData = chunk;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const jsonString = fullData.toString('utf8');
|
|
201
|
+
|
|
202
|
+
// Debug large payloads
|
|
203
|
+
if (jsonString.length > 1000000) {
|
|
204
|
+
log(`Parsing large JSON: ${jsonString.length} bytes, starts with: ${jsonString.substring(0, 100)}`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
json = JSON.parse(jsonString);
|
|
208
|
+
} catch (e) {
|
|
209
|
+
if (abortedRef && abortedRef.aborted) return;
|
|
210
|
+
// Log the actual error for debugging
|
|
211
|
+
log(`JSON parse error: ${e.message}`);
|
|
212
|
+
|
|
213
|
+
// Log what we're trying to parse for debugging
|
|
214
|
+
let sample = '';
|
|
215
|
+
if (buffer) {
|
|
216
|
+
const fullData = Buffer.concat([buffer, chunk]);
|
|
217
|
+
sample = fullData.toString('utf8').substring(0, 200);
|
|
218
|
+
} else {
|
|
219
|
+
sample = chunk.toString('utf8').substring(0, 200);
|
|
220
|
+
}
|
|
221
|
+
log(`Failed to parse data starting with: ${sample}`);
|
|
222
|
+
|
|
223
|
+
res.writeStatus(config.HTTP_STATUS.BAD_REQUEST.toString()).end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
cb(json);
|
|
227
|
+
} else {
|
|
228
|
+
if (buffer) {
|
|
229
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
230
|
+
} else {
|
|
231
|
+
buffer = Buffer.from(chunk); // Ensure it's a Buffer
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// Handle OPTIONS requests for CORS preflight
|
|
238
|
+
app.options('/*', (res, req) => {
|
|
239
|
+
res.onAborted(() => {
|
|
240
|
+
log('OPTIONS request aborted');
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
res.cork(() => {
|
|
244
|
+
setCorsHeaders(res);
|
|
245
|
+
res.writeStatus(config.HTTP_STATUS.NO_CONTENT.toString()).end();
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Configure route
|
|
250
|
+
app.post('/api/v1/configure', (res, req) => {
|
|
251
|
+
const abortedRef = { aborted: false };
|
|
252
|
+
res.onAborted(() => {
|
|
253
|
+
abortedRef.aborted = true;
|
|
254
|
+
log('Configure request aborted');
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
readJson(res, async (body) => {
|
|
258
|
+
if (abortedRef.aborted) return;
|
|
259
|
+
try {
|
|
260
|
+
const result = await createConfigureRoute(queueManager)(body);
|
|
261
|
+
if (abortedRef.aborted) return;
|
|
262
|
+
eventManager.emit('queue.created', result);
|
|
263
|
+
res.cork(() => {
|
|
264
|
+
setCorsHeaders(res);
|
|
265
|
+
res.writeStatus(config.HTTP_STATUS.CREATED.toString()).end(JSON.stringify(result));
|
|
266
|
+
});
|
|
267
|
+
} catch (error) {
|
|
268
|
+
if (abortedRef.aborted) return;
|
|
269
|
+
log('Configure error:', error);
|
|
270
|
+
res.cork(() => {
|
|
271
|
+
setCorsHeaders(res);
|
|
272
|
+
res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}, abortedRef);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// Push route
|
|
279
|
+
app.post('/api/v1/push', (res, req) => {
|
|
280
|
+
const abortedRef = { aborted: false };
|
|
281
|
+
res.onAborted(() => {
|
|
282
|
+
abortedRef.aborted = true;
|
|
283
|
+
log('Push request aborted');
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
readJson(res, async (body) => {
|
|
287
|
+
if (abortedRef.aborted) return;
|
|
288
|
+
try {
|
|
289
|
+
const result = await createPushRoute(queueManager)(body);
|
|
290
|
+
messageCount += result.messages.length;
|
|
291
|
+
requestCount++;
|
|
292
|
+
if (abortedRef.aborted) return;
|
|
293
|
+
|
|
294
|
+
// Emit events for pushed messages
|
|
295
|
+
for (const msg of result.messages) {
|
|
296
|
+
if (msg.status === 'queued') {
|
|
297
|
+
const item = body.items.find(i =>
|
|
298
|
+
i.transactionId === msg.transactionId || !i.transactionId
|
|
299
|
+
);
|
|
300
|
+
if (item) {
|
|
301
|
+
const partition = item.partition ?? 'Default';
|
|
302
|
+
const queuePath = `${item.queue}/${partition}`;
|
|
303
|
+
eventManager.emit('message.pushed', {
|
|
304
|
+
queue: item.queue,
|
|
305
|
+
partition: partition,
|
|
306
|
+
transactionId: msg.transactionId
|
|
307
|
+
});
|
|
308
|
+
eventManager.notifyMessageAvailable(queuePath);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
res.cork(() => {
|
|
314
|
+
setCorsHeaders(res);
|
|
315
|
+
res.writeStatus(config.HTTP_STATUS.CREATED.toString()).end(JSON.stringify(result));
|
|
316
|
+
});
|
|
317
|
+
} catch (error) {
|
|
318
|
+
if (abortedRef.aborted) return;
|
|
319
|
+
log('Push error:', error.message || error);
|
|
320
|
+
res.cork(() => {
|
|
321
|
+
setCorsHeaders(res);
|
|
322
|
+
res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}, abortedRef);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// Pop routes with new structure
|
|
329
|
+
app.get('/api/v1/pop/queue/:queue/partition/:partition', (res, req) => {
|
|
330
|
+
const queue = req.getParameter(0);
|
|
331
|
+
const partition = req.getParameter(1);
|
|
332
|
+
const query = new URLSearchParams(req.getQuery());
|
|
333
|
+
|
|
334
|
+
let aborted = false;
|
|
335
|
+
res.onAborted(() => {
|
|
336
|
+
aborted = true;
|
|
337
|
+
log('Pop request aborted');
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
createPopRoute(queueManager, eventManager)(
|
|
341
|
+
{
|
|
342
|
+
queue,
|
|
343
|
+
partition,
|
|
344
|
+
consumerGroup: query.get('consumerGroup') || query.get('consumer_group')
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
wait: query.get('wait') === 'true',
|
|
348
|
+
timeout: parseInt(query.get('timeout') || config.QUEUE.DEFAULT_TIMEOUT.toString()),
|
|
349
|
+
batch: parseInt(query.get('batch') || config.QUEUE.DEFAULT_BATCH_SIZE.toString()),
|
|
350
|
+
subscriptionMode: query.get('subscriptionMode'),
|
|
351
|
+
subscriptionFrom: query.get('subscriptionFrom')
|
|
352
|
+
}
|
|
353
|
+
).then(result => {
|
|
354
|
+
if (aborted) return;
|
|
355
|
+
|
|
356
|
+
// Emit events for processing messages
|
|
357
|
+
for (const msg of result.messages) {
|
|
358
|
+
eventManager.emit('message.processing', {
|
|
359
|
+
queue: msg.queue,
|
|
360
|
+
partition: msg.partition,
|
|
361
|
+
transactionId: msg.transactionId,
|
|
362
|
+
workerId: `worker-${process.pid}`
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
res.cork(() => {
|
|
367
|
+
setCorsHeaders(res);
|
|
368
|
+
if (result.messages.length > 0) {
|
|
369
|
+
res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify(result));
|
|
370
|
+
} else {
|
|
371
|
+
res.writeStatus(config.HTTP_STATUS.NO_CONTENT.toString()).end();
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
}).catch(error => {
|
|
375
|
+
if (aborted) return;
|
|
376
|
+
log('Pop error:', error);
|
|
377
|
+
res.cork(() => {
|
|
378
|
+
setCorsHeaders(res);
|
|
379
|
+
res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
app.get('/api/v1/pop/queue/:queue', (res, req) => {
|
|
385
|
+
const queue = req.getParameter(0);
|
|
386
|
+
const query = new URLSearchParams(req.getQuery());
|
|
387
|
+
|
|
388
|
+
let aborted = false;
|
|
389
|
+
res.onAborted(() => {
|
|
390
|
+
aborted = true;
|
|
391
|
+
log('Pop request aborted');
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
createPopRoute(queueManager, eventManager)(
|
|
395
|
+
{
|
|
396
|
+
queue,
|
|
397
|
+
consumerGroup: query.get('consumerGroup') || query.get('consumer_group')
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
wait: query.get('wait') === 'true',
|
|
401
|
+
timeout: parseInt(query.get('timeout') || config.QUEUE.DEFAULT_TIMEOUT.toString()),
|
|
402
|
+
batch: parseInt(query.get('batch') || config.QUEUE.DEFAULT_BATCH_SIZE.toString()),
|
|
403
|
+
subscriptionMode: query.get('subscriptionMode'),
|
|
404
|
+
subscriptionFrom: query.get('subscriptionFrom')
|
|
405
|
+
}
|
|
406
|
+
).then(result => {
|
|
407
|
+
if (aborted) return;
|
|
408
|
+
|
|
409
|
+
for (const msg of result.messages) {
|
|
410
|
+
eventManager.emit('message.processing', {
|
|
411
|
+
queue: msg.queue,
|
|
412
|
+
partition: msg.partition,
|
|
413
|
+
transactionId: msg.transactionId,
|
|
414
|
+
workerId: `worker-${process.pid}`
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
res.cork(() => {
|
|
419
|
+
setCorsHeaders(res);
|
|
420
|
+
if (result.messages.length > 0) {
|
|
421
|
+
res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify(result));
|
|
422
|
+
} else {
|
|
423
|
+
res.writeStatus(config.HTTP_STATUS.NO_CONTENT.toString()).end();
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
}).catch(error => {
|
|
427
|
+
if (aborted) return;
|
|
428
|
+
log('Pop error:', error);
|
|
429
|
+
res.cork(() => {
|
|
430
|
+
setCorsHeaders(res);
|
|
431
|
+
res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
// Pop with filters (namespace or task)
|
|
437
|
+
app.get('/api/v1/pop', (res, req) => {
|
|
438
|
+
const query = new URLSearchParams(req.getQuery());
|
|
439
|
+
const namespace = query.get('namespace');
|
|
440
|
+
const task = query.get('task');
|
|
441
|
+
|
|
442
|
+
let aborted = false;
|
|
443
|
+
res.onAborted(() => {
|
|
444
|
+
aborted = true;
|
|
445
|
+
log('Pop request aborted');
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
createPopRoute(queueManager, eventManager)(
|
|
449
|
+
{
|
|
450
|
+
namespace,
|
|
451
|
+
task,
|
|
452
|
+
consumerGroup: query.get('consumerGroup') || query.get('consumer_group')
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
wait: query.get('wait') === 'true',
|
|
456
|
+
timeout: parseInt(query.get('timeout') || config.QUEUE.DEFAULT_TIMEOUT.toString()),
|
|
457
|
+
batch: parseInt(query.get('batch') || config.QUEUE.DEFAULT_BATCH_SIZE.toString()),
|
|
458
|
+
subscriptionMode: query.get('subscriptionMode'),
|
|
459
|
+
subscriptionFrom: query.get('subscriptionFrom')
|
|
460
|
+
}
|
|
461
|
+
).then(result => {
|
|
462
|
+
if (aborted) return;
|
|
463
|
+
|
|
464
|
+
for (const msg of result.messages) {
|
|
465
|
+
eventManager.emit('message.processing', {
|
|
466
|
+
queue: msg.queue,
|
|
467
|
+
partition: msg.partition,
|
|
468
|
+
transactionId: msg.transactionId,
|
|
469
|
+
workerId: `worker-${process.pid}`
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
res.cork(() => {
|
|
474
|
+
setCorsHeaders(res);
|
|
475
|
+
if (result.messages.length > 0) {
|
|
476
|
+
res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify(result));
|
|
477
|
+
} else {
|
|
478
|
+
res.writeStatus(config.HTTP_STATUS.NO_CONTENT.toString()).end();
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
}).catch(error => {
|
|
482
|
+
if (aborted) return;
|
|
483
|
+
log('Pop error:', error);
|
|
484
|
+
res.cork(() => {
|
|
485
|
+
setCorsHeaders(res);
|
|
486
|
+
res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
// ACK route
|
|
492
|
+
app.post('/api/v1/ack', (res, req) => {
|
|
493
|
+
const abortedRef = { aborted: false };
|
|
494
|
+
res.onAborted(() => {
|
|
495
|
+
abortedRef.aborted = true;
|
|
496
|
+
log('ACK request aborted');
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
readJson(res, async (body) => {
|
|
500
|
+
if (abortedRef.aborted) return;
|
|
501
|
+
try {
|
|
502
|
+
const result = await createAckRoute(queueManager)(body);
|
|
503
|
+
if (abortedRef.aborted) return;
|
|
504
|
+
|
|
505
|
+
// Emit completion or failure event
|
|
506
|
+
if (body.status === 'completed') {
|
|
507
|
+
eventManager.emit('message.completed', {
|
|
508
|
+
transactionId: body.transactionId
|
|
509
|
+
});
|
|
510
|
+
} else {
|
|
511
|
+
eventManager.emit('message.failed', {
|
|
512
|
+
transactionId: body.transactionId,
|
|
513
|
+
error: body.error
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
res.cork(() => {
|
|
518
|
+
setCorsHeaders(res);
|
|
519
|
+
res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify(result));
|
|
520
|
+
});
|
|
521
|
+
} catch (error) {
|
|
522
|
+
if (abortedRef.aborted) return;
|
|
523
|
+
log('ACK error:', error);
|
|
524
|
+
res.cork(() => {
|
|
525
|
+
setCorsHeaders(res);
|
|
526
|
+
res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
}, abortedRef);
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
// Batch ACK route
|
|
533
|
+
app.post('/api/v1/ack/batch', (res, req) => {
|
|
534
|
+
const abortedRef = { aborted: false };
|
|
535
|
+
res.onAborted(() => {
|
|
536
|
+
abortedRef.aborted = true;
|
|
537
|
+
log('Batch ACK request aborted');
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
readJson(res, async (body) => {
|
|
541
|
+
if (abortedRef.aborted) return;
|
|
542
|
+
try {
|
|
543
|
+
const results = await queueManager.acknowledgeMessages(body.acknowledgments, body.consumerGroup);
|
|
544
|
+
if (abortedRef.aborted) return;
|
|
545
|
+
|
|
546
|
+
// Emit events for each acknowledgment
|
|
547
|
+
for (const ack of body.acknowledgments) {
|
|
548
|
+
if (ack.status === 'completed') {
|
|
549
|
+
eventManager.emit('message.completed', {
|
|
550
|
+
transactionId: ack.transactionId
|
|
551
|
+
});
|
|
552
|
+
} else {
|
|
553
|
+
eventManager.emit('message.failed', {
|
|
554
|
+
transactionId: ack.transactionId,
|
|
555
|
+
error: ack.error
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
res.cork(() => {
|
|
561
|
+
setCorsHeaders(res);
|
|
562
|
+
res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify({
|
|
563
|
+
processed: results.length,
|
|
564
|
+
results
|
|
565
|
+
}));
|
|
566
|
+
});
|
|
567
|
+
} catch (error) {
|
|
568
|
+
if (abortedRef.aborted) return;
|
|
569
|
+
log('Batch ACK error:', error);
|
|
570
|
+
res.cork(() => {
|
|
571
|
+
setCorsHeaders(res);
|
|
572
|
+
res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
}, abortedRef);
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
// Analytics routes
|
|
579
|
+
app.get('/api/v1/analytics/queues', (res, req) => {
|
|
580
|
+
const query = new URLSearchParams(req.getQuery());
|
|
581
|
+
const fromDateTime = query.get('fromDateTime');
|
|
582
|
+
const toDateTime = query.get('toDateTime');
|
|
583
|
+
const queue = query.get('queue');
|
|
584
|
+
const namespace = query.get('namespace');
|
|
585
|
+
const task = query.get('task');
|
|
586
|
+
|
|
587
|
+
let aborted = false;
|
|
588
|
+
res.onAborted(() => {
|
|
589
|
+
aborted = true;
|
|
590
|
+
log('Analytics request aborted');
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
analyticsRoutes.getQueues({ fromDateTime, toDateTime, queue, namespace, task }).then(result => {
|
|
594
|
+
if (aborted) return;
|
|
595
|
+
res.cork(() => {
|
|
596
|
+
setCorsHeaders(res);
|
|
597
|
+
res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify(result));
|
|
598
|
+
});
|
|
599
|
+
}).catch(error => {
|
|
600
|
+
if (aborted) return;
|
|
601
|
+
log('Analytics error:', error.message || error);
|
|
602
|
+
res.cork(() => {
|
|
603
|
+
setCorsHeaders(res);
|
|
604
|
+
res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
|
|
605
|
+
});
|
|
606
|
+
});
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
app.get('/api/v1/analytics/queue/:queue', (res, req) => {
|
|
610
|
+
const queue = req.getParameter(0);
|
|
611
|
+
const query = new URLSearchParams(req.getQuery());
|
|
612
|
+
const fromDateTime = query.get('fromDateTime');
|
|
613
|
+
const toDateTime = query.get('toDateTime');
|
|
614
|
+
|
|
615
|
+
let aborted = false;
|
|
616
|
+
res.onAborted(() => {
|
|
617
|
+
aborted = true;
|
|
618
|
+
log('Analytics request aborted');
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
analyticsRoutes.getQueueStats(queue, { fromDateTime, toDateTime }).then(result => {
|
|
622
|
+
if (aborted) return;
|
|
623
|
+
res.cork(() => {
|
|
624
|
+
setCorsHeaders(res);
|
|
625
|
+
res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify(result));
|
|
626
|
+
});
|
|
627
|
+
}).catch(error => {
|
|
628
|
+
if (aborted) return;
|
|
629
|
+
log('Analytics error:', error.message || error);
|
|
630
|
+
res.cork(() => {
|
|
631
|
+
setCorsHeaders(res);
|
|
632
|
+
res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
|
|
633
|
+
});
|
|
634
|
+
});
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
// Analytics with filters
|
|
638
|
+
app.get('/api/v1/analytics', (res, req) => {
|
|
639
|
+
const query = new URLSearchParams(req.getQuery());
|
|
640
|
+
const namespace = query.get('namespace');
|
|
641
|
+
const task = query.get('task');
|
|
642
|
+
const fromDateTime = query.get('fromDateTime');
|
|
643
|
+
const toDateTime = query.get('toDateTime');
|
|
644
|
+
const queue = query.get('queue');
|
|
645
|
+
|
|
646
|
+
let aborted = false;
|
|
647
|
+
res.onAborted(() => {
|
|
648
|
+
aborted = true;
|
|
649
|
+
log('Analytics request aborted');
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
const filters = { fromDateTime, toDateTime };
|
|
653
|
+
const getStats = namespace
|
|
654
|
+
? analyticsRoutes.getNamespaceStats(namespace, filters)
|
|
655
|
+
: task
|
|
656
|
+
? analyticsRoutes.getTaskStats(task, filters)
|
|
657
|
+
: analyticsRoutes.getQueues({ ...filters, queue, namespace, task });
|
|
658
|
+
|
|
659
|
+
getStats.then(result => {
|
|
660
|
+
if (aborted) return;
|
|
661
|
+
res.cork(() => {
|
|
662
|
+
setCorsHeaders(res);
|
|
663
|
+
res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify(result));
|
|
664
|
+
});
|
|
665
|
+
}).catch(error => {
|
|
666
|
+
if (aborted) return;
|
|
667
|
+
log('Analytics error:', error.message || error);
|
|
668
|
+
res.cork(() => {
|
|
669
|
+
setCorsHeaders(res);
|
|
670
|
+
res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
|
|
671
|
+
});
|
|
672
|
+
});
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
app.get('/api/v1/analytics/queue-depths', (res, req) => {
|
|
676
|
+
const query = new URLSearchParams(req.getQuery());
|
|
677
|
+
const fromDateTime = query.get('fromDateTime');
|
|
678
|
+
const toDateTime = query.get('toDateTime');
|
|
679
|
+
const queue = query.get('queue');
|
|
680
|
+
const namespace = query.get('namespace');
|
|
681
|
+
const task = query.get('task');
|
|
682
|
+
|
|
683
|
+
let aborted = false;
|
|
684
|
+
res.onAborted(() => {
|
|
685
|
+
aborted = true;
|
|
686
|
+
log('Analytics request aborted');
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
analyticsRoutes.getQueueDepths({ fromDateTime, toDateTime, queue, namespace, task }).then(result => {
|
|
690
|
+
if (aborted) return;
|
|
691
|
+
res.cork(() => {
|
|
692
|
+
setCorsHeaders(res);
|
|
693
|
+
res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify(result));
|
|
694
|
+
});
|
|
695
|
+
}).catch(error => {
|
|
696
|
+
if (aborted) return;
|
|
697
|
+
log('Analytics error:', error.message || error);
|
|
698
|
+
res.cork(() => {
|
|
699
|
+
setCorsHeaders(res);
|
|
700
|
+
res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
|
|
701
|
+
});
|
|
702
|
+
});
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
app.get('/api/v1/analytics/throughput', (res, req) => {
|
|
706
|
+
const query = new URLSearchParams(req.getQuery());
|
|
707
|
+
const fromDateTime = query.get('fromDateTime');
|
|
708
|
+
const toDateTime = query.get('toDateTime');
|
|
709
|
+
const queue = query.get('queue');
|
|
710
|
+
const namespace = query.get('namespace');
|
|
711
|
+
const task = query.get('task');
|
|
712
|
+
|
|
713
|
+
let aborted = false;
|
|
714
|
+
res.onAborted(() => {
|
|
715
|
+
aborted = true;
|
|
716
|
+
log('Analytics request aborted');
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
analyticsRoutes.getThroughput(pool, { fromDateTime, toDateTime, queue, namespace, task }).then(result => {
|
|
720
|
+
if (aborted) return;
|
|
721
|
+
res.cork(() => {
|
|
722
|
+
setCorsHeaders(res);
|
|
723
|
+
res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify(result));
|
|
724
|
+
});
|
|
725
|
+
}).catch(error => {
|
|
726
|
+
if (aborted) return;
|
|
727
|
+
log('Analytics error:', error.message || error);
|
|
728
|
+
res.cork(() => {
|
|
729
|
+
setCorsHeaders(res);
|
|
730
|
+
res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
|
|
731
|
+
});
|
|
732
|
+
});
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
app.get('/api/v1/analytics/queue-lag', (res, req) => {
|
|
736
|
+
const query = new URLSearchParams(req.getQuery());
|
|
737
|
+
const queue = query.get('queue');
|
|
738
|
+
const namespace = query.get('namespace');
|
|
739
|
+
const task = query.get('task');
|
|
740
|
+
const fromDateTime = query.get('fromDateTime');
|
|
741
|
+
const toDateTime = query.get('toDateTime');
|
|
742
|
+
|
|
743
|
+
let aborted = false;
|
|
744
|
+
res.onAborted(() => {
|
|
745
|
+
aborted = true;
|
|
746
|
+
log('Queue lag request aborted');
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
analyticsRoutes.getQueueLag({ queue, namespace, task, fromDateTime, toDateTime }).then(result => {
|
|
750
|
+
if (aborted) return;
|
|
751
|
+
res.cork(() => {
|
|
752
|
+
setCorsHeaders(res);
|
|
753
|
+
res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify(result));
|
|
754
|
+
});
|
|
755
|
+
}).catch(error => {
|
|
756
|
+
if (aborted) return;
|
|
757
|
+
log('Queue lag error:', error);
|
|
758
|
+
res.cork(() => {
|
|
759
|
+
setCorsHeaders(res);
|
|
760
|
+
res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
|
|
761
|
+
});
|
|
762
|
+
});
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
// Queue stats route
|
|
766
|
+
app.get('/api/v1/analytics/queue-stats', (res, req) => {
|
|
767
|
+
const query = new URLSearchParams(req.getQuery());
|
|
768
|
+
const queue = query.get('queue');
|
|
769
|
+
const namespace = query.get('namespace');
|
|
770
|
+
const task = query.get('task');
|
|
771
|
+
const fromDateTime = query.get('fromDateTime');
|
|
772
|
+
const toDateTime = query.get('toDateTime');
|
|
773
|
+
|
|
774
|
+
let aborted = false;
|
|
775
|
+
res.onAborted(() => {
|
|
776
|
+
aborted = true;
|
|
777
|
+
log('Queue stats request aborted');
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
queueManager.getQueueStats({ queue, namespace, task, fromDateTime, toDateTime }).then(result => {
|
|
781
|
+
if (aborted) return;
|
|
782
|
+
res.cork(() => {
|
|
783
|
+
setCorsHeaders(res);
|
|
784
|
+
res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify(result));
|
|
785
|
+
});
|
|
786
|
+
}).catch(error => {
|
|
787
|
+
if (aborted) return;
|
|
788
|
+
log('Queue stats error:', error);
|
|
789
|
+
res.cork(() => {
|
|
790
|
+
setCorsHeaders(res);
|
|
791
|
+
res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
|
|
792
|
+
});
|
|
793
|
+
});
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
// Messages routes
|
|
797
|
+
app.get('/api/v1/messages', (res, req) => {
|
|
798
|
+
const query = new URLSearchParams(req.getQuery());
|
|
799
|
+
|
|
800
|
+
let aborted = false;
|
|
801
|
+
res.onAborted(() => {
|
|
802
|
+
aborted = true;
|
|
803
|
+
log('Messages list request aborted');
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
const filters = {
|
|
807
|
+
ns: query.get('ns'),
|
|
808
|
+
task: query.get('task'),
|
|
809
|
+
queue: query.get('queue'),
|
|
810
|
+
status: query.get('status'),
|
|
811
|
+
limit: parseInt(query.get('limit') || config.API.DEFAULT_LIMIT.toString()),
|
|
812
|
+
offset: parseInt(query.get('offset') || config.API.DEFAULT_OFFSET.toString())
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
messagesRoutes.listMessages(filters).then(result => {
|
|
816
|
+
if (aborted) return;
|
|
817
|
+
res.cork(() => {
|
|
818
|
+
setCorsHeaders(res);
|
|
819
|
+
res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify({ messages: result }));
|
|
820
|
+
});
|
|
821
|
+
}).catch(error => {
|
|
822
|
+
if (aborted) return;
|
|
823
|
+
log('List messages error:', error);
|
|
824
|
+
res.cork(() => {
|
|
825
|
+
setCorsHeaders(res);
|
|
826
|
+
res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
|
|
827
|
+
});
|
|
828
|
+
});
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
app.get('/api/v1/messages/:transactionId', (res, req) => {
|
|
832
|
+
const transactionId = req.getParameter(0);
|
|
833
|
+
|
|
834
|
+
let aborted = false;
|
|
835
|
+
res.onAborted(() => {
|
|
836
|
+
aborted = true;
|
|
837
|
+
log('Get message request aborted');
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
messagesRoutes.getMessage(transactionId).then(result => {
|
|
841
|
+
if (aborted) return;
|
|
842
|
+
res.cork(() => {
|
|
843
|
+
setCorsHeaders(res);
|
|
844
|
+
res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify(result));
|
|
845
|
+
});
|
|
846
|
+
}).catch(error => {
|
|
847
|
+
if (aborted) return;
|
|
848
|
+
log('Get message error:', error);
|
|
849
|
+
res.cork(() => {
|
|
850
|
+
setCorsHeaders(res);
|
|
851
|
+
res.writeStatus(error.message === 'Message not found' ? config.HTTP_STATUS.NOT_FOUND.toString() : config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString())
|
|
852
|
+
.end(JSON.stringify({ error: error.message }));
|
|
853
|
+
});
|
|
854
|
+
});
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
app.del('/api/v1/messages/:transactionId', (res, req) => {
|
|
858
|
+
const transactionId = req.getParameter(0);
|
|
859
|
+
|
|
860
|
+
let aborted = false;
|
|
861
|
+
res.onAborted(() => {
|
|
862
|
+
aborted = true;
|
|
863
|
+
log('Delete message request aborted');
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
messagesRoutes.deleteMessage(transactionId).then(result => {
|
|
867
|
+
if (aborted) return;
|
|
868
|
+
res.cork(() => {
|
|
869
|
+
setCorsHeaders(res);
|
|
870
|
+
res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify(result));
|
|
871
|
+
});
|
|
872
|
+
}).catch(error => {
|
|
873
|
+
if (aborted) return;
|
|
874
|
+
log('Delete message error:', error);
|
|
875
|
+
res.cork(() => {
|
|
876
|
+
setCorsHeaders(res);
|
|
877
|
+
res.writeStatus(error.message === 'Message not found' ? config.HTTP_STATUS.NOT_FOUND.toString() : config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString())
|
|
878
|
+
.end(JSON.stringify({ error: error.message }));
|
|
879
|
+
});
|
|
880
|
+
});
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
app.post('/api/v1/messages/:transactionId/retry', (res, req) => {
|
|
884
|
+
const transactionId = req.getParameter(0);
|
|
885
|
+
|
|
886
|
+
let aborted = false;
|
|
887
|
+
res.onAborted(() => {
|
|
888
|
+
aborted = true;
|
|
889
|
+
log('Retry message request aborted');
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
messagesRoutes.retryMessage(transactionId).then(result => {
|
|
893
|
+
if (aborted) return;
|
|
894
|
+
res.cork(() => {
|
|
895
|
+
setCorsHeaders(res);
|
|
896
|
+
res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify(result));
|
|
897
|
+
});
|
|
898
|
+
}).catch(error => {
|
|
899
|
+
if (aborted) return;
|
|
900
|
+
log('Retry message error:', error);
|
|
901
|
+
res.cork(() => {
|
|
902
|
+
setCorsHeaders(res);
|
|
903
|
+
res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
|
|
904
|
+
});
|
|
905
|
+
});
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
app.post('/api/v1/messages/:transactionId/dlq', (res, req) => {
|
|
909
|
+
const transactionId = req.getParameter(0);
|
|
910
|
+
|
|
911
|
+
let aborted = false;
|
|
912
|
+
res.onAborted(() => {
|
|
913
|
+
aborted = true;
|
|
914
|
+
log('Move to DLQ request aborted');
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
messagesRoutes.moveToDLQ(transactionId).then(result => {
|
|
918
|
+
if (aborted) return;
|
|
919
|
+
res.cork(() => {
|
|
920
|
+
setCorsHeaders(res);
|
|
921
|
+
res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify(result));
|
|
922
|
+
});
|
|
923
|
+
}).catch(error => {
|
|
924
|
+
if (aborted) return;
|
|
925
|
+
log('Move to DLQ error:', error);
|
|
926
|
+
res.cork(() => {
|
|
927
|
+
setCorsHeaders(res);
|
|
928
|
+
res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
|
|
929
|
+
});
|
|
930
|
+
});
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
app.get('/api/v1/messages/:transactionId/related', (res, req) => {
|
|
934
|
+
const transactionId = req.getParameter(0);
|
|
935
|
+
|
|
936
|
+
let aborted = false;
|
|
937
|
+
res.onAborted(() => {
|
|
938
|
+
aborted = true;
|
|
939
|
+
log('Get related messages request aborted');
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
messagesRoutes.getRelatedMessages(transactionId).then(result => {
|
|
943
|
+
if (aborted) return;
|
|
944
|
+
res.cork(() => {
|
|
945
|
+
setCorsHeaders(res);
|
|
946
|
+
res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify({ messages: result }));
|
|
947
|
+
});
|
|
948
|
+
}).catch(error => {
|
|
949
|
+
if (aborted) return;
|
|
950
|
+
log('Get related messages error:', error);
|
|
951
|
+
res.cork(() => {
|
|
952
|
+
setCorsHeaders(res);
|
|
953
|
+
res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
|
|
954
|
+
});
|
|
955
|
+
});
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
app.del('/api/v1/queues/:queue/clear', (res, req) => {
|
|
959
|
+
const queue = req.getParameter(0);
|
|
960
|
+
const query = new URLSearchParams(req.getQuery());
|
|
961
|
+
const partition = query.get('partition');
|
|
962
|
+
|
|
963
|
+
let aborted = false;
|
|
964
|
+
res.onAborted(() => {
|
|
965
|
+
aborted = true;
|
|
966
|
+
log('Clear queue request aborted');
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
messagesRoutes.clearQueue(queue, partition).then(result => {
|
|
970
|
+
if (aborted) return;
|
|
971
|
+
res.cork(() => {
|
|
972
|
+
setCorsHeaders(res);
|
|
973
|
+
res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify(result));
|
|
974
|
+
});
|
|
975
|
+
}).catch(error => {
|
|
976
|
+
if (aborted) return;
|
|
977
|
+
log('Clear queue error:', error);
|
|
978
|
+
res.cork(() => {
|
|
979
|
+
setCorsHeaders(res);
|
|
980
|
+
res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
|
|
981
|
+
});
|
|
982
|
+
});
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
// Resources routes
|
|
986
|
+
app.get('/api/v1/resources/queues', (res, req) => {
|
|
987
|
+
const query = new URLSearchParams(req.getQuery());
|
|
988
|
+
const namespace = query.get('namespace');
|
|
989
|
+
const task = query.get('task');
|
|
990
|
+
|
|
991
|
+
let aborted = false;
|
|
992
|
+
res.onAborted(() => {
|
|
993
|
+
aborted = true;
|
|
994
|
+
log('Get queues request aborted');
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
resourcesRoutes.getQueues({ namespace, task }).then(result => {
|
|
998
|
+
if (aborted) return;
|
|
999
|
+
res.cork(() => {
|
|
1000
|
+
setCorsHeaders(res);
|
|
1001
|
+
res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify({ queues: result }));
|
|
1002
|
+
});
|
|
1003
|
+
}).catch(error => {
|
|
1004
|
+
if (aborted) return;
|
|
1005
|
+
log('Get queues error:', error);
|
|
1006
|
+
res.cork(() => {
|
|
1007
|
+
setCorsHeaders(res);
|
|
1008
|
+
res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
|
|
1009
|
+
});
|
|
1010
|
+
});
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
app.get('/api/v1/resources/queues/:queue', (res, req) => {
|
|
1014
|
+
const queue = req.getParameter(0);
|
|
1015
|
+
|
|
1016
|
+
let aborted = false;
|
|
1017
|
+
res.onAborted(() => {
|
|
1018
|
+
aborted = true;
|
|
1019
|
+
log('Get queue request aborted');
|
|
1020
|
+
});
|
|
1021
|
+
|
|
1022
|
+
resourcesRoutes.getQueue(queue).then(result => {
|
|
1023
|
+
if (aborted) return;
|
|
1024
|
+
res.cork(() => {
|
|
1025
|
+
setCorsHeaders(res);
|
|
1026
|
+
res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify(result));
|
|
1027
|
+
});
|
|
1028
|
+
}).catch(error => {
|
|
1029
|
+
if (aborted) return;
|
|
1030
|
+
log('Get queue error:', error);
|
|
1031
|
+
res.cork(() => {
|
|
1032
|
+
setCorsHeaders(res);
|
|
1033
|
+
res.writeStatus(error.message === 'Queue not found' ? config.HTTP_STATUS.NOT_FOUND.toString() : config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString())
|
|
1034
|
+
.end(JSON.stringify({ error: error.message }));
|
|
1035
|
+
});
|
|
1036
|
+
});
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
app.get('/api/v1/resources/partitions', (res, req) => {
|
|
1040
|
+
const query = new URLSearchParams(req.getQuery());
|
|
1041
|
+
const queue = query.get('queue');
|
|
1042
|
+
const minDepth = query.get('minDepth');
|
|
1043
|
+
|
|
1044
|
+
let aborted = false;
|
|
1045
|
+
res.onAborted(() => {
|
|
1046
|
+
aborted = true;
|
|
1047
|
+
log('Get partitions request aborted');
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
resourcesRoutes.getPartitions({ queue, minDepth: minDepth ? parseInt(minDepth) : null }).then(result => {
|
|
1051
|
+
if (aborted) return;
|
|
1052
|
+
res.cork(() => {
|
|
1053
|
+
setCorsHeaders(res);
|
|
1054
|
+
res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify({ partitions: result }));
|
|
1055
|
+
});
|
|
1056
|
+
}).catch(error => {
|
|
1057
|
+
if (aborted) return;
|
|
1058
|
+
log('Get partitions error:', error);
|
|
1059
|
+
res.cork(() => {
|
|
1060
|
+
setCorsHeaders(res);
|
|
1061
|
+
res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
|
|
1062
|
+
});
|
|
1063
|
+
});
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
app.get('/api/v1/resources/namespaces', (res, req) => {
|
|
1067
|
+
let aborted = false;
|
|
1068
|
+
res.onAborted(() => {
|
|
1069
|
+
aborted = true;
|
|
1070
|
+
log('Get namespaces request aborted');
|
|
1071
|
+
});
|
|
1072
|
+
|
|
1073
|
+
resourcesRoutes.getNamespaces().then(result => {
|
|
1074
|
+
if (aborted) return;
|
|
1075
|
+
res.cork(() => {
|
|
1076
|
+
setCorsHeaders(res);
|
|
1077
|
+
res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify({ namespaces: result }));
|
|
1078
|
+
});
|
|
1079
|
+
}).catch(error => {
|
|
1080
|
+
if (aborted) return;
|
|
1081
|
+
log('Get namespaces error:', error);
|
|
1082
|
+
res.cork(() => {
|
|
1083
|
+
setCorsHeaders(res);
|
|
1084
|
+
res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
|
|
1085
|
+
});
|
|
1086
|
+
});
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
app.get('/api/v1/resources/tasks', (res, req) => {
|
|
1090
|
+
let aborted = false;
|
|
1091
|
+
res.onAborted(() => {
|
|
1092
|
+
aborted = true;
|
|
1093
|
+
log('Get tasks request aborted');
|
|
1094
|
+
});
|
|
1095
|
+
|
|
1096
|
+
resourcesRoutes.getTasks().then(result => {
|
|
1097
|
+
if (aborted) return;
|
|
1098
|
+
res.cork(() => {
|
|
1099
|
+
setCorsHeaders(res);
|
|
1100
|
+
res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify({ tasks: result }));
|
|
1101
|
+
});
|
|
1102
|
+
}).catch(error => {
|
|
1103
|
+
if (aborted) return;
|
|
1104
|
+
log('Get tasks error:', error);
|
|
1105
|
+
res.cork(() => {
|
|
1106
|
+
setCorsHeaders(res);
|
|
1107
|
+
res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
|
|
1108
|
+
});
|
|
1109
|
+
});
|
|
1110
|
+
});
|
|
1111
|
+
|
|
1112
|
+
app.get('/api/v1/resources/overview', (res, req) => {
|
|
1113
|
+
let aborted = false;
|
|
1114
|
+
res.onAborted(() => {
|
|
1115
|
+
aborted = true;
|
|
1116
|
+
log('Get overview request aborted');
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
resourcesRoutes.getSystemOverview().then(result => {
|
|
1120
|
+
if (aborted) return;
|
|
1121
|
+
res.cork(() => {
|
|
1122
|
+
setCorsHeaders(res);
|
|
1123
|
+
res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify(result));
|
|
1124
|
+
});
|
|
1125
|
+
}).catch(error => {
|
|
1126
|
+
if (aborted) return;
|
|
1127
|
+
log('Get overview error:', error);
|
|
1128
|
+
res.cork(() => {
|
|
1129
|
+
setCorsHeaders(res);
|
|
1130
|
+
res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
|
|
1131
|
+
});
|
|
1132
|
+
});
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
// Health check with performance stats
|
|
1136
|
+
app.get('/health', async (res, req) => {
|
|
1137
|
+
let aborted = false;
|
|
1138
|
+
res.onAborted(() => {
|
|
1139
|
+
aborted = true;
|
|
1140
|
+
log('Health check aborted');
|
|
1141
|
+
});
|
|
1142
|
+
|
|
1143
|
+
try {
|
|
1144
|
+
await pool.query('SELECT 1');
|
|
1145
|
+
if (aborted) return;
|
|
1146
|
+
|
|
1147
|
+
const uptime = (Date.now() - startTime) / 1000;
|
|
1148
|
+
res.cork(() => {
|
|
1149
|
+
setCorsHeaders(res);
|
|
1150
|
+
res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify({
|
|
1151
|
+
status: 'healthy',
|
|
1152
|
+
uptime: `${Math.floor(uptime)}s`,
|
|
1153
|
+
connections: wsServer.getConnectionCount(),
|
|
1154
|
+
stats: {
|
|
1155
|
+
requests: requestCount,
|
|
1156
|
+
messages: messageCount,
|
|
1157
|
+
requestsPerSecond: (requestCount / uptime).toFixed(2),
|
|
1158
|
+
messagesPerSecond: (messageCount / uptime).toFixed(2),
|
|
1159
|
+
pool: {
|
|
1160
|
+
total: pool.totalCount,
|
|
1161
|
+
idle: pool.idleCount,
|
|
1162
|
+
waiting: pool.waitingCount
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
}));
|
|
1166
|
+
});
|
|
1167
|
+
} catch (error) {
|
|
1168
|
+
if (aborted) return;
|
|
1169
|
+
res.cork(() => {
|
|
1170
|
+
setCorsHeaders(res);
|
|
1171
|
+
res.writeStatus(config.HTTP_STATUS.SERVICE_UNAVAILABLE.toString()).end(JSON.stringify({
|
|
1172
|
+
status: 'unhealthy',
|
|
1173
|
+
error: error.message
|
|
1174
|
+
}));
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
});
|
|
1178
|
+
|
|
1179
|
+
// Metrics endpoint for performance monitoring
|
|
1180
|
+
app.get('/metrics', (res, req) => {
|
|
1181
|
+
res.onAborted(() => {
|
|
1182
|
+
log('Metrics request aborted');
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
const uptime = (Date.now() - startTime) / 1000;
|
|
1186
|
+
const metrics = {
|
|
1187
|
+
uptime: uptime,
|
|
1188
|
+
requests: {
|
|
1189
|
+
total: requestCount,
|
|
1190
|
+
rate: requestCount / uptime
|
|
1191
|
+
},
|
|
1192
|
+
messages: {
|
|
1193
|
+
total: messageCount,
|
|
1194
|
+
rate: messageCount / uptime
|
|
1195
|
+
},
|
|
1196
|
+
database: {
|
|
1197
|
+
poolSize: pool.totalCount,
|
|
1198
|
+
idleConnections: pool.idleCount,
|
|
1199
|
+
waitingRequests: pool.waitingCount
|
|
1200
|
+
},
|
|
1201
|
+
memory: process.memoryUsage(),
|
|
1202
|
+
cpu: process.cpuUsage()
|
|
1203
|
+
};
|
|
1204
|
+
|
|
1205
|
+
res.cork(() => {
|
|
1206
|
+
setCorsHeaders(res);
|
|
1207
|
+
res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify(metrics));
|
|
1208
|
+
});
|
|
1209
|
+
});
|
|
1210
|
+
|
|
1211
|
+
// Variable to hold system event consumer stop function
|
|
1212
|
+
let stopSystemEventConsumer = null;
|
|
1213
|
+
|
|
1214
|
+
// Start server
|
|
1215
|
+
app.listen(HOST, PORT, async (token) => {
|
|
1216
|
+
if (token) {
|
|
1217
|
+
log(`ā
Queen Server running at http://${HOST}:${PORT}`);
|
|
1218
|
+
log(`š Features enabled:`);
|
|
1219
|
+
log(` - Encryption: ${encryptionEnabled ? 'ā
' : 'ā (set QUEEN_ENCRYPTION_KEY)'}`);
|
|
1220
|
+
log(` - Retention: ā
`);
|
|
1221
|
+
log(` - Eviction: ā
`);
|
|
1222
|
+
log(` - WebSocket Dashboard: ws://${HOST}:${PORT}/ws/dashboard`);
|
|
1223
|
+
log(` - Server Instance ID: ${SERVER_INSTANCE_ID}`);
|
|
1224
|
+
|
|
1225
|
+
// Start system event synchronization and consumption
|
|
1226
|
+
try {
|
|
1227
|
+
// Create temporary client for synchronization
|
|
1228
|
+
const syncClient = createQueenClient({
|
|
1229
|
+
baseUrl: `http://localhost:${PORT}`
|
|
1230
|
+
});
|
|
1231
|
+
|
|
1232
|
+
// Synchronize system events (catch up on missed events)
|
|
1233
|
+
await syncSystemEvents(syncClient, systemEventManager, SERVER_INSTANCE_ID);
|
|
1234
|
+
|
|
1235
|
+
// Start consuming system events
|
|
1236
|
+
// Note: consume() returns the stop function directly
|
|
1237
|
+
stopSystemEventConsumer = syncClient.consume({
|
|
1238
|
+
queue: SYSTEM_QUEUE,
|
|
1239
|
+
consumerGroup: SERVER_INSTANCE_ID,
|
|
1240
|
+
handler: async (message) => {
|
|
1241
|
+
await systemEventManager.processSystemEvent(message.payload);
|
|
1242
|
+
},
|
|
1243
|
+
options: {
|
|
1244
|
+
batch: 10,
|
|
1245
|
+
wait: true
|
|
1246
|
+
}
|
|
1247
|
+
});
|
|
1248
|
+
|
|
1249
|
+
log(`ā
System event consumer started`);
|
|
1250
|
+
} catch (error) {
|
|
1251
|
+
log(`ā ļø Failed to start system event consumer: ${error.message}`);
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
log(`šÆ Ready to process messages with high performance!`);
|
|
1255
|
+
} else {
|
|
1256
|
+
log(`Failed to start server on port ${PORT}`);
|
|
1257
|
+
process.exit(1);
|
|
1258
|
+
}
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
// Graceful shutdown
|
|
1262
|
+
const shutdown = async (signal) => {
|
|
1263
|
+
log(`\nš Received ${signal}, shutting down gracefully...`);
|
|
1264
|
+
|
|
1265
|
+
// Stop system event consumer
|
|
1266
|
+
if (stopSystemEventConsumer) {
|
|
1267
|
+
stopSystemEventConsumer();
|
|
1268
|
+
log('ā
System event consumer stopped');
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// Stop background jobs
|
|
1272
|
+
if (stopRetention) stopRetention();
|
|
1273
|
+
if (stopEviction) stopEviction();
|
|
1274
|
+
|
|
1275
|
+
const uptime = (Date.now() - startTime) / 1000;
|
|
1276
|
+
if (messageCount > 0) {
|
|
1277
|
+
log(`š Final stats: ${messageCount} messages processed in ${uptime.toFixed(1)}s`);
|
|
1278
|
+
log(` Average: ${(messageCount / uptime).toFixed(2)} messages/second`);
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
await pool.end();
|
|
1282
|
+
process.exit(0);
|
|
1283
|
+
};
|
|
1284
|
+
|
|
1285
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
1286
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|