Skip to content

fix(orchestrator): enforce sandbox TTL on the node via WaitForExit#3194

Open
AdaAibaby wants to merge 2 commits into
e2b-dev:mainfrom
AdaAibaby:fix/orchestrator-sandbox-timeout-enforcement
Open

fix(orchestrator): enforce sandbox TTL on the node via WaitForExit#3194
AdaAibaby wants to merge 2 commits into
e2b-dev:mainfrom
AdaAibaby:fix/orchestrator-sandbox-timeout-enforcement

Conversation

@AdaAibaby

@AdaAibaby AdaAibaby commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Problem

WaitForExit was defined in sandbox.go but never called anywhere in the codebase. The API-layer evictor (polling Redis every 50 ms) was the sole mechanism to kill expired sandboxes. When the API service restarts, the evictor stops and Firecracker VMs accumulate on nodes indefinitely.

Closes #3193

Changes

packages/orchestrator/pkg/server/sandboxes.go

Replace sbx.Wait with sbx.WaitForExit in setupSandboxLifecycle. The lifecycle goroutine now races against the sandbox TTL. On timeout, Stop() is called explicitly before the existing Close/cleanup path.

packages/orchestrator/pkg/sandbox/sandbox.go

Replace the one-shot time.After in WaitForExit with a time.NewTimer loop that re-checks endAt after each fire. A KeepAlive that calls SetEndAt mid-flight now correctly resets the node-side deadline.

packages/orchestrator/pkg/sandbox/wait_for_exit_test.go (new)

5 unit tests covering all branches of WaitForExit:

  • TestWaitForExit_AlreadyExpired — past endAt returns error immediately
  • TestWaitForExit_ExitBeforeTTL — clean exit before TTL returns nil
  • TestWaitForExit_ExitWithError — FC error is wrapped and returned
  • TestWaitForExit_KeepAliveExtendsTTLSetEndAt extension resets the timer; sandbox is not killed at original deadline
  • TestWaitForExit_ContextCancelled — ctx cancel returns nil

All pass with -race.

Behaviour after this fix

Scenario Before After
API service restarts VMs run until node reboot Node kills VM at endAt independently
KeepAlive extends TTL Node ignores extension (timer not reset) Node re-arms timer on each fire
Normal sandbox exit FC exits → cleanup runs Unchanged
API evictor fires before endAt Kills normally Unchanged (Stop is idempotent)

Testing

cd packages/orchestrator && go test -run TestWaitForExit -v -race -count=1 ./pkg/sandbox/

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Using time.Until(s.GetEndAt()) > 0 to detect if the deadline was extended can cause a busy loop or unnecessary timer re-creations if the timer fires slightly early. Comparing the current deadline against the previously observed deadline using s.GetEndAt().After(endAt) ensures the loop only continues if the deadline was actually extended. Additionally, logging 'sandbox TTL expired, stopping' is misleading when the exit wait fails because the Firecracker process exited prematurely; a generic message like 'sandbox exit wait failed, stopping' should be used instead.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread packages/orchestrator/pkg/sandbox/sandbox.go
Comment thread packages/orchestrator/pkg/server/sandboxes.go
adababys added 2 commits July 3, 2026 12:11
WaitForExit was defined but never called, leaving the API-layer evictor as
the sole mechanism to kill expired sandboxes.  If the API service restarts
the evictor goroutine stops and VMs accumulate indefinitely on nodes.

Two changes:

1. setupSandboxLifecycle (sandboxes.go): replace sbx.Wait with
   sbx.WaitForExit so the lifecycle goroutine races against the sandbox TTL.
   On timeout, Stop() is called explicitly before the normal Close/cleanup
   path runs.

2. WaitForExit (sandbox.go): replace the one-shot time.After with a
   time.NewTimer loop that re-checks endAt after each fire.  This means a
   KeepAlive that calls SetEndAt mid-flight correctly resets the node-side
   deadline instead of being silently ignored.

Fixes e2b-dev#3193
5 cases covering the two code changes in the parent commit:

- AlreadyExpired: endAt in the past returns error immediately
- ExitBeforeTTL: clean FC exit before TTL returns nil
- ExitWithError: FC exits with error, error is wrapped and returned
- KeepAliveExtendsTTL: SetEndAt extension resets the timer loop;
  sandbox is NOT killed at the original deadline
- ContextCancelled: ctx cancel returns nil without killing

All pass with -race.
@AdaAibaby AdaAibaby force-pushed the fix/orchestrator-sandbox-timeout-enforcement branch from 4b62c59 to 4826a8e Compare July 3, 2026 04:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

orchestrator: node has no independent sandbox timeout enforcement — VMs leak indefinitely if API evictor stops

2 participants