deepline 0.1.178 → 0.1.180
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/coordinator-entry.ts +132 -7
- package/dist/bundling-sources/apps/play-runner-workers/src/dedup-do.ts +25 -8
- package/dist/bundling-sources/apps/play-runner-workers/src/durable-object-deploy-handoff.ts +8 -2
- package/dist/bundling-sources/apps/play-runner-workers/src/entry.ts +414 -196
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/batching.ts +42 -2
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/coordinator-progress.ts +289 -0
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/tool-receipts.ts +112 -1
- package/dist/bundling-sources/sdk/src/client.ts +237 -0
- package/dist/bundling-sources/sdk/src/http.ts +100 -9
- package/dist/bundling-sources/sdk/src/release.ts +2 -2
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +10 -2
- package/dist/bundling-sources/shared_libs/play-runtime/batch-runtime.ts +58 -2
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +99 -25
- package/dist/bundling-sources/shared_libs/play-runtime/db-session.ts +7 -2
- package/dist/bundling-sources/shared_libs/play-runtime/persistence-latch.ts +167 -0
- package/dist/bundling-sources/shared_libs/play-runtime/receipt-salvage.ts +158 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-failure.ts +44 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +74 -30
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +6 -0
- package/dist/cli/index.js +338 -13
- package/dist/cli/index.mjs +338 -13
- package/dist/index.d.mts +44 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.js +229 -9
- package/dist/index.mjs +229 -9
- package/package.json +1 -1
|
@@ -155,10 +155,10 @@ import {
|
|
|
155
155
|
import type { RuntimeStepReceipt } from '../../../shared_libs/play-runtime/ctx-types';
|
|
156
156
|
import {
|
|
157
157
|
canReclaimFailedWorkerToolReceipt,
|
|
158
|
-
canReclaimTimedOutWorkerToolReceipt,
|
|
159
158
|
markWorkerToolReceiptResultCached,
|
|
160
159
|
markWorkerToolReceiptResultExecution,
|
|
161
160
|
planWorkerToolReceiptGroups,
|
|
161
|
+
reclaimRunningReceiptGroupAfterWait,
|
|
162
162
|
resolveWorkerToolReceiptGroupWaitMaxAttempts,
|
|
163
163
|
resolveWorkerToolRuntimeTimeoutMs,
|
|
164
164
|
} from './runtime/tool-receipts';
|
|
@@ -181,6 +181,10 @@ import {
|
|
|
181
181
|
setHarnessBinding,
|
|
182
182
|
} from '../../../sdk/src/plays/harness-stub';
|
|
183
183
|
import { createHarnessWorkerReceiptStore } from './runtime/harness-receipt-store';
|
|
184
|
+
import {
|
|
185
|
+
createCoordinatorProgressPublisher,
|
|
186
|
+
isCoordinatorProgressAuthorizationError,
|
|
187
|
+
} from './runtime/coordinator-progress';
|
|
184
188
|
import {
|
|
185
189
|
hydrateSerializedResultDatasets,
|
|
186
190
|
projectTerminalResultDatasets,
|
|
@@ -212,6 +216,22 @@ import {
|
|
|
212
216
|
} from '../../../shared_libs/play-runtime/csv-rename';
|
|
213
217
|
import { coordinatorRequestHeaders } from '../../../shared_libs/play-runtime/coordinator-headers';
|
|
214
218
|
import { normalizePlayRunFailure } from '../../../shared_libs/play-runtime/run-failure';
|
|
219
|
+
import {
|
|
220
|
+
createRuntimePersistenceLatch,
|
|
221
|
+
isRuntimePersistenceCircuitOpenError,
|
|
222
|
+
partitionDispatchWaves,
|
|
223
|
+
PROVIDER_DISPATCH_WAVE_SIZE,
|
|
224
|
+
RuntimePersistenceCircuitOpenError,
|
|
225
|
+
shouldShortenCompleteReceiptLadder,
|
|
226
|
+
tripRuntimePersistenceLatch,
|
|
227
|
+
type RuntimePersistenceLatch,
|
|
228
|
+
} from '../../../shared_libs/play-runtime/persistence-latch';
|
|
229
|
+
import {
|
|
230
|
+
createReceiptSalvageBuffer,
|
|
231
|
+
receiptSalvageFailureSuffix,
|
|
232
|
+
type ReceiptSalvage,
|
|
233
|
+
type ReceiptSalvageBuffer,
|
|
234
|
+
} from '../../../shared_libs/play-runtime/receipt-salvage';
|
|
215
235
|
import { createSecretRedactionContext } from '../../../shared_libs/play-runtime/secret-redaction';
|
|
216
236
|
import {
|
|
217
237
|
assertNoSecretTaint,
|
|
@@ -1735,10 +1755,23 @@ function isRuntimeReceiptPersistenceError(error: unknown): boolean {
|
|
|
1735
1755
|
|
|
1736
1756
|
function isRunFatalWorkerRowError(error: unknown): boolean {
|
|
1737
1757
|
return (
|
|
1738
|
-
|
|
1758
|
+
isCoordinatorProgressAuthorizationError(error) ||
|
|
1759
|
+
isRowIsolationExemptError(error) ||
|
|
1760
|
+
isRuntimeReceiptPersistenceError(error) ||
|
|
1761
|
+
isRuntimePersistenceCircuitOpenError(error)
|
|
1739
1762
|
);
|
|
1740
1763
|
}
|
|
1741
1764
|
|
|
1765
|
+
/**
|
|
1766
|
+
* completeReceipt salvage retry ladder. On a persistence failure after a billed
|
|
1767
|
+
* call we retry the receipt completion past the server-side 5s connect window:
|
|
1768
|
+
* 1 initial attempt + 3 retries at 1s / 3s / 9s (single isolate — no jitter).
|
|
1769
|
+
* Bounded by the run deadline (`runtimeDeadlineMs`); we stop early unless there
|
|
1770
|
+
* is at least one backoff's worth plus headroom of budget left.
|
|
1771
|
+
*/
|
|
1772
|
+
const COMPLETE_RECEIPT_RETRY_BACKOFFS_MS = [1_000, 3_000, 9_000] as const;
|
|
1773
|
+
const COMPLETE_RECEIPT_RETRY_MIN_HEADROOM_MS = 10_000;
|
|
1774
|
+
|
|
1742
1775
|
// Fallback batch-chunk parallelism when a tool declares no provider rate hints.
|
|
1743
1776
|
// Matches the prior hardcoded `Math.min(4, ...)` so undeclared providers keep
|
|
1744
1777
|
// their previous batching behavior; declared providers tighten via the
|
|
@@ -1814,8 +1847,96 @@ class WorkerToolBatchScheduler {
|
|
|
1814
1847
|
private readonly receiptStore?: WorkerRuntimeReceiptStore,
|
|
1815
1848
|
private readonly allowLocalRetryReceipts = false,
|
|
1816
1849
|
private readonly runtimeDeadlineMs?: number,
|
|
1850
|
+
// Map-wide (or ctx-wide for the root scheduler) persistence-failure circuit
|
|
1851
|
+
// breaker. Shared by every row worker so the first persistence failure
|
|
1852
|
+
// halts NEW provider dispatch across the whole map.
|
|
1853
|
+
private readonly latch: RuntimePersistenceLatch = createRuntimePersistenceLatch(),
|
|
1854
|
+
// Run-scoped buffer for orphaned billed results whose receipt could not be
|
|
1855
|
+
// completed even after the retry ladder.
|
|
1856
|
+
private readonly salvage?: ReceiptSalvageBuffer,
|
|
1817
1857
|
) {}
|
|
1818
1858
|
|
|
1859
|
+
/**
|
|
1860
|
+
* Run `completeReceipt`/`completeReceipts` with the salvage retry ladder.
|
|
1861
|
+
* Retries a receipt-completion failure past the server-side 5s connect window
|
|
1862
|
+
* so a transient blip does not orphan a billed result. Bounded by the run
|
|
1863
|
+
* deadline; skips retries when the latch has already burned a ladder in this
|
|
1864
|
+
* map so many in-flight orphans do not each serialize a long loop.
|
|
1865
|
+
*/
|
|
1866
|
+
private async completeReceiptWithRetryLadder<T>(
|
|
1867
|
+
run: () => Promise<T>,
|
|
1868
|
+
onGiveUp?: (attempts: number) => void,
|
|
1869
|
+
): Promise<T> {
|
|
1870
|
+
let attempt = 0;
|
|
1871
|
+
// A ladder that has already started retrying keeps its full budget even if
|
|
1872
|
+
// a peer trips the latch mid-flight — only FRESH entrants collapse once the
|
|
1873
|
+
// failure is confirmed. This is what guarantees the first ladder rides out
|
|
1874
|
+
// a genuine transient blip.
|
|
1875
|
+
let committedToFullLadder = false;
|
|
1876
|
+
const giveUp = (error: unknown): never => {
|
|
1877
|
+
this.latch.latchRetryBudgetExhausted = true;
|
|
1878
|
+
onGiveUp?.(attempt);
|
|
1879
|
+
throw error;
|
|
1880
|
+
};
|
|
1881
|
+
while (true) {
|
|
1882
|
+
try {
|
|
1883
|
+
return await run();
|
|
1884
|
+
} catch (error) {
|
|
1885
|
+
attempt += 1;
|
|
1886
|
+
if (
|
|
1887
|
+
shouldShortenCompleteReceiptLadder(this.latch, committedToFullLadder) ||
|
|
1888
|
+
attempt > COMPLETE_RECEIPT_RETRY_BACKOFFS_MS.length ||
|
|
1889
|
+
this.abortSignal?.aborted
|
|
1890
|
+
) {
|
|
1891
|
+
return giveUp(error);
|
|
1892
|
+
}
|
|
1893
|
+
const backoffMs = COMPLETE_RECEIPT_RETRY_BACKOFFS_MS[attempt - 1]!;
|
|
1894
|
+
// resolveRuntimeDeadlineRemainingMs THROWS WorkflowAbortError once the
|
|
1895
|
+
// run deadline has passed — the correct loud outcome (there is no
|
|
1896
|
+
// separate per-chunk step timeout). Route that throw through giveUp so
|
|
1897
|
+
// the billed result is still salvaged and the ladder budget flag is
|
|
1898
|
+
// set: a billed output must never lose its durable trace because the
|
|
1899
|
+
// clock ran out mid-ladder.
|
|
1900
|
+
let remainingMs: number;
|
|
1901
|
+
try {
|
|
1902
|
+
remainingMs = resolveRuntimeDeadlineRemainingMs(
|
|
1903
|
+
this.runtimeDeadlineMs,
|
|
1904
|
+
);
|
|
1905
|
+
} catch (deadlineError) {
|
|
1906
|
+
return giveUp(deadlineError);
|
|
1907
|
+
}
|
|
1908
|
+
if (remainingMs <= backoffMs + COMPLETE_RECEIPT_RETRY_MIN_HEADROOM_MS) {
|
|
1909
|
+
return giveUp(error);
|
|
1910
|
+
}
|
|
1911
|
+
// Past every give-up gate: this ladder is now committed to the full
|
|
1912
|
+
// budget and must not be collapsed by a peer tripping the latch mid-sleep.
|
|
1913
|
+
committedToFullLadder = true;
|
|
1914
|
+
await sleepWorkerMs(backoffMs);
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
/**
|
|
1920
|
+
* Record a billed-but-unpersisted tool output into the run-scoped salvage
|
|
1921
|
+
* buffer. Called only after the completeReceipt ladder is exhausted, with the
|
|
1922
|
+
* exact serialized payload a receipt would have stored.
|
|
1923
|
+
*/
|
|
1924
|
+
private recordReceiptSalvage(input: {
|
|
1925
|
+
claimed: ClaimedWorkerToolBatchRequest;
|
|
1926
|
+
output: unknown;
|
|
1927
|
+
attempts: number;
|
|
1928
|
+
}): void {
|
|
1929
|
+
if (!this.salvage || !input.claimed.receiptKey) return;
|
|
1930
|
+
this.salvage.push({
|
|
1931
|
+
receiptKey: input.claimed.receiptKey,
|
|
1932
|
+
toolId: input.claimed.request.toolId,
|
|
1933
|
+
cacheKey: input.claimed.request.cacheKey,
|
|
1934
|
+
output: input.output,
|
|
1935
|
+
completeAttempts: input.attempts,
|
|
1936
|
+
firstErrorAt: nowMs(),
|
|
1937
|
+
});
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1819
1940
|
/**
|
|
1820
1941
|
* Report a provider 429 / Retry-After back into the Governor's shared pacer
|
|
1821
1942
|
* so future acquires for this (org, provider) bucket back off across all
|
|
@@ -1858,9 +1979,7 @@ class WorkerToolBatchScheduler {
|
|
|
1858
1979
|
id,
|
|
1859
1980
|
cacheKey: receiptKey,
|
|
1860
1981
|
receiptKey,
|
|
1861
|
-
force:
|
|
1862
|
-
options?.force === true ||
|
|
1863
|
-
!!this.req.force,
|
|
1982
|
+
force: options?.force === true || !!this.req.force,
|
|
1864
1983
|
receiptWaitMaxAttempts: resolveRuntimeToolReceiptWaitMaxAttempts(
|
|
1865
1984
|
typeof runtimeOptions?.receiptWaitMs === 'number'
|
|
1866
1985
|
? { max_wait_ms: runtimeOptions.receiptWaitMs }
|
|
@@ -2050,37 +2169,73 @@ class WorkerToolBatchScheduler {
|
|
|
2050
2169
|
);
|
|
2051
2170
|
return [];
|
|
2052
2171
|
}
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2172
|
+
// A `running` receipt left by a DIFFERENT run whose last write is stale will
|
|
2173
|
+
// never be completed by a live worker. Polling it burns one service-binding
|
|
2174
|
+
// subrequest per tick, up to the receipt's wait budget (~1320) — many such
|
|
2175
|
+
// orphans on a resume exhaust Cloudflare's per-invocation subrequest budget
|
|
2176
|
+
// and kill the run. The orchestrator reclaims those immediately (skipping the
|
|
2177
|
+
// poll) and re-executes, matching the "on resume it reclaims and re-executes"
|
|
2178
|
+
// contract; live/fresh/same-run owners still poll before reclaiming. Kept as
|
|
2179
|
+
// an injected helper so the skip-poll decision is unit-testable without
|
|
2180
|
+
// loading this cloudflare:workers Worker entrypoint.
|
|
2181
|
+
return await reclaimRunningReceiptGroupAfterWait<ClaimedWorkerToolBatchRequest>(
|
|
2182
|
+
{
|
|
2183
|
+
ownership: {
|
|
2184
|
+
ownerRunId: input.runningReceipt.runId,
|
|
2185
|
+
currentRunId: this.req.runId,
|
|
2186
|
+
updatedAt: input.runningReceipt.updatedAt,
|
|
2187
|
+
},
|
|
2188
|
+
timeoutError: new RuntimeReceiptWaitTimeoutError(input.receiptKey),
|
|
2189
|
+
poll: async () => {
|
|
2190
|
+
try {
|
|
2191
|
+
const receipt = await this.waitForDurableToolReceipt({
|
|
2192
|
+
receiptKey: input.receiptKey,
|
|
2193
|
+
maxAttempts: resolveWorkerToolReceiptGroupWaitMaxAttempts(
|
|
2194
|
+
input.group,
|
|
2195
|
+
(groupRequest) => groupRequest.receiptWaitMaxAttempts,
|
|
2196
|
+
),
|
|
2197
|
+
});
|
|
2198
|
+
await this.resolveCompletedDurableToolReceiptGroup({
|
|
2199
|
+
group: input.group,
|
|
2200
|
+
receiptKey: input.receiptKey,
|
|
2201
|
+
receipt,
|
|
2202
|
+
source: 'in_flight',
|
|
2203
|
+
});
|
|
2204
|
+
return { kind: 'completed' };
|
|
2205
|
+
} catch (error) {
|
|
2206
|
+
if (error instanceof RuntimeReceiptWaitTimeoutError) {
|
|
2207
|
+
return { kind: 'timed_out', error };
|
|
2208
|
+
}
|
|
2209
|
+
return { kind: 'errored', error };
|
|
2210
|
+
}
|
|
2211
|
+
},
|
|
2212
|
+
reclaim: (rejectionOnFallthrough) =>
|
|
2213
|
+
this.forceReclaimRunningDurableToolReceiptGroup({
|
|
2214
|
+
group: input.group,
|
|
2215
|
+
request,
|
|
2216
|
+
followers,
|
|
2217
|
+
receiptKey: input.receiptKey,
|
|
2218
|
+
rejectionOnFallthrough,
|
|
2219
|
+
}),
|
|
2220
|
+
reject: (error) => this.rejectRawRequests(input.group, error),
|
|
2221
|
+
},
|
|
2222
|
+
);
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
/**
|
|
2226
|
+
* Steal a `running` receipt (reclaimRunning) and route by claim disposition.
|
|
2227
|
+
* Shared by the post-poll-timeout reclaim and the dead-owner fast path, so
|
|
2228
|
+
* both dispose of a reclaimed/reused/failed receipt identically.
|
|
2229
|
+
*/
|
|
2230
|
+
private async forceReclaimRunningDurableToolReceiptGroup(input: {
|
|
2231
|
+
group: WorkerToolBatchRequest[];
|
|
2232
|
+
request: WorkerToolBatchRequest;
|
|
2233
|
+
followers: WorkerToolBatchRequest[];
|
|
2234
|
+
receiptKey: string;
|
|
2235
|
+
rejectionOnFallthrough: unknown;
|
|
2236
|
+
}): Promise<ClaimedWorkerToolBatchRequest[]> {
|
|
2237
|
+
if (!this.receiptStore) {
|
|
2238
|
+
this.rejectRawRequests(input.group, input.rejectionOnFallthrough);
|
|
2084
2239
|
return [];
|
|
2085
2240
|
}
|
|
2086
2241
|
let claim: WorkerRuntimeReceiptClaim;
|
|
@@ -2096,7 +2251,13 @@ class WorkerToolBatchScheduler {
|
|
|
2096
2251
|
return [];
|
|
2097
2252
|
}
|
|
2098
2253
|
if (claim.disposition === 'claimed') {
|
|
2099
|
-
return [
|
|
2254
|
+
return [
|
|
2255
|
+
{
|
|
2256
|
+
request: input.request,
|
|
2257
|
+
receiptKey: input.receiptKey,
|
|
2258
|
+
followers: input.followers,
|
|
2259
|
+
},
|
|
2260
|
+
];
|
|
2100
2261
|
}
|
|
2101
2262
|
if (claim.disposition === 'reused') {
|
|
2102
2263
|
await this.resolveCompletedDurableToolReceiptGroup({
|
|
@@ -2114,7 +2275,7 @@ class WorkerToolBatchScheduler {
|
|
|
2114
2275
|
failedReceipt: claim.receipt,
|
|
2115
2276
|
});
|
|
2116
2277
|
}
|
|
2117
|
-
this.rejectRawRequests(input.group,
|
|
2278
|
+
this.rejectRawRequests(input.group, input.rejectionOnFallthrough);
|
|
2118
2279
|
return [];
|
|
2119
2280
|
}
|
|
2120
2281
|
|
|
@@ -2199,6 +2360,7 @@ class WorkerToolBatchScheduler {
|
|
|
2199
2360
|
await this.failDurableToolRequest(claimed, error);
|
|
2200
2361
|
return error;
|
|
2201
2362
|
} catch (receiptError) {
|
|
2363
|
+
tripRuntimePersistenceLatch(this.latch, receiptError);
|
|
2202
2364
|
return new RuntimeReceiptPersistenceError(
|
|
2203
2365
|
'Tool call failed and durable receipt could not be marked failed',
|
|
2204
2366
|
[error, receiptError],
|
|
@@ -2350,6 +2512,9 @@ class WorkerToolBatchScheduler {
|
|
|
2350
2512
|
claimedRequests.push({ request, receiptKey, followers });
|
|
2351
2513
|
}
|
|
2352
2514
|
} catch (error) {
|
|
2515
|
+
// A receipt claim failure means the tenant Postgres is already
|
|
2516
|
+
// unwritable: trip the breaker so remaining groups/rows stop dispatching.
|
|
2517
|
+
tripRuntimePersistenceLatch(this.latch, error);
|
|
2353
2518
|
for (const group of receiptGroupPlan.durableGroups) {
|
|
2354
2519
|
this.rejectRawRequests(group.requests, error);
|
|
2355
2520
|
}
|
|
@@ -2368,12 +2533,25 @@ class WorkerToolBatchScheduler {
|
|
|
2368
2533
|
kind: 'live',
|
|
2369
2534
|
receiptKey: claimed.receiptKey,
|
|
2370
2535
|
});
|
|
2371
|
-
const
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2536
|
+
const serializedOutput = serializeDurableStepValue(ownerResult);
|
|
2537
|
+
const completed = await this.completeReceiptWithRetryLadder(
|
|
2538
|
+
() =>
|
|
2539
|
+
this.receiptStore!.completeReceipt({
|
|
2540
|
+
playName: this.req.playName,
|
|
2541
|
+
runId: this.req.runId,
|
|
2542
|
+
key: claimed.receiptKey!,
|
|
2543
|
+
output: serializedOutput,
|
|
2544
|
+
}),
|
|
2545
|
+
// Ladder exhausted: the call billed but the receipt could not land.
|
|
2546
|
+
// Salvage the exact serialized output onto the run record before the
|
|
2547
|
+
// caller turns this into a RuntimeReceiptPersistenceError.
|
|
2548
|
+
(attempts) =>
|
|
2549
|
+
this.recordReceiptSalvage({
|
|
2550
|
+
claimed,
|
|
2551
|
+
output: serializedOutput,
|
|
2552
|
+
attempts,
|
|
2553
|
+
}),
|
|
2554
|
+
);
|
|
2377
2555
|
if (
|
|
2378
2556
|
completed &&
|
|
2379
2557
|
(completed.status === 'completed' || completed.status === 'skipped') &&
|
|
@@ -2449,14 +2627,32 @@ class WorkerToolBatchScheduler {
|
|
|
2449
2627
|
);
|
|
2450
2628
|
return results;
|
|
2451
2629
|
}
|
|
2452
|
-
const
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2630
|
+
const serializedByEntry = durableEntries.map((entry) =>
|
|
2631
|
+
serializeDurableStepValue(entry.ownerResult),
|
|
2632
|
+
);
|
|
2633
|
+
const completed = await this.completeReceiptWithRetryLadder(
|
|
2634
|
+
() =>
|
|
2635
|
+
this.receiptStore!.completeReceipts!({
|
|
2636
|
+
playName: this.req.playName,
|
|
2637
|
+
receipts: durableEntries.map((entry, entryIndex) => ({
|
|
2638
|
+
runId: this.req.runId,
|
|
2639
|
+
key: entry.claimed.receiptKey!,
|
|
2640
|
+
output: serializedByEntry[entryIndex],
|
|
2641
|
+
})),
|
|
2642
|
+
}),
|
|
2643
|
+
// Ladder exhausted: every entry in this batch billed but could not be
|
|
2644
|
+
// marked completed. Salvage each serialized output before the caller
|
|
2645
|
+
// turns this into a RuntimeReceiptPersistenceError.
|
|
2646
|
+
(attempts) => {
|
|
2647
|
+
durableEntries.forEach((entry, entryIndex) => {
|
|
2648
|
+
this.recordReceiptSalvage({
|
|
2649
|
+
claimed: entry.claimed,
|
|
2650
|
+
output: serializedByEntry[entryIndex],
|
|
2651
|
+
attempts,
|
|
2652
|
+
});
|
|
2653
|
+
});
|
|
2654
|
+
},
|
|
2655
|
+
);
|
|
2460
2656
|
durableEntries.forEach((entry, resultIndex) => {
|
|
2461
2657
|
results[entry.index] = this.completedDurableToolRequestResult({
|
|
2462
2658
|
claimed: entry.claimed,
|
|
@@ -2510,6 +2706,17 @@ class WorkerToolBatchScheduler {
|
|
|
2510
2706
|
toolId: string,
|
|
2511
2707
|
requests: WorkerToolBatchRequest[],
|
|
2512
2708
|
): Promise<void> {
|
|
2709
|
+
// Circuit breaker, gated BEFORE the receipt claim: never-dispatched rows
|
|
2710
|
+
// get NO receipt (not even `running`), so a re-run claims them fresh and
|
|
2711
|
+
// executes them exactly once.
|
|
2712
|
+
if (this.latch.tripped) {
|
|
2713
|
+
this.latch.preventedCallCount += requests.length;
|
|
2714
|
+
this.rejectRawRequests(
|
|
2715
|
+
requests,
|
|
2716
|
+
new RuntimePersistenceCircuitOpenError(this.latch),
|
|
2717
|
+
);
|
|
2718
|
+
return;
|
|
2719
|
+
}
|
|
2513
2720
|
const { claimedRequests, deferredClaimedRequests } =
|
|
2514
2721
|
await this.prepareDurableToolRequests(requests);
|
|
2515
2722
|
await this.executeClaimedToolRequests(
|
|
@@ -2545,9 +2752,23 @@ class WorkerToolBatchScheduler {
|
|
|
2545
2752
|
claimedRequests.length < 2
|
|
2546
2753
|
) {
|
|
2547
2754
|
const groupStartedAt = nowMs();
|
|
2548
|
-
|
|
2549
|
-
|
|
2755
|
+
const dispatchOne = async (
|
|
2756
|
+
claimed: ClaimedWorkerToolBatchRequest,
|
|
2757
|
+
): Promise<void> => {
|
|
2550
2758
|
const { request } = claimed;
|
|
2759
|
+
// Circuit breaker: the latch may have tripped after this group was
|
|
2760
|
+
// claimed but before this particular call started. Do NOT call the
|
|
2761
|
+
// provider. This row has a `running` receipt (claimed, never
|
|
2762
|
+
// executed); on resume it reclaims and re-executes — safe, it never
|
|
2763
|
+
// billed.
|
|
2764
|
+
if (this.latch.tripped) {
|
|
2765
|
+
this.latch.preventedCallCount += 1 + claimed.followers.length;
|
|
2766
|
+
this.rejectRequests(
|
|
2767
|
+
claimed,
|
|
2768
|
+
new RuntimePersistenceCircuitOpenError(this.latch),
|
|
2769
|
+
);
|
|
2770
|
+
return;
|
|
2771
|
+
}
|
|
2551
2772
|
const toolContract = await this.resolvePacing(toolId).catch(
|
|
2552
2773
|
() => null,
|
|
2553
2774
|
);
|
|
@@ -2586,6 +2807,7 @@ class WorkerToolBatchScheduler {
|
|
|
2586
2807
|
await this.completeDurableToolRequest(claimed, result),
|
|
2587
2808
|
);
|
|
2588
2809
|
} catch (receiptError) {
|
|
2810
|
+
tripRuntimePersistenceLatch(this.latch, receiptError);
|
|
2589
2811
|
this.rejectRequests(
|
|
2590
2812
|
claimed,
|
|
2591
2813
|
new RuntimeReceiptPersistenceError(
|
|
@@ -2596,8 +2818,32 @@ class WorkerToolBatchScheduler {
|
|
|
2596
2818
|
} finally {
|
|
2597
2819
|
slot.release();
|
|
2598
2820
|
}
|
|
2599
|
-
|
|
2600
|
-
|
|
2821
|
+
};
|
|
2822
|
+
// Bounded dispatch waves: an unbatched tool group can carry a whole chunk's
|
|
2823
|
+
// rows (enrich compiles to one ~245-row chunk). Dispatching them all at
|
|
2824
|
+
// once billed every call before the first receipt-completion failure could
|
|
2825
|
+
// trip the latch (the 250/250 incident). Instead, dispatch one wave, AWAIT
|
|
2826
|
+
// it so any completion failure trips the latch, then read the latch before
|
|
2827
|
+
// the next wave — remaining waves are prevented (counted + rejected) exactly
|
|
2828
|
+
// like the mid-wave gate above. The wave calls stay fully concurrent; the
|
|
2829
|
+
// only added cost on the healthy path is one latch read per wave.
|
|
2830
|
+
for (const wave of partitionDispatchWaves(
|
|
2831
|
+
claimedRequests,
|
|
2832
|
+
() => 1,
|
|
2833
|
+
PROVIDER_DISPATCH_WAVE_SIZE,
|
|
2834
|
+
)) {
|
|
2835
|
+
if (this.latch.tripped) {
|
|
2836
|
+
for (const claimed of wave) {
|
|
2837
|
+
this.latch.preventedCallCount += 1 + claimed.followers.length;
|
|
2838
|
+
this.rejectRequests(
|
|
2839
|
+
claimed,
|
|
2840
|
+
new RuntimePersistenceCircuitOpenError(this.latch),
|
|
2841
|
+
);
|
|
2842
|
+
}
|
|
2843
|
+
continue;
|
|
2844
|
+
}
|
|
2845
|
+
await Promise.all(wave.map(dispatchOne));
|
|
2846
|
+
}
|
|
2601
2847
|
recordRunnerPerfTrace({
|
|
2602
2848
|
req: this.req,
|
|
2603
2849
|
phase: 'runner.tool.group',
|
|
@@ -2635,6 +2881,7 @@ class WorkerToolBatchScheduler {
|
|
|
2635
2881
|
await this.failDurableToolRequests(claimedRequests, error),
|
|
2636
2882
|
settleRequest: (claimed, result) => this.settleRequests(claimed, result),
|
|
2637
2883
|
rejectRequest: (claimed, error) => this.rejectRequests(claimed, error),
|
|
2884
|
+
latch: this.latch,
|
|
2638
2885
|
});
|
|
2639
2886
|
recordRunnerPerfTrace({
|
|
2640
2887
|
req: this.req,
|
|
@@ -2692,6 +2939,7 @@ async function executeBatchedWorkerToolGroup(input: {
|
|
|
2692
2939
|
request: ClaimedWorkerToolBatchRequest,
|
|
2693
2940
|
error: unknown,
|
|
2694
2941
|
) => void;
|
|
2942
|
+
latch: RuntimePersistenceLatch;
|
|
2695
2943
|
}): Promise<void> {
|
|
2696
2944
|
const compiledBatches = compileRequestsWithStrategy({
|
|
2697
2945
|
requests: input.requests,
|
|
@@ -2719,7 +2967,20 @@ async function executeBatchedWorkerToolGroup(input: {
|
|
|
2719
2967
|
1,
|
|
2720
2968
|
Math.min(input.suggestedParallelism, compiledBatches.length || 1),
|
|
2721
2969
|
),
|
|
2970
|
+
// Bounded dispatch wave: cap the provider calls (counted by batch member
|
|
2971
|
+
// size) a single chunk dispatches before the latch is read again, so a
|
|
2972
|
+
// persistence failure prevents the rest of a large batch group instead of
|
|
2973
|
+
// billing it all. A single over-budget batch still forms its own wave.
|
|
2974
|
+
maxChunkWeight: PROVIDER_DISPATCH_WAVE_SIZE,
|
|
2975
|
+
weightOf: (batch) => batch.memberRequests.length,
|
|
2722
2976
|
execute: async (batch) => {
|
|
2977
|
+
// Circuit breaker: once a persistence failure has occurred in this map,
|
|
2978
|
+
// do NOT dispatch this batch's provider call. The chunk plumbing rejects
|
|
2979
|
+
// its member requests with the latch error.
|
|
2980
|
+
if (input.latch.tripped) {
|
|
2981
|
+
input.latch.preventedCallCount += batch.memberRequests.length;
|
|
2982
|
+
throw new RuntimePersistenceCircuitOpenError(input.latch);
|
|
2983
|
+
}
|
|
2723
2984
|
const toolContract = await input
|
|
2724
2985
|
.resolveToolContract(batch.batchOperation)
|
|
2725
2986
|
.catch(() => null);
|
|
@@ -2768,6 +3029,7 @@ async function executeBatchedWorkerToolGroup(input: {
|
|
|
2768
3029
|
try {
|
|
2769
3030
|
await input.failRequests(entry.request.memberRequests, entry.error);
|
|
2770
3031
|
} catch (receiptError) {
|
|
3032
|
+
tripRuntimePersistenceLatch(input.latch, receiptError);
|
|
2771
3033
|
rejection = new RuntimeReceiptPersistenceError(
|
|
2772
3034
|
'Tool call failed and durable receipts could not be marked failed',
|
|
2773
3035
|
[entry.error, receiptError],
|
|
@@ -2798,6 +3060,7 @@ async function executeBatchedWorkerToolGroup(input: {
|
|
|
2798
3060
|
})),
|
|
2799
3061
|
);
|
|
2800
3062
|
} catch (receiptError) {
|
|
3063
|
+
tripRuntimePersistenceLatch(input.latch, receiptError);
|
|
2801
3064
|
const rejection = new RuntimeReceiptPersistenceError(
|
|
2802
3065
|
'Tool call succeeded but durable receipts could not be marked completed',
|
|
2803
3066
|
[receiptError],
|
|
@@ -2819,6 +3082,7 @@ async function executeBatchedWorkerToolGroup(input: {
|
|
|
2819
3082
|
try {
|
|
2820
3083
|
await input.failRequests(input.requests, error);
|
|
2821
3084
|
} catch (receiptError) {
|
|
3085
|
+
tripRuntimePersistenceLatch(input.latch, receiptError);
|
|
2822
3086
|
rejection = new RuntimeReceiptPersistenceError(
|
|
2823
3087
|
'Tool call failed and durable receipts could not be marked failed',
|
|
2824
3088
|
[error, receiptError],
|
|
@@ -4147,10 +4411,15 @@ async function prepareMapRows(input: {
|
|
|
4147
4411
|
|
|
4148
4412
|
function assertNotAborted(signal: AbortSignal | undefined): void {
|
|
4149
4413
|
if (signal?.aborted) {
|
|
4414
|
+
if (isCoordinatorProgressAuthorizationError(signal.reason)) {
|
|
4415
|
+
throw signal.reason;
|
|
4416
|
+
}
|
|
4150
4417
|
throw new WorkflowAbortError(
|
|
4151
|
-
|
|
4152
|
-
? signal.reason
|
|
4153
|
-
: '
|
|
4418
|
+
signal.reason instanceof Error && signal.reason.message
|
|
4419
|
+
? signal.reason.message
|
|
4420
|
+
: typeof signal.reason === 'string' && signal.reason
|
|
4421
|
+
? signal.reason
|
|
4422
|
+
: 'Play run cancelled.',
|
|
4154
4423
|
);
|
|
4155
4424
|
}
|
|
4156
4425
|
}
|
|
@@ -4478,6 +4747,7 @@ function createMinimalWorkerCtx(
|
|
|
4478
4747
|
abortSignal?: AbortSignal,
|
|
4479
4748
|
callbacks?: WorkerCtxCallbacks,
|
|
4480
4749
|
runtimeDeadlineMs?: number,
|
|
4750
|
+
receiptSalvage?: ReceiptSalvageBuffer,
|
|
4481
4751
|
): unknown {
|
|
4482
4752
|
const { governor, resolvePacing: resolveToolPacing } =
|
|
4483
4753
|
createGovernorForRun(req);
|
|
@@ -4591,6 +4861,10 @@ function createMinimalWorkerCtx(
|
|
|
4591
4861
|
staleAfterSeconds,
|
|
4592
4862
|
});
|
|
4593
4863
|
};
|
|
4864
|
+
// Ctx-wide breaker for top-level ctx.tool calls (outside any map). Each map
|
|
4865
|
+
// creates its own map-scoped latch (see runMap) so per-map dispatch stats and
|
|
4866
|
+
// the runMap failure message stay map-local.
|
|
4867
|
+
const rootPersistenceLatch = createRuntimePersistenceLatch();
|
|
4594
4868
|
const rootToolBatchScheduler = new WorkerToolBatchScheduler(
|
|
4595
4869
|
req,
|
|
4596
4870
|
governor,
|
|
@@ -4602,6 +4876,8 @@ function createMinimalWorkerCtx(
|
|
|
4602
4876
|
receiptStore,
|
|
4603
4877
|
true,
|
|
4604
4878
|
runtimeDeadlineMs,
|
|
4879
|
+
rootPersistenceLatch,
|
|
4880
|
+
receiptSalvage,
|
|
4605
4881
|
);
|
|
4606
4882
|
// Local ancestry chain that always ENDS with the currently-executing play
|
|
4607
4883
|
// (req.playName). The /api/v2/plays/run lineage validator requires the
|
|
@@ -4637,6 +4913,12 @@ function createMinimalWorkerCtx(
|
|
|
4637
4913
|
): Promise<unknown> => {
|
|
4638
4914
|
const mapStartedAt = nowMs();
|
|
4639
4915
|
const mapNodeId = `map:${name}`;
|
|
4916
|
+
// Map-wide persistence-failure circuit breaker. Shared across every chunk's
|
|
4917
|
+
// scheduler (the scheduler is rebuilt per chunk) and the map's sheet-persist
|
|
4918
|
+
// flush chain, so the first persistence failure halts NEW provider dispatch
|
|
4919
|
+
// for the rest of the map and its prevented-call count surfaces in the
|
|
4920
|
+
// runMap failure message below.
|
|
4921
|
+
const persistenceLatch = createRuntimePersistenceLatch();
|
|
4640
4922
|
const inputRows = rows;
|
|
4641
4923
|
const rowCountHint = datasetRowCountHint(inputRows);
|
|
4642
4924
|
const baseOffset = 0;
|
|
@@ -5080,6 +5362,8 @@ function createMinimalWorkerCtx(
|
|
|
5080
5362
|
receiptStore,
|
|
5081
5363
|
false,
|
|
5082
5364
|
runtimeDeadlineMs,
|
|
5365
|
+
persistenceLatch,
|
|
5366
|
+
receiptSalvage,
|
|
5083
5367
|
);
|
|
5084
5368
|
let stepCellsCompleted = 0;
|
|
5085
5369
|
let stepCellsSkipped = 0;
|
|
@@ -5222,6 +5506,9 @@ function createMinimalWorkerCtx(
|
|
|
5222
5506
|
});
|
|
5223
5507
|
persistFlushChain = task.catch((error) => {
|
|
5224
5508
|
persistFailure ??= error;
|
|
5509
|
+
// Output-sheet flush failed: trip the breaker so the scheduler stops
|
|
5510
|
+
// dispatching new provider calls for the rest of this map.
|
|
5511
|
+
tripRuntimePersistenceLatch(persistenceLatch, error);
|
|
5225
5512
|
});
|
|
5226
5513
|
return task;
|
|
5227
5514
|
};
|
|
@@ -5370,11 +5657,18 @@ function createMinimalWorkerCtx(
|
|
|
5370
5657
|
request.input,
|
|
5371
5658
|
workflowStep,
|
|
5372
5659
|
{
|
|
5373
|
-
force:
|
|
5374
|
-
request.force === true ||
|
|
5375
|
-
!!req.force,
|
|
5660
|
+
force: request.force === true || !!req.force,
|
|
5376
5661
|
staleAfterSeconds: request.staleAfterSeconds,
|
|
5377
5662
|
},
|
|
5663
|
+
// Parity with the play-level ctx.tools.execute (and the
|
|
5664
|
+
// cjs runner's map path): the public ToolExecutionRequest
|
|
5665
|
+
// runtime options must not be silently dropped for map
|
|
5666
|
+
// rows — receiptWaitMs bounds how long a resumed run
|
|
5667
|
+
// waits on a stuck `running` receipt before reclaiming.
|
|
5668
|
+
{
|
|
5669
|
+
timeoutMs: request.timeoutMs,
|
|
5670
|
+
receiptWaitMs: request.receiptWaitMs,
|
|
5671
|
+
},
|
|
5378
5672
|
);
|
|
5379
5673
|
},
|
|
5380
5674
|
},
|
|
@@ -5668,7 +5962,13 @@ function createMinimalWorkerCtx(
|
|
|
5668
5962
|
result.status === 'rejected',
|
|
5669
5963
|
);
|
|
5670
5964
|
if (rejectedWorker) {
|
|
5671
|
-
if (
|
|
5965
|
+
if (
|
|
5966
|
+
isRuntimeReceiptPersistenceError(rejectedWorker.reason) ||
|
|
5967
|
+
isRuntimePersistenceCircuitOpenError(rejectedWorker.reason)
|
|
5968
|
+
) {
|
|
5969
|
+
// Persistence-failure / circuit-breaker rejections are returned as a
|
|
5970
|
+
// fatal summary (NOT rethrown into the durable step) so the chunk is
|
|
5971
|
+
// not retried and provider calls are not re-billed.
|
|
5672
5972
|
return await fatalMapChunkSummary(rejectedWorker.reason);
|
|
5673
5973
|
}
|
|
5674
5974
|
throw rejectedWorker.reason;
|
|
@@ -5872,7 +6172,7 @@ function createMinimalWorkerCtx(
|
|
|
5872
6172
|
? ` cells=${totalStepCells}/${totalStepCellsCompleted}/${totalStepCellsSkipped}`
|
|
5873
6173
|
: '';
|
|
5874
6174
|
callbacks?.onMapCompleted?.(mapNodeId, completedAt);
|
|
5875
|
-
|
|
6175
|
+
await updateMapProgress({
|
|
5876
6176
|
completed: totalRowsWritten,
|
|
5877
6177
|
total: totalRowsWritten + totalRowsFailed,
|
|
5878
6178
|
failed: totalRowsFailed,
|
|
@@ -5975,10 +6275,16 @@ function createMinimalWorkerCtx(
|
|
|
5975
6275
|
}
|
|
5976
6276
|
const chunkResult = await runChunkStep(chunkRows, chunkStart, chunkIndex);
|
|
5977
6277
|
if (chunkResult.fatalError) {
|
|
6278
|
+
const circuitBreakerSentence =
|
|
6279
|
+
persistenceLatch.tripped && persistenceLatch.preventedCallCount > 0
|
|
6280
|
+
? `Circuit breaker prevented ${persistenceLatch.preventedCallCount} queued provider call(s) ` +
|
|
6281
|
+
`from dispatching after the first persistence failure. `
|
|
6282
|
+
: '';
|
|
5978
6283
|
throw new Error(
|
|
5979
6284
|
`ctx.dataset("${name}") stopped after a runtime persistence failure ` +
|
|
5980
6285
|
`outside the retryable chunk step. Provider calls already executed for ` +
|
|
5981
6286
|
`${chunkResult.rowsExecuted} row(s), so the chunk was not retried. ` +
|
|
6287
|
+
circuitBreakerSentence +
|
|
5982
6288
|
`Fix the runtime persistence cause and re-run to resume. ` +
|
|
5983
6289
|
`First error: ${chunkResult.fatalError}`,
|
|
5984
6290
|
);
|
|
@@ -6360,9 +6666,7 @@ function createMinimalWorkerCtx(
|
|
|
6360
6666
|
request.input,
|
|
6361
6667
|
workflowStep,
|
|
6362
6668
|
{
|
|
6363
|
-
force:
|
|
6364
|
-
request.force === true ||
|
|
6365
|
-
!!req.force,
|
|
6669
|
+
force: request.force === true || !!req.force,
|
|
6366
6670
|
staleAfterSeconds: request.staleAfterSeconds,
|
|
6367
6671
|
},
|
|
6368
6672
|
{
|
|
@@ -7196,9 +7500,6 @@ async function executeRunRequest(
|
|
|
7196
7500
|
let lastLedgerFlushAt = startedAt;
|
|
7197
7501
|
let ledgerFlushInFlight: Promise<void> = Promise.resolve();
|
|
7198
7502
|
let ledgerFlushQueueDepth = 0;
|
|
7199
|
-
let lastCoordinatorProgressPublishAt = 0;
|
|
7200
|
-
let coordinatorProgressPublishInFlight: Promise<void> = Promise.resolve();
|
|
7201
|
-
let coordinatorProgressPublishQueueDepth = 0;
|
|
7202
7503
|
|
|
7203
7504
|
const appendRunLogLine = (line: string) => {
|
|
7204
7505
|
const trimmed = redactSecretsFromLogString(line.trim());
|
|
@@ -7225,130 +7526,24 @@ async function executeRunRequest(
|
|
|
7225
7526
|
};
|
|
7226
7527
|
|
|
7227
7528
|
const stepProgressSnapshot = () => ({ ...stepProgressByNodeId });
|
|
7228
|
-
|
|
7229
|
-
|
|
7230
|
-
|
|
7231
|
-
|
|
7232
|
-
|
|
7233
|
-
|
|
7234
|
-
|
|
7235
|
-
|
|
7236
|
-
|
|
7237
|
-
|
|
7238
|
-
|
|
7239
|
-
});
|
|
7240
|
-
return;
|
|
7241
|
-
}
|
|
7242
|
-
const publishStartedAt = nowMs();
|
|
7243
|
-
const liveNodeProgress = stepProgressSnapshot();
|
|
7244
|
-
const activeEntry =
|
|
7245
|
-
Object.entries(liveNodeProgress).find(
|
|
7246
|
-
([, progress]) => typeof progress.completedAt !== 'number',
|
|
7247
|
-
) ?? Object.entries(liveNodeProgress).at(-1);
|
|
7248
|
-
const activeNodeId = activeEntry?.[0] ?? null;
|
|
7249
|
-
const activeProgress = activeEntry?.[1] ?? null;
|
|
7250
|
-
const activeArtifactTableNamespace =
|
|
7251
|
-
typeof activeProgress?.artifactTableNamespace === 'string'
|
|
7252
|
-
? activeProgress.artifactTableNamespace
|
|
7253
|
-
: null;
|
|
7254
|
-
const activeCompleted =
|
|
7255
|
-
typeof activeProgress?.completed === 'number'
|
|
7256
|
-
? activeProgress.completed
|
|
7257
|
-
: null;
|
|
7258
|
-
const activeTotal =
|
|
7259
|
-
typeof activeProgress?.total === 'number' ? activeProgress.total : null;
|
|
7260
|
-
const activeMessage =
|
|
7261
|
-
typeof activeProgress?.message === 'string'
|
|
7262
|
-
? activeProgress.message
|
|
7263
|
-
: null;
|
|
7264
|
-
const response = await fetch(
|
|
7265
|
-
`${coordinatorUrl.replace(/\/$/, '')}/dedup/${encodeURIComponent(
|
|
7266
|
-
req.runId,
|
|
7267
|
-
)}/event-add`,
|
|
7268
|
-
{
|
|
7269
|
-
method: 'POST',
|
|
7270
|
-
headers: {
|
|
7271
|
-
'x-deepline-request-id': makeRequestId(),
|
|
7272
|
-
...coordinatorRequestHeaders({
|
|
7273
|
-
runId: req.runId,
|
|
7274
|
-
contentType: 'application/json',
|
|
7275
|
-
internalToken: req.coordinatorInternalToken,
|
|
7276
|
-
}),
|
|
7277
|
-
},
|
|
7278
|
-
body: JSON.stringify({
|
|
7279
|
-
runId: req.runId,
|
|
7280
|
-
type: 'progress',
|
|
7281
|
-
status: 'running',
|
|
7282
|
-
ts: occurredAt,
|
|
7283
|
-
logs: runLogBuffer,
|
|
7284
|
-
activeNodeId,
|
|
7285
|
-
activeArtifactTableNamespace,
|
|
7286
|
-
updatedAt: occurredAt,
|
|
7287
|
-
liveNodeProgress,
|
|
7288
|
-
}),
|
|
7289
|
-
},
|
|
7290
|
-
);
|
|
7291
|
-
if (!response.ok) {
|
|
7529
|
+
const coordinatorProgressPublisher = createCoordinatorProgressPublisher({
|
|
7530
|
+
runId: req.runId,
|
|
7531
|
+
coordinatorUrl: req.coordinatorUrl,
|
|
7532
|
+
coordinatorInternalToken: req.coordinatorInternalToken,
|
|
7533
|
+
abortController,
|
|
7534
|
+
getRunLogBuffer: () => runLogBuffer,
|
|
7535
|
+
getLiveNodeProgress: stepProgressSnapshot,
|
|
7536
|
+
makeRequestId,
|
|
7537
|
+
nowMs,
|
|
7538
|
+
heartbeatIntervalMs: MAP_EXECUTION_HEARTBEAT_INTERVAL_MS,
|
|
7539
|
+
recordTrace: (trace) =>
|
|
7292
7540
|
recordRunnerPerfTrace({
|
|
7293
7541
|
req,
|
|
7294
|
-
phase:
|
|
7295
|
-
ms:
|
|
7296
|
-
extra:
|
|
7297
|
-
|
|
7298
|
-
|
|
7299
|
-
activeNodeId,
|
|
7300
|
-
activeArtifactTableNamespace,
|
|
7301
|
-
activeCompleted,
|
|
7302
|
-
activeTotal,
|
|
7303
|
-
activeMessage,
|
|
7304
|
-
},
|
|
7305
|
-
});
|
|
7306
|
-
throw new Error(`coordinator progress event failed ${response.status}`);
|
|
7307
|
-
}
|
|
7308
|
-
recordRunnerPerfTrace({
|
|
7309
|
-
req,
|
|
7310
|
-
phase: 'runner.coordinator_progress_publish',
|
|
7311
|
-
ms: nowMs() - publishStartedAt,
|
|
7312
|
-
extra: {
|
|
7313
|
-
status: 'ok',
|
|
7314
|
-
activeNodeId,
|
|
7315
|
-
activeArtifactTableNamespace,
|
|
7316
|
-
activeCompleted,
|
|
7317
|
-
activeTotal,
|
|
7318
|
-
activeMessage,
|
|
7319
|
-
},
|
|
7320
|
-
});
|
|
7321
|
-
};
|
|
7322
|
-
|
|
7323
|
-
const flushCoordinatorProgressEvent = (force: boolean): Promise<void> => {
|
|
7324
|
-
const now = nowMs();
|
|
7325
|
-
if (
|
|
7326
|
-
!force &&
|
|
7327
|
-
now - lastCoordinatorProgressPublishAt <
|
|
7328
|
-
MAP_EXECUTION_HEARTBEAT_INTERVAL_MS
|
|
7329
|
-
) {
|
|
7330
|
-
return Promise.resolve();
|
|
7331
|
-
}
|
|
7332
|
-
if (!force && coordinatorProgressPublishQueueDepth > 0) {
|
|
7333
|
-
return Promise.resolve();
|
|
7334
|
-
}
|
|
7335
|
-
lastCoordinatorProgressPublishAt = now;
|
|
7336
|
-
coordinatorProgressPublishQueueDepth += 1;
|
|
7337
|
-
coordinatorProgressPublishInFlight = coordinatorProgressPublishInFlight
|
|
7338
|
-
.catch(() => undefined)
|
|
7339
|
-
.then(async () => {
|
|
7340
|
-
try {
|
|
7341
|
-
await publishCoordinatorProgressEvent(now);
|
|
7342
|
-
} finally {
|
|
7343
|
-
coordinatorProgressPublishQueueDepth = Math.max(
|
|
7344
|
-
0,
|
|
7345
|
-
coordinatorProgressPublishQueueDepth - 1,
|
|
7346
|
-
);
|
|
7347
|
-
}
|
|
7348
|
-
})
|
|
7349
|
-
.catch(() => undefined);
|
|
7350
|
-
return force ? coordinatorProgressPublishInFlight : Promise.resolve();
|
|
7351
|
-
};
|
|
7542
|
+
phase: trace.phase,
|
|
7543
|
+
ms: trace.ms,
|
|
7544
|
+
extra: trace.extra,
|
|
7545
|
+
}),
|
|
7546
|
+
});
|
|
7352
7547
|
|
|
7353
7548
|
const appendStepLifecycleEvent = (event: PlayStepLifecycleEvent) => {
|
|
7354
7549
|
updateStepProgress({
|
|
@@ -7537,7 +7732,7 @@ async function executeRunRequest(
|
|
|
7537
7732
|
updateStepProgress(input);
|
|
7538
7733
|
const force = Boolean(input.forceFlush);
|
|
7539
7734
|
const ledgerFlush = flushLedgerEvents(force);
|
|
7540
|
-
const progressFlush =
|
|
7735
|
+
const progressFlush = coordinatorProgressPublisher.flush(force);
|
|
7541
7736
|
return force
|
|
7542
7737
|
? Promise.all([ledgerFlush, progressFlush]).then(() => undefined)
|
|
7543
7738
|
: Promise.resolve();
|
|
@@ -7576,6 +7771,11 @@ async function executeRunRequest(
|
|
|
7576
7771
|
stepLifecycle?.markPreDatasetStepsStarted(startedAt);
|
|
7577
7772
|
flushLedgerEvents(false);
|
|
7578
7773
|
const runtimeDeadlineMs = nowMs() + STANDARD_PLAY_RUNTIME_LIMIT_MS;
|
|
7774
|
+
// Run-scoped buffer for provider results that billed but whose durable
|
|
7775
|
+
// receipt could not be marked completed even after the retry ladder. On
|
|
7776
|
+
// terminal failure the buffer is built into a size-capped salvage record and
|
|
7777
|
+
// attached to the run.failed ledger event so billed data is never memory-only.
|
|
7778
|
+
const receiptSalvage = createReceiptSalvageBuffer();
|
|
7579
7779
|
const ctx = createMinimalWorkerCtx(
|
|
7580
7780
|
req,
|
|
7581
7781
|
wrappedEmit,
|
|
@@ -7584,6 +7784,7 @@ async function executeRunRequest(
|
|
|
7584
7784
|
abortSignal,
|
|
7585
7785
|
workerCallbacks,
|
|
7586
7786
|
runtimeDeadlineMs,
|
|
7787
|
+
receiptSalvage,
|
|
7587
7788
|
);
|
|
7588
7789
|
// Hard wall-clock cap on active user-code runtime. CF Workflows does not
|
|
7589
7790
|
// impose a play-level execution ceiling on this substrate, so without this a
|
|
@@ -7778,8 +7979,16 @@ async function executeRunRequest(
|
|
|
7778
7979
|
stepLifecycle?.markStartedFailed(nowMs());
|
|
7779
7980
|
// A runtime-limit abort is a timeout failure, not a user cancellation, so
|
|
7780
7981
|
// it should be reported as run.failed with the limit message rather than
|
|
7781
|
-
// run.cancelled.
|
|
7782
|
-
|
|
7982
|
+
// run.cancelled. The deadline can also surface as a WorkflowAbortError
|
|
7983
|
+
// thrown synchronously from the completeReceipt retry ladder BEFORE the
|
|
7984
|
+
// runtimeDeadlineTimer macrotask flips the flag — match the limit message
|
|
7985
|
+
// directly so that race can never misclassify a timeout as a cancel (which
|
|
7986
|
+
// would silently drop the salvage buffer of billed-but-unpersisted rows).
|
|
7987
|
+
const runtimeLimitError =
|
|
7988
|
+
runtimeLimitExceeded ||
|
|
7989
|
+
(error instanceof Error &&
|
|
7990
|
+
error.message === STANDARD_PLAY_RUNTIME_LIMIT_MESSAGE);
|
|
7991
|
+
const aborted = isAbortLikeError(error) && !runtimeLimitError;
|
|
7783
7992
|
if (aborted) {
|
|
7784
7993
|
// Flip the controller so any concurrent user code observes the abort
|
|
7785
7994
|
// through ctx.signal. We mark the run cancelled instead of failed.
|
|
@@ -7788,7 +7997,15 @@ async function executeRunRequest(
|
|
|
7788
7997
|
);
|
|
7789
7998
|
}
|
|
7790
7999
|
const failure = normalizePlayRunFailure(error);
|
|
7791
|
-
|
|
8000
|
+
// Billed-but-unpersisted salvage rides on the run.failed event's `result`.
|
|
8001
|
+
// The run STILL FAILS — salvage is a loud marker, never a success fallback.
|
|
8002
|
+
const salvage: ReceiptSalvage | null = aborted
|
|
8003
|
+
? null
|
|
8004
|
+
: receiptSalvage.build();
|
|
8005
|
+
const message =
|
|
8006
|
+
salvage && salvage.totalEntries > 0
|
|
8007
|
+
? `${failure.message} ${receiptSalvageFailureSuffix(salvage)}`
|
|
8008
|
+
: failure.message;
|
|
7792
8009
|
const errorBilling = extractErrorBilling(error);
|
|
7793
8010
|
if (options?.persistResultDatasets) {
|
|
7794
8011
|
appendRunLogLine(
|
|
@@ -7817,6 +8034,7 @@ async function executeRunRequest(
|
|
|
7817
8034
|
...(failure.cause ? { cause: failure.cause } : {}),
|
|
7818
8035
|
},
|
|
7819
8036
|
],
|
|
8037
|
+
...(salvage && salvage.totalEntries > 0 ? { salvage } : {}),
|
|
7820
8038
|
},
|
|
7821
8039
|
});
|
|
7822
8040
|
recordRunnerPerfTrace({
|