deepline 0.1.25 → 0.1.27
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 +118 -85
- package/dist/cli/index.mjs +103 -69
- package/dist/index.d.mts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -2
- package/dist/index.mjs +2 -2
- package/dist/repo/apps/play-runner-workers/src/coordinator-entry.ts +434 -102
- package/dist/repo/apps/play-runner-workers/src/dedup-do.ts +6 -1
- package/dist/repo/apps/play-runner-workers/src/entry.ts +1169 -719
- package/dist/repo/apps/play-runner-workers/src/runtime/dataset-handles.ts +418 -0
- package/dist/repo/sdk/src/client.ts +5 -1
- package/dist/repo/sdk/src/plays/bundle-play-file.ts +1 -1
- package/dist/repo/sdk/src/plays/harness-stub.ts +25 -55
- package/dist/repo/sdk/src/version.ts +1 -1
- package/dist/repo/shared_libs/play-runtime/execution-plan.ts +18 -8
- package/dist/repo/shared_libs/play-runtime/scheduler-backend.ts +5 -4
- package/dist/repo/shared_libs/play-runtime/step-lifecycle-tracker.ts +228 -0
- package/dist/repo/shared_libs/plays/bundling/index.ts +90 -51
- package/package.json +1 -1
- package/dist/repo/shared_libs/play-runtime/runtime-actions.ts +0 -208
|
@@ -28,7 +28,7 @@ import type {
|
|
|
28
28
|
PlayRuntimeManifest,
|
|
29
29
|
PlayRuntimeManifestMap,
|
|
30
30
|
} from '../../../shared_libs/plays/compiler-manifest';
|
|
31
|
-
import {
|
|
31
|
+
import type { PlayRunLedgerEvent } from '../../../shared_libs/play-runtime/run-ledger';
|
|
32
32
|
import {
|
|
33
33
|
COORDINATOR_INTERNAL_TOKEN_HEADER,
|
|
34
34
|
COORDINATOR_RUN_SCOPE_HEADER,
|
|
@@ -45,11 +45,22 @@ export type PlayWorkflowParams = {
|
|
|
45
45
|
artifactHash: string;
|
|
46
46
|
graphHash: string;
|
|
47
47
|
input: Record<string, unknown>;
|
|
48
|
-
inputFile?: {
|
|
48
|
+
inputFile?: {
|
|
49
|
+
name?: string;
|
|
50
|
+
r2Key?: string;
|
|
51
|
+
storageKey?: string;
|
|
52
|
+
path?: string;
|
|
53
|
+
fileName?: string;
|
|
54
|
+
logicalPath?: string;
|
|
55
|
+
contentType?: string;
|
|
56
|
+
bytes?: number;
|
|
57
|
+
} | null;
|
|
49
58
|
inlineCsv?: { name: string; rows: Record<string, unknown>[] } | null;
|
|
50
59
|
packagedFiles?: Array<{
|
|
51
60
|
playPath: string;
|
|
52
61
|
storageKey: string;
|
|
62
|
+
contentType?: string;
|
|
63
|
+
bytes?: number;
|
|
53
64
|
inlineText?: string;
|
|
54
65
|
}> | null;
|
|
55
66
|
contractSnapshot?: unknown;
|
|
@@ -221,7 +232,7 @@ interface CoordinatorEnv {
|
|
|
221
232
|
/**
|
|
222
233
|
* Service binding to the long-lived Play Harness Worker
|
|
223
234
|
* (apps/play-harness-worker). Provides typed RPC access to leaf-level
|
|
224
|
-
* helpers (
|
|
235
|
+
* helpers (runtime-API HTTP forwarder, Neon dataset IO, …) that we
|
|
225
236
|
* deliberately keep OUT of every per-graphHash play bundle.
|
|
226
237
|
*
|
|
227
238
|
* Optional: when missing (e.g. an older deploy that hasn't been wired
|
|
@@ -508,6 +519,8 @@ type DynamicWorkflowMetadata = {
|
|
|
508
519
|
packagedFiles?: Array<{
|
|
509
520
|
playPath: string;
|
|
510
521
|
storageKey: string;
|
|
522
|
+
contentType?: string;
|
|
523
|
+
bytes?: number;
|
|
511
524
|
inlineText?: string;
|
|
512
525
|
}> | null;
|
|
513
526
|
};
|
|
@@ -534,10 +547,10 @@ const WORKFLOW_POOL_READY_POLL_MS = 250;
|
|
|
534
547
|
const WORKFLOW_POOL_REFILL_ON_MISS_TIMEOUT_MS = 2_500;
|
|
535
548
|
const WORKFLOW_POOL_REFILL_ON_MISS_MIN_AVAILABLE = 4;
|
|
536
549
|
const WORKFLOW_POOL_CONTROL_TIMEOUT_MS = 750;
|
|
550
|
+
const WORKFLOW_POOL_START_ACK_TIMEOUT_MS = 750;
|
|
551
|
+
const WORKFLOW_POOL_START_ACK_POLL_MS = 25;
|
|
537
552
|
const SUBMIT_INITIAL_STATE_MAX_WAIT_MS = 0;
|
|
538
553
|
const SUBMIT_INITIAL_STATE_POLL_MS = 50;
|
|
539
|
-
const WORKFLOW_POOL_DISABLED_REASON =
|
|
540
|
-
'Cloudflare Workflows start runs directly; waitForEvent is reserved for real durable external waits.';
|
|
541
554
|
function buildDynamicWorkflowMetadata(
|
|
542
555
|
params: PlayWorkflowParams,
|
|
543
556
|
): DynamicWorkflowMetadata {
|
|
@@ -610,7 +623,7 @@ function readWorkflowTraceContext(event: unknown): {
|
|
|
610
623
|
}
|
|
611
624
|
|
|
612
625
|
function workflowPoolEnabled(): boolean {
|
|
613
|
-
return
|
|
626
|
+
return WORKFLOW_POOL_TARGET_SIZE > 0;
|
|
614
627
|
}
|
|
615
628
|
|
|
616
629
|
function workflowPoolTargetSize(): number {
|
|
@@ -876,8 +889,33 @@ async function mapRunToWorkflowInstance(input: {
|
|
|
876
889
|
env: CoordinatorEnv;
|
|
877
890
|
runId: string;
|
|
878
891
|
instanceId: string;
|
|
879
|
-
|
|
880
|
-
|
|
892
|
+
started?: boolean;
|
|
893
|
+
}): Promise<boolean> {
|
|
894
|
+
const body = await callWorkflowPool<{ mapped?: unknown }>(
|
|
895
|
+
input.env,
|
|
896
|
+
'/pool-map-run',
|
|
897
|
+
{
|
|
898
|
+
method: 'POST',
|
|
899
|
+
body: JSON.stringify({
|
|
900
|
+
runId: input.runId,
|
|
901
|
+
instanceId: input.instanceId,
|
|
902
|
+
started: input.started === true,
|
|
903
|
+
version: WORKFLOW_POOL_PROTOCOL_VERSION,
|
|
904
|
+
}),
|
|
905
|
+
},
|
|
906
|
+
);
|
|
907
|
+
return body.mapped !== false;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
async function blockWorkflowPoolRun(input: {
|
|
911
|
+
env: CoordinatorEnv;
|
|
912
|
+
runId: string;
|
|
913
|
+
instanceId: string;
|
|
914
|
+
}): Promise<{ blocked: boolean; started: boolean }> {
|
|
915
|
+
const body = await callWorkflowPool<{
|
|
916
|
+
blocked?: unknown;
|
|
917
|
+
started?: unknown;
|
|
918
|
+
}>(input.env, '/pool-block-run', {
|
|
881
919
|
method: 'POST',
|
|
882
920
|
body: JSON.stringify({
|
|
883
921
|
runId: input.runId,
|
|
@@ -885,6 +923,82 @@ async function mapRunToWorkflowInstance(input: {
|
|
|
885
923
|
version: WORKFLOW_POOL_PROTOCOL_VERSION,
|
|
886
924
|
}),
|
|
887
925
|
});
|
|
926
|
+
return {
|
|
927
|
+
blocked: body.blocked === true,
|
|
928
|
+
started: body.started === true,
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
async function readWorkflowPoolRunMapping(input: {
|
|
933
|
+
env: CoordinatorEnv;
|
|
934
|
+
runId: string;
|
|
935
|
+
}): Promise<{ instanceId: string | null; startedAt: number | null }> {
|
|
936
|
+
const body = await callWorkflowPool<{
|
|
937
|
+
instanceId?: unknown;
|
|
938
|
+
startedAt?: unknown;
|
|
939
|
+
}>(
|
|
940
|
+
input.env,
|
|
941
|
+
`/pool-resolve-run?runId=${encodeURIComponent(input.runId)}&version=${encodeURIComponent(
|
|
942
|
+
WORKFLOW_POOL_PROTOCOL_VERSION,
|
|
943
|
+
)}`,
|
|
944
|
+
).catch(() => ({ instanceId: null, startedAt: null }));
|
|
945
|
+
return {
|
|
946
|
+
instanceId:
|
|
947
|
+
typeof body.instanceId === 'string' && body.instanceId
|
|
948
|
+
? body.instanceId
|
|
949
|
+
: null,
|
|
950
|
+
startedAt:
|
|
951
|
+
typeof body.startedAt === 'number' && Number.isFinite(body.startedAt)
|
|
952
|
+
? body.startedAt
|
|
953
|
+
: null,
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
async function waitForWorkflowPoolStartAck(input: {
|
|
958
|
+
env: CoordinatorEnv;
|
|
959
|
+
runId: string;
|
|
960
|
+
instanceId: string;
|
|
961
|
+
timeoutMs: number;
|
|
962
|
+
}): Promise<{
|
|
963
|
+
acknowledged: boolean;
|
|
964
|
+
ms: number;
|
|
965
|
+
polls: number;
|
|
966
|
+
startedAt: number | null;
|
|
967
|
+
mappedInstanceId: string | null;
|
|
968
|
+
}> {
|
|
969
|
+
const startedAt = Date.now();
|
|
970
|
+
let polls = 0;
|
|
971
|
+
let latestMapping: { instanceId: string | null; startedAt: number | null } = {
|
|
972
|
+
instanceId: null,
|
|
973
|
+
startedAt: null,
|
|
974
|
+
};
|
|
975
|
+
while (Date.now() - startedAt < input.timeoutMs) {
|
|
976
|
+
polls += 1;
|
|
977
|
+
latestMapping = await readWorkflowPoolRunMapping({
|
|
978
|
+
env: input.env,
|
|
979
|
+
runId: input.runId,
|
|
980
|
+
});
|
|
981
|
+
if (
|
|
982
|
+
latestMapping.instanceId === input.instanceId &&
|
|
983
|
+
latestMapping.startedAt !== null
|
|
984
|
+
) {
|
|
985
|
+
return {
|
|
986
|
+
acknowledged: true,
|
|
987
|
+
ms: Date.now() - startedAt,
|
|
988
|
+
polls,
|
|
989
|
+
startedAt: latestMapping.startedAt,
|
|
990
|
+
mappedInstanceId: latestMapping.instanceId,
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
await sleep(WORKFLOW_POOL_START_ACK_POLL_MS);
|
|
994
|
+
}
|
|
995
|
+
return {
|
|
996
|
+
acknowledged: false,
|
|
997
|
+
ms: Date.now() - startedAt,
|
|
998
|
+
polls,
|
|
999
|
+
startedAt: latestMapping.startedAt,
|
|
1000
|
+
mappedInstanceId: latestMapping.instanceId,
|
|
1001
|
+
};
|
|
888
1002
|
}
|
|
889
1003
|
|
|
890
1004
|
async function resolveWorkflowInstanceIdForRun(
|
|
@@ -894,15 +1008,8 @@ async function resolveWorkflowInstanceIdForRun(
|
|
|
894
1008
|
if (!workflowPoolEnabled()) {
|
|
895
1009
|
return workflowInstanceId(runId);
|
|
896
1010
|
}
|
|
897
|
-
const
|
|
898
|
-
|
|
899
|
-
`/pool-resolve-run?runId=${encodeURIComponent(runId)}&version=${encodeURIComponent(
|
|
900
|
-
WORKFLOW_POOL_PROTOCOL_VERSION,
|
|
901
|
-
)}`,
|
|
902
|
-
).catch(() => ({ instanceId: null }));
|
|
903
|
-
return typeof body.instanceId === 'string' && body.instanceId
|
|
904
|
-
? body.instanceId
|
|
905
|
-
: workflowInstanceId(runId);
|
|
1011
|
+
const mapping = await readWorkflowPoolRunMapping({ env, runId });
|
|
1012
|
+
return mapping.instanceId ? mapping.instanceId : workflowInstanceId(runId);
|
|
906
1013
|
}
|
|
907
1014
|
|
|
908
1015
|
async function clearWorkflowPool(env: CoordinatorEnv): Promise<number> {
|
|
@@ -914,7 +1021,10 @@ async function clearWorkflowPool(env: CoordinatorEnv): Promise<number> {
|
|
|
914
1021
|
);
|
|
915
1022
|
await Promise.all(
|
|
916
1023
|
entries.map(async (entry) => {
|
|
917
|
-
const instance = await env
|
|
1024
|
+
const instance = await getWorkflowPoolInstance(env, entry.id);
|
|
1025
|
+
if (!instance) {
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
918
1028
|
try {
|
|
919
1029
|
await instance.terminate().catch(() => undefined);
|
|
920
1030
|
} finally {
|
|
@@ -929,6 +1039,27 @@ function workflowStatusName(status: InstanceStatus | null): string {
|
|
|
929
1039
|
return typeof status?.status === 'string' ? status.status : 'unknown';
|
|
930
1040
|
}
|
|
931
1041
|
|
|
1042
|
+
function isWorkflowInstanceNotFoundError(error: unknown): boolean {
|
|
1043
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1044
|
+
return /not[ _]found|not_found|does not exist|no such instance|404/i.test(
|
|
1045
|
+
message,
|
|
1046
|
+
);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
async function getWorkflowPoolInstance(
|
|
1050
|
+
env: CoordinatorEnv,
|
|
1051
|
+
instanceId: string,
|
|
1052
|
+
): Promise<WorkflowInstance | null> {
|
|
1053
|
+
try {
|
|
1054
|
+
return await env.PLAY_WORKFLOW.get(instanceId);
|
|
1055
|
+
} catch (error) {
|
|
1056
|
+
if (isWorkflowInstanceNotFoundError(error)) {
|
|
1057
|
+
return null;
|
|
1058
|
+
}
|
|
1059
|
+
throw error;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
932
1063
|
function workflowPoolStatusIsReady(statusName: string): boolean {
|
|
933
1064
|
// This is only a liveness guard. Readiness itself comes from the pooled
|
|
934
1065
|
// Workflow calling /pool-ready after waitForEvent("play_start") has been
|
|
@@ -1011,7 +1142,11 @@ async function refillWorkflowPoolOnce(
|
|
|
1011
1142
|
const promotedIds: string[] = [];
|
|
1012
1143
|
const removedIds: string[] = [];
|
|
1013
1144
|
for (const entry of warmingEntries) {
|
|
1014
|
-
const instance = await env
|
|
1145
|
+
const instance = await getWorkflowPoolInstance(env, entry.id);
|
|
1146
|
+
if (!instance) {
|
|
1147
|
+
removedIds.push(entry.id);
|
|
1148
|
+
continue;
|
|
1149
|
+
}
|
|
1015
1150
|
try {
|
|
1016
1151
|
if (entry.state === 'ready' && entry.readyAt !== null) {
|
|
1017
1152
|
promotedIds.push(entry.id);
|
|
@@ -1238,7 +1373,21 @@ async function submitViaPooledWorkflow(input: {
|
|
|
1238
1373
|
return null;
|
|
1239
1374
|
}
|
|
1240
1375
|
|
|
1241
|
-
const instance = await input.env
|
|
1376
|
+
const instance = await getWorkflowPoolInstance(input.env, pooledInstanceId);
|
|
1377
|
+
if (!instance) {
|
|
1378
|
+
await blockWorkflowPoolRun({
|
|
1379
|
+
env: input.env,
|
|
1380
|
+
runId: input.params.runId,
|
|
1381
|
+
instanceId: pooledInstanceId,
|
|
1382
|
+
}).catch(() => undefined);
|
|
1383
|
+
input.recordSubmitTiming({
|
|
1384
|
+
phase: 'coordinator.workflow_pool_ready_check',
|
|
1385
|
+
ms: Date.now() - leaseStartedAt,
|
|
1386
|
+
graphHash: input.params.graphHash ?? null,
|
|
1387
|
+
extra: { instanceId: pooledInstanceId, status: 'missing' },
|
|
1388
|
+
});
|
|
1389
|
+
return null;
|
|
1390
|
+
}
|
|
1242
1391
|
const readyCheckStartedAt = Date.now();
|
|
1243
1392
|
const status = await instance.status().catch(() => null);
|
|
1244
1393
|
const statusName = workflowStatusName(status);
|
|
@@ -1249,6 +1398,11 @@ async function submitViaPooledWorkflow(input: {
|
|
|
1249
1398
|
extra: { instanceId: pooledInstanceId, status: statusName },
|
|
1250
1399
|
});
|
|
1251
1400
|
if (!workflowPoolStatusIsReady(statusName)) {
|
|
1401
|
+
await blockWorkflowPoolRun({
|
|
1402
|
+
env: input.env,
|
|
1403
|
+
runId: input.params.runId,
|
|
1404
|
+
instanceId: pooledInstanceId,
|
|
1405
|
+
}).catch(() => undefined);
|
|
1252
1406
|
await instance.terminate().catch(() => undefined);
|
|
1253
1407
|
disposeRpcStub(instance);
|
|
1254
1408
|
return null;
|
|
@@ -1260,6 +1414,11 @@ async function submitViaPooledWorkflow(input: {
|
|
|
1260
1414
|
payload: buildDispatcherEnvelope(input.params),
|
|
1261
1415
|
});
|
|
1262
1416
|
} catch (error) {
|
|
1417
|
+
await blockWorkflowPoolRun({
|
|
1418
|
+
env: input.env,
|
|
1419
|
+
runId: input.params.runId,
|
|
1420
|
+
instanceId: pooledInstanceId,
|
|
1421
|
+
}).catch(() => undefined);
|
|
1263
1422
|
disposeRpcStub(instance);
|
|
1264
1423
|
console.warn('[coordinator.workflow_pool] sendEvent failed; falling back', {
|
|
1265
1424
|
runId: input.params.runId,
|
|
@@ -1274,7 +1433,63 @@ async function submitViaPooledWorkflow(input: {
|
|
|
1274
1433
|
graphHash: input.params.graphHash ?? null,
|
|
1275
1434
|
extra: { instanceId: pooledInstanceId },
|
|
1276
1435
|
});
|
|
1277
|
-
|
|
1436
|
+
const ack = await waitForWorkflowPoolStartAck({
|
|
1437
|
+
env: input.env,
|
|
1438
|
+
runId: input.params.runId,
|
|
1439
|
+
instanceId: pooledInstanceId,
|
|
1440
|
+
timeoutMs: WORKFLOW_POOL_START_ACK_TIMEOUT_MS,
|
|
1441
|
+
});
|
|
1442
|
+
if (ack.acknowledged) {
|
|
1443
|
+
input.recordSubmitTiming({
|
|
1444
|
+
phase: 'coordinator.workflow_pool_start_ack',
|
|
1445
|
+
ms: ack.ms,
|
|
1446
|
+
graphHash: input.params.graphHash ?? null,
|
|
1447
|
+
extra: {
|
|
1448
|
+
acknowledged: true,
|
|
1449
|
+
instanceId: pooledInstanceId,
|
|
1450
|
+
polls: ack.polls,
|
|
1451
|
+
startedAt: ack.startedAt,
|
|
1452
|
+
},
|
|
1453
|
+
});
|
|
1454
|
+
return instance;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
const blockStartedAt = Date.now();
|
|
1458
|
+
const block = await blockWorkflowPoolRun({
|
|
1459
|
+
env: input.env,
|
|
1460
|
+
runId: input.params.runId,
|
|
1461
|
+
instanceId: pooledInstanceId,
|
|
1462
|
+
}).catch(() => ({ blocked: false, started: false }));
|
|
1463
|
+
input.recordSubmitTiming({
|
|
1464
|
+
phase: 'coordinator.workflow_pool_start_ack',
|
|
1465
|
+
ms: ack.ms,
|
|
1466
|
+
graphHash: input.params.graphHash ?? null,
|
|
1467
|
+
extra: {
|
|
1468
|
+
acknowledged: block.started,
|
|
1469
|
+
instanceId: pooledInstanceId,
|
|
1470
|
+
polls: ack.polls,
|
|
1471
|
+
startedAt: ack.startedAt,
|
|
1472
|
+
mappedInstanceId: ack.mappedInstanceId,
|
|
1473
|
+
blocked: block.blocked,
|
|
1474
|
+
blockMs: Date.now() - blockStartedAt,
|
|
1475
|
+
},
|
|
1476
|
+
});
|
|
1477
|
+
if (block.started) {
|
|
1478
|
+
return instance;
|
|
1479
|
+
}
|
|
1480
|
+
await instance.terminate().catch(() => undefined);
|
|
1481
|
+
disposeRpcStub(instance);
|
|
1482
|
+
input.recordSubmitTiming({
|
|
1483
|
+
phase: 'coordinator.workflow_pool_fallback',
|
|
1484
|
+
ms: Date.now() - sendStartedAt,
|
|
1485
|
+
graphHash: input.params.graphHash ?? null,
|
|
1486
|
+
extra: {
|
|
1487
|
+
reason: 'start_ack_timeout',
|
|
1488
|
+
instanceId: pooledInstanceId,
|
|
1489
|
+
ackTimeoutMs: WORKFLOW_POOL_START_ACK_TIMEOUT_MS,
|
|
1490
|
+
},
|
|
1491
|
+
});
|
|
1492
|
+
return null;
|
|
1278
1493
|
}
|
|
1279
1494
|
|
|
1280
1495
|
function readWorkflowPayload(event: unknown): Record<string, unknown> | null {
|
|
@@ -1311,16 +1526,21 @@ async function markWorkflowRuntimeFailure(input: {
|
|
|
1311
1526
|
});
|
|
1312
1527
|
const bypass = input.env.VERCEL_PROTECTION_BYPASS_TOKEN?.trim();
|
|
1313
1528
|
if (bypass) headers.set('x-vercel-protection-bypass', bypass);
|
|
1314
|
-
const body = JSON.stringify(
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1529
|
+
const body = JSON.stringify({
|
|
1530
|
+
action: 'append_run_events',
|
|
1531
|
+
playId: runId,
|
|
1532
|
+
events: [
|
|
1533
|
+
{
|
|
1534
|
+
type: 'run.failed',
|
|
1535
|
+
runId,
|
|
1536
|
+
source: 'coordinator',
|
|
1537
|
+
occurredAt: Date.now(),
|
|
1538
|
+
error: `DynamicWorkflow runner failed: ${errorName}: ${errorMessage}${
|
|
1539
|
+
errorStack ? `\n${errorStack}` : ''
|
|
1540
|
+
}`,
|
|
1541
|
+
} satisfies PlayRunLedgerEvent,
|
|
1542
|
+
],
|
|
1543
|
+
});
|
|
1324
1544
|
const url = `${baseUrl.replace(/\/$/, '')}/api/v2/plays/internal/runtime`;
|
|
1325
1545
|
const backoffMs = [200, 500, 1500];
|
|
1326
1546
|
let lastError: unknown = null;
|
|
@@ -1621,6 +1841,15 @@ function buildChildWorkflowParams(input: {
|
|
|
1621
1841
|
function runRequestFromPlayWorkflowParams(
|
|
1622
1842
|
params: PlayWorkflowParams,
|
|
1623
1843
|
): Record<string, unknown> {
|
|
1844
|
+
const inputFileName = String(
|
|
1845
|
+
params.inputFile?.name ??
|
|
1846
|
+
params.inputFile?.fileName ??
|
|
1847
|
+
params.inputFile?.logicalPath ??
|
|
1848
|
+
params.inputFile?.path ??
|
|
1849
|
+
'',
|
|
1850
|
+
);
|
|
1851
|
+
const inputStorageKey =
|
|
1852
|
+
params.inputFile?.r2Key ?? params.inputFile?.storageKey ?? null;
|
|
1624
1853
|
return {
|
|
1625
1854
|
runId: params.runId,
|
|
1626
1855
|
callbackUrl: params.baseUrl,
|
|
@@ -1632,12 +1861,22 @@ function runRequestFromPlayWorkflowParams(
|
|
|
1632
1861
|
userEmail: params.userEmail,
|
|
1633
1862
|
runtimeInput: params.input,
|
|
1634
1863
|
inlineCsv: params.inlineCsv ?? null,
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
(params.inputFile.name || params.inputFile.path)
|
|
1864
|
+
inputFiles:
|
|
1865
|
+
inputStorageKey && inputFileName
|
|
1638
1866
|
? {
|
|
1639
|
-
[
|
|
1640
|
-
|
|
1867
|
+
[inputFileName]: {
|
|
1868
|
+
logicalPath:
|
|
1869
|
+
params.inputFile?.logicalPath ??
|
|
1870
|
+
params.inputFile?.path ??
|
|
1871
|
+
inputFileName,
|
|
1872
|
+
fileName: params.inputFile?.fileName ?? inputFileName,
|
|
1873
|
+
storageKey: inputStorageKey,
|
|
1874
|
+
contentType: params.inputFile?.contentType ?? null,
|
|
1875
|
+
bytes:
|
|
1876
|
+
typeof params.inputFile?.bytes === 'number'
|
|
1877
|
+
? params.inputFile.bytes
|
|
1878
|
+
: null,
|
|
1879
|
+
},
|
|
1641
1880
|
}
|
|
1642
1881
|
: null,
|
|
1643
1882
|
packagedFiles: params.packagedFiles ?? null,
|
|
@@ -1996,13 +2235,6 @@ export class RuntimeApi extends WorkerEntrypoint<CoordinatorEnv, undefined> {
|
|
|
1996
2235
|
? this.env.DEEPLINE_API_BASE_URL.trim()
|
|
1997
2236
|
: 'https://code.deepline.com';
|
|
1998
2237
|
const target = new URL(incoming.pathname + incoming.search, apiBaseUrl);
|
|
1999
|
-
const runtimeStatusBody =
|
|
2000
|
-
incoming.pathname === '/api/v2/plays/internal/runtime'
|
|
2001
|
-
? await request
|
|
2002
|
-
.clone()
|
|
2003
|
-
.json()
|
|
2004
|
-
.catch(() => null)
|
|
2005
|
-
: null;
|
|
2006
2238
|
const forwarded = new Request(target.toString(), request);
|
|
2007
2239
|
const bypassToken = this.env.VERCEL_PROTECTION_BYPASS_TOKEN;
|
|
2008
2240
|
if (typeof bypassToken === 'string' && bypassToken) {
|
|
@@ -2018,41 +2250,9 @@ export class RuntimeApi extends WorkerEntrypoint<CoordinatorEnv, undefined> {
|
|
|
2018
2250
|
`[RUNTIME_API] ${incoming.pathname} failed: status=${res.status} ` +
|
|
2019
2251
|
`target=${target.toString()} body=${body.slice(0, 500)}`,
|
|
2020
2252
|
);
|
|
2021
|
-
} else {
|
|
2022
|
-
await this.recordRuntimeStatusEvent(runtimeStatusBody).catch(() => null);
|
|
2023
2253
|
}
|
|
2024
2254
|
return res;
|
|
2025
2255
|
}
|
|
2026
|
-
|
|
2027
|
-
private async recordRuntimeStatusEvent(body: unknown): Promise<void> {
|
|
2028
|
-
if (!isRecord(body) || body.action !== 'update_run_status') {
|
|
2029
|
-
return;
|
|
2030
|
-
}
|
|
2031
|
-
const runId = typeof body.playId === 'string' ? body.playId : '';
|
|
2032
|
-
const status = typeof body.status === 'string' ? body.status : '';
|
|
2033
|
-
if (!runId || !status) {
|
|
2034
|
-
return;
|
|
2035
|
-
}
|
|
2036
|
-
await appendCoordinatorRunEvent(this.env, {
|
|
2037
|
-
runId,
|
|
2038
|
-
type: 'progress',
|
|
2039
|
-
status,
|
|
2040
|
-
ts: Date.now(),
|
|
2041
|
-
logs: sanitizeLiveLogLines(body.liveLogs) ?? undefined,
|
|
2042
|
-
activeNodeId:
|
|
2043
|
-
typeof body.activeNodeId === 'string' ? body.activeNodeId : null,
|
|
2044
|
-
activeArtifactTableNamespace:
|
|
2045
|
-
typeof body.activeArtifactTableNamespace === 'string'
|
|
2046
|
-
? body.activeArtifactTableNamespace
|
|
2047
|
-
: null,
|
|
2048
|
-
updatedAt:
|
|
2049
|
-
typeof body.lastCheckpointAt === 'number'
|
|
2050
|
-
? body.lastCheckpointAt
|
|
2051
|
-
: null,
|
|
2052
|
-
liveNodeProgress:
|
|
2053
|
-
body.liveNodeProgress !== undefined ? body.liveNodeProgress : undefined,
|
|
2054
|
-
});
|
|
2055
|
-
}
|
|
2056
2256
|
}
|
|
2057
2257
|
|
|
2058
2258
|
export class CoordinatorControl extends WorkerEntrypoint<
|
|
@@ -2188,9 +2388,35 @@ export class DynamicWorkflow extends WorkflowEntrypoint<
|
|
|
2188
2388
|
dispatchedEvent = {
|
|
2189
2389
|
payload: startEvent.payload,
|
|
2190
2390
|
timestamp: startEvent.timestamp,
|
|
2191
|
-
instanceId: workflowEvent.instanceId,
|
|
2391
|
+
instanceId: workflowEvent.instanceId ?? pooledPayload.poolId,
|
|
2192
2392
|
};
|
|
2193
2393
|
const dispatchedTrace = readWorkflowTraceContext(dispatchedEvent);
|
|
2394
|
+
const mapped = await mapRunToWorkflowInstance({
|
|
2395
|
+
env: this.env,
|
|
2396
|
+
runId: dispatchedTrace.runId,
|
|
2397
|
+
instanceId: pooledPayload.poolId,
|
|
2398
|
+
started: true,
|
|
2399
|
+
}).catch((error) => {
|
|
2400
|
+
console.warn('[coordinator.workflow_pool] start ack failed', {
|
|
2401
|
+
poolId: pooledPayload.poolId,
|
|
2402
|
+
runId: dispatchedTrace.runId,
|
|
2403
|
+
message: error instanceof Error ? error.message : String(error),
|
|
2404
|
+
});
|
|
2405
|
+
return false;
|
|
2406
|
+
});
|
|
2407
|
+
if (!mapped) {
|
|
2408
|
+
trace({
|
|
2409
|
+
runId: dispatchedTrace.runId,
|
|
2410
|
+
phase: 'coordinator.workflow_pool_start_blocked',
|
|
2411
|
+
ms: 0,
|
|
2412
|
+
graphHash: dispatchedTrace.graphHash,
|
|
2413
|
+
extra: {
|
|
2414
|
+
instanceId: pooledPayload.poolId,
|
|
2415
|
+
eventType: startEvent.type,
|
|
2416
|
+
},
|
|
2417
|
+
});
|
|
2418
|
+
return { ok: false, blocked: true, runId: dispatchedTrace.runId };
|
|
2419
|
+
}
|
|
2194
2420
|
const eventDeliveryMs = Math.max(
|
|
2195
2421
|
0,
|
|
2196
2422
|
Date.now() - startEvent.timestamp.getTime(),
|
|
@@ -2474,7 +2700,15 @@ const coordinatorEntrypoint = {
|
|
|
2474
2700
|
const entries = await listWorkflowPoolEntries(env);
|
|
2475
2701
|
const detailed = [];
|
|
2476
2702
|
for (const entry of entries) {
|
|
2477
|
-
const instance = await env
|
|
2703
|
+
const instance = await getWorkflowPoolInstance(env, entry.id);
|
|
2704
|
+
if (!instance) {
|
|
2705
|
+
detailed.push({
|
|
2706
|
+
...entry,
|
|
2707
|
+
status: 'missing',
|
|
2708
|
+
mappedStatus: 'failed',
|
|
2709
|
+
});
|
|
2710
|
+
continue;
|
|
2711
|
+
}
|
|
2478
2712
|
try {
|
|
2479
2713
|
const status = await instance.status().catch(() => null);
|
|
2480
2714
|
detailed.push({
|
|
@@ -2538,6 +2772,14 @@ const coordinatorEntrypoint = {
|
|
|
2538
2772
|
async tail(events: unknown[], env: CoordinatorEnv): Promise<void> {
|
|
2539
2773
|
await flushTailRunLogs(events, env);
|
|
2540
2774
|
},
|
|
2775
|
+
async scheduled(
|
|
2776
|
+
_controller: unknown,
|
|
2777
|
+
env: CoordinatorEnv,
|
|
2778
|
+
ctx?: ExecutionContext,
|
|
2779
|
+
): Promise<void> {
|
|
2780
|
+
if (!workflowPoolEnabled()) return;
|
|
2781
|
+
ctx?.waitUntil(refillWorkflowPool(env).catch(() => undefined));
|
|
2782
|
+
},
|
|
2541
2783
|
};
|
|
2542
2784
|
|
|
2543
2785
|
export default coordinatorEntrypoint;
|
|
@@ -2700,36 +2942,100 @@ async function handleWorkflowRoute(input: {
|
|
|
2700
2942
|
'Start apps/play-harness-worker before the coordinator or fix wrangler.toml services.',
|
|
2701
2943
|
);
|
|
2702
2944
|
}
|
|
2945
|
+
const preloadedDbSessions = Array.isArray(params.preloadedDbSessions)
|
|
2946
|
+
? params.preloadedDbSessions
|
|
2947
|
+
: [];
|
|
2948
|
+
if (preloadedDbSessions.length > 0 && params.executorToken) {
|
|
2949
|
+
const prewarmStartedAt = Date.now();
|
|
2950
|
+
recordSubmitTiming({
|
|
2951
|
+
phase: 'coordinator.harness_prewarm_postgres_start',
|
|
2952
|
+
ms: 0,
|
|
2953
|
+
graphHash: params.graphHash ?? null,
|
|
2954
|
+
extra: { sessions: preloadedDbSessions.length },
|
|
2955
|
+
});
|
|
2956
|
+
const prewarmPromise = env.HARNESS.prewarmPostgresSessions({
|
|
2957
|
+
executorToken: params.executorToken,
|
|
2958
|
+
sessions: preloadedDbSessions,
|
|
2959
|
+
})
|
|
2960
|
+
.then((result) => {
|
|
2961
|
+
recordSubmitTiming({
|
|
2962
|
+
phase: 'coordinator.harness_prewarm_postgres',
|
|
2963
|
+
ms: Date.now() - prewarmStartedAt,
|
|
2964
|
+
graphHash: params.graphHash ?? null,
|
|
2965
|
+
extra: {
|
|
2966
|
+
status: 'ok',
|
|
2967
|
+
sessions: result.sessions,
|
|
2968
|
+
},
|
|
2969
|
+
});
|
|
2970
|
+
})
|
|
2971
|
+
.catch((error: unknown) => {
|
|
2972
|
+
recordSubmitTiming({
|
|
2973
|
+
phase: 'coordinator.harness_prewarm_postgres',
|
|
2974
|
+
ms: Date.now() - prewarmStartedAt,
|
|
2975
|
+
graphHash: params.graphHash ?? null,
|
|
2976
|
+
extra: {
|
|
2977
|
+
status: 'failed',
|
|
2978
|
+
sessions: preloadedDbSessions.length,
|
|
2979
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2980
|
+
},
|
|
2981
|
+
});
|
|
2982
|
+
});
|
|
2983
|
+
input.ctx?.waitUntil(prewarmPromise);
|
|
2984
|
+
}
|
|
2703
2985
|
let instance: WorkflowInstance | null = null;
|
|
2704
2986
|
try {
|
|
2705
|
-
const
|
|
2987
|
+
const statusEventStartedAt = Date.now();
|
|
2988
|
+
await appendCoordinatorRunEvent(env, {
|
|
2989
|
+
runId: submittedRunId,
|
|
2990
|
+
type: 'status',
|
|
2991
|
+
status: 'running',
|
|
2992
|
+
ts: Date.now(),
|
|
2993
|
+
});
|
|
2706
2994
|
recordSubmitTiming({
|
|
2707
|
-
phase: 'coordinator.
|
|
2708
|
-
ms:
|
|
2995
|
+
phase: 'coordinator.submit_status_event',
|
|
2996
|
+
ms: Date.now() - statusEventStartedAt,
|
|
2709
2997
|
graphHash: params.graphHash ?? null,
|
|
2710
|
-
extra: {
|
|
2711
|
-
usedPool: false,
|
|
2712
|
-
disabled: true,
|
|
2713
|
-
reason: WORKFLOW_POOL_DISABLED_REASON,
|
|
2714
|
-
},
|
|
2715
2998
|
});
|
|
2716
|
-
const
|
|
2717
|
-
|
|
2999
|
+
const dispatchStartedAt = Date.now();
|
|
3000
|
+
const poolAttemptStartedAt = Date.now();
|
|
3001
|
+
instance = await submitViaPooledWorkflow({
|
|
2718
3002
|
env,
|
|
2719
|
-
id: defaultInstanceId,
|
|
2720
3003
|
params,
|
|
3004
|
+
recordSubmitTiming,
|
|
2721
3005
|
});
|
|
2722
3006
|
recordSubmitTiming({
|
|
2723
|
-
phase: 'coordinator.
|
|
2724
|
-
ms: Date.now() -
|
|
3007
|
+
phase: 'coordinator.workflow_pool_attempt',
|
|
3008
|
+
ms: Date.now() - poolAttemptStartedAt,
|
|
2725
3009
|
graphHash: params.graphHash ?? null,
|
|
2726
|
-
extra: {
|
|
3010
|
+
extra: {
|
|
3011
|
+
usedPool: Boolean(instance),
|
|
3012
|
+
enabled: workflowPoolEnabled(),
|
|
3013
|
+
},
|
|
2727
3014
|
});
|
|
3015
|
+
if (!instance) {
|
|
3016
|
+
const createStartedAt = Date.now();
|
|
3017
|
+
instance = await createDynamicWorkflowInstance({
|
|
3018
|
+
env,
|
|
3019
|
+
id: defaultInstanceId,
|
|
3020
|
+
params,
|
|
3021
|
+
});
|
|
3022
|
+
recordSubmitTiming({
|
|
3023
|
+
phase: 'coordinator.workflow_create',
|
|
3024
|
+
ms: Date.now() - createStartedAt,
|
|
3025
|
+
graphHash: params.graphHash ?? null,
|
|
3026
|
+
extra: { instanceId: instance.id },
|
|
3027
|
+
});
|
|
3028
|
+
}
|
|
2728
3029
|
recordSubmitTiming({
|
|
2729
3030
|
phase: 'coordinator.dispatch_workflow',
|
|
2730
3031
|
ms: Date.now() - dispatchStartedAt,
|
|
2731
3032
|
graphHash: params.graphHash ?? null,
|
|
2732
|
-
extra: {
|
|
3033
|
+
extra: {
|
|
3034
|
+
startMode:
|
|
3035
|
+
instance.id === defaultInstanceId
|
|
3036
|
+
? 'direct_workflow_create'
|
|
3037
|
+
: 'pooled_workflow_start_event',
|
|
3038
|
+
},
|
|
2733
3039
|
});
|
|
2734
3040
|
const initialWaitMsRaw = Number(
|
|
2735
3041
|
new URL(request.url).searchParams.get('initialWaitMs') ?? '0',
|
|
@@ -2763,6 +3069,9 @@ async function handleWorkflowRoute(input: {
|
|
|
2763
3069
|
ms: totalMs,
|
|
2764
3070
|
graphHash: params.graphHash ?? null,
|
|
2765
3071
|
});
|
|
3072
|
+
if (workflowPoolEnabled() && instance.id === defaultInstanceId) {
|
|
3073
|
+
input.ctx?.waitUntil(refillWorkflowPool(env).catch(() => undefined));
|
|
3074
|
+
}
|
|
2766
3075
|
return Response.json({
|
|
2767
3076
|
runId,
|
|
2768
3077
|
status: 'submitted',
|
|
@@ -3018,11 +3327,19 @@ async function handleWorkflowRoute(input: {
|
|
|
3018
3327
|
afterSeq,
|
|
3019
3328
|
timeoutMs: waitMs,
|
|
3020
3329
|
}).catch(() => null);
|
|
3330
|
+
const rawEvents = eventResult?.events ?? [];
|
|
3331
|
+
const terminalEventIndex = rawEvents.findIndex(
|
|
3332
|
+
(event) => event.type === 'terminal',
|
|
3333
|
+
);
|
|
3334
|
+
const events =
|
|
3335
|
+
terminalEventIndex >= 0
|
|
3336
|
+
? rawEvents.slice(0, terminalEventIndex + 1)
|
|
3337
|
+
: rawEvents;
|
|
3021
3338
|
const coordinatorTrace =
|
|
3022
|
-
includeTrace &&
|
|
3339
|
+
includeTrace && events.length
|
|
3023
3340
|
? await listCoordinatorPerfTrace(env, runId).catch(() => [])
|
|
3024
3341
|
: [];
|
|
3025
|
-
const terminalEvent =
|
|
3342
|
+
const terminalEvent = events.find(
|
|
3026
3343
|
(event): event is Extract<CoordinatorRunEvent, { type: 'terminal' }> =>
|
|
3027
3344
|
event.type === 'terminal',
|
|
3028
3345
|
);
|
|
@@ -3038,7 +3355,7 @@ async function handleWorkflowRoute(input: {
|
|
|
3038
3355
|
completedAt: terminalEvent.ts,
|
|
3039
3356
|
liveLogs: sanitizeLiveLogLines(terminalEvent.liveLogs),
|
|
3040
3357
|
liveNodeProgress: terminalEvent.liveNodeProgress ?? null,
|
|
3041
|
-
events
|
|
3358
|
+
events,
|
|
3042
3359
|
latestSeq: eventResult?.latestSeq ?? afterSeq,
|
|
3043
3360
|
wait: null,
|
|
3044
3361
|
coordinatorObserve: {
|
|
@@ -3054,7 +3371,7 @@ async function handleWorkflowRoute(input: {
|
|
|
3054
3371
|
return Response.json({
|
|
3055
3372
|
runId,
|
|
3056
3373
|
status: 'running',
|
|
3057
|
-
events
|
|
3374
|
+
events,
|
|
3058
3375
|
latestSeq: eventResult?.latestSeq ?? afterSeq,
|
|
3059
3376
|
wait: null,
|
|
3060
3377
|
coordinatorObserve: {
|
|
@@ -3252,7 +3569,7 @@ function stableHash(value: string): string {
|
|
|
3252
3569
|
return (hash >>> 0).toString(36);
|
|
3253
3570
|
}
|
|
3254
3571
|
|
|
3255
|
-
const DYNAMIC_PLAY_WORKER_HARNESS_VERSION = '
|
|
3572
|
+
const DYNAMIC_PLAY_WORKER_HARNESS_VERSION = 'h16-coordinator-only-prewarm';
|
|
3256
3573
|
const DYNAMIC_WORKER_BUNDLED_CODE_CACHE_MAX_ENTRIES = 64;
|
|
3257
3574
|
const dynamicWorkerBundledCodeCache = new Map<string, string>();
|
|
3258
3575
|
|
|
@@ -3548,6 +3865,14 @@ function normalizePackagedFiles(
|
|
|
3548
3865
|
.map((entry) => ({
|
|
3549
3866
|
playPath: String(entry.playPath ?? '').replace(/^\.\//, ''),
|
|
3550
3867
|
storageKey: String(entry.storageKey ?? ''),
|
|
3868
|
+
contentType:
|
|
3869
|
+
typeof entry.contentType === 'string' ? entry.contentType : undefined,
|
|
3870
|
+
bytes:
|
|
3871
|
+
typeof entry.bytes === 'number' &&
|
|
3872
|
+
Number.isSafeInteger(entry.bytes) &&
|
|
3873
|
+
entry.bytes >= 0
|
|
3874
|
+
? entry.bytes
|
|
3875
|
+
: undefined,
|
|
3551
3876
|
inlineText:
|
|
3552
3877
|
typeof entry.inlineText === 'string' ? entry.inlineText : undefined,
|
|
3553
3878
|
}))
|
|
@@ -4062,8 +4387,13 @@ function mapWorkflowResult(
|
|
|
4062
4387
|
runId: string,
|
|
4063
4388
|
status: InstanceStatus,
|
|
4064
4389
|
): Record<string, unknown> {
|
|
4065
|
-
const
|
|
4066
|
-
const mapped = resolveTerminalStatus(status,
|
|
4390
|
+
const rawError = readWorkflowError(status);
|
|
4391
|
+
const mapped = resolveTerminalStatus(status, rawError);
|
|
4392
|
+
const error =
|
|
4393
|
+
rawError ??
|
|
4394
|
+
(mapped === 'failed'
|
|
4395
|
+
? `Cloudflare workflow reported ${String(status.status ?? 'failed')} without an error payload.`
|
|
4396
|
+
: null);
|
|
4067
4397
|
const output =
|
|
4068
4398
|
status.output && typeof status.output === 'object'
|
|
4069
4399
|
? (status.output as Record<string, unknown>)
|
|
@@ -4131,9 +4461,11 @@ function resolveTerminalStatus(
|
|
|
4131
4461
|
function readWorkflowError(status: InstanceStatus): string | null {
|
|
4132
4462
|
const error = status.error as unknown;
|
|
4133
4463
|
if (!error) return null;
|
|
4134
|
-
if (typeof error === 'string') return error;
|
|
4464
|
+
if (typeof error === 'string') return error.trim() || null;
|
|
4135
4465
|
if (typeof error === 'object' && 'message' in error) {
|
|
4136
|
-
|
|
4466
|
+
const message = String((error as { message?: unknown }).message ?? '');
|
|
4467
|
+
return message.trim() || null;
|
|
4137
4468
|
}
|
|
4138
|
-
|
|
4469
|
+
const message = String(error);
|
|
4470
|
+
return message.trim() || null;
|
|
4139
4471
|
}
|