Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions src/sandbox/render/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export type RenderTimings = {
verifyTimeoutMs: number;
/** Client-side abort lands this long after the in-sandbox `timeout`, which should fire first. */
investigationGraceMs: number;
/** Budget for terminate, retries included, so cleanup can't hold an answer that's already computed. */
cleanupTimeoutMs: number;
};

const DEFAULT_TIMINGS: RenderTimings = {
Expand All @@ -27,6 +29,7 @@ const DEFAULT_TIMINGS: RenderTimings = {
snapshotTimeoutMs: 20 * 60_000,
verifyTimeoutMs: 5 * 60_000,
investigationGraceMs: 30_000,
cleanupTimeoutMs: 10_000,
};

const OUTPUT_TAIL = 64 * 1024;
Expand Down Expand Up @@ -138,13 +141,23 @@ export class SandboxClient {
}
}

/** Idempotent: a sandbox that no longer exists counts as terminated. */
/**
* Idempotent: a sandbox that no longer exists counts as terminated. The SDK's terminate takes no signal, so
* the budget is a race; past it, the Sandbox's provider-side timeout is the backstop.
*/
async terminate(id: string): Promise<void> {
const signal = AbortSignal.timeout(this.t.cleanupTimeoutMs);
let timer: NodeJS.Timeout | undefined;
const budget = new Promise<never>((_, reject) => {
timer = setTimeout(() => reject(new Error(`terminate ${id} did not finish within ${this.t.cleanupTimeoutMs}ms`)), this.t.cleanupTimeoutMs);
});
try {
await this.call(() => this.api.terminate(id), { retryOn: TRANSIENT });
await Promise.race([this.call(() => this.api.terminate(id), { retryOn: TRANSIENT, signal }), budget]);
} catch (err) {
if (err instanceof ClientError && err.statusCode === 404) return;
throw err;
} finally {
clearTimeout(timer);
}
}

Expand Down
14 changes: 14 additions & 0 deletions test/sandbox-render.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,20 @@ describe("runInvestigation", () => {
});
});

describe("SandboxClient.terminate", () => {
it("gives up after its cleanup budget when the terminate request hangs", async () => {
const api = fake.api();
api.terminate = () => new Promise(() => {});
const original = client(undefined, api);
const bounded = new SandboxClient(api, original.cfg, original.log, { ...original.t, cleanupTimeoutMs: 20 });
const started = Date.now();

await expect(bounded.terminate("sbx-hung")).rejects.toThrow("did not finish within 20ms");
expect(Date.now() - started).toBeLessThan(1_000);
await expect(bounded.terminateQuietly("sbx-hung")).resolves.toBeUndefined();
});
});

describe("configureSandboxes", () => {
it("maps comma-separated domains and wildcards to HTTPS egress rules", async () => {
const cfg = await loadRenderConfig({
Expand Down
Loading