queen-mq 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/API.md +1116 -0
- package/CACHE.md +519 -0
- package/DASHBOARD-V3.md +478 -0
- package/DASHBOARD.md +382 -0
- package/MOD_QUEUE.md +453 -0
- package/PARTITION_LOCKING_DESIGN.md +989 -0
- package/PLAN.md +707 -0
- package/QUERY_ANALSYS.md +72 -0
- package/QUEUE_BUS.md +334 -0
- package/README.md +1495 -0
- package/V2-PLAN.md +236 -0
- package/assets/dashboard.png +0 -0
- package/dashboard/.vscode/extensions.json +3 -0
- package/dashboard/README.md +5 -0
- package/dashboard/index.html +14 -0
- package/dashboard/package-lock.json +1458 -0
- package/dashboard/package.json +25 -0
- package/dashboard/public/vite.svg +1 -0
- package/dashboard/src/App.vue +29 -0
- package/dashboard/src/assets/styles/main.css +908 -0
- package/dashboard/src/assets/vue.svg +1 -0
- package/dashboard/src/components/cards/MetricCard.vue +298 -0
- package/dashboard/src/components/charts/QueueDepthChart.vue +276 -0
- package/dashboard/src/components/charts/QueueLagChart.vue +436 -0
- package/dashboard/src/components/charts/ThroughputChart.vue +302 -0
- package/dashboard/src/components/common/ActivityFeed.vue +251 -0
- package/dashboard/src/components/layout/AppHeader.vue +208 -0
- package/dashboard/src/components/layout/AppLayout.vue +88 -0
- package/dashboard/src/components/layout/AppSidebar.vue +261 -0
- package/dashboard/src/main.js +44 -0
- package/dashboard/src/router.js +54 -0
- package/dashboard/src/services/api.js +187 -0
- package/dashboard/src/services/websocket.js +167 -0
- package/dashboard/src/utils/constants.js +56 -0
- package/dashboard/src/utils/helpers.js +118 -0
- package/dashboard/src/views/Analytics.vue +912 -0
- package/dashboard/src/views/Dashboard.vue +906 -0
- package/dashboard/src/views/Messages.vue +437 -0
- package/dashboard/src/views/QueueDetail.vue +501 -0
- package/dashboard/src/views/Queues.vue +333 -0
- package/dashboard/vite.config.js +30 -0
- package/debug-namespace.js +110 -0
- package/docs/long-polling.md +159 -0
- package/docs/multi-server-cache-solutions.md +185 -0
- package/docs/performance-tuning.md +222 -0
- package/examples/bus-mode.js +239 -0
- package/examples/continuous-consumer-optimized.js +215 -0
- package/examples/continuous-consumer.js +159 -0
- package/examples/continuous-producer.js +343 -0
- package/examples/mixed-mode.js +277 -0
- package/examples/multi-server-test.js +305 -0
- package/examples/single.js +64 -0
- package/examples/smartchat-dealyed.js +42 -0
- package/examples/smartchat.js +52 -0
- package/examples/test-cache-invalidation.js +119 -0
- package/examples/test-cache-multi-server.js +245 -0
- package/examples/test-minimal-client.js +112 -0
- package/examples/test-queue-creation-policy.js +137 -0
- package/init-db.js +20 -0
- package/package.json +36 -0
- package/src/client/client.js +291 -0
- package/src/client/index.js +6 -0
- package/src/client/queenClient.js +513 -0
- package/src/client/utils/http.js +172 -0
- package/src/client/utils/loadBalancer.js +152 -0
- package/src/client/utils/retry.js +35 -0
- package/src/config.js +215 -0
- package/src/database/connection.js +103 -0
- package/src/database/poolManager.js +192 -0
- package/src/database/schema-v2.sql +214 -0
- package/src/managers/eventManager.js +59 -0
- package/src/managers/queueManagerOptimized.js +1512 -0
- package/src/managers/resourceCache.js +96 -0
- package/src/managers/systemEventManager.js +127 -0
- package/src/routes/ack.js +26 -0
- package/src/routes/analytics.js +812 -0
- package/src/routes/configure.js +46 -0
- package/src/routes/messages.js +298 -0
- package/src/routes/pop.js +85 -0
- package/src/routes/push.js +28 -0
- package/src/routes/resources.js +296 -0
- package/src/server.js +1286 -0
- package/src/services/encryptionService.js +82 -0
- package/src/services/evictionService.js +131 -0
- package/src/services/retentionService.js +129 -0
- package/src/services/startupSync.js +35 -0
- package/src/test/test.js +4521 -0
- package/src/utils/logger.js +44 -0
- package/src/utils/uuid.js +5 -0
- package/src/websocket/wsServer.js +221 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export const createConfigureRoute = (queueManager) => {
|
|
2
|
+
return async (body) => {
|
|
3
|
+
const { queue, partition, namespace, task, options = {} } = body;
|
|
4
|
+
|
|
5
|
+
if (!queue) {
|
|
6
|
+
throw new Error('queue is required');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Note: partition parameter is now ignored but still accepted for backward compatibility
|
|
10
|
+
// All configuration is at queue level now
|
|
11
|
+
|
|
12
|
+
const validOptions = {
|
|
13
|
+
leaseTime: options.leaseTime || 300,
|
|
14
|
+
maxSize: options.maxSize || 10000,
|
|
15
|
+
ttl: options.ttl || 3600,
|
|
16
|
+
retryLimit: options.retryLimit || 3,
|
|
17
|
+
retryDelay: options.retryDelay || 1000,
|
|
18
|
+
deadLetterQueue: options.deadLetterQueue || false,
|
|
19
|
+
dlqAfterMaxRetries: options.dlqAfterMaxRetries || false,
|
|
20
|
+
priority: options.priority || 0,
|
|
21
|
+
delayedProcessing: options.delayedProcessing || 0,
|
|
22
|
+
windowBuffer: options.windowBuffer || 0,
|
|
23
|
+
retentionSeconds: options.retentionSeconds || 0,
|
|
24
|
+
completedRetentionSeconds: options.completedRetentionSeconds || 0,
|
|
25
|
+
retentionEnabled: options.retentionEnabled || false,
|
|
26
|
+
encryptionEnabled: options.encryptionEnabled,
|
|
27
|
+
maxWaitTimeSeconds: options.maxWaitTimeSeconds
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Call the new queue-only configuration method
|
|
31
|
+
const result = await queueManager.configureQueue(queue, validOptions, namespace, task);
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
queue,
|
|
35
|
+
namespace: result.namespace,
|
|
36
|
+
task: result.task,
|
|
37
|
+
configured: true,
|
|
38
|
+
options: result.options,
|
|
39
|
+
// Include partition in response for backward compatibility but mark as deprecated
|
|
40
|
+
...(partition && {
|
|
41
|
+
partition,
|
|
42
|
+
_deprecation_notice: 'Partition-level configuration is deprecated. All configuration is now at queue level.'
|
|
43
|
+
})
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
};
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { log, LogTypes } from '../utils/logger.js';
|
|
2
|
+
import config from '../config.js';
|
|
3
|
+
|
|
4
|
+
export const createMessagesRoutes = (pool, queueManager) => {
|
|
5
|
+
|
|
6
|
+
// List messages with filters
|
|
7
|
+
const listMessages = async (filters = {}) => {
|
|
8
|
+
const { queue, partition, namespace, task, status, limit = config.API.DEFAULT_LIMIT, offset = config.API.DEFAULT_OFFSET } = filters;
|
|
9
|
+
|
|
10
|
+
let query = `
|
|
11
|
+
SELECT
|
|
12
|
+
m.id,
|
|
13
|
+
m.transaction_id,
|
|
14
|
+
m.payload,
|
|
15
|
+
m.status,
|
|
16
|
+
m.worker_id,
|
|
17
|
+
m.created_at,
|
|
18
|
+
m.locked_at,
|
|
19
|
+
m.completed_at,
|
|
20
|
+
m.failed_at,
|
|
21
|
+
m.error_message,
|
|
22
|
+
m.retry_count,
|
|
23
|
+
m.lease_expires_at,
|
|
24
|
+
q.name || '/' || p.name as queue_path,
|
|
25
|
+
q.name as queue_name,
|
|
26
|
+
p.name as partition_name,
|
|
27
|
+
q.namespace,
|
|
28
|
+
q.task
|
|
29
|
+
FROM queen.messages m
|
|
30
|
+
JOIN queen.partitions p ON p.id = m.partition_id
|
|
31
|
+
JOIN queen.queues q ON q.id = p.queue_id
|
|
32
|
+
WHERE 1=1
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
const params = [];
|
|
36
|
+
let paramCount = 0;
|
|
37
|
+
|
|
38
|
+
if (queue) {
|
|
39
|
+
params.push(queue);
|
|
40
|
+
query += ` AND q.name = $${++paramCount}`;
|
|
41
|
+
}
|
|
42
|
+
if (partition) {
|
|
43
|
+
params.push(partition);
|
|
44
|
+
query += ` AND p.name = $${++paramCount}`;
|
|
45
|
+
}
|
|
46
|
+
if (namespace) {
|
|
47
|
+
params.push(namespace);
|
|
48
|
+
query += ` AND q.namespace = $${++paramCount}`;
|
|
49
|
+
}
|
|
50
|
+
if (task) {
|
|
51
|
+
params.push(task);
|
|
52
|
+
query += ` AND q.task = $${++paramCount}`;
|
|
53
|
+
}
|
|
54
|
+
if (status) {
|
|
55
|
+
params.push(status);
|
|
56
|
+
query += ` AND m.status = $${++paramCount}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
query += ` ORDER BY m.created_at DESC`;
|
|
60
|
+
|
|
61
|
+
params.push(limit);
|
|
62
|
+
query += ` LIMIT $${++paramCount}`;
|
|
63
|
+
|
|
64
|
+
params.push(offset);
|
|
65
|
+
query += ` OFFSET $${++paramCount}`;
|
|
66
|
+
|
|
67
|
+
const result = await pool.query(query, params);
|
|
68
|
+
|
|
69
|
+
return result.rows.map(row => ({
|
|
70
|
+
id: row.id,
|
|
71
|
+
transactionId: row.transaction_id,
|
|
72
|
+
queuePath: row.queue_path,
|
|
73
|
+
queue: row.queue_name,
|
|
74
|
+
partition: row.partition_name,
|
|
75
|
+
namespace: row.namespace,
|
|
76
|
+
task: row.task,
|
|
77
|
+
payload: row.payload,
|
|
78
|
+
status: row.status,
|
|
79
|
+
workerId: row.worker_id,
|
|
80
|
+
createdAt: row.created_at,
|
|
81
|
+
lockedAt: row.locked_at,
|
|
82
|
+
completedAt: row.completed_at,
|
|
83
|
+
failedAt: row.failed_at,
|
|
84
|
+
errorMessage: row.error_message,
|
|
85
|
+
retryCount: row.retry_count,
|
|
86
|
+
leaseExpiresAt: row.lease_expires_at
|
|
87
|
+
}));
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Get single message by transaction ID
|
|
91
|
+
const getMessage = async (transactionId) => {
|
|
92
|
+
const result = await pool.query(`
|
|
93
|
+
SELECT
|
|
94
|
+
m.*,
|
|
95
|
+
q.name || '/' || p.name as queue_path,
|
|
96
|
+
q.name as queue_name,
|
|
97
|
+
p.name as partition_name,
|
|
98
|
+
q.namespace,
|
|
99
|
+
q.task,
|
|
100
|
+
q.lease_time,
|
|
101
|
+
q.retry_limit,
|
|
102
|
+
q.retry_delay,
|
|
103
|
+
q.ttl,
|
|
104
|
+
q.priority
|
|
105
|
+
FROM queen.messages m
|
|
106
|
+
JOIN queen.partitions p ON p.id = m.partition_id
|
|
107
|
+
JOIN queen.queues q ON q.id = p.queue_id
|
|
108
|
+
WHERE m.transaction_id = $1
|
|
109
|
+
`, [transactionId]);
|
|
110
|
+
|
|
111
|
+
if (result.rows.length === 0) {
|
|
112
|
+
throw new Error('Message not found');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const row = result.rows[0];
|
|
116
|
+
return {
|
|
117
|
+
id: row.id,
|
|
118
|
+
transactionId: row.transaction_id,
|
|
119
|
+
queuePath: row.queue_path,
|
|
120
|
+
queue: row.queue_name,
|
|
121
|
+
partition: row.partition_name,
|
|
122
|
+
namespace: row.namespace,
|
|
123
|
+
task: row.task,
|
|
124
|
+
payload: row.payload,
|
|
125
|
+
status: row.status,
|
|
126
|
+
workerId: row.worker_id,
|
|
127
|
+
createdAt: row.created_at,
|
|
128
|
+
lockedAt: row.locked_at,
|
|
129
|
+
completedAt: row.completed_at,
|
|
130
|
+
failedAt: row.failed_at,
|
|
131
|
+
errorMessage: row.error_message,
|
|
132
|
+
retryCount: row.retry_count,
|
|
133
|
+
leaseExpiresAt: row.lease_expires_at,
|
|
134
|
+
queueConfig: {
|
|
135
|
+
leaseTime: row.lease_time,
|
|
136
|
+
retryLimit: row.retry_limit,
|
|
137
|
+
retryDelay: row.retry_delay,
|
|
138
|
+
ttl: row.ttl,
|
|
139
|
+
priority: row.priority
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// Delete message
|
|
145
|
+
const deleteMessage = async (transactionId) => {
|
|
146
|
+
const result = await pool.query(`
|
|
147
|
+
DELETE FROM queen.messages
|
|
148
|
+
WHERE transaction_id = $1
|
|
149
|
+
RETURNING id
|
|
150
|
+
`, [transactionId]);
|
|
151
|
+
|
|
152
|
+
if (result.rows.length === 0) {
|
|
153
|
+
throw new Error('Message not found');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
log(`${LogTypes.DELETE} | TransactionId: ${transactionId} | MessageId: ${result.rows[0].id}`);
|
|
157
|
+
return { deleted: true, transactionId };
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// Retry message
|
|
161
|
+
const retryMessage = async (transactionId) => {
|
|
162
|
+
const result = await pool.query(`
|
|
163
|
+
UPDATE queen.messages
|
|
164
|
+
SET
|
|
165
|
+
status = 'pending',
|
|
166
|
+
worker_id = NULL,
|
|
167
|
+
locked_at = NULL,
|
|
168
|
+
lease_expires_at = NULL,
|
|
169
|
+
retry_count = retry_count + 1,
|
|
170
|
+
error_message = NULL
|
|
171
|
+
WHERE transaction_id = $1
|
|
172
|
+
AND status IN ('failed', 'dead_letter')
|
|
173
|
+
RETURNING id, retry_count
|
|
174
|
+
`, [transactionId]);
|
|
175
|
+
|
|
176
|
+
if (result.rows.length === 0) {
|
|
177
|
+
throw new Error('Message not found or not in failed state');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
log(`${LogTypes.RETRY} | TransactionId: ${transactionId} | NewRetryCount: ${result.rows[0].retry_count}`);
|
|
181
|
+
return { retried: true, transactionId };
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// Move message to DLQ
|
|
185
|
+
const moveToDLQ = async (transactionId) => {
|
|
186
|
+
const result = await pool.query(`
|
|
187
|
+
UPDATE queen.messages
|
|
188
|
+
SET
|
|
189
|
+
status = 'dead_letter',
|
|
190
|
+
failed_at = NOW()
|
|
191
|
+
WHERE transaction_id = $1
|
|
192
|
+
AND status = 'failed'
|
|
193
|
+
RETURNING id
|
|
194
|
+
`, [transactionId]);
|
|
195
|
+
|
|
196
|
+
if (result.rows.length === 0) {
|
|
197
|
+
throw new Error('Message not found or not in failed state');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
log(`${LogTypes.MANUAL_DLQ} | TransactionId: ${transactionId} | MessageId: ${result.rows[0].id}`);
|
|
201
|
+
return { movedToDLQ: true, transactionId };
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// Get related messages (same partition, near in time)
|
|
205
|
+
const getRelatedMessages = async (transactionId) => {
|
|
206
|
+
// First get the message details
|
|
207
|
+
const msgResult = await pool.query(`
|
|
208
|
+
SELECT partition_id, created_at
|
|
209
|
+
FROM queen.messages
|
|
210
|
+
WHERE transaction_id = $1
|
|
211
|
+
`, [transactionId]);
|
|
212
|
+
|
|
213
|
+
if (msgResult.rows.length === 0) {
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const { partition_id, created_at } = msgResult.rows[0];
|
|
218
|
+
|
|
219
|
+
// Get messages from same partition within 1 hour
|
|
220
|
+
const result = await pool.query(`
|
|
221
|
+
SELECT
|
|
222
|
+
m.transaction_id,
|
|
223
|
+
m.status,
|
|
224
|
+
m.created_at,
|
|
225
|
+
m.payload
|
|
226
|
+
FROM queen.messages m
|
|
227
|
+
WHERE m.partition_id = $1
|
|
228
|
+
AND m.transaction_id != $2
|
|
229
|
+
AND m.created_at BETWEEN $3::timestamp - INTERVAL '1 hour'
|
|
230
|
+
AND $3::timestamp + INTERVAL '1 hour'
|
|
231
|
+
ORDER BY ABS(EXTRACT(EPOCH FROM (m.created_at - $3::timestamp)))
|
|
232
|
+
LIMIT ${config.ANALYTICS.MAX_RELATED_MESSAGES}
|
|
233
|
+
`, [partition_id, transactionId, created_at]);
|
|
234
|
+
|
|
235
|
+
return result.rows.map(row => ({
|
|
236
|
+
transactionId: row.transaction_id,
|
|
237
|
+
status: row.status,
|
|
238
|
+
createdAt: row.created_at,
|
|
239
|
+
payload: row.payload
|
|
240
|
+
}));
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
// Clear all messages in a queue or partition
|
|
244
|
+
const clearQueue = async (queue, partition = null) => {
|
|
245
|
+
let query;
|
|
246
|
+
let params;
|
|
247
|
+
|
|
248
|
+
if (partition) {
|
|
249
|
+
// Clear specific partition
|
|
250
|
+
query = `
|
|
251
|
+
DELETE FROM queen.messages
|
|
252
|
+
WHERE partition_id IN (
|
|
253
|
+
SELECT p.id
|
|
254
|
+
FROM queen.partitions p
|
|
255
|
+
JOIN queen.queues q ON q.id = p.queue_id
|
|
256
|
+
WHERE q.name = $1 AND p.name = $2
|
|
257
|
+
)
|
|
258
|
+
RETURNING id
|
|
259
|
+
`;
|
|
260
|
+
params = [queue, partition];
|
|
261
|
+
} else {
|
|
262
|
+
// Clear entire queue (all partitions)
|
|
263
|
+
query = `
|
|
264
|
+
DELETE FROM queen.messages
|
|
265
|
+
WHERE partition_id IN (
|
|
266
|
+
SELECT p.id
|
|
267
|
+
FROM queen.partitions p
|
|
268
|
+
JOIN queen.queues q ON q.id = p.queue_id
|
|
269
|
+
WHERE q.name = $1
|
|
270
|
+
)
|
|
271
|
+
RETURNING id
|
|
272
|
+
`;
|
|
273
|
+
params = [queue];
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const result = await pool.query(query, params);
|
|
277
|
+
|
|
278
|
+
const clearedCount = result.rowCount || 0;
|
|
279
|
+
log(`${LogTypes.CLEAR_QUEUE} | Queue: ${queue} | Partition: ${partition || 'all'} | Count: ${clearedCount} messages deleted`);
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
cleared: true,
|
|
283
|
+
count: clearedCount,
|
|
284
|
+
queue,
|
|
285
|
+
partition: partition || 'all'
|
|
286
|
+
};
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
listMessages,
|
|
291
|
+
getMessage,
|
|
292
|
+
deleteMessage,
|
|
293
|
+
retryMessage,
|
|
294
|
+
moveToDLQ,
|
|
295
|
+
getRelatedMessages,
|
|
296
|
+
clearQueue
|
|
297
|
+
};
|
|
298
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import config from '../config.js';
|
|
2
|
+
|
|
3
|
+
export const createPopRoute = (queueManager, eventManager) => {
|
|
4
|
+
return async (scope, options = {}) => {
|
|
5
|
+
const {
|
|
6
|
+
wait = false,
|
|
7
|
+
timeout = config.QUEUE.DEFAULT_TIMEOUT,
|
|
8
|
+
batch = config.QUEUE.DEFAULT_BATCH_SIZE,
|
|
9
|
+
consumerGroup = scope.consumerGroup || null,
|
|
10
|
+
subscriptionMode = scope.subscriptionMode || null,
|
|
11
|
+
subscriptionFrom = scope.subscriptionFrom || null
|
|
12
|
+
} = options;
|
|
13
|
+
const maxTimeout = config.QUEUE.MAX_TIMEOUT;
|
|
14
|
+
const effectiveTimeout = Math.min(timeout, maxTimeout);
|
|
15
|
+
|
|
16
|
+
// Handle different pop scenarios
|
|
17
|
+
if (scope.namespace || scope.task) {
|
|
18
|
+
// Pop with filters (namespace or task)
|
|
19
|
+
let result = await queueManager.popMessagesWithFilters(scope, { batch, wait: false, timeout: effectiveTimeout });
|
|
20
|
+
|
|
21
|
+
if (result.messages.length > 0 || !wait) {
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Simple polling for filtered pops (no specific queue path to wait on)
|
|
26
|
+
const startTime = Date.now();
|
|
27
|
+
const pollInterval = Math.min(config.QUEUE.POLL_INTERVAL_FILTERED, effectiveTimeout / 10);
|
|
28
|
+
|
|
29
|
+
while (Date.now() - startTime < effectiveTimeout) {
|
|
30
|
+
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
31
|
+
result = await queueManager.popMessagesWithFilters(scope, { batch, wait: false });
|
|
32
|
+
if (result.messages.length > 0) {
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return { messages: [] };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Regular queue/partition pop (now with consumer group support)
|
|
41
|
+
const popScope = {
|
|
42
|
+
...scope,
|
|
43
|
+
consumerGroup: consumerGroup || scope.consumerGroup
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const popOptions = {
|
|
47
|
+
batch,
|
|
48
|
+
wait: false,
|
|
49
|
+
timeout: effectiveTimeout,
|
|
50
|
+
subscriptionMode,
|
|
51
|
+
subscriptionFrom
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
let result = await queueManager.popMessages(popScope, popOptions);
|
|
55
|
+
|
|
56
|
+
if (result.messages.length > 0 || !wait) {
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Long polling with event-driven optimization
|
|
61
|
+
const queuePath = scope.partition
|
|
62
|
+
? `${scope.queue}/${scope.partition}`
|
|
63
|
+
: scope.queue;
|
|
64
|
+
|
|
65
|
+
const startTime = Date.now();
|
|
66
|
+
const pollInterval = Math.min(config.QUEUE.POLL_INTERVAL, effectiveTimeout / 10);
|
|
67
|
+
|
|
68
|
+
while (Date.now() - startTime < effectiveTimeout) {
|
|
69
|
+
// Wait for message notification or short timeout
|
|
70
|
+
const notification = await Promise.race([
|
|
71
|
+
eventManager.waitForMessage(queuePath, pollInterval),
|
|
72
|
+
new Promise(resolve => setTimeout(() => resolve(null), pollInterval))
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
// Try to get messages whether notified or not
|
|
76
|
+
result = await queueManager.popMessages(popScope, popOptions);
|
|
77
|
+
if (result.messages.length > 0) {
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Timeout reached, no messages
|
|
83
|
+
return { messages: [] };
|
|
84
|
+
};
|
|
85
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const createPushRoute = (queueManager) => {
|
|
2
|
+
return async (body) => {
|
|
3
|
+
const { items = [], config = {} } = body;
|
|
4
|
+
|
|
5
|
+
if (!items || items.length === 0) {
|
|
6
|
+
throw new Error('No items to push');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Validate items
|
|
10
|
+
for (const item of items) {
|
|
11
|
+
if (!item.queue) {
|
|
12
|
+
throw new Error('Each item must have a queue');
|
|
13
|
+
}
|
|
14
|
+
// Allow missing, null or undefined payloads - they'll be stored as null in JSONB
|
|
15
|
+
// If payload is not provided, set it to null
|
|
16
|
+
if (!('payload' in item)) {
|
|
17
|
+
item.payload = null;
|
|
18
|
+
}
|
|
19
|
+
// partition is optional, defaults to "Default"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const results = await queueManager.pushMessages(items);
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
messages: results
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
};
|