deepline 0.3.128 → 0.3.129
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/sdk/src/client.ts +34 -9
- package/dist/bundling-sources/sdk/src/http.ts +22 -0
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-data-plane/r2.ts +22 -0
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +265 -2
- package/dist/bundling-sources/shared_libs/play-runtime/bounded-batch-function.ts +682 -0
- package/dist/bundling-sources/shared_libs/play-runtime/child-execution-strategy.ts +21 -5
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +5390 -1732
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +107 -1
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/commands/roll-execution-relay.ts +7 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-machines.ts +229 -7
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-registry.ts +25 -4
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-gateway-capacity.ts +71 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-gateway-drain.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-topology.ts +4 -1
- package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +694 -28
- package/dist/bundling-sources/shared_libs/play-runtime/egress-bandwidth-probe.ts +167 -0
- package/dist/bundling-sources/shared_libs/play-runtime/execute-server-timing.ts +103 -0
- package/dist/bundling-sources/shared_libs/play-runtime/execution-gateway-receipt-output-reference.ts +34 -0
- package/dist/bundling-sources/shared_libs/play-runtime/execution-gateway-response-size.ts +27 -0
- package/dist/bundling-sources/shared_libs/play-runtime/gateway-postgres-admission.ts +66 -1
- package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +425 -71
- package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +210 -32
- package/dist/bundling-sources/shared_libs/play-runtime/governor/in-memory-rate-state-backend.ts +27 -13
- package/dist/bundling-sources/shared_libs/play-runtime/governor/policy.ts +19 -6
- package/dist/bundling-sources/shared_libs/play-runtime/governor/rate-state-backend.ts +61 -2
- package/dist/bundling-sources/shared_libs/play-runtime/integration-operation-reader-fence/contracts.ts +33 -1
- package/dist/bundling-sources/shared_libs/play-runtime/integration-operation-reader-fence/job.ts +42 -1
- package/dist/bundling-sources/shared_libs/play-runtime/launch-source-policy.ts +11 -11
- package/dist/bundling-sources/shared_libs/play-runtime/ledger-safe-payload.ts +49 -16
- package/dist/bundling-sources/shared_libs/play-runtime/map-memory-limits.ts +1653 -98
- package/dist/bundling-sources/shared_libs/play-runtime/memory-ledger.ts +807 -0
- package/dist/bundling-sources/shared_libs/play-runtime/memory-pressure-signal.ts +138 -0
- package/dist/bundling-sources/shared_libs/play-runtime/output-size-limits.ts +85 -3
- package/dist/bundling-sources/shared_libs/play-runtime/persistence-latch.ts +60 -2
- package/dist/bundling-sources/shared_libs/play-runtime/play-run-recovery-policy.ts +46 -1
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/contracts.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/job.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/ports.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/postgres-adapter.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runtime-semantics.ts +70 -27
- package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +56 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/adaptive-provider-capacity-controller.ts +192 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/epoch-aimd-rate-controller.ts +194 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/fixture-capacity-simulator.ts +220 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/gradient2-concurrency-controller.ts +214 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/provider-capacity-policy.ts +43 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/provider-concurrency-policy.ts +30 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/provider-rate-policy.ts +95 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/provider-throttle-classifier.ts +40 -0
- package/dist/bundling-sources/shared_libs/play-runtime/resource-governor.ts +1535 -11
- package/dist/bundling-sources/shared_libs/play-runtime/run-failure.ts +49 -2
- package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +6 -1
- package/dist/bundling-sources/shared_libs/play-runtime/run-log-buffer.ts +55 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/egress-probe.ts +221 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +339 -29
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-deadline.ts +28 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-progress-push.ts +192 -165
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts +6 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-payload-transport.ts +90 -5
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-runtime-watchdog.ts +281 -6
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +27 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/local-process.ts +19 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/modal.ts +108 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/runtime-sandbox-reconciliation.ts +40 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/types.ts +11 -5
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-actions.ts +30 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api-compression.ts +276 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +723 -115
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-capacity-policy.ts +98 -2
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-contract.ts +3 -2
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-environment.ts +41 -4
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-flow.ts +448 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-map-row-feed.ts +215 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-operation-contract.ts +11 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-receipt-store-adapter.ts +161 -14
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-receipt-value.ts +184 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-receipt-writer.ts +607 -121
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-sandbox-placement-policy.ts +4 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-sheet-row-writer.ts +21 -0
- package/dist/bundling-sources/shared_libs/play-runtime/sandbox-runs/contract.ts +15 -2
- package/dist/bundling-sources/shared_libs/play-runtime/sandbox-runs/runner-backend-adapter.ts +71 -14
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-play-deploy-task.ts +2 -2
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +288 -119
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-shared.ts +8 -6
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-worker-required-env.ts +28 -3
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/canonical-integration-operation-store.ts +179 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/gateway-heavy-request-admission.ts +80 -2
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/gateway-progress-registry.ts +31 -2
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/integration-operation-store-contract.ts +104 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/integration-operation-value-reference.ts +47 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/integration-operation-value-retirement.ts +193 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/integration-operation-value-storage.ts +420 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-pool.ts +41 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-progress.ts +134 -3
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts +1657 -95
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +885 -73
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-completion-coalescer.ts +452 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-gateway.ts +601 -42
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-terminal-transition.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-receipt-value-storage.ts +371 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/worker-resource-metrics.ts +86 -7
- package/dist/bundling-sources/shared_libs/play-runtime/secret-redaction.ts +153 -22
- package/dist/bundling-sources/shared_libs/play-runtime/tool-call/index.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/tool-call/receipt-cohort.ts +188 -38
- package/dist/bundling-sources/shared_libs/play-runtime/tool-call/runtime-step-receipts-adapter.ts +28 -13
- package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +44 -1
- package/dist/bundling-sources/shared_libs/play-runtime/tool-result.ts +56 -4
- package/dist/bundling-sources/shared_libs/play-runtime/work-receipts.ts +2 -0
- package/dist/bundling-sources/shared_libs/plays/authoring-contract.ts +16 -10
- package/dist/bundling-sources/shared_libs/plays/dataset.ts +4 -1
- package/dist/cli/index.js +250 -38
- package/dist/cli/index.mjs +250 -38
- package/dist/{compiler-manifest-DfSnbCTW.d.mts → compiler-manifest-BIqRyj5m.d.mts} +1 -1
- package/dist/{compiler-manifest-DfSnbCTW.d.ts → compiler-manifest-BIqRyj5m.d.ts} +1 -1
- package/dist/index.d.mts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +231 -35
- package/dist/index.mjs +231 -35
- package/dist/install-integrity.json +31 -2
- package/dist/plays/bundle-play-file.d.mts +2 -2
- package/dist/plays/bundle-play-file.d.ts +2 -2
- package/dist/plays/bundle-play-file.mjs +65 -1
- package/dist/release.d.mts +1 -1
- package/dist/release.d.ts +1 -1
- package/dist/release.js +1 -1
- package/dist/release.mjs +1 -1
- package/package.json +1 -1
|
@@ -6,6 +6,15 @@ import {
|
|
|
6
6
|
} from './tool-result';
|
|
7
7
|
import { assertNoSecretTaint } from './secret-capability';
|
|
8
8
|
import type { RuntimeStepReceipt } from './ctx-types';
|
|
9
|
+
import {
|
|
10
|
+
percentile,
|
|
11
|
+
runtimeJsonByteLength,
|
|
12
|
+
type RuntimeMemoryLedgerHandle,
|
|
13
|
+
} from './memory-ledger';
|
|
14
|
+
import type {
|
|
15
|
+
RuntimeMemoryOccupancy,
|
|
16
|
+
RuntimeMemoryOccupancyReader,
|
|
17
|
+
} from './map-memory-limits';
|
|
9
18
|
import type { DurableReceiptRecoverySource } from './tool-execution-outcome';
|
|
10
19
|
import {
|
|
11
20
|
isPlayExecutionSuspendedError,
|
|
@@ -30,6 +39,619 @@ import type {
|
|
|
30
39
|
|
|
31
40
|
export const DURABLE_RECEIPT_WAIT_MAX_ATTEMPTS = 240;
|
|
32
41
|
export const DURABLE_RECEIPT_WAIT_DELAY_MS = 250;
|
|
42
|
+
// A running receipt owned elsewhere is re-read with exponential backoff up to
|
|
43
|
+
// this cap. Fixed 250 ms polls from every waiting group were most of the
|
|
44
|
+
// gateway read traffic in a 1k map. Because the delay grows, the attempt count
|
|
45
|
+
// alone no longer bounds wall time: every backoff loop must clamp itself to
|
|
46
|
+
// `runtimeReceiptWaitBudgetMs`.
|
|
47
|
+
export const DURABLE_RECEIPT_WAIT_MAX_DELAY_MS = 2_000;
|
|
48
|
+
// The hydration lane re-examines its queue on this cadence while turns are
|
|
49
|
+
// waiting, so headroom freed outside the lane reaches a waiter promptly. It is
|
|
50
|
+
// a probe interval, not a width: nothing about it bounds concurrency.
|
|
51
|
+
const RUNTIME_RECEIPT_HYDRATION_PROBE_MS = 25;
|
|
52
|
+
|
|
53
|
+
/** Backoff delay before re-reading a running receipt on the given attempt (1-based). */
|
|
54
|
+
export function runtimeReceiptWaitDelayMs(attempt: number): number {
|
|
55
|
+
const exponent = Math.max(0, Math.min(attempt, 8) - 1);
|
|
56
|
+
return Math.min(
|
|
57
|
+
DURABLE_RECEIPT_WAIT_MAX_DELAY_MS,
|
|
58
|
+
DURABLE_RECEIPT_WAIT_DELAY_MS * 2 ** exponent,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Whether a claim that came back `existing` is actually blocked by live work.
|
|
64
|
+
*
|
|
65
|
+
* A non-forced claim's status set is `('pending','failed')`, so a `running`
|
|
66
|
+
* row returns `existing` whether its owner is alive or long dead. Only an
|
|
67
|
+
* unexpired lease proves the former; anything else must be reclaimed and
|
|
68
|
+
* re-executed rather than waited on.
|
|
69
|
+
*/
|
|
70
|
+
export function runtimeReceiptClaimLeaseIsLive(
|
|
71
|
+
claim: Pick<RuntimeStepReceipt, 'leaseExpiresAt'>,
|
|
72
|
+
nowMs: number = Date.now(),
|
|
73
|
+
): boolean {
|
|
74
|
+
const expiresAtMs = Date.parse(claim.leaseExpiresAt ?? '');
|
|
75
|
+
return !Number.isFinite(expiresAtMs) || expiresAtMs > nowMs;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Wall-clock budget that a fixed-delay attempt count used to express. */
|
|
79
|
+
export function runtimeReceiptWaitBudgetMs(
|
|
80
|
+
maxAttempts: number,
|
|
81
|
+
delayMs: number = DURABLE_RECEIPT_WAIT_DELAY_MS,
|
|
82
|
+
): number {
|
|
83
|
+
return Math.max(delayMs, maxAttempts * delayMs);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Settled per-key hydration samples the lane keeps to price a turn. Bounded,
|
|
88
|
+
* so past this many the reservoir below chooses which to keep.
|
|
89
|
+
*/
|
|
90
|
+
export const RUNTIME_RECEIPT_HYDRATION_SAMPLE_RESERVOIR = 256;
|
|
91
|
+
/**
|
|
92
|
+
* Settled turns the lane needs before its own measured p90 per-key cost
|
|
93
|
+
* replaces the ledger's per-owner price. Sixteen matches the settled-row count
|
|
94
|
+
* map admission waits for before it trusts a p90, and for the same reason: it
|
|
95
|
+
* is enough for a percentile to mean something without making the first wave
|
|
96
|
+
* of a map wait for a tenth of itself.
|
|
97
|
+
*/
|
|
98
|
+
export const RUNTIME_RECEIPT_HYDRATION_SAMPLE_TURNS = 16;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Bounded semantic origins for completed-receipt reads. These are runtime
|
|
102
|
+
* paths, never receipt keys, tool ids, or caller inputs, so the telemetry that
|
|
103
|
+
* explains a large read cannot become another retention surface.
|
|
104
|
+
*/
|
|
105
|
+
export const RUNTIME_RECEIPT_HYDRATION_SOURCES = [
|
|
106
|
+
'durable_operation',
|
|
107
|
+
'direct_tool_legacy_cache',
|
|
108
|
+
'direct_tool_result_receipt',
|
|
109
|
+
'map_cohort_read',
|
|
110
|
+
'map_legacy_claim',
|
|
111
|
+
'map_receipt_wait',
|
|
112
|
+
'receipt_completion_read_back',
|
|
113
|
+
'unknown',
|
|
114
|
+
] as const;
|
|
115
|
+
|
|
116
|
+
export type RuntimeReceiptHydrationSource =
|
|
117
|
+
(typeof RUNTIME_RECEIPT_HYDRATION_SOURCES)[number];
|
|
118
|
+
|
|
119
|
+
export type RuntimeReceiptHydrationSourceSnapshot = {
|
|
120
|
+
turns: number;
|
|
121
|
+
receiptKeys: number;
|
|
122
|
+
bytesAdmitted: number;
|
|
123
|
+
bytesMeasured: number;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export type RuntimeReceiptHydrationGateSnapshot = {
|
|
127
|
+
/** Turns in flight, and turns queued behind the byte budget. */
|
|
128
|
+
active: number;
|
|
129
|
+
waiting: number;
|
|
130
|
+
/** Turns taken, and how long callers queued for them. */
|
|
131
|
+
turns: number;
|
|
132
|
+
queuedTurns: number;
|
|
133
|
+
totalWaitMs: number;
|
|
134
|
+
maxWaitMs: number;
|
|
135
|
+
/** Ledger bytes reserved by the turns in flight right now. */
|
|
136
|
+
bytesHeld: number;
|
|
137
|
+
bytesHeldPeak: number;
|
|
138
|
+
/** Ledger bytes this lane has admitted over its life. */
|
|
139
|
+
bytesAdmitted: number;
|
|
140
|
+
/** Bytes turns reported actually holding, replacing their reservation. */
|
|
141
|
+
bytesMeasured: number;
|
|
142
|
+
/** Admission attempts the byte budget refused. */
|
|
143
|
+
holds: number;
|
|
144
|
+
/** Collections taken to answer a hold before it was declared. */
|
|
145
|
+
holdCollections: number;
|
|
146
|
+
/**
|
|
147
|
+
* Turns admitted because nothing else was in flight. A turn larger than the
|
|
148
|
+
* whole budget still runs: one turn may always run, or a map whose payloads
|
|
149
|
+
* exceed the target would never finish a single row.
|
|
150
|
+
*/
|
|
151
|
+
progressAdmissions: number;
|
|
152
|
+
/** Turns admitted with no readable heap, where byte admission cannot apply. */
|
|
153
|
+
unpricedAdmissions: number;
|
|
154
|
+
/** Ledger bytes the last decision priced one receipt key at. */
|
|
155
|
+
perKeyBytes: number | null;
|
|
156
|
+
/** Settled per-key samples behind that price; zero means it is the prior. */
|
|
157
|
+
perKeySamples: number;
|
|
158
|
+
/** Live bytes under the heap target at the last decision. */
|
|
159
|
+
headroomBytes: number | null;
|
|
160
|
+
/** Bounded provenance for the byte totals above. */
|
|
161
|
+
bySource: Record<
|
|
162
|
+
RuntimeReceiptHydrationSource,
|
|
163
|
+
RuntimeReceiptHydrationSourceSnapshot
|
|
164
|
+
>;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
export type RuntimeReceiptHydrationTurn = {
|
|
168
|
+
/**
|
|
169
|
+
* Replace this turn's reservation with the bytes the read actually holds.
|
|
170
|
+
* Called once the response is in hand and before it is expanded, so the
|
|
171
|
+
* ledger stops carrying an estimate the moment a measurement exists.
|
|
172
|
+
*/
|
|
173
|
+
settleBytes(bytes: number): void;
|
|
174
|
+
/** Drop the reservation. Idempotent. */
|
|
175
|
+
release(): void;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
export type RuntimeReceiptHydrationGate = {
|
|
179
|
+
/**
|
|
180
|
+
* Take a turn on the hydration lane. `receiptKeys` is how many receipt keys
|
|
181
|
+
* the turn will read: the turn's expected bytes are that count times the
|
|
182
|
+
* price of one key. A manifest-aware reader may instead supply its exact
|
|
183
|
+
* serialized `expectedBytes` before redeeming the body.
|
|
184
|
+
*/
|
|
185
|
+
acquire(input?: {
|
|
186
|
+
receiptKeys?: number;
|
|
187
|
+
expectedBytes?: number;
|
|
188
|
+
source?: RuntimeReceiptHydrationSource;
|
|
189
|
+
/**
|
|
190
|
+
* Wall-clock bound on the queue wait. A turn that cannot be admitted
|
|
191
|
+
* within it rejects with `RuntimeReceiptHydrationTurnTimeoutError` rather
|
|
192
|
+
* than parking forever behind a holder that never released.
|
|
193
|
+
*/
|
|
194
|
+
timeoutMs?: number;
|
|
195
|
+
/** Abort the queue wait; the signal's reason is what rejects. */
|
|
196
|
+
signal?: AbortSignal;
|
|
197
|
+
}): Promise<RuntimeReceiptHydrationTurn>;
|
|
198
|
+
snapshot(): RuntimeReceiptHydrationGateSnapshot;
|
|
199
|
+
/**
|
|
200
|
+
* Release the admission timer and reject every still-queued turn. A closed
|
|
201
|
+
* lane will never admit again, so leaving those promises pending is the
|
|
202
|
+
* unbounded wait this contract forbids. Idempotent.
|
|
203
|
+
*/
|
|
204
|
+
close(): void;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* The completed-receipt hydration lane, admitted by bytes.
|
|
209
|
+
*
|
|
210
|
+
* A hydration turn holds the receipt gateway's response — the serialized
|
|
211
|
+
* outputs of every key it read — from the moment the response is parsed until
|
|
212
|
+
* the turn returns, plus the JSON expansion of those outputs while the turn
|
|
213
|
+
* hands them to their callers. Nothing else in the runtime accounts for those
|
|
214
|
+
* bytes, so this lane registers them on the byte ledger itself: an expected
|
|
215
|
+
* size on admission, the measured size once the response is read, and nothing
|
|
216
|
+
* at all once the turn is over.
|
|
217
|
+
*
|
|
218
|
+
* It used to be a counting semaphore of 8. That bound had no relation to
|
|
219
|
+
* bytes: eight turns of a megabyte-per-key shape hold two orders of magnitude
|
|
220
|
+
* more memory than eight turns of a fresh map's empty claims, and the constant
|
|
221
|
+
* could not tell them apart. Worse, the slot is taken at the dispatch group's
|
|
222
|
+
* claim and held across the gateway round trip (jacob p50 2.2 s, p90 6.3 s)
|
|
223
|
+
* before a single byte is expanded, so on a 1,000-row map 58% of turns queued
|
|
224
|
+
* 3.8 s each for a lane that was not protecting anything measurable.
|
|
225
|
+
*
|
|
226
|
+
* Policy, in order:
|
|
227
|
+
* 1. Nothing in flight admits immediately. One turn may always run, so a
|
|
228
|
+
* turn whose payload exceeds the whole budget still makes progress
|
|
229
|
+
* instead of deadlocking the run. This is the only floor in the lane and
|
|
230
|
+
* it is exactly one turn.
|
|
231
|
+
* 2. A turn's expected bytes are `receiptKeys x perKeyBytes`. `perKeyBytes`
|
|
232
|
+
* is the lane's own measured p90 of settled per-key sizes once it has
|
|
233
|
+
* enough samples, and until then the ledger's per-owner price — the same
|
|
234
|
+
* p90-or-prior map row admission uses.
|
|
235
|
+
* 3. Admission compares those expected live bytes — the reservation times
|
|
236
|
+
* the `hydrationReadBack` factor, the category it registers on — against
|
|
237
|
+
* the headroom `runtimeMemoryOccupancy` reports: heap target minus
|
|
238
|
+
* (baseline + the ledger's per-category prediction). Because the
|
|
239
|
+
* reservation is registered on the same ledger, an admitted turn's bytes
|
|
240
|
+
* are already inside the next turn's occupancy; the lane does not keep a
|
|
241
|
+
* second set of books.
|
|
242
|
+
* 4. Over budget is a question about garbage before it is a verdict about
|
|
243
|
+
* turns: the lane collects (rate limited, through the shared reader) and
|
|
244
|
+
* re-reads before it holds.
|
|
245
|
+
* 5. Waiters are FIFO. The head of the queue is the only candidate, so a
|
|
246
|
+
* small turn cannot overtake a large one that has been waiting.
|
|
247
|
+
*/
|
|
248
|
+
export function createRuntimeReceiptHydrationGate(input: {
|
|
249
|
+
/** Byte occupancy of this process, shared with map row admission. */
|
|
250
|
+
occupancy: RuntimeMemoryOccupancyReader;
|
|
251
|
+
/** Registers this lane's reservations on the runtime byte ledger. */
|
|
252
|
+
reserve: (bytes: number) => RuntimeMemoryLedgerHandle;
|
|
253
|
+
/**
|
|
254
|
+
* Re-examines the queue while turns wait, so headroom freed by work outside
|
|
255
|
+
* this lane (a row settling, a body released) reaches a waiter without
|
|
256
|
+
* waiting for another turn to finish. Runs only while someone is waiting.
|
|
257
|
+
*/
|
|
258
|
+
probeIntervalMs?: number;
|
|
259
|
+
sampleTurns?: number;
|
|
260
|
+
sampleReservoirSize?: number;
|
|
261
|
+
/** Uniform [0, 1) source for the sample reservoir; tests inject a sequence. */
|
|
262
|
+
random?: () => number;
|
|
263
|
+
now?: () => number;
|
|
264
|
+
}): RuntimeReceiptHydrationGate {
|
|
265
|
+
const occupancyReader = input.occupancy;
|
|
266
|
+
const now = input.now ?? Date.now;
|
|
267
|
+
const probeIntervalMs = Math.max(
|
|
268
|
+
1,
|
|
269
|
+
Math.floor(input.probeIntervalMs ?? RUNTIME_RECEIPT_HYDRATION_PROBE_MS),
|
|
270
|
+
);
|
|
271
|
+
const sampleTurns = Math.max(
|
|
272
|
+
1,
|
|
273
|
+
Math.floor(input.sampleTurns ?? RUNTIME_RECEIPT_HYDRATION_SAMPLE_TURNS),
|
|
274
|
+
);
|
|
275
|
+
const sampleReservoirSize = Math.max(
|
|
276
|
+
1,
|
|
277
|
+
Math.floor(
|
|
278
|
+
input.sampleReservoirSize ?? RUNTIME_RECEIPT_HYDRATION_SAMPLE_RESERVOIR,
|
|
279
|
+
),
|
|
280
|
+
);
|
|
281
|
+
const uniform = (): number => {
|
|
282
|
+
const value = input.random ? input.random() : Math.random();
|
|
283
|
+
return Number.isFinite(value)
|
|
284
|
+
? Math.min(0.999999999, Math.max(0, value))
|
|
285
|
+
: 0;
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
let active = 0;
|
|
289
|
+
let turns = 0;
|
|
290
|
+
let queuedTurns = 0;
|
|
291
|
+
let totalWaitMs = 0;
|
|
292
|
+
let maxWaitMs = 0;
|
|
293
|
+
let bytesHeld = 0;
|
|
294
|
+
let bytesHeldPeak = 0;
|
|
295
|
+
let bytesAdmitted = 0;
|
|
296
|
+
let bytesMeasured = 0;
|
|
297
|
+
let holds = 0;
|
|
298
|
+
let holdCollections = 0;
|
|
299
|
+
let progressAdmissions = 0;
|
|
300
|
+
let unpricedAdmissions = 0;
|
|
301
|
+
let perKeyBytes: number | null = null;
|
|
302
|
+
let headroomBytes: number | null = null;
|
|
303
|
+
let closed = false;
|
|
304
|
+
let sampledTurns = 0;
|
|
305
|
+
const perKeySamples: number[] = [];
|
|
306
|
+
const bySource = Object.fromEntries(
|
|
307
|
+
RUNTIME_RECEIPT_HYDRATION_SOURCES.map((source) => [
|
|
308
|
+
source,
|
|
309
|
+
{ turns: 0, receiptKeys: 0, bytesAdmitted: 0, bytesMeasured: 0 },
|
|
310
|
+
]),
|
|
311
|
+
) as Record<
|
|
312
|
+
RuntimeReceiptHydrationSource,
|
|
313
|
+
RuntimeReceiptHydrationSourceSnapshot
|
|
314
|
+
>;
|
|
315
|
+
type HydrationWaiter = {
|
|
316
|
+
receiptKeys: number;
|
|
317
|
+
expectedBytes: number | null;
|
|
318
|
+
source: RuntimeReceiptHydrationSource;
|
|
319
|
+
queuedAtMs: number;
|
|
320
|
+
/** Set when the waiter is admitted, so `acquire` can tell a queue apart
|
|
321
|
+
* from an immediate grant without scanning the queue for itself. */
|
|
322
|
+
admitted: boolean;
|
|
323
|
+
resolve: (turn: RuntimeReceiptHydrationTurn) => void;
|
|
324
|
+
/** Settles a waiter the lane will never admit. Cleared once admitted. */
|
|
325
|
+
reject: (reason: unknown) => void;
|
|
326
|
+
abandon: (() => void) | null;
|
|
327
|
+
};
|
|
328
|
+
const waiters: HydrationWaiter[] = [];
|
|
329
|
+
let timer: ReturnType<typeof setInterval> | null = null;
|
|
330
|
+
|
|
331
|
+
const recordPerKeySample = (bytesPerKey: number): void => {
|
|
332
|
+
sampledTurns += 1;
|
|
333
|
+
if (perKeySamples.length < sampleReservoirSize) {
|
|
334
|
+
perKeySamples.push(bytesPerKey);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
// Vitter's Algorithm R, for the same reason the ledger's settled-owner
|
|
338
|
+
// reservoir uses it: a cache-hit wave and a fresh-claim wave settle at
|
|
339
|
+
// very different sizes, and keeping the most recent samples would price
|
|
340
|
+
// the lane from whichever wave happened to be last.
|
|
341
|
+
const slot = Math.floor(uniform() * sampledTurns);
|
|
342
|
+
if (slot < sampleReservoirSize) perKeySamples[slot] = bytesPerKey;
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
/** Ledger bytes one receipt key is expected to cost. */
|
|
346
|
+
const priceKey = (occupancy: RuntimeMemoryOccupancy): number => {
|
|
347
|
+
const measured =
|
|
348
|
+
perKeySamples.length >= sampleTurns
|
|
349
|
+
? percentile(perKeySamples, 0.9)
|
|
350
|
+
: null;
|
|
351
|
+
// The lane's own measurement is what a receipt read costs. Before it has
|
|
352
|
+
// one, a receipt key is priced as a row: the ledger's settled p90 per
|
|
353
|
+
// owner, or the unmeasured prior behind it. That is an overestimate for a
|
|
354
|
+
// claim that returns no output, and deliberately so — the alternative is
|
|
355
|
+
// admitting an unknown payload against a budget it may not fit.
|
|
356
|
+
return measured === null ? occupancy.ownerReserveLedgerBytes : measured;
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
/** Prefer a manifest's exact serialized body size over the conservative prior. */
|
|
360
|
+
const expectedHydrationBytes = (
|
|
361
|
+
waiter: HydrationWaiter,
|
|
362
|
+
occupancy: RuntimeMemoryOccupancy,
|
|
363
|
+
): number =>
|
|
364
|
+
waiter.expectedBytes === null
|
|
365
|
+
? Math.max(1, Math.ceil(waiter.receiptKeys * priceKey(occupancy)))
|
|
366
|
+
: Math.max(1, waiter.expectedBytes);
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Live bytes per ledger byte for what this lane actually registers.
|
|
370
|
+
*
|
|
371
|
+
* The lane's reservations land on `hydrationReadBack`, so that is the
|
|
372
|
+
* category whose factor converts them. The process-wide mix-weighted factor
|
|
373
|
+
* is a different question's answer: a run whose ledger is mostly decoded
|
|
374
|
+
* provider bodies reports ~3x, and pricing a receipt read at 3x its wire
|
|
375
|
+
* size holds turns against headroom the read was never going to occupy.
|
|
376
|
+
*/
|
|
377
|
+
const hydrationFactor = (occupancy: RuntimeMemoryOccupancy): number =>
|
|
378
|
+
occupancy.categoryFactors.hydrationReadBack;
|
|
379
|
+
|
|
380
|
+
const stopTimer = (): void => {
|
|
381
|
+
if (timer === null) return;
|
|
382
|
+
clearInterval(timer);
|
|
383
|
+
timer = null;
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
const startTimer = (): void => {
|
|
387
|
+
if (timer !== null || closed || waiters.length === 0) return;
|
|
388
|
+
timer = setInterval(() => {
|
|
389
|
+
admitWaiters();
|
|
390
|
+
}, probeIntervalMs);
|
|
391
|
+
if (typeof timer === 'object' && 'unref' in timer) timer.unref();
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
/** Drop a waiter that will never be admitted and settle it loudly. */
|
|
395
|
+
const abandonWaiter = (waiter: HydrationWaiter, reason: unknown): void => {
|
|
396
|
+
const index = waiters.indexOf(waiter);
|
|
397
|
+
if (index >= 0) waiters.splice(index, 1);
|
|
398
|
+
waiter.abandon?.();
|
|
399
|
+
waiter.abandon = null;
|
|
400
|
+
waiter.reject(reason);
|
|
401
|
+
if (waiters.length === 0) stopTimer();
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
const admit = (waiter: HydrationWaiter, reservedBytes: number): void => {
|
|
405
|
+
waiters.shift();
|
|
406
|
+
waiter.admitted = true;
|
|
407
|
+
waiter.abandon?.();
|
|
408
|
+
waiter.abandon = null;
|
|
409
|
+
active += 1;
|
|
410
|
+
turns += 1;
|
|
411
|
+
const waitedMs = Math.max(0, now() - waiter.queuedAtMs);
|
|
412
|
+
totalWaitMs += waitedMs;
|
|
413
|
+
maxWaitMs = Math.max(maxWaitMs, waitedMs);
|
|
414
|
+
const handle = input.reserve(reservedBytes);
|
|
415
|
+
bytesAdmitted += reservedBytes;
|
|
416
|
+
const source = bySource[waiter.source];
|
|
417
|
+
source.turns += 1;
|
|
418
|
+
source.receiptKeys += waiter.receiptKeys;
|
|
419
|
+
source.bytesAdmitted += reservedBytes;
|
|
420
|
+
bytesHeld += reservedBytes;
|
|
421
|
+
bytesHeldPeak = Math.max(bytesHeldPeak, bytesHeld);
|
|
422
|
+
let entryBytes = reservedBytes;
|
|
423
|
+
let settled = false;
|
|
424
|
+
let released = false;
|
|
425
|
+
waiter.resolve({
|
|
426
|
+
settleBytes(bytes) {
|
|
427
|
+
if (released || settled) return;
|
|
428
|
+
if (!Number.isFinite(bytes) || bytes < 0) {
|
|
429
|
+
throw new RangeError(
|
|
430
|
+
`Receipt hydration turn reported ${String(bytes)} bytes; a turn's measured size must be a finite, non-negative byte count.`,
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
settled = true;
|
|
434
|
+
const measuredBytes = Math.ceil(bytes);
|
|
435
|
+
handle.update(measuredBytes);
|
|
436
|
+
bytesHeld = Math.max(0, bytesHeld - entryBytes + measuredBytes);
|
|
437
|
+
bytesHeldPeak = Math.max(bytesHeldPeak, bytesHeld);
|
|
438
|
+
bytesMeasured += measuredBytes;
|
|
439
|
+
source.bytesMeasured += measuredBytes;
|
|
440
|
+
entryBytes = measuredBytes;
|
|
441
|
+
recordPerKeySample(
|
|
442
|
+
Math.ceil(measuredBytes / Math.max(1, waiter.receiptKeys)),
|
|
443
|
+
);
|
|
444
|
+
// A turn that turned out smaller than its reservation just freed
|
|
445
|
+
// headroom; hand it to whoever is waiting rather than to the clock.
|
|
446
|
+
admitWaiters();
|
|
447
|
+
},
|
|
448
|
+
release() {
|
|
449
|
+
if (released) return;
|
|
450
|
+
released = true;
|
|
451
|
+
handle.release();
|
|
452
|
+
bytesHeld = Math.max(0, bytesHeld - entryBytes);
|
|
453
|
+
entryBytes = 0;
|
|
454
|
+
active = Math.max(0, active - 1);
|
|
455
|
+
admitWaiters();
|
|
456
|
+
},
|
|
457
|
+
});
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
function admitWaiters(): void {
|
|
461
|
+
if (closed) return;
|
|
462
|
+
while (waiters.length > 0) {
|
|
463
|
+
const waiter = waiters[0];
|
|
464
|
+
if (!waiter) break;
|
|
465
|
+
// One turn may always run. Without this a group whose expected bytes
|
|
466
|
+
// exceed the whole heap target could never start, and the run would
|
|
467
|
+
// stall on a lane that is protecting an empty process.
|
|
468
|
+
if (active === 0) {
|
|
469
|
+
progressAdmissions += 1;
|
|
470
|
+
const occupancy = occupancyReader.read();
|
|
471
|
+
admit(
|
|
472
|
+
waiter,
|
|
473
|
+
occupancy === null ? 0 : expectedHydrationBytes(waiter, occupancy),
|
|
474
|
+
);
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
let occupancy = occupancyReader.read();
|
|
478
|
+
if (occupancy === null) {
|
|
479
|
+
// No readable heap: this runtime cannot state a budget, so the lane
|
|
480
|
+
// does not invent one. The structural receipt limits still apply.
|
|
481
|
+
unpricedAdmissions += 1;
|
|
482
|
+
admit(waiter, 0);
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
let reservedBytes = expectedHydrationBytes(waiter, occupancy);
|
|
486
|
+
perKeyBytes = Math.ceil(reservedBytes / Math.max(1, waiter.receiptKeys));
|
|
487
|
+
headroomBytes = occupancy.headroomBytes;
|
|
488
|
+
if (
|
|
489
|
+
reservedBytes * hydrationFactor(occupancy) <=
|
|
490
|
+
occupancy.headroomBytes
|
|
491
|
+
) {
|
|
492
|
+
admit(waiter, reservedBytes);
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
// Over budget is a question about garbage first. Collect and re-read
|
|
496
|
+
// before concluding that the turns in flight are what is full.
|
|
497
|
+
const collected = occupancyReader.collectAndRead();
|
|
498
|
+
if (collected) {
|
|
499
|
+
holdCollections += 1;
|
|
500
|
+
occupancy = collected;
|
|
501
|
+
reservedBytes = expectedHydrationBytes(waiter, occupancy);
|
|
502
|
+
perKeyBytes = Math.ceil(
|
|
503
|
+
reservedBytes / Math.max(1, waiter.receiptKeys),
|
|
504
|
+
);
|
|
505
|
+
headroomBytes = occupancy.headroomBytes;
|
|
506
|
+
if (
|
|
507
|
+
reservedBytes * hydrationFactor(occupancy) <=
|
|
508
|
+
occupancy.headroomBytes
|
|
509
|
+
) {
|
|
510
|
+
admit(waiter, reservedBytes);
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
holds += 1;
|
|
515
|
+
break;
|
|
516
|
+
}
|
|
517
|
+
if (waiters.length === 0) stopTimer();
|
|
518
|
+
else startTimer();
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return {
|
|
522
|
+
acquire(request) {
|
|
523
|
+
const requestedKeys = request?.receiptKeys;
|
|
524
|
+
const receiptKeys =
|
|
525
|
+
typeof requestedKeys === 'number' && Number.isFinite(requestedKeys)
|
|
526
|
+
? Math.max(1, Math.floor(requestedKeys))
|
|
527
|
+
: 1;
|
|
528
|
+
const source =
|
|
529
|
+
request?.source &&
|
|
530
|
+
RUNTIME_RECEIPT_HYDRATION_SOURCES.includes(request.source)
|
|
531
|
+
? request.source
|
|
532
|
+
: 'unknown';
|
|
533
|
+
const expectedBytes =
|
|
534
|
+
typeof request?.expectedBytes === 'number' &&
|
|
535
|
+
Number.isFinite(request.expectedBytes) &&
|
|
536
|
+
request.expectedBytes >= 0
|
|
537
|
+
? Math.ceil(request.expectedBytes)
|
|
538
|
+
: null;
|
|
539
|
+
const timeoutMs =
|
|
540
|
+
typeof request?.timeoutMs === 'number' &&
|
|
541
|
+
Number.isFinite(request.timeoutMs) &&
|
|
542
|
+
request.timeoutMs >= 0
|
|
543
|
+
? Math.floor(request.timeoutMs)
|
|
544
|
+
: null;
|
|
545
|
+
const signal = request?.signal;
|
|
546
|
+
if (closed) {
|
|
547
|
+
return Promise.reject(new RuntimeReceiptHydrationGateClosedError());
|
|
548
|
+
}
|
|
549
|
+
if (signal?.aborted) {
|
|
550
|
+
return Promise.reject(hydrationAbortReason(signal));
|
|
551
|
+
}
|
|
552
|
+
return new Promise<RuntimeReceiptHydrationTurn>((resolve, reject) => {
|
|
553
|
+
const queuedAtMs = now();
|
|
554
|
+
const waiter: HydrationWaiter = {
|
|
555
|
+
receiptKeys,
|
|
556
|
+
expectedBytes,
|
|
557
|
+
source,
|
|
558
|
+
queuedAtMs,
|
|
559
|
+
admitted: false,
|
|
560
|
+
resolve,
|
|
561
|
+
reject,
|
|
562
|
+
abandon: null,
|
|
563
|
+
};
|
|
564
|
+
waiters.push(waiter);
|
|
565
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
566
|
+
const onAbort = () =>
|
|
567
|
+
abandonWaiter(waiter, hydrationAbortReason(signal!));
|
|
568
|
+
if (timeoutMs !== null || signal) {
|
|
569
|
+
if (timeoutMs !== null) {
|
|
570
|
+
timer = setTimeout(() => {
|
|
571
|
+
timer = null;
|
|
572
|
+
abandonWaiter(
|
|
573
|
+
waiter,
|
|
574
|
+
new RuntimeReceiptHydrationTurnTimeoutError(
|
|
575
|
+
Math.max(0, now() - queuedAtMs),
|
|
576
|
+
),
|
|
577
|
+
);
|
|
578
|
+
}, timeoutMs);
|
|
579
|
+
if (typeof timer === 'object' && 'unref' in timer) timer.unref();
|
|
580
|
+
}
|
|
581
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
582
|
+
waiter.abandon = () => {
|
|
583
|
+
if (timer !== null) clearTimeout(timer);
|
|
584
|
+
timer = null;
|
|
585
|
+
signal?.removeEventListener('abort', onAbort);
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
admitWaiters();
|
|
589
|
+
// Counted only when the byte budget actually made this turn wait, so
|
|
590
|
+
// `queuedTurns / turns` stays the share of turns the lane delayed.
|
|
591
|
+
if (!waiter.admitted) queuedTurns += 1;
|
|
592
|
+
});
|
|
593
|
+
},
|
|
594
|
+
snapshot: () => ({
|
|
595
|
+
active,
|
|
596
|
+
waiting: waiters.length,
|
|
597
|
+
turns,
|
|
598
|
+
queuedTurns,
|
|
599
|
+
totalWaitMs,
|
|
600
|
+
maxWaitMs,
|
|
601
|
+
bytesHeld,
|
|
602
|
+
bytesHeldPeak,
|
|
603
|
+
bytesAdmitted,
|
|
604
|
+
bytesMeasured,
|
|
605
|
+
holds,
|
|
606
|
+
holdCollections,
|
|
607
|
+
progressAdmissions,
|
|
608
|
+
unpricedAdmissions,
|
|
609
|
+
perKeyBytes,
|
|
610
|
+
perKeySamples: perKeySamples.length,
|
|
611
|
+
headroomBytes,
|
|
612
|
+
bySource: Object.fromEntries(
|
|
613
|
+
RUNTIME_RECEIPT_HYDRATION_SOURCES.map((source) => [
|
|
614
|
+
source,
|
|
615
|
+
{ ...bySource[source] },
|
|
616
|
+
]),
|
|
617
|
+
) as Record<
|
|
618
|
+
RuntimeReceiptHydrationSource,
|
|
619
|
+
RuntimeReceiptHydrationSourceSnapshot
|
|
620
|
+
>,
|
|
621
|
+
}),
|
|
622
|
+
close() {
|
|
623
|
+
closed = true;
|
|
624
|
+
stopTimer();
|
|
625
|
+
// A closed lane will never admit again. Stranding queued turns on an
|
|
626
|
+
// unsettleable promise is the wait this contract forbids.
|
|
627
|
+
const stranded = waiters.splice(0, waiters.length);
|
|
628
|
+
for (const waiter of stranded) {
|
|
629
|
+
waiter.abandon?.();
|
|
630
|
+
waiter.abandon = null;
|
|
631
|
+
waiter.reject(new RuntimeReceiptHydrationGateClosedError());
|
|
632
|
+
}
|
|
633
|
+
},
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Serialized size of the receipt outputs a hydration turn is about to expand
|
|
639
|
+
* and hold. Only stored outputs are measured: a claim that returned no output
|
|
640
|
+
* costs the lane nothing but its own row, and serializing the receipt envelope
|
|
641
|
+
* would charge the lane for bytes it does not retain.
|
|
642
|
+
*/
|
|
643
|
+
export function runtimeStepReceiptPayloadBytes(
|
|
644
|
+
receipts: Iterable<RuntimeStepReceipt | null | undefined>,
|
|
645
|
+
): number {
|
|
646
|
+
let bytes = 0;
|
|
647
|
+
for (const receipt of receipts) {
|
|
648
|
+
const output = receipt?.output;
|
|
649
|
+
if (output === undefined || output === null) continue;
|
|
650
|
+
bytes += runtimeJsonByteLength(output);
|
|
651
|
+
}
|
|
652
|
+
return bytes;
|
|
653
|
+
}
|
|
654
|
+
|
|
33
655
|
const TOOL_RECEIPT_DEFAULT_WAIT_MS = 300_000;
|
|
34
656
|
const TOOL_RECEIPT_COMPLETION_BUFFER_MS = 30_000;
|
|
35
657
|
const TOOL_RECEIPT_MAX_WAIT_MS = 30 * 60_000;
|
|
@@ -37,6 +659,32 @@ const DANGEROUS_SIDE_EFFECT_LOCK_WAIT_MS = 15_000;
|
|
|
37
659
|
export const COMPLETED_RECEIPT_CACHE_INSERT_LEASE_ID =
|
|
38
660
|
'__deepline_completed_cache_insert__';
|
|
39
661
|
|
|
662
|
+
/** The error an aborted queue wait settles with. */
|
|
663
|
+
function hydrationAbortReason(signal: AbortSignal): unknown {
|
|
664
|
+
if (signal.reason instanceof Error) return signal.reason;
|
|
665
|
+
return new Error(
|
|
666
|
+
typeof signal.reason === 'string' && signal.reason.trim()
|
|
667
|
+
? signal.reason
|
|
668
|
+
: 'Receipt hydration turn aborted.',
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
export class RuntimeReceiptHydrationTurnTimeoutError extends Error {
|
|
673
|
+
constructor(readonly waitedMs: number) {
|
|
674
|
+
super(
|
|
675
|
+
`Timed out after ${waitedMs}ms waiting for a receipt hydration turn.`,
|
|
676
|
+
);
|
|
677
|
+
this.name = 'RuntimeReceiptHydrationTurnTimeoutError';
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
export class RuntimeReceiptHydrationGateClosedError extends Error {
|
|
682
|
+
constructor() {
|
|
683
|
+
super('Receipt hydration lane closed while a turn was still queued.');
|
|
684
|
+
this.name = 'RuntimeReceiptHydrationGateClosedError';
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
40
688
|
export class RuntimeReceiptWaitTimeoutError extends Error {
|
|
41
689
|
constructor(key: string) {
|
|
42
690
|
super(`Timed out waiting for durable receipt ${key}.`);
|
|
@@ -520,6 +1168,7 @@ export async function executeWithDurableRuntimeReceipt<T>(input: {
|
|
|
520
1168
|
);
|
|
521
1169
|
}
|
|
522
1170
|
const deadline = Date.now() + DANGEROUS_SIDE_EFFECT_LOCK_WAIT_MS;
|
|
1171
|
+
let completedWaitAttempt = 0;
|
|
523
1172
|
while (!ownsLock) {
|
|
524
1173
|
const lock = await input.store.acquireExecutionLock({
|
|
525
1174
|
receiptKey: input.receiptKey,
|
|
@@ -538,7 +1187,13 @@ export async function executeWithDurableRuntimeReceipt<T>(input: {
|
|
|
538
1187
|
if (Date.now() >= deadline) {
|
|
539
1188
|
throw new RuntimeReceiptWaitTimeoutError(input.receiptKey);
|
|
540
1189
|
}
|
|
541
|
-
|
|
1190
|
+
completedWaitAttempt += 1;
|
|
1191
|
+
await sleepReceiptWait(
|
|
1192
|
+
Math.min(
|
|
1193
|
+
runtimeReceiptWaitDelayMs(completedWaitAttempt),
|
|
1194
|
+
Math.max(0, deadline - Date.now()),
|
|
1195
|
+
),
|
|
1196
|
+
);
|
|
542
1197
|
}
|
|
543
1198
|
if (input.force !== true) {
|
|
544
1199
|
const afterLock = await inspectCompletedReceipt(
|
|
@@ -616,33 +1271,30 @@ export async function executeWithDurableRuntimeReceipt<T>(input: {
|
|
|
616
1271
|
if (input.force === true) {
|
|
617
1272
|
return result;
|
|
618
1273
|
}
|
|
619
|
-
//
|
|
620
|
-
//
|
|
621
|
-
//
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
1274
|
+
// The hydration window bounds how many stored payloads expand at once.
|
|
1275
|
+
// A completion is a write, and on wire format 3 it answers with a
|
|
1276
|
+
// compact acknowledgement carrying no payload at all — nothing expands,
|
|
1277
|
+
// so nothing needs a turn. Holding a slot of the 8-wide run-global
|
|
1278
|
+
// window across the whole gateway round trip only queued unrelated rows
|
|
1279
|
+
// behind this row's write. Only a response that really carries a stored
|
|
1280
|
+
// payload decodes under the gate.
|
|
1281
|
+
const completed = await input.store.complete(
|
|
1282
|
+
input.receiptKey,
|
|
1283
|
+
input.runId,
|
|
1284
|
+
isToolExecuteResult(result)
|
|
1285
|
+
? serializeToolExecuteResult(result)
|
|
1286
|
+
: result,
|
|
1287
|
+
COMPLETED_RECEIPT_CACHE_INSERT_LEASE_ID,
|
|
1288
|
+
);
|
|
1289
|
+
if (
|
|
1290
|
+
completed?.status === 'completed' ||
|
|
1291
|
+
completed?.status === 'skipped'
|
|
1292
|
+
) {
|
|
1293
|
+
if (completed.output === undefined) return result;
|
|
1294
|
+
return await withCompletedReceiptHydration(
|
|
1295
|
+
async () => await recoverCompletedReceipt(completed, 'owner'),
|
|
630
1296
|
);
|
|
631
|
-
|
|
632
|
-
completed?.status === 'completed' ||
|
|
633
|
-
completed?.status === 'skipped'
|
|
634
|
-
) {
|
|
635
|
-
return {
|
|
636
|
-
kind: 'completed' as const,
|
|
637
|
-
output:
|
|
638
|
-
completed.output === undefined
|
|
639
|
-
? result
|
|
640
|
-
: await recoverCompletedReceipt(completed, 'owner'),
|
|
641
|
-
};
|
|
642
|
-
}
|
|
643
|
-
return { kind: 'unresolved' as const };
|
|
644
|
-
});
|
|
645
|
-
if (published.kind === 'completed') return published.output;
|
|
1297
|
+
}
|
|
646
1298
|
const winner = await inspectCompletedReceipt(
|
|
647
1299
|
() => input.store.get(input.receiptKey),
|
|
648
1300
|
'owner',
|
|
@@ -704,8 +1356,22 @@ export async function executeWithDurableRuntimeReceipt<T>(input: {
|
|
|
704
1356
|
input.runningReceiptWaitMaxAttempts ?? DURABLE_RECEIPT_WAIT_MAX_ATTEMPTS;
|
|
705
1357
|
const delayMs =
|
|
706
1358
|
input.runningReceiptWaitDelayMs ?? DURABLE_RECEIPT_WAIT_DELAY_MS;
|
|
1359
|
+
// An explicit delay keeps the caller's fixed cadence; the default backs off.
|
|
1360
|
+
const backoff = input.runningReceiptWaitDelayMs == null;
|
|
1361
|
+
const waitDeadline =
|
|
1362
|
+
Date.now() + runtimeReceiptWaitBudgetMs(maxAttempts, delayMs);
|
|
707
1363
|
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
708
|
-
if (attempt > 0)
|
|
1364
|
+
if (attempt > 0) {
|
|
1365
|
+
if (backoff && Date.now() >= waitDeadline) break;
|
|
1366
|
+
await sleepReceiptWait(
|
|
1367
|
+
backoff
|
|
1368
|
+
? Math.min(
|
|
1369
|
+
runtimeReceiptWaitDelayMs(attempt),
|
|
1370
|
+
Math.max(0, waitDeadline - Date.now()),
|
|
1371
|
+
)
|
|
1372
|
+
: delayMs,
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
709
1375
|
const current = await input.store.get(input.receiptKey);
|
|
710
1376
|
if (current?.status === 'completed' || current?.status === 'skipped') {
|
|
711
1377
|
return {
|