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.
- package/API.md +1116 -0
- package/CACHE.md +519 -0
- package/DASHBOARD-V3.md +478 -0
- package/DASHBOARD.md +382 -0
- package/MOD_QUEUE.md +453 -0
- package/PARTITION_LOCKING_DESIGN.md +989 -0
- package/PLAN.md +707 -0
- package/QUERY_ANALSYS.md +72 -0
- package/QUEUE_BUS.md +334 -0
- package/README.md +1495 -0
- package/V2-PLAN.md +236 -0
- package/assets/dashboard.png +0 -0
- package/dashboard/.vscode/extensions.json +3 -0
- package/dashboard/README.md +5 -0
- package/dashboard/index.html +14 -0
- package/dashboard/package-lock.json +1458 -0
- package/dashboard/package.json +25 -0
- package/dashboard/public/vite.svg +1 -0
- package/dashboard/src/App.vue +29 -0
- package/dashboard/src/assets/styles/main.css +908 -0
- package/dashboard/src/assets/vue.svg +1 -0
- package/dashboard/src/components/cards/MetricCard.vue +298 -0
- package/dashboard/src/components/charts/QueueDepthChart.vue +276 -0
- package/dashboard/src/components/charts/QueueLagChart.vue +436 -0
- package/dashboard/src/components/charts/ThroughputChart.vue +302 -0
- package/dashboard/src/components/common/ActivityFeed.vue +251 -0
- package/dashboard/src/components/layout/AppHeader.vue +208 -0
- package/dashboard/src/components/layout/AppLayout.vue +88 -0
- package/dashboard/src/components/layout/AppSidebar.vue +261 -0
- package/dashboard/src/main.js +44 -0
- package/dashboard/src/router.js +54 -0
- package/dashboard/src/services/api.js +187 -0
- package/dashboard/src/services/websocket.js +167 -0
- package/dashboard/src/utils/constants.js +56 -0
- package/dashboard/src/utils/helpers.js +118 -0
- package/dashboard/src/views/Analytics.vue +912 -0
- package/dashboard/src/views/Dashboard.vue +906 -0
- package/dashboard/src/views/Messages.vue +437 -0
- package/dashboard/src/views/QueueDetail.vue +501 -0
- package/dashboard/src/views/Queues.vue +333 -0
- package/dashboard/vite.config.js +30 -0
- package/debug-namespace.js +110 -0
- package/docs/long-polling.md +159 -0
- package/docs/multi-server-cache-solutions.md +185 -0
- package/docs/performance-tuning.md +222 -0
- package/examples/bus-mode.js +239 -0
- package/examples/continuous-consumer-optimized.js +215 -0
- package/examples/continuous-consumer.js +159 -0
- package/examples/continuous-producer.js +343 -0
- package/examples/mixed-mode.js +277 -0
- package/examples/multi-server-test.js +305 -0
- package/examples/single.js +64 -0
- package/examples/smartchat-dealyed.js +42 -0
- package/examples/smartchat.js +52 -0
- package/examples/test-cache-invalidation.js +119 -0
- package/examples/test-cache-multi-server.js +245 -0
- package/examples/test-minimal-client.js +112 -0
- package/examples/test-queue-creation-policy.js +137 -0
- package/init-db.js +20 -0
- package/package.json +36 -0
- package/src/client/client.js +291 -0
- package/src/client/index.js +6 -0
- package/src/client/queenClient.js +513 -0
- package/src/client/utils/http.js +172 -0
- package/src/client/utils/loadBalancer.js +152 -0
- package/src/client/utils/retry.js +35 -0
- package/src/config.js +215 -0
- package/src/database/connection.js +103 -0
- package/src/database/poolManager.js +192 -0
- package/src/database/schema-v2.sql +214 -0
- package/src/managers/eventManager.js +59 -0
- package/src/managers/queueManagerOptimized.js +1512 -0
- package/src/managers/resourceCache.js +96 -0
- package/src/managers/systemEventManager.js +127 -0
- package/src/routes/ack.js +26 -0
- package/src/routes/analytics.js +812 -0
- package/src/routes/configure.js +46 -0
- package/src/routes/messages.js +298 -0
- package/src/routes/pop.js +85 -0
- package/src/routes/push.js +28 -0
- package/src/routes/resources.js +296 -0
- package/src/server.js +1286 -0
- package/src/services/encryptionService.js +82 -0
- package/src/services/evictionService.js +131 -0
- package/src/services/retentionService.js +129 -0
- package/src/services/startupSync.js +35 -0
- package/src/test/test.js +4521 -0
- package/src/utils/logger.js +44 -0
- package/src/utils/uuid.js +5 -0
- 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;
|