Skip to content

Commit a549393

Browse files
icecrasher321claude
andcommitted
fix(sandboxes): keep the row restore to a refused delete only
Cursor and Greptile, independently, on the same code. `deleteImage` and `rebuildIfReadopted` shared one try/catch, so a rebuild failure after a *successful* provider delete was handled as if the provider had refused: the catch put the claimed row back, `ready` status and all, pointing at a template that no longer exists. That is the one state resolution cannot repair — it fixes a row that is missing or failed, never one claiming to be ready — so it reintroduced the permanent breakage an earlier commit had just closed, through the error path rather than the happy one. Restoring now belongs strictly to a refused delete. Once the template is gone the row stays gone, and the rebuild runs past that catch. The rebuild also swallows its own failures: it follows a delete that already succeeded, so it must not be reported as a failed release, and inside the sweep it must not reject the rest of its chunk. The adopter's next run still reaches the normal repair path. The regression test drives a rebuild failure and asserts no row is restored. It fails against the original shape — rebuild inside the shared try, no inner catch — which is what the two reviewers were describing. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent edcfb40 commit a549393

2 files changed

Lines changed: 42 additions & 11 deletions

File tree

apps/sim/lib/execution/remote-sandbox/image-registry.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,23 @@ describe('releaseSandboxImage', () => {
226226
expect(enqueued()).toBe(false)
227227
})
228228

229+
/**
230+
* Restoring belongs to a refused delete and nothing else. Once the template is
231+
* gone, putting the row back would recreate a `ready` row pointing at nothing —
232+
* the one state resolution cannot repair.
233+
*/
234+
it('does not restore the row when the post-delete rebuild fails', async () => {
235+
stubClaim([READY_IMAGE])
236+
mockSelect.mockImplementation(() => {
237+
throw new Error('registry unreachable')
238+
})
239+
240+
await releaseSandboxImage('hash-1')
241+
242+
expect(mockDeleteImage).toHaveBeenCalledTimes(1)
243+
expect(mockInsert).not.toHaveBeenCalled()
244+
})
245+
229246
it('skips the provider when the claimed row never had an image', async () => {
230247
stubClaim([{ ...READY_IMAGE, imageRef: null }])
231248

apps/sim/lib/execution/remote-sandbox/image-registry.ts

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -379,15 +379,25 @@ async function rebuildIfReadopted(
379379
specHash: string,
380380
spec: SandboxSpec
381381
): Promise<void> {
382-
const [readopted] = await db
383-
.select({ id: sandboxImage.id })
384-
.from(sandboxImage)
385-
.where(and(eq(sandboxImage.provider, providerId), eq(sandboxImage.specHash, specHash)))
386-
.limit(1)
387-
if (!readopted) return
388-
389-
logger.warn('Sandbox image was re-adopted mid-delete; rebuilding it now', { specHash })
390-
await ensureSandboxImage(spec, specHash, { imageKnownGone: true })
382+
try {
383+
const [readopted] = await db
384+
.select({ id: sandboxImage.id })
385+
.from(sandboxImage)
386+
.where(and(eq(sandboxImage.provider, providerId), eq(sandboxImage.specHash, specHash)))
387+
.limit(1)
388+
if (!readopted) return
389+
390+
logger.warn('Sandbox image was re-adopted mid-delete; rebuilding it now', { specHash })
391+
await ensureSandboxImage(spec, specHash, { imageKnownGone: true })
392+
} catch (error) {
393+
// Opportunistic: the delete it follows has already succeeded, so failing here
394+
// must not surface as a failed release, and in the sweep must not reject the
395+
// rest of its chunk. The adopter's next run still reaches the repair path.
396+
logger.warn('Failed to rebuild a re-adopted sandbox image', {
397+
specHash,
398+
error: getErrorMessage(error),
399+
})
400+
}
391401
}
392402

393403
/**
@@ -444,8 +454,6 @@ async function claimAndDeleteImage(
444454
buildId: claimed.buildId ?? '',
445455
providerImageId: claimed.providerImageId ?? undefined,
446456
})
447-
await rebuildIfReadopted(providerId, specHash, claimed.spec as SandboxSpec)
448-
return 'released'
449457
} catch (error) {
450458
await db
451459
.insert(sandboxImage)
@@ -466,6 +474,12 @@ async function claimAndDeleteImage(
466474
})
467475
return 'failed'
468476
}
477+
478+
// Deliberately past the catch above. The template is gone for good by now, so a
479+
// failure here must not restore the row: that path puts back a `ready` row whose
480+
// imageRef points at nothing, which is the one state resolution cannot repair.
481+
await rebuildIfReadopted(providerId, specHash, claimed.spec as SandboxSpec)
482+
return 'released'
469483
}
470484

471485
/** Most rows one sweep will touch. The next run picks up whatever is left. */

0 commit comments

Comments
 (0)