deepline 0.1.181 → 0.1.183
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/bundling-sources/apps/play-runner-workers/src/child-play-await.ts +203 -6
- package/dist/bundling-sources/apps/play-runner-workers/src/child-play-submit.ts +8 -1
- package/dist/bundling-sources/apps/play-runner-workers/src/coordinator-entry.ts +633 -107
- package/dist/bundling-sources/apps/play-runner-workers/src/dedup-do.ts +3 -2
- package/dist/bundling-sources/apps/play-runner-workers/src/entry.ts +1932 -1838
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/harness-receipt-store.ts +116 -0
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/map-chunk-plan.ts +542 -78
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/output-datasets.ts +124 -12
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/receipts.ts +292 -11
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/run-work-dispatcher.ts +519 -0
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/tool-dispatch.ts +2381 -0
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/tool-receipts.ts +18 -5
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/work-budget.ts +130 -0
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/worker-platform-budget.ts +39 -0
- package/dist/bundling-sources/apps/play-runner-workers/src/workflow-retry-state.ts +2 -0
- package/dist/bundling-sources/sdk/src/client.ts +48 -5
- package/dist/bundling-sources/sdk/src/http.ts +23 -8
- package/dist/bundling-sources/sdk/src/play.ts +200 -26
- package/dist/bundling-sources/sdk/src/plays/harness-stub.ts +56 -3
- package/dist/bundling-sources/sdk/src/release.ts +2 -2
- package/dist/bundling-sources/sdk/src/types.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +132 -10
- package/dist/bundling-sources/shared_libs/play-runtime/auth-scope-resolver.ts +92 -0
- package/dist/bundling-sources/shared_libs/play-runtime/batch-runtime.ts +4 -4
- package/dist/bundling-sources/shared_libs/play-runtime/batching-types.ts +8 -0
- package/dist/bundling-sources/shared_libs/play-runtime/child-run-id.ts +101 -0
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +1857 -314
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +106 -6
- package/dist/bundling-sources/shared_libs/play-runtime/dedup-backend.ts +0 -0
- package/dist/bundling-sources/shared_libs/play-runtime/default-batch-strategies.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/durability-store.ts +4 -0
- package/dist/bundling-sources/shared_libs/play-runtime/durable-call-cache.ts +116 -16
- package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +186 -20
- package/dist/bundling-sources/shared_libs/play-runtime/dynamic-worker-version.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/execution-ledger-store.ts +133 -0
- package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +245 -0
- package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +5 -5
- package/dist/bundling-sources/shared_libs/play-runtime/governor/policy.ts +14 -0
- package/dist/bundling-sources/shared_libs/play-runtime/lease-policy.ts +52 -0
- package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +41 -0
- package/dist/bundling-sources/shared_libs/play-runtime/providers.ts +3 -2
- package/dist/bundling-sources/shared_libs/play-runtime/query-result-dataset.ts +120 -0
- package/dist/bundling-sources/shared_libs/play-runtime/receipt-sql.ts +124 -0
- package/dist/bundling-sources/shared_libs/play-runtime/receipt-status.ts +6 -2
- package/dist/bundling-sources/shared_libs/play-runtime/row-isolation.ts +48 -2
- package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +158 -15
- package/dist/bundling-sources/shared_libs/play-runtime/run-terminal-source.ts +45 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-actions.ts +9 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +2037 -262
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-contract.ts +42 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-sheet-attempt-state-machine.ts +183 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-sheet-errors.ts +88 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +8 -1
- package/dist/bundling-sources/shared_libs/play-runtime/sheet-attempt-sql.ts +103 -0
- package/dist/bundling-sources/shared_libs/play-runtime/suspension.ts +19 -1
- package/dist/bundling-sources/shared_libs/play-runtime/test-runtime-seams.ts +343 -0
- package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +43 -0
- package/dist/bundling-sources/shared_libs/play-runtime/tool-http-errors.ts +11 -0
- package/dist/bundling-sources/shared_libs/play-runtime/tool-result.ts +139 -17
- package/dist/bundling-sources/shared_libs/play-runtime/work-receipt-state-machine.ts +437 -0
- package/dist/bundling-sources/shared_libs/play-runtime/work-receipts.ts +83 -6
- package/dist/bundling-sources/shared_libs/security/safe-outbound-fetch.ts +24 -17
- package/dist/cli/index.js +85 -23
- package/dist/cli/index.mjs +85 -23
- package/dist/index.d.mts +4 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +274 -35
- package/dist/index.mjs +274 -35
- package/dist/plays/bundle-play-file.mjs +2 -2
- package/package.json +1 -1
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
sha256Hex,
|
|
6
6
|
} from '../../../../shared_libs/plays/row-identity';
|
|
7
7
|
import {
|
|
8
|
+
createDeferredPlayDataset,
|
|
9
|
+
isPlayDataset,
|
|
8
10
|
isSerializedPlayDataset,
|
|
9
11
|
type SerializedPlayDataset,
|
|
10
12
|
} from '../../../../shared_libs/plays/dataset';
|
|
@@ -60,6 +62,22 @@ function datasetMetadata(value: unknown): Record<string, unknown> | null {
|
|
|
60
62
|
}
|
|
61
63
|
}
|
|
62
64
|
|
|
65
|
+
function datasetCountFromMetadata(
|
|
66
|
+
metadata: Record<string, unknown> | null,
|
|
67
|
+
): number {
|
|
68
|
+
return typeof metadata?.count === 'number' && Number.isFinite(metadata.count)
|
|
69
|
+
? Math.max(0, Math.floor(metadata.count))
|
|
70
|
+
: 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function datasetPreviewFromMetadata(
|
|
74
|
+
metadata: Record<string, unknown> | null,
|
|
75
|
+
): OutputDatasetRow[] {
|
|
76
|
+
return Array.isArray(metadata?.preview)
|
|
77
|
+
? metadata.preview.filter(isRecord).map((row) => ({ ...row }))
|
|
78
|
+
: [];
|
|
79
|
+
}
|
|
80
|
+
|
|
63
81
|
function isRowArray(
|
|
64
82
|
value: unknown,
|
|
65
83
|
path: readonly string[],
|
|
@@ -226,6 +244,47 @@ function disambiguateTableNamespace(
|
|
|
226
244
|
return candidate;
|
|
227
245
|
}
|
|
228
246
|
|
|
247
|
+
function collectExistingDatasetTableNamespaces(value: unknown): Set<string> {
|
|
248
|
+
const namespaces = new Set<string>();
|
|
249
|
+
const seen = new WeakSet<object>();
|
|
250
|
+
|
|
251
|
+
const addNamespace = (namespace: unknown) => {
|
|
252
|
+
if (typeof namespace !== 'string' || !namespace) return;
|
|
253
|
+
try {
|
|
254
|
+
namespaces.add(normalizeTableNamespace(namespace));
|
|
255
|
+
} catch {
|
|
256
|
+
// Historical malformed namespaces should not block generated result
|
|
257
|
+
// names. The original handle/envelope remains unchanged.
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const visit = (candidate: unknown, depth: number): void => {
|
|
262
|
+
if (depth > 20 || candidate == null || typeof candidate !== 'object') {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (seen.has(candidate)) return;
|
|
266
|
+
seen.add(candidate);
|
|
267
|
+
if (isSerializedPlayDataset(candidate)) {
|
|
268
|
+
addNamespace(candidate.tableNamespace);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (isDatasetLike(candidate)) {
|
|
272
|
+
addNamespace(datasetMetadata(candidate)?.tableNamespace);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (Array.isArray(candidate)) {
|
|
276
|
+
for (const entry of candidate) visit(entry, depth + 1);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
for (const child of Object.values(candidate as Record<string, unknown>)) {
|
|
280
|
+
visit(child, depth + 1);
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
visit(value, 0);
|
|
285
|
+
return namespaces;
|
|
286
|
+
}
|
|
287
|
+
|
|
229
288
|
export function projectTerminalResultDatasets(
|
|
230
289
|
value: unknown,
|
|
231
290
|
createDataset: DatasetFactory,
|
|
@@ -239,30 +298,84 @@ export function projectTerminalResultDatasets(
|
|
|
239
298
|
const handles: ProjectedResultDatasetHandle[] = [];
|
|
240
299
|
const seen = new WeakMap<object, unknown>();
|
|
241
300
|
const seenHandles = new WeakSet<object>();
|
|
301
|
+
const projectedHandles = new WeakMap<object, unknown>();
|
|
242
302
|
const usedNamespaces = seedUsedNamespaces(options.reservedTableNamespaces);
|
|
303
|
+
for (const namespace of collectExistingDatasetTableNamespaces(value)) {
|
|
304
|
+
usedNamespaces.add(namespace);
|
|
305
|
+
}
|
|
243
306
|
const maxTableNamespaceLength = maxTableNamespaceLengthForPlay(
|
|
244
307
|
options.playName,
|
|
245
308
|
);
|
|
246
309
|
|
|
247
|
-
const
|
|
248
|
-
if (!isRecord(handle)
|
|
310
|
+
const projectHandle = (handle: unknown, path: readonly string[]): unknown => {
|
|
311
|
+
if (!isRecord(handle)) return handle;
|
|
312
|
+
const existingProjection = projectedHandles.get(handle);
|
|
313
|
+
if (existingProjection) return existingProjection;
|
|
249
314
|
const metadata = datasetMetadata(handle);
|
|
250
|
-
|
|
251
|
-
|
|
315
|
+
let projectedHandle: unknown = handle;
|
|
316
|
+
let tableNamespace =
|
|
317
|
+
typeof metadata?.tableNamespace === 'string' && metadata.tableNamespace
|
|
252
318
|
? metadata.tableNamespace
|
|
253
319
|
: null;
|
|
254
|
-
if (!tableNamespace)
|
|
255
|
-
|
|
320
|
+
if (!tableNamespace && isPlayDataset<OutputDatasetRow>(handle)) {
|
|
321
|
+
const resolvedPath = path.length > 0 ? path : ['rows'];
|
|
322
|
+
tableNamespace = disambiguateTableNamespace(
|
|
323
|
+
resultTableNamespaceForPath(resolvedPath, maxTableNamespaceLength),
|
|
324
|
+
resolvedPath,
|
|
325
|
+
usedNamespaces,
|
|
326
|
+
maxTableNamespaceLength,
|
|
327
|
+
);
|
|
328
|
+
projectedHandle = createDeferredPlayDataset({
|
|
329
|
+
datasetKind: 'csv',
|
|
330
|
+
datasetId:
|
|
331
|
+
typeof metadata?.datasetId === 'string'
|
|
332
|
+
? metadata.datasetId
|
|
333
|
+
: `csv:${tableNamespace}`,
|
|
334
|
+
count: datasetCountFromMetadata(metadata),
|
|
335
|
+
previewRows: datasetPreviewFromMetadata(metadata),
|
|
336
|
+
sourceLabel:
|
|
337
|
+
typeof metadata?.sourceLabel === 'string'
|
|
338
|
+
? metadata.sourceLabel
|
|
339
|
+
: null,
|
|
340
|
+
tableNamespace,
|
|
341
|
+
resolvers: {
|
|
342
|
+
count: () => handle.count(),
|
|
343
|
+
peek: (limit) => handle.peek(limit),
|
|
344
|
+
materialize: (limit) => handle.materialize(limit),
|
|
345
|
+
iterate: () =>
|
|
346
|
+
({
|
|
347
|
+
async *[Symbol.asyncIterator]() {
|
|
348
|
+
yield* handle;
|
|
349
|
+
},
|
|
350
|
+
}) as AsyncIterable<OutputDatasetRow>,
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
projectedHandles.set(handle, projectedHandle);
|
|
355
|
+
if (!tableNamespace || seenHandles.has(projectedHandle as object)) {
|
|
356
|
+
return projectedHandle;
|
|
357
|
+
}
|
|
358
|
+
try {
|
|
359
|
+
usedNamespaces.add(normalizeTableNamespace(tableNamespace));
|
|
360
|
+
} catch {
|
|
361
|
+
// Existing dataset handles can carry historical namespaces. Keep the
|
|
362
|
+
// handle, but do not let a malformed namespace poison new result names.
|
|
363
|
+
}
|
|
364
|
+
seenHandles.add(projectedHandle as object);
|
|
365
|
+
const projectedMetadata =
|
|
366
|
+
projectedHandle === handle ? metadata : datasetMetadata(projectedHandle);
|
|
256
367
|
const datasetKind =
|
|
257
|
-
|
|
258
|
-
|
|
368
|
+
projectedMetadata?.datasetKind === 'csv' ||
|
|
369
|
+
projectedMetadata?.datasetKind === 'map'
|
|
370
|
+
? projectedMetadata.datasetKind
|
|
259
371
|
: null;
|
|
260
372
|
handles.push({
|
|
261
373
|
path: path.length > 0 ? path.join('.') : 'rows',
|
|
262
374
|
tableNamespace,
|
|
263
375
|
datasetKind,
|
|
264
|
-
handle,
|
|
376
|
+
handle: projectedHandle,
|
|
265
377
|
});
|
|
378
|
+
return projectedHandle;
|
|
266
379
|
};
|
|
267
380
|
|
|
268
381
|
const promoteRows = (
|
|
@@ -287,7 +400,7 @@ export function projectTerminalResultDatasets(
|
|
|
287
400
|
rows,
|
|
288
401
|
handle,
|
|
289
402
|
});
|
|
290
|
-
|
|
403
|
+
projectHandle(handle, path);
|
|
291
404
|
return handle;
|
|
292
405
|
};
|
|
293
406
|
|
|
@@ -298,8 +411,7 @@ export function projectTerminalResultDatasets(
|
|
|
298
411
|
): unknown => {
|
|
299
412
|
if (depth > 20 || candidate == null) return candidate;
|
|
300
413
|
if (isDatasetLike(candidate)) {
|
|
301
|
-
|
|
302
|
-
return candidate;
|
|
414
|
+
return projectHandle(candidate, path);
|
|
303
415
|
}
|
|
304
416
|
if (isSerializedPlayDataset(candidate)) {
|
|
305
417
|
return candidate;
|
|
@@ -3,10 +3,20 @@ import {
|
|
|
3
3
|
type WorkReceipt,
|
|
4
4
|
type WorkReceiptClaim,
|
|
5
5
|
type WorkReceiptCommand,
|
|
6
|
+
type WorkReceiptFailureKind,
|
|
6
7
|
type WorkReceiptGetCommand,
|
|
7
8
|
type WorkReceiptStatus,
|
|
8
|
-
type WorkReceiptStore,
|
|
9
9
|
} from '../../../../shared_libs/play-runtime/work-receipts';
|
|
10
|
+
import type { ExecutionLedgerStore } from '../../../../shared_libs/play-runtime/execution-ledger-store';
|
|
11
|
+
import {
|
|
12
|
+
PLAY_RUNTIME_WORK_RECEIPT_LEASE_TTL_MS,
|
|
13
|
+
runtimeLeaseHeartbeatIntervalMs,
|
|
14
|
+
} from '../../../../shared_libs/play-runtime/lease-policy';
|
|
15
|
+
import {
|
|
16
|
+
isPlayExecutionSuspendedError,
|
|
17
|
+
isPlayRowExecutionSuspendedError,
|
|
18
|
+
} from '../../../../shared_libs/play-runtime/suspension';
|
|
19
|
+
import type { WorkerWorkBudgetMeter } from './work-budget';
|
|
10
20
|
|
|
11
21
|
export type RuntimeReceiptStatus = WorkReceiptStatus;
|
|
12
22
|
|
|
@@ -17,14 +27,34 @@ export type WorkerRuntimeReceiptGetCommand = WorkReceiptGetCommand;
|
|
|
17
27
|
|
|
18
28
|
export type WorkerRuntimeReceiptClaim = WorkReceiptClaim;
|
|
19
29
|
|
|
20
|
-
export type WorkerRuntimeReceiptStore =
|
|
30
|
+
export type WorkerRuntimeReceiptStore = Pick<
|
|
31
|
+
ExecutionLedgerStore,
|
|
32
|
+
'claimReceipt' | 'completeReceipt' | 'failReceipt'
|
|
33
|
+
> &
|
|
34
|
+
Partial<
|
|
35
|
+
Pick<
|
|
36
|
+
ExecutionLedgerStore,
|
|
37
|
+
| 'getReceipt'
|
|
38
|
+
| 'claimReceipts'
|
|
39
|
+
| 'completeReceipts'
|
|
40
|
+
| 'failReceipts'
|
|
41
|
+
| 'releaseReceipt'
|
|
42
|
+
| 'heartbeatReceipts'
|
|
43
|
+
>
|
|
44
|
+
>;
|
|
21
45
|
|
|
22
46
|
type RuntimeReceiptContext = {
|
|
23
47
|
orgId?: string | null;
|
|
24
48
|
playName: string;
|
|
25
49
|
runId: string;
|
|
50
|
+
runAttempt?: number | null;
|
|
26
51
|
key: string;
|
|
27
52
|
receiptStore: WorkerRuntimeReceiptStore;
|
|
53
|
+
budgetMeter?: WorkerWorkBudgetMeter;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type WorkerRuntimeReceiptExecutionContext = {
|
|
57
|
+
leaseId: string | null;
|
|
28
58
|
};
|
|
29
59
|
|
|
30
60
|
const WORKER_RECEIPT_DEFAULT_WAIT_MS = 300_000;
|
|
@@ -32,6 +62,9 @@ const WORKER_RECEIPT_WAIT_DELAY_MS = 250;
|
|
|
32
62
|
const WORKER_RECEIPT_WAIT_MAX_ATTEMPTS = Math.ceil(
|
|
33
63
|
WORKER_RECEIPT_DEFAULT_WAIT_MS / WORKER_RECEIPT_WAIT_DELAY_MS,
|
|
34
64
|
);
|
|
65
|
+
const WORKER_RECEIPT_HEARTBEAT_INTERVAL_MS = runtimeLeaseHeartbeatIntervalMs(
|
|
66
|
+
PLAY_RUNTIME_WORK_RECEIPT_LEASE_TTL_MS,
|
|
67
|
+
);
|
|
35
68
|
|
|
36
69
|
class RuntimeReceiptWaitTimeoutError extends Error {
|
|
37
70
|
constructor(key: string) {
|
|
@@ -40,6 +73,32 @@ class RuntimeReceiptWaitTimeoutError extends Error {
|
|
|
40
73
|
}
|
|
41
74
|
}
|
|
42
75
|
|
|
76
|
+
export class RuntimeLeaseLostError extends Error {
|
|
77
|
+
readonly keys: string[];
|
|
78
|
+
readonly runId: string;
|
|
79
|
+
readonly leaseId: string;
|
|
80
|
+
|
|
81
|
+
constructor(input: {
|
|
82
|
+
keys: string[];
|
|
83
|
+
runId: string;
|
|
84
|
+
leaseId: string;
|
|
85
|
+
cause?: unknown;
|
|
86
|
+
}) {
|
|
87
|
+
const keySummary =
|
|
88
|
+
input.keys.length === 1
|
|
89
|
+
? input.keys[0]!
|
|
90
|
+
: `${input.keys.length} receipts`;
|
|
91
|
+
super(
|
|
92
|
+
`Runtime receipt ${keySummary} lost lease ${input.leaseId} before execution finished.`,
|
|
93
|
+
{ cause: input.cause },
|
|
94
|
+
);
|
|
95
|
+
this.name = 'RuntimeLeaseLostError';
|
|
96
|
+
this.keys = input.keys;
|
|
97
|
+
this.runId = input.runId;
|
|
98
|
+
this.leaseId = input.leaseId;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
43
102
|
function scopedReceiptKey(input: {
|
|
44
103
|
orgId?: string | null;
|
|
45
104
|
playName: string;
|
|
@@ -60,6 +119,15 @@ function errorMessage(error: unknown): string {
|
|
|
60
119
|
return error instanceof Error ? error.message : String(error);
|
|
61
120
|
}
|
|
62
121
|
|
|
122
|
+
function runtimeReceiptFailureKindForError(
|
|
123
|
+
error: unknown,
|
|
124
|
+
): WorkReceiptFailureKind {
|
|
125
|
+
return isPlayExecutionSuspendedError(error) ||
|
|
126
|
+
isPlayRowExecutionSuspendedError(error)
|
|
127
|
+
? 'repairable'
|
|
128
|
+
: 'terminal';
|
|
129
|
+
}
|
|
130
|
+
|
|
63
131
|
async function waitForReusableReceipt<T>(input: {
|
|
64
132
|
key: string;
|
|
65
133
|
playName: string;
|
|
@@ -94,6 +162,141 @@ async function waitForReusableReceipt<T>(input: {
|
|
|
94
162
|
throw new RuntimeReceiptWaitTimeoutError(input.key);
|
|
95
163
|
}
|
|
96
164
|
|
|
165
|
+
type WorkerRuntimeReceiptHeartbeatTarget = {
|
|
166
|
+
key: string | null | undefined;
|
|
167
|
+
leaseId: string | null | undefined;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
function heartbeatGroups(
|
|
171
|
+
targets: WorkerRuntimeReceiptHeartbeatTarget[],
|
|
172
|
+
): Array<{ leaseId: string; keys: string[] }> {
|
|
173
|
+
const byLeaseId = new Map<string, string[]>();
|
|
174
|
+
for (const target of targets) {
|
|
175
|
+
const key = target.key?.trim();
|
|
176
|
+
const leaseId = target.leaseId?.trim();
|
|
177
|
+
if (!key || !leaseId) continue;
|
|
178
|
+
const keys = byLeaseId.get(leaseId);
|
|
179
|
+
if (keys) {
|
|
180
|
+
keys.push(key);
|
|
181
|
+
} else {
|
|
182
|
+
byLeaseId.set(leaseId, [key]);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return Array.from(byLeaseId, ([leaseId, keys]) => ({ leaseId, keys }));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function buildLeaseLostError(input: {
|
|
189
|
+
keys: string[];
|
|
190
|
+
runId: string;
|
|
191
|
+
leaseId: string;
|
|
192
|
+
cause?: unknown;
|
|
193
|
+
}): RuntimeLeaseLostError {
|
|
194
|
+
return new RuntimeLeaseLostError(input);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export async function executeWithWorkerReceiptHeartbeats<T>(input: {
|
|
198
|
+
playName: string;
|
|
199
|
+
runId: string;
|
|
200
|
+
runAttempt?: number | null;
|
|
201
|
+
receiptStore?: WorkerRuntimeReceiptStore | null;
|
|
202
|
+
budgetMeter?: WorkerWorkBudgetMeter;
|
|
203
|
+
targets: WorkerRuntimeReceiptHeartbeatTarget[];
|
|
204
|
+
heartbeatIntervalMs?: number;
|
|
205
|
+
execute: () => Promise<T> | T;
|
|
206
|
+
}): Promise<T> {
|
|
207
|
+
const groups = heartbeatGroups(input.targets);
|
|
208
|
+
const heartbeatReceipts = input.receiptStore?.heartbeatReceipts;
|
|
209
|
+
if (groups.length === 0 || !heartbeatReceipts) {
|
|
210
|
+
return await input.execute();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const heartbeatIntervalMs =
|
|
214
|
+
input.heartbeatIntervalMs ?? WORKER_RECEIPT_HEARTBEAT_INTERVAL_MS;
|
|
215
|
+
if (!Number.isFinite(heartbeatIntervalMs) || heartbeatIntervalMs <= 0) {
|
|
216
|
+
throw new Error('Runtime receipt heartbeat interval must be positive.');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
let stopped = false;
|
|
220
|
+
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
221
|
+
let rejectLeaseLost!: (error: RuntimeLeaseLostError) => void;
|
|
222
|
+
const leaseLost = new Promise<never>((_, reject) => {
|
|
223
|
+
rejectLeaseLost = reject;
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const stop = () => {
|
|
227
|
+
stopped = true;
|
|
228
|
+
if (timeout !== null) {
|
|
229
|
+
clearTimeout(timeout);
|
|
230
|
+
timeout = null;
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const heartbeatOnce = async () => {
|
|
235
|
+
for (const group of groups) {
|
|
236
|
+
input.budgetMeter?.count('heartbeat');
|
|
237
|
+
const receipts = await heartbeatReceipts({
|
|
238
|
+
playName: input.playName,
|
|
239
|
+
runId: input.runId,
|
|
240
|
+
...(input.runAttempt != null ? { runAttempt: input.runAttempt } : {}),
|
|
241
|
+
leaseId: group.leaseId,
|
|
242
|
+
keys: group.keys,
|
|
243
|
+
});
|
|
244
|
+
for (let index = 0; index < group.keys.length; index += 1) {
|
|
245
|
+
const receipt = receipts[index];
|
|
246
|
+
if (
|
|
247
|
+
!receipt ||
|
|
248
|
+
receipt.status !== 'running' ||
|
|
249
|
+
receipt.leaseId !== group.leaseId
|
|
250
|
+
) {
|
|
251
|
+
throw buildLeaseLostError({
|
|
252
|
+
keys: group.keys,
|
|
253
|
+
runId: input.runId,
|
|
254
|
+
leaseId: group.leaseId,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const schedule = () => {
|
|
262
|
+
timeout = setTimeout(() => {
|
|
263
|
+
void (async () => {
|
|
264
|
+
try {
|
|
265
|
+
await heartbeatOnce();
|
|
266
|
+
} catch (error) {
|
|
267
|
+
stop();
|
|
268
|
+
if (error instanceof RuntimeLeaseLostError) {
|
|
269
|
+
rejectLeaseLost(error);
|
|
270
|
+
} else {
|
|
271
|
+
rejectLeaseLost(
|
|
272
|
+
buildLeaseLostError({
|
|
273
|
+
keys: groups.flatMap((group) => group.keys),
|
|
274
|
+
runId: input.runId,
|
|
275
|
+
leaseId: groups[0]?.leaseId ?? 'unknown',
|
|
276
|
+
cause: error,
|
|
277
|
+
}),
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (!stopped) {
|
|
283
|
+
schedule();
|
|
284
|
+
}
|
|
285
|
+
})();
|
|
286
|
+
}, heartbeatIntervalMs);
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
await heartbeatOnce();
|
|
290
|
+
schedule();
|
|
291
|
+
const execution = Promise.resolve().then(input.execute);
|
|
292
|
+
execution.catch(() => {});
|
|
293
|
+
try {
|
|
294
|
+
return await Promise.race([execution, leaseLost]);
|
|
295
|
+
} finally {
|
|
296
|
+
stop();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
97
300
|
function shouldRepairSameRunRunningReceipt(input: {
|
|
98
301
|
repairRunningReceiptForSameRun?: boolean;
|
|
99
302
|
currentRunId: string;
|
|
@@ -111,19 +314,65 @@ async function executeAndPersistReceipt<T>(input: {
|
|
|
111
314
|
key: string;
|
|
112
315
|
playName: string;
|
|
113
316
|
runId: string;
|
|
114
|
-
|
|
317
|
+
runAttempt?: number | null;
|
|
318
|
+
leaseId?: string | null;
|
|
319
|
+
execute: (context: WorkerRuntimeReceiptExecutionContext) => Promise<T> | T;
|
|
115
320
|
receiptStore: WorkerRuntimeReceiptStore;
|
|
116
321
|
ownership: 'claimed' | 'reconciled';
|
|
322
|
+
budgetMeter?: WorkerWorkBudgetMeter;
|
|
323
|
+
heartbeatIntervalMs?: number;
|
|
117
324
|
}): Promise<T> {
|
|
118
325
|
let output: T;
|
|
119
326
|
try {
|
|
120
|
-
|
|
327
|
+
const leaseId = input.leaseId?.trim() || null;
|
|
328
|
+
output = await executeWithWorkerReceiptHeartbeats({
|
|
329
|
+
playName: input.playName,
|
|
330
|
+
runId: input.runId,
|
|
331
|
+
...(input.runAttempt != null ? { runAttempt: input.runAttempt } : {}),
|
|
332
|
+
receiptStore: input.receiptStore,
|
|
333
|
+
budgetMeter: input.budgetMeter,
|
|
334
|
+
heartbeatIntervalMs: input.heartbeatIntervalMs,
|
|
335
|
+
targets: [{ key: input.key, leaseId }],
|
|
336
|
+
execute: () =>
|
|
337
|
+
input.execute({
|
|
338
|
+
leaseId,
|
|
339
|
+
}),
|
|
340
|
+
});
|
|
121
341
|
} catch (error) {
|
|
342
|
+
if (error instanceof RuntimeLeaseLostError) {
|
|
343
|
+
throw error;
|
|
344
|
+
}
|
|
345
|
+
if (
|
|
346
|
+
isPlayExecutionSuspendedError(error) ||
|
|
347
|
+
isPlayRowExecutionSuspendedError(error)
|
|
348
|
+
) {
|
|
349
|
+
if (!input.receiptStore.releaseReceipt) {
|
|
350
|
+
throw new Error(
|
|
351
|
+
`Runtime receipt ${input.key} suspended but the receipt store cannot release ownership: ${errorMessage(error)}`,
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
const released = await input.receiptStore.releaseReceipt({
|
|
355
|
+
playName: input.playName,
|
|
356
|
+
runId: input.runId,
|
|
357
|
+
...(input.runAttempt != null ? { runAttempt: input.runAttempt } : {}),
|
|
358
|
+
key: input.key,
|
|
359
|
+
...(input.leaseId ? { leaseId: input.leaseId } : {}),
|
|
360
|
+
});
|
|
361
|
+
if (!released || released.status !== 'pending') {
|
|
362
|
+
throw new Error(
|
|
363
|
+
`Runtime receipt ${input.key} suspended but failed to release ownership: ${errorMessage(error)}`,
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
throw error;
|
|
367
|
+
}
|
|
122
368
|
const failed = await input.receiptStore.failReceipt({
|
|
123
369
|
playName: input.playName,
|
|
124
370
|
runId: input.runId,
|
|
371
|
+
...(input.runAttempt != null ? { runAttempt: input.runAttempt } : {}),
|
|
125
372
|
key: input.key,
|
|
373
|
+
...(input.leaseId ? { leaseId: input.leaseId } : {}),
|
|
126
374
|
error: errorMessage(error),
|
|
375
|
+
failureKind: runtimeReceiptFailureKindForError(error),
|
|
127
376
|
});
|
|
128
377
|
if (!failed) {
|
|
129
378
|
throw new Error(
|
|
@@ -136,16 +385,20 @@ async function executeAndPersistReceipt<T>(input: {
|
|
|
136
385
|
const completed = await input.receiptStore.completeReceipt({
|
|
137
386
|
playName: input.playName,
|
|
138
387
|
runId: input.runId,
|
|
388
|
+
...(input.runAttempt != null ? { runAttempt: input.runAttempt } : {}),
|
|
139
389
|
key: input.key,
|
|
390
|
+
...(input.leaseId ? { leaseId: input.leaseId } : {}),
|
|
140
391
|
output,
|
|
141
392
|
});
|
|
142
|
-
if (!completed) {
|
|
143
|
-
return output;
|
|
144
|
-
}
|
|
145
393
|
if (
|
|
146
|
-
|
|
147
|
-
completed.
|
|
394
|
+
!completed ||
|
|
395
|
+
(completed.status !== 'completed' && completed.status !== 'skipped')
|
|
148
396
|
) {
|
|
397
|
+
throw new Error(
|
|
398
|
+
`Runtime receipt ${input.key} lost receipt ownership before completion.`,
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
if (completed.output !== undefined) {
|
|
149
402
|
return receiptOutput<T>(completed);
|
|
150
403
|
}
|
|
151
404
|
return output;
|
|
@@ -153,11 +406,13 @@ async function executeAndPersistReceipt<T>(input: {
|
|
|
153
406
|
|
|
154
407
|
export async function runWorkerRuntimeReceiptBoundary<T>(
|
|
155
408
|
input: RuntimeReceiptContext & {
|
|
156
|
-
execute: () => Promise<T> | T;
|
|
409
|
+
execute: (context: WorkerRuntimeReceiptExecutionContext) => Promise<T> | T;
|
|
157
410
|
repairRunningReceiptForSameRun?: boolean;
|
|
158
411
|
repairRunningReceiptForSameRunAfterWaitTimeout?: boolean;
|
|
159
412
|
runningReceiptWaitMaxAttempts?: number;
|
|
160
413
|
runningReceiptWaitDelayMs?: number;
|
|
414
|
+
runtimeReceiptHeartbeatIntervalMs?: number;
|
|
415
|
+
runtimeReceiptLeaseTtlMs?: number;
|
|
161
416
|
reclaimRunning?: boolean;
|
|
162
417
|
},
|
|
163
418
|
): Promise<T> {
|
|
@@ -166,13 +421,19 @@ export async function runWorkerRuntimeReceiptBoundary<T>(
|
|
|
166
421
|
const claimed = await receiptStore.claimReceipt({
|
|
167
422
|
playName: input.playName,
|
|
168
423
|
runId: input.runId,
|
|
424
|
+
...(input.runAttempt != null ? { runAttempt: input.runAttempt } : {}),
|
|
169
425
|
key,
|
|
426
|
+
leaseAware: true,
|
|
427
|
+
leaseTtlMs: input.runtimeReceiptLeaseTtlMs,
|
|
170
428
|
...(input.reclaimRunning === true ? { reclaimRunning: true } : {}),
|
|
171
429
|
});
|
|
172
430
|
if (claimed.disposition === 'reused') {
|
|
173
431
|
return receiptOutput<T>(claimed.receipt);
|
|
174
432
|
}
|
|
175
|
-
if (
|
|
433
|
+
if (
|
|
434
|
+
claimed.disposition === 'running' ||
|
|
435
|
+
claimed.disposition === 'blocked_active_lease'
|
|
436
|
+
) {
|
|
176
437
|
if (
|
|
177
438
|
shouldRepairSameRunRunningReceipt({
|
|
178
439
|
repairRunningReceiptForSameRun: input.repairRunningReceiptForSameRun,
|
|
@@ -183,7 +444,10 @@ export async function runWorkerRuntimeReceiptBoundary<T>(
|
|
|
183
444
|
const repaired = await receiptStore.claimReceipt({
|
|
184
445
|
playName: input.playName,
|
|
185
446
|
runId: input.runId,
|
|
447
|
+
...(input.runAttempt != null ? { runAttempt: input.runAttempt } : {}),
|
|
186
448
|
key,
|
|
449
|
+
leaseAware: true,
|
|
450
|
+
leaseTtlMs: input.runtimeReceiptLeaseTtlMs,
|
|
187
451
|
reclaimRunning: true,
|
|
188
452
|
});
|
|
189
453
|
if (repaired.disposition === 'reused') {
|
|
@@ -199,9 +463,13 @@ export async function runWorkerRuntimeReceiptBoundary<T>(
|
|
|
199
463
|
key,
|
|
200
464
|
playName: input.playName,
|
|
201
465
|
runId: input.runId,
|
|
466
|
+
...(input.runAttempt != null ? { runAttempt: input.runAttempt } : {}),
|
|
467
|
+
leaseId: repaired.receipt.leaseId ?? null,
|
|
202
468
|
execute: input.execute,
|
|
203
469
|
receiptStore,
|
|
204
470
|
ownership: 'claimed',
|
|
471
|
+
budgetMeter: input.budgetMeter,
|
|
472
|
+
heartbeatIntervalMs: input.runtimeReceiptHeartbeatIntervalMs,
|
|
205
473
|
});
|
|
206
474
|
}
|
|
207
475
|
}
|
|
@@ -226,7 +494,10 @@ export async function runWorkerRuntimeReceiptBoundary<T>(
|
|
|
226
494
|
const repaired = await receiptStore.claimReceipt({
|
|
227
495
|
playName: input.playName,
|
|
228
496
|
runId: input.runId,
|
|
497
|
+
...(input.runAttempt != null ? { runAttempt: input.runAttempt } : {}),
|
|
229
498
|
key,
|
|
499
|
+
leaseAware: true,
|
|
500
|
+
leaseTtlMs: input.runtimeReceiptLeaseTtlMs,
|
|
230
501
|
reclaimRunning: true,
|
|
231
502
|
});
|
|
232
503
|
if (repaired.disposition === 'reused') {
|
|
@@ -242,9 +513,15 @@ export async function runWorkerRuntimeReceiptBoundary<T>(
|
|
|
242
513
|
key,
|
|
243
514
|
playName: input.playName,
|
|
244
515
|
runId: input.runId,
|
|
516
|
+
...(input.runAttempt != null
|
|
517
|
+
? { runAttempt: input.runAttempt }
|
|
518
|
+
: {}),
|
|
519
|
+
leaseId: repaired.receipt.leaseId ?? null,
|
|
245
520
|
execute: input.execute,
|
|
246
521
|
receiptStore,
|
|
247
522
|
ownership: 'claimed',
|
|
523
|
+
budgetMeter: input.budgetMeter,
|
|
524
|
+
heartbeatIntervalMs: input.runtimeReceiptHeartbeatIntervalMs,
|
|
248
525
|
});
|
|
249
526
|
}
|
|
250
527
|
}
|
|
@@ -261,8 +538,12 @@ export async function runWorkerRuntimeReceiptBoundary<T>(
|
|
|
261
538
|
key,
|
|
262
539
|
playName: input.playName,
|
|
263
540
|
runId: input.runId,
|
|
541
|
+
...(input.runAttempt != null ? { runAttempt: input.runAttempt } : {}),
|
|
542
|
+
leaseId: claimed.receipt.leaseId ?? null,
|
|
264
543
|
execute: input.execute,
|
|
265
544
|
receiptStore,
|
|
266
545
|
ownership: 'claimed',
|
|
546
|
+
budgetMeter: input.budgetMeter,
|
|
547
|
+
heartbeatIntervalMs: input.runtimeReceiptHeartbeatIntervalMs,
|
|
267
548
|
});
|
|
268
549
|
}
|