deepline 0.1.182 → 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 +1747 -1890
- 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/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 +41 -0
- package/dist/bundling-sources/sdk/src/http.ts +23 -8
- 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 +1605 -281
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +105 -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/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/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 +76 -17
- package/dist/cli/index.mjs +76 -17
- package/dist/index.d.mts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +63 -9
- package/dist/index.mjs +63 -9
- package/dist/plays/bundle-play-file.mjs +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export const PLAY_RUNTIME_CONTRACT = 2;
|
|
2
|
+
export const PLAY_RUNTIME_CONTRACT_MIN_SUPPORTED = 1;
|
|
3
|
+
export const PLAY_RUNTIME_CONTRACT_HEADER = 'x-deepline-runtime-contract';
|
|
4
|
+
|
|
5
|
+
export const RUNTIME_CONTRACT_CHANGELOG = [
|
|
6
|
+
{
|
|
7
|
+
contract: 2,
|
|
8
|
+
changed:
|
|
9
|
+
'Runtime receipt batch responses use positional nulls and v2 lease-aware receipt semantics.',
|
|
10
|
+
compatOwner: 'runtime',
|
|
11
|
+
compatRemoval: 'ledger cutover milestone M1',
|
|
12
|
+
},
|
|
13
|
+
] as const;
|
|
14
|
+
|
|
15
|
+
export type RuntimeContractResolution =
|
|
16
|
+
| { ok: true; contract: number }
|
|
17
|
+
| {
|
|
18
|
+
ok: false;
|
|
19
|
+
requestedContract: number;
|
|
20
|
+
currentContract: number;
|
|
21
|
+
minSupportedContract: number;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function resolvePlayRuntimeContract(
|
|
25
|
+
rawContract: string | null | undefined,
|
|
26
|
+
): RuntimeContractResolution {
|
|
27
|
+
const trimmed = rawContract?.trim();
|
|
28
|
+
const requestedContract = trimmed ? Number(trimmed) : 1;
|
|
29
|
+
if (
|
|
30
|
+
!Number.isInteger(requestedContract) ||
|
|
31
|
+
requestedContract < PLAY_RUNTIME_CONTRACT_MIN_SUPPORTED ||
|
|
32
|
+
requestedContract > PLAY_RUNTIME_CONTRACT
|
|
33
|
+
) {
|
|
34
|
+
return {
|
|
35
|
+
ok: false,
|
|
36
|
+
requestedContract,
|
|
37
|
+
currentContract: PLAY_RUNTIME_CONTRACT,
|
|
38
|
+
minSupportedContract: PLAY_RUNTIME_CONTRACT_MIN_SUPPORTED,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return { ok: true, contract: requestedContract };
|
|
42
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
export type RuntimeSheetRowStatus =
|
|
2
|
+
| 'pending'
|
|
3
|
+
| 'running'
|
|
4
|
+
| 'failed'
|
|
5
|
+
| 'enriched'
|
|
6
|
+
| 'stale';
|
|
7
|
+
|
|
8
|
+
export type RuntimeSheetAttemptIdentity = {
|
|
9
|
+
attemptId: string | null;
|
|
10
|
+
attemptOwnerRunId: string | null;
|
|
11
|
+
attemptExpiresAt: string | null;
|
|
12
|
+
attemptSeq?: number | null;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type RuntimeSheetRowAttemptInput = RuntimeSheetAttemptIdentity & {
|
|
16
|
+
status: RuntimeSheetRowStatus;
|
|
17
|
+
runId: string | null;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type RuntimeSheetAttemptRelation =
|
|
21
|
+
| { kind: 'unowned' }
|
|
22
|
+
| { kind: 'same_attempt' }
|
|
23
|
+
| { kind: 'expired' }
|
|
24
|
+
| { kind: 'same_owner_newer_attempt' }
|
|
25
|
+
| { kind: 'same_owner_stale_attempt' }
|
|
26
|
+
| { kind: 'other_active_owner' }
|
|
27
|
+
| {
|
|
28
|
+
kind: 'malformed';
|
|
29
|
+
reason: 'invalid_row_expiry';
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type RuntimeSheetPrepareDecision =
|
|
33
|
+
| { kind: 'claim_executable' }
|
|
34
|
+
| { kind: 'reuse_completed' }
|
|
35
|
+
| { kind: 'blocked_by_active_owner' };
|
|
36
|
+
|
|
37
|
+
export type RuntimeSheetTerminalDecision =
|
|
38
|
+
| { kind: 'write_terminal' }
|
|
39
|
+
| { kind: 'reject_stale_attempt' };
|
|
40
|
+
|
|
41
|
+
export type RuntimeSheetReleaseDecision =
|
|
42
|
+
| { kind: 'release' }
|
|
43
|
+
| { kind: 'skip' };
|
|
44
|
+
|
|
45
|
+
function normalize(value: string | null | undefined): string | null {
|
|
46
|
+
const trimmed = value?.trim();
|
|
47
|
+
return trimmed ? trimmed : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function parseOptionalMs(value: string | null): number | null {
|
|
51
|
+
if (!value) return null;
|
|
52
|
+
const parsed = Date.parse(value);
|
|
53
|
+
return Number.isFinite(parsed) ? parsed : Number.NaN;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function normalizeAttemptSeq(value: number | null | undefined): number {
|
|
57
|
+
return Number.isFinite(value) ? Math.trunc(value as number) : 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function classifyRuntimeSheetAttemptRelation(input: {
|
|
61
|
+
row: RuntimeSheetRowAttemptInput;
|
|
62
|
+
request: RuntimeSheetAttemptIdentity;
|
|
63
|
+
nowMs?: number;
|
|
64
|
+
}): RuntimeSheetAttemptRelation {
|
|
65
|
+
const nowMs = input.nowMs ?? Date.now();
|
|
66
|
+
const rowAttemptId = normalize(input.row.attemptId);
|
|
67
|
+
const requestAttemptId = normalize(input.request.attemptId);
|
|
68
|
+
if (!rowAttemptId) return { kind: 'unowned' };
|
|
69
|
+
if (rowAttemptId === requestAttemptId) return { kind: 'same_attempt' };
|
|
70
|
+
|
|
71
|
+
const rowExpiresAt = normalize(input.row.attemptExpiresAt);
|
|
72
|
+
const rowExpiresAtMs = parseOptionalMs(rowExpiresAt);
|
|
73
|
+
if (rowExpiresAtMs === null) {
|
|
74
|
+
return { kind: 'malformed', reason: 'invalid_row_expiry' };
|
|
75
|
+
}
|
|
76
|
+
if (Number.isNaN(rowExpiresAtMs)) {
|
|
77
|
+
return { kind: 'malformed', reason: 'invalid_row_expiry' };
|
|
78
|
+
}
|
|
79
|
+
if (rowExpiresAtMs <= nowMs) return { kind: 'expired' };
|
|
80
|
+
|
|
81
|
+
const rowOwner =
|
|
82
|
+
normalize(input.row.attemptOwnerRunId) ?? normalize(input.row.runId);
|
|
83
|
+
const requestOwner = normalize(input.request.attemptOwnerRunId);
|
|
84
|
+
const rowAttemptSeq = normalizeAttemptSeq(input.row.attemptSeq);
|
|
85
|
+
const requestAttemptSeq = normalizeAttemptSeq(input.request.attemptSeq);
|
|
86
|
+
if (rowOwner && requestOwner && rowOwner === requestOwner) {
|
|
87
|
+
return requestAttemptSeq > rowAttemptSeq
|
|
88
|
+
? { kind: 'same_owner_newer_attempt' }
|
|
89
|
+
: { kind: 'same_owner_stale_attempt' };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { kind: 'other_active_owner' };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function decideRuntimeSheetPrepare(input: {
|
|
96
|
+
row: RuntimeSheetRowAttemptInput;
|
|
97
|
+
request: RuntimeSheetAttemptIdentity;
|
|
98
|
+
missingRequiredOutput?: boolean;
|
|
99
|
+
nowMs?: number;
|
|
100
|
+
}): RuntimeSheetPrepareDecision {
|
|
101
|
+
if (input.row.status === 'stale' || input.row.status === 'failed') {
|
|
102
|
+
return { kind: 'claim_executable' };
|
|
103
|
+
}
|
|
104
|
+
const relation = classifyRuntimeSheetAttemptRelation(input);
|
|
105
|
+
if (input.row.status === 'pending' || input.row.status === 'running') {
|
|
106
|
+
return relation.kind === 'other_active_owner' || relation.kind === 'malformed'
|
|
107
|
+
? { kind: 'blocked_by_active_owner' }
|
|
108
|
+
: { kind: 'claim_executable' };
|
|
109
|
+
}
|
|
110
|
+
if (input.row.status === 'enriched') {
|
|
111
|
+
if (!input.missingRequiredOutput) return { kind: 'reuse_completed' };
|
|
112
|
+
return relation.kind === 'same_owner_stale_attempt' ||
|
|
113
|
+
relation.kind === 'malformed'
|
|
114
|
+
? { kind: 'blocked_by_active_owner' }
|
|
115
|
+
: { kind: 'claim_executable' };
|
|
116
|
+
}
|
|
117
|
+
return assertNever(input.row.status);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function decideRuntimeSheetTerminalWrite(input: {
|
|
121
|
+
row: RuntimeSheetRowAttemptInput;
|
|
122
|
+
request: RuntimeSheetAttemptIdentity;
|
|
123
|
+
nowMs?: number;
|
|
124
|
+
}): RuntimeSheetTerminalDecision {
|
|
125
|
+
const relation = classifyRuntimeSheetAttemptRelation(input);
|
|
126
|
+
if (
|
|
127
|
+
relation.kind === 'unowned' ||
|
|
128
|
+
relation.kind === 'same_attempt' ||
|
|
129
|
+
relation.kind === 'expired' ||
|
|
130
|
+
relation.kind === 'same_owner_newer_attempt'
|
|
131
|
+
) {
|
|
132
|
+
return { kind: 'write_terminal' };
|
|
133
|
+
}
|
|
134
|
+
if (input.row.status === 'enriched') {
|
|
135
|
+
const rowAttemptSeq = normalizeAttemptSeq(input.row.attemptSeq);
|
|
136
|
+
const requestAttemptSeq = normalizeAttemptSeq(input.request.attemptSeq);
|
|
137
|
+
if (requestAttemptSeq > rowAttemptSeq) {
|
|
138
|
+
return { kind: 'write_terminal' };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return { kind: 'reject_stale_attempt' };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Decide whether a controlled run-fatal teardown may release (clear the attempt
|
|
146
|
+
* lease on) a row. Only rows the releasing attempt currently owns are cleared:
|
|
147
|
+
* the row must carry an attempt lease whose owner run id and attempt generation
|
|
148
|
+
* (seq) match the releasing run. When the request carries a concrete attempt id
|
|
149
|
+
* the row's attempt id must match it too; when the request attempt id is null,
|
|
150
|
+
* every attempt id the run holds at that generation is releasable (the workers
|
|
151
|
+
* runner mints a distinct attempt id per map chunk under one owner + seq). Row
|
|
152
|
+
* status is irrelevant — the lease strands work receipts and blocks the write
|
|
153
|
+
* fence whether the row is pending, running, failed, or enriched — but an
|
|
154
|
+
* unowned row is never touched.
|
|
155
|
+
*/
|
|
156
|
+
export function decideRuntimeSheetRelease(input: {
|
|
157
|
+
row: RuntimeSheetRowAttemptInput;
|
|
158
|
+
request: RuntimeSheetAttemptIdentity;
|
|
159
|
+
}): RuntimeSheetReleaseDecision {
|
|
160
|
+
const rowAttemptId = normalize(input.row.attemptId);
|
|
161
|
+
if (!rowAttemptId) return { kind: 'skip' };
|
|
162
|
+
const rowOwner =
|
|
163
|
+
normalize(input.row.attemptOwnerRunId) ?? normalize(input.row.runId);
|
|
164
|
+
const requestOwner = normalize(input.request.attemptOwnerRunId);
|
|
165
|
+
if (!rowOwner || !requestOwner || rowOwner !== requestOwner) {
|
|
166
|
+
return { kind: 'skip' };
|
|
167
|
+
}
|
|
168
|
+
if (
|
|
169
|
+
normalizeAttemptSeq(input.row.attemptSeq) !==
|
|
170
|
+
normalizeAttemptSeq(input.request.attemptSeq)
|
|
171
|
+
) {
|
|
172
|
+
return { kind: 'skip' };
|
|
173
|
+
}
|
|
174
|
+
const requestAttemptId = normalize(input.request.attemptId);
|
|
175
|
+
if (requestAttemptId && requestAttemptId !== rowAttemptId) {
|
|
176
|
+
return { kind: 'skip' };
|
|
177
|
+
}
|
|
178
|
+
return { kind: 'release' };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function assertNever(value: never): never {
|
|
182
|
+
throw new Error(`Unhandled runtime sheet row status: ${String(value)}`);
|
|
183
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { resolveMapRowOutcomeKey } from './map-row-outcome';
|
|
2
|
+
|
|
3
|
+
export interface RuntimeSheetBlockedRowDetail {
|
|
4
|
+
rowKey: string | null;
|
|
5
|
+
attemptId: string | null;
|
|
6
|
+
attemptOwnerRunId: string | null;
|
|
7
|
+
attemptExpiresAt: string | null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function stringField(
|
|
11
|
+
row: Record<string, unknown>,
|
|
12
|
+
...fieldNames: string[]
|
|
13
|
+
): string | null {
|
|
14
|
+
for (const fieldName of fieldNames) {
|
|
15
|
+
const value = row[fieldName];
|
|
16
|
+
if (typeof value === 'string' && value.trim()) {
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function runtimeSheetBlockedRowDetail(
|
|
24
|
+
row: Record<string, unknown>,
|
|
25
|
+
): RuntimeSheetBlockedRowDetail {
|
|
26
|
+
return {
|
|
27
|
+
rowKey: resolveMapRowOutcomeKey(row),
|
|
28
|
+
attemptId: stringField(
|
|
29
|
+
row,
|
|
30
|
+
'attemptId',
|
|
31
|
+
'_attempt_id',
|
|
32
|
+
'__deeplineAttemptId',
|
|
33
|
+
),
|
|
34
|
+
attemptOwnerRunId: stringField(
|
|
35
|
+
row,
|
|
36
|
+
'attemptOwnerRunId',
|
|
37
|
+
'_attempt_owner_run_id',
|
|
38
|
+
'__deeplineAttemptOwnerRunId',
|
|
39
|
+
),
|
|
40
|
+
attemptExpiresAt: stringField(
|
|
41
|
+
row,
|
|
42
|
+
'attemptExpiresAt',
|
|
43
|
+
'_attempt_expires_at',
|
|
44
|
+
'__deeplineAttemptExpiresAt',
|
|
45
|
+
),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class RuntimeSheetRowsBlockedError extends Error {
|
|
50
|
+
readonly tableNamespace: string;
|
|
51
|
+
readonly rowKeys: string[];
|
|
52
|
+
readonly owningAttempts: string[];
|
|
53
|
+
readonly blockedRowDetails: RuntimeSheetBlockedRowDetail[];
|
|
54
|
+
|
|
55
|
+
constructor(input: {
|
|
56
|
+
tableNamespace: string;
|
|
57
|
+
blockedRows: Record<string, unknown>[];
|
|
58
|
+
}) {
|
|
59
|
+
const blockedRowDetails = input.blockedRows.map(
|
|
60
|
+
runtimeSheetBlockedRowDetail,
|
|
61
|
+
);
|
|
62
|
+
const rowKeys = blockedRowDetails
|
|
63
|
+
.map((detail) => detail.rowKey)
|
|
64
|
+
.filter((key): key is string => Boolean(key))
|
|
65
|
+
.slice(0, 20);
|
|
66
|
+
const owningAttempts = blockedRowDetails
|
|
67
|
+
.map((detail) => detail.attemptId)
|
|
68
|
+
.filter((attempt): attempt is string => Boolean(attempt));
|
|
69
|
+
const uniqueOwningAttempts = Array.from(new Set(owningAttempts)).slice(
|
|
70
|
+
0,
|
|
71
|
+
20,
|
|
72
|
+
);
|
|
73
|
+
super(
|
|
74
|
+
`ctx.dataset("${input.tableNamespace}") is blocked by ${input.blockedRows.length} active runtime sheet row lease(s) owned by another attempt. Retry after the owning attempt finishes or the row leases expire.` +
|
|
75
|
+
(rowKeys.length > 0
|
|
76
|
+
? ` Blocked row keys: ${rowKeys.join(', ')}.`
|
|
77
|
+
: '') +
|
|
78
|
+
(uniqueOwningAttempts.length > 0
|
|
79
|
+
? ` Owning attempts: ${uniqueOwningAttempts.join(', ')}.`
|
|
80
|
+
: ''),
|
|
81
|
+
);
|
|
82
|
+
this.name = 'RuntimeSheetRowsBlockedError';
|
|
83
|
+
this.tableNamespace = input.tableNamespace;
|
|
84
|
+
this.rowKeys = rowKeys;
|
|
85
|
+
this.owningAttempts = uniqueOwningAttempts;
|
|
86
|
+
this.blockedRowDetails = blockedRowDetails.slice(0, 20);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -19,6 +19,7 @@ import type { ExecutionPlan } from './execution-plan';
|
|
|
19
19
|
import type { PlayRuntimeManifestMap } from '../plays/compiler-manifest';
|
|
20
20
|
import type { PreloadedRuntimeDbSession } from './db-session';
|
|
21
21
|
import type { PlayRunnerRuntimeTiming } from './protocol';
|
|
22
|
+
import type { RuntimeTestPolicyOverrides } from './test-runtime-seams';
|
|
22
23
|
|
|
23
24
|
export const PLAY_SCHEDULER_BACKENDS = {
|
|
24
25
|
temporal: 'temporal',
|
|
@@ -83,7 +84,9 @@ export type PlaySchedulerSubmitInput = {
|
|
|
83
84
|
artifactHash: string;
|
|
84
85
|
graphHash: string;
|
|
85
86
|
input: Record<string, unknown>;
|
|
86
|
-
/**
|
|
87
|
+
/** Start a fresh run graph and recompute runtime-sheet rows. */
|
|
88
|
+
force?: boolean;
|
|
89
|
+
/** Explicit cache bypass for completed ctx.tools.execute receipts. */
|
|
87
90
|
forceToolRefresh?: boolean | null;
|
|
88
91
|
inputFile?: {
|
|
89
92
|
name?: string;
|
|
@@ -149,6 +152,10 @@ export type PlaySchedulerSubmitInput = {
|
|
|
149
152
|
runtimeDeployVersion?: string | null;
|
|
150
153
|
/** Request-scoped Vercel Deployment Protection bypass for preview runtime callbacks. */
|
|
151
154
|
vercelProtectionBypassToken?: string | null;
|
|
155
|
+
/** Request-scoped, dev-only runtime fault injection header for black-box durability tests. */
|
|
156
|
+
runtimeTestFaultHeader?: string | null;
|
|
157
|
+
/** Request-scoped, dev-only runtime policy overrides for black-box durability tests. */
|
|
158
|
+
testPolicyOverrides?: RuntimeTestPolicyOverrides | null;
|
|
152
159
|
/** Millisecond epoch timestamp captured immediately before scheduler submit. */
|
|
153
160
|
submittedAtMs?: number;
|
|
154
161
|
/** Scheduler backend that owns orchestration for this run. */
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
export function activeRuntimeSheetAttemptFenceSql(
|
|
2
|
+
tableAlias: string,
|
|
3
|
+
attemptIdExpression: string,
|
|
4
|
+
attemptOwnerRunIdExpression: string,
|
|
5
|
+
attemptExpiresAtExpression: string,
|
|
6
|
+
attemptSeqExpression?: string,
|
|
7
|
+
): string {
|
|
8
|
+
const activeSameOwnerNewerAttempt =
|
|
9
|
+
attemptSeqExpression === undefined
|
|
10
|
+
? `${tableAlias}._attempt_expires_at < ${attemptExpiresAtExpression}`
|
|
11
|
+
// Same owner + same sequence is a retry of the same logical run attempt.
|
|
12
|
+
// It may reclaim non-terminal rows; terminal rows are still guarded by
|
|
13
|
+
// newerTerminalRuntimeSheetRowSql before this predicate is considered.
|
|
14
|
+
: `COALESCE(${tableAlias}._attempt_seq, 0) <= ${attemptSeqExpression}::integer`;
|
|
15
|
+
return `(
|
|
16
|
+
/* owner: runtime; remove null-attempt compat after ledger cutover M1 */
|
|
17
|
+
${tableAlias}._attempt_id IS NULL
|
|
18
|
+
OR ${tableAlias}._attempt_id = ${attemptIdExpression}
|
|
19
|
+
OR ${tableAlias}._attempt_expires_at <= now()
|
|
20
|
+
OR (
|
|
21
|
+
coalesce(${tableAlias}._attempt_owner_run_id, ${tableAlias}._run_id) = ${attemptOwnerRunIdExpression}
|
|
22
|
+
AND ${activeSameOwnerNewerAttempt}
|
|
23
|
+
)
|
|
24
|
+
)`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Fence for a controlled attempt-lease RELEASE (run-fatal teardown). Matches
|
|
29
|
+
* only rows the releasing attempt currently owns: the row must carry an attempt
|
|
30
|
+
* lease whose owner run id and attempt generation match the releasing run. When
|
|
31
|
+
* `attemptIdExpression` is a non-null param it further pins the exact attempt id
|
|
32
|
+
* (single-attempt callers like the in-process app runner); when it is NULL the
|
|
33
|
+
* predicate releases every attempt id the run holds at that generation (the
|
|
34
|
+
* workers runner mints a distinct attempt id per map chunk under one owner run
|
|
35
|
+
* id + seq). It never matches a row owned by a different run, so a release can
|
|
36
|
+
* never weaken another live attempt's lease.
|
|
37
|
+
*/
|
|
38
|
+
export function releasableRuntimeSheetAttemptFenceSql(
|
|
39
|
+
tableAlias: string,
|
|
40
|
+
attemptIdExpression: string,
|
|
41
|
+
attemptOwnerRunIdExpression: string,
|
|
42
|
+
attemptSeqExpression: string,
|
|
43
|
+
): string {
|
|
44
|
+
return `(
|
|
45
|
+
${tableAlias}._attempt_id IS NOT NULL
|
|
46
|
+
AND coalesce(${tableAlias}._attempt_owner_run_id, ${tableAlias}._run_id) = ${attemptOwnerRunIdExpression}
|
|
47
|
+
AND COALESCE(${tableAlias}._attempt_seq, 0) = ${attemptSeqExpression}::integer
|
|
48
|
+
AND (${attemptIdExpression} IS NULL OR ${tableAlias}._attempt_id = ${attemptIdExpression})
|
|
49
|
+
)`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function sameOwnerTerminalAttemptEpochSql(
|
|
53
|
+
tableAlias: string,
|
|
54
|
+
attemptOwnerRunIdExpression: string,
|
|
55
|
+
attemptSeqExpression: string,
|
|
56
|
+
): string {
|
|
57
|
+
return `(
|
|
58
|
+
${tableAlias}._status = 'enriched'
|
|
59
|
+
AND coalesce(${tableAlias}._attempt_owner_run_id, ${tableAlias}._run_id) = ${attemptOwnerRunIdExpression}
|
|
60
|
+
AND COALESCE(${tableAlias}._attempt_seq, 0) = ${attemptSeqExpression}::integer
|
|
61
|
+
)`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function newerTerminalRuntimeSheetRowSql(
|
|
65
|
+
tableAlias: string,
|
|
66
|
+
attemptExpiresAtExpression: string,
|
|
67
|
+
attemptSeqExpression?: string,
|
|
68
|
+
attemptIdExpression?: string,
|
|
69
|
+
attemptOwnerRunIdExpression?: string,
|
|
70
|
+
): string {
|
|
71
|
+
if (attemptSeqExpression !== undefined) {
|
|
72
|
+
const sameSeqDifferentAttemptSql = attemptOwnerRunIdExpression
|
|
73
|
+
? `OR (
|
|
74
|
+
COALESCE(${tableAlias}._attempt_seq, 0) = ${attemptSeqExpression}::integer
|
|
75
|
+
AND ${tableAlias}._attempt_id IS NOT NULL
|
|
76
|
+
AND ${attemptIdExpression} IS NOT NULL
|
|
77
|
+
AND ${tableAlias}._attempt_id IS DISTINCT FROM ${attemptIdExpression}
|
|
78
|
+
AND (
|
|
79
|
+
coalesce(${tableAlias}._attempt_owner_run_id, ${tableAlias}._run_id) IS DISTINCT FROM ${attemptOwnerRunIdExpression}
|
|
80
|
+
OR ${tableAlias}._attempt_expires_at > ${attemptExpiresAtExpression}
|
|
81
|
+
)
|
|
82
|
+
)`
|
|
83
|
+
: `OR (
|
|
84
|
+
COALESCE(${tableAlias}._attempt_seq, 0) = ${attemptSeqExpression}::integer
|
|
85
|
+
AND ${tableAlias}._attempt_id IS NOT NULL
|
|
86
|
+
AND ${attemptIdExpression} IS NOT NULL
|
|
87
|
+
AND ${tableAlias}._attempt_id IS DISTINCT FROM ${attemptIdExpression}
|
|
88
|
+
)`;
|
|
89
|
+
return `(
|
|
90
|
+
${tableAlias}._status = 'enriched'
|
|
91
|
+
AND (
|
|
92
|
+
COALESCE(${tableAlias}._attempt_seq, 0) > ${attemptSeqExpression}::integer
|
|
93
|
+
${attemptIdExpression ? sameSeqDifferentAttemptSql : ''}
|
|
94
|
+
)
|
|
95
|
+
)`;
|
|
96
|
+
}
|
|
97
|
+
return `(
|
|
98
|
+
${tableAlias}._status = 'enriched'
|
|
99
|
+
AND ${tableAlias}._attempt_expires_at IS NOT NULL
|
|
100
|
+
AND ${attemptExpiresAtExpression} IS NOT NULL
|
|
101
|
+
AND ${tableAlias}._attempt_expires_at > ${attemptExpiresAtExpression}
|
|
102
|
+
)`;
|
|
103
|
+
}
|
|
@@ -17,6 +17,20 @@ export type PlayExecutionSuspension =
|
|
|
17
17
|
eventKey: string;
|
|
18
18
|
timeoutMs: number;
|
|
19
19
|
}>;
|
|
20
|
+
}
|
|
21
|
+
| {
|
|
22
|
+
kind: 'child_play';
|
|
23
|
+
boundaryId: string;
|
|
24
|
+
childRunId: string;
|
|
25
|
+
childPlayName: string;
|
|
26
|
+
}
|
|
27
|
+
| {
|
|
28
|
+
kind: 'child_play_batch';
|
|
29
|
+
boundaries: Array<{
|
|
30
|
+
boundaryId: string;
|
|
31
|
+
childRunId: string;
|
|
32
|
+
childPlayName: string;
|
|
33
|
+
}>;
|
|
20
34
|
};
|
|
21
35
|
|
|
22
36
|
export class PlayExecutionSuspendedError extends Error {
|
|
@@ -28,7 +42,11 @@ export class PlayExecutionSuspendedError extends Error {
|
|
|
28
42
|
? `Play execution suspended for durable sleep (${suspension.delayMs}ms).`
|
|
29
43
|
: suspension.kind === 'integration_event_batch'
|
|
30
44
|
? `Play execution suspended waiting for ${suspension.boundaries.length} integration events.`
|
|
31
|
-
:
|
|
45
|
+
: suspension.kind === 'child_play_batch'
|
|
46
|
+
? `Play execution suspended waiting for ${suspension.boundaries.length} child plays.`
|
|
47
|
+
: suspension.kind === 'child_play'
|
|
48
|
+
? `Play execution suspended waiting for child play ${JSON.stringify(suspension.childPlayName)}.`
|
|
49
|
+
: `Play execution suspended waiting for integration event ${JSON.stringify(suspension.eventKey)}.`,
|
|
32
50
|
);
|
|
33
51
|
this.name = 'PlayExecutionSuspendedError';
|
|
34
52
|
this.suspension = suspension;
|