@relaybus/mongodb 0.3.0 → 0.3.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/dist/index.cjs +580 -465
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +581 -466
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { createUuidV7, ConfigurationError, EventValidationError } from '@relaybus/core';
|
|
2
|
-
import {
|
|
2
|
+
import { LaneLeaseTokenFormat, PositiveInteger, HandedOutLedger, OwnerSerializedClaims, CompositeKey, LeaseSettlement, DeliveryLeaseIdentity } from '@relaybus/core/internal';
|
|
3
3
|
|
|
4
|
-
// src/
|
|
5
|
-
|
|
6
|
-
// src/lanes.ts
|
|
4
|
+
// src/documents.ts
|
|
7
5
|
var LANE_COUNT = 256;
|
|
8
6
|
var FNV_OFFSET_BASIS = 2166136261;
|
|
9
7
|
var FNV_PRIME = 16777619;
|
|
@@ -15,16 +13,20 @@ function laneOf(key, eventId) {
|
|
|
15
13
|
}
|
|
16
14
|
return hash & LANE_COUNT - 1;
|
|
17
15
|
}
|
|
16
|
+
var LEASE_TOKENS = new LaneLeaseTokenFormat({
|
|
17
|
+
laneCount: LANE_COUNT,
|
|
18
|
+
epochDigits: 15,
|
|
19
|
+
claimsDigits: 15
|
|
20
|
+
});
|
|
18
21
|
function formatLeaseToken(laneId, { epoch, claims }) {
|
|
19
|
-
return
|
|
22
|
+
return LEASE_TOKENS.format(laneId, { epoch, claims });
|
|
20
23
|
}
|
|
21
|
-
var LEASE_TOKEN_PATTERN = /^(\d{1,3}):(\d{1,15}):(\d{1,15})$/;
|
|
22
24
|
function parseLeaseToken(token) {
|
|
23
|
-
const
|
|
24
|
-
if (
|
|
25
|
+
const parts = LEASE_TOKENS.parse(token);
|
|
26
|
+
if (parts === void 0) {
|
|
25
27
|
return void 0;
|
|
26
28
|
}
|
|
27
|
-
return { laneId:
|
|
29
|
+
return { laneId: parts.lane, epoch: Number(parts.epoch), claims: parts.claims };
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
// src/documents.ts
|
|
@@ -202,9 +204,6 @@ async function runInTransactionWith(state, {
|
|
|
202
204
|
throw error;
|
|
203
205
|
}
|
|
204
206
|
}
|
|
205
|
-
function leaseKey(deliveryId, leaseToken) {
|
|
206
|
-
return `${deliveryId}\0${leaseToken}`;
|
|
207
|
-
}
|
|
208
207
|
function assertInTransaction(session, operation) {
|
|
209
208
|
if (!session.inTransaction()) {
|
|
210
209
|
throw new ConfigurationError(
|
|
@@ -250,16 +249,8 @@ var MongoAppend = class {
|
|
|
250
249
|
const duplicateEventIds = [];
|
|
251
250
|
const newEvents = [];
|
|
252
251
|
const newDeliveries = [];
|
|
253
|
-
const knownHandlers = /* @__PURE__ */ new Map();
|
|
254
252
|
const addedHandlers = /* @__PURE__ */ new Map();
|
|
255
|
-
const orderingByEventId
|
|
256
|
-
for (const [eventId, stored] of storedEvents) {
|
|
257
|
-
orderingByEventId.set(eventId, {
|
|
258
|
-
key: stored.key ?? null,
|
|
259
|
-
sequence: stored.sequence ?? null
|
|
260
|
-
});
|
|
261
|
-
knownHandlers.set(eventId, new Set(stored.handlers));
|
|
262
|
-
}
|
|
253
|
+
const { orderingByEventId, knownHandlers } = indexStoredEvents(storedEvents);
|
|
263
254
|
for (const { event, deliveries } of entries) {
|
|
264
255
|
let ordering = orderingByEventId.get(event.id);
|
|
265
256
|
if (ordering === void 0) {
|
|
@@ -289,47 +280,23 @@ var MongoAppend = class {
|
|
|
289
280
|
const { session, plan, storedEvents, now } = input;
|
|
290
281
|
const { newEvents, newDeliveries, addedHandlers } = plan;
|
|
291
282
|
if (newEvents.length > 0) {
|
|
292
|
-
await this.#state.events.bulkWrite(
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
handlers: addedHandlers.get(event.id) ?? [],
|
|
297
|
-
createdAt: now
|
|
298
|
-
});
|
|
299
|
-
return {
|
|
300
|
-
updateOne: {
|
|
301
|
-
filter: { _id: document._id },
|
|
302
|
-
update: { $setOnInsert: document },
|
|
303
|
-
upsert: true
|
|
304
|
-
}
|
|
305
|
-
};
|
|
306
|
-
}),
|
|
307
|
-
{ session, ordered: true }
|
|
308
|
-
);
|
|
283
|
+
await this.#state.events.bulkWrite(newEventOperations({ newEvents, addedHandlers, now }), {
|
|
284
|
+
session,
|
|
285
|
+
ordered: true
|
|
286
|
+
});
|
|
309
287
|
}
|
|
310
288
|
const extendedEvents = [...addedHandlers].filter(([eventId]) => storedEvents.has(eventId));
|
|
311
289
|
if (extendedEvents.length > 0) {
|
|
312
|
-
await this.#state.events.bulkWrite(
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
update: { $inc: { revision: 1 }, $push: { handlers: { $each: handlers } } }
|
|
317
|
-
}
|
|
318
|
-
})),
|
|
319
|
-
{ session, ordered: true }
|
|
320
|
-
);
|
|
290
|
+
await this.#state.events.bulkWrite(extendedEventOperations(extendedEvents), {
|
|
291
|
+
session,
|
|
292
|
+
ordered: true
|
|
293
|
+
});
|
|
321
294
|
}
|
|
322
295
|
if (newDeliveries.length > 0) {
|
|
323
|
-
await this.#state.deliveries.bulkWrite(
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
update: { $setOnInsert: document },
|
|
328
|
-
upsert: true
|
|
329
|
-
}
|
|
330
|
-
})),
|
|
331
|
-
{ session, ordered: true }
|
|
332
|
-
);
|
|
296
|
+
await this.#state.deliveries.bulkWrite(newDeliveryOperations(newDeliveries), {
|
|
297
|
+
session,
|
|
298
|
+
ordered: true
|
|
299
|
+
});
|
|
333
300
|
}
|
|
334
301
|
}
|
|
335
302
|
/** Atomically increments the counter of `key`; conflicting appends of the key serialize here. */
|
|
@@ -342,8 +309,52 @@ var MongoAppend = class {
|
|
|
342
309
|
return counter.value;
|
|
343
310
|
}
|
|
344
311
|
};
|
|
345
|
-
|
|
346
|
-
|
|
312
|
+
function indexStoredEvents(storedEvents) {
|
|
313
|
+
const orderingByEventId = /* @__PURE__ */ new Map();
|
|
314
|
+
const knownHandlers = /* @__PURE__ */ new Map();
|
|
315
|
+
for (const [eventId, stored] of storedEvents) {
|
|
316
|
+
orderingByEventId.set(eventId, {
|
|
317
|
+
key: stored.key ?? null,
|
|
318
|
+
sequence: stored.sequence ?? null
|
|
319
|
+
});
|
|
320
|
+
knownHandlers.set(eventId, new Set(stored.handlers));
|
|
321
|
+
}
|
|
322
|
+
return { orderingByEventId, knownHandlers };
|
|
323
|
+
}
|
|
324
|
+
function newEventOperations(input) {
|
|
325
|
+
const { newEvents, addedHandlers, now } = input;
|
|
326
|
+
return newEvents.map(({ event, sequence }) => {
|
|
327
|
+
const document = toEventDocument(event, {
|
|
328
|
+
sequence,
|
|
329
|
+
handlers: addedHandlers.get(event.id) ?? [],
|
|
330
|
+
createdAt: now
|
|
331
|
+
});
|
|
332
|
+
return {
|
|
333
|
+
updateOne: {
|
|
334
|
+
filter: { _id: document._id },
|
|
335
|
+
update: { $setOnInsert: document },
|
|
336
|
+
upsert: true
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
function extendedEventOperations(extendedEvents) {
|
|
342
|
+
return extendedEvents.map(([eventId, handlers]) => ({
|
|
343
|
+
updateOne: {
|
|
344
|
+
filter: { _id: eventId },
|
|
345
|
+
update: { $inc: { revision: 1 }, $push: { handlers: { $each: handlers } } }
|
|
346
|
+
}
|
|
347
|
+
}));
|
|
348
|
+
}
|
|
349
|
+
function newDeliveryOperations(newDeliveries) {
|
|
350
|
+
return newDeliveries.map((document) => ({
|
|
351
|
+
updateOne: {
|
|
352
|
+
filter: { eventId: document.eventId, handler: document.handler },
|
|
353
|
+
update: { $setOnInsert: document },
|
|
354
|
+
upsert: true
|
|
355
|
+
}
|
|
356
|
+
}));
|
|
357
|
+
}
|
|
347
358
|
var ClaimScan = class {
|
|
348
359
|
#keys = /* @__PURE__ */ new Map();
|
|
349
360
|
/** Whether an unfinished delivery this scan knows of comes before the delivery with `sequence`. */
|
|
@@ -387,13 +398,104 @@ var ClaimScan = class {
|
|
|
387
398
|
}
|
|
388
399
|
};
|
|
389
400
|
function orderingIdentity(handler, key) {
|
|
390
|
-
return
|
|
401
|
+
return CompositeKey.of(handler, key);
|
|
391
402
|
}
|
|
403
|
+
var MongoLaneReconciler = class {
|
|
404
|
+
#state;
|
|
405
|
+
constructor(state) {
|
|
406
|
+
this.#state = state;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Takes every delivery this instance does not run out of the lanes `owner`
|
|
410
|
+
* holds on it: every listed delivery whose current claim (lane epoch and
|
|
411
|
+
* claim generation now) was not handed out by this instance, or was settled.
|
|
412
|
+
* One transaction, and the lanes are written first under their epoch and
|
|
413
|
+
* holder: a worker taking a lane over meanwhile conflicts with it, so its
|
|
414
|
+
* claim is never touched. The claim generation of every delivery taken out
|
|
415
|
+
* moves on, so no lease of those claims (a completion that failed and is
|
|
416
|
+
* retried later, say) can ever match a later claim of the same delivery.
|
|
417
|
+
* Registrations of this owner that are not the current claim of a listed
|
|
418
|
+
* delivery are dropped.
|
|
419
|
+
*/
|
|
420
|
+
async reconcile(owner) {
|
|
421
|
+
await runInTransaction(this.#state, async (session) => {
|
|
422
|
+
const held = await this.#state.lanes.find({ owner, holder: this.#state.instanceId }, { session }).toArray();
|
|
423
|
+
const claimsById = await this.#claimGenerations(
|
|
424
|
+
held.flatMap((lane) => lane.inFlight),
|
|
425
|
+
session
|
|
426
|
+
);
|
|
427
|
+
const { unclaimed, current } = this.#splitUnclaimed(held, claimsById);
|
|
428
|
+
this.#state.handedOut.forgetWhere(
|
|
429
|
+
(entry, identity) => entry.owner === owner && !current.has(identity)
|
|
430
|
+
);
|
|
431
|
+
if (held.length === 0) {
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
await this.#takeOut(unclaimed, session);
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
/** The claim generation now of every listed delivery that is still pending. */
|
|
438
|
+
async #claimGenerations(listed, session) {
|
|
439
|
+
return new Map(
|
|
440
|
+
(await this.#state.deliveries.find({ _id: { $in: listed }, status: "pending" }, { session, projection: { claims: 1 } }).toArray()).map((document) => [document._id, document.claims ?? 0])
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Per held lane, the listed deliveries this instance did not hand out under their current claim,
|
|
445
|
+
* and the identities of all current claims.
|
|
446
|
+
*/
|
|
447
|
+
#splitUnclaimed(held, claimsById) {
|
|
448
|
+
const current = /* @__PURE__ */ new Set();
|
|
449
|
+
const unclaimed = held.map((lane) => ({
|
|
450
|
+
lane,
|
|
451
|
+
ids: lane.inFlight.filter((id) => {
|
|
452
|
+
const key = DeliveryLeaseIdentity.of(
|
|
453
|
+
id,
|
|
454
|
+
formatLeaseToken(lane._id, { epoch: lane.epoch, claims: claimsById.get(id) ?? 0 })
|
|
455
|
+
);
|
|
456
|
+
current.add(key);
|
|
457
|
+
return !this.#state.handedOut.has(key);
|
|
458
|
+
})
|
|
459
|
+
}));
|
|
460
|
+
return { unclaimed, current };
|
|
461
|
+
}
|
|
462
|
+
/** Takes the unclaimed deliveries out of their lanes, then moves their claim generation on. */
|
|
463
|
+
async #takeOut(unclaimed, session) {
|
|
464
|
+
await this.#state.lanes.bulkWrite(
|
|
465
|
+
unclaimed.map(({ lane, ids }) => ({
|
|
466
|
+
updateOne: {
|
|
467
|
+
filter: { _id: lane._id, epoch: lane.epoch, holder: this.#state.instanceId },
|
|
468
|
+
update: { $pull: { inFlight: { $in: ids } }, $inc: { revision: 1 } }
|
|
469
|
+
}
|
|
470
|
+
})),
|
|
471
|
+
{ session, ordered: false }
|
|
472
|
+
);
|
|
473
|
+
await this.#state.deliveries.updateMany(
|
|
474
|
+
{ _id: { $in: unclaimed.flatMap((entry) => entry.ids) }, status: "pending" },
|
|
475
|
+
{ $inc: { claims: 1 } },
|
|
476
|
+
{ session }
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
};
|
|
392
480
|
|
|
393
481
|
// src/mongo-claim.ts
|
|
394
482
|
var PARK_BACKLOG_THRESHOLD = 32;
|
|
395
483
|
var INITIAL_LANES_PER_ROUND = 4;
|
|
396
484
|
var LANES_PER_CANDIDATE_QUERY = 128;
|
|
485
|
+
function toClaimedDeliveries(claimed, context) {
|
|
486
|
+
const { scope, events, request } = context;
|
|
487
|
+
return claimed.map((document) => ({
|
|
488
|
+
deliveryId: document._id,
|
|
489
|
+
leaseToken: formatLeaseToken(document.laneId, {
|
|
490
|
+
epoch: scope.get(document.laneId).epoch,
|
|
491
|
+
claims: document.claims ?? 0
|
|
492
|
+
}),
|
|
493
|
+
handler: document.handler,
|
|
494
|
+
attempt: document.attempts + 1,
|
|
495
|
+
leaseExpiresAt: request.leaseExpiresAt,
|
|
496
|
+
event: events.get(document.eventId)
|
|
497
|
+
}));
|
|
498
|
+
}
|
|
397
499
|
function scopeOf(held) {
|
|
398
500
|
return new Map(
|
|
399
501
|
held.map((lane) => [
|
|
@@ -407,33 +509,23 @@ function scopeOf(held) {
|
|
|
407
509
|
])
|
|
408
510
|
);
|
|
409
511
|
}
|
|
410
|
-
var MongoClaims = class {
|
|
512
|
+
var MongoClaims = class extends OwnerSerializedClaims {
|
|
411
513
|
#state;
|
|
412
514
|
#laneLeases;
|
|
413
515
|
#ordering;
|
|
414
516
|
#queries;
|
|
517
|
+
#reconciler;
|
|
415
518
|
constructor(state, collaborators) {
|
|
519
|
+
super();
|
|
416
520
|
this.#state = state;
|
|
417
521
|
this.#laneLeases = collaborators.laneLeases;
|
|
418
522
|
this.#ordering = collaborators.ordering;
|
|
419
523
|
this.#queries = collaborators.queries;
|
|
524
|
+
this.#reconciler = new MongoLaneReconciler(state);
|
|
420
525
|
}
|
|
421
|
-
/**
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
return [];
|
|
425
|
-
}
|
|
426
|
-
const owner = request.leaseOwner;
|
|
427
|
-
const previous = this.#state.claimsInProgress.get(owner) ?? Promise.resolve();
|
|
428
|
-
const claimed = previous.then(() => this.#claimReconciled(request));
|
|
429
|
-
const settled = Promise.allSettled([claimed]);
|
|
430
|
-
this.#state.claimsInProgress.set(owner, settled);
|
|
431
|
-
void settled.then(() => {
|
|
432
|
-
if (this.#state.claimsInProgress.get(owner) === settled) {
|
|
433
|
-
this.#state.claimsInProgress.delete(owner);
|
|
434
|
-
}
|
|
435
|
-
});
|
|
436
|
-
return claimed;
|
|
526
|
+
/** Mongo claims nothing for a limit of 0 or below (a limit that is not a number is tried). */
|
|
527
|
+
claimsNothing(request) {
|
|
528
|
+
return request.handlers.length === 0 || request.limit <= 0;
|
|
437
529
|
}
|
|
438
530
|
/**
|
|
439
531
|
* Runs one claim and remembers what it hands out. When the claim fails at any
|
|
@@ -443,17 +535,17 @@ var MongoClaims = class {
|
|
|
443
535
|
* handed out and seen settled, is taken out of them, so nothing stays listed
|
|
444
536
|
* as running that nobody runs.
|
|
445
537
|
*/
|
|
446
|
-
async
|
|
538
|
+
async claimInTurn(request) {
|
|
447
539
|
const owner = request.leaseOwner;
|
|
448
540
|
if (this.#state.unreconciledOwners.has(owner)) {
|
|
449
|
-
await this.#
|
|
541
|
+
await this.#reconciler.reconcile(owner);
|
|
450
542
|
this.#state.unreconciledOwners.delete(owner);
|
|
451
543
|
}
|
|
452
544
|
try {
|
|
453
545
|
const claimed = await this.#claimNow(request);
|
|
454
546
|
for (const delivery of claimed) {
|
|
455
547
|
const { laneId, epoch } = parseLeaseToken(delivery.leaseToken);
|
|
456
|
-
this.#state.handedOut.
|
|
548
|
+
this.#state.handedOut.register(delivery, {
|
|
457
549
|
deliveryId: delivery.deliveryId,
|
|
458
550
|
owner: request.leaseOwner,
|
|
459
551
|
laneId,
|
|
@@ -462,80 +554,15 @@ var MongoClaims = class {
|
|
|
462
554
|
}
|
|
463
555
|
return claimed;
|
|
464
556
|
} catch (error) {
|
|
465
|
-
const [reconciled] = await Promise.allSettled([this.#
|
|
557
|
+
const [reconciled] = await Promise.allSettled([this.#reconciler.reconcile(owner)]);
|
|
466
558
|
if (reconciled.status === "rejected") {
|
|
467
559
|
this.#state.unreconciledOwners.add(owner);
|
|
468
560
|
}
|
|
469
561
|
throw error;
|
|
470
562
|
}
|
|
471
563
|
}
|
|
472
|
-
/**
|
|
473
|
-
* Takes every delivery this instance does not run out of the lanes `owner`
|
|
474
|
-
* holds on it: every listed delivery whose current claim (lane epoch and
|
|
475
|
-
* claim generation now) was not handed out by this instance, or was settled.
|
|
476
|
-
* One transaction, and the lanes are written first under their epoch and
|
|
477
|
-
* holder: a worker taking a lane over meanwhile conflicts with it, so its
|
|
478
|
-
* claim is never touched. The claim generation of every delivery taken out
|
|
479
|
-
* moves on, so no lease of those claims (a completion that failed and is
|
|
480
|
-
* retried later, say) can ever match a later claim of the same delivery.
|
|
481
|
-
* Registrations of this owner that are not the current claim of a listed
|
|
482
|
-
* delivery are dropped.
|
|
483
|
-
*/
|
|
484
|
-
async #reconcileLanes(owner) {
|
|
485
|
-
await runInTransaction(this.#state, async (session) => {
|
|
486
|
-
const held = await this.#state.lanes.find({ owner, holder: this.#state.instanceId }, { session }).toArray();
|
|
487
|
-
const listed = held.flatMap((lane) => lane.inFlight);
|
|
488
|
-
const claimsById = new Map(
|
|
489
|
-
(await this.#state.deliveries.find(
|
|
490
|
-
{ _id: { $in: listed }, status: "pending" },
|
|
491
|
-
{ session, projection: { claims: 1 } }
|
|
492
|
-
).toArray()).map((document) => [document._id, document.claims ?? 0])
|
|
493
|
-
);
|
|
494
|
-
const current = /* @__PURE__ */ new Set();
|
|
495
|
-
const unclaimed = held.map((lane) => ({
|
|
496
|
-
lane,
|
|
497
|
-
ids: lane.inFlight.filter((id) => {
|
|
498
|
-
const key = leaseKey(
|
|
499
|
-
id,
|
|
500
|
-
formatLeaseToken(lane._id, { epoch: lane.epoch, claims: claimsById.get(id) ?? 0 })
|
|
501
|
-
);
|
|
502
|
-
current.add(key);
|
|
503
|
-
return !this.#state.handedOut.has(key);
|
|
504
|
-
})
|
|
505
|
-
}));
|
|
506
|
-
for (const [key, entry] of this.#state.handedOut) {
|
|
507
|
-
if (entry.owner === owner && !current.has(key)) {
|
|
508
|
-
this.#state.handedOut.delete(key);
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
if (held.length === 0) {
|
|
512
|
-
return;
|
|
513
|
-
}
|
|
514
|
-
await this.#state.lanes.bulkWrite(
|
|
515
|
-
unclaimed.map(({ lane, ids }) => ({
|
|
516
|
-
updateOne: {
|
|
517
|
-
filter: { _id: lane._id, epoch: lane.epoch, holder: this.#state.instanceId },
|
|
518
|
-
update: { $pull: { inFlight: { $in: ids } }, $inc: { revision: 1 } }
|
|
519
|
-
}
|
|
520
|
-
})),
|
|
521
|
-
{ session, ordered: false }
|
|
522
|
-
);
|
|
523
|
-
await this.#state.deliveries.updateMany(
|
|
524
|
-
{ _id: { $in: unclaimed.flatMap((entry) => entry.ids) }, status: "pending" },
|
|
525
|
-
{ $inc: { claims: 1 } },
|
|
526
|
-
{ session }
|
|
527
|
-
);
|
|
528
|
-
});
|
|
529
|
-
}
|
|
530
564
|
async #claimNow(request) {
|
|
531
|
-
const
|
|
532
|
-
this.#state.lanes.find({
|
|
533
|
-
owner: request.leaseOwner,
|
|
534
|
-
holder: this.#state.instanceId,
|
|
535
|
-
leaseExpiresAt: { $gt: request.now }
|
|
536
|
-
}).toArray(),
|
|
537
|
-
this.#ordering.unparkNextDeliveries(request.handlers)
|
|
538
|
-
]);
|
|
565
|
+
const held = await this.#readHeldLanes(request);
|
|
539
566
|
await this.#laneLeases.chargeLost(held);
|
|
540
567
|
const scope = scopeOf(held);
|
|
541
568
|
const running = this.#loadRunning(held);
|
|
@@ -555,17 +582,19 @@ var MongoClaims = class {
|
|
|
555
582
|
scope
|
|
556
583
|
);
|
|
557
584
|
await this.#ordering.parkWaiting(scan.waitingBacklogs(PARK_BACKLOG_THRESHOLD, handedBack));
|
|
558
|
-
return claimed
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
585
|
+
return toClaimedDeliveries(claimed, { scope, events, request });
|
|
586
|
+
}
|
|
587
|
+
/** The lanes the owner holds on this instance, read while the next deliveries are unparked. */
|
|
588
|
+
async #readHeldLanes(request) {
|
|
589
|
+
const [held] = await Promise.all([
|
|
590
|
+
this.#state.lanes.find({
|
|
591
|
+
owner: request.leaseOwner,
|
|
592
|
+
holder: this.#state.instanceId,
|
|
593
|
+
leaseExpiresAt: { $gt: request.now }
|
|
594
|
+
}).toArray(),
|
|
595
|
+
this.#ordering.unparkNextDeliveries(request.handlers)
|
|
596
|
+
]);
|
|
597
|
+
return held;
|
|
569
598
|
}
|
|
570
599
|
/** The deliveries still running in the held lanes, with their (handler, key) identities. */
|
|
571
600
|
#loadRunning(held) {
|
|
@@ -627,14 +656,12 @@ var MongoClaims = class {
|
|
|
627
656
|
#forgetSettled(held, { runningIds, owner }) {
|
|
628
657
|
const gone = new Set(
|
|
629
658
|
held.flatMap(
|
|
630
|
-
(lane) => lane.inFlight.filter((id) => !runningIds.has(id)).map((id) => `${
|
|
659
|
+
(lane) => lane.inFlight.filter((id) => !runningIds.has(id)).map((id) => CompositeKey.of(id, `${lane._id}:${lane.epoch}`))
|
|
631
660
|
)
|
|
632
661
|
);
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
}
|
|
637
|
-
}
|
|
662
|
+
this.#state.handedOut.forgetWhere(
|
|
663
|
+
(entry) => entry.owner === owner && gone.has(CompositeKey.of(entry.deliveryId, `${entry.laneId}:${entry.epoch}`))
|
|
664
|
+
);
|
|
638
665
|
}
|
|
639
666
|
/**
|
|
640
667
|
* Gives back the recorded keyed picks that a delivery requeued during the
|
|
@@ -696,20 +723,34 @@ var MongoClaims = class {
|
|
|
696
723
|
);
|
|
697
724
|
}
|
|
698
725
|
};
|
|
699
|
-
|
|
700
|
-
// src/mongo-completion.ts
|
|
701
726
|
var FINISHED_FIELDS = { dueAt: "", lane: "" };
|
|
702
|
-
var MongoCompletion = class {
|
|
727
|
+
var MongoCompletion = class extends LeaseSettlement {
|
|
703
728
|
#state;
|
|
704
729
|
constructor(state) {
|
|
730
|
+
super();
|
|
705
731
|
this.#state = state;
|
|
706
732
|
}
|
|
707
|
-
/** Implements {@link MongoEventStore.
|
|
708
|
-
async
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
return false;
|
|
733
|
+
/** Implements {@link MongoEventStore.complete}. */
|
|
734
|
+
async complete(lease, { now, session }) {
|
|
735
|
+
if (session !== void 0) {
|
|
736
|
+
assertInTransaction(session, "complete");
|
|
712
737
|
}
|
|
738
|
+
return (await this.#completeLeases([lease], { now, session }))[0];
|
|
739
|
+
}
|
|
740
|
+
/** Implements {@link MongoEventStore.completeMany}. */
|
|
741
|
+
completeMany(leases, now) {
|
|
742
|
+
return this.#completeLeases(leases, { now, session: void 0 });
|
|
743
|
+
}
|
|
744
|
+
/** A lease this store issued; any other token cannot be held. */
|
|
745
|
+
findHeld(lease) {
|
|
746
|
+
return parseLeaseToken(lease.leaseToken);
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Renews the lane lease of a running claim. One transaction: a stale lease must not extend the
|
|
750
|
+
* lane of a newer claim of the delivery. A claim ending or starting meanwhile writes the delivery
|
|
751
|
+
* or the lane, so the two conflict.
|
|
752
|
+
*/
|
|
753
|
+
writeExtension(held, { lease, leaseExpiresAt }) {
|
|
713
754
|
const deliveryId = String(lease.deliveryId);
|
|
714
755
|
return runInTransaction(this.#state, async (session) => {
|
|
715
756
|
const running = await this.#state.deliveries.findOne(ofClaim({ ...held, deliveryId }), {
|
|
@@ -727,41 +768,26 @@ var MongoCompletion = class {
|
|
|
727
768
|
return renewed.matchedCount === 1;
|
|
728
769
|
});
|
|
729
770
|
}
|
|
730
|
-
/**
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
}
|
|
741
|
-
/** Implements {@link MongoEventStore.reschedule}. */
|
|
742
|
-
async reschedule(lease, {
|
|
743
|
-
availableAt,
|
|
744
|
-
error,
|
|
745
|
-
now
|
|
746
|
-
}) {
|
|
747
|
-
return this.#leaveFlight(lease, {
|
|
748
|
-
$set: { availableAt, dueAt: availableAt, lastError: toErrorInfo(error), updatedAt: now },
|
|
749
|
-
$inc: { attempts: 1, claims: 1 }
|
|
771
|
+
/**
|
|
772
|
+
* Applies the change to the running delivery and takes it out of its lane's in-flight list, if
|
|
773
|
+
* the lane still has the lease's epoch.
|
|
774
|
+
*/
|
|
775
|
+
writeChange(held, { lease, change }) {
|
|
776
|
+
const deliveryId = String(lease.deliveryId);
|
|
777
|
+
return this.#leaveFlightNow(held, {
|
|
778
|
+
deliveryId,
|
|
779
|
+
ofThisClaim: ofClaim({ ...held, deliveryId }),
|
|
780
|
+
update: updateFor(change)
|
|
750
781
|
});
|
|
751
782
|
}
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
return this.#leaveFlight(lease, {
|
|
755
|
-
$set: { availableAt: now, dueAt: now, updatedAt: now },
|
|
756
|
-
$inc: { claims: 1 }
|
|
757
|
-
});
|
|
783
|
+
forgetHandedOut(lease) {
|
|
784
|
+
this.#forget(lease);
|
|
758
785
|
}
|
|
759
|
-
/**
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
$inc: { attempts: 1, claims: 1 }
|
|
786
|
+
/** Forgets a lease as handed out: its claim ended. */
|
|
787
|
+
#forget(lease) {
|
|
788
|
+
this.#state.handedOut.forget({
|
|
789
|
+
deliveryId: String(lease.deliveryId),
|
|
790
|
+
leaseToken: String(lease.leaseToken)
|
|
765
791
|
});
|
|
766
792
|
}
|
|
767
793
|
/**
|
|
@@ -773,7 +799,7 @@ var MongoCompletion = class {
|
|
|
773
799
|
async #settle(leases, operation) {
|
|
774
800
|
const result = await operation();
|
|
775
801
|
for (const lease of leases) {
|
|
776
|
-
this.#
|
|
802
|
+
this.#forget(lease);
|
|
777
803
|
}
|
|
778
804
|
return result;
|
|
779
805
|
}
|
|
@@ -900,23 +926,7 @@ var MongoCompletion = class {
|
|
|
900
926
|
session === void 0 ? {} : { session }
|
|
901
927
|
);
|
|
902
928
|
}
|
|
903
|
-
/**
|
|
904
|
-
* Applies `update` to a running delivery and takes it out of its lane's
|
|
905
|
-
* in-flight list, if the lane still has the lease's epoch. One transaction:
|
|
906
|
-
* the lane write fences the change against a takeover.
|
|
907
|
-
*/
|
|
908
|
-
async #leaveFlight(lease, update) {
|
|
909
|
-
const held = parseLeaseToken(lease.leaseToken);
|
|
910
|
-
if (held === void 0) {
|
|
911
|
-
return false;
|
|
912
|
-
}
|
|
913
|
-
const deliveryId = String(lease.deliveryId);
|
|
914
|
-
const ofThisClaim = ofClaim({ ...held, deliveryId });
|
|
915
|
-
return this.#settle(
|
|
916
|
-
[lease],
|
|
917
|
-
() => this.#leaveFlightNow(held, { deliveryId, ofThisClaim, update })
|
|
918
|
-
);
|
|
919
|
-
}
|
|
929
|
+
/** One transaction: the lane write fences the change against a takeover. */
|
|
920
930
|
async #leaveFlightNow(held, {
|
|
921
931
|
deliveryId,
|
|
922
932
|
ofThisClaim,
|
|
@@ -957,7 +967,28 @@ function ofClaim(lease) {
|
|
|
957
967
|
return { _id: lease.deliveryId, status: "pending", claims: atCount(lease.claims) };
|
|
958
968
|
}
|
|
959
969
|
function leaseIdentity(lease) {
|
|
960
|
-
return
|
|
970
|
+
return DeliveryLeaseIdentity.of(
|
|
971
|
+
lease.deliveryId,
|
|
972
|
+
formatLeaseToken(lease.laneId, { epoch: lease.epoch, claims: lease.claims })
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
function updateFor(change) {
|
|
976
|
+
if (change.kind === "reschedule") {
|
|
977
|
+
const { availableAt, error, now } = change;
|
|
978
|
+
return {
|
|
979
|
+
$set: { availableAt, dueAt: availableAt, lastError: toErrorInfo(error), updatedAt: now },
|
|
980
|
+
$inc: { attempts: 1, claims: 1 }
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
if (change.kind === "release") {
|
|
984
|
+
const { now } = change;
|
|
985
|
+
return { $set: { availableAt: now, dueAt: now, updatedAt: now }, $inc: { claims: 1 } };
|
|
986
|
+
}
|
|
987
|
+
return {
|
|
988
|
+
$set: { status: "dead", lastError: toErrorInfo(change.error), updatedAt: change.now },
|
|
989
|
+
$unset: FINISHED_FIELDS,
|
|
990
|
+
$inc: { attempts: 1, claims: 1 }
|
|
991
|
+
};
|
|
961
992
|
}
|
|
962
993
|
|
|
963
994
|
// src/mongo-lane-leases.ts
|
|
@@ -1106,10 +1137,36 @@ var MongoLaneLeases = class {
|
|
|
1106
1137
|
request
|
|
1107
1138
|
}) {
|
|
1108
1139
|
const picksByLane = /* @__PURE__ */ new Map();
|
|
1109
|
-
const handedBack = /* @__PURE__ */ new Set();
|
|
1110
1140
|
for (const document of picked) {
|
|
1111
1141
|
picksByLane.set(document.laneId, [...picksByLane.get(document.laneId) ?? [], document._id]);
|
|
1112
1142
|
}
|
|
1143
|
+
const { operations, handedBack } = this.#planLaneRecords(scope, {
|
|
1144
|
+
picksByLane,
|
|
1145
|
+
runningIds,
|
|
1146
|
+
request
|
|
1147
|
+
});
|
|
1148
|
+
if (operations.length === 0) {
|
|
1149
|
+
return { recorded: /* @__PURE__ */ new Set(), handedBack };
|
|
1150
|
+
}
|
|
1151
|
+
const result = await this.#state.lanes.bulkWrite(operations, { ordered: false });
|
|
1152
|
+
if (result.matchedCount === operations.length) {
|
|
1153
|
+
return { recorded: new Set(picksByLane.keys()), handedBack };
|
|
1154
|
+
}
|
|
1155
|
+
return {
|
|
1156
|
+
recorded: await this.#readRecorded(scope, { laneIds: [...picksByLane.keys()], request }),
|
|
1157
|
+
handedBack
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
/**
|
|
1161
|
+
* One conditional write per lane of the claim: a picked lane records its picks, a lane that
|
|
1162
|
+
* yielded nothing and runs nothing is freed (and reported as handed back).
|
|
1163
|
+
*/
|
|
1164
|
+
#planLaneRecords(scope, {
|
|
1165
|
+
picksByLane,
|
|
1166
|
+
runningIds,
|
|
1167
|
+
request
|
|
1168
|
+
}) {
|
|
1169
|
+
const handedBack = /* @__PURE__ */ new Set();
|
|
1113
1170
|
const operations = [];
|
|
1114
1171
|
for (const lane of scope.values()) {
|
|
1115
1172
|
const gone = lane.listed.filter((id) => !runningIds.has(id));
|
|
@@ -1125,17 +1182,7 @@ var MongoLaneLeases = class {
|
|
|
1125
1182
|
handedBack.add(lane.laneId);
|
|
1126
1183
|
}
|
|
1127
1184
|
}
|
|
1128
|
-
|
|
1129
|
-
return { recorded: /* @__PURE__ */ new Set(), handedBack };
|
|
1130
|
-
}
|
|
1131
|
-
const result = await this.#state.lanes.bulkWrite(operations, { ordered: false });
|
|
1132
|
-
if (result.matchedCount === operations.length) {
|
|
1133
|
-
return { recorded: new Set(picksByLane.keys()), handedBack };
|
|
1134
|
-
}
|
|
1135
|
-
return {
|
|
1136
|
-
recorded: await this.#readRecorded(scope, { laneIds: [...picksByLane.keys()], request }),
|
|
1137
|
-
handedBack
|
|
1138
|
-
};
|
|
1185
|
+
return { operations, handedBack };
|
|
1139
1186
|
}
|
|
1140
1187
|
/** The lanes of `laneIds` whose record of this claim applied, read back after a partial bulk write. */
|
|
1141
1188
|
async #readRecorded(scope, { laneIds, request }) {
|
|
@@ -1253,30 +1300,11 @@ var MongoOrdering = class {
|
|
|
1253
1300
|
async pickCandidates(input) {
|
|
1254
1301
|
const { lanes, request, scan, running, picked } = input;
|
|
1255
1302
|
const pageSize = Math.max((request.limit - picked.length) * 2, MINIMUM_CANDIDATE_BATCH_SIZE);
|
|
1256
|
-
const filter =
|
|
1257
|
-
laneId: { $in: lanes.map((lane) => lane.laneId) },
|
|
1258
|
-
handler: { $in: [...request.handlers] },
|
|
1259
|
-
lane: "ready",
|
|
1260
|
-
dueAt: { $lte: request.now },
|
|
1261
|
-
_id: { $nin: lanes.flatMap((lane) => lane.listed) }
|
|
1262
|
-
};
|
|
1303
|
+
const filter = candidateFilter(lanes, request);
|
|
1263
1304
|
let after;
|
|
1264
1305
|
for (; ; ) {
|
|
1265
1306
|
const [page, { keys }] = await Promise.all([
|
|
1266
|
-
this.#state.deliveries.find(
|
|
1267
|
-
after === void 0 ? filter : {
|
|
1268
|
-
$and: [
|
|
1269
|
-
filter,
|
|
1270
|
-
{
|
|
1271
|
-
$or: [
|
|
1272
|
-
{ dueAt: { $gt: after.dueAt } },
|
|
1273
|
-
{ dueAt: after.dueAt, _id: { $gt: after._id } }
|
|
1274
|
-
]
|
|
1275
|
-
}
|
|
1276
|
-
]
|
|
1277
|
-
},
|
|
1278
|
-
{ sort: { dueAt: 1, _id: 1 }, limit: pageSize }
|
|
1279
|
-
).toArray(),
|
|
1307
|
+
this.#state.deliveries.find(pageAfter(filter, after), { sort: { dueAt: 1, _id: 1 }, limit: pageSize }).toArray(),
|
|
1280
1308
|
running
|
|
1281
1309
|
]);
|
|
1282
1310
|
if (await this.#pickFromPage(page, { request, scan, picked, keys })) {
|
|
@@ -1392,17 +1420,7 @@ var MongoOrdering = class {
|
|
|
1392
1420
|
if (anyParked === null) {
|
|
1393
1421
|
return;
|
|
1394
1422
|
}
|
|
1395
|
-
const lowest =
|
|
1396
|
-
[...new Set(handlers)].map(
|
|
1397
|
-
(handler) => this.#state.deliveries.aggregate([
|
|
1398
|
-
{ $match: { handler, lane: "parked" } },
|
|
1399
|
-
{ $sort: { handler: 1, key: 1, sequence: 1 } },
|
|
1400
|
-
{
|
|
1401
|
-
$group: { _id: "$key", id: { $first: "$_id" }, sequence: { $first: "$sequence" } }
|
|
1402
|
-
}
|
|
1403
|
-
]).toArray().then((groups) => groups.map((group) => ({ ...group, handler })))
|
|
1404
|
-
)
|
|
1405
|
-
)).flat();
|
|
1423
|
+
const lowest = await this.#lowestParkedPerKey(handlers);
|
|
1406
1424
|
const next = await Promise.all(
|
|
1407
1425
|
lowest.map(
|
|
1408
1426
|
async (parked) => await this.#findUnfinishedPredecessor(parked.handler, {
|
|
@@ -1419,7 +1437,46 @@ var MongoOrdering = class {
|
|
|
1419
1437
|
);
|
|
1420
1438
|
}
|
|
1421
1439
|
}
|
|
1440
|
+
/**
|
|
1441
|
+
* The lowest parked delivery of every parked key of `handlers`, one aggregation per handler in
|
|
1442
|
+
* parallel.
|
|
1443
|
+
*/
|
|
1444
|
+
async #lowestParkedPerKey(handlers) {
|
|
1445
|
+
return (await Promise.all(
|
|
1446
|
+
[...new Set(handlers)].map(
|
|
1447
|
+
(handler) => this.#state.deliveries.aggregate([
|
|
1448
|
+
{ $match: { handler, lane: "parked" } },
|
|
1449
|
+
{ $sort: { handler: 1, key: 1, sequence: 1 } },
|
|
1450
|
+
{
|
|
1451
|
+
$group: { _id: "$key", id: { $first: "$_id" }, sequence: { $first: "$sequence" } }
|
|
1452
|
+
}
|
|
1453
|
+
]).toArray().then((groups) => groups.map((group) => ({ ...group, handler })))
|
|
1454
|
+
)
|
|
1455
|
+
)).flat();
|
|
1456
|
+
}
|
|
1422
1457
|
};
|
|
1458
|
+
function candidateFilter(lanes, request) {
|
|
1459
|
+
return {
|
|
1460
|
+
laneId: { $in: lanes.map((lane) => lane.laneId) },
|
|
1461
|
+
handler: { $in: [...request.handlers] },
|
|
1462
|
+
lane: "ready",
|
|
1463
|
+
dueAt: { $lte: request.now },
|
|
1464
|
+
_id: { $nin: lanes.flatMap((lane) => lane.listed) }
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
function pageAfter(filter, after) {
|
|
1468
|
+
return after === void 0 ? filter : {
|
|
1469
|
+
$and: [
|
|
1470
|
+
filter,
|
|
1471
|
+
{
|
|
1472
|
+
$or: [
|
|
1473
|
+
{ dueAt: { $gt: after.dueAt } },
|
|
1474
|
+
{ dueAt: after.dueAt, _id: { $gt: after._id } }
|
|
1475
|
+
]
|
|
1476
|
+
}
|
|
1477
|
+
]
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1423
1480
|
function isClaimable(candidate, { scan, blockedKeys }) {
|
|
1424
1481
|
const { key, sequence } = candidate;
|
|
1425
1482
|
if (key === void 0) {
|
|
@@ -1446,6 +1503,21 @@ var MongoPurge = class {
|
|
|
1446
1503
|
/** Implements {@link MongoEventStore.purge}. */
|
|
1447
1504
|
async purge(request) {
|
|
1448
1505
|
const total = { deletedDeliveries: 0, deletedEvents: 0 };
|
|
1506
|
+
const steps = this.#purgeSteps(request);
|
|
1507
|
+
const { completedBefore, deadBefore } = request;
|
|
1508
|
+
for (const step of steps) {
|
|
1509
|
+
const result = await step();
|
|
1510
|
+
total.deletedDeliveries += result.deletedDeliveries;
|
|
1511
|
+
total.deletedEvents += result.deletedEvents;
|
|
1512
|
+
}
|
|
1513
|
+
const orphanCutoff = [completedBefore, deadBefore].filter((cutoff) => cutoff !== void 0).sort((first, second) => first.getTime() - second.getTime())[0];
|
|
1514
|
+
if (orphanCutoff !== void 0) {
|
|
1515
|
+
total.deletedEvents += await this.#purgeOrphanEvents(orphanCutoff, completedBefore);
|
|
1516
|
+
}
|
|
1517
|
+
return total;
|
|
1518
|
+
}
|
|
1519
|
+
/** The purges the request asks for, in the order they run. */
|
|
1520
|
+
#purgeSteps(request) {
|
|
1449
1521
|
const steps = [];
|
|
1450
1522
|
const { completedBefore, deadBefore } = request;
|
|
1451
1523
|
if (completedBefore !== void 0) {
|
|
@@ -1472,16 +1544,7 @@ var MongoPurge = class {
|
|
|
1472
1544
|
})
|
|
1473
1545
|
);
|
|
1474
1546
|
}
|
|
1475
|
-
|
|
1476
|
-
const result = await step();
|
|
1477
|
-
total.deletedDeliveries += result.deletedDeliveries;
|
|
1478
|
-
total.deletedEvents += result.deletedEvents;
|
|
1479
|
-
}
|
|
1480
|
-
const orphanCutoff = [completedBefore, deadBefore].filter((cutoff) => cutoff !== void 0).sort((first, second) => first.getTime() - second.getTime())[0];
|
|
1481
|
-
if (orphanCutoff !== void 0) {
|
|
1482
|
-
total.deletedEvents += await this.#purgeOrphanEvents(orphanCutoff, completedBefore);
|
|
1483
|
-
}
|
|
1484
|
-
return total;
|
|
1547
|
+
return steps;
|
|
1485
1548
|
}
|
|
1486
1549
|
/**
|
|
1487
1550
|
* Deletes batches of matching documents (deliveries, or completion history
|
|
@@ -1499,35 +1562,10 @@ var MongoPurge = class {
|
|
|
1499
1562
|
let deletedDeliveries = 0;
|
|
1500
1563
|
let deletedEvents = 0;
|
|
1501
1564
|
for (; ; ) {
|
|
1502
|
-
const step = await runInTransaction(
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
}
|
|
1507
|
-
const eventIds = [...new Set(batch.map((document) => document.eventId))];
|
|
1508
|
-
const deleted = await collection.deleteMany(
|
|
1509
|
-
{ _id: { $in: batch.map((document) => document._id) } },
|
|
1510
|
-
{ session }
|
|
1511
|
-
);
|
|
1512
|
-
const stillReferenced = await this.#referencedEventIds(eventIds, session);
|
|
1513
|
-
const events = await this.#state.events.deleteMany(
|
|
1514
|
-
{
|
|
1515
|
-
_id: { $in: eventIds.filter((eventId) => !stillReferenced.has(eventId)) },
|
|
1516
|
-
// An event a delivery of which completed stays as deduplication record until
|
|
1517
|
-
// `completedBefore` passes its last completion, whatever this batch purged.
|
|
1518
|
-
$or: [
|
|
1519
|
-
{ completedAt: { $exists: false } },
|
|
1520
|
-
...completedBefore === void 0 ? [] : [{ completedAt: { $lt: completedBefore } }]
|
|
1521
|
-
]
|
|
1522
|
-
},
|
|
1523
|
-
{ session }
|
|
1524
|
-
);
|
|
1525
|
-
return {
|
|
1526
|
-
batchSize: batch.length,
|
|
1527
|
-
deletedDeliveries: deleted.deletedCount,
|
|
1528
|
-
deletedEvents: events.deletedCount
|
|
1529
|
-
};
|
|
1530
|
-
});
|
|
1565
|
+
const step = await runInTransaction(
|
|
1566
|
+
this.#state,
|
|
1567
|
+
(session) => this.#purgeBatch(collection, { filter, completedBefore, session })
|
|
1568
|
+
);
|
|
1531
1569
|
deletedDeliveries += step.deletedDeliveries;
|
|
1532
1570
|
deletedEvents += step.deletedEvents;
|
|
1533
1571
|
if (step.batchSize < this.#state.purgeBatchSize) {
|
|
@@ -1535,6 +1573,40 @@ var MongoPurge = class {
|
|
|
1535
1573
|
}
|
|
1536
1574
|
}
|
|
1537
1575
|
}
|
|
1576
|
+
/** One batch of {@link MongoPurge.#purgeMatching}, inside the caller's transaction. */
|
|
1577
|
+
async #purgeBatch(collection, {
|
|
1578
|
+
filter,
|
|
1579
|
+
completedBefore,
|
|
1580
|
+
session
|
|
1581
|
+
}) {
|
|
1582
|
+
const batch = await collection.find(filter, { session, projection: { eventId: 1 }, limit: this.#state.purgeBatchSize }).toArray();
|
|
1583
|
+
if (batch.length === 0) {
|
|
1584
|
+
return { batchSize: 0, deletedDeliveries: 0, deletedEvents: 0 };
|
|
1585
|
+
}
|
|
1586
|
+
const eventIds = [...new Set(batch.map((document) => document.eventId))];
|
|
1587
|
+
const deleted = await collection.deleteMany(
|
|
1588
|
+
{ _id: { $in: batch.map((document) => document._id) } },
|
|
1589
|
+
{ session }
|
|
1590
|
+
);
|
|
1591
|
+
const stillReferenced = await this.#referencedEventIds(eventIds, session);
|
|
1592
|
+
const events = await this.#state.events.deleteMany(
|
|
1593
|
+
{
|
|
1594
|
+
_id: { $in: eventIds.filter((eventId) => !stillReferenced.has(eventId)) },
|
|
1595
|
+
// An event a delivery of which completed stays as deduplication record until
|
|
1596
|
+
// `completedBefore` passes its last completion, whatever this batch purged.
|
|
1597
|
+
$or: [
|
|
1598
|
+
{ completedAt: { $exists: false } },
|
|
1599
|
+
...completedBefore === void 0 ? [] : [{ completedAt: { $lt: completedBefore } }]
|
|
1600
|
+
]
|
|
1601
|
+
},
|
|
1602
|
+
{ session }
|
|
1603
|
+
);
|
|
1604
|
+
return {
|
|
1605
|
+
batchSize: batch.length,
|
|
1606
|
+
deletedDeliveries: deleted.deletedCount,
|
|
1607
|
+
deletedEvents: events.deletedCount
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1538
1610
|
/** A view of a collection that purge deletes from: deliveries or completion history records. */
|
|
1539
1611
|
#purgeable(name) {
|
|
1540
1612
|
return this.#state.database.collection(name, {
|
|
@@ -1568,33 +1640,12 @@ var MongoPurge = class {
|
|
|
1568
1640
|
for (; ; ) {
|
|
1569
1641
|
const step = await runInTransaction(this.#state, async (session) => {
|
|
1570
1642
|
const orphans = await this.#state.events.aggregate(
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
// Completed: by the time of its last completion, and only under the completed cutoff.
|
|
1578
|
-
...completedBefore === void 0 ? [] : [{ completedAt: { $lt: completedBefore } }]
|
|
1579
|
-
]
|
|
1580
|
-
}
|
|
1581
|
-
},
|
|
1582
|
-
{ $sort: { createdAt: 1, _id: 1 } },
|
|
1583
|
-
...references.flatMap((from) => [
|
|
1584
|
-
{
|
|
1585
|
-
$lookup: {
|
|
1586
|
-
from,
|
|
1587
|
-
localField: "_id",
|
|
1588
|
-
foreignField: "eventId",
|
|
1589
|
-
pipeline: [{ $limit: 1 }, { $project: { _id: 1 } }],
|
|
1590
|
-
as: "references"
|
|
1591
|
-
}
|
|
1592
|
-
},
|
|
1593
|
-
{ $match: { references: { $size: 0 } } }
|
|
1594
|
-
]),
|
|
1595
|
-
{ $limit: this.#state.purgeBatchSize },
|
|
1596
|
-
{ $project: { _id: 1 } }
|
|
1597
|
-
],
|
|
1643
|
+
orphanPipeline({
|
|
1644
|
+
cutoff,
|
|
1645
|
+
completedBefore,
|
|
1646
|
+
references,
|
|
1647
|
+
limit: this.#state.purgeBatchSize
|
|
1648
|
+
}),
|
|
1598
1649
|
{ session }
|
|
1599
1650
|
).toArray();
|
|
1600
1651
|
const deleted = await this.#state.events.deleteMany(
|
|
@@ -1610,6 +1661,36 @@ var MongoPurge = class {
|
|
|
1610
1661
|
}
|
|
1611
1662
|
}
|
|
1612
1663
|
};
|
|
1664
|
+
function orphanPipeline(input) {
|
|
1665
|
+
const { cutoff, completedBefore, references, limit } = input;
|
|
1666
|
+
return [
|
|
1667
|
+
{
|
|
1668
|
+
$match: {
|
|
1669
|
+
$or: [
|
|
1670
|
+
// Never completed (no subscribers, or every delivery dead): by age.
|
|
1671
|
+
{ completedAt: { $exists: false }, createdAt: { $lt: cutoff } },
|
|
1672
|
+
// Completed: by the time of its last completion, and only under the completed cutoff.
|
|
1673
|
+
...completedBefore === void 0 ? [] : [{ completedAt: { $lt: completedBefore } }]
|
|
1674
|
+
]
|
|
1675
|
+
}
|
|
1676
|
+
},
|
|
1677
|
+
{ $sort: { createdAt: 1, _id: 1 } },
|
|
1678
|
+
...references.flatMap((from) => [
|
|
1679
|
+
{
|
|
1680
|
+
$lookup: {
|
|
1681
|
+
from,
|
|
1682
|
+
localField: "_id",
|
|
1683
|
+
foreignField: "eventId",
|
|
1684
|
+
pipeline: [{ $limit: 1 }, { $project: { _id: 1 } }],
|
|
1685
|
+
as: "references"
|
|
1686
|
+
}
|
|
1687
|
+
},
|
|
1688
|
+
{ $match: { references: { $size: 0 } } }
|
|
1689
|
+
]),
|
|
1690
|
+
{ $limit: limit },
|
|
1691
|
+
{ $project: { _id: 1 } }
|
|
1692
|
+
];
|
|
1693
|
+
}
|
|
1613
1694
|
|
|
1614
1695
|
// src/mongo-queries.ts
|
|
1615
1696
|
var DEFAULT_LIST_LIMIT = 100;
|
|
@@ -1625,14 +1706,7 @@ var MongoQueries = class {
|
|
|
1625
1706
|
const limit = query.limit ?? DEFAULT_LIST_LIMIT;
|
|
1626
1707
|
const lanes = await this.#state.lanes.find({ "inFlight.0": { $exists: true } }, { projection: { epoch: 0 } }).toArray();
|
|
1627
1708
|
const listed = lanes.flatMap((lane) => lane.inFlight);
|
|
1628
|
-
const
|
|
1629
|
-
["processing", { status: "pending", _id: { $in: listed } }],
|
|
1630
|
-
["pending", { status: "pending", _id: { $nin: listed } }]
|
|
1631
|
-
]);
|
|
1632
|
-
const filter = {
|
|
1633
|
-
...common,
|
|
1634
|
-
...status === void 0 ? {} : derivedStatuses.get(status) ?? { status }
|
|
1635
|
-
};
|
|
1709
|
+
const filter = { ...common, ...statusFilter(status, listed) };
|
|
1636
1710
|
const sort = { createdAt: 1, _id: 1 };
|
|
1637
1711
|
const documents = await this.#state.deliveries.find(filter, { sort, limit }).toArray();
|
|
1638
1712
|
const history = this.#state.completionHistory && (status === void 0 || status === "completed") ? await this.#state.completions.find(common, { sort, limit }).toArray() : [];
|
|
@@ -1640,23 +1714,7 @@ var MongoQueries = class {
|
|
|
1640
1714
|
...documents.map((document) => document.eventId),
|
|
1641
1715
|
...history.map((document) => document.eventId)
|
|
1642
1716
|
]);
|
|
1643
|
-
|
|
1644
|
-
const records = [
|
|
1645
|
-
...documents.map((document) => {
|
|
1646
|
-
const lane = laneById.get(document.laneId);
|
|
1647
|
-
const runs = document.status === "pending" && lane?.inFlight.includes(document._id) === true;
|
|
1648
|
-
return toDeliveryRecord(document, {
|
|
1649
|
-
event: events.get(document.eventId),
|
|
1650
|
-
runningIn: runs ? lane : void 0
|
|
1651
|
-
});
|
|
1652
|
-
}),
|
|
1653
|
-
...history.map(
|
|
1654
|
-
(document) => toCompletedDeliveryRecord(document, events.get(document.eventId))
|
|
1655
|
-
)
|
|
1656
|
-
];
|
|
1657
|
-
return records.sort(
|
|
1658
|
-
(left, right) => left.createdAt.getTime() - right.createdAt.getTime() || compareText(left.id, right.id)
|
|
1659
|
-
).slice(0, limit);
|
|
1717
|
+
return toSortedRecords({ documents, history, events, lanes }).slice(0, limit);
|
|
1660
1718
|
}
|
|
1661
1719
|
/** Implements {@link MongoEventStore.requeue}. */
|
|
1662
1720
|
async requeue(deliveryIds, now) {
|
|
@@ -1711,7 +1769,7 @@ var MongoQueries = class {
|
|
|
1711
1769
|
return [];
|
|
1712
1770
|
}
|
|
1713
1771
|
return this.#state.deliveries.find(
|
|
1714
|
-
{ _id: { $in:
|
|
1772
|
+
{ _id: { $in: listed }, status: "pending" },
|
|
1715
1773
|
{ projection: { handler: 1, key: 1, laneId: 1 } }
|
|
1716
1774
|
).toArray();
|
|
1717
1775
|
}
|
|
@@ -1720,6 +1778,36 @@ var MongoQueries = class {
|
|
|
1720
1778
|
return new Map(documents.map((document) => [document._id, toEnvelope(document)]));
|
|
1721
1779
|
}
|
|
1722
1780
|
};
|
|
1781
|
+
function statusFilter(status, listed) {
|
|
1782
|
+
if (status === void 0) {
|
|
1783
|
+
return {};
|
|
1784
|
+
}
|
|
1785
|
+
const derivedStatuses = /* @__PURE__ */ new Map([
|
|
1786
|
+
["processing", { status: "pending", _id: { $in: listed } }],
|
|
1787
|
+
["pending", { status: "pending", _id: { $nin: listed } }]
|
|
1788
|
+
]);
|
|
1789
|
+
return derivedStatuses.get(status) ?? { status };
|
|
1790
|
+
}
|
|
1791
|
+
function toSortedRecords(input) {
|
|
1792
|
+
const { documents, history, events, lanes } = input;
|
|
1793
|
+
const laneById = new Map(lanes.map((lane) => [lane._id, lane]));
|
|
1794
|
+
const records = [
|
|
1795
|
+
...documents.map((document) => {
|
|
1796
|
+
const lane = laneById.get(document.laneId);
|
|
1797
|
+
const runs = document.status === "pending" && lane?.inFlight.includes(document._id) === true;
|
|
1798
|
+
return toDeliveryRecord(document, {
|
|
1799
|
+
event: events.get(document.eventId),
|
|
1800
|
+
runningIn: runs ? lane : void 0
|
|
1801
|
+
});
|
|
1802
|
+
}),
|
|
1803
|
+
...history.map(
|
|
1804
|
+
(document) => toCompletedDeliveryRecord(document, events.get(document.eventId))
|
|
1805
|
+
)
|
|
1806
|
+
];
|
|
1807
|
+
return records.sort(
|
|
1808
|
+
(left, right) => left.createdAt.getTime() - right.createdAt.getTime() || compareText(left.id, right.id)
|
|
1809
|
+
);
|
|
1810
|
+
}
|
|
1723
1811
|
function compareText(left, right) {
|
|
1724
1812
|
return Number(left > right) - Number(left < right);
|
|
1725
1813
|
}
|
|
@@ -1769,54 +1857,10 @@ var MongoSchema = class {
|
|
|
1769
1857
|
}
|
|
1770
1858
|
/** Creates the indexes of every collection; existing indexes are left as they are. */
|
|
1771
1859
|
async #createIndexes() {
|
|
1772
|
-
await this.#state.deliveries.createIndexes(
|
|
1773
|
-
|
|
1774
|
-
{ key: { eventId: 1, handler: 1 }, name: "event_handler", unique: true },
|
|
1775
|
-
// Claim: due work of a lane in due order. Holds ready deliveries only. The handler is
|
|
1776
|
-
// last, so a query over many lanes and handlers stays a merge of one scan per lane.
|
|
1777
|
-
{ key: CLAIM_INDEX_KEY, name: "claim", partialFilterExpression: { lane: "ready" } },
|
|
1778
|
-
// Ordering: unfinished predecessors of a keyed delivery, parked or not.
|
|
1779
|
-
{
|
|
1780
|
-
key: { handler: 1, key: 1, sequence: 1 },
|
|
1781
|
-
name: "ordering",
|
|
1782
|
-
partialFilterExpression: { dueAt: { $exists: true }, sequence: { $exists: true } }
|
|
1783
|
-
},
|
|
1784
|
-
// Parked deliveries: the lowest parked delivery of each key, to move it back once it is next.
|
|
1785
|
-
{
|
|
1786
|
-
key: { handler: 1, key: 1, sequence: 1 },
|
|
1787
|
-
name: "parked",
|
|
1788
|
-
partialFilterExpression: { lane: "parked" }
|
|
1789
|
-
},
|
|
1790
|
-
// Statistics: oldest due delivery across handlers.
|
|
1791
|
-
{ key: { dueAt: 1 }, name: "due", partialFilterExpression: { dueAt: { $exists: true } } },
|
|
1792
|
-
// Purge of completed deliveries left by 0.1.0.
|
|
1793
|
-
{
|
|
1794
|
-
key: { completedAt: 1 },
|
|
1795
|
-
name: "completed",
|
|
1796
|
-
partialFilterExpression: { completedAt: { $exists: true } }
|
|
1797
|
-
},
|
|
1798
|
-
// Listing and status counts.
|
|
1799
|
-
{ key: { status: 1, createdAt: 1 }, name: "status_created" },
|
|
1800
|
-
// Purge of dead deliveries.
|
|
1801
|
-
{ key: { status: 1, updatedAt: 1 }, name: "status_updated" },
|
|
1802
|
-
{ key: { handler: 1, createdAt: 1 }, name: "handler_created" },
|
|
1803
|
-
{ key: { eventType: 1, createdAt: 1 }, name: "type_created" }
|
|
1804
|
-
]);
|
|
1805
|
-
await this.#state.events.createIndexes([
|
|
1806
|
-
{ key: { createdAt: 1, _id: 1 }, name: "created" },
|
|
1807
|
-
// Purge of events whose deliveries all completed: they are kept until `completedBefore` passes this.
|
|
1808
|
-
{
|
|
1809
|
-
key: { completedAt: 1, _id: 1 },
|
|
1810
|
-
name: "completed",
|
|
1811
|
-
partialFilterExpression: { completedAt: { $exists: true } }
|
|
1812
|
-
}
|
|
1813
|
-
]);
|
|
1860
|
+
await this.#state.deliveries.createIndexes(deliveryIndexes());
|
|
1861
|
+
await this.#state.events.createIndexes(eventIndexes());
|
|
1814
1862
|
if (this.#state.completionHistory) {
|
|
1815
|
-
await this.#state.completions.createIndexes(
|
|
1816
|
-
{ key: { completedAt: 1, _id: 1 }, name: "completed" },
|
|
1817
|
-
{ key: { eventId: 1 }, name: "event" },
|
|
1818
|
-
{ key: { createdAt: 1, _id: 1 }, name: "created" }
|
|
1819
|
-
]);
|
|
1863
|
+
await this.#state.completions.createIndexes(completionIndexes());
|
|
1820
1864
|
}
|
|
1821
1865
|
}
|
|
1822
1866
|
async #createCollectionIfMissing(name) {
|
|
@@ -1879,36 +1923,29 @@ var MongoSchema = class {
|
|
|
1879
1923
|
{ projection: { occurredAt: 1 }, batchSize: UPGRADE_BATCH_SIZE }
|
|
1880
1924
|
),
|
|
1881
1925
|
async (events) => {
|
|
1882
|
-
const known =
|
|
1883
|
-
(await this.#state.deliveries.aggregate([
|
|
1884
|
-
{ $match: { eventId: { $in: events.map((event) => event._id) } } },
|
|
1885
|
-
{
|
|
1886
|
-
$group: {
|
|
1887
|
-
_id: "$eventId",
|
|
1888
|
-
handlers: { $addToSet: "$handler" },
|
|
1889
|
-
createdAt: { $min: "$createdAt" }
|
|
1890
|
-
}
|
|
1891
|
-
}
|
|
1892
|
-
]).toArray()).map((group) => [group._id, group])
|
|
1893
|
-
);
|
|
1926
|
+
const known = await this.#legacyEventGroups(events.map((event) => event._id));
|
|
1894
1927
|
await this.#state.events.bulkWrite(
|
|
1895
|
-
events.map((event) =>
|
|
1896
|
-
const group = known.get(event._id);
|
|
1897
|
-
return {
|
|
1898
|
-
updateOne: {
|
|
1899
|
-
// Only an event nobody converted yet: another process may have finished and subscribed a handler.
|
|
1900
|
-
filter: { _id: event._id, handlers: { $exists: false } },
|
|
1901
|
-
update: {
|
|
1902
|
-
$set: group === void 0 ? { handlers: [], createdAt: event.occurredAt } : { handlers: group.handlers, createdAt: group.createdAt }
|
|
1903
|
-
}
|
|
1904
|
-
}
|
|
1905
|
-
};
|
|
1906
|
-
}),
|
|
1928
|
+
events.map((event) => convertEvent(event, known.get(event._id))),
|
|
1907
1929
|
{ ordered: false }
|
|
1908
1930
|
);
|
|
1909
1931
|
}
|
|
1910
1932
|
);
|
|
1911
1933
|
}
|
|
1934
|
+
/** The handlers and first creation time of the deliveries of each of `eventIds`, by event id. */
|
|
1935
|
+
async #legacyEventGroups(eventIds) {
|
|
1936
|
+
return new Map(
|
|
1937
|
+
(await this.#state.deliveries.aggregate([
|
|
1938
|
+
{ $match: { eventId: { $in: [...eventIds] } } },
|
|
1939
|
+
{
|
|
1940
|
+
$group: {
|
|
1941
|
+
_id: "$eventId",
|
|
1942
|
+
handlers: { $addToSet: "$handler" },
|
|
1943
|
+
createdAt: { $min: "$createdAt" }
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
]).toArray()).map((group) => [group._id, group])
|
|
1947
|
+
);
|
|
1948
|
+
}
|
|
1912
1949
|
/** Creates the lane documents that do not exist yet. */
|
|
1913
1950
|
async #seedLanes() {
|
|
1914
1951
|
await this.#state.lanes.bulkWrite(
|
|
@@ -1949,6 +1986,70 @@ var MongoSchema = class {
|
|
|
1949
1986
|
}
|
|
1950
1987
|
}
|
|
1951
1988
|
};
|
|
1989
|
+
function deliveryIndexes() {
|
|
1990
|
+
return [
|
|
1991
|
+
// Idempotent appends and lookups by event.
|
|
1992
|
+
{ key: { eventId: 1, handler: 1 }, name: "event_handler", unique: true },
|
|
1993
|
+
// Claim: due work of a lane in due order. Holds ready deliveries only. The handler is
|
|
1994
|
+
// last, so a query over many lanes and handlers stays a merge of one scan per lane.
|
|
1995
|
+
{ key: CLAIM_INDEX_KEY, name: "claim", partialFilterExpression: { lane: "ready" } },
|
|
1996
|
+
// Ordering: unfinished predecessors of a keyed delivery, parked or not.
|
|
1997
|
+
{
|
|
1998
|
+
key: { handler: 1, key: 1, sequence: 1 },
|
|
1999
|
+
name: "ordering",
|
|
2000
|
+
partialFilterExpression: { dueAt: { $exists: true }, sequence: { $exists: true } }
|
|
2001
|
+
},
|
|
2002
|
+
// Parked deliveries: the lowest parked delivery of each key, to move it back once it is next.
|
|
2003
|
+
{
|
|
2004
|
+
key: { handler: 1, key: 1, sequence: 1 },
|
|
2005
|
+
name: "parked",
|
|
2006
|
+
partialFilterExpression: { lane: "parked" }
|
|
2007
|
+
},
|
|
2008
|
+
// Statistics: oldest due delivery across handlers.
|
|
2009
|
+
{ key: { dueAt: 1 }, name: "due", partialFilterExpression: { dueAt: { $exists: true } } },
|
|
2010
|
+
// Purge of completed deliveries left by 0.1.0.
|
|
2011
|
+
{
|
|
2012
|
+
key: { completedAt: 1 },
|
|
2013
|
+
name: "completed",
|
|
2014
|
+
partialFilterExpression: { completedAt: { $exists: true } }
|
|
2015
|
+
},
|
|
2016
|
+
// Listing and status counts.
|
|
2017
|
+
{ key: { status: 1, createdAt: 1 }, name: "status_created" },
|
|
2018
|
+
// Purge of dead deliveries.
|
|
2019
|
+
{ key: { status: 1, updatedAt: 1 }, name: "status_updated" },
|
|
2020
|
+
{ key: { handler: 1, createdAt: 1 }, name: "handler_created" },
|
|
2021
|
+
{ key: { eventType: 1, createdAt: 1 }, name: "type_created" }
|
|
2022
|
+
];
|
|
2023
|
+
}
|
|
2024
|
+
function eventIndexes() {
|
|
2025
|
+
return [
|
|
2026
|
+
{ key: { createdAt: 1, _id: 1 }, name: "created" },
|
|
2027
|
+
// Purge of events whose deliveries all completed: they are kept until `completedBefore` passes this.
|
|
2028
|
+
{
|
|
2029
|
+
key: { completedAt: 1, _id: 1 },
|
|
2030
|
+
name: "completed",
|
|
2031
|
+
partialFilterExpression: { completedAt: { $exists: true } }
|
|
2032
|
+
}
|
|
2033
|
+
];
|
|
2034
|
+
}
|
|
2035
|
+
function completionIndexes() {
|
|
2036
|
+
return [
|
|
2037
|
+
{ key: { completedAt: 1, _id: 1 }, name: "completed" },
|
|
2038
|
+
{ key: { eventId: 1 }, name: "event" },
|
|
2039
|
+
{ key: { createdAt: 1, _id: 1 }, name: "created" }
|
|
2040
|
+
];
|
|
2041
|
+
}
|
|
2042
|
+
function convertEvent(event, group) {
|
|
2043
|
+
return {
|
|
2044
|
+
updateOne: {
|
|
2045
|
+
// Only an event nobody converted yet: another process may have finished and subscribed a handler.
|
|
2046
|
+
filter: { _id: event._id, handlers: { $exists: false } },
|
|
2047
|
+
update: {
|
|
2048
|
+
$set: group === void 0 ? { handlers: [], createdAt: event.occurredAt } : { handlers: group.handlers, createdAt: group.createdAt }
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
};
|
|
2052
|
+
}
|
|
1952
2053
|
function isObsoleteIndex(index) {
|
|
1953
2054
|
return OBSOLETE_INDEXES.includes(index.name) || index.name === "claim" && Object.keys(index.key).join() !== Object.keys(CLAIM_INDEX_KEY).join();
|
|
1954
2055
|
}
|
|
@@ -1985,13 +2086,48 @@ function assertDatabaseName(name) {
|
|
|
1985
2086
|
}
|
|
1986
2087
|
}
|
|
1987
2088
|
|
|
1988
|
-
// src/mongo-
|
|
2089
|
+
// src/mongo-store-factory.ts
|
|
1989
2090
|
var DEFAULT_TRANSACTION_OPTIONS = {
|
|
1990
2091
|
readConcern: { level: "snapshot" },
|
|
1991
2092
|
writeConcern: { w: "majority" },
|
|
1992
2093
|
readPreference: "primary"
|
|
1993
2094
|
};
|
|
1994
2095
|
var DEFAULT_PURGE_BATCH_SIZE = 500;
|
|
2096
|
+
function createMongoStoreState(options) {
|
|
2097
|
+
const prefix = options.collectionPrefix ?? "relaybus_";
|
|
2098
|
+
assertCollectionPrefix(prefix);
|
|
2099
|
+
if (options.database !== void 0) {
|
|
2100
|
+
assertDatabaseName(options.database);
|
|
2101
|
+
}
|
|
2102
|
+
const purgeBatchSize = PositiveInteger.of(
|
|
2103
|
+
options.purgeBatchSize ?? DEFAULT_PURGE_BATCH_SIZE,
|
|
2104
|
+
"purgeBatchSize"
|
|
2105
|
+
);
|
|
2106
|
+
const database = options.client.db(options.database);
|
|
2107
|
+
const collectionOptions = {
|
|
2108
|
+
readPreference: "primary",
|
|
2109
|
+
writeConcern: { w: "majority" }
|
|
2110
|
+
};
|
|
2111
|
+
const state = {
|
|
2112
|
+
client: options.client,
|
|
2113
|
+
database,
|
|
2114
|
+
events: database.collection(`${prefix}events`, collectionOptions),
|
|
2115
|
+
deliveries: database.collection(`${prefix}deliveries`, collectionOptions),
|
|
2116
|
+
keySequences: database.collection(`${prefix}key_sequences`, collectionOptions),
|
|
2117
|
+
lanes: database.collection(`${prefix}lanes`, collectionOptions),
|
|
2118
|
+
completions: database.collection(`${prefix}completed_deliveries`, collectionOptions),
|
|
2119
|
+
completionHistory: options.completionHistory ?? false,
|
|
2120
|
+
transactionOptions: options.transactionOptions ?? DEFAULT_TRANSACTION_OPTIONS,
|
|
2121
|
+
purgeBatchSize,
|
|
2122
|
+
logger: options.logger ?? console,
|
|
2123
|
+
instanceId: createUuidV7(),
|
|
2124
|
+
handedOut: new HandedOutLedger(),
|
|
2125
|
+
unreconciledOwners: /* @__PURE__ */ new Set()
|
|
2126
|
+
};
|
|
2127
|
+
return state;
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
// src/mongo-event-store.ts
|
|
1995
2131
|
var MongoEventStore = class {
|
|
1996
2132
|
#state;
|
|
1997
2133
|
#schema;
|
|
@@ -2002,37 +2138,7 @@ var MongoEventStore = class {
|
|
|
2002
2138
|
#queries;
|
|
2003
2139
|
#purger;
|
|
2004
2140
|
constructor(options) {
|
|
2005
|
-
const
|
|
2006
|
-
assertCollectionPrefix(prefix);
|
|
2007
|
-
if (options.database !== void 0) {
|
|
2008
|
-
assertDatabaseName(options.database);
|
|
2009
|
-
}
|
|
2010
|
-
const purgeBatchSize = assertPositiveInteger(
|
|
2011
|
-
options.purgeBatchSize ?? DEFAULT_PURGE_BATCH_SIZE,
|
|
2012
|
-
"purgeBatchSize"
|
|
2013
|
-
);
|
|
2014
|
-
const database = options.client.db(options.database);
|
|
2015
|
-
const collectionOptions = {
|
|
2016
|
-
readPreference: "primary",
|
|
2017
|
-
writeConcern: { w: "majority" }
|
|
2018
|
-
};
|
|
2019
|
-
const state = {
|
|
2020
|
-
client: options.client,
|
|
2021
|
-
database,
|
|
2022
|
-
events: database.collection(`${prefix}events`, collectionOptions),
|
|
2023
|
-
deliveries: database.collection(`${prefix}deliveries`, collectionOptions),
|
|
2024
|
-
keySequences: database.collection(`${prefix}key_sequences`, collectionOptions),
|
|
2025
|
-
lanes: database.collection(`${prefix}lanes`, collectionOptions),
|
|
2026
|
-
completions: database.collection(`${prefix}completed_deliveries`, collectionOptions),
|
|
2027
|
-
completionHistory: options.completionHistory ?? false,
|
|
2028
|
-
transactionOptions: options.transactionOptions ?? DEFAULT_TRANSACTION_OPTIONS,
|
|
2029
|
-
purgeBatchSize,
|
|
2030
|
-
logger: options.logger ?? console,
|
|
2031
|
-
instanceId: createUuidV7(),
|
|
2032
|
-
claimsInProgress: /* @__PURE__ */ new Map(),
|
|
2033
|
-
handedOut: /* @__PURE__ */ new Map(),
|
|
2034
|
-
unreconciledOwners: /* @__PURE__ */ new Set()
|
|
2035
|
-
};
|
|
2141
|
+
const state = createMongoStoreState(options);
|
|
2036
2142
|
this.#state = state;
|
|
2037
2143
|
const queries = new MongoQueries(state);
|
|
2038
2144
|
const ordering = new MongoOrdering(state);
|
|
@@ -2115,7 +2221,7 @@ var MongoEventStore = class {
|
|
|
2115
2221
|
*/
|
|
2116
2222
|
// eslint-disable-next-line relaybus/max-two-params -- EventStore port signature
|
|
2117
2223
|
extendLease(lease, leaseExpiresAt, _now) {
|
|
2118
|
-
return this.#completion.extendLease(lease, leaseExpiresAt);
|
|
2224
|
+
return Promise.resolve(this.#completion.extendLease({ lease, leaseExpiresAt }));
|
|
2119
2225
|
}
|
|
2120
2226
|
/**
|
|
2121
2227
|
* Deletes the delivery (and records it in the completion history, when kept).
|
|
@@ -2139,16 +2245,25 @@ var MongoEventStore = class {
|
|
|
2139
2245
|
/** Returns a running delivery to pending, due at `availableAt`, and counts the attempt that ended. */
|
|
2140
2246
|
// eslint-disable-next-line relaybus/max-two-params -- EventStore port signature
|
|
2141
2247
|
reschedule(lease, availableAt, error, now) {
|
|
2142
|
-
return
|
|
2248
|
+
return Promise.resolve(
|
|
2249
|
+
this.#completion.changeClaim({
|
|
2250
|
+
lease,
|
|
2251
|
+
change: { kind: "reschedule", availableAt, error, now }
|
|
2252
|
+
})
|
|
2253
|
+
);
|
|
2143
2254
|
}
|
|
2144
2255
|
/** Gives a running delivery back without counting the attempt: pending, due `now`. */
|
|
2145
2256
|
release(lease, now) {
|
|
2146
|
-
return
|
|
2257
|
+
return Promise.resolve(
|
|
2258
|
+
this.#completion.changeClaim({ lease, change: { kind: "release", now } })
|
|
2259
|
+
);
|
|
2147
2260
|
}
|
|
2148
2261
|
/** Moves a running delivery to dead and counts the attempt that ended. */
|
|
2149
2262
|
// eslint-disable-next-line relaybus/max-two-params -- EventStore port signature
|
|
2150
2263
|
bury(lease, error, now) {
|
|
2151
|
-
return
|
|
2264
|
+
return Promise.resolve(
|
|
2265
|
+
this.#completion.changeClaim({ lease, change: { kind: "bury", error, now } })
|
|
2266
|
+
);
|
|
2152
2267
|
}
|
|
2153
2268
|
/**
|
|
2154
2269
|
* Frees every lane `leaseOwner` holds that runs no delivery, so other workers
|