deepline 0.1.19 → 0.1.20
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 +54 -12
- package/dist/cli/index.mjs +54 -12
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -2
- package/dist/index.mjs +3 -2
- package/dist/repo/apps/play-runner-workers/src/coordinator-entry.ts +149 -68
- package/dist/repo/apps/play-runner-workers/src/dedup-do.ts +85 -60
- package/dist/repo/apps/play-runner-workers/src/entry.ts +144 -15
- package/dist/repo/sdk/src/http.ts +19 -3
- package/dist/repo/sdk/src/version.ts +1 -1
- package/dist/repo/shared_libs/plays/row-identity.ts +59 -4
- package/package.json +1 -1
|
@@ -156,6 +156,7 @@ interface CoordinatorEnv {
|
|
|
156
156
|
DEEPLINE_API_BASE_URL: string;
|
|
157
157
|
DEEPLINE_INTERNAL_TOKEN?: string;
|
|
158
158
|
DEEPLINE_TAIL_LOG_TOKEN?: string;
|
|
159
|
+
DEEPLINE_COORDINATOR_DEPLOY_MARKER?: string;
|
|
159
160
|
VERCEL_PROTECTION_BYPASS_TOKEN?: string;
|
|
160
161
|
DEEPLINE_PLAY_PREVIEW_SLUG?: string;
|
|
161
162
|
/**
|
|
@@ -369,6 +370,7 @@ const WORKFLOW_POOL_READY_TIMEOUT_MS = 1_500;
|
|
|
369
370
|
const WORKFLOW_POOL_READY_POLL_MS = 250;
|
|
370
371
|
const WORKFLOW_POOL_REFILL_ON_MISS_TIMEOUT_MS = 2_500;
|
|
371
372
|
const WORKFLOW_POOL_REFILL_ON_MISS_MIN_AVAILABLE = 1;
|
|
373
|
+
const WORKFLOW_POOL_CONTROL_TIMEOUT_MS = 750;
|
|
372
374
|
|
|
373
375
|
function buildDynamicWorkflowMetadata(
|
|
374
376
|
params: PlayWorkflowParams,
|
|
@@ -469,26 +471,48 @@ function workflowPoolDurableObject(env: CoordinatorEnv): DurableObjectStub {
|
|
|
469
471
|
async function callWorkflowPool<T>(
|
|
470
472
|
env: CoordinatorEnv,
|
|
471
473
|
path: string,
|
|
472
|
-
init?: RequestInit,
|
|
474
|
+
init?: RequestInit & { timeoutMs?: number },
|
|
473
475
|
): Promise<T> {
|
|
474
|
-
const
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
...init,
|
|
478
|
-
headers: {
|
|
479
|
-
'content-type': 'application/json',
|
|
480
|
-
...(init?.headers ?? {}),
|
|
481
|
-
},
|
|
482
|
-
},
|
|
476
|
+
const timeoutMs = Math.max(
|
|
477
|
+
1,
|
|
478
|
+
Math.floor(init?.timeoutMs ?? WORKFLOW_POOL_CONTROL_TIMEOUT_MS),
|
|
483
479
|
);
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
480
|
+
const controller = new AbortController();
|
|
481
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
482
|
+
try {
|
|
483
|
+
const fetchInit: RequestInit = { ...(init ?? {}) };
|
|
484
|
+
delete (fetchInit as { timeoutMs?: number }).timeoutMs;
|
|
485
|
+
delete fetchInit.signal;
|
|
486
|
+
const response = await workflowPoolDurableObject(env).fetch(
|
|
487
|
+
`https://deepline.workflow-pool.internal${path}`,
|
|
488
|
+
{
|
|
489
|
+
...fetchInit,
|
|
490
|
+
signal: controller.signal,
|
|
491
|
+
headers: {
|
|
492
|
+
'content-type': 'application/json',
|
|
493
|
+
...(init?.headers ?? {}),
|
|
494
|
+
},
|
|
495
|
+
},
|
|
489
496
|
);
|
|
497
|
+
if (!response.ok) {
|
|
498
|
+
throw new Error(
|
|
499
|
+
`workflow pool ${path} failed ${response.status}: ${(
|
|
500
|
+
await response.text().catch(() => '')
|
|
501
|
+
).slice(0, 400)}`,
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
return (await response.json()) as T;
|
|
505
|
+
} catch (error) {
|
|
506
|
+
if (
|
|
507
|
+
error instanceof Error &&
|
|
508
|
+
(error.name === 'AbortError' || error.message.includes('aborted'))
|
|
509
|
+
) {
|
|
510
|
+
throw new Error(`workflow pool ${path} timed out after ${timeoutMs}ms`);
|
|
511
|
+
}
|
|
512
|
+
throw error;
|
|
513
|
+
} finally {
|
|
514
|
+
clearTimeout(timer);
|
|
490
515
|
}
|
|
491
|
-
return (await response.json()) as T;
|
|
492
516
|
}
|
|
493
517
|
|
|
494
518
|
type WorkflowPoolCounts = {
|
|
@@ -903,7 +927,11 @@ async function submitViaPooledWorkflow(input: {
|
|
|
903
927
|
return null;
|
|
904
928
|
}
|
|
905
929
|
const leaseStartedAt = Date.now();
|
|
906
|
-
let
|
|
930
|
+
let leaseError: string | null = null;
|
|
931
|
+
let pooledInstanceId = await leaseWorkflowPoolId(input.env).catch((error) => {
|
|
932
|
+
leaseError = error instanceof Error ? error.message : String(error);
|
|
933
|
+
return null;
|
|
934
|
+
});
|
|
907
935
|
const missCounts = pooledInstanceId
|
|
908
936
|
? null
|
|
909
937
|
: await workflowPoolCount(input.env).catch(() => null);
|
|
@@ -913,6 +941,7 @@ async function submitViaPooledWorkflow(input: {
|
|
|
913
941
|
graphHash: input.params.graphHash ?? null,
|
|
914
942
|
extra: {
|
|
915
943
|
pooled: Boolean(pooledInstanceId),
|
|
944
|
+
...(leaseError ? { error: leaseError } : {}),
|
|
916
945
|
...(missCounts
|
|
917
946
|
? {
|
|
918
947
|
availableAfterMiss: missCounts.available,
|
|
@@ -952,12 +981,19 @@ async function submitViaPooledWorkflow(input: {
|
|
|
952
981
|
});
|
|
953
982
|
if (refillResult?.available) {
|
|
954
983
|
const retryStartedAt = Date.now();
|
|
955
|
-
|
|
984
|
+
let retryLeaseError: string | null = null;
|
|
985
|
+
pooledInstanceId = await leaseWorkflowPoolId(input.env).catch((error) => {
|
|
986
|
+
retryLeaseError = error instanceof Error ? error.message : String(error);
|
|
987
|
+
return null;
|
|
988
|
+
});
|
|
956
989
|
input.recordSubmitTiming({
|
|
957
990
|
phase: 'coordinator.workflow_pool_lease_retry',
|
|
958
991
|
ms: Date.now() - retryStartedAt,
|
|
959
992
|
graphHash: input.params.graphHash ?? null,
|
|
960
|
-
extra: {
|
|
993
|
+
extra: {
|
|
994
|
+
pooled: Boolean(pooledInstanceId),
|
|
995
|
+
...(retryLeaseError ? { error: retryLeaseError } : {}),
|
|
996
|
+
},
|
|
961
997
|
});
|
|
962
998
|
}
|
|
963
999
|
}
|
|
@@ -967,38 +1003,26 @@ async function submitViaPooledWorkflow(input: {
|
|
|
967
1003
|
}
|
|
968
1004
|
|
|
969
1005
|
const instance = await input.env.PLAY_WORKFLOW.get(pooledInstanceId);
|
|
1006
|
+
const readyCheckStartedAt = Date.now();
|
|
1007
|
+
const status = await instance.status().catch(() => null);
|
|
1008
|
+
const statusName = workflowStatusName(status);
|
|
1009
|
+
input.recordSubmitTiming({
|
|
1010
|
+
phase: 'coordinator.workflow_pool_ready_check',
|
|
1011
|
+
ms: Date.now() - readyCheckStartedAt,
|
|
1012
|
+
graphHash: input.params.graphHash ?? null,
|
|
1013
|
+
extra: { instanceId: pooledInstanceId, status: statusName },
|
|
1014
|
+
});
|
|
1015
|
+
if (!workflowPoolStatusIsReady(statusName)) {
|
|
1016
|
+
await instance.terminate().catch(() => undefined);
|
|
1017
|
+
disposeRpcStub(instance);
|
|
1018
|
+
return null;
|
|
1019
|
+
}
|
|
1020
|
+
const sendStartedAt = Date.now();
|
|
970
1021
|
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
1022
|
await instance.sendEvent({
|
|
987
1023
|
type: WORKFLOW_POOL_START_EVENT_TYPE,
|
|
988
1024
|
payload: buildDispatcherEnvelope(input.params),
|
|
989
1025
|
});
|
|
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
1026
|
} catch (error) {
|
|
1003
1027
|
disposeRpcStub(instance);
|
|
1004
1028
|
console.warn('[coordinator.workflow_pool] sendEvent failed; falling back', {
|
|
@@ -1008,6 +1032,27 @@ async function submitViaPooledWorkflow(input: {
|
|
|
1008
1032
|
});
|
|
1009
1033
|
return null;
|
|
1010
1034
|
}
|
|
1035
|
+
try {
|
|
1036
|
+
await mapRunToWorkflowInstance({
|
|
1037
|
+
env: input.env,
|
|
1038
|
+
runId: input.params.runId,
|
|
1039
|
+
instanceId: pooledInstanceId,
|
|
1040
|
+
});
|
|
1041
|
+
} catch (error) {
|
|
1042
|
+
disposeRpcStub(instance);
|
|
1043
|
+
throw new Error(
|
|
1044
|
+
`workflow pool mapRunToWorkflowInstance failed after pooled workflow start for ${input.params.runId}: ${
|
|
1045
|
+
error instanceof Error ? error.message : String(error)
|
|
1046
|
+
}`,
|
|
1047
|
+
);
|
|
1048
|
+
}
|
|
1049
|
+
input.recordSubmitTiming({
|
|
1050
|
+
phase: 'coordinator.workflow_pool_send_event',
|
|
1051
|
+
ms: Date.now() - sendStartedAt,
|
|
1052
|
+
graphHash: input.params.graphHash ?? null,
|
|
1053
|
+
extra: { instanceId: pooledInstanceId },
|
|
1054
|
+
});
|
|
1055
|
+
return instance;
|
|
1011
1056
|
}
|
|
1012
1057
|
|
|
1013
1058
|
function readWorkflowPayload(event: unknown): Record<string, unknown> | null {
|
|
@@ -2029,6 +2074,29 @@ const coordinatorEntrypoint = {
|
|
|
2029
2074
|
if (authError) return authError;
|
|
2030
2075
|
return await handleCoordinatorWarmup(request, env, ctx);
|
|
2031
2076
|
}
|
|
2077
|
+
if (url.pathname === '/tail-log-token/probe') {
|
|
2078
|
+
const authError = authorizeCoordinatorControlRequest({ request, env });
|
|
2079
|
+
if (authError) return authError;
|
|
2080
|
+
const expectedTailLogToken = env.DEEPLINE_TAIL_LOG_TOKEN?.trim();
|
|
2081
|
+
if (!expectedTailLogToken) {
|
|
2082
|
+
return Response.json(
|
|
2083
|
+
{ ok: false, error: 'tail log token is not configured' },
|
|
2084
|
+
{ status: 503 },
|
|
2085
|
+
);
|
|
2086
|
+
}
|
|
2087
|
+
const actualTailLogToken =
|
|
2088
|
+
request.headers.get('x-deepline-tail-log-token')?.trim() ?? '';
|
|
2089
|
+
if (actualTailLogToken !== expectedTailLogToken) {
|
|
2090
|
+
return Response.json(
|
|
2091
|
+
{ ok: false, error: 'tail log token mismatch' },
|
|
2092
|
+
{ status: 401 },
|
|
2093
|
+
);
|
|
2094
|
+
}
|
|
2095
|
+
return Response.json({
|
|
2096
|
+
ok: true,
|
|
2097
|
+
deployMarker: env.DEEPLINE_COORDINATOR_DEPLOY_MARKER ?? null,
|
|
2098
|
+
});
|
|
2099
|
+
}
|
|
2032
2100
|
if (url.pathname === '/workflow-pool/refill') {
|
|
2033
2101
|
const internalAuthError = authorizeCoordinatorControlRequest({
|
|
2034
2102
|
request,
|
|
@@ -2798,7 +2866,7 @@ function loadDynamicPlayWorkerSync(
|
|
|
2798
2866
|
}
|
|
2799
2867
|
const artifactIdentity =
|
|
2800
2868
|
metadata.artifactHash?.trim() || stableHash(artifactStorageKey);
|
|
2801
|
-
const workerCacheKey = `play:${graphHash}:${artifactIdentity}:harness=
|
|
2869
|
+
const workerCacheKey = `play:${graphHash}:${artifactIdentity}:harness=h5-runtime-api-coordinator`;
|
|
2802
2870
|
const runIdForTrace = metadata.runId ?? graphHash;
|
|
2803
2871
|
const loaderGetStartedAt = Date.now();
|
|
2804
2872
|
const stub = env.LOADER.get(workerCacheKey, async () => {
|
|
@@ -2829,6 +2897,10 @@ function loadDynamicPlayWorkerSync(
|
|
|
2829
2897
|
// tool execution, DB session, and artifact callbacks. This avoids a
|
|
2830
2898
|
// public fetch hop when Cloudflare exposes the RuntimeApi export.
|
|
2831
2899
|
...makeRuntimeApiEnvBinding(),
|
|
2900
|
+
// In-process coordinator control bridge used by ctx.runPlay and
|
|
2901
|
+
// parent terminal signals. This keeps scalar child plays inline with
|
|
2902
|
+
// the parent instead of round-tripping through nested Workflow waits.
|
|
2903
|
+
...makeCoordinatorControlBinding(),
|
|
2832
2904
|
// NOTE: We intentionally do NOT pass `env.PLAYS_BUCKET` (an R2Bucket
|
|
2833
2905
|
// binding) through to the per-play Worker's env. Including a raw
|
|
2834
2906
|
// R2Bucket in the dynamically-loaded Worker's env makes Cloudflare
|
|
@@ -2880,7 +2952,7 @@ async function loadDynamicPlayWorker(
|
|
|
2880
2952
|
}
|
|
2881
2953
|
const artifactIdentity =
|
|
2882
2954
|
metadata.artifactHash?.trim() || stableHash(artifactStorageKey);
|
|
2883
|
-
const workerCacheKey = `play:${graphHash}:${artifactIdentity}:harness=
|
|
2955
|
+
const workerCacheKey = `play:${graphHash}:${artifactIdentity}:harness=h5-runtime-api-coordinator`;
|
|
2884
2956
|
const runIdForTrace = metadata.runId ?? graphHash;
|
|
2885
2957
|
const loaderGetStartedAt = Date.now();
|
|
2886
2958
|
const stub = env.LOADER.get(workerCacheKey, async () => {
|
|
@@ -2902,11 +2974,11 @@ async function loadDynamicPlayWorker(
|
|
|
2902
2974
|
// Mirror of the sync loader (above) — see that copy for the
|
|
2903
2975
|
// architectural rationale. The dynamic worker env is intentionally
|
|
2904
2976
|
// minimal; runtime callbacks use RUNTIME_API, file reads go through
|
|
2905
|
-
// HARNESS, and child workflow control uses the
|
|
2906
|
-
// run request.
|
|
2977
|
+
// HARNESS, and child workflow control uses the COORDINATOR binding.
|
|
2907
2978
|
HARNESS: env.HARNESS,
|
|
2908
2979
|
VERCEL_PROTECTION_BYPASS_TOKEN: env.VERCEL_PROTECTION_BYPASS_TOKEN,
|
|
2909
2980
|
...makeRuntimeApiEnvBinding(),
|
|
2981
|
+
...makeCoordinatorControlBinding(),
|
|
2910
2982
|
},
|
|
2911
2983
|
};
|
|
2912
2984
|
});
|
|
@@ -3219,6 +3291,7 @@ async function handleCoordinatorWarmup(
|
|
|
3219
3291
|
* worker uses its existing `fetch(req.baseUrl + path)` transport.
|
|
3220
3292
|
*/
|
|
3221
3293
|
let loggedMissingRuntimeApiExport = false;
|
|
3294
|
+
let loggedMissingCoordinatorControlExport = false;
|
|
3222
3295
|
|
|
3223
3296
|
function makeRuntimeApiEnvBinding():
|
|
3224
3297
|
| { RUNTIME_API: { fetch(req: Request): Promise<Response> } }
|
|
@@ -3241,20 +3314,24 @@ function makeRuntimeApiEnvBinding():
|
|
|
3241
3314
|
return { RUNTIME_API: ctor({ props: undefined }) };
|
|
3242
3315
|
}
|
|
3243
3316
|
|
|
3244
|
-
function makeCoordinatorControlBinding():
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3317
|
+
function makeCoordinatorControlBinding():
|
|
3318
|
+
| {
|
|
3319
|
+
COORDINATOR: {
|
|
3320
|
+
submitChild(
|
|
3321
|
+
parentRunId: string,
|
|
3322
|
+
body: Record<string, unknown>,
|
|
3323
|
+
): Promise<{ workflowId?: string; runId?: string; error?: unknown }>;
|
|
3324
|
+
signal(
|
|
3325
|
+
runId: string,
|
|
3326
|
+
body: Record<string, unknown>,
|
|
3327
|
+
): Promise<Record<string, unknown>>;
|
|
3328
|
+
recordPerfTrace(
|
|
3329
|
+
runId: string,
|
|
3330
|
+
payload: CoordinatorPerfTracePayload,
|
|
3331
|
+
): Promise<void>;
|
|
3332
|
+
};
|
|
3333
|
+
}
|
|
3334
|
+
| Record<string, never> {
|
|
3258
3335
|
const exports = workersExports as unknown as {
|
|
3259
3336
|
CoordinatorControl?: (init: { props: undefined }) => {
|
|
3260
3337
|
submitChild(
|
|
@@ -3273,11 +3350,15 @@ function makeCoordinatorControlBinding(): {
|
|
|
3273
3350
|
};
|
|
3274
3351
|
const ctor = exports.CoordinatorControl;
|
|
3275
3352
|
if (typeof ctor !== 'function') {
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3353
|
+
if (!loggedMissingCoordinatorControlExport) {
|
|
3354
|
+
loggedMissingCoordinatorControlExport = true;
|
|
3355
|
+
console.warn(
|
|
3356
|
+
'[coordinator] CoordinatorControl is not registered on cloudflare:workers exports; using public coordinator transport.',
|
|
3357
|
+
);
|
|
3358
|
+
}
|
|
3359
|
+
return {};
|
|
3279
3360
|
}
|
|
3280
|
-
return ctor({ props: undefined });
|
|
3361
|
+
return { COORDINATOR: ctor({ props: undefined }) };
|
|
3281
3362
|
}
|
|
3282
3363
|
|
|
3283
3364
|
async function loadStoredPlayArtifactFromR2(
|
|
@@ -87,6 +87,7 @@ const COORDINATOR_TRACE_MAX_ENTRIES = 200;
|
|
|
87
87
|
const FINISH_ALARM_DELAY_MS = 60_000; // self-evict 1 min after finish() called
|
|
88
88
|
const WORKFLOW_POOL_DEFAULT_TTL_MS = 8 * 60 * 1000;
|
|
89
89
|
const WORKFLOW_POOL_RUN_MAPPING_TTL_MS = 60 * 60 * 1000;
|
|
90
|
+
const WORKFLOW_POOL_READY_MAX_AGE_MS = 20_000;
|
|
90
91
|
|
|
91
92
|
interface DedupEnv {
|
|
92
93
|
PLAY_DEDUP: DurableObjectNamespace;
|
|
@@ -248,7 +249,9 @@ export class PlayDedup implements DurableObject {
|
|
|
248
249
|
}
|
|
249
250
|
}
|
|
250
251
|
|
|
251
|
-
return new Response('{}', {
|
|
252
|
+
return new Response('{}', {
|
|
253
|
+
headers: { 'content-type': 'application/json' },
|
|
254
|
+
});
|
|
252
255
|
}
|
|
253
256
|
|
|
254
257
|
private async handleAwait(req: Request): Promise<Response> {
|
|
@@ -313,7 +316,9 @@ export class PlayDedup implements DurableObject {
|
|
|
313
316
|
// Schedule alarm to evict after a grace period (any straggler awaits
|
|
314
317
|
// get a chance to resolve). Grace = 60s.
|
|
315
318
|
await this.state.storage.setAlarm(Date.now() + FINISH_ALARM_DELAY_MS);
|
|
316
|
-
return new Response('{}', {
|
|
319
|
+
return new Response('{}', {
|
|
320
|
+
headers: { 'content-type': 'application/json' },
|
|
321
|
+
});
|
|
317
322
|
}
|
|
318
323
|
|
|
319
324
|
private async handleDebug(_req: Request): Promise<Response> {
|
|
@@ -321,7 +326,11 @@ export class PlayDedup implements DurableObject {
|
|
|
321
326
|
const dump: Record<string, DedupEntry> = {};
|
|
322
327
|
for (const [k, v] of all) dump[k] = v as DedupEntry;
|
|
323
328
|
return new Response(
|
|
324
|
-
JSON.stringify({
|
|
329
|
+
JSON.stringify({
|
|
330
|
+
size: all.size,
|
|
331
|
+
entries: dump,
|
|
332
|
+
waiters: this.waiters.size,
|
|
333
|
+
}),
|
|
325
334
|
{ headers: { 'content-type': 'application/json' } },
|
|
326
335
|
);
|
|
327
336
|
}
|
|
@@ -339,7 +348,8 @@ export class PlayDedup implements DurableObject {
|
|
|
339
348
|
value.entry !== undefined &&
|
|
340
349
|
value.entry.version === version &&
|
|
341
350
|
value.entry.expiresAt > now &&
|
|
342
|
-
value.entry.readyAt !== null
|
|
351
|
+
value.entry.readyAt !== null &&
|
|
352
|
+
now - value.entry.readyAt <= WORKFLOW_POOL_READY_MAX_AGE_MS
|
|
343
353
|
);
|
|
344
354
|
}
|
|
345
355
|
|
|
@@ -360,6 +370,8 @@ export class PlayDedup implements DurableObject {
|
|
|
360
370
|
if (
|
|
361
371
|
!entry ||
|
|
362
372
|
entry.expiresAt <= now ||
|
|
373
|
+
(entry.readyAt !== null &&
|
|
374
|
+
now - entry.readyAt > WORKFLOW_POOL_READY_MAX_AGE_MS) ||
|
|
363
375
|
(version && entry.version !== version)
|
|
364
376
|
) {
|
|
365
377
|
expiredKeys.push(key);
|
|
@@ -380,16 +392,15 @@ export class PlayDedup implements DurableObject {
|
|
|
380
392
|
}
|
|
381
393
|
|
|
382
394
|
private async handlePoolAdd(req: Request): Promise<Response> {
|
|
383
|
-
const body = (await req.json().catch(() => null)) as
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
const version = typeof body?.version === 'string' ? body.version.trim() : '';
|
|
395
|
+
const body = (await req.json().catch(() => null)) as {
|
|
396
|
+
ids?: unknown;
|
|
397
|
+
ttlMs?: unknown;
|
|
398
|
+
version?: unknown;
|
|
399
|
+
readyAt?: unknown;
|
|
400
|
+
ready?: unknown;
|
|
401
|
+
} | null;
|
|
402
|
+
const version =
|
|
403
|
+
typeof body?.version === 'string' ? body.version.trim() : '';
|
|
393
404
|
if (!version) {
|
|
394
405
|
return new Response('version is required', { status: 400 });
|
|
395
406
|
}
|
|
@@ -445,7 +456,9 @@ export class PlayDedup implements DurableObject {
|
|
|
445
456
|
const sorted = [...entries.entries()]
|
|
446
457
|
.map(([key, entry]) => ({ key, entry }))
|
|
447
458
|
.filter((entry) => this.isReadyWorkflowPoolEntry(entry, version, now))
|
|
448
|
-
|
|
459
|
+
// Lease the freshest ready instance. Older waiting Workflows are more
|
|
460
|
+
// likely to be hibernated, so "pooled" otherwise still pays a wake tax.
|
|
461
|
+
.sort((a, b) => b.entry.readyAt - a.entry.readyAt);
|
|
449
462
|
const selected = sorted[0];
|
|
450
463
|
if (!selected) return;
|
|
451
464
|
leasedId = selected.entry.id;
|
|
@@ -461,7 +474,8 @@ export class PlayDedup implements DurableObject {
|
|
|
461
474
|
if (!version) {
|
|
462
475
|
return new Response('version is required', { status: 400 });
|
|
463
476
|
}
|
|
464
|
-
|
|
477
|
+
const now = Date.now();
|
|
478
|
+
await this.gcWorkflowPool(now, version);
|
|
465
479
|
const entries = await this.state.storage.list<WorkflowPoolEntry>({
|
|
466
480
|
prefix: WORKFLOW_POOL_KEY_PREFIX,
|
|
467
481
|
});
|
|
@@ -469,8 +483,14 @@ export class PlayDedup implements DurableObject {
|
|
|
469
483
|
let warming = 0;
|
|
470
484
|
for (const entry of entries.values()) {
|
|
471
485
|
if (entry.version !== version) continue;
|
|
472
|
-
if (
|
|
473
|
-
|
|
486
|
+
if (
|
|
487
|
+
entry.readyAt === null ||
|
|
488
|
+
now - entry.readyAt > WORKFLOW_POOL_READY_MAX_AGE_MS
|
|
489
|
+
) {
|
|
490
|
+
warming += 1;
|
|
491
|
+
} else {
|
|
492
|
+
available += 1;
|
|
493
|
+
}
|
|
474
494
|
}
|
|
475
495
|
return new Response(JSON.stringify({ available, warming }), {
|
|
476
496
|
headers: { 'content-type': 'application/json' },
|
|
@@ -502,13 +522,12 @@ export class PlayDedup implements DurableObject {
|
|
|
502
522
|
}
|
|
503
523
|
|
|
504
524
|
private async handlePoolPromote(req: Request): Promise<Response> {
|
|
505
|
-
const body = (await req.json().catch(() => null)) as
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
const version = typeof body?.version === 'string' ? body.version.trim() : '';
|
|
525
|
+
const body = (await req.json().catch(() => null)) as {
|
|
526
|
+
ids?: unknown;
|
|
527
|
+
version?: unknown;
|
|
528
|
+
} | null;
|
|
529
|
+
const version =
|
|
530
|
+
typeof body?.version === 'string' ? body.version.trim() : '';
|
|
512
531
|
if (!version) {
|
|
513
532
|
return new Response('version is required', { status: 400 });
|
|
514
533
|
}
|
|
@@ -536,19 +555,21 @@ export class PlayDedup implements DurableObject {
|
|
|
536
555
|
await this.state.storage.put(writes);
|
|
537
556
|
}
|
|
538
557
|
});
|
|
539
|
-
return new Response(
|
|
540
|
-
|
|
541
|
-
|
|
558
|
+
return new Response(
|
|
559
|
+
JSON.stringify({ promoted: Object.keys(writes).length }),
|
|
560
|
+
{
|
|
561
|
+
headers: { 'content-type': 'application/json' },
|
|
562
|
+
},
|
|
563
|
+
);
|
|
542
564
|
}
|
|
543
565
|
|
|
544
566
|
private async handlePoolDelete(req: Request): Promise<Response> {
|
|
545
|
-
const body = (await req.json().catch(() => null)) as
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
const version = typeof body?.version === 'string' ? body.version.trim() : '';
|
|
567
|
+
const body = (await req.json().catch(() => null)) as {
|
|
568
|
+
ids?: unknown;
|
|
569
|
+
version?: unknown;
|
|
570
|
+
} | null;
|
|
571
|
+
const version =
|
|
572
|
+
typeof body?.version === 'string' ? body.version.trim() : '';
|
|
552
573
|
if (!version) {
|
|
553
574
|
return new Response('version is required', { status: 400 });
|
|
554
575
|
}
|
|
@@ -575,34 +596,32 @@ export class PlayDedup implements DurableObject {
|
|
|
575
596
|
}
|
|
576
597
|
|
|
577
598
|
private async handlePoolMapRun(req: Request): Promise<Response> {
|
|
578
|
-
const body = (await req.json().catch(() => null)) as
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
}
|
|
584
|
-
| null;
|
|
599
|
+
const body = (await req.json().catch(() => null)) as {
|
|
600
|
+
runId?: unknown;
|
|
601
|
+
instanceId?: unknown;
|
|
602
|
+
version?: unknown;
|
|
603
|
+
} | null;
|
|
585
604
|
const runId = typeof body?.runId === 'string' ? body.runId : '';
|
|
586
605
|
const instanceId =
|
|
587
606
|
typeof body?.instanceId === 'string' ? body.instanceId : '';
|
|
588
|
-
const version =
|
|
607
|
+
const version =
|
|
608
|
+
typeof body?.version === 'string' ? body.version.trim() : '';
|
|
589
609
|
if (!runId || !instanceId || !version) {
|
|
590
610
|
return new Response('runId, instanceId, and version are required', {
|
|
591
611
|
status: 400,
|
|
592
612
|
});
|
|
593
613
|
}
|
|
594
614
|
const now = Date.now();
|
|
595
|
-
await this.state.storage.put(
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
);
|
|
605
|
-
return new Response('{}', { headers: { 'content-type': 'application/json' } });
|
|
615
|
+
await this.state.storage.put(`${WORKFLOW_POOL_RUN_KEY_PREFIX}${runId}`, {
|
|
616
|
+
runId,
|
|
617
|
+
instanceId,
|
|
618
|
+
version,
|
|
619
|
+
createdAt: now,
|
|
620
|
+
expiresAt: now + WORKFLOW_POOL_RUN_MAPPING_TTL_MS,
|
|
621
|
+
} satisfies WorkflowRunMapping);
|
|
622
|
+
return new Response('{}', {
|
|
623
|
+
headers: { 'content-type': 'application/json' },
|
|
624
|
+
});
|
|
606
625
|
}
|
|
607
626
|
|
|
608
627
|
private async handlePoolResolveRun(req: Request): Promise<Response> {
|
|
@@ -620,7 +639,9 @@ export class PlayDedup implements DurableObject {
|
|
|
620
639
|
`${WORKFLOW_POOL_RUN_KEY_PREFIX}${runId}`,
|
|
621
640
|
);
|
|
622
641
|
if (mapping && mapping.version !== version) {
|
|
623
|
-
await this.state.storage.delete(
|
|
642
|
+
await this.state.storage.delete(
|
|
643
|
+
`${WORKFLOW_POOL_RUN_KEY_PREFIX}${runId}`,
|
|
644
|
+
);
|
|
624
645
|
return new Response(JSON.stringify({ instanceId: null }), {
|
|
625
646
|
headers: { 'content-type': 'application/json' },
|
|
626
647
|
});
|
|
@@ -658,9 +679,9 @@ export class PlayDedup implements DurableObject {
|
|
|
658
679
|
}
|
|
659
680
|
|
|
660
681
|
private async handleTraceAdd(req: Request): Promise<Response> {
|
|
661
|
-
const body = (await req
|
|
662
|
-
|
|
663
|
-
| null;
|
|
682
|
+
const body = (await req
|
|
683
|
+
.json()
|
|
684
|
+
.catch(() => null)) as Partial<CoordinatorTraceEntry> | null;
|
|
664
685
|
if (
|
|
665
686
|
!body ||
|
|
666
687
|
(body.source !== 'coordinator' && body.source !== 'dynamic_worker') ||
|
|
@@ -693,7 +714,9 @@ export class PlayDedup implements DurableObject {
|
|
|
693
714
|
await this.state.storage.delete([...entries.keys()].slice(0, overflow));
|
|
694
715
|
}
|
|
695
716
|
});
|
|
696
|
-
return new Response('{}', {
|
|
717
|
+
return new Response('{}', {
|
|
718
|
+
headers: { 'content-type': 'application/json' },
|
|
719
|
+
});
|
|
697
720
|
}
|
|
698
721
|
|
|
699
722
|
private async handleTraceList(): Promise<Response> {
|
|
@@ -702,7 +725,9 @@ export class PlayDedup implements DurableObject {
|
|
|
702
725
|
});
|
|
703
726
|
return new Response(
|
|
704
727
|
JSON.stringify({
|
|
705
|
-
entries: [...entries.values()].sort(
|
|
728
|
+
entries: [...entries.values()].sort(
|
|
729
|
+
(left, right) => left.ts - right.ts,
|
|
730
|
+
),
|
|
706
731
|
}),
|
|
707
732
|
{ headers: { 'content-type': 'application/json' } },
|
|
708
733
|
);
|