@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/README.md
CHANGED
|
@@ -285,17 +285,37 @@ Control execution guarantees:
|
|
|
285
285
|
```typescript
|
|
286
286
|
import { StepSemantics } from "@aws/durable-execution-sdk-js";
|
|
287
287
|
|
|
288
|
-
// At-
|
|
288
|
+
// At-least-once per retry (default)
|
|
289
|
+
await context.step("retriable-operation", async () => sendNotification(), {
|
|
290
|
+
semantics: StepSemantics.AtLeastOncePerRetry,
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// At-most-once per retry
|
|
289
294
|
await context.step("idempotent-operation", async () => updateDatabase(), {
|
|
290
295
|
semantics: StepSemantics.AtMostOncePerRetry,
|
|
291
296
|
});
|
|
297
|
+
```
|
|
292
298
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
299
|
+
**Important**: These semantics apply _per retry_, not per overall execution:
|
|
300
|
+
|
|
301
|
+
- **AtLeastOncePerRetry**: The step will execute at least once on each retry attempt. If the step succeeds but the checkpoint fails (e.g., sandbox crash), the step will re-execute on replay.
|
|
302
|
+
- **AtMostOncePerRetry**: The step will execute at most once per retry attempt. A checkpoint is created before execution, so if a failure occurs after the checkpoint but before step completion, the previous step retry attempt is skipped on replay.
|
|
303
|
+
|
|
304
|
+
**To achieve at-most-once semantics on a step-level**, use a custom retry strategy:
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
await context.step(
|
|
308
|
+
"truly-once-only",
|
|
309
|
+
async () => callThatCannotTolerateDuplicates(),
|
|
310
|
+
{
|
|
311
|
+
semantics: StepSemantics.AtMostOncePerRetry,
|
|
312
|
+
retryStrategy: () => ({ shouldRetry: false }), // No retries
|
|
313
|
+
},
|
|
314
|
+
);
|
|
297
315
|
```
|
|
298
316
|
|
|
317
|
+
Without this, a step using `AtMostOncePerRetry` with retries enabled could still execute multiple times across different retry attempts.
|
|
318
|
+
|
|
299
319
|
### Jitter Strategies
|
|
300
320
|
|
|
301
321
|
Prevent thundering herd:
|
package/dist/index.mjs
CHANGED
|
@@ -165,11 +165,56 @@ var DurableLogLevel;
|
|
|
165
165
|
})(DurableLogLevel || (DurableLogLevel = {}));
|
|
166
166
|
|
|
167
167
|
/**
|
|
168
|
+
* Execution semantics for step operations.
|
|
169
|
+
*
|
|
170
|
+
* @remarks
|
|
171
|
+
* These semantics control how step execution is checkpointed and replayed. **Important**: The guarantees apply *per
|
|
172
|
+
* retry attempt*, not per overall workflow execution.
|
|
173
|
+
*
|
|
174
|
+
* With retries enabled (the default), a step could execute multiple times across different retry attempts even when
|
|
175
|
+
* using `AtMostOncePerRetry`. To achieve step-level at-most-once execution, combine `AtMostOncePerRetry` with a retry
|
|
176
|
+
* strategy that disables retries (`shouldRetry: false`).
|
|
177
|
+
*
|
|
178
|
+
* @example
|
|
179
|
+
* ```typescript
|
|
180
|
+
* // At-least-once per retry (default) - safe for idempotent operations
|
|
181
|
+
* await context.step("send-notification", async () => sendEmail(), {
|
|
182
|
+
* semantics: StepSemantics.AtLeastOncePerRetry,
|
|
183
|
+
* });
|
|
184
|
+
*
|
|
185
|
+
* // At-most-once per retry - for non-idempotent operations
|
|
186
|
+
* await context.step("charge-payment", async () => processPayment(), {
|
|
187
|
+
* semantics: StepSemantics.AtMostOncePerRetry,
|
|
188
|
+
* retryStrategy: () => ({ shouldRetry: false }),
|
|
189
|
+
* });
|
|
190
|
+
* ```
|
|
191
|
+
*
|
|
168
192
|
* @public
|
|
169
193
|
*/
|
|
170
194
|
var StepSemantics;
|
|
171
195
|
(function (StepSemantics) {
|
|
196
|
+
/**
|
|
197
|
+
* At-most-once execution per retry attempt.
|
|
198
|
+
*
|
|
199
|
+
* @remarks
|
|
200
|
+
* A checkpoint is created before step execution. If a failure occurs after the checkpoint
|
|
201
|
+
* but before step completion, the previous step retry attempt is skipped on replay.
|
|
202
|
+
*
|
|
203
|
+
* **Note**: This is "at-most-once *per retry*". With multiple retry attempts, the step
|
|
204
|
+
* could still execute multiple times across different retries. To guarantee the step
|
|
205
|
+
* executes at most once, disable retries by returning
|
|
206
|
+
* `{ shouldRetry: false }` from your retry strategy.
|
|
207
|
+
*/
|
|
172
208
|
StepSemantics["AtMostOncePerRetry"] = "AT_MOST_ONCE_PER_RETRY";
|
|
209
|
+
/**
|
|
210
|
+
* At-least-once execution per retry attempt (default).
|
|
211
|
+
*
|
|
212
|
+
* @remarks
|
|
213
|
+
* The step will execute at least once on each retry attempt. If the step succeeds
|
|
214
|
+
* but the checkpoint fails (e.g., due to a sandbox crash), the step will re-execute
|
|
215
|
+
* on replay. This is the safer default for operations that are idempotent or can
|
|
216
|
+
* tolerate duplicate execution.
|
|
217
|
+
*/
|
|
173
218
|
StepSemantics["AtLeastOncePerRetry"] = "AT_LEAST_ONCE_PER_RETRY";
|
|
174
219
|
})(StepSemantics || (StepSemantics = {}));
|
|
175
220
|
/**
|
|
@@ -567,6 +612,26 @@ class StepInterruptedError extends Error {
|
|
|
567
612
|
}
|
|
568
613
|
}
|
|
569
614
|
|
|
615
|
+
/**
|
|
616
|
+
* Shared constants to avoid circular dependencies
|
|
617
|
+
*/
|
|
618
|
+
/**
|
|
619
|
+
* Controls whether stack traces are stored in error objects
|
|
620
|
+
* TODO: Accept this as configuration parameter in the future
|
|
621
|
+
*/
|
|
622
|
+
/**
|
|
623
|
+
* Checkpoint manager termination cooldown in milliseconds
|
|
624
|
+
* After the last operation completes, the checkpoint manager waits this duration
|
|
625
|
+
* before terminating to allow for any final checkpoint operations
|
|
626
|
+
*/
|
|
627
|
+
const CHECKPOINT_TERMINATION_COOLDOWN_MS = 20;
|
|
628
|
+
/**
|
|
629
|
+
* Maximum polling duration in milliseconds (15 minutes)
|
|
630
|
+
* Used to cap setTimeout delays to prevent 32-bit signed integer overflow
|
|
631
|
+
* and limit polling duration for long-running operations
|
|
632
|
+
*/
|
|
633
|
+
const MAX_POLL_DURATION_MS = 15 * 60 * 1000;
|
|
634
|
+
|
|
570
635
|
/**
|
|
571
636
|
* Base class for all durable operation errors
|
|
572
637
|
* @public
|
|
@@ -1355,6 +1420,7 @@ const createInvokeHandler = (context, checkpoint, createStepId, parentId, checkA
|
|
|
1355
1420
|
Payload: serializedPayload,
|
|
1356
1421
|
ChainedInvokeOptions: {
|
|
1357
1422
|
FunctionName: funcId,
|
|
1423
|
+
...(config?.tenantId && { TenantId: config.tenantId }),
|
|
1358
1424
|
},
|
|
1359
1425
|
});
|
|
1360
1426
|
}
|
|
@@ -2485,6 +2551,10 @@ class BatchResultImpl {
|
|
|
2485
2551
|
* Restores methods to deserialized BatchResult data
|
|
2486
2552
|
*/
|
|
2487
2553
|
function restoreBatchResult(data) {
|
|
2554
|
+
// If data is already a BatchResultImpl instance, return it as-is
|
|
2555
|
+
if (data instanceof BatchResultImpl) {
|
|
2556
|
+
return data;
|
|
2557
|
+
}
|
|
2488
2558
|
if (data &&
|
|
2489
2559
|
typeof data === "object" &&
|
|
2490
2560
|
"all" in data &&
|
|
@@ -2794,7 +2864,13 @@ class ConcurrencyController {
|
|
|
2794
2864
|
tryStartNext();
|
|
2795
2865
|
}
|
|
2796
2866
|
};
|
|
2797
|
-
|
|
2867
|
+
if (items.length === 0) {
|
|
2868
|
+
log("🎉", `${this.operationName} completed with no items`);
|
|
2869
|
+
resolve(new BatchResultImpl([], getCompletionReason(0)));
|
|
2870
|
+
}
|
|
2871
|
+
else {
|
|
2872
|
+
tryStartNext();
|
|
2873
|
+
}
|
|
2798
2874
|
});
|
|
2799
2875
|
}
|
|
2800
2876
|
}
|
|
@@ -2946,7 +3022,7 @@ const getStepData = (stepData, stepId) => {
|
|
|
2946
3022
|
};
|
|
2947
3023
|
|
|
2948
3024
|
class DurableContextImpl {
|
|
2949
|
-
|
|
3025
|
+
_executionContext;
|
|
2950
3026
|
lambdaContext;
|
|
2951
3027
|
_stepPrefix;
|
|
2952
3028
|
_stepCounter = 0;
|
|
@@ -2958,8 +3034,9 @@ class DurableContextImpl {
|
|
|
2958
3034
|
modeManagement;
|
|
2959
3035
|
durableExecution;
|
|
2960
3036
|
logger;
|
|
2961
|
-
|
|
2962
|
-
|
|
3037
|
+
executionContext;
|
|
3038
|
+
constructor(_executionContext, lambdaContext, durableExecutionMode, inheritedLogger, stepPrefix, durableExecution, parentId) {
|
|
3039
|
+
this._executionContext = _executionContext;
|
|
2963
3040
|
this.lambdaContext = lambdaContext;
|
|
2964
3041
|
this._stepPrefix = stepPrefix;
|
|
2965
3042
|
this._parentId = parentId;
|
|
@@ -2967,6 +3044,9 @@ class DurableContextImpl {
|
|
|
2967
3044
|
this.durableLogger = inheritedLogger;
|
|
2968
3045
|
this.durableLogger.configureDurableLoggingContext?.(this.getDurableLoggingContext());
|
|
2969
3046
|
this.logger = this.createModeAwareLogger(inheritedLogger);
|
|
3047
|
+
this.executionContext = {
|
|
3048
|
+
durableExecutionArn: _executionContext.durableExecutionArn,
|
|
3049
|
+
};
|
|
2970
3050
|
this.durableExecutionMode = durableExecutionMode;
|
|
2971
3051
|
this.checkpoint = durableExecution.checkpointManager;
|
|
2972
3052
|
this.modeManagement = new ModeManagement(this.captureExecutionState.bind(this), this.checkAndUpdateReplayMode.bind(this), this.checkForNonResolvingPromise.bind(this), () => this.durableExecutionMode, (mode) => {
|
|
@@ -2978,9 +3058,9 @@ class DurableContextImpl {
|
|
|
2978
3058
|
getDurableLogData: () => {
|
|
2979
3059
|
const activeContext = getActiveContext();
|
|
2980
3060
|
const result = {
|
|
2981
|
-
executionArn: this.
|
|
2982
|
-
requestId: this.
|
|
2983
|
-
tenantId: this.
|
|
3061
|
+
executionArn: this._executionContext.durableExecutionArn,
|
|
3062
|
+
requestId: this._executionContext.requestId,
|
|
3063
|
+
tenantId: this._executionContext.tenantId,
|
|
2984
3064
|
operationId: !activeContext || activeContext?.contextId === "root"
|
|
2985
3065
|
? undefined
|
|
2986
3066
|
: hashId(activeContext.contextId),
|
|
@@ -3057,7 +3137,7 @@ class DurableContextImpl {
|
|
|
3057
3137
|
checkAndUpdateReplayMode() {
|
|
3058
3138
|
if (this.durableExecutionMode === DurableExecutionMode.ReplayMode) {
|
|
3059
3139
|
const nextStepId = this.getNextStepId();
|
|
3060
|
-
const nextStepData = this.
|
|
3140
|
+
const nextStepData = this._executionContext.getStepData(nextStepId);
|
|
3061
3141
|
if (!nextStepData) {
|
|
3062
3142
|
this.durableExecutionMode = DurableExecutionMode.ExecutionMode;
|
|
3063
3143
|
}
|
|
@@ -3066,7 +3146,7 @@ class DurableContextImpl {
|
|
|
3066
3146
|
captureExecutionState() {
|
|
3067
3147
|
const wasInReplayMode = this.durableExecutionMode === DurableExecutionMode.ReplayMode;
|
|
3068
3148
|
const nextStepId = this.getNextStepId();
|
|
3069
|
-
const stepData = this.
|
|
3149
|
+
const stepData = this._executionContext.getStepData(nextStepId);
|
|
3070
3150
|
const wasNotFinished = !!(stepData &&
|
|
3071
3151
|
stepData.Status !== OperationStatus.SUCCEEDED &&
|
|
3072
3152
|
stepData.Status !== OperationStatus.FAILED);
|
|
@@ -3075,7 +3155,7 @@ class DurableContextImpl {
|
|
|
3075
3155
|
checkForNonResolvingPromise() {
|
|
3076
3156
|
if (this.durableExecutionMode === DurableExecutionMode.ReplaySucceededContext) {
|
|
3077
3157
|
const nextStepId = this.getNextStepId();
|
|
3078
|
-
const nextStepData = this.
|
|
3158
|
+
const nextStepData = this._executionContext.getStepData(nextStepId);
|
|
3079
3159
|
if (nextStepData &&
|
|
3080
3160
|
nextStepData.Status !== OperationStatus.SUCCEEDED &&
|
|
3081
3161
|
nextStepData.Status !== OperationStatus.FAILED) {
|
|
@@ -3091,16 +3171,16 @@ class DurableContextImpl {
|
|
|
3091
3171
|
return this.modeManagement.withDurableModeManagement(operation);
|
|
3092
3172
|
}
|
|
3093
3173
|
step(nameOrFn, fnOrOptions, maybeOptions) {
|
|
3094
|
-
validateContextUsage(this._stepPrefix, "step", this.
|
|
3174
|
+
validateContextUsage(this._stepPrefix, "step", this._executionContext.terminationManager);
|
|
3095
3175
|
return this.withDurableModeManagement(() => {
|
|
3096
|
-
const stepHandler = createStepHandler(this.
|
|
3176
|
+
const stepHandler = createStepHandler(this._executionContext, this.checkpoint, this.lambdaContext, this.createStepId.bind(this), this.durableLogger, this._parentId);
|
|
3097
3177
|
return stepHandler(nameOrFn, fnOrOptions, maybeOptions);
|
|
3098
3178
|
});
|
|
3099
3179
|
}
|
|
3100
3180
|
invoke(nameOrFuncId, funcIdOrInput, inputOrConfig, maybeConfig) {
|
|
3101
|
-
validateContextUsage(this._stepPrefix, "invoke", this.
|
|
3181
|
+
validateContextUsage(this._stepPrefix, "invoke", this._executionContext.terminationManager);
|
|
3102
3182
|
return this.withDurableModeManagement(() => {
|
|
3103
|
-
const invokeHandler = createInvokeHandler(this.
|
|
3183
|
+
const invokeHandler = createInvokeHandler(this._executionContext, this.checkpoint, this.createStepId.bind(this), this._parentId, this.checkAndUpdateReplayMode.bind(this));
|
|
3104
3184
|
return invokeHandler(...[
|
|
3105
3185
|
nameOrFuncId,
|
|
3106
3186
|
funcIdOrInput,
|
|
@@ -3110,18 +3190,18 @@ class DurableContextImpl {
|
|
|
3110
3190
|
});
|
|
3111
3191
|
}
|
|
3112
3192
|
runInChildContext(nameOrFn, fnOrOptions, maybeOptions) {
|
|
3113
|
-
validateContextUsage(this._stepPrefix, "runInChildContext", this.
|
|
3193
|
+
validateContextUsage(this._stepPrefix, "runInChildContext", this._executionContext.terminationManager);
|
|
3114
3194
|
return this.withDurableModeManagement(() => {
|
|
3115
|
-
const blockHandler = createRunInChildContextHandler(this.
|
|
3195
|
+
const blockHandler = createRunInChildContextHandler(this._executionContext, this.checkpoint, this.lambdaContext, this.createStepId.bind(this), () => this.durableLogger,
|
|
3116
3196
|
// Adapter function to maintain compatibility
|
|
3117
3197
|
(executionContext, parentContext, durableExecutionMode, inheritedLogger, stepPrefix, _checkpointToken, parentId) => createDurableContext(executionContext, parentContext, durableExecutionMode, inheritedLogger, stepPrefix, this.durableExecution, parentId), this._parentId);
|
|
3118
3198
|
return blockHandler(nameOrFn, fnOrOptions, maybeOptions);
|
|
3119
3199
|
});
|
|
3120
3200
|
}
|
|
3121
3201
|
wait(nameOrDuration, maybeDuration) {
|
|
3122
|
-
validateContextUsage(this._stepPrefix, "wait", this.
|
|
3202
|
+
validateContextUsage(this._stepPrefix, "wait", this._executionContext.terminationManager);
|
|
3123
3203
|
return this.withDurableModeManagement(() => {
|
|
3124
|
-
const waitHandler = createWaitHandler(this.
|
|
3204
|
+
const waitHandler = createWaitHandler(this._executionContext, this.checkpoint, this.createStepId.bind(this), this._parentId, this.checkAndUpdateReplayMode.bind(this));
|
|
3125
3205
|
return typeof nameOrDuration === "string"
|
|
3126
3206
|
? waitHandler(nameOrDuration, maybeDuration)
|
|
3127
3207
|
: waitHandler(nameOrDuration);
|
|
@@ -3153,23 +3233,23 @@ class DurableContextImpl {
|
|
|
3153
3233
|
}
|
|
3154
3234
|
}
|
|
3155
3235
|
createCallback(nameOrConfig, maybeConfig) {
|
|
3156
|
-
validateContextUsage(this._stepPrefix, "createCallback", this.
|
|
3236
|
+
validateContextUsage(this._stepPrefix, "createCallback", this._executionContext.terminationManager);
|
|
3157
3237
|
return this.withDurableModeManagement(() => {
|
|
3158
|
-
const callbackFactory = createCallback(this.
|
|
3238
|
+
const callbackFactory = createCallback(this._executionContext, this.checkpoint, this.createStepId.bind(this), this.checkAndUpdateReplayMode.bind(this), this._parentId);
|
|
3159
3239
|
return callbackFactory(nameOrConfig, maybeConfig);
|
|
3160
3240
|
});
|
|
3161
3241
|
}
|
|
3162
3242
|
waitForCallback(nameOrSubmitter, submitterOrConfig, maybeConfig) {
|
|
3163
|
-
validateContextUsage(this._stepPrefix, "waitForCallback", this.
|
|
3243
|
+
validateContextUsage(this._stepPrefix, "waitForCallback", this._executionContext.terminationManager);
|
|
3164
3244
|
return this.withDurableModeManagement(() => {
|
|
3165
|
-
const waitForCallbackHandler = createWaitForCallbackHandler(this.
|
|
3245
|
+
const waitForCallbackHandler = createWaitForCallbackHandler(this._executionContext, this.getNextStepId.bind(this), this.runInChildContext.bind(this));
|
|
3166
3246
|
return waitForCallbackHandler(nameOrSubmitter, submitterOrConfig, maybeConfig);
|
|
3167
3247
|
});
|
|
3168
3248
|
}
|
|
3169
3249
|
waitForCondition(nameOrCheckFunc, checkFuncOrConfig, maybeConfig) {
|
|
3170
|
-
validateContextUsage(this._stepPrefix, "waitForCondition", this.
|
|
3250
|
+
validateContextUsage(this._stepPrefix, "waitForCondition", this._executionContext.terminationManager);
|
|
3171
3251
|
return this.withDurableModeManagement(() => {
|
|
3172
|
-
const waitForConditionHandler = createWaitForConditionHandler(this.
|
|
3252
|
+
const waitForConditionHandler = createWaitForConditionHandler(this._executionContext, this.checkpoint, this.createStepId.bind(this), this.durableLogger, this._parentId);
|
|
3173
3253
|
return typeof nameOrCheckFunc === "string" ||
|
|
3174
3254
|
nameOrCheckFunc === undefined
|
|
3175
3255
|
? waitForConditionHandler(nameOrCheckFunc, checkFuncOrConfig, maybeConfig)
|
|
@@ -3177,23 +3257,23 @@ class DurableContextImpl {
|
|
|
3177
3257
|
});
|
|
3178
3258
|
}
|
|
3179
3259
|
map(nameOrItems, itemsOrMapFunc, mapFuncOrConfig, maybeConfig) {
|
|
3180
|
-
validateContextUsage(this._stepPrefix, "map", this.
|
|
3260
|
+
validateContextUsage(this._stepPrefix, "map", this._executionContext.terminationManager);
|
|
3181
3261
|
return this.withDurableModeManagement(() => {
|
|
3182
|
-
const mapHandler = createMapHandler(this.
|
|
3262
|
+
const mapHandler = createMapHandler(this._executionContext, this._executeConcurrently.bind(this));
|
|
3183
3263
|
return mapHandler(nameOrItems, itemsOrMapFunc, mapFuncOrConfig, maybeConfig);
|
|
3184
3264
|
});
|
|
3185
3265
|
}
|
|
3186
3266
|
parallel(nameOrBranches, branchesOrConfig, maybeConfig) {
|
|
3187
|
-
validateContextUsage(this._stepPrefix, "parallel", this.
|
|
3267
|
+
validateContextUsage(this._stepPrefix, "parallel", this._executionContext.terminationManager);
|
|
3188
3268
|
return this.withDurableModeManagement(() => {
|
|
3189
|
-
const parallelHandler = createParallelHandler(this.
|
|
3269
|
+
const parallelHandler = createParallelHandler(this._executionContext, this._executeConcurrently.bind(this));
|
|
3190
3270
|
return parallelHandler(nameOrBranches, branchesOrConfig, maybeConfig);
|
|
3191
3271
|
});
|
|
3192
3272
|
}
|
|
3193
3273
|
_executeConcurrently(nameOrItems, itemsOrExecutor, executorOrConfig, maybeConfig) {
|
|
3194
|
-
validateContextUsage(this._stepPrefix, "_executeConcurrently", this.
|
|
3274
|
+
validateContextUsage(this._stepPrefix, "_executeConcurrently", this._executionContext.terminationManager);
|
|
3195
3275
|
return this.withDurableModeManagement(() => {
|
|
3196
|
-
const concurrentExecutionHandler = createConcurrentExecutionHandler(this.
|
|
3276
|
+
const concurrentExecutionHandler = createConcurrentExecutionHandler(this._executionContext, this.runInChildContext.bind(this), this.skipNextOperation.bind(this));
|
|
3197
3277
|
const promise = concurrentExecutionHandler(nameOrItems, itemsOrExecutor, executorOrConfig, maybeConfig);
|
|
3198
3278
|
// Prevent unhandled promise rejections
|
|
3199
3279
|
promise?.catch(() => { });
|
|
@@ -3232,6 +3312,13 @@ class CheckpointUnrecoverableExecutionError extends UnrecoverableExecutionError
|
|
|
3232
3312
|
}
|
|
3233
3313
|
|
|
3234
3314
|
const STEP_DATA_UPDATED_EVENT = "stepDataUpdated";
|
|
3315
|
+
const TERMINAL_STATUSES = [
|
|
3316
|
+
OperationStatus.SUCCEEDED,
|
|
3317
|
+
OperationStatus.CANCELLED,
|
|
3318
|
+
OperationStatus.FAILED,
|
|
3319
|
+
OperationStatus.STOPPED,
|
|
3320
|
+
OperationStatus.TIMED_OUT,
|
|
3321
|
+
];
|
|
3235
3322
|
class CheckpointManager {
|
|
3236
3323
|
durableExecutionArn;
|
|
3237
3324
|
stepData;
|
|
@@ -3246,6 +3333,7 @@ class CheckpointManager {
|
|
|
3246
3333
|
forceCheckpointPromises = [];
|
|
3247
3334
|
queueCompletionResolver = null;
|
|
3248
3335
|
MAX_PAYLOAD_SIZE = 750 * 1024; // 750KB in bytes
|
|
3336
|
+
MAX_ITEMS_IN_BATCH = 250;
|
|
3249
3337
|
isTerminating = false;
|
|
3250
3338
|
static textEncoder = new TextEncoder();
|
|
3251
3339
|
// Operation lifecycle tracking
|
|
@@ -3253,7 +3341,6 @@ class CheckpointManager {
|
|
|
3253
3341
|
// Termination cooldown
|
|
3254
3342
|
terminationTimer = null;
|
|
3255
3343
|
terminationReason = null;
|
|
3256
|
-
TERMINATION_COOLDOWN_MS = 50;
|
|
3257
3344
|
constructor(durableExecutionArn, stepData, storage, terminationManager, initialTaskToken, stepDataEmitter, logger, finishedAncestors) {
|
|
3258
3345
|
this.durableExecutionArn = durableExecutionArn;
|
|
3259
3346
|
this.stepData = stepData;
|
|
@@ -3407,7 +3494,9 @@ class CheckpointManager {
|
|
|
3407
3494
|
while (this.queue.length > 0) {
|
|
3408
3495
|
const nextItem = this.queue[0];
|
|
3409
3496
|
const itemSize = CheckpointManager.textEncoder.encode(JSON.stringify(nextItem)).length;
|
|
3410
|
-
if (currentSize + itemSize > this.MAX_PAYLOAD_SIZE
|
|
3497
|
+
if ((currentSize + itemSize > this.MAX_PAYLOAD_SIZE ||
|
|
3498
|
+
batch.length >= this.MAX_ITEMS_IN_BATCH) &&
|
|
3499
|
+
batch.length > 0) {
|
|
3411
3500
|
break;
|
|
3412
3501
|
}
|
|
3413
3502
|
this.queue.shift();
|
|
@@ -3589,6 +3678,11 @@ class CheckpointManager {
|
|
|
3589
3678
|
if (op.state !== OperationLifecycleState.RETRY_WAITING) {
|
|
3590
3679
|
throw new Error(`Operation ${stepId} must be in RETRY_WAITING state, got ${op.state}`);
|
|
3591
3680
|
}
|
|
3681
|
+
// Resolve immediately if the step was completed already
|
|
3682
|
+
const stepData = this.stepData[hashId(stepId)];
|
|
3683
|
+
if (stepData?.Status && TERMINAL_STATUSES.includes(stepData.Status)) {
|
|
3684
|
+
return Promise.resolve();
|
|
3685
|
+
}
|
|
3592
3686
|
// Start timer with polling
|
|
3593
3687
|
this.startTimerWithPolling(stepId, op.endTimestamp);
|
|
3594
3688
|
// Return promise that resolves when status changes
|
|
@@ -3604,6 +3698,11 @@ class CheckpointManager {
|
|
|
3604
3698
|
if (op.state !== OperationLifecycleState.IDLE_AWAITED) {
|
|
3605
3699
|
throw new Error(`Operation ${stepId} must be in IDLE_AWAITED state, got ${op.state}`);
|
|
3606
3700
|
}
|
|
3701
|
+
// Resolve immediately if the step was completed already
|
|
3702
|
+
const stepData = this.stepData[hashId(stepId)];
|
|
3703
|
+
if (stepData?.Status && TERMINAL_STATUSES.includes(stepData.Status)) {
|
|
3704
|
+
return Promise.resolve();
|
|
3705
|
+
}
|
|
3607
3706
|
// Start timer with polling
|
|
3608
3707
|
this.startTimerWithPolling(stepId, op.endTimestamp);
|
|
3609
3708
|
// Return promise that resolves when status changes
|
|
@@ -3653,28 +3752,28 @@ class CheckpointManager {
|
|
|
3653
3752
|
op.resolver = undefined;
|
|
3654
3753
|
}
|
|
3655
3754
|
}
|
|
3656
|
-
|
|
3755
|
+
/**
|
|
3756
|
+
* Determines if the function should terminate.
|
|
3757
|
+
* @returns TerminationReason if the function should terminate, or undefined if the function should not terminate
|
|
3758
|
+
*/
|
|
3759
|
+
shouldTerminate() {
|
|
3657
3760
|
// Rule 1: Can't terminate if checkpoint queue is not empty
|
|
3658
3761
|
if (this.queue.length > 0) {
|
|
3659
|
-
|
|
3660
|
-
return;
|
|
3762
|
+
return undefined;
|
|
3661
3763
|
}
|
|
3662
3764
|
// Rule 2: Can't terminate if checkpoint is currently processing
|
|
3663
3765
|
if (this.isProcessing) {
|
|
3664
|
-
|
|
3665
|
-
return;
|
|
3766
|
+
return undefined;
|
|
3666
3767
|
}
|
|
3667
3768
|
// Rule 3: Can't terminate if there are pending force checkpoint promises
|
|
3668
3769
|
if (this.forceCheckpointPromises.length > 0) {
|
|
3669
|
-
|
|
3670
|
-
return;
|
|
3770
|
+
return undefined;
|
|
3671
3771
|
}
|
|
3672
3772
|
const allOps = Array.from(this.operations.values());
|
|
3673
3773
|
// Rule 4: Can't terminate if any operation is EXECUTING
|
|
3674
3774
|
const hasExecuting = allOps.some((op) => op.state === OperationLifecycleState.EXECUTING);
|
|
3675
3775
|
if (hasExecuting) {
|
|
3676
|
-
|
|
3677
|
-
return;
|
|
3776
|
+
return undefined;
|
|
3678
3777
|
}
|
|
3679
3778
|
// Rule 5: Clean up operations whose ancestors are complete or pending completion
|
|
3680
3779
|
for (const op of allOps) {
|
|
@@ -3697,12 +3796,17 @@ class CheckpointManager {
|
|
|
3697
3796
|
op.state === OperationLifecycleState.IDLE_NOT_AWAITED ||
|
|
3698
3797
|
op.state === OperationLifecycleState.IDLE_AWAITED);
|
|
3699
3798
|
if (hasWaiting) {
|
|
3700
|
-
|
|
3701
|
-
this.scheduleTermination(reason);
|
|
3799
|
+
return this.determineTerminationReason(remainingOps);
|
|
3702
3800
|
}
|
|
3703
|
-
|
|
3704
|
-
|
|
3801
|
+
return undefined;
|
|
3802
|
+
}
|
|
3803
|
+
checkAndTerminate() {
|
|
3804
|
+
const terminationReason = this.shouldTerminate();
|
|
3805
|
+
if (terminationReason) {
|
|
3806
|
+
this.scheduleTermination(terminationReason);
|
|
3807
|
+
return;
|
|
3705
3808
|
}
|
|
3809
|
+
this.abortTermination();
|
|
3706
3810
|
}
|
|
3707
3811
|
abortTermination() {
|
|
3708
3812
|
if (this.terminationTimer) {
|
|
@@ -3723,11 +3827,16 @@ class CheckpointManager {
|
|
|
3723
3827
|
this.terminationReason = reason;
|
|
3724
3828
|
log("⏱️", "Scheduling termination", {
|
|
3725
3829
|
reason,
|
|
3726
|
-
cooldownMs:
|
|
3830
|
+
cooldownMs: CHECKPOINT_TERMINATION_COOLDOWN_MS,
|
|
3727
3831
|
});
|
|
3728
3832
|
this.terminationTimer = setTimeout(() => {
|
|
3833
|
+
if (!this.shouldTerminate()) {
|
|
3834
|
+
log("🔄", "Termination conditions no longer valid after cooldown, aborting termination");
|
|
3835
|
+
this.abortTermination();
|
|
3836
|
+
return;
|
|
3837
|
+
}
|
|
3729
3838
|
this.executeTermination(reason);
|
|
3730
|
-
},
|
|
3839
|
+
}, CHECKPOINT_TERMINATION_COOLDOWN_MS);
|
|
3731
3840
|
}
|
|
3732
3841
|
executeTermination(reason) {
|
|
3733
3842
|
log("🛑", "Executing termination after cooldown", { reason });
|
|
@@ -3762,6 +3871,10 @@ class CheckpointManager {
|
|
|
3762
3871
|
const timestamp = endTimestamp instanceof Date ? endTimestamp : new Date(endTimestamp);
|
|
3763
3872
|
// Wait until endTimestamp
|
|
3764
3873
|
delay = Math.max(0, timestamp.getTime() - Date.now());
|
|
3874
|
+
// Skip setTimeout if delay exceeds MAX_POLL_DURATION_MS (Lambda will timeout before it fires)
|
|
3875
|
+
if (delay > MAX_POLL_DURATION_MS) {
|
|
3876
|
+
return;
|
|
3877
|
+
}
|
|
3765
3878
|
}
|
|
3766
3879
|
else {
|
|
3767
3880
|
// No timestamp, start polling immediately (1 second delay)
|
|
@@ -3781,7 +3894,6 @@ class CheckpointManager {
|
|
|
3781
3894
|
if (!op)
|
|
3782
3895
|
return;
|
|
3783
3896
|
// Check if we've exceeded max polling duration (15 minutes)
|
|
3784
|
-
const MAX_POLL_DURATION_MS = 15 * 60 * 1000; // 15 minutes
|
|
3785
3897
|
if (op.pollStartTime &&
|
|
3786
3898
|
Date.now() - op.pollStartTime > MAX_POLL_DURATION_MS) {
|
|
3787
3899
|
// Stop polling after 15 minutes to prevent indefinite resource consumption.
|
|
@@ -3893,6 +4005,13 @@ class TerminationManager extends EventEmitter {
|
|
|
3893
4005
|
// align the default behaviour of how logs are emitted to match the RIC logging behaviour for consistency.
|
|
3894
4006
|
// For custom logic, users can implement their own logger to log data differently.
|
|
3895
4007
|
// See: https://github.com/aws/aws-lambda-nodejs-runtime-interface-client/blob/962ed28eefbc052389c4de4366b1c0c49ee08a13/src/LogPatch.js
|
|
4008
|
+
/**
|
|
4009
|
+
* Format options for util.formatWithOptions.
|
|
4010
|
+
* Using breakLength: Infinity prevents util.inspect from inserting newlines
|
|
4011
|
+
* when formatting objects, regardless of object size (fixes issue #322).
|
|
4012
|
+
* Defined at module level to avoid creating a new object on every function call.
|
|
4013
|
+
*/
|
|
4014
|
+
const FORMAT_OPTIONS = { breakLength: Infinity };
|
|
3896
4015
|
/**
|
|
3897
4016
|
* JSON.stringify replacer function for Error objects.
|
|
3898
4017
|
* Based on AWS Lambda Runtime Interface Client LogPatch functionality.
|
|
@@ -3958,11 +4077,11 @@ function formatDurableLogData(level, logData, ...messageParams) {
|
|
|
3958
4077
|
return JSON.stringify(result, jsonErrorReplacer);
|
|
3959
4078
|
}
|
|
3960
4079
|
catch (_) {
|
|
3961
|
-
result.message = util.
|
|
4080
|
+
result.message = util.formatWithOptions(FORMAT_OPTIONS, result.message);
|
|
3962
4081
|
return JSON.stringify(result);
|
|
3963
4082
|
}
|
|
3964
4083
|
}
|
|
3965
|
-
result.message = util.
|
|
4084
|
+
result.message = util.formatWithOptions(FORMAT_OPTIONS, ...messageParams);
|
|
3966
4085
|
for (const param of messageParams) {
|
|
3967
4086
|
if (param instanceof Error) {
|
|
3968
4087
|
result.errorType = param?.constructor?.name ?? "UnknownError";
|
|
@@ -4117,6 +4236,20 @@ const createDefaultLogger = (executionContext) => {
|
|
|
4117
4236
|
return new DefaultLogger(executionContext);
|
|
4118
4237
|
};
|
|
4119
4238
|
|
|
4239
|
+
/**
|
|
4240
|
+
* SDK metadata injected by Rollup at build time from package.json.
|
|
4241
|
+
*
|
|
4242
|
+
* At build time, Rollup replaces "@aws/durable-execution-sdk-js" and
|
|
4243
|
+
* "1.0.3" with actual values from package.json.
|
|
4244
|
+
*
|
|
4245
|
+
* Defaults are provided for test environments where Rollup doesn't run
|
|
4246
|
+
* and process.env values are undefined.
|
|
4247
|
+
*
|
|
4248
|
+
* @internal
|
|
4249
|
+
*/
|
|
4250
|
+
const SDK_NAME = "@aws/durable-execution-sdk-js";
|
|
4251
|
+
const SDK_VERSION = "1.0.3";
|
|
4252
|
+
|
|
4120
4253
|
let defaultLambdaClient;
|
|
4121
4254
|
/**
|
|
4122
4255
|
* Durable execution client which uses an API-based LambdaClient
|
|
@@ -4131,6 +4264,7 @@ class DurableExecutionApiClient {
|
|
|
4131
4264
|
if (!client) {
|
|
4132
4265
|
if (!defaultLambdaClient) {
|
|
4133
4266
|
defaultLambdaClient = new LambdaClient({
|
|
4267
|
+
customUserAgent: [[SDK_NAME, SDK_VERSION]],
|
|
4134
4268
|
requestHandler: {
|
|
4135
4269
|
connectionTimeout: 5000,
|
|
4136
4270
|
socketTimeout: 50000,
|
|
@@ -4329,8 +4463,9 @@ async function runHandler(event, context, executionContext, durableExecutionMode
|
|
|
4329
4463
|
const durableContext = createDurableContext(executionContext, context, durableExecutionMode,
|
|
4330
4464
|
// Default logger may not have the same type as Logger, but we should always provide a default logger even if the user overrides it
|
|
4331
4465
|
createDefaultLogger(), undefined, durableExecution);
|
|
4332
|
-
// Extract customerHandlerEvent from the
|
|
4333
|
-
|
|
4466
|
+
// Extract customerHandlerEvent from the complete operations array (after pagination)
|
|
4467
|
+
// This ensures we get the full payload even for large payloads that are paginated
|
|
4468
|
+
const initialExecutionEvent = executionContext._stepData[Object.keys(executionContext._stepData)[0]];
|
|
4334
4469
|
const customerHandlerEvent = JSON.parse(initialExecutionEvent?.ExecutionDetails?.InputPayload ?? "{}");
|
|
4335
4470
|
try {
|
|
4336
4471
|
log("🎯", `Starting handler execution, handler event: ${customerHandlerEvent}`);
|
|
@@ -4477,16 +4612,10 @@ async function runHandler(event, context, executionContext, durableExecutionMode
|
|
|
4477
4612
|
* Validates that the event is a proper durable execution input
|
|
4478
4613
|
*/
|
|
4479
4614
|
function validateDurableExecutionEvent(event) {
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
}
|
|
4485
|
-
}
|
|
4486
|
-
catch {
|
|
4487
|
-
const msg = `Unexpected payload provided to start the durable execution.
|
|
4488
|
-
Check your resource configurations to confirm the durability is set.`;
|
|
4489
|
-
throw new Error(msg);
|
|
4615
|
+
const eventObj = event;
|
|
4616
|
+
if (!eventObj?.DurableExecutionArn || !eventObj?.CheckpointToken) {
|
|
4617
|
+
throw new Error("Unexpected payload provided to start the durable execution.\n" +
|
|
4618
|
+
"Check your resource configurations to confirm the durability is set.");
|
|
4490
4619
|
}
|
|
4491
4620
|
}
|
|
4492
4621
|
/**
|
|
@@ -4564,14 +4693,7 @@ const withDurableExecution = (handler, config) => {
|
|
|
4564
4693
|
return async (event, context) => {
|
|
4565
4694
|
validateDurableExecutionEvent(event);
|
|
4566
4695
|
const { executionContext, durableExecutionMode, checkpointToken } = await initializeExecutionContext(event, context, config?.client);
|
|
4567
|
-
|
|
4568
|
-
try {
|
|
4569
|
-
response = await runHandler(event, context, executionContext, durableExecutionMode, checkpointToken, handler);
|
|
4570
|
-
return response;
|
|
4571
|
-
}
|
|
4572
|
-
catch (err) {
|
|
4573
|
-
throw err;
|
|
4574
|
-
}
|
|
4696
|
+
return runHandler(event, context, executionContext, durableExecutionMode, checkpointToken, handler);
|
|
4575
4697
|
};
|
|
4576
4698
|
};
|
|
4577
4699
|
|