queen-mq 0.12.0 → 0.12.2
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 +5 -3
- package/client-v2/builders/TransactionBuilder.js +11 -4
- package/client-v2/http/HttpClient.js +7 -2
- package/client-v2/utils/defaults.js +2 -1
- package/package.json +1 -1
- package/test-v2/bootstrap.js +316 -0
- package/test-v2/run.js +4 -15
- package/test-v2/transaction.js +66 -0
- package/test-v2/AI_TEST_SUMMARY.md +0 -226
- package/test-v2/GETTING_STARTED.md +0 -181
- package/test-v2/MAINTENANCE_TEST.md +0 -148
- package/test-v2/README_SUBSCRIPTION_TESTS.md +0 -201
- package/test-v2/ai_buffering.js +0 -194
- package/test-v2/ai_error_handling.js +0 -223
- package/test-v2/ai_lease_renewal.js +0 -206
- package/test-v2/ai_mixed_scenarios.js +0 -278
- package/test-v2/ai_priority.js +0 -169
- package/test-v2/ai_resources.js +0 -217
- package/test-v2/ai_ttl_retention.js +0 -170
package/client-v2/Queen.js
CHANGED
|
@@ -77,7 +77,7 @@ export class Queen {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
#createHttpClient() {
|
|
80
|
-
const { urls, timeoutMillis, retryAttempts, retryDelayMillis, loadBalancingStrategy, affinityHashRing, healthRetryAfterMillis, enableFailover, bearerToken } = this.#config
|
|
80
|
+
const { urls, timeoutMillis, retryAttempts, retryDelayMillis, loadBalancingStrategy, affinityHashRing, healthRetryAfterMillis, enableFailover, bearerToken, headers } = this.#config
|
|
81
81
|
|
|
82
82
|
if (urls.length === 1) {
|
|
83
83
|
// Single server
|
|
@@ -86,7 +86,8 @@ export class Queen {
|
|
|
86
86
|
timeoutMillis,
|
|
87
87
|
retryAttempts,
|
|
88
88
|
retryDelayMillis,
|
|
89
|
-
bearerToken
|
|
89
|
+
bearerToken,
|
|
90
|
+
headers
|
|
90
91
|
})
|
|
91
92
|
}
|
|
92
93
|
|
|
@@ -101,7 +102,8 @@ export class Queen {
|
|
|
101
102
|
retryAttempts,
|
|
102
103
|
retryDelayMillis,
|
|
103
104
|
enableFailover,
|
|
104
|
-
bearerToken
|
|
105
|
+
bearerToken,
|
|
106
|
+
headers
|
|
105
107
|
})
|
|
106
108
|
}
|
|
107
109
|
|
|
@@ -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)
|
|
@@ -12,6 +12,7 @@ export class HttpClient {
|
|
|
12
12
|
#retryDelayMillis
|
|
13
13
|
#enableFailover
|
|
14
14
|
#bearerToken
|
|
15
|
+
#headers
|
|
15
16
|
|
|
16
17
|
constructor(options = {}) {
|
|
17
18
|
const {
|
|
@@ -21,7 +22,8 @@ export class HttpClient {
|
|
|
21
22
|
retryAttempts = 3,
|
|
22
23
|
retryDelayMillis = 1000,
|
|
23
24
|
enableFailover = true,
|
|
24
|
-
bearerToken = null
|
|
25
|
+
bearerToken = null,
|
|
26
|
+
headers = {}
|
|
25
27
|
} = options
|
|
26
28
|
|
|
27
29
|
this.#baseUrl = baseUrl
|
|
@@ -31,6 +33,7 @@ export class HttpClient {
|
|
|
31
33
|
this.#retryDelayMillis = retryDelayMillis
|
|
32
34
|
this.#enableFailover = enableFailover
|
|
33
35
|
this.#bearerToken = bearerToken
|
|
36
|
+
this.#headers = headers || {}
|
|
34
37
|
|
|
35
38
|
logger.log('HttpClient.constructor', {
|
|
36
39
|
hasLoadBalancer: !!loadBalancer,
|
|
@@ -38,7 +41,8 @@ export class HttpClient {
|
|
|
38
41
|
timeoutMillis,
|
|
39
42
|
retryAttempts,
|
|
40
43
|
enableFailover,
|
|
41
|
-
hasAuth: !!bearerToken
|
|
44
|
+
hasAuth: !!bearerToken,
|
|
45
|
+
customHeaders: Object.keys(this.#headers).length
|
|
42
46
|
})
|
|
43
47
|
}
|
|
44
48
|
|
|
@@ -54,6 +58,7 @@ export class HttpClient {
|
|
|
54
58
|
if (this.#bearerToken) {
|
|
55
59
|
headers['Authorization'] = `Bearer ${this.#bearerToken}`
|
|
56
60
|
}
|
|
61
|
+
Object.assign(headers, this.#headers)
|
|
57
62
|
|
|
58
63
|
const options = {
|
|
59
64
|
method,
|
|
@@ -14,7 +14,8 @@ export const CLIENT_DEFAULTS = {
|
|
|
14
14
|
affinityHashRing: 128, // Number of virtual nodes per server for affinity strategy
|
|
15
15
|
enableFailover: true, // Auto-failover to other servers
|
|
16
16
|
healthRetryAfterMillis: 5000, // Retry unhealthy backends after 5 seconds
|
|
17
|
-
bearerToken: null
|
|
17
|
+
bearerToken: null, // Bearer token for proxy authentication
|
|
18
|
+
headers: {} // Custom headers to include in every request
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export const QUEUE_DEFAULTS = {
|
package/package.json
CHANGED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consumer Group Bootstrap Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests the partition_consumers bootstrap optimization:
|
|
5
|
+
* When a new consumer group registers (via subscription mode), all existing
|
|
6
|
+
* partition_consumers rows should be pre-seeded in bulk, avoiding slow
|
|
7
|
+
* one-at-a-time discovery.
|
|
8
|
+
*
|
|
9
|
+
* Test scenarios:
|
|
10
|
+
* 1. testCgBootstrapNew: CG with subscriptionMode('new')
|
|
11
|
+
* 2. testCgBootstrapTimestamp: CG with subscriptionFrom(timestamp)
|
|
12
|
+
*
|
|
13
|
+
* Both push 5000 partitions, register a CG (triggering bootstrap), then
|
|
14
|
+
* consume new messages and verify all partitions were discovered fast.
|
|
15
|
+
*
|
|
16
|
+
* Run: node run.js testCgBootstrapNew
|
|
17
|
+
* node run.js testCgBootstrapTimestamp
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const PARTITION_COUNT = 5000
|
|
21
|
+
const QUEUE_NAME_NEW = 'test-queue-v2-bootstrap-new'
|
|
22
|
+
const QUEUE_NAME_TS = 'test-queue-v2-bootstrap-ts'
|
|
23
|
+
|
|
24
|
+
async function pushToPartitions(client, queueName, partitionCount, prefix = '') {
|
|
25
|
+
const PARALLEL_BATCH = 50
|
|
26
|
+
let pushed = 0
|
|
27
|
+
for (let i = 0; i < partitionCount; i += PARALLEL_BATCH) {
|
|
28
|
+
const batch = []
|
|
29
|
+
for (let j = i; j < Math.min(i + PARALLEL_BATCH, partitionCount); j++) {
|
|
30
|
+
batch.push(
|
|
31
|
+
client
|
|
32
|
+
.queue(queueName)
|
|
33
|
+
.partition(`p-${j}`)
|
|
34
|
+
.push([{ data: { id: j, prefix } }])
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
await Promise.all(batch)
|
|
38
|
+
pushed += batch.length
|
|
39
|
+
}
|
|
40
|
+
return pushed
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Test bootstrap with subscriptionMode('new')
|
|
45
|
+
* - Push historical messages to 5000 partitions
|
|
46
|
+
* - Register CG with mode 'new' (triggers bootstrap)
|
|
47
|
+
* - Push new messages to all partitions
|
|
48
|
+
* - Consume with CG and verify all new messages are received fast
|
|
49
|
+
*/
|
|
50
|
+
export async function testCgBootstrapNew(client) {
|
|
51
|
+
// Cleanup
|
|
52
|
+
try {
|
|
53
|
+
await client.deleteConsumerGroup('bootstrap-cg-new', true)
|
|
54
|
+
} catch (e) {
|
|
55
|
+
// Ignore
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 1. Create queue
|
|
59
|
+
const queue = await client.queue(QUEUE_NAME_NEW).create()
|
|
60
|
+
if (!queue.configured) {
|
|
61
|
+
return { success: false, message: 'Queue not created' }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 2. Push 1 message to each of 5000 partitions (historical messages)
|
|
65
|
+
console.log(` Pushing to ${PARTITION_COUNT} partitions...`)
|
|
66
|
+
const pushStart = Date.now()
|
|
67
|
+
await pushToPartitions(client, QUEUE_NAME_NEW, PARTITION_COUNT, 'historical')
|
|
68
|
+
const pushTime = Date.now() - pushStart
|
|
69
|
+
console.log(` Push complete: ${PARTITION_COUNT} partitions in ${pushTime}ms`)
|
|
70
|
+
|
|
71
|
+
// 3. Consume all messages in queue mode (no CG) - baseline
|
|
72
|
+
console.log(` Consuming in queue mode (no CG)...`)
|
|
73
|
+
const queueModeStart = Date.now()
|
|
74
|
+
let queueModeCount = 0
|
|
75
|
+
|
|
76
|
+
await client
|
|
77
|
+
.queue(QUEUE_NAME_NEW)
|
|
78
|
+
.concurrency(10)
|
|
79
|
+
.batch(1)
|
|
80
|
+
.wait(false)
|
|
81
|
+
.idleMillis(5000)
|
|
82
|
+
.consume(async msgs => {
|
|
83
|
+
queueModeCount += msgs.length
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const queueModeTime = Date.now() - queueModeStart
|
|
87
|
+
console.log(` Queue mode: consumed ${queueModeCount} messages in ${queueModeTime}ms`)
|
|
88
|
+
|
|
89
|
+
if (queueModeCount !== PARTITION_COUNT) {
|
|
90
|
+
return {
|
|
91
|
+
success: false,
|
|
92
|
+
message: `Queue mode consumed ${queueModeCount}/${PARTITION_COUNT} messages in ${queueModeTime}ms`
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 4. Register a new CG with 'new' mode
|
|
97
|
+
// The first pop triggers metadata registration + bootstrap (seeds all partition_consumers)
|
|
98
|
+
console.log(` Registering CG 'bootstrap-cg-new' with mode 'new'...`)
|
|
99
|
+
const registerStart = Date.now()
|
|
100
|
+
|
|
101
|
+
const firstPop = await client
|
|
102
|
+
.queue(QUEUE_NAME_NEW)
|
|
103
|
+
.group('bootstrap-cg-new')
|
|
104
|
+
.subscriptionMode('new')
|
|
105
|
+
.batch(1)
|
|
106
|
+
.wait(false)
|
|
107
|
+
.pop()
|
|
108
|
+
|
|
109
|
+
const registerTime = Date.now() - registerStart
|
|
110
|
+
console.log(` CG registered + bootstrap in ${registerTime}ms (first pop returned ${firstPop.length} msgs)`)
|
|
111
|
+
|
|
112
|
+
// First pop should return 0 messages (mode 'new' skips historical)
|
|
113
|
+
if (firstPop.length !== 0) {
|
|
114
|
+
return {
|
|
115
|
+
success: false,
|
|
116
|
+
message: `CG with mode 'new' should skip historical messages, got ${firstPop.length}`
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 5. Push NEW messages to all 5000 partitions
|
|
121
|
+
console.log(` Pushing new messages to ${PARTITION_COUNT} partitions...`)
|
|
122
|
+
const pushNewStart = Date.now()
|
|
123
|
+
await pushToPartitions(client, QUEUE_NAME_NEW, PARTITION_COUNT, 'new')
|
|
124
|
+
const pushNewTime = Date.now() - pushNewStart
|
|
125
|
+
console.log(` Push new complete: ${PARTITION_COUNT} partitions in ${pushNewTime}ms`)
|
|
126
|
+
|
|
127
|
+
// 6. Consume all new messages with the CG
|
|
128
|
+
// If bootstrap worked, all partitions are already known -> fast consumption
|
|
129
|
+
// If bootstrap didn't work, it would discover one-at-a-time -> very slow
|
|
130
|
+
console.log(` Consuming with CG 'bootstrap-cg-new'...`)
|
|
131
|
+
const cgConsumeStart = Date.now()
|
|
132
|
+
let cgConsumeCount = 0
|
|
133
|
+
|
|
134
|
+
await client
|
|
135
|
+
.queue(QUEUE_NAME_NEW)
|
|
136
|
+
.group('bootstrap-cg-new')
|
|
137
|
+
.subscriptionMode('new')
|
|
138
|
+
.concurrency(10)
|
|
139
|
+
.batch(1)
|
|
140
|
+
.wait(false)
|
|
141
|
+
.idleMillis(5000)
|
|
142
|
+
.consume(async msgs => {
|
|
143
|
+
cgConsumeCount += msgs.length
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
const cgConsumeTime = Date.now() - cgConsumeStart
|
|
147
|
+
console.log(` CG consume: ${cgConsumeCount} messages in ${cgConsumeTime}ms`)
|
|
148
|
+
|
|
149
|
+
// 7. Drain new messages in queue mode too (cleanup)
|
|
150
|
+
console.log(` Draining new messages in queue mode...`)
|
|
151
|
+
let queueDrainCount = 0
|
|
152
|
+
await client
|
|
153
|
+
.queue(QUEUE_NAME_NEW)
|
|
154
|
+
.concurrency(10)
|
|
155
|
+
.batch(1)
|
|
156
|
+
.wait(false)
|
|
157
|
+
.idleMillis(5000)
|
|
158
|
+
.consume(async msgs => {
|
|
159
|
+
queueDrainCount += msgs.length
|
|
160
|
+
})
|
|
161
|
+
console.log(` Queue mode drain: ${queueDrainCount} messages`)
|
|
162
|
+
|
|
163
|
+
// 8. Results
|
|
164
|
+
const allConsumed = cgConsumeCount === PARTITION_COUNT
|
|
165
|
+
const bootstrapFast = registerTime < 10000
|
|
166
|
+
|
|
167
|
+
console.log(` ---- Results (subscriptionMode='new') ----`)
|
|
168
|
+
console.log(` Partitions: ${PARTITION_COUNT}`)
|
|
169
|
+
console.log(` Push (historical): ${pushTime}ms`)
|
|
170
|
+
console.log(` Queue mode consume: ${queueModeTime}ms (${queueModeCount} msgs)`)
|
|
171
|
+
console.log(` CG registration: ${registerTime}ms (includes bootstrap)`)
|
|
172
|
+
console.log(` Push (new): ${pushNewTime}ms`)
|
|
173
|
+
console.log(` CG consume: ${cgConsumeTime}ms (${cgConsumeCount} msgs)`)
|
|
174
|
+
console.log(` Bootstrap fast: ${bootstrapFast ? 'YES' : 'NO'} (${registerTime}ms < 10000ms)`)
|
|
175
|
+
|
|
176
|
+
if (!allConsumed) {
|
|
177
|
+
return {
|
|
178
|
+
success: false,
|
|
179
|
+
message: `CG consumed ${cgConsumeCount}/${PARTITION_COUNT} in ${cgConsumeTime}ms (bootstrap: ${registerTime}ms)`
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
success: true,
|
|
185
|
+
message: `[new] ${PARTITION_COUNT} partitions: push=${pushTime}ms, queue_mode=${queueModeTime}ms, bootstrap=${registerTime}ms, cg_consume=${cgConsumeTime}ms`
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Test bootstrap with subscriptionFrom(timestamp)
|
|
191
|
+
* - Push historical messages to 5000 partitions
|
|
192
|
+
* - Record a cutoff timestamp
|
|
193
|
+
* - Register CG with subscriptionFrom(cutoff) (triggers bootstrap via 'timestamp' path)
|
|
194
|
+
* - Push new messages to all partitions
|
|
195
|
+
* - Consume with CG and verify all new messages are received fast
|
|
196
|
+
*/
|
|
197
|
+
export async function testCgBootstrapTimestamp(client) {
|
|
198
|
+
// Cleanup
|
|
199
|
+
try {
|
|
200
|
+
await client.deleteConsumerGroup('bootstrap-cg-ts', true)
|
|
201
|
+
} catch (e) {
|
|
202
|
+
// Ignore
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 1. Create queue
|
|
206
|
+
const queue = await client.queue(QUEUE_NAME_TS).create()
|
|
207
|
+
if (!queue.configured) {
|
|
208
|
+
return { success: false, message: 'Queue not created' }
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 2. Push 1 message to each of 5000 partitions (historical messages)
|
|
212
|
+
console.log(` Pushing to ${PARTITION_COUNT} partitions...`)
|
|
213
|
+
const pushStart = Date.now()
|
|
214
|
+
await pushToPartitions(client, QUEUE_NAME_TS, PARTITION_COUNT, 'historical')
|
|
215
|
+
const pushTime = Date.now() - pushStart
|
|
216
|
+
console.log(` Push complete: ${PARTITION_COUNT} partitions in ${pushTime}ms`)
|
|
217
|
+
|
|
218
|
+
// 3. Record cutoff timestamp (after historical, before new messages)
|
|
219
|
+
// Wait before capturing the cutoff to handle clock skew between the
|
|
220
|
+
// client (Date.now) and PostgreSQL (NOW()). Without this, the last
|
|
221
|
+
// push batch may have a PG created_at AFTER the client-side cutoff.
|
|
222
|
+
await new Promise(resolve => setTimeout(resolve, 2000))
|
|
223
|
+
const cutoffTimestamp = new Date().toISOString()
|
|
224
|
+
console.log(` Cutoff timestamp: ${cutoffTimestamp}`)
|
|
225
|
+
|
|
226
|
+
// 4. Register a new CG with subscriptionFrom(timestamp)
|
|
227
|
+
// The first pop triggers metadata registration + bootstrap (seeds all partition_consumers)
|
|
228
|
+
console.log(` Registering CG 'bootstrap-cg-ts' with subscriptionFrom('${cutoffTimestamp}')...`)
|
|
229
|
+
const registerStart = Date.now()
|
|
230
|
+
|
|
231
|
+
const firstPop = await client
|
|
232
|
+
.queue(QUEUE_NAME_TS)
|
|
233
|
+
.group('bootstrap-cg-ts')
|
|
234
|
+
.subscriptionFrom(cutoffTimestamp)
|
|
235
|
+
.batch(1)
|
|
236
|
+
.wait(false)
|
|
237
|
+
.pop()
|
|
238
|
+
|
|
239
|
+
const registerTime = Date.now() - registerStart
|
|
240
|
+
console.log(` CG registered + bootstrap in ${registerTime}ms (first pop returned ${firstPop.length} msgs)`)
|
|
241
|
+
|
|
242
|
+
// First pop should return 0 messages (all messages are before the cutoff)
|
|
243
|
+
if (firstPop.length !== 0) {
|
|
244
|
+
return {
|
|
245
|
+
success: false,
|
|
246
|
+
message: `CG with subscriptionFrom should skip historical messages, got ${firstPop.length}`
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// 5. Push NEW messages to all 5000 partitions (after cutoff)
|
|
251
|
+
console.log(` Pushing new messages to ${PARTITION_COUNT} partitions...`)
|
|
252
|
+
const pushNewStart = Date.now()
|
|
253
|
+
await pushToPartitions(client, QUEUE_NAME_TS, PARTITION_COUNT, 'new')
|
|
254
|
+
const pushNewTime = Date.now() - pushNewStart
|
|
255
|
+
console.log(` Push new complete: ${PARTITION_COUNT} partitions in ${pushNewTime}ms`)
|
|
256
|
+
|
|
257
|
+
// 6. Consume all new messages with the CG
|
|
258
|
+
// If bootstrap worked, all partitions are already known -> fast consumption
|
|
259
|
+
// If bootstrap didn't work, it would discover one-at-a-time -> very slow
|
|
260
|
+
console.log(` Consuming with CG 'bootstrap-cg-ts'...`)
|
|
261
|
+
const cgConsumeStart = Date.now()
|
|
262
|
+
let cgConsumeCount = 0
|
|
263
|
+
|
|
264
|
+
await client
|
|
265
|
+
.queue(QUEUE_NAME_TS)
|
|
266
|
+
.group('bootstrap-cg-ts')
|
|
267
|
+
.subscriptionFrom(cutoffTimestamp)
|
|
268
|
+
.concurrency(10)
|
|
269
|
+
.batch(1)
|
|
270
|
+
.wait(false)
|
|
271
|
+
.idleMillis(5000)
|
|
272
|
+
.consume(async msgs => {
|
|
273
|
+
cgConsumeCount += msgs.length
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
const cgConsumeTime = Date.now() - cgConsumeStart
|
|
277
|
+
console.log(` CG consume: ${cgConsumeCount} messages in ${cgConsumeTime}ms`)
|
|
278
|
+
|
|
279
|
+
// 7. Drain new messages in queue mode too (cleanup)
|
|
280
|
+
console.log(` Draining new messages in queue mode...`)
|
|
281
|
+
let queueDrainCount = 0
|
|
282
|
+
await client
|
|
283
|
+
.queue(QUEUE_NAME_TS)
|
|
284
|
+
.concurrency(10)
|
|
285
|
+
.batch(1)
|
|
286
|
+
.wait(false)
|
|
287
|
+
.idleMillis(5000)
|
|
288
|
+
.consume(async msgs => {
|
|
289
|
+
queueDrainCount += msgs.length
|
|
290
|
+
})
|
|
291
|
+
console.log(` Queue mode drain: ${queueDrainCount} messages`)
|
|
292
|
+
|
|
293
|
+
// 8. Results
|
|
294
|
+
const allConsumed = cgConsumeCount === PARTITION_COUNT
|
|
295
|
+
const bootstrapFast = registerTime < 10000
|
|
296
|
+
|
|
297
|
+
console.log(` ---- Results (subscriptionFrom=timestamp) ----`)
|
|
298
|
+
console.log(` Partitions: ${PARTITION_COUNT}`)
|
|
299
|
+
console.log(` Push (historical): ${pushTime}ms`)
|
|
300
|
+
console.log(` CG registration: ${registerTime}ms (includes bootstrap)`)
|
|
301
|
+
console.log(` Push (new): ${pushNewTime}ms`)
|
|
302
|
+
console.log(` CG consume: ${cgConsumeTime}ms (${cgConsumeCount} msgs)`)
|
|
303
|
+
console.log(` Bootstrap fast: ${bootstrapFast ? 'YES' : 'NO'} (${registerTime}ms < 10000ms)`)
|
|
304
|
+
|
|
305
|
+
if (!allConsumed) {
|
|
306
|
+
return {
|
|
307
|
+
success: false,
|
|
308
|
+
message: `CG consumed ${cgConsumeCount}/${PARTITION_COUNT} in ${cgConsumeTime}ms (bootstrap: ${registerTime}ms)`
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
success: true,
|
|
314
|
+
message: `[timestamp] ${PARTITION_COUNT} partitions: push=${pushTime}ms, bootstrap=${registerTime}ms, cg_consume=${cgConsumeTime}ms`
|
|
315
|
+
}
|
|
316
|
+
}
|
package/test-v2/run.js
CHANGED
|
@@ -11,13 +11,7 @@ import * as transactionTests from './transaction.js'
|
|
|
11
11
|
import * as subscriptionTests from './subscription.js'
|
|
12
12
|
import * as maintenanceTests from './maintenance.js'
|
|
13
13
|
import * as retentionTests from './retention.js'
|
|
14
|
-
import * as
|
|
15
|
-
import * as leaseRenewalTests from './ai_lease_renewal.js'
|
|
16
|
-
import * as resourcesTests from './ai_resources.js'
|
|
17
|
-
import * as bufferingTests from './ai_buffering.js'
|
|
18
|
-
import * as priorityTests from './ai_priority.js'
|
|
19
|
-
import * as ttlRetentionTests from './ai_ttl_retention.js'
|
|
20
|
-
import * as mixedScenariosTests from './ai_mixed_scenarios.js'
|
|
14
|
+
import * as bootstrapTests from './bootstrap.js'
|
|
21
15
|
import { LoadBalancer } from '../client-v2/http/LoadBalancer.js';
|
|
22
16
|
|
|
23
17
|
|
|
@@ -118,17 +112,12 @@ async function main() {
|
|
|
118
112
|
transactionTests,
|
|
119
113
|
subscriptionTests,
|
|
120
114
|
retentionTests,
|
|
121
|
-
maintenanceTests
|
|
115
|
+
maintenanceTests,
|
|
116
|
+
bootstrapTests
|
|
122
117
|
]
|
|
123
118
|
|
|
124
119
|
const aiTests = [
|
|
125
|
-
|
|
126
|
-
leaseRenewalTests,
|
|
127
|
-
resourcesTests,
|
|
128
|
-
bufferingTests,
|
|
129
|
-
priorityTests,
|
|
130
|
-
ttlRetentionTests,
|
|
131
|
-
mixedScenariosTests
|
|
120
|
+
|
|
132
121
|
]
|
|
133
122
|
|
|
134
123
|
const allTests = [...humanTests, ...aiTests]
|
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
|
+
|