queen-mq 2.6.0 → 2.7.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/client-v2/consumer/ConsumerManager.js +92 -30
- package/client-v2/http/HttpClient.js +8 -5
- package/client-v2/streams/operators/ReduceOperator.js +15 -3
- package/client-v2/streams/operators/WindowSessionOperator.js +9 -2
- package/client-v2/streams/operators/WindowTumblingOperator.js +9 -2
- package/client-v2/streams/runtime/Runner.js +356 -20
- package/package.json +2 -2
- package/test-v2/consumer-unit/transientPop.test.js +361 -0
- package/test-v2/stream/claims.js +129 -0
- package/test-v2/stream/index.js +2 -0
- package/test-v2/streams-unit/claims.test.js +246 -0
- package/test-v2/streams-unit/fakeServer.js +41 -2
|
@@ -19,13 +19,22 @@
|
|
|
19
19
|
* - build push_items / run foreach effects
|
|
20
20
|
* - commit cycle (state ops + push items + ack) atomically
|
|
21
21
|
*
|
|
22
|
-
* flush timer:
|
|
22
|
+
* flush timer, processing-time windows with a reducer (broker 2.7):
|
|
23
|
+
* each cycle notes when the partition's earliest open window closes ->
|
|
24
|
+
* when that time comes, claim the partition (POST /streams/v1/claim: a
|
|
25
|
+
* lease with an empty batch) -> close what is ripe on the broker's
|
|
26
|
+
* clock -> commit a cycle that acks the empty batch (count 0) under
|
|
27
|
+
* the claim's lease. A sweep (POST /streams/v1/state/partitions) finds
|
|
28
|
+
* the windows nobody tracks: after a restart, or left by another runner.
|
|
29
|
+
*
|
|
30
|
+
* flush timer, event-time windows or a broker without the claim route:
|
|
23
31
|
* periodically wake -> for partitions touched recently, query state
|
|
24
32
|
* for ripe windows -> apply post-reducer ops + foreach + sink push,
|
|
25
33
|
* commit no-ack cycle (state deletes + sink push, no source ack).
|
|
26
34
|
*
|
|
27
35
|
* stop():
|
|
28
|
-
* - signal abort, wait for in-flight cycle and flush
|
|
36
|
+
* - signal abort, wait for in-flight cycle and flush (a flush at most
|
|
37
|
+
* 10 s), return.
|
|
29
38
|
*
|
|
30
39
|
* Retry / fault model unchanged from v0.1: cycle commit failures roll
|
|
31
40
|
* back; Queen redelivers via lease/retry. Stateless / reducer / foreach
|
|
@@ -46,9 +55,40 @@ const DEFAULT_MAX_PARTITIONS = 4
|
|
|
46
55
|
const DEFAULT_IDLE_SLEEP_MS = 100
|
|
47
56
|
// Track partitions we've touched within this rolling window. Drives the
|
|
48
57
|
// idle-flush sweep — anything older than this is assumed to be owned by a
|
|
49
|
-
// different worker and we don't speculatively flush it.
|
|
58
|
+
// different worker and we don't speculatively flush it. Only for a broker
|
|
59
|
+
// without POST /streams/v1/claim (before 2.7), and for event-time windows.
|
|
50
60
|
const PARTITION_RECENCY_MS = 5 * 60 * 1000 // 5 minutes
|
|
51
61
|
|
|
62
|
+
// Closes under a claim (broker 2.7, issue #153).
|
|
63
|
+
//
|
|
64
|
+
// How often a runner asks the broker for the partitions holding the query's
|
|
65
|
+
// state: after a restart, a scale-down, or for partitions another runner
|
|
66
|
+
// left, these are the windows nobody tracks.
|
|
67
|
+
const SWEEP_EVERY_MS = 30_000
|
|
68
|
+
// How old a partition's newest state row must be before a later sweep takes
|
|
69
|
+
// it on: a younger one is some runner's live work, which tracks it already.
|
|
70
|
+
// The first sweep after a start takes every partition: this runner tracks
|
|
71
|
+
// nothing yet.
|
|
72
|
+
const SWEEP_UNTOUCHED_MS = 30_000
|
|
73
|
+
// Partitions per page of the sweep.
|
|
74
|
+
const SWEEP_PAGE = 1000
|
|
75
|
+
// Partitions per claim, which is also the most one flush tick closes: a
|
|
76
|
+
// sweep that finds thousands of quiet partitions (after an outage) works
|
|
77
|
+
// through them a batch per tick instead of all at once.
|
|
78
|
+
const CLAIM_BATCH = 100
|
|
79
|
+
// The lease a claim asks for. A close is one state read and one commit.
|
|
80
|
+
const CLAIM_LEASE_S = 30
|
|
81
|
+
// How soon to try again a partition that someone else holds, or whose claim
|
|
82
|
+
// or close failed.
|
|
83
|
+
const CLAIM_RETRY_MS = 1000
|
|
84
|
+
// How long stop() waits for a flush in flight.
|
|
85
|
+
const STOP_FLUSH_WAIT_MS = 10_000
|
|
86
|
+
|
|
87
|
+
// Whether the broker serves POST /streams/v1/claim.
|
|
88
|
+
const CLAIMS_UNKNOWN = 'unknown'
|
|
89
|
+
const CLAIMS_YES = 'yes'
|
|
90
|
+
const CLAIMS_NO = 'no' // an older broker: the unleased flush of recent partitions
|
|
91
|
+
|
|
52
92
|
const WATERMARK_STATE_KEY = '__wm__' // reserved internal key for per-partition watermark
|
|
53
93
|
|
|
54
94
|
export class Runner {
|
|
@@ -100,6 +140,14 @@ export class Runner {
|
|
|
100
140
|
this._partitionMutexes = new Map() // partitionId -> tail promise
|
|
101
141
|
this._recentPartitions = new Map() // partitionId -> { partitionName, touchedAt }
|
|
102
142
|
this._partitionWatermarks = new Map() // partitionId -> wmMs (cache; the broker is source of truth)
|
|
143
|
+
// Processing-time windows still open, by partition: what the idle flush
|
|
144
|
+
// claims and closes once their time has come. atMs is on the broker's
|
|
145
|
+
// clock (processing time is the messages' createdAt).
|
|
146
|
+
this._due = new Map() // partitionId -> { partitionName, atMs }
|
|
147
|
+
this._claims = CLAIMS_UNKNOWN
|
|
148
|
+
this._nextSweepAt = 0 // the first flush tick sweeps
|
|
149
|
+
this._sweepUntouchedMs = 0 // the first sweep takes every partition
|
|
150
|
+
this._flushPromise = null // the flush tick in flight, for stop()
|
|
103
151
|
this._stats = {
|
|
104
152
|
cyclesTotal: 0,
|
|
105
153
|
flushCyclesTotal: 0,
|
|
@@ -157,6 +205,9 @@ export class Runner {
|
|
|
157
205
|
}
|
|
158
206
|
|
|
159
207
|
this._loopPromise = this._loop()
|
|
208
|
+
// The first flush tick runs now rather than one interval later: its sweep
|
|
209
|
+
// finds the windows a previous runner left open.
|
|
210
|
+
if (this._flushTimers.length > 0) this._flushTick().catch(() => {})
|
|
160
211
|
return this
|
|
161
212
|
}
|
|
162
213
|
|
|
@@ -165,8 +216,17 @@ export class Runner {
|
|
|
165
216
|
for (const t of this._flushTimers) clearInterval(t)
|
|
166
217
|
this._flushTimers = []
|
|
167
218
|
if (this._loopPromise) await this._loopPromise
|
|
168
|
-
// Allow any in-flight flush to drain.
|
|
169
|
-
|
|
219
|
+
// Allow any in-flight flush to drain. A close cut short would keep its
|
|
220
|
+
// claim on the partition until the lease ran out, so it gets up to 10 s;
|
|
221
|
+
// past that stop() returns and the flush ends on its own.
|
|
222
|
+
if (this._flushPromise) {
|
|
223
|
+
let timer
|
|
224
|
+
await Promise.race([
|
|
225
|
+
this._flushPromise.catch(() => {}),
|
|
226
|
+
new Promise(resolve => { timer = setTimeout(resolve, STOP_FLUSH_WAIT_MS) })
|
|
227
|
+
])
|
|
228
|
+
clearTimeout(timer)
|
|
229
|
+
}
|
|
170
230
|
}
|
|
171
231
|
|
|
172
232
|
metrics() {
|
|
@@ -187,7 +247,10 @@ export class Runner {
|
|
|
187
247
|
const groups = this._groupByPartition(messages)
|
|
188
248
|
for (const group of groups) {
|
|
189
249
|
if (this._isStopped()) break
|
|
190
|
-
|
|
250
|
+
// The recent map feeds only the unleased flush.
|
|
251
|
+
if (!this._leasedCloses() || this._claims !== CLAIMS_YES) {
|
|
252
|
+
this._touchPartition(group.partitionId, group.partitionName)
|
|
253
|
+
}
|
|
191
254
|
try {
|
|
192
255
|
await this._processPartitionCycle(group)
|
|
193
256
|
this._stats.cyclesTotal++
|
|
@@ -373,12 +436,14 @@ export class Runner {
|
|
|
373
436
|
// 3. Window operator + reducer (if present).
|
|
374
437
|
let stateOps = []
|
|
375
438
|
let emits = []
|
|
439
|
+
let nextCloseMs = null
|
|
376
440
|
if (stages.window) {
|
|
377
441
|
const result = stages.window.windowKind === 'session'
|
|
378
|
-
? await this._processSessionWindow(stages, envelopes, partitionId)
|
|
442
|
+
? await this._processSessionWindow(stages, envelopes, partitionId, orderedMessages)
|
|
379
443
|
: await this._processStatelessWindow(stages, envelopes, partitionId, orderedMessages)
|
|
380
444
|
stateOps = result.stateOps
|
|
381
445
|
emits = result.emits
|
|
446
|
+
nextCloseMs = result.nextCloseMs ?? null
|
|
382
447
|
} else {
|
|
383
448
|
// No window: each remaining envelope becomes an emit.
|
|
384
449
|
emits = envelopes.map(env => ({ key: env.key, value: env.value, sourceMsg: env.msg }))
|
|
@@ -427,6 +492,9 @@ export class Runner {
|
|
|
427
492
|
pushItems,
|
|
428
493
|
ack
|
|
429
494
|
})
|
|
495
|
+
// What stays open closes when its time comes, under a claim.
|
|
496
|
+
// A broker without claims never closes these, so they are not kept.
|
|
497
|
+
if (this._leasedCloses() && this._claims !== CLAIMS_NO) this._setDue(partitionId, partitionName, nextCloseMs)
|
|
430
498
|
}
|
|
431
499
|
|
|
432
500
|
// --------------------------------------------------------------- gate path
|
|
@@ -756,7 +824,7 @@ export class Runner {
|
|
|
756
824
|
|
|
757
825
|
// ----------------------------------------------------- session window path
|
|
758
826
|
|
|
759
|
-
async _processSessionWindow(stages, envelopes, partitionId) {
|
|
827
|
+
async _processSessionWindow(stages, envelopes, partitionId, orderedMessages) {
|
|
760
828
|
const window = stages.window
|
|
761
829
|
const reducer = stages.reducer
|
|
762
830
|
if (!reducer) {
|
|
@@ -822,8 +890,17 @@ export class Runner {
|
|
|
822
890
|
this._partitionWatermarks.set(partitionId, advancedWm)
|
|
823
891
|
nowMs = advancedWm
|
|
824
892
|
} else {
|
|
825
|
-
//
|
|
826
|
-
|
|
893
|
+
// Processing time: the batch's newest createdAt. That is the broker's
|
|
894
|
+
// clock, the one the claimed closes use too; this host's Date.now()
|
|
895
|
+
// could close a session early when it runs ahead, and would close
|
|
896
|
+
// every session of an old backlog at once. Date.now() only when no
|
|
897
|
+
// message carries a createdAt.
|
|
898
|
+
let newest = -Infinity
|
|
899
|
+
for (const m of orderedMessages || []) {
|
|
900
|
+
const t = Date.parse(m.createdAt || '')
|
|
901
|
+
if (Number.isFinite(t) && t > newest) newest = t
|
|
902
|
+
}
|
|
903
|
+
nowMs = newest > -Infinity ? newest : Date.now()
|
|
827
904
|
}
|
|
828
905
|
|
|
829
906
|
const reducerFn = reducer.fn
|
|
@@ -886,26 +963,53 @@ export class Runner {
|
|
|
886
963
|
// ------------------------------------------------ idle flush (per-window)
|
|
887
964
|
|
|
888
965
|
async _flushTick() {
|
|
966
|
+
// A tick that would overlap the one still running is skipped.
|
|
889
967
|
if (this._stopped || this._flushInFlight) return
|
|
890
968
|
const stages = this.stream.stages
|
|
891
969
|
if (!stages.window) return
|
|
892
970
|
this._flushInFlight = true
|
|
971
|
+
const run = this._flushTickInner()
|
|
972
|
+
this._flushPromise = run
|
|
893
973
|
try {
|
|
894
|
-
|
|
895
|
-
for (const [partitionId, info] of partitions) {
|
|
896
|
-
if (this._isStopped()) break
|
|
897
|
-
try {
|
|
898
|
-
await this._flushPartition(partitionId, info.partitionName)
|
|
899
|
-
} catch (err) {
|
|
900
|
-
this._reportError(err, { phase: 'flush', partition: partitionId })
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
this._stats.lastFlushAt = new Date().toISOString()
|
|
974
|
+
await run
|
|
904
975
|
} finally {
|
|
905
976
|
this._flushInFlight = false
|
|
906
977
|
}
|
|
907
978
|
}
|
|
908
979
|
|
|
980
|
+
async _flushTickInner() {
|
|
981
|
+
if (!this._leasedCloses() || this._claims === CLAIMS_NO) {
|
|
982
|
+
await this._flushRecent()
|
|
983
|
+
} else {
|
|
984
|
+
if (Date.now() >= this._nextSweepAt) {
|
|
985
|
+
await this._sweep(this._sweepUntouchedMs)
|
|
986
|
+
this._nextSweepAt = Date.now() + SWEEP_EVERY_MS
|
|
987
|
+
this._sweepUntouchedMs = SWEEP_UNTOUCHED_MS
|
|
988
|
+
}
|
|
989
|
+
// Not when stopping: a claim we would not close holds its partition
|
|
990
|
+
// until the lease runs out.
|
|
991
|
+
if (this._claims !== CLAIMS_NO && !this._isStopped()) await this._closeDue()
|
|
992
|
+
}
|
|
993
|
+
this._stats.lastFlushAt = new Date().toISOString()
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
/**
|
|
997
|
+
* The unleased flush of the partitions this process popped in the last
|
|
998
|
+
* five minutes: event-time windows, and every window on a broker without
|
|
999
|
+
* the claim route.
|
|
1000
|
+
*/
|
|
1001
|
+
async _flushRecent() {
|
|
1002
|
+
const partitions = Array.from(this._recentPartitions.entries())
|
|
1003
|
+
for (const [partitionId, info] of partitions) {
|
|
1004
|
+
if (this._isStopped()) break
|
|
1005
|
+
try {
|
|
1006
|
+
await this._flushPartition(partitionId, info.partitionName)
|
|
1007
|
+
} catch (err) {
|
|
1008
|
+
this._reportError(err, { phase: 'flush', partition: partitionId })
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
909
1013
|
async _flushPartition(partitionId, partitionName) {
|
|
910
1014
|
// Mutual exclusion with the pop/cycle loop (see _partitionMutexes).
|
|
911
1015
|
return this._withPartitionLock(partitionId, () => this._flushPartitionInner(partitionId, partitionName))
|
|
@@ -1068,6 +1172,238 @@ export class Runner {
|
|
|
1068
1172
|
return map
|
|
1069
1173
|
}
|
|
1070
1174
|
|
|
1175
|
+
// ------------------------------------------------ closes under a claim
|
|
1176
|
+
//
|
|
1177
|
+
// A processing-time window closes when its time comes even if its
|
|
1178
|
+
// partition gets no more messages. The runner keeps, per partition, when
|
|
1179
|
+
// the earliest open window closes (from each cycle it commits, and from
|
|
1180
|
+
// the sweep), claims the partition when that time has come
|
|
1181
|
+
// (POST /streams/v1/claim: a lease with an empty batch, granted only when
|
|
1182
|
+
// nobody holds the partition and the group has read all of it), and
|
|
1183
|
+
// closes the windows in a cycle that acks the empty batch. The broker
|
|
1184
|
+
// refuses that cycle whole when the lease is gone, so two runners never
|
|
1185
|
+
// close one window, and a close never deletes a window another runner
|
|
1186
|
+
// just extended. The clock is the broker's, the one createdAt comes from.
|
|
1187
|
+
|
|
1188
|
+
/**
|
|
1189
|
+
* Whether this stream closes its windows under claims: processing-time
|
|
1190
|
+
* windows with a reducer and an idle flush. Event-time windows close on
|
|
1191
|
+
* the watermark, which only events move.
|
|
1192
|
+
*/
|
|
1193
|
+
_leasedCloses() {
|
|
1194
|
+
const { window, reducer } = this.stream.stages
|
|
1195
|
+
return !!(window && !window.eventTimeFn && window.idleFlushMs > 0 && reducer)
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
/** Track (or forget, atMs null) when the partition's earliest open window closes. */
|
|
1199
|
+
_setDue(partitionId, partitionName, atMs) {
|
|
1200
|
+
if (!partitionId) return
|
|
1201
|
+
if (typeof atMs === 'number' && Number.isFinite(atMs)) {
|
|
1202
|
+
this._due.set(partitionId, { partitionName, atMs })
|
|
1203
|
+
} else {
|
|
1204
|
+
this._due.delete(partitionId)
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
/** Try a tracked partition again no sooner than atMs. */
|
|
1209
|
+
_retryDue(partitionId, atMs) {
|
|
1210
|
+
const d = this._due.get(partitionId)
|
|
1211
|
+
if (d) d.atMs = Math.max(d.atMs, atMs)
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
_claimsOff() {
|
|
1215
|
+
if (this._claims !== CLAIMS_NO) {
|
|
1216
|
+
this.logger.info('broker has no stream claims; idle flush of recent partitions', { queryId: this.queryId })
|
|
1217
|
+
}
|
|
1218
|
+
this._claims = CLAIMS_NO
|
|
1219
|
+
// Nothing left that would never be closed.
|
|
1220
|
+
this._due.clear()
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
/**
|
|
1224
|
+
* The partitions holding the query's state that nobody touched for
|
|
1225
|
+
* `untouchedMs` (0: all of them) and this runner does not track yet:
|
|
1226
|
+
* tracked as due now, so the next ticks claim them a batch at a time.
|
|
1227
|
+
*/
|
|
1228
|
+
async _sweep(untouchedMs) {
|
|
1229
|
+
let after = null
|
|
1230
|
+
for (;;) {
|
|
1231
|
+
if (this._isStopped()) return
|
|
1232
|
+
const body = { query_id: this._serverQueryId, limit: SWEEP_PAGE }
|
|
1233
|
+
if (untouchedMs > 0) body.untouched_for_ms = untouchedMs
|
|
1234
|
+
if (after) body.after = after
|
|
1235
|
+
let res
|
|
1236
|
+
try {
|
|
1237
|
+
res = await this.http.post('/streams/v1/state/partitions', body)
|
|
1238
|
+
} catch (err) {
|
|
1239
|
+
// A broker before 2.7: no claims either.
|
|
1240
|
+
if (err && err.status === 404) return this._claimsOff()
|
|
1241
|
+
this._reportError(err, { phase: 'sweep' })
|
|
1242
|
+
return
|
|
1243
|
+
}
|
|
1244
|
+
if (!res) return
|
|
1245
|
+
if (res.success === false) {
|
|
1246
|
+
this._reportError(new Error(res.error || 'state/partitions failed'), { phase: 'sweep' })
|
|
1247
|
+
return
|
|
1248
|
+
}
|
|
1249
|
+
const now = Date.now()
|
|
1250
|
+
for (const p of res.partitions || []) {
|
|
1251
|
+
if (!p || !p.partition_id || this._due.has(p.partition_id)) continue
|
|
1252
|
+
this._due.set(p.partition_id, { partitionName: p.partition || '', atMs: now })
|
|
1253
|
+
}
|
|
1254
|
+
if (!res.next) return
|
|
1255
|
+
after = res.next
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
/** Claim the partitions whose windows are due, and close them. */
|
|
1260
|
+
async _closeDue() {
|
|
1261
|
+
const window = this.stream.stages.window
|
|
1262
|
+
const now = Date.now()
|
|
1263
|
+
const ready = []
|
|
1264
|
+
for (const [partitionId, d] of this._due) {
|
|
1265
|
+
if (d.atMs <= now) ready.push([partitionId, d.atMs])
|
|
1266
|
+
}
|
|
1267
|
+
if (ready.length === 0) return
|
|
1268
|
+
ready.sort((a, b) => a[1] - b[1])
|
|
1269
|
+
const partitionIds = ready.slice(0, CLAIM_BATCH).map(([partitionId]) => partitionId)
|
|
1270
|
+
|
|
1271
|
+
let res
|
|
1272
|
+
try {
|
|
1273
|
+
res = await this.http.post('/streams/v1/claim', {
|
|
1274
|
+
query_id: this._serverQueryId,
|
|
1275
|
+
partition_ids: partitionIds,
|
|
1276
|
+
consumer_group: this.consumerGroup,
|
|
1277
|
+
lease_seconds: CLAIM_LEASE_S
|
|
1278
|
+
})
|
|
1279
|
+
} catch (err) {
|
|
1280
|
+
if (err && err.status === 404) return this._claimsOff()
|
|
1281
|
+
this._reportError(err, { phase: 'claim' })
|
|
1282
|
+
const retryAt = Date.now() + CLAIM_RETRY_MS
|
|
1283
|
+
for (const partitionId of partitionIds) this._retryDue(partitionId, retryAt)
|
|
1284
|
+
return
|
|
1285
|
+
}
|
|
1286
|
+
if (!res) return
|
|
1287
|
+
this._claims = CLAIMS_YES
|
|
1288
|
+
if (res.success === false) {
|
|
1289
|
+
this._reportError(new Error(res.error || 'claim refused'), { phase: 'claim' })
|
|
1290
|
+
return
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
for (const p of res.partitions || []) {
|
|
1294
|
+
const partitionId = p.partition_id
|
|
1295
|
+
let partitionName = p.partition
|
|
1296
|
+
if (!partitionName) {
|
|
1297
|
+
const d = this._due.get(partitionId)
|
|
1298
|
+
partitionName = d ? d.partitionName : ''
|
|
1299
|
+
}
|
|
1300
|
+
if (p.status === 'claimed') {
|
|
1301
|
+
// Stopping: the claims not closed yet lapse with their lease,
|
|
1302
|
+
// nothing is lost.
|
|
1303
|
+
if (this._isStopped()) continue
|
|
1304
|
+
const clockMs = Number.isFinite(res.now_ms) ? res.now_ms : Date.now()
|
|
1305
|
+
try {
|
|
1306
|
+
await this._closeClaimed(partitionId, partitionName, res.lease_id, clockMs)
|
|
1307
|
+
} catch (err) {
|
|
1308
|
+
// Its claim lapses with the lease; the next try comes after it.
|
|
1309
|
+
this._reportError(err, { phase: 'flush', partition: partitionId })
|
|
1310
|
+
this._retryDue(partitionId, Date.now() + CLAIM_RETRY_MS)
|
|
1311
|
+
}
|
|
1312
|
+
} else if (p.status === 'busy') {
|
|
1313
|
+
// Someone holds it (a cycle, maybe another runner's): come back
|
|
1314
|
+
// after an idle-flush interval.
|
|
1315
|
+
this._retryDue(partitionId, Date.now() + Math.max(window.idleFlushMs, CLAIM_RETRY_MS))
|
|
1316
|
+
} else {
|
|
1317
|
+
// backlog: messages wait, and the cycle that takes them tracks the
|
|
1318
|
+
// partition again. missing: nothing to close there.
|
|
1319
|
+
this._setDue(partitionId, partitionName, null)
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
/**
|
|
1325
|
+
* Close what is ripe on a claimed partition at `clockMs` (the broker's
|
|
1326
|
+
* time at the claim), in a cycle that acks the claim's empty batch, and
|
|
1327
|
+
* track when the windows left open close. The due time is set while the
|
|
1328
|
+
* partition lock is held, as a cycle sets it: set after, it could
|
|
1329
|
+
* overwrite the newer time of a cycle that ran in between.
|
|
1330
|
+
*/
|
|
1331
|
+
_closeClaimed(partitionId, partitionName, leaseId, clockMs) {
|
|
1332
|
+
// Mutual exclusion with the pop/cycle loop (see _partitionMutexes).
|
|
1333
|
+
return this._withPartitionLock(partitionId, async () => {
|
|
1334
|
+
const stages = this.stream.stages
|
|
1335
|
+
const window = stages.window
|
|
1336
|
+
const reducer = stages.reducer
|
|
1337
|
+
// Every row: the windows left open say when to come back.
|
|
1338
|
+
const loadedAll = await getState({
|
|
1339
|
+
http: this.http,
|
|
1340
|
+
queryId: this._serverQueryId,
|
|
1341
|
+
partitionId,
|
|
1342
|
+
keys: []
|
|
1343
|
+
})
|
|
1344
|
+
let result
|
|
1345
|
+
if (window.windowKind === 'session') {
|
|
1346
|
+
const sessionState = new Map()
|
|
1347
|
+
for (const [key, value] of loadedAll.entries()) {
|
|
1348
|
+
if (key.startsWith(window.operatorTag + '\u001fopen\u001f')) sessionState.set(key, value)
|
|
1349
|
+
}
|
|
1350
|
+
result = await window.runSession({
|
|
1351
|
+
envelopes: [],
|
|
1352
|
+
loadedState: sessionState,
|
|
1353
|
+
reducerFn: reducer.fn,
|
|
1354
|
+
reducerInitial: () => reducer._initial ? reducer._initial() : 0,
|
|
1355
|
+
nowMs: clockMs
|
|
1356
|
+
})
|
|
1357
|
+
} else {
|
|
1358
|
+
result = await reducer.run({
|
|
1359
|
+
envelopes: [],
|
|
1360
|
+
loadedState: loadedAll,
|
|
1361
|
+
streamTimeMs: clockMs,
|
|
1362
|
+
operatorTag: window.operatorTag,
|
|
1363
|
+
gracePeriodMs: window.gracePeriodMs
|
|
1364
|
+
})
|
|
1365
|
+
}
|
|
1366
|
+
const closed = result.emits.length > 0
|
|
1367
|
+
const emits = await this._applyPostStages(stages.post, result.emits, partitionId, partitionName)
|
|
1368
|
+
const pushItems = await this._terminal(emits, partitionName, partitionId)
|
|
1369
|
+
// The empty batch's ack: count 0 keeps the cursor where it is and the
|
|
1370
|
+
// lease goes back, also when nothing was ripe (another runner closed
|
|
1371
|
+
// it first).
|
|
1372
|
+
await commitCycle({
|
|
1373
|
+
http: this.http,
|
|
1374
|
+
queryId: this._serverQueryId,
|
|
1375
|
+
partitionId,
|
|
1376
|
+
consumerGroup: this.consumerGroup,
|
|
1377
|
+
stateOps: result.stateOps,
|
|
1378
|
+
pushItems,
|
|
1379
|
+
ack: { transactionId: '', leaseId, status: 'completed', count: 0 },
|
|
1380
|
+
releaseLease: true
|
|
1381
|
+
})
|
|
1382
|
+
if (closed) this._stats.flushCyclesTotal++
|
|
1383
|
+
this._stats.stateOpsTotal += result.stateOps.length
|
|
1384
|
+
this._stats.pushItemsTotal += pushItems.length
|
|
1385
|
+
this._setDue(partitionId, partitionName, result.nextCloseMs ?? null)
|
|
1386
|
+
})
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
/** Sink push items for the emits, or run foreach on them (then none). */
|
|
1390
|
+
async _terminal(emits, partitionName, partitionId) {
|
|
1391
|
+
const sink = this.stream.stages.sink
|
|
1392
|
+
if (sink && sink.kind === 'sink') {
|
|
1393
|
+
return sink.buildPushItems(
|
|
1394
|
+
emits.map(e => ({ key: e.key, value: e.value, sourceMsg: e.sourceMsg || null })),
|
|
1395
|
+
partitionName
|
|
1396
|
+
)
|
|
1397
|
+
}
|
|
1398
|
+
if (sink && sink.kind === 'foreach') {
|
|
1399
|
+
await sink.runEffects(emits.map(e => ({
|
|
1400
|
+
value: e.value,
|
|
1401
|
+
ctx: this._buildEmitCtx(e, partitionName, partitionId)
|
|
1402
|
+
})))
|
|
1403
|
+
}
|
|
1404
|
+
return []
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1071
1407
|
// --------------------------------------------------------------- helpers
|
|
1072
1408
|
|
|
1073
1409
|
_extractQueueName(qb) {
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "queen-mq",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Partitioned message queue on a replicated broker log — broker client + fluent streaming SDK (windows, joins, gates) in one package",
|
|
6
6
|
"main": "client-v2/index.js",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"test": "npm run test:unit && node test-v2/run.js human",
|
|
9
|
-
"test:unit": "node --test test-v2/streams-unit/configHash.test.js test-v2/streams-unit/operators.test.js test-v2/streams-unit/cycle.test.js test-v2/streams-unit/eventTime.test.js test-v2/streams-unit/ack.test.js test-v2/streams-unit/gate.test.js test-v2/http-unit/retry429.test.js test-v2/http-unit/hostHeader.test.js test-v2/http-unit/pushStatus.test.js test-v2/http-unit/renew.test.js test-v2/consumer-unit/handlerError.test.js test-v2/consumer-unit/nackScope.test.js test-v2/consumer-unit/stopOnAbort.test.js test-v2/kv-unit/kvWire.test.js test-v2/kv-unit/timerWire.test.js test-v2/kv-unit/txnWire.test.js test-v2/kv-unit/locksWire.test.js test-v2/buffer-unit/buffer.test.js test-v2/conflation-unit/conflationWire.test.js test-v2/autopilot-unit/autopilotWire.test.js test-v2/ephemeral-unit/ephemeralWire.test.js test-v2/ephemeral-unit/ephemeralBuffer.test.js test-v2/ephemeral-unit/durableSinkPin.test.js test-v2/runner-unit/fatalExit.test.js test-v2/pop-unit/popDefaults.test.js test-v2/admin-unit/removedRoutes.test.js test-v2/consumer-unit/supervision.test.js test-v2/wire-unit/pushBinary.test.js test-v2/wire-unit/popFrames.test.js test-v2/wire-unit/transactionFrames.test.js test-v2/reads-unit/offsetReads.test.js",
|
|
9
|
+
"test:unit": "node --test test-v2/streams-unit/configHash.test.js test-v2/streams-unit/operators.test.js test-v2/streams-unit/cycle.test.js test-v2/streams-unit/eventTime.test.js test-v2/streams-unit/ack.test.js test-v2/streams-unit/gate.test.js test-v2/streams-unit/claims.test.js test-v2/http-unit/retry429.test.js test-v2/http-unit/hostHeader.test.js test-v2/http-unit/pushStatus.test.js test-v2/http-unit/renew.test.js test-v2/consumer-unit/handlerError.test.js test-v2/consumer-unit/nackScope.test.js test-v2/consumer-unit/stopOnAbort.test.js test-v2/kv-unit/kvWire.test.js test-v2/kv-unit/timerWire.test.js test-v2/kv-unit/txnWire.test.js test-v2/kv-unit/locksWire.test.js test-v2/buffer-unit/buffer.test.js test-v2/conflation-unit/conflationWire.test.js test-v2/autopilot-unit/autopilotWire.test.js test-v2/ephemeral-unit/ephemeralWire.test.js test-v2/ephemeral-unit/ephemeralBuffer.test.js test-v2/ephemeral-unit/durableSinkPin.test.js test-v2/runner-unit/fatalExit.test.js test-v2/pop-unit/popDefaults.test.js test-v2/admin-unit/removedRoutes.test.js test-v2/consumer-unit/supervision.test.js test-v2/wire-unit/pushBinary.test.js test-v2/wire-unit/popFrames.test.js test-v2/wire-unit/transactionFrames.test.js test-v2/reads-unit/offsetReads.test.js test-v2/consumer-unit/transientPop.test.js",
|
|
10
10
|
"test:unit:e2e": "node --test test-v2/streams-unit/e2e.test.js",
|
|
11
11
|
"test:integration": "node test-v2/run.js human",
|
|
12
12
|
"test:streams": "node test-v2/run.js stream",
|