deepline 0.1.78 → 0.1.79
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/dist/cli/index.js +3 -8
- package/dist/cli/index.mjs +3 -8
- package/dist/index.d.mts +23 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +2 -2
- package/dist/index.mjs +2 -2
- package/dist/repo/apps/play-runner-workers/src/coordinator-entry.ts +273 -83
- package/dist/repo/apps/play-runner-workers/src/dedup-do.ts +18 -3
- package/dist/repo/apps/play-runner-workers/src/workflow-retry-state.ts +203 -0
- package/dist/repo/sdk/src/release.ts +2 -2
- package/dist/repo/shared_libs/plays/static-pipeline.ts +261 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -229,10 +229,10 @@ var import_node_path2 = require("path");
|
|
|
229
229
|
|
|
230
230
|
// src/release.ts
|
|
231
231
|
var SDK_RELEASE = {
|
|
232
|
-
version: "0.1.
|
|
232
|
+
version: "0.1.79",
|
|
233
233
|
apiContract: "2026-06-dataset-column-cell-stale-hard-cutover",
|
|
234
234
|
supportPolicy: {
|
|
235
|
-
latest: "0.1.
|
|
235
|
+
latest: "0.1.79",
|
|
236
236
|
minimumSupported: "0.1.53",
|
|
237
237
|
deprecatedBelow: "0.1.53"
|
|
238
238
|
}
|
|
@@ -7734,7 +7734,6 @@ ${hint}`;
|
|
|
7734
7734
|
}
|
|
7735
7735
|
|
|
7736
7736
|
// src/cli/commands/play.ts
|
|
7737
|
-
var PLAY_START_STREAM_FAST_COMPLETION_WAIT_MS = 2500;
|
|
7738
7737
|
var PLAY_RUN_RESERVED_BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
|
|
7739
7738
|
"--json",
|
|
7740
7739
|
"--wait",
|
|
@@ -8639,10 +8638,6 @@ async function startAndWaitForPlayCompletionByStreamOnce(input2) {
|
|
|
8639
8638
|
let eventCount = 0;
|
|
8640
8639
|
let firstRunIdMs = null;
|
|
8641
8640
|
let lastPhase = null;
|
|
8642
|
-
const startRequest = {
|
|
8643
|
-
...input2.request,
|
|
8644
|
-
waitForCompletionMs: typeof input2.request.waitForCompletionMs === "number" ? input2.request.waitForCompletionMs : PLAY_START_STREAM_FAST_COMPLETION_WAIT_MS
|
|
8645
|
-
};
|
|
8646
8641
|
const timeout = input2.waitTimeoutMs === null ? null : setTimeout(
|
|
8647
8642
|
() => {
|
|
8648
8643
|
timedOut = true;
|
|
@@ -8651,7 +8646,7 @@ async function startAndWaitForPlayCompletionByStreamOnce(input2) {
|
|
|
8651
8646
|
Math.max(1, input2.waitTimeoutMs)
|
|
8652
8647
|
);
|
|
8653
8648
|
try {
|
|
8654
|
-
for await (const event of input2.client.startPlayRunStream(
|
|
8649
|
+
for await (const event of input2.client.startPlayRunStream(input2.request, {
|
|
8655
8650
|
signal: controller.signal
|
|
8656
8651
|
})) {
|
|
8657
8652
|
eventCount += 1;
|
package/dist/cli/index.mjs
CHANGED
|
@@ -206,10 +206,10 @@ import { join as join2 } from "path";
|
|
|
206
206
|
|
|
207
207
|
// src/release.ts
|
|
208
208
|
var SDK_RELEASE = {
|
|
209
|
-
version: "0.1.
|
|
209
|
+
version: "0.1.79",
|
|
210
210
|
apiContract: "2026-06-dataset-column-cell-stale-hard-cutover",
|
|
211
211
|
supportPolicy: {
|
|
212
|
-
latest: "0.1.
|
|
212
|
+
latest: "0.1.79",
|
|
213
213
|
minimumSupported: "0.1.53",
|
|
214
214
|
deprecatedBelow: "0.1.53"
|
|
215
215
|
}
|
|
@@ -7737,7 +7737,6 @@ ${hint}`;
|
|
|
7737
7737
|
}
|
|
7738
7738
|
|
|
7739
7739
|
// src/cli/commands/play.ts
|
|
7740
|
-
var PLAY_START_STREAM_FAST_COMPLETION_WAIT_MS = 2500;
|
|
7741
7740
|
var PLAY_RUN_RESERVED_BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
|
|
7742
7741
|
"--json",
|
|
7743
7742
|
"--wait",
|
|
@@ -8642,10 +8641,6 @@ async function startAndWaitForPlayCompletionByStreamOnce(input2) {
|
|
|
8642
8641
|
let eventCount = 0;
|
|
8643
8642
|
let firstRunIdMs = null;
|
|
8644
8643
|
let lastPhase = null;
|
|
8645
|
-
const startRequest = {
|
|
8646
|
-
...input2.request,
|
|
8647
|
-
waitForCompletionMs: typeof input2.request.waitForCompletionMs === "number" ? input2.request.waitForCompletionMs : PLAY_START_STREAM_FAST_COMPLETION_WAIT_MS
|
|
8648
|
-
};
|
|
8649
8644
|
const timeout = input2.waitTimeoutMs === null ? null : setTimeout(
|
|
8650
8645
|
() => {
|
|
8651
8646
|
timedOut = true;
|
|
@@ -8654,7 +8649,7 @@ async function startAndWaitForPlayCompletionByStreamOnce(input2) {
|
|
|
8654
8649
|
Math.max(1, input2.waitTimeoutMs)
|
|
8655
8650
|
);
|
|
8656
8651
|
try {
|
|
8657
|
-
for await (const event of input2.client.startPlayRunStream(
|
|
8652
|
+
for await (const event of input2.client.startPlayRunStream(input2.request, {
|
|
8658
8653
|
signal: controller.signal
|
|
8659
8654
|
})) {
|
|
8660
8655
|
eventCount += 1;
|
package/dist/index.d.mts
CHANGED
|
@@ -16,6 +16,7 @@ type PlayArtifactKind = (typeof PLAY_ARTIFACT_KINDS)[keyof typeof PLAY_ARTIFACT_
|
|
|
16
16
|
interface PlayStaticPipelineSnapshot {
|
|
17
17
|
tableNamespace?: string;
|
|
18
18
|
inputFields?: string[];
|
|
19
|
+
rowKeyFields?: string[];
|
|
19
20
|
csvArg?: string;
|
|
20
21
|
hasInlineData?: boolean;
|
|
21
22
|
csvDescription?: string;
|
|
@@ -39,11 +40,30 @@ interface PlaySheetColumnContractSnapshot {
|
|
|
39
40
|
outputSqlName?: string;
|
|
40
41
|
stepId?: string;
|
|
41
42
|
toolId?: string;
|
|
43
|
+
isRowKey?: boolean;
|
|
42
44
|
}
|
|
43
45
|
interface PlaySheetContractSnapshot {
|
|
44
46
|
tableNamespace: string;
|
|
45
47
|
columns: PlaySheetColumnContractSnapshot[];
|
|
46
48
|
}
|
|
49
|
+
type PlayStaticColumnProducerKindSnapshot = 'tool' | 'waterfall' | 'stepProgram' | 'playCall' | 'transform';
|
|
50
|
+
interface PlayStaticColumnProducerSnapshot {
|
|
51
|
+
id: string;
|
|
52
|
+
kind: PlayStaticColumnProducerKindSnapshot;
|
|
53
|
+
field: string;
|
|
54
|
+
toolId?: string;
|
|
55
|
+
playId?: string;
|
|
56
|
+
conditional?: boolean;
|
|
57
|
+
sourceRange?: PlayStaticSourceRangeSnapshot;
|
|
58
|
+
steps?: PlayStaticColumnProducerSnapshot[];
|
|
59
|
+
substep: PlayStaticSubstepSnapshot;
|
|
60
|
+
}
|
|
61
|
+
interface PlayStaticDatasetColumnSnapshot {
|
|
62
|
+
id: string;
|
|
63
|
+
source: PlaySheetColumnSourceSnapshot;
|
|
64
|
+
sqlName?: string;
|
|
65
|
+
producers: PlayStaticColumnProducerSnapshot[];
|
|
66
|
+
}
|
|
47
67
|
interface PlayStaticSourceRangeSnapshot {
|
|
48
68
|
sourcePath?: string;
|
|
49
69
|
startLine: number;
|
|
@@ -68,8 +88,11 @@ type PlayStaticSubstepSnapshot = PlayStaticSubstepMetadataSnapshot & ({
|
|
|
68
88
|
name?: string;
|
|
69
89
|
tableNamespace?: string;
|
|
70
90
|
inputFields?: string[];
|
|
91
|
+
rowKeyFields?: string[];
|
|
71
92
|
outputFields?: string[];
|
|
93
|
+
columns?: PlayStaticDatasetColumnSnapshot[];
|
|
72
94
|
waterfallIds?: string[];
|
|
95
|
+
steps?: PlayStaticSubstepSnapshot[];
|
|
73
96
|
sheetContract?: PlaySheetContractSnapshot | null;
|
|
74
97
|
description?: string;
|
|
75
98
|
sourceRange?: PlayStaticSourceRangeSnapshot;
|
package/dist/index.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ type PlayArtifactKind = (typeof PLAY_ARTIFACT_KINDS)[keyof typeof PLAY_ARTIFACT_
|
|
|
16
16
|
interface PlayStaticPipelineSnapshot {
|
|
17
17
|
tableNamespace?: string;
|
|
18
18
|
inputFields?: string[];
|
|
19
|
+
rowKeyFields?: string[];
|
|
19
20
|
csvArg?: string;
|
|
20
21
|
hasInlineData?: boolean;
|
|
21
22
|
csvDescription?: string;
|
|
@@ -39,11 +40,30 @@ interface PlaySheetColumnContractSnapshot {
|
|
|
39
40
|
outputSqlName?: string;
|
|
40
41
|
stepId?: string;
|
|
41
42
|
toolId?: string;
|
|
43
|
+
isRowKey?: boolean;
|
|
42
44
|
}
|
|
43
45
|
interface PlaySheetContractSnapshot {
|
|
44
46
|
tableNamespace: string;
|
|
45
47
|
columns: PlaySheetColumnContractSnapshot[];
|
|
46
48
|
}
|
|
49
|
+
type PlayStaticColumnProducerKindSnapshot = 'tool' | 'waterfall' | 'stepProgram' | 'playCall' | 'transform';
|
|
50
|
+
interface PlayStaticColumnProducerSnapshot {
|
|
51
|
+
id: string;
|
|
52
|
+
kind: PlayStaticColumnProducerKindSnapshot;
|
|
53
|
+
field: string;
|
|
54
|
+
toolId?: string;
|
|
55
|
+
playId?: string;
|
|
56
|
+
conditional?: boolean;
|
|
57
|
+
sourceRange?: PlayStaticSourceRangeSnapshot;
|
|
58
|
+
steps?: PlayStaticColumnProducerSnapshot[];
|
|
59
|
+
substep: PlayStaticSubstepSnapshot;
|
|
60
|
+
}
|
|
61
|
+
interface PlayStaticDatasetColumnSnapshot {
|
|
62
|
+
id: string;
|
|
63
|
+
source: PlaySheetColumnSourceSnapshot;
|
|
64
|
+
sqlName?: string;
|
|
65
|
+
producers: PlayStaticColumnProducerSnapshot[];
|
|
66
|
+
}
|
|
47
67
|
interface PlayStaticSourceRangeSnapshot {
|
|
48
68
|
sourcePath?: string;
|
|
49
69
|
startLine: number;
|
|
@@ -68,8 +88,11 @@ type PlayStaticSubstepSnapshot = PlayStaticSubstepMetadataSnapshot & ({
|
|
|
68
88
|
name?: string;
|
|
69
89
|
tableNamespace?: string;
|
|
70
90
|
inputFields?: string[];
|
|
91
|
+
rowKeyFields?: string[];
|
|
71
92
|
outputFields?: string[];
|
|
93
|
+
columns?: PlayStaticDatasetColumnSnapshot[];
|
|
72
94
|
waterfallIds?: string[];
|
|
95
|
+
steps?: PlayStaticSubstepSnapshot[];
|
|
73
96
|
sheetContract?: PlaySheetContractSnapshot | null;
|
|
74
97
|
description?: string;
|
|
75
98
|
sourceRange?: PlayStaticSourceRangeSnapshot;
|
package/dist/index.js
CHANGED
|
@@ -241,10 +241,10 @@ var import_node_path2 = require("path");
|
|
|
241
241
|
|
|
242
242
|
// src/release.ts
|
|
243
243
|
var SDK_RELEASE = {
|
|
244
|
-
version: "0.1.
|
|
244
|
+
version: "0.1.79",
|
|
245
245
|
apiContract: "2026-06-dataset-column-cell-stale-hard-cutover",
|
|
246
246
|
supportPolicy: {
|
|
247
|
-
latest: "0.1.
|
|
247
|
+
latest: "0.1.79",
|
|
248
248
|
minimumSupported: "0.1.53",
|
|
249
249
|
deprecatedBelow: "0.1.53"
|
|
250
250
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -179,10 +179,10 @@ import { join as join2 } from "path";
|
|
|
179
179
|
|
|
180
180
|
// src/release.ts
|
|
181
181
|
var SDK_RELEASE = {
|
|
182
|
-
version: "0.1.
|
|
182
|
+
version: "0.1.79",
|
|
183
183
|
apiContract: "2026-06-dataset-column-cell-stale-hard-cutover",
|
|
184
184
|
supportPolicy: {
|
|
185
|
-
latest: "0.1.
|
|
185
|
+
latest: "0.1.79",
|
|
186
186
|
minimumSupported: "0.1.53",
|
|
187
187
|
deprecatedBelow: "0.1.53"
|
|
188
188
|
}
|
|
@@ -53,6 +53,14 @@ import {
|
|
|
53
53
|
decideWorkflowPlatformRetry,
|
|
54
54
|
PLATFORM_DEPLOY_WORKFLOW_RETRY_LIMIT,
|
|
55
55
|
} from './workflow-retry';
|
|
56
|
+
import {
|
|
57
|
+
WORKFLOW_RETRY_PARAMS_EXTERNALIZE_AFTER_BYTES,
|
|
58
|
+
WORKFLOW_RETRY_PARAMS_MAX_BYTES,
|
|
59
|
+
buildWorkflowRetryParams,
|
|
60
|
+
jsonByteLength,
|
|
61
|
+
workflowRetryParamsStorageKey,
|
|
62
|
+
type WorkflowRetryParamsRef,
|
|
63
|
+
} from './workflow-retry-state';
|
|
56
64
|
import { sanitizeLiveLogLines } from './runtime/live-progress';
|
|
57
65
|
|
|
58
66
|
export { DynamicWorkflowBinding };
|
|
@@ -563,7 +571,7 @@ const WORKFLOW_POOL_PROTOCOL_VERSION =
|
|
|
563
571
|
const WORKFLOW_POOL_DO_NAME = 'workflow-pool:v2';
|
|
564
572
|
const WORKFLOW_POOL_START_EVENT_TYPE = 'play_start';
|
|
565
573
|
const WORKFLOW_POOL_TTL_MS = 8 * 60 * 1000;
|
|
566
|
-
const WORKFLOW_POOL_TARGET_SIZE =
|
|
574
|
+
const WORKFLOW_POOL_TARGET_SIZE = 4;
|
|
567
575
|
const WORKFLOW_POOL_READY_TIMEOUT_MS = 1_500;
|
|
568
576
|
const WORKFLOW_POOL_READY_POLL_MS = 250;
|
|
569
577
|
const WORKFLOW_POOL_REFILL_ON_MISS_TIMEOUT_MS = 2_500;
|
|
@@ -571,6 +579,8 @@ const WORKFLOW_POOL_REFILL_ON_MISS_MIN_AVAILABLE = 4;
|
|
|
571
579
|
const WORKFLOW_POOL_CONTROL_TIMEOUT_MS = 750;
|
|
572
580
|
const WORKFLOW_POOL_START_ACK_TIMEOUT_MS = 750;
|
|
573
581
|
const WORKFLOW_POOL_START_ACK_POLL_MS = 25;
|
|
582
|
+
const WORKFLOW_POOL_MISS_CLAIM_RETRY_TIMEOUT_MS = 3_000;
|
|
583
|
+
const WORKFLOW_POOL_MISS_CLAIM_RETRY_POLL_MS = 50;
|
|
574
584
|
const SUBMIT_INITIAL_STATE_MAX_WAIT_MS = 0;
|
|
575
585
|
const SUBMIT_INITIAL_STATE_POLL_MS = 50;
|
|
576
586
|
const WORKFLOW_RETRY_STATE_TTL_MS = 60 * 60 * 1000;
|
|
@@ -1117,6 +1127,115 @@ async function leaseWorkflowPoolId(
|
|
|
1117
1127
|
return typeof body.id === 'string' && body.id ? body.id : null;
|
|
1118
1128
|
}
|
|
1119
1129
|
|
|
1130
|
+
async function leaseWorkflowPoolIdWithMissRecovery(input: {
|
|
1131
|
+
env: CoordinatorEnv;
|
|
1132
|
+
runId: string;
|
|
1133
|
+
recordSubmitTiming: (timing: CoordinatorTiming) => void;
|
|
1134
|
+
graphHash?: string | null;
|
|
1135
|
+
}): Promise<{
|
|
1136
|
+
pooledInstanceId: string | null;
|
|
1137
|
+
missCounts: WorkflowPoolCounts | null;
|
|
1138
|
+
leaseError: string | null;
|
|
1139
|
+
}> {
|
|
1140
|
+
let leaseError: string | null = null;
|
|
1141
|
+
let pooledInstanceId = await leaseWorkflowPoolId(
|
|
1142
|
+
input.env,
|
|
1143
|
+
input.runId,
|
|
1144
|
+
).catch((error) => {
|
|
1145
|
+
leaseError = error instanceof Error ? error.message : String(error);
|
|
1146
|
+
return null;
|
|
1147
|
+
});
|
|
1148
|
+
let missCounts = pooledInstanceId
|
|
1149
|
+
? null
|
|
1150
|
+
: await workflowPoolCount(input.env).catch(() => null);
|
|
1151
|
+
if (
|
|
1152
|
+
pooledInstanceId ||
|
|
1153
|
+
leaseError ||
|
|
1154
|
+
!missCounts ||
|
|
1155
|
+
missCounts.available + missCounts.warming <= 0
|
|
1156
|
+
) {
|
|
1157
|
+
return { pooledInstanceId, missCounts, leaseError };
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
const recoveryStartedAt = Date.now();
|
|
1161
|
+
const refill = await refillWorkflowPool(input.env, {
|
|
1162
|
+
minAvailable: 1,
|
|
1163
|
+
waitReady: true,
|
|
1164
|
+
waitTimeoutMs: WORKFLOW_POOL_REFILL_ON_MISS_TIMEOUT_MS,
|
|
1165
|
+
}).catch((error) => {
|
|
1166
|
+
input.recordSubmitTiming({
|
|
1167
|
+
phase: 'coordinator.workflow_pool_refill_on_miss',
|
|
1168
|
+
ms: Date.now() - recoveryStartedAt,
|
|
1169
|
+
graphHash: input.graphHash ?? null,
|
|
1170
|
+
extra: {
|
|
1171
|
+
status: 'failed',
|
|
1172
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1173
|
+
available: missCounts?.available ?? null,
|
|
1174
|
+
warming: missCounts?.warming ?? null,
|
|
1175
|
+
},
|
|
1176
|
+
});
|
|
1177
|
+
return null;
|
|
1178
|
+
});
|
|
1179
|
+
if (refill) {
|
|
1180
|
+
input.recordSubmitTiming({
|
|
1181
|
+
phase: 'coordinator.workflow_pool_refill_on_miss',
|
|
1182
|
+
ms: Date.now() - recoveryStartedAt,
|
|
1183
|
+
graphHash: input.graphHash ?? null,
|
|
1184
|
+
extra: {
|
|
1185
|
+
status: 'ok',
|
|
1186
|
+
available: refill.available,
|
|
1187
|
+
warming: refill.warming,
|
|
1188
|
+
created: refill.created,
|
|
1189
|
+
promoted: refill.promoted,
|
|
1190
|
+
removed: refill.removed,
|
|
1191
|
+
waitedMs: refill.waitedMs,
|
|
1192
|
+
waitIterations: refill.waitIterations,
|
|
1193
|
+
},
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
let retryCount = 0;
|
|
1198
|
+
const retryStartedAt = Date.now();
|
|
1199
|
+
while (
|
|
1200
|
+
Date.now() - retryStartedAt <
|
|
1201
|
+
WORKFLOW_POOL_MISS_CLAIM_RETRY_TIMEOUT_MS
|
|
1202
|
+
) {
|
|
1203
|
+
retryCount += 1;
|
|
1204
|
+
pooledInstanceId = await leaseWorkflowPoolId(input.env, input.runId).catch(
|
|
1205
|
+
(error) => {
|
|
1206
|
+
leaseError = error instanceof Error ? error.message : String(error);
|
|
1207
|
+
return null;
|
|
1208
|
+
},
|
|
1209
|
+
);
|
|
1210
|
+
if (pooledInstanceId || leaseError) {
|
|
1211
|
+
break;
|
|
1212
|
+
}
|
|
1213
|
+
missCounts = await workflowPoolCount(input.env).catch(() => missCounts);
|
|
1214
|
+
if (!missCounts || missCounts.available + missCounts.warming <= 0) {
|
|
1215
|
+
break;
|
|
1216
|
+
}
|
|
1217
|
+
await sleep(WORKFLOW_POOL_MISS_CLAIM_RETRY_POLL_MS);
|
|
1218
|
+
}
|
|
1219
|
+
input.recordSubmitTiming({
|
|
1220
|
+
phase: 'coordinator.workflow_pool_claim_retry',
|
|
1221
|
+
ms: Date.now() - retryStartedAt,
|
|
1222
|
+
graphHash: input.graphHash ?? null,
|
|
1223
|
+
extra: {
|
|
1224
|
+
pooled: Boolean(pooledInstanceId),
|
|
1225
|
+
retries: retryCount,
|
|
1226
|
+
...(leaseError ? { error: leaseError } : {}),
|
|
1227
|
+
...(missCounts
|
|
1228
|
+
? {
|
|
1229
|
+
availableAfterRetry: missCounts.available,
|
|
1230
|
+
warmingAfterRetry: missCounts.warming,
|
|
1231
|
+
}
|
|
1232
|
+
: {}),
|
|
1233
|
+
},
|
|
1234
|
+
});
|
|
1235
|
+
|
|
1236
|
+
return { pooledInstanceId, missCounts, leaseError };
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1120
1239
|
async function mapRunToWorkflowInstance(input: {
|
|
1121
1240
|
env: CoordinatorEnv;
|
|
1122
1241
|
runId: string;
|
|
@@ -1191,56 +1310,120 @@ async function persistWorkflowRetryState(input: {
|
|
|
1191
1310
|
runId: string;
|
|
1192
1311
|
params: PlayWorkflowParams;
|
|
1193
1312
|
}): Promise<void> {
|
|
1194
|
-
const retryParams
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
})) ?? null,
|
|
1313
|
+
const retryParams = buildWorkflowRetryParams(input.params);
|
|
1314
|
+
const paramsBytes = jsonByteLength(retryParams);
|
|
1315
|
+
if (paramsBytes > WORKFLOW_RETRY_PARAMS_MAX_BYTES) {
|
|
1316
|
+
throw new Error(
|
|
1317
|
+
`workflow retry params too large: ${paramsBytes} bytes exceeds ${WORKFLOW_RETRY_PARAMS_MAX_BYTES}. Pass large payloads as staged files or ctx.csv inputs instead of inline JSON.`,
|
|
1318
|
+
);
|
|
1319
|
+
}
|
|
1320
|
+
let body: {
|
|
1321
|
+
runId: string;
|
|
1322
|
+
params?: PlayWorkflowParams;
|
|
1323
|
+
paramsRef?: WorkflowRetryParamsRef;
|
|
1324
|
+
paramsBytes: number;
|
|
1325
|
+
ttlMs: number;
|
|
1208
1326
|
};
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1327
|
+
if (paramsBytes > WORKFLOW_RETRY_PARAMS_EXTERNALIZE_AFTER_BYTES) {
|
|
1328
|
+
const serialized = JSON.stringify(retryParams);
|
|
1329
|
+
const hash = stableHash(serialized);
|
|
1330
|
+
const storageKey = workflowRetryParamsStorageKey({
|
|
1212
1331
|
runId: input.runId,
|
|
1213
|
-
|
|
1332
|
+
hash,
|
|
1333
|
+
});
|
|
1334
|
+
await input.env.PLAYS_BUCKET.put(storageKey, serialized, {
|
|
1335
|
+
httpMetadata: { contentType: 'application/json' },
|
|
1336
|
+
});
|
|
1337
|
+
body = {
|
|
1338
|
+
runId: input.runId,
|
|
1339
|
+
paramsRef: {
|
|
1340
|
+
storageKind: 'r2',
|
|
1341
|
+
storageKey,
|
|
1342
|
+
bytes: paramsBytes,
|
|
1343
|
+
hash,
|
|
1344
|
+
expiresAt: Date.now() + WORKFLOW_RETRY_STATE_TTL_MS,
|
|
1345
|
+
},
|
|
1346
|
+
paramsBytes,
|
|
1214
1347
|
ttlMs: WORKFLOW_RETRY_STATE_TTL_MS,
|
|
1215
|
-
}
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1348
|
+
};
|
|
1349
|
+
} else {
|
|
1350
|
+
body = {
|
|
1218
1351
|
runId: input.runId,
|
|
1219
|
-
|
|
1220
|
-
|
|
1352
|
+
params: retryParams,
|
|
1353
|
+
paramsBytes,
|
|
1354
|
+
ttlMs: WORKFLOW_RETRY_STATE_TTL_MS,
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
await callWorkflowPool<{ ok?: unknown }>(input.env, '/run-retry-state-put', {
|
|
1358
|
+
method: 'POST',
|
|
1359
|
+
body: JSON.stringify(body),
|
|
1221
1360
|
});
|
|
1222
1361
|
}
|
|
1223
1362
|
|
|
1224
|
-
function
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1363
|
+
async function hydrateWorkflowRetryParams(input: {
|
|
1364
|
+
env: CoordinatorEnv;
|
|
1365
|
+
params: unknown;
|
|
1366
|
+
paramsRef: unknown;
|
|
1367
|
+
}): Promise<PlayWorkflowParams | null> {
|
|
1368
|
+
if (isRecord(input.params)) {
|
|
1369
|
+
return input.params as PlayWorkflowParams;
|
|
1370
|
+
}
|
|
1371
|
+
if (!isRecord(input.paramsRef)) {
|
|
1372
|
+
return null;
|
|
1373
|
+
}
|
|
1374
|
+
const storageKind = input.paramsRef.storageKind;
|
|
1375
|
+
const storageKey = input.paramsRef.storageKey;
|
|
1376
|
+
const expectedBytes = input.paramsRef.bytes;
|
|
1377
|
+
const expectedHash = input.paramsRef.hash;
|
|
1378
|
+
if (
|
|
1379
|
+
storageKind !== 'r2' ||
|
|
1380
|
+
typeof storageKey !== 'string' ||
|
|
1381
|
+
!storageKey.startsWith('plays/workflow-retry-params/') ||
|
|
1382
|
+
typeof expectedBytes !== 'number' ||
|
|
1383
|
+
!Number.isFinite(expectedBytes) ||
|
|
1384
|
+
typeof expectedHash !== 'string' ||
|
|
1385
|
+
!expectedHash
|
|
1386
|
+
) {
|
|
1387
|
+
throw new Error('Invalid workflow retry params reference.');
|
|
1388
|
+
}
|
|
1389
|
+
const object = await input.env.PLAYS_BUCKET.get(storageKey);
|
|
1390
|
+
if (!object) {
|
|
1391
|
+
throw new Error(`Workflow retry params missing from R2: ${storageKey}`);
|
|
1392
|
+
}
|
|
1393
|
+
const text = await object.text();
|
|
1394
|
+
const actualBytes = new TextEncoder().encode(text).length;
|
|
1395
|
+
if (actualBytes !== expectedBytes) {
|
|
1396
|
+
throw new Error(
|
|
1397
|
+
`Workflow retry params byte length mismatch: expected ${expectedBytes}, got ${actualBytes}.`,
|
|
1398
|
+
);
|
|
1399
|
+
}
|
|
1400
|
+
const actualHash = stableHash(text);
|
|
1401
|
+
if (actualHash !== expectedHash) {
|
|
1402
|
+
throw new Error('Workflow retry params hash mismatch.');
|
|
1403
|
+
}
|
|
1404
|
+
const parsed = JSON.parse(text) as unknown;
|
|
1405
|
+
return isRecord(parsed) ? (parsed as PlayWorkflowParams) : null;
|
|
1230
1406
|
}
|
|
1231
1407
|
|
|
1232
|
-
function
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
const
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1408
|
+
function workflowRetryStatePersistenceErrorResponse(input: {
|
|
1409
|
+
runId: string;
|
|
1410
|
+
error: unknown;
|
|
1411
|
+
}): Response {
|
|
1412
|
+
const message =
|
|
1413
|
+
input.error instanceof Error ? input.error.message : String(input.error);
|
|
1414
|
+
return Response.json(
|
|
1415
|
+
{
|
|
1416
|
+
error: {
|
|
1417
|
+
code: 'WORKFLOW_RETRY_STATE_PERSISTENCE_FAILED',
|
|
1418
|
+
message:
|
|
1419
|
+
'Failed to persist workflow retry state before dispatching the play run.',
|
|
1420
|
+
phase: 'coordinator_retry_state_persistence',
|
|
1421
|
+
runId: input.runId,
|
|
1422
|
+
cause: message,
|
|
1423
|
+
},
|
|
1424
|
+
},
|
|
1425
|
+
{ status: 503 },
|
|
1426
|
+
);
|
|
1244
1427
|
}
|
|
1245
1428
|
|
|
1246
1429
|
async function claimWorkflowPlatformRetry(input: {
|
|
@@ -1255,6 +1438,7 @@ async function claimWorkflowPlatformRetry(input: {
|
|
|
1255
1438
|
claimed?: unknown;
|
|
1256
1439
|
attempts?: unknown;
|
|
1257
1440
|
params?: unknown;
|
|
1441
|
+
paramsRef?: unknown;
|
|
1258
1442
|
}>(input.env, '/run-retry-claim', {
|
|
1259
1443
|
method: 'POST',
|
|
1260
1444
|
body: JSON.stringify({
|
|
@@ -1262,10 +1446,15 @@ async function claimWorkflowPlatformRetry(input: {
|
|
|
1262
1446
|
maxAttempts: PLATFORM_DEPLOY_WORKFLOW_RETRY_LIMIT,
|
|
1263
1447
|
}),
|
|
1264
1448
|
});
|
|
1449
|
+
const params = await hydrateWorkflowRetryParams({
|
|
1450
|
+
env: input.env,
|
|
1451
|
+
params: body.params,
|
|
1452
|
+
paramsRef: body.paramsRef,
|
|
1453
|
+
});
|
|
1265
1454
|
return {
|
|
1266
1455
|
claimed: body.claimed === true,
|
|
1267
1456
|
attempts: typeof body.attempts === 'number' ? body.attempts : 0,
|
|
1268
|
-
params
|
|
1457
|
+
params,
|
|
1269
1458
|
};
|
|
1270
1459
|
}
|
|
1271
1460
|
|
|
@@ -1719,17 +1908,13 @@ async function submitViaPooledWorkflow(input: {
|
|
|
1719
1908
|
return null;
|
|
1720
1909
|
}
|
|
1721
1910
|
const leaseStartedAt = Date.now();
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
});
|
|
1730
|
-
const missCounts = pooledInstanceId
|
|
1731
|
-
? null
|
|
1732
|
-
: await workflowPoolCount(input.env).catch(() => null);
|
|
1911
|
+
const { pooledInstanceId, missCounts, leaseError } =
|
|
1912
|
+
await leaseWorkflowPoolIdWithMissRecovery({
|
|
1913
|
+
env: input.env,
|
|
1914
|
+
runId: input.params.runId,
|
|
1915
|
+
recordSubmitTiming: input.recordSubmitTiming,
|
|
1916
|
+
graphHash: input.params.graphHash ?? null,
|
|
1917
|
+
});
|
|
1733
1918
|
input.recordSubmitTiming({
|
|
1734
1919
|
phase: 'coordinator.workflow_pool_lease',
|
|
1735
1920
|
ms: Date.now() - leaseStartedAt,
|
|
@@ -1746,30 +1931,6 @@ async function submitViaPooledWorkflow(input: {
|
|
|
1746
1931
|
},
|
|
1747
1932
|
});
|
|
1748
1933
|
|
|
1749
|
-
if (!pooledInstanceId) {
|
|
1750
|
-
// A pool miss must not block the user path. Refilling is handled by the
|
|
1751
|
-
// caller's waitUntil after submit, so fall through to cold create now.
|
|
1752
|
-
const counts =
|
|
1753
|
-
missCounts ?? (await workflowPoolCount(input.env).catch(() => null));
|
|
1754
|
-
input.recordSubmitTiming({
|
|
1755
|
-
phase: 'coordinator.workflow_pool_refill_on_miss',
|
|
1756
|
-
ms: 0,
|
|
1757
|
-
graphHash: input.params.graphHash ?? null,
|
|
1758
|
-
extra: {
|
|
1759
|
-
skipped: true,
|
|
1760
|
-
reason: 'pool_miss_does_not_block_submit',
|
|
1761
|
-
...(counts
|
|
1762
|
-
? {
|
|
1763
|
-
available: counts.available,
|
|
1764
|
-
warming: counts.warming,
|
|
1765
|
-
waitedMs: 0,
|
|
1766
|
-
waitIterations: 0,
|
|
1767
|
-
}
|
|
1768
|
-
: {}),
|
|
1769
|
-
},
|
|
1770
|
-
});
|
|
1771
|
-
}
|
|
1772
|
-
|
|
1773
1934
|
if (!pooledInstanceId) {
|
|
1774
1935
|
return null;
|
|
1775
1936
|
}
|
|
@@ -3606,11 +3767,40 @@ async function handleWorkflowRoute(input: {
|
|
|
3606
3767
|
params,
|
|
3607
3768
|
recordSubmitTiming,
|
|
3608
3769
|
});
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3770
|
+
try {
|
|
3771
|
+
const retryStateStartedAt = Date.now();
|
|
3772
|
+
await persistWorkflowRetryState({
|
|
3773
|
+
env,
|
|
3774
|
+
runId: submittedRunId,
|
|
3775
|
+
params: workflowParams,
|
|
3776
|
+
});
|
|
3777
|
+
recordSubmitTiming({
|
|
3778
|
+
phase: 'coordinator.retry_state_persistence',
|
|
3779
|
+
ms: Date.now() - retryStateStartedAt,
|
|
3780
|
+
graphHash: params.graphHash ?? null,
|
|
3781
|
+
});
|
|
3782
|
+
} catch (error) {
|
|
3783
|
+
const errorMessage =
|
|
3784
|
+
error instanceof Error ? error.message : String(error);
|
|
3785
|
+
console.error('[coordinator] workflow retry state persistence failed', {
|
|
3786
|
+
code: 'WORKFLOW_RETRY_STATE_PERSISTENCE_FAILED',
|
|
3787
|
+
runId: submittedRunId,
|
|
3788
|
+
error: errorMessage,
|
|
3789
|
+
});
|
|
3790
|
+
recordSubmitTiming({
|
|
3791
|
+
phase: 'coordinator.retry_state_persistence',
|
|
3792
|
+
ms: 0,
|
|
3793
|
+
graphHash: params.graphHash ?? null,
|
|
3794
|
+
extra: {
|
|
3795
|
+
status: 'failed',
|
|
3796
|
+
error: errorMessage,
|
|
3797
|
+
},
|
|
3798
|
+
});
|
|
3799
|
+
return workflowRetryStatePersistenceErrorResponse({
|
|
3800
|
+
runId: submittedRunId,
|
|
3801
|
+
error,
|
|
3802
|
+
});
|
|
3803
|
+
}
|
|
3614
3804
|
let instance: WorkflowInstance | null = null;
|
|
3615
3805
|
try {
|
|
3616
3806
|
const statusEventStartedAt = Date.now();
|
|
@@ -113,6 +113,8 @@ type WorkflowRunMapping = {
|
|
|
113
113
|
type WorkflowRunRetryState = {
|
|
114
114
|
runId: string;
|
|
115
115
|
params: unknown;
|
|
116
|
+
paramsRef?: unknown;
|
|
117
|
+
paramsBytes?: number;
|
|
116
118
|
retryAttempts: number;
|
|
117
119
|
updatedAt: number;
|
|
118
120
|
expiresAt: number;
|
|
@@ -1059,8 +1061,10 @@ export class PlayDedup implements DurableObject {
|
|
|
1059
1061
|
ttlMs?: unknown;
|
|
1060
1062
|
} | null;
|
|
1061
1063
|
const runId = typeof body?.runId === 'string' ? body.runId : '';
|
|
1062
|
-
if (!runId || !body || !('params' in body)) {
|
|
1063
|
-
return new Response('runId and params are required', {
|
|
1064
|
+
if (!runId || !body || (!('params' in body) && !('paramsRef' in body))) {
|
|
1065
|
+
return new Response('runId and params or paramsRef are required', {
|
|
1066
|
+
status: 400,
|
|
1067
|
+
});
|
|
1064
1068
|
}
|
|
1065
1069
|
const now = Date.now();
|
|
1066
1070
|
const ttlMs =
|
|
@@ -1075,7 +1079,14 @@ export class PlayDedup implements DurableObject {
|
|
|
1075
1079
|
const existing = await this.state.storage.get<WorkflowRunRetryState>(key);
|
|
1076
1080
|
const retryState = {
|
|
1077
1081
|
runId,
|
|
1078
|
-
params: body.params,
|
|
1082
|
+
params: 'params' in body ? body.params : null,
|
|
1083
|
+
paramsRef: 'paramsRef' in body ? body.paramsRef : null,
|
|
1084
|
+
paramsBytes:
|
|
1085
|
+
typeof (body as { paramsBytes?: unknown }).paramsBytes ===
|
|
1086
|
+
'number' &&
|
|
1087
|
+
Number.isFinite((body as { paramsBytes?: number }).paramsBytes)
|
|
1088
|
+
? (body as { paramsBytes: number }).paramsBytes
|
|
1089
|
+
: undefined,
|
|
1079
1090
|
retryAttempts:
|
|
1080
1091
|
existing?.runId === runId &&
|
|
1081
1092
|
typeof existing.retryAttempts === 'number'
|
|
@@ -1128,6 +1139,8 @@ export class PlayDedup implements DurableObject {
|
|
|
1128
1139
|
claimed: false,
|
|
1129
1140
|
attempts: existing.retryAttempts,
|
|
1130
1141
|
params: existing.params,
|
|
1142
|
+
paramsRef: existing.paramsRef ?? null,
|
|
1143
|
+
paramsBytes: existing.paramsBytes ?? null,
|
|
1131
1144
|
};
|
|
1132
1145
|
return;
|
|
1133
1146
|
}
|
|
@@ -1142,6 +1155,8 @@ export class PlayDedup implements DurableObject {
|
|
|
1142
1155
|
claimed: true,
|
|
1143
1156
|
attempts: nextAttempts,
|
|
1144
1157
|
params: existing.params,
|
|
1158
|
+
paramsRef: existing.paramsRef ?? null,
|
|
1159
|
+
paramsBytes: existing.paramsBytes ?? null,
|
|
1145
1160
|
};
|
|
1146
1161
|
});
|
|
1147
1162
|
return new Response(JSON.stringify(response), {
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import type { ExecutionPlan } from '../../../shared_libs/play-runtime/execution-plan';
|
|
2
|
+
import type { PlayCallGovernanceSnapshot } from '../../../shared_libs/play-runtime/scheduler-backend';
|
|
3
|
+
import type { PreloadedRuntimeDbSession } from '../../../shared_libs/play-runtime/db-session';
|
|
4
|
+
import type {
|
|
5
|
+
PlayRuntimeManifest,
|
|
6
|
+
PlayRuntimeManifestMap,
|
|
7
|
+
} from '../../../shared_libs/plays/compiler-manifest';
|
|
8
|
+
|
|
9
|
+
export const WORKFLOW_RETRY_STATE_TARGET_BYTES = 100_000;
|
|
10
|
+
export const WORKFLOW_RETRY_PARAMS_EXTERNALIZE_AFTER_BYTES =
|
|
11
|
+
WORKFLOW_RETRY_STATE_TARGET_BYTES;
|
|
12
|
+
export const WORKFLOW_RETRY_PARAMS_MAX_BYTES = 1024 * 1024;
|
|
13
|
+
|
|
14
|
+
export type WorkflowRetryParamsRef = {
|
|
15
|
+
storageKind: 'r2';
|
|
16
|
+
storageKey: string;
|
|
17
|
+
bytes: number;
|
|
18
|
+
hash: string;
|
|
19
|
+
expiresAt: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type WorkflowRetryPlayParams = {
|
|
23
|
+
runId: string;
|
|
24
|
+
playId: string;
|
|
25
|
+
playName: string;
|
|
26
|
+
artifactStorageKey: string;
|
|
27
|
+
artifactHash: string;
|
|
28
|
+
graphHash: string;
|
|
29
|
+
input: Record<string, unknown>;
|
|
30
|
+
inputFile?: {
|
|
31
|
+
name?: string;
|
|
32
|
+
r2Key?: string;
|
|
33
|
+
storageKey?: string;
|
|
34
|
+
path?: string;
|
|
35
|
+
fileName?: string;
|
|
36
|
+
logicalPath?: string;
|
|
37
|
+
contentType?: string;
|
|
38
|
+
bytes?: number;
|
|
39
|
+
} | null;
|
|
40
|
+
inlineCsv?: { name: string; rows: Record<string, unknown>[] } | null;
|
|
41
|
+
packagedFiles?: Array<{
|
|
42
|
+
playPath: string;
|
|
43
|
+
storageKey: string;
|
|
44
|
+
contentType?: string;
|
|
45
|
+
bytes?: number;
|
|
46
|
+
inlineText?: string;
|
|
47
|
+
}> | null;
|
|
48
|
+
contractSnapshot?: unknown;
|
|
49
|
+
executionPlan?: ExecutionPlan | null;
|
|
50
|
+
childPlayManifests?: PlayRuntimeManifestMap | null;
|
|
51
|
+
playCallGovernance?: PlayCallGovernanceSnapshot | null;
|
|
52
|
+
preloadedDbSessions?: PreloadedRuntimeDbSession[] | null;
|
|
53
|
+
preloadedDbSessionRef?: {
|
|
54
|
+
runId: string;
|
|
55
|
+
sessionCount: number;
|
|
56
|
+
expiresAt: number;
|
|
57
|
+
} | null;
|
|
58
|
+
dynamicWorkerCode?: string | null;
|
|
59
|
+
executorToken: string;
|
|
60
|
+
baseUrl: string;
|
|
61
|
+
orgId: string;
|
|
62
|
+
userEmail: string;
|
|
63
|
+
userId?: string | null;
|
|
64
|
+
runtimeBackend: string;
|
|
65
|
+
dedupBackend: string;
|
|
66
|
+
totalRows?: number;
|
|
67
|
+
coordinatorUrl?: string | null;
|
|
68
|
+
coordinatorInternalToken?: string | null;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export type WorkflowRetryStatePayload<TParams = WorkflowRetryPlayParams> =
|
|
72
|
+
| {
|
|
73
|
+
params: TParams;
|
|
74
|
+
paramsRef?: null;
|
|
75
|
+
paramsBytes: number;
|
|
76
|
+
}
|
|
77
|
+
| {
|
|
78
|
+
params: null;
|
|
79
|
+
paramsRef: WorkflowRetryParamsRef;
|
|
80
|
+
paramsBytes: number;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export function buildWorkflowRetryParams<
|
|
84
|
+
TParams extends WorkflowRetryPlayParams,
|
|
85
|
+
>(params: TParams): TParams {
|
|
86
|
+
const retryParams = {
|
|
87
|
+
...params,
|
|
88
|
+
dynamicWorkerCode: null,
|
|
89
|
+
contractSnapshot: stripRetrySourceSnapshot(params.contractSnapshot),
|
|
90
|
+
childPlayManifests: stripRetryChildManifestCode(params.childPlayManifests),
|
|
91
|
+
packagedFiles: stripRetryPackagedFiles(params.packagedFiles),
|
|
92
|
+
} satisfies WorkflowRetryPlayParams as TParams;
|
|
93
|
+
if (jsonByteLength(retryParams) <= WORKFLOW_RETRY_STATE_TARGET_BYTES) {
|
|
94
|
+
return retryParams;
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
...retryParams,
|
|
98
|
+
contractSnapshot: stripRetryContractSnapshotToArtifact(
|
|
99
|
+
retryParams.contractSnapshot,
|
|
100
|
+
params,
|
|
101
|
+
),
|
|
102
|
+
childPlayManifests: stripRetryChildManifestToArtifact(
|
|
103
|
+
retryParams.childPlayManifests,
|
|
104
|
+
),
|
|
105
|
+
} satisfies WorkflowRetryPlayParams as TParams;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function jsonByteLength(value: unknown): number {
|
|
109
|
+
return new TextEncoder().encode(JSON.stringify(value)).length;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function workflowRetryParamsStorageKey(input: {
|
|
113
|
+
runId: string;
|
|
114
|
+
hash: string;
|
|
115
|
+
}): string {
|
|
116
|
+
const safeRunId = input.runId
|
|
117
|
+
.toLowerCase()
|
|
118
|
+
.replace(/[^a-z0-9_-]+/g, '-')
|
|
119
|
+
.slice(0, 140);
|
|
120
|
+
return `plays/workflow-retry-params/${safeRunId || 'run'}/${input.hash}.json`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function stripRetryPackagedFiles(
|
|
124
|
+
files: WorkflowRetryPlayParams['packagedFiles'],
|
|
125
|
+
): WorkflowRetryPlayParams['packagedFiles'] {
|
|
126
|
+
return (
|
|
127
|
+
files?.map((file) => ({
|
|
128
|
+
playPath: file.playPath,
|
|
129
|
+
storageKey: file.storageKey,
|
|
130
|
+
contentType: file.contentType,
|
|
131
|
+
bytes: file.bytes,
|
|
132
|
+
})) ?? null
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function stripRetrySourceSnapshot(snapshot: unknown): unknown {
|
|
137
|
+
if (!isRecord(snapshot)) return snapshot;
|
|
138
|
+
const rest = { ...snapshot };
|
|
139
|
+
delete rest.sourceCode;
|
|
140
|
+
delete rest.sourceFiles;
|
|
141
|
+
delete rest.bundledCode;
|
|
142
|
+
return rest;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function stripRetryContractSnapshotToArtifact(
|
|
146
|
+
snapshot: unknown,
|
|
147
|
+
params: WorkflowRetryPlayParams,
|
|
148
|
+
): unknown {
|
|
149
|
+
if (!isRecord(snapshot)) return snapshot;
|
|
150
|
+
return {
|
|
151
|
+
source: snapshot.source ?? 'artifact',
|
|
152
|
+
revisionVersion: snapshot.revisionVersion ?? null,
|
|
153
|
+
staticPipeline: snapshot.staticPipeline ?? null,
|
|
154
|
+
billingLimit: snapshot.billingLimit ?? null,
|
|
155
|
+
artifactMetadata: {
|
|
156
|
+
storageKey: params.artifactStorageKey,
|
|
157
|
+
artifactHash: params.artifactHash,
|
|
158
|
+
graphHash: params.graphHash,
|
|
159
|
+
},
|
|
160
|
+
codeFormat: snapshot.codeFormat ?? null,
|
|
161
|
+
compatibility: snapshot.compatibility ?? null,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function stripRetryChildManifestCode(
|
|
166
|
+
manifests: PlayRuntimeManifestMap | null | undefined,
|
|
167
|
+
): PlayRuntimeManifestMap | null {
|
|
168
|
+
if (!manifests) return null;
|
|
169
|
+
const stripped: PlayRuntimeManifestMap = {};
|
|
170
|
+
for (const [key, manifest] of Object.entries(manifests)) {
|
|
171
|
+
const rest = { ...manifest };
|
|
172
|
+
delete rest.bundledCode;
|
|
173
|
+
delete rest.sourceCode;
|
|
174
|
+
delete (rest as Record<string, unknown>).sourceMap;
|
|
175
|
+
stripped[key] = rest;
|
|
176
|
+
}
|
|
177
|
+
return stripped;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function stripRetryChildManifestToArtifact(
|
|
181
|
+
manifests: PlayRuntimeManifestMap | null | undefined,
|
|
182
|
+
): PlayRuntimeManifestMap | null {
|
|
183
|
+
if (!manifests) return null;
|
|
184
|
+
const stripped: PlayRuntimeManifestMap = {};
|
|
185
|
+
for (const [key, manifest] of Object.entries(manifests)) {
|
|
186
|
+
stripped[key] = {
|
|
187
|
+
playName: manifest.playName,
|
|
188
|
+
graphHash: manifest.graphHash,
|
|
189
|
+
artifactStorageKey: manifest.artifactStorageKey,
|
|
190
|
+
artifactHash: manifest.artifactHash,
|
|
191
|
+
staticPipelineHash: manifest.staticPipelineHash,
|
|
192
|
+
staticPipeline: manifest.staticPipeline,
|
|
193
|
+
compiledAt: manifest.compiledAt,
|
|
194
|
+
compilerVersion: manifest.compilerVersion,
|
|
195
|
+
maxCreditsPerRun: manifest.maxCreditsPerRun,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
return stripped;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
202
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
203
|
+
}
|
|
@@ -50,10 +50,10 @@ export type SdkRelease = {
|
|
|
50
50
|
};
|
|
51
51
|
|
|
52
52
|
export const SDK_RELEASE = {
|
|
53
|
-
version: '0.1.
|
|
53
|
+
version: '0.1.79',
|
|
54
54
|
apiContract: '2026-06-dataset-column-cell-stale-hard-cutover',
|
|
55
55
|
supportPolicy: {
|
|
56
|
-
latest: '0.1.
|
|
56
|
+
latest: '0.1.79',
|
|
57
57
|
minimumSupported: '0.1.53',
|
|
58
58
|
deprecatedBelow: '0.1.53',
|
|
59
59
|
},
|
|
@@ -3,6 +3,7 @@ import { normalizeTableNamespace } from './row-identity';
|
|
|
3
3
|
export interface PlayStaticPipeline {
|
|
4
4
|
tableNamespace?: string;
|
|
5
5
|
inputFields?: string[];
|
|
6
|
+
rowKeyFields?: string[];
|
|
6
7
|
csvArg?: string;
|
|
7
8
|
hasInlineData?: boolean;
|
|
8
9
|
csvDescription?: string;
|
|
@@ -32,6 +33,7 @@ export interface PlaySheetColumnContract {
|
|
|
32
33
|
outputSqlName?: string;
|
|
33
34
|
stepId?: string;
|
|
34
35
|
toolId?: string;
|
|
36
|
+
isRowKey?: boolean;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
export interface PlaySheetContract {
|
|
@@ -39,6 +41,40 @@ export interface PlaySheetContract {
|
|
|
39
41
|
columns: PlaySheetColumnContract[];
|
|
40
42
|
}
|
|
41
43
|
|
|
44
|
+
export type PlayStaticColumnProducerKind =
|
|
45
|
+
| 'tool'
|
|
46
|
+
| 'waterfall'
|
|
47
|
+
| 'stepProgram'
|
|
48
|
+
| 'playCall'
|
|
49
|
+
| 'transform';
|
|
50
|
+
|
|
51
|
+
export interface PlayStaticColumnProducer {
|
|
52
|
+
id: string;
|
|
53
|
+
kind: PlayStaticColumnProducerKind;
|
|
54
|
+
field: string;
|
|
55
|
+
toolId?: string;
|
|
56
|
+
playId?: string;
|
|
57
|
+
conditional?: boolean;
|
|
58
|
+
sourceRange?: PlayStaticSourceRange;
|
|
59
|
+
steps?: PlayStaticColumnProducer[];
|
|
60
|
+
substep: PlayStaticSubstep;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface PlayStaticDatasetColumn {
|
|
64
|
+
id: string;
|
|
65
|
+
source: PlaySheetColumnSource;
|
|
66
|
+
sqlName?: string;
|
|
67
|
+
producers: PlayStaticColumnProducer[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface PlayCompiledStaticGraph {
|
|
71
|
+
topLevel: PlayStaticSubstep[];
|
|
72
|
+
datasets: Array<{
|
|
73
|
+
tableNamespace: string;
|
|
74
|
+
columns: PlayStaticDatasetColumn[];
|
|
75
|
+
}>;
|
|
76
|
+
}
|
|
77
|
+
|
|
42
78
|
export function ensureCompiledSheetContract(
|
|
43
79
|
pipeline: PlayStaticPipeline | null | undefined,
|
|
44
80
|
): PlayStaticPipeline | null | undefined {
|
|
@@ -92,8 +128,27 @@ function truncateStaticSubstepsForStorage(
|
|
|
92
128
|
return {
|
|
93
129
|
...base,
|
|
94
130
|
inputFields: base.inputFields ? [...base.inputFields] : undefined,
|
|
131
|
+
rowKeyFields: base.rowKeyFields ? [...base.rowKeyFields] : undefined,
|
|
95
132
|
outputFields: base.outputFields ? [...base.outputFields] : undefined,
|
|
133
|
+
columns: base.columns
|
|
134
|
+
? base.columns.map((column) => ({
|
|
135
|
+
...column,
|
|
136
|
+
producers: column.producers.map((producer) => ({
|
|
137
|
+
...producer,
|
|
138
|
+
sourceRange: cloneStorageSafeSourceRange(producer.sourceRange),
|
|
139
|
+
steps: producer.steps
|
|
140
|
+
? producer.steps.map((stepProducer) => ({
|
|
141
|
+
...stepProducer,
|
|
142
|
+
sourceRange: cloneStorageSafeSourceRange(
|
|
143
|
+
stepProducer.sourceRange,
|
|
144
|
+
),
|
|
145
|
+
}))
|
|
146
|
+
: undefined,
|
|
147
|
+
})),
|
|
148
|
+
}))
|
|
149
|
+
: undefined,
|
|
96
150
|
waterfallIds: base.waterfallIds ? [...base.waterfallIds] : undefined,
|
|
151
|
+
steps: truncateStaticSubstepsForStorage(base.steps, input),
|
|
97
152
|
sheetContract: cloneStorageSafeSheetContract(base.sheetContract),
|
|
98
153
|
};
|
|
99
154
|
}
|
|
@@ -153,6 +208,9 @@ export function truncateStaticPipelineForStorage(
|
|
|
153
208
|
return {
|
|
154
209
|
...pipeline,
|
|
155
210
|
inputFields: pipeline.inputFields ? [...pipeline.inputFields] : undefined,
|
|
211
|
+
rowKeyFields: pipeline.rowKeyFields
|
|
212
|
+
? [...pipeline.rowKeyFields]
|
|
213
|
+
: undefined,
|
|
156
214
|
fields: [...(pipeline.fields ?? [])],
|
|
157
215
|
stages: truncateStaticSubstepsForStorage(pipeline.stages, {
|
|
158
216
|
embeddedPlayCallPipelineDepth,
|
|
@@ -198,8 +256,11 @@ export type PlayStaticSubstep = PlayStaticSubstepMetadata &
|
|
|
198
256
|
name?: string;
|
|
199
257
|
tableNamespace?: string;
|
|
200
258
|
inputFields?: string[];
|
|
259
|
+
rowKeyFields?: string[];
|
|
201
260
|
outputFields?: string[];
|
|
261
|
+
columns?: PlayStaticDatasetColumn[];
|
|
202
262
|
waterfallIds?: string[];
|
|
263
|
+
steps?: PlayStaticSubstep[];
|
|
203
264
|
sheetContract?: PlaySheetContract | null;
|
|
204
265
|
description?: string;
|
|
205
266
|
sourceRange?: PlayStaticSourceRange;
|
|
@@ -210,6 +271,8 @@ export type PlayStaticSubstep = PlayStaticSubstepMetadata &
|
|
|
210
271
|
type: 'tool';
|
|
211
272
|
toolId: string;
|
|
212
273
|
field: string;
|
|
274
|
+
paramsSource?: string;
|
|
275
|
+
sourceText?: string;
|
|
213
276
|
description?: string;
|
|
214
277
|
inLoop?: boolean;
|
|
215
278
|
isEventWait?: boolean;
|
|
@@ -263,6 +326,7 @@ export type PlayStaticSubstep = PlayStaticSubstepMetadata &
|
|
|
263
326
|
| {
|
|
264
327
|
type: 'run_javascript';
|
|
265
328
|
alias: string;
|
|
329
|
+
sourceText?: string;
|
|
266
330
|
description?: string;
|
|
267
331
|
sourceRange?: PlayStaticSourceRange;
|
|
268
332
|
callDepth?: number;
|
|
@@ -271,6 +335,7 @@ export type PlayStaticSubstep = PlayStaticSubstepMetadata &
|
|
|
271
335
|
| {
|
|
272
336
|
type: 'code';
|
|
273
337
|
field: string;
|
|
338
|
+
sourceText?: string;
|
|
274
339
|
description?: string;
|
|
275
340
|
sourceRange?: PlayStaticSourceRange;
|
|
276
341
|
callDepth?: number;
|
|
@@ -289,6 +354,12 @@ export function getCompiledPipelineSubsteps(
|
|
|
289
354
|
|
|
290
355
|
export function getTopLevelPipelineSubsteps(
|
|
291
356
|
pipeline: PlayStaticPipeline | null | undefined,
|
|
357
|
+
): PlayStaticSubstep[] {
|
|
358
|
+
return compileStaticGraph(pipeline).topLevel;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function getRawTopLevelPipelineSubsteps(
|
|
362
|
+
pipeline: PlayStaticPipeline | null | undefined,
|
|
292
363
|
): PlayStaticSubstep[] {
|
|
293
364
|
if (!pipeline) {
|
|
294
365
|
return [];
|
|
@@ -328,6 +399,14 @@ export function flattenStaticSubsteps(
|
|
|
328
399
|
|
|
329
400
|
for (const substep of substeps) {
|
|
330
401
|
flattened.push(substep);
|
|
402
|
+
if (substep.type === 'dataset' && substep.steps?.length) {
|
|
403
|
+
flattened.push(...flattenStaticSubsteps(substep.steps));
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
if (substep.type === 'step_suite') {
|
|
407
|
+
flattened.push(...flattenStaticSubsteps(substep.steps));
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
331
410
|
if (substep.type === 'play_call' && substep.pipeline) {
|
|
332
411
|
const nestedSubsteps = getCompiledPipelineSubsteps(substep.pipeline);
|
|
333
412
|
if (nestedSubsteps.length > 0) {
|
|
@@ -345,6 +424,137 @@ export function flattenStaticPipeline(
|
|
|
345
424
|
return flattenStaticSubsteps(getCompiledPipelineSubsteps(pipeline));
|
|
346
425
|
}
|
|
347
426
|
|
|
427
|
+
export function compileStaticGraph(
|
|
428
|
+
pipeline: PlayStaticPipeline | null | undefined,
|
|
429
|
+
): PlayCompiledStaticGraph {
|
|
430
|
+
const rawTopLevel = getRawTopLevelPipelineSubsteps(pipeline);
|
|
431
|
+
const datasets: PlayCompiledStaticGraph['datasets'] = [];
|
|
432
|
+
const topLevel = rawTopLevel.map((substep) => {
|
|
433
|
+
if (substep.type !== 'dataset') {
|
|
434
|
+
return substep;
|
|
435
|
+
}
|
|
436
|
+
const columns = compileDatasetColumns(substep);
|
|
437
|
+
const tableNamespace = (substep.tableNamespace ?? substep.field).trim();
|
|
438
|
+
if (tableNamespace) {
|
|
439
|
+
datasets.push({ tableNamespace, columns });
|
|
440
|
+
}
|
|
441
|
+
return {
|
|
442
|
+
...substep,
|
|
443
|
+
columns,
|
|
444
|
+
} satisfies PlayStaticSubstep;
|
|
445
|
+
});
|
|
446
|
+
return { topLevel, datasets };
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function compileDatasetColumns(
|
|
450
|
+
dataset: Extract<PlayStaticSubstep, { type: 'dataset' }>,
|
|
451
|
+
): PlayStaticDatasetColumn[] {
|
|
452
|
+
const columnsById = new Map<string, PlayStaticDatasetColumn>();
|
|
453
|
+
const ensureColumn = (
|
|
454
|
+
id: string,
|
|
455
|
+
source: PlaySheetColumnSource,
|
|
456
|
+
sqlName?: string,
|
|
457
|
+
) => {
|
|
458
|
+
const trimmed = id.trim();
|
|
459
|
+
if (!trimmed) return null;
|
|
460
|
+
const existing = columnsById.get(trimmed);
|
|
461
|
+
if (existing) {
|
|
462
|
+
if (!existing.sqlName && sqlName) existing.sqlName = sqlName;
|
|
463
|
+
return existing;
|
|
464
|
+
}
|
|
465
|
+
const column: PlayStaticDatasetColumn = {
|
|
466
|
+
id: trimmed,
|
|
467
|
+
source,
|
|
468
|
+
...(sqlName ? { sqlName } : {}),
|
|
469
|
+
producers: [],
|
|
470
|
+
};
|
|
471
|
+
columnsById.set(trimmed, column);
|
|
472
|
+
return column;
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
for (const column of dataset.sheetContract?.columns ?? []) {
|
|
476
|
+
ensureColumn(column.id, column.source, column.sqlName);
|
|
477
|
+
}
|
|
478
|
+
for (const field of dataset.inputFields ?? []) {
|
|
479
|
+
ensureColumn(field, 'input', sqlSafePlayColumnName(field));
|
|
480
|
+
}
|
|
481
|
+
for (const field of dataset.outputFields ?? []) {
|
|
482
|
+
ensureColumn(field, 'datasetColumn', sqlSafePlayColumnName(field));
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
for (const substep of dataset.steps ?? []) {
|
|
486
|
+
const field = fieldForColumnProducer(substep);
|
|
487
|
+
if (!field) continue;
|
|
488
|
+
const column = ensureColumn(
|
|
489
|
+
field,
|
|
490
|
+
'datasetColumn',
|
|
491
|
+
sqlSafePlayColumnName(field),
|
|
492
|
+
);
|
|
493
|
+
if (!column) continue;
|
|
494
|
+
column.producers.push(columnProducerFromSubstep(substep, field));
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return [...columnsById.values()];
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function fieldForColumnProducer(substep: PlayStaticSubstep): string | null {
|
|
501
|
+
if ('field' in substep && typeof substep.field === 'string') {
|
|
502
|
+
return substep.field.trim() || null;
|
|
503
|
+
}
|
|
504
|
+
if (substep.type === 'run_javascript') {
|
|
505
|
+
return substep.alias.trim() || null;
|
|
506
|
+
}
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function columnProducerFromSubstep(
|
|
511
|
+
substep: PlayStaticSubstep,
|
|
512
|
+
field: string,
|
|
513
|
+
): PlayStaticColumnProducer {
|
|
514
|
+
const steps =
|
|
515
|
+
substep.type === 'step_suite'
|
|
516
|
+
? substep.steps
|
|
517
|
+
.map((step) => {
|
|
518
|
+
const stepField = fieldForColumnProducer(step) ?? field;
|
|
519
|
+
return columnProducerFromSubstep(step, stepField);
|
|
520
|
+
})
|
|
521
|
+
.filter((producer) => producer.field.trim())
|
|
522
|
+
: undefined;
|
|
523
|
+
const kind: PlayStaticColumnProducerKind =
|
|
524
|
+
substep.type === 'tool'
|
|
525
|
+
? 'tool'
|
|
526
|
+
: substep.type === 'waterfall'
|
|
527
|
+
? 'waterfall'
|
|
528
|
+
: substep.type === 'step_suite'
|
|
529
|
+
? 'stepProgram'
|
|
530
|
+
: substep.type === 'play_call'
|
|
531
|
+
? 'playCall'
|
|
532
|
+
: 'transform';
|
|
533
|
+
return {
|
|
534
|
+
id: producerId(substep, field),
|
|
535
|
+
kind,
|
|
536
|
+
field,
|
|
537
|
+
...(substep.type === 'tool' ? { toolId: substep.toolId } : {}),
|
|
538
|
+
...(substep.type === 'play_call' ? { playId: substep.playId } : {}),
|
|
539
|
+
...(substep.conditional ? { conditional: true } : {}),
|
|
540
|
+
...(substep.sourceRange ? { sourceRange: substep.sourceRange } : {}),
|
|
541
|
+
...(steps && steps.length > 0 ? { steps } : {}),
|
|
542
|
+
substep,
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function producerId(substep: PlayStaticSubstep, field: string): string {
|
|
547
|
+
if (substep.type === 'tool') return `${field}:tool:${substep.toolId}`;
|
|
548
|
+
if (substep.type === 'waterfall') {
|
|
549
|
+
return `${field}:waterfall:${substep.id ?? substep.tool ?? 'unknown'}`;
|
|
550
|
+
}
|
|
551
|
+
if (substep.type === 'play_call') return `${field}:play:${substep.playId}`;
|
|
552
|
+
if (substep.type === 'step_suite') return `${field}:steps`;
|
|
553
|
+
if (substep.type === 'run_javascript')
|
|
554
|
+
return `${field}:transform:${substep.alias}`;
|
|
555
|
+
return `${field}:transform`;
|
|
556
|
+
}
|
|
557
|
+
|
|
348
558
|
export function resolveSheetContractForTableNamespace(
|
|
349
559
|
pipeline: PlayStaticPipeline | null | undefined,
|
|
350
560
|
tableNamespace: string | null | undefined,
|
|
@@ -400,6 +610,50 @@ export function resolveSheetContractForTableNamespace(
|
|
|
400
610
|
return resolveFromPipeline(pipeline);
|
|
401
611
|
}
|
|
402
612
|
|
|
613
|
+
export function resolveStaticDatasetColumnsForTableNamespace(
|
|
614
|
+
pipeline: PlayStaticPipeline | null | undefined,
|
|
615
|
+
tableNamespace: string | null | undefined,
|
|
616
|
+
): PlayStaticDatasetColumn[] {
|
|
617
|
+
const requestedNamespace = tableNamespace?.trim();
|
|
618
|
+
if (!pipeline || !requestedNamespace) {
|
|
619
|
+
return [];
|
|
620
|
+
}
|
|
621
|
+
const normalizedNamespace = normalizeTableNamespace(requestedNamespace);
|
|
622
|
+
const seen = new Set<PlayStaticPipeline>();
|
|
623
|
+
|
|
624
|
+
const resolveFromPipeline = (
|
|
625
|
+
currentPipeline: PlayStaticPipeline | null | undefined,
|
|
626
|
+
): PlayStaticDatasetColumn[] | null => {
|
|
627
|
+
if (!currentPipeline || seen.has(currentPipeline)) {
|
|
628
|
+
return null;
|
|
629
|
+
}
|
|
630
|
+
seen.add(currentPipeline);
|
|
631
|
+
|
|
632
|
+
const compiled = compileStaticGraph(currentPipeline);
|
|
633
|
+
const matchingDataset = compiled.datasets.find(
|
|
634
|
+
(dataset) =>
|
|
635
|
+
normalizeTableNamespace(dataset.tableNamespace) ===
|
|
636
|
+
normalizedNamespace,
|
|
637
|
+
);
|
|
638
|
+
if (matchingDataset) {
|
|
639
|
+
return matchingDataset.columns;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
for (const substep of getCompiledPipelineSubsteps(currentPipeline)) {
|
|
643
|
+
if (substep.type === 'play_call') {
|
|
644
|
+
const nestedColumns = resolveFromPipeline(substep.pipeline);
|
|
645
|
+
if (nestedColumns) {
|
|
646
|
+
return nestedColumns;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
return null;
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
return resolveFromPipeline(pipeline) ?? [];
|
|
655
|
+
}
|
|
656
|
+
|
|
403
657
|
export function sqlSafePlayColumnName(id: string): string {
|
|
404
658
|
const normalized = id
|
|
405
659
|
.trim()
|
|
@@ -441,8 +695,14 @@ export function compileSheetContract(pipeline: PlayStaticPipeline): {
|
|
|
441
695
|
const inputFields = pipeline.inputFields?.length
|
|
442
696
|
? pipeline.inputFields
|
|
443
697
|
: [tableNamespace];
|
|
698
|
+
const rowKeyFieldSet = new Set(pipeline.rowKeyFields ?? []);
|
|
444
699
|
for (const inputField of inputFields) {
|
|
445
|
-
addColumn({
|
|
700
|
+
addColumn({
|
|
701
|
+
id: inputField,
|
|
702
|
+
source: 'input',
|
|
703
|
+
field: inputField,
|
|
704
|
+
...(rowKeyFieldSet.has(inputField) ? { isRowKey: true } : {}),
|
|
705
|
+
});
|
|
446
706
|
}
|
|
447
707
|
|
|
448
708
|
for (const field of pipeline.fields) {
|