deepline 0.1.20 → 0.1.22
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 +845 -337
- package/dist/cli/index.mjs +902 -389
- package/dist/index.d.mts +23 -58
- package/dist/index.d.ts +23 -58
- package/dist/index.js +195 -92
- package/dist/index.mjs +195 -92
- package/dist/repo/apps/play-runner-workers/src/coordinator-entry.ts +888 -227
- package/dist/repo/apps/play-runner-workers/src/dedup-do.ts +540 -36
- package/dist/repo/apps/play-runner-workers/src/entry.ts +477 -374
- package/dist/repo/sdk/src/client.ts +245 -118
- package/dist/repo/sdk/src/http.ts +19 -1
- package/dist/repo/sdk/src/plays/harness-stub.ts +12 -0
- package/dist/repo/sdk/src/types.ts +8 -14
- package/dist/repo/sdk/src/version.ts +1 -1
- package/dist/repo/shared_libs/play-runtime/profiles.ts +4 -14
- package/dist/repo/shared_libs/play-runtime/runtime-actions.ts +1 -1
- package/dist/repo/shared_libs/play-runtime/scheduler-backend.ts +3 -6
- package/package.json +1 -1
|
@@ -48,17 +48,26 @@ type AwaitRequest = {
|
|
|
48
48
|
timeoutMs: number;
|
|
49
49
|
};
|
|
50
50
|
|
|
51
|
+
type WorkflowPoolEntryState = 'warming' | 'ready';
|
|
52
|
+
|
|
51
53
|
type WorkflowPoolEntry = {
|
|
52
54
|
id: string;
|
|
53
55
|
version: string;
|
|
56
|
+
state: WorkflowPoolEntryState;
|
|
54
57
|
createdAt: number;
|
|
55
58
|
readyAt: number | null;
|
|
56
59
|
expiresAt: number;
|
|
57
60
|
};
|
|
58
61
|
|
|
62
|
+
type WorkflowRunMappingState = 'claimed' | 'started' | 'blocked';
|
|
63
|
+
|
|
59
64
|
type WorkflowRunMapping = {
|
|
60
65
|
runId: string;
|
|
61
66
|
instanceId: string;
|
|
67
|
+
state: WorkflowRunMappingState;
|
|
68
|
+
blockedInstanceId?: string | null;
|
|
69
|
+
claimedAt?: number | null;
|
|
70
|
+
startedAt?: number | null;
|
|
62
71
|
version: string;
|
|
63
72
|
createdAt: number;
|
|
64
73
|
expiresAt: number;
|
|
@@ -74,20 +83,83 @@ type CoordinatorTraceEntry = {
|
|
|
74
83
|
[key: string]: unknown;
|
|
75
84
|
};
|
|
76
85
|
|
|
86
|
+
type CoordinatorTerminalState = {
|
|
87
|
+
runId: string;
|
|
88
|
+
status: 'completed' | 'failed' | 'cancelled';
|
|
89
|
+
result?: unknown;
|
|
90
|
+
error?: string | null;
|
|
91
|
+
totalRows?: unknown;
|
|
92
|
+
durationMs?: unknown;
|
|
93
|
+
playName?: string | null;
|
|
94
|
+
completedAt: number;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
type CoordinatorRunEvent =
|
|
98
|
+
| {
|
|
99
|
+
seq: number;
|
|
100
|
+
runId: string;
|
|
101
|
+
type: 'status';
|
|
102
|
+
status: string;
|
|
103
|
+
ts: number;
|
|
104
|
+
logs?: string[];
|
|
105
|
+
}
|
|
106
|
+
| {
|
|
107
|
+
seq: number;
|
|
108
|
+
runId: string;
|
|
109
|
+
type: 'log';
|
|
110
|
+
line: string;
|
|
111
|
+
ts: number;
|
|
112
|
+
}
|
|
113
|
+
| {
|
|
114
|
+
seq: number;
|
|
115
|
+
runId: string;
|
|
116
|
+
type: 'progress';
|
|
117
|
+
status: string;
|
|
118
|
+
ts: number;
|
|
119
|
+
logs?: string[];
|
|
120
|
+
activeNodeId?: string | null;
|
|
121
|
+
activeArtifactTableNamespace?: string | null;
|
|
122
|
+
updatedAt?: number | null;
|
|
123
|
+
}
|
|
124
|
+
| {
|
|
125
|
+
seq: number;
|
|
126
|
+
runId: string;
|
|
127
|
+
type: 'terminal';
|
|
128
|
+
status: 'completed' | 'failed' | 'cancelled';
|
|
129
|
+
ts: number;
|
|
130
|
+
result?: unknown;
|
|
131
|
+
error?: string | null;
|
|
132
|
+
totalRows?: unknown;
|
|
133
|
+
durationMs?: unknown;
|
|
134
|
+
playName?: string | null;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
type OmitRunEventSequence<T> = T extends unknown ? Omit<T, 'seq'> : never;
|
|
138
|
+
type CoordinatorRunEventInput = OmitRunEventSequence<CoordinatorRunEvent>;
|
|
139
|
+
|
|
77
140
|
type ReadyWorkflowPoolEntryRecord = {
|
|
78
141
|
key: string;
|
|
79
|
-
entry: WorkflowPoolEntry & { readyAt: number };
|
|
142
|
+
entry: WorkflowPoolEntry & { state: 'ready'; readyAt: number };
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
type WorkflowPoolCounts = {
|
|
146
|
+
available: number;
|
|
147
|
+
warming: number;
|
|
80
148
|
};
|
|
81
149
|
|
|
82
150
|
const DEDUP_KEY_PREFIX = 'd:';
|
|
83
151
|
const WORKFLOW_POOL_KEY_PREFIX = 'p:';
|
|
84
152
|
const WORKFLOW_POOL_RUN_KEY_PREFIX = 'm:';
|
|
85
153
|
const COORDINATOR_TRACE_KEY_PREFIX = 't:';
|
|
154
|
+
const COORDINATOR_RUN_EVENT_KEY_PREFIX = 'e:';
|
|
155
|
+
const COORDINATOR_TERMINAL_KEY = 'terminal';
|
|
156
|
+
const COORDINATOR_RUN_EVENT_SEQUENCE_KEY = 'event-seq';
|
|
86
157
|
const COORDINATOR_TRACE_MAX_ENTRIES = 200;
|
|
158
|
+
const COORDINATOR_RUN_EVENT_MAX_ENTRIES = 500;
|
|
87
159
|
const FINISH_ALARM_DELAY_MS = 60_000; // self-evict 1 min after finish() called
|
|
88
160
|
const WORKFLOW_POOL_DEFAULT_TTL_MS = 8 * 60 * 1000;
|
|
89
161
|
const WORKFLOW_POOL_RUN_MAPPING_TTL_MS = 60 * 60 * 1000;
|
|
90
|
-
const WORKFLOW_POOL_READY_MAX_AGE_MS =
|
|
162
|
+
const WORKFLOW_POOL_READY_MAX_AGE_MS = 7 * 60_000;
|
|
91
163
|
|
|
92
164
|
interface DedupEnv {
|
|
93
165
|
PLAY_DEDUP: DurableObjectNamespace;
|
|
@@ -97,6 +169,8 @@ export class PlayDedup implements DurableObject {
|
|
|
97
169
|
// Promises waiting on /await endpoints. Keyed by `${toolId}:${inputHash}`.
|
|
98
170
|
// Resolved by /publish; rejected on timeout.
|
|
99
171
|
private waiters: Map<string, Set<(value: unknown) => void>> = new Map();
|
|
172
|
+
private runEventWaiters: Set<(value: CoordinatorRunEvent) => void> =
|
|
173
|
+
new Set();
|
|
100
174
|
|
|
101
175
|
constructor(
|
|
102
176
|
private readonly state: DurableObjectState,
|
|
@@ -111,6 +185,10 @@ export class PlayDedup implements DurableObject {
|
|
|
111
185
|
return `${COORDINATOR_TRACE_KEY_PREFIX}${String(entry.ts).padStart(13, '0')}:${crypto.randomUUID().slice(0, 8)}`;
|
|
112
186
|
}
|
|
113
187
|
|
|
188
|
+
private runEventKey(event: CoordinatorRunEvent): string {
|
|
189
|
+
return `${COORDINATOR_RUN_EVENT_KEY_PREFIX}${String(event.seq).padStart(13, '0')}`;
|
|
190
|
+
}
|
|
191
|
+
|
|
114
192
|
private waiterKey(toolId: string, inputHash: string): string {
|
|
115
193
|
return `${toolId}:${inputHash}`;
|
|
116
194
|
}
|
|
@@ -131,18 +209,22 @@ export class PlayDedup implements DurableObject {
|
|
|
131
209
|
return await this.handleDebug(req);
|
|
132
210
|
case '/pool-add':
|
|
133
211
|
return await this.handlePoolAdd(req);
|
|
134
|
-
case '/pool-
|
|
135
|
-
return await this.
|
|
212
|
+
case '/pool-claim':
|
|
213
|
+
return await this.handlePoolClaim(req);
|
|
136
214
|
case '/pool-count':
|
|
137
215
|
return await this.handlePoolCount(req);
|
|
138
216
|
case '/pool-list':
|
|
139
217
|
return await this.handlePoolList(req);
|
|
140
218
|
case '/pool-promote':
|
|
141
219
|
return await this.handlePoolPromote(req);
|
|
220
|
+
case '/pool-ready':
|
|
221
|
+
return await this.handlePoolReady(req);
|
|
142
222
|
case '/pool-delete':
|
|
143
223
|
return await this.handlePoolDelete(req);
|
|
144
224
|
case '/pool-map-run':
|
|
145
225
|
return await this.handlePoolMapRun(req);
|
|
226
|
+
case '/pool-block-run':
|
|
227
|
+
return await this.handlePoolBlockRun(req);
|
|
146
228
|
case '/pool-resolve-run':
|
|
147
229
|
return await this.handlePoolResolveRun(req);
|
|
148
230
|
case '/pool-clear':
|
|
@@ -151,6 +233,14 @@ export class PlayDedup implements DurableObject {
|
|
|
151
233
|
return await this.handleTraceAdd(req);
|
|
152
234
|
case '/trace-list':
|
|
153
235
|
return await this.handleTraceList();
|
|
236
|
+
case '/event-add':
|
|
237
|
+
return await this.handleRunEventAdd(req);
|
|
238
|
+
case '/event-list':
|
|
239
|
+
return await this.handleRunEventList(req);
|
|
240
|
+
case '/terminal-set':
|
|
241
|
+
return await this.handleTerminalSet(req);
|
|
242
|
+
case '/terminal-get':
|
|
243
|
+
return await this.handleTerminalGet();
|
|
154
244
|
default:
|
|
155
245
|
return new Response('not found', { status: 404 });
|
|
156
246
|
}
|
|
@@ -168,6 +258,7 @@ export class PlayDedup implements DurableObject {
|
|
|
168
258
|
// be garbage-collected by CF's hot pool.
|
|
169
259
|
await this.state.storage.deleteAll();
|
|
170
260
|
this.waiters.clear();
|
|
261
|
+
this.runEventWaiters.clear();
|
|
171
262
|
}
|
|
172
263
|
|
|
173
264
|
private async handleLookupOrClaim(req: Request): Promise<Response> {
|
|
@@ -339,17 +430,28 @@ export class PlayDedup implements DurableObject {
|
|
|
339
430
|
return new URL(req.url).searchParams.get('version')?.trim() ?? '';
|
|
340
431
|
}
|
|
341
432
|
|
|
433
|
+
private workflowPoolMinReadyAgeMs(req: Request): number {
|
|
434
|
+
const raw = Number(new URL(req.url).searchParams.get('minReadyAgeMs') ?? 0);
|
|
435
|
+
if (!Number.isFinite(raw) || raw <= 0) {
|
|
436
|
+
return 0;
|
|
437
|
+
}
|
|
438
|
+
return Math.min(Math.floor(raw), WORKFLOW_POOL_DEFAULT_TTL_MS);
|
|
439
|
+
}
|
|
440
|
+
|
|
342
441
|
private isReadyWorkflowPoolEntry(
|
|
343
442
|
value: { key: string; entry: WorkflowPoolEntry | undefined },
|
|
344
443
|
version: string,
|
|
345
444
|
now: number,
|
|
445
|
+
minReadyAgeMs = 0,
|
|
346
446
|
): value is ReadyWorkflowPoolEntryRecord {
|
|
347
447
|
return (
|
|
348
448
|
value.entry !== undefined &&
|
|
349
449
|
value.entry.version === version &&
|
|
350
450
|
value.entry.expiresAt > now &&
|
|
451
|
+
value.entry.state === 'ready' &&
|
|
351
452
|
value.entry.readyAt !== null &&
|
|
352
|
-
now - value.entry.readyAt <= WORKFLOW_POOL_READY_MAX_AGE_MS
|
|
453
|
+
now - value.entry.readyAt <= WORKFLOW_POOL_READY_MAX_AGE_MS &&
|
|
454
|
+
now - value.entry.readyAt >= minReadyAgeMs
|
|
353
455
|
);
|
|
354
456
|
}
|
|
355
457
|
|
|
@@ -370,7 +472,8 @@ export class PlayDedup implements DurableObject {
|
|
|
370
472
|
if (
|
|
371
473
|
!entry ||
|
|
372
474
|
entry.expiresAt <= now ||
|
|
373
|
-
(entry.
|
|
475
|
+
(entry.state === 'ready' &&
|
|
476
|
+
entry.readyAt !== null &&
|
|
374
477
|
now - entry.readyAt > WORKFLOW_POOL_READY_MAX_AGE_MS) ||
|
|
375
478
|
(version && entry.version !== version)
|
|
376
479
|
) {
|
|
@@ -420,18 +523,34 @@ export class PlayDedup implements DurableObject {
|
|
|
420
523
|
body.ttlMs > 0
|
|
421
524
|
? Math.min(body.ttlMs, WORKFLOW_POOL_DEFAULT_TTL_MS)
|
|
422
525
|
: WORKFLOW_POOL_DEFAULT_TTL_MS;
|
|
423
|
-
const writes: Record<string, WorkflowPoolEntry> = {};
|
|
424
|
-
for (const id of ids) {
|
|
425
|
-
writes[`${WORKFLOW_POOL_KEY_PREFIX}${id}`] = {
|
|
426
|
-
id,
|
|
427
|
-
version,
|
|
428
|
-
createdAt: now,
|
|
429
|
-
readyAt: ready ? readyAt : null,
|
|
430
|
-
expiresAt: now + ttlMs,
|
|
431
|
-
};
|
|
432
|
-
}
|
|
433
526
|
await this.state.blockConcurrencyWhile(async () => {
|
|
434
527
|
await this.gcWorkflowPool(now, version);
|
|
528
|
+
const writes: Record<string, WorkflowPoolEntry> = {};
|
|
529
|
+
const keys = ids.map((id) => `${WORKFLOW_POOL_KEY_PREFIX}${id}`);
|
|
530
|
+
const existing = (await this.state.storage.get<WorkflowPoolEntry>(
|
|
531
|
+
keys,
|
|
532
|
+
)) as Map<string, WorkflowPoolEntry>;
|
|
533
|
+
for (const id of ids) {
|
|
534
|
+
const key = `${WORKFLOW_POOL_KEY_PREFIX}${id}`;
|
|
535
|
+
const existingEntry = existing.get(key);
|
|
536
|
+
const existingReadyAt =
|
|
537
|
+
existingEntry?.version === version &&
|
|
538
|
+
existingEntry.expiresAt > now &&
|
|
539
|
+
existingEntry.state === 'ready' &&
|
|
540
|
+
existingEntry.readyAt !== null
|
|
541
|
+
? existingEntry.readyAt
|
|
542
|
+
: null;
|
|
543
|
+
const nextReadyAt = ready ? readyAt : existingReadyAt;
|
|
544
|
+
writes[key] = {
|
|
545
|
+
id,
|
|
546
|
+
version,
|
|
547
|
+
state: nextReadyAt !== null ? 'ready' : 'warming',
|
|
548
|
+
createdAt:
|
|
549
|
+
existingEntry?.version === version ? existingEntry.createdAt : now,
|
|
550
|
+
readyAt: nextReadyAt,
|
|
551
|
+
expiresAt: now + ttlMs,
|
|
552
|
+
};
|
|
553
|
+
}
|
|
435
554
|
if (Object.keys(writes).length > 0) {
|
|
436
555
|
await this.state.storage.put(writes);
|
|
437
556
|
}
|
|
@@ -441,30 +560,72 @@ export class PlayDedup implements DurableObject {
|
|
|
441
560
|
});
|
|
442
561
|
}
|
|
443
562
|
|
|
444
|
-
private async
|
|
563
|
+
private async handlePoolClaim(req: Request): Promise<Response> {
|
|
445
564
|
const version = this.workflowPoolVersion(req);
|
|
446
565
|
if (!version) {
|
|
447
566
|
return new Response('version is required', { status: 400 });
|
|
448
567
|
}
|
|
449
|
-
|
|
568
|
+
const body = (await req.json().catch(() => null)) as {
|
|
569
|
+
runId?: unknown;
|
|
570
|
+
} | null;
|
|
571
|
+
const runId = typeof body?.runId === 'string' ? body.runId : '';
|
|
572
|
+
if (!runId) {
|
|
573
|
+
return new Response('runId is required', { status: 400 });
|
|
574
|
+
}
|
|
575
|
+
const minReadyAgeMs = this.workflowPoolMinReadyAgeMs(req);
|
|
576
|
+
let claimedId: string | null = null;
|
|
577
|
+
let counts: WorkflowPoolCounts = { available: 0, warming: 0 };
|
|
450
578
|
await this.state.blockConcurrencyWhile(async () => {
|
|
451
579
|
const now = Date.now();
|
|
452
580
|
await this.gcWorkflowPool(now, version);
|
|
581
|
+
const mappingKey = `${WORKFLOW_POOL_RUN_KEY_PREFIX}${runId}`;
|
|
582
|
+
const existingMapping =
|
|
583
|
+
await this.state.storage.get<WorkflowRunMapping>(mappingKey);
|
|
584
|
+
if (existingMapping?.version === version) {
|
|
585
|
+
if (
|
|
586
|
+
existingMapping.state === 'claimed' ||
|
|
587
|
+
existingMapping.state === 'started'
|
|
588
|
+
) {
|
|
589
|
+
claimedId = existingMapping.instanceId || null;
|
|
590
|
+
}
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
453
593
|
const entries = await this.state.storage.list<WorkflowPoolEntry>({
|
|
454
594
|
prefix: WORKFLOW_POOL_KEY_PREFIX,
|
|
455
595
|
});
|
|
596
|
+
counts = this.countWorkflowPoolEntries(
|
|
597
|
+
entries,
|
|
598
|
+
version,
|
|
599
|
+
now,
|
|
600
|
+
minReadyAgeMs,
|
|
601
|
+
);
|
|
456
602
|
const sorted = [...entries.entries()]
|
|
457
603
|
.map(([key, entry]) => ({ key, entry }))
|
|
458
|
-
.filter((entry) =>
|
|
459
|
-
|
|
460
|
-
|
|
604
|
+
.filter((entry) =>
|
|
605
|
+
this.isReadyWorkflowPoolEntry(entry, version, now, minReadyAgeMs),
|
|
606
|
+
)
|
|
461
607
|
.sort((a, b) => b.entry.readyAt - a.entry.readyAt);
|
|
462
608
|
const selected = sorted[0];
|
|
463
609
|
if (!selected) return;
|
|
464
|
-
|
|
610
|
+
claimedId = selected.entry.id;
|
|
465
611
|
await this.state.storage.delete(selected.key);
|
|
612
|
+
await this.state.storage.put(mappingKey, {
|
|
613
|
+
runId,
|
|
614
|
+
instanceId: claimedId,
|
|
615
|
+
state: 'claimed',
|
|
616
|
+
blockedInstanceId: null,
|
|
617
|
+
claimedAt: now,
|
|
618
|
+
startedAt: null,
|
|
619
|
+
version,
|
|
620
|
+
createdAt: now,
|
|
621
|
+
expiresAt: now + WORKFLOW_POOL_RUN_MAPPING_TTL_MS,
|
|
622
|
+
} satisfies WorkflowRunMapping);
|
|
623
|
+
counts = {
|
|
624
|
+
available: Math.max(0, counts.available - 1),
|
|
625
|
+
warming: counts.warming,
|
|
626
|
+
};
|
|
466
627
|
});
|
|
467
|
-
return new Response(JSON.stringify({ id:
|
|
628
|
+
return new Response(JSON.stringify({ id: claimedId, ...counts }), {
|
|
468
629
|
headers: { 'content-type': 'application/json' },
|
|
469
630
|
});
|
|
470
631
|
}
|
|
@@ -474,27 +635,45 @@ export class PlayDedup implements DurableObject {
|
|
|
474
635
|
if (!version) {
|
|
475
636
|
return new Response('version is required', { status: 400 });
|
|
476
637
|
}
|
|
638
|
+
const minReadyAgeMs = this.workflowPoolMinReadyAgeMs(req);
|
|
477
639
|
const now = Date.now();
|
|
478
640
|
await this.gcWorkflowPool(now, version);
|
|
479
641
|
const entries = await this.state.storage.list<WorkflowPoolEntry>({
|
|
480
642
|
prefix: WORKFLOW_POOL_KEY_PREFIX,
|
|
481
643
|
});
|
|
644
|
+
const counts = this.countWorkflowPoolEntries(
|
|
645
|
+
entries,
|
|
646
|
+
version,
|
|
647
|
+
now,
|
|
648
|
+
minReadyAgeMs,
|
|
649
|
+
);
|
|
650
|
+
return new Response(JSON.stringify(counts), {
|
|
651
|
+
headers: { 'content-type': 'application/json' },
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
private countWorkflowPoolEntries(
|
|
656
|
+
entries: Map<string, WorkflowPoolEntry>,
|
|
657
|
+
version: string,
|
|
658
|
+
now: number,
|
|
659
|
+
minReadyAgeMs: number,
|
|
660
|
+
): WorkflowPoolCounts {
|
|
482
661
|
let available = 0;
|
|
483
662
|
let warming = 0;
|
|
484
663
|
for (const entry of entries.values()) {
|
|
485
664
|
if (entry.version !== version) continue;
|
|
486
665
|
if (
|
|
666
|
+
entry.state !== 'ready' ||
|
|
487
667
|
entry.readyAt === null ||
|
|
488
|
-
now - entry.readyAt > WORKFLOW_POOL_READY_MAX_AGE_MS
|
|
668
|
+
now - entry.readyAt > WORKFLOW_POOL_READY_MAX_AGE_MS ||
|
|
669
|
+
now - entry.readyAt < minReadyAgeMs
|
|
489
670
|
) {
|
|
490
671
|
warming += 1;
|
|
491
672
|
} else {
|
|
492
673
|
available += 1;
|
|
493
674
|
}
|
|
494
675
|
}
|
|
495
|
-
return
|
|
496
|
-
headers: { 'content-type': 'application/json' },
|
|
497
|
-
});
|
|
676
|
+
return { available, warming };
|
|
498
677
|
}
|
|
499
678
|
|
|
500
679
|
private async handlePoolList(req: Request): Promise<Response> {
|
|
@@ -512,6 +691,7 @@ export class PlayDedup implements DurableObject {
|
|
|
512
691
|
.filter((entry) => entry.version === version)
|
|
513
692
|
.map((entry) => ({
|
|
514
693
|
id: entry.id,
|
|
694
|
+
state: entry.state,
|
|
515
695
|
createdAt: entry.createdAt,
|
|
516
696
|
readyAt: entry.readyAt,
|
|
517
697
|
expiresAt: entry.expiresAt,
|
|
@@ -548,6 +728,7 @@ export class PlayDedup implements DurableObject {
|
|
|
548
728
|
}
|
|
549
729
|
writes[key] = {
|
|
550
730
|
...entry,
|
|
731
|
+
state: 'ready',
|
|
551
732
|
readyAt: now,
|
|
552
733
|
};
|
|
553
734
|
}
|
|
@@ -563,6 +744,38 @@ export class PlayDedup implements DurableObject {
|
|
|
563
744
|
);
|
|
564
745
|
}
|
|
565
746
|
|
|
747
|
+
private async handlePoolReady(req: Request): Promise<Response> {
|
|
748
|
+
const body = (await req.json().catch(() => null)) as {
|
|
749
|
+
poolId?: unknown;
|
|
750
|
+
version?: unknown;
|
|
751
|
+
} | null;
|
|
752
|
+
const poolId = typeof body?.poolId === 'string' ? body.poolId : '';
|
|
753
|
+
const version =
|
|
754
|
+
typeof body?.version === 'string' ? body.version.trim() : '';
|
|
755
|
+
if (!poolId || !version) {
|
|
756
|
+
return new Response('poolId and version are required', { status: 400 });
|
|
757
|
+
}
|
|
758
|
+
const now = Date.now();
|
|
759
|
+
let ready = false;
|
|
760
|
+
await this.state.blockConcurrencyWhile(async () => {
|
|
761
|
+
await this.gcWorkflowPool(now, version);
|
|
762
|
+
const key = `${WORKFLOW_POOL_KEY_PREFIX}${poolId}`;
|
|
763
|
+
const entry = await this.state.storage.get<WorkflowPoolEntry>(key);
|
|
764
|
+
if (!entry || entry.version !== version || entry.expiresAt <= now) {
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
await this.state.storage.put(key, {
|
|
768
|
+
...entry,
|
|
769
|
+
state: 'ready',
|
|
770
|
+
readyAt: entry.readyAt ?? now,
|
|
771
|
+
});
|
|
772
|
+
ready = true;
|
|
773
|
+
});
|
|
774
|
+
return new Response(JSON.stringify({ ready }), {
|
|
775
|
+
headers: { 'content-type': 'application/json' },
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
|
|
566
779
|
private async handlePoolDelete(req: Request): Promise<Response> {
|
|
567
780
|
const body = (await req.json().catch(() => null)) as {
|
|
568
781
|
ids?: unknown;
|
|
@@ -600,26 +813,112 @@ export class PlayDedup implements DurableObject {
|
|
|
600
813
|
runId?: unknown;
|
|
601
814
|
instanceId?: unknown;
|
|
602
815
|
version?: unknown;
|
|
816
|
+
started?: unknown;
|
|
603
817
|
} | null;
|
|
604
818
|
const runId = typeof body?.runId === 'string' ? body.runId : '';
|
|
605
819
|
const instanceId =
|
|
606
820
|
typeof body?.instanceId === 'string' ? body.instanceId : '';
|
|
607
821
|
const version =
|
|
608
822
|
typeof body?.version === 'string' ? body.version.trim() : '';
|
|
823
|
+
const started = body?.started === true;
|
|
609
824
|
if (!runId || !instanceId || !version) {
|
|
610
825
|
return new Response('runId, instanceId, and version are required', {
|
|
611
826
|
status: 400,
|
|
612
827
|
});
|
|
613
828
|
}
|
|
614
829
|
const now = Date.now();
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
830
|
+
let mapped = true;
|
|
831
|
+
const key = `${WORKFLOW_POOL_RUN_KEY_PREFIX}${runId}`;
|
|
832
|
+
await this.state.blockConcurrencyWhile(async () => {
|
|
833
|
+
const existing = await this.state.storage.get<WorkflowRunMapping>(key);
|
|
834
|
+
if (
|
|
835
|
+
existing?.version === version &&
|
|
836
|
+
existing.state === 'blocked' &&
|
|
837
|
+
existing.blockedInstanceId === instanceId
|
|
838
|
+
) {
|
|
839
|
+
mapped = false;
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
const alreadyStarted =
|
|
843
|
+
existing?.version === version &&
|
|
844
|
+
existing.instanceId === instanceId &&
|
|
845
|
+
existing.state === 'started' &&
|
|
846
|
+
typeof existing.startedAt === 'number';
|
|
847
|
+
const startedAt = started || alreadyStarted ? now : null;
|
|
848
|
+
await this.state.storage.put(key, {
|
|
849
|
+
runId,
|
|
850
|
+
instanceId,
|
|
851
|
+
state: startedAt !== null ? 'started' : 'claimed',
|
|
852
|
+
blockedInstanceId: null,
|
|
853
|
+
claimedAt:
|
|
854
|
+
existing?.version === version && typeof existing.claimedAt === 'number'
|
|
855
|
+
? existing.claimedAt
|
|
856
|
+
: now,
|
|
857
|
+
startedAt,
|
|
858
|
+
version,
|
|
859
|
+
createdAt:
|
|
860
|
+
existing?.version === version && existing.createdAt
|
|
861
|
+
? existing.createdAt
|
|
862
|
+
: now,
|
|
863
|
+
expiresAt: now + WORKFLOW_POOL_RUN_MAPPING_TTL_MS,
|
|
864
|
+
} satisfies WorkflowRunMapping);
|
|
865
|
+
});
|
|
866
|
+
return new Response(JSON.stringify({ mapped }), {
|
|
867
|
+
headers: { 'content-type': 'application/json' },
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
private async handlePoolBlockRun(req: Request): Promise<Response> {
|
|
872
|
+
const body = (await req.json().catch(() => null)) as {
|
|
873
|
+
runId?: unknown;
|
|
874
|
+
instanceId?: unknown;
|
|
875
|
+
version?: unknown;
|
|
876
|
+
} | null;
|
|
877
|
+
const runId = typeof body?.runId === 'string' ? body.runId : '';
|
|
878
|
+
const instanceId =
|
|
879
|
+
typeof body?.instanceId === 'string' ? body.instanceId : '';
|
|
880
|
+
const version =
|
|
881
|
+
typeof body?.version === 'string' ? body.version.trim() : '';
|
|
882
|
+
if (!runId || !instanceId || !version) {
|
|
883
|
+
return new Response('runId, instanceId, and version are required', {
|
|
884
|
+
status: 400,
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
const now = Date.now();
|
|
888
|
+
const key = `${WORKFLOW_POOL_RUN_KEY_PREFIX}${runId}`;
|
|
889
|
+
let started = false;
|
|
890
|
+
await this.state.blockConcurrencyWhile(async () => {
|
|
891
|
+
const existing = await this.state.storage.get<WorkflowRunMapping>(key);
|
|
892
|
+
if (
|
|
893
|
+
existing?.version === version &&
|
|
894
|
+
existing.instanceId === instanceId &&
|
|
895
|
+
existing.state === 'started' &&
|
|
896
|
+
typeof existing.startedAt === 'number'
|
|
897
|
+
) {
|
|
898
|
+
started = true;
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
await this.state.storage.put(key, {
|
|
902
|
+
runId,
|
|
903
|
+
instanceId: '',
|
|
904
|
+
state: 'blocked',
|
|
905
|
+
blockedInstanceId: instanceId,
|
|
906
|
+
claimedAt:
|
|
907
|
+
existing?.version === version && typeof existing.claimedAt === 'number'
|
|
908
|
+
? existing.claimedAt
|
|
909
|
+
: now,
|
|
910
|
+
startedAt: null,
|
|
911
|
+
version,
|
|
912
|
+
createdAt: now,
|
|
913
|
+
expiresAt: now + WORKFLOW_POOL_RUN_MAPPING_TTL_MS,
|
|
914
|
+
} satisfies WorkflowRunMapping);
|
|
915
|
+
});
|
|
916
|
+
if (started) {
|
|
917
|
+
return new Response(JSON.stringify({ blocked: false, started: true }), {
|
|
918
|
+
headers: { 'content-type': 'application/json' },
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
return new Response(JSON.stringify({ blocked: true, started: false }), {
|
|
623
922
|
headers: { 'content-type': 'application/json' },
|
|
624
923
|
});
|
|
625
924
|
}
|
|
@@ -647,7 +946,10 @@ export class PlayDedup implements DurableObject {
|
|
|
647
946
|
});
|
|
648
947
|
}
|
|
649
948
|
return new Response(
|
|
650
|
-
JSON.stringify({
|
|
949
|
+
JSON.stringify({
|
|
950
|
+
instanceId: mapping?.instanceId || null,
|
|
951
|
+
startedAt: mapping?.startedAt ?? null,
|
|
952
|
+
}),
|
|
651
953
|
{ headers: { 'content-type': 'application/json' } },
|
|
652
954
|
);
|
|
653
955
|
}
|
|
@@ -732,4 +1034,206 @@ export class PlayDedup implements DurableObject {
|
|
|
732
1034
|
{ headers: { 'content-type': 'application/json' } },
|
|
733
1035
|
);
|
|
734
1036
|
}
|
|
1037
|
+
|
|
1038
|
+
private async handleTerminalSet(req: Request): Promise<Response> {
|
|
1039
|
+
const body = (await req
|
|
1040
|
+
.json()
|
|
1041
|
+
.catch(() => null)) as Partial<CoordinatorTerminalState> | null;
|
|
1042
|
+
if (
|
|
1043
|
+
!body ||
|
|
1044
|
+
typeof body.runId !== 'string' ||
|
|
1045
|
+
(body.status !== 'completed' &&
|
|
1046
|
+
body.status !== 'failed' &&
|
|
1047
|
+
body.status !== 'cancelled')
|
|
1048
|
+
) {
|
|
1049
|
+
return new Response('invalid terminal state', { status: 400 });
|
|
1050
|
+
}
|
|
1051
|
+
const state: CoordinatorTerminalState = {
|
|
1052
|
+
runId: body.runId,
|
|
1053
|
+
status: body.status,
|
|
1054
|
+
result: body.result,
|
|
1055
|
+
error: typeof body.error === 'string' ? body.error : null,
|
|
1056
|
+
totalRows: body.totalRows,
|
|
1057
|
+
durationMs: body.durationMs,
|
|
1058
|
+
playName: typeof body.playName === 'string' ? body.playName : null,
|
|
1059
|
+
completedAt:
|
|
1060
|
+
typeof body.completedAt === 'number' ? body.completedAt : Date.now(),
|
|
1061
|
+
};
|
|
1062
|
+
await this.state.storage.put(COORDINATOR_TERMINAL_KEY, state);
|
|
1063
|
+
const event = await this.storeRunEvent({
|
|
1064
|
+
runId: state.runId,
|
|
1065
|
+
type: 'terminal',
|
|
1066
|
+
status: state.status,
|
|
1067
|
+
result: state.result,
|
|
1068
|
+
error: state.error,
|
|
1069
|
+
totalRows: state.totalRows,
|
|
1070
|
+
durationMs: state.durationMs,
|
|
1071
|
+
playName: state.playName,
|
|
1072
|
+
ts: state.completedAt,
|
|
1073
|
+
});
|
|
1074
|
+
this.wakeRunEventWaiters(event);
|
|
1075
|
+
return new Response('{}', {
|
|
1076
|
+
headers: { 'content-type': 'application/json' },
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
private async handleTerminalGet(): Promise<Response> {
|
|
1081
|
+
const state = await this.state.storage.get<CoordinatorTerminalState>(
|
|
1082
|
+
COORDINATOR_TERMINAL_KEY,
|
|
1083
|
+
);
|
|
1084
|
+
return new Response(JSON.stringify({ state: state ?? null }), {
|
|
1085
|
+
headers: { 'content-type': 'application/json' },
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
private async storeRunEvent(
|
|
1090
|
+
input: CoordinatorRunEventInput,
|
|
1091
|
+
): Promise<CoordinatorRunEvent> {
|
|
1092
|
+
let event: CoordinatorRunEvent | null = null;
|
|
1093
|
+
await this.state.blockConcurrencyWhile(async () => {
|
|
1094
|
+
const current =
|
|
1095
|
+
(await this.state.storage.get<number>(
|
|
1096
|
+
COORDINATOR_RUN_EVENT_SEQUENCE_KEY,
|
|
1097
|
+
)) ?? 0;
|
|
1098
|
+
const seq = current + 1;
|
|
1099
|
+
event = { ...input, seq } as CoordinatorRunEvent;
|
|
1100
|
+
await this.state.storage.put({
|
|
1101
|
+
[COORDINATOR_RUN_EVENT_SEQUENCE_KEY]: seq,
|
|
1102
|
+
[this.runEventKey(event)]: event,
|
|
1103
|
+
});
|
|
1104
|
+
const entries = await this.state.storage.list<CoordinatorRunEvent>({
|
|
1105
|
+
prefix: COORDINATOR_RUN_EVENT_KEY_PREFIX,
|
|
1106
|
+
});
|
|
1107
|
+
const overflow = entries.size - COORDINATOR_RUN_EVENT_MAX_ENTRIES;
|
|
1108
|
+
if (overflow > 0) {
|
|
1109
|
+
await this.state.storage.delete([...entries.keys()].slice(0, overflow));
|
|
1110
|
+
}
|
|
1111
|
+
});
|
|
1112
|
+
if (!event) {
|
|
1113
|
+
throw new Error('failed to store run event');
|
|
1114
|
+
}
|
|
1115
|
+
return event;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
private wakeRunEventWaiters(event: CoordinatorRunEvent): void {
|
|
1119
|
+
for (const resolve of this.runEventWaiters) {
|
|
1120
|
+
resolve(event);
|
|
1121
|
+
}
|
|
1122
|
+
this.runEventWaiters.clear();
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
private async handleRunEventAdd(req: Request): Promise<Response> {
|
|
1126
|
+
const body = (await req.json().catch(() => null)) as
|
|
1127
|
+
| Partial<CoordinatorRunEvent>
|
|
1128
|
+
| null;
|
|
1129
|
+
if (!body || typeof body.runId !== 'string') {
|
|
1130
|
+
return new Response('invalid run event', { status: 400 });
|
|
1131
|
+
}
|
|
1132
|
+
let eventInput: CoordinatorRunEventInput | null = null;
|
|
1133
|
+
const ts = typeof body.ts === 'number' ? body.ts : Date.now();
|
|
1134
|
+
if (body.type === 'log' && typeof body.line === 'string') {
|
|
1135
|
+
eventInput = {
|
|
1136
|
+
runId: body.runId,
|
|
1137
|
+
type: 'log',
|
|
1138
|
+
line: body.line,
|
|
1139
|
+
ts,
|
|
1140
|
+
};
|
|
1141
|
+
} else if (body.type === 'status' && typeof body.status === 'string') {
|
|
1142
|
+
eventInput = {
|
|
1143
|
+
runId: body.runId,
|
|
1144
|
+
type: 'status',
|
|
1145
|
+
status: body.status,
|
|
1146
|
+
ts,
|
|
1147
|
+
logs: Array.isArray(body.logs)
|
|
1148
|
+
? body.logs.filter((line): line is string => typeof line === 'string')
|
|
1149
|
+
: undefined,
|
|
1150
|
+
};
|
|
1151
|
+
} else if (body.type === 'progress' && typeof body.status === 'string') {
|
|
1152
|
+
eventInput = {
|
|
1153
|
+
runId: body.runId,
|
|
1154
|
+
type: 'progress',
|
|
1155
|
+
status: body.status,
|
|
1156
|
+
ts,
|
|
1157
|
+
logs: Array.isArray(body.logs)
|
|
1158
|
+
? body.logs.filter((line): line is string => typeof line === 'string')
|
|
1159
|
+
: undefined,
|
|
1160
|
+
activeNodeId:
|
|
1161
|
+
typeof body.activeNodeId === 'string' ? body.activeNodeId : null,
|
|
1162
|
+
activeArtifactTableNamespace:
|
|
1163
|
+
typeof body.activeArtifactTableNamespace === 'string'
|
|
1164
|
+
? body.activeArtifactTableNamespace
|
|
1165
|
+
: null,
|
|
1166
|
+
updatedAt: typeof body.updatedAt === 'number' ? body.updatedAt : null,
|
|
1167
|
+
};
|
|
1168
|
+
} else if (
|
|
1169
|
+
body.type === 'terminal' &&
|
|
1170
|
+
(body.status === 'completed' ||
|
|
1171
|
+
body.status === 'failed' ||
|
|
1172
|
+
body.status === 'cancelled')
|
|
1173
|
+
) {
|
|
1174
|
+
eventInput = {
|
|
1175
|
+
runId: body.runId,
|
|
1176
|
+
type: 'terminal',
|
|
1177
|
+
status: body.status,
|
|
1178
|
+
ts,
|
|
1179
|
+
result: body.result,
|
|
1180
|
+
error: typeof body.error === 'string' ? body.error : null,
|
|
1181
|
+
totalRows: body.totalRows,
|
|
1182
|
+
durationMs: body.durationMs,
|
|
1183
|
+
playName: typeof body.playName === 'string' ? body.playName : null,
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
if (!eventInput) {
|
|
1187
|
+
return new Response('invalid run event', { status: 400 });
|
|
1188
|
+
}
|
|
1189
|
+
const event = await this.storeRunEvent(eventInput);
|
|
1190
|
+
this.wakeRunEventWaiters(event);
|
|
1191
|
+
return new Response(JSON.stringify({ event }), {
|
|
1192
|
+
headers: { 'content-type': 'application/json' },
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
private async handleRunEventList(req: Request): Promise<Response> {
|
|
1197
|
+
const url = new URL(req.url);
|
|
1198
|
+
const afterSeq = Math.max(
|
|
1199
|
+
0,
|
|
1200
|
+
Math.floor(Number(url.searchParams.get('afterSeq') ?? '0')),
|
|
1201
|
+
);
|
|
1202
|
+
const timeoutMs = Math.min(
|
|
1203
|
+
Math.max(Number(url.searchParams.get('timeoutMs') ?? '0'), 0),
|
|
1204
|
+
30_000,
|
|
1205
|
+
);
|
|
1206
|
+
const readEvents = async () => {
|
|
1207
|
+
const entries = await this.state.storage.list<CoordinatorRunEvent>({
|
|
1208
|
+
prefix: COORDINATOR_RUN_EVENT_KEY_PREFIX,
|
|
1209
|
+
});
|
|
1210
|
+
return [...entries.values()]
|
|
1211
|
+
.filter((event) => event.seq > afterSeq)
|
|
1212
|
+
.sort((left, right) => left.seq - right.seq);
|
|
1213
|
+
};
|
|
1214
|
+
let events = await readEvents();
|
|
1215
|
+
if (events.length === 0 && timeoutMs > 0) {
|
|
1216
|
+
await new Promise<void>((resolve) => {
|
|
1217
|
+
let settled = false;
|
|
1218
|
+
const finish = () => {
|
|
1219
|
+
if (settled) return;
|
|
1220
|
+
settled = true;
|
|
1221
|
+
this.runEventWaiters.delete(onEvent);
|
|
1222
|
+
clearTimeout(timeout);
|
|
1223
|
+
resolve();
|
|
1224
|
+
};
|
|
1225
|
+
const onEvent = () => finish();
|
|
1226
|
+
const timeout = setTimeout(finish, timeoutMs);
|
|
1227
|
+
this.runEventWaiters.add(onEvent);
|
|
1228
|
+
});
|
|
1229
|
+
events = await readEvents();
|
|
1230
|
+
}
|
|
1231
|
+
const latestSeq =
|
|
1232
|
+
(await this.state.storage.get<number>(
|
|
1233
|
+
COORDINATOR_RUN_EVENT_SEQUENCE_KEY,
|
|
1234
|
+
)) ?? afterSeq;
|
|
1235
|
+
return new Response(JSON.stringify({ events, latestSeq }), {
|
|
1236
|
+
headers: { 'content-type': 'application/json' },
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
735
1239
|
}
|