Repository navigation
fix(providers): teardown terminates a sandbox by id, so one that already finished is counted down - #208
Merged
earakely-scale merged 3 commits intoOct 14, 2026
Conversation
…ad of reporting it up Teardown gets a handle with get_sandbox before it terminates. The Modal providers' get_sandbox read the sandbox's tunnels, which Modal refuses for a sandbox that has finished (killed, out of memory, past its timeout) with ConflictError, so teardown logged "was not terminated" and "may still be up" for a sandbox that was down. get_sandbox now returns a handle without tunnels when the tunnels call conflicts and poll() has an exit code. Modal's terminate() is a no-op on a finished sandbox, so teardown counts it as down. Modal raises ConflictError for other refusals too, so a conflict while poll() says running is raised as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main's unit job fails at its lint step: the SmolVM checks import the local_stack fixture with a noqa for F401 but take it as a parameter, which ruff reports as F811, and one test has an f-string without placeholders. The checks now import it as ``local_stack as local_stack``, the explicit re-export another test already uses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ady finished is counted down Teardown got a handle with get_sandbox before it terminated a sandbox. Modal's get_sandbox reads the sandbox's tunnels, which Modal refuses for a sandbox that has finished, so teardown reported a killed or timed-out sandbox as "may still be up". Making get_sandbox return a handle for a finished sandbox instead (this PR's first version) would have hidden dead containers from env reattach, which relies on get_sandbox raising. SandboxProvider.terminate_sandbox(sandbox_id) terminates a sandbox the provider made, by id; one that has already finished or is gone counts as terminated. The base default is get_sandbox then terminate. Every provider in the repo implements it without get_sandbox's extras: - modal, modal_vm: Sandbox.from_id then terminate (a no-op once finished), with NotFoundError counted as gone. - e2b: the class-level kill by id, which returns False for a sandbox already absent. - tenki: client.get then the session close TenkiSandbox.terminate does, now shared as close_session. - tensorlake: connect then terminate, skipping info(), with SandboxNotFoundError counted as gone. - sail_vm: through get_sandbox, since its terminate also deletes the model-key policy only the live Sailbox's applied policy names; NotFoundError counts as gone. - smol_vm: a machine Machine.list doesn't list is gone; a listed one is connected and deleted, skipping the Docker setup and host configuration get_sandbox runs. - local and chained: the base default. A chain's get_sandbox finds the owner; a per-provider terminate_sandbox loop would let the first provider count another provider's sandbox as gone. Both teardown paths call terminate_sandbox for a record that names its backend. A record that doesn't still goes through the slot provider's live handle, since "not found" from a provider that didn't make the sandbox doesn't mean it's gone. Local work-folder removal keys on the provider's ON_THIS_MACHINE. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
earakely-scale
force-pushed
the
edgararakelyan/fd-3946-modal-finished-sandbox
branch
from
October 14, 2026 22:06
b6277c4 to
ef41dd9
Compare
earakely-scale
deleted the
edgararakelyan/fd-3946-modal-finished-sandbox
branch
October 14, 2026 23:30
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A sandbox can finish before teardown reaches it: killed, out of memory, or past its timeout. On Modal, teardown then logged two warnings for a sandbox that was already down:
teardown: sb-… (modal) was not terminated: ConflictError: Sandbox has already finished with status terminatedepisode …: ['sb-…'] may still be up"may still be up" is how a real leak is reported, so a false one on every dead env hides the true ones.
Cause: both teardown paths (
teardown_runand theteardown_sandboxesstep) got a handle withget_sandboxbefore terminating. Modal'sget_sandboxreads the sandbox's tunnels, which Modal refuses for a finished sandbox.Why not fix
get_sandbox: this PR's first version hadget_sandboxreturn a handle for a finished sandbox. That would hide dead containers from env reattach, which relies onget_sandboxraising:_reattach_containersraises for a dead server and skips a dead database, and MultiEnv raises for a dead database.Fix: a new
SandboxProvider.terminate_sandbox(sandbox_id)terminates a sandbox the provider made, by id. A sandbox that has already finished or is gone counts as terminated. The base default isget_sandboxthenterminate().terminate_sandboxSandbox.from_idthenterminate(), with no tunnels and no execterminate()is a no-op once finished;NotFoundErrorcounts as gonekill(id), with no connectkillreturns Falseclient.getthen the session closeTenkiSandbox.terminatedoes, now shared asclose_sessionSessionNotFoundError, or an ended stateconnectthenterminate(), skippinginfo()SandboxNotFoundErrorget_sandbox, since itsterminate()also deletes the model-key policy that only the live Sailbox's applied policy namesNotFoundErrorMachine.list, then connect anddelete()for a listed machine, skipping the Docker setup and host configurationterminate()Two choices worth a look:
get_sandboxfinds the provider that owns the sandbox. Looping over each provider'sterminate_sandboxinstead would let the first provider count another provider's sandbox as "gone".terminate_sandboxis only called when the record names the backend that made the sandbox. A record without one, which no current deploy path writes, still goes through the slot provider'sget_sandbox. Local work-folder removal now keys on the provider'sON_THIS_MACHINE.Known limits:
Lint commit: a separate commit clears the six
rufffindings thatmain's unit job currently fails on. They're in SmolVM's checks: F811 on an imported fixture, and one placeholder-less f-string. The commit can be split out if preferred.Test plan
Unit tests for every provider:
get_sandbox's extras: no tunnels, noinfo(), no connect for E2B, no Docker setup for smol;get_sandboxstill raises for a finished sandbox, which is the reattach contract.Teardown tests:
teardown_runregression test through the real Modal provider, with only the SDK faked: a finished sandbox lands interminated, nothing is instill_up, and no tunnel is read.With
main's teardown restored, the three new teardown tests fail.Full unit suite: 8119 passed. The one failure is
smol_vm_provider_test::test_vm_host_entry_updates_without_dropping_other_guest_names, which fails onmaintoo on macOS because BSDseddiffers from GNU's. It's skipped in CI, where thesmolextra isn't installed, which also means this PR's new smol test only runs wheresmolmachinesis installed.ruff checkis clean.Real backends, from Linux VMs, with this commit installed:
teardown_runterminated, nothing failedterminated, nothing failedterminated, nothing failedget_sandboxon a killed sandbox still raisesConflictError.🤖 Generated with Claude Code


Confidence Score: 5/5
The PR appears safe to merge; no actionable new defect was established.Summary
Adds
SandboxProvider.terminate_sandboxand routes typed teardown records through it.No actionable new issue was established. No previous review threads were supplied.
Diagram
%%{init: {'theme': 'neutral'}}%% flowchart TD A[Recorded sandbox] --> B{Backend recorded?} B -->|Yes| C[Build recorded provider] C --> D[terminate_sandbox] D --> E[Backend-specific termination] B -->|No| F[Use configured provider] F --> G[get_sandbox] G --> H[Handle terminate] E --> I[Record successful teardown] H --> I E -->|Error| J[Report possible leftover] H -->|Error| JReviews (2) · Last reviewed commit: "fix(providers): teardown terminates a sa..." · Reviewed by Greptile