@aws/durable-execution-sdk-js 1.0.1 → 1.0.2
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 +25 -5
- package/dist/index.mjs +101 -33
- package/dist/index.mjs.map +1 -1
- package/dist-cjs/index.js +101 -33
- package/dist-cjs/index.js.map +1 -1
- package/dist-types/handlers/concurrent-execution-handler/concurrent-execution-handler.d.ts.map +1 -1
- package/dist-types/index.d.ts +1 -1
- package/dist-types/index.d.ts.map +1 -1
- package/dist-types/types/step.d.ts +45 -0
- package/dist-types/types/step.d.ts.map +1 -1
- package/dist-types/utils/checkpoint/checkpoint-manager.d.ts +6 -0
- package/dist-types/utils/checkpoint/checkpoint-manager.d.ts.map +1 -1
- package/dist-types/with-durable-execution.d.ts.map +1 -1
- package/package.json +1 -2
package/dist-cjs/index.js
CHANGED
|
@@ -167,11 +167,56 @@ var DurableLogLevel;
|
|
|
167
167
|
})(DurableLogLevel || (DurableLogLevel = {}));
|
|
168
168
|
|
|
169
169
|
/**
|
|
170
|
+
* Execution semantics for step operations.
|
|
171
|
+
*
|
|
172
|
+
* @remarks
|
|
173
|
+
* These semantics control how step execution is checkpointed and replayed. **Important**: The guarantees apply *per
|
|
174
|
+
* retry attempt*, not per overall workflow execution.
|
|
175
|
+
*
|
|
176
|
+
* With retries enabled (the default), a step could execute multiple times across different retry attempts even when
|
|
177
|
+
* using `AtMostOncePerRetry`. To achieve step-level at-most-once execution, combine `AtMostOncePerRetry` with a retry
|
|
178
|
+
* strategy that disables retries (`shouldRetry: false`).
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* ```typescript
|
|
182
|
+
* // At-least-once per retry (default) - safe for idempotent operations
|
|
183
|
+
* await context.step("send-notification", async () => sendEmail(), {
|
|
184
|
+
* semantics: StepSemantics.AtLeastOncePerRetry,
|
|
185
|
+
* });
|
|
186
|
+
*
|
|
187
|
+
* // At-most-once per retry - for non-idempotent operations
|
|
188
|
+
* await context.step("charge-payment", async () => processPayment(), {
|
|
189
|
+
* semantics: StepSemantics.AtMostOncePerRetry,
|
|
190
|
+
* retryStrategy: () => ({ shouldRetry: false }),
|
|
191
|
+
* });
|
|
192
|
+
* ```
|
|
193
|
+
*
|
|
170
194
|
* @public
|
|
171
195
|
*/
|
|
172
196
|
exports.StepSemantics = void 0;
|
|
173
197
|
(function (StepSemantics) {
|
|
198
|
+
/**
|
|
199
|
+
* At-most-once execution per retry attempt.
|
|
200
|
+
*
|
|
201
|
+
* @remarks
|
|
202
|
+
* A checkpoint is created before step execution. If a failure occurs after the checkpoint
|
|
203
|
+
* but before step completion, the previous step retry attempt is skipped on replay.
|
|
204
|
+
*
|
|
205
|
+
* **Note**: This is "at-most-once *per retry*". With multiple retry attempts, the step
|
|
206
|
+
* could still execute multiple times across different retries. To guarantee the step
|
|
207
|
+
* executes at most once, disable retries by returning
|
|
208
|
+
* `{ shouldRetry: false }` from your retry strategy.
|
|
209
|
+
*/
|
|
174
210
|
StepSemantics["AtMostOncePerRetry"] = "AT_MOST_ONCE_PER_RETRY";
|
|
211
|
+
/**
|
|
212
|
+
* At-least-once execution per retry attempt (default).
|
|
213
|
+
*
|
|
214
|
+
* @remarks
|
|
215
|
+
* The step will execute at least once on each retry attempt. If the step succeeds
|
|
216
|
+
* but the checkpoint fails (e.g., due to a sandbox crash), the step will re-execute
|
|
217
|
+
* on replay. This is the safer default for operations that are idempotent or can
|
|
218
|
+
* tolerate duplicate execution.
|
|
219
|
+
*/
|
|
175
220
|
StepSemantics["AtLeastOncePerRetry"] = "AT_LEAST_ONCE_PER_RETRY";
|
|
176
221
|
})(exports.StepSemantics || (exports.StepSemantics = {}));
|
|
177
222
|
/**
|
|
@@ -2796,7 +2841,13 @@ class ConcurrencyController {
|
|
|
2796
2841
|
tryStartNext();
|
|
2797
2842
|
}
|
|
2798
2843
|
};
|
|
2799
|
-
|
|
2844
|
+
if (items.length === 0) {
|
|
2845
|
+
log("🎉", `${this.operationName} completed with no items`);
|
|
2846
|
+
resolve(new BatchResultImpl([], getCompletionReason(0)));
|
|
2847
|
+
}
|
|
2848
|
+
else {
|
|
2849
|
+
tryStartNext();
|
|
2850
|
+
}
|
|
2800
2851
|
});
|
|
2801
2852
|
}
|
|
2802
2853
|
}
|
|
@@ -3234,6 +3285,13 @@ class CheckpointUnrecoverableExecutionError extends UnrecoverableExecutionError
|
|
|
3234
3285
|
}
|
|
3235
3286
|
|
|
3236
3287
|
const STEP_DATA_UPDATED_EVENT = "stepDataUpdated";
|
|
3288
|
+
const TERMINAL_STATUSES = [
|
|
3289
|
+
clientLambda.OperationStatus.SUCCEEDED,
|
|
3290
|
+
clientLambda.OperationStatus.CANCELLED,
|
|
3291
|
+
clientLambda.OperationStatus.FAILED,
|
|
3292
|
+
clientLambda.OperationStatus.STOPPED,
|
|
3293
|
+
clientLambda.OperationStatus.TIMED_OUT,
|
|
3294
|
+
];
|
|
3237
3295
|
class CheckpointManager {
|
|
3238
3296
|
durableExecutionArn;
|
|
3239
3297
|
stepData;
|
|
@@ -3248,6 +3306,7 @@ class CheckpointManager {
|
|
|
3248
3306
|
forceCheckpointPromises = [];
|
|
3249
3307
|
queueCompletionResolver = null;
|
|
3250
3308
|
MAX_PAYLOAD_SIZE = 750 * 1024; // 750KB in bytes
|
|
3309
|
+
MAX_ITEMS_IN_BATCH = 250;
|
|
3251
3310
|
isTerminating = false;
|
|
3252
3311
|
static textEncoder = new TextEncoder();
|
|
3253
3312
|
// Operation lifecycle tracking
|
|
@@ -3409,7 +3468,9 @@ class CheckpointManager {
|
|
|
3409
3468
|
while (this.queue.length > 0) {
|
|
3410
3469
|
const nextItem = this.queue[0];
|
|
3411
3470
|
const itemSize = CheckpointManager.textEncoder.encode(JSON.stringify(nextItem)).length;
|
|
3412
|
-
if (currentSize + itemSize > this.MAX_PAYLOAD_SIZE
|
|
3471
|
+
if ((currentSize + itemSize > this.MAX_PAYLOAD_SIZE ||
|
|
3472
|
+
batch.length >= this.MAX_ITEMS_IN_BATCH) &&
|
|
3473
|
+
batch.length > 0) {
|
|
3413
3474
|
break;
|
|
3414
3475
|
}
|
|
3415
3476
|
this.queue.shift();
|
|
@@ -3591,6 +3652,11 @@ class CheckpointManager {
|
|
|
3591
3652
|
if (op.state !== OperationLifecycleState.RETRY_WAITING) {
|
|
3592
3653
|
throw new Error(`Operation ${stepId} must be in RETRY_WAITING state, got ${op.state}`);
|
|
3593
3654
|
}
|
|
3655
|
+
// Resolve immediately if the step was completed already
|
|
3656
|
+
const stepData = this.stepData[hashId(stepId)];
|
|
3657
|
+
if (stepData?.Status && TERMINAL_STATUSES.includes(stepData.Status)) {
|
|
3658
|
+
return Promise.resolve();
|
|
3659
|
+
}
|
|
3594
3660
|
// Start timer with polling
|
|
3595
3661
|
this.startTimerWithPolling(stepId, op.endTimestamp);
|
|
3596
3662
|
// Return promise that resolves when status changes
|
|
@@ -3606,6 +3672,11 @@ class CheckpointManager {
|
|
|
3606
3672
|
if (op.state !== OperationLifecycleState.IDLE_AWAITED) {
|
|
3607
3673
|
throw new Error(`Operation ${stepId} must be in IDLE_AWAITED state, got ${op.state}`);
|
|
3608
3674
|
}
|
|
3675
|
+
// Resolve immediately if the step was completed already
|
|
3676
|
+
const stepData = this.stepData[hashId(stepId)];
|
|
3677
|
+
if (stepData?.Status && TERMINAL_STATUSES.includes(stepData.Status)) {
|
|
3678
|
+
return Promise.resolve();
|
|
3679
|
+
}
|
|
3609
3680
|
// Start timer with polling
|
|
3610
3681
|
this.startTimerWithPolling(stepId, op.endTimestamp);
|
|
3611
3682
|
// Return promise that resolves when status changes
|
|
@@ -3655,28 +3726,28 @@ class CheckpointManager {
|
|
|
3655
3726
|
op.resolver = undefined;
|
|
3656
3727
|
}
|
|
3657
3728
|
}
|
|
3658
|
-
|
|
3729
|
+
/**
|
|
3730
|
+
* Determines if the function should terminate.
|
|
3731
|
+
* @returns TerminationReason if the function should terminate, or undefined if the function should not terminate
|
|
3732
|
+
*/
|
|
3733
|
+
shouldTerminate() {
|
|
3659
3734
|
// Rule 1: Can't terminate if checkpoint queue is not empty
|
|
3660
3735
|
if (this.queue.length > 0) {
|
|
3661
|
-
|
|
3662
|
-
return;
|
|
3736
|
+
return undefined;
|
|
3663
3737
|
}
|
|
3664
3738
|
// Rule 2: Can't terminate if checkpoint is currently processing
|
|
3665
3739
|
if (this.isProcessing) {
|
|
3666
|
-
|
|
3667
|
-
return;
|
|
3740
|
+
return undefined;
|
|
3668
3741
|
}
|
|
3669
3742
|
// Rule 3: Can't terminate if there are pending force checkpoint promises
|
|
3670
3743
|
if (this.forceCheckpointPromises.length > 0) {
|
|
3671
|
-
|
|
3672
|
-
return;
|
|
3744
|
+
return undefined;
|
|
3673
3745
|
}
|
|
3674
3746
|
const allOps = Array.from(this.operations.values());
|
|
3675
3747
|
// Rule 4: Can't terminate if any operation is EXECUTING
|
|
3676
3748
|
const hasExecuting = allOps.some((op) => op.state === OperationLifecycleState.EXECUTING);
|
|
3677
3749
|
if (hasExecuting) {
|
|
3678
|
-
|
|
3679
|
-
return;
|
|
3750
|
+
return undefined;
|
|
3680
3751
|
}
|
|
3681
3752
|
// Rule 5: Clean up operations whose ancestors are complete or pending completion
|
|
3682
3753
|
for (const op of allOps) {
|
|
@@ -3699,12 +3770,17 @@ class CheckpointManager {
|
|
|
3699
3770
|
op.state === OperationLifecycleState.IDLE_NOT_AWAITED ||
|
|
3700
3771
|
op.state === OperationLifecycleState.IDLE_AWAITED);
|
|
3701
3772
|
if (hasWaiting) {
|
|
3702
|
-
|
|
3703
|
-
this.scheduleTermination(reason);
|
|
3773
|
+
return this.determineTerminationReason(remainingOps);
|
|
3704
3774
|
}
|
|
3705
|
-
|
|
3706
|
-
|
|
3775
|
+
return undefined;
|
|
3776
|
+
}
|
|
3777
|
+
checkAndTerminate() {
|
|
3778
|
+
const terminationReason = this.shouldTerminate();
|
|
3779
|
+
if (terminationReason) {
|
|
3780
|
+
this.scheduleTermination(terminationReason);
|
|
3781
|
+
return;
|
|
3707
3782
|
}
|
|
3783
|
+
this.abortTermination();
|
|
3708
3784
|
}
|
|
3709
3785
|
abortTermination() {
|
|
3710
3786
|
if (this.terminationTimer) {
|
|
@@ -3728,6 +3804,11 @@ class CheckpointManager {
|
|
|
3728
3804
|
cooldownMs: this.TERMINATION_COOLDOWN_MS,
|
|
3729
3805
|
});
|
|
3730
3806
|
this.terminationTimer = setTimeout(() => {
|
|
3807
|
+
if (!this.shouldTerminate()) {
|
|
3808
|
+
log("🔄", "Termination conditions no longer valid after cooldown, aborting termination");
|
|
3809
|
+
this.abortTermination();
|
|
3810
|
+
return;
|
|
3811
|
+
}
|
|
3731
3812
|
this.executeTermination(reason);
|
|
3732
3813
|
}, this.TERMINATION_COOLDOWN_MS);
|
|
3733
3814
|
}
|
|
@@ -4479,16 +4560,10 @@ async function runHandler(event, context, executionContext, durableExecutionMode
|
|
|
4479
4560
|
* Validates that the event is a proper durable execution input
|
|
4480
4561
|
*/
|
|
4481
4562
|
function validateDurableExecutionEvent(event) {
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
}
|
|
4487
|
-
}
|
|
4488
|
-
catch {
|
|
4489
|
-
const msg = `Unexpected payload provided to start the durable execution.
|
|
4490
|
-
Check your resource configurations to confirm the durability is set.`;
|
|
4491
|
-
throw new Error(msg);
|
|
4563
|
+
const eventObj = event;
|
|
4564
|
+
if (!eventObj?.DurableExecutionArn || !eventObj?.CheckpointToken) {
|
|
4565
|
+
throw new Error("Unexpected payload provided to start the durable execution.\n" +
|
|
4566
|
+
"Check your resource configurations to confirm the durability is set.");
|
|
4492
4567
|
}
|
|
4493
4568
|
}
|
|
4494
4569
|
/**
|
|
@@ -4566,14 +4641,7 @@ const withDurableExecution = (handler, config) => {
|
|
|
4566
4641
|
return async (event, context) => {
|
|
4567
4642
|
validateDurableExecutionEvent(event);
|
|
4568
4643
|
const { executionContext, durableExecutionMode, checkpointToken } = await initializeExecutionContext(event, context, config?.client);
|
|
4569
|
-
|
|
4570
|
-
try {
|
|
4571
|
-
response = await runHandler(event, context, executionContext, durableExecutionMode, checkpointToken, handler);
|
|
4572
|
-
return response;
|
|
4573
|
-
}
|
|
4574
|
-
catch (err) {
|
|
4575
|
-
throw err;
|
|
4576
|
-
}
|
|
4644
|
+
return runHandler(event, context, executionContext, durableExecutionMode, checkpointToken, handler);
|
|
4577
4645
|
};
|
|
4578
4646
|
};
|
|
4579
4647
|
|