queen-mq 0.7.4 → 0.12.1
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/client-v2/Queen.js +27 -3
- package/client-v2/admin/Admin.js +414 -0
- package/client-v2/admin/index.js +6 -0
- package/client-v2/builders/TransactionBuilder.js +11 -4
- package/client-v2/http/HttpClient.js +12 -3
- package/client-v2/index.js +1 -0
- package/client-v2/utils/defaults.js +2 -1
- package/client-v2/utils/logger.js +13 -2
- package/package.json +1 -1
- package/test-v2/consume.js +6 -57
- package/test-v2/load.js +22 -20
- package/test-v2/maintenance.js +2 -0
- package/test-v2/push.js +0 -29
- package/test-v2/run.js +24 -3
- package/test-v2/transaction.js +66 -0
package/client-v2/Queen.js
CHANGED
|
@@ -10,6 +10,7 @@ import { QueueBuilder } from './builders/QueueBuilder.js'
|
|
|
10
10
|
import { TransactionBuilder } from './builders/TransactionBuilder.js'
|
|
11
11
|
import { StreamBuilder } from './stream/StreamBuilder.js'
|
|
12
12
|
import { StreamConsumer } from './stream/StreamConsumer.js'
|
|
13
|
+
import { Admin } from './admin/Admin.js'
|
|
13
14
|
import { CLIENT_DEFAULTS } from './utils/defaults.js'
|
|
14
15
|
import { validateUrl, validateUrls } from './utils/validation.js'
|
|
15
16
|
import * as logger from './utils/logger.js'
|
|
@@ -19,6 +20,7 @@ export class Queen {
|
|
|
19
20
|
#bufferManager
|
|
20
21
|
#config
|
|
21
22
|
#shutdownHandlers = []
|
|
23
|
+
#admin = null
|
|
22
24
|
|
|
23
25
|
constructor(config = {}) {
|
|
24
26
|
logger.log('Queen.constructor', { config: typeof config === 'object' && !Array.isArray(config) ? { ...config, urls: config.urls?.length || 0 } : { type: typeof config } })
|
|
@@ -75,7 +77,7 @@ export class Queen {
|
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
#createHttpClient() {
|
|
78
|
-
const { urls, timeoutMillis, retryAttempts, retryDelayMillis, loadBalancingStrategy, affinityHashRing, healthRetryAfterMillis, enableFailover } = this.#config
|
|
80
|
+
const { urls, timeoutMillis, retryAttempts, retryDelayMillis, loadBalancingStrategy, affinityHashRing, healthRetryAfterMillis, enableFailover, bearerToken } = this.#config
|
|
79
81
|
|
|
80
82
|
if (urls.length === 1) {
|
|
81
83
|
// Single server
|
|
@@ -83,7 +85,8 @@ export class Queen {
|
|
|
83
85
|
baseUrl: urls[0],
|
|
84
86
|
timeoutMillis,
|
|
85
87
|
retryAttempts,
|
|
86
|
-
retryDelayMillis
|
|
88
|
+
retryDelayMillis,
|
|
89
|
+
bearerToken
|
|
87
90
|
})
|
|
88
91
|
}
|
|
89
92
|
|
|
@@ -97,11 +100,17 @@ export class Queen {
|
|
|
97
100
|
timeoutMillis,
|
|
98
101
|
retryAttempts,
|
|
99
102
|
retryDelayMillis,
|
|
100
|
-
enableFailover
|
|
103
|
+
enableFailover,
|
|
104
|
+
bearerToken
|
|
101
105
|
})
|
|
102
106
|
}
|
|
103
107
|
|
|
104
108
|
#setupGracefulShutdown() {
|
|
109
|
+
// Skip signal handlers in browser environment
|
|
110
|
+
if (typeof process === 'undefined' || typeof process.on !== 'function') {
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
|
|
105
114
|
let signalReceivedCount = 0;
|
|
106
115
|
const shutdown = async (signal) => {
|
|
107
116
|
console.log(`\nReceived ${signal}, shutting down gracefully...`)
|
|
@@ -140,6 +149,21 @@ export class Queen {
|
|
|
140
149
|
return new QueueBuilder(this, this.#httpClient, this.#bufferManager, name)
|
|
141
150
|
}
|
|
142
151
|
|
|
152
|
+
// ===========================
|
|
153
|
+
// Admin API Entry Point
|
|
154
|
+
// ===========================
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Get the Admin API for administrative and observability operations
|
|
158
|
+
* @returns {Admin} Admin API instance (lazily initialized, singleton)
|
|
159
|
+
*/
|
|
160
|
+
get admin() {
|
|
161
|
+
if (!this.#admin) {
|
|
162
|
+
this.#admin = new Admin(this.#httpClient)
|
|
163
|
+
}
|
|
164
|
+
return this.#admin
|
|
165
|
+
}
|
|
166
|
+
|
|
143
167
|
// ===========================
|
|
144
168
|
// Transaction API
|
|
145
169
|
// ===========================
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin API for Queen - Administrative and observability endpoints
|
|
3
|
+
* These APIs are typically used by dashboards and admin tools, not regular applications
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as logger from '../utils/logger.js'
|
|
7
|
+
|
|
8
|
+
export class Admin {
|
|
9
|
+
#httpClient
|
|
10
|
+
|
|
11
|
+
constructor(httpClient) {
|
|
12
|
+
this.#httpClient = httpClient
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// ===========================
|
|
16
|
+
// Resources API
|
|
17
|
+
// ===========================
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get system overview with queue counts, message stats, etc.
|
|
21
|
+
* @returns {Promise<object>}
|
|
22
|
+
*/
|
|
23
|
+
async getOverview() {
|
|
24
|
+
logger.log('Admin.getOverview', {})
|
|
25
|
+
return this.#httpClient.get('/api/v1/resources/overview')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get all namespaces
|
|
30
|
+
* @returns {Promise<object>}
|
|
31
|
+
*/
|
|
32
|
+
async getNamespaces() {
|
|
33
|
+
logger.log('Admin.getNamespaces', {})
|
|
34
|
+
return this.#httpClient.get('/api/v1/resources/namespaces')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get all tasks
|
|
39
|
+
* @returns {Promise<object>}
|
|
40
|
+
*/
|
|
41
|
+
async getTasks() {
|
|
42
|
+
logger.log('Admin.getTasks', {})
|
|
43
|
+
return this.#httpClient.get('/api/v1/resources/tasks')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ===========================
|
|
47
|
+
// Queues API
|
|
48
|
+
// ===========================
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* List all queues
|
|
52
|
+
* @param {object} params - Query parameters (limit, offset, search, etc.)
|
|
53
|
+
* @returns {Promise<object>}
|
|
54
|
+
*/
|
|
55
|
+
async listQueues(params = {}) {
|
|
56
|
+
logger.log('Admin.listQueues', { params })
|
|
57
|
+
const queryString = this.#buildQueryString(params)
|
|
58
|
+
return this.#httpClient.get(`/api/v1/resources/queues${queryString}`)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get queue details
|
|
63
|
+
* @param {string} name - Queue name
|
|
64
|
+
* @returns {Promise<object>}
|
|
65
|
+
*/
|
|
66
|
+
async getQueue(name) {
|
|
67
|
+
logger.log('Admin.getQueue', { name })
|
|
68
|
+
return this.#httpClient.get(`/api/v1/resources/queues/${encodeURIComponent(name)}`)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Clear all messages from a queue
|
|
73
|
+
* @param {string} name - Queue name
|
|
74
|
+
* @param {string} [partition] - Optional partition to clear
|
|
75
|
+
* @returns {Promise<object>}
|
|
76
|
+
*/
|
|
77
|
+
async clearQueue(name, partition = null) {
|
|
78
|
+
logger.log('Admin.clearQueue', { name, partition })
|
|
79
|
+
const queryString = partition ? `?partition=${encodeURIComponent(partition)}` : ''
|
|
80
|
+
return this.#httpClient.delete(`/api/v1/queues/${encodeURIComponent(name)}/clear${queryString}`)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get all partitions
|
|
85
|
+
* @param {object} params - Query parameters (queue, limit, etc.)
|
|
86
|
+
* @returns {Promise<object>}
|
|
87
|
+
*/
|
|
88
|
+
async getPartitions(params = {}) {
|
|
89
|
+
logger.log('Admin.getPartitions', { params })
|
|
90
|
+
const queryString = this.#buildQueryString(params)
|
|
91
|
+
return this.#httpClient.get(`/api/v1/resources/partitions${queryString}`)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ===========================
|
|
95
|
+
// Messages API
|
|
96
|
+
// ===========================
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* List messages with filters
|
|
100
|
+
* @param {object} params - Query parameters (queue, partition, status, limit, offset, etc.)
|
|
101
|
+
* @returns {Promise<object>}
|
|
102
|
+
*/
|
|
103
|
+
async listMessages(params = {}) {
|
|
104
|
+
logger.log('Admin.listMessages', { params })
|
|
105
|
+
const queryString = this.#buildQueryString(params)
|
|
106
|
+
return this.#httpClient.get(`/api/v1/messages${queryString}`)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get a specific message
|
|
111
|
+
* @param {string} partitionId - Partition ID
|
|
112
|
+
* @param {string} transactionId - Transaction ID
|
|
113
|
+
* @returns {Promise<object>}
|
|
114
|
+
*/
|
|
115
|
+
async getMessage(partitionId, transactionId) {
|
|
116
|
+
logger.log('Admin.getMessage', { partitionId, transactionId })
|
|
117
|
+
return this.#httpClient.get(`/api/v1/messages/${partitionId}/${transactionId}`)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Delete a specific message
|
|
122
|
+
* @param {string} partitionId - Partition ID
|
|
123
|
+
* @param {string} transactionId - Transaction ID
|
|
124
|
+
* @returns {Promise<object>}
|
|
125
|
+
*/
|
|
126
|
+
async deleteMessage(partitionId, transactionId) {
|
|
127
|
+
logger.log('Admin.deleteMessage', { partitionId, transactionId })
|
|
128
|
+
return this.#httpClient.delete(`/api/v1/messages/${partitionId}/${transactionId}`)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Retry a failed message
|
|
133
|
+
* @param {string} partitionId - Partition ID
|
|
134
|
+
* @param {string} transactionId - Transaction ID
|
|
135
|
+
* @returns {Promise<object>}
|
|
136
|
+
*/
|
|
137
|
+
async retryMessage(partitionId, transactionId) {
|
|
138
|
+
logger.log('Admin.retryMessage', { partitionId, transactionId })
|
|
139
|
+
return this.#httpClient.post(`/api/v1/messages/${partitionId}/${transactionId}/retry`, {})
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Move a message to the Dead Letter Queue
|
|
144
|
+
* @param {string} partitionId - Partition ID
|
|
145
|
+
* @param {string} transactionId - Transaction ID
|
|
146
|
+
* @returns {Promise<object>}
|
|
147
|
+
*/
|
|
148
|
+
async moveMessageToDLQ(partitionId, transactionId) {
|
|
149
|
+
logger.log('Admin.moveMessageToDLQ', { partitionId, transactionId })
|
|
150
|
+
return this.#httpClient.post(`/api/v1/messages/${partitionId}/${transactionId}/dlq`, {})
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ===========================
|
|
154
|
+
// Traces API
|
|
155
|
+
// ===========================
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Get traces by name
|
|
159
|
+
* @param {string} traceName - Trace name to search for
|
|
160
|
+
* @param {object} params - Query parameters (limit, offset, from, to, etc.)
|
|
161
|
+
* @returns {Promise<object>}
|
|
162
|
+
*/
|
|
163
|
+
async getTracesByName(traceName, params = {}) {
|
|
164
|
+
logger.log('Admin.getTracesByName', { traceName, params })
|
|
165
|
+
const queryString = this.#buildQueryString(params)
|
|
166
|
+
return this.#httpClient.get(`/api/v1/traces/by-name/${encodeURIComponent(traceName)}${queryString}`)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Get available trace names
|
|
171
|
+
* @param {object} params - Query parameters (limit, search, etc.)
|
|
172
|
+
* @returns {Promise<object>}
|
|
173
|
+
*/
|
|
174
|
+
async getTraceNames(params = {}) {
|
|
175
|
+
logger.log('Admin.getTraceNames', { params })
|
|
176
|
+
const queryString = this.#buildQueryString(params)
|
|
177
|
+
return this.#httpClient.get(`/api/v1/traces/names${queryString}`)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Get traces for a specific message
|
|
182
|
+
* @param {string} partitionId - Partition ID
|
|
183
|
+
* @param {string} transactionId - Transaction ID
|
|
184
|
+
* @returns {Promise<object>}
|
|
185
|
+
*/
|
|
186
|
+
async getTracesForMessage(partitionId, transactionId) {
|
|
187
|
+
logger.log('Admin.getTracesForMessage', { partitionId, transactionId })
|
|
188
|
+
return this.#httpClient.get(`/api/v1/traces/${partitionId}/${transactionId}`)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ===========================
|
|
192
|
+
// Analytics/Status API
|
|
193
|
+
// ===========================
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Get system status
|
|
197
|
+
* @param {object} params - Query parameters
|
|
198
|
+
* @returns {Promise<object>}
|
|
199
|
+
*/
|
|
200
|
+
async getStatus(params = {}) {
|
|
201
|
+
logger.log('Admin.getStatus', { params })
|
|
202
|
+
const queryString = this.#buildQueryString(params)
|
|
203
|
+
return this.#httpClient.get(`/api/v1/status${queryString}`)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Get queue statistics
|
|
208
|
+
* @param {object} params - Query parameters (limit, offset, etc.)
|
|
209
|
+
* @returns {Promise<object>}
|
|
210
|
+
*/
|
|
211
|
+
async getQueueStats(params = {}) {
|
|
212
|
+
logger.log('Admin.getQueueStats', { params })
|
|
213
|
+
const queryString = this.#buildQueryString(params)
|
|
214
|
+
return this.#httpClient.get(`/api/v1/status/queues${queryString}`)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Get detailed statistics for a specific queue
|
|
219
|
+
* @param {string} name - Queue name
|
|
220
|
+
* @param {object} params - Query parameters
|
|
221
|
+
* @returns {Promise<object>}
|
|
222
|
+
*/
|
|
223
|
+
async getQueueDetail(name, params = {}) {
|
|
224
|
+
logger.log('Admin.getQueueDetail', { name, params })
|
|
225
|
+
const queryString = this.#buildQueryString(params)
|
|
226
|
+
return this.#httpClient.get(`/api/v1/status/queues/${encodeURIComponent(name)}${queryString}`)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Get analytics data
|
|
231
|
+
* @param {object} params - Query parameters (from, to, interval, etc.)
|
|
232
|
+
* @returns {Promise<object>}
|
|
233
|
+
*/
|
|
234
|
+
async getAnalytics(params = {}) {
|
|
235
|
+
logger.log('Admin.getAnalytics', { params })
|
|
236
|
+
const queryString = this.#buildQueryString(params)
|
|
237
|
+
return this.#httpClient.get(`/api/v1/status/analytics${queryString}`)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ===========================
|
|
241
|
+
// Consumer Groups API
|
|
242
|
+
// ===========================
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* List all consumer groups
|
|
246
|
+
* @returns {Promise<object>}
|
|
247
|
+
*/
|
|
248
|
+
async listConsumerGroups() {
|
|
249
|
+
logger.log('Admin.listConsumerGroups', {})
|
|
250
|
+
return this.#httpClient.get('/api/v1/consumer-groups')
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Refresh consumer group statistics
|
|
255
|
+
* @returns {Promise<object>}
|
|
256
|
+
*/
|
|
257
|
+
async refreshConsumerStats() {
|
|
258
|
+
logger.log('Admin.refreshConsumerStats', {})
|
|
259
|
+
return this.#httpClient.post('/api/v1/stats/refresh', {})
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Get consumer group details
|
|
264
|
+
* @param {string} name - Consumer group name
|
|
265
|
+
* @returns {Promise<object>}
|
|
266
|
+
*/
|
|
267
|
+
async getConsumerGroup(name) {
|
|
268
|
+
logger.log('Admin.getConsumerGroup', { name })
|
|
269
|
+
return this.#httpClient.get(`/api/v1/consumer-groups/${encodeURIComponent(name)}`)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Get lagging consumer groups
|
|
274
|
+
* @param {number} [minLagSeconds=60] - Minimum lag in seconds to be considered lagging
|
|
275
|
+
* @returns {Promise<object>}
|
|
276
|
+
*/
|
|
277
|
+
async getLaggingConsumers(minLagSeconds = 60) {
|
|
278
|
+
logger.log('Admin.getLaggingConsumers', { minLagSeconds })
|
|
279
|
+
return this.#httpClient.get(`/api/v1/consumer-groups/lagging?minLagSeconds=${minLagSeconds}`)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Delete a consumer group for a specific queue
|
|
284
|
+
* @param {string} consumerGroup - Consumer group name
|
|
285
|
+
* @param {string} queueName - Queue name
|
|
286
|
+
* @param {boolean} [deleteMetadata=true] - Whether to delete subscription metadata
|
|
287
|
+
* @returns {Promise<object>}
|
|
288
|
+
*/
|
|
289
|
+
async deleteConsumerGroupForQueue(consumerGroup, queueName, deleteMetadata = true) {
|
|
290
|
+
logger.log('Admin.deleteConsumerGroupForQueue', { consumerGroup, queueName, deleteMetadata })
|
|
291
|
+
const url = `/api/v1/consumer-groups/${encodeURIComponent(consumerGroup)}/queues/${encodeURIComponent(queueName)}?deleteMetadata=${deleteMetadata}`
|
|
292
|
+
return this.#httpClient.delete(url)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Seek consumer group offset for a queue
|
|
297
|
+
* @param {string} consumerGroup - Consumer group name
|
|
298
|
+
* @param {string} queueName - Queue name
|
|
299
|
+
* @param {object} options - Seek options (timestamp, position, etc.)
|
|
300
|
+
* @returns {Promise<object>}
|
|
301
|
+
*/
|
|
302
|
+
async seekConsumerGroup(consumerGroup, queueName, options = {}) {
|
|
303
|
+
logger.log('Admin.seekConsumerGroup', { consumerGroup, queueName, options })
|
|
304
|
+
const url = `/api/v1/consumer-groups/${encodeURIComponent(consumerGroup)}/queues/${encodeURIComponent(queueName)}/seek`
|
|
305
|
+
return this.#httpClient.post(url, options)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ===========================
|
|
309
|
+
// System API
|
|
310
|
+
// ===========================
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Health check
|
|
314
|
+
* @returns {Promise<object>}
|
|
315
|
+
*/
|
|
316
|
+
async health() {
|
|
317
|
+
logger.log('Admin.health', {})
|
|
318
|
+
return this.#httpClient.get('/health')
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Get Prometheus metrics
|
|
323
|
+
* @returns {Promise<string>} Raw metrics text
|
|
324
|
+
*/
|
|
325
|
+
async metrics() {
|
|
326
|
+
logger.log('Admin.metrics', {})
|
|
327
|
+
return this.#httpClient.get('/metrics')
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Get push maintenance mode status
|
|
332
|
+
* @returns {Promise<object>}
|
|
333
|
+
*/
|
|
334
|
+
async getMaintenanceMode() {
|
|
335
|
+
logger.log('Admin.getMaintenanceMode', {})
|
|
336
|
+
return this.#httpClient.get('/api/v1/system/maintenance')
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Set push maintenance mode
|
|
341
|
+
* @param {boolean} enabled - Enable or disable maintenance mode
|
|
342
|
+
* @returns {Promise<object>}
|
|
343
|
+
*/
|
|
344
|
+
async setMaintenanceMode(enabled) {
|
|
345
|
+
logger.log('Admin.setMaintenanceMode', { enabled })
|
|
346
|
+
return this.#httpClient.post('/api/v1/system/maintenance', { enabled })
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Get pop maintenance mode status
|
|
351
|
+
* @returns {Promise<object>}
|
|
352
|
+
*/
|
|
353
|
+
async getPopMaintenanceMode() {
|
|
354
|
+
logger.log('Admin.getPopMaintenanceMode', {})
|
|
355
|
+
return this.#httpClient.get('/api/v1/system/maintenance/pop')
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Set pop maintenance mode
|
|
360
|
+
* @param {boolean} enabled - Enable or disable pop maintenance mode
|
|
361
|
+
* @returns {Promise<object>}
|
|
362
|
+
*/
|
|
363
|
+
async setPopMaintenanceMode(enabled) {
|
|
364
|
+
logger.log('Admin.setPopMaintenanceMode', { enabled })
|
|
365
|
+
return this.#httpClient.post('/api/v1/system/maintenance/pop', { enabled })
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Get system metrics (CPU, memory, connections, etc.)
|
|
370
|
+
* @param {object} params - Query parameters (from, to, etc.)
|
|
371
|
+
* @returns {Promise<object>}
|
|
372
|
+
*/
|
|
373
|
+
async getSystemMetrics(params = {}) {
|
|
374
|
+
logger.log('Admin.getSystemMetrics', { params })
|
|
375
|
+
const queryString = this.#buildQueryString(params)
|
|
376
|
+
return this.#httpClient.get(`/api/v1/analytics/system-metrics${queryString}`)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Get worker metrics
|
|
381
|
+
* @param {object} params - Query parameters
|
|
382
|
+
* @returns {Promise<object>}
|
|
383
|
+
*/
|
|
384
|
+
async getWorkerMetrics(params = {}) {
|
|
385
|
+
logger.log('Admin.getWorkerMetrics', { params })
|
|
386
|
+
const queryString = this.#buildQueryString(params)
|
|
387
|
+
return this.#httpClient.get(`/api/v1/analytics/worker-metrics${queryString}`)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Get PostgreSQL statistics
|
|
392
|
+
* @returns {Promise<object>}
|
|
393
|
+
*/
|
|
394
|
+
async getPostgresStats() {
|
|
395
|
+
logger.log('Admin.getPostgresStats', {})
|
|
396
|
+
return this.#httpClient.get('/api/v1/analytics/postgres-stats')
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ===========================
|
|
400
|
+
// Helper Methods
|
|
401
|
+
// ===========================
|
|
402
|
+
|
|
403
|
+
#buildQueryString(params) {
|
|
404
|
+
const searchParams = new URLSearchParams()
|
|
405
|
+
for (const [key, value] of Object.entries(params)) {
|
|
406
|
+
if (value !== undefined && value !== null) {
|
|
407
|
+
searchParams.append(key, String(value))
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
const queryString = searchParams.toString()
|
|
411
|
+
return queryString ? `?${queryString}` : ''
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
@@ -13,10 +13,10 @@ export class TransactionBuilder {
|
|
|
13
13
|
this.#httpClient = httpClient
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
ack(messages, status = 'completed') {
|
|
16
|
+
ack(messages, status = 'completed', context = {}) {
|
|
17
17
|
const msgs = Array.isArray(messages) ? messages : [messages]
|
|
18
18
|
|
|
19
|
-
logger.log('TransactionBuilder.ack', { count: msgs.length, status })
|
|
19
|
+
logger.log('TransactionBuilder.ack', { count: msgs.length, status, consumerGroup: context.consumerGroup })
|
|
20
20
|
|
|
21
21
|
msgs.forEach(msg => {
|
|
22
22
|
const transactionId = typeof msg === 'string' ? msg : (msg.transactionId || msg.id)
|
|
@@ -32,12 +32,19 @@ export class TransactionBuilder {
|
|
|
32
32
|
throw new Error('Message must have partitionId property to ensure message uniqueness')
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
const operation = {
|
|
36
36
|
type: 'ack',
|
|
37
37
|
transactionId,
|
|
38
38
|
partitionId,
|
|
39
39
|
status
|
|
40
|
-
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Add consumerGroup if provided in context
|
|
43
|
+
if (context.consumerGroup) {
|
|
44
|
+
operation.consumerGroup = context.consumerGroup
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
this.#operations.push(operation)
|
|
41
48
|
|
|
42
49
|
if (leaseId) {
|
|
43
50
|
this.#requiredLeases.push(leaseId)
|
|
@@ -11,6 +11,7 @@ export class HttpClient {
|
|
|
11
11
|
#retryAttempts
|
|
12
12
|
#retryDelayMillis
|
|
13
13
|
#enableFailover
|
|
14
|
+
#bearerToken
|
|
14
15
|
|
|
15
16
|
constructor(options = {}) {
|
|
16
17
|
const {
|
|
@@ -19,7 +20,8 @@ export class HttpClient {
|
|
|
19
20
|
timeoutMillis = 30000,
|
|
20
21
|
retryAttempts = 3,
|
|
21
22
|
retryDelayMillis = 1000,
|
|
22
|
-
enableFailover = true
|
|
23
|
+
enableFailover = true,
|
|
24
|
+
bearerToken = null
|
|
23
25
|
} = options
|
|
24
26
|
|
|
25
27
|
this.#baseUrl = baseUrl
|
|
@@ -28,13 +30,15 @@ export class HttpClient {
|
|
|
28
30
|
this.#retryAttempts = retryAttempts
|
|
29
31
|
this.#retryDelayMillis = retryDelayMillis
|
|
30
32
|
this.#enableFailover = enableFailover
|
|
33
|
+
this.#bearerToken = bearerToken
|
|
31
34
|
|
|
32
35
|
logger.log('HttpClient.constructor', {
|
|
33
36
|
hasLoadBalancer: !!loadBalancer,
|
|
34
37
|
baseUrl: baseUrl || 'load-balanced',
|
|
35
38
|
timeoutMillis,
|
|
36
39
|
retryAttempts,
|
|
37
|
-
enableFailover
|
|
40
|
+
enableFailover,
|
|
41
|
+
hasAuth: !!bearerToken
|
|
38
42
|
})
|
|
39
43
|
}
|
|
40
44
|
|
|
@@ -46,10 +50,15 @@ export class HttpClient {
|
|
|
46
50
|
const timeoutId = setTimeout(() => controller.abort(), effectiveTimeout)
|
|
47
51
|
|
|
48
52
|
try {
|
|
53
|
+
const headers = { 'Content-Type': 'application/json' }
|
|
54
|
+
if (this.#bearerToken) {
|
|
55
|
+
headers['Authorization'] = `Bearer ${this.#bearerToken}`
|
|
56
|
+
}
|
|
57
|
+
|
|
49
58
|
const options = {
|
|
50
59
|
method,
|
|
51
60
|
signal: controller.signal,
|
|
52
|
-
headers
|
|
61
|
+
headers
|
|
53
62
|
}
|
|
54
63
|
|
|
55
64
|
if (body) {
|
package/client-v2/index.js
CHANGED
|
@@ -13,7 +13,8 @@ export const CLIENT_DEFAULTS = {
|
|
|
13
13
|
loadBalancingStrategy: 'affinity', // 'round-robin', 'session', or 'affinity'
|
|
14
14
|
affinityHashRing: 128, // Number of virtual nodes per server for affinity strategy
|
|
15
15
|
enableFailover: true, // Auto-failover to other servers
|
|
16
|
-
healthRetryAfterMillis: 5000 // Retry unhealthy backends after 5 seconds
|
|
16
|
+
healthRetryAfterMillis: 5000, // Retry unhealthy backends after 5 seconds
|
|
17
|
+
bearerToken: null // Bearer token for proxy authentication
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
export const QUEUE_DEFAULTS = {
|
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Logger utility for Queen Client v2
|
|
3
|
-
* Controlled by QUEEN_CLIENT_LOG environment variable
|
|
3
|
+
* Controlled by QUEEN_CLIENT_LOG environment variable (Node.js)
|
|
4
|
+
* or window.QUEEN_CLIENT_LOG (Browser)
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
|
-
const LOG_ENABLED =
|
|
7
|
+
const LOG_ENABLED = (() => {
|
|
8
|
+
// Node.js environment
|
|
9
|
+
if (typeof process !== 'undefined' && process.env) {
|
|
10
|
+
return process.env.QUEEN_CLIENT_LOG === 'true'
|
|
11
|
+
}
|
|
12
|
+
// Browser environment
|
|
13
|
+
if (typeof window !== 'undefined') {
|
|
14
|
+
return window.QUEEN_CLIENT_LOG === true
|
|
15
|
+
}
|
|
16
|
+
return false
|
|
17
|
+
})()
|
|
7
18
|
|
|
8
19
|
/**
|
|
9
20
|
* Get formatted timestamp
|
package/package.json
CHANGED
package/test-v2/consume.js
CHANGED
|
@@ -51,60 +51,6 @@ export async function testConsumerTrace(client) {
|
|
|
51
51
|
return { success: msgToReturn !== null, message: 'Consumer test completed successfully' }
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
export async function testConsumerNamespace(client) {
|
|
55
|
-
const queue = await client.queue('test-queue-v2-namespace')
|
|
56
|
-
.namespace('test-namespace')
|
|
57
|
-
.create()
|
|
58
|
-
if (!queue.configured) {
|
|
59
|
-
return { success: false, message: 'Queue not created' }
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
await client
|
|
63
|
-
.queue('test-queue-v2-namespace')
|
|
64
|
-
.push([{ data: { message: 'Hello, world!' } }])
|
|
65
|
-
|
|
66
|
-
let msgToReturn = null
|
|
67
|
-
|
|
68
|
-
await client
|
|
69
|
-
.queue()
|
|
70
|
-
.namespace('test-namespace')
|
|
71
|
-
.batch(1)
|
|
72
|
-
.limit(1)
|
|
73
|
-
.each()
|
|
74
|
-
.consume(async msg => {
|
|
75
|
-
msgToReturn = msg
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
return { success: msgToReturn !== null, message: 'Consumer test completed successfully' }
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export async function testConsumerTask(client) {
|
|
82
|
-
const queue = await client.queue('test-queue-v2-task')
|
|
83
|
-
.task('test-task')
|
|
84
|
-
.create()
|
|
85
|
-
if (!queue.configured) {
|
|
86
|
-
return { success: false, message: 'Queue not created' }
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
await client
|
|
90
|
-
.queue('test-queue-v2-task')
|
|
91
|
-
.push([{ data: { message: 'Hello, world!' } }])
|
|
92
|
-
|
|
93
|
-
let msgToReturn = null
|
|
94
|
-
|
|
95
|
-
await client
|
|
96
|
-
.queue()
|
|
97
|
-
.task('test-task')
|
|
98
|
-
.batch(1)
|
|
99
|
-
.limit(1)
|
|
100
|
-
.each()
|
|
101
|
-
.consume(async msg => {
|
|
102
|
-
msgToReturn = msg
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
return { success: msgToReturn !== null, message: 'Consumer test completed successfully' }
|
|
106
|
-
}
|
|
107
|
-
|
|
108
54
|
export async function testConsumerWithPartition(client) {
|
|
109
55
|
const queue = await client.queue('test-queue-v2-consume-with-partition').create()
|
|
110
56
|
if (!queue.configured) {
|
|
@@ -252,6 +198,7 @@ export async function testConsumerOrderingBatch(client) {
|
|
|
252
198
|
|
|
253
199
|
|
|
254
200
|
export async function testConsumerBatchConsumeBatch(client) {
|
|
201
|
+
await client.queue('test-queue-v2-consume-batch').delete()
|
|
255
202
|
const queue = await client.queue('test-queue-v2-consume-batch').create()
|
|
256
203
|
if (!queue.configured) {
|
|
257
204
|
return { success: false, message: 'Queue not created' }
|
|
@@ -271,7 +218,7 @@ export async function testConsumerBatchConsumeBatch(client) {
|
|
|
271
218
|
.queue('test-queue-v2-consume-batch')
|
|
272
219
|
.batch(10)
|
|
273
220
|
.wait(false)
|
|
274
|
-
.limit(
|
|
221
|
+
.limit(3)
|
|
275
222
|
.consume(async msgs => {
|
|
276
223
|
msgLength = msgs.length
|
|
277
224
|
})
|
|
@@ -465,12 +412,13 @@ export async function manualAck(client) {
|
|
|
465
412
|
|
|
466
413
|
await client
|
|
467
414
|
.queue('test-queue-v2-manual-ack')
|
|
468
|
-
.concurrency(
|
|
415
|
+
.concurrency(1)
|
|
469
416
|
.subscriptionMode('from_beginning')
|
|
470
417
|
.batch(1000)
|
|
471
418
|
.wait(false)
|
|
472
|
-
.limit(
|
|
419
|
+
.limit(10000)
|
|
473
420
|
.consume(async msgs => {
|
|
421
|
+
console.log('Messages: ' + msgs.length)
|
|
474
422
|
for (const msg of msgs) {
|
|
475
423
|
if (lastId === null) {
|
|
476
424
|
lastId = msg.data.id
|
|
@@ -525,6 +473,7 @@ export async function retries(client) {
|
|
|
525
473
|
.limit(300) // Allow up to 300 messages (3 batches of 100)
|
|
526
474
|
.consume(async msgs => {
|
|
527
475
|
attemptCount++;
|
|
476
|
+
console.log('Attempt: ' + attemptCount)
|
|
528
477
|
if (attemptCount < 3) {
|
|
529
478
|
// Fail first 2 attempts, succeed on 3rd
|
|
530
479
|
throw new Error('Test error - triggering retry')
|
package/test-v2/load.js
CHANGED
|
@@ -11,7 +11,7 @@ export async function testLoad(client) {
|
|
|
11
11
|
let messages = [];
|
|
12
12
|
for (let i = 0; i < messagesToPush; i++) {
|
|
13
13
|
messages.push({ data: { id: i } })
|
|
14
|
-
if (messages.length >=
|
|
14
|
+
if (messages.length >= 100) {
|
|
15
15
|
await client.queue('test-queue-v2-load').push(messages)
|
|
16
16
|
messages = [];
|
|
17
17
|
}
|
|
@@ -24,11 +24,10 @@ export async function testLoad(client) {
|
|
|
24
24
|
await client
|
|
25
25
|
.queue('test-queue-v2-load')
|
|
26
26
|
.concurrency(10)
|
|
27
|
-
.batch(
|
|
27
|
+
.batch(100)
|
|
28
28
|
.wait(false)
|
|
29
|
-
.limit(
|
|
29
|
+
.limit(10000)
|
|
30
30
|
.consume(async msgs => {
|
|
31
|
-
console.log(`Messages: ${msgs.length}`)
|
|
32
31
|
for (const msg of msgs) {
|
|
33
32
|
if (lastId === null) {
|
|
34
33
|
lastId = msg.data.id
|
|
@@ -60,35 +59,37 @@ export async function testLoadPartition(client) {
|
|
|
60
59
|
|
|
61
60
|
let messages = [];
|
|
62
61
|
let k = 0
|
|
63
|
-
for (let
|
|
62
|
+
for (let k = 0; k < 10; k++) {
|
|
63
|
+
for (let i = 0; i < messagesToPush / 10; i++) {
|
|
64
64
|
messages.push({ data: { id: i } })
|
|
65
|
-
if (messages.length >=
|
|
65
|
+
if (messages.length >= 100) {
|
|
66
66
|
await client
|
|
67
67
|
.queue('test-queue-v2-load-partition')
|
|
68
68
|
.partition(k.toString())
|
|
69
69
|
.push(messages)
|
|
70
70
|
messages = [];
|
|
71
|
-
k++;
|
|
72
71
|
}
|
|
73
72
|
}
|
|
74
|
-
|
|
73
|
+
}
|
|
75
74
|
|
|
76
75
|
let uniqueIds = new Set();
|
|
77
|
-
|
|
76
|
+
|
|
78
77
|
|
|
79
78
|
for (let i = 0; i < 10; i++) {
|
|
79
|
+
let lastId = null;
|
|
80
80
|
await client
|
|
81
81
|
.queue('test-queue-v2-load-partition')
|
|
82
82
|
.partition(i.toString())
|
|
83
|
-
.batch(
|
|
83
|
+
.batch(100)
|
|
84
84
|
.wait(false)
|
|
85
|
-
.limit(
|
|
85
|
+
.limit(10000)
|
|
86
86
|
.consume(async msgs => {
|
|
87
87
|
for (const msg of msgs) {
|
|
88
88
|
if (lastId === null) {
|
|
89
89
|
lastId = msg.data.id
|
|
90
90
|
} else {
|
|
91
91
|
if (msg.data.id !== lastId + 1) {
|
|
92
|
+
console.log(`Message ordering violation: ${msg.data.id} !== ${lastId + 1}`)
|
|
92
93
|
throw new Error('Message ordering violation')
|
|
93
94
|
}
|
|
94
95
|
lastId = msg.data.id
|
|
@@ -98,14 +99,15 @@ export async function testLoadPartition(client) {
|
|
|
98
99
|
})
|
|
99
100
|
}
|
|
100
101
|
|
|
102
|
+
console.log(uniqueIds.size, messagesToPush)
|
|
101
103
|
const uniqueIdsCount = uniqueIds.size;
|
|
102
104
|
|
|
103
|
-
return { success: uniqueIdsCount === messagesToPush, message: 'Load test completed successfully' }
|
|
105
|
+
return { success: uniqueIdsCount === messagesToPush / 10, message: 'Load test completed successfully' }
|
|
104
106
|
}
|
|
105
107
|
|
|
106
108
|
export async function testLoadConsumerGroup(client) {
|
|
107
109
|
const queue = await client
|
|
108
|
-
.queue('
|
|
110
|
+
.queue('')
|
|
109
111
|
.create()
|
|
110
112
|
if (!queue.configured) {
|
|
111
113
|
return { success: false, message: 'Queue not created' }
|
|
@@ -116,7 +118,7 @@ export async function testLoadConsumerGroup(client) {
|
|
|
116
118
|
let messages = [];
|
|
117
119
|
for (let i = 0; i < messagesToPush; i++) {
|
|
118
120
|
messages.push({ data: { id: i } })
|
|
119
|
-
if (messages.length >=
|
|
121
|
+
if (messages.length >= 100) {
|
|
120
122
|
await client.queue('test-queue-v2-load-consumer-group').push(messages)
|
|
121
123
|
messages = [];
|
|
122
124
|
}
|
|
@@ -129,11 +131,11 @@ export async function testLoadConsumerGroup(client) {
|
|
|
129
131
|
await client
|
|
130
132
|
.queue('test-queue-v2-load-consumer-group')
|
|
131
133
|
.group('test-consumer-group-a')
|
|
132
|
-
.subscriptionMode('
|
|
134
|
+
.subscriptionMode('all')
|
|
133
135
|
.concurrency(10)
|
|
134
|
-
.batch(
|
|
136
|
+
.batch(100)
|
|
135
137
|
.wait(false)
|
|
136
|
-
.limit(
|
|
138
|
+
.limit(10000)
|
|
137
139
|
.consume(async msgs => {
|
|
138
140
|
for (const msg of msgs) {
|
|
139
141
|
if (lastIdA === null) {
|
|
@@ -154,11 +156,11 @@ export async function testLoadConsumerGroup(client) {
|
|
|
154
156
|
await client
|
|
155
157
|
.queue('test-queue-v2-load-consumer-group')
|
|
156
158
|
.group('test-consumer-group-b')
|
|
157
|
-
.subscriptionMode('
|
|
159
|
+
.subscriptionMode('all')
|
|
158
160
|
.concurrency(10)
|
|
159
|
-
.batch(
|
|
161
|
+
.batch(100)
|
|
160
162
|
.wait(false)
|
|
161
|
-
.limit(
|
|
163
|
+
.limit(10000)
|
|
162
164
|
.consume(async msgs => {
|
|
163
165
|
for (const msg of msgs) {
|
|
164
166
|
if (lastIdB === null) {
|
package/test-v2/maintenance.js
CHANGED
|
@@ -71,12 +71,14 @@ export async function test_maintenance_mode(client = null) {
|
|
|
71
71
|
|
|
72
72
|
// Acknowledge messages
|
|
73
73
|
await queen.ack(messages);
|
|
74
|
+
|
|
74
75
|
}
|
|
75
76
|
} catch (err) {
|
|
76
77
|
if (consumerActive) {
|
|
77
78
|
console.error(' ❌ Consumer error:', err.message);
|
|
78
79
|
}
|
|
79
80
|
}
|
|
81
|
+
await sleep(10);
|
|
80
82
|
}
|
|
81
83
|
})();
|
|
82
84
|
|
package/test-v2/push.js
CHANGED
|
@@ -115,35 +115,6 @@ export async function pushBufferedMessage(client) {
|
|
|
115
115
|
return { success: true }
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
export async function pushMaxQueueSize(client) {
|
|
119
|
-
const queue = await client.queue('test-queue-max-size').config({ maxSize: 10 }).create()
|
|
120
|
-
if (!queue.configured) {
|
|
121
|
-
return { success: false, message: 'Queue not created' }
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Necessary to create a partition consumer to trigger the capacity check
|
|
125
|
-
const popToCreateParititonConsumer = await client
|
|
126
|
-
.queue('test-queue-max-size')
|
|
127
|
-
.batch(1)
|
|
128
|
-
.wait(false)
|
|
129
|
-
.pop()
|
|
130
|
-
|
|
131
|
-
try {
|
|
132
|
-
for (let i = 0; i < 20; i++) {
|
|
133
|
-
const result = await client
|
|
134
|
-
.queue('test-queue-max-size')
|
|
135
|
-
.push([{ data: { message: i } }])
|
|
136
|
-
}
|
|
137
|
-
} catch (error) {
|
|
138
|
-
if (!error.message.includes(' Queue "test-queue-max-size" would exceed max capacity (10).')) {
|
|
139
|
-
return { success: true, message: 'Correct error message' }
|
|
140
|
-
} else {
|
|
141
|
-
return { success: false, message: 'Incorrect error message' }
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
return { success: false }
|
|
145
|
-
}
|
|
146
|
-
|
|
147
118
|
export async function pushDelayedMessage(client) {
|
|
148
119
|
const queue = await client.queue('test-queue-delayed')
|
|
149
120
|
.config({ delayedProcessing: 2 })
|
package/test-v2/run.js
CHANGED
|
@@ -18,11 +18,26 @@ import * as bufferingTests from './ai_buffering.js'
|
|
|
18
18
|
import * as priorityTests from './ai_priority.js'
|
|
19
19
|
import * as ttlRetentionTests from './ai_ttl_retention.js'
|
|
20
20
|
import * as mixedScenariosTests from './ai_mixed_scenarios.js'
|
|
21
|
+
import { LoadBalancer } from '../client-v2/http/LoadBalancer.js';
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
// Test configuration
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
|
|
26
|
+
export const TEST_CONFIG_SINGLE = {
|
|
27
|
+
baseUrls: ['http://localhost:6632'],
|
|
28
|
+
loadBalancingStrategy: 'affinity',
|
|
29
|
+
dbConfig: {
|
|
30
|
+
host: process.env.PG_HOST || 'localhost',
|
|
31
|
+
port: process.env.PG_PORT || 5432,
|
|
32
|
+
database: process.env.PG_DB || 'postgres',
|
|
33
|
+
user: process.env.PG_USER || 'postgres',
|
|
34
|
+
password: process.env.PG_PASSWORD || 'postgres'
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const TEST_CONFIG_MULTIPLE = {
|
|
39
|
+
baseUrls: ['http://localhost:6632','http://localhost:6633'],
|
|
40
|
+
loadBalancingStrategy: 'round-robin',
|
|
26
41
|
dbConfig: {
|
|
27
42
|
host: process.env.PG_HOST || 'localhost',
|
|
28
43
|
port: process.env.PG_PORT || 5432,
|
|
@@ -32,6 +47,9 @@ export const TEST_CONFIG = {
|
|
|
32
47
|
}
|
|
33
48
|
};
|
|
34
49
|
|
|
50
|
+
export const TEST_CONFIG = process.env.TEST_CONFIG === 'multiple' ? TEST_CONFIG_MULTIPLE : TEST_CONFIG_SINGLE;
|
|
51
|
+
console.log('TEST_CONFIG:', TEST_CONFIG);
|
|
52
|
+
|
|
35
53
|
// Global test state
|
|
36
54
|
export let dbPool;
|
|
37
55
|
|
|
@@ -82,7 +100,10 @@ export const cleanupTestData = async () => {
|
|
|
82
100
|
};
|
|
83
101
|
|
|
84
102
|
async function main() {
|
|
85
|
-
const client = new Queen(
|
|
103
|
+
const client = new Queen({
|
|
104
|
+
urls: TEST_CONFIG.baseUrls,
|
|
105
|
+
loadBalancingStrategy: TEST_CONFIG.loadBalancingStrategy
|
|
106
|
+
})
|
|
86
107
|
await initDb()
|
|
87
108
|
|
|
88
109
|
// Separate human and AI tests
|
package/test-v2/transaction.js
CHANGED
|
@@ -503,3 +503,69 @@ export async function transactionRollback(client) {
|
|
|
503
503
|
}
|
|
504
504
|
}
|
|
505
505
|
|
|
506
|
+
export async function transactionAckWithConsumerGroup(client) {
|
|
507
|
+
const queueA = await client.queue('test-queue-v2-txn-cg-a').create()
|
|
508
|
+
const queueB = await client.queue('test-queue-v2-txn-cg-b').create()
|
|
509
|
+
if (!queueA.configured || !queueB.configured) {
|
|
510
|
+
return { success: false, message: 'Queues not created' }
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const consumerGroup = 'test-consumer-group-txn'
|
|
514
|
+
|
|
515
|
+
// Push messages to queue A
|
|
516
|
+
await client.queue('test-queue-v2-txn-cg-a').push([
|
|
517
|
+
{ data: { value: 1 } },
|
|
518
|
+
{ data: { value: 2 } }
|
|
519
|
+
])
|
|
520
|
+
|
|
521
|
+
// Pop with consumer group
|
|
522
|
+
const messages = await client.queue('test-queue-v2-txn-cg-a')
|
|
523
|
+
.group(consumerGroup)
|
|
524
|
+
.batch(2)
|
|
525
|
+
.wait(false)
|
|
526
|
+
.pop()
|
|
527
|
+
|
|
528
|
+
if (messages.length !== 2) {
|
|
529
|
+
return { success: false, message: `Expected 2 messages, got ${messages.length}` }
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Transaction: ACK with consumer group context, push to B
|
|
533
|
+
await client
|
|
534
|
+
.transaction()
|
|
535
|
+
.ack(messages[0], 'completed', { consumerGroup })
|
|
536
|
+
.ack(messages[1], 'completed', { consumerGroup })
|
|
537
|
+
.queue('test-queue-v2-txn-cg-b')
|
|
538
|
+
.push([{ data: { sum: messages[0].data.value + messages[1].data.value } }])
|
|
539
|
+
.commit()
|
|
540
|
+
|
|
541
|
+
// Verify: Pop again with same consumer group should get no messages
|
|
542
|
+
// (consumer group cursor should have advanced)
|
|
543
|
+
const messagesAfterAck = await client.queue('test-queue-v2-txn-cg-a')
|
|
544
|
+
.group(consumerGroup)
|
|
545
|
+
.batch(2)
|
|
546
|
+
.wait(false)
|
|
547
|
+
.pop()
|
|
548
|
+
|
|
549
|
+
// Verify: Queue B has the result
|
|
550
|
+
const resultB = await client.queue('test-queue-v2-txn-cg-b').batch(1).wait(false).pop()
|
|
551
|
+
|
|
552
|
+
// Verify: Using a different consumer group should still see the messages
|
|
553
|
+
const messagesOtherGroup = await client.queue('test-queue-v2-txn-cg-a')
|
|
554
|
+
.group('other-consumer-group')
|
|
555
|
+
.batch(2)
|
|
556
|
+
.wait(false)
|
|
557
|
+
.pop()
|
|
558
|
+
|
|
559
|
+
const success = messagesAfterAck.length === 0 &&
|
|
560
|
+
resultB.length === 1 &&
|
|
561
|
+
resultB[0].data.sum === 3 &&
|
|
562
|
+
messagesOtherGroup.length === 2
|
|
563
|
+
|
|
564
|
+
return {
|
|
565
|
+
success,
|
|
566
|
+
message: success
|
|
567
|
+
? 'Transaction ack with consumer group completed - cursor advanced correctly'
|
|
568
|
+
: `Failed: afterAck=${messagesAfterAck.length} (expected 0), resultB=${resultB.length} (expected 1), otherGroup=${messagesOtherGroup.length} (expected 2)`
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|