@lostgradient/weft 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/cli-main.js +63 -63
- package/dist/core/context/activity-retry-state.d.ts +6 -5
- package/dist/core/context/activity-retry-state.js +31 -21
- package/dist/core/context/durable-activity.d.ts +117 -0
- package/dist/core/context/durable-activity.js +79 -0
- package/dist/core/context/operation-request.d.ts +15 -0
- package/dist/core/context/parallel-operations.js +1 -0
- package/dist/core/context/run-operation-cached-request.d.ts +8 -0
- package/dist/core/context/run-operation-cached-request.js +59 -0
- package/dist/core/context/run-operation.d.ts +23 -5
- package/dist/core/context/run-operation.js +62 -47
- package/dist/core/engine/activity-heartbeat-tracking.d.ts +8 -17
- package/dist/core/engine/activity-heartbeat-tracking.js +7 -1
- package/dist/core/engine/activity-reconciliation.d.ts +1 -0
- package/dist/core/engine/activity-reconciliation.js +7 -2
- package/dist/core/engine/anonymous-signal-sequence.js +6 -4
- package/dist/core/engine/async-activity-completion.d.ts +5 -7
- package/dist/core/engine/async-activity-completion.js +13 -9
- package/dist/core/engine/bulk-operations-purge.js +2 -1
- package/dist/core/engine/bulk-operations.js +8 -7
- package/dist/core/engine/callback-checkpoint-persistence.d.ts +3 -0
- package/dist/core/engine/callback-checkpoint-persistence.js +25 -0
- package/dist/core/engine/callback-creators-bundles.js +4 -1
- package/dist/core/engine/checkpoint-io.d.ts +4 -1
- package/dist/core/engine/checkpoint-io.js +19 -10
- package/dist/core/engine/completed-review-storage.d.ts +2 -1
- package/dist/core/engine/completed-review-storage.js +4 -3
- package/dist/core/engine/index.d.ts +31 -0
- package/dist/core/engine/index.js +6 -1
- package/dist/core/engine/internals.d.ts +2 -1
- package/dist/core/engine/lease-manager.js +2 -2
- package/dist/core/engine/memo-durable-activity.d.ts +11 -0
- package/dist/core/engine/memo-durable-activity.js +282 -0
- package/dist/core/engine/operations-activity.d.ts +5 -1
- package/dist/core/engine/operations-activity.js +19 -7
- package/dist/core/engine/operations-data.d.ts +4 -1
- package/dist/core/engine/operations-data.js +3 -3
- package/dist/core/engine/reviews.js +7 -4
- package/dist/core/engine/schedule-timer.js +2 -0
- package/dist/core/engine/storage-io.js +1 -1
- package/dist/core/json.js +1 -1
- package/dist/core/weft-error.d.ts +1 -1
- package/dist/core/weft-error.js +2 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +6 -0
- package/dist/json-schema.js +1 -1
- package/dist/mcp/cli.js +26 -26
- package/dist/server/handler.js +21 -21
- package/dist/server/index.js +17 -17
- package/dist/server/runtime/websocket-worker.js +7 -2
- package/dist/server/serve-internals.d.ts +28 -0
- package/dist/server/serve-internals.js +4 -2
- package/dist/service-worker/index.js +22 -22
- package/dist/service-worker/setup.d.ts +18 -1
- package/dist/service-worker/setup.js +7 -4
- package/dist/storage/typed-storage.d.ts +1 -1
- package/dist/storage/typed-storage.js +1 -1
- package/dist/testing/index.js +27 -27
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
resolveActivityTimeout
|
|
17
17
|
} from "./activity-schedule-to-close.js";
|
|
18
18
|
import { getInternals, hasContextInternals } from "./internals.js";
|
|
19
|
+
import { getCachedRunActivityRequest } from "./run-operation-cached-request.js";
|
|
19
20
|
import { isActivityCallOptions } from "./session-state.js";
|
|
20
21
|
import { captureCallerStack } from "./validation.js";
|
|
21
22
|
export function asConcreteContext(context) {
|
|
@@ -42,13 +43,27 @@ function parseRunArguments(activity, rest) {
|
|
|
42
43
|
return { input: values[0], options };
|
|
43
44
|
}
|
|
44
45
|
export const readActivityRetryAttemptForTesting = readActivityRetryAttempt, readCompletedRetrySleepCountForTesting = readCompletedRetrySleepCount, completeActivityRetryAttemptForTesting = completeActivityRetryAttempt;
|
|
45
|
-
|
|
46
|
+
const ACTIVITY_RETRY_SLEEP_DEADLINE_LOCAL_PREFIX = "__weftActivityRetrySleepDeadline:";
|
|
47
|
+
export function readOrInitActivityRetrySleepFireAt(context, operationId, duration) {
|
|
48
|
+
const internals = getInternals(context), localKey = `${ACTIVITY_RETRY_SLEEP_DEADLINE_LOCAL_PREFIX}${operationId}`, existing = internals.checkpointLocals[localKey];
|
|
49
|
+
if (typeof existing === "number" && Number.isFinite(existing))
|
|
50
|
+
return existing;
|
|
51
|
+
if (existing !== void 0)
|
|
52
|
+
throw Error(`Invalid checkpointed activity retry sleep deadline ${JSON.stringify(existing)} for "${operationId}".`);
|
|
53
|
+
const scheduledFireAt = internals.getNow() + duration;
|
|
54
|
+
internals.checkpointLocals = {
|
|
55
|
+
...internals.checkpointLocals,
|
|
56
|
+
[localKey]: scheduledFireAt
|
|
57
|
+
};
|
|
58
|
+
return scheduledFireAt;
|
|
59
|
+
}
|
|
60
|
+
export function resolveScheduleToCloseBudget(internals, retryStateKey, scheduleToCloseTimeout) {
|
|
46
61
|
const budgetMs = parseScheduleToCloseBudgetMs(scheduleToCloseTimeout);
|
|
47
62
|
if (budgetMs === void 0)
|
|
48
63
|
return;
|
|
49
64
|
return {
|
|
50
65
|
budgetMs,
|
|
51
|
-
dispatchedAt: readOrInitActivityDispatchedAt(internals,
|
|
66
|
+
dispatchedAt: readOrInitActivityDispatchedAt(internals, retryStateKey, internals.getNow())
|
|
52
67
|
};
|
|
53
68
|
}
|
|
54
69
|
function getActivityName(activity, explicitName) {
|
|
@@ -59,39 +74,7 @@ function getActivityName(activity, explicitName) {
|
|
|
59
74
|
function getActivityFunction(activity) {
|
|
60
75
|
return typeof activity === "function" ? activity : void 0;
|
|
61
76
|
}
|
|
62
|
-
function
|
|
63
|
-
const hasCachedResult = internals.accumulatedResults?.has(step) ?? !1;
|
|
64
|
-
if (hasCachedResult) {
|
|
65
|
-
const cachedResult = internals.accumulatedResults?.get(step);
|
|
66
|
-
internals.stepIndex += readCompletedRetrySleepCount(internals, step);
|
|
67
|
-
if (internals.explainMode)
|
|
68
|
-
console.log(`[weft] ctx.run(${activityName}) \u2192 Returning cached result from step ${step}`);
|
|
69
|
-
return {
|
|
70
|
-
request: { type: "activity", operationId: "", activityName, input },
|
|
71
|
-
step,
|
|
72
|
-
hasCachedResult,
|
|
73
|
-
cachedResult,
|
|
74
|
-
retryAttempt: 1
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
const retryAttempt = readActivityRetryAttempt(internals, step);
|
|
78
|
-
if (retryAttempt !== void 0) {
|
|
79
|
-
internals.stepIndex += retryAttempt - 2;
|
|
80
|
-
return {
|
|
81
|
-
request: { type: "activity", operationId: "", activityName, input },
|
|
82
|
-
step,
|
|
83
|
-
hasCachedResult: !1,
|
|
84
|
-
retryAttempt
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
return {
|
|
88
|
-
request: { type: "activity", operationId: "", activityName, input },
|
|
89
|
-
step,
|
|
90
|
-
hasCachedResult,
|
|
91
|
-
retryAttempt: 1
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
function createFreshRunActivityRequest(internals, step, activityName, activityFunction, input, options) {
|
|
77
|
+
function createFreshRunActivityRequest(internals, step, activityName, activityFunction, input, options, activityStateKey) {
|
|
95
78
|
const queue = options?.queue ?? "default";
|
|
96
79
|
if (internals.explainMode) {
|
|
97
80
|
console.log(`[weft] ctx.run(${activityName}, ${JSON.stringify(input)})`);
|
|
@@ -103,6 +86,7 @@ function createFreshRunActivityRequest(internals, step, activityName, activityFu
|
|
|
103
86
|
operationId: crypto.randomUUID(),
|
|
104
87
|
activityName,
|
|
105
88
|
step,
|
|
89
|
+
...activityStateKey === void 0 ? {} : { activityStateKey },
|
|
106
90
|
...activityFunction !== void 0 ? { fn: activityFunction } : {},
|
|
107
91
|
input,
|
|
108
92
|
callerStack: captureCallerStack(),
|
|
@@ -130,7 +114,7 @@ function isNonRetryableActivityError(error, policy) {
|
|
|
130
114
|
export function shouldRetryActivityError(error, policy, attempt) {
|
|
131
115
|
return policy !== void 0 && attempt < policy.maxAttempts && !isNonRetryableActivityError(error, policy);
|
|
132
116
|
}
|
|
133
|
-
function prepareActivityRetryRequest(request, attempt) {
|
|
117
|
+
export function prepareActivityRetryRequest(request, attempt) {
|
|
134
118
|
if (attempt === 1)
|
|
135
119
|
return request;
|
|
136
120
|
return {
|
|
@@ -140,45 +124,76 @@ function prepareActivityRetryRequest(request, attempt) {
|
|
|
140
124
|
};
|
|
141
125
|
}
|
|
142
126
|
export function createRunActivityRequest(context, activity, rest, explicitName) {
|
|
143
|
-
|
|
127
|
+
parseRunArguments(activity, rest);
|
|
128
|
+
const internals = getInternals(context), step = internals.stepIndex++;
|
|
129
|
+
return createRunActivityRequestAtStep(context, activity, rest, step, {
|
|
130
|
+
...explicitName === void 0 ? {} : { explicitName },
|
|
131
|
+
advanceStepIndexForCachedRetryState: !0,
|
|
132
|
+
retryStateKey: step,
|
|
133
|
+
cacheResultStep: step
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
export function createRunActivityRequestAtStep(context, activity, rest, step, configuration = {}) {
|
|
137
|
+
const { input, options } = parseRunArguments(activity, rest), activityName = getActivityName(activity, configuration.explicitName), activityFunction = getActivityFunction(activity), internals = getInternals(context), retryStateKey = configuration.retryStateKey ?? step, cacheResultStep = configuration.cacheResultStep === !1 ? void 0 : configuration.cacheResultStep ?? step, cachedRequest = getCachedRunActivityRequest(internals, step, retryStateKey, cacheResultStep, activityName, input, {
|
|
138
|
+
advanceStepIndexForCachedRetryState: configuration.advanceStepIndexForCachedRetryState ?? !1
|
|
139
|
+
});
|
|
144
140
|
if (cachedRequest.hasCachedResult)
|
|
145
141
|
return cachedRequest;
|
|
146
|
-
const retryPolicy = resolveActivityRetryPolicy(activity, options), scheduleToCloseTimeout = resolveActivityScheduleToCloseTimeout(activity, options)
|
|
142
|
+
const retryPolicy = resolveActivityRetryPolicy(activity, options), scheduleToCloseTimeout = resolveActivityScheduleToCloseTimeout(activity, options);
|
|
147
143
|
return {
|
|
148
|
-
request: createFreshRunActivityRequest(internals, step, activityName, activityFunction, input,
|
|
144
|
+
request: createFreshRunActivityRequest(internals, step, activityName, activityFunction, input, resolveDispatchedActivityOptions(activity, options), configuration.activityStateKey),
|
|
149
145
|
step,
|
|
146
|
+
retryStateKey,
|
|
147
|
+
...cacheResultStep === void 0 ? {} : { cacheResultStep },
|
|
150
148
|
hasCachedResult: !1,
|
|
151
149
|
retryAttempt: cachedRequest.retryAttempt,
|
|
152
150
|
...retryPolicy === void 0 ? {} : { retryPolicy },
|
|
153
151
|
...scheduleToCloseTimeout === void 0 ? {} : { scheduleToCloseTimeout }
|
|
154
152
|
};
|
|
155
153
|
}
|
|
154
|
+
function resolveDispatchedActivityOptions(activity, options) {
|
|
155
|
+
const effectiveTimeout = resolveActivityTimeout(activity, options);
|
|
156
|
+
return effectiveTimeout === void 0 ? options : { ...options, timeout: effectiveTimeout };
|
|
157
|
+
}
|
|
156
158
|
export function* runActivityWithRetry(context, activity, rest, explicitName) {
|
|
159
|
+
return yield* runPreparedActivityWithRetry(context, createRunActivityRequest(context, activity, rest, explicitName), defaultActivityRetrySleep(context));
|
|
160
|
+
}
|
|
161
|
+
export function* runActivityWithRetryAtStep(context, activity, rest, step, configuration = {}) {
|
|
162
|
+
return yield* runPreparedActivityWithRetry(context, createRunActivityRequestAtStep(context, activity, rest, step, configuration), configuration.retrySleep ?? defaultActivityRetrySleep(context));
|
|
163
|
+
}
|
|
164
|
+
function defaultActivityRetrySleep(context) {
|
|
165
|
+
return function* sleepForRetry(duration) {
|
|
166
|
+
yield* context.sleep(duration);
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function* runPreparedActivityWithRetry(context, prepared, retrySleep) {
|
|
157
170
|
const {
|
|
158
171
|
request,
|
|
159
|
-
|
|
172
|
+
retryStateKey,
|
|
173
|
+
cacheResultStep,
|
|
160
174
|
hasCachedResult,
|
|
161
175
|
cachedResult,
|
|
162
176
|
retryAttempt,
|
|
163
177
|
retryPolicy,
|
|
164
178
|
scheduleToCloseTimeout
|
|
165
|
-
} =
|
|
179
|
+
} = prepared;
|
|
166
180
|
if (hasCachedResult)
|
|
167
181
|
return cachedResult;
|
|
168
|
-
const internals = getInternals(context), budget = retryPolicy === void 0 ? void 0 : resolveScheduleToCloseBudget(internals,
|
|
182
|
+
const internals = getInternals(context), budget = retryPolicy === void 0 ? void 0 : resolveScheduleToCloseBudget(internals, retryStateKey, scheduleToCloseTimeout);
|
|
169
183
|
let attempt = retryAttempt;
|
|
170
184
|
if (attempt > 1) {
|
|
171
185
|
if (retryPolicy === void 0)
|
|
172
186
|
throw Error(`Missing activity retry policy for checkpointed retry attempt ${attempt}`);
|
|
173
|
-
yield*
|
|
187
|
+
yield* retrySleep(calculateBackoff(attempt - 1, retryPolicy), attempt);
|
|
174
188
|
}
|
|
175
189
|
while (!0) {
|
|
176
190
|
if (attempt > 1)
|
|
177
191
|
assertScheduleToCloseBudgetNotExhausted(budget, request.activityName, internals.getNow());
|
|
178
192
|
try {
|
|
179
193
|
const result = yield prepareActivityRetryRequest(request, attempt);
|
|
180
|
-
|
|
181
|
-
|
|
194
|
+
if (cacheResultStep !== void 0)
|
|
195
|
+
context.accumulatedResults.set(cacheResultStep, result);
|
|
196
|
+
completeActivityRetryAttempt(internals, retryStateKey, attempt - 1);
|
|
182
197
|
return result;
|
|
183
198
|
} catch (error) {
|
|
184
199
|
if (!shouldRetryActivityError(error, retryPolicy, attempt))
|
|
@@ -186,8 +201,8 @@ export function* runActivityWithRetry(context, activity, rest, explicitName) {
|
|
|
186
201
|
const backoff = calculateBackoff(attempt, retryPolicy), now = internals.getNow();
|
|
187
202
|
assertScheduleToCloseBudgetNotExhausted(budget, request.activityName, now, now + backoff);
|
|
188
203
|
const nextAttempt = attempt + 1;
|
|
189
|
-
writeActivityRetryAttempt(internals,
|
|
190
|
-
yield*
|
|
204
|
+
writeActivityRetryAttempt(internals, retryStateKey, nextAttempt);
|
|
205
|
+
yield* retrySleep(backoff, nextAttempt);
|
|
191
206
|
attempt = nextAttempt;
|
|
192
207
|
}
|
|
193
208
|
}
|
|
@@ -1,19 +1,10 @@
|
|
|
1
1
|
import type { ActivityContext } from '../types.ts';
|
|
2
2
|
import type { EngineInternals } from './internals.ts';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
* later step's first attempt never inherits an earlier step's heartbeat, and
|
|
9
|
-
* concurrent `ctx.all` activities never clobber one another. The step is stable
|
|
10
|
-
* across retry attempts (assigned once at `stepIndex++`). Held only in engine
|
|
11
|
-
* memory and cleared by workflowId (the outer key) on terminal cleanup and purge.
|
|
12
|
-
*/
|
|
13
|
-
/** Record the heartbeat the current attempt of a step sent. */
|
|
14
|
-
export declare function recordLastHeartbeatForStep(internals: EngineInternals, workflowId: string, step: number, details: unknown): void;
|
|
15
|
-
/** Read the heartbeat a prior attempt of this step recorded, or `undefined`. */
|
|
16
|
-
export declare function readLastHeartbeatForStep(internals: EngineInternals, workflowId: string, step: number): unknown;
|
|
3
|
+
export type ActivityHeartbeatKey = number | string;
|
|
4
|
+
/** Record the heartbeat the current attempt of an activity sent. */
|
|
5
|
+
export declare function recordLastHeartbeatForStep(internals: EngineInternals, workflowId: string, step: ActivityHeartbeatKey, details: unknown): void;
|
|
6
|
+
/** Read the heartbeat a prior attempt of this activity recorded, or `undefined`. */
|
|
7
|
+
export declare function readLastHeartbeatForStep(internals: EngineInternals, workflowId: string, step: ActivityHeartbeatKey): unknown;
|
|
17
8
|
/**
|
|
18
9
|
* Drop the heartbeat tracked for a single step once that step has completed
|
|
19
10
|
* successfully — after inline verify, and (on the idempotency path) after the
|
|
@@ -26,7 +17,7 @@ export declare function readLastHeartbeatForStep(internals: EngineInternals, wor
|
|
|
26
17
|
* resumable-batch heartbeat a retry depends on, so this is only ever called from
|
|
27
18
|
* the non-speculative success path.
|
|
28
19
|
*/
|
|
29
|
-
export declare function clearLastHeartbeatForStep(internals: EngineInternals, workflowId: string, step:
|
|
20
|
+
export declare function clearLastHeartbeatForStep(internals: EngineInternals, workflowId: string, step: ActivityHeartbeatKey): void;
|
|
30
21
|
/**
|
|
31
22
|
* #493: in development mode, emit a COARSE warning when an inline activity RETRY
|
|
32
23
|
* (`attempt > 1`) starts with no `lastHeartbeatDetails` — the closest runtime
|
|
@@ -53,7 +44,7 @@ export declare function clearLastHeartbeatForStep(internals: EngineInternals, wo
|
|
|
53
44
|
* routing model would need this gate to key on the resolved per-dispatch mode
|
|
54
45
|
* instead.
|
|
55
46
|
*/
|
|
56
|
-
export declare function warnIfRetryMissingHeartbeat(internals: EngineInternals, workflowId: string, step:
|
|
47
|
+
export declare function warnIfRetryMissingHeartbeat(internals: EngineInternals, workflowId: string, step: ActivityHeartbeatKey, attempt: number): void;
|
|
57
48
|
/**
|
|
58
49
|
* Build the {@link ActivityContext} handed to an inline activity function. The
|
|
59
50
|
* signal comes from the per-workflow AbortController (so workflow cancellation
|
|
@@ -61,4 +52,4 @@ export declare function warnIfRetryMissingHeartbeat(internals: EngineInternals,
|
|
|
61
52
|
* tracking above; `completeAsync` is supplied by the caller (it owns the
|
|
62
53
|
* async-completion token machinery).
|
|
63
54
|
*/
|
|
64
|
-
export declare function buildActivityContext(internals: EngineInternals, workflowId: string, step:
|
|
55
|
+
export declare function buildActivityContext(internals: EngineInternals, workflowId: string, step: ActivityHeartbeatKey, signal: AbortSignal, completeAsync: () => never): ActivityContext;
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { DevelopmentWarningEvent } from "../events.js";
|
|
2
|
+
function activityHeartbeatKeyLabel(key) {
|
|
3
|
+
return typeof key === "number" ? `step ${String(key)}` : `activity state key ${key}`;
|
|
4
|
+
}
|
|
5
|
+
function activityHeartbeatFieldPath(key) {
|
|
6
|
+
return typeof key === "number" ? `step.${String(key)}.lastHeartbeatDetails` : `activityStateKey.${key}.lastHeartbeatDetails`;
|
|
7
|
+
}
|
|
2
8
|
export function recordLastHeartbeatForStep(internals, workflowId, step, details) {
|
|
3
9
|
let byStep = internals.lastHeartbeatDetailsByStep.get(workflowId);
|
|
4
10
|
if (byStep === void 0) {
|
|
@@ -27,7 +33,7 @@ export function warnIfRetryMissingHeartbeat(internals, workflowId, step, attempt
|
|
|
27
33
|
return;
|
|
28
34
|
if (readLastHeartbeatForStep(internals, workflowId, step) !== void 0)
|
|
29
35
|
return;
|
|
30
|
-
internals.engine.dispatchEvent(new DevelopmentWarningEvent(workflowId, `Activity retry (attempt ${attempt}) at
|
|
36
|
+
internals.engine.dispatchEvent(new DevelopmentWarningEvent(workflowId, `Activity retry (attempt ${attempt}) at ${activityHeartbeatKeyLabel(step)} has no lastHeartbeatDetails. Either the previous attempt never recorded heartbeat details (it never called heartbeat(), or called it with no details), or the engine process restarted and discarded the in-memory heartbeat (it is not durable). The resumable-batch pattern only resumes across in-process retries; design the activity to restart cleanly when lastHeartbeatDetails is undefined.`, [activityHeartbeatFieldPath(step)]));
|
|
31
37
|
}
|
|
32
38
|
export function buildActivityContext(internals, workflowId, step, signal, completeAsync) {
|
|
33
39
|
return {
|
|
@@ -102,6 +102,7 @@ export declare function resolveStartedActivityReconciliationRecord(internals: En
|
|
|
102
102
|
}>;
|
|
103
103
|
export declare function writeActivityReconciliationTransition(storage: Storage, reference: ActivityReconciliationReference, expectedRecord: ActivityReconciliationRecord, nextRecord: ActivityReconciliationRecord): Promise<void>;
|
|
104
104
|
export declare function stageActivityReconciliationTransitionWithAtomicWorkflowCommit(internals: EngineInternals, workflowId: string, reference: ActivityReconciliationReference, expectedRecord: ActivityReconciliationRecord, nextRecord: ActivityReconciliationRecord): void;
|
|
105
|
+
export declare function commitActivityReconciliationTransitionWithFencedWrite(internals: EngineInternals, reference: ActivityReconciliationReference, expectedRecord: ActivityReconciliationRecord, nextRecord: ActivityReconciliationRecord): Promise<void>;
|
|
105
106
|
export declare function normalizePreDispatchVerificationResult(result: ActivityVerificationResult): 'not-completed' | 'completed-result-unavailable' | 'indeterminate' | {
|
|
106
107
|
result: unknown;
|
|
107
108
|
};
|
|
@@ -7,6 +7,7 @@ import { decode, encode } from "../codec.js";
|
|
|
7
7
|
import { assertPayloadWithinLimit } from "../payload-size.js";
|
|
8
8
|
import { WeftError } from "../weft-error.js";
|
|
9
9
|
import { stageAtomicWorkflowCommitSideEffects } from "./checkpoint-side-effects.js";
|
|
10
|
+
import { commitFencedEngineWrite } from "./fenced-write.js";
|
|
10
11
|
|
|
11
12
|
export class ActivityReconciliationCapabilityError extends WeftError {
|
|
12
13
|
constructor() {
|
|
@@ -83,7 +84,7 @@ export async function resolveStartedActivityReconciliationRecord(internals, work
|
|
|
83
84
|
ownerId: crypto.randomUUID(),
|
|
84
85
|
updatedAt: internals.options.getNow()
|
|
85
86
|
};
|
|
86
|
-
await
|
|
87
|
+
await commitActivityReconciliationTransitionWithFencedWrite(internals, reference, record, nextRecord);
|
|
87
88
|
return nextRecord;
|
|
88
89
|
}
|
|
89
90
|
if (normalized === "completed-result-unavailable")
|
|
@@ -92,7 +93,7 @@ export async function resolveStartedActivityReconciliationRecord(internals, work
|
|
|
92
93
|
throw new ActivityReconciliationIndeterminateError(`Activity "${operation.activityName}" reconciliation is indeterminate.`);
|
|
93
94
|
validateActivityResultForReconciliation(normalized.result, internals.options.payloadSizePolicy.maxBytes);
|
|
94
95
|
const completedRecord = createCompletedActivityReconciliationRecord(record, normalized.result, internals.options.getNow());
|
|
95
|
-
await
|
|
96
|
+
await commitActivityReconciliationTransitionWithFencedWrite(internals, reference, record, completedRecord);
|
|
96
97
|
return { completedResult: normalized.result };
|
|
97
98
|
}
|
|
98
99
|
export async function writeActivityReconciliationTransition(storage, reference, expectedRecord, nextRecord) {
|
|
@@ -103,6 +104,10 @@ export async function writeActivityReconciliationTransition(storage, reference,
|
|
|
103
104
|
export function stageActivityReconciliationTransitionWithAtomicWorkflowCommit(internals, workflowId, reference, expectedRecord, nextRecord) {
|
|
104
105
|
stageAtomicWorkflowCommitSideEffects(internals, workflowId, buildActivityReconciliationTransitionSideEffects(reference, expectedRecord, nextRecord));
|
|
105
106
|
}
|
|
107
|
+
export async function commitActivityReconciliationTransitionWithFencedWrite(internals, reference, expectedRecord, nextRecord) {
|
|
108
|
+
const sideEffects = buildActivityReconciliationTransitionSideEffects(reference, expectedRecord, nextRecord);
|
|
109
|
+
await commitFencedEngineWrite(internals, sideEffects.operations, sideEffects.conditions, () => new ActivityReconciliationConflictError("Activity reconciliation completion lost compare-and-set ownership."));
|
|
110
|
+
}
|
|
106
111
|
function buildActivityReconciliationTransitionSideEffects(reference, expectedRecord, nextRecord) {
|
|
107
112
|
return {
|
|
108
113
|
conditions: [{ key: reference.key, expectedValue: encode(expectedRecord) }],
|
|
@@ -79,16 +79,18 @@ async function scanNextAnonymousSignalSequence(internals, workflowId) {
|
|
|
79
79
|
if (sequence !== null && sequence >= nextSequence)
|
|
80
80
|
nextSequence = sequence + 1;
|
|
81
81
|
}
|
|
82
|
+
if (!Number.isSafeInteger(nextSequence))
|
|
83
|
+
throw Error(`Anonymous signal sequence overflow for workflow "${workflowId}": computed ${nextSequence}`);
|
|
82
84
|
return nextSequence;
|
|
83
85
|
}
|
|
84
86
|
function extractAnonymousSignalSequence(key) {
|
|
85
|
-
const
|
|
86
|
-
if (
|
|
87
|
+
const idComponent = key.slice(key.lastIndexOf(":") + 1), marker = "anonymous%3A";
|
|
88
|
+
if (!idComponent.startsWith(marker))
|
|
87
89
|
return null;
|
|
88
|
-
const sequenceStart =
|
|
90
|
+
const sequenceStart = marker.length, sequenceEnd = idComponent.indexOf("%3A", sequenceStart);
|
|
89
91
|
if (sequenceEnd === -1)
|
|
90
92
|
return null;
|
|
91
|
-
const sequence = Number(
|
|
93
|
+
const sequence = Number(idComponent.slice(sequenceStart, sequenceEnd));
|
|
92
94
|
return Number.isSafeInteger(sequence) && sequence >= 0 ? sequence : null;
|
|
93
95
|
}
|
|
94
96
|
function decodeSignalSequence(bytes) {
|
|
@@ -26,9 +26,6 @@
|
|
|
26
26
|
* arrives after token recovery but before replay has adopted the workflow
|
|
27
27
|
* generator, the engine buffers the completion or failure outcome and drains it
|
|
28
28
|
* when replay reaches the same deterministic token.
|
|
29
|
-
*
|
|
30
|
-
* Internal-only. Imported from `src/core/engine/**` and the `ActivityContext`
|
|
31
|
-
* construction path.
|
|
32
29
|
*/
|
|
33
30
|
import type { OperationOutcome } from '../types.ts';
|
|
34
31
|
import { WeftError } from '../weft-error.ts';
|
|
@@ -101,13 +98,14 @@ export type PendingAsyncActivityResolution = {
|
|
|
101
98
|
/**
|
|
102
99
|
* Derive the durable, deterministic task token for an async activity.
|
|
103
100
|
*
|
|
104
|
-
* The token is anchored to the workflow id, the
|
|
101
|
+
* The token is anchored to the workflow id, the activity state key, and the
|
|
105
102
|
* dispatch attempt — all of which are stable across replay — so a workflow that
|
|
106
103
|
* crashes while parked on an async activity mints the identical token after
|
|
107
|
-
* recovery. `
|
|
108
|
-
*
|
|
104
|
+
* recovery. Plain `ctx.run()` uses the workflow step as the state key.
|
|
105
|
+
* `operationId` is deliberately excluded because it is regenerated on every
|
|
106
|
+
* yield and would change on replay.
|
|
109
107
|
*/
|
|
110
|
-
export declare function deriveAsyncActivityToken(workflowId: string, step: number, attempt: number): string;
|
|
108
|
+
export declare function deriveAsyncActivityToken(workflowId: string, step: number | string, attempt: number): string;
|
|
111
109
|
/**
|
|
112
110
|
* Register a deferred activity: record it in memory and durably, then announce
|
|
113
111
|
* the token via {@link ActivityAsyncPendingEvent}. Idempotent on `token`: if the
|
|
@@ -3,6 +3,8 @@ import { decode, encode } from "../codec.js";
|
|
|
3
3
|
import { ActivityAsyncPendingEvent } from "../events.js";
|
|
4
4
|
import { assertPayloadWithinLimit } from "../payload-size.js";
|
|
5
5
|
import { WeftError } from "../weft-error.js";
|
|
6
|
+
import { stageAtomicWorkflowCommitSideEffects } from "./checkpoint-side-effects.js";
|
|
7
|
+
import { commitFencedEngineWrite } from "./fenced-write.js";
|
|
6
8
|
const ASYNC_ACTIVITY_TOKEN_PREFIX = "async-act:v1", ASYNC_ACTIVITY_KEY_PREFIX = "async-act:v1:";
|
|
7
9
|
export function asyncActivityWorkflowPrefix(workflowId) {
|
|
8
10
|
return `${ASYNC_ACTIVITY_KEY_PREFIX}${encodeStorageKeyComponent(workflowId)}:`;
|
|
@@ -33,7 +35,7 @@ function isPersistedAsyncActivity(value) {
|
|
|
33
35
|
export function deriveAsyncActivityToken(workflowId, step, attempt) {
|
|
34
36
|
return `${ASYNC_ACTIVITY_TOKEN_PREFIX}:${workflowId}:${step}:${attempt}`;
|
|
35
37
|
}
|
|
36
|
-
function
|
|
38
|
+
function buildPersistPendingAsyncActivityOperation(pending) {
|
|
37
39
|
const record = {
|
|
38
40
|
version: 1,
|
|
39
41
|
token: pending.token,
|
|
@@ -44,12 +46,16 @@ function persistPendingAsyncActivity(storage, pending) {
|
|
|
44
46
|
attempt: pending.attempt,
|
|
45
47
|
createdAt: pending.createdAt
|
|
46
48
|
};
|
|
47
|
-
return
|
|
49
|
+
return {
|
|
50
|
+
type: "put",
|
|
51
|
+
key: KEYS.asyncActivity(pending.workflowId, pending.token),
|
|
52
|
+
value: encode(record)
|
|
53
|
+
};
|
|
48
54
|
}
|
|
49
55
|
export async function registerPendingAsyncActivity(internals, pending) {
|
|
50
56
|
const alreadyRegistered = internals.pendingAsyncActivities.has(pending.token);
|
|
51
57
|
internals.pendingAsyncActivities.set(pending.token, pending);
|
|
52
|
-
await
|
|
58
|
+
await commitFencedEngineWrite(internals, [buildPersistPendingAsyncActivityOperation(pending)], [], () => Error(`Async activity registration for token "${pending.token}" lost its precondition.`));
|
|
53
59
|
if (!alreadyRegistered)
|
|
54
60
|
internals.engine.dispatchEvent(new ActivityAsyncPendingEvent(pending.token, pending.operationId, pending.workflowId, pending.activityName, pending.attempt));
|
|
55
61
|
}
|
|
@@ -87,12 +93,10 @@ async function consumePendingAsyncActivity(internals, token) {
|
|
|
87
93
|
if (!pending)
|
|
88
94
|
throw new AsyncActivityTokenNotFoundError(token);
|
|
89
95
|
internals.pendingAsyncActivities.delete(token);
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
throw error;
|
|
95
|
-
}
|
|
96
|
+
stageAtomicWorkflowCommitSideEffects(internals, pending.workflowId, {
|
|
97
|
+
conditions: [],
|
|
98
|
+
operations: [{ type: "delete", key: KEYS.asyncActivity(pending.workflowId, token) }]
|
|
99
|
+
});
|
|
96
100
|
return pending;
|
|
97
101
|
}
|
|
98
102
|
function shouldBufferPendingAsyncActivityResolution(internals, workflowId) {
|
|
@@ -10,6 +10,7 @@ import { buildIndexOperations } from "../search-attributes.js";
|
|
|
10
10
|
import { buildWorkflowTagIndexOperations, normalizeWorkflowTags } from "../workflow-tags.js";
|
|
11
11
|
import { asyncActivityWorkflowPrefix } from "./async-activity-completion.js";
|
|
12
12
|
import { forgetCommittedCheckpointBytes } from "./checkpoint-commit-snapshots.js";
|
|
13
|
+
import { commitFencedEngineWrite } from "./fenced-write.js";
|
|
13
14
|
import { streamWorkflowStates } from "./listing.js";
|
|
14
15
|
import { createTerminalCleanupTimerId } from "./state-utilities.js";
|
|
15
16
|
import {
|
|
@@ -131,7 +132,7 @@ function getWorkflowRetentionDeadline(internals, state) {
|
|
|
131
132
|
}
|
|
132
133
|
export async function purgeWorkflow(internals, state, cleanupWaiters) {
|
|
133
134
|
const deleteOperations = await collectWorkflowPurgeDeleteOperations(internals, state);
|
|
134
|
-
await internals
|
|
135
|
+
await commitFencedEngineWrite(internals, deleteOperations, [], () => Error(`Purge commit for workflow "${state.id}" lost its precondition.`));
|
|
135
136
|
clearPurgedWorkflowInMemoryState(internals, state.id, cleanupWaiters);
|
|
136
137
|
}
|
|
137
138
|
export async function collectWorkflowPurgeDeleteOperations(internals, state) {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
KEYS,
|
|
3
3
|
requireStorageCapability,
|
|
4
|
-
storageConditionalBatch,
|
|
5
4
|
storageHas
|
|
6
5
|
} from "../../storage/interface.js";
|
|
7
6
|
import { assertScopedBulkWorkflowFilter } from "../bulk-workflow-filter.js";
|
|
@@ -24,6 +23,10 @@ import {
|
|
|
24
23
|
withBulkAuditEvent
|
|
25
24
|
} from "./bulk-operations-shared.js";
|
|
26
25
|
import { BulkDeleteRequiresTerminalWorkflowsError } from "./errors.js";
|
|
26
|
+
import {
|
|
27
|
+
assertLeaseHeldForEngineWork,
|
|
28
|
+
commitFencedEngineWriteAllowingPreconditionFailure
|
|
29
|
+
} from "./fenced-write.js";
|
|
27
30
|
import { BULK_OPERATION_BATCH_SIZE } from "./listing.js";
|
|
28
31
|
import { createTerminalCleanupTimerId } from "./state-utilities.js";
|
|
29
32
|
import { loadWorkflowState, runSerializedWorkflowStateWrite } from "./storage-io.js";
|
|
@@ -132,6 +135,7 @@ async function runBulkFailedWorkflowRetry(internals, filter, options = {}) {
|
|
|
132
135
|
return withBulkAuditEvent(internals, preparation, options, result, retried);
|
|
133
136
|
}
|
|
134
137
|
export async function retryFailedAll(internals, filter, options = {}) {
|
|
138
|
+
assertLeaseHeldForEngineWork(internals);
|
|
135
139
|
return runBulkFailedWorkflowRetry(internals, filter, options);
|
|
136
140
|
}
|
|
137
141
|
async function retryFailedWorkflow(internals, workflowId) {
|
|
@@ -194,12 +198,9 @@ async function reactivateFailedWorkflowFromCheckpointSerialized(internals, workf
|
|
|
194
198
|
throw Error(`Workflow concurrency admission for "${lastConcurrencyStateKey ?? workflowId}" changed too many times while retrying failed workflow "${workflowId}"`);
|
|
195
199
|
}
|
|
196
200
|
async function commitFailedWorkflowReactivation(internals, operations, conditions) {
|
|
197
|
-
if (conditions.length
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
requireStorageCapability(internals.storage, "conditionalBatch", "retry failed workflow");
|
|
202
|
-
return storageConditionalBatch(internals.storage, conditions, operations);
|
|
201
|
+
if (conditions.length > 0)
|
|
202
|
+
requireStorageCapability(internals.storage, "conditionalBatch", "retry failed workflow");
|
|
203
|
+
return commitFencedEngineWriteAllowingPreconditionFailure(internals, operations, conditions);
|
|
203
204
|
}
|
|
204
205
|
function buildReactivatedWorkflowState(internals, state) {
|
|
205
206
|
const reactivatedState = {
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ContextOperationRequest } from '../context.ts';
|
|
2
|
+
import type { Engine } from './index.ts';
|
|
3
|
+
export declare function persistCheckpointForDataOperation<TWorkflows extends object, TActivities extends object>(engine: Engine<TWorkflows, TActivities>, workflowId: string, operation: ContextOperationRequest): Promise<void>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { HISTORY_CIRCUIT_BREAKER_REASON } from "../types.js";
|
|
2
|
+
import { validateAttributeValueSizes } from "./attributes-tags.js";
|
|
3
|
+
import { createTerminationCallbacks } from "./callback-creators-core.js";
|
|
4
|
+
import {
|
|
5
|
+
appendTimelineBatchOperations,
|
|
6
|
+
persistCheckpoint,
|
|
7
|
+
pruneCheckpointHistory
|
|
8
|
+
} from "./checkpoint-io.js";
|
|
9
|
+
import { getInternals } from "./internals.js";
|
|
10
|
+
import { swallowPromiseRejection } from "./strategy-helpers.js";
|
|
11
|
+
import { terminateWorkflow } from "./termination.js";
|
|
12
|
+
export function persistCheckpointForDataOperation(engine, workflowId, operation) {
|
|
13
|
+
return persistCheckpoint(getInternals(engine), workflowId, operation, void 0, {
|
|
14
|
+
appendTimelineBatchOperations: (id, checkpointOperation, step, timestamp, operations) => appendTimelineBatchOperations(getInternals(engine), id, checkpointOperation, step, timestamp, operations),
|
|
15
|
+
swallowPromiseRejection: (promise) => {
|
|
16
|
+
swallowPromiseRejection(promise);
|
|
17
|
+
},
|
|
18
|
+
validateAttributeValueSizes,
|
|
19
|
+
pruneCheckpointHistory: (id, step) => pruneCheckpointHistory(getInternals(engine), id, step),
|
|
20
|
+
dispatchEvent: (event) => {
|
|
21
|
+
engine.dispatchEvent(event);
|
|
22
|
+
},
|
|
23
|
+
enforceHistoryCircuitBreaker: (id) => terminateWorkflow(getInternals(engine), id, "timed-out", createTerminationCallbacks(engine), HISTORY_CIRCUIT_BREAKER_REASON)
|
|
24
|
+
}, { timeline: "preserve-pending" });
|
|
25
|
+
}
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
broadcast as broadcastFromInternals,
|
|
4
4
|
dispatchPendingUpdateReceived as dispatchPendingUpdateReceivedFromBroadcast
|
|
5
5
|
} from "./broadcast.js";
|
|
6
|
+
import { persistCheckpointForDataOperation } from "./callback-checkpoint-persistence.js";
|
|
6
7
|
import {
|
|
7
8
|
createBroadcastCallbacks,
|
|
8
9
|
createGuardCallbacks,
|
|
@@ -97,7 +98,9 @@ export function createConditionOperationCallbacks(engine) {
|
|
|
97
98
|
}
|
|
98
99
|
export function createDataOperationCallbacks(engine) {
|
|
99
100
|
return {
|
|
100
|
-
runOperationWithResult: (workflowId, operation, execute) => runOperationWithResultForEngine(engine, workflowId, operation, execute)
|
|
101
|
+
runOperationWithResult: (workflowId, operation, execute) => runOperationWithResultForEngine(engine, workflowId, operation, execute),
|
|
102
|
+
persistCheckpoint: (workflowId, operation) => persistCheckpointForDataOperation(engine, workflowId, operation),
|
|
103
|
+
getActivityOperationCallbacks: () => createActivityOperationCallbacks(engine)
|
|
101
104
|
};
|
|
102
105
|
}
|
|
103
106
|
export function createStateOperationCallbacks(engine) {
|
|
@@ -6,6 +6,9 @@ type PendingTimelineEntryValue = {
|
|
|
6
6
|
startedAt: number;
|
|
7
7
|
entry: WorkflowTimelineEntry;
|
|
8
8
|
};
|
|
9
|
+
export type PersistCheckpointOptions = {
|
|
10
|
+
timeline?: 'record-operation' | 'preserve-pending';
|
|
11
|
+
};
|
|
9
12
|
type PersistCheckpointCallbacks = {
|
|
10
13
|
appendTimelineBatchOperations: (workflowId: string, operation: ContextOperationRequest, step: number, timestamp: number, operations: BatchOperation[]) => PendingTimelineEntryValue;
|
|
11
14
|
swallowPromiseRejection: (promise: Promise<void>) => void;
|
|
@@ -26,7 +29,7 @@ type DevelopmentCheckpointCallbacks = {
|
|
|
26
29
|
};
|
|
27
30
|
export declare function appendTimelineBatchOperations(internals: EngineInternals, workflowId: string, operation: ContextOperationRequest, step: number, timestamp: number, operations: BatchOperation[]): PendingTimelineEntryValue;
|
|
28
31
|
/** Persist a workflow checkpoint, history entry, timeline record, and event log record. */
|
|
29
|
-
export declare function persistCheckpoint(internals: EngineInternals, workflowId: string, operation: ContextOperationRequest, workerCheckpointBytes: ArrayBuffer | undefined, callbacks: PersistCheckpointCallbacks): Promise<void>;
|
|
32
|
+
export declare function persistCheckpoint(internals: EngineInternals, workflowId: string, operation: ContextOperationRequest, workerCheckpointBytes: ArrayBuffer | undefined, callbacks: PersistCheckpointCallbacks, options?: PersistCheckpointOptions): Promise<void>;
|
|
30
33
|
/** Delete the single checkpoint history entry that overflows the retention limit. */
|
|
31
34
|
export declare function pruneCheckpointHistory(internals: EngineInternals, workflowId: string, currentStep: number): Promise<void>;
|
|
32
35
|
/** Validate checkpoint serialization in development mode and dispatch warnings. */
|
|
@@ -60,13 +60,13 @@ export function appendTimelineBatchOperations(internals, workflowId, operation,
|
|
|
60
60
|
entry
|
|
61
61
|
};
|
|
62
62
|
}
|
|
63
|
-
export async function persistCheckpoint(internals, workflowId, operation, workerCheckpointBytes, callbacks) {
|
|
63
|
+
export async function persistCheckpoint(internals, workflowId, operation, workerCheckpointBytes, callbacks, options = {}) {
|
|
64
64
|
if (internals.inlineStrategy?.getContext(workflowId))
|
|
65
|
-
await persistInlineCheckpoint(internals, workflowId, operation, callbacks);
|
|
65
|
+
await persistInlineCheckpoint(internals, workflowId, operation, callbacks, options);
|
|
66
66
|
else if (workerCheckpointBytes && workerCheckpointBytes.byteLength > 0)
|
|
67
|
-
await persistWorkerCheckpoint(internals, workflowId, operation, workerCheckpointBytes, callbacks);
|
|
67
|
+
await persistWorkerCheckpoint(internals, workflowId, operation, workerCheckpointBytes, callbacks, options);
|
|
68
68
|
}
|
|
69
|
-
async function persistInlineCheckpoint(internals, workflowId, operation, callbacks) {
|
|
69
|
+
async function persistInlineCheckpoint(internals, workflowId, operation, callbacks, options) {
|
|
70
70
|
const current = internals.checkpoints.get(workflowId), context = internals.inlineStrategy?.getContext(workflowId);
|
|
71
71
|
if (!current || !context)
|
|
72
72
|
return;
|
|
@@ -76,13 +76,13 @@ async function persistInlineCheckpoint(internals, workflowId, operation, callbac
|
|
|
76
76
|
...pendingAttributeChanges !== void 0 ? { searchAttributes: pendingAttributeChanges } : {}
|
|
77
77
|
}), pruned = pruneCheckpointReplayState(advanced, resolvePendingOperationStep(operation, context.stepIndex)), commit = createCheckpointCommit(internals, workflowId, pruned.checkpoint, serializeCheckpoint(attachTransientCheckpointReplayPayload(pruned.checkpoint, pruned.replayPayload)), pruned.replayPayload);
|
|
78
78
|
appendAttributeOperations(workflowId, commit, previousAttributes, pendingAttributeChanges, hasPendingAttributeChangesValue, callbacks);
|
|
79
|
-
await commitCheckpoint(internals, workflowId, operation, commit, callbacks);
|
|
79
|
+
await commitCheckpoint(internals, workflowId, operation, commit, callbacks, options);
|
|
80
80
|
if (hasPendingAttributeChangesValue)
|
|
81
81
|
callbacks.dispatchEvent(new AttributesChangedEvent(workflowId, { ...pendingAttributeChanges }));
|
|
82
82
|
}
|
|
83
|
-
async function persistWorkerCheckpoint(internals, workflowId, operation, workerCheckpointBytes, callbacks) {
|
|
83
|
+
async function persistWorkerCheckpoint(internals, workflowId, operation, workerCheckpointBytes, callbacks, options) {
|
|
84
84
|
const serialized = new Uint8Array(workerCheckpointBytes), checkpoint = deserializeCheckpoint(serialized), workerReplayPayload = readCheckpointReplayPayload(checkpoint), pruned = pruneCheckpointReplayState(checkpoint, resolvePendingOperationStep(operation, checkpoint.step)), replayPayload = mergeCheckpointReplayPayloads(workerReplayPayload, pruned.replayPayload), prunedSerialized = serializeCheckpoint(attachTransientCheckpointReplayPayload(pruned.checkpoint, replayPayload));
|
|
85
|
-
await commitCheckpoint(internals, workflowId, operation, createCheckpointCommit(internals, workflowId, pruned.checkpoint, prunedSerialized, replayPayload), callbacks);
|
|
85
|
+
await commitCheckpoint(internals, workflowId, operation, createCheckpointCommit(internals, workflowId, pruned.checkpoint, prunedSerialized, replayPayload), callbacks, options);
|
|
86
86
|
}
|
|
87
87
|
function createCheckpointCommit(internals, workflowId, checkpoint, serialized, replayPayload) {
|
|
88
88
|
const operations = [
|
|
@@ -116,9 +116,9 @@ function appendAttributeOperations(workflowId, commit, previousAttributes, pendi
|
|
|
116
116
|
});
|
|
117
117
|
commit.operations.push(...buildIndexOperations(workflowId, previousAttributes, commit.checkpoint.searchAttributes));
|
|
118
118
|
}
|
|
119
|
-
async function commitCheckpoint(internals, workflowId, operation, commit, callbacks) {
|
|
119
|
+
async function commitCheckpoint(internals, workflowId, operation, commit, callbacks, options) {
|
|
120
120
|
dispatchCheckpointSizeWarning(internals, workflowId, commit, callbacks);
|
|
121
|
-
const nextPendingTimelineEntry = callbacks.appendTimelineBatchOperations(workflowId, operation, commit.step, commit.timestamp, commit.operations), { newHead, timestamp } = appendCheckpointEventLog(internals, workflowId, commit), retentionWindow = internals.options.historyPolicy.retentionWindow, compaction = retentionWindow === null ? null : await appendCompactionOperations(internals.storage, workflowId, newHead.sequence, retentionWindow, commit.operations), pendingSideEffects = takePendingAtomicWorkflowCommitSideEffects(internals, workflowId);
|
|
121
|
+
const nextPendingTimelineEntry = options.timeline === "preserve-pending" ? preservePendingTimelineEntry(internals, workflowId, commit.operations) : callbacks.appendTimelineBatchOperations(workflowId, operation, commit.step, commit.timestamp, commit.operations), { newHead, timestamp } = appendCheckpointEventLog(internals, workflowId, commit), retentionWindow = internals.options.historyPolicy.retentionWindow, compaction = retentionWindow === null ? null : await appendCompactionOperations(internals.storage, workflowId, newHead.sequence, retentionWindow, commit.operations), pendingSideEffects = takePendingAtomicWorkflowCommitSideEffects(internals, workflowId);
|
|
122
122
|
if (pendingSideEffects !== void 0)
|
|
123
123
|
commit.operations.push(...pendingSideEffects.operations);
|
|
124
124
|
const storageSupportsConditionalBatch = internals.storage.capabilities().conditionalBatch, sideEffectConditions = checkpointSideEffectConditions(pendingSideEffects, storageSupportsConditionalBatch), conditions = buildCheckpointCommitConditions(workflowId, commit, storageSupportsConditionalBatch, sideEffectConditions);
|
|
@@ -129,7 +129,10 @@ async function commitCheckpoint(internals, workflowId, operation, commit, callba
|
|
|
129
129
|
clearPendingAtomicWorkflowCommitSideEffects(internals, workflowId);
|
|
130
130
|
if (commit.expectedSerialized !== void 0)
|
|
131
131
|
rememberCommittedCheckpointBytes(internals, workflowId, commit.serialized);
|
|
132
|
-
|
|
132
|
+
if (nextPendingTimelineEntry === void 0)
|
|
133
|
+
internals.pendingTimelineEntries.delete(workflowId);
|
|
134
|
+
else
|
|
135
|
+
internals.pendingTimelineEntries.set(workflowId, nextPendingTimelineEntry);
|
|
133
136
|
internals.checkpoints.set(workflowId, commit.checkpoint);
|
|
134
137
|
internals.eventLogHeads.set(workflowId, newHead);
|
|
135
138
|
if (compaction !== null)
|
|
@@ -146,6 +149,12 @@ async function commitCheckpoint(internals, workflowId, operation, commit, callba
|
|
|
146
149
|
if (historyEventLimitBreached(internals, newHead.sequence))
|
|
147
150
|
await callbacks.enforceHistoryCircuitBreaker(workflowId);
|
|
148
151
|
}
|
|
152
|
+
function preservePendingTimelineEntry(internals, workflowId, operations) {
|
|
153
|
+
const pendingTimelineOperation = buildPendingTimelineOperation(internals, workflowId);
|
|
154
|
+
if (pendingTimelineOperation)
|
|
155
|
+
operations.push(pendingTimelineOperation);
|
|
156
|
+
return internals.pendingTimelineEntries.get(workflowId);
|
|
157
|
+
}
|
|
149
158
|
function checkpointSideEffectConditions(pendingSideEffects, storageSupportsConditionalBatch) {
|
|
150
159
|
if (!storageSupportsConditionalBatch)
|
|
151
160
|
return [];
|