queen-mq 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/API.md +1226 -0
- package/AUTH.md +2044 -0
- package/LICENSE.md +202 -0
- package/README.md +276 -53
- package/WEBAPP.md +1889 -0
- package/assets/dashboard-01.png +0 -0
- package/assets/queen-logo-blue.svg +210 -0
- package/assets/queen-logo-cyan.svg +210 -0
- package/assets/queen-logo-indigo.svg +210 -0
- package/assets/queen-logo-orange.svg +210 -0
- package/assets/queen-logo-pink.svg +210 -0
- package/assets/queen-logo-purple.svg +210 -0
- package/assets/queen-logo-rose.svg +239 -0
- package/assets/queen-logo.svg +263 -0
- package/examples/batch-processing.js +58 -0
- package/examples/test-dashboard-api.js +200 -0
- package/package.json +4 -2
- package/server.log +1 -0
- package/src/benchmark/consumer.js +207 -0
- package/src/benchmark/consumer_multi.js +216 -0
- package/src/benchmark/producer.js +75 -0
- package/src/benchmark/producer_multi.js +115 -0
- package/src/client/client.js +192 -16
- package/src/client/queenClient.js +5 -0
- package/src/cluster-server.js +242 -0
- package/src/config.js +19 -5
- package/src/database/connection.js +42 -16
- package/src/database/poolManager.js +7 -0
- package/src/database/schema-v2.sql +194 -130
- package/src/managers/queueManagerOptimized.js +823 -933
- package/src/managers/systemEventManager.js +8 -3
- package/src/routes/messages.js +127 -57
- package/src/routes/pop.js +27 -43
- package/src/routes/resources.js +61 -27
- package/src/routes/status.js +1037 -0
- package/src/server.js +308 -272
- package/src/services/evictionService.js +57 -28
- package/src/services/retentionService.js +44 -11
- package/src/test/advanced-pattern-tests.js +5 -5
- package/src/test/bus-mode-tests.js +24 -11
- package/src/test/edge-case-tests.js +12 -5
- package/src/test/enterprise-tests.js +48 -15
- package/src/test/utils.js +1 -1
- package/src/utils/streaming.js +231 -0
- package/src/utils/uuid.js +2 -2
- package/src/websocket/wsServer.js +10 -3
- package/test-keepalive-v2.sh +22 -0
- package/webapp/COLOR_GUIDE.md +118 -0
- package/webapp/README.md +143 -0
- package/webapp/index.html +14 -0
- package/webapp/package-lock.json +3184 -0
- package/webapp/package.json +25 -0
- package/webapp/postcss.config.js +7 -0
- package/webapp/public/assets/queen-logo-blue.svg +210 -0
- package/webapp/public/assets/queen-logo-cyan.svg +210 -0
- package/webapp/public/assets/queen-logo-indigo.svg +210 -0
- package/webapp/public/assets/queen-logo-orange.svg +210 -0
- package/webapp/public/assets/queen-logo-pink.svg +210 -0
- package/webapp/public/assets/queen-logo-purple.svg +210 -0
- package/webapp/public/assets/queen-logo-rose.svg +239 -0
- package/webapp/public/assets/queen-logo.svg +263 -0
- package/webapp/src/App.vue +19 -0
- package/webapp/src/api/analytics.js +10 -0
- package/webapp/src/api/client.js +29 -0
- package/webapp/src/api/consumers.js +52 -0
- package/webapp/src/api/health.js +7 -0
- package/webapp/src/api/messages.js +26 -0
- package/webapp/src/api/queues.js +14 -0
- package/webapp/src/api/resources.js +8 -0
- package/webapp/src/assets/styles/main.css +357 -0
- package/webapp/src/components/analytics/AnalyticsFilters.vue +87 -0
- package/webapp/src/components/analytics/AnalyticsMetrics.vue +57 -0
- package/webapp/src/components/analytics/MessageDistributionChart.vue +111 -0
- package/webapp/src/components/analytics/MessageFlowChart.vue +173 -0
- package/webapp/src/components/analytics/TimeRangeSelector.vue +27 -0
- package/webapp/src/components/analytics/TopQueuesChart.vue +132 -0
- package/webapp/src/components/common/ConfirmDialog.vue +56 -0
- package/webapp/src/components/common/LoadingSpinner.vue +6 -0
- package/webapp/src/components/common/MetricCard.vue +43 -0
- package/webapp/src/components/common/StatusBadge.vue +45 -0
- package/webapp/src/components/dashboard/MessageStatusCard.vue +50 -0
- package/webapp/src/components/dashboard/PerformanceCard.vue +38 -0
- package/webapp/src/components/dashboard/ThroughputChart.vue +182 -0
- package/webapp/src/components/dashboard/TopQueuesTable.vue +53 -0
- package/webapp/src/components/layout/AppLayout.vue +110 -0
- package/webapp/src/components/layout/AppSidebar.vue +304 -0
- package/webapp/src/components/messages/MessageDetailPanel.vue +242 -0
- package/webapp/src/components/messages/MessageFilters.vue +114 -0
- package/webapp/src/components/queue-detail/PartitionList.vue +79 -0
- package/webapp/src/components/queue-detail/PushMessageModal.vue +175 -0
- package/webapp/src/components/queue-detail/QueueConfig.vue +63 -0
- package/webapp/src/components/queue-detail/QueueDetailHeader.vue +53 -0
- package/webapp/src/components/queue-detail/RecentMessages.vue +76 -0
- package/webapp/src/components/queues/CreateQueueModal.vue +193 -0
- package/webapp/src/components/queues/QueueFilters.vue +90 -0
- package/webapp/src/composables/useApi.js +34 -0
- package/webapp/src/composables/useTheme.js +36 -0
- package/webapp/src/main.js +11 -0
- package/webapp/src/router/index.js +42 -0
- package/webapp/src/utils/colors.js +96 -0
- package/webapp/src/utils/formatters.js +49 -0
- package/webapp/src/views/Analytics.vue +377 -0
- package/webapp/src/views/ConsumerGroups.vue +433 -0
- package/webapp/src/views/Dashboard.vue +418 -0
- package/webapp/src/views/Messages.vue +361 -0
- package/webapp/src/views/QueueDetail.vue +582 -0
- package/webapp/src/views/Queues.vue +496 -0
- package/webapp/tailwind.config.js +25 -0
- package/webapp/vite.config.js +10 -0
- package/dashboard/.vscode/extensions.json +0 -3
- package/dashboard/README.md +0 -5
- package/dashboard/index.html +0 -14
- package/dashboard/package-lock.json +0 -1458
- package/dashboard/package.json +0 -25
- package/dashboard/public/vite.svg +0 -1
- package/dashboard/src/App.vue +0 -29
- package/dashboard/src/assets/styles/main.css +0 -908
- package/dashboard/src/assets/vue.svg +0 -1
- package/dashboard/src/components/cards/MetricCard.vue +0 -298
- package/dashboard/src/components/charts/QueueDepthChart.vue +0 -276
- package/dashboard/src/components/charts/QueueLagChart.vue +0 -436
- package/dashboard/src/components/charts/ThroughputChart.vue +0 -302
- package/dashboard/src/components/common/ActivityFeed.vue +0 -251
- package/dashboard/src/components/layout/AppHeader.vue +0 -208
- package/dashboard/src/components/layout/AppLayout.vue +0 -88
- package/dashboard/src/components/layout/AppSidebar.vue +0 -261
- package/dashboard/src/main.js +0 -44
- package/dashboard/src/router.js +0 -54
- package/dashboard/src/services/api.js +0 -187
- package/dashboard/src/services/websocket.js +0 -167
- package/dashboard/src/utils/constants.js +0 -56
- package/dashboard/src/utils/helpers.js +0 -118
- package/dashboard/src/views/Analytics.vue +0 -912
- package/dashboard/src/views/Dashboard.vue +0 -906
- package/dashboard/src/views/Messages.vue +0 -437
- package/dashboard/src/views/QueueDetail.vue +0 -501
- package/dashboard/src/views/Queues.vue +0 -333
- package/dashboard/vite.config.js +0 -30
- package/src/routes/analytics.js +0 -812
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Queen Message Queue - Clustered Server
|
|
5
|
+
*
|
|
6
|
+
* This module enables multi-core utilization using Node.js cluster module.
|
|
7
|
+
* Each worker runs an independent instance of the Queen server, sharing the same port.
|
|
8
|
+
*
|
|
9
|
+
* Benefits:
|
|
10
|
+
* - 10x throughput improvement (utilizes all CPU cores)
|
|
11
|
+
* - Automatic load balancing across workers
|
|
12
|
+
* - Fault tolerance (automatic worker restart on crash)
|
|
13
|
+
* - Zero-downtime deploys (rolling worker restarts)
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* node src/cluster-server.js
|
|
17
|
+
*
|
|
18
|
+
* Environment Variables:
|
|
19
|
+
* QUEEN_WORKERS=N - Number of workers (default: CPU count)
|
|
20
|
+
* WORKER_ID=name - Base worker ID prefix (default: queen-worker)
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import cluster from 'cluster';
|
|
24
|
+
import os from 'os';
|
|
25
|
+
import { log } from './utils/logger.js';
|
|
26
|
+
|
|
27
|
+
// Configuration
|
|
28
|
+
const numWorkers = parseInt(process.env.QUEEN_WORKERS) || os.cpus().length;
|
|
29
|
+
const workerIdPrefix = process.env.WORKER_ID || 'queen-worker';
|
|
30
|
+
|
|
31
|
+
if (cluster.isPrimary) {
|
|
32
|
+
console.log('');
|
|
33
|
+
console.log('═══════════════════════════════════════════════════════════');
|
|
34
|
+
console.log('🚀 Queen Message Queue - Clustered Mode');
|
|
35
|
+
console.log('═══════════════════════════════════════════════════════════');
|
|
36
|
+
console.log(`📊 System: ${os.cpus()[0].model}`);
|
|
37
|
+
console.log(`🔢 CPU Cores: ${os.cpus().length}`);
|
|
38
|
+
console.log(`👷 Starting Workers: ${numWorkers}`);
|
|
39
|
+
console.log(`🆔 Worker ID Prefix: ${workerIdPrefix}`);
|
|
40
|
+
console.log('═══════════════════════════════════════════════════════════');
|
|
41
|
+
console.log('');
|
|
42
|
+
|
|
43
|
+
// Track worker statistics
|
|
44
|
+
const workerStats = {
|
|
45
|
+
startTime: Date.now(),
|
|
46
|
+
totalRestarts: 0,
|
|
47
|
+
workers: new Map()
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Graceful shutdown state (declared early for use in event handlers)
|
|
51
|
+
let isShuttingDown = false;
|
|
52
|
+
let forceExitTimeout = null;
|
|
53
|
+
let statusInterval = null;
|
|
54
|
+
|
|
55
|
+
// Fork workers
|
|
56
|
+
for (let i = 0; i < numWorkers; i++) {
|
|
57
|
+
forkWorker(i, workerStats);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Handle worker exit
|
|
61
|
+
cluster.on('exit', (worker, code, signal) => {
|
|
62
|
+
const workerInfo = workerStats.workers.get(worker.id);
|
|
63
|
+
const workerId = workerInfo ? workerInfo.workerId : `worker-${worker.id}`;
|
|
64
|
+
const workerIndex = workerInfo ? workerInfo.index : 0;
|
|
65
|
+
|
|
66
|
+
if (code !== 0 && !worker.exitedAfterDisconnect) {
|
|
67
|
+
// Unexpected crash
|
|
68
|
+
console.log('');
|
|
69
|
+
console.log(`⚠️ Worker ${workerId} (PID: ${worker.process.pid}) crashed!`);
|
|
70
|
+
console.log(` Exit code: ${code}, Signal: ${signal}`);
|
|
71
|
+
console.log(` Restarting worker...`);
|
|
72
|
+
|
|
73
|
+
workerStats.totalRestarts++;
|
|
74
|
+
|
|
75
|
+
// Remove from stats
|
|
76
|
+
workerStats.workers.delete(worker.id);
|
|
77
|
+
|
|
78
|
+
// Restart with same index
|
|
79
|
+
setTimeout(() => {
|
|
80
|
+
forkWorker(workerIndex, workerStats);
|
|
81
|
+
}, 1000); // Wait 1 second before restart
|
|
82
|
+
} else {
|
|
83
|
+
// Graceful shutdown
|
|
84
|
+
console.log(`✅ Worker ${workerId} (PID: ${worker.process.pid}) exited gracefully`);
|
|
85
|
+
workerStats.workers.delete(worker.id);
|
|
86
|
+
|
|
87
|
+
// Check if all workers have exited during shutdown
|
|
88
|
+
if (isShuttingDown && Object.keys(cluster.workers).length === 0) {
|
|
89
|
+
console.log('✅ All workers exited, shutting down master process');
|
|
90
|
+
clearInterval(statusInterval);
|
|
91
|
+
clearTimeout(forceExitTimeout);
|
|
92
|
+
process.exit(0);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Handle worker online
|
|
98
|
+
cluster.on('online', (worker) => {
|
|
99
|
+
const workerInfo = workerStats.workers.get(worker.id);
|
|
100
|
+
if (workerInfo) {
|
|
101
|
+
// Update PID now that it's available
|
|
102
|
+
workerInfo.pid = worker.process.pid;
|
|
103
|
+
console.log(`✅ Worker ${workerInfo.workerId} (PID: ${worker.process.pid}) online and ready`);
|
|
104
|
+
} else {
|
|
105
|
+
console.log(`✅ Worker ${worker.id} (PID: ${worker.process.pid}) online`);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Handle worker listening
|
|
110
|
+
cluster.on('listening', (worker, address) => {
|
|
111
|
+
const workerInfo = workerStats.workers.get(worker.id);
|
|
112
|
+
const workerId = workerInfo ? workerInfo.workerId : `worker-${worker.id}`;
|
|
113
|
+
console.log(`🎧 Worker ${workerId} listening on ${address.address}:${address.port}`);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Handle cluster errors
|
|
117
|
+
cluster.on('error', (error) => {
|
|
118
|
+
console.error('⚠️ Cluster error:', error.message);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Handle uncaught exceptions in master
|
|
122
|
+
process.on('uncaughtException', (error) => {
|
|
123
|
+
console.error('❌ Uncaught exception in master process:', error);
|
|
124
|
+
console.error(error.stack);
|
|
125
|
+
// Don't exit - try to keep the cluster running
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
129
|
+
console.error('❌ Unhandled rejection in master process:', reason);
|
|
130
|
+
// Don't exit - try to keep the cluster running
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Graceful shutdown handling
|
|
134
|
+
const gracefulShutdown = (signal) => {
|
|
135
|
+
if (isShuttingDown) return;
|
|
136
|
+
isShuttingDown = true;
|
|
137
|
+
|
|
138
|
+
console.log('');
|
|
139
|
+
console.log(`🛑 Received ${signal}, shutting down gracefully...`);
|
|
140
|
+
console.log(`📊 Total worker restarts during uptime: ${workerStats.totalRestarts}`);
|
|
141
|
+
console.log(`⏱️ Uptime: ${((Date.now() - workerStats.startTime) / 1000 / 60).toFixed(2)} minutes`);
|
|
142
|
+
|
|
143
|
+
// Stop status reporting during shutdown
|
|
144
|
+
clearInterval(statusInterval);
|
|
145
|
+
|
|
146
|
+
// Check if we already have no workers
|
|
147
|
+
if (Object.keys(cluster.workers).length === 0) {
|
|
148
|
+
console.log('✅ No active workers, exiting immediately');
|
|
149
|
+
process.exit(0);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Disconnect all workers
|
|
153
|
+
for (const id in cluster.workers) {
|
|
154
|
+
cluster.workers[id].disconnect();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Force exit if workers don't shut down in 10 seconds (reduced from 30)
|
|
158
|
+
forceExitTimeout = setTimeout(() => {
|
|
159
|
+
console.log('⚠️ Forcing shutdown after 10 second timeout');
|
|
160
|
+
console.log(` Remaining workers: ${Object.keys(cluster.workers).length}`);
|
|
161
|
+
process.exit(0);
|
|
162
|
+
}, 10000);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
166
|
+
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
167
|
+
|
|
168
|
+
// Status reporting every 60 seconds
|
|
169
|
+
statusInterval = setInterval(() => {
|
|
170
|
+
// Don't show status during shutdown
|
|
171
|
+
if (isShuttingDown) return;
|
|
172
|
+
|
|
173
|
+
const aliveWorkers = Object.keys(cluster.workers).length;
|
|
174
|
+
const uptime = ((Date.now() - workerStats.startTime) / 1000 / 60).toFixed(2);
|
|
175
|
+
|
|
176
|
+
console.log('');
|
|
177
|
+
console.log(`📊 Cluster Status (${new Date().toISOString()})`);
|
|
178
|
+
console.log(` Active Workers: ${aliveWorkers}/${numWorkers}`);
|
|
179
|
+
console.log(` Total Restarts: ${workerStats.totalRestarts}`);
|
|
180
|
+
console.log(` Uptime: ${uptime} minutes`);
|
|
181
|
+
}, 60000);
|
|
182
|
+
|
|
183
|
+
} else {
|
|
184
|
+
// Worker process - run the normal server
|
|
185
|
+
const workerId = process.env.WORKER_ID || `worker-${process.pid}`;
|
|
186
|
+
const workerIndex = process.env.WORKER_INDEX || '0';
|
|
187
|
+
|
|
188
|
+
console.log(`👷 Worker ${workerId} (index: ${workerIndex}, PID: ${process.pid}) starting...`);
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
// Import and run the main server
|
|
192
|
+
await import('./server.js');
|
|
193
|
+
|
|
194
|
+
// Worker-specific metrics could be added here
|
|
195
|
+
// e.g., track messages processed, errors, etc.
|
|
196
|
+
} catch (error) {
|
|
197
|
+
console.error(`❌ Worker ${workerId} failed to start:`, error.message);
|
|
198
|
+
console.error(error.stack);
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Handle uncaught exceptions in worker
|
|
203
|
+
process.on('uncaughtException', (error) => {
|
|
204
|
+
console.error(`❌ Uncaught exception in worker ${workerId}:`, error);
|
|
205
|
+
process.exit(1);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
209
|
+
console.error(`❌ Unhandled rejection in worker ${workerId}:`, reason);
|
|
210
|
+
process.exit(1);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Fork a new worker with proper environment setup
|
|
216
|
+
*/
|
|
217
|
+
function forkWorker(index, stats) {
|
|
218
|
+
const workerId = `${workerIdPrefix}-${index}`;
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
const worker = cluster.fork({
|
|
222
|
+
WORKER_ID: workerId,
|
|
223
|
+
WORKER_INDEX: index.toString(),
|
|
224
|
+
// Each worker gets a unique ID for system events
|
|
225
|
+
SYSTEM_WORKER_ID: `${workerIdPrefix}-${index}-${Date.now()}`
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Store worker info immediately (PID will be updated in 'online' event)
|
|
229
|
+
stats.workers.set(worker.id, {
|
|
230
|
+
workerId,
|
|
231
|
+
index,
|
|
232
|
+
startTime: Date.now(),
|
|
233
|
+
pid: null // Will be set when worker comes online
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
return worker;
|
|
237
|
+
} catch (error) {
|
|
238
|
+
console.error(`❌ Failed to fork worker ${workerId}:`, error.message);
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
package/src/config.js
CHANGED
|
@@ -23,12 +23,17 @@ export const DATABASE = {
|
|
|
23
23
|
PASSWORD: process.env.PG_PASSWORD || 'postgres',
|
|
24
24
|
PORT: parseInt(process.env.PG_PORT) || 5432,
|
|
25
25
|
|
|
26
|
+
// SSL configuration
|
|
27
|
+
USE_SSL: process.env.PG_USE_SSL === 'true',
|
|
28
|
+
SSL_REJECT_UNAUTHORIZED: process.env.PG_SSL_REJECT_UNAUTHORIZED !== 'false',
|
|
29
|
+
|
|
26
30
|
// Pool configuration
|
|
27
|
-
POOL_SIZE: parseInt(process.env.DB_POOL_SIZE) ||
|
|
31
|
+
POOL_SIZE: parseInt(process.env.DB_POOL_SIZE) || 150,
|
|
28
32
|
IDLE_TIMEOUT: parseInt(process.env.DB_IDLE_TIMEOUT) || 30000, // 30 seconds
|
|
29
33
|
CONNECTION_TIMEOUT: parseInt(process.env.DB_CONNECTION_TIMEOUT) || 2000, // 2 seconds
|
|
30
34
|
STATEMENT_TIMEOUT: parseInt(process.env.DB_STATEMENT_TIMEOUT) || 30000, // 30 seconds
|
|
31
35
|
QUERY_TIMEOUT: parseInt(process.env.DB_QUERY_TIMEOUT) || 30000, // 30 seconds
|
|
36
|
+
LOCK_TIMEOUT: parseInt(process.env.DB_LOCK_TIMEOUT) || 10000, // 10 seconds
|
|
32
37
|
|
|
33
38
|
// Pool manager settings
|
|
34
39
|
MAX_RETRIES: parseInt(process.env.DB_MAX_RETRIES) || 3
|
|
@@ -43,8 +48,14 @@ export const QUEUE = {
|
|
|
43
48
|
BATCH_INSERT_SIZE: parseInt(process.env.BATCH_INSERT_SIZE) || 1000,
|
|
44
49
|
|
|
45
50
|
// Long polling
|
|
46
|
-
POLL_INTERVAL: parseInt(process.env.QUEUE_POLL_INTERVAL) || 100, // 100ms
|
|
47
|
-
POLL_INTERVAL_FILTERED: parseInt(process.env.QUEUE_POLL_INTERVAL_FILTERED) ||
|
|
51
|
+
POLL_INTERVAL: parseInt(process.env.QUEUE_POLL_INTERVAL) || 100, // 100ms - initial poll interval
|
|
52
|
+
POLL_INTERVAL_FILTERED: parseInt(process.env.QUEUE_POLL_INTERVAL_FILTERED) || 50, // 50ms for better distribution across consumers
|
|
53
|
+
MAX_POLL_INTERVAL: parseInt(process.env.QUEUE_MAX_POLL_INTERVAL) || 2000, // 2000ms - max poll interval after backoff
|
|
54
|
+
BACKOFF_THRESHOLD: parseInt(process.env.QUEUE_BACKOFF_THRESHOLD) || 5, // Number of empty polls before backoff starts
|
|
55
|
+
BACKOFF_MULTIPLIER: parseFloat(process.env.QUEUE_BACKOFF_MULTIPLIER) || 2, // Exponential backoff multiplier
|
|
56
|
+
|
|
57
|
+
// Partition selection for filtered pops (namespace/task)
|
|
58
|
+
MAX_PARTITION_CANDIDATES: parseInt(process.env.MAX_PARTITION_CANDIDATES) || 100, // Number of candidate partitions to fetch for lease acquisition
|
|
48
59
|
|
|
49
60
|
// Queue defaults
|
|
50
61
|
DEFAULT_LEASE_TIME: parseInt(process.env.DEFAULT_LEASE_TIME) || 300, // 5 minutes
|
|
@@ -72,7 +83,7 @@ export const QUEUE = {
|
|
|
72
83
|
// System Events Configuration
|
|
73
84
|
export const SYSTEM_EVENTS = {
|
|
74
85
|
// Enable/disable system event propagation
|
|
75
|
-
ENABLED: process.env.QUEEN_SYSTEM_EVENTS_ENABLED === 'true' ||
|
|
86
|
+
ENABLED: process.env.QUEEN_SYSTEM_EVENTS_ENABLED === 'true' || false,
|
|
76
87
|
|
|
77
88
|
// Batching window for event publishing (milliseconds)
|
|
78
89
|
BATCH_MS: parseInt(process.env.QUEEN_SYSTEM_EVENTS_BATCH_MS) || 10,
|
|
@@ -91,6 +102,9 @@ export const JOBS = {
|
|
|
91
102
|
RETENTION_BATCH_SIZE: parseInt(process.env.RETENTION_BATCH_SIZE) || 1000,
|
|
92
103
|
PARTITION_CLEANUP_DAYS: parseInt(process.env.PARTITION_CLEANUP_DAYS) || 7,
|
|
93
104
|
|
|
105
|
+
// Metrics retention (messages_consumed table)
|
|
106
|
+
METRICS_RETENTION_DAYS: parseInt(process.env.METRICS_RETENTION_DAYS) || 90, // Keep 90 days of metrics by default
|
|
107
|
+
|
|
94
108
|
// Eviction service
|
|
95
109
|
EVICTION_INTERVAL: parseInt(process.env.EVICTION_INTERVAL) || 60000, // 1 minute
|
|
96
110
|
EVICTION_BATCH_SIZE: parseInt(process.env.EVICTION_BATCH_SIZE) || 1000,
|
|
@@ -129,7 +143,7 @@ export const CLIENT = {
|
|
|
129
143
|
|
|
130
144
|
// API Configuration
|
|
131
145
|
export const API = {
|
|
132
|
-
MAX_BODY_SIZE: parseInt(process.env.MAX_BODY_SIZE) ||
|
|
146
|
+
MAX_BODY_SIZE: parseInt(process.env.MAX_BODY_SIZE) || 100 * 1024 * 1024, // 100MB max body size
|
|
133
147
|
// Pagination
|
|
134
148
|
DEFAULT_LIMIT: parseInt(process.env.API_DEFAULT_LIMIT) || 100,
|
|
135
149
|
MAX_LIMIT: parseInt(process.env.API_MAX_LIMIT) || 1000,
|
|
@@ -2,22 +2,44 @@ import pg from 'pg';
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
|
+
import config from '../config.js';
|
|
5
6
|
|
|
6
7
|
const { Pool } = pg;
|
|
7
8
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Create a PostgreSQL connection pool with proper configuration
|
|
12
|
+
* This is the single source of truth for pool creation across the application
|
|
13
|
+
*/
|
|
10
14
|
export const createPool = () => {
|
|
11
|
-
|
|
12
|
-
user:
|
|
13
|
-
host:
|
|
14
|
-
database:
|
|
15
|
-
password:
|
|
16
|
-
port:
|
|
17
|
-
max:
|
|
18
|
-
idleTimeoutMillis:
|
|
19
|
-
connectionTimeoutMillis:
|
|
20
|
-
|
|
15
|
+
const poolConfig = {
|
|
16
|
+
user: config.DATABASE.USER,
|
|
17
|
+
host: config.DATABASE.HOST,
|
|
18
|
+
database: config.DATABASE.DATABASE,
|
|
19
|
+
password: config.DATABASE.PASSWORD,
|
|
20
|
+
port: config.DATABASE.PORT,
|
|
21
|
+
max: config.DATABASE.POOL_SIZE,
|
|
22
|
+
idleTimeoutMillis: config.DATABASE.IDLE_TIMEOUT,
|
|
23
|
+
connectionTimeoutMillis: config.DATABASE.CONNECTION_TIMEOUT,
|
|
24
|
+
// Note: statement_timeout and query_timeout cannot be set as startup parameters
|
|
25
|
+
// They should be set per-query or per-transaction using SET commands
|
|
26
|
+
application_name: config.SERVER.APPLICATION_NAME,
|
|
27
|
+
// PERFORMANCE NOTE: TCP_NODELAY for latency reduction
|
|
28
|
+
// The node-postgres driver enables TCP_NODELAY by default on sockets
|
|
29
|
+
// For server-side: Edit postgresql.conf and add: tcp_nodelay = on
|
|
30
|
+
// Reference: https://stackoverflow.com/questions/60634455/how-does-one-configure-tcp-nodelay-for-libpq-and-postgres-server
|
|
31
|
+
keepAlive: true,
|
|
32
|
+
keepAliveInitialDelayMillis: 10000
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Add SSL configuration if enabled
|
|
36
|
+
if (config.DATABASE.USE_SSL) {
|
|
37
|
+
poolConfig.ssl = {
|
|
38
|
+
rejectUnauthorized: config.DATABASE.SSL_REJECT_UNAUTHORIZED
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return new Pool(poolConfig);
|
|
21
43
|
};
|
|
22
44
|
|
|
23
45
|
// Initialize database schema
|
|
@@ -74,8 +96,8 @@ export const withTransaction = async (pool, callback, isolationLevel = 'READ COM
|
|
|
74
96
|
}
|
|
75
97
|
|
|
76
98
|
// Set timeouts to prevent long blocks
|
|
77
|
-
await client.query(
|
|
78
|
-
await client.query(
|
|
99
|
+
await client.query(`SET LOCAL statement_timeout = ${config.DATABASE.STATEMENT_TIMEOUT}`);
|
|
100
|
+
await client.query(`SET LOCAL lock_timeout = ${config.DATABASE.LOCK_TIMEOUT}`);
|
|
79
101
|
|
|
80
102
|
const result = await callback(client);
|
|
81
103
|
await client.query('COMMIT');
|
|
@@ -83,13 +105,17 @@ export const withTransaction = async (pool, callback, isolationLevel = 'READ COM
|
|
|
83
105
|
} catch (error) {
|
|
84
106
|
await client.query('ROLLBACK');
|
|
85
107
|
|
|
86
|
-
// Check for
|
|
87
|
-
|
|
108
|
+
// Check for retriable errors:
|
|
109
|
+
// 40001: serialization_failure
|
|
110
|
+
// 40P01: deadlock_detected
|
|
111
|
+
// 55P03: lock_not_available (lock timeout)
|
|
112
|
+
// 25P02: in_failed_sql_transaction (transaction already aborted - retry whole transaction)
|
|
113
|
+
if ((error.code === '40001' || error.code === '40P01' || error.code === '55P03' || error.code === '25P02') && attempt < maxAttempts) {
|
|
88
114
|
// Exponential backoff: 100ms, 200ms, 400ms
|
|
89
115
|
const delay = Math.min(100 * Math.pow(2, attempt - 1), 1000);
|
|
90
116
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
91
117
|
attempt++;
|
|
92
|
-
console.log(`Transaction retry attempt ${attempt}/${maxAttempts} after ${error.code}`);
|
|
118
|
+
console.log(`Transaction retry attempt ${attempt}/${maxAttempts} after ${error.code} (${error.message})`);
|
|
93
119
|
continue;
|
|
94
120
|
}
|
|
95
121
|
|
|
@@ -22,6 +22,13 @@ export class PoolManager {
|
|
|
22
22
|
connectionTimeoutMillis: appConfig.DATABASE.CONNECTION_TIMEOUT,
|
|
23
23
|
...config
|
|
24
24
|
};
|
|
25
|
+
|
|
26
|
+
// Add SSL configuration if enabled
|
|
27
|
+
if (appConfig.DATABASE.USE_SSL) {
|
|
28
|
+
this.config.ssl = {
|
|
29
|
+
rejectUnauthorized: appConfig.DATABASE.SSL_REJECT_UNAUTHORIZED
|
|
30
|
+
};
|
|
31
|
+
}
|
|
25
32
|
|
|
26
33
|
this.pool = new Pool(this.config);
|
|
27
34
|
this.waitingQueue = [];
|