deepline 0.3.128 → 0.3.130
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/daytona-sandbox-janitor.ts +4 -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 +114 -4
- 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/absurd.ts +28 -8
- 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 +146 -15
- 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 +904 -77
- 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
|
@@ -10,7 +10,7 @@ import type {
|
|
|
10
10
|
} from '../protocol';
|
|
11
11
|
|
|
12
12
|
export type PlayRunnerRuntimeResource = {
|
|
13
|
-
kind: 'daytona_sandbox' | 'modal_sandbox';
|
|
13
|
+
kind: 'daytona_sandbox' | 'modal_sandbox' | 'agentcore_runtime_session';
|
|
14
14
|
sandboxId: string;
|
|
15
15
|
/** Runtime estate that owns provider credentials and durable cleanup. */
|
|
16
16
|
runtimeEnvironment?: 'preview' | 'production';
|
|
@@ -19,6 +19,12 @@ export type PlayRunnerRuntimeResource = {
|
|
|
19
19
|
daytonaOrganizationId?: string;
|
|
20
20
|
/** Stable, non-secret Modal ownership domain returned at acquisition. */
|
|
21
21
|
modalAppId?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Stable, non-secret ownership domain for a session-based runtime. The
|
|
24
|
+
* provider adapter supplies this value; the shared flow only persists it as
|
|
25
|
+
* the cleanup routing domain.
|
|
26
|
+
*/
|
|
27
|
+
agentcoreRuntimeArn?: string;
|
|
22
28
|
billingStartedAt: number;
|
|
23
29
|
billingEndedAt?: number | null;
|
|
24
30
|
/**
|
|
@@ -85,7 +91,7 @@ export type PlayRunnerRuntimeLifecycleEvent = {
|
|
|
85
91
|
* resource identity to cross the no-replay boundary.
|
|
86
92
|
*/
|
|
87
93
|
export type PlayRunnerCommandAcceptance = {
|
|
88
|
-
provider: 'daytona' | 'modal';
|
|
94
|
+
provider: 'daytona' | 'modal' | 'agentcore_runtime';
|
|
89
95
|
sandboxId: string;
|
|
90
96
|
};
|
|
91
97
|
|
|
@@ -171,7 +177,7 @@ export interface PlayRunnerCallbacks {
|
|
|
171
177
|
onAmbiguousModalCreateResolved?: () => void | Promise<void>;
|
|
172
178
|
/** Reserve one physical-sandbox slot immediately before a provider create. */
|
|
173
179
|
reserveSandboxCapacity?: (
|
|
174
|
-
provider: 'daytona' | 'modal',
|
|
180
|
+
provider: 'daytona' | 'modal' | 'agentcore_runtime',
|
|
175
181
|
providerAttempt?: number,
|
|
176
182
|
) => Promise<{ leaseId: string; providerOperationToken: string | null }>;
|
|
177
183
|
/** Release an unbound reservation when provider create never produced a resource. */
|
|
@@ -182,11 +188,11 @@ export interface PlayRunnerCallbacks {
|
|
|
182
188
|
* pre-code recovery outcome instead.
|
|
183
189
|
*/
|
|
184
190
|
shouldAttemptSandboxProvider?: (
|
|
185
|
-
provider: 'daytona' | 'modal',
|
|
191
|
+
provider: 'daytona' | 'modal' | 'agentcore_runtime',
|
|
186
192
|
) => Promise<{ allowed: boolean; probeToken: string | null }>;
|
|
187
193
|
/** A durably recorded resource may close only the probe that owns it. */
|
|
188
194
|
markSandboxProviderHealthy?: (
|
|
189
|
-
provider: 'daytona' | 'modal',
|
|
195
|
+
provider: 'daytona' | 'modal' | 'agentcore_runtime',
|
|
190
196
|
probeToken: string | null,
|
|
191
197
|
) => Promise<void>;
|
|
192
198
|
/**
|
|
@@ -6,6 +6,10 @@ import type { PlayRunLedgerEvent } from './run-ledger';
|
|
|
6
6
|
import type { WorkReceiptFailureKind } from './work-receipts';
|
|
7
7
|
import type { ToolExecutionFailureV1 } from '../plays/tool-execution-error';
|
|
8
8
|
import type { DocflowObservationUpsert } from './docflow-observation';
|
|
9
|
+
import type {
|
|
10
|
+
RuntimeReceiptValueInlineFields,
|
|
11
|
+
RuntimeReceiptValueReference,
|
|
12
|
+
} from './runtime-receipt-value';
|
|
9
13
|
|
|
10
14
|
export type RuntimeAppendRunEventsAction = {
|
|
11
15
|
action: 'append_run_events';
|
|
@@ -96,12 +100,16 @@ export type RuntimeReceiptAction =
|
|
|
96
100
|
playName: string;
|
|
97
101
|
runId: string;
|
|
98
102
|
key: string;
|
|
103
|
+
/** Omit receipt outputs while probing completed-cache manifests. */
|
|
104
|
+
hydrateOutput?: boolean;
|
|
99
105
|
}
|
|
100
106
|
| {
|
|
101
107
|
action: 'get_runtime_step_receipts';
|
|
102
108
|
playName: string;
|
|
103
109
|
runId: string;
|
|
104
110
|
keys: string[];
|
|
111
|
+
/** Omit receipt outputs while probing completed-cache manifests. */
|
|
112
|
+
hydrateOutput?: boolean;
|
|
105
113
|
}
|
|
106
114
|
| {
|
|
107
115
|
action: 'acquire_runtime_receipt_execution_lock';
|
|
@@ -123,6 +131,8 @@ export type RuntimeReceiptAction =
|
|
|
123
131
|
playName: string;
|
|
124
132
|
runId: string;
|
|
125
133
|
key: string;
|
|
134
|
+
/** Omit terminal output bodies while establishing a receipt claim. */
|
|
135
|
+
hydrateOutput?: boolean;
|
|
126
136
|
leaseId?: string;
|
|
127
137
|
runAttempt?: number | null;
|
|
128
138
|
leaseAware?: boolean;
|
|
@@ -135,6 +145,8 @@ export type RuntimeReceiptAction =
|
|
|
135
145
|
playName: string;
|
|
136
146
|
runId: string;
|
|
137
147
|
keys: string[];
|
|
148
|
+
/** Omit terminal output bodies while establishing receipt claims. */
|
|
149
|
+
hydrateOutput?: boolean;
|
|
138
150
|
leaseIds?: string[];
|
|
139
151
|
runAttempt?: number | null;
|
|
140
152
|
leaseAware?: boolean;
|
|
@@ -150,6 +162,7 @@ export type RuntimeReceiptAction =
|
|
|
150
162
|
runAttempt?: number | null;
|
|
151
163
|
leaseId?: string | null;
|
|
152
164
|
output: unknown;
|
|
165
|
+
inlineFields?: RuntimeReceiptValueInlineFields;
|
|
153
166
|
}
|
|
154
167
|
| {
|
|
155
168
|
action: 'release_runtime_step_receipt';
|
|
@@ -205,6 +218,18 @@ export type RuntimeReceiptAction =
|
|
|
205
218
|
runId: string;
|
|
206
219
|
receipts: RuntimeReceiptCompletion[];
|
|
207
220
|
}
|
|
221
|
+
| {
|
|
222
|
+
/**
|
|
223
|
+
* New named action: the execution relay has already persisted the full
|
|
224
|
+
* serialized result to the receipt-owned R2 key. A receiver that does
|
|
225
|
+
* not know this action rejects it loudly; the runner then retries the
|
|
226
|
+
* established full-payload completion without replaying provider work.
|
|
227
|
+
*/
|
|
228
|
+
action: 'complete_runtime_step_receipts_by_reference';
|
|
229
|
+
playName: string;
|
|
230
|
+
runId: string;
|
|
231
|
+
receipts: RuntimeReceiptReferenceCompletion[];
|
|
232
|
+
}
|
|
208
233
|
| {
|
|
209
234
|
action: 'fail_runtime_step_receipts';
|
|
210
235
|
playName: string;
|
|
@@ -234,6 +259,11 @@ export type RuntimeReceiptLease = {
|
|
|
234
259
|
|
|
235
260
|
export type RuntimeReceiptCompletion = RuntimeReceiptLease & {
|
|
236
261
|
output: unknown;
|
|
262
|
+
inlineFields?: RuntimeReceiptValueInlineFields;
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
export type RuntimeReceiptReferenceCompletion = RuntimeReceiptLease & {
|
|
266
|
+
outputRef: RuntimeReceiptValueReference;
|
|
237
267
|
};
|
|
238
268
|
|
|
239
269
|
export type RuntimeReceiptFailure = RuntimeReceiptLease & {
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire compression for runtime API request bodies.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS. A fixture sandbox sits ~95 ms from the Fly receipt gateway
|
|
5
|
+
* (Daytona eu, Fly iad). Measured on run 34104186304, a ~100 KB receipt
|
|
6
|
+
* completion request (12 receipts x ~8 KB) took 765 ms p50 on the runner's
|
|
7
|
+
* clock while the gateway's own `Server-Timing` accounted for 50 ms of it. The
|
|
8
|
+
* missing 700 ms is not the gateway and not the network's bandwidth: it is TCP
|
|
9
|
+
* congestion-window growth. A keep-alive connection that idles between
|
|
10
|
+
* completion batches has its cwnd reset toward the initial window (~10 MSS,
|
|
11
|
+
* ~14 KB), so a 100 KB upload is delivered over 5-8 round trips of ~95 ms each
|
|
12
|
+
* before the gateway has the whole body to parse. jai's ~1 MB completion
|
|
13
|
+
* requests pay the same arithmetic at ~5.4 s.
|
|
14
|
+
*
|
|
15
|
+
* Moving the sandbox region is an ops action and is being pursued separately.
|
|
16
|
+
* Independent of it, the round trips are a function of BYTES, so shrinking the
|
|
17
|
+
* body shrinks the wall directly. Measured gzip level 6 on realistic receipt
|
|
18
|
+
* payloads: high-entropy enrichment output compresses to ~24% (a 100 KB body
|
|
19
|
+
* becomes ~24 KB, which fits in 1-2 congestion windows instead of 5-8), and
|
|
20
|
+
* repetitive output to ~4-6%.
|
|
21
|
+
*
|
|
22
|
+
* COMPATIBILITY. A gateway that does not understand `content-encoding: gzip`
|
|
23
|
+
* would hand gzip bytes to `JSON.parse` and fail every request, so the client
|
|
24
|
+
* never compresses speculatively. It compresses only after it has seen the
|
|
25
|
+
* gateway advertise `x-deepline-runtime-api-accepts: gzip` on a response from
|
|
26
|
+
* that base URL. The first request to any origin is therefore always
|
|
27
|
+
* uncompressed, and an older gateway (or an app route that has not shipped the
|
|
28
|
+
* decoder) simply never advertises and never receives a compressed body. In the
|
|
29
|
+
* other direction, a newer gateway keeps accepting uncompressed bodies from
|
|
30
|
+
* older runners forever: the encoding is opt-in per request, not a version gate.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { gzip, gunzip } from 'node:zlib';
|
|
34
|
+
import { promisify } from 'node:util';
|
|
35
|
+
|
|
36
|
+
const gzipAsync = promisify(gzip);
|
|
37
|
+
const gunzipAsync = promisify(gunzip);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Bodies at or above this size are gzipped before they are sent.
|
|
41
|
+
*
|
|
42
|
+
* 16 KiB is one congestion window plus change. Below it the body already fits
|
|
43
|
+
* in the first flight of an established connection (initial window ~10 MSS,
|
|
44
|
+
* ~14 KB), so compressing buys no round trip — it only spends CPU on the
|
|
45
|
+
* runner's single vCPU and adds a gzip header the gateway has to inflate. At
|
|
46
|
+
* and above it, every ~14 KB removed from the wire removes roughly one ~95 ms
|
|
47
|
+
* round trip from the request (measured above: 100 KB = 765 ms client / 50 ms
|
|
48
|
+
* server; the ~700 ms difference is cwnd growth, not bandwidth).
|
|
49
|
+
*
|
|
50
|
+
* The threshold is compared against the UTF-8 byte length of the serialized
|
|
51
|
+
* body, not its character count.
|
|
52
|
+
*/
|
|
53
|
+
export const RUNTIME_API_REQUEST_COMPRESSION_MIN_BYTES = 16 * 1024;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* zlib level for request bodies. Level 6 is the measured optimum at one vCPU
|
|
57
|
+
* on the sandbox's ~15 Mbit/s link (51 runs, 6.17 MB raw batch): level 6
|
|
58
|
+
* costs 539 ms and ships 889 KB, level 1 costs 418 ms and ships 1306 KB, no
|
|
59
|
+
* gzip costs 183 ms and ships 6025 KB. Batches per second are bounded by
|
|
60
|
+
* whichever side binds: min(CPU, link) = 1.90 at level 6, 1.44 at level 1,
|
|
61
|
+
* 0.31 with no gzip. Level 1 saves 121 ms of CPU and buys 417 KB of wire that
|
|
62
|
+
* costs 223 ms. If the search moves, it moves up from 6, not down. See the
|
|
63
|
+
* cost table in `gzipRuntimeApiRequestBody`.
|
|
64
|
+
*/
|
|
65
|
+
export const RUNTIME_API_REQUEST_COMPRESSION_LEVEL = 6;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The gateway's advertisement that it can decode compressed request bodies.
|
|
69
|
+
*
|
|
70
|
+
* Present on EVERY gateway response, including errors and health probes, so a
|
|
71
|
+
* client learns the capability from whatever response it happens to get first
|
|
72
|
+
* rather than needing a successful request of a particular action.
|
|
73
|
+
*/
|
|
74
|
+
export const RUNTIME_API_ACCEPTS_HEADER = 'x-deepline-runtime-api-accepts';
|
|
75
|
+
|
|
76
|
+
/** The only encoding this contract defines. */
|
|
77
|
+
export const RUNTIME_API_ACCEPTS_GZIP = 'gzip';
|
|
78
|
+
|
|
79
|
+
/** Does an `x-deepline-runtime-api-accepts` value advertise gzip? */
|
|
80
|
+
export function runtimeApiAcceptsHeaderAdvertisesGzip(
|
|
81
|
+
header: string | null | undefined,
|
|
82
|
+
): boolean {
|
|
83
|
+
if (typeof header !== 'string') return false;
|
|
84
|
+
return header
|
|
85
|
+
.split(',')
|
|
86
|
+
.some((token) => token.trim().toLowerCase() === RUNTIME_API_ACCEPTS_GZIP);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Is a request's `content-encoding` gzip? */
|
|
90
|
+
export function isGzipContentEncoding(
|
|
91
|
+
header: string | null | undefined,
|
|
92
|
+
): boolean {
|
|
93
|
+
return typeof header === 'string' && header.trim().toLowerCase() === 'gzip';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Origins observed to advertise gzip, keyed by base URL origin.
|
|
98
|
+
*
|
|
99
|
+
* State is per process and per origin, never per request: a runner talks to one
|
|
100
|
+
* receipt gateway and one app runtime API for its whole life, so one observed
|
|
101
|
+
* response enables compression until that origin proves otherwise. The state
|
|
102
|
+
* cannot be monotonic: a rolling Fly fleet can route two requests for the same
|
|
103
|
+
* hostname to different Machine revisions, and an older revision cannot decode
|
|
104
|
+
* a gzip body even if its newer peer advertised support moments earlier.
|
|
105
|
+
*/
|
|
106
|
+
const gzipCapableOrigins = new Set<string>();
|
|
107
|
+
|
|
108
|
+
function compressionOriginKey(baseUrl: string): string {
|
|
109
|
+
const trimmed = baseUrl.trim();
|
|
110
|
+
if (!trimmed) return '';
|
|
111
|
+
try {
|
|
112
|
+
return new URL(trimmed).origin;
|
|
113
|
+
} catch {
|
|
114
|
+
// A non-URL base is still a stable key; it just cannot be normalized.
|
|
115
|
+
return trimmed.replace(/\/$/, '');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Record what one response said about its gateway's request-decoding support.
|
|
121
|
+
*
|
|
122
|
+
* Called for every response, successful or not. A response that carries no
|
|
123
|
+
* advertisement teaches nothing and never revokes a prior advertisement.
|
|
124
|
+
*/
|
|
125
|
+
export function noteRuntimeApiCompressionCapability(input: {
|
|
126
|
+
baseUrl: string;
|
|
127
|
+
header: string | null | undefined;
|
|
128
|
+
}): void {
|
|
129
|
+
if (!runtimeApiAcceptsHeaderAdvertisesGzip(input.header)) return;
|
|
130
|
+
const key = compressionOriginKey(input.baseUrl);
|
|
131
|
+
if (key) gzipCapableOrigins.add(key);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Forget an origin after its legacy JSON decoder rejected a gzip body.
|
|
136
|
+
*
|
|
137
|
+
* The caller retries that exact request as plain JSON before any durable action
|
|
138
|
+
* can have run. A subsequent response from a newer Machine may advertise gzip
|
|
139
|
+
* again, but a rolling hostname is never treated as permanently capable.
|
|
140
|
+
*/
|
|
141
|
+
export function revokeRuntimeApiCompressionCapability(baseUrl: string): void {
|
|
142
|
+
const key = compressionOriginKey(baseUrl);
|
|
143
|
+
if (key) gzipCapableOrigins.delete(key);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Is this the exact response emitted when a pre-gzip gateway parsed compressed
|
|
148
|
+
* bytes as JSON? Restrict the fallback to this known pre-dispatch failure so a
|
|
149
|
+
* normal 400 from a gzip-capable gateway is never replayed.
|
|
150
|
+
*/
|
|
151
|
+
export function isLegacyRuntimeApiGzipDecoderFailure(input: {
|
|
152
|
+
contentEncoding: RuntimeApiRequestBody['contentEncoding'];
|
|
153
|
+
status: number;
|
|
154
|
+
responseText: string;
|
|
155
|
+
}): boolean {
|
|
156
|
+
if (input.contentEncoding !== 'gzip' || input.status !== 400) return false;
|
|
157
|
+
try {
|
|
158
|
+
const parsed = JSON.parse(input.responseText) as { error?: unknown };
|
|
159
|
+
return parsed.error === 'JSON body is required.';
|
|
160
|
+
} catch {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Has this origin advertised that it can decode gzipped request bodies? */
|
|
166
|
+
export function runtimeApiOriginAcceptsGzip(baseUrl: string): boolean {
|
|
167
|
+
const key = compressionOriginKey(baseUrl);
|
|
168
|
+
return key ? gzipCapableOrigins.has(key) : false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Forget every observed capability. Tests only; there is no production reset. */
|
|
172
|
+
export function resetRuntimeApiCompressionCapabilities(): void {
|
|
173
|
+
gzipCapableOrigins.clear();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export type RuntimeApiRequestBody = {
|
|
177
|
+
/** What goes on the wire. */
|
|
178
|
+
body: string | Uint8Array;
|
|
179
|
+
/** `gzip` when compressed, otherwise null (send no `content-encoding`). */
|
|
180
|
+
contentEncoding: 'gzip' | null;
|
|
181
|
+
/** UTF-8 bytes of the serialized JSON, before any compression. */
|
|
182
|
+
rawBytes: number;
|
|
183
|
+
/** Bytes actually sent. Equals `rawBytes` when uncompressed. */
|
|
184
|
+
wireBytes: number;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Compress a serialized request body when it is worth compressing.
|
|
189
|
+
*
|
|
190
|
+
* ASYNC, NOT SYNC. Measured on this codebase's realistic receipt-completion
|
|
191
|
+
* payloads (level 6): a 100 KB body costs 2.2 ms and a 1 MB body 26.5 ms, and
|
|
192
|
+
* `gzipSync` and `promisify(gzip)` have the SAME wall (2.24/2.37 ms and
|
|
193
|
+
* 26.55/27.13 ms). Identical wall, but the async form runs on the libuv
|
|
194
|
+
* threadpool instead of blocking the runner's single-vCPU event loop, so the
|
|
195
|
+
* map keeps advancing rows during the compression of a large completion batch.
|
|
196
|
+
* A 26 ms synchronous stall on every 1 MB completion is exactly the kind of
|
|
197
|
+
* per-batch pause the throughput work is removing, so the free version wins.
|
|
198
|
+
*/
|
|
199
|
+
export async function gzipRuntimeApiRequestBody(input: {
|
|
200
|
+
serialized: string;
|
|
201
|
+
/** Compress only when the destination has advertised support. */
|
|
202
|
+
originAcceptsGzip: boolean;
|
|
203
|
+
minBytes?: number;
|
|
204
|
+
}): Promise<RuntimeApiRequestBody> {
|
|
205
|
+
const rawBytes = Buffer.byteLength(input.serialized, 'utf8');
|
|
206
|
+
const minBytes = input.minBytes ?? RUNTIME_API_REQUEST_COMPRESSION_MIN_BYTES;
|
|
207
|
+
if (!input.originAcceptsGzip || rawBytes < minBytes) {
|
|
208
|
+
return {
|
|
209
|
+
body: input.serialized,
|
|
210
|
+
contentEncoding: null,
|
|
211
|
+
rawBytes,
|
|
212
|
+
wireBytes: rawBytes,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
const compressed = await gzipAsync(Buffer.from(input.serialized, 'utf8'), {
|
|
216
|
+
level: RUNTIME_API_REQUEST_COMPRESSION_LEVEL,
|
|
217
|
+
});
|
|
218
|
+
return {
|
|
219
|
+
body: compressed,
|
|
220
|
+
contentEncoding: 'gzip',
|
|
221
|
+
rawBytes,
|
|
222
|
+
wireBytes: compressed.byteLength,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* A gzipped request body whose DECOMPRESSED size exceeds the gateway limit.
|
|
228
|
+
*
|
|
229
|
+
* Loud and typed rather than a generic parse failure: a compressed body is the
|
|
230
|
+
* one shape where the transport's own byte guard cannot see the real size, so
|
|
231
|
+
* the caller must be told the limit it actually crossed. The gateway surfaces
|
|
232
|
+
* this as the same 413 an oversized uncompressed body gets.
|
|
233
|
+
*/
|
|
234
|
+
export class RuntimeApiDecompressedBodyTooLargeError extends Error {
|
|
235
|
+
constructor(
|
|
236
|
+
readonly maxBytes: number,
|
|
237
|
+
readonly wireBytes: number,
|
|
238
|
+
) {
|
|
239
|
+
super(
|
|
240
|
+
`Runtime API request body decompressed to more than the ${maxBytes} byte limit ` +
|
|
241
|
+
`(gzip wire size ${wireBytes} bytes). Send fewer receipts per request.`,
|
|
242
|
+
);
|
|
243
|
+
this.name = 'RuntimeApiDecompressedBodyTooLargeError';
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Inflate a gzipped request body under a hard output cap.
|
|
249
|
+
*
|
|
250
|
+
* `maxOutputLength` makes zlib abort the inflate as soon as the output crosses
|
|
251
|
+
* the cap, so a zip bomb costs the gateway the bytes it was actually sent plus
|
|
252
|
+
* one buffer of the limit — never the decompressed size. Without it a 500 KB
|
|
253
|
+
* body could expand to gigabytes before any byte guard downstream saw it.
|
|
254
|
+
*
|
|
255
|
+
* Async for the same reason the client compresses asynchronously: inflate is
|
|
256
|
+
* cheap (0.2 ms for 100 KB, 2.2 ms for 1 MB) but the gateway multiplexes many
|
|
257
|
+
* runners on one event loop, and the threadpool version costs ~1 ms more wall
|
|
258
|
+
* for work that no longer blocks every other in-flight request.
|
|
259
|
+
*/
|
|
260
|
+
export async function gunzipRuntimeApiRequestBody(input: {
|
|
261
|
+
body: Buffer;
|
|
262
|
+
maxBytes: number;
|
|
263
|
+
}): Promise<Buffer> {
|
|
264
|
+
try {
|
|
265
|
+
return await gunzipAsync(input.body, { maxOutputLength: input.maxBytes });
|
|
266
|
+
} catch (error) {
|
|
267
|
+
const code = (error as { code?: unknown } | null)?.code;
|
|
268
|
+
if (code === 'ERR_BUFFER_TOO_LARGE' || code === 'ERR_OUT_OF_RANGE') {
|
|
269
|
+
throw new RuntimeApiDecompressedBodyTooLargeError(
|
|
270
|
+
input.maxBytes,
|
|
271
|
+
input.body.byteLength,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
}
|