queen-mq 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PROGRAMMATIC_SERVER.md +190 -0
- package/README.md +27 -0
- package/examples/batch-processing.js +2 -2
- package/examples/programmatic-server.js +58 -0
- package/examples/test-cache-multi-server.js +2 -2
- package/examples/test-connection-recovery.js +66 -0
- package/package.json +1 -1
- package/src/client/client.js +27 -1
- package/src/client/index.js +4 -3
- package/src/managers/queueManagerOptimized.js +64 -35
- package/src/routes/messages.js +8 -8
- package/src/routes/status.js +4 -4
- package/src/server.js +534 -504
- package/src/services/startupSync.js +1 -1
- package/src/test/advanced-pattern-tests.js +1 -1
- package/src/test/core-tests.js +110 -0
- package/src/test/test-new.js +3 -1
- package/src/test/test.js +1 -1
- package/src/test/window-buffer-test.js +114 -0
- package/webapp/src/components/messages/MessageFilters.vue +1 -14
- package/webapp/src/views/Analytics.vue +5 -5
- package/webapp/src/views/Messages.vue +4 -2
- package/src/client/queenClient.js +0 -518
package/src/server.js
CHANGED
|
@@ -9,7 +9,7 @@ import { createResourceCache } from './managers/resourceCache.js';
|
|
|
9
9
|
import { createEventManager } from './managers/eventManager.js';
|
|
10
10
|
import { SystemEventManager, SYSTEM_QUEUE } from './managers/systemEventManager.js';
|
|
11
11
|
import { syncSystemEvents } from './services/startupSync.js';
|
|
12
|
-
import { createQueenClient } from './client/queenClient.js';
|
|
12
|
+
//import { createQueenClient } from './client/queenClient.js';
|
|
13
13
|
import { createPushRoute } from './routes/push.js';
|
|
14
14
|
import { createPopRoute } from './routes/pop.js';
|
|
15
15
|
import { createAckRoute } from './routes/ack.js';
|
|
@@ -26,379 +26,392 @@ import { streamJSON, sendJSON, sendError } from './utils/streaming.js';
|
|
|
26
26
|
|
|
27
27
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Start Queen MQ Server
|
|
31
|
+
* @param {Object} options - Server configuration options
|
|
32
|
+
* @param {number} options.port - Server port (default: from config)
|
|
33
|
+
* @param {string} options.host - Server host (default: from config)
|
|
34
|
+
* @param {string} options.workerId - Worker ID for clustering (default: from config)
|
|
35
|
+
* @returns {Promise<Object>} Server instance with shutdown method
|
|
36
|
+
*/
|
|
37
|
+
export async function QueenServer(options = {}) {
|
|
38
|
+
const PORT = options.port || config.SERVER.PORT;
|
|
39
|
+
const HOST = options.host || config.SERVER.HOST;
|
|
31
40
|
|
|
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;
|
|
41
|
+
// Use WORKER_ID from config as the server instance ID for consumer groups
|
|
42
|
+
// This ensures the same server (identified by WORKER_ID) maintains its position in the event stream
|
|
43
|
+
const SERVER_INSTANCE_ID = options.workerId || config.SERVER.WORKER_ID;
|
|
35
44
|
|
|
36
|
-
|
|
37
|
-
let requestCount = 0;
|
|
38
|
-
let messageCount = 0;
|
|
39
|
-
const startTime = Date.now();
|
|
45
|
+
const SKIP_DATABASE_MIGRATION = (options.skipDatabaseMigration || process.env.SKIP_DATABASE_MIGRATION) || false;
|
|
40
46
|
|
|
41
|
-
//
|
|
42
|
-
let
|
|
47
|
+
// Performance monitoring
|
|
48
|
+
let requestCount = 0;
|
|
49
|
+
let messageCount = 0;
|
|
50
|
+
const startTime = Date.now();
|
|
43
51
|
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
const resourceCache = createResourceCache();
|
|
47
|
-
const eventManager = createEventManager();
|
|
52
|
+
// Shutdown flag to prevent background jobs from running during shutdown
|
|
53
|
+
let isShuttingDown = false;
|
|
48
54
|
|
|
49
|
-
// Initialize
|
|
50
|
-
const
|
|
55
|
+
// Initialize components
|
|
56
|
+
const pool = createPool();
|
|
57
|
+
const resourceCache = createResourceCache();
|
|
58
|
+
const eventManager = createEventManager();
|
|
51
59
|
|
|
52
|
-
//
|
|
53
|
-
|
|
60
|
+
// Initialize system event manager
|
|
61
|
+
const systemEventManager = new SystemEventManager(pool, SERVER_INSTANCE_ID);
|
|
54
62
|
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
const messagesRoutes = createMessagesRoutes(pool, queueManager);
|
|
58
|
-
const resourcesRoutes = createResourcesRoutes(pool, systemEventManager);
|
|
59
|
-
const statusRoutes = createStatusRoutes(pool);
|
|
63
|
+
// Register cache event handlers
|
|
64
|
+
resourceCache.registerEventHandlers(systemEventManager);
|
|
60
65
|
|
|
61
|
-
//
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
66
|
+
// Always use optimized queue manager - it's better for all scenarios
|
|
67
|
+
const queueManager = createOptimizedQueueManager(pool, resourceCache, eventManager, systemEventManager);
|
|
68
|
+
const messagesRoutes = createMessagesRoutes(pool, queueManager);
|
|
69
|
+
const resourcesRoutes = createResourcesRoutes(pool, systemEventManager);
|
|
70
|
+
const statusRoutes = createStatusRoutes(pool);
|
|
71
|
+
|
|
72
|
+
// Initialize database with migrations
|
|
73
|
+
const initDatabaseWithMigrations = async () => {
|
|
74
|
+
// Run base schema
|
|
75
|
+
await initDatabase(pool);
|
|
76
|
+
|
|
77
|
+
// Run extension migrations
|
|
78
|
+
const migrationPath = path.join(__dirname, '..', 'migrations', '001-extend-features.sql');
|
|
79
|
+
if (fs.existsSync(migrationPath)) {
|
|
80
|
+
const migration = fs.readFileSync(migrationPath, 'utf8');
|
|
81
|
+
try {
|
|
82
|
+
await pool.query(migration);
|
|
83
|
+
log('✅ Extension features migration applied');
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (!error.message.includes('already exists')) {
|
|
86
|
+
log('Migration error:', error);
|
|
87
|
+
}
|
|
76
88
|
}
|
|
77
89
|
}
|
|
78
|
-
}
|
|
79
|
-
};
|
|
90
|
+
};
|
|
80
91
|
|
|
81
|
-
|
|
92
|
+
if (!SKIP_DATABASE_MIGRATION) {
|
|
93
|
+
await initDatabaseWithMigrations();
|
|
94
|
+
}
|
|
82
95
|
|
|
83
|
-
// Initialize system queue
|
|
84
|
-
async function initializeSystemQueue() {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
96
|
+
// Initialize system queue
|
|
97
|
+
async function initializeSystemQueue() {
|
|
98
|
+
try {
|
|
99
|
+
// Direct database operation to create system queue
|
|
100
|
+
await pool.query(`
|
|
101
|
+
INSERT INTO queen.queues (name, ttl, priority, max_queue_size)
|
|
102
|
+
VALUES ($1, 300, 100, 10000)
|
|
103
|
+
ON CONFLICT (name) DO UPDATE SET
|
|
104
|
+
ttl = EXCLUDED.ttl,
|
|
105
|
+
priority = EXCLUDED.priority,
|
|
106
|
+
max_queue_size = EXCLUDED.max_queue_size
|
|
107
|
+
`, [SYSTEM_QUEUE]);
|
|
108
|
+
|
|
109
|
+
await pool.query(`
|
|
110
|
+
INSERT INTO queen.partitions (queue_id, name)
|
|
111
|
+
SELECT id, 'Default' FROM queen.queues WHERE name = $1
|
|
112
|
+
ON CONFLICT (queue_id, name) DO NOTHING
|
|
113
|
+
`, [SYSTEM_QUEUE]);
|
|
114
|
+
|
|
115
|
+
log('✅ System event queue initialized');
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error('Failed to initialize system queue:', error);
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
106
120
|
}
|
|
107
|
-
}
|
|
108
121
|
|
|
109
|
-
await initializeSystemQueue();
|
|
122
|
+
await initializeSystemQueue();
|
|
110
123
|
|
|
111
|
-
// Initialize extended features
|
|
112
|
-
log('🚀 Initializing Queen Services...');
|
|
124
|
+
// Initialize extended features
|
|
125
|
+
log('🚀 Initializing Queen Services...');
|
|
113
126
|
|
|
114
|
-
// 1. Encryption service
|
|
115
|
-
const encryptionEnabled = initEncryption();
|
|
127
|
+
// 1. Encryption service
|
|
128
|
+
const encryptionEnabled = initEncryption();
|
|
116
129
|
|
|
117
|
-
// 2. Retention service
|
|
118
|
-
const stopRetention = startRetentionJob(pool);
|
|
130
|
+
// 2. Retention service
|
|
131
|
+
const stopRetention = startRetentionJob(pool);
|
|
119
132
|
|
|
120
|
-
// 3. Eviction service
|
|
121
|
-
const stopEviction = startEvictionJob(pool, eventManager);
|
|
133
|
+
// 3. Eviction service
|
|
134
|
+
const stopEviction = startEvictionJob(pool, eventManager);
|
|
122
135
|
|
|
123
|
-
// Background job for lease reclamation
|
|
124
|
-
const leaseReclaimInterval = setInterval(async () => {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
136
|
+
// Background job for lease reclamation
|
|
137
|
+
const leaseReclaimInterval = setInterval(async () => {
|
|
138
|
+
if (isShuttingDown) return;
|
|
139
|
+
try {
|
|
140
|
+
const reclaimed = await queueManager.reclaimExpiredLeases();
|
|
141
|
+
if (reclaimed > 0) {
|
|
142
|
+
log(`Reclaimed ${reclaimed} expired leases`);
|
|
143
|
+
}
|
|
144
|
+
} catch (error) {
|
|
145
|
+
if (!isShuttingDown) {
|
|
146
|
+
log('Error reclaiming leases:', error);
|
|
147
|
+
}
|
|
134
148
|
}
|
|
135
|
-
}
|
|
136
|
-
}, config.JOBS.LEASE_RECLAIM_INTERVAL);
|
|
149
|
+
}, config.JOBS.LEASE_RECLAIM_INTERVAL);
|
|
137
150
|
|
|
138
|
-
// Create server
|
|
139
|
-
const app = uWS.App();
|
|
151
|
+
// Create server
|
|
152
|
+
const app = uWS.App();
|
|
140
153
|
|
|
141
|
-
// Initialize WebSocket server
|
|
142
|
-
const wsServer = createWebSocketServer(app, eventManager);
|
|
154
|
+
// Initialize WebSocket server
|
|
155
|
+
const wsServer = createWebSocketServer(app, eventManager);
|
|
143
156
|
|
|
144
|
-
// Background job for queue depth updates
|
|
145
|
-
const queueDepthInterval = setInterval(async () => {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
157
|
+
// Background job for queue depth updates
|
|
158
|
+
const queueDepthInterval = setInterval(async () => {
|
|
159
|
+
if (isShuttingDown) return;
|
|
160
|
+
try {
|
|
161
|
+
await wsServer.updateQueueDepths(queueManager);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
if (!isShuttingDown) {
|
|
164
|
+
log('Error updating queue depths:', error);
|
|
165
|
+
}
|
|
152
166
|
}
|
|
153
|
-
}
|
|
154
|
-
}, config.JOBS.QUEUE_DEPTH_UPDATE_INTERVAL);
|
|
167
|
+
}, config.JOBS.QUEUE_DEPTH_UPDATE_INTERVAL);
|
|
155
168
|
|
|
156
|
-
// Background job for system stats
|
|
157
|
-
const systemStatsInterval = setInterval(async () => {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
169
|
+
// Background job for system stats
|
|
170
|
+
const systemStatsInterval = setInterval(async () => {
|
|
171
|
+
if (isShuttingDown) return;
|
|
172
|
+
try {
|
|
173
|
+
await wsServer.sendSystemStats(pool);
|
|
174
|
+
} catch (error) {
|
|
175
|
+
if (!isShuttingDown) {
|
|
176
|
+
log('Error sending system stats:', error);
|
|
177
|
+
}
|
|
164
178
|
}
|
|
165
|
-
}
|
|
166
|
-
}, config.JOBS.SYSTEM_STATS_UPDATE_INTERVAL);
|
|
179
|
+
}, config.JOBS.SYSTEM_STATS_UPDATE_INTERVAL);
|
|
167
180
|
|
|
168
|
-
// CORS headers helper
|
|
169
|
-
const setCorsHeaders = (res) => {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
};
|
|
181
|
+
// CORS headers helper
|
|
182
|
+
const setCorsHeaders = (res) => {
|
|
183
|
+
res.writeHeader('Access-Control-Allow-Origin', config.API.CORS_ALLOWED_ORIGINS);
|
|
184
|
+
res.writeHeader('Access-Control-Allow-Methods', config.API.CORS_ALLOWED_METHODS);
|
|
185
|
+
res.writeHeader('Access-Control-Allow-Headers', config.API.CORS_ALLOWED_HEADERS);
|
|
186
|
+
res.writeHeader('Access-Control-Max-Age', config.API.CORS_MAX_AGE.toString());
|
|
187
|
+
};
|
|
175
188
|
|
|
176
|
-
// Helper to read JSON body
|
|
177
|
-
const readJson = (res, cb, abortedRef) => {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
res.onData((ab, isLast) => {
|
|
182
|
-
const chunk = Buffer.from(ab);
|
|
189
|
+
// Helper to read JSON body
|
|
190
|
+
const readJson = (res, cb, abortedRef) => {
|
|
191
|
+
let buffer;
|
|
192
|
+
const maxBodySize = config.API.MAX_BODY_SIZE; // 10MB max body size
|
|
183
193
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
if (isLast) {
|
|
193
|
-
let json;
|
|
194
|
-
try {
|
|
195
|
-
let fullData;
|
|
196
|
-
if (buffer) {
|
|
197
|
-
fullData = Buffer.concat([buffer, chunk]);
|
|
198
|
-
} else {
|
|
199
|
-
fullData = chunk;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const jsonString = fullData.toString('utf8');
|
|
203
|
-
|
|
204
|
-
// Debug large payloads
|
|
205
|
-
if (jsonString.length > 1000000) {
|
|
206
|
-
log(`Parsing large JSON: ${jsonString.length} bytes, starts with: ${jsonString.substring(0, 100)}`);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
json = JSON.parse(jsonString);
|
|
210
|
-
} catch (e) {
|
|
194
|
+
res.onData((ab, isLast) => {
|
|
195
|
+
const chunk = Buffer.from(ab);
|
|
196
|
+
|
|
197
|
+
// Check size limit
|
|
198
|
+
const currentSize = (buffer ? buffer.length : 0) + chunk.length;
|
|
199
|
+
if (currentSize > maxBodySize) {
|
|
211
200
|
if (abortedRef && abortedRef.aborted) return;
|
|
212
|
-
|
|
213
|
-
log(`JSON parse error: ${e.message}`);
|
|
214
|
-
|
|
215
|
-
// Log what we're trying to parse for debugging
|
|
216
|
-
let sample = '';
|
|
217
|
-
if (buffer) {
|
|
218
|
-
const fullData = Buffer.concat([buffer, chunk]);
|
|
219
|
-
sample = fullData.toString('utf8').substring(0, 200);
|
|
220
|
-
} else {
|
|
221
|
-
sample = chunk.toString('utf8').substring(0, 200);
|
|
222
|
-
}
|
|
223
|
-
log(`Failed to parse data starting with: ${sample}`);
|
|
224
|
-
|
|
225
|
-
res.writeStatus(config.HTTP_STATUS.BAD_REQUEST.toString()).end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
201
|
+
res.writeStatus(config.HTTP_STATUS.BAD_REQUEST.toString()).end(JSON.stringify({ error: 'Request body too large' }));
|
|
226
202
|
return;
|
|
227
203
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
204
|
+
|
|
205
|
+
if (isLast) {
|
|
206
|
+
let json;
|
|
207
|
+
try {
|
|
208
|
+
let fullData;
|
|
209
|
+
if (buffer) {
|
|
210
|
+
fullData = Buffer.concat([buffer, chunk]);
|
|
211
|
+
} else {
|
|
212
|
+
fullData = chunk;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const jsonString = fullData.toString('utf8');
|
|
216
|
+
|
|
217
|
+
// Debug large payloads
|
|
218
|
+
if (jsonString.length > 1000000) {
|
|
219
|
+
log(`Parsing large JSON: ${jsonString.length} bytes, starts with: ${jsonString.substring(0, 100)}`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
json = JSON.parse(jsonString);
|
|
223
|
+
} catch (e) {
|
|
224
|
+
if (abortedRef && abortedRef.aborted) return;
|
|
225
|
+
// Log the actual error for debugging
|
|
226
|
+
log(`JSON parse error: ${e.message}`);
|
|
227
|
+
|
|
228
|
+
// Log what we're trying to parse for debugging
|
|
229
|
+
let sample = '';
|
|
230
|
+
if (buffer) {
|
|
231
|
+
const fullData = Buffer.concat([buffer, chunk]);
|
|
232
|
+
sample = fullData.toString('utf8').substring(0, 200);
|
|
233
|
+
} else {
|
|
234
|
+
sample = chunk.toString('utf8').substring(0, 200);
|
|
235
|
+
}
|
|
236
|
+
log(`Failed to parse data starting with: ${sample}`);
|
|
237
|
+
|
|
238
|
+
res.writeStatus(config.HTTP_STATUS.BAD_REQUEST.toString()).end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
cb(json);
|
|
232
242
|
} else {
|
|
233
|
-
|
|
243
|
+
if (buffer) {
|
|
244
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
245
|
+
} else {
|
|
246
|
+
buffer = Buffer.from(chunk); // Ensure it's a Buffer
|
|
247
|
+
}
|
|
234
248
|
}
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
};
|
|
249
|
+
});
|
|
250
|
+
};
|
|
238
251
|
|
|
239
|
-
// Handle OPTIONS requests for CORS preflight
|
|
240
|
-
app.options('/*', (res, req) => {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
252
|
+
// Handle OPTIONS requests for CORS preflight
|
|
253
|
+
app.options('/*', (res, req) => {
|
|
254
|
+
res.onAborted(() => {
|
|
255
|
+
log('OPTIONS request aborted');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
res.cork(() => {
|
|
259
|
+
setCorsHeaders(res);
|
|
260
|
+
res.writeStatus(config.HTTP_STATUS.NO_CONTENT.toString()).end();
|
|
261
|
+
});
|
|
248
262
|
});
|
|
249
|
-
});
|
|
250
263
|
|
|
251
|
-
// Configure route
|
|
252
|
-
app.post('/api/v1/configure', (res, req) => {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
if (abortedRef.aborted) return;
|
|
261
|
-
try {
|
|
262
|
-
const result = await createConfigureRoute(queueManager)(body);
|
|
263
|
-
if (abortedRef.aborted) return;
|
|
264
|
-
eventManager.emit('queue.created', result);
|
|
265
|
-
res.cork(() => {
|
|
266
|
-
setCorsHeaders(res);
|
|
267
|
-
res.writeStatus(config.HTTP_STATUS.CREATED.toString()).end(JSON.stringify(result));
|
|
268
|
-
});
|
|
269
|
-
} catch (error) {
|
|
264
|
+
// Configure route
|
|
265
|
+
app.post('/api/v1/configure', (res, req) => {
|
|
266
|
+
const abortedRef = { aborted: false };
|
|
267
|
+
res.onAborted(() => {
|
|
268
|
+
abortedRef.aborted = true;
|
|
269
|
+
log('Configure request aborted');
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
readJson(res, async (body) => {
|
|
270
273
|
if (abortedRef.aborted) return;
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
274
|
+
try {
|
|
275
|
+
const result = await createConfigureRoute(queueManager)(body);
|
|
276
|
+
if (abortedRef.aborted) return;
|
|
277
|
+
eventManager.emit('queue.created', result);
|
|
278
|
+
res.cork(() => {
|
|
279
|
+
setCorsHeaders(res);
|
|
280
|
+
res.writeStatus(config.HTTP_STATUS.CREATED.toString()).end(JSON.stringify(result));
|
|
281
|
+
});
|
|
282
|
+
} catch (error) {
|
|
283
|
+
if (abortedRef.aborted) return;
|
|
284
|
+
log('Configure error:', error);
|
|
285
|
+
res.cork(() => {
|
|
286
|
+
setCorsHeaders(res);
|
|
287
|
+
res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}, abortedRef);
|
|
286
291
|
});
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
292
|
+
|
|
293
|
+
// Push route
|
|
294
|
+
app.post('/api/v1/push', (res, req) => {
|
|
295
|
+
const abortedRef = { aborted: false };
|
|
296
|
+
res.onAborted(() => {
|
|
297
|
+
abortedRef.aborted = true;
|
|
298
|
+
log('Push request aborted');
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
readJson(res, async (body) => {
|
|
294
302
|
if (abortedRef.aborted) return;
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
303
|
+
try {
|
|
304
|
+
const result = await createPushRoute(queueManager)(body);
|
|
305
|
+
messageCount += result.messages.length;
|
|
306
|
+
requestCount++;
|
|
307
|
+
if (abortedRef.aborted) return;
|
|
308
|
+
|
|
309
|
+
// Emit events for pushed messages
|
|
310
|
+
for (const msg of result.messages) {
|
|
311
|
+
if (msg.status === 'queued') {
|
|
312
|
+
const item = body.items.find(i =>
|
|
313
|
+
i.transactionId === msg.transactionId || !i.transactionId
|
|
314
|
+
);
|
|
315
|
+
if (item) {
|
|
316
|
+
const partition = item.partition ?? 'Default';
|
|
317
|
+
const queuePath = `${item.queue}/${partition}`;
|
|
318
|
+
eventManager.emit('message.pushed', {
|
|
319
|
+
queue: item.queue,
|
|
320
|
+
partition: partition,
|
|
321
|
+
transactionId: msg.transactionId
|
|
322
|
+
});
|
|
323
|
+
eventManager.notifyMessageAvailable(queuePath);
|
|
324
|
+
}
|
|
311
325
|
}
|
|
312
326
|
}
|
|
327
|
+
|
|
328
|
+
res.cork(() => {
|
|
329
|
+
setCorsHeaders(res);
|
|
330
|
+
res.writeStatus(config.HTTP_STATUS.CREATED.toString()).end(JSON.stringify(result));
|
|
331
|
+
});
|
|
332
|
+
} catch (error) {
|
|
333
|
+
if (abortedRef.aborted) return;
|
|
334
|
+
log('Push error:', error.message || error);
|
|
335
|
+
res.cork(() => {
|
|
336
|
+
setCorsHeaders(res);
|
|
337
|
+
res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
|
|
338
|
+
});
|
|
313
339
|
}
|
|
314
|
-
|
|
315
|
-
res.cork(() => {
|
|
316
|
-
setCorsHeaders(res);
|
|
317
|
-
res.writeStatus(config.HTTP_STATUS.CREATED.toString()).end(JSON.stringify(result));
|
|
318
|
-
});
|
|
319
|
-
} catch (error) {
|
|
320
|
-
if (abortedRef.aborted) return;
|
|
321
|
-
log('Push error:', error.message || error);
|
|
322
|
-
res.cork(() => {
|
|
323
|
-
setCorsHeaders(res);
|
|
324
|
-
res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
}, abortedRef);
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
// Pop routes with new structure
|
|
331
|
-
app.get('/api/v1/pop/queue/:queue/partition/:partition', (res, req) => {
|
|
332
|
-
const queue = req.getParameter(0);
|
|
333
|
-
const partition = req.getParameter(1);
|
|
334
|
-
const query = new URLSearchParams(req.getQuery());
|
|
335
|
-
|
|
336
|
-
let aborted = false;
|
|
337
|
-
res.onAborted(() => {
|
|
338
|
-
aborted = true;
|
|
339
|
-
log('Pop request aborted');
|
|
340
|
+
}, abortedRef);
|
|
340
341
|
});
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
},
|
|
348
|
-
{
|
|
349
|
-
wait: query.get('wait') === 'true',
|
|
350
|
-
timeout: parseInt(query.get('timeout') || config.QUEUE.DEFAULT_TIMEOUT.toString()),
|
|
351
|
-
batch: parseInt(query.get('batch') || config.QUEUE.DEFAULT_BATCH_SIZE.toString()),
|
|
352
|
-
subscriptionMode: query.get('subscriptionMode'),
|
|
353
|
-
subscriptionFrom: query.get('subscriptionFrom')
|
|
354
|
-
}
|
|
355
|
-
).then(result => {
|
|
356
|
-
if (aborted) return;
|
|
342
|
+
|
|
343
|
+
// Pop routes with new structure
|
|
344
|
+
app.get('/api/v1/pop/queue/:queue/partition/:partition', (res, req) => {
|
|
345
|
+
const queue = req.getParameter(0);
|
|
346
|
+
const partition = req.getParameter(1);
|
|
347
|
+
const query = new URLSearchParams(req.getQuery());
|
|
357
348
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
transactionId: msg.transactionId,
|
|
364
|
-
workerId: `worker-${process.pid}`
|
|
365
|
-
});
|
|
366
|
-
}
|
|
349
|
+
let aborted = false;
|
|
350
|
+
res.onAborted(() => {
|
|
351
|
+
aborted = true;
|
|
352
|
+
log('Pop request aborted');
|
|
353
|
+
});
|
|
367
354
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
}
|
|
374
|
-
|
|
355
|
+
createPopRoute(queueManager, eventManager)(
|
|
356
|
+
{
|
|
357
|
+
queue,
|
|
358
|
+
partition,
|
|
359
|
+
consumerGroup: query.get('consumerGroup') || query.get('consumer_group')
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
wait: query.get('wait') === 'true',
|
|
363
|
+
timeout: parseInt(query.get('timeout') || config.QUEUE.DEFAULT_TIMEOUT.toString()),
|
|
364
|
+
batch: parseInt(query.get('batch') || config.QUEUE.DEFAULT_BATCH_SIZE.toString()),
|
|
365
|
+
subscriptionMode: query.get('subscriptionMode'),
|
|
366
|
+
subscriptionFrom: query.get('subscriptionFrom')
|
|
367
|
+
}
|
|
368
|
+
).then(result => {
|
|
369
|
+
if (aborted) return;
|
|
370
|
+
|
|
371
|
+
// Emit events for processing messages
|
|
372
|
+
for (const msg of result.messages) {
|
|
373
|
+
eventManager.emit('message.processing', {
|
|
374
|
+
queue: msg.queue,
|
|
375
|
+
partition: msg.partition,
|
|
376
|
+
transactionId: msg.transactionId,
|
|
377
|
+
workerId: `worker-${process.pid}`
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Use streaming with backpressure for large responses
|
|
382
|
+
if (result.messages.length === 0) {
|
|
383
|
+
res.cork(() => {
|
|
384
|
+
setCorsHeaders(res);
|
|
385
|
+
res.writeStatus(config.HTTP_STATUS.NO_CONTENT.toString()).end();
|
|
386
|
+
});
|
|
387
|
+
} else {
|
|
388
|
+
res.cork(() => {
|
|
389
|
+
setCorsHeaders(res);
|
|
390
|
+
res.writeStatus(config.HTTP_STATUS.OK.toString());
|
|
391
|
+
});
|
|
392
|
+
streamJSON(res, result, { aborted });
|
|
393
|
+
}
|
|
394
|
+
}).catch(error => {
|
|
395
|
+
if (aborted) return;
|
|
396
|
+
log('Pop error:', error);
|
|
375
397
|
res.cork(() => {
|
|
376
398
|
setCorsHeaders(res);
|
|
377
|
-
res.writeStatus(config.HTTP_STATUS.OK.toString());
|
|
378
399
|
});
|
|
379
|
-
|
|
380
|
-
}
|
|
381
|
-
}).catch(error => {
|
|
382
|
-
if (aborted) return;
|
|
383
|
-
log('Pop error:', error);
|
|
384
|
-
res.cork(() => {
|
|
385
|
-
setCorsHeaders(res);
|
|
400
|
+
sendError(res, error, config.HTTP_STATUS.INTERNAL_SERVER_ERROR);
|
|
386
401
|
});
|
|
387
|
-
sendError(res, error, config.HTTP_STATUS.INTERNAL_SERVER_ERROR);
|
|
388
402
|
});
|
|
389
|
-
});
|
|
390
403
|
|
|
391
|
-
app.get('/api/v1/pop/queue/:queue', (res, req) => {
|
|
392
|
-
|
|
393
|
-
|
|
404
|
+
app.get('/api/v1/pop/queue/:queue', (res, req) => {
|
|
405
|
+
const queue = req.getParameter(0);
|
|
406
|
+
const query = new URLSearchParams(req.getQuery());
|
|
394
407
|
|
|
395
|
-
|
|
396
|
-
|
|
408
|
+
let aborted = false;
|
|
409
|
+
res.onAborted(() => {
|
|
397
410
|
aborted = true;
|
|
398
411
|
log('Pop request aborted');
|
|
399
412
|
});
|
|
400
413
|
|
|
401
|
-
|
|
414
|
+
createPopRoute(queueManager, eventManager)(
|
|
402
415
|
{
|
|
403
416
|
queue,
|
|
404
417
|
consumerGroup: query.get('consumerGroup') || query.get('consumer_group')
|
|
@@ -446,18 +459,18 @@ app.get('/api/v1/pop/queue/:queue', (res, req) => {
|
|
|
446
459
|
});
|
|
447
460
|
|
|
448
461
|
// Pop with filters (namespace or task)
|
|
449
|
-
app.get('/api/v1/pop', (res, req) => {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
462
|
+
app.get('/api/v1/pop', (res, req) => {
|
|
463
|
+
const query = new URLSearchParams(req.getQuery());
|
|
464
|
+
const namespace = query.get('namespace');
|
|
465
|
+
const task = query.get('task');
|
|
453
466
|
|
|
454
|
-
|
|
455
|
-
|
|
467
|
+
let aborted = false;
|
|
468
|
+
res.onAborted(() => {
|
|
456
469
|
aborted = true;
|
|
457
470
|
log('Pop request aborted');
|
|
458
471
|
});
|
|
459
472
|
|
|
460
|
-
|
|
473
|
+
createPopRoute(queueManager, eventManager)(
|
|
461
474
|
{
|
|
462
475
|
namespace,
|
|
463
476
|
task,
|
|
@@ -506,14 +519,14 @@ app.get('/api/v1/pop', (res, req) => {
|
|
|
506
519
|
});
|
|
507
520
|
|
|
508
521
|
// ACK route
|
|
509
|
-
app.post('/api/v1/ack', (res, req) => {
|
|
510
|
-
|
|
511
|
-
|
|
522
|
+
app.post('/api/v1/ack', (res, req) => {
|
|
523
|
+
const abortedRef = { aborted: false };
|
|
524
|
+
res.onAborted(() => {
|
|
512
525
|
abortedRef.aborted = true;
|
|
513
526
|
log('ACK request aborted');
|
|
514
527
|
});
|
|
515
528
|
|
|
516
|
-
|
|
529
|
+
readJson(res, async (body) => {
|
|
517
530
|
if (abortedRef.aborted) return;
|
|
518
531
|
try {
|
|
519
532
|
const result = await createAckRoute(queueManager)(body);
|
|
@@ -547,14 +560,14 @@ app.post('/api/v1/ack', (res, req) => {
|
|
|
547
560
|
});
|
|
548
561
|
|
|
549
562
|
// Batch ACK route
|
|
550
|
-
app.post('/api/v1/ack/batch', (res, req) => {
|
|
551
|
-
|
|
552
|
-
|
|
563
|
+
app.post('/api/v1/ack/batch', (res, req) => {
|
|
564
|
+
const abortedRef = { aborted: false };
|
|
565
|
+
res.onAborted(() => {
|
|
553
566
|
abortedRef.aborted = true;
|
|
554
567
|
log('Batch ACK request aborted');
|
|
555
568
|
});
|
|
556
569
|
|
|
557
|
-
|
|
570
|
+
readJson(res, async (body) => {
|
|
558
571
|
if (abortedRef.aborted) return;
|
|
559
572
|
try {
|
|
560
573
|
const results = await queueManager.acknowledgeMessages(body.acknowledgments, body.consumerGroup);
|
|
@@ -593,16 +606,16 @@ app.post('/api/v1/ack/batch', (res, req) => {
|
|
|
593
606
|
});
|
|
594
607
|
|
|
595
608
|
// Messages routes
|
|
596
|
-
app.get('/api/v1/messages', (res, req) => {
|
|
597
|
-
|
|
609
|
+
app.get('/api/v1/messages', (res, req) => {
|
|
610
|
+
const query = new URLSearchParams(req.getQuery());
|
|
598
611
|
|
|
599
|
-
|
|
600
|
-
|
|
612
|
+
let aborted = false;
|
|
613
|
+
res.onAborted(() => {
|
|
601
614
|
aborted = true;
|
|
602
615
|
log('Messages list request aborted');
|
|
603
616
|
});
|
|
604
617
|
|
|
605
|
-
|
|
618
|
+
const filters = {
|
|
606
619
|
ns: query.get('ns'),
|
|
607
620
|
task: query.get('task'),
|
|
608
621
|
queue: query.get('queue'),
|
|
@@ -611,7 +624,7 @@ app.get('/api/v1/messages', (res, req) => {
|
|
|
611
624
|
offset: parseInt(query.get('offset') || config.API.DEFAULT_OFFSET.toString())
|
|
612
625
|
};
|
|
613
626
|
|
|
614
|
-
|
|
627
|
+
messagesRoutes.listMessages(filters).then(result => {
|
|
615
628
|
if (aborted) return;
|
|
616
629
|
res.cork(() => {
|
|
617
630
|
setCorsHeaders(res);
|
|
@@ -627,16 +640,16 @@ app.get('/api/v1/messages', (res, req) => {
|
|
|
627
640
|
});
|
|
628
641
|
});
|
|
629
642
|
|
|
630
|
-
app.get('/api/v1/messages/:transactionId', (res, req) => {
|
|
631
|
-
|
|
643
|
+
app.get('/api/v1/messages/:transactionId', (res, req) => {
|
|
644
|
+
const transactionId = req.getParameter(0);
|
|
632
645
|
|
|
633
|
-
|
|
634
|
-
|
|
646
|
+
let aborted = false;
|
|
647
|
+
res.onAborted(() => {
|
|
635
648
|
aborted = true;
|
|
636
649
|
log('Get message request aborted');
|
|
637
650
|
});
|
|
638
651
|
|
|
639
|
-
|
|
652
|
+
messagesRoutes.getMessage(transactionId).then(result => {
|
|
640
653
|
if (aborted) return;
|
|
641
654
|
res.cork(() => {
|
|
642
655
|
setCorsHeaders(res);
|
|
@@ -653,16 +666,16 @@ app.get('/api/v1/messages/:transactionId', (res, req) => {
|
|
|
653
666
|
});
|
|
654
667
|
});
|
|
655
668
|
|
|
656
|
-
app.del('/api/v1/messages/:transactionId', (res, req) => {
|
|
657
|
-
|
|
669
|
+
app.del('/api/v1/messages/:transactionId', (res, req) => {
|
|
670
|
+
const transactionId = req.getParameter(0);
|
|
658
671
|
|
|
659
|
-
|
|
660
|
-
|
|
672
|
+
let aborted = false;
|
|
673
|
+
res.onAborted(() => {
|
|
661
674
|
aborted = true;
|
|
662
675
|
log('Delete message request aborted');
|
|
663
676
|
});
|
|
664
677
|
|
|
665
|
-
|
|
678
|
+
messagesRoutes.deleteMessage(transactionId).then(result => {
|
|
666
679
|
if (aborted) return;
|
|
667
680
|
res.cork(() => {
|
|
668
681
|
setCorsHeaders(res);
|
|
@@ -679,16 +692,16 @@ app.del('/api/v1/messages/:transactionId', (res, req) => {
|
|
|
679
692
|
});
|
|
680
693
|
});
|
|
681
694
|
|
|
682
|
-
app.post('/api/v1/messages/:transactionId/retry', (res, req) => {
|
|
683
|
-
|
|
695
|
+
app.post('/api/v1/messages/:transactionId/retry', (res, req) => {
|
|
696
|
+
const transactionId = req.getParameter(0);
|
|
684
697
|
|
|
685
|
-
|
|
686
|
-
|
|
698
|
+
let aborted = false;
|
|
699
|
+
res.onAborted(() => {
|
|
687
700
|
aborted = true;
|
|
688
701
|
log('Retry message request aborted');
|
|
689
702
|
});
|
|
690
703
|
|
|
691
|
-
|
|
704
|
+
messagesRoutes.retryMessage(transactionId).then(result => {
|
|
692
705
|
if (aborted) return;
|
|
693
706
|
res.cork(() => {
|
|
694
707
|
setCorsHeaders(res);
|
|
@@ -704,16 +717,16 @@ app.post('/api/v1/messages/:transactionId/retry', (res, req) => {
|
|
|
704
717
|
});
|
|
705
718
|
});
|
|
706
719
|
|
|
707
|
-
app.post('/api/v1/messages/:transactionId/dlq', (res, req) => {
|
|
708
|
-
|
|
720
|
+
app.post('/api/v1/messages/:transactionId/dlq', (res, req) => {
|
|
721
|
+
const transactionId = req.getParameter(0);
|
|
709
722
|
|
|
710
|
-
|
|
711
|
-
|
|
723
|
+
let aborted = false;
|
|
724
|
+
res.onAborted(() => {
|
|
712
725
|
aborted = true;
|
|
713
726
|
log('Move to DLQ request aborted');
|
|
714
727
|
});
|
|
715
728
|
|
|
716
|
-
|
|
729
|
+
messagesRoutes.moveToDLQ(transactionId).then(result => {
|
|
717
730
|
if (aborted) return;
|
|
718
731
|
res.cork(() => {
|
|
719
732
|
setCorsHeaders(res);
|
|
@@ -729,16 +742,16 @@ app.post('/api/v1/messages/:transactionId/dlq', (res, req) => {
|
|
|
729
742
|
});
|
|
730
743
|
});
|
|
731
744
|
|
|
732
|
-
app.get('/api/v1/messages/:transactionId/related', (res, req) => {
|
|
733
|
-
|
|
745
|
+
app.get('/api/v1/messages/:transactionId/related', (res, req) => {
|
|
746
|
+
const transactionId = req.getParameter(0);
|
|
734
747
|
|
|
735
|
-
|
|
736
|
-
|
|
748
|
+
let aborted = false;
|
|
749
|
+
res.onAborted(() => {
|
|
737
750
|
aborted = true;
|
|
738
751
|
log('Get related messages request aborted');
|
|
739
752
|
});
|
|
740
753
|
|
|
741
|
-
|
|
754
|
+
messagesRoutes.getRelatedMessages(transactionId).then(result => {
|
|
742
755
|
if (aborted) return;
|
|
743
756
|
res.cork(() => {
|
|
744
757
|
setCorsHeaders(res);
|
|
@@ -754,18 +767,18 @@ app.get('/api/v1/messages/:transactionId/related', (res, req) => {
|
|
|
754
767
|
});
|
|
755
768
|
});
|
|
756
769
|
|
|
757
|
-
app.del('/api/v1/queues/:queue/clear', (res, req) => {
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
770
|
+
app.del('/api/v1/queues/:queue/clear', (res, req) => {
|
|
771
|
+
const queue = req.getParameter(0);
|
|
772
|
+
const query = new URLSearchParams(req.getQuery());
|
|
773
|
+
const partition = query.get('partition');
|
|
761
774
|
|
|
762
|
-
|
|
763
|
-
|
|
775
|
+
let aborted = false;
|
|
776
|
+
res.onAborted(() => {
|
|
764
777
|
aborted = true;
|
|
765
778
|
log('Clear queue request aborted');
|
|
766
779
|
});
|
|
767
780
|
|
|
768
|
-
|
|
781
|
+
messagesRoutes.clearQueue(queue, partition).then(result => {
|
|
769
782
|
if (aborted) return;
|
|
770
783
|
res.cork(() => {
|
|
771
784
|
setCorsHeaders(res);
|
|
@@ -782,18 +795,18 @@ app.del('/api/v1/queues/:queue/clear', (res, req) => {
|
|
|
782
795
|
});
|
|
783
796
|
|
|
784
797
|
// Resources routes
|
|
785
|
-
app.get('/api/v1/resources/queues', (res, req) => {
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
798
|
+
app.get('/api/v1/resources/queues', (res, req) => {
|
|
799
|
+
const query = new URLSearchParams(req.getQuery());
|
|
800
|
+
const namespace = query.get('namespace');
|
|
801
|
+
const task = query.get('task');
|
|
789
802
|
|
|
790
|
-
|
|
791
|
-
|
|
803
|
+
let aborted = false;
|
|
804
|
+
res.onAborted(() => {
|
|
792
805
|
aborted = true;
|
|
793
806
|
log('Get queues request aborted');
|
|
794
807
|
});
|
|
795
808
|
|
|
796
|
-
|
|
809
|
+
resourcesRoutes.getQueues({ namespace, task }).then(result => {
|
|
797
810
|
if (aborted) return;
|
|
798
811
|
res.cork(() => {
|
|
799
812
|
setCorsHeaders(res);
|
|
@@ -809,16 +822,16 @@ app.get('/api/v1/resources/queues', (res, req) => {
|
|
|
809
822
|
});
|
|
810
823
|
});
|
|
811
824
|
|
|
812
|
-
app.get('/api/v1/resources/queues/:queue', (res, req) => {
|
|
813
|
-
|
|
825
|
+
app.get('/api/v1/resources/queues/:queue', (res, req) => {
|
|
826
|
+
const queue = req.getParameter(0);
|
|
814
827
|
|
|
815
|
-
|
|
816
|
-
|
|
828
|
+
let aborted = false;
|
|
829
|
+
res.onAborted(() => {
|
|
817
830
|
aborted = true;
|
|
818
831
|
log('Get queue request aborted');
|
|
819
832
|
});
|
|
820
833
|
|
|
821
|
-
|
|
834
|
+
resourcesRoutes.getQueue(queue).then(result => {
|
|
822
835
|
if (aborted) return;
|
|
823
836
|
res.cork(() => {
|
|
824
837
|
setCorsHeaders(res);
|
|
@@ -835,18 +848,18 @@ app.get('/api/v1/resources/queues/:queue', (res, req) => {
|
|
|
835
848
|
});
|
|
836
849
|
});
|
|
837
850
|
|
|
838
|
-
app.get('/api/v1/resources/partitions', (res, req) => {
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
851
|
+
app.get('/api/v1/resources/partitions', (res, req) => {
|
|
852
|
+
const query = new URLSearchParams(req.getQuery());
|
|
853
|
+
const queue = query.get('queue');
|
|
854
|
+
const minDepth = query.get('minDepth');
|
|
842
855
|
|
|
843
|
-
|
|
844
|
-
|
|
856
|
+
let aborted = false;
|
|
857
|
+
res.onAborted(() => {
|
|
845
858
|
aborted = true;
|
|
846
859
|
log('Get partitions request aborted');
|
|
847
860
|
});
|
|
848
861
|
|
|
849
|
-
|
|
862
|
+
resourcesRoutes.getPartitions({ queue, minDepth: minDepth ? parseInt(minDepth) : null }).then(result => {
|
|
850
863
|
if (aborted) return;
|
|
851
864
|
res.cork(() => {
|
|
852
865
|
setCorsHeaders(res);
|
|
@@ -862,14 +875,14 @@ app.get('/api/v1/resources/partitions', (res, req) => {
|
|
|
862
875
|
});
|
|
863
876
|
});
|
|
864
877
|
|
|
865
|
-
app.get('/api/v1/resources/namespaces', (res, req) => {
|
|
866
|
-
|
|
867
|
-
|
|
878
|
+
app.get('/api/v1/resources/namespaces', (res, req) => {
|
|
879
|
+
let aborted = false;
|
|
880
|
+
res.onAborted(() => {
|
|
868
881
|
aborted = true;
|
|
869
882
|
log('Get namespaces request aborted');
|
|
870
883
|
});
|
|
871
884
|
|
|
872
|
-
|
|
885
|
+
resourcesRoutes.getNamespaces().then(result => {
|
|
873
886
|
if (aborted) return;
|
|
874
887
|
res.cork(() => {
|
|
875
888
|
setCorsHeaders(res);
|
|
@@ -885,14 +898,14 @@ app.get('/api/v1/resources/namespaces', (res, req) => {
|
|
|
885
898
|
});
|
|
886
899
|
});
|
|
887
900
|
|
|
888
|
-
app.get('/api/v1/resources/tasks', (res, req) => {
|
|
889
|
-
|
|
890
|
-
|
|
901
|
+
app.get('/api/v1/resources/tasks', (res, req) => {
|
|
902
|
+
let aborted = false;
|
|
903
|
+
res.onAborted(() => {
|
|
891
904
|
aborted = true;
|
|
892
905
|
log('Get tasks request aborted');
|
|
893
906
|
});
|
|
894
907
|
|
|
895
|
-
|
|
908
|
+
resourcesRoutes.getTasks().then(result => {
|
|
896
909
|
if (aborted) return;
|
|
897
910
|
res.cork(() => {
|
|
898
911
|
setCorsHeaders(res);
|
|
@@ -908,14 +921,14 @@ app.get('/api/v1/resources/tasks', (res, req) => {
|
|
|
908
921
|
});
|
|
909
922
|
});
|
|
910
923
|
|
|
911
|
-
app.get('/api/v1/resources/overview', (res, req) => {
|
|
912
|
-
|
|
913
|
-
|
|
924
|
+
app.get('/api/v1/resources/overview', (res, req) => {
|
|
925
|
+
let aborted = false;
|
|
926
|
+
res.onAborted(() => {
|
|
914
927
|
aborted = true;
|
|
915
928
|
log('Get overview request aborted');
|
|
916
929
|
});
|
|
917
930
|
|
|
918
|
-
|
|
931
|
+
resourcesRoutes.getSystemOverview().then(result => {
|
|
919
932
|
if (aborted) return;
|
|
920
933
|
res.cork(() => {
|
|
921
934
|
setCorsHeaders(res);
|
|
@@ -932,16 +945,16 @@ app.get('/api/v1/resources/overview', (res, req) => {
|
|
|
932
945
|
});
|
|
933
946
|
|
|
934
947
|
// Delete queue
|
|
935
|
-
app.del('/api/v1/resources/queues/:queue', (res, req) => {
|
|
936
|
-
|
|
948
|
+
app.del('/api/v1/resources/queues/:queue', (res, req) => {
|
|
949
|
+
const queue = req.getParameter(0);
|
|
937
950
|
|
|
938
|
-
|
|
939
|
-
|
|
951
|
+
let aborted = false;
|
|
952
|
+
res.onAborted(() => {
|
|
940
953
|
aborted = true;
|
|
941
954
|
log('Delete queue request aborted');
|
|
942
955
|
});
|
|
943
956
|
|
|
944
|
-
|
|
957
|
+
resourcesRoutes.deleteQueue(queue).then(result => {
|
|
945
958
|
if (aborted) return;
|
|
946
959
|
res.cork(() => {
|
|
947
960
|
setCorsHeaders(res);
|
|
@@ -962,16 +975,16 @@ app.del('/api/v1/resources/queues/:queue', (res, req) => {
|
|
|
962
975
|
// ============================================================================
|
|
963
976
|
|
|
964
977
|
// 1. Dashboard Status - GET /api/v1/status
|
|
965
|
-
app.get('/api/v1/status', (res, req) => {
|
|
966
|
-
|
|
978
|
+
app.get('/api/v1/status', (res, req) => {
|
|
979
|
+
const query = new URLSearchParams(req.getQuery());
|
|
967
980
|
|
|
968
|
-
|
|
969
|
-
|
|
981
|
+
let aborted = false;
|
|
982
|
+
res.onAborted(() => {
|
|
970
983
|
aborted = true;
|
|
971
984
|
log('Status request aborted');
|
|
972
985
|
});
|
|
973
986
|
|
|
974
|
-
|
|
987
|
+
const filters = {
|
|
975
988
|
from: query.get('from'),
|
|
976
989
|
to: query.get('to'),
|
|
977
990
|
queue: query.get('queue'),
|
|
@@ -979,7 +992,7 @@ app.get('/api/v1/status', (res, req) => {
|
|
|
979
992
|
task: query.get('task')
|
|
980
993
|
};
|
|
981
994
|
|
|
982
|
-
|
|
995
|
+
statusRoutes.getStatus(filters).then(result => {
|
|
983
996
|
if (aborted) return;
|
|
984
997
|
res.cork(() => {
|
|
985
998
|
setCorsHeaders(res);
|
|
@@ -996,16 +1009,16 @@ app.get('/api/v1/status', (res, req) => {
|
|
|
996
1009
|
});
|
|
997
1010
|
|
|
998
1011
|
// 2. Queues List - GET /api/v1/status/queues
|
|
999
|
-
app.get('/api/v1/status/queues', (res, req) => {
|
|
1000
|
-
|
|
1012
|
+
app.get('/api/v1/status/queues', (res, req) => {
|
|
1013
|
+
const query = new URLSearchParams(req.getQuery());
|
|
1001
1014
|
|
|
1002
|
-
|
|
1003
|
-
|
|
1015
|
+
let aborted = false;
|
|
1016
|
+
res.onAborted(() => {
|
|
1004
1017
|
aborted = true;
|
|
1005
1018
|
log('Status queues request aborted');
|
|
1006
1019
|
});
|
|
1007
1020
|
|
|
1008
|
-
|
|
1021
|
+
const filters = {
|
|
1009
1022
|
from: query.get('from'),
|
|
1010
1023
|
to: query.get('to'),
|
|
1011
1024
|
namespace: query.get('namespace'),
|
|
@@ -1014,7 +1027,7 @@ app.get('/api/v1/status/queues', (res, req) => {
|
|
|
1014
1027
|
offset: query.get('offset')
|
|
1015
1028
|
};
|
|
1016
1029
|
|
|
1017
|
-
|
|
1030
|
+
statusRoutes.getQueues(filters).then(result => {
|
|
1018
1031
|
if (aborted) return;
|
|
1019
1032
|
res.cork(() => {
|
|
1020
1033
|
setCorsHeaders(res);
|
|
@@ -1031,22 +1044,22 @@ app.get('/api/v1/status/queues', (res, req) => {
|
|
|
1031
1044
|
});
|
|
1032
1045
|
|
|
1033
1046
|
// 3. Queue Detail - GET /api/v1/status/queues/:queueName
|
|
1034
|
-
app.get('/api/v1/status/queues/:queue', (res, req) => {
|
|
1035
|
-
|
|
1036
|
-
|
|
1047
|
+
app.get('/api/v1/status/queues/:queue', (res, req) => {
|
|
1048
|
+
const queueName = req.getParameter(0);
|
|
1049
|
+
const query = new URLSearchParams(req.getQuery());
|
|
1037
1050
|
|
|
1038
|
-
|
|
1039
|
-
|
|
1051
|
+
let aborted = false;
|
|
1052
|
+
res.onAborted(() => {
|
|
1040
1053
|
aborted = true;
|
|
1041
1054
|
log('Status queue detail request aborted');
|
|
1042
1055
|
});
|
|
1043
1056
|
|
|
1044
|
-
|
|
1057
|
+
const filters = {
|
|
1045
1058
|
from: query.get('from'),
|
|
1046
1059
|
to: query.get('to')
|
|
1047
1060
|
};
|
|
1048
1061
|
|
|
1049
|
-
|
|
1062
|
+
statusRoutes.getQueueDetail(queueName, filters).then(result => {
|
|
1050
1063
|
if (aborted) return;
|
|
1051
1064
|
res.cork(() => {
|
|
1052
1065
|
setCorsHeaders(res);
|
|
@@ -1066,17 +1079,17 @@ app.get('/api/v1/status/queues/:queue', (res, req) => {
|
|
|
1066
1079
|
});
|
|
1067
1080
|
|
|
1068
1081
|
// 4. Queue Messages - GET /api/v1/status/queues/:queueName/messages
|
|
1069
|
-
app.get('/api/v1/status/queues/:queue/messages', (res, req) => {
|
|
1070
|
-
|
|
1071
|
-
|
|
1082
|
+
app.get('/api/v1/status/queues/:queue/messages', (res, req) => {
|
|
1083
|
+
const queueName = req.getParameter(0);
|
|
1084
|
+
const query = new URLSearchParams(req.getQuery());
|
|
1072
1085
|
|
|
1073
|
-
|
|
1074
|
-
|
|
1086
|
+
let aborted = false;
|
|
1087
|
+
res.onAborted(() => {
|
|
1075
1088
|
aborted = true;
|
|
1076
1089
|
log('Status queue messages request aborted');
|
|
1077
1090
|
});
|
|
1078
1091
|
|
|
1079
|
-
|
|
1092
|
+
const filters = {
|
|
1080
1093
|
status: query.get('status'),
|
|
1081
1094
|
partition: query.get('partition'),
|
|
1082
1095
|
from: query.get('from'),
|
|
@@ -1085,7 +1098,7 @@ app.get('/api/v1/status/queues/:queue/messages', (res, req) => {
|
|
|
1085
1098
|
offset: query.get('offset')
|
|
1086
1099
|
};
|
|
1087
1100
|
|
|
1088
|
-
|
|
1101
|
+
statusRoutes.getQueueMessages(queueName, filters).then(result => {
|
|
1089
1102
|
if (aborted) return;
|
|
1090
1103
|
res.cork(() => {
|
|
1091
1104
|
setCorsHeaders(res);
|
|
@@ -1102,16 +1115,16 @@ app.get('/api/v1/status/queues/:queue/messages', (res, req) => {
|
|
|
1102
1115
|
});
|
|
1103
1116
|
|
|
1104
1117
|
// 5. Analytics - GET /api/v1/status/analytics
|
|
1105
|
-
app.get('/api/v1/status/analytics', (res, req) => {
|
|
1106
|
-
|
|
1118
|
+
app.get('/api/v1/status/analytics', (res, req) => {
|
|
1119
|
+
const query = new URLSearchParams(req.getQuery());
|
|
1107
1120
|
|
|
1108
|
-
|
|
1109
|
-
|
|
1121
|
+
let aborted = false;
|
|
1122
|
+
res.onAborted(() => {
|
|
1110
1123
|
aborted = true;
|
|
1111
1124
|
log('Status analytics request aborted');
|
|
1112
1125
|
});
|
|
1113
1126
|
|
|
1114
|
-
|
|
1127
|
+
const filters = {
|
|
1115
1128
|
from: query.get('from'),
|
|
1116
1129
|
to: query.get('to'),
|
|
1117
1130
|
queue: query.get('queue'),
|
|
@@ -1120,7 +1133,7 @@ app.get('/api/v1/status/analytics', (res, req) => {
|
|
|
1120
1133
|
interval: query.get('interval') || 'hour'
|
|
1121
1134
|
};
|
|
1122
1135
|
|
|
1123
|
-
|
|
1136
|
+
statusRoutes.getAnalytics(filters).then(result => {
|
|
1124
1137
|
if (aborted) return;
|
|
1125
1138
|
res.cork(() => {
|
|
1126
1139
|
setCorsHeaders(res);
|
|
@@ -1137,14 +1150,14 @@ app.get('/api/v1/status/analytics', (res, req) => {
|
|
|
1137
1150
|
});
|
|
1138
1151
|
|
|
1139
1152
|
// Health check with performance stats
|
|
1140
|
-
app.get('/health', async (res, req) => {
|
|
1141
|
-
|
|
1142
|
-
|
|
1153
|
+
app.get('/health', async (res, req) => {
|
|
1154
|
+
let aborted = false;
|
|
1155
|
+
res.onAborted(() => {
|
|
1143
1156
|
aborted = true;
|
|
1144
1157
|
log('Health check aborted');
|
|
1145
1158
|
});
|
|
1146
1159
|
|
|
1147
|
-
|
|
1160
|
+
try {
|
|
1148
1161
|
await pool.query('SELECT 1');
|
|
1149
1162
|
if (aborted) return;
|
|
1150
1163
|
|
|
@@ -1181,13 +1194,13 @@ app.get('/health', async (res, req) => {
|
|
|
1181
1194
|
});
|
|
1182
1195
|
|
|
1183
1196
|
// Metrics endpoint for performance monitoring
|
|
1184
|
-
app.get('/metrics', (res, req) => {
|
|
1185
|
-
|
|
1197
|
+
app.get('/metrics', (res, req) => {
|
|
1198
|
+
res.onAborted(() => {
|
|
1186
1199
|
log('Metrics request aborted');
|
|
1187
1200
|
});
|
|
1188
1201
|
|
|
1189
|
-
|
|
1190
|
-
|
|
1202
|
+
const uptime = (Date.now() - startTime) / 1000;
|
|
1203
|
+
const metrics = {
|
|
1191
1204
|
uptime: uptime,
|
|
1192
1205
|
requests: {
|
|
1193
1206
|
total: requestCount,
|
|
@@ -1206,7 +1219,7 @@ app.get('/metrics', (res, req) => {
|
|
|
1206
1219
|
cpu: process.cpuUsage()
|
|
1207
1220
|
};
|
|
1208
1221
|
|
|
1209
|
-
|
|
1222
|
+
res.cork(() => {
|
|
1210
1223
|
setCorsHeaders(res);
|
|
1211
1224
|
res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify(metrics));
|
|
1212
1225
|
});
|
|
@@ -1229,7 +1242,7 @@ app.listen(HOST, PORT, async (token) => {
|
|
|
1229
1242
|
|
|
1230
1243
|
// Start system event synchronization and consumption (only if enabled)
|
|
1231
1244
|
if (config.SYSTEM_EVENTS.ENABLED) {
|
|
1232
|
-
try {
|
|
1245
|
+
/*try { // TODO: Uncomment this when we have a way to synchronize system events
|
|
1233
1246
|
// Create temporary client for synchronization
|
|
1234
1247
|
const syncClient = createQueenClient({
|
|
1235
1248
|
baseUrl: `http://localhost:${PORT}`
|
|
@@ -1244,7 +1257,7 @@ app.listen(HOST, PORT, async (token) => {
|
|
|
1244
1257
|
queue: SYSTEM_QUEUE,
|
|
1245
1258
|
consumerGroup: SERVER_INSTANCE_ID,
|
|
1246
1259
|
handler: async (message) => {
|
|
1247
|
-
await systemEventManager.processSystemEvent(message.
|
|
1260
|
+
await systemEventManager.processSystemEvent(message.data);
|
|
1248
1261
|
},
|
|
1249
1262
|
options: {
|
|
1250
1263
|
batch: 10,
|
|
@@ -1255,7 +1268,7 @@ app.listen(HOST, PORT, async (token) => {
|
|
|
1255
1268
|
log(`✅ System event consumer started`);
|
|
1256
1269
|
} catch (error) {
|
|
1257
1270
|
log(`⚠️ Failed to start system event consumer: ${error.message}`);
|
|
1258
|
-
}
|
|
1271
|
+
}*/
|
|
1259
1272
|
}
|
|
1260
1273
|
|
|
1261
1274
|
log(`🎯 Ready to process messages with high performance!`);
|
|
@@ -1265,58 +1278,75 @@ app.listen(HOST, PORT, async (token) => {
|
|
|
1265
1278
|
}
|
|
1266
1279
|
});
|
|
1267
1280
|
|
|
1268
|
-
// Graceful shutdown
|
|
1269
|
-
const shutdown = async (signal) => {
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
};
|
|
1281
|
+
// Graceful shutdown
|
|
1282
|
+
const shutdown = async (signal) => {
|
|
1283
|
+
log(`\n🛑 Received ${signal}, shutting down gracefully...`);
|
|
1284
|
+
|
|
1285
|
+
// Set shutdown flag immediately to prevent new operations
|
|
1286
|
+
isShuttingDown = true;
|
|
1287
|
+
|
|
1288
|
+
// Stop system event consumer
|
|
1289
|
+
if (stopSystemEventConsumer) {
|
|
1290
|
+
stopSystemEventConsumer();
|
|
1291
|
+
log('✅ System event consumer stopped');
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
// Stop background jobs
|
|
1295
|
+
if (stopRetention) {
|
|
1296
|
+
stopRetention();
|
|
1297
|
+
log('✅ Retention job stopped');
|
|
1298
|
+
}
|
|
1299
|
+
if (stopEviction) {
|
|
1300
|
+
stopEviction();
|
|
1301
|
+
log('✅ Eviction job stopped');
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
// Clear background intervals
|
|
1305
|
+
if (leaseReclaimInterval) {
|
|
1306
|
+
clearInterval(leaseReclaimInterval);
|
|
1307
|
+
log('✅ Lease reclaim interval stopped');
|
|
1308
|
+
}
|
|
1309
|
+
if (queueDepthInterval) {
|
|
1310
|
+
clearInterval(queueDepthInterval);
|
|
1311
|
+
log('✅ Queue depth update interval stopped');
|
|
1312
|
+
}
|
|
1313
|
+
if (systemStatsInterval) {
|
|
1314
|
+
clearInterval(systemStatsInterval);
|
|
1315
|
+
log('✅ System stats interval stopped');
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
// Wait a moment for any in-flight operations to complete
|
|
1319
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
1320
|
+
|
|
1321
|
+
const uptime = (Date.now() - startTime) / 1000;
|
|
1322
|
+
if (messageCount > 0) {
|
|
1323
|
+
log(`📊 Final stats: ${messageCount} messages processed in ${uptime.toFixed(1)}s`);
|
|
1324
|
+
log(` Average: ${(messageCount / uptime).toFixed(2)} messages/second`);
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
// Close database pool last
|
|
1328
|
+
await pool.end();
|
|
1329
|
+
log('✅ Database pool closed');
|
|
1330
|
+
|
|
1331
|
+
process.exit(0);
|
|
1332
|
+
};
|
|
1333
|
+
|
|
1334
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
1335
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
1320
1336
|
|
|
1321
|
-
|
|
1322
|
-
|
|
1337
|
+
// Return server instance with shutdown method
|
|
1338
|
+
return {
|
|
1339
|
+
port: PORT,
|
|
1340
|
+
host: HOST,
|
|
1341
|
+
shutdown,
|
|
1342
|
+
app
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
// Auto-start server if run directly (not imported)
|
|
1347
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
1348
|
+
QueenServer().catch(error => {
|
|
1349
|
+
console.error('Failed to start Queen server:', error);
|
|
1350
|
+
process.exit(1);
|
|
1351
|
+
});
|
|
1352
|
+
}
|