@beignet/core 0.0.49 → 0.0.51
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/CHANGELOG.md +30 -0
- package/README.md +213 -32
- package/dist/application/index.d.ts +5 -5
- package/dist/application/index.d.ts.map +1 -1
- package/dist/application/index.js +5 -4
- package/dist/application/index.js.map +1 -1
- package/dist/events/index.d.ts +54 -5
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +188 -34
- package/dist/events/index.js.map +1 -1
- package/dist/events/payload-state.d.ts +4 -0
- package/dist/events/payload-state.d.ts.map +1 -0
- package/dist/events/payload-state.js +11 -0
- package/dist/events/payload-state.js.map +1 -0
- package/dist/idempotency/index.d.ts +7 -3
- package/dist/idempotency/index.d.ts.map +1 -1
- package/dist/idempotency/index.js +45 -12
- package/dist/idempotency/index.js.map +1 -1
- package/dist/locks/index.d.ts.map +1 -1
- package/dist/locks/index.js +0 -4
- package/dist/locks/index.js.map +1 -1
- package/dist/mail/index.d.ts.map +1 -1
- package/dist/mail/index.js +6 -3
- package/dist/mail/index.js.map +1 -1
- package/dist/outbox/index.d.ts +163 -15
- package/dist/outbox/index.d.ts.map +1 -1
- package/dist/outbox/index.js +1009 -152
- package/dist/outbox/index.js.map +1 -1
- package/dist/payments/index.d.ts.map +1 -1
- package/dist/payments/index.js +0 -4
- package/dist/payments/index.js.map +1 -1
- package/dist/ports/best-effort-work.d.ts +21 -0
- package/dist/ports/best-effort-work.d.ts.map +1 -0
- package/dist/ports/best-effort-work.js +2 -0
- package/dist/ports/best-effort-work.js.map +1 -0
- package/dist/ports/events.d.ts +7 -5
- package/dist/ports/events.d.ts.map +1 -1
- package/dist/ports/index.d.ts +7 -2
- package/dist/ports/index.d.ts.map +1 -1
- package/dist/ports/index.js +1 -0
- package/dist/ports/index.js.map +1 -1
- package/dist/ports/testing.d.ts +28 -0
- package/dist/ports/testing.d.ts.map +1 -1
- package/dist/ports/testing.js +50 -0
- package/dist/ports/testing.js.map +1 -1
- package/dist/ports/unit-of-work.d.ts +9 -7
- package/dist/ports/unit-of-work.d.ts.map +1 -1
- package/dist/ports/unit-of-work.js +16 -6
- package/dist/ports/unit-of-work.js.map +1 -1
- package/dist/providers/index.d.ts +1 -1
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/provider.d.ts +8 -43
- package/dist/providers/provider.d.ts.map +1 -1
- package/dist/providers/provider.js.map +1 -1
- package/dist/search/index.d.ts.map +1 -1
- package/dist/search/index.js +0 -4
- package/dist/search/index.js.map +1 -1
- package/dist/server/hooks/cors.d.ts +7 -2
- package/dist/server/hooks/cors.d.ts.map +1 -1
- package/dist/server/hooks/cors.js +31 -2
- package/dist/server/hooks/cors.js.map +1 -1
- package/dist/server/hooks/logging.d.ts +2 -2
- package/dist/server/hooks/logging.d.ts.map +1 -1
- package/dist/server/hooks/logging.js.map +1 -1
- package/dist/server/hooks/security.d.ts +2 -2
- package/dist/server/hooks/security.d.ts.map +1 -1
- package/dist/server/hooks/security.js.map +1 -1
- package/dist/server/http.d.ts +21 -2
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +4 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/instrumentation.d.ts.map +1 -1
- package/dist/server/instrumentation.js +5 -3
- package/dist/server/instrumentation.js.map +1 -1
- package/dist/server/request-executor.d.ts.map +1 -1
- package/dist/server/request-executor.js +9 -0
- package/dist/server/request-executor.js.map +1 -1
- package/dist/server/request-preparation.d.ts.map +1 -1
- package/dist/server/request-preparation.js +20 -2
- package/dist/server/request-preparation.js.map +1 -1
- package/dist/server/response-finalization.d.ts +2 -2
- package/dist/server/response-finalization.d.ts.map +1 -1
- package/dist/server/response-finalization.js +25 -8
- package/dist/server/response-finalization.js.map +1 -1
- package/dist/server/route-matching.d.ts.map +1 -1
- package/dist/server/route-matching.js +12 -1
- package/dist/server/route-matching.js.map +1 -1
- package/dist/server/server-sent-events.d.ts +94 -0
- package/dist/server/server-sent-events.d.ts.map +1 -0
- package/dist/server/server-sent-events.js +275 -0
- package/dist/server/server-sent-events.js.map +1 -0
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +75 -31
- package/dist/server/server.js.map +1 -1
- package/dist/server/trusted-proxy-internal.d.ts +4 -0
- package/dist/server/trusted-proxy-internal.d.ts.map +1 -1
- package/dist/server/trusted-proxy-internal.js +20 -0
- package/dist/server/trusted-proxy-internal.js.map +1 -1
- package/dist/server/trusted-proxy.d.ts.map +1 -1
- package/dist/server/trusted-proxy.js +3 -8
- package/dist/server/trusted-proxy.js.map +1 -1
- package/dist/testing/index.d.ts +17 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +18 -7
- package/dist/testing/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/app-architecture/SKILL.md +37 -3
- package/src/application/index.ts +18 -7
- package/src/events/index.ts +268 -40
- package/src/events/payload-state.ts +24 -0
- package/src/idempotency/index.ts +65 -17
- package/src/locks/index.ts +0 -4
- package/src/mail/index.ts +7 -3
- package/src/outbox/index.ts +1382 -175
- package/src/payments/index.ts +0 -4
- package/src/ports/best-effort-work.ts +21 -0
- package/src/ports/events.ts +9 -4
- package/src/ports/index.ts +12 -0
- package/src/ports/testing.ts +76 -0
- package/src/ports/unit-of-work.ts +28 -14
- package/src/providers/index.ts +0 -1
- package/src/providers/provider.ts +8 -45
- package/src/search/index.ts +0 -4
- package/src/server/hooks/cors.ts +53 -5
- package/src/server/hooks/logging.ts +6 -2
- package/src/server/hooks/security.ts +8 -4
- package/src/server/http.ts +23 -2
- package/src/server/index.ts +4 -0
- package/src/server/instrumentation.ts +12 -4
- package/src/server/request-executor.ts +14 -0
- package/src/server/request-preparation.ts +23 -3
- package/src/server/response-finalization.ts +51 -15
- package/src/server/route-matching.ts +24 -1
- package/src/server/server-sent-events.ts +415 -0
- package/src/server/server.ts +98 -41
- package/src/server/trusted-proxy-internal.ts +20 -0
- package/src/server/trusted-proxy.ts +6 -7
- package/src/testing/index.ts +50 -9
- package/dist/query-codec.d.ts +0 -27
- package/dist/query-codec.d.ts.map +0 -1
- package/dist/query-codec.js +0 -245
- package/dist/query-codec.js.map +0 -1
package/dist/outbox/index.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* should be delivered after the owning database transaction commits.
|
|
6
6
|
*/
|
|
7
7
|
import { parseEventPayload, } from "../events/index.js";
|
|
8
|
+
import { isEventPayloadParsed, markEventPayloadParsed, } from "../events/payload-state.js";
|
|
8
9
|
import { getJobRetryDelayMs, getJobRetryMaxAttempts, parseJobPayload, SINGLE_ATTEMPT_DISPATCH, shouldRetryJob, } from "../jobs/index.js";
|
|
9
10
|
import { createProviderInstrumentation, } from "../providers/index.js";
|
|
10
11
|
import { captureTraceCarrier, parseTraceCarrier, resolveTracingPort, runWithTracing, } from "../tracing/index.js";
|
|
@@ -12,6 +13,18 @@ import { captureTraceCarrier, parseTraceCarrier, resolveTracingPort, runWithTrac
|
|
|
12
13
|
* Default lease duration for claimed outbox messages.
|
|
13
14
|
*/
|
|
14
15
|
export const DEFAULT_OUTBOX_LEASE_MS = 30_000;
|
|
16
|
+
/**
|
|
17
|
+
* Default maximum messages handled by one bounded drain pass.
|
|
18
|
+
*/
|
|
19
|
+
export const DEFAULT_OUTBOX_BATCH_SIZE = 100;
|
|
20
|
+
/**
|
|
21
|
+
* Default number of outbox messages delivered concurrently.
|
|
22
|
+
*/
|
|
23
|
+
export const DEFAULT_OUTBOX_CONCURRENCY = 1;
|
|
24
|
+
/**
|
|
25
|
+
* Default maximum time Beignet renews a claim for one delivery.
|
|
26
|
+
*/
|
|
27
|
+
export const DEFAULT_OUTBOX_MAX_ACTIVE_MS = 300_000;
|
|
15
28
|
/**
|
|
16
29
|
* Default maximum delivery attempts before a message is dead-lettered.
|
|
17
30
|
*/
|
|
@@ -52,6 +65,49 @@ export class OutboxClaimError extends Error {
|
|
|
52
65
|
this.id = args.id;
|
|
53
66
|
}
|
|
54
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Error persisted when an eligible message has exhausted its claim attempts
|
|
70
|
+
* without reaching a terminal settlement.
|
|
71
|
+
*/
|
|
72
|
+
export class OutboxAbandonedClaimError extends Error {
|
|
73
|
+
/** Message ID whose attempt budget was exhausted. */
|
|
74
|
+
id;
|
|
75
|
+
/** Number of claims already made. */
|
|
76
|
+
attempts;
|
|
77
|
+
/** Maximum permitted claim attempts. */
|
|
78
|
+
maxAttempts;
|
|
79
|
+
constructor(args) {
|
|
80
|
+
super(`Outbox message "${args.id}" exhausted ${args.maxAttempts} claim attempts without a terminal settlement.`);
|
|
81
|
+
this.name = "OutboxAbandonedClaimError";
|
|
82
|
+
this.id = args.id;
|
|
83
|
+
this.attempts = args.attempts;
|
|
84
|
+
this.maxAttempts = args.maxAttempts;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** Error surfaced when an outbox worker no longer has a confirmed claim. */
|
|
88
|
+
export class OutboxLeaseLostError extends Error {
|
|
89
|
+
/** Message ID whose ownership was lost. */
|
|
90
|
+
id;
|
|
91
|
+
constructor(args) {
|
|
92
|
+
super(args.message ??
|
|
93
|
+
`Outbox claim for message "${args.id}" is no longer active.`, args.cause === undefined ? undefined : { cause: args.cause });
|
|
94
|
+
this.name = "OutboxLeaseLostError";
|
|
95
|
+
this.id = args.id;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** Error surfaced when delivery exceeds the configured active-claim window. */
|
|
99
|
+
export class OutboxMaxActiveDurationError extends Error {
|
|
100
|
+
/** Message ID whose active window elapsed. */
|
|
101
|
+
id;
|
|
102
|
+
/** Configured maximum active duration. */
|
|
103
|
+
maxActiveMs;
|
|
104
|
+
constructor(args) {
|
|
105
|
+
super(`Outbox delivery for message "${args.id}" exceeded the ${args.maxActiveMs}ms active-claim limit.`);
|
|
106
|
+
this.name = "OutboxMaxActiveDurationError";
|
|
107
|
+
this.id = args.id;
|
|
108
|
+
this.maxActiveMs = args.maxActiveMs;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
55
111
|
/**
|
|
56
112
|
* Error thrown when an outbox admin operation cannot be completed safely.
|
|
57
113
|
*/
|
|
@@ -425,9 +481,11 @@ export function createMemoryOutbox(storeOptions = {}) {
|
|
|
425
481
|
assertPositiveInteger("limit", options.limit);
|
|
426
482
|
const now = options.now ?? storeNow();
|
|
427
483
|
const leaseMs = options.leaseMs ?? DEFAULT_OUTBOX_LEASE_MS;
|
|
484
|
+
assertValidDate("now", now);
|
|
428
485
|
assertPositiveInteger("leaseMs", leaseMs);
|
|
429
486
|
const lockedUntil = new Date(now.getTime() + leaseMs);
|
|
430
487
|
const claimed = [];
|
|
488
|
+
const deadLettered = [];
|
|
431
489
|
const eligible = [...messages.values()]
|
|
432
490
|
.filter((message) => isEligible(message, now))
|
|
433
491
|
.sort((a, b) => {
|
|
@@ -438,6 +496,20 @@ export function createMemoryOutbox(storeOptions = {}) {
|
|
|
438
496
|
})
|
|
439
497
|
.slice(0, options.limit);
|
|
440
498
|
for (const message of eligible) {
|
|
499
|
+
if (message.attempts >= message.maxAttempts) {
|
|
500
|
+
message.status = "deadLettered";
|
|
501
|
+
message.lastError = serializeOutboxError(new OutboxAbandonedClaimError({
|
|
502
|
+
id: message.id,
|
|
503
|
+
attempts: message.attempts,
|
|
504
|
+
maxAttempts: message.maxAttempts,
|
|
505
|
+
}));
|
|
506
|
+
message.claimToken = null;
|
|
507
|
+
message.claimedAt = null;
|
|
508
|
+
message.lockedUntil = null;
|
|
509
|
+
message.updatedAt = cloneDate(now);
|
|
510
|
+
deadLettered.push(copyMessage(message));
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
441
513
|
message.status = "claimed";
|
|
442
514
|
message.attempts += 1;
|
|
443
515
|
message.claimToken = createStoreId();
|
|
@@ -446,13 +518,41 @@ export function createMemoryOutbox(storeOptions = {}) {
|
|
|
446
518
|
message.updatedAt = cloneDate(now);
|
|
447
519
|
claimed.push(toClaimedMessage(message));
|
|
448
520
|
}
|
|
449
|
-
return claimed;
|
|
521
|
+
return { claimed, deadLettered };
|
|
522
|
+
},
|
|
523
|
+
async renewClaim(input) {
|
|
524
|
+
assertNonEmptyString("id", input.id);
|
|
525
|
+
assertNonEmptyString("claimToken", input.claimToken);
|
|
526
|
+
const message = getClaimedOrThrow(input.id, input.claimToken);
|
|
527
|
+
const now = input.now ?? storeNow();
|
|
528
|
+
const leaseMs = input.leaseMs ?? DEFAULT_OUTBOX_LEASE_MS;
|
|
529
|
+
assertValidDate("now", now);
|
|
530
|
+
assertPositiveInteger("leaseMs", leaseMs);
|
|
531
|
+
if (message.lockedUntil === null ||
|
|
532
|
+
message.lockedUntil.getTime() <= now.getTime()) {
|
|
533
|
+
throw new OutboxClaimError({
|
|
534
|
+
id: input.id,
|
|
535
|
+
message: `Outbox message "${input.id}" no longer has an active claim to renew.`,
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
const lockedUntil = new Date(now.getTime() + leaseMs);
|
|
539
|
+
message.lockedUntil = cloneDate(lockedUntil);
|
|
540
|
+
message.updatedAt = cloneDate(now);
|
|
541
|
+
return { lockedUntil: cloneDate(lockedUntil) };
|
|
450
542
|
},
|
|
451
543
|
async markDelivered(input) {
|
|
452
544
|
assertNonEmptyString("id", input.id);
|
|
453
545
|
assertNonEmptyString("claimToken", input.claimToken);
|
|
454
546
|
const message = getClaimedOrThrow(input.id, input.claimToken);
|
|
455
547
|
const now = input.now ?? storeNow();
|
|
548
|
+
assertValidDate("now", now);
|
|
549
|
+
if (message.lockedUntil === null ||
|
|
550
|
+
message.lockedUntil.getTime() <= now.getTime()) {
|
|
551
|
+
throw new OutboxClaimError({
|
|
552
|
+
id: input.id,
|
|
553
|
+
message: `Outbox message "${input.id}" no longer has an active claim to settle.`,
|
|
554
|
+
});
|
|
555
|
+
}
|
|
456
556
|
message.status = "delivered";
|
|
457
557
|
message.deliveredAt = cloneDate(now);
|
|
458
558
|
message.claimToken = null;
|
|
@@ -465,6 +565,16 @@ export function createMemoryOutbox(storeOptions = {}) {
|
|
|
465
565
|
assertNonEmptyString("claimToken", input.claimToken);
|
|
466
566
|
const message = getClaimedOrThrow(input.id, input.claimToken);
|
|
467
567
|
const now = input.now ?? storeNow();
|
|
568
|
+
assertValidDate("now", now);
|
|
569
|
+
if (input.retryAt)
|
|
570
|
+
assertValidDate("retryAt", input.retryAt);
|
|
571
|
+
if (message.lockedUntil === null ||
|
|
572
|
+
message.lockedUntil.getTime() <= now.getTime()) {
|
|
573
|
+
throw new OutboxClaimError({
|
|
574
|
+
id: input.id,
|
|
575
|
+
message: `Outbox message "${input.id}" no longer has an active claim to settle.`,
|
|
576
|
+
});
|
|
577
|
+
}
|
|
468
578
|
message.status = input.deadLetter ? "deadLettered" : "pending";
|
|
469
579
|
message.lastError = serializeOutboxError(input.error);
|
|
470
580
|
message.availableAt = input.retryAt
|
|
@@ -505,7 +615,10 @@ export function defineOutboxRegistry(input) {
|
|
|
505
615
|
* Validate an event payload and enqueue it as an outbox message.
|
|
506
616
|
*/
|
|
507
617
|
export async function enqueueEvent(outbox, event, payload, options = {}) {
|
|
508
|
-
await parseEventPayload(event, payload);
|
|
618
|
+
const parsed = await parseEventPayload(event, payload);
|
|
619
|
+
return await enqueueParsedEvent(outbox, event, parsed, options);
|
|
620
|
+
}
|
|
621
|
+
async function enqueueParsedEvent(outbox, event, payload, options) {
|
|
509
622
|
const trace = parseTraceCarrier(options.trace) ?? captureTraceCarrier(options.tracing);
|
|
510
623
|
return outbox.enqueue({
|
|
511
624
|
id: options.id,
|
|
@@ -539,7 +652,10 @@ export async function enqueueJob(outbox, job, payload, options = {}) {
|
|
|
539
652
|
export function createOutboxEventRecorder(outbox, options = {}) {
|
|
540
653
|
return {
|
|
541
654
|
async record(event, payload, publishOptions) {
|
|
542
|
-
|
|
655
|
+
const parsed = isEventPayloadParsed(publishOptions)
|
|
656
|
+
? payload
|
|
657
|
+
: await parseEventPayload(event, payload);
|
|
658
|
+
await enqueueParsedEvent(outbox, event, parsed, {
|
|
543
659
|
...options,
|
|
544
660
|
trace: publishOptions?.trace ?? options.trace,
|
|
545
661
|
});
|
|
@@ -612,8 +728,8 @@ async function deliverOutboxMessage(options, message, trace) {
|
|
|
612
728
|
if (!event) {
|
|
613
729
|
throw new OutboxRegistryError(`Outbox registry does not include event "${message.name}".`);
|
|
614
730
|
}
|
|
615
|
-
await parseEventPayload(event, message.payload);
|
|
616
|
-
await options.eventBus.publish(event,
|
|
731
|
+
const payload = await parseEventPayload(event, message.payload);
|
|
732
|
+
await options.eventBus.publish(event, payload, markEventPayloadParsed(trace ? { trace } : undefined));
|
|
617
733
|
return;
|
|
618
734
|
}
|
|
619
735
|
if (!options.jobs) {
|
|
@@ -640,18 +756,835 @@ async function deliverOutboxMessage(options, message, trace) {
|
|
|
640
756
|
}
|
|
641
757
|
await options.jobs.dispatch(job, message.payload, trace ? { trace } : undefined);
|
|
642
758
|
}
|
|
759
|
+
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
760
|
+
const OUTBOX_SETTLEMENT_ATTEMPTS = 3;
|
|
761
|
+
const OUTBOX_SETTLEMENT_RETRY_DELAY_MS = 100;
|
|
762
|
+
function defaultOutboxWait(delayMs, signal) {
|
|
763
|
+
return new Promise((resolve) => {
|
|
764
|
+
if (signal.aborted) {
|
|
765
|
+
resolve();
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
let timer;
|
|
769
|
+
const finish = () => {
|
|
770
|
+
if (timer !== undefined)
|
|
771
|
+
clearTimeout(timer);
|
|
772
|
+
signal.removeEventListener("abort", finish);
|
|
773
|
+
resolve();
|
|
774
|
+
};
|
|
775
|
+
timer = setTimeout(finish, delayMs);
|
|
776
|
+
signal.addEventListener("abort", finish, { once: true });
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
function readOutboxNow(now) {
|
|
780
|
+
const value = now();
|
|
781
|
+
assertValidDate("now", value);
|
|
782
|
+
return value;
|
|
783
|
+
}
|
|
784
|
+
function assertTimerDuration(name, value) {
|
|
785
|
+
assertPositiveInteger(name, value);
|
|
786
|
+
if (value > MAX_TIMER_DELAY_MS) {
|
|
787
|
+
throw new Error(`${name} must be at most ${MAX_TIMER_DELAY_MS}`);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
function resolveDrainRuntime(options) {
|
|
791
|
+
const batchSize = options.batchSize ?? DEFAULT_OUTBOX_BATCH_SIZE;
|
|
792
|
+
const concurrency = options.concurrency ?? DEFAULT_OUTBOX_CONCURRENCY;
|
|
793
|
+
const leaseMs = options.leaseMs ?? DEFAULT_OUTBOX_LEASE_MS;
|
|
794
|
+
const heartbeatMs = options.heartbeatMs ?? Math.max(1, Math.floor(leaseMs / 3));
|
|
795
|
+
const maxActiveMs = options.maxActiveMs ?? DEFAULT_OUTBOX_MAX_ACTIVE_MS;
|
|
796
|
+
assertPositiveInteger("batchSize", batchSize);
|
|
797
|
+
assertPositiveInteger("concurrency", concurrency);
|
|
798
|
+
if (concurrency > batchSize) {
|
|
799
|
+
throw new Error("concurrency must be less than or equal to batchSize");
|
|
800
|
+
}
|
|
801
|
+
assertTimerDuration("leaseMs", leaseMs);
|
|
802
|
+
if (leaseMs < 2)
|
|
803
|
+
throw new Error("leaseMs must be at least 2");
|
|
804
|
+
assertTimerDuration("heartbeatMs", heartbeatMs);
|
|
805
|
+
if (heartbeatMs >= leaseMs) {
|
|
806
|
+
throw new Error("heartbeatMs must be shorter than leaseMs");
|
|
807
|
+
}
|
|
808
|
+
assertTimerDuration("maxActiveMs", maxActiveMs);
|
|
809
|
+
return {
|
|
810
|
+
batchSize,
|
|
811
|
+
concurrency,
|
|
812
|
+
leaseMs,
|
|
813
|
+
heartbeatMs,
|
|
814
|
+
maxActiveMs,
|
|
815
|
+
now: options.now ?? (() => new Date()),
|
|
816
|
+
wait: options.wait ?? defaultOutboxWait,
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
async function notifyLeaseFailure(options, failure) {
|
|
820
|
+
try {
|
|
821
|
+
await options.onLeaseError?.(failure);
|
|
822
|
+
}
|
|
823
|
+
catch {
|
|
824
|
+
// Lease observers cannot change delivery ownership or recovery behavior.
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
async function notifySettlementFailure(options, failure) {
|
|
828
|
+
try {
|
|
829
|
+
await options.onSettlementError?.(failure);
|
|
830
|
+
}
|
|
831
|
+
catch {
|
|
832
|
+
// Settlement observers cannot replace the unknown storage outcome.
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
async function runBoundedClaimRenewal(options) {
|
|
836
|
+
const remainingMs = options.deadline.getTime() - options.renewalAt.getTime();
|
|
837
|
+
if (remainingMs <= 0)
|
|
838
|
+
return { kind: "deadline" };
|
|
839
|
+
if (options.signal.aborted)
|
|
840
|
+
return { kind: "stopped" };
|
|
841
|
+
const waitController = new AbortController();
|
|
842
|
+
const stopWaiting = () => waitController.abort();
|
|
843
|
+
options.signal.addEventListener("abort", stopWaiting, { once: true });
|
|
844
|
+
const renewal = Promise.resolve()
|
|
845
|
+
.then(() => options.outbox.renewClaim({
|
|
846
|
+
id: options.message.id,
|
|
847
|
+
claimToken: options.message.claimToken,
|
|
848
|
+
now: options.renewalAt,
|
|
849
|
+
leaseMs: options.runtime.leaseMs,
|
|
850
|
+
}))
|
|
851
|
+
.then((result) => ({ kind: "succeeded", result }), (error) => ({ kind: "failed", error }));
|
|
852
|
+
const deadline = Promise.resolve()
|
|
853
|
+
.then(() => options.runtime.wait(remainingMs, waitController.signal))
|
|
854
|
+
.then(() => options.signal.aborted
|
|
855
|
+
? { kind: "stopped" }
|
|
856
|
+
: { kind: "deadline" }, (error) => options.signal.aborted
|
|
857
|
+
? { kind: "stopped" }
|
|
858
|
+
: { kind: "waitFailed", error });
|
|
859
|
+
const result = await Promise.race([renewal, deadline]);
|
|
860
|
+
options.signal.removeEventListener("abort", stopWaiting);
|
|
861
|
+
waitController.abort();
|
|
862
|
+
if (result.kind !== "succeeded")
|
|
863
|
+
return result;
|
|
864
|
+
try {
|
|
865
|
+
const completedAt = readOutboxNow(options.runtime.now);
|
|
866
|
+
if (completedAt.getTime() >= options.deadline.getTime()) {
|
|
867
|
+
return { kind: "deadline" };
|
|
868
|
+
}
|
|
869
|
+
assertValidDate("lockedUntil", result.result.lockedUntil);
|
|
870
|
+
if (result.result.lockedUntil.getTime() <= completedAt.getTime()) {
|
|
871
|
+
return {
|
|
872
|
+
kind: "failed",
|
|
873
|
+
error: new Error(`Outbox claim renewal for message "${options.message.id}" returned an expired lease.`),
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
return {
|
|
877
|
+
kind: "succeeded",
|
|
878
|
+
lockedUntil: cloneDate(result.result.lockedUntil),
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
catch (error) {
|
|
882
|
+
return { kind: "failed", error };
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
function createClaimHeartbeat(options, runtime, message, activeUntil) {
|
|
886
|
+
const controller = new AbortController();
|
|
887
|
+
let stopped = false;
|
|
888
|
+
let lockedUntil = cloneDate(message.lockedUntil);
|
|
889
|
+
let renewing = false;
|
|
890
|
+
let firstRenewalError;
|
|
891
|
+
let terminalFailure;
|
|
892
|
+
let resolveLost = () => { };
|
|
893
|
+
const lost = new Promise((resolve) => {
|
|
894
|
+
resolveLost = resolve;
|
|
895
|
+
});
|
|
896
|
+
const fail = (failure) => {
|
|
897
|
+
if (terminalFailure)
|
|
898
|
+
return;
|
|
899
|
+
terminalFailure = failure;
|
|
900
|
+
stopped = true;
|
|
901
|
+
controller.abort();
|
|
902
|
+
resolveLost(failure);
|
|
903
|
+
};
|
|
904
|
+
const run = async () => {
|
|
905
|
+
try {
|
|
906
|
+
let delayMs = runtime.heartbeatMs;
|
|
907
|
+
while (!stopped) {
|
|
908
|
+
try {
|
|
909
|
+
await runtime.wait(delayMs, controller.signal);
|
|
910
|
+
}
|
|
911
|
+
catch (error) {
|
|
912
|
+
if (stopped || controller.signal.aborted)
|
|
913
|
+
return;
|
|
914
|
+
fail({
|
|
915
|
+
error: new OutboxLeaseLostError({
|
|
916
|
+
id: message.id,
|
|
917
|
+
message: `Outbox heartbeat scheduling failed for message "${message.id}".`,
|
|
918
|
+
cause: error,
|
|
919
|
+
}),
|
|
920
|
+
message,
|
|
921
|
+
operation: "renewClaim",
|
|
922
|
+
state: "lost",
|
|
923
|
+
confirmedLost: false,
|
|
924
|
+
});
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
if (stopped || controller.signal.aborted)
|
|
928
|
+
return;
|
|
929
|
+
const renewalAt = readOutboxNow(runtime.now);
|
|
930
|
+
const renewalDeadline = new Date(Math.min(lockedUntil.getTime(), activeUntil.getTime()));
|
|
931
|
+
const activeDeadlineEndsFirst = activeUntil.getTime() <= lockedUntil.getTime();
|
|
932
|
+
if (renewalAt.getTime() >= renewalDeadline.getTime()) {
|
|
933
|
+
fail({
|
|
934
|
+
error: activeDeadlineEndsFirst
|
|
935
|
+
? new OutboxMaxActiveDurationError({
|
|
936
|
+
id: message.id,
|
|
937
|
+
maxActiveMs: runtime.maxActiveMs,
|
|
938
|
+
})
|
|
939
|
+
: new OutboxLeaseLostError({ id: message.id }),
|
|
940
|
+
message,
|
|
941
|
+
operation: activeDeadlineEndsFirst
|
|
942
|
+
? "maxActiveDuration"
|
|
943
|
+
: "renewClaim",
|
|
944
|
+
state: "lost",
|
|
945
|
+
confirmedLost: !activeDeadlineEndsFirst,
|
|
946
|
+
});
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
renewing = true;
|
|
950
|
+
let renewal;
|
|
951
|
+
try {
|
|
952
|
+
renewal = await runBoundedClaimRenewal({
|
|
953
|
+
outbox: options.outbox,
|
|
954
|
+
message,
|
|
955
|
+
runtime,
|
|
956
|
+
renewalAt,
|
|
957
|
+
deadline: renewalDeadline,
|
|
958
|
+
signal: controller.signal,
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
catch (error) {
|
|
962
|
+
renewal = { kind: "failed", error };
|
|
963
|
+
}
|
|
964
|
+
finally {
|
|
965
|
+
renewing = false;
|
|
966
|
+
}
|
|
967
|
+
if (renewal.kind === "stopped")
|
|
968
|
+
return;
|
|
969
|
+
if (renewal.kind === "waitFailed") {
|
|
970
|
+
fail({
|
|
971
|
+
error: new OutboxLeaseLostError({
|
|
972
|
+
id: message.id,
|
|
973
|
+
message: `Could not enforce the renewal deadline for outbox message "${message.id}".`,
|
|
974
|
+
cause: renewal.error,
|
|
975
|
+
}),
|
|
976
|
+
message,
|
|
977
|
+
operation: "renewClaim",
|
|
978
|
+
state: "lost",
|
|
979
|
+
confirmedLost: false,
|
|
980
|
+
});
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
if (renewal.kind === "deadline") {
|
|
984
|
+
fail({
|
|
985
|
+
error: activeDeadlineEndsFirst
|
|
986
|
+
? new OutboxMaxActiveDurationError({
|
|
987
|
+
id: message.id,
|
|
988
|
+
maxActiveMs: runtime.maxActiveMs,
|
|
989
|
+
})
|
|
990
|
+
: new OutboxLeaseLostError({
|
|
991
|
+
id: message.id,
|
|
992
|
+
message: `Outbox claim renewal for message "${message.id}" did not complete before the lease deadline.`,
|
|
993
|
+
}),
|
|
994
|
+
message,
|
|
995
|
+
operation: activeDeadlineEndsFirst
|
|
996
|
+
? "maxActiveDuration"
|
|
997
|
+
: "renewClaim",
|
|
998
|
+
state: "lost",
|
|
999
|
+
confirmedLost: false,
|
|
1000
|
+
});
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
if (renewal.kind === "succeeded") {
|
|
1004
|
+
lockedUntil = renewal.lockedUntil;
|
|
1005
|
+
delayMs = runtime.heartbeatMs;
|
|
1006
|
+
continue;
|
|
1007
|
+
}
|
|
1008
|
+
const error = renewal.error;
|
|
1009
|
+
if (error instanceof OutboxClaimError) {
|
|
1010
|
+
fail({
|
|
1011
|
+
error,
|
|
1012
|
+
message,
|
|
1013
|
+
operation: "renewClaim",
|
|
1014
|
+
state: "lost",
|
|
1015
|
+
confirmedLost: true,
|
|
1016
|
+
});
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
firstRenewalError ??= error;
|
|
1020
|
+
if (stopped)
|
|
1021
|
+
return;
|
|
1022
|
+
const retryAt = readOutboxNow(runtime.now);
|
|
1023
|
+
const remainingMs = renewalDeadline.getTime() - retryAt.getTime();
|
|
1024
|
+
if (remainingMs <= 0) {
|
|
1025
|
+
fail({
|
|
1026
|
+
error: activeDeadlineEndsFirst
|
|
1027
|
+
? new OutboxMaxActiveDurationError({
|
|
1028
|
+
id: message.id,
|
|
1029
|
+
maxActiveMs: runtime.maxActiveMs,
|
|
1030
|
+
})
|
|
1031
|
+
: new OutboxLeaseLostError({
|
|
1032
|
+
id: message.id,
|
|
1033
|
+
message: `Outbox claim renewal for message "${message.id}" did not recover before the lease expired.`,
|
|
1034
|
+
cause: error,
|
|
1035
|
+
}),
|
|
1036
|
+
message,
|
|
1037
|
+
operation: activeDeadlineEndsFirst
|
|
1038
|
+
? "maxActiveDuration"
|
|
1039
|
+
: "renewClaim",
|
|
1040
|
+
state: "lost",
|
|
1041
|
+
confirmedLost: false,
|
|
1042
|
+
});
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
delayMs = Math.max(1, Math.min(runtime.heartbeatMs, Math.floor(remainingMs / 3)));
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
catch (error) {
|
|
1049
|
+
if (stopped || controller.signal.aborted)
|
|
1050
|
+
return;
|
|
1051
|
+
fail({
|
|
1052
|
+
error: new OutboxLeaseLostError({
|
|
1053
|
+
id: message.id,
|
|
1054
|
+
message: `Outbox claim renewal failed unexpectedly for message "${message.id}".`,
|
|
1055
|
+
cause: error,
|
|
1056
|
+
}),
|
|
1057
|
+
message,
|
|
1058
|
+
operation: "renewClaim",
|
|
1059
|
+
state: "lost",
|
|
1060
|
+
confirmedLost: false,
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
};
|
|
1064
|
+
const running = run();
|
|
1065
|
+
const stop = async (abortRenewal = true) => {
|
|
1066
|
+
stopped = true;
|
|
1067
|
+
if (abortRenewal || !renewing)
|
|
1068
|
+
controller.abort();
|
|
1069
|
+
await running;
|
|
1070
|
+
controller.abort();
|
|
1071
|
+
};
|
|
1072
|
+
return {
|
|
1073
|
+
lost,
|
|
1074
|
+
stop: () => stop(true),
|
|
1075
|
+
async stopAndExtend() {
|
|
1076
|
+
await stop(false);
|
|
1077
|
+
if (terminalFailure) {
|
|
1078
|
+
return { lockedUntil, failure: terminalFailure };
|
|
1079
|
+
}
|
|
1080
|
+
const renewalAt = readOutboxNow(runtime.now);
|
|
1081
|
+
const activeDeadlineEndsFirst = activeUntil.getTime() <= lockedUntil.getTime();
|
|
1082
|
+
const finalRenewalDeadline = new Date(Math.min(lockedUntil.getTime(), activeUntil.getTime()));
|
|
1083
|
+
if (renewalAt.getTime() >= finalRenewalDeadline.getTime()) {
|
|
1084
|
+
return {
|
|
1085
|
+
lockedUntil,
|
|
1086
|
+
failure: {
|
|
1087
|
+
error: activeDeadlineEndsFirst
|
|
1088
|
+
? new OutboxMaxActiveDurationError({
|
|
1089
|
+
id: message.id,
|
|
1090
|
+
maxActiveMs: runtime.maxActiveMs,
|
|
1091
|
+
})
|
|
1092
|
+
: new OutboxLeaseLostError({ id: message.id }),
|
|
1093
|
+
message,
|
|
1094
|
+
operation: activeDeadlineEndsFirst
|
|
1095
|
+
? "maxActiveDuration"
|
|
1096
|
+
: "renewClaim",
|
|
1097
|
+
state: "lost",
|
|
1098
|
+
confirmedLost: !activeDeadlineEndsFirst,
|
|
1099
|
+
},
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
const finalController = new AbortController();
|
|
1103
|
+
const renewal = await runBoundedClaimRenewal({
|
|
1104
|
+
outbox: options.outbox,
|
|
1105
|
+
message,
|
|
1106
|
+
runtime,
|
|
1107
|
+
renewalAt,
|
|
1108
|
+
deadline: finalRenewalDeadline,
|
|
1109
|
+
signal: finalController.signal,
|
|
1110
|
+
});
|
|
1111
|
+
finalController.abort();
|
|
1112
|
+
if (renewal.kind === "succeeded") {
|
|
1113
|
+
return {
|
|
1114
|
+
lockedUntil: renewal.lockedUntil,
|
|
1115
|
+
renewalError: firstRenewalError,
|
|
1116
|
+
renewalRecovered: firstRenewalError === undefined ? undefined : true,
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
if (renewal.kind === "failed") {
|
|
1120
|
+
if (!(renewal.error instanceof OutboxClaimError)) {
|
|
1121
|
+
const failedAt = readOutboxNow(runtime.now);
|
|
1122
|
+
if (failedAt.getTime() < lockedUntil.getTime() &&
|
|
1123
|
+
failedAt.getTime() < activeUntil.getTime()) {
|
|
1124
|
+
return {
|
|
1125
|
+
lockedUntil,
|
|
1126
|
+
renewalError: renewal.error,
|
|
1127
|
+
renewalRecovered: false,
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
if (activeDeadlineEndsFirst &&
|
|
1131
|
+
failedAt.getTime() >= activeUntil.getTime()) {
|
|
1132
|
+
return {
|
|
1133
|
+
lockedUntil,
|
|
1134
|
+
failure: {
|
|
1135
|
+
error: new OutboxMaxActiveDurationError({
|
|
1136
|
+
id: message.id,
|
|
1137
|
+
maxActiveMs: runtime.maxActiveMs,
|
|
1138
|
+
}),
|
|
1139
|
+
message,
|
|
1140
|
+
operation: "maxActiveDuration",
|
|
1141
|
+
state: "lost",
|
|
1142
|
+
confirmedLost: false,
|
|
1143
|
+
},
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
return {
|
|
1148
|
+
lockedUntil,
|
|
1149
|
+
failure: {
|
|
1150
|
+
error: renewal.error,
|
|
1151
|
+
message,
|
|
1152
|
+
operation: "renewClaim",
|
|
1153
|
+
state: "lost",
|
|
1154
|
+
confirmedLost: renewal.error instanceof OutboxClaimError,
|
|
1155
|
+
},
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
return {
|
|
1159
|
+
lockedUntil,
|
|
1160
|
+
failure: {
|
|
1161
|
+
error: renewal.kind === "waitFailed"
|
|
1162
|
+
? new OutboxLeaseLostError({
|
|
1163
|
+
id: message.id,
|
|
1164
|
+
message: activeDeadlineEndsFirst
|
|
1165
|
+
? `Could not enforce the maximum active duration for outbox message "${message.id}".`
|
|
1166
|
+
: `Could not enforce the final renewal deadline for outbox message "${message.id}".`,
|
|
1167
|
+
cause: renewal.error,
|
|
1168
|
+
})
|
|
1169
|
+
: activeDeadlineEndsFirst
|
|
1170
|
+
? new OutboxMaxActiveDurationError({
|
|
1171
|
+
id: message.id,
|
|
1172
|
+
maxActiveMs: runtime.maxActiveMs,
|
|
1173
|
+
})
|
|
1174
|
+
: new OutboxLeaseLostError({
|
|
1175
|
+
id: message.id,
|
|
1176
|
+
message: `Outbox claim renewal for message "${message.id}" did not complete before settlement.`,
|
|
1177
|
+
}),
|
|
1178
|
+
message,
|
|
1179
|
+
operation: activeDeadlineEndsFirst
|
|
1180
|
+
? "maxActiveDuration"
|
|
1181
|
+
: "renewClaim",
|
|
1182
|
+
state: "lost",
|
|
1183
|
+
confirmedLost: false,
|
|
1184
|
+
},
|
|
1185
|
+
};
|
|
1186
|
+
},
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
async function settleClaim(options) {
|
|
1190
|
+
let lastError;
|
|
1191
|
+
for (let attempt = 1; attempt <= OUTBOX_SETTLEMENT_ATTEMPTS; attempt += 1) {
|
|
1192
|
+
const settlementAt = readOutboxNow(options.runtime.now);
|
|
1193
|
+
const settlementDeadline = new Date(Math.min(options.lockedUntil.getTime(), options.activeUntil.getTime()));
|
|
1194
|
+
const settlement = await runBoundedSettlement({
|
|
1195
|
+
runtime: options.runtime,
|
|
1196
|
+
settlementAt,
|
|
1197
|
+
deadline: settlementDeadline,
|
|
1198
|
+
settle: options.settle,
|
|
1199
|
+
});
|
|
1200
|
+
if (settlement.kind === "succeeded") {
|
|
1201
|
+
return { ok: true };
|
|
1202
|
+
}
|
|
1203
|
+
if (settlement.kind === "deadline") {
|
|
1204
|
+
return {
|
|
1205
|
+
ok: false,
|
|
1206
|
+
error: new OutboxLeaseLostError({
|
|
1207
|
+
id: options.message.id,
|
|
1208
|
+
message: `Outbox ${options.operation} for message "${options.message.id}" did not complete before its settlement deadline.`,
|
|
1209
|
+
}),
|
|
1210
|
+
claimLost: true,
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
if (settlement.kind === "waitFailed") {
|
|
1214
|
+
return { ok: false, error: settlement.error, claimLost: false };
|
|
1215
|
+
}
|
|
1216
|
+
const error = settlement.error;
|
|
1217
|
+
lastError = error;
|
|
1218
|
+
if (error instanceof OutboxClaimError) {
|
|
1219
|
+
return { ok: false, error, claimLost: true };
|
|
1220
|
+
}
|
|
1221
|
+
const remainingMs = settlementDeadline.getTime() -
|
|
1222
|
+
readOutboxNow(options.runtime.now).getTime();
|
|
1223
|
+
if (attempt >= OUTBOX_SETTLEMENT_ATTEMPTS || remainingMs <= 0)
|
|
1224
|
+
break;
|
|
1225
|
+
const waitController = new AbortController();
|
|
1226
|
+
try {
|
|
1227
|
+
await options.runtime.wait(Math.max(1, Math.min(OUTBOX_SETTLEMENT_RETRY_DELAY_MS * 2 ** (attempt - 1), remainingMs)), waitController.signal);
|
|
1228
|
+
}
|
|
1229
|
+
catch (error) {
|
|
1230
|
+
return { ok: false, error, claimLost: false };
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
return { ok: false, error: lastError, claimLost: false };
|
|
1234
|
+
}
|
|
1235
|
+
async function runBoundedSettlement(options) {
|
|
1236
|
+
const remainingMs = options.deadline.getTime() - options.settlementAt.getTime();
|
|
1237
|
+
if (remainingMs <= 0)
|
|
1238
|
+
return { kind: "deadline" };
|
|
1239
|
+
const waitController = new AbortController();
|
|
1240
|
+
const settlement = Promise.resolve()
|
|
1241
|
+
.then(() => options.settle(options.settlementAt))
|
|
1242
|
+
.then(() => ({ kind: "succeeded" }), (error) => ({ kind: "failed", error }));
|
|
1243
|
+
const deadline = Promise.resolve()
|
|
1244
|
+
.then(() => options.runtime.wait(remainingMs, waitController.signal))
|
|
1245
|
+
.then(() => ({ kind: "deadline" }), (error) => ({ kind: "waitFailed", error }));
|
|
1246
|
+
const result = await Promise.race([settlement, deadline]);
|
|
1247
|
+
waitController.abort();
|
|
1248
|
+
return result;
|
|
1249
|
+
}
|
|
1250
|
+
async function recordDeadLetter(options, instrumentation, jobInstrumentation, message, error, details = {}) {
|
|
1251
|
+
try {
|
|
1252
|
+
await options.onDeadLetter?.(error, message);
|
|
1253
|
+
}
|
|
1254
|
+
catch {
|
|
1255
|
+
// Dead-letter observers must not change the settled message state.
|
|
1256
|
+
}
|
|
1257
|
+
instrumentation.record({
|
|
1258
|
+
type: "outbox",
|
|
1259
|
+
...options.instrumentationContext,
|
|
1260
|
+
messageId: message.id,
|
|
1261
|
+
messageKind: message.kind,
|
|
1262
|
+
messageName: message.name,
|
|
1263
|
+
status: "deadLettered",
|
|
1264
|
+
details: outboxInstrumentationDetails(message, {
|
|
1265
|
+
...details,
|
|
1266
|
+
error: serializeOutboxError(error),
|
|
1267
|
+
}),
|
|
1268
|
+
});
|
|
1269
|
+
if (message.kind === "job") {
|
|
1270
|
+
jobInstrumentation.record({
|
|
1271
|
+
type: "job",
|
|
1272
|
+
...options.instrumentationContext,
|
|
1273
|
+
jobName: message.name,
|
|
1274
|
+
status: "deadLettered",
|
|
1275
|
+
details: outboxInstrumentationDetails(message, {
|
|
1276
|
+
...details,
|
|
1277
|
+
error: serializeOutboxError(error),
|
|
1278
|
+
}),
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
async function processClaimedMessage(options, runtime, instrumentation, jobInstrumentation, tracing, message) {
|
|
1283
|
+
const emptyOutcome = () => ({
|
|
1284
|
+
delivered: 0,
|
|
1285
|
+
retried: 0,
|
|
1286
|
+
deadLettered: 0,
|
|
1287
|
+
settlementFailed: 0,
|
|
1288
|
+
leaseLost: 0,
|
|
1289
|
+
});
|
|
1290
|
+
const outcome = emptyOutcome();
|
|
1291
|
+
const startedAt = readOutboxNow(runtime.now);
|
|
1292
|
+
const activeUntil = new Date(startedAt.getTime() + runtime.maxActiveMs);
|
|
1293
|
+
const heartbeat = createClaimHeartbeat(options, runtime, message, activeUntil);
|
|
1294
|
+
const lifetimeController = new AbortController();
|
|
1295
|
+
const parentTrace = parseTraceCarrier(message.trace);
|
|
1296
|
+
const traceAttributes = {
|
|
1297
|
+
"beignet.outbox.message_kind": message.kind,
|
|
1298
|
+
"beignet.outbox.message_name": message.name,
|
|
1299
|
+
};
|
|
1300
|
+
const delivery = Promise.resolve()
|
|
1301
|
+
.then(() => runWithTracing(tracing, {
|
|
1302
|
+
name: `beignet.outbox deliver ${message.name}`,
|
|
1303
|
+
type: "outbox",
|
|
1304
|
+
kind: "consumer",
|
|
1305
|
+
parent: parentTrace,
|
|
1306
|
+
attributes: traceAttributes,
|
|
1307
|
+
metricAttributes: traceAttributes,
|
|
1308
|
+
}, (span) => deliverOutboxMessage(options, message, captureTraceCarrier(span?.context ?? parentTrace))))
|
|
1309
|
+
.then(() => ({ kind: "succeeded" }), (error) => ({ kind: "failed", error }));
|
|
1310
|
+
const maximumActive = Promise.resolve()
|
|
1311
|
+
.then(() => runtime.wait(runtime.maxActiveMs, lifetimeController.signal))
|
|
1312
|
+
.then(() => ({ kind: "maxActive" }), (error) => ({ kind: "waitFailed", error }));
|
|
1313
|
+
const leaseLost = heartbeat.lost.then((failure) => ({
|
|
1314
|
+
kind: "leaseLost",
|
|
1315
|
+
failure,
|
|
1316
|
+
}));
|
|
1317
|
+
const deliveryOutcome = await Promise.race([
|
|
1318
|
+
delivery,
|
|
1319
|
+
maximumActive,
|
|
1320
|
+
leaseLost,
|
|
1321
|
+
]);
|
|
1322
|
+
lifetimeController.abort();
|
|
1323
|
+
if (deliveryOutcome.kind === "maxActive") {
|
|
1324
|
+
await heartbeat.stop();
|
|
1325
|
+
const failure = {
|
|
1326
|
+
error: new OutboxMaxActiveDurationError({
|
|
1327
|
+
id: message.id,
|
|
1328
|
+
maxActiveMs: runtime.maxActiveMs,
|
|
1329
|
+
}),
|
|
1330
|
+
message,
|
|
1331
|
+
operation: "maxActiveDuration",
|
|
1332
|
+
state: "lost",
|
|
1333
|
+
confirmedLost: false,
|
|
1334
|
+
};
|
|
1335
|
+
await notifyLeaseFailure(options, failure);
|
|
1336
|
+
instrumentation.custom({
|
|
1337
|
+
name: "outbox.lease.lost",
|
|
1338
|
+
label: "Outbox claim no longer confirmed",
|
|
1339
|
+
summary: `Stopped renewing ${message.kind} "${message.name}" after its active-delivery limit`,
|
|
1340
|
+
details: outboxInstrumentationDetails(message, {
|
|
1341
|
+
operation: failure.operation,
|
|
1342
|
+
state: failure.state,
|
|
1343
|
+
error: serializeOutboxError(failure.error),
|
|
1344
|
+
}),
|
|
1345
|
+
});
|
|
1346
|
+
outcome.leaseLost = 1;
|
|
1347
|
+
return outcome;
|
|
1348
|
+
}
|
|
1349
|
+
if (deliveryOutcome.kind === "waitFailed") {
|
|
1350
|
+
await heartbeat.stop();
|
|
1351
|
+
const failure = {
|
|
1352
|
+
error: new OutboxLeaseLostError({
|
|
1353
|
+
id: message.id,
|
|
1354
|
+
message: `Could not enforce the active-delivery limit for outbox message "${message.id}".`,
|
|
1355
|
+
cause: deliveryOutcome.error,
|
|
1356
|
+
}),
|
|
1357
|
+
message,
|
|
1358
|
+
operation: "maxActiveDuration",
|
|
1359
|
+
state: "lost",
|
|
1360
|
+
confirmedLost: false,
|
|
1361
|
+
};
|
|
1362
|
+
await notifyLeaseFailure(options, failure);
|
|
1363
|
+
instrumentation.custom({
|
|
1364
|
+
name: "outbox.lease.lost",
|
|
1365
|
+
label: "Outbox claim no longer confirmed",
|
|
1366
|
+
summary: `Could not enforce the active-delivery limit for ${message.kind} "${message.name}"`,
|
|
1367
|
+
details: outboxInstrumentationDetails(message, {
|
|
1368
|
+
operation: failure.operation,
|
|
1369
|
+
state: failure.state,
|
|
1370
|
+
error: serializeOutboxError(failure.error),
|
|
1371
|
+
}),
|
|
1372
|
+
});
|
|
1373
|
+
outcome.leaseLost = 1;
|
|
1374
|
+
return outcome;
|
|
1375
|
+
}
|
|
1376
|
+
if (deliveryOutcome.kind === "leaseLost") {
|
|
1377
|
+
await heartbeat.stop();
|
|
1378
|
+
await notifyLeaseFailure(options, deliveryOutcome.failure);
|
|
1379
|
+
instrumentation.custom({
|
|
1380
|
+
name: "outbox.lease.lost",
|
|
1381
|
+
label: "Outbox claim lost",
|
|
1382
|
+
summary: `Could not keep the claim for ${message.kind} "${message.name}" active`,
|
|
1383
|
+
details: outboxInstrumentationDetails(message, {
|
|
1384
|
+
operation: deliveryOutcome.failure.operation,
|
|
1385
|
+
state: deliveryOutcome.failure.state,
|
|
1386
|
+
confirmedLost: deliveryOutcome.failure.confirmedLost,
|
|
1387
|
+
error: serializeOutboxError(deliveryOutcome.failure.error),
|
|
1388
|
+
}),
|
|
1389
|
+
});
|
|
1390
|
+
outcome.leaseLost = 1;
|
|
1391
|
+
return outcome;
|
|
1392
|
+
}
|
|
1393
|
+
const lease = await heartbeat.stopAndExtend();
|
|
1394
|
+
if (lease.failure) {
|
|
1395
|
+
await notifyLeaseFailure(options, lease.failure);
|
|
1396
|
+
instrumentation.custom({
|
|
1397
|
+
name: "outbox.lease.lost",
|
|
1398
|
+
label: "Outbox claim lost",
|
|
1399
|
+
summary: `Could not confirm the claim for ${message.kind} "${message.name}" before settlement`,
|
|
1400
|
+
details: outboxInstrumentationDetails(message, {
|
|
1401
|
+
operation: lease.failure.operation,
|
|
1402
|
+
state: lease.failure.state,
|
|
1403
|
+
confirmedLost: lease.failure.confirmedLost,
|
|
1404
|
+
error: serializeOutboxError(lease.failure.error),
|
|
1405
|
+
}),
|
|
1406
|
+
});
|
|
1407
|
+
outcome.leaseLost = 1;
|
|
1408
|
+
return outcome;
|
|
1409
|
+
}
|
|
1410
|
+
const reportRenewalOutcome = async () => {
|
|
1411
|
+
if (lease.renewalError === undefined)
|
|
1412
|
+
return;
|
|
1413
|
+
const failure = {
|
|
1414
|
+
error: lease.renewalError,
|
|
1415
|
+
message,
|
|
1416
|
+
operation: "renewClaim",
|
|
1417
|
+
state: lease.renewalRecovered ? "recovered" : "degraded",
|
|
1418
|
+
confirmedLost: false,
|
|
1419
|
+
};
|
|
1420
|
+
await notifyLeaseFailure(options, failure);
|
|
1421
|
+
instrumentation.custom({
|
|
1422
|
+
name: lease.renewalRecovered
|
|
1423
|
+
? "outbox.lease.renewal.recovered"
|
|
1424
|
+
: "outbox.lease.renewal.degraded",
|
|
1425
|
+
label: lease.renewalRecovered
|
|
1426
|
+
? "Outbox claim renewal recovered"
|
|
1427
|
+
: "Outbox claim renewal degraded",
|
|
1428
|
+
summary: lease.renewalRecovered
|
|
1429
|
+
? `Recovered claim renewal for ${message.kind} "${message.name}"`
|
|
1430
|
+
: `Continued ${message.kind} "${message.name}" settlement under its last confirmed lease`,
|
|
1431
|
+
details: outboxInstrumentationDetails(message, {
|
|
1432
|
+
state: failure.state,
|
|
1433
|
+
error: serializeOutboxError(lease.renewalError),
|
|
1434
|
+
}),
|
|
1435
|
+
});
|
|
1436
|
+
};
|
|
1437
|
+
if (deliveryOutcome.kind === "succeeded") {
|
|
1438
|
+
const settlement = await settleClaim({
|
|
1439
|
+
operation: "markDelivered",
|
|
1440
|
+
message,
|
|
1441
|
+
lockedUntil: lease.lockedUntil,
|
|
1442
|
+
activeUntil,
|
|
1443
|
+
runtime,
|
|
1444
|
+
settle: (now) => options.outbox.markDelivered({
|
|
1445
|
+
id: message.id,
|
|
1446
|
+
claimToken: message.claimToken,
|
|
1447
|
+
now,
|
|
1448
|
+
}),
|
|
1449
|
+
});
|
|
1450
|
+
await reportRenewalOutcome();
|
|
1451
|
+
if (!settlement.ok) {
|
|
1452
|
+
const failure = {
|
|
1453
|
+
error: settlement.error,
|
|
1454
|
+
message,
|
|
1455
|
+
operation: "markDelivered",
|
|
1456
|
+
deliverySucceeded: true,
|
|
1457
|
+
};
|
|
1458
|
+
await notifySettlementFailure(options, failure);
|
|
1459
|
+
instrumentation.custom({
|
|
1460
|
+
name: "outbox.settlement.failed",
|
|
1461
|
+
label: "Outbox settlement failed",
|
|
1462
|
+
summary: `Delivered ${message.kind} "${message.name}", but could not confirm its durable acknowledgement`,
|
|
1463
|
+
details: outboxInstrumentationDetails(message, {
|
|
1464
|
+
operation: failure.operation,
|
|
1465
|
+
deliverySucceeded: true,
|
|
1466
|
+
settlementError: serializeOutboxError(settlement.error),
|
|
1467
|
+
}),
|
|
1468
|
+
});
|
|
1469
|
+
outcome.settlementFailed = 1;
|
|
1470
|
+
if (settlement.claimLost)
|
|
1471
|
+
outcome.leaseLost = 1;
|
|
1472
|
+
return outcome;
|
|
1473
|
+
}
|
|
1474
|
+
instrumentation.record({
|
|
1475
|
+
type: "outbox",
|
|
1476
|
+
...options.instrumentationContext,
|
|
1477
|
+
messageId: message.id,
|
|
1478
|
+
messageKind: message.kind,
|
|
1479
|
+
messageName: message.name,
|
|
1480
|
+
status: "delivered",
|
|
1481
|
+
details: outboxInstrumentationDetails(message),
|
|
1482
|
+
});
|
|
1483
|
+
outcome.delivered = 1;
|
|
1484
|
+
return outcome;
|
|
1485
|
+
}
|
|
1486
|
+
const deliveryError = deliveryOutcome.error;
|
|
1487
|
+
const failedAt = readOutboxNow(runtime.now);
|
|
1488
|
+
const shouldRetry = shouldRetryOutboxMessage(options, message, deliveryError);
|
|
1489
|
+
const deadLetter = !shouldRetry;
|
|
1490
|
+
const retryDelayMs = deadLetter
|
|
1491
|
+
? 0
|
|
1492
|
+
: resolveRetryDelayMs(options, message, deliveryError, failedAt);
|
|
1493
|
+
const retryAt = deadLetter
|
|
1494
|
+
? undefined
|
|
1495
|
+
: new Date(failedAt.getTime() + retryDelayMs);
|
|
1496
|
+
const settlement = await settleClaim({
|
|
1497
|
+
operation: "markFailed",
|
|
1498
|
+
message,
|
|
1499
|
+
lockedUntil: lease.lockedUntil,
|
|
1500
|
+
activeUntil,
|
|
1501
|
+
runtime,
|
|
1502
|
+
settle: (now) => options.outbox.markFailed({
|
|
1503
|
+
id: message.id,
|
|
1504
|
+
claimToken: message.claimToken,
|
|
1505
|
+
error: deliveryError,
|
|
1506
|
+
deadLetter,
|
|
1507
|
+
now,
|
|
1508
|
+
retryAt,
|
|
1509
|
+
}),
|
|
1510
|
+
});
|
|
1511
|
+
await reportRenewalOutcome();
|
|
1512
|
+
try {
|
|
1513
|
+
await options.onError?.(deliveryError, message);
|
|
1514
|
+
}
|
|
1515
|
+
catch {
|
|
1516
|
+
// Delivery observers cannot change retry, dead-letter, or recovery state.
|
|
1517
|
+
}
|
|
1518
|
+
if (!settlement.ok) {
|
|
1519
|
+
const failure = {
|
|
1520
|
+
error: settlement.error,
|
|
1521
|
+
message,
|
|
1522
|
+
operation: "markFailed",
|
|
1523
|
+
deliverySucceeded: false,
|
|
1524
|
+
deliveryError,
|
|
1525
|
+
};
|
|
1526
|
+
await notifySettlementFailure(options, failure);
|
|
1527
|
+
instrumentation.custom({
|
|
1528
|
+
name: "outbox.settlement.failed",
|
|
1529
|
+
label: "Outbox settlement failed",
|
|
1530
|
+
summary: `Could not settle failed ${message.kind} "${message.name}"`,
|
|
1531
|
+
details: outboxInstrumentationDetails(message, {
|
|
1532
|
+
operation: failure.operation,
|
|
1533
|
+
deliverySucceeded: false,
|
|
1534
|
+
deliveryError: serializeOutboxError(deliveryError),
|
|
1535
|
+
settlementError: serializeOutboxError(settlement.error),
|
|
1536
|
+
}),
|
|
1537
|
+
});
|
|
1538
|
+
outcome.settlementFailed = 1;
|
|
1539
|
+
if (settlement.claimLost)
|
|
1540
|
+
outcome.leaseLost = 1;
|
|
1541
|
+
return outcome;
|
|
1542
|
+
}
|
|
1543
|
+
if (deadLetter) {
|
|
1544
|
+
await recordDeadLetter(options, instrumentation, jobInstrumentation, message, deliveryError);
|
|
1545
|
+
outcome.deadLettered = 1;
|
|
1546
|
+
return outcome;
|
|
1547
|
+
}
|
|
1548
|
+
instrumentation.record({
|
|
1549
|
+
type: "outbox",
|
|
1550
|
+
...options.instrumentationContext,
|
|
1551
|
+
messageId: message.id,
|
|
1552
|
+
messageKind: message.kind,
|
|
1553
|
+
messageName: message.name,
|
|
1554
|
+
status: "retryScheduled",
|
|
1555
|
+
details: outboxInstrumentationDetails(message, {
|
|
1556
|
+
retryDelayMs,
|
|
1557
|
+
retryAt: retryAt?.toISOString(),
|
|
1558
|
+
error: serializeOutboxError(deliveryError),
|
|
1559
|
+
}),
|
|
1560
|
+
});
|
|
1561
|
+
if (message.kind === "job") {
|
|
1562
|
+
jobInstrumentation.record({
|
|
1563
|
+
type: "job",
|
|
1564
|
+
...options.instrumentationContext,
|
|
1565
|
+
jobName: message.name,
|
|
1566
|
+
status: "retryScheduled",
|
|
1567
|
+
details: outboxInstrumentationDetails(message, {
|
|
1568
|
+
retryDelayMs,
|
|
1569
|
+
retryAt: retryAt?.toISOString(),
|
|
1570
|
+
error: serializeOutboxError(deliveryError),
|
|
1571
|
+
}),
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
outcome.retried = 1;
|
|
1575
|
+
return outcome;
|
|
1576
|
+
}
|
|
643
1577
|
/**
|
|
644
|
-
* Claim and deliver one
|
|
1578
|
+
* Claim and deliver one bounded set of outbox messages.
|
|
645
1579
|
*
|
|
646
|
-
*
|
|
647
|
-
*
|
|
648
|
-
*
|
|
649
|
-
*
|
|
650
|
-
* then dead-lettered.
|
|
1580
|
+
* The drain claims only enough messages to fill active delivery slots and
|
|
1581
|
+
* renews each active claim until delivery settles or reaches its configured
|
|
1582
|
+
* maximum duration. It remains an at-least-once transport: a process can still
|
|
1583
|
+
* terminate after the external effect succeeds but before acknowledgement.
|
|
651
1584
|
*/
|
|
652
1585
|
export async function drainOutbox(options) {
|
|
653
|
-
const
|
|
654
|
-
|
|
1586
|
+
const runtime = resolveDrainRuntime(options);
|
|
1587
|
+
assertOutboxDrainPort(options.outbox);
|
|
655
1588
|
assertOutboxDeliveryCapabilities(options);
|
|
656
1589
|
const instrumentation = createProviderInstrumentation(options.instrumentation, {
|
|
657
1590
|
providerName: "outbox",
|
|
@@ -662,157 +1595,81 @@ export async function drainOutbox(options) {
|
|
|
662
1595
|
watcher: "jobs",
|
|
663
1596
|
});
|
|
664
1597
|
const tracing = resolveTracingPort(options.instrumentation);
|
|
665
|
-
const now = options.now ?? new Date();
|
|
666
|
-
const messages = await options.outbox.claimBatch({
|
|
667
|
-
limit: batchSize,
|
|
668
|
-
now,
|
|
669
|
-
leaseMs: options.leaseMs,
|
|
670
|
-
});
|
|
671
1598
|
const result = {
|
|
672
|
-
claimed:
|
|
1599
|
+
claimed: 0,
|
|
673
1600
|
delivered: 0,
|
|
674
1601
|
retried: 0,
|
|
675
1602
|
deadLettered: 0,
|
|
1603
|
+
abandonedDeadLettered: 0,
|
|
1604
|
+
settlementFailed: 0,
|
|
1605
|
+
leaseLost: 0,
|
|
676
1606
|
};
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
await
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
parent: parentTrace,
|
|
689
|
-
attributes: traceAttributes,
|
|
690
|
-
metricAttributes: traceAttributes,
|
|
691
|
-
}, (span) => deliverOutboxMessage(options, message, captureTraceCarrier(span?.context ?? parentTrace)));
|
|
692
|
-
await options.outbox.markDelivered({
|
|
693
|
-
id: message.id,
|
|
694
|
-
claimToken: message.claimToken,
|
|
695
|
-
now,
|
|
1607
|
+
let remaining = runtime.batchSize;
|
|
1608
|
+
let sourceExhausted = false;
|
|
1609
|
+
while (remaining > 0 && !sourceExhausted) {
|
|
1610
|
+
const active = [];
|
|
1611
|
+
const abandoned = [];
|
|
1612
|
+
while (active.length < runtime.concurrency && remaining > 0) {
|
|
1613
|
+
const limit = Math.min(runtime.concurrency - active.length, remaining);
|
|
1614
|
+
const selected = await options.outbox.claimBatch({
|
|
1615
|
+
limit,
|
|
1616
|
+
now: readOutboxNow(runtime.now),
|
|
1617
|
+
leaseMs: runtime.leaseMs,
|
|
696
1618
|
});
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
messageKind: message.kind,
|
|
702
|
-
messageName: message.name,
|
|
703
|
-
status: "delivered",
|
|
704
|
-
details: outboxInstrumentationDetails(message),
|
|
705
|
-
});
|
|
706
|
-
result.delivered += 1;
|
|
707
|
-
}
|
|
708
|
-
catch (error) {
|
|
709
|
-
try {
|
|
710
|
-
await options.onError?.(error, message);
|
|
1619
|
+
const selectedCount = selected.claimed.length + selected.deadLettered.length;
|
|
1620
|
+
if (selectedCount === 0) {
|
|
1621
|
+
sourceExhausted = true;
|
|
1622
|
+
break;
|
|
711
1623
|
}
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
// dead-lettered even if the observer fails.
|
|
715
|
-
}
|
|
716
|
-
const shouldRetry = shouldRetryOutboxMessage(options, message, error);
|
|
717
|
-
const deadLetter = !shouldRetry;
|
|
718
|
-
const retryDelayMs = deadLetter
|
|
719
|
-
? 0
|
|
720
|
-
: resolveRetryDelayMs(options, message, error, now);
|
|
721
|
-
try {
|
|
722
|
-
await options.outbox.markFailed({
|
|
723
|
-
id: message.id,
|
|
724
|
-
claimToken: message.claimToken,
|
|
725
|
-
error,
|
|
726
|
-
deadLetter,
|
|
727
|
-
now,
|
|
728
|
-
retryAt: deadLetter
|
|
729
|
-
? undefined
|
|
730
|
-
: new Date(now.getTime() + retryDelayMs),
|
|
731
|
-
});
|
|
732
|
-
}
|
|
733
|
-
catch (settlementError) {
|
|
734
|
-
try {
|
|
735
|
-
await options.onSettlementError?.(settlementError, message, error);
|
|
736
|
-
}
|
|
737
|
-
catch {
|
|
738
|
-
// Preserve the settlement failure when its observer also fails.
|
|
739
|
-
}
|
|
740
|
-
instrumentation.custom({
|
|
741
|
-
name: "outbox.settlement.failed",
|
|
742
|
-
label: "Outbox settlement failed",
|
|
743
|
-
summary: `Could not settle failed ${message.kind} "${message.name}"`,
|
|
744
|
-
details: outboxInstrumentationDetails(message, {
|
|
745
|
-
deliveryError: serializeOutboxError(error),
|
|
746
|
-
settlementError: serializeOutboxError(settlementError),
|
|
747
|
-
}),
|
|
748
|
-
});
|
|
749
|
-
continue;
|
|
750
|
-
}
|
|
751
|
-
if (deadLetter) {
|
|
752
|
-
try {
|
|
753
|
-
await options.onDeadLetter?.(error, message);
|
|
754
|
-
}
|
|
755
|
-
catch {
|
|
756
|
-
// Dead-letter observers must not change the settled message state.
|
|
757
|
-
}
|
|
758
|
-
instrumentation.record({
|
|
759
|
-
type: "outbox",
|
|
760
|
-
...options.instrumentationContext,
|
|
761
|
-
messageId: message.id,
|
|
762
|
-
messageKind: message.kind,
|
|
763
|
-
messageName: message.name,
|
|
764
|
-
status: "deadLettered",
|
|
765
|
-
details: outboxInstrumentationDetails(message, {
|
|
766
|
-
error: serializeOutboxError(error),
|
|
767
|
-
}),
|
|
768
|
-
});
|
|
769
|
-
if (message.kind === "job") {
|
|
770
|
-
jobInstrumentation.record({
|
|
771
|
-
type: "job",
|
|
772
|
-
...options.instrumentationContext,
|
|
773
|
-
jobName: message.name,
|
|
774
|
-
status: "deadLettered",
|
|
775
|
-
details: outboxInstrumentationDetails(message, {
|
|
776
|
-
error: serializeOutboxError(error),
|
|
777
|
-
}),
|
|
778
|
-
});
|
|
779
|
-
}
|
|
780
|
-
result.deadLettered += 1;
|
|
781
|
-
}
|
|
782
|
-
else {
|
|
783
|
-
const retryAt = new Date(now.getTime() + retryDelayMs).toISOString();
|
|
784
|
-
instrumentation.record({
|
|
785
|
-
type: "outbox",
|
|
786
|
-
...options.instrumentationContext,
|
|
787
|
-
messageId: message.id,
|
|
788
|
-
messageKind: message.kind,
|
|
789
|
-
messageName: message.name,
|
|
790
|
-
status: "retryScheduled",
|
|
791
|
-
details: outboxInstrumentationDetails(message, {
|
|
792
|
-
retryDelayMs,
|
|
793
|
-
retryAt,
|
|
794
|
-
error: serializeOutboxError(error),
|
|
795
|
-
}),
|
|
796
|
-
});
|
|
797
|
-
if (message.kind === "job") {
|
|
798
|
-
jobInstrumentation.record({
|
|
799
|
-
type: "job",
|
|
800
|
-
...options.instrumentationContext,
|
|
801
|
-
jobName: message.name,
|
|
802
|
-
status: "retryScheduled",
|
|
803
|
-
details: outboxInstrumentationDetails(message, {
|
|
804
|
-
retryDelayMs,
|
|
805
|
-
retryAt,
|
|
806
|
-
error: serializeOutboxError(error),
|
|
807
|
-
}),
|
|
808
|
-
});
|
|
809
|
-
}
|
|
810
|
-
result.retried += 1;
|
|
1624
|
+
if (selectedCount > limit) {
|
|
1625
|
+
throw new Error(`Outbox claimBatch returned ${selectedCount} messages for a limit of ${limit}.`);
|
|
811
1626
|
}
|
|
1627
|
+
remaining -= selectedCount;
|
|
1628
|
+
result.claimed += selected.claimed.length;
|
|
1629
|
+
active.push(...selected.claimed);
|
|
1630
|
+
abandoned.push(...selected.deadLettered);
|
|
1631
|
+
}
|
|
1632
|
+
// Construct active delivery promises first so their heartbeats protect
|
|
1633
|
+
// freshly claimed rows while abandoned-message observers run.
|
|
1634
|
+
const outcomePromises = active.map((message) => processClaimedMessage(options, runtime, instrumentation, jobInstrumentation, tracing, message));
|
|
1635
|
+
const abandonedPromises = abandoned.map(async (message) => {
|
|
1636
|
+
const error = new OutboxAbandonedClaimError({
|
|
1637
|
+
id: message.id,
|
|
1638
|
+
attempts: message.attempts,
|
|
1639
|
+
maxAttempts: message.maxAttempts,
|
|
1640
|
+
});
|
|
1641
|
+
await recordDeadLetter(options, instrumentation, jobInstrumentation, message, error, { abandoned: true });
|
|
1642
|
+
});
|
|
1643
|
+
const [outcomes] = await Promise.all([
|
|
1644
|
+
Promise.all(outcomePromises),
|
|
1645
|
+
Promise.all(abandonedPromises),
|
|
1646
|
+
]);
|
|
1647
|
+
result.deadLettered += abandoned.length;
|
|
1648
|
+
result.abandonedDeadLettered += abandoned.length;
|
|
1649
|
+
for (const outcome of outcomes) {
|
|
1650
|
+
result.delivered += outcome.delivered;
|
|
1651
|
+
result.retried += outcome.retried;
|
|
1652
|
+
result.deadLettered += outcome.deadLettered;
|
|
1653
|
+
result.settlementFailed += outcome.settlementFailed;
|
|
1654
|
+
result.leaseLost += outcome.leaseLost;
|
|
812
1655
|
}
|
|
813
1656
|
}
|
|
814
1657
|
return result;
|
|
815
1658
|
}
|
|
1659
|
+
function assertOutboxDrainPort(outbox) {
|
|
1660
|
+
const candidate = outbox;
|
|
1661
|
+
const missing = [
|
|
1662
|
+
"claimBatch",
|
|
1663
|
+
"renewClaim",
|
|
1664
|
+
"markDelivered",
|
|
1665
|
+
"markFailed",
|
|
1666
|
+
].filter((method) => typeof candidate[method] !== "function");
|
|
1667
|
+
if (missing.length > 0) {
|
|
1668
|
+
throw new Error(`Cannot drain this outbox: the outbox port is missing ${missing
|
|
1669
|
+
.map((method) => `${method}()`)
|
|
1670
|
+
.join(", ")}.`);
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
816
1673
|
function assertOutboxDeliveryCapabilities(options) {
|
|
817
1674
|
const missing = [];
|
|
818
1675
|
if (options.registry.events.size > 0 && !options.eventBus) {
|