deepline 0.1.19 → 0.1.21
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 +391 -129
- package/dist/cli/index.mjs +391 -129
- package/dist/index.d.mts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +53 -7
- package/dist/index.mjs +53 -7
- package/dist/repo/apps/play-runner-workers/src/coordinator-entry.ts +999 -257
- package/dist/repo/apps/play-runner-workers/src/dedup-do.ts +604 -75
- package/dist/repo/apps/play-runner-workers/src/entry.ts +442 -357
- package/dist/repo/sdk/src/client.ts +46 -4
- package/dist/repo/sdk/src/http.ts +38 -4
- package/dist/repo/sdk/src/plays/harness-stub.ts +12 -0
- package/dist/repo/sdk/src/version.ts +1 -1
- package/dist/repo/shared_libs/play-runtime/scheduler-backend.ts +3 -6
- package/dist/repo/shared_libs/plays/row-identity.ts +59 -4
- package/package.json +1 -1
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
} from '@cloudflare/dynamic-workflows';
|
|
24
24
|
import type { ExecutionPlan } from '../../../shared_libs/play-runtime/execution-plan';
|
|
25
25
|
import type { PlayCallGovernanceSnapshot } from '../../../shared_libs/play-runtime/scheduler-backend';
|
|
26
|
+
import type { PreloadedRuntimeDbSession } from '../../../shared_libs/play-runtime/db-session';
|
|
26
27
|
import type {
|
|
27
28
|
PlayRuntimeManifest,
|
|
28
29
|
PlayRuntimeManifestMap,
|
|
@@ -54,6 +55,7 @@ export type PlayWorkflowParams = {
|
|
|
54
55
|
executionPlan?: ExecutionPlan | null;
|
|
55
56
|
childPlayManifests?: PlayRuntimeManifestMap | null;
|
|
56
57
|
playCallGovernance?: PlayCallGovernanceSnapshot | null;
|
|
58
|
+
preloadedDbSessions?: PreloadedRuntimeDbSession[] | null;
|
|
57
59
|
dynamicWorkerCode?: string | null;
|
|
58
60
|
executorToken: string;
|
|
59
61
|
baseUrl: string;
|
|
@@ -115,6 +117,57 @@ type CoordinatorPerfTraceInput = {
|
|
|
115
117
|
|
|
116
118
|
type CoordinatorPerfTraceSink = (event: CoordinatorPerfTraceInput) => void;
|
|
117
119
|
|
|
120
|
+
type CoordinatorTerminalState = {
|
|
121
|
+
runId: string;
|
|
122
|
+
status: 'completed' | 'failed' | 'cancelled';
|
|
123
|
+
result?: unknown;
|
|
124
|
+
error?: string | null;
|
|
125
|
+
totalRows?: unknown;
|
|
126
|
+
durationMs?: unknown;
|
|
127
|
+
playName?: string | null;
|
|
128
|
+
completedAt?: number;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
type CoordinatorRunEvent =
|
|
132
|
+
| {
|
|
133
|
+
seq?: number;
|
|
134
|
+
runId: string;
|
|
135
|
+
type: 'status';
|
|
136
|
+
status: string;
|
|
137
|
+
ts: number;
|
|
138
|
+
logs?: string[];
|
|
139
|
+
}
|
|
140
|
+
| {
|
|
141
|
+
seq?: number;
|
|
142
|
+
runId: string;
|
|
143
|
+
type: 'log';
|
|
144
|
+
line: string;
|
|
145
|
+
ts: number;
|
|
146
|
+
}
|
|
147
|
+
| {
|
|
148
|
+
seq?: number;
|
|
149
|
+
runId: string;
|
|
150
|
+
type: 'progress';
|
|
151
|
+
status: string;
|
|
152
|
+
ts: number;
|
|
153
|
+
logs?: string[];
|
|
154
|
+
activeNodeId?: string | null;
|
|
155
|
+
activeArtifactTableNamespace?: string | null;
|
|
156
|
+
updatedAt?: number | null;
|
|
157
|
+
}
|
|
158
|
+
| {
|
|
159
|
+
seq?: number;
|
|
160
|
+
runId: string;
|
|
161
|
+
type: 'terminal';
|
|
162
|
+
status: 'completed' | 'failed' | 'cancelled';
|
|
163
|
+
ts: number;
|
|
164
|
+
result?: unknown;
|
|
165
|
+
error?: string | null;
|
|
166
|
+
totalRows?: unknown;
|
|
167
|
+
durationMs?: unknown;
|
|
168
|
+
playName?: string | null;
|
|
169
|
+
};
|
|
170
|
+
|
|
118
171
|
type InlineWorkerRunResponse = {
|
|
119
172
|
status?: 'completed' | 'failed';
|
|
120
173
|
result?: unknown;
|
|
@@ -156,6 +209,7 @@ interface CoordinatorEnv {
|
|
|
156
209
|
DEEPLINE_API_BASE_URL: string;
|
|
157
210
|
DEEPLINE_INTERNAL_TOKEN?: string;
|
|
158
211
|
DEEPLINE_TAIL_LOG_TOKEN?: string;
|
|
212
|
+
DEEPLINE_COORDINATOR_DEPLOY_MARKER?: string;
|
|
159
213
|
VERCEL_PROTECTION_BYPASS_TOKEN?: string;
|
|
160
214
|
DEEPLINE_PLAY_PREVIEW_SLUG?: string;
|
|
161
215
|
/**
|
|
@@ -175,7 +229,12 @@ interface CoordinatorEnv {
|
|
|
175
229
|
HARNESS?: import('../../play-harness-worker/src/rpc-types').PlayHarnessRpc;
|
|
176
230
|
}
|
|
177
231
|
|
|
178
|
-
const WORKFLOW_READ_ONLY_ACTIONS = new Set([
|
|
232
|
+
const WORKFLOW_READ_ONLY_ACTIONS = new Set([
|
|
233
|
+
'',
|
|
234
|
+
'result',
|
|
235
|
+
'status',
|
|
236
|
+
'tail',
|
|
237
|
+
]);
|
|
179
238
|
|
|
180
239
|
function authorizeCoordinatorControlRequest(input: {
|
|
181
240
|
request: Request;
|
|
@@ -326,6 +385,106 @@ async function listCoordinatorPerfTrace(
|
|
|
326
385
|
);
|
|
327
386
|
}
|
|
328
387
|
|
|
388
|
+
async function writeCoordinatorTerminalState(
|
|
389
|
+
env: CoordinatorEnv,
|
|
390
|
+
state: CoordinatorTerminalState,
|
|
391
|
+
): Promise<void> {
|
|
392
|
+
const stub = env.PLAY_DEDUP.get(env.PLAY_DEDUP.idFromName(state.runId));
|
|
393
|
+
const response = await stub.fetch(
|
|
394
|
+
'https://deepline.dedup.internal/terminal-set',
|
|
395
|
+
{
|
|
396
|
+
method: 'POST',
|
|
397
|
+
headers: { 'content-type': 'application/json' },
|
|
398
|
+
body: JSON.stringify({
|
|
399
|
+
...state,
|
|
400
|
+
completedAt: state.completedAt ?? Date.now(),
|
|
401
|
+
}),
|
|
402
|
+
},
|
|
403
|
+
);
|
|
404
|
+
if (!response.ok) {
|
|
405
|
+
throw new Error(`coordinator terminal set failed ${response.status}`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async function appendCoordinatorRunEvent(
|
|
410
|
+
env: CoordinatorEnv,
|
|
411
|
+
event: CoordinatorRunEvent,
|
|
412
|
+
): Promise<void> {
|
|
413
|
+
const stub = env.PLAY_DEDUP.get(env.PLAY_DEDUP.idFromName(event.runId));
|
|
414
|
+
const response = await stub.fetch('https://deepline.dedup.internal/event-add', {
|
|
415
|
+
method: 'POST',
|
|
416
|
+
headers: { 'content-type': 'application/json' },
|
|
417
|
+
body: JSON.stringify(event),
|
|
418
|
+
});
|
|
419
|
+
if (!response.ok) {
|
|
420
|
+
throw new Error(`coordinator event append failed ${response.status}`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async function listCoordinatorRunEvents(input: {
|
|
425
|
+
env: CoordinatorEnv;
|
|
426
|
+
runId: string;
|
|
427
|
+
afterSeq: number;
|
|
428
|
+
timeoutMs: number;
|
|
429
|
+
}): Promise<{ events: CoordinatorRunEvent[]; latestSeq: number }> {
|
|
430
|
+
const stub = input.env.PLAY_DEDUP.get(
|
|
431
|
+
input.env.PLAY_DEDUP.idFromName(input.runId),
|
|
432
|
+
);
|
|
433
|
+
const response = await stub.fetch(
|
|
434
|
+
`https://deepline.dedup.internal/event-list?afterSeq=${encodeURIComponent(
|
|
435
|
+
String(Math.max(0, Math.floor(input.afterSeq))),
|
|
436
|
+
)}&timeoutMs=${encodeURIComponent(
|
|
437
|
+
String(Math.max(0, Math.floor(input.timeoutMs))),
|
|
438
|
+
)}`,
|
|
439
|
+
);
|
|
440
|
+
if (!response.ok) {
|
|
441
|
+
throw new Error(`coordinator event list failed ${response.status}`);
|
|
442
|
+
}
|
|
443
|
+
const body = (await response.json().catch(() => ({}))) as {
|
|
444
|
+
events?: unknown;
|
|
445
|
+
latestSeq?: unknown;
|
|
446
|
+
};
|
|
447
|
+
return {
|
|
448
|
+
events: Array.isArray(body.events)
|
|
449
|
+
? body.events.filter(
|
|
450
|
+
(event): event is CoordinatorRunEvent =>
|
|
451
|
+
isRecord(event) &&
|
|
452
|
+
typeof event.runId === 'string' &&
|
|
453
|
+
event.runId === input.runId &&
|
|
454
|
+
typeof event.type === 'string' &&
|
|
455
|
+
typeof event.ts === 'number',
|
|
456
|
+
)
|
|
457
|
+
: [],
|
|
458
|
+
latestSeq: typeof body.latestSeq === 'number' ? body.latestSeq : input.afterSeq,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async function readCoordinatorTerminalState(
|
|
463
|
+
env: CoordinatorEnv,
|
|
464
|
+
runId: string,
|
|
465
|
+
): Promise<CoordinatorTerminalState | null> {
|
|
466
|
+
const stub = env.PLAY_DEDUP.get(env.PLAY_DEDUP.idFromName(runId));
|
|
467
|
+
const response = await stub.fetch(
|
|
468
|
+
'https://deepline.dedup.internal/terminal-get',
|
|
469
|
+
);
|
|
470
|
+
if (!response.ok) {
|
|
471
|
+
throw new Error(`coordinator terminal get failed ${response.status}`);
|
|
472
|
+
}
|
|
473
|
+
const body = (await response.json().catch(() => ({}))) as {
|
|
474
|
+
state?: unknown;
|
|
475
|
+
};
|
|
476
|
+
const state = body.state;
|
|
477
|
+
if (!isRecord(state) || state.runId !== runId) return null;
|
|
478
|
+
if (
|
|
479
|
+
state.status !== 'completed' &&
|
|
480
|
+
state.status !== 'failed' &&
|
|
481
|
+
state.status !== 'cancelled'
|
|
482
|
+
) {
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
return state as CoordinatorTerminalState;
|
|
486
|
+
}
|
|
487
|
+
|
|
329
488
|
function workflowEventType(name: string): string {
|
|
330
489
|
const normalized = name
|
|
331
490
|
.trim()
|
|
@@ -360,16 +519,20 @@ type PooledWorkflowBootstrapPayload = {
|
|
|
360
519
|
};
|
|
361
520
|
|
|
362
521
|
const WORKFLOW_POOL_PROTOCOL_VERSION =
|
|
363
|
-
'pooled-workflow-wait-
|
|
522
|
+
'pooled-workflow-wait-v14-ready-signal-http-storage';
|
|
364
523
|
const WORKFLOW_POOL_DO_NAME = 'workflow-pool:v2';
|
|
365
524
|
const WORKFLOW_POOL_START_EVENT_TYPE = 'play_start';
|
|
366
525
|
const WORKFLOW_POOL_TTL_MS = 8 * 60 * 1000;
|
|
367
|
-
const WORKFLOW_POOL_TARGET_SIZE =
|
|
526
|
+
const WORKFLOW_POOL_TARGET_SIZE = 16;
|
|
368
527
|
const WORKFLOW_POOL_READY_TIMEOUT_MS = 1_500;
|
|
369
528
|
const WORKFLOW_POOL_READY_POLL_MS = 250;
|
|
370
529
|
const WORKFLOW_POOL_REFILL_ON_MISS_TIMEOUT_MS = 2_500;
|
|
371
|
-
const WORKFLOW_POOL_REFILL_ON_MISS_MIN_AVAILABLE =
|
|
372
|
-
|
|
530
|
+
const WORKFLOW_POOL_REFILL_ON_MISS_MIN_AVAILABLE = 4;
|
|
531
|
+
const WORKFLOW_POOL_CONTROL_TIMEOUT_MS = 750;
|
|
532
|
+
const SUBMIT_INITIAL_STATE_MAX_WAIT_MS = 0;
|
|
533
|
+
const SUBMIT_INITIAL_STATE_POLL_MS = 50;
|
|
534
|
+
const WORKFLOW_POOL_DISABLED_REASON =
|
|
535
|
+
'Cloudflare Workflows start runs directly; waitForEvent is reserved for real durable external waits.';
|
|
373
536
|
function buildDynamicWorkflowMetadata(
|
|
374
537
|
params: PlayWorkflowParams,
|
|
375
538
|
): DynamicWorkflowMetadata {
|
|
@@ -440,13 +603,45 @@ function readWorkflowTraceContext(event: unknown): {
|
|
|
440
603
|
}
|
|
441
604
|
|
|
442
605
|
function workflowPoolEnabled(): boolean {
|
|
443
|
-
return
|
|
606
|
+
return false;
|
|
444
607
|
}
|
|
445
608
|
|
|
446
609
|
function workflowPoolTargetSize(): number {
|
|
447
610
|
return WORKFLOW_POOL_TARGET_SIZE;
|
|
448
611
|
}
|
|
449
612
|
|
|
613
|
+
async function waitForSubmitInitialState(input: {
|
|
614
|
+
instance: WorkflowInstance;
|
|
615
|
+
runId: string;
|
|
616
|
+
waitMs: number;
|
|
617
|
+
}): Promise<Record<string, unknown> | null> {
|
|
618
|
+
const waitMs = Math.max(
|
|
619
|
+
0,
|
|
620
|
+
Math.min(Math.floor(input.waitMs), SUBMIT_INITIAL_STATE_MAX_WAIT_MS),
|
|
621
|
+
);
|
|
622
|
+
if (waitMs <= 0) return null;
|
|
623
|
+
const startedAt = Date.now();
|
|
624
|
+
let status = await input.instance.status();
|
|
625
|
+
while (Date.now() - startedAt < waitMs) {
|
|
626
|
+
const result = mapWorkflowResult(input.runId, status);
|
|
627
|
+
if (
|
|
628
|
+
result.status === 'completed' ||
|
|
629
|
+
result.status === 'failed' ||
|
|
630
|
+
result.status === 'cancelled'
|
|
631
|
+
) {
|
|
632
|
+
return result as unknown as Record<string, unknown>;
|
|
633
|
+
}
|
|
634
|
+
await sleep(SUBMIT_INITIAL_STATE_POLL_MS);
|
|
635
|
+
status = await input.instance.status();
|
|
636
|
+
}
|
|
637
|
+
const result = mapWorkflowResult(input.runId, status);
|
|
638
|
+
return result.status === 'completed' ||
|
|
639
|
+
result.status === 'failed' ||
|
|
640
|
+
result.status === 'cancelled'
|
|
641
|
+
? (result as unknown as Record<string, unknown>)
|
|
642
|
+
: null;
|
|
643
|
+
}
|
|
644
|
+
|
|
450
645
|
async function createDynamicWorkflowInstance(input: {
|
|
451
646
|
env: CoordinatorEnv;
|
|
452
647
|
id: string;
|
|
@@ -469,26 +664,57 @@ function workflowPoolDurableObject(env: CoordinatorEnv): DurableObjectStub {
|
|
|
469
664
|
async function callWorkflowPool<T>(
|
|
470
665
|
env: CoordinatorEnv,
|
|
471
666
|
path: string,
|
|
472
|
-
init?: RequestInit,
|
|
667
|
+
init?: RequestInit & { timeoutMs?: number },
|
|
473
668
|
): Promise<T> {
|
|
474
|
-
const
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
...init,
|
|
478
|
-
headers: {
|
|
479
|
-
'content-type': 'application/json',
|
|
480
|
-
...(init?.headers ?? {}),
|
|
481
|
-
},
|
|
482
|
-
},
|
|
669
|
+
const timeoutMs = Math.max(
|
|
670
|
+
1,
|
|
671
|
+
Math.floor(init?.timeoutMs ?? WORKFLOW_POOL_CONTROL_TIMEOUT_MS),
|
|
483
672
|
);
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
673
|
+
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
674
|
+
try {
|
|
675
|
+
const fetchInit: RequestInit = { ...(init ?? {}) };
|
|
676
|
+
delete (fetchInit as { timeoutMs?: number }).timeoutMs;
|
|
677
|
+
delete fetchInit.signal;
|
|
678
|
+
const response = await Promise.race([
|
|
679
|
+
workflowPoolDurableObject(env).fetch(
|
|
680
|
+
`https://deepline.workflow-pool.internal${path}`,
|
|
681
|
+
{
|
|
682
|
+
...fetchInit,
|
|
683
|
+
headers: {
|
|
684
|
+
'content-type': 'application/json',
|
|
685
|
+
...(init?.headers ?? {}),
|
|
686
|
+
},
|
|
687
|
+
},
|
|
688
|
+
),
|
|
689
|
+
new Promise<Response>((_, reject) => {
|
|
690
|
+
timeoutId = setTimeout(
|
|
691
|
+
() =>
|
|
692
|
+
reject(
|
|
693
|
+
new Error(`workflow pool ${path} timed out after ${timeoutMs}ms`),
|
|
694
|
+
),
|
|
695
|
+
timeoutMs,
|
|
696
|
+
);
|
|
697
|
+
}),
|
|
698
|
+
]);
|
|
699
|
+
if (!response.ok) {
|
|
700
|
+
throw new Error(
|
|
701
|
+
`workflow pool ${path} failed ${response.status}: ${(
|
|
702
|
+
await response.text().catch(() => '')
|
|
703
|
+
).slice(0, 400)}`,
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
return (await response.json()) as T;
|
|
707
|
+
} catch (error) {
|
|
708
|
+
if (
|
|
709
|
+
error instanceof Error &&
|
|
710
|
+
(error.name === 'AbortError' || error.message.includes('aborted'))
|
|
711
|
+
) {
|
|
712
|
+
throw new Error(`workflow pool ${path} timed out after ${timeoutMs}ms`);
|
|
713
|
+
}
|
|
714
|
+
throw error;
|
|
715
|
+
} finally {
|
|
716
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
490
717
|
}
|
|
491
|
-
return (await response.json()) as T;
|
|
492
718
|
}
|
|
493
719
|
|
|
494
720
|
type WorkflowPoolCounts = {
|
|
@@ -507,6 +733,7 @@ type WorkflowPoolRefillResult = WorkflowPoolCounts & {
|
|
|
507
733
|
|
|
508
734
|
type WorkflowPoolListEntry = {
|
|
509
735
|
id: string;
|
|
736
|
+
state: string;
|
|
510
737
|
createdAt: number;
|
|
511
738
|
readyAt: number | null;
|
|
512
739
|
expiresAt: number;
|
|
@@ -544,6 +771,7 @@ async function listWorkflowPoolEntries(
|
|
|
544
771
|
)
|
|
545
772
|
.map((entry) => ({
|
|
546
773
|
id: typeof entry.id === 'string' ? entry.id : '',
|
|
774
|
+
state: typeof entry.state === 'string' ? entry.state : '',
|
|
547
775
|
createdAt:
|
|
548
776
|
typeof entry.createdAt === 'number' && Number.isFinite(entry.createdAt)
|
|
549
777
|
? entry.createdAt
|
|
@@ -578,6 +806,24 @@ async function addWorkflowPoolIds(
|
|
|
578
806
|
});
|
|
579
807
|
}
|
|
580
808
|
|
|
809
|
+
async function markWorkflowPoolIdReady(
|
|
810
|
+
env: CoordinatorEnv,
|
|
811
|
+
poolId: string,
|
|
812
|
+
): Promise<boolean> {
|
|
813
|
+
const body = await callWorkflowPool<{ ready?: unknown }>(
|
|
814
|
+
env,
|
|
815
|
+
'/pool-ready',
|
|
816
|
+
{
|
|
817
|
+
method: 'POST',
|
|
818
|
+
body: JSON.stringify({
|
|
819
|
+
poolId,
|
|
820
|
+
version: WORKFLOW_POOL_PROTOCOL_VERSION,
|
|
821
|
+
}),
|
|
822
|
+
},
|
|
823
|
+
);
|
|
824
|
+
return body.ready === true;
|
|
825
|
+
}
|
|
826
|
+
|
|
581
827
|
async function promoteWorkflowPoolIds(
|
|
582
828
|
env: CoordinatorEnv,
|
|
583
829
|
ids: string[],
|
|
@@ -608,13 +854,14 @@ async function deleteWorkflowPoolIds(
|
|
|
608
854
|
|
|
609
855
|
async function leaseWorkflowPoolId(
|
|
610
856
|
env: CoordinatorEnv,
|
|
857
|
+
runId: string,
|
|
611
858
|
): Promise<string | null> {
|
|
612
859
|
const body = await callWorkflowPool<{ id?: unknown }>(
|
|
613
860
|
env,
|
|
614
|
-
`/pool-
|
|
861
|
+
`/pool-claim?version=${encodeURIComponent(WORKFLOW_POOL_PROTOCOL_VERSION)}`,
|
|
615
862
|
{
|
|
616
863
|
method: 'POST',
|
|
617
|
-
body:
|
|
864
|
+
body: JSON.stringify({ runId }),
|
|
618
865
|
},
|
|
619
866
|
);
|
|
620
867
|
return typeof body.id === 'string' && body.id ? body.id : null;
|
|
@@ -678,27 +925,36 @@ function workflowStatusName(status: InstanceStatus | null): string {
|
|
|
678
925
|
}
|
|
679
926
|
|
|
680
927
|
function workflowPoolStatusIsReady(statusName: string): boolean {
|
|
681
|
-
//
|
|
682
|
-
//
|
|
683
|
-
//
|
|
684
|
-
|
|
685
|
-
return statusName === 'waiting';
|
|
928
|
+
// This is only a liveness guard. Readiness itself comes from the pooled
|
|
929
|
+
// Workflow calling /pool-ready after waitForEvent("play_start") has been
|
|
930
|
+
// created, because Cloudflare may report an armed wait as "running".
|
|
931
|
+
return statusName === 'running' || statusName === 'waiting';
|
|
686
932
|
}
|
|
687
933
|
|
|
688
|
-
async function
|
|
934
|
+
async function waitForWorkflowPoolReadySignal(input: {
|
|
935
|
+
env: CoordinatorEnv;
|
|
936
|
+
instance: WorkflowInstance;
|
|
937
|
+
poolId: string;
|
|
938
|
+
}): Promise<{
|
|
689
939
|
ready: boolean;
|
|
690
940
|
status: string;
|
|
691
941
|
ms: number;
|
|
692
942
|
polls: number;
|
|
693
943
|
}> {
|
|
694
944
|
const startedAt = Date.now();
|
|
695
|
-
let
|
|
945
|
+
let lastStatusName = 'unknown';
|
|
696
946
|
let polls = 0;
|
|
697
947
|
while (Date.now() - startedAt < WORKFLOW_POOL_READY_TIMEOUT_MS) {
|
|
698
|
-
lastStatus = await instance.status();
|
|
699
948
|
polls += 1;
|
|
700
|
-
const
|
|
701
|
-
|
|
949
|
+
const [entry, status] = await Promise.all([
|
|
950
|
+
listWorkflowPoolEntries(input.env)
|
|
951
|
+
.then((entries) => entries.find((candidate) => candidate.id === input.poolId))
|
|
952
|
+
.catch(() => undefined),
|
|
953
|
+
input.instance.status().catch(() => null),
|
|
954
|
+
]);
|
|
955
|
+
const statusName = workflowStatusName(status);
|
|
956
|
+
lastStatusName = statusName;
|
|
957
|
+
if (entry?.state === 'ready' && entry.readyAt !== null) {
|
|
702
958
|
return {
|
|
703
959
|
ready: true,
|
|
704
960
|
status: statusName,
|
|
@@ -709,7 +965,8 @@ async function waitForWorkflowPoolReady(instance: WorkflowInstance): Promise<{
|
|
|
709
965
|
if (
|
|
710
966
|
statusName === 'complete' ||
|
|
711
967
|
statusName === 'errored' ||
|
|
712
|
-
statusName === 'terminated'
|
|
968
|
+
statusName === 'terminated' ||
|
|
969
|
+
statusName === 'unknown'
|
|
713
970
|
) {
|
|
714
971
|
return {
|
|
715
972
|
ready: false,
|
|
@@ -718,13 +975,11 @@ async function waitForWorkflowPoolReady(instance: WorkflowInstance): Promise<{
|
|
|
718
975
|
polls,
|
|
719
976
|
};
|
|
720
977
|
}
|
|
721
|
-
await
|
|
722
|
-
setTimeout(resolve, WORKFLOW_POOL_READY_POLL_MS),
|
|
723
|
-
);
|
|
978
|
+
await sleep(WORKFLOW_POOL_READY_POLL_MS);
|
|
724
979
|
}
|
|
725
980
|
return {
|
|
726
981
|
ready: false,
|
|
727
|
-
status:
|
|
982
|
+
status: lastStatusName,
|
|
728
983
|
ms: Date.now() - startedAt,
|
|
729
984
|
polls,
|
|
730
985
|
};
|
|
@@ -751,11 +1006,13 @@ async function refillWorkflowPoolOnce(
|
|
|
751
1006
|
for (const entry of warmingEntries) {
|
|
752
1007
|
const instance = await env.PLAY_WORKFLOW.get(entry.id);
|
|
753
1008
|
try {
|
|
1009
|
+
if (entry.state === 'ready' && entry.readyAt !== null) {
|
|
1010
|
+
promotedIds.push(entry.id);
|
|
1011
|
+
continue;
|
|
1012
|
+
}
|
|
754
1013
|
const status = await instance.status().catch(() => null);
|
|
755
1014
|
const statusName = workflowStatusName(status);
|
|
756
|
-
if (
|
|
757
|
-
promotedIds.push(entry.id);
|
|
758
|
-
} else if (
|
|
1015
|
+
if (
|
|
759
1016
|
statusName === 'complete' ||
|
|
760
1017
|
statusName === 'errored' ||
|
|
761
1018
|
statusName === 'terminated' ||
|
|
@@ -784,48 +1041,64 @@ async function refillWorkflowPoolOnce(
|
|
|
784
1041
|
removed: removedIds.length,
|
|
785
1042
|
};
|
|
786
1043
|
}
|
|
787
|
-
const
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
});
|
|
799
|
-
try {
|
|
800
|
-
const readiness = await waitForWorkflowPoolReady(instance);
|
|
801
|
-
recordCoordinatorPerfTrace({
|
|
802
|
-
runId: poolId,
|
|
803
|
-
phase: 'coordinator.workflow_pool_ready',
|
|
804
|
-
ms: readiness.ms,
|
|
805
|
-
graphHash: 'workflow-pool',
|
|
806
|
-
extra: {
|
|
807
|
-
ready: readiness.ready,
|
|
808
|
-
status: readiness.status,
|
|
809
|
-
polls: readiness.polls,
|
|
810
|
-
},
|
|
1044
|
+
const created = await Promise.all(
|
|
1045
|
+
Array.from({ length: needed }, async () => {
|
|
1046
|
+
const poolId = `pool-v2-${Date.now().toString(36)}-${crypto.randomUUID().slice(0, 12)}`;
|
|
1047
|
+
await addWorkflowPoolIds(env, [poolId], { ready: false });
|
|
1048
|
+
const instance = await env.PLAY_WORKFLOW.create({
|
|
1049
|
+
id: poolId,
|
|
1050
|
+
params: {
|
|
1051
|
+
__deeplinePooledWorkflow: true,
|
|
1052
|
+
poolId,
|
|
1053
|
+
createdAt: Date.now(),
|
|
1054
|
+
} satisfies PooledWorkflowBootstrapPayload,
|
|
811
1055
|
});
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
1056
|
+
try {
|
|
1057
|
+
const readiness = await waitForWorkflowPoolReadySignal({
|
|
1058
|
+
env,
|
|
1059
|
+
instance,
|
|
1060
|
+
poolId,
|
|
1061
|
+
});
|
|
1062
|
+
recordCoordinatorPerfTrace({
|
|
1063
|
+
runId: poolId,
|
|
1064
|
+
phase: 'coordinator.workflow_pool_ready',
|
|
1065
|
+
ms: readiness.ms,
|
|
1066
|
+
graphHash: 'workflow-pool',
|
|
1067
|
+
extra: {
|
|
1068
|
+
ready: readiness.ready,
|
|
1069
|
+
status: readiness.status,
|
|
1070
|
+
polls: readiness.polls,
|
|
1071
|
+
},
|
|
1072
|
+
});
|
|
1073
|
+
if (readiness.ready) {
|
|
1074
|
+
return { id: poolId, state: 'ready' as const };
|
|
1075
|
+
}
|
|
1076
|
+
if (
|
|
1077
|
+
readiness.status === 'complete' ||
|
|
1078
|
+
readiness.status === 'errored' ||
|
|
1079
|
+
readiness.status === 'terminated' ||
|
|
1080
|
+
readiness.status === 'unknown'
|
|
1081
|
+
) {
|
|
1082
|
+
await instance.terminate().catch(() => undefined);
|
|
1083
|
+
return { id: poolId, state: 'removed' as const };
|
|
1084
|
+
}
|
|
1085
|
+
return { id: poolId, state: 'warming' as const };
|
|
1086
|
+
} finally {
|
|
1087
|
+
disposeRpcStub(instance);
|
|
824
1088
|
}
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
1089
|
+
}),
|
|
1090
|
+
);
|
|
1091
|
+
const readyCreatedIds = created
|
|
1092
|
+
.filter((entry) => entry.state === 'ready')
|
|
1093
|
+
.map((entry) => entry.id);
|
|
1094
|
+
const warmingCreatedIds = created
|
|
1095
|
+
.filter((entry) => entry.state === 'warming')
|
|
1096
|
+
.map((entry) => entry.id);
|
|
1097
|
+
removedIds.push(
|
|
1098
|
+
...created
|
|
1099
|
+
.filter((entry) => entry.state === 'removed')
|
|
1100
|
+
.map((entry) => entry.id),
|
|
1101
|
+
);
|
|
829
1102
|
await Promise.all([
|
|
830
1103
|
addWorkflowPoolIds(env, readyCreatedIds, { ready: true }),
|
|
831
1104
|
addWorkflowPoolIds(env, warmingCreatedIds, { ready: false }),
|
|
@@ -903,7 +1176,14 @@ async function submitViaPooledWorkflow(input: {
|
|
|
903
1176
|
return null;
|
|
904
1177
|
}
|
|
905
1178
|
const leaseStartedAt = Date.now();
|
|
906
|
-
let
|
|
1179
|
+
let leaseError: string | null = null;
|
|
1180
|
+
const pooledInstanceId = await leaseWorkflowPoolId(
|
|
1181
|
+
input.env,
|
|
1182
|
+
input.params.runId,
|
|
1183
|
+
).catch((error) => {
|
|
1184
|
+
leaseError = error instanceof Error ? error.message : String(error);
|
|
1185
|
+
return null;
|
|
1186
|
+
});
|
|
907
1187
|
const missCounts = pooledInstanceId
|
|
908
1188
|
? null
|
|
909
1189
|
: await workflowPoolCount(input.env).catch(() => null);
|
|
@@ -913,6 +1193,7 @@ async function submitViaPooledWorkflow(input: {
|
|
|
913
1193
|
graphHash: input.params.graphHash ?? null,
|
|
914
1194
|
extra: {
|
|
915
1195
|
pooled: Boolean(pooledInstanceId),
|
|
1196
|
+
...(leaseError ? { error: leaseError } : {}),
|
|
916
1197
|
...(missCounts
|
|
917
1198
|
? {
|
|
918
1199
|
availableAfterMiss: missCounts.available,
|
|
@@ -923,43 +1204,26 @@ async function submitViaPooledWorkflow(input: {
|
|
|
923
1204
|
});
|
|
924
1205
|
|
|
925
1206
|
if (!pooledInstanceId) {
|
|
926
|
-
// A pool miss
|
|
927
|
-
//
|
|
928
|
-
|
|
929
|
-
const refillStartedAt = Date.now();
|
|
930
|
-
const refillResult = await refillWorkflowPool(input.env, {
|
|
931
|
-
waitReady: true,
|
|
932
|
-
minAvailable: WORKFLOW_POOL_REFILL_ON_MISS_MIN_AVAILABLE,
|
|
933
|
-
waitTimeoutMs: WORKFLOW_POOL_REFILL_ON_MISS_TIMEOUT_MS,
|
|
934
|
-
}).catch(() => null);
|
|
1207
|
+
// A pool miss must not block the user path. Refilling is handled by the
|
|
1208
|
+
// caller's waitUntil after submit, so fall through to cold create now.
|
|
1209
|
+
const counts = missCounts ?? (await workflowPoolCount(input.env).catch(() => null));
|
|
935
1210
|
input.recordSubmitTiming({
|
|
936
1211
|
phase: 'coordinator.workflow_pool_refill_on_miss',
|
|
937
|
-
ms:
|
|
1212
|
+
ms: 0,
|
|
938
1213
|
graphHash: input.params.graphHash ?? null,
|
|
939
|
-
extra:
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
available:
|
|
945
|
-
warming:
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
},
|
|
1214
|
+
extra: {
|
|
1215
|
+
skipped: true,
|
|
1216
|
+
reason: 'pool_miss_does_not_block_submit',
|
|
1217
|
+
...(counts
|
|
1218
|
+
? {
|
|
1219
|
+
available: counts.available,
|
|
1220
|
+
warming: counts.warming,
|
|
1221
|
+
waitedMs: 0,
|
|
1222
|
+
waitIterations: 0,
|
|
1223
|
+
}
|
|
1224
|
+
: {}),
|
|
1225
|
+
},
|
|
952
1226
|
});
|
|
953
|
-
if (refillResult?.available) {
|
|
954
|
-
const retryStartedAt = Date.now();
|
|
955
|
-
pooledInstanceId = await leaseWorkflowPoolId(input.env);
|
|
956
|
-
input.recordSubmitTiming({
|
|
957
|
-
phase: 'coordinator.workflow_pool_lease_retry',
|
|
958
|
-
ms: Date.now() - retryStartedAt,
|
|
959
|
-
graphHash: input.params.graphHash ?? null,
|
|
960
|
-
extra: { pooled: Boolean(pooledInstanceId) },
|
|
961
|
-
});
|
|
962
|
-
}
|
|
963
1227
|
}
|
|
964
1228
|
|
|
965
1229
|
if (!pooledInstanceId) {
|
|
@@ -967,38 +1231,26 @@ async function submitViaPooledWorkflow(input: {
|
|
|
967
1231
|
}
|
|
968
1232
|
|
|
969
1233
|
const instance = await input.env.PLAY_WORKFLOW.get(pooledInstanceId);
|
|
1234
|
+
const readyCheckStartedAt = Date.now();
|
|
1235
|
+
const status = await instance.status().catch(() => null);
|
|
1236
|
+
const statusName = workflowStatusName(status);
|
|
1237
|
+
input.recordSubmitTiming({
|
|
1238
|
+
phase: 'coordinator.workflow_pool_ready_check',
|
|
1239
|
+
ms: Date.now() - readyCheckStartedAt,
|
|
1240
|
+
graphHash: input.params.graphHash ?? null,
|
|
1241
|
+
extra: { instanceId: pooledInstanceId, status: statusName },
|
|
1242
|
+
});
|
|
1243
|
+
if (!workflowPoolStatusIsReady(statusName)) {
|
|
1244
|
+
await instance.terminate().catch(() => undefined);
|
|
1245
|
+
disposeRpcStub(instance);
|
|
1246
|
+
return null;
|
|
1247
|
+
}
|
|
1248
|
+
const sendStartedAt = Date.now();
|
|
970
1249
|
try {
|
|
971
|
-
const readyCheckStartedAt = Date.now();
|
|
972
|
-
const status = await instance.status().catch(() => null);
|
|
973
|
-
const statusName = workflowStatusName(status);
|
|
974
|
-
input.recordSubmitTiming({
|
|
975
|
-
phase: 'coordinator.workflow_pool_ready_check',
|
|
976
|
-
ms: Date.now() - readyCheckStartedAt,
|
|
977
|
-
graphHash: input.params.graphHash ?? null,
|
|
978
|
-
extra: { instanceId: pooledInstanceId, status: statusName },
|
|
979
|
-
});
|
|
980
|
-
if (!workflowPoolStatusIsReady(statusName)) {
|
|
981
|
-
await instance.terminate().catch(() => undefined);
|
|
982
|
-
disposeRpcStub(instance);
|
|
983
|
-
return null;
|
|
984
|
-
}
|
|
985
|
-
const sendStartedAt = Date.now();
|
|
986
1250
|
await instance.sendEvent({
|
|
987
1251
|
type: WORKFLOW_POOL_START_EVENT_TYPE,
|
|
988
1252
|
payload: buildDispatcherEnvelope(input.params),
|
|
989
1253
|
});
|
|
990
|
-
await mapRunToWorkflowInstance({
|
|
991
|
-
env: input.env,
|
|
992
|
-
runId: input.params.runId,
|
|
993
|
-
instanceId: pooledInstanceId,
|
|
994
|
-
});
|
|
995
|
-
input.recordSubmitTiming({
|
|
996
|
-
phase: 'coordinator.workflow_pool_send_event',
|
|
997
|
-
ms: Date.now() - sendStartedAt,
|
|
998
|
-
graphHash: input.params.graphHash ?? null,
|
|
999
|
-
extra: { instanceId: pooledInstanceId },
|
|
1000
|
-
});
|
|
1001
|
-
return instance;
|
|
1002
1254
|
} catch (error) {
|
|
1003
1255
|
disposeRpcStub(instance);
|
|
1004
1256
|
console.warn('[coordinator.workflow_pool] sendEvent failed; falling back', {
|
|
@@ -1008,6 +1260,13 @@ async function submitViaPooledWorkflow(input: {
|
|
|
1008
1260
|
});
|
|
1009
1261
|
return null;
|
|
1010
1262
|
}
|
|
1263
|
+
input.recordSubmitTiming({
|
|
1264
|
+
phase: 'coordinator.workflow_pool_send_event',
|
|
1265
|
+
ms: Date.now() - sendStartedAt,
|
|
1266
|
+
graphHash: input.params.graphHash ?? null,
|
|
1267
|
+
extra: { instanceId: pooledInstanceId },
|
|
1268
|
+
});
|
|
1269
|
+
return instance;
|
|
1011
1270
|
}
|
|
1012
1271
|
|
|
1013
1272
|
function readWorkflowPayload(event: unknown): Record<string, unknown> | null {
|
|
@@ -1371,6 +1630,7 @@ function runRequestFromPlayWorkflowParams(params: PlayWorkflowParams): Record<st
|
|
|
1371
1630
|
executionPlan: params.executionPlan ?? null,
|
|
1372
1631
|
childPlayManifests: params.childPlayManifests ?? null,
|
|
1373
1632
|
playCallGovernance: params.playCallGovernance ?? null,
|
|
1633
|
+
preloadedDbSessions: params.preloadedDbSessions ?? null,
|
|
1374
1634
|
coordinatorUrl: params.coordinatorUrl ?? null,
|
|
1375
1635
|
totalRows: params.totalRows,
|
|
1376
1636
|
};
|
|
@@ -1725,6 +1985,13 @@ export class RuntimeApi extends WorkerEntrypoint<CoordinatorEnv, undefined> {
|
|
|
1725
1985
|
? this.env.DEEPLINE_API_BASE_URL.trim()
|
|
1726
1986
|
: 'https://code.deepline.com';
|
|
1727
1987
|
const target = new URL(incoming.pathname + incoming.search, apiBaseUrl);
|
|
1988
|
+
const runtimeStatusBody =
|
|
1989
|
+
incoming.pathname === '/api/v2/plays/internal/runtime'
|
|
1990
|
+
? await request
|
|
1991
|
+
.clone()
|
|
1992
|
+
.json()
|
|
1993
|
+
.catch(() => null)
|
|
1994
|
+
: null;
|
|
1728
1995
|
const forwarded = new Request(target.toString(), request);
|
|
1729
1996
|
const bypassToken = this.env.VERCEL_PROTECTION_BYPASS_TOKEN;
|
|
1730
1997
|
if (typeof bypassToken === 'string' && bypassToken) {
|
|
@@ -1740,9 +2007,41 @@ export class RuntimeApi extends WorkerEntrypoint<CoordinatorEnv, undefined> {
|
|
|
1740
2007
|
`[RUNTIME_API] ${incoming.pathname} failed: status=${res.status} ` +
|
|
1741
2008
|
`target=${target.toString()} body=${body.slice(0, 500)}`,
|
|
1742
2009
|
);
|
|
2010
|
+
} else {
|
|
2011
|
+
await this.recordRuntimeStatusEvent(runtimeStatusBody).catch(() => null);
|
|
1743
2012
|
}
|
|
1744
2013
|
return res;
|
|
1745
2014
|
}
|
|
2015
|
+
|
|
2016
|
+
private async recordRuntimeStatusEvent(body: unknown): Promise<void> {
|
|
2017
|
+
if (!isRecord(body) || body.action !== 'update_run_status') {
|
|
2018
|
+
return;
|
|
2019
|
+
}
|
|
2020
|
+
const runId = typeof body.playId === 'string' ? body.playId : '';
|
|
2021
|
+
const status = typeof body.status === 'string' ? body.status : '';
|
|
2022
|
+
if (!runId || !status) {
|
|
2023
|
+
return;
|
|
2024
|
+
}
|
|
2025
|
+
await appendCoordinatorRunEvent(this.env, {
|
|
2026
|
+
runId,
|
|
2027
|
+
type: 'progress',
|
|
2028
|
+
status,
|
|
2029
|
+
ts: Date.now(),
|
|
2030
|
+
logs: Array.isArray(body.liveLogs)
|
|
2031
|
+
? body.liveLogs.filter((line): line is string => typeof line === 'string')
|
|
2032
|
+
: undefined,
|
|
2033
|
+
activeNodeId:
|
|
2034
|
+
typeof body.activeNodeId === 'string' ? body.activeNodeId : null,
|
|
2035
|
+
activeArtifactTableNamespace:
|
|
2036
|
+
typeof body.activeArtifactTableNamespace === 'string'
|
|
2037
|
+
? body.activeArtifactTableNamespace
|
|
2038
|
+
: null,
|
|
2039
|
+
updatedAt:
|
|
2040
|
+
typeof body.lastCheckpointAt === 'number'
|
|
2041
|
+
? body.lastCheckpointAt
|
|
2042
|
+
: null,
|
|
2043
|
+
});
|
|
2044
|
+
}
|
|
1746
2045
|
}
|
|
1747
2046
|
|
|
1748
2047
|
export class CoordinatorControl extends WorkerEntrypoint<
|
|
@@ -1810,6 +2109,13 @@ export class CoordinatorControl extends WorkerEntrypoint<
|
|
|
1810
2109
|
}
|
|
1811
2110
|
await appendCoordinatorPerfTrace(this.env, payload);
|
|
1812
2111
|
}
|
|
2112
|
+
|
|
2113
|
+
async recordRunEvent(runId: string, event: CoordinatorRunEvent): Promise<void> {
|
|
2114
|
+
if (!runId || event.runId !== runId) {
|
|
2115
|
+
throw new Error('Run event runId mismatch.');
|
|
2116
|
+
}
|
|
2117
|
+
await appendCoordinatorRunEvent(this.env, event);
|
|
2118
|
+
}
|
|
1813
2119
|
}
|
|
1814
2120
|
|
|
1815
2121
|
/**
|
|
@@ -1844,6 +2150,7 @@ export class DynamicWorkflow extends WorkflowEntrypoint<
|
|
|
1844
2150
|
});
|
|
1845
2151
|
let dispatchedEvent = event;
|
|
1846
2152
|
if (isPooledWorkflowBootstrapPayload(workflowEvent.payload)) {
|
|
2153
|
+
const pooledPayload = workflowEvent.payload;
|
|
1847
2154
|
const waitingStep = step as {
|
|
1848
2155
|
waitForEvent<T>(
|
|
1849
2156
|
name: string,
|
|
@@ -1851,10 +2158,19 @@ export class DynamicWorkflow extends WorkflowEntrypoint<
|
|
|
1851
2158
|
): Promise<{ payload: Readonly<T>; timestamp: Date; type: string }>;
|
|
1852
2159
|
};
|
|
1853
2160
|
const waitStartedAt = Date.now();
|
|
1854
|
-
const
|
|
2161
|
+
const startEventPromise = waitingStep.waitForEvent<DispatcherEnvelope>(
|
|
1855
2162
|
'wait for pooled play start',
|
|
1856
2163
|
{ type: WORKFLOW_POOL_START_EVENT_TYPE, timeout: '10 minutes' },
|
|
1857
2164
|
);
|
|
2165
|
+
await markWorkflowPoolIdReady(this.env, pooledPayload.poolId).catch(
|
|
2166
|
+
(error) => {
|
|
2167
|
+
console.warn('[coordinator.workflow_pool] ready signal failed', {
|
|
2168
|
+
poolId: pooledPayload.poolId,
|
|
2169
|
+
message: error instanceof Error ? error.message : String(error),
|
|
2170
|
+
});
|
|
2171
|
+
},
|
|
2172
|
+
);
|
|
2173
|
+
const startEvent = await startEventPromise;
|
|
1858
2174
|
dispatchedEvent = {
|
|
1859
2175
|
payload: startEvent.payload,
|
|
1860
2176
|
timestamp: startEvent.timestamp,
|
|
@@ -1956,6 +2272,18 @@ export class DynamicWorkflow extends WorkflowEntrypoint<
|
|
|
1956
2272
|
run(e: unknown, s: unknown): Promise<unknown>;
|
|
1957
2273
|
}
|
|
1958
2274
|
).run(innerEvent, innerStep);
|
|
2275
|
+
const output = isRecord(result) ? result : null;
|
|
2276
|
+
await writeCoordinatorTerminalState(env, {
|
|
2277
|
+
runId: runIdForTrace,
|
|
2278
|
+
status: 'completed',
|
|
2279
|
+
result: output?.result ?? result,
|
|
2280
|
+
totalRows: output?.totalRows ?? output?.outputRows ?? null,
|
|
2281
|
+
durationMs: output?.durationMs ?? null,
|
|
2282
|
+
playName:
|
|
2283
|
+
typeof output?.playName === 'string'
|
|
2284
|
+
? output.playName
|
|
2285
|
+
: null,
|
|
2286
|
+
});
|
|
1959
2287
|
trace({
|
|
1960
2288
|
runId: runIdForTrace,
|
|
1961
2289
|
phase: 'coordinator.runner_run',
|
|
@@ -1993,6 +2321,14 @@ export class DynamicWorkflow extends WorkflowEntrypoint<
|
|
|
1993
2321
|
},
|
|
1994
2322
|
);
|
|
1995
2323
|
});
|
|
2324
|
+
await writeCoordinatorTerminalState(env, {
|
|
2325
|
+
runId: runIdForTrace,
|
|
2326
|
+
status: 'failed',
|
|
2327
|
+
error:
|
|
2328
|
+
innerError instanceof Error
|
|
2329
|
+
? innerError.message
|
|
2330
|
+
: String(innerError),
|
|
2331
|
+
}).catch(() => undefined);
|
|
1996
2332
|
throw innerError;
|
|
1997
2333
|
}
|
|
1998
2334
|
},
|
|
@@ -2006,7 +2342,7 @@ const coordinatorEntrypoint = {
|
|
|
2006
2342
|
/**
|
|
2007
2343
|
* HTTP entrypoint for the Vercel app to dispatch into. Routes:
|
|
2008
2344
|
* POST /workflow/{runId}/submit → PLAY_WORKFLOW.create({ id, params })
|
|
2009
|
-
* GET /workflow/{runId}/
|
|
2345
|
+
* GET /workflow/{runId}/tail → ordered live run events after a cursor
|
|
2010
2346
|
* POST /workflow/{runId}/cancel → Workflow instance terminate
|
|
2011
2347
|
* POST /workflow/{runId}/signal → integration_event
|
|
2012
2348
|
* GET /workflow/{runId}/result → terminal envelope
|
|
@@ -2029,6 +2365,34 @@ const coordinatorEntrypoint = {
|
|
|
2029
2365
|
if (authError) return authError;
|
|
2030
2366
|
return await handleCoordinatorWarmup(request, env, ctx);
|
|
2031
2367
|
}
|
|
2368
|
+
if (url.pathname === '/tail-log-token/probe') {
|
|
2369
|
+
const authError = authorizeCoordinatorControlRequest({ request, env });
|
|
2370
|
+
if (authError) return authError;
|
|
2371
|
+
const expectedTailLogToken = env.DEEPLINE_TAIL_LOG_TOKEN?.trim();
|
|
2372
|
+
if (!expectedTailLogToken) {
|
|
2373
|
+
return Response.json(
|
|
2374
|
+
{ ok: false, error: 'tail log token is not configured' },
|
|
2375
|
+
{ status: 503 },
|
|
2376
|
+
);
|
|
2377
|
+
}
|
|
2378
|
+
const actualTailLogToken =
|
|
2379
|
+
request.headers.get('x-deepline-tail-log-token')?.trim() ?? '';
|
|
2380
|
+
if (actualTailLogToken !== expectedTailLogToken) {
|
|
2381
|
+
return Response.json(
|
|
2382
|
+
{ ok: false, error: 'tail log token mismatch' },
|
|
2383
|
+
{ status: 401 },
|
|
2384
|
+
);
|
|
2385
|
+
}
|
|
2386
|
+
return Response.json({
|
|
2387
|
+
ok: true,
|
|
2388
|
+
deployMarker: env.DEEPLINE_COORDINATOR_DEPLOY_MARKER ?? null,
|
|
2389
|
+
});
|
|
2390
|
+
}
|
|
2391
|
+
if (url.pathname === '/staged-files/put') {
|
|
2392
|
+
const authError = authorizeCoordinatorControlRequest({ request, env });
|
|
2393
|
+
if (authError) return authError;
|
|
2394
|
+
return await handleStagedFilePut(request, env);
|
|
2395
|
+
}
|
|
2032
2396
|
if (url.pathname === '/workflow-pool/refill') {
|
|
2033
2397
|
const internalAuthError = authorizeCoordinatorControlRequest({
|
|
2034
2398
|
request,
|
|
@@ -2083,6 +2447,33 @@ const coordinatorEntrypoint = {
|
|
|
2083
2447
|
ms: Date.now() - startedAt,
|
|
2084
2448
|
});
|
|
2085
2449
|
}
|
|
2450
|
+
if (url.pathname === '/workflow-pool/debug') {
|
|
2451
|
+
const internalAuthError = authorizeCoordinatorControlRequest({
|
|
2452
|
+
request,
|
|
2453
|
+
env,
|
|
2454
|
+
});
|
|
2455
|
+
if (internalAuthError) return internalAuthError;
|
|
2456
|
+
const entries = await listWorkflowPoolEntries(env);
|
|
2457
|
+
const detailed = [];
|
|
2458
|
+
for (const entry of entries) {
|
|
2459
|
+
const instance = await env.PLAY_WORKFLOW.get(entry.id);
|
|
2460
|
+
try {
|
|
2461
|
+
const status = await instance.status().catch(() => null);
|
|
2462
|
+
detailed.push({
|
|
2463
|
+
...entry,
|
|
2464
|
+
status: workflowStatusName(status),
|
|
2465
|
+
mappedStatus: status ? mapWorkflowStatus(status) : 'running',
|
|
2466
|
+
});
|
|
2467
|
+
} finally {
|
|
2468
|
+
disposeRpcStub(instance);
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
return Response.json({
|
|
2472
|
+
ok: true,
|
|
2473
|
+
enabled: workflowPoolEnabled(),
|
|
2474
|
+
entries: detailed,
|
|
2475
|
+
});
|
|
2476
|
+
}
|
|
2086
2477
|
|
|
2087
2478
|
// Workflow routes: /workflow/{runId}/{action}
|
|
2088
2479
|
const wfMatch = url.pathname.match(/^\/workflow\/([^/]+)(?:\/(.+))?$/);
|
|
@@ -2294,45 +2685,54 @@ async function handleWorkflowRoute(input: {
|
|
|
2294
2685
|
let instance: WorkflowInstance | null = null;
|
|
2295
2686
|
try {
|
|
2296
2687
|
const dispatchStartedAt = Date.now();
|
|
2297
|
-
|
|
2298
|
-
|
|
2688
|
+
recordSubmitTiming({
|
|
2689
|
+
phase: 'coordinator.workflow_pool_attempt',
|
|
2690
|
+
ms: 0,
|
|
2691
|
+
graphHash: params.graphHash ?? null,
|
|
2692
|
+
extra: {
|
|
2693
|
+
usedPool: false,
|
|
2694
|
+
disabled: true,
|
|
2695
|
+
reason: WORKFLOW_POOL_DISABLED_REASON,
|
|
2696
|
+
},
|
|
2697
|
+
});
|
|
2698
|
+
const createStartedAt = Date.now();
|
|
2699
|
+
instance = await createDynamicWorkflowInstance({
|
|
2299
2700
|
env,
|
|
2701
|
+
id: defaultInstanceId,
|
|
2300
2702
|
params,
|
|
2301
|
-
recordSubmitTiming,
|
|
2302
2703
|
});
|
|
2303
|
-
const usedWorkflowPool = Boolean(instance);
|
|
2304
2704
|
recordSubmitTiming({
|
|
2305
|
-
phase: 'coordinator.
|
|
2306
|
-
ms: Date.now() -
|
|
2705
|
+
phase: 'coordinator.workflow_create',
|
|
2706
|
+
ms: Date.now() - createStartedAt,
|
|
2307
2707
|
graphHash: params.graphHash ?? null,
|
|
2308
|
-
extra: {
|
|
2708
|
+
extra: { instanceId: instance.id },
|
|
2309
2709
|
});
|
|
2310
|
-
if (!instance) {
|
|
2311
|
-
const createStartedAt = Date.now();
|
|
2312
|
-
instance = await createDynamicWorkflowInstance({
|
|
2313
|
-
env,
|
|
2314
|
-
id: defaultInstanceId,
|
|
2315
|
-
params,
|
|
2316
|
-
});
|
|
2317
|
-
recordSubmitTiming({
|
|
2318
|
-
phase: 'coordinator.workflow_create',
|
|
2319
|
-
ms: Date.now() - createStartedAt,
|
|
2320
|
-
graphHash: params.graphHash ?? null,
|
|
2321
|
-
extra: { instanceId: instance.id, pooled: false },
|
|
2322
|
-
});
|
|
2323
|
-
} else {
|
|
2324
|
-
recordSubmitTiming({
|
|
2325
|
-
phase: 'coordinator.workflow_create',
|
|
2326
|
-
ms: 0,
|
|
2327
|
-
graphHash: params.graphHash ?? null,
|
|
2328
|
-
extra: { instanceId: instance.id, pooled: true },
|
|
2329
|
-
});
|
|
2330
|
-
}
|
|
2331
2710
|
recordSubmitTiming({
|
|
2332
2711
|
phase: 'coordinator.dispatch_workflow',
|
|
2333
2712
|
ms: Date.now() - dispatchStartedAt,
|
|
2334
2713
|
graphHash: params.graphHash ?? null,
|
|
2335
|
-
extra: {
|
|
2714
|
+
extra: { startMode: 'direct_workflow_create' },
|
|
2715
|
+
});
|
|
2716
|
+
const initialWaitMsRaw = Number(
|
|
2717
|
+
new URL(request.url).searchParams.get('initialWaitMs') ?? '0',
|
|
2718
|
+
);
|
|
2719
|
+
const initialStateStartedAt = Date.now();
|
|
2720
|
+
const instanceState = await waitForSubmitInitialState({
|
|
2721
|
+
instance,
|
|
2722
|
+
runId: submittedRunId,
|
|
2723
|
+
waitMs: Number.isFinite(initialWaitMsRaw) ? initialWaitMsRaw : 0,
|
|
2724
|
+
});
|
|
2725
|
+
recordSubmitTiming({
|
|
2726
|
+
phase: 'coordinator.submit_initial_state',
|
|
2727
|
+
ms: Date.now() - initialStateStartedAt,
|
|
2728
|
+
graphHash: params.graphHash ?? null,
|
|
2729
|
+
extra: {
|
|
2730
|
+
waitMs: Number.isFinite(initialWaitMsRaw) ? initialWaitMsRaw : 0,
|
|
2731
|
+
status:
|
|
2732
|
+
typeof instanceState?.status === 'string'
|
|
2733
|
+
? instanceState.status
|
|
2734
|
+
: null,
|
|
2735
|
+
},
|
|
2336
2736
|
});
|
|
2337
2737
|
const totalMs = Date.now() - submitStartedAt;
|
|
2338
2738
|
recordSubmitTiming({
|
|
@@ -2348,11 +2748,11 @@ async function handleWorkflowRoute(input: {
|
|
|
2348
2748
|
return Response.json({
|
|
2349
2749
|
runId,
|
|
2350
2750
|
status: 'submitted',
|
|
2351
|
-
|
|
2751
|
+
workflowInstanceId: instance.id,
|
|
2752
|
+
instanceState,
|
|
2352
2753
|
coordinatorTimings,
|
|
2353
2754
|
});
|
|
2354
2755
|
} finally {
|
|
2355
|
-
input.ctx?.waitUntil(refillWorkflowPool(env).catch(() => undefined));
|
|
2356
2756
|
disposeRpcStub(instance);
|
|
2357
2757
|
}
|
|
2358
2758
|
}
|
|
@@ -2582,12 +2982,84 @@ async function handleWorkflowRoute(input: {
|
|
|
2582
2982
|
}
|
|
2583
2983
|
}
|
|
2584
2984
|
|
|
2985
|
+
if (action === 'tail') {
|
|
2986
|
+
const url = new URL(request.url);
|
|
2987
|
+
const waitMs = Math.min(
|
|
2988
|
+
Math.max(Number(url.searchParams.get('waitMs') ?? '0'), 0),
|
|
2989
|
+
30_000,
|
|
2990
|
+
);
|
|
2991
|
+
const afterSeq = Math.max(
|
|
2992
|
+
0,
|
|
2993
|
+
Math.floor(Number(url.searchParams.get('afterSeq') ?? '0')),
|
|
2994
|
+
);
|
|
2995
|
+
const includeTrace = url.searchParams.get('trace') === '1';
|
|
2996
|
+
const statusStartedAt = Date.now();
|
|
2997
|
+
const eventResult = await listCoordinatorRunEvents({
|
|
2998
|
+
env,
|
|
2999
|
+
runId,
|
|
3000
|
+
afterSeq,
|
|
3001
|
+
timeoutMs: waitMs,
|
|
3002
|
+
}).catch(() => null);
|
|
3003
|
+
const coordinatorTrace =
|
|
3004
|
+
includeTrace && eventResult?.events.length
|
|
3005
|
+
? await listCoordinatorPerfTrace(env, runId).catch(() => [])
|
|
3006
|
+
: [];
|
|
3007
|
+
const terminalEvent = eventResult?.events.find(
|
|
3008
|
+
(event): event is Extract<CoordinatorRunEvent, { type: 'terminal' }> =>
|
|
3009
|
+
event.type === 'terminal',
|
|
3010
|
+
);
|
|
3011
|
+
if (terminalEvent) {
|
|
3012
|
+
return Response.json({
|
|
3013
|
+
runId,
|
|
3014
|
+
...(terminalEvent.playName ? { playName: terminalEvent.playName } : {}),
|
|
3015
|
+
status: terminalEvent.status,
|
|
3016
|
+
result: terminalEvent.result ?? null,
|
|
3017
|
+
error: terminalEvent.error ?? null,
|
|
3018
|
+
totalRows: terminalEvent.totalRows ?? null,
|
|
3019
|
+
durationMs: terminalEvent.durationMs ?? null,
|
|
3020
|
+
events: eventResult?.events ?? [],
|
|
3021
|
+
latestSeq: eventResult?.latestSeq ?? afterSeq,
|
|
3022
|
+
wait: null,
|
|
3023
|
+
coordinatorObserve: {
|
|
3024
|
+
ms: Date.now() - statusStartedAt,
|
|
3025
|
+
waitMs,
|
|
3026
|
+
workflowStatus: 'terminal-event',
|
|
3027
|
+
statusPolls: 0,
|
|
3028
|
+
instanceId: null,
|
|
3029
|
+
},
|
|
3030
|
+
...(includeTrace ? { coordinatorTrace } : {}),
|
|
3031
|
+
});
|
|
3032
|
+
}
|
|
3033
|
+
return Response.json({
|
|
3034
|
+
runId,
|
|
3035
|
+
status: 'running',
|
|
3036
|
+
events: eventResult?.events ?? [],
|
|
3037
|
+
latestSeq: eventResult?.latestSeq ?? afterSeq,
|
|
3038
|
+
wait: null,
|
|
3039
|
+
coordinatorObserve: {
|
|
3040
|
+
ms: Date.now() - statusStartedAt,
|
|
3041
|
+
waitMs,
|
|
3042
|
+
workflowStatus:
|
|
3043
|
+
eventResult?.events.length ? 'event' : 'event-timeout',
|
|
3044
|
+
statusPolls: 0,
|
|
3045
|
+
instanceId: null,
|
|
3046
|
+
},
|
|
3047
|
+
...(includeTrace ? { coordinatorTrace } : {}),
|
|
3048
|
+
});
|
|
3049
|
+
}
|
|
3050
|
+
|
|
2585
3051
|
// get() throws if the instance doesn't exist (Workflows local-mode wipes
|
|
2586
3052
|
// state on wrangler dev reload, and superseded `--force` runs may target
|
|
2587
3053
|
// an instance that was never created). Treat that as a no-op cancel.
|
|
2588
3054
|
let instance: WorkflowInstance | null = null;
|
|
2589
3055
|
try {
|
|
2590
|
-
const
|
|
3056
|
+
const requestedInstanceId = new URL(request.url).searchParams
|
|
3057
|
+
.get('instanceId')
|
|
3058
|
+
?.trim();
|
|
3059
|
+
const instanceId =
|
|
3060
|
+
requestedInstanceId && !isWorkflowMutatingAction(action)
|
|
3061
|
+
? requestedInstanceId
|
|
3062
|
+
: await resolveWorkflowInstanceIdForRun(env, runId);
|
|
2591
3063
|
instance = await env.PLAY_WORKFLOW.get(instanceId);
|
|
2592
3064
|
} catch (error) {
|
|
2593
3065
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -2655,42 +3127,38 @@ async function handleWorkflowRoute(input: {
|
|
|
2655
3127
|
});
|
|
2656
3128
|
}
|
|
2657
3129
|
if (
|
|
2658
|
-
action === 'result' ||
|
|
2659
|
-
action === 'status' ||
|
|
2660
|
-
action === 'observe' ||
|
|
2661
|
-
action === ''
|
|
3130
|
+
action === 'result' || action === 'status' || action === ''
|
|
2662
3131
|
) {
|
|
2663
|
-
const observeWaitMs =
|
|
2664
|
-
action === 'observe'
|
|
2665
|
-
? Math.min(
|
|
2666
|
-
Math.max(
|
|
2667
|
-
Number(new URL(request.url).searchParams.get('waitMs') ?? '0'),
|
|
2668
|
-
0,
|
|
2669
|
-
),
|
|
2670
|
-
2_000,
|
|
2671
|
-
)
|
|
2672
|
-
: 0;
|
|
2673
3132
|
const includeTrace =
|
|
2674
3133
|
new URL(request.url).searchParams.get('trace') === '1';
|
|
2675
3134
|
const statusStartedAt = Date.now();
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
3135
|
+
const terminalState = await readCoordinatorTerminalState(env, runId).catch(
|
|
3136
|
+
() => null,
|
|
3137
|
+
);
|
|
3138
|
+
if (terminalState) {
|
|
3139
|
+
const coordinatorTrace = includeTrace
|
|
3140
|
+
? await listCoordinatorPerfTrace(env, runId).catch(() => [])
|
|
3141
|
+
: [];
|
|
3142
|
+
return Response.json({
|
|
3143
|
+
runId,
|
|
3144
|
+
...(terminalState.playName ? { playName: terminalState.playName } : {}),
|
|
3145
|
+
status: terminalState.status,
|
|
3146
|
+
result: terminalState.result ?? null,
|
|
3147
|
+
error: terminalState.error ?? null,
|
|
3148
|
+
totalRows: terminalState.totalRows ?? null,
|
|
3149
|
+
durationMs: terminalState.durationMs ?? null,
|
|
3150
|
+
wait: null,
|
|
3151
|
+
coordinatorObserve: {
|
|
3152
|
+
ms: Date.now() - statusStartedAt,
|
|
3153
|
+
waitMs: 0,
|
|
3154
|
+
workflowStatus: 'terminal-cache',
|
|
3155
|
+
statusPolls: 0,
|
|
3156
|
+
instanceId: instance.id,
|
|
3157
|
+
},
|
|
3158
|
+
...(includeTrace ? { coordinatorTrace } : {}),
|
|
3159
|
+
});
|
|
2693
3160
|
}
|
|
3161
|
+
const status = await instance.status();
|
|
2694
3162
|
const result = mapWorkflowResult(runId, status);
|
|
2695
3163
|
const observeMs = Date.now() - statusStartedAt;
|
|
2696
3164
|
// If we forced a permanent-error fail-fast (status='failed' even
|
|
@@ -2729,9 +3197,9 @@ async function handleWorkflowRoute(input: {
|
|
|
2729
3197
|
...result,
|
|
2730
3198
|
coordinatorObserve: {
|
|
2731
3199
|
ms: observeMs,
|
|
2732
|
-
waitMs:
|
|
3200
|
+
waitMs: 0,
|
|
2733
3201
|
workflowStatus: status.status,
|
|
2734
|
-
statusPolls,
|
|
3202
|
+
statusPolls: 1,
|
|
2735
3203
|
instanceId: instance.id,
|
|
2736
3204
|
},
|
|
2737
3205
|
...(includeTrace ? { coordinatorTrace } : {}),
|
|
@@ -2760,6 +3228,54 @@ function stableHash(value: string): string {
|
|
|
2760
3228
|
return (hash >>> 0).toString(36);
|
|
2761
3229
|
}
|
|
2762
3230
|
|
|
3231
|
+
const DYNAMIC_PLAY_WORKER_HARNESS_VERSION =
|
|
3232
|
+
'h6-runtime-api-coordinator-deploy-scoped';
|
|
3233
|
+
const DYNAMIC_WORKER_BUNDLED_CODE_CACHE_MAX_ENTRIES = 64;
|
|
3234
|
+
const dynamicWorkerBundledCodeCache = new Map<string, string>();
|
|
3235
|
+
|
|
3236
|
+
function dynamicPlayWorkerCacheKey(input: {
|
|
3237
|
+
env: CoordinatorEnv;
|
|
3238
|
+
graphHash: string;
|
|
3239
|
+
artifactIdentity: string;
|
|
3240
|
+
}): string {
|
|
3241
|
+
const deployMarker =
|
|
3242
|
+
input.env.DEEPLINE_COORDINATOR_DEPLOY_MARKER?.trim() || 'local';
|
|
3243
|
+
return [
|
|
3244
|
+
'play',
|
|
3245
|
+
input.graphHash,
|
|
3246
|
+
input.artifactIdentity,
|
|
3247
|
+
`harness=${DYNAMIC_PLAY_WORKER_HARNESS_VERSION}`,
|
|
3248
|
+
`deploy=${deployMarker}`,
|
|
3249
|
+
].join(':');
|
|
3250
|
+
}
|
|
3251
|
+
|
|
3252
|
+
function dynamicWorkerBundledCodeCacheKey(input: {
|
|
3253
|
+
artifactStorageKey: string;
|
|
3254
|
+
artifactHash?: string | null;
|
|
3255
|
+
}): string {
|
|
3256
|
+
return `${input.artifactHash?.trim() || 'no-hash'}:${input.artifactStorageKey}`;
|
|
3257
|
+
}
|
|
3258
|
+
|
|
3259
|
+
function readDynamicWorkerBundledCodeCache(key: string): string | null {
|
|
3260
|
+
const cached = dynamicWorkerBundledCodeCache.get(key);
|
|
3261
|
+
if (cached === undefined) return null;
|
|
3262
|
+
dynamicWorkerBundledCodeCache.delete(key);
|
|
3263
|
+
dynamicWorkerBundledCodeCache.set(key, cached);
|
|
3264
|
+
return cached;
|
|
3265
|
+
}
|
|
3266
|
+
|
|
3267
|
+
function writeDynamicWorkerBundledCodeCache(key: string, code: string): void {
|
|
3268
|
+
dynamicWorkerBundledCodeCache.set(key, code);
|
|
3269
|
+
while (
|
|
3270
|
+
dynamicWorkerBundledCodeCache.size >
|
|
3271
|
+
DYNAMIC_WORKER_BUNDLED_CODE_CACHE_MAX_ENTRIES
|
|
3272
|
+
) {
|
|
3273
|
+
const oldestKey = dynamicWorkerBundledCodeCache.keys().next().value;
|
|
3274
|
+
if (typeof oldestKey !== 'string') break;
|
|
3275
|
+
dynamicWorkerBundledCodeCache.delete(oldestKey);
|
|
3276
|
+
}
|
|
3277
|
+
}
|
|
3278
|
+
|
|
2763
3279
|
/**
|
|
2764
3280
|
* Synchronous wrapper around env.LOADER.get for use inside the
|
|
2765
3281
|
* createDynamicWorkflowEntrypoint loader callback. The framework's loader
|
|
@@ -2798,7 +3314,11 @@ function loadDynamicPlayWorkerSync(
|
|
|
2798
3314
|
}
|
|
2799
3315
|
const artifactIdentity =
|
|
2800
3316
|
metadata.artifactHash?.trim() || stableHash(artifactStorageKey);
|
|
2801
|
-
const workerCacheKey =
|
|
3317
|
+
const workerCacheKey = dynamicPlayWorkerCacheKey({
|
|
3318
|
+
env,
|
|
3319
|
+
graphHash,
|
|
3320
|
+
artifactIdentity,
|
|
3321
|
+
});
|
|
2802
3322
|
const runIdForTrace = metadata.runId ?? graphHash;
|
|
2803
3323
|
const loaderGetStartedAt = Date.now();
|
|
2804
3324
|
const stub = env.LOADER.get(workerCacheKey, async () => {
|
|
@@ -2829,6 +3349,10 @@ function loadDynamicPlayWorkerSync(
|
|
|
2829
3349
|
// tool execution, DB session, and artifact callbacks. This avoids a
|
|
2830
3350
|
// public fetch hop when Cloudflare exposes the RuntimeApi export.
|
|
2831
3351
|
...makeRuntimeApiEnvBinding(),
|
|
3352
|
+
// In-process coordinator control bridge used by ctx.runPlay and
|
|
3353
|
+
// parent terminal signals. This keeps scalar child plays inline with
|
|
3354
|
+
// the parent instead of round-tripping through nested Workflow waits.
|
|
3355
|
+
...makeCoordinatorControlBinding(),
|
|
2832
3356
|
// NOTE: We intentionally do NOT pass `env.PLAYS_BUCKET` (an R2Bucket
|
|
2833
3357
|
// binding) through to the per-play Worker's env. Including a raw
|
|
2834
3358
|
// R2Bucket in the dynamically-loaded Worker's env makes Cloudflare
|
|
@@ -2880,7 +3404,11 @@ async function loadDynamicPlayWorker(
|
|
|
2880
3404
|
}
|
|
2881
3405
|
const artifactIdentity =
|
|
2882
3406
|
metadata.artifactHash?.trim() || stableHash(artifactStorageKey);
|
|
2883
|
-
const workerCacheKey =
|
|
3407
|
+
const workerCacheKey = dynamicPlayWorkerCacheKey({
|
|
3408
|
+
env,
|
|
3409
|
+
graphHash,
|
|
3410
|
+
artifactIdentity,
|
|
3411
|
+
});
|
|
2884
3412
|
const runIdForTrace = metadata.runId ?? graphHash;
|
|
2885
3413
|
const loaderGetStartedAt = Date.now();
|
|
2886
3414
|
const stub = env.LOADER.get(workerCacheKey, async () => {
|
|
@@ -2902,11 +3430,11 @@ async function loadDynamicPlayWorker(
|
|
|
2902
3430
|
// Mirror of the sync loader (above) — see that copy for the
|
|
2903
3431
|
// architectural rationale. The dynamic worker env is intentionally
|
|
2904
3432
|
// minimal; runtime callbacks use RUNTIME_API, file reads go through
|
|
2905
|
-
// HARNESS, and child workflow control uses the
|
|
2906
|
-
// run request.
|
|
3433
|
+
// HARNESS, and child workflow control uses the COORDINATOR binding.
|
|
2907
3434
|
HARNESS: env.HARNESS,
|
|
2908
3435
|
VERCEL_PROTECTION_BYPASS_TOKEN: env.VERCEL_PROTECTION_BYPASS_TOKEN,
|
|
2909
3436
|
...makeRuntimeApiEnvBinding(),
|
|
3437
|
+
...makeCoordinatorControlBinding(),
|
|
2910
3438
|
},
|
|
2911
3439
|
};
|
|
2912
3440
|
});
|
|
@@ -2930,31 +3458,42 @@ async function loadDynamicWorkerBundledCode(input: {
|
|
|
2930
3458
|
trace: CoordinatorPerfTraceSink;
|
|
2931
3459
|
}): Promise<string> {
|
|
2932
3460
|
const callbackStartedAt = Date.now();
|
|
2933
|
-
let codeSource: 'inline' | 'r2' = 'inline';
|
|
3461
|
+
let codeSource: 'inline' | 'r2' | 'memory' = 'inline';
|
|
2934
3462
|
let r2Ms = 0;
|
|
2935
|
-
const
|
|
2936
|
-
|
|
2937
|
-
:
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
3463
|
+
const codeCacheKey = dynamicWorkerBundledCodeCacheKey({
|
|
3464
|
+
artifactStorageKey: input.artifactStorageKey,
|
|
3465
|
+
artifactHash: input.metadata.artifactHash,
|
|
3466
|
+
});
|
|
3467
|
+
let bundledCode = input.metadata.dynamicWorkerCode ?? null;
|
|
3468
|
+
if (!bundledCode) {
|
|
3469
|
+
bundledCode = readDynamicWorkerBundledCodeCache(codeCacheKey);
|
|
3470
|
+
if (bundledCode) {
|
|
3471
|
+
codeSource = 'memory';
|
|
3472
|
+
}
|
|
3473
|
+
}
|
|
3474
|
+
if (!bundledCode) {
|
|
3475
|
+
codeSource = 'r2';
|
|
3476
|
+
const r2StartedAt = Date.now();
|
|
3477
|
+
try {
|
|
3478
|
+
const artifact = await loadStoredPlayArtifactFromR2(
|
|
3479
|
+
input.env,
|
|
3480
|
+
input.artifactStorageKey,
|
|
3481
|
+
);
|
|
3482
|
+
bundledCode = artifact?.artifact?.bundledCode ?? null;
|
|
3483
|
+
if (typeof bundledCode === 'string' && bundledCode.length > 0) {
|
|
3484
|
+
writeDynamicWorkerBundledCodeCache(codeCacheKey, bundledCode);
|
|
3485
|
+
}
|
|
3486
|
+
} finally {
|
|
3487
|
+
r2Ms = Date.now() - r2StartedAt;
|
|
3488
|
+
input.trace({
|
|
3489
|
+
runId: input.runIdForTrace,
|
|
3490
|
+
phase: 'coordinator.loader_code_r2_get',
|
|
3491
|
+
ms: r2Ms,
|
|
3492
|
+
graphHash: input.graphHash,
|
|
3493
|
+
extra: { artifactStorageKey: input.artifactStorageKey },
|
|
3494
|
+
});
|
|
3495
|
+
}
|
|
3496
|
+
}
|
|
2958
3497
|
if (typeof bundledCode !== 'string' || bundledCode.length === 0) {
|
|
2959
3498
|
throw new Error(
|
|
2960
3499
|
`Stored play artifact ${input.artifactStorageKey} does not contain bundledCode.`,
|
|
@@ -3046,6 +3585,192 @@ export default {
|
|
|
3046
3585
|
};
|
|
3047
3586
|
`;
|
|
3048
3587
|
|
|
3588
|
+
function isAllowedStagedFileStorageKey(key: string): boolean {
|
|
3589
|
+
if (!key || key.length > 2_048) return false;
|
|
3590
|
+
if (key.startsWith('/') || key.includes('..') || key.includes('\\')) {
|
|
3591
|
+
return false;
|
|
3592
|
+
}
|
|
3593
|
+
return key.startsWith('plays/v2/orgs/') || key.includes('/plays/v2/orgs/');
|
|
3594
|
+
}
|
|
3595
|
+
|
|
3596
|
+
function decodeBase64ToUint8Array(value: string): Uint8Array {
|
|
3597
|
+
const binary = atob(value);
|
|
3598
|
+
const bytes = new Uint8Array(binary.length);
|
|
3599
|
+
for (let index = 0; index < binary.length; index += 1) {
|
|
3600
|
+
bytes[index] = binary.charCodeAt(index);
|
|
3601
|
+
}
|
|
3602
|
+
return bytes;
|
|
3603
|
+
}
|
|
3604
|
+
|
|
3605
|
+
async function handleStagedFilePut(
|
|
3606
|
+
request: Request,
|
|
3607
|
+
env: CoordinatorEnv,
|
|
3608
|
+
): Promise<Response> {
|
|
3609
|
+
if (request.method !== 'POST') {
|
|
3610
|
+
return new Response('method not allowed', { status: 405 });
|
|
3611
|
+
}
|
|
3612
|
+
const url = new URL(request.url);
|
|
3613
|
+
const rawKey = url.searchParams.get('key')?.trim() ?? '';
|
|
3614
|
+
if (rawKey) {
|
|
3615
|
+
const key = rawKey;
|
|
3616
|
+
const contentType =
|
|
3617
|
+
url.searchParams.get('contentType')?.trim() ||
|
|
3618
|
+
'application/octet-stream';
|
|
3619
|
+
const expectedBytes = Number(url.searchParams.get('bytes') ?? 'NaN');
|
|
3620
|
+
if (!isAllowedStagedFileStorageKey(key)) {
|
|
3621
|
+
return Response.json(
|
|
3622
|
+
{ error: 'invalid staged file key' },
|
|
3623
|
+
{ status: 400 },
|
|
3624
|
+
);
|
|
3625
|
+
}
|
|
3626
|
+
if (!Number.isSafeInteger(expectedBytes) || expectedBytes < 0) {
|
|
3627
|
+
return Response.json({ error: 'bytes is required' }, { status: 400 });
|
|
3628
|
+
}
|
|
3629
|
+
const existing = await headExistingStagedFile(env, key, expectedBytes);
|
|
3630
|
+
if (existing.exists) {
|
|
3631
|
+
console.info('[perf][coordinator.staged_file_put]', {
|
|
3632
|
+
key,
|
|
3633
|
+
bytes: expectedBytes,
|
|
3634
|
+
headMs: existing.ms,
|
|
3635
|
+
putMs: 0,
|
|
3636
|
+
ms: existing.ms,
|
|
3637
|
+
transport: 'raw',
|
|
3638
|
+
skipped: true,
|
|
3639
|
+
});
|
|
3640
|
+
return Response.json({
|
|
3641
|
+
ok: true,
|
|
3642
|
+
key,
|
|
3643
|
+
bytes: expectedBytes,
|
|
3644
|
+
existed: true,
|
|
3645
|
+
timingsMs: { head: existing.ms, put: 0 },
|
|
3646
|
+
});
|
|
3647
|
+
}
|
|
3648
|
+
const readStartedAt = Date.now();
|
|
3649
|
+
const bytes = new Uint8Array(await request.arrayBuffer());
|
|
3650
|
+
const readMs = Date.now() - readStartedAt;
|
|
3651
|
+
if (bytes.byteLength !== expectedBytes) {
|
|
3652
|
+
return Response.json(
|
|
3653
|
+
{ error: 'staged file byte length mismatch' },
|
|
3654
|
+
{ status: 400 },
|
|
3655
|
+
);
|
|
3656
|
+
}
|
|
3657
|
+
const putStartedAt = Date.now();
|
|
3658
|
+
await env.PLAYS_BUCKET.put(key, bytes, {
|
|
3659
|
+
httpMetadata: { contentType },
|
|
3660
|
+
});
|
|
3661
|
+
const putMs = Date.now() - putStartedAt;
|
|
3662
|
+
console.info('[perf][coordinator.staged_file_put]', {
|
|
3663
|
+
key,
|
|
3664
|
+
bytes: bytes.byteLength,
|
|
3665
|
+
readMs,
|
|
3666
|
+
putMs,
|
|
3667
|
+
ms: readMs + putMs,
|
|
3668
|
+
transport: 'raw',
|
|
3669
|
+
});
|
|
3670
|
+
return Response.json({
|
|
3671
|
+
ok: true,
|
|
3672
|
+
key,
|
|
3673
|
+
bytes: bytes.byteLength,
|
|
3674
|
+
timingsMs: { read: readMs, put: putMs },
|
|
3675
|
+
});
|
|
3676
|
+
}
|
|
3677
|
+
const body = (await request.json().catch(() => null)) as
|
|
3678
|
+
| {
|
|
3679
|
+
key?: unknown;
|
|
3680
|
+
contentBase64?: unknown;
|
|
3681
|
+
contentType?: unknown;
|
|
3682
|
+
bytes?: unknown;
|
|
3683
|
+
}
|
|
3684
|
+
| null;
|
|
3685
|
+
const key = typeof body?.key === 'string' ? body.key.trim() : '';
|
|
3686
|
+
const contentBase64 =
|
|
3687
|
+
typeof body?.contentBase64 === 'string' ? body.contentBase64 : '';
|
|
3688
|
+
const contentType =
|
|
3689
|
+
typeof body?.contentType === 'string' && body.contentType.trim()
|
|
3690
|
+
? body.contentType.trim()
|
|
3691
|
+
: 'application/octet-stream';
|
|
3692
|
+
const expectedBytes = typeof body?.bytes === 'number' ? body.bytes : NaN;
|
|
3693
|
+
if (!isAllowedStagedFileStorageKey(key)) {
|
|
3694
|
+
return Response.json(
|
|
3695
|
+
{ error: 'invalid staged file key' },
|
|
3696
|
+
{ status: 400 },
|
|
3697
|
+
);
|
|
3698
|
+
}
|
|
3699
|
+
if (
|
|
3700
|
+
!contentBase64 ||
|
|
3701
|
+
!Number.isSafeInteger(expectedBytes) ||
|
|
3702
|
+
expectedBytes < 0
|
|
3703
|
+
) {
|
|
3704
|
+
return Response.json(
|
|
3705
|
+
{ error: 'contentBase64 and bytes are required' },
|
|
3706
|
+
{ status: 400 },
|
|
3707
|
+
);
|
|
3708
|
+
}
|
|
3709
|
+
const existing = await headExistingStagedFile(env, key, expectedBytes);
|
|
3710
|
+
if (existing.exists) {
|
|
3711
|
+
console.info('[perf][coordinator.staged_file_put]', {
|
|
3712
|
+
key,
|
|
3713
|
+
bytes: expectedBytes,
|
|
3714
|
+
headMs: existing.ms,
|
|
3715
|
+
putMs: 0,
|
|
3716
|
+
ms: existing.ms,
|
|
3717
|
+
transport: 'base64',
|
|
3718
|
+
skipped: true,
|
|
3719
|
+
});
|
|
3720
|
+
return Response.json({
|
|
3721
|
+
ok: true,
|
|
3722
|
+
key,
|
|
3723
|
+
bytes: expectedBytes,
|
|
3724
|
+
existed: true,
|
|
3725
|
+
timingsMs: { head: existing.ms, put: 0 },
|
|
3726
|
+
});
|
|
3727
|
+
}
|
|
3728
|
+
const decodeStartedAt = Date.now();
|
|
3729
|
+
const bytes = decodeBase64ToUint8Array(contentBase64);
|
|
3730
|
+
const decodeMs = Date.now() - decodeStartedAt;
|
|
3731
|
+
if (bytes.byteLength !== expectedBytes) {
|
|
3732
|
+
return Response.json(
|
|
3733
|
+
{ error: 'staged file byte length mismatch' },
|
|
3734
|
+
{ status: 400 },
|
|
3735
|
+
);
|
|
3736
|
+
}
|
|
3737
|
+
const putStartedAt = Date.now();
|
|
3738
|
+
await env.PLAYS_BUCKET.put(key, bytes, {
|
|
3739
|
+
httpMetadata: { contentType },
|
|
3740
|
+
});
|
|
3741
|
+
const putMs = Date.now() - putStartedAt;
|
|
3742
|
+
console.info('[perf][coordinator.staged_file_put]', {
|
|
3743
|
+
key,
|
|
3744
|
+
bytes: bytes.byteLength,
|
|
3745
|
+
decodeMs,
|
|
3746
|
+
putMs,
|
|
3747
|
+
ms: decodeMs + putMs,
|
|
3748
|
+
});
|
|
3749
|
+
return Response.json({
|
|
3750
|
+
ok: true,
|
|
3751
|
+
key,
|
|
3752
|
+
bytes: bytes.byteLength,
|
|
3753
|
+
timingsMs: { decode: decodeMs, put: putMs },
|
|
3754
|
+
});
|
|
3755
|
+
}
|
|
3756
|
+
|
|
3757
|
+
async function headExistingStagedFile(
|
|
3758
|
+
env: CoordinatorEnv,
|
|
3759
|
+
key: string,
|
|
3760
|
+
expectedBytes: number,
|
|
3761
|
+
): Promise<{ exists: boolean; ms: number }> {
|
|
3762
|
+
const startedAt = Date.now();
|
|
3763
|
+
const object = await env.PLAYS_BUCKET.head(key).catch(() => null);
|
|
3764
|
+
const ms = Date.now() - startedAt;
|
|
3765
|
+
if (!object) {
|
|
3766
|
+
return { exists: false, ms };
|
|
3767
|
+
}
|
|
3768
|
+
if (typeof object.size === 'number' && object.size !== expectedBytes) {
|
|
3769
|
+
return { exists: false, ms };
|
|
3770
|
+
}
|
|
3771
|
+
return { exists: true, ms };
|
|
3772
|
+
}
|
|
3773
|
+
|
|
3049
3774
|
async function handleCoordinatorWarmup(
|
|
3050
3775
|
request: Request,
|
|
3051
3776
|
env: CoordinatorEnv,
|
|
@@ -3219,6 +3944,7 @@ async function handleCoordinatorWarmup(
|
|
|
3219
3944
|
* worker uses its existing `fetch(req.baseUrl + path)` transport.
|
|
3220
3945
|
*/
|
|
3221
3946
|
let loggedMissingRuntimeApiExport = false;
|
|
3947
|
+
let loggedMissingCoordinatorControlExport = false;
|
|
3222
3948
|
|
|
3223
3949
|
function makeRuntimeApiEnvBinding():
|
|
3224
3950
|
| { RUNTIME_API: { fetch(req: Request): Promise<Response> } }
|
|
@@ -3241,20 +3967,28 @@ function makeRuntimeApiEnvBinding():
|
|
|
3241
3967
|
return { RUNTIME_API: ctor({ props: undefined }) };
|
|
3242
3968
|
}
|
|
3243
3969
|
|
|
3244
|
-
function makeCoordinatorControlBinding():
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3970
|
+
function makeCoordinatorControlBinding():
|
|
3971
|
+
| {
|
|
3972
|
+
COORDINATOR: {
|
|
3973
|
+
submitChild(
|
|
3974
|
+
parentRunId: string,
|
|
3975
|
+
body: Record<string, unknown>,
|
|
3976
|
+
): Promise<{ workflowId?: string; runId?: string; error?: unknown }>;
|
|
3977
|
+
signal(
|
|
3978
|
+
runId: string,
|
|
3979
|
+
body: Record<string, unknown>,
|
|
3980
|
+
): Promise<Record<string, unknown>>;
|
|
3981
|
+
recordPerfTrace(
|
|
3982
|
+
runId: string,
|
|
3983
|
+
payload: CoordinatorPerfTracePayload,
|
|
3984
|
+
): Promise<void>;
|
|
3985
|
+
recordRunEvent(
|
|
3986
|
+
runId: string,
|
|
3987
|
+
event: CoordinatorRunEvent,
|
|
3988
|
+
): Promise<void>;
|
|
3989
|
+
};
|
|
3990
|
+
}
|
|
3991
|
+
| Record<string, never> {
|
|
3258
3992
|
const exports = workersExports as unknown as {
|
|
3259
3993
|
CoordinatorControl?: (init: { props: undefined }) => {
|
|
3260
3994
|
submitChild(
|
|
@@ -3269,15 +4003,23 @@ function makeCoordinatorControlBinding(): {
|
|
|
3269
4003
|
runId: string,
|
|
3270
4004
|
payload: CoordinatorPerfTracePayload,
|
|
3271
4005
|
): Promise<void>;
|
|
4006
|
+
recordRunEvent(
|
|
4007
|
+
runId: string,
|
|
4008
|
+
event: CoordinatorRunEvent,
|
|
4009
|
+
): Promise<void>;
|
|
3272
4010
|
};
|
|
3273
4011
|
};
|
|
3274
4012
|
const ctor = exports.CoordinatorControl;
|
|
3275
4013
|
if (typeof ctor !== 'function') {
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
4014
|
+
if (!loggedMissingCoordinatorControlExport) {
|
|
4015
|
+
loggedMissingCoordinatorControlExport = true;
|
|
4016
|
+
console.warn(
|
|
4017
|
+
'[coordinator] CoordinatorControl is not registered on cloudflare:workers exports; using public coordinator transport.',
|
|
4018
|
+
);
|
|
4019
|
+
}
|
|
4020
|
+
return {};
|
|
3279
4021
|
}
|
|
3280
|
-
return ctor({ props: undefined });
|
|
4022
|
+
return { COORDINATOR: ctor({ props: undefined }) };
|
|
3281
4023
|
}
|
|
3282
4024
|
|
|
3283
4025
|
async function loadStoredPlayArtifactFromR2(
|