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.
Files changed (139) hide show
  1. package/API.md +1226 -0
  2. package/AUTH.md +2044 -0
  3. package/LICENSE.md +202 -0
  4. package/README.md +276 -53
  5. package/WEBAPP.md +1889 -0
  6. package/assets/dashboard-01.png +0 -0
  7. package/assets/queen-logo-blue.svg +210 -0
  8. package/assets/queen-logo-cyan.svg +210 -0
  9. package/assets/queen-logo-indigo.svg +210 -0
  10. package/assets/queen-logo-orange.svg +210 -0
  11. package/assets/queen-logo-pink.svg +210 -0
  12. package/assets/queen-logo-purple.svg +210 -0
  13. package/assets/queen-logo-rose.svg +239 -0
  14. package/assets/queen-logo.svg +263 -0
  15. package/examples/batch-processing.js +58 -0
  16. package/examples/test-dashboard-api.js +200 -0
  17. package/package.json +4 -2
  18. package/server.log +1 -0
  19. package/src/benchmark/consumer.js +207 -0
  20. package/src/benchmark/consumer_multi.js +216 -0
  21. package/src/benchmark/producer.js +75 -0
  22. package/src/benchmark/producer_multi.js +115 -0
  23. package/src/client/client.js +192 -16
  24. package/src/client/queenClient.js +5 -0
  25. package/src/cluster-server.js +242 -0
  26. package/src/config.js +19 -5
  27. package/src/database/connection.js +42 -16
  28. package/src/database/poolManager.js +7 -0
  29. package/src/database/schema-v2.sql +194 -130
  30. package/src/managers/queueManagerOptimized.js +823 -933
  31. package/src/managers/systemEventManager.js +8 -3
  32. package/src/routes/messages.js +127 -57
  33. package/src/routes/pop.js +27 -43
  34. package/src/routes/resources.js +61 -27
  35. package/src/routes/status.js +1037 -0
  36. package/src/server.js +308 -272
  37. package/src/services/evictionService.js +57 -28
  38. package/src/services/retentionService.js +44 -11
  39. package/src/test/advanced-pattern-tests.js +5 -5
  40. package/src/test/bus-mode-tests.js +24 -11
  41. package/src/test/edge-case-tests.js +12 -5
  42. package/src/test/enterprise-tests.js +48 -15
  43. package/src/test/utils.js +1 -1
  44. package/src/utils/streaming.js +231 -0
  45. package/src/utils/uuid.js +2 -2
  46. package/src/websocket/wsServer.js +10 -3
  47. package/test-keepalive-v2.sh +22 -0
  48. package/webapp/COLOR_GUIDE.md +118 -0
  49. package/webapp/README.md +143 -0
  50. package/webapp/index.html +14 -0
  51. package/webapp/package-lock.json +3184 -0
  52. package/webapp/package.json +25 -0
  53. package/webapp/postcss.config.js +7 -0
  54. package/webapp/public/assets/queen-logo-blue.svg +210 -0
  55. package/webapp/public/assets/queen-logo-cyan.svg +210 -0
  56. package/webapp/public/assets/queen-logo-indigo.svg +210 -0
  57. package/webapp/public/assets/queen-logo-orange.svg +210 -0
  58. package/webapp/public/assets/queen-logo-pink.svg +210 -0
  59. package/webapp/public/assets/queen-logo-purple.svg +210 -0
  60. package/webapp/public/assets/queen-logo-rose.svg +239 -0
  61. package/webapp/public/assets/queen-logo.svg +263 -0
  62. package/webapp/src/App.vue +19 -0
  63. package/webapp/src/api/analytics.js +10 -0
  64. package/webapp/src/api/client.js +29 -0
  65. package/webapp/src/api/consumers.js +52 -0
  66. package/webapp/src/api/health.js +7 -0
  67. package/webapp/src/api/messages.js +26 -0
  68. package/webapp/src/api/queues.js +14 -0
  69. package/webapp/src/api/resources.js +8 -0
  70. package/webapp/src/assets/styles/main.css +357 -0
  71. package/webapp/src/components/analytics/AnalyticsFilters.vue +87 -0
  72. package/webapp/src/components/analytics/AnalyticsMetrics.vue +57 -0
  73. package/webapp/src/components/analytics/MessageDistributionChart.vue +111 -0
  74. package/webapp/src/components/analytics/MessageFlowChart.vue +173 -0
  75. package/webapp/src/components/analytics/TimeRangeSelector.vue +27 -0
  76. package/webapp/src/components/analytics/TopQueuesChart.vue +132 -0
  77. package/webapp/src/components/common/ConfirmDialog.vue +56 -0
  78. package/webapp/src/components/common/LoadingSpinner.vue +6 -0
  79. package/webapp/src/components/common/MetricCard.vue +43 -0
  80. package/webapp/src/components/common/StatusBadge.vue +45 -0
  81. package/webapp/src/components/dashboard/MessageStatusCard.vue +50 -0
  82. package/webapp/src/components/dashboard/PerformanceCard.vue +38 -0
  83. package/webapp/src/components/dashboard/ThroughputChart.vue +182 -0
  84. package/webapp/src/components/dashboard/TopQueuesTable.vue +53 -0
  85. package/webapp/src/components/layout/AppLayout.vue +110 -0
  86. package/webapp/src/components/layout/AppSidebar.vue +304 -0
  87. package/webapp/src/components/messages/MessageDetailPanel.vue +242 -0
  88. package/webapp/src/components/messages/MessageFilters.vue +114 -0
  89. package/webapp/src/components/queue-detail/PartitionList.vue +79 -0
  90. package/webapp/src/components/queue-detail/PushMessageModal.vue +175 -0
  91. package/webapp/src/components/queue-detail/QueueConfig.vue +63 -0
  92. package/webapp/src/components/queue-detail/QueueDetailHeader.vue +53 -0
  93. package/webapp/src/components/queue-detail/RecentMessages.vue +76 -0
  94. package/webapp/src/components/queues/CreateQueueModal.vue +193 -0
  95. package/webapp/src/components/queues/QueueFilters.vue +90 -0
  96. package/webapp/src/composables/useApi.js +34 -0
  97. package/webapp/src/composables/useTheme.js +36 -0
  98. package/webapp/src/main.js +11 -0
  99. package/webapp/src/router/index.js +42 -0
  100. package/webapp/src/utils/colors.js +96 -0
  101. package/webapp/src/utils/formatters.js +49 -0
  102. package/webapp/src/views/Analytics.vue +377 -0
  103. package/webapp/src/views/ConsumerGroups.vue +433 -0
  104. package/webapp/src/views/Dashboard.vue +418 -0
  105. package/webapp/src/views/Messages.vue +361 -0
  106. package/webapp/src/views/QueueDetail.vue +582 -0
  107. package/webapp/src/views/Queues.vue +496 -0
  108. package/webapp/tailwind.config.js +25 -0
  109. package/webapp/vite.config.js +10 -0
  110. package/dashboard/.vscode/extensions.json +0 -3
  111. package/dashboard/README.md +0 -5
  112. package/dashboard/index.html +0 -14
  113. package/dashboard/package-lock.json +0 -1458
  114. package/dashboard/package.json +0 -25
  115. package/dashboard/public/vite.svg +0 -1
  116. package/dashboard/src/App.vue +0 -29
  117. package/dashboard/src/assets/styles/main.css +0 -908
  118. package/dashboard/src/assets/vue.svg +0 -1
  119. package/dashboard/src/components/cards/MetricCard.vue +0 -298
  120. package/dashboard/src/components/charts/QueueDepthChart.vue +0 -276
  121. package/dashboard/src/components/charts/QueueLagChart.vue +0 -436
  122. package/dashboard/src/components/charts/ThroughputChart.vue +0 -302
  123. package/dashboard/src/components/common/ActivityFeed.vue +0 -251
  124. package/dashboard/src/components/layout/AppHeader.vue +0 -208
  125. package/dashboard/src/components/layout/AppLayout.vue +0 -88
  126. package/dashboard/src/components/layout/AppSidebar.vue +0 -261
  127. package/dashboard/src/main.js +0 -44
  128. package/dashboard/src/router.js +0 -54
  129. package/dashboard/src/services/api.js +0 -187
  130. package/dashboard/src/services/websocket.js +0 -167
  131. package/dashboard/src/utils/constants.js +0 -56
  132. package/dashboard/src/utils/helpers.js +0 -118
  133. package/dashboard/src/views/Analytics.vue +0 -912
  134. package/dashboard/src/views/Dashboard.vue +0 -906
  135. package/dashboard/src/views/Messages.vue +0 -437
  136. package/dashboard/src/views/QueueDetail.vue +0 -501
  137. package/dashboard/src/views/Queues.vue +0 -333
  138. package/dashboard/vite.config.js +0 -30
  139. 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) || 20,
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) || 1000, // 1 second
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' || 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) || 10 * 1024 * 1024, // 10MB 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
- // Create connection pool
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
- return new Pool({
12
- user: process.env.PG_USER || 'postgres',
13
- host: process.env.PG_HOST || 'localhost',
14
- database: process.env.PG_DB || 'postgres',
15
- password: process.env.PG_PASSWORD || 'postgres',
16
- port: process.env.PG_PORT || 5432,
17
- max: process.env.DB_POOL_SIZE || 20,
18
- idleTimeoutMillis: 30000,
19
- connectionTimeoutMillis: 2000,
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('SET LOCAL statement_timeout = 30000'); // 30 seconds
78
- await client.query('SET LOCAL lock_timeout = 5000'); // 5 seconds
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 serialization failure or deadlock
87
- if ((error.code === '40001' || error.code === '40P01') && attempt < maxAttempts) {
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 = [];