Repository navigation
fix(gateway): a container deploy cancelled mid-create leaves no containers running - #171
Conversation
…iners running - The container gateway records each MCP server and sidecar container as it's created, not after the whole batch returns, so the close a cancelled or failed deploy gets terminates the ones already made. - Modal's create_container lets a create request it was cancelled during run to its end, and terminates the sandbox Modal made all the same, so a cancel that lands mid-request leaves nothing running either. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
When Modal returns the sandbox a cancelled create asked for and terminating it fails, the error is no longer dropped: a warning names the sandbox, which runs until its timeout, and the error. Waiting for the create and terminating it are separate steps, so a failed create stays quiet as before. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…d terminates what it creates later A cancel that lands while Modal's create request is in flight waited for the request with no bound, so a request that hung held up a chain's provisioning deadline, a Ctrl-C or an activity's cancel. It now waits 30 s at most (the request takes a few seconds as a rule); past that, or on a second cancel, the cancel goes on, a warning says so, and the sandbox the request creates is terminated when it ends. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
| created = await asyncio.wait_for(asyncio.shield(creating), _CANCELLED_CREATE_WAIT_SECONDS) | ||
| except (Exception, asyncio.CancelledError): # it failed, outlasted the wait, or a second cancel came | ||
| if not creating.done(): | ||
| creating.add_done_callback(_terminate_when_created) |
There was a problem hiding this comment.
When a cancelled create exceeds the 30-second wait, cleanup is left to a background callback. The deploy CLI uses asyncio.run(), which cancels pending tasks when the deploy exits. If the create is still pending, _terminate_when_created skips it because creating was cancelled. Modal can still create the sandbox remotely, leaving it running until its timeout.
Keep cleanup alive beyond the deploy’s event loop, or retain enough information to recover and terminate the sandbox after shutdown.
Knowledge Base Used: Provider integrations and sandbox deployment
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agent_env/providers/sandbox_providers/modal_sandbox.py
Line: 394-397
Comment:
**CLI shutdown drops cleanup**
When a cancelled create exceeds the 30-second wait, cleanup is left to a background callback. The deploy CLI uses `asyncio.run()`, which cancels pending tasks when the deploy exits. If the create is still pending, `_terminate_when_created` skips it because `creating` was cancelled. Modal can still create the sandbox remotely, leaving it running until its timeout.
Keep cleanup alive beyond the deploy’s event loop, or retain enough information to recover and terminate the sandbox after shutdown.
**Knowledge Base Used:** [Provider integrations and sandbox deployment](https://app.greptile.com/scale-ai/-/custom-context/knowledge-base/scaleapi/agentenv-framework/-/docs/provider-integrations.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Valid, and it is the limit this change accepts. Waiting with no limit is what let a hung request hold up a deadline or a Ctrl-C, so the cap stays. A process that exits cannot run cleanup after it has exited. Modal's sandbox timeout bounds a sandbox that is created after that point. 2adc73f makes such a sandbox traceable: when the callback sees the create was cancelled before it ended (as asyncio.run does at exit), it logs a warning with the create's image, app and region. test_a_process_that_exits_while_a_cancelled_create_hangs_names_the_create_in_a_warning covers this.
There was a problem hiding this comment.
Issue 1 is understood and does not require another code change. The 30-second bound intentionally allows cancellation to proceed when Modal does not answer; if the process then exits, no in-process cleanup can run, and the sandbox’s configured Modal timeout is the remaining safety bound. Commit 2adc73f makes that residual case observable by warning when the create was cancelled before Modal answered, including the image/app/region context needed to locate the sandbox.
Issue 2 was valid and is fixed in 2adc73f: the late-termination callback is now registered even when the create has completed by the time the bounded wait ends. Since add_done_callback runs immediately for an already-completed future, a sandbox returned in that race is terminated rather than leaked. The added regression test covers this case.
…p, and name a create the process abandons The wait can time out or be cancelled in the same step Modal answers, which left the create done but its sandbox running: the late-termination callback was only registered while the create was still pending. It is now always registered, and runs at once on a create that has already ended. A create still pending when the process exits is cancelled by asyncio.run, so a sandbox Modal makes for it runs until its timeout. The callback now logs that with the create's details, so the sandbox can be found. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A gateway deploy in containers that's cancelled while it creates them no longer leaves containers running on Modal.
Why
A chaos run of #169 cancelled a Modal container gateway deploy 6 s in and left two of its containers running until their timeout. Two windows leak:
_deploy_via_containerscreates the MCP servers and the pgweb and db-mcp sidecars with oneasyncio.gather, and added them to_container_sandboxesonly once the whole batch returned. A cancel while the batch was pending dropped the containers already made, so the close_rungives a cancelled deploy never saw them. The service-db container, recorded as it's created, was torn down.Sandbox._experimental_creategave up on the request, and the sandbox Modal made ran until its timeout.Prod's Modal container gateways run through the same code; a run cancelled at the wrong moment pays for its containers until their timeout.
What
_provision_mcpand_provision_sidecaradd their container to_container_sandboxesthe momentcreate_containerreturns, as_stand_up_servicedbalready did, so a deploy that fails or is cancelled mid-batch closes every container it made.ModalSandboxProvider.create_containerruns the create request to its end even when it's cancelled (asyncio.shield), then terminates what Modal created and re-raises the cancellation; a terminate that fails logs a warning naming the sandbox, which then runs until its timeout. The cancel waits for the request 30 s at most (it takes a few seconds as a rule), so a request that hangs can't hold up a chain's provisioning deadline, a Ctrl-C or an activity's cancel; past that, or on a second cancel, the cancel goes on, a warning says so, and the sandbox the request creates is terminated when it ends, including when it ends in the same step the wait gives up. A process that exits with the request still pending can't clean up after it: a warning names the create, and a sandbox Modal makes for it runs until its timeout. A cancel after the request returned was already handled: that path terminates the sandbox on anyBaseException. A second Ctrl-C during the wait stops it, as it stops the rest of a teardown.Tests
Unit tier and the protocol suite: 7,464 passed. New:
_run, while db-mcp is still being created: the service-db, MCP server and pgweb containers it made are all terminated;Live check
From this machine on Modal, a bundle whose MCP server deploys through the container gateway with local Postgres state, cancelled with Ctrl-C partway through its deploy:
🤖 Generated with Claude Code
The PR still needs the shutdown cleanup gap fixed before merging.
Fix with agent prompt
Summary
The PR records gateway containers as they are created and cleans up Modal sandboxes returned after cancellation.
Reviews (4) · Last reviewed commit: "Terminate a sandbox created just as a ca..." · Reviewed by Greptile