queen-mq 0.2.0 → 0.2.22
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 +186 -763
- package/{src → client-js}/benchmark/consumer.js +6 -4
- package/{src → client-js}/benchmark/producer.js +9 -4
- package/{src → client-js}/client/client.js +277 -7
- package/{src → client-js}/client/index.js +1 -4
- package/{src → client-js}/client/utils/http.js +3 -2
- package/{src → client-js}/client/utils/retry.js +7 -1
- package/{src → client-js}/test/README.md +30 -9
- package/{src → client-js}/test/core-tests.js +7 -2
- package/{src → client-js}/test/edge-case-tests.js +2 -1
- package/client-js/test/human.js +162 -0
- package/client-js/test/partition-transaction-tests.js +482 -0
- package/client-js/test/qos0-tests.js +334 -0
- package/{src → client-js}/test/test-new.js +55 -0
- package/package.json +8 -17
- package/init-db.js +0 -20
- package/src/cluster-server.js +0 -242
- package/src/config.js +0 -229
- package/src/database/connection.js +0 -129
- package/src/database/poolManager.js +0 -199
- package/src/database/schema-v2.sql +0 -278
- package/src/managers/eventManager.js +0 -59
- package/src/managers/queueManagerOptimized.js +0 -1634
- package/src/managers/resourceCache.js +0 -96
- package/src/managers/systemEventManager.js +0 -132
- package/src/routes/ack.js +0 -26
- package/src/routes/configure.js +0 -46
- package/src/routes/messages.js +0 -368
- package/src/routes/pop.js +0 -69
- package/src/routes/push.js +0 -28
- package/src/routes/resources.js +0 -330
- package/src/routes/status.js +0 -1037
- package/src/server.js +0 -1633
- package/src/test/MIGRATION_ISSUES.md +0 -174
- package/src/test/test.js +0 -4524
- package/src/utils/streaming.js +0 -231
- package/src/webapp-dist/assets/Analytics-DJrE4Rz2.css +0 -1
- package/src/webapp-dist/assets/Analytics-HEMXKmfJ.js +0 -1
- package/src/webapp-dist/assets/ConfirmDialog-YEoCdE7x.js +0 -1
- package/src/webapp-dist/assets/ConsumerGroups-CC5VJywp.css +0 -1
- package/src/webapp-dist/assets/ConsumerGroups-D8Z20HhZ.js +0 -1
- package/src/webapp-dist/assets/Dashboard-BRFOz6it.css +0 -1
- package/src/webapp-dist/assets/Dashboard-Bpx9I32h.js +0 -1
- package/src/webapp-dist/assets/LoadingSpinner-BPttXcbs.js +0 -1
- package/src/webapp-dist/assets/Messages-BOfVHKma.css +0 -1
- package/src/webapp-dist/assets/Messages-EKURCbLl.js +0 -1
- package/src/webapp-dist/assets/QueueDetail-BXXA7xoZ.css +0 -1
- package/src/webapp-dist/assets/QueueDetail-Bmxg0KiA.js +0 -1
- package/src/webapp-dist/assets/Queues-CvrJkrZT.css +0 -1
- package/src/webapp-dist/assets/Queues-yUx9_3-9.js +0 -1
- package/src/webapp-dist/assets/StatusBadge-C1bBtXHh.css +0 -1
- package/src/webapp-dist/assets/StatusBadge-C6N84XmO.js +0 -1
- package/src/webapp-dist/assets/analytics-BapTekLM.js +0 -1
- package/src/webapp-dist/assets/colors-5z6Q_kUr.js +0 -18
- package/src/webapp-dist/assets/index-B7w2bKT6.css +0 -1
- package/src/webapp-dist/assets/index-CUSnwYTH.js +0 -31
- package/src/webapp-dist/assets/messages-CRV6E-xU.js +0 -1
- package/src/webapp-dist/assets/queen-logo-blue.svg +0 -210
- package/src/webapp-dist/assets/queen-logo-cyan.svg +0 -210
- package/src/webapp-dist/assets/queen-logo-indigo.svg +0 -210
- package/src/webapp-dist/assets/queen-logo-orange.svg +0 -210
- package/src/webapp-dist/assets/queen-logo-pink.svg +0 -210
- package/src/webapp-dist/assets/queen-logo-purple.svg +0 -210
- package/src/webapp-dist/assets/queen-logo-rose.svg +0 -239
- package/src/webapp-dist/assets/queen-logo.svg +0 -263
- package/src/webapp-dist/assets/queues-BdUbxfBC.js +0 -1
- package/src/webapp-dist/assets/resources-Bs9U-ceF.js +0 -1
- package/src/webapp-dist/index.html +0 -15
- package/src/websocket/wsServer.js +0 -228
- /package/{src → client-js}/benchmark/consumer_multi.js +0 -0
- /package/{src → client-js}/benchmark/producer_multi.js +0 -0
- /package/{src → client-js}/client/utils/loadBalancer.js +0 -0
- /package/{src → client-js}/services/encryptionService.js +0 -0
- /package/{src → client-js}/services/evictionService.js +0 -0
- /package/{src → client-js}/services/retentionService.js +0 -0
- /package/{src → client-js}/services/startupSync.js +0 -0
- /package/{src → client-js}/test/advanced-client-tests.js +0 -0
- /package/{src → client-js}/test/advanced-pattern-tests.js +0 -0
- /package/{src → client-js}/test/bus-mode-tests.js +0 -0
- /package/{src → client-js}/test/enterprise-tests.js +0 -0
- /package/{src → client-js}/test/partition-locking-tests.js +0 -0
- /package/{src → client-js}/test/utils.js +0 -0
- /package/{src → client-js}/test/window-buffer-test.js +0 -0
- /package/{src → client-js}/utils/logger.js +0 -0
- /package/{src → client-js}/utils/uuid.js +0 -0
|
@@ -1,1634 +0,0 @@
|
|
|
1
|
-
import { generateUUID } from '../utils/uuid.js';
|
|
2
|
-
import { withTransaction } from '../database/connection.js';
|
|
3
|
-
import * as encryption from '../services/encryptionService.js';
|
|
4
|
-
import { log, LogTypes } from '../utils/logger.js';
|
|
5
|
-
import config from '../config.js';
|
|
6
|
-
import { EventTypes } from './systemEventManager.js';
|
|
7
|
-
|
|
8
|
-
export const createOptimizedQueueManager = (pool, resourceCache, eventManager, systemEventManager = null) => {
|
|
9
|
-
|
|
10
|
-
// Batch size for database operations
|
|
11
|
-
const BATCH_INSERT_SIZE = config.QUEUE.BATCH_INSERT_SIZE;
|
|
12
|
-
|
|
13
|
-
// Performance metrics tracking for filtered pops
|
|
14
|
-
const popMetrics = {
|
|
15
|
-
partitionQueryTimes: [],
|
|
16
|
-
leaseAttempts: 0,
|
|
17
|
-
leaseSuccesses: 0,
|
|
18
|
-
messageQueryTimes: [],
|
|
19
|
-
totalPopTimes: [],
|
|
20
|
-
batchSizes: [],
|
|
21
|
-
lastLogTime: Date.now()
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
// Performance metrics for direct pops (queue/partition)
|
|
25
|
-
const directPopMetrics = {
|
|
26
|
-
leaseAcquisitionTimes: [],
|
|
27
|
-
messageQueryTimes: [],
|
|
28
|
-
statusUpdateTimes: [],
|
|
29
|
-
leaseUpdateTimes: [],
|
|
30
|
-
decryptionTimes: [],
|
|
31
|
-
totalPopTimes: [],
|
|
32
|
-
batchSizes: [],
|
|
33
|
-
lastLogTime: Date.now()
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
// Log metrics periodically (every 5 seconds OR after 10 samples)
|
|
37
|
-
const logMetrics = () => {
|
|
38
|
-
const now = Date.now();
|
|
39
|
-
const timeSinceLastLog = now - Math.max(popMetrics.lastLogTime, directPopMetrics.lastLogTime);
|
|
40
|
-
const hasEnoughSamples = popMetrics.totalPopTimes.length >= 10 || directPopMetrics.totalPopTimes.length >= 10;
|
|
41
|
-
|
|
42
|
-
// Only log if enough time passed OR we have enough samples
|
|
43
|
-
if (timeSinceLastLog < 5000 && !hasEnoughSamples) return;
|
|
44
|
-
|
|
45
|
-
if (popMetrics.totalPopTimes.length === 0 && directPopMetrics.totalPopTimes.length === 0) return; // No data yet
|
|
46
|
-
|
|
47
|
-
const avg = arr => arr.length > 0 ? (arr.reduce((a,b) => a+b, 0) / arr.length).toFixed(2) : 0;
|
|
48
|
-
const max = arr => arr.length > 0 ? Math.max(...arr).toFixed(2) : 0;
|
|
49
|
-
const min = arr => arr.length > 0 ? Math.min(...arr).toFixed(2) : 0;
|
|
50
|
-
|
|
51
|
-
if (popMetrics.totalPopTimes.length > 0) {
|
|
52
|
-
log(`📊 PERF METRICS (filtered pop) | Samples: ${popMetrics.totalPopTimes.length}`);
|
|
53
|
-
log(` - Partition Query: avg=${avg(popMetrics.partitionQueryTimes)}ms max=${max(popMetrics.partitionQueryTimes)}ms`);
|
|
54
|
-
log(` - Message Query: avg=${avg(popMetrics.messageQueryTimes)}ms max=${max(popMetrics.messageQueryTimes)}ms`);
|
|
55
|
-
log(` - Total Pop: avg=${avg(popMetrics.totalPopTimes)}ms max=${max(popMetrics.totalPopTimes)}ms`);
|
|
56
|
-
log(` - Lease Success Rate: ${popMetrics.leaseSuccesses}/${popMetrics.leaseAttempts} (${(popMetrics.leaseSuccesses/popMetrics.leaseAttempts*100).toFixed(1)}%)`);
|
|
57
|
-
log(` - Avg Batch Size: ${avg(popMetrics.batchSizes)} messages`);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (directPopMetrics.totalPopTimes.length > 0) {
|
|
61
|
-
log(`📊 PERF METRICS (direct pop) | Samples: ${directPopMetrics.totalPopTimes.length}`);
|
|
62
|
-
log(` - Lease Acquisition: avg=${avg(directPopMetrics.leaseAcquisitionTimes)}ms max=${max(directPopMetrics.leaseAcquisitionTimes)}ms`);
|
|
63
|
-
log(` - Message Query: avg=${avg(directPopMetrics.messageQueryTimes)}ms max=${max(directPopMetrics.messageQueryTimes)}ms`);
|
|
64
|
-
log(` - Status Update: avg=${avg(directPopMetrics.statusUpdateTimes)}ms max=${max(directPopMetrics.statusUpdateTimes)}ms`);
|
|
65
|
-
log(` - Lease Update: avg=${avg(directPopMetrics.leaseUpdateTimes)}ms max=${max(directPopMetrics.leaseUpdateTimes)}ms`);
|
|
66
|
-
log(` - Decryption/Format: avg=${avg(directPopMetrics.decryptionTimes)}ms max=${max(directPopMetrics.decryptionTimes)}ms`);
|
|
67
|
-
log(` - Total Pop: avg=${avg(directPopMetrics.totalPopTimes)}ms max=${max(directPopMetrics.totalPopTimes)}ms`);
|
|
68
|
-
log(` - Avg Batch Size: ${avg(directPopMetrics.batchSizes)} messages`);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Reset metrics after logging
|
|
72
|
-
popMetrics.partitionQueryTimes = [];
|
|
73
|
-
popMetrics.leaseAttempts = 0;
|
|
74
|
-
popMetrics.leaseSuccesses = 0;
|
|
75
|
-
popMetrics.messageQueryTimes = [];
|
|
76
|
-
popMetrics.totalPopTimes = [];
|
|
77
|
-
popMetrics.batchSizes = [];
|
|
78
|
-
popMetrics.lastLogTime = now;
|
|
79
|
-
|
|
80
|
-
directPopMetrics.leaseAcquisitionTimes = [];
|
|
81
|
-
directPopMetrics.messageQueryTimes = [];
|
|
82
|
-
directPopMetrics.statusUpdateTimes = [];
|
|
83
|
-
directPopMetrics.leaseUpdateTimes = [];
|
|
84
|
-
directPopMetrics.decryptionTimes = [];
|
|
85
|
-
directPopMetrics.totalPopTimes = [];
|
|
86
|
-
directPopMetrics.batchSizes = [];
|
|
87
|
-
directPopMetrics.lastLogTime = now;
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
// Ensure queue exists and partition exist (with caching)
|
|
91
|
-
// Queue must be created via configure endpoint, but partitions can be created on-demand
|
|
92
|
-
const ensureResources = async (client, queueName, partitionName = 'Default', namespace = null, task = null) => {
|
|
93
|
-
// CRITICAL: System queues bypass cache completely
|
|
94
|
-
if (queueName.startsWith('__system_')) {
|
|
95
|
-
// Direct database query for system queues
|
|
96
|
-
const result = await client.query(`
|
|
97
|
-
SELECT
|
|
98
|
-
q.id as queue_id,
|
|
99
|
-
q.name as queue_name,
|
|
100
|
-
p.id as partition_id,
|
|
101
|
-
q.*
|
|
102
|
-
FROM queen.queues q
|
|
103
|
-
JOIN queen.partitions p ON p.queue_id = q.id
|
|
104
|
-
WHERE q.name = $1 AND p.name = $2
|
|
105
|
-
`, [queueName, partitionName]);
|
|
106
|
-
|
|
107
|
-
if (result.rows.length === 0) {
|
|
108
|
-
throw new Error(`System queue ${queueName} not found`);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const row = result.rows[0];
|
|
112
|
-
return {
|
|
113
|
-
queueId: row.queue_id,
|
|
114
|
-
queueName: row.queue_name,
|
|
115
|
-
partitionId: row.partition_id,
|
|
116
|
-
queueConfig: {
|
|
117
|
-
leaseTime: row.lease_time,
|
|
118
|
-
retryLimit: row.retry_limit,
|
|
119
|
-
retryDelay: row.retry_delay,
|
|
120
|
-
maxSize: row.max_size,
|
|
121
|
-
ttl: row.ttl,
|
|
122
|
-
deadLetterQueue: row.dead_letter_queue,
|
|
123
|
-
dlqAfterMaxRetries: row.dlq_after_max_retries,
|
|
124
|
-
delayedProcessing: row.delayed_processing,
|
|
125
|
-
windowBuffer: row.window_buffer,
|
|
126
|
-
retentionSeconds: row.retention_seconds,
|
|
127
|
-
completedRetentionSeconds: row.completed_retention_seconds,
|
|
128
|
-
retentionEnabled: row.retention_enabled,
|
|
129
|
-
priority: row.priority,
|
|
130
|
-
maxQueueSize: row.max_queue_size
|
|
131
|
-
},
|
|
132
|
-
encryptionEnabled: false, // System queues never encrypted
|
|
133
|
-
maxWaitTimeSeconds: row.max_wait_time_seconds
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Check cache first, but skip cache if we're updating namespace/task
|
|
138
|
-
const cacheKey = `${queueName}:${partitionName}`;
|
|
139
|
-
const cached = resourceCache.checkResource(queueName, partitionName);
|
|
140
|
-
if (cached && namespace === null && task === null) return cached;
|
|
141
|
-
|
|
142
|
-
// Get queue - don't create if it doesn't exist
|
|
143
|
-
const queueResult = await client.query(
|
|
144
|
-
`SELECT id, name, namespace, task, encryption_enabled, max_wait_time_seconds,
|
|
145
|
-
lease_time, retry_limit, retry_delay, max_size, ttl, dead_letter_queue,
|
|
146
|
-
dlq_after_max_retries, delayed_processing, window_buffer, retention_seconds,
|
|
147
|
-
completed_retention_seconds, retention_enabled, priority, max_queue_size
|
|
148
|
-
FROM queen.queues
|
|
149
|
-
WHERE name = $1`,
|
|
150
|
-
[queueName]
|
|
151
|
-
);
|
|
152
|
-
|
|
153
|
-
if (queueResult.rows.length === 0) {
|
|
154
|
-
// Queue doesn't exist - throw error instead of creating
|
|
155
|
-
const errorMsg = `Queue '${queueName}' does not exist. Please create it using the configure endpoint first.`;
|
|
156
|
-
log(`${LogTypes.ERROR} | ${errorMsg}`);
|
|
157
|
-
throw new Error(errorMsg);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const queue = queueResult.rows[0];
|
|
161
|
-
const queueId = queue.id;
|
|
162
|
-
|
|
163
|
-
// Insert or get partition (no more options column)
|
|
164
|
-
const partitionResult = await client.query(
|
|
165
|
-
`INSERT INTO queen.partitions (queue_id, name) VALUES ($1, $2)
|
|
166
|
-
ON CONFLICT (queue_id, name) DO UPDATE SET name = EXCLUDED.name
|
|
167
|
-
RETURNING id`,
|
|
168
|
-
[queueId, partitionName]
|
|
169
|
-
);
|
|
170
|
-
|
|
171
|
-
const result = {
|
|
172
|
-
queueId,
|
|
173
|
-
queueName: queue.name,
|
|
174
|
-
namespace: queue.namespace,
|
|
175
|
-
task: queue.task,
|
|
176
|
-
partitionId: partitionResult.rows[0].id,
|
|
177
|
-
// All configuration now comes from queue level
|
|
178
|
-
queueConfig: {
|
|
179
|
-
leaseTime: queue.lease_time,
|
|
180
|
-
retryLimit: queue.retry_limit,
|
|
181
|
-
retryDelay: queue.retry_delay,
|
|
182
|
-
maxSize: queue.max_size,
|
|
183
|
-
ttl: queue.ttl,
|
|
184
|
-
deadLetterQueue: queue.dead_letter_queue,
|
|
185
|
-
dlqAfterMaxRetries: queue.dlq_after_max_retries,
|
|
186
|
-
delayedProcessing: queue.delayed_processing,
|
|
187
|
-
windowBuffer: queue.window_buffer,
|
|
188
|
-
retentionSeconds: queue.retention_seconds,
|
|
189
|
-
completedRetentionSeconds: queue.completed_retention_seconds,
|
|
190
|
-
retentionEnabled: queue.retention_enabled,
|
|
191
|
-
priority: queue.priority,
|
|
192
|
-
maxQueueSize: queue.max_queue_size
|
|
193
|
-
},
|
|
194
|
-
encryptionEnabled: queue.encryption_enabled,
|
|
195
|
-
maxWaitTimeSeconds: queue.max_wait_time_seconds
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
// Cache the result
|
|
199
|
-
resourceCache.cacheResource(queueName, partitionName, result);
|
|
200
|
-
|
|
201
|
-
return result;
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
// Internal push operation that accepts a client
|
|
205
|
-
const pushMessagesInternal = async (client, items) => {
|
|
206
|
-
const pushStartTime = Date.now();
|
|
207
|
-
|
|
208
|
-
// Group items by queue and partition for efficient resource lookup
|
|
209
|
-
const partitionGroups = {};
|
|
210
|
-
for (const item of items) {
|
|
211
|
-
const partition = item.partition ?? 'Default';
|
|
212
|
-
const key = `${item.queue}:${partition}`;
|
|
213
|
-
if (!partitionGroups[key]) {
|
|
214
|
-
partitionGroups[key] = [];
|
|
215
|
-
}
|
|
216
|
-
partitionGroups[key].push(item);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const allResults = [];
|
|
220
|
-
|
|
221
|
-
// OPTIMIZATION: Check queue capacity once per queue, not per partition
|
|
222
|
-
// Build a map of queue -> total batch size across all partitions
|
|
223
|
-
const queueBatchSizes = {};
|
|
224
|
-
for (const [partitionKey, partitionItems] of Object.entries(partitionGroups)) {
|
|
225
|
-
const [queueName] = partitionKey.split(':');
|
|
226
|
-
queueBatchSizes[queueName] = (queueBatchSizes[queueName] || 0) + partitionItems.length;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Check capacity for all queues with max_queue_size set (single query)
|
|
230
|
-
const queuesToCheck = Object.keys(queueBatchSizes);
|
|
231
|
-
if (queuesToCheck.length > 0) {
|
|
232
|
-
const capacityCheck = await client.query(
|
|
233
|
-
`SELECT
|
|
234
|
-
q.name,
|
|
235
|
-
q.max_queue_size,
|
|
236
|
-
COALESCE(SUM(pc.pending_estimate), 0)::integer as current_depth
|
|
237
|
-
FROM queen.queues q
|
|
238
|
-
LEFT JOIN queen.partitions p ON p.queue_id = q.id
|
|
239
|
-
LEFT JOIN queen.partition_consumers pc ON pc.partition_id = p.id
|
|
240
|
-
AND pc.consumer_group = '__QUEUE_MODE__'
|
|
241
|
-
WHERE q.name = ANY($1::varchar[])
|
|
242
|
-
AND q.max_queue_size > 0
|
|
243
|
-
GROUP BY q.name, q.max_queue_size`,
|
|
244
|
-
[queuesToCheck]
|
|
245
|
-
);
|
|
246
|
-
|
|
247
|
-
// Check each queue's capacity
|
|
248
|
-
for (const row of capacityCheck.rows) {
|
|
249
|
-
const currentDepth = parseInt(row.current_depth);
|
|
250
|
-
const batchSize = queueBatchSizes[row.name];
|
|
251
|
-
const maxSize = parseInt(row.max_queue_size);
|
|
252
|
-
if (currentDepth + batchSize > maxSize) {
|
|
253
|
-
throw new Error(`Queue '${row.name}' would exceed max capacity (${maxSize}). Current: ${currentDepth}, Batch: ${batchSize}`);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// Process each partition group
|
|
259
|
-
for (const [partitionKey, partitionItems] of Object.entries(partitionGroups)) {
|
|
260
|
-
const [queueName, partitionName] = partitionKey.split(':');
|
|
261
|
-
|
|
262
|
-
// Use transaction for better error handling with retry on stale cache
|
|
263
|
-
let retryCount = 0;
|
|
264
|
-
const maxRetries = 1;
|
|
265
|
-
|
|
266
|
-
while (retryCount <= maxRetries) {
|
|
267
|
-
try {
|
|
268
|
-
// Ensure resources exist (cached after first call)
|
|
269
|
-
const resources = await ensureResources(client, queueName, partitionName);
|
|
270
|
-
const { partitionId, encryptionEnabled, queueConfig } = resources;
|
|
271
|
-
|
|
272
|
-
// Process items in batches
|
|
273
|
-
const batchResults = [];
|
|
274
|
-
|
|
275
|
-
for (let i = 0; i < partitionItems.length; i += BATCH_INSERT_SIZE) {
|
|
276
|
-
const batch = partitionItems.slice(i, i + BATCH_INSERT_SIZE);
|
|
277
|
-
|
|
278
|
-
// Prepare batch data
|
|
279
|
-
const messageIds = [];
|
|
280
|
-
const transactionIds = [];
|
|
281
|
-
const traceIds = [];
|
|
282
|
-
const payloads = [];
|
|
283
|
-
const encryptedFlags = [];
|
|
284
|
-
const duplicates = [];
|
|
285
|
-
|
|
286
|
-
// ⚡ OPTIMIZATION #1: Batch duplicate check - single query for all transaction IDs
|
|
287
|
-
const allTransactionIds = batch.map(item => item.transactionId || generateUUID());
|
|
288
|
-
|
|
289
|
-
// Skip duplicate check if no transaction IDs were explicitly provided (all generated)
|
|
290
|
-
const hasExplicitTxnIds = batch.some(item => item.transactionId);
|
|
291
|
-
let existingTxnIds = new Map();
|
|
292
|
-
|
|
293
|
-
if (hasExplicitTxnIds) {
|
|
294
|
-
// Use more efficient LEFT JOIN approach instead of ANY() for better index usage
|
|
295
|
-
const dupCheck = await client.query(
|
|
296
|
-
`SELECT t.txn_id as transaction_id, m.id
|
|
297
|
-
FROM UNNEST($1::varchar[]) AS t(txn_id)
|
|
298
|
-
LEFT JOIN queen.messages m ON m.transaction_id = t.txn_id
|
|
299
|
-
WHERE m.id IS NOT NULL`,
|
|
300
|
-
[allTransactionIds]
|
|
301
|
-
);
|
|
302
|
-
|
|
303
|
-
// Create a map of existing transaction IDs for O(1) lookup
|
|
304
|
-
existingTxnIds = new Map(dupCheck.rows.map(row => [row.transaction_id, row.id]));
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// ⚡ OPTIMIZATION #2: Parallel encryption - encrypt all payloads concurrently
|
|
308
|
-
const encryptionTasks = batch.map(async (item, idx) => {
|
|
309
|
-
const transactionId = allTransactionIds[idx];
|
|
310
|
-
|
|
311
|
-
// Check if this transaction ID already exists
|
|
312
|
-
if (existingTxnIds.has(transactionId)) {
|
|
313
|
-
return {
|
|
314
|
-
isDuplicate: true,
|
|
315
|
-
id: existingTxnIds.get(transactionId),
|
|
316
|
-
transactionId
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// Handle encryption if enabled
|
|
321
|
-
let payload = item.payload;
|
|
322
|
-
let isEncrypted = false;
|
|
323
|
-
|
|
324
|
-
if (encryptionEnabled && encryption.isEncryptionEnabled()) {
|
|
325
|
-
try {
|
|
326
|
-
payload = await encryption.encryptPayload(item.payload);
|
|
327
|
-
isEncrypted = true;
|
|
328
|
-
} catch (error) {
|
|
329
|
-
log(`ERROR: Encryption failed for transaction ${transactionId}:`, error);
|
|
330
|
-
// Continue with unencrypted payload
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
return {
|
|
335
|
-
isDuplicate: false,
|
|
336
|
-
messageId: generateUUID(),
|
|
337
|
-
transactionId,
|
|
338
|
-
traceId: item.traceId || null,
|
|
339
|
-
payload,
|
|
340
|
-
isEncrypted
|
|
341
|
-
};
|
|
342
|
-
});
|
|
343
|
-
|
|
344
|
-
// Wait for all encryption tasks to complete in parallel
|
|
345
|
-
const encryptionResults = await Promise.all(encryptionTasks);
|
|
346
|
-
|
|
347
|
-
// Process results
|
|
348
|
-
for (const result of encryptionResults) {
|
|
349
|
-
if (result.isDuplicate) {
|
|
350
|
-
duplicates.push({
|
|
351
|
-
id: result.id,
|
|
352
|
-
transactionId: result.transactionId,
|
|
353
|
-
status: 'duplicate'
|
|
354
|
-
});
|
|
355
|
-
continue;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// Handle large payloads and special cases
|
|
359
|
-
let jsonPayload;
|
|
360
|
-
try {
|
|
361
|
-
// Handle null/undefined payloads - convert to JSON null string
|
|
362
|
-
if (result.payload === null || result.payload === undefined) {
|
|
363
|
-
jsonPayload = 'null'; // JSONB expects the string 'null' for null values
|
|
364
|
-
} else {
|
|
365
|
-
// Try to stringify the payload
|
|
366
|
-
jsonPayload = JSON.stringify(result.payload);
|
|
367
|
-
}
|
|
368
|
-
} catch (error) {
|
|
369
|
-
// If JSON.stringify fails (e.g., circular references, too large),
|
|
370
|
-
// store as a string representation
|
|
371
|
-
log(`WARN: Failed to stringify payload for message ${result.messageId}:`, error.message);
|
|
372
|
-
jsonPayload = JSON.stringify({
|
|
373
|
-
error: 'Payload serialization failed',
|
|
374
|
-
type: typeof result.payload,
|
|
375
|
-
message: error.message
|
|
376
|
-
});
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
messageIds.push(result.messageId);
|
|
380
|
-
transactionIds.push(result.transactionId);
|
|
381
|
-
traceIds.push(result.traceId);
|
|
382
|
-
payloads.push(jsonPayload);
|
|
383
|
-
encryptedFlags.push(result.isEncrypted);
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// Batch insert messages with trace_id
|
|
387
|
-
if (messageIds.length > 0) {
|
|
388
|
-
const insertQuery = `
|
|
389
|
-
INSERT INTO queen.messages (id, transaction_id, trace_id, partition_id, payload, is_encrypted)
|
|
390
|
-
SELECT * FROM UNNEST($1::uuid[], $2::varchar[], $3::uuid[], $4::uuid[], $5::jsonb[], $6::boolean[])
|
|
391
|
-
RETURNING id, transaction_id, trace_id
|
|
392
|
-
`;
|
|
393
|
-
|
|
394
|
-
let insertResult;
|
|
395
|
-
try {
|
|
396
|
-
insertResult = await client.query(insertQuery, [
|
|
397
|
-
messageIds,
|
|
398
|
-
transactionIds,
|
|
399
|
-
traceIds,
|
|
400
|
-
Array(messageIds.length).fill(partitionId),
|
|
401
|
-
payloads,
|
|
402
|
-
encryptedFlags
|
|
403
|
-
]);
|
|
404
|
-
} catch (error) {
|
|
405
|
-
// If foreign key constraint error, the cached partition is stale
|
|
406
|
-
if (error.code === '23503' && error.constraint === 'messages_partition_id_fkey') {
|
|
407
|
-
// Invalidate cache and throw a more helpful error
|
|
408
|
-
resourceCache.invalidate(queueName, partitionName);
|
|
409
|
-
throw new Error(`Stale cache detected for queue '${queueName}' partition '${partitionName}'. Please retry the operation.`);
|
|
410
|
-
}
|
|
411
|
-
throw error;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
// Format results
|
|
415
|
-
for (const row of insertResult.rows) {
|
|
416
|
-
batchResults.push({
|
|
417
|
-
id: row.id,
|
|
418
|
-
transactionId: row.transaction_id,
|
|
419
|
-
traceId: row.trace_id,
|
|
420
|
-
status: 'queued'
|
|
421
|
-
});
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// Add duplicates to results
|
|
426
|
-
batchResults.push(...duplicates);
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
// Notify event manager about new messages
|
|
430
|
-
const queuePath = `${queueName}/${partitionName}`;
|
|
431
|
-
eventManager.notifyMessageAvailable(queuePath);
|
|
432
|
-
|
|
433
|
-
allResults.push(...batchResults);
|
|
434
|
-
break; // Success, exit retry loop
|
|
435
|
-
} catch (error) {
|
|
436
|
-
// Check if it's a stale cache error
|
|
437
|
-
if (error.message && error.message.includes('Stale cache detected') && retryCount < maxRetries) {
|
|
438
|
-
retryCount++;
|
|
439
|
-
// Cache already invalidated in the error handler, just retry
|
|
440
|
-
continue;
|
|
441
|
-
}
|
|
442
|
-
throw error; // Re-throw if not a stale cache error or max retries exceeded
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
const duration = Date.now() - pushStartTime;
|
|
448
|
-
log(`[PUSH] Pushed ${items.length} items → ${allResults.length} results in ${duration}ms (${(items.length / (duration / 1000)).toFixed(0)} msg/s)`);
|
|
449
|
-
return allResults;
|
|
450
|
-
};
|
|
451
|
-
|
|
452
|
-
// Public wrapper for push that creates its own transaction
|
|
453
|
-
const pushMessagesBatch = async (items) => {
|
|
454
|
-
return withTransaction(pool, async (client) => {
|
|
455
|
-
return pushMessagesInternal(client, items);
|
|
456
|
-
});
|
|
457
|
-
};
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
// Acknowledge messages (now per consumer group)
|
|
461
|
-
// CURSOR-BASED: Single ACK now routes through batch ACK to advance cursor properly
|
|
462
|
-
const acknowledgeMessage = async (transactionId, status = 'completed', error = null, consumerGroup = null, leaseId = null) => {
|
|
463
|
-
// If lease ID provided, validate it first
|
|
464
|
-
if (leaseId) {
|
|
465
|
-
log(`Validating lease ${leaseId} for transaction ${transactionId}`);
|
|
466
|
-
const valid = await pool.query(`
|
|
467
|
-
SELECT 1 FROM queen.partition_consumers pc
|
|
468
|
-
JOIN queen.messages m ON m.partition_id = pc.partition_id
|
|
469
|
-
WHERE m.transaction_id = $1
|
|
470
|
-
AND pc.worker_id = $2
|
|
471
|
-
AND pc.lease_expires_at > NOW()
|
|
472
|
-
`, [transactionId, leaseId]);
|
|
473
|
-
|
|
474
|
-
if (valid.rows.length === 0) {
|
|
475
|
-
log(`Invalid lease ${leaseId} for transaction ${transactionId} - validation failed`);
|
|
476
|
-
throw new Error('Invalid lease - cannot ACK');
|
|
477
|
-
}
|
|
478
|
-
log(`Lease ${leaseId} validated successfully`);
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
const batchResult = await acknowledgeMessages([{ transactionId, status, error }], consumerGroup);
|
|
482
|
-
return batchResult[0] || { status: 'not_found', transaction_id: transactionId };
|
|
483
|
-
};
|
|
484
|
-
|
|
485
|
-
// Internal acknowledge operation that accepts a client
|
|
486
|
-
const acknowledgeMessagesInternal = async (client, acknowledgments, consumerGroup = null) => {
|
|
487
|
-
const ackStartTime = Date.now();
|
|
488
|
-
const results = [];
|
|
489
|
-
const actualConsumerGroup = consumerGroup || '__QUEUE_MODE__';
|
|
490
|
-
|
|
491
|
-
// Group by status for efficient processing
|
|
492
|
-
const grouped = {
|
|
493
|
-
completed: [],
|
|
494
|
-
failed: []
|
|
495
|
-
};
|
|
496
|
-
|
|
497
|
-
acknowledgments.forEach(ack => {
|
|
498
|
-
if (grouped[ack.status]) {
|
|
499
|
-
grouped[ack.status].push(ack);
|
|
500
|
-
}
|
|
501
|
-
});
|
|
502
|
-
|
|
503
|
-
const totalMessages = acknowledgments.length;
|
|
504
|
-
const failedCount = grouped.failed.length;
|
|
505
|
-
const successCount = grouped.completed.length;
|
|
506
|
-
|
|
507
|
-
// Determine if this is a TOTAL batch failure or partial failure
|
|
508
|
-
// Total batch failure: all messages being ACKed right now are failures AND
|
|
509
|
-
// we're ACKing the complete leased batch
|
|
510
|
-
const allIds = acknowledgments.map(a => a.transactionId);
|
|
511
|
-
const batchSizeInfo = await client.query(`
|
|
512
|
-
SELECT pc.batch_size, pc.partition_id
|
|
513
|
-
FROM queen.messages m
|
|
514
|
-
JOIN queen.partition_consumers pc ON pc.partition_id = m.partition_id
|
|
515
|
-
AND pc.consumer_group = $2
|
|
516
|
-
WHERE m.transaction_id = ANY($1::varchar[])
|
|
517
|
-
LIMIT 1
|
|
518
|
-
`, [allIds, actualConsumerGroup]);
|
|
519
|
-
|
|
520
|
-
if (batchSizeInfo.rows.length === 0) {
|
|
521
|
-
throw new Error('Cannot find leased batch size for acknowledgment');
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
const leasedBatchSize = batchSizeInfo.rows[0].batch_size;
|
|
525
|
-
|
|
526
|
-
// Total batch failure:
|
|
527
|
-
// 1. All messages in this ACK call are failures (failedCount === totalMessages)
|
|
528
|
-
// 2. This ACK call covers the complete leased batch (totalMessages === leasedBatchSize)
|
|
529
|
-
const isTotalBatchFailure = (failedCount === totalMessages) && (totalMessages === leasedBatchSize);
|
|
530
|
-
|
|
531
|
-
// CURSOR-BASED APPROACH: Handle total batch failure vs partial failure differently
|
|
532
|
-
|
|
533
|
-
if (isTotalBatchFailure) {
|
|
534
|
-
// ═══════════════════════════════════════════════════════════════
|
|
535
|
-
// TOTAL BATCH FAILURE: Check retry limit before retrying
|
|
536
|
-
// ═══════════════════════════════════════════════════════════════
|
|
537
|
-
|
|
538
|
-
// Get queue retry limit and current batch retry count
|
|
539
|
-
const allIds = acknowledgments.map(a => a.transactionId);
|
|
540
|
-
const batchRetryInfo = await client.query(`
|
|
541
|
-
SELECT
|
|
542
|
-
q.retry_limit,
|
|
543
|
-
pc.batch_retry_count,
|
|
544
|
-
pc.partition_id,
|
|
545
|
-
p.name as partition_name
|
|
546
|
-
FROM queen.messages m
|
|
547
|
-
JOIN queen.partitions p ON p.id = m.partition_id
|
|
548
|
-
JOIN queen.queues q ON q.id = p.queue_id
|
|
549
|
-
JOIN queen.partition_consumers pc ON pc.partition_id = m.partition_id
|
|
550
|
-
AND pc.consumer_group = $2
|
|
551
|
-
WHERE m.transaction_id = ANY($1::varchar[])
|
|
552
|
-
LIMIT 1
|
|
553
|
-
`, [allIds, actualConsumerGroup]);
|
|
554
|
-
|
|
555
|
-
if (batchRetryInfo.rows.length === 0) {
|
|
556
|
-
throw new Error('Cannot find queue retry limit for batch failure');
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
const { retry_limit, batch_retry_count, partition_id, partition_name } = batchRetryInfo.rows[0];
|
|
560
|
-
const currentRetryCount = batch_retry_count || 0;
|
|
561
|
-
|
|
562
|
-
const ackTime = Date.now() - ackStartTime;
|
|
563
|
-
const throughput = ackTime > 0 ? (totalMessages / (ackTime / 1000)).toFixed(0) : 0;
|
|
564
|
-
|
|
565
|
-
if (currentRetryCount >= retry_limit) {
|
|
566
|
-
// ═══════════════════════════════════════════════════════════════
|
|
567
|
-
// BATCH RETRY LIMIT EXCEEDED: Move to DLQ and advance cursor
|
|
568
|
-
// ═══════════════════════════════════════════════════════════════
|
|
569
|
-
log(`[CURSOR] Batch retry limit exceeded (${currentRetryCount}/${retry_limit}) - moving batch to DLQ and advancing cursor`);
|
|
570
|
-
log(`[${LogTypes.ACK_BATCH}] Batch retry limit exceeded: ${failedCount} msgs → DLQ | ${throughput} msg/s | ${ackTime}ms | ConsumerGroup: ${consumerGroup || 'QUEUE_MODE'}`);
|
|
571
|
-
|
|
572
|
-
// Move all messages to DLQ
|
|
573
|
-
const failedErrors = grouped.failed.map(a => a.error || `Batch retry limit exceeded (${currentRetryCount}/${retry_limit})`);
|
|
574
|
-
await client.query(`
|
|
575
|
-
INSERT INTO queen.dead_letter_queue (message_id, partition_id, consumer_group, error_message, original_created_at)
|
|
576
|
-
SELECT
|
|
577
|
-
m.id,
|
|
578
|
-
m.partition_id,
|
|
579
|
-
$2,
|
|
580
|
-
e.error_message,
|
|
581
|
-
m.created_at
|
|
582
|
-
FROM queen.messages m
|
|
583
|
-
CROSS JOIN LATERAL UNNEST($1::varchar[], $3::text[]) AS e(txn_id, error_message)
|
|
584
|
-
WHERE m.transaction_id = e.txn_id
|
|
585
|
-
AND m.transaction_id = ANY($1::varchar[])
|
|
586
|
-
ON CONFLICT DO NOTHING
|
|
587
|
-
`, [allIds, actualConsumerGroup, failedErrors]);
|
|
588
|
-
|
|
589
|
-
// Get last message for cursor advancement
|
|
590
|
-
const lastMessageInfo = await client.query(`
|
|
591
|
-
SELECT id, created_at
|
|
592
|
-
FROM queen.messages m
|
|
593
|
-
WHERE m.transaction_id = ANY($1::varchar[])
|
|
594
|
-
ORDER BY m.created_at DESC, m.id DESC
|
|
595
|
-
LIMIT 1
|
|
596
|
-
`, [allIds]);
|
|
597
|
-
|
|
598
|
-
const lastMessage = lastMessageInfo.rows[0];
|
|
599
|
-
|
|
600
|
-
// Advance cursor and reset batch retry count
|
|
601
|
-
await client.query(`
|
|
602
|
-
UPDATE queen.partition_consumers
|
|
603
|
-
SET
|
|
604
|
-
-- Advance cursor past failed batch
|
|
605
|
-
last_consumed_created_at = $1,
|
|
606
|
-
last_consumed_id = $2,
|
|
607
|
-
total_messages_consumed = total_messages_consumed + $3,
|
|
608
|
-
total_batches_consumed = total_batches_consumed + 1,
|
|
609
|
-
last_consumed_at = NOW(),
|
|
610
|
-
|
|
611
|
-
-- Reset batch retry count
|
|
612
|
-
batch_retry_count = 0,
|
|
613
|
-
|
|
614
|
-
-- Update pending estimate
|
|
615
|
-
pending_estimate = GREATEST(0, pending_estimate - $3),
|
|
616
|
-
last_stats_update = NOW(),
|
|
617
|
-
|
|
618
|
-
-- Release lease
|
|
619
|
-
lease_expires_at = NULL,
|
|
620
|
-
lease_acquired_at = NULL,
|
|
621
|
-
message_batch = NULL,
|
|
622
|
-
batch_size = 0,
|
|
623
|
-
acked_count = 0
|
|
624
|
-
WHERE partition_id = $4
|
|
625
|
-
AND consumer_group = $5
|
|
626
|
-
`, [lastMessage.created_at, lastMessage.id, totalMessages, partition_id, actualConsumerGroup]);
|
|
627
|
-
|
|
628
|
-
grouped.failed.forEach(ack => {
|
|
629
|
-
results.push({ transactionId: ack.transactionId, status: 'failed_dlq' });
|
|
630
|
-
});
|
|
631
|
-
|
|
632
|
-
} else {
|
|
633
|
-
// ═══════════════════════════════════════════════════════════════
|
|
634
|
-
// BATCH RETRY: Increment retry count and retry same batch
|
|
635
|
-
// ═══════════════════════════════════════════════════════════════
|
|
636
|
-
log(`[CURSOR] Total batch failure - cursor NOT advanced, same batch will retry (attempt ${currentRetryCount + 1}/${retry_limit})`);
|
|
637
|
-
log(`[${LogTypes.ACK_BATCH}] Total batch failure: ${failedCount} msgs | Retry ${currentRetryCount + 1}/${retry_limit} | ${throughput} msg/s | ${ackTime}ms | ConsumerGroup: ${consumerGroup || 'QUEUE_MODE'}`);
|
|
638
|
-
|
|
639
|
-
// Increment batch retry count and release lease
|
|
640
|
-
await client.query(`
|
|
641
|
-
UPDATE queen.partition_consumers pc
|
|
642
|
-
SET lease_expires_at = NULL,
|
|
643
|
-
lease_acquired_at = NULL,
|
|
644
|
-
message_batch = NULL,
|
|
645
|
-
batch_size = 0,
|
|
646
|
-
acked_count = 0,
|
|
647
|
-
batch_retry_count = COALESCE(batch_retry_count, 0) + 1
|
|
648
|
-
WHERE pc.partition_id = $1
|
|
649
|
-
AND pc.consumer_group = $2
|
|
650
|
-
`, [partition_id, actualConsumerGroup]);
|
|
651
|
-
|
|
652
|
-
grouped.failed.forEach(ack => {
|
|
653
|
-
results.push({ transactionId: ack.transactionId, status: 'failed_retry' });
|
|
654
|
-
});
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
} else {
|
|
658
|
-
// ═══════════════════════════════════════════════════════════════
|
|
659
|
-
// PARTIAL SUCCESS: Advance cursor, move failed to DLQ
|
|
660
|
-
// ═══════════════════════════════════════════════════════════════
|
|
661
|
-
|
|
662
|
-
// Get all messages in this batch (successful and failed) to find cursor position
|
|
663
|
-
const allIds = acknowledgments.map(a => a.transactionId);
|
|
664
|
-
const batchInfo = await client.query(`
|
|
665
|
-
SELECT
|
|
666
|
-
m.id,
|
|
667
|
-
m.transaction_id,
|
|
668
|
-
m.created_at,
|
|
669
|
-
m.partition_id,
|
|
670
|
-
p.name as partition_name
|
|
671
|
-
FROM queen.messages m
|
|
672
|
-
JOIN queen.partitions p ON p.id = m.partition_id
|
|
673
|
-
WHERE m.transaction_id = ANY($1::varchar[])
|
|
674
|
-
ORDER BY m.created_at DESC, m.id DESC
|
|
675
|
-
LIMIT 1
|
|
676
|
-
`, [allIds]);
|
|
677
|
-
|
|
678
|
-
if (batchInfo.rows.length === 0) {
|
|
679
|
-
throw new Error('No messages found for acknowledgment');
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
const lastMessage = batchInfo.rows[0];
|
|
683
|
-
const partitionId = lastMessage.partition_id;
|
|
684
|
-
const partitionName = lastMessage.partition_name;
|
|
685
|
-
|
|
686
|
-
// Advance cursor + update lease atomically in partition_consumers
|
|
687
|
-
const cursorUpdateResult = await client.query(`
|
|
688
|
-
UPDATE queen.partition_consumers
|
|
689
|
-
SET
|
|
690
|
-
-- Advance cursor (regardless of success/failure)
|
|
691
|
-
last_consumed_created_at = $1,
|
|
692
|
-
last_consumed_id = $2,
|
|
693
|
-
total_messages_consumed = total_messages_consumed + $3,
|
|
694
|
-
total_batches_consumed = total_batches_consumed + 1,
|
|
695
|
-
last_consumed_at = NOW(),
|
|
696
|
-
|
|
697
|
-
-- Reset batch retry count on successful processing
|
|
698
|
-
batch_retry_count = 0,
|
|
699
|
-
|
|
700
|
-
-- Update pending estimate
|
|
701
|
-
pending_estimate = GREATEST(0, pending_estimate - $3),
|
|
702
|
-
last_stats_update = NOW(),
|
|
703
|
-
|
|
704
|
-
-- Update/release lease
|
|
705
|
-
acked_count = acked_count + $3,
|
|
706
|
-
lease_expires_at = CASE
|
|
707
|
-
WHEN acked_count + $3 >= batch_size AND batch_size > 0
|
|
708
|
-
THEN NULL
|
|
709
|
-
ELSE lease_expires_at
|
|
710
|
-
END,
|
|
711
|
-
lease_acquired_at = CASE
|
|
712
|
-
WHEN acked_count + $3 >= batch_size AND batch_size > 0
|
|
713
|
-
THEN NULL
|
|
714
|
-
ELSE lease_acquired_at
|
|
715
|
-
END,
|
|
716
|
-
message_batch = CASE
|
|
717
|
-
WHEN acked_count + $3 >= batch_size AND batch_size > 0
|
|
718
|
-
THEN NULL
|
|
719
|
-
ELSE message_batch
|
|
720
|
-
END,
|
|
721
|
-
batch_size = CASE
|
|
722
|
-
WHEN acked_count + $3 >= batch_size AND batch_size > 0
|
|
723
|
-
THEN 0
|
|
724
|
-
ELSE batch_size
|
|
725
|
-
END
|
|
726
|
-
WHERE partition_id = $4
|
|
727
|
-
AND consumer_group = $5
|
|
728
|
-
RETURNING total_messages_consumed, total_batches_consumed,
|
|
729
|
-
(lease_expires_at IS NULL) as lease_released
|
|
730
|
-
`, [lastMessage.created_at, lastMessage.id, totalMessages, partitionId, actualConsumerGroup]);
|
|
731
|
-
|
|
732
|
-
const newTotalConsumed = cursorUpdateResult.rows[0]?.total_messages_consumed || 0;
|
|
733
|
-
const newTotalBatches = cursorUpdateResult.rows[0]?.total_batches_consumed || 0;
|
|
734
|
-
const leaseReleased = cursorUpdateResult.rows[0]?.lease_released || false;
|
|
735
|
-
|
|
736
|
-
const ackTime = Date.now() - ackStartTime;
|
|
737
|
-
const throughput = ackTime > 0 ? (totalMessages / (ackTime / 1000)).toFixed(0) : 0;
|
|
738
|
-
|
|
739
|
-
log(`[${LogTypes.ACK_BATCH}] Success: ${successCount} | Failed: ${failedCount} | ${throughput} msg/s | ${ackTime}ms | Cursor: ${newTotalConsumed} msgs | Lease: ${leaseReleased ? 'Released' : 'Active'} | ConsumerGroup: ${consumerGroup || 'QUEUE_MODE'}`);
|
|
740
|
-
|
|
741
|
-
// Move failed messages to DLQ (individual failures, not batch retry)
|
|
742
|
-
if (grouped.failed.length > 0) {
|
|
743
|
-
const failedIds = grouped.failed.map(a => a.transactionId);
|
|
744
|
-
const failedErrors = grouped.failed.map(a => a.error || 'Unknown error');
|
|
745
|
-
|
|
746
|
-
await client.query(`
|
|
747
|
-
INSERT INTO queen.dead_letter_queue (message_id, partition_id, consumer_group, error_message, original_created_at)
|
|
748
|
-
SELECT
|
|
749
|
-
m.id,
|
|
750
|
-
m.partition_id,
|
|
751
|
-
$2,
|
|
752
|
-
e.error_message,
|
|
753
|
-
m.created_at
|
|
754
|
-
FROM queen.messages m
|
|
755
|
-
CROSS JOIN LATERAL UNNEST($1::varchar[], $3::text[]) AS e(txn_id, error_message)
|
|
756
|
-
WHERE m.transaction_id = e.txn_id
|
|
757
|
-
AND m.transaction_id = ANY($1::varchar[])
|
|
758
|
-
ON CONFLICT DO NOTHING
|
|
759
|
-
`, [failedIds, actualConsumerGroup, failedErrors]);
|
|
760
|
-
|
|
761
|
-
log(`[${LogTypes.ACK_BATCH}] Moved ${failedCount} failed messages to DLQ | ConsumerGroup: ${consumerGroup || 'QUEUE_MODE'}`);
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
// Track consumption metrics for time-series analytics
|
|
765
|
-
await client.query(`
|
|
766
|
-
INSERT INTO queen.messages_consumed (partition_id, consumer_group, messages_completed, messages_failed)
|
|
767
|
-
VALUES ($1, $2, $3, $4)
|
|
768
|
-
`, [partitionId, actualConsumerGroup, successCount, failedCount]);
|
|
769
|
-
|
|
770
|
-
// Add results
|
|
771
|
-
grouped.completed.forEach(ack => {
|
|
772
|
-
results.push({ transactionId: ack.transactionId, status: 'completed' });
|
|
773
|
-
});
|
|
774
|
-
|
|
775
|
-
grouped.failed.forEach(ack => {
|
|
776
|
-
results.push({ transactionId: ack.transactionId, status: 'failed_dlq' });
|
|
777
|
-
});
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
return results;
|
|
781
|
-
};
|
|
782
|
-
|
|
783
|
-
// Public wrapper for acknowledge that creates its own transaction
|
|
784
|
-
const acknowledgeMessages = async (acknowledgments, consumerGroup = null) => {
|
|
785
|
-
return withTransaction(pool, async (client) => {
|
|
786
|
-
return acknowledgeMessagesInternal(client, acknowledgments, consumerGroup);
|
|
787
|
-
});
|
|
788
|
-
};
|
|
789
|
-
|
|
790
|
-
// Reclaim expired leases from partition_consumers
|
|
791
|
-
const reclaimExpiredLeases = async () => {
|
|
792
|
-
return withTransaction(pool, async (client) => {
|
|
793
|
-
// Clear expired leases from partition_consumers
|
|
794
|
-
const result = await client.query(
|
|
795
|
-
`UPDATE queen.partition_consumers
|
|
796
|
-
SET lease_expires_at = NULL,
|
|
797
|
-
lease_acquired_at = NULL,
|
|
798
|
-
message_batch = NULL,
|
|
799
|
-
batch_size = 0,
|
|
800
|
-
acked_count = 0,
|
|
801
|
-
worker_id = NULL
|
|
802
|
-
WHERE lease_expires_at IS NOT NULL
|
|
803
|
-
AND lease_expires_at < NOW()
|
|
804
|
-
RETURNING partition_id, consumer_group`
|
|
805
|
-
);
|
|
806
|
-
|
|
807
|
-
if (result.rows.length > 0) {
|
|
808
|
-
const byGroup = {};
|
|
809
|
-
result.rows.forEach(row => {
|
|
810
|
-
const group = row.consumer_group || 'QUEUE_MODE';
|
|
811
|
-
byGroup[group] = (byGroup[group] || 0) + 1;
|
|
812
|
-
});
|
|
813
|
-
|
|
814
|
-
Object.entries(byGroup).forEach(([group, count]) => {
|
|
815
|
-
log(`${LogTypes.RECLAIM} | Reclaimed ${count} expired leases for consumer group: ${group}`);
|
|
816
|
-
});
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
return result.rows.length;
|
|
820
|
-
});
|
|
821
|
-
};
|
|
822
|
-
|
|
823
|
-
// Get queue statistics (using partition_consumers for fast estimates)
|
|
824
|
-
const getQueueStats = async (filters = {}) => {
|
|
825
|
-
const { queue, namespace, task, fromDateTime, toDateTime } = filters;
|
|
826
|
-
|
|
827
|
-
let query = `
|
|
828
|
-
SELECT
|
|
829
|
-
q.name as queue,
|
|
830
|
-
q.namespace,
|
|
831
|
-
q.task,
|
|
832
|
-
p.name as partition,
|
|
833
|
-
COALESCE(pc.pending_estimate, 0) as pending,
|
|
834
|
-
CASE WHEN pc.lease_expires_at IS NOT NULL THEN 1 ELSE 0 END as processing,
|
|
835
|
-
COALESCE(pc.total_messages_consumed, 0) as completed,
|
|
836
|
-
0 as failed,
|
|
837
|
-
(SELECT COUNT(*) FROM queen.dead_letter_queue dlq
|
|
838
|
-
WHERE dlq.partition_id = p.id
|
|
839
|
-
AND dlq.consumer_group = COALESCE(pc.consumer_group, '__QUEUE_MODE__')) as dead_letter,
|
|
840
|
-
(SELECT COUNT(*) FROM queen.messages m
|
|
841
|
-
WHERE m.partition_id = p.id
|
|
842
|
-
${fromDateTime ? 'AND m.created_at >= $' + (1 + (queue ? 1 : 0) + (namespace ? 1 : 0) + (task ? 1 : 0)) + '::timestamp' : ''}
|
|
843
|
-
${toDateTime ? 'AND m.created_at <= $' + (1 + (queue ? 1 : 0) + (namespace ? 1 : 0) + (task ? 1 : 0) + (fromDateTime ? 1 : 0)) + '::timestamp' : ''}
|
|
844
|
-
) as total_messages,
|
|
845
|
-
(SELECT COUNT(DISTINCT consumer_group)
|
|
846
|
-
FROM queen.partition_consumers
|
|
847
|
-
WHERE partition_id = p.id
|
|
848
|
-
AND consumer_group != '__QUEUE_MODE__') as consumer_groups_count
|
|
849
|
-
FROM queen.queues q
|
|
850
|
-
LEFT JOIN queen.partitions p ON p.queue_id = q.id
|
|
851
|
-
LEFT JOIN queen.partition_consumers pc ON pc.partition_id = p.id
|
|
852
|
-
AND pc.consumer_group = '__QUEUE_MODE__'
|
|
853
|
-
`;
|
|
854
|
-
|
|
855
|
-
const conditions = [];
|
|
856
|
-
const params = [];
|
|
857
|
-
|
|
858
|
-
if (queue) {
|
|
859
|
-
conditions.push(`q.name = $${params.length + 1}`);
|
|
860
|
-
params.push(queue);
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
if (namespace) {
|
|
864
|
-
conditions.push(`q.namespace = $${params.length + 1}`);
|
|
865
|
-
params.push(namespace);
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
if (task) {
|
|
869
|
-
conditions.push(`q.task = $${params.length + 1}`);
|
|
870
|
-
params.push(task);
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
// Add date params for subqueries
|
|
874
|
-
if (fromDateTime) {
|
|
875
|
-
params.push(fromDateTime);
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
if (toDateTime) {
|
|
879
|
-
params.push(toDateTime);
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
if (conditions.length > 0) {
|
|
883
|
-
query += ` WHERE ${conditions.join(' AND ')}`;
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
query += ` ORDER BY q.name, p.name`;
|
|
887
|
-
|
|
888
|
-
const result = await pool.query(query, params);
|
|
889
|
-
return result.rows;
|
|
890
|
-
};
|
|
891
|
-
|
|
892
|
-
// Get queue lag statistics (simplified with partition_consumers)
|
|
893
|
-
const getQueueLag = async (filters = {}) => {
|
|
894
|
-
const { queue, namespace, task, fromDateTime, toDateTime } = filters;
|
|
895
|
-
|
|
896
|
-
let query = `
|
|
897
|
-
WITH lag_stats AS (
|
|
898
|
-
SELECT
|
|
899
|
-
q.name as queue,
|
|
900
|
-
q.namespace,
|
|
901
|
-
q.task,
|
|
902
|
-
p.name as partition,
|
|
903
|
-
COALESCE(pc.pending_estimate, 0) as pending_count,
|
|
904
|
-
CASE WHEN pc.lease_expires_at IS NOT NULL THEN 1 ELSE 0 END as processing_count,
|
|
905
|
-
COALESCE(pc.pending_estimate, 0) + CASE WHEN pc.lease_expires_at IS NOT NULL THEN 1 ELSE 0 END as total_backlog,
|
|
906
|
-
COALESCE(pc.total_messages_consumed, 0) as completed_messages,
|
|
907
|
-
0 as avg_processing_time_seconds,
|
|
908
|
-
0 as median_processing_time_seconds,
|
|
909
|
-
0 as p95_processing_time_seconds,
|
|
910
|
-
(SELECT MIN(created_at) FROM queen.messages m
|
|
911
|
-
WHERE m.partition_id = p.id
|
|
912
|
-
AND (m.created_at, m.id) > (COALESCE(pc.last_consumed_created_at, '1970-01-01'::timestamptz),
|
|
913
|
-
COALESCE(pc.last_consumed_id, '00000000-0000-0000-0000-000000000000'::uuid))
|
|
914
|
-
) as oldest_unprocessed,
|
|
915
|
-
(SELECT MAX(created_at) FROM queen.messages m
|
|
916
|
-
WHERE m.partition_id = p.id
|
|
917
|
-
AND (m.created_at, m.id) <= (COALESCE(pc.last_consumed_created_at, NOW()::timestamptz),
|
|
918
|
-
COALESCE(pc.last_consumed_id, gen_random_uuid()))
|
|
919
|
-
) as newest_completed
|
|
920
|
-
FROM queen.queues q
|
|
921
|
-
LEFT JOIN queen.partitions p ON p.queue_id = q.id
|
|
922
|
-
LEFT JOIN queen.partition_consumers pc ON pc.partition_id = p.id
|
|
923
|
-
AND pc.consumer_group = '__QUEUE_MODE__'
|
|
924
|
-
`;
|
|
925
|
-
|
|
926
|
-
const conditions = [];
|
|
927
|
-
const params = [];
|
|
928
|
-
|
|
929
|
-
if (queue) {
|
|
930
|
-
conditions.push(`q.name = $${params.length + 1}`);
|
|
931
|
-
params.push(queue);
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
if (namespace) {
|
|
935
|
-
conditions.push(`q.namespace = $${params.length + 1}`);
|
|
936
|
-
params.push(namespace);
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
if (task) {
|
|
940
|
-
conditions.push(`q.task = $${params.length + 1}`);
|
|
941
|
-
params.push(task);
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
if (conditions.length > 0) {
|
|
945
|
-
query += ` WHERE ${conditions.join(' AND ')}`;
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
query += ` GROUP BY q.name, q.namespace, q.task, p.name, pc.pending_estimate,
|
|
949
|
-
pc.lease_expires_at, pc.total_messages_consumed,
|
|
950
|
-
pc.last_consumed_created_at, pc.last_consumed_id, p.id
|
|
951
|
-
)
|
|
952
|
-
SELECT
|
|
953
|
-
queue,
|
|
954
|
-
namespace,
|
|
955
|
-
task,
|
|
956
|
-
partition,
|
|
957
|
-
jsonb_build_object(
|
|
958
|
-
'pendingCount', pending_count,
|
|
959
|
-
'processingCount', processing_count,
|
|
960
|
-
'totalBacklog', total_backlog,
|
|
961
|
-
'completedMessages', completed_messages,
|
|
962
|
-
'avgProcessingTimeSeconds', avg_processing_time_seconds,
|
|
963
|
-
'medianProcessingTimeSeconds', median_processing_time_seconds,
|
|
964
|
-
'p95ProcessingTimeSeconds', p95_processing_time_seconds,
|
|
965
|
-
'estimatedLagSeconds', CASE
|
|
966
|
-
WHEN oldest_unprocessed IS NOT NULL
|
|
967
|
-
THEN EXTRACT(EPOCH FROM (NOW() - oldest_unprocessed))
|
|
968
|
-
ELSE 0
|
|
969
|
-
END,
|
|
970
|
-
'medianLagSeconds', CASE
|
|
971
|
-
WHEN oldest_unprocessed IS NOT NULL AND newest_completed IS NOT NULL
|
|
972
|
-
THEN EXTRACT(EPOCH FROM (newest_completed - oldest_unprocessed)) / 2
|
|
973
|
-
ELSE 0
|
|
974
|
-
END,
|
|
975
|
-
'p95LagSeconds', CASE
|
|
976
|
-
WHEN oldest_unprocessed IS NOT NULL
|
|
977
|
-
THEN EXTRACT(EPOCH FROM (NOW() - oldest_unprocessed)) * 1.5
|
|
978
|
-
ELSE 0
|
|
979
|
-
END
|
|
980
|
-
) as stats
|
|
981
|
-
FROM lag_stats
|
|
982
|
-
ORDER BY queue, partition`;
|
|
983
|
-
|
|
984
|
-
const result = await pool.query(query, params);
|
|
985
|
-
return result.rows;
|
|
986
|
-
};
|
|
987
|
-
|
|
988
|
-
/**
|
|
989
|
-
* Configure queue settings with namespace and task support
|
|
990
|
-
* Updates queue configuration options and optionally sets namespace/task
|
|
991
|
-
*/
|
|
992
|
-
const configureQueue = async (queueName, options = {}, namespace = null, task = null) => {
|
|
993
|
-
return withTransaction(pool, async (client) => {
|
|
994
|
-
// Check if queue exists (for update vs create detection)
|
|
995
|
-
const existingQueue = await client.query(
|
|
996
|
-
'SELECT * FROM queen.queues WHERE name = $1',
|
|
997
|
-
[queueName]
|
|
998
|
-
);
|
|
999
|
-
const isUpdate = existingQueue.rows.length > 0;
|
|
1000
|
-
|
|
1001
|
-
// First create/update the queue with namespace and task
|
|
1002
|
-
const queueResult = await client.query(
|
|
1003
|
-
`INSERT INTO queen.queues (name, namespace, task) VALUES ($1, $2, $3)
|
|
1004
|
-
ON CONFLICT (name) DO UPDATE SET
|
|
1005
|
-
namespace = CASE WHEN EXCLUDED.namespace IS NOT NULL THEN EXCLUDED.namespace ELSE queen.queues.namespace END,
|
|
1006
|
-
task = CASE WHEN EXCLUDED.task IS NOT NULL THEN EXCLUDED.task ELSE queen.queues.task END
|
|
1007
|
-
RETURNING id, name, namespace, task`,
|
|
1008
|
-
[queueName, namespace || null, task || null]
|
|
1009
|
-
);
|
|
1010
|
-
|
|
1011
|
-
// Build update query for all configuration options
|
|
1012
|
-
const updates = [];
|
|
1013
|
-
const params = [];
|
|
1014
|
-
let paramIndex = 1;
|
|
1015
|
-
|
|
1016
|
-
// Map options to database columns
|
|
1017
|
-
const optionMappings = {
|
|
1018
|
-
leaseTime: 'lease_time',
|
|
1019
|
-
retryLimit: 'retry_limit',
|
|
1020
|
-
retryDelay: 'retry_delay',
|
|
1021
|
-
maxSize: 'max_size',
|
|
1022
|
-
ttl: 'ttl',
|
|
1023
|
-
deadLetterQueue: 'dead_letter_queue',
|
|
1024
|
-
dlqAfterMaxRetries: 'dlq_after_max_retries',
|
|
1025
|
-
delayedProcessing: 'delayed_processing',
|
|
1026
|
-
windowBuffer: 'window_buffer',
|
|
1027
|
-
retentionSeconds: 'retention_seconds',
|
|
1028
|
-
completedRetentionSeconds: 'completed_retention_seconds',
|
|
1029
|
-
retentionEnabled: 'retention_enabled',
|
|
1030
|
-
priority: 'priority',
|
|
1031
|
-
encryptionEnabled: 'encryption_enabled',
|
|
1032
|
-
maxWaitTimeSeconds: 'max_wait_time_seconds',
|
|
1033
|
-
maxQueueSize: 'max_queue_size'
|
|
1034
|
-
};
|
|
1035
|
-
|
|
1036
|
-
// Detect what changed (for update events)
|
|
1037
|
-
const changes = {};
|
|
1038
|
-
|
|
1039
|
-
// Process each option
|
|
1040
|
-
for (const [optionKey, columnName] of Object.entries(optionMappings)) {
|
|
1041
|
-
if (options[optionKey] !== undefined) {
|
|
1042
|
-
// Track changes for update events
|
|
1043
|
-
if (isUpdate && existingQueue.rows[0]) {
|
|
1044
|
-
const old = existingQueue.rows[0];
|
|
1045
|
-
const newValue = options[optionKey];
|
|
1046
|
-
if (old[columnName] !== newValue) {
|
|
1047
|
-
changes[optionKey] = { old: old[columnName], new: newValue };
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
updates.push(`${columnName} = $${paramIndex}`);
|
|
1052
|
-
|
|
1053
|
-
// Handle different data types
|
|
1054
|
-
if (typeof options[optionKey] === 'boolean') {
|
|
1055
|
-
params.push(!!options[optionKey]);
|
|
1056
|
-
} else if (typeof options[optionKey] === 'number' || !isNaN(options[optionKey])) {
|
|
1057
|
-
params.push(parseInt(options[optionKey]) || 0);
|
|
1058
|
-
} else {
|
|
1059
|
-
params.push(options[optionKey]);
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
paramIndex++;
|
|
1063
|
-
}
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
// Apply updates if any
|
|
1067
|
-
if (updates.length > 0) {
|
|
1068
|
-
params.push(queueName);
|
|
1069
|
-
const updateQuery = `UPDATE queen.queues SET ${updates.join(', ')} WHERE name = $${paramIndex}`;
|
|
1070
|
-
await client.query(updateQuery, params);
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
// Emit appropriate event (if systemEventManager is available)
|
|
1074
|
-
if (systemEventManager) {
|
|
1075
|
-
await systemEventManager.emit(
|
|
1076
|
-
isUpdate ? EventTypes.QUEUE_UPDATED : EventTypes.QUEUE_CREATED,
|
|
1077
|
-
{
|
|
1078
|
-
entityType: 'queue',
|
|
1079
|
-
entityId: queueName,
|
|
1080
|
-
changes: isUpdate ? changes : options,
|
|
1081
|
-
namespace,
|
|
1082
|
-
task
|
|
1083
|
-
}
|
|
1084
|
-
);
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
// Local cache invalidation (immediate)
|
|
1088
|
-
resourceCache.invalidateQueue(queueName);
|
|
1089
|
-
|
|
1090
|
-
return {
|
|
1091
|
-
queue: queueName,
|
|
1092
|
-
namespace: queueResult.rows[0].namespace,
|
|
1093
|
-
task: queueResult.rows[0].task,
|
|
1094
|
-
options
|
|
1095
|
-
};
|
|
1096
|
-
});
|
|
1097
|
-
};
|
|
1098
|
-
|
|
1099
|
-
/**
|
|
1100
|
-
* ═══════════════════════════════════════════════════════════════════════════
|
|
1101
|
-
* UNIFIED POP FUNCTION - Handles all 3 access patterns in one implementation
|
|
1102
|
-
* ═══════════════════════════════════════════════════════════════════════════
|
|
1103
|
-
*
|
|
1104
|
-
* This function unifies:
|
|
1105
|
-
* 1. Direct partition access (queue + partition specified)
|
|
1106
|
-
* 2. Queue-level access (queue only, find unlocked partition)
|
|
1107
|
-
* 3. Filtered access (namespace/task, find matching queues + partitions)
|
|
1108
|
-
*
|
|
1109
|
-
* Benefits over separate functions:
|
|
1110
|
-
* - Single source of truth for lease acquisition
|
|
1111
|
-
* - Consistent multi-candidate retry logic
|
|
1112
|
-
* - Easier to maintain and optimize
|
|
1113
|
-
* - Shared backpressure and metrics
|
|
1114
|
-
*/
|
|
1115
|
-
const uniquePop = async (scope, options = {}) => {
|
|
1116
|
-
const {
|
|
1117
|
-
queue,
|
|
1118
|
-
partition,
|
|
1119
|
-
namespace,
|
|
1120
|
-
task,
|
|
1121
|
-
consumerGroup
|
|
1122
|
-
} = scope;
|
|
1123
|
-
|
|
1124
|
-
const {
|
|
1125
|
-
wait = false,
|
|
1126
|
-
timeout = config.QUEUE.DEFAULT_TIMEOUT,
|
|
1127
|
-
batch = config.QUEUE.DEFAULT_BATCH_SIZE,
|
|
1128
|
-
subscriptionMode = null,
|
|
1129
|
-
subscriptionFrom = null
|
|
1130
|
-
} = options;
|
|
1131
|
-
|
|
1132
|
-
const startTime = Date.now();
|
|
1133
|
-
const actualConsumerGroup = consumerGroup || '__QUEUE_MODE__';
|
|
1134
|
-
|
|
1135
|
-
// Determine access mode
|
|
1136
|
-
const accessMode = partition ? 'direct' : (namespace || task ? 'filtered' : 'queue');
|
|
1137
|
-
|
|
1138
|
-
return withTransaction(pool, async (client) => {
|
|
1139
|
-
const connectionWaitTime = Date.now() - startTime;
|
|
1140
|
-
if (connectionWaitTime > 1000) {
|
|
1141
|
-
log(`⚠️ Warning connection wait time: ${connectionWaitTime}ms`);
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
// ─────────────────────────────────────────────────────────────────────
|
|
1145
|
-
// PHASE 1: Find Candidate Partitions (mode-specific queries)
|
|
1146
|
-
// ─────────────────────────────────────────────────────────────────────
|
|
1147
|
-
|
|
1148
|
-
const candidateQueryStart = Date.now();
|
|
1149
|
-
let candidatePartitions = [];
|
|
1150
|
-
let queueInfo = null;
|
|
1151
|
-
|
|
1152
|
-
if (accessMode === 'direct') {
|
|
1153
|
-
// CASE 1: Direct partition access - single partition, no search needed
|
|
1154
|
-
const result = await client.query(`
|
|
1155
|
-
SELECT
|
|
1156
|
-
q.id as queue_id,
|
|
1157
|
-
q.name as queue_name,
|
|
1158
|
-
q.lease_time,
|
|
1159
|
-
q.retry_limit,
|
|
1160
|
-
q.delayed_processing,
|
|
1161
|
-
q.max_wait_time_seconds,
|
|
1162
|
-
q.window_buffer,
|
|
1163
|
-
q.priority,
|
|
1164
|
-
p.id as partition_id,
|
|
1165
|
-
p.name as partition_name
|
|
1166
|
-
FROM queen.queues q
|
|
1167
|
-
JOIN queen.partitions p ON p.queue_id = q.id
|
|
1168
|
-
WHERE q.name = $1 AND p.name = $2
|
|
1169
|
-
AND (q.window_buffer = 0 OR NOT EXISTS (
|
|
1170
|
-
SELECT 1 FROM queen.messages m
|
|
1171
|
-
WHERE m.partition_id = p.id
|
|
1172
|
-
AND m.created_at > NOW() - INTERVAL '1 second' * q.window_buffer
|
|
1173
|
-
))
|
|
1174
|
-
`, [queue, partition]);
|
|
1175
|
-
|
|
1176
|
-
if (result.rows.length > 0) {
|
|
1177
|
-
candidatePartitions = [result.rows[0]];
|
|
1178
|
-
queueInfo = result.rows[0];
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
} else if (accessMode === 'queue') {
|
|
1182
|
-
// CASE 2: Queue-level access - find unlocked partitions WITH unconsumed messages
|
|
1183
|
-
const result = await client.query(`
|
|
1184
|
-
SELECT
|
|
1185
|
-
q.id as queue_id,
|
|
1186
|
-
q.name as queue_name,
|
|
1187
|
-
q.lease_time,
|
|
1188
|
-
q.retry_limit,
|
|
1189
|
-
q.delayed_processing,
|
|
1190
|
-
q.max_wait_time_seconds,
|
|
1191
|
-
q.window_buffer,
|
|
1192
|
-
q.priority,
|
|
1193
|
-
p.id as partition_id,
|
|
1194
|
-
p.name as partition_name
|
|
1195
|
-
FROM queen.queues q
|
|
1196
|
-
JOIN queen.partitions p ON p.queue_id = q.id
|
|
1197
|
-
LEFT JOIN queen.partition_consumers pc ON pc.partition_id = p.id
|
|
1198
|
-
AND pc.consumer_group = $1
|
|
1199
|
-
WHERE q.name = $2
|
|
1200
|
-
AND NOT EXISTS (
|
|
1201
|
-
SELECT 1 FROM queen.partition_consumers pc2
|
|
1202
|
-
WHERE pc2.partition_id = p.id
|
|
1203
|
-
AND pc2.consumer_group = $1
|
|
1204
|
-
AND pc2.lease_expires_at IS NOT NULL
|
|
1205
|
-
AND pc2.lease_expires_at > NOW()
|
|
1206
|
-
)
|
|
1207
|
-
AND (q.window_buffer = 0 OR NOT EXISTS (
|
|
1208
|
-
SELECT 1 FROM queen.messages m
|
|
1209
|
-
WHERE m.partition_id = p.id
|
|
1210
|
-
AND m.created_at > NOW() - INTERVAL '1 second' * q.window_buffer
|
|
1211
|
-
))
|
|
1212
|
-
AND EXISTS (
|
|
1213
|
-
SELECT 1 FROM queen.messages m2
|
|
1214
|
-
WHERE m2.partition_id = p.id
|
|
1215
|
-
AND m2.id > COALESCE(pc.last_consumed_id, '00000000-0000-0000-0000-000000000000'::uuid)
|
|
1216
|
-
AND (q.delayed_processing = 0 OR m2.created_at <= NOW() - INTERVAL '1 second' * q.delayed_processing)
|
|
1217
|
-
AND (q.max_wait_time_seconds = 0 OR m2.created_at > NOW() - INTERVAL '1 second' * q.max_wait_time_seconds)
|
|
1218
|
-
)
|
|
1219
|
-
ORDER BY RANDOM()
|
|
1220
|
-
LIMIT $3
|
|
1221
|
-
`, [actualConsumerGroup, queue, config.QUEUE.MAX_PARTITION_CANDIDATES]);
|
|
1222
|
-
|
|
1223
|
-
if (result.rows.length > 0) {
|
|
1224
|
-
candidatePartitions = result.rows;
|
|
1225
|
-
queueInfo = result.rows[0];
|
|
1226
|
-
}
|
|
1227
|
-
|
|
1228
|
-
} else {
|
|
1229
|
-
// CASE 3: Filtered access - namespace/task
|
|
1230
|
-
// Use CTE to first filter partitions by windowBuffer, then find messages
|
|
1231
|
-
let filterQuery = `
|
|
1232
|
-
WITH eligible_partitions AS (
|
|
1233
|
-
SELECT DISTINCT
|
|
1234
|
-
q.id as queue_id,
|
|
1235
|
-
q.name as queue_name,
|
|
1236
|
-
q.lease_time,
|
|
1237
|
-
q.retry_limit,
|
|
1238
|
-
q.delayed_processing,
|
|
1239
|
-
q.max_wait_time_seconds,
|
|
1240
|
-
q.window_buffer,
|
|
1241
|
-
q.priority,
|
|
1242
|
-
p.id as partition_id,
|
|
1243
|
-
p.name as partition_name
|
|
1244
|
-
FROM queen.queues q
|
|
1245
|
-
JOIN queen.partitions p ON p.queue_id = q.id
|
|
1246
|
-
WHERE (q.window_buffer = 0 OR NOT EXISTS (
|
|
1247
|
-
SELECT 1 FROM queen.messages m2
|
|
1248
|
-
WHERE m2.partition_id = p.id
|
|
1249
|
-
AND m2.created_at > NOW() - INTERVAL '1 second' * q.window_buffer
|
|
1250
|
-
))
|
|
1251
|
-
AND NOT EXISTS (
|
|
1252
|
-
SELECT 1 FROM queen.partition_consumers pc2
|
|
1253
|
-
WHERE pc2.partition_id = p.id
|
|
1254
|
-
AND pc2.consumer_group = $1
|
|
1255
|
-
AND pc2.lease_expires_at IS NOT NULL
|
|
1256
|
-
AND pc2.lease_expires_at > NOW()
|
|
1257
|
-
)
|
|
1258
|
-
)
|
|
1259
|
-
SELECT
|
|
1260
|
-
ep.*,
|
|
1261
|
-
MIN(m.created_at) as oldest_message
|
|
1262
|
-
FROM queen.messages m
|
|
1263
|
-
JOIN eligible_partitions ep ON m.partition_id = ep.partition_id
|
|
1264
|
-
LEFT JOIN queen.partition_consumers pc ON ep.partition_id = pc.partition_id
|
|
1265
|
-
AND pc.consumer_group = $1
|
|
1266
|
-
WHERE m.id > COALESCE(pc.last_consumed_id, '00000000-0000-0000-0000-000000000000'::uuid)
|
|
1267
|
-
AND (ep.delayed_processing = 0 OR m.created_at <= NOW() - INTERVAL '1 second' * ep.delayed_processing)
|
|
1268
|
-
AND (ep.max_wait_time_seconds = 0 OR
|
|
1269
|
-
m.created_at > NOW() - INTERVAL '1 second' * ep.max_wait_time_seconds)
|
|
1270
|
-
`;
|
|
1271
|
-
|
|
1272
|
-
const params = [actualConsumerGroup];
|
|
1273
|
-
|
|
1274
|
-
//Build namespace/task filters for the CTE
|
|
1275
|
-
let cteFilters = '';
|
|
1276
|
-
if (namespace) {
|
|
1277
|
-
params.push(namespace);
|
|
1278
|
-
cteFilters += ` AND q.namespace = $${params.length}`;
|
|
1279
|
-
}
|
|
1280
|
-
|
|
1281
|
-
if (task) {
|
|
1282
|
-
params.push(task);
|
|
1283
|
-
cteFilters += ` AND q.task = $${params.length}`;
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
// Insert CTE filters before the closing parenthesis
|
|
1287
|
-
filterQuery = filterQuery.replace(' )\n )', ` )${cteFilters}\n )`);
|
|
1288
|
-
|
|
1289
|
-
params.push(config.QUEUE.MAX_PARTITION_CANDIDATES);
|
|
1290
|
-
filterQuery += `
|
|
1291
|
-
GROUP BY ep.queue_id, ep.queue_name, ep.lease_time, ep.retry_limit, ep.delayed_processing,
|
|
1292
|
-
ep.max_wait_time_seconds, ep.window_buffer, ep.priority,
|
|
1293
|
-
ep.partition_id, ep.partition_name
|
|
1294
|
-
HAVING COUNT(*) > 0
|
|
1295
|
-
ORDER BY ep.priority DESC, MIN(m.created_at) ASC, RANDOM()
|
|
1296
|
-
LIMIT $${params.length}
|
|
1297
|
-
`;
|
|
1298
|
-
|
|
1299
|
-
const result = await client.query(filterQuery, params);
|
|
1300
|
-
|
|
1301
|
-
if (result.rows.length > 0) {
|
|
1302
|
-
candidatePartitions = result.rows;
|
|
1303
|
-
queueInfo = result.rows[0];
|
|
1304
|
-
}
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
const candidateQueryTime = Date.now() - candidateQueryStart;
|
|
1308
|
-
|
|
1309
|
-
if (candidatePartitions.length === 0) {
|
|
1310
|
-
return { messages: [] };
|
|
1311
|
-
}
|
|
1312
|
-
|
|
1313
|
-
// ─────────────────────────────────────────────────────────────────────
|
|
1314
|
-
// PHASE 2: Try to Acquire Lease on a Candidate Partition (shared logic)
|
|
1315
|
-
// ─────────────────────────────────────────────────────────────────────
|
|
1316
|
-
|
|
1317
|
-
let acquiredPartition = null;
|
|
1318
|
-
const MAX_ATTEMPTS = accessMode === 'direct' ? 1 : 3;
|
|
1319
|
-
|
|
1320
|
-
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
1321
|
-
// Shuffle candidates to distribute across consumers
|
|
1322
|
-
const shuffled = [...candidatePartitions];
|
|
1323
|
-
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
1324
|
-
const j = Math.floor(Math.random() * (i + 1));
|
|
1325
|
-
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
// Try to acquire lease on each candidate
|
|
1329
|
-
for (const candidate of shuffled) {
|
|
1330
|
-
const leaseAcqStart = Date.now();
|
|
1331
|
-
|
|
1332
|
-
// Try to acquire lease - check if lease is available based on DB state
|
|
1333
|
-
// The key insight: released_at IS NULL means someone holds the lease
|
|
1334
|
-
// We can only acquire if: released_at IS NOT NULL OR lease expired
|
|
1335
|
-
try {
|
|
1336
|
-
// Get or create cursor position for this partition/consumer group
|
|
1337
|
-
// Handle subscription modes for bus mode (new-only, from-timestamp, etc.)
|
|
1338
|
-
let initialCursorId = '00000000-0000-0000-0000-000000000000';
|
|
1339
|
-
let initialCursorTimestamp = null;
|
|
1340
|
-
|
|
1341
|
-
// For bus mode with subscription preferences
|
|
1342
|
-
if (consumerGroup && (subscriptionMode || subscriptionFrom)) {
|
|
1343
|
-
if (subscriptionMode === 'new' || subscriptionMode === 'new-only' || subscriptionFrom === 'now') {
|
|
1344
|
-
// Start from NOW - get the latest message ID to use as cursor
|
|
1345
|
-
const latestMsg = await client.query(`
|
|
1346
|
-
SELECT id, created_at FROM queen.messages
|
|
1347
|
-
WHERE partition_id = $1
|
|
1348
|
-
ORDER BY created_at DESC, id DESC
|
|
1349
|
-
LIMIT 1
|
|
1350
|
-
`, [candidate.partition_id]);
|
|
1351
|
-
|
|
1352
|
-
if (latestMsg.rows.length > 0) {
|
|
1353
|
-
initialCursorId = latestMsg.rows[0].id;
|
|
1354
|
-
initialCursorTimestamp = latestMsg.rows[0].created_at;
|
|
1355
|
-
}
|
|
1356
|
-
}
|
|
1357
|
-
// else: subscriptionFrom='all' or no preference → start from beginning (00000000...)
|
|
1358
|
-
}
|
|
1359
|
-
|
|
1360
|
-
// Step 1: Ensure cursor exists (idempotent)
|
|
1361
|
-
await client.query(`
|
|
1362
|
-
INSERT INTO queen.partition_consumers (
|
|
1363
|
-
partition_id,
|
|
1364
|
-
consumer_group,
|
|
1365
|
-
last_consumed_id,
|
|
1366
|
-
last_consumed_created_at
|
|
1367
|
-
)
|
|
1368
|
-
VALUES ($1, $2, $3, $4)
|
|
1369
|
-
ON CONFLICT (partition_id, consumer_group) DO NOTHING
|
|
1370
|
-
`, [candidate.partition_id, actualConsumerGroup, initialCursorId, initialCursorTimestamp]);
|
|
1371
|
-
|
|
1372
|
-
// Generate unique lease ID for this POP operation
|
|
1373
|
-
const leaseId = generateUUID();
|
|
1374
|
-
|
|
1375
|
-
// Step 2: Try to acquire lease and get cursor position
|
|
1376
|
-
const leaseResult = await client.query(`
|
|
1377
|
-
UPDATE queen.partition_consumers
|
|
1378
|
-
SET lease_expires_at = NOW() + INTERVAL '1 second' * $3,
|
|
1379
|
-
lease_acquired_at = NOW(),
|
|
1380
|
-
worker_id = $4, -- Store lease ID in worker_id field
|
|
1381
|
-
message_batch = NULL,
|
|
1382
|
-
batch_size = 0,
|
|
1383
|
-
acked_count = 0
|
|
1384
|
-
WHERE partition_id = $1
|
|
1385
|
-
AND consumer_group = $2
|
|
1386
|
-
AND (lease_expires_at IS NULL OR lease_expires_at <= NOW())
|
|
1387
|
-
RETURNING partition_id, last_consumed_id, last_consumed_created_at,
|
|
1388
|
-
total_messages_consumed, lease_expires_at, worker_id as lease_id
|
|
1389
|
-
`, [candidate.partition_id, actualConsumerGroup, candidate.lease_time, leaseId]);
|
|
1390
|
-
|
|
1391
|
-
// Check if we acquired the lease
|
|
1392
|
-
const acquired = leaseResult.rows.length > 0;
|
|
1393
|
-
const result = leaseResult.rows[0];
|
|
1394
|
-
|
|
1395
|
-
if (acquired) {
|
|
1396
|
-
// Lease acquired! Now check if this partition has messages
|
|
1397
|
-
// If not, release and try next candidate
|
|
1398
|
-
const messageQueryStart = Date.now();
|
|
1399
|
-
|
|
1400
|
-
const cursor = result;
|
|
1401
|
-
const cursorId = cursor.last_consumed_id || '00000000-0000-0000-0000-000000000000';
|
|
1402
|
-
const totalConsumed = cursor.total_messages_consumed || 0;
|
|
1403
|
-
|
|
1404
|
-
// Cursor-based query using ONLY UUID comparison (UUIDv7 is time-ordered)
|
|
1405
|
-
// Conditionally add timestamp filters only when configured
|
|
1406
|
-
const hasDelayedProcessing = candidate.delayed_processing > 0;
|
|
1407
|
-
const hasEviction = candidate.max_wait_time_seconds > 0;
|
|
1408
|
-
|
|
1409
|
-
let whereClause = 'm.partition_id = $4 AND m.id > $5::uuid';
|
|
1410
|
-
let queryParams = [
|
|
1411
|
-
candidate.partition_name,
|
|
1412
|
-
candidate.queue_name,
|
|
1413
|
-
candidate.priority || 0,
|
|
1414
|
-
candidate.partition_id,
|
|
1415
|
-
cursorId
|
|
1416
|
-
];
|
|
1417
|
-
let paramIndex = 6;
|
|
1418
|
-
|
|
1419
|
-
if (hasDelayedProcessing) {
|
|
1420
|
-
whereClause += ` AND m.created_at <= NOW() - INTERVAL '1 second' * $${paramIndex}`;
|
|
1421
|
-
queryParams.push(candidate.delayed_processing);
|
|
1422
|
-
paramIndex++;
|
|
1423
|
-
}
|
|
1424
|
-
|
|
1425
|
-
if (hasEviction) {
|
|
1426
|
-
whereClause += ` AND m.created_at > NOW() - INTERVAL '1 second' * $${paramIndex}`;
|
|
1427
|
-
queryParams.push(candidate.max_wait_time_seconds);
|
|
1428
|
-
paramIndex++;
|
|
1429
|
-
}
|
|
1430
|
-
|
|
1431
|
-
queryParams.push(batch);
|
|
1432
|
-
const limitParam = `$${paramIndex}`;
|
|
1433
|
-
|
|
1434
|
-
const messagesResult = await client.query(`
|
|
1435
|
-
SELECT
|
|
1436
|
-
m.id,
|
|
1437
|
-
m.transaction_id,
|
|
1438
|
-
m.trace_id,
|
|
1439
|
-
m.payload,
|
|
1440
|
-
m.is_encrypted,
|
|
1441
|
-
m.created_at,
|
|
1442
|
-
$1 as partition_name,
|
|
1443
|
-
$2 as queue_name,
|
|
1444
|
-
$3 as priority,
|
|
1445
|
-
0 as retry_count
|
|
1446
|
-
FROM queen.messages m
|
|
1447
|
-
WHERE ${whereClause}
|
|
1448
|
-
ORDER BY m.created_at ASC, m.id ASC
|
|
1449
|
-
LIMIT ${limitParam}
|
|
1450
|
-
FOR UPDATE OF m SKIP LOCKED
|
|
1451
|
-
`, queryParams)
|
|
1452
|
-
.catch(err => {
|
|
1453
|
-
if (err.code === '55P03') { // Lock not available
|
|
1454
|
-
return { rows: [] };
|
|
1455
|
-
}
|
|
1456
|
-
throw err;
|
|
1457
|
-
});
|
|
1458
|
-
|
|
1459
|
-
const messageQueryTime = Date.now() - messageQueryStart;
|
|
1460
|
-
const messages = messagesResult.rows;
|
|
1461
|
-
|
|
1462
|
-
// Check if we found messages
|
|
1463
|
-
if (messages.length === 0) {
|
|
1464
|
-
// No messages in this partition (cursor exhausted) - release lease and try next candidate
|
|
1465
|
-
log(`[CURSOR] Partition ${candidate.partition_name} exhausted after ${totalConsumed} messages`);
|
|
1466
|
-
await client.query(`
|
|
1467
|
-
UPDATE queen.partition_consumers
|
|
1468
|
-
SET lease_expires_at = NULL,
|
|
1469
|
-
lease_acquired_at = NULL
|
|
1470
|
-
WHERE partition_id = $1 AND consumer_group = $2
|
|
1471
|
-
`, [candidate.partition_id, actualConsumerGroup]);
|
|
1472
|
-
continue; // Try next candidate partition
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
|
-
// Success! We have messages from this partition
|
|
1476
|
-
acquiredPartition = candidate;
|
|
1477
|
-
acquiredPartition.leaseId = leaseId; // Store lease ID for later use
|
|
1478
|
-
|
|
1479
|
-
// ─────────────────────────────────────────────────────────────────────
|
|
1480
|
-
// PHASE 4: Update Partition Lease with Message Batch
|
|
1481
|
-
// ─────────────────────────────────────────────────────────────────────
|
|
1482
|
-
|
|
1483
|
-
const leaseUpdateStart = Date.now();
|
|
1484
|
-
const messageIds = messages.map(m => m.id);
|
|
1485
|
-
await client.query(`
|
|
1486
|
-
UPDATE queen.partition_consumers
|
|
1487
|
-
SET message_batch = $1::jsonb,
|
|
1488
|
-
batch_size = $2::integer,
|
|
1489
|
-
acked_count = 0
|
|
1490
|
-
WHERE partition_id = $3 AND consumer_group = $4
|
|
1491
|
-
`, [JSON.stringify(messageIds), messageIds.length, acquiredPartition.partition_id, actualConsumerGroup]);
|
|
1492
|
-
const leaseUpdateTime = Date.now() - leaseUpdateStart;
|
|
1493
|
-
|
|
1494
|
-
log(`[CURSOR-LEASE] Partition: ${acquiredPartition.partition_name} | Leased ${messageIds.length} messages for processing`);
|
|
1495
|
-
|
|
1496
|
-
// ─────────────────────────────────────────────────────────────────────
|
|
1497
|
-
// PHASE 5: Decrypt and Format Messages
|
|
1498
|
-
// ─────────────────────────────────────────────────────────────────────
|
|
1499
|
-
|
|
1500
|
-
const decryptStart = Date.now();
|
|
1501
|
-
const formattedMessages = await Promise.all(messages.map(async (msg) => {
|
|
1502
|
-
let payload = msg.payload;
|
|
1503
|
-
|
|
1504
|
-
if (msg.is_encrypted && encryption.isEncryptionEnabled()) {
|
|
1505
|
-
try {
|
|
1506
|
-
payload = await encryption.decryptPayload(msg.payload);
|
|
1507
|
-
} catch (error) {
|
|
1508
|
-
log(`${LogTypes.ERROR} | Failed to decrypt message ${msg.transaction_id}: ${error.message}`);
|
|
1509
|
-
}
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
return {
|
|
1513
|
-
id: msg.id,
|
|
1514
|
-
transactionId: msg.transaction_id,
|
|
1515
|
-
traceId: msg.trace_id,
|
|
1516
|
-
queue: msg.queue_name,
|
|
1517
|
-
partition: msg.partition_name,
|
|
1518
|
-
data: payload,
|
|
1519
|
-
retryCount: msg.retry_count || 0,
|
|
1520
|
-
priority: msg.priority || 0,
|
|
1521
|
-
createdAt: msg.created_at,
|
|
1522
|
-
consumerGroup: consumerGroup || null,
|
|
1523
|
-
leaseId: acquiredPartition.leaseId // Add lease ID to each message
|
|
1524
|
-
};
|
|
1525
|
-
}));
|
|
1526
|
-
const decryptTime = Date.now() - decryptStart;
|
|
1527
|
-
|
|
1528
|
-
// ─────────────────────────────────────────────────────────────────────
|
|
1529
|
-
// PHASE 6: Metrics and Logging
|
|
1530
|
-
// ─────────────────────────────────────────────────────────────────────
|
|
1531
|
-
|
|
1532
|
-
const totalTime = Date.now() - startTime;
|
|
1533
|
-
const overhead = totalTime - (connectionWaitTime + candidateQueryTime + messageQueryTime + leaseUpdateTime + decryptTime);
|
|
1534
|
-
const throughput = totalTime > 0 ? (formattedMessages.length / (totalTime / 1000)).toFixed(0) : 0;
|
|
1535
|
-
|
|
1536
|
-
log(`[${LogTypes.POP}] Mode: ${accessMode} | Count: ${formattedMessages.length} | ${throughput} msg/s | ConsumerGroup: ${actualConsumerGroup === '__QUEUE_MODE__' ? 'QUEUE_MODE' : actualConsumerGroup}`);
|
|
1537
|
-
log(` ⏱️ ConnWait:${connectionWaitTime.toFixed(0)}ms CandidateQ:${candidateQueryTime.toFixed(0)}ms MsgQuery:${messageQueryTime.toFixed(0)}ms LeaseUpd:${leaseUpdateTime.toFixed(0)}ms Decrypt:${decryptTime.toFixed(0)}ms Overhead:${overhead.toFixed(0)}ms Total:${totalTime.toFixed(0)}ms`);
|
|
1538
|
-
|
|
1539
|
-
return { messages: formattedMessages };
|
|
1540
|
-
}
|
|
1541
|
-
} catch (err) {
|
|
1542
|
-
// Unexpected error
|
|
1543
|
-
if (!candidate.queue_name.startsWith('__')) {
|
|
1544
|
-
log(`LEASE ERROR: Partition=${candidate.partition_name} | Error: ${err.message}`);
|
|
1545
|
-
}
|
|
1546
|
-
throw err;
|
|
1547
|
-
}
|
|
1548
|
-
}
|
|
1549
|
-
|
|
1550
|
-
if (acquiredPartition) break;
|
|
1551
|
-
}
|
|
1552
|
-
|
|
1553
|
-
// If we exhausted all attempts and couldn't get messages
|
|
1554
|
-
if (!acquiredPartition) {
|
|
1555
|
-
log(`WARN: Failed to acquire any partition with messages after ${MAX_ATTEMPTS} attempts`);
|
|
1556
|
-
return { messages: [] };
|
|
1557
|
-
}
|
|
1558
|
-
});
|
|
1559
|
-
};
|
|
1560
|
-
|
|
1561
|
-
// Execute multiple operations in a single transaction
|
|
1562
|
-
const executeTransaction = async (operations, requiredLeases = []) => {
|
|
1563
|
-
return withTransaction(pool, async (client) => {
|
|
1564
|
-
// 1. Validate all required leases upfront
|
|
1565
|
-
if (requiredLeases.length > 0) {
|
|
1566
|
-
const leaseCheck = await client.query(`
|
|
1567
|
-
SELECT COUNT(*) = $2 as all_valid
|
|
1568
|
-
FROM queen.partition_consumers
|
|
1569
|
-
WHERE worker_id = ANY($1::varchar[])
|
|
1570
|
-
AND lease_expires_at > NOW()
|
|
1571
|
-
`, [requiredLeases, requiredLeases.length]);
|
|
1572
|
-
|
|
1573
|
-
if (!leaseCheck.rows[0].all_valid) {
|
|
1574
|
-
throw new Error('One or more leases invalid or expired');
|
|
1575
|
-
}
|
|
1576
|
-
}
|
|
1577
|
-
|
|
1578
|
-
// 2. Execute operations in order
|
|
1579
|
-
const results = [];
|
|
1580
|
-
for (const op of operations) {
|
|
1581
|
-
switch (op.type) {
|
|
1582
|
-
case 'push':
|
|
1583
|
-
const pushResult = await pushMessagesInternal(client, op.items);
|
|
1584
|
-
results.push({ type: 'push', result: pushResult });
|
|
1585
|
-
break;
|
|
1586
|
-
|
|
1587
|
-
case 'ack':
|
|
1588
|
-
const acks = [{
|
|
1589
|
-
transactionId: op.transactionId,
|
|
1590
|
-
status: op.status || 'completed',
|
|
1591
|
-
error: op.error
|
|
1592
|
-
}];
|
|
1593
|
-
const ackResult = await acknowledgeMessagesInternal(client, acks, op.consumerGroup);
|
|
1594
|
-
results.push({ type: 'ack', result: ackResult });
|
|
1595
|
-
break;
|
|
1596
|
-
|
|
1597
|
-
case 'extend':
|
|
1598
|
-
const extendResult = await client.query(`
|
|
1599
|
-
UPDATE queen.partition_consumers
|
|
1600
|
-
SET lease_expires_at = GREATEST(lease_expires_at, NOW() + INTERVAL '1 second' * $1)
|
|
1601
|
-
WHERE worker_id = $2 AND lease_expires_at > NOW()
|
|
1602
|
-
RETURNING lease_expires_at
|
|
1603
|
-
`, [op.seconds || 60, op.leaseId]);
|
|
1604
|
-
|
|
1605
|
-
if (extendResult.rows.length === 0) {
|
|
1606
|
-
throw new Error(`Cannot extend lease ${op.leaseId} - not found or expired`);
|
|
1607
|
-
}
|
|
1608
|
-
results.push({ type: 'extend', result: extendResult.rows[0] });
|
|
1609
|
-
break;
|
|
1610
|
-
|
|
1611
|
-
default:
|
|
1612
|
-
throw new Error(`Unknown operation type: ${op.type}`);
|
|
1613
|
-
}
|
|
1614
|
-
}
|
|
1615
|
-
|
|
1616
|
-
return results;
|
|
1617
|
-
});
|
|
1618
|
-
};
|
|
1619
|
-
|
|
1620
|
-
// Export all functions
|
|
1621
|
-
return {
|
|
1622
|
-
pushMessages: pushMessagesBatch,
|
|
1623
|
-
pushMessagesBatch,
|
|
1624
|
-
acknowledgeMessage,
|
|
1625
|
-
acknowledgeMessages,
|
|
1626
|
-
ackMessage: acknowledgeMessage, // Alias for compatibility
|
|
1627
|
-
reclaimExpiredLeases,
|
|
1628
|
-
getQueueStats,
|
|
1629
|
-
getQueueLag,
|
|
1630
|
-
uniquePop, // ← New unified function
|
|
1631
|
-
configureQueue,
|
|
1632
|
-
executeTransaction // ← New transaction support
|
|
1633
|
-
};
|
|
1634
|
-
};
|