queen-mq 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/API.md +1116 -0
  2. package/CACHE.md +519 -0
  3. package/DASHBOARD-V3.md +478 -0
  4. package/DASHBOARD.md +382 -0
  5. package/MOD_QUEUE.md +453 -0
  6. package/PARTITION_LOCKING_DESIGN.md +989 -0
  7. package/PLAN.md +707 -0
  8. package/QUERY_ANALSYS.md +72 -0
  9. package/QUEUE_BUS.md +334 -0
  10. package/README.md +1495 -0
  11. package/V2-PLAN.md +236 -0
  12. package/assets/dashboard.png +0 -0
  13. package/dashboard/.vscode/extensions.json +3 -0
  14. package/dashboard/README.md +5 -0
  15. package/dashboard/index.html +14 -0
  16. package/dashboard/package-lock.json +1458 -0
  17. package/dashboard/package.json +25 -0
  18. package/dashboard/public/vite.svg +1 -0
  19. package/dashboard/src/App.vue +29 -0
  20. package/dashboard/src/assets/styles/main.css +908 -0
  21. package/dashboard/src/assets/vue.svg +1 -0
  22. package/dashboard/src/components/cards/MetricCard.vue +298 -0
  23. package/dashboard/src/components/charts/QueueDepthChart.vue +276 -0
  24. package/dashboard/src/components/charts/QueueLagChart.vue +436 -0
  25. package/dashboard/src/components/charts/ThroughputChart.vue +302 -0
  26. package/dashboard/src/components/common/ActivityFeed.vue +251 -0
  27. package/dashboard/src/components/layout/AppHeader.vue +208 -0
  28. package/dashboard/src/components/layout/AppLayout.vue +88 -0
  29. package/dashboard/src/components/layout/AppSidebar.vue +261 -0
  30. package/dashboard/src/main.js +44 -0
  31. package/dashboard/src/router.js +54 -0
  32. package/dashboard/src/services/api.js +187 -0
  33. package/dashboard/src/services/websocket.js +167 -0
  34. package/dashboard/src/utils/constants.js +56 -0
  35. package/dashboard/src/utils/helpers.js +118 -0
  36. package/dashboard/src/views/Analytics.vue +912 -0
  37. package/dashboard/src/views/Dashboard.vue +906 -0
  38. package/dashboard/src/views/Messages.vue +437 -0
  39. package/dashboard/src/views/QueueDetail.vue +501 -0
  40. package/dashboard/src/views/Queues.vue +333 -0
  41. package/dashboard/vite.config.js +30 -0
  42. package/debug-namespace.js +110 -0
  43. package/docs/long-polling.md +159 -0
  44. package/docs/multi-server-cache-solutions.md +185 -0
  45. package/docs/performance-tuning.md +222 -0
  46. package/examples/bus-mode.js +239 -0
  47. package/examples/continuous-consumer-optimized.js +215 -0
  48. package/examples/continuous-consumer.js +159 -0
  49. package/examples/continuous-producer.js +343 -0
  50. package/examples/mixed-mode.js +277 -0
  51. package/examples/multi-server-test.js +305 -0
  52. package/examples/single.js +64 -0
  53. package/examples/smartchat-dealyed.js +42 -0
  54. package/examples/smartchat.js +52 -0
  55. package/examples/test-cache-invalidation.js +119 -0
  56. package/examples/test-cache-multi-server.js +245 -0
  57. package/examples/test-minimal-client.js +112 -0
  58. package/examples/test-queue-creation-policy.js +137 -0
  59. package/init-db.js +20 -0
  60. package/package.json +36 -0
  61. package/src/client/client.js +291 -0
  62. package/src/client/index.js +6 -0
  63. package/src/client/queenClient.js +513 -0
  64. package/src/client/utils/http.js +172 -0
  65. package/src/client/utils/loadBalancer.js +152 -0
  66. package/src/client/utils/retry.js +35 -0
  67. package/src/config.js +215 -0
  68. package/src/database/connection.js +103 -0
  69. package/src/database/poolManager.js +192 -0
  70. package/src/database/schema-v2.sql +214 -0
  71. package/src/managers/eventManager.js +59 -0
  72. package/src/managers/queueManagerOptimized.js +1512 -0
  73. package/src/managers/resourceCache.js +96 -0
  74. package/src/managers/systemEventManager.js +127 -0
  75. package/src/routes/ack.js +26 -0
  76. package/src/routes/analytics.js +812 -0
  77. package/src/routes/configure.js +46 -0
  78. package/src/routes/messages.js +298 -0
  79. package/src/routes/pop.js +85 -0
  80. package/src/routes/push.js +28 -0
  81. package/src/routes/resources.js +296 -0
  82. package/src/server.js +1286 -0
  83. package/src/services/encryptionService.js +82 -0
  84. package/src/services/evictionService.js +131 -0
  85. package/src/services/retentionService.js +129 -0
  86. package/src/services/startupSync.js +35 -0
  87. package/src/test/test.js +4521 -0
  88. package/src/utils/logger.js +44 -0
  89. package/src/utils/uuid.js +5 -0
  90. package/src/websocket/wsServer.js +221 -0
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Load balancer utility for distributing requests across multiple servers
3
+ */
4
+
5
+ export const LoadBalancingStrategy = {
6
+ ROUND_ROBIN: 'ROUND_ROBIN',
7
+ SESSION: 'SESSION'
8
+ };
9
+
10
+ /**
11
+ * Create a load balancer for distributing requests across multiple base URLs
12
+ */
13
+ export const createLoadBalancer = (urls, strategy = LoadBalancingStrategy.ROUND_ROBIN) => {
14
+ if (!Array.isArray(urls) || urls.length === 0) {
15
+ throw new Error('URLs must be a non-empty array');
16
+ }
17
+
18
+ // Normalize URLs (remove trailing slashes)
19
+ const normalizedUrls = urls.map(url => url.replace(/\/$/, ''));
20
+
21
+ // Round robin state
22
+ let currentIndex = 0;
23
+
24
+ // Session affinity state - map of session ID to URL index
25
+ const sessionMap = new Map();
26
+
27
+ // Generate a session ID for the current client instance
28
+ const sessionId = generateSessionId();
29
+
30
+ /**
31
+ * Get the next URL based on the configured strategy
32
+ */
33
+ const getNextUrl = (options = {}) => {
34
+ const { sessionKey = sessionId } = options;
35
+
36
+ switch (strategy) {
37
+ case LoadBalancingStrategy.ROUND_ROBIN:
38
+ // Round robin: cycle through URLs
39
+ const url = normalizedUrls[currentIndex];
40
+ currentIndex = (currentIndex + 1) % normalizedUrls.length;
41
+ return url;
42
+
43
+ case LoadBalancingStrategy.SESSION:
44
+ // Session affinity: stick to the same server per session
45
+ if (!sessionMap.has(sessionKey)) {
46
+ // Assign a server to this session (using round robin for initial assignment)
47
+ const assignedIndex = currentIndex;
48
+ sessionMap.set(sessionKey, assignedIndex);
49
+ currentIndex = (currentIndex + 1) % normalizedUrls.length;
50
+ }
51
+ return normalizedUrls[sessionMap.get(sessionKey)];
52
+
53
+ default:
54
+ throw new Error(`Unknown load balancing strategy: ${strategy}`);
55
+ }
56
+ };
57
+
58
+ /**
59
+ * Get all configured URLs
60
+ */
61
+ const getAllUrls = () => [...normalizedUrls];
62
+
63
+ /**
64
+ * Get current strategy
65
+ */
66
+ const getStrategy = () => strategy;
67
+
68
+ /**
69
+ * Reset the load balancer state
70
+ */
71
+ const reset = () => {
72
+ currentIndex = 0;
73
+ sessionMap.clear();
74
+ };
75
+
76
+ /**
77
+ * Get statistics about URL usage (for debugging)
78
+ */
79
+ const getStats = () => {
80
+ return {
81
+ strategy,
82
+ urls: normalizedUrls,
83
+ currentIndex,
84
+ sessionCount: sessionMap.size,
85
+ sessions: Array.from(sessionMap.entries()).map(([key, index]) => ({
86
+ sessionKey: key,
87
+ url: normalizedUrls[index]
88
+ }))
89
+ };
90
+ };
91
+
92
+ return {
93
+ getNextUrl,
94
+ getAllUrls,
95
+ getStrategy,
96
+ reset,
97
+ getStats
98
+ };
99
+ };
100
+
101
+ /**
102
+ * Generate a unique session ID
103
+ */
104
+ const generateSessionId = () => {
105
+ return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
106
+ };
107
+
108
+ /**
109
+ * Create a failover wrapper that tries different servers on failure
110
+ */
111
+ export const createFailoverWrapper = (loadBalancer, maxRetries = 3) => {
112
+ const attemptedUrls = new Set();
113
+
114
+ const executeWithFailover = async (requestFn, options = {}) => {
115
+ const urls = loadBalancer.getAllUrls();
116
+ let lastError = null;
117
+
118
+ // Try each URL up to maxRetries times total
119
+ for (let attempt = 0; attempt < Math.min(maxRetries, urls.length); attempt++) {
120
+ const url = loadBalancer.getNextUrl(options);
121
+
122
+ // Skip if we've already tried this URL in this request
123
+ if (attemptedUrls.has(url)) {
124
+ continue;
125
+ }
126
+
127
+ attemptedUrls.add(url);
128
+
129
+ try {
130
+ const result = await requestFn(url);
131
+ attemptedUrls.clear(); // Clear for next request
132
+ return result;
133
+ } catch (error) {
134
+ lastError = error;
135
+ console.warn(`Request failed for ${url}, attempting next server...`, error.message);
136
+
137
+ // If it's a client error (4xx), don't retry on other servers
138
+ if (error.status && error.status >= 400 && error.status < 500) {
139
+ attemptedUrls.clear();
140
+ throw error;
141
+ }
142
+ }
143
+ }
144
+
145
+ attemptedUrls.clear();
146
+ throw lastError || new Error('All servers failed');
147
+ };
148
+
149
+ return {
150
+ executeWithFailover
151
+ };
152
+ };
@@ -0,0 +1,35 @@
1
+ import config from '../../config.js';
2
+
3
+ export const withRetry = async (fn, attempts = config.CLIENT.DEFAULT_RETRY_ATTEMPTS, delay = config.CLIENT.DEFAULT_RETRY_DELAY, backoff = config.CLIENT.DEFAULT_RETRY_BACKOFF) => {
4
+ let lastError;
5
+
6
+ for (let i = 0; i < attempts; i++) {
7
+ try {
8
+ return await fn();
9
+ } catch (error) {
10
+ lastError = error;
11
+
12
+ // Don't retry on client errors (4xx)
13
+ if (error.status && error.status >= 400 && error.status < 500) {
14
+ throw error;
15
+ }
16
+
17
+ if (i < attempts - 1) {
18
+ const waitTime = delay * Math.pow(backoff, i);
19
+ await new Promise(resolve => setTimeout(resolve, waitTime));
20
+ }
21
+ }
22
+ }
23
+
24
+ throw lastError;
25
+ };
26
+
27
+ export const createRetryableFunction = (fn, options = {}) => {
28
+ const {
29
+ attempts = config.CLIENT.DEFAULT_RETRY_ATTEMPTS,
30
+ delay = config.CLIENT.DEFAULT_RETRY_DELAY,
31
+ backoff = config.CLIENT.DEFAULT_RETRY_BACKOFF
32
+ } = options;
33
+
34
+ return (...args) => withRetry(() => fn(...args), attempts, delay, backoff);
35
+ };
package/src/config.js ADDED
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Centralized Configuration for Queen Message Queue System
3
+ *
4
+ * All configuration values can be overridden using environment variables.
5
+ * This file provides default values and documentation for all settings.
6
+ */
7
+ import os from 'os';
8
+
9
+ // Server Configuration
10
+ export const SERVER = {
11
+ PORT: parseInt(process.env.PORT) || 6632,
12
+ HOST: process.env.HOST || '0.0.0.0',
13
+ WORKER_ID: process.env.WORKER_ID || `worker-${os.hostname()}-${process.pid}`,
14
+ APPLICATION_NAME: process.env.APP_NAME || 'queen-mq'
15
+ };
16
+
17
+ // Database Configuration
18
+ export const DATABASE = {
19
+ // Connection settings
20
+ USER: process.env.PG_USER || 'postgres',
21
+ HOST: process.env.PG_HOST || 'localhost',
22
+ DATABASE: process.env.PG_DB || 'postgres',
23
+ PASSWORD: process.env.PG_PASSWORD || 'postgres',
24
+ PORT: parseInt(process.env.PG_PORT) || 5432,
25
+
26
+ // Pool configuration
27
+ POOL_SIZE: parseInt(process.env.DB_POOL_SIZE) || 20,
28
+ IDLE_TIMEOUT: parseInt(process.env.DB_IDLE_TIMEOUT) || 30000, // 30 seconds
29
+ CONNECTION_TIMEOUT: parseInt(process.env.DB_CONNECTION_TIMEOUT) || 2000, // 2 seconds
30
+ STATEMENT_TIMEOUT: parseInt(process.env.DB_STATEMENT_TIMEOUT) || 30000, // 30 seconds
31
+ QUERY_TIMEOUT: parseInt(process.env.DB_QUERY_TIMEOUT) || 30000, // 30 seconds
32
+
33
+ // Pool manager settings
34
+ MAX_RETRIES: parseInt(process.env.DB_MAX_RETRIES) || 3
35
+ };
36
+
37
+ // Queue Processing Configuration
38
+ export const QUEUE = {
39
+ // Pop operation defaults
40
+ DEFAULT_TIMEOUT: parseInt(process.env.DEFAULT_TIMEOUT) || 30000, // 30 seconds
41
+ MAX_TIMEOUT: parseInt(process.env.MAX_TIMEOUT) || 60000, // 60 seconds
42
+ DEFAULT_BATCH_SIZE: parseInt(process.env.DEFAULT_BATCH_SIZE) || 1,
43
+ BATCH_INSERT_SIZE: parseInt(process.env.BATCH_INSERT_SIZE) || 1000,
44
+
45
+ // 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
48
+
49
+ // Queue defaults
50
+ DEFAULT_LEASE_TIME: parseInt(process.env.DEFAULT_LEASE_TIME) || 300, // 5 minutes
51
+ DEFAULT_RETRY_LIMIT: parseInt(process.env.DEFAULT_RETRY_LIMIT) || 3,
52
+ DEFAULT_RETRY_DELAY: parseInt(process.env.DEFAULT_RETRY_DELAY) || 1000, // 1 second
53
+ DEFAULT_MAX_SIZE: parseInt(process.env.DEFAULT_MAX_SIZE) || 10000,
54
+ DEFAULT_TTL: parseInt(process.env.DEFAULT_TTL) || 3600, // 1 hour
55
+ DEFAULT_PRIORITY: parseInt(process.env.DEFAULT_PRIORITY) || 0,
56
+ DEFAULT_DELAYED_PROCESSING: parseInt(process.env.DEFAULT_DELAYED_PROCESSING) || 0,
57
+ DEFAULT_WINDOW_BUFFER: parseInt(process.env.DEFAULT_WINDOW_BUFFER) || 0,
58
+
59
+ // Dead Letter Queue
60
+ DEFAULT_DLQ_ENABLED: process.env.DEFAULT_DLQ_ENABLED === 'true' || false,
61
+ DEFAULT_DLQ_AFTER_MAX_RETRIES: process.env.DEFAULT_DLQ_AFTER_MAX_RETRIES === 'true' || false,
62
+
63
+ // Retention defaults
64
+ DEFAULT_RETENTION_SECONDS: parseInt(process.env.DEFAULT_RETENTION_SECONDS) || 0,
65
+ DEFAULT_COMPLETED_RETENTION_SECONDS: parseInt(process.env.DEFAULT_COMPLETED_RETENTION_SECONDS) || 0,
66
+ DEFAULT_RETENTION_ENABLED: process.env.DEFAULT_RETENTION_ENABLED === 'true' || false,
67
+
68
+ // Eviction
69
+ DEFAULT_MAX_WAIT_TIME_SECONDS: parseInt(process.env.DEFAULT_MAX_WAIT_TIME_SECONDS) || 0
70
+ };
71
+
72
+ // System Events Configuration
73
+ export const SYSTEM_EVENTS = {
74
+ // Enable/disable system event propagation
75
+ ENABLED: process.env.QUEEN_SYSTEM_EVENTS_ENABLED === 'true' || true,
76
+
77
+ // Batching window for event publishing (milliseconds)
78
+ BATCH_MS: parseInt(process.env.QUEEN_SYSTEM_EVENTS_BATCH_MS) || 10,
79
+
80
+ // Timeout for startup synchronization (milliseconds)
81
+ SYNC_TIMEOUT: parseInt(process.env.QUEEN_SYSTEM_EVENTS_SYNC_TIMEOUT) || 30000
82
+ };
83
+
84
+ // Background Jobs Configuration
85
+ export const JOBS = {
86
+ // Lease reclamation
87
+ LEASE_RECLAIM_INTERVAL: parseInt(process.env.LEASE_RECLAIM_INTERVAL) || 5000, // 5 seconds
88
+
89
+ // Retention service
90
+ RETENTION_INTERVAL: parseInt(process.env.RETENTION_INTERVAL) || 300000, // 5 minutes
91
+ RETENTION_BATCH_SIZE: parseInt(process.env.RETENTION_BATCH_SIZE) || 1000,
92
+ PARTITION_CLEANUP_DAYS: parseInt(process.env.PARTITION_CLEANUP_DAYS) || 7,
93
+
94
+ // Eviction service
95
+ EVICTION_INTERVAL: parseInt(process.env.EVICTION_INTERVAL) || 60000, // 1 minute
96
+ EVICTION_BATCH_SIZE: parseInt(process.env.EVICTION_BATCH_SIZE) || 1000,
97
+
98
+ // WebSocket updates
99
+ QUEUE_DEPTH_UPDATE_INTERVAL: parseInt(process.env.QUEUE_DEPTH_UPDATE_INTERVAL) || 5000, // 5 seconds
100
+ SYSTEM_STATS_UPDATE_INTERVAL: parseInt(process.env.SYSTEM_STATS_UPDATE_INTERVAL) || 10000 // 10 seconds
101
+ };
102
+
103
+ // WebSocket Configuration
104
+ export const WEBSOCKET = {
105
+ COMPRESSION: parseInt(process.env.WS_COMPRESSION) || 0,
106
+ MAX_PAYLOAD_LENGTH: parseInt(process.env.WS_MAX_PAYLOAD_LENGTH) || 16384, // 16KB
107
+ IDLE_TIMEOUT: parseInt(process.env.WS_IDLE_TIMEOUT) || 60, // 60 seconds
108
+ MAX_CONNECTIONS: parseInt(process.env.WS_MAX_CONNECTIONS) || 1000,
109
+ HEARTBEAT_INTERVAL: parseInt(process.env.WS_HEARTBEAT_INTERVAL) || 30000 // 30 seconds
110
+ };
111
+
112
+ // Encryption Configuration
113
+ export const ENCRYPTION = {
114
+ KEY_ENV_VAR: 'QUEEN_ENCRYPTION_KEY',
115
+ ALGORITHM: 'aes-256-gcm',
116
+ KEY_LENGTH: 32, // bytes
117
+ IV_LENGTH: 16 // bytes
118
+ };
119
+
120
+ // Client SDK Configuration
121
+ export const CLIENT = {
122
+ DEFAULT_BASE_URL: process.env.QUEEN_BASE_URL || 'http://localhost:6632',
123
+ DEFAULT_RETRY_ATTEMPTS: parseInt(process.env.CLIENT_RETRY_ATTEMPTS) || 3,
124
+ DEFAULT_RETRY_DELAY: parseInt(process.env.CLIENT_RETRY_DELAY) || 1000, // 1 second
125
+ DEFAULT_RETRY_BACKOFF: parseFloat(process.env.CLIENT_RETRY_BACKOFF) || 2,
126
+ CONNECTION_POOL_SIZE: parseInt(process.env.CLIENT_POOL_SIZE) || 10,
127
+ REQUEST_TIMEOUT: parseInt(process.env.CLIENT_REQUEST_TIMEOUT) || 30000 // 30 seconds
128
+ };
129
+
130
+ // API Configuration
131
+ export const API = {
132
+ MAX_BODY_SIZE: parseInt(process.env.MAX_BODY_SIZE) || 10 * 1024 * 1024, // 10MB max body size
133
+ // Pagination
134
+ DEFAULT_LIMIT: parseInt(process.env.API_DEFAULT_LIMIT) || 100,
135
+ MAX_LIMIT: parseInt(process.env.API_MAX_LIMIT) || 1000,
136
+ DEFAULT_OFFSET: parseInt(process.env.API_DEFAULT_OFFSET) || 0,
137
+
138
+ // CORS
139
+ CORS_MAX_AGE: parseInt(process.env.CORS_MAX_AGE) || 86400, // 24 hours
140
+ CORS_ALLOWED_ORIGINS: process.env.CORS_ALLOWED_ORIGINS || '*',
141
+ CORS_ALLOWED_METHODS: process.env.CORS_ALLOWED_METHODS || 'GET, POST, PUT, DELETE, OPTIONS',
142
+ CORS_ALLOWED_HEADERS: process.env.CORS_ALLOWED_HEADERS || 'Content-Type, Authorization'
143
+ };
144
+
145
+ // Analytics Configuration
146
+ export const ANALYTICS = {
147
+ // Processing time calculations
148
+ RECENT_COMPLETION_HOURS: parseInt(process.env.ANALYTICS_RECENT_HOURS) || 24,
149
+ MIN_COMPLETED_FOR_STATS: parseInt(process.env.ANALYTICS_MIN_COMPLETED) || 5,
150
+
151
+ // Time windows
152
+ RECENT_MESSAGE_WINDOW: parseInt(process.env.RECENT_MESSAGE_WINDOW) || 60, // 1 minute in seconds
153
+ RELATED_MESSAGE_WINDOW: parseInt(process.env.RELATED_MESSAGE_WINDOW) || 3600, // 1 hour in seconds
154
+ MAX_RELATED_MESSAGES: parseInt(process.env.MAX_RELATED_MESSAGES) || 10
155
+ };
156
+
157
+ // Performance Monitoring
158
+ export const MONITORING = {
159
+ ENABLE_REQUEST_COUNTING: process.env.ENABLE_REQUEST_COUNTING !== 'false',
160
+ ENABLE_MESSAGE_COUNTING: process.env.ENABLE_MESSAGE_COUNTING !== 'false',
161
+ METRICS_ENDPOINT_ENABLED: process.env.METRICS_ENDPOINT_ENABLED !== 'false',
162
+ HEALTH_CHECK_ENABLED: process.env.HEALTH_CHECK_ENABLED !== 'false'
163
+ };
164
+
165
+ // Logging Configuration
166
+ export const LOGGING = {
167
+ ENABLE_LOGGING: process.env.ENABLE_LOGGING !== 'false',
168
+ LOG_LEVEL: process.env.LOG_LEVEL || 'info',
169
+ LOG_FORMAT: process.env.LOG_FORMAT || 'json',
170
+ LOG_TIMESTAMP: process.env.LOG_TIMESTAMP !== 'false'
171
+ };
172
+
173
+ // HTTP Response Status Codes
174
+ export const HTTP_STATUS = {
175
+ OK: 200,
176
+ CREATED: 201,
177
+ NO_CONTENT: 204,
178
+ BAD_REQUEST: 400,
179
+ NOT_FOUND: 404,
180
+ INTERNAL_SERVER_ERROR: 500,
181
+ SERVICE_UNAVAILABLE: 503
182
+ };
183
+
184
+ // Export a function to get the complete configuration
185
+ export const getConfig = () => ({
186
+ server: SERVER,
187
+ database: DATABASE,
188
+ queue: QUEUE,
189
+ jobs: JOBS,
190
+ websocket: WEBSOCKET,
191
+ encryption: ENCRYPTION,
192
+ client: CLIENT,
193
+ api: API,
194
+ analytics: ANALYTICS,
195
+ monitoring: MONITORING,
196
+ logging: LOGGING,
197
+ httpStatus: HTTP_STATUS
198
+ });
199
+
200
+ // Export default configuration object
201
+ export default {
202
+ SERVER,
203
+ DATABASE,
204
+ QUEUE,
205
+ SYSTEM_EVENTS,
206
+ JOBS,
207
+ WEBSOCKET,
208
+ ENCRYPTION,
209
+ CLIENT,
210
+ API,
211
+ ANALYTICS,
212
+ MONITORING,
213
+ LOGGING,
214
+ HTTP_STATUS
215
+ };
@@ -0,0 +1,103 @@
1
+ import pg from 'pg';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+
6
+ const { Pool } = pg;
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+
9
+ // Create connection pool
10
+ 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
+ });
21
+ };
22
+
23
+ // Initialize database schema
24
+ export const initDatabase = async (pool) => {
25
+ try {
26
+ // Check if the queen schema already exists
27
+ const schemaCheck = await pool.query(`
28
+ SELECT schema_name
29
+ FROM information_schema.schemata
30
+ WHERE schema_name = 'queen'
31
+ `);
32
+
33
+ if (schemaCheck.rows.length > 0) {
34
+ console.log('Database schema already exists, skipping initialization');
35
+ return;
36
+ }
37
+
38
+ // Schema doesn't exist, create it
39
+ const schemaPath = path.join(__dirname, 'schema-v2.sql');
40
+ const schema = fs.readFileSync(schemaPath, 'utf8');
41
+
42
+ await pool.query(schema);
43
+ console.log('Database schema V2 initialized');
44
+ } catch (error) {
45
+ console.error('Failed to initialize database schema:', error);
46
+ throw error;
47
+ }
48
+ };
49
+
50
+ // Valid transaction isolation levels
51
+ const VALID_ISOLATION_LEVELS = [
52
+ 'READ COMMITTED',
53
+ 'REPEATABLE READ',
54
+ 'SERIALIZABLE'
55
+ ];
56
+
57
+ // Helper for transactions with retry logic
58
+ export const withTransaction = async (pool, callback, isolationLevel = 'READ COMMITTED') => {
59
+ // Validate isolation level
60
+ if (isolationLevel && !VALID_ISOLATION_LEVELS.includes(isolationLevel)) {
61
+ throw new Error(`Invalid isolation level: ${isolationLevel}`);
62
+ }
63
+
64
+ const maxAttempts = 3;
65
+ let attempt = 1;
66
+
67
+ while (attempt <= maxAttempts) {
68
+ const client = await pool.connect();
69
+ try {
70
+ await client.query('BEGIN');
71
+
72
+ if (isolationLevel) {
73
+ await client.query(`SET TRANSACTION ISOLATION LEVEL ${isolationLevel}`);
74
+ }
75
+
76
+ // 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
79
+
80
+ const result = await callback(client);
81
+ await client.query('COMMIT');
82
+ return result;
83
+ } catch (error) {
84
+ await client.query('ROLLBACK');
85
+
86
+ // Check for serialization failure or deadlock
87
+ if ((error.code === '40001' || error.code === '40P01') && attempt < maxAttempts) {
88
+ // Exponential backoff: 100ms, 200ms, 400ms
89
+ const delay = Math.min(100 * Math.pow(2, attempt - 1), 1000);
90
+ await new Promise(resolve => setTimeout(resolve, delay));
91
+ attempt++;
92
+ console.log(`Transaction retry attempt ${attempt}/${maxAttempts} after ${error.code}`);
93
+ continue;
94
+ }
95
+
96
+ throw error;
97
+ } finally {
98
+ client.release();
99
+ }
100
+ }
101
+
102
+ throw new Error('Max transaction retry attempts exceeded');
103
+ };
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Connection Pool Manager for High Concurrency
3
+ *
4
+ * This module provides optimized connection pooling for high-throughput scenarios
5
+ */
6
+
7
+ import pg from 'pg';
8
+ import appConfig from '../config.js';
9
+
10
+ const { Pool } = pg;
11
+
12
+ export class PoolManager {
13
+ constructor(config) {
14
+ this.config = {
15
+ user: appConfig.DATABASE.USER,
16
+ host: appConfig.DATABASE.HOST,
17
+ database: appConfig.DATABASE.DATABASE,
18
+ password: appConfig.DATABASE.PASSWORD,
19
+ port: appConfig.DATABASE.PORT,
20
+ max: appConfig.DATABASE.POOL_SIZE,
21
+ idleTimeoutMillis: appConfig.DATABASE.IDLE_TIMEOUT,
22
+ connectionTimeoutMillis: appConfig.DATABASE.CONNECTION_TIMEOUT,
23
+ ...config
24
+ };
25
+
26
+ this.pool = new Pool(this.config);
27
+ this.waitingQueue = [];
28
+ this.activeConnections = 0;
29
+ this.maxRetries = appConfig.DATABASE.MAX_RETRIES;
30
+
31
+ // Monitor pool events
32
+ this.pool.on('connect', () => {
33
+ this.activeConnections++;
34
+ });
35
+
36
+ this.pool.on('remove', () => {
37
+ this.activeConnections--;
38
+ });
39
+
40
+ this.pool.on('error', (err) => {
41
+ console.error('Unexpected pool error:', err);
42
+ });
43
+ }
44
+
45
+ /**
46
+ * Execute a query with automatic retry and connection management
47
+ */
48
+ async query(text, params, retries = this.maxRetries) {
49
+ for (let attempt = 0; attempt < retries; attempt++) {
50
+ try {
51
+ return await this.pool.query(text, params);
52
+ } catch (error) {
53
+ if (error.message.includes('timeout exceeded when trying to connect')) {
54
+ // Connection timeout - retry with exponential backoff
55
+ if (attempt < retries - 1) {
56
+ const delay = Math.min(100 * Math.pow(2, attempt), 1000);
57
+ await new Promise(resolve => setTimeout(resolve, delay));
58
+ continue;
59
+ }
60
+ }
61
+ throw error;
62
+ }
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Get a client with retry logic
68
+ */
69
+ async getClient(retries = this.maxRetries) {
70
+ for (let attempt = 0; attempt < retries; attempt++) {
71
+ try {
72
+ const client = await this.pool.connect();
73
+
74
+ // Wrap release to ensure proper cleanup
75
+ const originalRelease = client.release.bind(client);
76
+ client.release = (err) => {
77
+ originalRelease(err);
78
+ this.processWaitingQueue();
79
+ };
80
+
81
+ return client;
82
+ } catch (error) {
83
+ if (error.message.includes('timeout exceeded when trying to connect')) {
84
+ if (attempt < retries - 1) {
85
+ const delay = Math.min(100 * Math.pow(2, attempt), 1000);
86
+ await new Promise(resolve => setTimeout(resolve, delay));
87
+ continue;
88
+ }
89
+ }
90
+ throw error;
91
+ }
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Execute a function with a client, ensuring proper cleanup
97
+ */
98
+ async withClient(fn) {
99
+ const client = await this.getClient();
100
+ try {
101
+ return await fn(client);
102
+ } finally {
103
+ client.release();
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Execute multiple queries in parallel with connection management
109
+ */
110
+ async parallelQueries(queries) {
111
+ const promises = queries.map(({ text, params }) =>
112
+ this.query(text, params)
113
+ );
114
+ return Promise.all(promises);
115
+ }
116
+
117
+ /**
118
+ * Batch execute with optimal connection usage
119
+ */
120
+ async batchExecute(items, batchSize, executeFn) {
121
+ const results = [];
122
+ const batches = [];
123
+
124
+ // Split items into batches
125
+ for (let i = 0; i < items.length; i += batchSize) {
126
+ batches.push(items.slice(i, i + batchSize));
127
+ }
128
+
129
+ // Process batches with controlled concurrency
130
+ const concurrency = Math.min(Math.floor(this.config.max / 2), 10);
131
+
132
+ for (let i = 0; i < batches.length; i += concurrency) {
133
+ const batchPromises = batches
134
+ .slice(i, i + concurrency)
135
+ .map(batch => this.withClient(client => executeFn(client, batch)));
136
+
137
+ const batchResults = await Promise.all(batchPromises);
138
+ results.push(...batchResults.flat());
139
+ }
140
+
141
+ return results;
142
+ }
143
+
144
+ /**
145
+ * Process any waiting queue operations
146
+ */
147
+ processWaitingQueue() {
148
+ if (this.waitingQueue.length > 0 && this.pool.idleCount > 0) {
149
+ const { resolve } = this.waitingQueue.shift();
150
+ this.getClient().then(resolve).catch(() => {
151
+ // Re-queue if failed
152
+ this.waitingQueue.push({ resolve });
153
+ });
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Get pool statistics
159
+ */
160
+ getStats() {
161
+ return {
162
+ total: this.pool.totalCount,
163
+ idle: this.pool.idleCount,
164
+ waiting: this.pool.waitingCount,
165
+ active: this.activeConnections,
166
+ queueLength: this.waitingQueue.length,
167
+ config: {
168
+ max: this.config.max,
169
+ connectionTimeout: this.config.connectionTimeoutMillis
170
+ }
171
+ };
172
+ }
173
+
174
+ /**
175
+ * Gracefully close the pool
176
+ */
177
+ async close() {
178
+ await this.pool.end();
179
+ }
180
+ }
181
+
182
+ // Create a singleton instance
183
+ let poolManager;
184
+
185
+ export const getPoolManager = (config) => {
186
+ if (!poolManager) {
187
+ poolManager = new PoolManager(config);
188
+ }
189
+ return poolManager;
190
+ };
191
+
192
+ export default PoolManager;