queen-mq 2.0.0 → 2.0.3
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 +60 -5
- package/client-v2/Queen.js +2 -2
- package/client-v2/README.md +8 -2
- package/client-v2/admin/Admin.js +30 -8
- package/client-v2/builders/QueueBuilder.js +73 -24
- package/client-v2/consumer/ConsumerManager.js +71 -15
- package/client-v2/ephemeral/Ephemeral.js +40 -8
- package/client-v2/http/HttpClient.js +82 -13
- package/client-v2/utils/defaults.js +3 -2
- package/package.json +2 -2
- package/test-v2/admin-unit/removedRoutes.test.js +63 -0
- package/test-v2/consumer-unit/nackScope.test.js +88 -0
- package/test-v2/consumer-unit/stopOnAbort.test.js +537 -0
- package/test-v2/ephemeral-unit/ephemeralWire.test.js +24 -1
- package/test-v2/pop-unit/popDefaults.test.js +236 -0
- package/test-v2/pop.js +68 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pop() and consume() share one builder but not their defaults.
|
|
3
|
+
*
|
|
4
|
+
* autoAck commitOnDelivery wait
|
|
5
|
+
* pop() never sent off; on sends autoAck true (POP_DEFAULTS)
|
|
6
|
+
* consume true, client-side refused true (CONSUME_DEFAULTS)
|
|
7
|
+
*
|
|
8
|
+
* autoAck() is consume()'s ack after the handler and never reaches the wire.
|
|
9
|
+
* The broker's at-most-once commit at delivery is the pop's own option,
|
|
10
|
+
* commitOnDelivery(), which sends autoAck=true; consume() always leases, so it
|
|
11
|
+
* refuses that option before any request.
|
|
12
|
+
* POP_DEFAULTS.wait said false while every pop long-polled; the long poll is
|
|
13
|
+
* what callers rely on, so it stays, and POP_DEFAULTS and the guide say so.
|
|
14
|
+
*
|
|
15
|
+
* The broker contract (server/src/handlers/data.rs, PopParams): autoAck=true
|
|
16
|
+
* commits the messages at delivery, with an empty leaseId; an absent autoAck
|
|
17
|
+
* is false. An absent wait is false too, but this SDK always sends wait.
|
|
18
|
+
*
|
|
19
|
+
* Same style as conflation-unit/conflationWire.test.js: a real node:http
|
|
20
|
+
* server playing a canned plan, real fetch, and every assertion is about the
|
|
21
|
+
* bytes that crossed the socket.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { describe, it } from 'node:test'
|
|
25
|
+
import assert from 'node:assert/strict'
|
|
26
|
+
|
|
27
|
+
import { Queen } from '../../client-v2/index.js'
|
|
28
|
+
import { withPlanServer, ok } from '../kv-unit/_planServer.js'
|
|
29
|
+
|
|
30
|
+
const QUEUE = 'q'
|
|
31
|
+
const GROUP = 'g'
|
|
32
|
+
|
|
33
|
+
const frame = (n = 1) => ({
|
|
34
|
+
transactionId: `txn-${n}`,
|
|
35
|
+
partitionId: `part-${n}`,
|
|
36
|
+
partition: 'Default',
|
|
37
|
+
payload: { n },
|
|
38
|
+
leaseId: 'lease-1',
|
|
39
|
+
consumerGroup: GROUP
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const popBody = (frames = [frame()]) => ok({ messages: frames, partitionsClaimed: frames.length })
|
|
43
|
+
|
|
44
|
+
// Acks answer with one result per acknowledgment; the plan only needs enough
|
|
45
|
+
// of them for the consume() cases below.
|
|
46
|
+
const ackBody = ok([{ index: 0, transactionId: 'txn-1', success: true, error: null }])
|
|
47
|
+
|
|
48
|
+
function query(url) {
|
|
49
|
+
const i = url.indexOf('?')
|
|
50
|
+
return new URLSearchParams(i < 0 ? '' : url.slice(i + 1))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const pops = (hits) => hits.filter(h => h.method === 'GET' && h.url.startsWith('/api/v1/pop'))
|
|
54
|
+
const acks = (hits) => hits.filter(h => h.method === 'POST' && h.url.startsWith('/api/v1/ack'))
|
|
55
|
+
const statusesOf = (hit) => (hit.body.acknowledgments || [hit.body]).map(a => a.status)
|
|
56
|
+
|
|
57
|
+
async function withQueen(plan, defaultResponse, run) {
|
|
58
|
+
await withPlanServer(plan, defaultResponse, async (url, hits) => {
|
|
59
|
+
const queen = new Queen({ url, handleSignals: false })
|
|
60
|
+
try {
|
|
61
|
+
await run(queen, hits)
|
|
62
|
+
} finally {
|
|
63
|
+
await queen.close()
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
describe('pop() — autoAck', () => {
|
|
69
|
+
// autoAck() is consume()'s ack after the handler and never reaches the
|
|
70
|
+
// wire: a pop comes back leased whatever autoAck() said. The broker's
|
|
71
|
+
// at-most-once autoAck is commitOnDelivery(), below.
|
|
72
|
+
for (const [label, build] of [
|
|
73
|
+
['autoAck() never called', (q) => q.group(GROUP)],
|
|
74
|
+
['autoAck(true)', (q) => q.group(GROUP).autoAck(true)],
|
|
75
|
+
['autoAck(false)', (q) => q.group(GROUP).autoAck(false)],
|
|
76
|
+
]) {
|
|
77
|
+
it(`sends no autoAck after ${label}`, async () => {
|
|
78
|
+
await withQueen([popBody(), popBody()], popBody(), async (queen, hits) => {
|
|
79
|
+
await build(queen.queue(QUEUE)).pop()
|
|
80
|
+
await build(queen.queue(QUEUE)).popResult()
|
|
81
|
+
|
|
82
|
+
for (const hit of pops(hits)) {
|
|
83
|
+
assert.equal(query(hit.url).has('autoAck'), false)
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
describe('pop() — commitOnDelivery', () => {
|
|
91
|
+
// The broker's commit at delivery, on the wire name every 2.x broker reads:
|
|
92
|
+
// the pop carries autoAck=true and nothing else changes.
|
|
93
|
+
it('pop() and popResult() send autoAck=true and nothing else', async () => {
|
|
94
|
+
await withQueen([popBody(), popBody(), popBody()], popBody(), async (queen, hits) => {
|
|
95
|
+
await queen.queue(QUEUE).group(GROUP).pop()
|
|
96
|
+
await queen.queue(QUEUE).group(GROUP).commitOnDelivery().pop()
|
|
97
|
+
await queen.queue(QUEUE).group(GROUP).commitOnDelivery(true).popResult()
|
|
98
|
+
|
|
99
|
+
const [plain, ...committed] = pops(hits)
|
|
100
|
+
assert.equal(committed.length, 2)
|
|
101
|
+
for (const hit of committed) {
|
|
102
|
+
const q = query(hit.url)
|
|
103
|
+
assert.equal(q.get('autoAck'), 'true')
|
|
104
|
+
q.delete('autoAck')
|
|
105
|
+
assert.deepEqual(Object.fromEntries(q), Object.fromEntries(query(plain.url)))
|
|
106
|
+
assert.equal(hit.url.split('?')[0], plain.url.split('?')[0])
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('sends no autoAck after commitOnDelivery(false)', async () => {
|
|
112
|
+
await withQueen([popBody()], popBody(), async (queen, hits) => {
|
|
113
|
+
await queen.queue(QUEUE).group(GROUP).commitOnDelivery(true).commitOnDelivery(false).pop()
|
|
114
|
+
|
|
115
|
+
assert.equal(query(pops(hits)[0].url).has('autoAck'), false)
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('does not depend on autoAck(), which stays consume()\'s', async () => {
|
|
120
|
+
await withQueen([popBody()], popBody(), async (queen, hits) => {
|
|
121
|
+
await queen.queue(QUEUE).group(GROUP).autoAck(false).commitOnDelivery().pop()
|
|
122
|
+
|
|
123
|
+
assert.equal(query(pops(hits)[0].url).get('autoAck'), 'true')
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('with conflation(), sends both and raises the broker\'s 400 instead of returning []', async () => {
|
|
128
|
+
// The broker refuses conflation together with a commit at delivery
|
|
129
|
+
// (server/src/handlers/data.rs, conflation_refusal).
|
|
130
|
+
const refusal = {
|
|
131
|
+
status: 400,
|
|
132
|
+
body: { success: false, error: 'conflation cannot be combined with autoAck', messages: [] }
|
|
133
|
+
}
|
|
134
|
+
await withQueen([refusal], refusal, async (queen, hits) => {
|
|
135
|
+
await assert.rejects(
|
|
136
|
+
() => queen.queue(QUEUE).group(GROUP).conflation().commitOnDelivery().pop(),
|
|
137
|
+
(err) => err.status === 400
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
const q = query(pops(hits)[0].url)
|
|
141
|
+
assert.equal(q.get('autoAck'), 'true')
|
|
142
|
+
assert.equal(q.get('conflation'), 'true')
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
describe('consume() refuses commitOnDelivery()', () => {
|
|
148
|
+
it('throws before any request', async () => {
|
|
149
|
+
await withQueen([], popBody(), async (queen, hits) => {
|
|
150
|
+
assert.throws(
|
|
151
|
+
() => queen.queue(QUEUE).group(GROUP).commitOnDelivery().consume(async () => {}),
|
|
152
|
+
{
|
|
153
|
+
message: 'commitOnDelivery() is a pop() option; consume() always leases its messages'
|
|
154
|
+
}
|
|
155
|
+
)
|
|
156
|
+
assert.equal(hits.length, 0)
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('consumes as before after commitOnDelivery(false)', async () => {
|
|
161
|
+
await withQueen([popBody(), ackBody], ackBody, async (queen, hits) => {
|
|
162
|
+
await queen.queue(QUEUE).group(GROUP).commitOnDelivery(false).limit(1).consume(async () => {})
|
|
163
|
+
|
|
164
|
+
assert.equal(query(pops(hits)[0].url).has('autoAck'), false)
|
|
165
|
+
assert.equal(acks(hits).length, 1)
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
describe('pop() — wait', () => {
|
|
171
|
+
it('long-polls when wait() was never called', async () => {
|
|
172
|
+
await withQueen([popBody()], popBody(), async (queen, hits) => {
|
|
173
|
+
await queen.queue(QUEUE).group(GROUP).pop()
|
|
174
|
+
|
|
175
|
+
assert.equal(query(pops(hits)[0].url).get('wait'), 'true')
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('long-polls for timeoutMillis() when only that was called', async () => {
|
|
180
|
+
await withQueen([popBody()], popBody(), async (queen, hits) => {
|
|
181
|
+
await queen.queue(QUEUE).timeoutMillis(2000).pop()
|
|
182
|
+
|
|
183
|
+
const q = query(pops(hits)[0].url)
|
|
184
|
+
assert.equal(q.get('wait'), 'true')
|
|
185
|
+
assert.equal(q.get('timeout'), '2000')
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('sends wait=false after wait(false)', async () => {
|
|
190
|
+
await withQueen([popBody()], popBody(), async (queen, hits) => {
|
|
191
|
+
await queen.queue(QUEUE).wait(false).pop()
|
|
192
|
+
|
|
193
|
+
assert.equal(query(pops(hits)[0].url).get('wait'), 'false')
|
|
194
|
+
})
|
|
195
|
+
})
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
describe('consume() keeps its own defaults', () => {
|
|
199
|
+
it('long-polls and never sends autoAck, also after autoAck(true): it acks after the handler', async () => {
|
|
200
|
+
await withQueen([popBody(), ackBody], ackBody, async (queen, hits) => {
|
|
201
|
+
await queen.queue(QUEUE).group(GROUP).autoAck(true).limit(1)
|
|
202
|
+
.consume(async () => {})
|
|
203
|
+
|
|
204
|
+
const q = query(pops(hits)[0].url)
|
|
205
|
+
assert.equal(q.get('wait'), 'true')
|
|
206
|
+
assert.equal(q.has('autoAck'), false)
|
|
207
|
+
assert.equal(acks(hits).length, 1, 'the consumer acked after the handler returned')
|
|
208
|
+
assert.deepEqual(statusesOf(acks(hits)[0]), ['completed'])
|
|
209
|
+
})
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('acks after the handler when autoAck() was never called', async () => {
|
|
213
|
+
await withQueen([popBody(), ackBody], ackBody, async (queen, hits) => {
|
|
214
|
+
await queen.queue(QUEUE).group(GROUP).limit(1).consume(async () => {})
|
|
215
|
+
|
|
216
|
+
assert.equal(query(pops(hits)[0].url).get('wait'), 'true')
|
|
217
|
+
assert.equal(acks(hits).length, 1)
|
|
218
|
+
})
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('sends no ack after autoAck(false)', async () => {
|
|
222
|
+
await withQueen([popBody()], popBody(), async (queen, hits) => {
|
|
223
|
+
await queen.queue(QUEUE).group(GROUP).autoAck(false).limit(1).consume(async () => {})
|
|
224
|
+
|
|
225
|
+
assert.equal(acks(hits).length, 0)
|
|
226
|
+
})
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it('sends wait=false after wait(false)', async () => {
|
|
230
|
+
await withQueen([popBody(), ackBody], ackBody, async (queen, hits) => {
|
|
231
|
+
await queen.queue(QUEUE).group(GROUP).wait(false).limit(1).consume(async () => {})
|
|
232
|
+
|
|
233
|
+
assert.equal(query(pops(hits)[0].url).get('wait'), 'false')
|
|
234
|
+
})
|
|
235
|
+
})
|
|
236
|
+
})
|
package/test-v2/pop.js
CHANGED
|
@@ -361,3 +361,71 @@ export async function renewReportsAReleasedLease(client) {
|
|
|
361
361
|
released.success === false && released.newExpiresAt === null && typeof released.error === 'string'
|
|
362
362
|
return { success, message: `live lease: ${JSON.stringify(live)}; after the ack: ${JSON.stringify(released)}` }
|
|
363
363
|
}
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
// autoAck() is consume()'s ack after the handler and never reaches the wire: a
|
|
367
|
+
// pop after autoAck(true) is leased like any pop, and with a 1 s lease the
|
|
368
|
+
// message comes back. The broker's at-most-once autoAck is commitOnDelivery().
|
|
369
|
+
export async function popAutoAckStaysLeased(client) {
|
|
370
|
+
const queueName = 'test-queue-v2-pop-auto-ack'
|
|
371
|
+
const queue = await client.queue(queueName).config({ leaseTime: 1 }).create()
|
|
372
|
+
if (!queue.configured) {
|
|
373
|
+
return { success: false, message: 'Queue not created' }
|
|
374
|
+
}
|
|
375
|
+
await client.queue(queueName).push([{ data: { n: 1 } }])
|
|
376
|
+
|
|
377
|
+
const [message] = await client.queue(queueName).batch(1).wait(true).timeoutMillis(5000).autoAck(true).pop()
|
|
378
|
+
if (!message) {
|
|
379
|
+
return { success: false, message: 'Nothing popped' }
|
|
380
|
+
}
|
|
381
|
+
// Past the 1 s lease: a leased message is delivered again now.
|
|
382
|
+
await new Promise(resolve => setTimeout(resolve, 2500))
|
|
383
|
+
const again = await client.queue(queueName).batch(1).wait(true).timeoutMillis(3000).pop()
|
|
384
|
+
|
|
385
|
+
const success = typeof message.leaseId === 'string' && message.leaseId !== '' && again.length === 1
|
|
386
|
+
return { success, message: `leaseId ${JSON.stringify(message.leaseId)}, delivered again after the lease: ${again.length}` }
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// commitOnDelivery() sends autoAck=true: the broker moves the group's cursor
|
|
390
|
+
// past the message as it hands it out, so the pop has no lease and the message
|
|
391
|
+
// does not come back after the 1 s lease of the queue.
|
|
392
|
+
export async function popCommitOnDeliveryNotRedelivered(client) {
|
|
393
|
+
const queueName = 'test-queue-v2-pop-commit-on-delivery'
|
|
394
|
+
const queue = await client.queue(queueName).config({ leaseTime: 1 }).create()
|
|
395
|
+
if (!queue.configured) {
|
|
396
|
+
return { success: false, message: 'Queue not created' }
|
|
397
|
+
}
|
|
398
|
+
await client.queue(queueName).push([{ data: { n: 1 } }])
|
|
399
|
+
|
|
400
|
+
const [message] = await client.queue(queueName).batch(1).wait(true).timeoutMillis(5000).commitOnDelivery().pop()
|
|
401
|
+
if (!message) {
|
|
402
|
+
return { success: false, message: 'Nothing popped' }
|
|
403
|
+
}
|
|
404
|
+
// Past the 1 s lease: a leased message would be delivered again now.
|
|
405
|
+
await new Promise(resolve => setTimeout(resolve, 2500))
|
|
406
|
+
const again = await client.queue(queueName).batch(1).wait(true).timeoutMillis(1500).pop()
|
|
407
|
+
|
|
408
|
+
const success = !message.leaseId && again.length === 0
|
|
409
|
+
return { success, message: `leaseId ${JSON.stringify(message.leaseId)}, delivered again after the lease: ${again.length}` }
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// pop() long-polls by default (POP_DEFAULTS.wait), and wait(false) returns at
|
|
413
|
+
// once.
|
|
414
|
+
export async function popLongPollsByDefault(client) {
|
|
415
|
+
const queueName = 'test-queue-v2-pop-wait-default'
|
|
416
|
+
const queue = await client.queue(queueName).create()
|
|
417
|
+
if (!queue.configured) {
|
|
418
|
+
return { success: false, message: 'Queue not created' }
|
|
419
|
+
}
|
|
420
|
+
let started = Date.now()
|
|
421
|
+
const waited = await client.queue(queueName).batch(1).timeoutMillis(1500).pop()
|
|
422
|
+
const waitedMillis = Date.now() - started
|
|
423
|
+
started = Date.now()
|
|
424
|
+
const atOnce = await client.queue(queueName).batch(1).wait(false).pop()
|
|
425
|
+
const atOnceMillis = Date.now() - started
|
|
426
|
+
|
|
427
|
+
return {
|
|
428
|
+
success: waited.length === 0 && waitedMillis >= 1000 && atOnce.length === 0 && atOnceMillis < 1000,
|
|
429
|
+
message: `empty pop: ${waitedMillis} ms by default (long poll of 1500 ms), ${atOnceMillis} ms with wait(false)`,
|
|
430
|
+
}
|
|
431
|
+
}
|