queen-mq 0.3.0 → 0.4.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/README.md +212 -144
- package/client-js/client-v2/Queen.js +28 -0
- package/client-js/client-v2/README.md +6 -2
- package/client-js/client-v2/builders/QueueBuilder.js +7 -1
- package/client-js/client-v2/builders/TransactionBuilder.js +20 -4
- package/client-js/client-v2/stream/StreamBuilder.js +72 -0
- package/client-js/client-v2/stream/StreamConsumer.js +156 -0
- package/client-js/client-v2/stream/Window.js +140 -0
- package/client-js/test-v2/MAINTENANCE_TEST.md +148 -0
- package/client-js/test-v2/consume.js +1 -0
- package/client-js/test-v2/maintenance.js +261 -0
- package/client-js/test-v2/retention.js +69 -0
- package/client-js/test-v2/run.js +7 -1
- package/package.json +2 -2
- package/client-js/client/client.js +0 -1536
- package/client-js/client/index.js +0 -4
- package/client-js/client/utils/http.js +0 -173
- package/client-js/client/utils/loadBalancer.js +0 -152
- package/client-js/client/utils/retry.js +0 -41
- package/client-js/services/encryptionService.js +0 -82
- package/client-js/services/evictionService.js +0 -160
- package/client-js/services/retentionService.js +0 -162
- package/client-js/services/startupSync.js +0 -35
- package/client-js/test/README.md +0 -224
- package/client-js/test/advanced-client-tests.js +0 -761
- package/client-js/test/advanced-pattern-tests.js +0 -1137
- package/client-js/test/bus-mode-tests.js +0 -361
- package/client-js/test/core-tests.js +0 -457
- package/client-js/test/edge-case-tests.js +0 -562
- package/client-js/test/enterprise-tests.js +0 -637
- package/client-js/test/human.js +0 -162
- package/client-js/test/partition-locking-tests.js +0 -545
- package/client-js/test/partition-transaction-tests.js +0 -482
- package/client-js/test/qos0-tests.js +0 -334
- package/client-js/test/test-new.js +0 -370
- package/client-js/test/utils.js +0 -169
- package/client-js/test/window-buffer-test.js +0 -114
- package/client-js/utils/logger.js +0 -44
- package/client-js/utils/uuid.js +0 -5
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
export const httpRequest = async (url, options = {}) => {
|
|
2
|
-
//console.log('httpRequest', url, options);
|
|
3
|
-
const response = await fetch(url, {
|
|
4
|
-
...options,
|
|
5
|
-
headers: {
|
|
6
|
-
'Content-Type': 'application/json',
|
|
7
|
-
...options.headers
|
|
8
|
-
}
|
|
9
|
-
});
|
|
10
|
-
//console.log('response', response);
|
|
11
|
-
// Handle 204 No Content
|
|
12
|
-
if (response.status === 204) {
|
|
13
|
-
return null; // No content
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// Handle errors
|
|
17
|
-
if (!response.ok) {
|
|
18
|
-
const error = new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
19
|
-
error.status = response.status;
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
const text = await response.text();
|
|
23
|
-
if (text) {
|
|
24
|
-
const body = JSON.parse(text);
|
|
25
|
-
error.message = body.error || error.message;
|
|
26
|
-
}
|
|
27
|
-
} catch (e) {
|
|
28
|
-
// Ignore JSON parse errors
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
throw error;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Handle successful responses
|
|
35
|
-
const contentType = response.headers.get('content-type');
|
|
36
|
-
const contentLength = response.headers.get('content-length');
|
|
37
|
-
|
|
38
|
-
// Check if there's actually content to parse
|
|
39
|
-
if (!contentType || !contentType.includes('application/json') || contentLength === '0') {
|
|
40
|
-
const text = await response.text();
|
|
41
|
-
if (!text || text.length === 0) {
|
|
42
|
-
return null; // Empty response
|
|
43
|
-
}
|
|
44
|
-
try {
|
|
45
|
-
return JSON.parse(text);
|
|
46
|
-
} catch (e) {
|
|
47
|
-
console.warn('Failed to parse response as JSON:', text);
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return response.json();
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
export const createHttpClient = ({ baseUrl, timeout = 30000 }) => {
|
|
56
|
-
const request = (method, path, body = null, requestTimeout = null) => {
|
|
57
|
-
const effectiveTimeout = requestTimeout || timeout;
|
|
58
|
-
const controller = new AbortController();
|
|
59
|
-
const timeoutId = setTimeout(() => controller.abort(), effectiveTimeout);
|
|
60
|
-
|
|
61
|
-
const options = {
|
|
62
|
-
method,
|
|
63
|
-
signal: controller.signal
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
if (body) {
|
|
67
|
-
options.body = JSON.stringify(body);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return httpRequest(`${baseUrl}${path}`, options)
|
|
71
|
-
.catch(error => {
|
|
72
|
-
// Enhance abort errors with more context
|
|
73
|
-
if (error.name === 'AbortError') {
|
|
74
|
-
const timeoutError = new Error(`Request timeout after ${effectiveTimeout}ms`);
|
|
75
|
-
timeoutError.name = 'AbortError';
|
|
76
|
-
timeoutError.timeout = effectiveTimeout;
|
|
77
|
-
throw timeoutError;
|
|
78
|
-
}
|
|
79
|
-
throw error;
|
|
80
|
-
})
|
|
81
|
-
.finally(() => clearTimeout(timeoutId));
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
return {
|
|
85
|
-
get: (path, requestTimeout) => request('GET', path, null, requestTimeout),
|
|
86
|
-
post: (path, body, requestTimeout) => request('POST', path, body, requestTimeout),
|
|
87
|
-
put: (path, body, requestTimeout) => request('PUT', path, body, requestTimeout),
|
|
88
|
-
delete: (path, requestTimeout) => request('DELETE', path, null, requestTimeout)
|
|
89
|
-
};
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Create an HTTP client with load balancing support
|
|
94
|
-
*/
|
|
95
|
-
export const createLoadBalancedHttpClient = ({ baseUrls, loadBalancer, timeout = 30000, enableFailover = true }) => {
|
|
96
|
-
const request = async (method, path, body = null, requestTimeout = null) => {
|
|
97
|
-
const effectiveTimeout = requestTimeout || timeout;
|
|
98
|
-
|
|
99
|
-
const executeRequest = async (baseUrl) => {
|
|
100
|
-
const controller = new AbortController();
|
|
101
|
-
const timeoutId = setTimeout(() => controller.abort(), effectiveTimeout);
|
|
102
|
-
|
|
103
|
-
const options = {
|
|
104
|
-
method,
|
|
105
|
-
signal: controller.signal
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
if (body) {
|
|
109
|
-
options.body = JSON.stringify(body);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
try {
|
|
113
|
-
return await httpRequest(`${baseUrl}${path}`, options);
|
|
114
|
-
} catch (error) {
|
|
115
|
-
// Enhance abort errors with more context
|
|
116
|
-
if (error.name === 'AbortError') {
|
|
117
|
-
const timeoutError = new Error(`Request timeout after ${effectiveTimeout}ms`);
|
|
118
|
-
timeoutError.name = 'AbortError';
|
|
119
|
-
timeoutError.timeout = effectiveTimeout;
|
|
120
|
-
throw timeoutError;
|
|
121
|
-
}
|
|
122
|
-
throw error;
|
|
123
|
-
} finally {
|
|
124
|
-
clearTimeout(timeoutId);
|
|
125
|
-
}
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
if (enableFailover) {
|
|
129
|
-
// Try with failover to different servers
|
|
130
|
-
const attemptedUrls = new Set();
|
|
131
|
-
const urls = loadBalancer.getAllUrls();
|
|
132
|
-
let lastError = null;
|
|
133
|
-
|
|
134
|
-
for (let attempt = 0; attempt < urls.length; attempt++) {
|
|
135
|
-
const url = loadBalancer.getNextUrl();
|
|
136
|
-
|
|
137
|
-
if (attemptedUrls.has(url)) {
|
|
138
|
-
continue;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
attemptedUrls.add(url);
|
|
142
|
-
|
|
143
|
-
try {
|
|
144
|
-
return await executeRequest(url);
|
|
145
|
-
} catch (error) {
|
|
146
|
-
lastError = error;
|
|
147
|
-
console.warn(`Request failed for ${url}: ${method} ${path} - ${error.message}`);
|
|
148
|
-
|
|
149
|
-
// Don't retry on client errors (4xx)
|
|
150
|
-
if (error.status && error.status >= 400 && error.status < 500) {
|
|
151
|
-
throw error;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Continue to next server for server errors or network issues
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
throw lastError || new Error('All servers failed');
|
|
159
|
-
} else {
|
|
160
|
-
// No failover, just use the next URL from load balancer
|
|
161
|
-
const baseUrl = loadBalancer.getNextUrl();
|
|
162
|
-
return executeRequest(baseUrl);
|
|
163
|
-
}
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
return {
|
|
167
|
-
get: (path, requestTimeout) => request('GET', path, null, requestTimeout),
|
|
168
|
-
post: (path, body, requestTimeout) => request('POST', path, body, requestTimeout),
|
|
169
|
-
put: (path, body, requestTimeout) => request('PUT', path, body, requestTimeout),
|
|
170
|
-
delete: (path, requestTimeout) => request('DELETE', path, null, requestTimeout),
|
|
171
|
-
getLoadBalancer: () => loadBalancer
|
|
172
|
-
};
|
|
173
|
-
};
|
|
@@ -1,152 +0,0 @@
|
|
|
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
|
-
};
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
const config = {
|
|
2
|
-
CLIENT: {
|
|
3
|
-
DEFAULT_RETRY_ATTEMPTS: 3,
|
|
4
|
-
DEFAULT_RETRY_DELAY: 1000,
|
|
5
|
-
DEFAULT_RETRY_BACKOFF: 2
|
|
6
|
-
}
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export const withRetry = async (fn, attempts = config.CLIENT.DEFAULT_RETRY_ATTEMPTS, delay = config.CLIENT.DEFAULT_RETRY_DELAY, backoff = config.CLIENT.DEFAULT_RETRY_BACKOFF) => {
|
|
10
|
-
let lastError;
|
|
11
|
-
|
|
12
|
-
for (let i = 0; i < attempts; i++) {
|
|
13
|
-
try {
|
|
14
|
-
return await fn();
|
|
15
|
-
} catch (error) {
|
|
16
|
-
lastError = error;
|
|
17
|
-
|
|
18
|
-
// Don't retry on client errors (4xx)
|
|
19
|
-
if (error.status && error.status >= 400 && error.status < 500) {
|
|
20
|
-
throw error;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if (i < attempts - 1) {
|
|
24
|
-
const waitTime = delay * Math.pow(backoff, i);
|
|
25
|
-
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
throw lastError;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export const createRetryableFunction = (fn, options = {}) => {
|
|
34
|
-
const {
|
|
35
|
-
attempts = config.CLIENT.DEFAULT_RETRY_ATTEMPTS,
|
|
36
|
-
delay = config.CLIENT.DEFAULT_RETRY_DELAY,
|
|
37
|
-
backoff = config.CLIENT.DEFAULT_RETRY_BACKOFF
|
|
38
|
-
} = options;
|
|
39
|
-
|
|
40
|
-
return (...args) => withRetry(() => fn(...args), attempts, delay, backoff);
|
|
41
|
-
};
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import crypto from 'crypto';
|
|
2
|
-
import { log } from '../utils/logger.js';
|
|
3
|
-
|
|
4
|
-
const ALGORITHM = 'aes-256-gcm';
|
|
5
|
-
const KEY_ENV = 'QUEEN_ENCRYPTION_KEY';
|
|
6
|
-
|
|
7
|
-
// Get and validate the encryption key
|
|
8
|
-
const getKey = () => {
|
|
9
|
-
const keyHex = process.env[KEY_ENV];
|
|
10
|
-
if (!keyHex) return null;
|
|
11
|
-
|
|
12
|
-
try {
|
|
13
|
-
const key = Buffer.from(keyHex, 'hex');
|
|
14
|
-
if (key.length !== 32) {
|
|
15
|
-
log(`ERROR: ${KEY_ENV} must be 32 bytes (64 hex characters)`);
|
|
16
|
-
return null;
|
|
17
|
-
}
|
|
18
|
-
return key;
|
|
19
|
-
} catch (error) {
|
|
20
|
-
log(`ERROR: Invalid ${KEY_ENV} format:`, error.message);
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
// Encrypt a payload
|
|
26
|
-
export const encryptPayload = async (payload) => {
|
|
27
|
-
const key = getKey();
|
|
28
|
-
if (!key) throw new Error('Encryption key not configured');
|
|
29
|
-
|
|
30
|
-
const iv = crypto.randomBytes(16);
|
|
31
|
-
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
32
|
-
|
|
33
|
-
const payloadStr = JSON.stringify(payload);
|
|
34
|
-
const encrypted = Buffer.concat([
|
|
35
|
-
cipher.update(payloadStr, 'utf8'),
|
|
36
|
-
cipher.final()
|
|
37
|
-
]);
|
|
38
|
-
|
|
39
|
-
return {
|
|
40
|
-
encrypted: encrypted.toString('base64'),
|
|
41
|
-
iv: iv.toString('base64'),
|
|
42
|
-
authTag: cipher.getAuthTag().toString('base64')
|
|
43
|
-
};
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
// Decrypt a payload
|
|
47
|
-
export const decryptPayload = async (encryptedData) => {
|
|
48
|
-
const key = getKey();
|
|
49
|
-
if (!key) throw new Error('Encryption key not configured');
|
|
50
|
-
|
|
51
|
-
const decipher = crypto.createDecipheriv(
|
|
52
|
-
ALGORITHM,
|
|
53
|
-
key,
|
|
54
|
-
Buffer.from(encryptedData.iv, 'base64')
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
decipher.setAuthTag(Buffer.from(encryptedData.authTag, 'base64'));
|
|
58
|
-
|
|
59
|
-
const decrypted = Buffer.concat([
|
|
60
|
-
decipher.update(Buffer.from(encryptedData.encrypted, 'base64')),
|
|
61
|
-
decipher.final()
|
|
62
|
-
]);
|
|
63
|
-
|
|
64
|
-
return JSON.parse(decrypted.toString('utf8'));
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
// Check if encryption is available
|
|
68
|
-
export const isEncryptionEnabled = () => {
|
|
69
|
-
const key = getKey();
|
|
70
|
-
return key !== null;
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
// Initialize and log status
|
|
74
|
-
export const initEncryption = () => {
|
|
75
|
-
const enabled = isEncryptionEnabled();
|
|
76
|
-
if (enabled) {
|
|
77
|
-
log('✅ Encryption service initialized');
|
|
78
|
-
} else {
|
|
79
|
-
log('⚠️ Encryption service disabled (QUEEN_ENCRYPTION_KEY not set)');
|
|
80
|
-
}
|
|
81
|
-
return enabled;
|
|
82
|
-
};
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
// Functional eviction service for stale messages
|
|
2
|
-
import { log, LogTypes } from '../utils/logger.js';
|
|
3
|
-
import config from '../config.js';
|
|
4
|
-
|
|
5
|
-
const EVICTION_INTERVAL = config.JOBS.EVICTION_INTERVAL;
|
|
6
|
-
|
|
7
|
-
// Evict messages that exceeded max wait time for a queue
|
|
8
|
-
// Note: Eviction is already handled by POP query filtering old messages
|
|
9
|
-
// This service optionally moves them to DLQ for visibility
|
|
10
|
-
const evictMessages = async (client, queueName, maxWaitTimeSeconds) => {
|
|
11
|
-
if (!maxWaitTimeSeconds || maxWaitTimeSeconds <= 0) return 0;
|
|
12
|
-
|
|
13
|
-
try {
|
|
14
|
-
// Find old messages that haven't been consumed and move to DLQ
|
|
15
|
-
// Use a WHERE EXISTS check to ensure message still exists before inserting into DLQ
|
|
16
|
-
const result = await client.query(`
|
|
17
|
-
WITH queue_partitions AS (
|
|
18
|
-
SELECT p.id, p.queue_id
|
|
19
|
-
FROM queen.partitions p
|
|
20
|
-
JOIN queen.queues q ON p.queue_id = q.id
|
|
21
|
-
WHERE q.name = $1
|
|
22
|
-
),
|
|
23
|
-
old_unconsumed_messages AS (
|
|
24
|
-
SELECT m.id, m.partition_id, m.created_at
|
|
25
|
-
FROM queen.messages m
|
|
26
|
-
JOIN queue_partitions qp ON m.partition_id = qp.id
|
|
27
|
-
LEFT JOIN queen.partition_consumers pc ON pc.partition_id = m.partition_id
|
|
28
|
-
AND pc.consumer_group = '__QUEUE_MODE__'
|
|
29
|
-
WHERE m.created_at < NOW() - INTERVAL '1 second' * $2
|
|
30
|
-
-- Message has not been consumed yet
|
|
31
|
-
AND ((m.created_at, m.id) > (COALESCE(pc.last_consumed_created_at, '1970-01-01'::timestamptz),
|
|
32
|
-
COALESCE(pc.last_consumed_id, '00000000-0000-0000-0000-000000000000'::uuid))
|
|
33
|
-
OR pc.last_consumed_id IS NULL)
|
|
34
|
-
)
|
|
35
|
-
INSERT INTO queen.dead_letter_queue (
|
|
36
|
-
message_id,
|
|
37
|
-
partition_id,
|
|
38
|
-
consumer_group,
|
|
39
|
-
error_message,
|
|
40
|
-
original_created_at
|
|
41
|
-
)
|
|
42
|
-
SELECT
|
|
43
|
-
om.id,
|
|
44
|
-
om.partition_id,
|
|
45
|
-
'__QUEUE_MODE__',
|
|
46
|
-
'Message exceeded maximum wait time',
|
|
47
|
-
om.created_at
|
|
48
|
-
FROM old_unconsumed_messages om
|
|
49
|
-
WHERE EXISTS (
|
|
50
|
-
SELECT 1 FROM queen.messages m WHERE m.id = om.id
|
|
51
|
-
)
|
|
52
|
-
ON CONFLICT DO NOTHING
|
|
53
|
-
RETURNING message_id
|
|
54
|
-
`, [queueName, maxWaitTimeSeconds]);
|
|
55
|
-
|
|
56
|
-
return result.rowCount || 0;
|
|
57
|
-
} catch (error) {
|
|
58
|
-
// Catch foreign key violations (messages deleted between SELECT and INSERT)
|
|
59
|
-
if (error.code === '23503') {
|
|
60
|
-
// Foreign key constraint violation - message was deleted
|
|
61
|
-
// This is a harmless race condition, just log it
|
|
62
|
-
log(`${LogTypes.EVICTION} | Queue: ${queueName} | Warning: Message deleted during eviction (race condition)`);
|
|
63
|
-
return 0;
|
|
64
|
-
}
|
|
65
|
-
// Re-throw other errors
|
|
66
|
-
throw error;
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
// Evict messages during pop operation (inline eviction)
|
|
71
|
-
export const evictOnPop = async (client, queueName) => {
|
|
72
|
-
const queueResult = await client.query(
|
|
73
|
-
'SELECT max_wait_time_seconds FROM queen.queues WHERE name = $1',
|
|
74
|
-
[queueName]
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
if (!queueResult.rows[0]) return 0;
|
|
78
|
-
|
|
79
|
-
const maxWaitTimeSeconds = queueResult.rows[0].max_wait_time_seconds;
|
|
80
|
-
const evictedCount = await evictMessages(client, queueName, maxWaitTimeSeconds);
|
|
81
|
-
|
|
82
|
-
if (evictedCount > 0) {
|
|
83
|
-
log(`${LogTypes.EVICTION} | Queue: ${queueName} | Count: ${evictedCount} | MaxWaitTime: ${maxWaitTimeSeconds}s`);
|
|
84
|
-
|
|
85
|
-
// Log to retention history (reusing the table)
|
|
86
|
-
await client.query(`
|
|
87
|
-
INSERT INTO queen.retention_history (messages_deleted, retention_type)
|
|
88
|
-
VALUES ($1, 'evicted')
|
|
89
|
-
`, [evictedCount]);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return evictedCount;
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
// Background eviction for all queues
|
|
96
|
-
const performEviction = async (pool) => {
|
|
97
|
-
const client = await pool.connect();
|
|
98
|
-
|
|
99
|
-
try {
|
|
100
|
-
// Get all queues with eviction configured
|
|
101
|
-
const queuesResult = await client.query(`
|
|
102
|
-
SELECT name, max_wait_time_seconds
|
|
103
|
-
FROM queen.queues
|
|
104
|
-
WHERE max_wait_time_seconds > 0
|
|
105
|
-
`);
|
|
106
|
-
|
|
107
|
-
let totalEvicted = 0;
|
|
108
|
-
for (const queue of queuesResult.rows) {
|
|
109
|
-
const evicted = await evictMessages(
|
|
110
|
-
client,
|
|
111
|
-
queue.name,
|
|
112
|
-
queue.max_wait_time_seconds
|
|
113
|
-
);
|
|
114
|
-
|
|
115
|
-
if (evicted > 0) {
|
|
116
|
-
log(`${LogTypes.EVICTION} | Queue: ${queue.name} | Count: ${evicted} | MaxWaitTime: ${queue.max_wait_time_seconds}s`);
|
|
117
|
-
totalEvicted += evicted;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (totalEvicted > 0) {
|
|
122
|
-
await client.query(`
|
|
123
|
-
INSERT INTO queen.retention_history (messages_deleted, retention_type)
|
|
124
|
-
VALUES ($1, 'evicted')
|
|
125
|
-
`, [totalEvicted]);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return totalEvicted;
|
|
129
|
-
} catch (error) {
|
|
130
|
-
log('Eviction error:', error);
|
|
131
|
-
return 0;
|
|
132
|
-
} finally {
|
|
133
|
-
client.release();
|
|
134
|
-
}
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
// Start the eviction job (optional background job)
|
|
138
|
-
export const startEvictionJob = (pool, eventManager) => {
|
|
139
|
-
const intervalId = setInterval(async () => {
|
|
140
|
-
try {
|
|
141
|
-
const evicted = await performEviction(pool);
|
|
142
|
-
if (evicted > 0 && eventManager) {
|
|
143
|
-
eventManager.emit('messages:evicted', { count: evicted });
|
|
144
|
-
}
|
|
145
|
-
} catch (error) {
|
|
146
|
-
log('Eviction job error:', error);
|
|
147
|
-
}
|
|
148
|
-
}, EVICTION_INTERVAL);
|
|
149
|
-
|
|
150
|
-
log(`⏰ Eviction job started (interval: ${EVICTION_INTERVAL}ms)`);
|
|
151
|
-
|
|
152
|
-
// Return cleanup function
|
|
153
|
-
return () => {
|
|
154
|
-
clearInterval(intervalId);
|
|
155
|
-
log('Eviction job stopped');
|
|
156
|
-
};
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
// Export for manual execution
|
|
160
|
-
export const runEviction = performEviction;
|