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