@relaybus/testing 0.0.0 → 0.2.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/README.md +39 -3
- package/dist/index.cjs +470 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +470 -25
- package/dist/index.js.map +1 -1
- package/package.json +18 -6
package/dist/index.js
CHANGED
|
@@ -32,6 +32,7 @@ function createDelivery(handler, availableAt = at(0)) {
|
|
|
32
32
|
return { id: createUuidV7(), handler, availableAt };
|
|
33
33
|
}
|
|
34
34
|
function eventStoreContract(name, options) {
|
|
35
|
+
const keepsCompleted = options.keepsCompletedRecords ?? false;
|
|
35
36
|
describe(`${name} satisfies the EventStore contract`, () => {
|
|
36
37
|
let store;
|
|
37
38
|
beforeEach(async () => {
|
|
@@ -43,6 +44,16 @@ function eventStoreContract(name, options) {
|
|
|
43
44
|
async function append(entries, now = at(0)) {
|
|
44
45
|
return store.transaction((transaction) => store.append(transaction, entries, now));
|
|
45
46
|
}
|
|
47
|
+
async function completeMany(target, leases, now) {
|
|
48
|
+
if (target.completeMany !== void 0) {
|
|
49
|
+
return target.completeMany(leases, now);
|
|
50
|
+
}
|
|
51
|
+
const outcomes = [];
|
|
52
|
+
for (const lease of leases) {
|
|
53
|
+
outcomes.push(await target.complete(lease, now));
|
|
54
|
+
}
|
|
55
|
+
return outcomes;
|
|
56
|
+
}
|
|
46
57
|
async function claim(handlers, now = at(0), limit = 100, owner = "worker-1") {
|
|
47
58
|
return store.claim({
|
|
48
59
|
handlers,
|
|
@@ -219,12 +230,19 @@ function eventStoreContract(name, options) {
|
|
|
219
230
|
}));
|
|
220
231
|
await append(entries);
|
|
221
232
|
const batches = await Promise.all(
|
|
222
|
-
Array.from(
|
|
223
|
-
|
|
224
|
-
(
|
|
225
|
-
|
|
233
|
+
Array.from({ length: 8 }, async (_, index) => {
|
|
234
|
+
const own = [];
|
|
235
|
+
for (; ; ) {
|
|
236
|
+
const batch = await claim(["handler-a"], at(0), 10, `worker-${index}`);
|
|
237
|
+
if (batch.length === 0) {
|
|
238
|
+
return own;
|
|
239
|
+
}
|
|
240
|
+
own.push(...batch);
|
|
241
|
+
}
|
|
242
|
+
})
|
|
226
243
|
);
|
|
227
|
-
const
|
|
244
|
+
const leftovers = await claim(["handler-a"], at(0), 100, "worker-late");
|
|
245
|
+
const claimedIds = [...batches.flat(), ...leftovers].map((delivery) => delivery.deliveryId);
|
|
228
246
|
expect(new Set(claimedIds).size).toBe(claimedIds.length);
|
|
229
247
|
expect(claimedIds).toHaveLength(40);
|
|
230
248
|
});
|
|
@@ -243,6 +261,10 @@ function eventStoreContract(name, options) {
|
|
|
243
261
|
false
|
|
244
262
|
);
|
|
245
263
|
expect(await store.complete(current, at(LEASE_MS + 2))).toBe(true);
|
|
264
|
+
if (!keepsCompleted) {
|
|
265
|
+
expect(await store.listDeliveries({})).toEqual([]);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
246
268
|
const [record] = await store.listDeliveries({});
|
|
247
269
|
expect(record).toMatchObject({
|
|
248
270
|
status: "completed",
|
|
@@ -283,7 +305,11 @@ function eventStoreContract(name, options) {
|
|
|
283
305
|
await store.transaction(async (transaction) => {
|
|
284
306
|
expect(await store.complete(delivery, at(2), transaction)).toBe(true);
|
|
285
307
|
});
|
|
286
|
-
|
|
308
|
+
if (keepsCompleted) {
|
|
309
|
+
expect((await store.listDeliveries({}))[0].status).toBe("completed");
|
|
310
|
+
} else {
|
|
311
|
+
expect(await store.listDeliveries({})).toEqual([]);
|
|
312
|
+
}
|
|
287
313
|
});
|
|
288
314
|
it("refuses a transactional completion with a lost lease", async () => {
|
|
289
315
|
await append([{ event: createEnvelope(), deliveries: [createDelivery("handler-a")] }]);
|
|
@@ -457,7 +483,11 @@ function eventStoreContract(name, options) {
|
|
|
457
483
|
});
|
|
458
484
|
expect((await store.statistics(at(LEASE_MS))).oldestDueAt).toEqual(at(200));
|
|
459
485
|
await store.complete(processing, at(1));
|
|
460
|
-
expect(await store.statistics(at(0))).toMatchObject({
|
|
486
|
+
expect(await store.statistics(at(0))).toMatchObject({
|
|
487
|
+
completed: keepsCompleted ? 1 : 0,
|
|
488
|
+
processing: 0,
|
|
489
|
+
oldestDueAt: null
|
|
490
|
+
});
|
|
461
491
|
});
|
|
462
492
|
it("counts an expired lease as due", async () => {
|
|
463
493
|
await append([
|
|
@@ -469,7 +499,7 @@ function eventStoreContract(name, options) {
|
|
|
469
499
|
});
|
|
470
500
|
});
|
|
471
501
|
describe("purge", () => {
|
|
472
|
-
|
|
502
|
+
async function appendDoneFailedShared() {
|
|
473
503
|
const done = createEnvelope();
|
|
474
504
|
const failed = createEnvelope();
|
|
475
505
|
const shared = createEnvelope();
|
|
@@ -484,23 +514,52 @@ function eventStoreContract(name, options) {
|
|
|
484
514
|
await store.bury(failedDelivery, { name: "Error", message: "x" }, at(10));
|
|
485
515
|
const [sharedDelivery] = await claim(["handler-c"]);
|
|
486
516
|
await store.complete(sharedDelivery, at(10));
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
deletedDeliveries:
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
517
|
+
return { shared };
|
|
518
|
+
}
|
|
519
|
+
it.runIf(keepsCompleted)(
|
|
520
|
+
"deletes old completed and dead deliveries and their orphaned events",
|
|
521
|
+
async () => {
|
|
522
|
+
const { shared } = await appendDoneFailedShared();
|
|
523
|
+
expect(await store.purge({})).toEqual({ deletedDeliveries: 0, deletedEvents: 0 });
|
|
524
|
+
expect(await store.purge({ completedBefore: at(10) })).toEqual({
|
|
525
|
+
deletedDeliveries: 0,
|
|
526
|
+
deletedEvents: 0
|
|
527
|
+
});
|
|
528
|
+
expect(await store.purge({ completedBefore: at(11) })).toEqual({
|
|
529
|
+
deletedDeliveries: 2,
|
|
530
|
+
deletedEvents: 1
|
|
531
|
+
});
|
|
532
|
+
expect(await store.purge({ deadBefore: at(11) })).toEqual({
|
|
533
|
+
deletedDeliveries: 1,
|
|
534
|
+
deletedEvents: 1
|
|
535
|
+
});
|
|
536
|
+
const remaining = await store.listDeliveries({});
|
|
537
|
+
expect(remaining.map((record) => record.handler)).toEqual(["handler-d"]);
|
|
538
|
+
expect(remaining[0].event.id).toBe(shared.id);
|
|
539
|
+
}
|
|
540
|
+
);
|
|
541
|
+
it.runIf(!keepsCompleted)(
|
|
542
|
+
"deletes old dead deliveries and the events no delivery references any more",
|
|
543
|
+
async () => {
|
|
544
|
+
const { shared } = await appendDoneFailedShared();
|
|
545
|
+
expect(await store.purge({})).toEqual({ deletedDeliveries: 0, deletedEvents: 0 });
|
|
546
|
+
expect(await store.purge({ completedBefore: at(0) })).toEqual({
|
|
547
|
+
deletedDeliveries: 0,
|
|
548
|
+
deletedEvents: 0
|
|
549
|
+
});
|
|
550
|
+
expect(await store.purge({ completedBefore: at(11) })).toEqual({
|
|
551
|
+
deletedDeliveries: 0,
|
|
552
|
+
deletedEvents: 1
|
|
553
|
+
});
|
|
554
|
+
expect(await store.purge({ deadBefore: at(11) })).toEqual({
|
|
555
|
+
deletedDeliveries: 1,
|
|
556
|
+
deletedEvents: 1
|
|
557
|
+
});
|
|
558
|
+
const remaining = await store.listDeliveries({});
|
|
559
|
+
expect(remaining.map((record) => record.handler)).toEqual(["handler-d"]);
|
|
560
|
+
expect(remaining[0].event.id).toBe(shared.id);
|
|
561
|
+
}
|
|
562
|
+
);
|
|
504
563
|
it("lets a purged event id be appended again", async () => {
|
|
505
564
|
const event = createEnvelope();
|
|
506
565
|
await append([{ event, deliveries: [createDelivery("handler-a")] }]);
|
|
@@ -766,6 +825,366 @@ function eventStoreContract(name, options) {
|
|
|
766
825
|
expect(listed.event.headers).toEqual({ a: "1" });
|
|
767
826
|
});
|
|
768
827
|
});
|
|
828
|
+
describe("completion", () => {
|
|
829
|
+
it("removes a completed delivery from every listing", async () => {
|
|
830
|
+
const event = createEnvelope();
|
|
831
|
+
await append([
|
|
832
|
+
{ event, deliveries: [createDelivery("handler-a"), createDelivery("handler-b")] }
|
|
833
|
+
]);
|
|
834
|
+
const [delivery] = await claim(["handler-a"]);
|
|
835
|
+
expect(await store.complete(delivery, at(5))).toBe(true);
|
|
836
|
+
const records = await store.listDeliveries({ eventId: event.id });
|
|
837
|
+
if (keepsCompleted) {
|
|
838
|
+
expect(records.map((record) => [record.handler, record.status]).sort()).toEqual([
|
|
839
|
+
["handler-a", "completed"],
|
|
840
|
+
["handler-b", "pending"]
|
|
841
|
+
]);
|
|
842
|
+
const [completed] = await store.listDeliveries({ status: "completed" });
|
|
843
|
+
expect(completed).toMatchObject({ handler: "handler-a", attempts: 1 });
|
|
844
|
+
expect(completed.completedAt).toEqual(at(5));
|
|
845
|
+
} else {
|
|
846
|
+
expect(records.map((record) => [record.handler, record.status])).toEqual([
|
|
847
|
+
["handler-b", "pending"]
|
|
848
|
+
]);
|
|
849
|
+
expect(await store.listDeliveries({ status: "completed" })).toEqual([]);
|
|
850
|
+
}
|
|
851
|
+
expect(await claim(["handler-a"], at(LEASE_MS * 10))).toEqual([]);
|
|
852
|
+
expect(await store.statistics(at(0))).toMatchObject({
|
|
853
|
+
pending: 1,
|
|
854
|
+
processing: 0,
|
|
855
|
+
completed: keepsCompleted ? 1 : 0
|
|
856
|
+
});
|
|
857
|
+
});
|
|
858
|
+
it("does not create the delivery of a handler that already completed again", async () => {
|
|
859
|
+
const event = createEnvelope();
|
|
860
|
+
await append([{ event, deliveries: [createDelivery("handler-a")] }]);
|
|
861
|
+
const [delivery] = await claim(["handler-a"]);
|
|
862
|
+
await store.complete(delivery, at(1));
|
|
863
|
+
const result = await append([
|
|
864
|
+
{ event, deliveries: [createDelivery("handler-a"), createDelivery("handler-b")] }
|
|
865
|
+
]);
|
|
866
|
+
expect(result).toEqual({
|
|
867
|
+
appendedEventIds: [],
|
|
868
|
+
duplicateEventIds: [event.id],
|
|
869
|
+
appendedDeliveryCount: 1
|
|
870
|
+
});
|
|
871
|
+
expect(await claim(["handler-a"], at(LEASE_MS * 10))).toEqual([]);
|
|
872
|
+
expect(await claim(["handler-b"], at(LEASE_MS * 10))).toHaveLength(1);
|
|
873
|
+
const again = await append([{ event, deliveries: [createDelivery("handler-a")] }]);
|
|
874
|
+
expect(again.appendedDeliveryCount).toBe(0);
|
|
875
|
+
});
|
|
876
|
+
it("completes many leases at once and reports each one", async () => {
|
|
877
|
+
await append(
|
|
878
|
+
Array.from({ length: 5 }, () => ({
|
|
879
|
+
event: createEnvelope(),
|
|
880
|
+
deliveries: [createDelivery("handler-a")]
|
|
881
|
+
}))
|
|
882
|
+
);
|
|
883
|
+
const claimed = await claim(["handler-a"]);
|
|
884
|
+
expect(claimed).toHaveLength(5);
|
|
885
|
+
expect(await completeMany(store, [], at(1))).toEqual([]);
|
|
886
|
+
expect(await completeMany(store, claimed.slice(0, 3), at(1))).toEqual([true, true, true]);
|
|
887
|
+
expect(await store.statistics(at(2))).toMatchObject({ processing: 2, pending: 0 });
|
|
888
|
+
expect(await completeMany(store, claimed, at(2))).toEqual([
|
|
889
|
+
false,
|
|
890
|
+
false,
|
|
891
|
+
false,
|
|
892
|
+
true,
|
|
893
|
+
true
|
|
894
|
+
]);
|
|
895
|
+
expect(await store.statistics(at(3))).toMatchObject({ processing: 0, pending: 0 });
|
|
896
|
+
});
|
|
897
|
+
it("completes a lease listed twice only once", async () => {
|
|
898
|
+
await append([{ event: createEnvelope(), deliveries: [createDelivery("handler-a")] }]);
|
|
899
|
+
const [delivery] = await claim(["handler-a"]);
|
|
900
|
+
expect(await completeMany(store, [delivery, delivery], at(1))).toEqual([true, false]);
|
|
901
|
+
});
|
|
902
|
+
it("fences every lease of a batch on its own", async () => {
|
|
903
|
+
await append(
|
|
904
|
+
Array.from({ length: 3 }, () => ({
|
|
905
|
+
event: createEnvelope(),
|
|
906
|
+
deliveries: [createDelivery("handler-a")]
|
|
907
|
+
}))
|
|
908
|
+
);
|
|
909
|
+
const stale = await claim(["handler-a"], at(0));
|
|
910
|
+
const current = await claim(["handler-a"], at(LEASE_MS), 100, "worker-2");
|
|
911
|
+
expect(current).toHaveLength(3);
|
|
912
|
+
await append(
|
|
913
|
+
[{ event: createEnvelope(), deliveries: [createDelivery("handler-a")] }],
|
|
914
|
+
at(1)
|
|
915
|
+
);
|
|
916
|
+
const [fresh] = await claim(["handler-a"], at(LEASE_MS + 1), 100, "worker-2");
|
|
917
|
+
expect(
|
|
918
|
+
await completeMany(store, [stale[0], fresh, stale[1], current[2]], at(LEASE_MS + 2))
|
|
919
|
+
).toEqual([false, true, false, true]);
|
|
920
|
+
expect(await store.statistics(at(LEASE_MS + 3))).toMatchObject({ processing: 2 });
|
|
921
|
+
});
|
|
922
|
+
it("does not complete a lease of a delivery that went back to pending", async () => {
|
|
923
|
+
const error = { name: "Error", message: "x" };
|
|
924
|
+
await append([
|
|
925
|
+
{ event: createEnvelope(), deliveries: [createDelivery("a"), createDelivery("b")] }
|
|
926
|
+
]);
|
|
927
|
+
const [retrying] = await claim(["a"]);
|
|
928
|
+
const [released] = await claim(["b"]);
|
|
929
|
+
await store.reschedule(retrying, at(1e3), error, at(1));
|
|
930
|
+
await store.release(released, at(1));
|
|
931
|
+
expect(await completeMany(store, [retrying, released], at(2))).toEqual([false, false]);
|
|
932
|
+
expect(await store.statistics(at(2))).toMatchObject({ pending: 2, processing: 0 });
|
|
933
|
+
});
|
|
934
|
+
});
|
|
935
|
+
describe("stale leases of one delivery", () => {
|
|
936
|
+
const error = { name: "Error", message: "x" };
|
|
937
|
+
it("refuses the lease of an attempt that was released when the same owner claims it again", async () => {
|
|
938
|
+
await append([{ event: createEnvelope(), deliveries: [createDelivery("handler-a")] }]);
|
|
939
|
+
const [stale] = await claim(["handler-a"], at(0), 100, "worker-1");
|
|
940
|
+
expect(await store.release(stale, at(1))).toBe(true);
|
|
941
|
+
const [current] = await claim(["handler-a"], at(2), 100, "worker-1");
|
|
942
|
+
expect(current.deliveryId).toBe(stale.deliveryId);
|
|
943
|
+
expect(current.leaseToken).not.toBe(stale.leaseToken);
|
|
944
|
+
expect(await store.complete(stale, at(3))).toBe(false);
|
|
945
|
+
expect(await store.bury(stale, error, at(3))).toBe(false);
|
|
946
|
+
expect(await store.reschedule(stale, at(9), error, at(3))).toBe(false);
|
|
947
|
+
expect(await store.release(stale, at(3))).toBe(false);
|
|
948
|
+
expect(await store.extendLease(stale, at(LEASE_MS * 3), at(3))).toBe(false);
|
|
949
|
+
expect(await store.complete(current, at(4))).toBe(true);
|
|
950
|
+
});
|
|
951
|
+
it("refuses the lease of an attempt that was rescheduled when the same owner claims it again", async () => {
|
|
952
|
+
await append([{ event: createEnvelope(), deliveries: [createDelivery("handler-a")] }]);
|
|
953
|
+
const [stale] = await claim(["handler-a"], at(0), 100, "worker-1");
|
|
954
|
+
expect(await store.reschedule(stale, at(10), error, at(1))).toBe(true);
|
|
955
|
+
const [current] = await claim(["handler-a"], at(10), 100, "worker-1");
|
|
956
|
+
expect(current).toMatchObject({ deliveryId: stale.deliveryId, attempt: 2 });
|
|
957
|
+
expect(await store.complete(stale, at(11))).toBe(false);
|
|
958
|
+
expect(await store.bury(stale, error, at(11))).toBe(false);
|
|
959
|
+
expect(await completeMany(store, [stale], at(11))).toEqual([false]);
|
|
960
|
+
expect(await store.complete(current, at(12))).toBe(true);
|
|
961
|
+
});
|
|
962
|
+
it("matches the results of completeMany to the lease, not to the delivery id", async () => {
|
|
963
|
+
await append([{ event: createEnvelope(), deliveries: [createDelivery("handler-a")] }]);
|
|
964
|
+
const [stale] = await claim(["handler-a"], at(0), 100, "worker-1");
|
|
965
|
+
await store.release(stale, at(1));
|
|
966
|
+
const [current] = await claim(["handler-a"], at(2), 100, "worker-1");
|
|
967
|
+
expect(await completeMany(store, [stale, current], at(3))).toEqual([false, true]);
|
|
968
|
+
await append([{ event: createEnvelope(), deliveries: [createDelivery("handler-b")] }]);
|
|
969
|
+
const [staleB] = await claim(["handler-b"], at(4), 100, "worker-1");
|
|
970
|
+
await store.release(staleB, at(5));
|
|
971
|
+
const [currentB] = await claim(["handler-b"], at(6), 100, "worker-1");
|
|
972
|
+
expect(await completeMany(store, [currentB, staleB], at(7))).toEqual([true, false]);
|
|
973
|
+
});
|
|
974
|
+
});
|
|
975
|
+
describe("retention of the deduplication record", () => {
|
|
976
|
+
const DAY = 864e5;
|
|
977
|
+
it("keeps an event for the retention window after its last delivery completed, however old it is", async () => {
|
|
978
|
+
const event = createEnvelope();
|
|
979
|
+
await append([{ event, deliveries: [createDelivery("handler-a")] }]);
|
|
980
|
+
const [delivery] = await claim(["handler-a"], at(30 * DAY));
|
|
981
|
+
expect(await store.complete(delivery, at(30 * DAY + 1))).toBe(true);
|
|
982
|
+
expect(await store.purge({ completedBefore: at(29 * DAY) })).toMatchObject({
|
|
983
|
+
deletedEvents: 0
|
|
984
|
+
});
|
|
985
|
+
const again = await append(
|
|
986
|
+
[{ event, deliveries: [createDelivery("handler-a")] }],
|
|
987
|
+
at(30 * DAY + 2)
|
|
988
|
+
);
|
|
989
|
+
expect(again.appendedDeliveryCount).toBe(0);
|
|
990
|
+
expect(await store.purge({ completedBefore: at(31 * DAY) })).toMatchObject({
|
|
991
|
+
deletedEvents: 1
|
|
992
|
+
});
|
|
993
|
+
const afterwards = await append(
|
|
994
|
+
[{ event, deliveries: [createDelivery("handler-a")] }],
|
|
995
|
+
at(31 * DAY)
|
|
996
|
+
);
|
|
997
|
+
expect(afterwards.appendedEventIds).toEqual([event.id]);
|
|
998
|
+
});
|
|
999
|
+
it("keeps the later completion time of an event when completions are recorded out of order", async () => {
|
|
1000
|
+
const event = createEnvelope();
|
|
1001
|
+
await append([
|
|
1002
|
+
{ event, deliveries: [createDelivery("handler-a"), createDelivery("handler-b")] }
|
|
1003
|
+
]);
|
|
1004
|
+
const claimed = await claim(["handler-a", "handler-b"]);
|
|
1005
|
+
expect(claimed).toHaveLength(2);
|
|
1006
|
+
const [first, second] = claimed.sort(
|
|
1007
|
+
(left, right) => left.handler.localeCompare(right.handler)
|
|
1008
|
+
);
|
|
1009
|
+
expect(await store.complete(first, at(200))).toBe(true);
|
|
1010
|
+
expect(await store.complete(second, at(100))).toBe(true);
|
|
1011
|
+
expect(await store.purge({ completedBefore: at(150) })).toMatchObject({ deletedEvents: 0 });
|
|
1012
|
+
const again = await append(
|
|
1013
|
+
[{ event, deliveries: [createDelivery("handler-a"), createDelivery("handler-b")] }],
|
|
1014
|
+
at(201)
|
|
1015
|
+
);
|
|
1016
|
+
expect(again.appendedDeliveryCount).toBe(0);
|
|
1017
|
+
});
|
|
1018
|
+
it("remembers every handler of an event appended twice in one transaction", async () => {
|
|
1019
|
+
const event = createEnvelope();
|
|
1020
|
+
const result = await store.transaction(async (transaction) => {
|
|
1021
|
+
await store.append(
|
|
1022
|
+
transaction,
|
|
1023
|
+
[{ event, deliveries: [createDelivery("handler-a")] }],
|
|
1024
|
+
at(0)
|
|
1025
|
+
);
|
|
1026
|
+
return store.append(
|
|
1027
|
+
transaction,
|
|
1028
|
+
[{ event, deliveries: [createDelivery("handler-b")] }],
|
|
1029
|
+
at(0)
|
|
1030
|
+
);
|
|
1031
|
+
});
|
|
1032
|
+
expect(result).toMatchObject({ duplicateEventIds: [event.id], appendedDeliveryCount: 1 });
|
|
1033
|
+
const claimed = await claim(["handler-a", "handler-b"]);
|
|
1034
|
+
expect(claimed).toHaveLength(2);
|
|
1035
|
+
for (const delivery of claimed) {
|
|
1036
|
+
expect(await store.complete(delivery, at(1))).toBe(true);
|
|
1037
|
+
}
|
|
1038
|
+
const again = await append(
|
|
1039
|
+
[{ event, deliveries: [createDelivery("handler-a"), createDelivery("handler-b")] }],
|
|
1040
|
+
at(2)
|
|
1041
|
+
);
|
|
1042
|
+
expect(again).toMatchObject({ duplicateEventIds: [event.id], appendedDeliveryCount: 0 });
|
|
1043
|
+
});
|
|
1044
|
+
it("does not let the dead-letter cutoff remove events whose deliveries completed", async () => {
|
|
1045
|
+
const event = createEnvelope();
|
|
1046
|
+
await append([{ event, deliveries: [createDelivery("handler-a")] }]);
|
|
1047
|
+
const [delivery] = await claim(["handler-a"]);
|
|
1048
|
+
await store.complete(delivery, at(1));
|
|
1049
|
+
expect(await store.purge({ deadBefore: at(100 * DAY) })).toMatchObject({
|
|
1050
|
+
deletedEvents: 0
|
|
1051
|
+
});
|
|
1052
|
+
const again = await append([{ event, deliveries: [createDelivery("handler-a")] }], at(2));
|
|
1053
|
+
expect(again.appendedDeliveryCount).toBe(0);
|
|
1054
|
+
});
|
|
1055
|
+
});
|
|
1056
|
+
describe("lost leases", () => {
|
|
1057
|
+
it("hands a delivery of a crashed worker to the next claimer with the attempt counted", async () => {
|
|
1058
|
+
await append(
|
|
1059
|
+
Array.from({ length: 4 }, () => ({
|
|
1060
|
+
event: createEnvelope(),
|
|
1061
|
+
deliveries: [createDelivery("handler-a")]
|
|
1062
|
+
}))
|
|
1063
|
+
);
|
|
1064
|
+
const first = await claim(["handler-a"], at(0), 100, "worker-1");
|
|
1065
|
+
expect(first.map((delivery) => delivery.attempt)).toEqual([1, 1, 1, 1]);
|
|
1066
|
+
expect(await claim(["handler-a"], at(LEASE_MS - 1), 100, "worker-2")).toEqual([]);
|
|
1067
|
+
const second = await claim(["handler-a"], at(LEASE_MS), 100, "worker-2");
|
|
1068
|
+
expect(second.map((delivery) => delivery.deliveryId).sort()).toEqual(
|
|
1069
|
+
first.map((delivery) => delivery.deliveryId).sort()
|
|
1070
|
+
);
|
|
1071
|
+
expect(second.map((delivery) => delivery.attempt)).toEqual([2, 2, 2, 2]);
|
|
1072
|
+
for (const stale of first) {
|
|
1073
|
+
expect(await store.complete(stale, at(LEASE_MS + 1))).toBe(false);
|
|
1074
|
+
}
|
|
1075
|
+
expect(await completeMany(store, first, at(LEASE_MS + 1))).toEqual([
|
|
1076
|
+
false,
|
|
1077
|
+
false,
|
|
1078
|
+
false,
|
|
1079
|
+
false
|
|
1080
|
+
]);
|
|
1081
|
+
expect(await completeMany(store, second, at(LEASE_MS + 2))).toEqual([
|
|
1082
|
+
true,
|
|
1083
|
+
true,
|
|
1084
|
+
true,
|
|
1085
|
+
true
|
|
1086
|
+
]);
|
|
1087
|
+
});
|
|
1088
|
+
it("keeps counting attempts of a poison delivery until it is dead-lettered", async () => {
|
|
1089
|
+
await append([{ event: createEnvelope(), deliveries: [createDelivery("handler-a")] }]);
|
|
1090
|
+
for (let crash = 0; crash < 4; crash += 1) {
|
|
1091
|
+
const [delivery] = await claim(
|
|
1092
|
+
["handler-a"],
|
|
1093
|
+
at(crash * LEASE_MS),
|
|
1094
|
+
100,
|
|
1095
|
+
`worker-${crash}`
|
|
1096
|
+
);
|
|
1097
|
+
expect(delivery.attempt).toBe(crash + 1);
|
|
1098
|
+
}
|
|
1099
|
+
const [last] = await claim(["handler-a"], at(4 * LEASE_MS), 100, "worker-4");
|
|
1100
|
+
expect(last.attempt).toBe(5);
|
|
1101
|
+
expect(
|
|
1102
|
+
await store.bury(last, { name: "Error", message: "poison" }, at(4 * LEASE_MS))
|
|
1103
|
+
).toBe(true);
|
|
1104
|
+
const [dead] = await store.listDeliveries({ status: "dead" });
|
|
1105
|
+
expect(dead).toMatchObject({ attempts: 5, lastError: { message: "poison" } });
|
|
1106
|
+
expect(await claim(["handler-a"], at(100 * LEASE_MS))).toEqual([]);
|
|
1107
|
+
});
|
|
1108
|
+
it("does not count an attempt that was reported as failed twice", async () => {
|
|
1109
|
+
await append([{ event: createEnvelope(), deliveries: [createDelivery("handler-a")] }]);
|
|
1110
|
+
const [delivery] = await claim(["handler-a"], at(0), 100, "worker-1");
|
|
1111
|
+
await store.reschedule(delivery, at(10), { name: "Error", message: "x" }, at(1));
|
|
1112
|
+
const [retry] = await claim(["handler-a"], at(LEASE_MS), 100, "worker-2");
|
|
1113
|
+
expect(retry.attempt).toBe(2);
|
|
1114
|
+
const [afterCrash] = await claim(["handler-a"], at(LEASE_MS * 2), 100, "worker-3");
|
|
1115
|
+
expect(afterCrash.attempt).toBe(3);
|
|
1116
|
+
});
|
|
1117
|
+
it("keeps the order of a key across a takeover", async () => {
|
|
1118
|
+
const [head, next] = await (async () => {
|
|
1119
|
+
const events = [createEnvelope({ key: "k" }), createEnvelope({ key: "k" })];
|
|
1120
|
+
for (const event of events) {
|
|
1121
|
+
await append([{ event, deliveries: [createDelivery("handler-a")] }]);
|
|
1122
|
+
}
|
|
1123
|
+
return events;
|
|
1124
|
+
})();
|
|
1125
|
+
const [stale] = await claim(["handler-a"], at(0), 100, "worker-1");
|
|
1126
|
+
expect(stale.event.id).toBe(head.id);
|
|
1127
|
+
const taken = await claim(["handler-a"], at(LEASE_MS), 100, "worker-2");
|
|
1128
|
+
expect(taken.map((delivery) => delivery.event.id)).toEqual([head.id]);
|
|
1129
|
+
expect(await store.complete(stale, at(LEASE_MS + 1))).toBe(false);
|
|
1130
|
+
expect(await store.complete(taken[0], at(LEASE_MS + 2))).toBe(true);
|
|
1131
|
+
const [following] = await claim(["handler-a"], at(LEASE_MS + 3), 100, "worker-2");
|
|
1132
|
+
expect(following.event.id).toBe(next.id);
|
|
1133
|
+
});
|
|
1134
|
+
it("lets every worker see only deliveries nobody else holds, and none twice", async () => {
|
|
1135
|
+
await append(
|
|
1136
|
+
Array.from({ length: 30 }, () => ({
|
|
1137
|
+
event: createEnvelope(),
|
|
1138
|
+
deliveries: [createDelivery("handler-a")]
|
|
1139
|
+
}))
|
|
1140
|
+
);
|
|
1141
|
+
const seen = [];
|
|
1142
|
+
for (let round = 0; round < 40; round += 1) {
|
|
1143
|
+
const owners = ["worker-1", "worker-2", "worker-3"];
|
|
1144
|
+
const batches = await Promise.all(
|
|
1145
|
+
owners.map((owner) => claim(["handler-a"], at(round), 4, owner))
|
|
1146
|
+
);
|
|
1147
|
+
for (const batch of batches) {
|
|
1148
|
+
seen.push(...batch.map((delivery) => delivery.deliveryId));
|
|
1149
|
+
expect(await completeMany(store, batch, at(round))).toEqual(batch.map(() => true));
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
expect(seen).toHaveLength(30);
|
|
1153
|
+
expect(new Set(seen).size).toBe(30);
|
|
1154
|
+
});
|
|
1155
|
+
});
|
|
1156
|
+
describe("relinquish", () => {
|
|
1157
|
+
it("hands idle leases to other workers at once and keeps leases with running work", async (context) => {
|
|
1158
|
+
if (store.relinquish === void 0) {
|
|
1159
|
+
return context.skip();
|
|
1160
|
+
}
|
|
1161
|
+
await append([
|
|
1162
|
+
{ event: createEnvelope(), deliveries: [createDelivery("handler-a")] },
|
|
1163
|
+
{ event: createEnvelope(), deliveries: [createDelivery("handler-a")] }
|
|
1164
|
+
]);
|
|
1165
|
+
const claimed = await claim(["handler-a"], at(0), 100, "worker-1");
|
|
1166
|
+
expect(claimed).toHaveLength(2);
|
|
1167
|
+
await store.relinquish("worker-1", at(1));
|
|
1168
|
+
expect(await store.complete(claimed[0], at(2))).toBe(true);
|
|
1169
|
+
expect(await store.complete(claimed[1], at(2))).toBe(true);
|
|
1170
|
+
await store.relinquish("worker-1", at(3));
|
|
1171
|
+
await append(
|
|
1172
|
+
[{ event: createEnvelope(), deliveries: [createDelivery("handler-a")] }],
|
|
1173
|
+
at(4)
|
|
1174
|
+
);
|
|
1175
|
+
expect(await claim(["handler-a"], at(4), 100, "worker-2")).toHaveLength(1);
|
|
1176
|
+
});
|
|
1177
|
+
it("does not give away a delivery that is still running", async (context) => {
|
|
1178
|
+
if (store.relinquish === void 0) {
|
|
1179
|
+
return context.skip();
|
|
1180
|
+
}
|
|
1181
|
+
await append([{ event: createEnvelope(), deliveries: [createDelivery("handler-a")] }]);
|
|
1182
|
+
const [running] = await claim(["handler-a"], at(0), 100, "worker-1");
|
|
1183
|
+
await store.relinquish("worker-1", at(1));
|
|
1184
|
+
expect(await claim(["handler-a"], at(2), 100, "worker-2")).toEqual([]);
|
|
1185
|
+
expect(await store.complete(running, at(3))).toBe(true);
|
|
1186
|
+
});
|
|
1187
|
+
});
|
|
769
1188
|
describe.skipIf(options.createPeerStore === void 0)("across store instances", () => {
|
|
770
1189
|
let peer;
|
|
771
1190
|
beforeEach(async () => {
|
|
@@ -810,6 +1229,32 @@ function eventStoreContract(name, options) {
|
|
|
810
1229
|
expect(await store.complete(stale, at(LEASE_MS + 1))).toBe(false);
|
|
811
1230
|
expect(await peer.complete(current, at(LEASE_MS + 1))).toBe(true);
|
|
812
1231
|
});
|
|
1232
|
+
it("completes batches in either process and fences the one that lost its lease", async () => {
|
|
1233
|
+
await append(
|
|
1234
|
+
Array.from({ length: 6 }, () => ({
|
|
1235
|
+
event: createEnvelope(),
|
|
1236
|
+
deliveries: [createDelivery("handler-a")]
|
|
1237
|
+
}))
|
|
1238
|
+
);
|
|
1239
|
+
const request = (owner, now) => ({
|
|
1240
|
+
handlers: ["handler-a"],
|
|
1241
|
+
limit: 100,
|
|
1242
|
+
leaseOwner: owner,
|
|
1243
|
+
leaseExpiresAt: new Date(now.getTime() + LEASE_MS),
|
|
1244
|
+
now
|
|
1245
|
+
});
|
|
1246
|
+
const stale = await store.claim(request("primary", at(0)));
|
|
1247
|
+
const current = await peer.claim(request("peer", at(LEASE_MS)));
|
|
1248
|
+
expect(current).toHaveLength(6);
|
|
1249
|
+
expect(await completeMany(store, stale, at(LEASE_MS + 1))).toEqual(stale.map(() => false));
|
|
1250
|
+
expect(await completeMany(peer, current, at(LEASE_MS + 1))).toEqual(
|
|
1251
|
+
current.map(() => true)
|
|
1252
|
+
);
|
|
1253
|
+
expect(await store.statistics(at(LEASE_MS + 2))).toMatchObject({
|
|
1254
|
+
pending: 0,
|
|
1255
|
+
processing: 0
|
|
1256
|
+
});
|
|
1257
|
+
});
|
|
813
1258
|
});
|
|
814
1259
|
});
|
|
815
1260
|
}
|