@aws/durable-execution-sdk-js 1.0.1 → 1.0.3
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 +192 -70
- package/dist/index.mjs.map +1 -1
- package/dist-cjs/index.js +192 -70
- package/dist-cjs/index.js.map +1 -1
- package/dist-types/context/durable-context/durable-context.d.ts +5 -2
- package/dist-types/context/durable-context/durable-context.d.ts.map +1 -1
- package/dist-types/context/durable-context/durable-context.test.d.ts +2 -0
- package/dist-types/context/durable-context/durable-context.test.d.ts.map +1 -0
- package/dist-types/durable-execution-api-client/durable-execution-api-client.d.ts.map +1 -1
- package/dist-types/handlers/concurrent-execution-handler/batch-result.d.ts.map +1 -1
- package/dist-types/handlers/concurrent-execution-handler/concurrent-execution-handler.d.ts.map +1 -1
- package/dist-types/handlers/invoke-handler/invoke-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/durable-context.d.ts +9 -0
- package/dist-types/types/durable-context.d.ts.map +1 -1
- package/dist-types/types/invoke.d.ts +2 -0
- package/dist-types/types/invoke.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 -1
- package/dist-types/utils/checkpoint/checkpoint-manager.d.ts.map +1 -1
- package/dist-types/utils/constants/constants.d.ts +12 -0
- package/dist-types/utils/constants/constants.d.ts.map +1 -1
- package/dist-types/utils/constants/version.d.ts +14 -0
- package/dist-types/utils/constants/version.d.ts.map +1 -0
- package/dist-types/utils/logger/default-logger.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
|
/**
|
|
@@ -569,6 +614,26 @@ class StepInterruptedError extends Error {
|
|
|
569
614
|
}
|
|
570
615
|
}
|
|
571
616
|
|
|
617
|
+
/**
|
|
618
|
+
* Shared constants to avoid circular dependencies
|
|
619
|
+
*/
|
|
620
|
+
/**
|
|
621
|
+
* Controls whether stack traces are stored in error objects
|
|
622
|
+
* TODO: Accept this as configuration parameter in the future
|
|
623
|
+
*/
|
|
624
|
+
/**
|
|
625
|
+
* Checkpoint manager termination cooldown in milliseconds
|
|
626
|
+
* After the last operation completes, the checkpoint manager waits this duration
|
|
627
|
+
* before terminating to allow for any final checkpoint operations
|
|
628
|
+
*/
|
|
629
|
+
const CHECKPOINT_TERMINATION_COOLDOWN_MS = 20;
|
|
630
|
+
/**
|
|
631
|
+
* Maximum polling duration in milliseconds (15 minutes)
|
|
632
|
+
* Used to cap setTimeout delays to prevent 32-bit signed integer overflow
|
|
633
|
+
* and limit polling duration for long-running operations
|
|
634
|
+
*/
|
|
635
|
+
const MAX_POLL_DURATION_MS = 15 * 60 * 1000;
|
|
636
|
+
|
|
572
637
|
/**
|
|
573
638
|
* Base class for all durable operation errors
|
|
574
639
|
* @public
|
|
@@ -1357,6 +1422,7 @@ const createInvokeHandler = (context, checkpoint, createStepId, parentId, checkA
|
|
|
1357
1422
|
Payload: serializedPayload,
|
|
1358
1423
|
ChainedInvokeOptions: {
|
|
1359
1424
|
FunctionName: funcId,
|
|
1425
|
+
...(config?.tenantId && { TenantId: config.tenantId }),
|
|
1360
1426
|
},
|
|
1361
1427
|
});
|
|
1362
1428
|
}
|
|
@@ -2487,6 +2553,10 @@ class BatchResultImpl {
|
|
|
2487
2553
|
* Restores methods to deserialized BatchResult data
|
|
2488
2554
|
*/
|
|
2489
2555
|
function restoreBatchResult(data) {
|
|
2556
|
+
// If data is already a BatchResultImpl instance, return it as-is
|
|
2557
|
+
if (data instanceof BatchResultImpl) {
|
|
2558
|
+
return data;
|
|
2559
|
+
}
|
|
2490
2560
|
if (data &&
|
|
2491
2561
|
typeof data === "object" &&
|
|
2492
2562
|
"all" in data &&
|
|
@@ -2796,7 +2866,13 @@ class ConcurrencyController {
|
|
|
2796
2866
|
tryStartNext();
|
|
2797
2867
|
}
|
|
2798
2868
|
};
|
|
2799
|
-
|
|
2869
|
+
if (items.length === 0) {
|
|
2870
|
+
log("🎉", `${this.operationName} completed with no items`);
|
|
2871
|
+
resolve(new BatchResultImpl([], getCompletionReason(0)));
|
|
2872
|
+
}
|
|
2873
|
+
else {
|
|
2874
|
+
tryStartNext();
|
|
2875
|
+
}
|
|
2800
2876
|
});
|
|
2801
2877
|
}
|
|
2802
2878
|
}
|
|
@@ -2948,7 +3024,7 @@ const getStepData = (stepData, stepId) => {
|
|
|
2948
3024
|
};
|
|
2949
3025
|
|
|
2950
3026
|
class DurableContextImpl {
|
|
2951
|
-
|
|
3027
|
+
_executionContext;
|
|
2952
3028
|
lambdaContext;
|
|
2953
3029
|
_stepPrefix;
|
|
2954
3030
|
_stepCounter = 0;
|
|
@@ -2960,8 +3036,9 @@ class DurableContextImpl {
|
|
|
2960
3036
|
modeManagement;
|
|
2961
3037
|
durableExecution;
|
|
2962
3038
|
logger;
|
|
2963
|
-
|
|
2964
|
-
|
|
3039
|
+
executionContext;
|
|
3040
|
+
constructor(_executionContext, lambdaContext, durableExecutionMode, inheritedLogger, stepPrefix, durableExecution, parentId) {
|
|
3041
|
+
this._executionContext = _executionContext;
|
|
2965
3042
|
this.lambdaContext = lambdaContext;
|
|
2966
3043
|
this._stepPrefix = stepPrefix;
|
|
2967
3044
|
this._parentId = parentId;
|
|
@@ -2969,6 +3046,9 @@ class DurableContextImpl {
|
|
|
2969
3046
|
this.durableLogger = inheritedLogger;
|
|
2970
3047
|
this.durableLogger.configureDurableLoggingContext?.(this.getDurableLoggingContext());
|
|
2971
3048
|
this.logger = this.createModeAwareLogger(inheritedLogger);
|
|
3049
|
+
this.executionContext = {
|
|
3050
|
+
durableExecutionArn: _executionContext.durableExecutionArn,
|
|
3051
|
+
};
|
|
2972
3052
|
this.durableExecutionMode = durableExecutionMode;
|
|
2973
3053
|
this.checkpoint = durableExecution.checkpointManager;
|
|
2974
3054
|
this.modeManagement = new ModeManagement(this.captureExecutionState.bind(this), this.checkAndUpdateReplayMode.bind(this), this.checkForNonResolvingPromise.bind(this), () => this.durableExecutionMode, (mode) => {
|
|
@@ -2980,9 +3060,9 @@ class DurableContextImpl {
|
|
|
2980
3060
|
getDurableLogData: () => {
|
|
2981
3061
|
const activeContext = getActiveContext();
|
|
2982
3062
|
const result = {
|
|
2983
|
-
executionArn: this.
|
|
2984
|
-
requestId: this.
|
|
2985
|
-
tenantId: this.
|
|
3063
|
+
executionArn: this._executionContext.durableExecutionArn,
|
|
3064
|
+
requestId: this._executionContext.requestId,
|
|
3065
|
+
tenantId: this._executionContext.tenantId,
|
|
2986
3066
|
operationId: !activeContext || activeContext?.contextId === "root"
|
|
2987
3067
|
? undefined
|
|
2988
3068
|
: hashId(activeContext.contextId),
|
|
@@ -3059,7 +3139,7 @@ class DurableContextImpl {
|
|
|
3059
3139
|
checkAndUpdateReplayMode() {
|
|
3060
3140
|
if (this.durableExecutionMode === DurableExecutionMode.ReplayMode) {
|
|
3061
3141
|
const nextStepId = this.getNextStepId();
|
|
3062
|
-
const nextStepData = this.
|
|
3142
|
+
const nextStepData = this._executionContext.getStepData(nextStepId);
|
|
3063
3143
|
if (!nextStepData) {
|
|
3064
3144
|
this.durableExecutionMode = DurableExecutionMode.ExecutionMode;
|
|
3065
3145
|
}
|
|
@@ -3068,7 +3148,7 @@ class DurableContextImpl {
|
|
|
3068
3148
|
captureExecutionState() {
|
|
3069
3149
|
const wasInReplayMode = this.durableExecutionMode === DurableExecutionMode.ReplayMode;
|
|
3070
3150
|
const nextStepId = this.getNextStepId();
|
|
3071
|
-
const stepData = this.
|
|
3151
|
+
const stepData = this._executionContext.getStepData(nextStepId);
|
|
3072
3152
|
const wasNotFinished = !!(stepData &&
|
|
3073
3153
|
stepData.Status !== clientLambda.OperationStatus.SUCCEEDED &&
|
|
3074
3154
|
stepData.Status !== clientLambda.OperationStatus.FAILED);
|
|
@@ -3077,7 +3157,7 @@ class DurableContextImpl {
|
|
|
3077
3157
|
checkForNonResolvingPromise() {
|
|
3078
3158
|
if (this.durableExecutionMode === DurableExecutionMode.ReplaySucceededContext) {
|
|
3079
3159
|
const nextStepId = this.getNextStepId();
|
|
3080
|
-
const nextStepData = this.
|
|
3160
|
+
const nextStepData = this._executionContext.getStepData(nextStepId);
|
|
3081
3161
|
if (nextStepData &&
|
|
3082
3162
|
nextStepData.Status !== clientLambda.OperationStatus.SUCCEEDED &&
|
|
3083
3163
|
nextStepData.Status !== clientLambda.OperationStatus.FAILED) {
|
|
@@ -3093,16 +3173,16 @@ class DurableContextImpl {
|
|
|
3093
3173
|
return this.modeManagement.withDurableModeManagement(operation);
|
|
3094
3174
|
}
|
|
3095
3175
|
step(nameOrFn, fnOrOptions, maybeOptions) {
|
|
3096
|
-
validateContextUsage(this._stepPrefix, "step", this.
|
|
3176
|
+
validateContextUsage(this._stepPrefix, "step", this._executionContext.terminationManager);
|
|
3097
3177
|
return this.withDurableModeManagement(() => {
|
|
3098
|
-
const stepHandler = createStepHandler(this.
|
|
3178
|
+
const stepHandler = createStepHandler(this._executionContext, this.checkpoint, this.lambdaContext, this.createStepId.bind(this), this.durableLogger, this._parentId);
|
|
3099
3179
|
return stepHandler(nameOrFn, fnOrOptions, maybeOptions);
|
|
3100
3180
|
});
|
|
3101
3181
|
}
|
|
3102
3182
|
invoke(nameOrFuncId, funcIdOrInput, inputOrConfig, maybeConfig) {
|
|
3103
|
-
validateContextUsage(this._stepPrefix, "invoke", this.
|
|
3183
|
+
validateContextUsage(this._stepPrefix, "invoke", this._executionContext.terminationManager);
|
|
3104
3184
|
return this.withDurableModeManagement(() => {
|
|
3105
|
-
const invokeHandler = createInvokeHandler(this.
|
|
3185
|
+
const invokeHandler = createInvokeHandler(this._executionContext, this.checkpoint, this.createStepId.bind(this), this._parentId, this.checkAndUpdateReplayMode.bind(this));
|
|
3106
3186
|
return invokeHandler(...[
|
|
3107
3187
|
nameOrFuncId,
|
|
3108
3188
|
funcIdOrInput,
|
|
@@ -3112,18 +3192,18 @@ class DurableContextImpl {
|
|
|
3112
3192
|
});
|
|
3113
3193
|
}
|
|
3114
3194
|
runInChildContext(nameOrFn, fnOrOptions, maybeOptions) {
|
|
3115
|
-
validateContextUsage(this._stepPrefix, "runInChildContext", this.
|
|
3195
|
+
validateContextUsage(this._stepPrefix, "runInChildContext", this._executionContext.terminationManager);
|
|
3116
3196
|
return this.withDurableModeManagement(() => {
|
|
3117
|
-
const blockHandler = createRunInChildContextHandler(this.
|
|
3197
|
+
const blockHandler = createRunInChildContextHandler(this._executionContext, this.checkpoint, this.lambdaContext, this.createStepId.bind(this), () => this.durableLogger,
|
|
3118
3198
|
// Adapter function to maintain compatibility
|
|
3119
3199
|
(executionContext, parentContext, durableExecutionMode, inheritedLogger, stepPrefix, _checkpointToken, parentId) => createDurableContext(executionContext, parentContext, durableExecutionMode, inheritedLogger, stepPrefix, this.durableExecution, parentId), this._parentId);
|
|
3120
3200
|
return blockHandler(nameOrFn, fnOrOptions, maybeOptions);
|
|
3121
3201
|
});
|
|
3122
3202
|
}
|
|
3123
3203
|
wait(nameOrDuration, maybeDuration) {
|
|
3124
|
-
validateContextUsage(this._stepPrefix, "wait", this.
|
|
3204
|
+
validateContextUsage(this._stepPrefix, "wait", this._executionContext.terminationManager);
|
|
3125
3205
|
return this.withDurableModeManagement(() => {
|
|
3126
|
-
const waitHandler = createWaitHandler(this.
|
|
3206
|
+
const waitHandler = createWaitHandler(this._executionContext, this.checkpoint, this.createStepId.bind(this), this._parentId, this.checkAndUpdateReplayMode.bind(this));
|
|
3127
3207
|
return typeof nameOrDuration === "string"
|
|
3128
3208
|
? waitHandler(nameOrDuration, maybeDuration)
|
|
3129
3209
|
: waitHandler(nameOrDuration);
|
|
@@ -3155,23 +3235,23 @@ class DurableContextImpl {
|
|
|
3155
3235
|
}
|
|
3156
3236
|
}
|
|
3157
3237
|
createCallback(nameOrConfig, maybeConfig) {
|
|
3158
|
-
validateContextUsage(this._stepPrefix, "createCallback", this.
|
|
3238
|
+
validateContextUsage(this._stepPrefix, "createCallback", this._executionContext.terminationManager);
|
|
3159
3239
|
return this.withDurableModeManagement(() => {
|
|
3160
|
-
const callbackFactory = createCallback(this.
|
|
3240
|
+
const callbackFactory = createCallback(this._executionContext, this.checkpoint, this.createStepId.bind(this), this.checkAndUpdateReplayMode.bind(this), this._parentId);
|
|
3161
3241
|
return callbackFactory(nameOrConfig, maybeConfig);
|
|
3162
3242
|
});
|
|
3163
3243
|
}
|
|
3164
3244
|
waitForCallback(nameOrSubmitter, submitterOrConfig, maybeConfig) {
|
|
3165
|
-
validateContextUsage(this._stepPrefix, "waitForCallback", this.
|
|
3245
|
+
validateContextUsage(this._stepPrefix, "waitForCallback", this._executionContext.terminationManager);
|
|
3166
3246
|
return this.withDurableModeManagement(() => {
|
|
3167
|
-
const waitForCallbackHandler = createWaitForCallbackHandler(this.
|
|
3247
|
+
const waitForCallbackHandler = createWaitForCallbackHandler(this._executionContext, this.getNextStepId.bind(this), this.runInChildContext.bind(this));
|
|
3168
3248
|
return waitForCallbackHandler(nameOrSubmitter, submitterOrConfig, maybeConfig);
|
|
3169
3249
|
});
|
|
3170
3250
|
}
|
|
3171
3251
|
waitForCondition(nameOrCheckFunc, checkFuncOrConfig, maybeConfig) {
|
|
3172
|
-
validateContextUsage(this._stepPrefix, "waitForCondition", this.
|
|
3252
|
+
validateContextUsage(this._stepPrefix, "waitForCondition", this._executionContext.terminationManager);
|
|
3173
3253
|
return this.withDurableModeManagement(() => {
|
|
3174
|
-
const waitForConditionHandler = createWaitForConditionHandler(this.
|
|
3254
|
+
const waitForConditionHandler = createWaitForConditionHandler(this._executionContext, this.checkpoint, this.createStepId.bind(this), this.durableLogger, this._parentId);
|
|
3175
3255
|
return typeof nameOrCheckFunc === "string" ||
|
|
3176
3256
|
nameOrCheckFunc === undefined
|
|
3177
3257
|
? waitForConditionHandler(nameOrCheckFunc, checkFuncOrConfig, maybeConfig)
|
|
@@ -3179,23 +3259,23 @@ class DurableContextImpl {
|
|
|
3179
3259
|
});
|
|
3180
3260
|
}
|
|
3181
3261
|
map(nameOrItems, itemsOrMapFunc, mapFuncOrConfig, maybeConfig) {
|
|
3182
|
-
validateContextUsage(this._stepPrefix, "map", this.
|
|
3262
|
+
validateContextUsage(this._stepPrefix, "map", this._executionContext.terminationManager);
|
|
3183
3263
|
return this.withDurableModeManagement(() => {
|
|
3184
|
-
const mapHandler = createMapHandler(this.
|
|
3264
|
+
const mapHandler = createMapHandler(this._executionContext, this._executeConcurrently.bind(this));
|
|
3185
3265
|
return mapHandler(nameOrItems, itemsOrMapFunc, mapFuncOrConfig, maybeConfig);
|
|
3186
3266
|
});
|
|
3187
3267
|
}
|
|
3188
3268
|
parallel(nameOrBranches, branchesOrConfig, maybeConfig) {
|
|
3189
|
-
validateContextUsage(this._stepPrefix, "parallel", this.
|
|
3269
|
+
validateContextUsage(this._stepPrefix, "parallel", this._executionContext.terminationManager);
|
|
3190
3270
|
return this.withDurableModeManagement(() => {
|
|
3191
|
-
const parallelHandler = createParallelHandler(this.
|
|
3271
|
+
const parallelHandler = createParallelHandler(this._executionContext, this._executeConcurrently.bind(this));
|
|
3192
3272
|
return parallelHandler(nameOrBranches, branchesOrConfig, maybeConfig);
|
|
3193
3273
|
});
|
|
3194
3274
|
}
|
|
3195
3275
|
_executeConcurrently(nameOrItems, itemsOrExecutor, executorOrConfig, maybeConfig) {
|
|
3196
|
-
validateContextUsage(this._stepPrefix, "_executeConcurrently", this.
|
|
3276
|
+
validateContextUsage(this._stepPrefix, "_executeConcurrently", this._executionContext.terminationManager);
|
|
3197
3277
|
return this.withDurableModeManagement(() => {
|
|
3198
|
-
const concurrentExecutionHandler = createConcurrentExecutionHandler(this.
|
|
3278
|
+
const concurrentExecutionHandler = createConcurrentExecutionHandler(this._executionContext, this.runInChildContext.bind(this), this.skipNextOperation.bind(this));
|
|
3199
3279
|
const promise = concurrentExecutionHandler(nameOrItems, itemsOrExecutor, executorOrConfig, maybeConfig);
|
|
3200
3280
|
// Prevent unhandled promise rejections
|
|
3201
3281
|
promise?.catch(() => { });
|
|
@@ -3234,6 +3314,13 @@ class CheckpointUnrecoverableExecutionError extends UnrecoverableExecutionError
|
|
|
3234
3314
|
}
|
|
3235
3315
|
|
|
3236
3316
|
const STEP_DATA_UPDATED_EVENT = "stepDataUpdated";
|
|
3317
|
+
const TERMINAL_STATUSES = [
|
|
3318
|
+
clientLambda.OperationStatus.SUCCEEDED,
|
|
3319
|
+
clientLambda.OperationStatus.CANCELLED,
|
|
3320
|
+
clientLambda.OperationStatus.FAILED,
|
|
3321
|
+
clientLambda.OperationStatus.STOPPED,
|
|
3322
|
+
clientLambda.OperationStatus.TIMED_OUT,
|
|
3323
|
+
];
|
|
3237
3324
|
class CheckpointManager {
|
|
3238
3325
|
durableExecutionArn;
|
|
3239
3326
|
stepData;
|
|
@@ -3248,6 +3335,7 @@ class CheckpointManager {
|
|
|
3248
3335
|
forceCheckpointPromises = [];
|
|
3249
3336
|
queueCompletionResolver = null;
|
|
3250
3337
|
MAX_PAYLOAD_SIZE = 750 * 1024; // 750KB in bytes
|
|
3338
|
+
MAX_ITEMS_IN_BATCH = 250;
|
|
3251
3339
|
isTerminating = false;
|
|
3252
3340
|
static textEncoder = new TextEncoder();
|
|
3253
3341
|
// Operation lifecycle tracking
|
|
@@ -3255,7 +3343,6 @@ class CheckpointManager {
|
|
|
3255
3343
|
// Termination cooldown
|
|
3256
3344
|
terminationTimer = null;
|
|
3257
3345
|
terminationReason = null;
|
|
3258
|
-
TERMINATION_COOLDOWN_MS = 50;
|
|
3259
3346
|
constructor(durableExecutionArn, stepData, storage, terminationManager, initialTaskToken, stepDataEmitter, logger, finishedAncestors) {
|
|
3260
3347
|
this.durableExecutionArn = durableExecutionArn;
|
|
3261
3348
|
this.stepData = stepData;
|
|
@@ -3409,7 +3496,9 @@ class CheckpointManager {
|
|
|
3409
3496
|
while (this.queue.length > 0) {
|
|
3410
3497
|
const nextItem = this.queue[0];
|
|
3411
3498
|
const itemSize = CheckpointManager.textEncoder.encode(JSON.stringify(nextItem)).length;
|
|
3412
|
-
if (currentSize + itemSize > this.MAX_PAYLOAD_SIZE
|
|
3499
|
+
if ((currentSize + itemSize > this.MAX_PAYLOAD_SIZE ||
|
|
3500
|
+
batch.length >= this.MAX_ITEMS_IN_BATCH) &&
|
|
3501
|
+
batch.length > 0) {
|
|
3413
3502
|
break;
|
|
3414
3503
|
}
|
|
3415
3504
|
this.queue.shift();
|
|
@@ -3591,6 +3680,11 @@ class CheckpointManager {
|
|
|
3591
3680
|
if (op.state !== OperationLifecycleState.RETRY_WAITING) {
|
|
3592
3681
|
throw new Error(`Operation ${stepId} must be in RETRY_WAITING state, got ${op.state}`);
|
|
3593
3682
|
}
|
|
3683
|
+
// Resolve immediately if the step was completed already
|
|
3684
|
+
const stepData = this.stepData[hashId(stepId)];
|
|
3685
|
+
if (stepData?.Status && TERMINAL_STATUSES.includes(stepData.Status)) {
|
|
3686
|
+
return Promise.resolve();
|
|
3687
|
+
}
|
|
3594
3688
|
// Start timer with polling
|
|
3595
3689
|
this.startTimerWithPolling(stepId, op.endTimestamp);
|
|
3596
3690
|
// Return promise that resolves when status changes
|
|
@@ -3606,6 +3700,11 @@ class CheckpointManager {
|
|
|
3606
3700
|
if (op.state !== OperationLifecycleState.IDLE_AWAITED) {
|
|
3607
3701
|
throw new Error(`Operation ${stepId} must be in IDLE_AWAITED state, got ${op.state}`);
|
|
3608
3702
|
}
|
|
3703
|
+
// Resolve immediately if the step was completed already
|
|
3704
|
+
const stepData = this.stepData[hashId(stepId)];
|
|
3705
|
+
if (stepData?.Status && TERMINAL_STATUSES.includes(stepData.Status)) {
|
|
3706
|
+
return Promise.resolve();
|
|
3707
|
+
}
|
|
3609
3708
|
// Start timer with polling
|
|
3610
3709
|
this.startTimerWithPolling(stepId, op.endTimestamp);
|
|
3611
3710
|
// Return promise that resolves when status changes
|
|
@@ -3655,28 +3754,28 @@ class CheckpointManager {
|
|
|
3655
3754
|
op.resolver = undefined;
|
|
3656
3755
|
}
|
|
3657
3756
|
}
|
|
3658
|
-
|
|
3757
|
+
/**
|
|
3758
|
+
* Determines if the function should terminate.
|
|
3759
|
+
* @returns TerminationReason if the function should terminate, or undefined if the function should not terminate
|
|
3760
|
+
*/
|
|
3761
|
+
shouldTerminate() {
|
|
3659
3762
|
// Rule 1: Can't terminate if checkpoint queue is not empty
|
|
3660
3763
|
if (this.queue.length > 0) {
|
|
3661
|
-
|
|
3662
|
-
return;
|
|
3764
|
+
return undefined;
|
|
3663
3765
|
}
|
|
3664
3766
|
// Rule 2: Can't terminate if checkpoint is currently processing
|
|
3665
3767
|
if (this.isProcessing) {
|
|
3666
|
-
|
|
3667
|
-
return;
|
|
3768
|
+
return undefined;
|
|
3668
3769
|
}
|
|
3669
3770
|
// Rule 3: Can't terminate if there are pending force checkpoint promises
|
|
3670
3771
|
if (this.forceCheckpointPromises.length > 0) {
|
|
3671
|
-
|
|
3672
|
-
return;
|
|
3772
|
+
return undefined;
|
|
3673
3773
|
}
|
|
3674
3774
|
const allOps = Array.from(this.operations.values());
|
|
3675
3775
|
// Rule 4: Can't terminate if any operation is EXECUTING
|
|
3676
3776
|
const hasExecuting = allOps.some((op) => op.state === OperationLifecycleState.EXECUTING);
|
|
3677
3777
|
if (hasExecuting) {
|
|
3678
|
-
|
|
3679
|
-
return;
|
|
3778
|
+
return undefined;
|
|
3680
3779
|
}
|
|
3681
3780
|
// Rule 5: Clean up operations whose ancestors are complete or pending completion
|
|
3682
3781
|
for (const op of allOps) {
|
|
@@ -3699,12 +3798,17 @@ class CheckpointManager {
|
|
|
3699
3798
|
op.state === OperationLifecycleState.IDLE_NOT_AWAITED ||
|
|
3700
3799
|
op.state === OperationLifecycleState.IDLE_AWAITED);
|
|
3701
3800
|
if (hasWaiting) {
|
|
3702
|
-
|
|
3703
|
-
this.scheduleTermination(reason);
|
|
3801
|
+
return this.determineTerminationReason(remainingOps);
|
|
3704
3802
|
}
|
|
3705
|
-
|
|
3706
|
-
|
|
3803
|
+
return undefined;
|
|
3804
|
+
}
|
|
3805
|
+
checkAndTerminate() {
|
|
3806
|
+
const terminationReason = this.shouldTerminate();
|
|
3807
|
+
if (terminationReason) {
|
|
3808
|
+
this.scheduleTermination(terminationReason);
|
|
3809
|
+
return;
|
|
3707
3810
|
}
|
|
3811
|
+
this.abortTermination();
|
|
3708
3812
|
}
|
|
3709
3813
|
abortTermination() {
|
|
3710
3814
|
if (this.terminationTimer) {
|
|
@@ -3725,11 +3829,16 @@ class CheckpointManager {
|
|
|
3725
3829
|
this.terminationReason = reason;
|
|
3726
3830
|
log("⏱️", "Scheduling termination", {
|
|
3727
3831
|
reason,
|
|
3728
|
-
cooldownMs:
|
|
3832
|
+
cooldownMs: CHECKPOINT_TERMINATION_COOLDOWN_MS,
|
|
3729
3833
|
});
|
|
3730
3834
|
this.terminationTimer = setTimeout(() => {
|
|
3835
|
+
if (!this.shouldTerminate()) {
|
|
3836
|
+
log("🔄", "Termination conditions no longer valid after cooldown, aborting termination");
|
|
3837
|
+
this.abortTermination();
|
|
3838
|
+
return;
|
|
3839
|
+
}
|
|
3731
3840
|
this.executeTermination(reason);
|
|
3732
|
-
},
|
|
3841
|
+
}, CHECKPOINT_TERMINATION_COOLDOWN_MS);
|
|
3733
3842
|
}
|
|
3734
3843
|
executeTermination(reason) {
|
|
3735
3844
|
log("🛑", "Executing termination after cooldown", { reason });
|
|
@@ -3764,6 +3873,10 @@ class CheckpointManager {
|
|
|
3764
3873
|
const timestamp = endTimestamp instanceof Date ? endTimestamp : new Date(endTimestamp);
|
|
3765
3874
|
// Wait until endTimestamp
|
|
3766
3875
|
delay = Math.max(0, timestamp.getTime() - Date.now());
|
|
3876
|
+
// Skip setTimeout if delay exceeds MAX_POLL_DURATION_MS (Lambda will timeout before it fires)
|
|
3877
|
+
if (delay > MAX_POLL_DURATION_MS) {
|
|
3878
|
+
return;
|
|
3879
|
+
}
|
|
3767
3880
|
}
|
|
3768
3881
|
else {
|
|
3769
3882
|
// No timestamp, start polling immediately (1 second delay)
|
|
@@ -3783,7 +3896,6 @@ class CheckpointManager {
|
|
|
3783
3896
|
if (!op)
|
|
3784
3897
|
return;
|
|
3785
3898
|
// Check if we've exceeded max polling duration (15 minutes)
|
|
3786
|
-
const MAX_POLL_DURATION_MS = 15 * 60 * 1000; // 15 minutes
|
|
3787
3899
|
if (op.pollStartTime &&
|
|
3788
3900
|
Date.now() - op.pollStartTime > MAX_POLL_DURATION_MS) {
|
|
3789
3901
|
// Stop polling after 15 minutes to prevent indefinite resource consumption.
|
|
@@ -3895,6 +4007,13 @@ class TerminationManager extends events.EventEmitter {
|
|
|
3895
4007
|
// align the default behaviour of how logs are emitted to match the RIC logging behaviour for consistency.
|
|
3896
4008
|
// For custom logic, users can implement their own logger to log data differently.
|
|
3897
4009
|
// See: https://github.com/aws/aws-lambda-nodejs-runtime-interface-client/blob/962ed28eefbc052389c4de4366b1c0c49ee08a13/src/LogPatch.js
|
|
4010
|
+
/**
|
|
4011
|
+
* Format options for util.formatWithOptions.
|
|
4012
|
+
* Using breakLength: Infinity prevents util.inspect from inserting newlines
|
|
4013
|
+
* when formatting objects, regardless of object size (fixes issue #322).
|
|
4014
|
+
* Defined at module level to avoid creating a new object on every function call.
|
|
4015
|
+
*/
|
|
4016
|
+
const FORMAT_OPTIONS = { breakLength: Infinity };
|
|
3898
4017
|
/**
|
|
3899
4018
|
* JSON.stringify replacer function for Error objects.
|
|
3900
4019
|
* Based on AWS Lambda Runtime Interface Client LogPatch functionality.
|
|
@@ -3960,11 +4079,11 @@ function formatDurableLogData(level, logData, ...messageParams) {
|
|
|
3960
4079
|
return JSON.stringify(result, jsonErrorReplacer);
|
|
3961
4080
|
}
|
|
3962
4081
|
catch (_) {
|
|
3963
|
-
result.message = util.
|
|
4082
|
+
result.message = util.formatWithOptions(FORMAT_OPTIONS, result.message);
|
|
3964
4083
|
return JSON.stringify(result);
|
|
3965
4084
|
}
|
|
3966
4085
|
}
|
|
3967
|
-
result.message = util.
|
|
4086
|
+
result.message = util.formatWithOptions(FORMAT_OPTIONS, ...messageParams);
|
|
3968
4087
|
for (const param of messageParams) {
|
|
3969
4088
|
if (param instanceof Error) {
|
|
3970
4089
|
result.errorType = param?.constructor?.name ?? "UnknownError";
|
|
@@ -4119,6 +4238,20 @@ const createDefaultLogger = (executionContext) => {
|
|
|
4119
4238
|
return new DefaultLogger(executionContext);
|
|
4120
4239
|
};
|
|
4121
4240
|
|
|
4241
|
+
/**
|
|
4242
|
+
* SDK metadata injected by Rollup at build time from package.json.
|
|
4243
|
+
*
|
|
4244
|
+
* At build time, Rollup replaces "@aws/durable-execution-sdk-js" and
|
|
4245
|
+
* "1.0.3" with actual values from package.json.
|
|
4246
|
+
*
|
|
4247
|
+
* Defaults are provided for test environments where Rollup doesn't run
|
|
4248
|
+
* and process.env values are undefined.
|
|
4249
|
+
*
|
|
4250
|
+
* @internal
|
|
4251
|
+
*/
|
|
4252
|
+
const SDK_NAME = "@aws/durable-execution-sdk-js";
|
|
4253
|
+
const SDK_VERSION = "1.0.3";
|
|
4254
|
+
|
|
4122
4255
|
let defaultLambdaClient;
|
|
4123
4256
|
/**
|
|
4124
4257
|
* Durable execution client which uses an API-based LambdaClient
|
|
@@ -4133,6 +4266,7 @@ class DurableExecutionApiClient {
|
|
|
4133
4266
|
if (!client) {
|
|
4134
4267
|
if (!defaultLambdaClient) {
|
|
4135
4268
|
defaultLambdaClient = new clientLambda.LambdaClient({
|
|
4269
|
+
customUserAgent: [[SDK_NAME, SDK_VERSION]],
|
|
4136
4270
|
requestHandler: {
|
|
4137
4271
|
connectionTimeout: 5000,
|
|
4138
4272
|
socketTimeout: 50000,
|
|
@@ -4331,8 +4465,9 @@ async function runHandler(event, context, executionContext, durableExecutionMode
|
|
|
4331
4465
|
const durableContext = createDurableContext(executionContext, context, durableExecutionMode,
|
|
4332
4466
|
// Default logger may not have the same type as Logger, but we should always provide a default logger even if the user overrides it
|
|
4333
4467
|
createDefaultLogger(), undefined, durableExecution);
|
|
4334
|
-
// Extract customerHandlerEvent from the
|
|
4335
|
-
|
|
4468
|
+
// Extract customerHandlerEvent from the complete operations array (after pagination)
|
|
4469
|
+
// This ensures we get the full payload even for large payloads that are paginated
|
|
4470
|
+
const initialExecutionEvent = executionContext._stepData[Object.keys(executionContext._stepData)[0]];
|
|
4336
4471
|
const customerHandlerEvent = JSON.parse(initialExecutionEvent?.ExecutionDetails?.InputPayload ?? "{}");
|
|
4337
4472
|
try {
|
|
4338
4473
|
log("🎯", `Starting handler execution, handler event: ${customerHandlerEvent}`);
|
|
@@ -4479,16 +4614,10 @@ async function runHandler(event, context, executionContext, durableExecutionMode
|
|
|
4479
4614
|
* Validates that the event is a proper durable execution input
|
|
4480
4615
|
*/
|
|
4481
4616
|
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);
|
|
4617
|
+
const eventObj = event;
|
|
4618
|
+
if (!eventObj?.DurableExecutionArn || !eventObj?.CheckpointToken) {
|
|
4619
|
+
throw new Error("Unexpected payload provided to start the durable execution.\n" +
|
|
4620
|
+
"Check your resource configurations to confirm the durability is set.");
|
|
4492
4621
|
}
|
|
4493
4622
|
}
|
|
4494
4623
|
/**
|
|
@@ -4566,14 +4695,7 @@ const withDurableExecution = (handler, config) => {
|
|
|
4566
4695
|
return async (event, context) => {
|
|
4567
4696
|
validateDurableExecutionEvent(event);
|
|
4568
4697
|
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
|
-
}
|
|
4698
|
+
return runHandler(event, context, executionContext, durableExecutionMode, checkpointToken, handler);
|
|
4577
4699
|
};
|
|
4578
4700
|
};
|
|
4579
4701
|
|