@cloudflare/sandbox 0.12.4 → 0.12.6
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/bridge/index.d.ts +30 -0
- package/dist/bridge/index.d.ts.map +1 -1
- package/dist/bridge/index.js +196 -77
- package/dist/bridge/index.js.map +1 -1
- package/dist/{contexts-mjA8ZsDG.d.ts → contexts-C186NnAB.d.ts} +53 -4
- package/dist/contexts-C186NnAB.d.ts.map +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/openai/index.d.ts +1 -1
- package/dist/opencode/index.d.ts +2 -2
- package/dist/{sandbox-5MHau7vJ.d.ts → sandbox-BtaWcmmG.d.ts} +165 -6
- package/dist/{sandbox-5MHau7vJ.d.ts.map → sandbox-BtaWcmmG.d.ts.map} +1 -1
- package/dist/{sandbox-CyqG4jca.js → sandbox-FBmZ9mj1.js} +908 -93
- package/dist/sandbox-FBmZ9mj1.js.map +1 -0
- package/package.json +2 -2
- package/dist/contexts-mjA8ZsDG.d.ts.map +0 -1
- package/dist/sandbox-CyqG4jca.js.map +0 -1
package/dist/bridge/index.d.ts
CHANGED
|
@@ -49,6 +49,8 @@ interface BridgeEnv {
|
|
|
49
49
|
SANDBOX_API_KEY?: string;
|
|
50
50
|
WARM_POOL_TARGET?: string;
|
|
51
51
|
WARM_POOL_REFRESH_INTERVAL?: string;
|
|
52
|
+
WARM_POOL_MAX_INSTANCES?: string;
|
|
53
|
+
WARM_POOL_SCALE_BATCH_SIZE?: string;
|
|
52
54
|
[key: string]: unknown;
|
|
53
55
|
}
|
|
54
56
|
//#endregion
|
|
@@ -75,6 +77,20 @@ interface WarmPoolConfig {
|
|
|
75
77
|
warmTarget?: number;
|
|
76
78
|
/** How often to check and replenish warm containers (ms). @default 10000 */
|
|
77
79
|
refreshInterval?: number;
|
|
80
|
+
/**
|
|
81
|
+
* Operator-declared max_instances ceiling, mirroring
|
|
82
|
+
* `containers[].max_instances` in wrangler.jsonc. Seeds the pool's capacity
|
|
83
|
+
* math so it never blindly attempts doomed starts. 0/undefined = auto-learn
|
|
84
|
+
* the ceiling reactively from platform errors (legacy behaviour).
|
|
85
|
+
* @default 0
|
|
86
|
+
*/
|
|
87
|
+
maxInstances?: number;
|
|
88
|
+
/**
|
|
89
|
+
* Number of containers to start in parallel per scale-up batch. Higher
|
|
90
|
+
* values fill the pool faster but risk the platform throttling a cold-start
|
|
91
|
+
* stampede. Clamped to [1, MAX_BATCH_SIZE]. @default 5
|
|
92
|
+
*/
|
|
93
|
+
scaleBatchSize?: number;
|
|
78
94
|
}
|
|
79
95
|
interface PoolStats {
|
|
80
96
|
/** Number of warm (unassigned) containers ready for use */
|
|
@@ -109,6 +125,13 @@ declare class WarmPool extends DurableObject<WarmPoolEnv> {
|
|
|
109
125
|
private knownMaxInstances;
|
|
110
126
|
private capacityExhausted;
|
|
111
127
|
private initialized;
|
|
128
|
+
/** Guards against overlapping eager refills triggered by concurrent pops. */
|
|
129
|
+
private refillInFlight;
|
|
130
|
+
/**
|
|
131
|
+
* The currently in-flight eager refill, or null. Exposed for tests to await
|
|
132
|
+
* the otherwise fire-and-forget refill deterministically.
|
|
133
|
+
*/
|
|
134
|
+
private refillPromise;
|
|
112
135
|
/**
|
|
113
136
|
* Get a container UUID for the given sandbox ID.
|
|
114
137
|
* - If this ID already has an assigned container that's still running, return it.
|
|
@@ -139,6 +162,13 @@ declare class WarmPool extends DurableObject<WarmPoolEnv> {
|
|
|
139
162
|
* Does not affect containers that are assigned to sandbox IDs.
|
|
140
163
|
*/
|
|
141
164
|
shutdownPrewarmed(): Promise<void>;
|
|
165
|
+
/**
|
|
166
|
+
* Kick a non-blocking refill toward warmTarget. Debounced via refillInFlight
|
|
167
|
+
* so a burst of concurrent pops triggers at most one adjustPool sweep at a
|
|
168
|
+
* time; capacity is respected because adjustPool clamps to remainingCapacity.
|
|
169
|
+
* Errors are swallowed so a failed refill never rejects the triggering call.
|
|
170
|
+
*/
|
|
171
|
+
private requestRefill;
|
|
142
172
|
alarm(): Promise<void>;
|
|
143
173
|
private init;
|
|
144
174
|
private persist;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/bridge/types.ts","../../src/bridge/helpers.ts","../../src/bridge/warm-pool.ts","../../src/bridge/index.ts"],"sourcesContent":[],"mappings":";;;;;;;AA6DgB,UAvCC,YAAA,CAuCD;EAEP;;;AAaT;;;;ICJgB,OAAA,CAAA,EAAU,MAAA;IAyBV;;;;AC7EhB;
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/bridge/types.ts","../../src/bridge/helpers.ts","../../src/bridge/warm-pool.ts","../../src/bridge/index.ts"],"sourcesContent":[],"mappings":";;;;;;;AA6DgB,UAvCC,YAAA,CAuCD;EAEP;;;AAaT;;;;ICJgB,OAAA,CAAA,EAAU,MAAA;IAyBV;;;;AC7EhB;AAqBA;AAWC;AAoDD;EAA4C,cAAA,CAAA,EAAA,MAAA;EAoCH;;;;;EAqFf,WAAA,CAAA,EAAA,MAAA;;;;;;;;UF3KT,cAAA;EGgCD,KAAA,EAAA,OAAM,EH9BT,OG8BS,EAAA,GAAA,EAAA,GAAA,EAAA,GAAA,EH5Bb,gBG4Ba,CAAA,EH3BjB,QG2BiB,GH3BN,OG2BM,CH3BE,QG2BF,CAAA;EACZ,SAAA,EAAA,UAAA,EH1BM,mBG0BN,EAAA,GAAA,EAAA,GAAA,EAAA,GAAA,EHxBD,gBGwBC,CAAA,EAAA,IAAA,GHvBE,OGuBF,CAAA,IAAA,CAAA;EACC,CAAA,GAAA,EAAA,MAAA,CAAA,EAAA,OAAA;;;;;;UHZM,SAAA;;;;;;;;;;;;;AAAjB;;;;ACJA;AAyBgB,iBAzBA,UAAA,CAyBoB,GAAA,EAAA,MAAA,CAAA,EAAA,MAAA;;;;AC7EpC;AAqBA;AA0DU,iBDFM,oBAAA,CCGL,QAAsB,EAAA,MAAA,CAAA,EAAA,MAAA,GAAA,IAAA;;;AFxBhB,UExDA,cAAA,CFwDS;;;;ECJV,eAAU,CAAA,EAAA,MAAA;EAyBV;;;;AC7EhB;AAqBA;AAWC;EAoDY,YAAS,CAAA,EAAA,MAAA;EAAsB;;;;;EA0GxB,cAAA,CAAA,EAAA,MAAA;;AAeuB,UAxL1B,SAAA,CAwL0B;EA+Bd;EAyDZ,IAAA,EAAA,MAAA;EAjNa;EAAa,QAAA,EAAA,MAAA;;;;EClB3B,MAAA,EDrCN,QCqCY,CDrCH,cCqCG,CAAA;EACZ;EACC,YAAA,EAAA,MAAA,GAAA,IAAA;;;;;;;UDWD,WAAA;WACC;;;cAIE,QAAA,SAAiB,cAAc;;;;;;;;;;;;;;;;;;;;;;;;mCAoCH;;;;;;sCAiDG;;;;wCAYE;;;;cAS1B,QAAQ;;;;;oBAeF,iBAAiB;;;;;uBA+Bd;;;;;;;;WAyDZ;;;;;;;;;;;;;;;;;;;;;;;;AAhRjB;AAWC;AAoDD;;;;;;;;;;AAiNiB,iBCnOD,MAAA,CDmOC,MAAA,EClOP,cDkOO,EAAA,MAAA,CAAA,ECjON,YDiOM,CAAA,EChOd,eDgOc,CChOE,SDgOF,CAAA"}
|
package/dist/bridge/index.js
CHANGED
|
@@ -1,25 +1,35 @@
|
|
|
1
1
|
import "../dist-B_eXrP83.js";
|
|
2
2
|
import "../errors-CpkKPbWC.js";
|
|
3
|
-
import { S as validateTunnelName, r as getSandbox, w as streamFile, x as validatePort, y as SandboxSecurityError } from "../sandbox-
|
|
4
|
-
import { DurableObject, env } from "cloudflare:workers";
|
|
3
|
+
import { S as validateTunnelName, r as getSandbox, w as streamFile, x as validatePort, y as SandboxSecurityError } from "../sandbox-FBmZ9mj1.js";
|
|
4
|
+
import { DurableObject, env, tracing } from "cloudflare:workers";
|
|
5
5
|
import { Hono } from "hono";
|
|
6
6
|
|
|
7
7
|
//#region src/bridge/pool.ts
|
|
8
8
|
/**
|
|
9
|
+
* Parse warm-pool configuration from bridge env vars.
|
|
10
|
+
*
|
|
11
|
+
* `WARM_POOL_MAX_INSTANCES` should match `containers[].max_instances` in
|
|
12
|
+
* wrangler.jsonc. 0/unset means auto-learn the ceiling reactively.
|
|
13
|
+
*/
|
|
14
|
+
function parsePoolConfig(env$1) {
|
|
15
|
+
return {
|
|
16
|
+
warmTarget: Number.parseInt(env$1.WARM_POOL_TARGET || "0", 10) || 0,
|
|
17
|
+
refreshInterval: Number.parseInt(env$1.WARM_POOL_REFRESH_INTERVAL || "10000", 10) || 1e4,
|
|
18
|
+
maxInstances: Number.parseInt(env$1.WARM_POOL_MAX_INSTANCES || "0", 10) || 0,
|
|
19
|
+
scaleBatchSize: Number.parseInt(env$1.WARM_POOL_SCALE_BATCH_SIZE || "5", 10) || 5
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
9
23
|
* Prime the warm pool — pushes current configuration to the WarmPool
|
|
10
24
|
* Durable Object so it starts its alarm loop.
|
|
11
25
|
*
|
|
12
26
|
* Called by the scheduled() handler and by POST /pool/prime.
|
|
13
27
|
*/
|
|
14
28
|
async function primePool(env$1, warmPoolBinding) {
|
|
15
|
-
const
|
|
16
|
-
const refreshInterval = Number.parseInt(env$1.WARM_POOL_REFRESH_INTERVAL || "10000", 10) || 1e4;
|
|
29
|
+
const config = parsePoolConfig(env$1);
|
|
17
30
|
const ns = env$1[warmPoolBinding];
|
|
18
31
|
const poolId = ns.idFromName("global-pool");
|
|
19
|
-
await ns.get(poolId).configure(
|
|
20
|
-
warmTarget,
|
|
21
|
-
refreshInterval
|
|
22
|
-
});
|
|
32
|
+
await ns.get(poolId).configure(config);
|
|
23
33
|
}
|
|
24
34
|
|
|
25
35
|
//#endregion
|
|
@@ -1761,6 +1771,51 @@ function esc(s) {
|
|
|
1761
1771
|
return String(s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1762
1772
|
}
|
|
1763
1773
|
|
|
1774
|
+
//#endregion
|
|
1775
|
+
//#region src/bridge/tracing.ts
|
|
1776
|
+
/**
|
|
1777
|
+
* Custom span instrumentation for the bridge API.
|
|
1778
|
+
*
|
|
1779
|
+
* Wraps each Hono route handler in a Cloudflare custom span
|
|
1780
|
+
* (`tracing.enterSpan`) named `bridge.<operation>`, seeding common attributes
|
|
1781
|
+
* (sandbox ID, container UUID, HTTP method/route, response status) and exposing
|
|
1782
|
+
* the active span so handlers can attach operation-specific metadata.
|
|
1783
|
+
*
|
|
1784
|
+
* See https://developers.cloudflare.com/workers/observability/traces/custom-spans/
|
|
1785
|
+
*/
|
|
1786
|
+
/** True when the runtime exposes the custom-span API. */
|
|
1787
|
+
function tracingAvailable() {
|
|
1788
|
+
return typeof tracing === "object" && tracing !== null && typeof tracing.enterSpan === "function";
|
|
1789
|
+
}
|
|
1790
|
+
/**
|
|
1791
|
+
* Wrap a route handler in a `bridge.<name>` span. Seeds common attributes and
|
|
1792
|
+
* stashes the span on the context for `annotate()`. When tracing is
|
|
1793
|
+
* unavailable the handler runs unchanged.
|
|
1794
|
+
*/
|
|
1795
|
+
function traced(name, handler) {
|
|
1796
|
+
if (!tracingAvailable()) return handler;
|
|
1797
|
+
return (c) => tracing.enterSpan(`bridge.${name}`, async (span) => {
|
|
1798
|
+
span.setAttribute("bridge.operation", name);
|
|
1799
|
+
span.setAttribute("http.request.method", c.req.method);
|
|
1800
|
+
span.setAttribute("http.route", c.req.routePath);
|
|
1801
|
+
const sandboxId = c.req.param("id");
|
|
1802
|
+
if (sandboxId) span.setAttribute("sandbox.id", sandboxId);
|
|
1803
|
+
const containerUUID = c.get("containerUUID");
|
|
1804
|
+
if (containerUUID) span.setAttribute("sandbox.container_uuid", containerUUID);
|
|
1805
|
+
c.set("span", span);
|
|
1806
|
+
const response = await handler(c);
|
|
1807
|
+
span.setAttribute("http.response.status_code", response.status);
|
|
1808
|
+
return response;
|
|
1809
|
+
});
|
|
1810
|
+
}
|
|
1811
|
+
/**
|
|
1812
|
+
* Attach an attribute to the active bridge span, if any. No-op when tracing is
|
|
1813
|
+
* disabled or the request is not sampled.
|
|
1814
|
+
*/
|
|
1815
|
+
function annotate(c, key, value) {
|
|
1816
|
+
c.get("span")?.setAttribute(key, value);
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1764
1819
|
//#endregion
|
|
1765
1820
|
//#region src/bridge/routes.ts
|
|
1766
1821
|
/** Typed wrapper around the SDK's getSandbox() that returns a BridgeSandbox. */
|
|
@@ -1859,7 +1914,7 @@ function createBridgeApp(config) {
|
|
|
1859
1914
|
}
|
|
1860
1915
|
return next();
|
|
1861
1916
|
});
|
|
1862
|
-
app.post(`${apiPrefix}/sandbox`, async (c) => {
|
|
1917
|
+
app.post(`${apiPrefix}/sandbox`, traced("create_sandbox", async (c) => {
|
|
1863
1918
|
const token = c.env.SANDBOX_API_KEY;
|
|
1864
1919
|
if (token) {
|
|
1865
1920
|
const authHeader = c.req.header("Authorization") ?? "";
|
|
@@ -1868,20 +1923,16 @@ function createBridgeApp(config) {
|
|
|
1868
1923
|
const bytes = new Uint8Array(16);
|
|
1869
1924
|
crypto.getRandomValues(bytes);
|
|
1870
1925
|
const id = base32Encode(bytes);
|
|
1926
|
+
annotate(c, "sandbox.id", id);
|
|
1871
1927
|
return c.json({ id });
|
|
1872
|
-
});
|
|
1928
|
+
}));
|
|
1873
1929
|
app.use(`${apiPrefix}/sandbox/:id/*`, async (c, next) => {
|
|
1874
1930
|
const sandboxId = c.req.param("id");
|
|
1875
|
-
const warmTarget = Number.parseInt(c.env.WARM_POOL_TARGET || "0", 10) || 0;
|
|
1876
|
-
const refreshInterval = Number.parseInt(c.env.WARM_POOL_REFRESH_INTERVAL || "10000", 10) || 1e4;
|
|
1877
1931
|
const poolNs = getWarmPoolNs(c.env);
|
|
1878
1932
|
const poolId = poolNs.idFromName("global-pool");
|
|
1879
1933
|
const poolStub = poolNs.get(poolId);
|
|
1880
1934
|
try {
|
|
1881
|
-
await poolStub.configure(
|
|
1882
|
-
warmTarget,
|
|
1883
|
-
refreshInterval
|
|
1884
|
-
});
|
|
1935
|
+
await poolStub.configure(parsePoolConfig(c.env));
|
|
1885
1936
|
const containerUUID = await poolStub.getContainer(sandboxId);
|
|
1886
1937
|
c.set("containerUUID", containerUUID);
|
|
1887
1938
|
} catch (err) {
|
|
@@ -1906,7 +1957,7 @@ function createBridgeApp(config) {
|
|
|
1906
1957
|
}
|
|
1907
1958
|
return next();
|
|
1908
1959
|
});
|
|
1909
|
-
app.post(`${apiPrefix}/sandbox/:id/exec`, async (c) => {
|
|
1960
|
+
app.post(`${apiPrefix}/sandbox/:id/exec`, traced("exec", async (c) => {
|
|
1910
1961
|
let body;
|
|
1911
1962
|
try {
|
|
1912
1963
|
body = await c.req.json();
|
|
@@ -1914,21 +1965,28 @@ function createBridgeApp(config) {
|
|
|
1914
1965
|
return errorJson("Invalid JSON body", "invalid_request", 400);
|
|
1915
1966
|
}
|
|
1916
1967
|
if (!Array.isArray(body.argv) || body.argv.length === 0) return errorJson("argv must be a non-empty array", "invalid_request", 400);
|
|
1968
|
+
annotate(c, "exec.argv_count", body.argv.length);
|
|
1969
|
+
if (body.argv[0]) annotate(c, "exec.command", body.argv[0]);
|
|
1917
1970
|
const sandbox = getSandboxWithoutDefaultSession(getSandboxNs(c.env), c.get("containerUUID"));
|
|
1918
1971
|
const rawSessionId = c.req.header("Session-Id");
|
|
1919
1972
|
let executor = sandbox;
|
|
1920
1973
|
if (rawSessionId) {
|
|
1921
1974
|
const sessionId = validateSessionId(rawSessionId);
|
|
1922
1975
|
if (!sessionId) return errorJson("Invalid session ID format", "invalid_request", 400);
|
|
1976
|
+
annotate(c, "session.id", sessionId);
|
|
1923
1977
|
executor = await sandbox.getSession(sessionId);
|
|
1924
1978
|
}
|
|
1925
1979
|
const command = body.argv.map(shellQuote).join(" ");
|
|
1926
1980
|
const opts = {};
|
|
1927
|
-
if (typeof body.timeout_ms === "number")
|
|
1981
|
+
if (typeof body.timeout_ms === "number") {
|
|
1982
|
+
opts.timeout = body.timeout_ms;
|
|
1983
|
+
annotate(c, "exec.timeout_ms", body.timeout_ms);
|
|
1984
|
+
}
|
|
1928
1985
|
if (typeof body.cwd === "string") {
|
|
1929
1986
|
const resolvedCwd = resolveWorkspacePath(body.cwd);
|
|
1930
1987
|
if (!resolvedCwd) return errorJson("cwd must resolve to a location within /workspace", "invalid_request", 403);
|
|
1931
1988
|
opts.cwd = resolvedCwd;
|
|
1989
|
+
annotate(c, "exec.cwd", resolvedCwd);
|
|
1932
1990
|
}
|
|
1933
1991
|
const { readable, writable } = new TransformStream();
|
|
1934
1992
|
const writer = writable.getWriter();
|
|
@@ -1978,8 +2036,8 @@ function createBridgeApp(config) {
|
|
|
1978
2036
|
"Cache-Control": "no-cache"
|
|
1979
2037
|
}
|
|
1980
2038
|
});
|
|
1981
|
-
});
|
|
1982
|
-
app.get(`${apiPrefix}/sandbox/:id/file/*`, async (c) => {
|
|
2039
|
+
}));
|
|
2040
|
+
app.get(`${apiPrefix}/sandbox/:id/file/*`, traced("read_file", async (c) => {
|
|
1983
2041
|
const sandboxId = c.req.param("id");
|
|
1984
2042
|
const fullPath = c.req.path;
|
|
1985
2043
|
const marker = `${apiPrefix}/sandbox/${sandboxId}/file/`;
|
|
@@ -1987,12 +2045,14 @@ function createBridgeApp(config) {
|
|
|
1987
2045
|
if (!relativePath) return errorJson("file path must not be empty", "invalid_request", 400);
|
|
1988
2046
|
const resolvedPath = resolveWorkspacePath(`/${relativePath}`);
|
|
1989
2047
|
if (!resolvedPath) return errorJson("path must resolve to a location within /workspace", "invalid_request", 403);
|
|
2048
|
+
annotate(c, "file.path", resolvedPath);
|
|
1990
2049
|
const sandbox = getSandboxWithoutDefaultSession(getSandboxNs(c.env), c.get("containerUUID"));
|
|
1991
2050
|
const rawSessionId = c.req.header("Session-Id");
|
|
1992
2051
|
let executor = sandbox;
|
|
1993
2052
|
if (rawSessionId) {
|
|
1994
2053
|
const sessionId = validateSessionId(rawSessionId);
|
|
1995
2054
|
if (!sessionId) return errorJson("Invalid session ID format", "invalid_request", 400);
|
|
2055
|
+
annotate(c, "session.id", sessionId);
|
|
1996
2056
|
executor = await sandbox.getSession(sessionId);
|
|
1997
2057
|
}
|
|
1998
2058
|
try {
|
|
@@ -2005,8 +2065,8 @@ function createBridgeApp(config) {
|
|
|
2005
2065
|
if (err.code === "FILE_NOT_FOUND") return errorJson(`File not found: ${resolvedPath}`, "workspace_read_not_found", 404);
|
|
2006
2066
|
return errorJson(`read failed: ${err instanceof Error ? err.message : String(err)}`, "exec_transport_error", 502);
|
|
2007
2067
|
}
|
|
2008
|
-
});
|
|
2009
|
-
app.put(`${apiPrefix}/sandbox/:id/file/*`, async (c) => {
|
|
2068
|
+
}));
|
|
2069
|
+
app.put(`${apiPrefix}/sandbox/:id/file/*`, traced("write_file", async (c) => {
|
|
2010
2070
|
const sandboxId = c.req.param("id");
|
|
2011
2071
|
const fullPath = c.req.path;
|
|
2012
2072
|
const marker = `${apiPrefix}/sandbox/${sandboxId}/file/`;
|
|
@@ -2014,16 +2074,19 @@ function createBridgeApp(config) {
|
|
|
2014
2074
|
if (!relativePath) return errorJson("file path must not be empty", "invalid_request", 400);
|
|
2015
2075
|
const resolvedPath = resolveWorkspacePath(`/${relativePath}`);
|
|
2016
2076
|
if (!resolvedPath) return errorJson("path must resolve to a location within /workspace", "invalid_request", 403);
|
|
2077
|
+
annotate(c, "file.path", resolvedPath);
|
|
2017
2078
|
const sandbox = getSandboxWithoutDefaultSession(getSandboxNs(c.env), c.get("containerUUID"));
|
|
2018
2079
|
const rawSessionId = c.req.header("Session-Id");
|
|
2019
2080
|
let executor = sandbox;
|
|
2020
2081
|
if (rawSessionId) {
|
|
2021
2082
|
const sessionId = validateSessionId(rawSessionId);
|
|
2022
2083
|
if (!sessionId) return errorJson("Invalid session ID format", "invalid_request", 400);
|
|
2084
|
+
annotate(c, "session.id", sessionId);
|
|
2023
2085
|
executor = await sandbox.getSession(sessionId);
|
|
2024
2086
|
}
|
|
2025
2087
|
try {
|
|
2026
2088
|
const buffer = await c.req.arrayBuffer();
|
|
2089
|
+
annotate(c, "file.size_bytes", buffer.byteLength);
|
|
2027
2090
|
const MAX_WRITE_BYTES = 32 * 1024 * 1024;
|
|
2028
2091
|
if (buffer.byteLength > MAX_WRITE_BYTES) return errorJson(`payload too large: ${buffer.byteLength} bytes exceeds the ${MAX_WRITE_BYTES}-byte limit`, "payload_too_large", 413);
|
|
2029
2092
|
const bytes = new Uint8Array(buffer);
|
|
@@ -2035,12 +2098,14 @@ function createBridgeApp(config) {
|
|
|
2035
2098
|
} catch (err) {
|
|
2036
2099
|
return errorJson(`write failed: ${err instanceof Error ? err.message : String(err)}`, "workspace_archive_write_error", 502);
|
|
2037
2100
|
}
|
|
2038
|
-
});
|
|
2039
|
-
app.post(`${apiPrefix}/sandbox/:id/tunnel/:port`, async (c) => {
|
|
2101
|
+
}));
|
|
2102
|
+
app.post(`${apiPrefix}/sandbox/:id/tunnel/:port`, traced("tunnel_create", async (c) => {
|
|
2040
2103
|
const port = Number(c.req.param("port"));
|
|
2041
2104
|
if (!validatePort(port)) return errorJson("Invalid port", "invalid_request", 400);
|
|
2105
|
+
annotate(c, "tunnel.port", port);
|
|
2042
2106
|
const options = parseTunnelOptions(await c.req.text());
|
|
2043
2107
|
if (options instanceof Response) return options;
|
|
2108
|
+
if (options?.name) annotate(c, "tunnel.name", options.name);
|
|
2044
2109
|
const sandbox = getSandboxWithoutDefaultSession(getSandboxNs(c.env), c.get("containerUUID"));
|
|
2045
2110
|
try {
|
|
2046
2111
|
const tunnel = await sandbox.tunnels.get(port, options);
|
|
@@ -2048,10 +2113,11 @@ function createBridgeApp(config) {
|
|
|
2048
2113
|
} catch (err) {
|
|
2049
2114
|
return errorJson(`tunnel failed: ${err instanceof Error ? err.message : String(err)}`, "tunnel_error", 502);
|
|
2050
2115
|
}
|
|
2051
|
-
});
|
|
2052
|
-
app.delete(`${apiPrefix}/sandbox/:id/tunnel/:port`, async (c) => {
|
|
2116
|
+
}));
|
|
2117
|
+
app.delete(`${apiPrefix}/sandbox/:id/tunnel/:port`, traced("tunnel_destroy", async (c) => {
|
|
2053
2118
|
const port = Number(c.req.param("port"));
|
|
2054
2119
|
if (!validatePort(port)) return errorJson("Invalid port", "invalid_request", 400);
|
|
2120
|
+
annotate(c, "tunnel.port", port);
|
|
2055
2121
|
const sandbox = getSandboxWithoutDefaultSession(getSandboxNs(c.env), c.get("containerUUID"));
|
|
2056
2122
|
try {
|
|
2057
2123
|
await sandbox.tunnels.destroy(port);
|
|
@@ -2059,17 +2125,19 @@ function createBridgeApp(config) {
|
|
|
2059
2125
|
} catch (err) {
|
|
2060
2126
|
return errorJson(`tunnel failed: ${err instanceof Error ? err.message : String(err)}`, "tunnel_error", 502);
|
|
2061
2127
|
}
|
|
2062
|
-
});
|
|
2063
|
-
app.get(`${apiPrefix}/sandbox/:id/running`, async (c) => {
|
|
2128
|
+
}));
|
|
2129
|
+
app.get(`${apiPrefix}/sandbox/:id/running`, traced("running", async (c) => {
|
|
2064
2130
|
const sandbox = getSandboxWithoutDefaultSession(getSandboxNs(c.env), c.get("containerUUID"));
|
|
2065
2131
|
try {
|
|
2066
2132
|
await sandbox.exec("true");
|
|
2133
|
+
annotate(c, "sandbox.running", true);
|
|
2067
2134
|
return c.json({ running: true });
|
|
2068
2135
|
} catch {
|
|
2136
|
+
annotate(c, "sandbox.running", false);
|
|
2069
2137
|
return c.json({ running: false });
|
|
2070
2138
|
}
|
|
2071
|
-
});
|
|
2072
|
-
app.get(`${apiPrefix}/sandbox/:id/pty`, async (c) => {
|
|
2139
|
+
}));
|
|
2140
|
+
app.get(`${apiPrefix}/sandbox/:id/pty`, traced("pty", async (c) => {
|
|
2073
2141
|
const upgrade = c.req.header("Upgrade");
|
|
2074
2142
|
if (!upgrade || upgrade.toLowerCase() !== "websocket") return errorJson("WebSocket upgrade required", "invalid_request", 400);
|
|
2075
2143
|
const sandbox = getSandboxWithoutDefaultSession(getSandboxNs(c.env), c.get("containerUUID"));
|
|
@@ -2080,26 +2148,33 @@ function createBridgeApp(config) {
|
|
|
2080
2148
|
const cols = colsParam ? Number(colsParam) : 80;
|
|
2081
2149
|
const rows = rowsParam ? Number(rowsParam) : 24;
|
|
2082
2150
|
if (Number.isNaN(cols) || Number.isNaN(rows)) return errorJson("cols and rows must be valid numbers", "invalid_request", 400);
|
|
2151
|
+
annotate(c, "pty.cols", cols);
|
|
2152
|
+
annotate(c, "pty.rows", rows);
|
|
2083
2153
|
const opts = {
|
|
2084
2154
|
cols,
|
|
2085
2155
|
rows
|
|
2086
2156
|
};
|
|
2087
|
-
if (shell)
|
|
2157
|
+
if (shell) {
|
|
2158
|
+
opts.shell = shell;
|
|
2159
|
+
annotate(c, "pty.shell", shell);
|
|
2160
|
+
}
|
|
2088
2161
|
try {
|
|
2089
2162
|
if (sessionId) {
|
|
2090
2163
|
const validatedId = validateSessionId(sessionId);
|
|
2091
2164
|
if (!validatedId) return errorJson("Invalid session ID format", "invalid_request", 400);
|
|
2165
|
+
annotate(c, "session.id", validatedId);
|
|
2092
2166
|
return await (await sandbox.getSession(validatedId)).terminal(c.req.raw, opts);
|
|
2093
2167
|
}
|
|
2094
2168
|
return await sandbox.terminal(c.req.raw, opts);
|
|
2095
2169
|
} catch (err) {
|
|
2096
2170
|
return errorJson(`terminal failed: ${err instanceof Error ? err.message : String(err)}`, "exec_transport_error", 502);
|
|
2097
2171
|
}
|
|
2098
|
-
});
|
|
2099
|
-
app.post(`${apiPrefix}/sandbox/:id/persist`, async (c) => {
|
|
2172
|
+
}));
|
|
2173
|
+
app.post(`${apiPrefix}/sandbox/:id/persist`, traced("persist", async (c) => {
|
|
2100
2174
|
const root = "/workspace";
|
|
2101
2175
|
const excludesParam = c.req.query("excludes") ?? "";
|
|
2102
2176
|
const excludes = excludesParam ? excludesParam.split(",").filter((s) => s.length > 0) : [];
|
|
2177
|
+
annotate(c, "persist.exclude_count", excludes.length);
|
|
2103
2178
|
for (const ex of excludes) if (ex.includes("..")) return errorJson("exclude paths must not contain \"..\"", "invalid_request", 400);
|
|
2104
2179
|
const sandbox = getSandboxWithoutDefaultSession(getSandboxNs(c.env), c.get("containerUUID"));
|
|
2105
2180
|
const tmpPath = `/tmp/sandbox-persist-${Date.now()}.tar`;
|
|
@@ -2117,8 +2192,8 @@ function createBridgeApp(config) {
|
|
|
2117
2192
|
} catch (err) {
|
|
2118
2193
|
return errorJson(`persist failed: ${err instanceof Error ? err.message : String(err)}`, "workspace_archive_read_error", 502);
|
|
2119
2194
|
}
|
|
2120
|
-
});
|
|
2121
|
-
app.post(`${apiPrefix}/sandbox/:id/hydrate`, async (c) => {
|
|
2195
|
+
}));
|
|
2196
|
+
app.post(`${apiPrefix}/sandbox/:id/hydrate`, traced("hydrate", async (c) => {
|
|
2122
2197
|
const root = "/workspace";
|
|
2123
2198
|
const sandbox = getSandboxWithoutDefaultSession(getSandboxNs(c.env), c.get("containerUUID"));
|
|
2124
2199
|
let tarBytes;
|
|
@@ -2128,6 +2203,7 @@ function createBridgeApp(config) {
|
|
|
2128
2203
|
} catch {
|
|
2129
2204
|
return errorJson("Could not read request body", "invalid_request", 400);
|
|
2130
2205
|
}
|
|
2206
|
+
annotate(c, "hydrate.size_bytes", tarBytes.byteLength);
|
|
2131
2207
|
if (tarBytes.byteLength === 0) return errorJson("Empty tar payload", "invalid_request", 400);
|
|
2132
2208
|
const MAX_HYDRATE_BYTES = 32 * 1024 * 1024;
|
|
2133
2209
|
if (tarBytes.byteLength > MAX_HYDRATE_BYTES) return errorJson(`tar payload too large: ${tarBytes.byteLength} bytes exceeds the ${MAX_HYDRATE_BYTES}-byte limit`, "invalid_request", 400);
|
|
@@ -2144,8 +2220,8 @@ function createBridgeApp(config) {
|
|
|
2144
2220
|
} catch (err) {
|
|
2145
2221
|
return errorJson(`hydrate failed: ${err instanceof Error ? err.message : String(err)}`, "workspace_archive_write_error", 502);
|
|
2146
2222
|
}
|
|
2147
|
-
});
|
|
2148
|
-
app.post(`${apiPrefix}/sandbox/:id/mount`, async (c) => {
|
|
2223
|
+
}));
|
|
2224
|
+
app.post(`${apiPrefix}/sandbox/:id/mount`, traced("mount", async (c) => {
|
|
2149
2225
|
let body;
|
|
2150
2226
|
try {
|
|
2151
2227
|
body = await c.req.json();
|
|
@@ -2160,6 +2236,8 @@ function createBridgeApp(config) {
|
|
|
2160
2236
|
if (optionsError) return optionsError;
|
|
2161
2237
|
const bucketName = resolveMountBucketName(body);
|
|
2162
2238
|
if (bucketName instanceof Response) return bucketName;
|
|
2239
|
+
annotate(c, "mount.bucket", bucketName);
|
|
2240
|
+
annotate(c, "mount.path", body.mountPath);
|
|
2163
2241
|
const sandbox = getSandboxWithoutDefaultSession(getSandboxNs(c.env), c.get("containerUUID"));
|
|
2164
2242
|
const sdkOptions = toSDKMountOptions(body.options);
|
|
2165
2243
|
try {
|
|
@@ -2168,8 +2246,8 @@ function createBridgeApp(config) {
|
|
|
2168
2246
|
} catch (err) {
|
|
2169
2247
|
return errorJson(`mount failed: ${err instanceof Error ? err.message : String(err)}`, "mount_error", 502);
|
|
2170
2248
|
}
|
|
2171
|
-
});
|
|
2172
|
-
app.post(`${apiPrefix}/sandbox/:id/unmount`, async (c) => {
|
|
2249
|
+
}));
|
|
2250
|
+
app.post(`${apiPrefix}/sandbox/:id/unmount`, traced("unmount", async (c) => {
|
|
2173
2251
|
let body;
|
|
2174
2252
|
try {
|
|
2175
2253
|
body = await c.req.json();
|
|
@@ -2180,6 +2258,7 @@ function createBridgeApp(config) {
|
|
|
2180
2258
|
if (!body.mountPath.startsWith("/")) return errorJson("mountPath must be an absolute path (start with /)", "invalid_request", 400);
|
|
2181
2259
|
const normalizedPath = new URL(body.mountPath, "file:///").pathname;
|
|
2182
2260
|
if (normalizedPath === "/") return errorJson("mountPath must not resolve to / (filesystem root)", "invalid_request", 400);
|
|
2261
|
+
annotate(c, "mount.path", normalizedPath);
|
|
2183
2262
|
const sandbox = getSandboxWithoutDefaultSession(getSandboxNs(c.env), c.get("containerUUID"));
|
|
2184
2263
|
try {
|
|
2185
2264
|
await sandbox.unmountBucket(normalizedPath);
|
|
@@ -2191,31 +2270,35 @@ function createBridgeApp(config) {
|
|
|
2191
2270
|
} catch (err) {
|
|
2192
2271
|
return errorJson(`unmount failed: ${err instanceof Error ? err.message : String(err)}`, "unmount_error", 502);
|
|
2193
2272
|
}
|
|
2194
|
-
});
|
|
2195
|
-
app.post(`${apiPrefix}/sandbox/:id/session`, async (c) => {
|
|
2273
|
+
}));
|
|
2274
|
+
app.post(`${apiPrefix}/sandbox/:id/session`, traced("session_create", async (c) => {
|
|
2196
2275
|
const sandbox = getSandbox$1(getSandboxNs(c.env), c.get("containerUUID"));
|
|
2197
2276
|
let body = {};
|
|
2198
2277
|
try {
|
|
2199
2278
|
body = await c.req.json();
|
|
2200
2279
|
} catch {}
|
|
2280
|
+
if (body.cwd) annotate(c, "session.cwd", body.cwd);
|
|
2201
2281
|
try {
|
|
2202
2282
|
const session = await sandbox.createSession(body);
|
|
2283
|
+
annotate(c, "session.id", session.id);
|
|
2203
2284
|
return c.json({ id: session.id });
|
|
2204
2285
|
} catch (err) {
|
|
2205
2286
|
return errorJson(`session create failed: ${err instanceof Error ? err.message : String(err)}`, "session_error", 502);
|
|
2206
2287
|
}
|
|
2207
|
-
});
|
|
2208
|
-
app.delete(`${apiPrefix}/sandbox/:id/session/:sid`, async (c) => {
|
|
2288
|
+
}));
|
|
2289
|
+
app.delete(`${apiPrefix}/sandbox/:id/session/:sid`, traced("session_delete", async (c) => {
|
|
2209
2290
|
const sandbox = getSandbox$1(getSandboxNs(c.env), c.get("containerUUID"));
|
|
2210
2291
|
const sid = c.req.param("sid");
|
|
2292
|
+
if (!sid) return errorJson("session ID must not be empty", "invalid_request", 400);
|
|
2293
|
+
annotate(c, "session.id", sid);
|
|
2211
2294
|
try {
|
|
2212
2295
|
const result = await sandbox.deleteSession(sid);
|
|
2213
2296
|
return c.json(result);
|
|
2214
2297
|
} catch (err) {
|
|
2215
2298
|
return errorJson(`session delete failed: ${err instanceof Error ? err.message : String(err)}`, "session_error", 502);
|
|
2216
2299
|
}
|
|
2217
|
-
});
|
|
2218
|
-
app.delete(`${apiPrefix}/sandbox/:id`, async (c) => {
|
|
2300
|
+
}));
|
|
2301
|
+
app.delete(`${apiPrefix}/sandbox/:id`, traced("destroy_sandbox", async (c) => {
|
|
2219
2302
|
const containerUUID = c.get("containerUUID");
|
|
2220
2303
|
const sandbox = getSandboxWithoutDefaultSession(getSandboxNs(c.env), containerUUID);
|
|
2221
2304
|
try {
|
|
@@ -2227,7 +2310,7 @@ function createBridgeApp(config) {
|
|
|
2227
2310
|
await poolNs.get(poolId).reportStopped(containerUUID);
|
|
2228
2311
|
} catch {}
|
|
2229
2312
|
return new Response(null, { status: 204 });
|
|
2230
|
-
});
|
|
2313
|
+
}));
|
|
2231
2314
|
app.get(config.healthPath, (c) => {
|
|
2232
2315
|
const errors = [];
|
|
2233
2316
|
if (!c.env[sandboxBinding]) errors.push(`Missing required Durable Object binding "${sandboxBinding}". Ensure your wrangler.jsonc has a binding named "${sandboxBinding}".`);
|
|
@@ -2246,40 +2329,34 @@ function createBridgeApp(config) {
|
|
|
2246
2329
|
}
|
|
2247
2330
|
return next();
|
|
2248
2331
|
});
|
|
2249
|
-
app.get(`${apiPrefix}/pool/stats`, async (c) => {
|
|
2250
|
-
const warmTarget = Number.parseInt(c.env.WARM_POOL_TARGET || "0", 10) || 0;
|
|
2251
|
-
const refreshInterval = Number.parseInt(c.env.WARM_POOL_REFRESH_INTERVAL || "10000", 10) || 1e4;
|
|
2332
|
+
app.get(`${apiPrefix}/pool/stats`, traced("pool_stats", async (c) => {
|
|
2252
2333
|
const poolNs = getWarmPoolNs(c.env);
|
|
2253
2334
|
const poolId = poolNs.idFromName("global-pool");
|
|
2254
2335
|
const poolStub = poolNs.get(poolId);
|
|
2255
2336
|
try {
|
|
2256
|
-
await poolStub.configure(
|
|
2257
|
-
warmTarget,
|
|
2258
|
-
refreshInterval
|
|
2259
|
-
});
|
|
2337
|
+
await poolStub.configure(parsePoolConfig(c.env));
|
|
2260
2338
|
} catch {}
|
|
2261
2339
|
const stats = await poolStub.getStats();
|
|
2340
|
+
annotate(c, "pool.warm", stats.warm);
|
|
2341
|
+
annotate(c, "pool.assigned", stats.assigned);
|
|
2342
|
+
annotate(c, "pool.total", stats.total);
|
|
2343
|
+
if (stats.maxInstances !== null) annotate(c, "pool.max_instances", stats.maxInstances);
|
|
2262
2344
|
return c.json(stats);
|
|
2263
|
-
});
|
|
2264
|
-
app.post(`${apiPrefix}/pool/shutdown-prewarmed`, async (c) => {
|
|
2265
|
-
const warmTarget = Number.parseInt(c.env.WARM_POOL_TARGET || "0", 10) || 0;
|
|
2266
|
-
const refreshInterval = Number.parseInt(c.env.WARM_POOL_REFRESH_INTERVAL || "10000", 10) || 1e4;
|
|
2345
|
+
}));
|
|
2346
|
+
app.post(`${apiPrefix}/pool/shutdown-prewarmed`, traced("pool_shutdown_prewarmed", async (c) => {
|
|
2267
2347
|
const poolNs = getWarmPoolNs(c.env);
|
|
2268
2348
|
const poolId = poolNs.idFromName("global-pool");
|
|
2269
2349
|
const poolStub = poolNs.get(poolId);
|
|
2270
2350
|
try {
|
|
2271
|
-
await poolStub.configure(
|
|
2272
|
-
warmTarget,
|
|
2273
|
-
refreshInterval
|
|
2274
|
-
});
|
|
2351
|
+
await poolStub.configure(parsePoolConfig(c.env));
|
|
2275
2352
|
} catch {}
|
|
2276
2353
|
await poolStub.shutdownPrewarmed();
|
|
2277
2354
|
return c.json({ ok: true });
|
|
2278
|
-
});
|
|
2279
|
-
app.post(`${apiPrefix}/pool/prime`, async (c) => {
|
|
2355
|
+
}));
|
|
2356
|
+
app.post(`${apiPrefix}/pool/prime`, traced("pool_prime", async (c) => {
|
|
2280
2357
|
await primePool(c.env, warmPoolBinding);
|
|
2281
2358
|
return c.json({ ok: true });
|
|
2282
|
-
});
|
|
2359
|
+
}));
|
|
2283
2360
|
const openapiAuth = async (c, next) => {
|
|
2284
2361
|
const token = c.env.SANDBOX_API_KEY;
|
|
2285
2362
|
if (token) {
|
|
@@ -2312,9 +2389,14 @@ function createBridgeApp(config) {
|
|
|
2312
2389
|
* Configuration is pushed in via `configure()` on every request (idempotent)
|
|
2313
2390
|
* so changes to wrangler vars take effect without manual intervention.
|
|
2314
2391
|
*/
|
|
2392
|
+
const DEFAULT_BATCH_SIZE = 5;
|
|
2393
|
+
/** Upper bound on parallel starts per batch to avoid a cold-start stampede. */
|
|
2394
|
+
const MAX_BATCH_SIZE = 20;
|
|
2315
2395
|
const DEFAULT_CONFIG = {
|
|
2316
2396
|
warmTarget: 0,
|
|
2317
|
-
refreshInterval: 1e4
|
|
2397
|
+
refreshInterval: 1e4,
|
|
2398
|
+
maxInstances: 0,
|
|
2399
|
+
scaleBatchSize: DEFAULT_BATCH_SIZE
|
|
2318
2400
|
};
|
|
2319
2401
|
var WarmPool = class extends DurableObject {
|
|
2320
2402
|
config = { ...DEFAULT_CONFIG };
|
|
@@ -2328,6 +2410,13 @@ var WarmPool = class extends DurableObject {
|
|
|
2328
2410
|
knownMaxInstances = null;
|
|
2329
2411
|
capacityExhausted = false;
|
|
2330
2412
|
initialized = false;
|
|
2413
|
+
/** Guards against overlapping eager refills triggered by concurrent pops. */
|
|
2414
|
+
refillInFlight = false;
|
|
2415
|
+
/**
|
|
2416
|
+
* The currently in-flight eager refill, or null. Exposed for tests to await
|
|
2417
|
+
* the otherwise fire-and-forget refill deterministically.
|
|
2418
|
+
*/
|
|
2419
|
+
refillPromise = null;
|
|
2331
2420
|
/**
|
|
2332
2421
|
* Get a container UUID for the given sandbox ID.
|
|
2333
2422
|
* - If this ID already has an assigned container that's still running, return it.
|
|
@@ -2346,6 +2435,7 @@ var WarmPool = class extends DurableObject {
|
|
|
2346
2435
|
this.warmContainers.delete(containerUUID$1);
|
|
2347
2436
|
this.assignments.set(sandboxId, containerUUID$1);
|
|
2348
2437
|
await this.persist();
|
|
2438
|
+
this.requestRefill();
|
|
2349
2439
|
return containerUUID$1;
|
|
2350
2440
|
}
|
|
2351
2441
|
if (this.remainingCapacity() <= 0) this.throwCapacityError();
|
|
@@ -2353,6 +2443,7 @@ var WarmPool = class extends DurableObject {
|
|
|
2353
2443
|
if (containerUUID) {
|
|
2354
2444
|
this.assignments.set(sandboxId, containerUUID);
|
|
2355
2445
|
await this.persist();
|
|
2446
|
+
this.requestRefill();
|
|
2356
2447
|
return containerUUID;
|
|
2357
2448
|
}
|
|
2358
2449
|
if (this.capacityExhausted) this.throwCapacityError();
|
|
@@ -2400,6 +2491,11 @@ var WarmPool = class extends DurableObject {
|
|
|
2400
2491
|
...DEFAULT_CONFIG,
|
|
2401
2492
|
...config
|
|
2402
2493
|
};
|
|
2494
|
+
this.config.scaleBatchSize = Math.min(MAX_BATCH_SIZE, Math.max(1, this.config.scaleBatchSize || DEFAULT_BATCH_SIZE));
|
|
2495
|
+
if (this.config.maxInstances > 0) {
|
|
2496
|
+
this.knownMaxInstances = this.knownMaxInstances === null ? this.config.maxInstances : Math.min(this.knownMaxInstances, this.config.maxInstances);
|
|
2497
|
+
await this.ctx.storage.put("knownMaxInstances", this.knownMaxInstances);
|
|
2498
|
+
}
|
|
2403
2499
|
await this.ctx.storage.put("config", this.config);
|
|
2404
2500
|
}
|
|
2405
2501
|
/**
|
|
@@ -2421,6 +2517,32 @@ var WarmPool = class extends DurableObject {
|
|
|
2421
2517
|
}
|
|
2422
2518
|
await this.persist();
|
|
2423
2519
|
}
|
|
2520
|
+
/**
|
|
2521
|
+
* Kick a non-blocking refill toward warmTarget. Debounced via refillInFlight
|
|
2522
|
+
* so a burst of concurrent pops triggers at most one adjustPool sweep at a
|
|
2523
|
+
* time; capacity is respected because adjustPool clamps to remainingCapacity.
|
|
2524
|
+
* Errors are swallowed so a failed refill never rejects the triggering call.
|
|
2525
|
+
*/
|
|
2526
|
+
requestRefill() {
|
|
2527
|
+
if (this.refillInFlight) return;
|
|
2528
|
+
if (this.warmContainers.size >= this.config.warmTarget) return;
|
|
2529
|
+
if (this.remainingCapacity() <= 0) return;
|
|
2530
|
+
this.refillInFlight = true;
|
|
2531
|
+
this.refillPromise = (async () => {
|
|
2532
|
+
try {
|
|
2533
|
+
await this.adjustPool();
|
|
2534
|
+
} catch (error) {
|
|
2535
|
+
console.error({
|
|
2536
|
+
message: "Eager refill failed",
|
|
2537
|
+
component: "warm-pool",
|
|
2538
|
+
error
|
|
2539
|
+
});
|
|
2540
|
+
} finally {
|
|
2541
|
+
this.refillInFlight = false;
|
|
2542
|
+
}
|
|
2543
|
+
})();
|
|
2544
|
+
this.ctx.waitUntil(this.refillPromise);
|
|
2545
|
+
}
|
|
2424
2546
|
async alarm() {
|
|
2425
2547
|
await this.init();
|
|
2426
2548
|
this.capacityExhausted = false;
|
|
@@ -2569,25 +2691,22 @@ var WarmPool = class extends DurableObject {
|
|
|
2569
2691
|
});
|
|
2570
2692
|
return;
|
|
2571
2693
|
}
|
|
2694
|
+
const batchSize = this.config.scaleBatchSize;
|
|
2572
2695
|
console.info({
|
|
2573
2696
|
message: "Scaling up pool",
|
|
2574
2697
|
component: "warm-pool",
|
|
2575
2698
|
starting: toStart,
|
|
2576
2699
|
needed: diff,
|
|
2577
|
-
capacity: this.remainingCapacity()
|
|
2700
|
+
capacity: this.remainingCapacity(),
|
|
2701
|
+
batchSize
|
|
2578
2702
|
});
|
|
2579
|
-
for (let i = 0; i < toStart; i
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
break;
|
|
2586
|
-
}
|
|
2587
|
-
const uuid = await this.startContainer();
|
|
2588
|
-
if (uuid) this.warmContainers.add(uuid);
|
|
2703
|
+
for (let i = 0; i < toStart && !this.capacityExhausted; i += batchSize) {
|
|
2704
|
+
const n = Math.min(batchSize, toStart - i);
|
|
2705
|
+
const results = await Promise.all(Array.from({ length: n }, () => this.startContainer()));
|
|
2706
|
+
for (const uuid of results) if (uuid) this.warmContainers.add(uuid);
|
|
2707
|
+
if (this.capacityExhausted) this.knownMaxInstances = this.warmContainers.size + this.assignments.size;
|
|
2708
|
+
await this.persist();
|
|
2589
2709
|
}
|
|
2590
|
-
await this.persist();
|
|
2591
2710
|
} else if (diff < 0) {
|
|
2592
2711
|
const excess = -diff;
|
|
2593
2712
|
console.info({
|