diff --git a/cloud/apps/sandbox-control/src/retention.ts b/cloud/apps/sandbox-control/src/retention.ts index 68841ad2bf3..6b7342096a2 100644 --- a/cloud/apps/sandbox-control/src/retention.ts +++ b/cloud/apps/sandbox-control/src/retention.ts @@ -240,6 +240,10 @@ async function clearOrphan(db: D1Database, kind: string, id: string) { await db.prepare('DELETE FROM provider_orphans WHERE kind = ? AND provider_id = ? AND deleted_at IS NULL').bind(kind, id).run() } +async function isPooledVm(db: D1Database, providerName: string): Promise { + return (await db.prepare('SELECT 1 AS hit FROM sandbox_pool WHERE provider_name = ?').bind(providerName).first<{ hit: number }>()) !== null +} + async function sweepOrphans(deps: LifecycleDeps, summary: HousekeepingSummary): Promise { const now = deps.now() const grace = retention(deps).orphanGraceMs @@ -251,6 +255,11 @@ async function sweepOrphans(deps: LifecycleDeps, summary: HousekeepingSummary): if (box.tags.orcaEnv !== env || !box.tags.orcaSandbox) { continue } + // A warm-pool VM has no sandboxes row until it is claimed; its record is the pool row. + if (box.tags.orcaPool && (await isPooledVm(deps.db, box.name))) { + await clearOrphan(deps.db, 'sandbox', box.name) + continue + } const row = await getSandboxByProviderName(deps.db, box.name) if (row && row.status !== 'destroyed') { await clearOrphan(deps.db, 'sandbox', box.name) diff --git a/cloud/apps/sandbox-control/test/retention.test.ts b/cloud/apps/sandbox-control/test/retention.test.ts index 1120711eafe..9ffcf6ec923 100644 --- a/cloud/apps/sandbox-control/test/retention.test.ts +++ b/cloud/apps/sandbox-control/test/retention.test.ts @@ -30,6 +30,30 @@ async function setup() { } describe('orphan sweep', () => { + it('never stops a warm-pool VM: its record is the pool row, not a sandboxes row', async () => { + const { h, deps, sweep } = await setup() + const name = 'orca-test-sbx_pooled' + await h.vercel.createSandbox({ + name, + snapshotId: GOLDEN, + vcpus: 4, + region: 'iad1', + timeoutMs: 30 * 60_000, + ports: [], + networkPolicy: { allow: {} }, + tags: { orcaSandbox: 'sbx_pooled', orcaEnv: 'test', orcaPool: '1' }, + snapshotExpirationMs: 0 + }) + await deps.db + .prepare( + `INSERT INTO sandbox_pool (id, provider_name, machine_size, region, snapshot_id, status, created_at, updated_at) VALUES ('sbx_pooled', ?, 'standard', 'iad1', ?, 'ready', 1, 1)` + ) + .bind(name, GOLDEN) + .run() + expect((await sweep()).orphanSandboxesSuspended).toBe(0) + expect(h.vercel.boxes.get(name)!.status).toBe('running') + }) + it('suspends a provider sandbox with no live D1 row, deletes it after the grace period, leaves tracked ones alone', async () => { const { h, sweep } = await setup() const tracked = await h.call('POST', '/v1/sandboxes', create('ws-1'))