Skip to content

fix(e2b): prevent double-execute on transport-retry; bound cancel-loop kill; close create race - #2327

Merged
federicodeponte merged 7 commits into
mainfrom
fix/e2b-double-execute-transport-retry
Jul 24, 2026
Merged

fix(e2b): prevent double-execute on transport-retry; bound cancel-loop kill; close create race#2327
federicodeponte merged 7 commits into
mainfrom
fix/e2b-double-execute-transport-retry

Conversation

@federicodeponte

Copy link
Copy Markdown
Member

Problem

The E2B transport-retry loop (e2b_driver.run) re-ran run.py from scratch in a NEW sandbox after a transient transport drop (Server disconnected / ConnectionTerminated / HPACK / reset / 503). The only safety net was a best-effort sandbox.kill() over the SAME dropped transport, swallowed on failure. If that kill silently failed, the original sandbox kept running (real customer side effects: emails, CRM writes, sends) while the retry ran a second time -> duplicate side effects.

Fix

  1. Confirm-dead-before-retry. _run_in_sandbox tracks whether the worker command started and, on a transport unwind, runs a bounded kill-and-verify (_terminate_and_confirm_dead): control-plane kill() (a DELETE against api.e2b.dev, a DIFFERENT transport than the dropped envd command stream) is authoritative proof of termination; if it keeps raising, fall back to polling is_running() within a bounded budget (not the unbounded 60s control timeout on a dead transport). It stamps the in-flight exception with command_started + confirmed_dead. run() retries ONLY when the command never started (create/upload phase: no side effects yet) OR the original is confirmed dead. Otherwise it returns the new terminal operator-action code sandbox_liveness_unconfirmed (retryable=False) instead of blind-retrying.
  2. Bound the cancel-loop kill. The shutdown/worker-deletion cancel loop bounds each cancel_sandbox kill and stops issuing kills once the overall budget is spent (stragglers handled by thread-join + startup recovery), so one hung kill can no longer blow the whole shutdown budget.
  3. Close the post-cancel create race. _run_in_sandbox re-checks run_cancel_requested immediately before spawning, so a run cancelled while it sat in pre-sandbox setup cannot spawn an orphan the cancel sweep already believes it terminated.
  4. Taxonomy. sandbox_liveness_unconfirmed registered as permanent/non-retryable (run_service), crash category (run_metrics), OPS alert (alerting), operator headline (public_view).

Residual (documented, not implemented)

Action-boundary idempotency keys keyed on run_id threaded into side-effecting connector calls. Those calls run INSIDE the sandbox via the worker's own code, so this is a large cross-cutting change. The confirmed-dead gate removes the double-execution that would be needed to trigger a duplicate; the idempotency key is a defense-in-depth follow-up.

Tests

New test_e2b_double_execute_prevention.py + test_cancel_loop_kill_bounded.py:

  • drop after command start + kill unconfirmed -> NO second sandbox, ends sandbox_liveness_unconfirmed
  • drop + sandbox confirmed dead -> retry proceeds (2 sandboxes, success)
  • post-cancel create race -> cancel-requested run spawns nothing
  • cancel-loop kill time-bounded on hung kills; cancel_sandbox forwards bounded request_timeout

Full e2b_driver + runner_sandbox suites green; the 12 pre-existing failures in the broad selection are identical on the base SHA (1d9e15e) and unrelated (git-clone/MCP/posthog environment).

Do NOT merge/deploy yet - parent gates merge + canary.

🤖 Generated with Claude Code

federicodeponte and others added 7 commits July 24, 2026 20:26
…p kill; close create race

The E2B transport-retry loop re-ran run.py from scratch in a new sandbox after a
transient transport drop (Server disconnected / ConnectionTerminated / HPACK /
reset / 503). The only safety net was a best-effort sandbox.kill() over the SAME
dropped transport, swallowed on failure. If that kill silently failed, the
original sandbox kept running (real side effects: emails, CRM writes, sends)
while the retry ran a second time -> duplicate customer side effects.

Fix:
- Never re-dispatch after a post-command-start drop until the ORIGINAL sandbox
  is CONFIRMED terminated. _run_in_sandbox now tracks whether the worker command
  started and, on transport unwind, runs a bounded kill-and-verify
  (_terminate_and_confirm_dead) using the control-plane kill (a separate
  transport from the dropped envd stream) plus an is_running() liveness probe.
  It stamps the in-flight exception with started + confirmed-dead. run() retries
  only when the command never started (create/upload phase: no side effects yet)
  OR the original is confirmed dead; otherwise it returns the new terminal
  operator-action code `sandbox_liveness_unconfirmed` (retryable=False) instead
  of blind-retrying. Bounded budget replaces the unbounded 60s kill on a dead
  transport.
- Bound the shutdown/worker-deletion cancel loop: each cancel_sandbox kill is
  time-bounded and the loop stops issuing kills once the overall budget is spent
  (stragglers handled by thread-join + startup recovery), so one hung kill can
  no longer blow the whole shutdown budget.
- Close the post-cancel create race: _run_in_sandbox re-checks cancel-requested
  immediately before spawning, so a run cancelled while it sat in pre-sandbox
  setup cannot spawn an orphan the cancel sweep already believes it terminated.
- Register sandbox_liveness_unconfirmed across the taxonomies (permanent/
  non-retryable, crash category, OPS alert, operator headline).

Residual (documented, not implemented): action-boundary idempotency keys keyed
on run_id threaded into side-effecting connector calls. Those calls run INSIDE
the sandbox via the worker's own code, so this is a large cross-cutting change;
the confirmed-dead gate above removes the double-execution that would be needed
to trigger a duplicate, so this is a defense-in-depth follow-up.

Tests: no-double-execute when kill unconfirmed (ends sandbox_liveness_unconfirmed,
1 sandbox); retry proceeds when confirmed dead (2 sandboxes, success);
post-cancel create race spawns nothing; cancel-loop kill is time-bounded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ncel gate, retry veto, unified cancel deadline

Adversarial Codex review found 3 P1 + 1 P2 in the first cut:

- P1: a transport drop on the post-completion result.json read retried once the
  sandbox was confirmed dead, but the worker had already RUN TO COMPLETION so its
  side effects were done -> retry duplicated them. Fix: track
  worker_command_completed (set when sandbox.commands.run returns) and never
  retry a completed run regardless of liveness.
- P1: create-race window between the pre-spawn cancel check and _register_sandbox
  (cancel during the up-to-120s Sandbox.create was missed by the cancel sweep and
  by the pre-spawn check). Fix: add a second cancel gate immediately before the
  worker command, after registration; together with registration this closes the
  window.
- P1: a worker manifest retry.on:[sandbox_liveness_unconfirmed] (or
  retryable=True) could override the permanent-code check and re-dispatch a
  liveness-unconfirmed run. Fix: _NEVER_RETRY_ERROR_CODES vetoes retry before the
  manifest-exact check.
- P2: the cancel loop gave the kill pass and the join pass each a full
  timeout_seconds (total ~2x). Fix: one shared overall_deadline for both passes.

Tests: completed-worker + result-read drop -> no retry; cancel during setup after
registration -> no command run; liveness-unconfirmed never retried even with
manifest opt-in.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nd 2)

Codex round 2 confirmed the round-1 fixes but flagged a residual P1: the
confirmed-dead -> retry path (Phase B) can still DUPLICATE a side effect the
worker committed before the transport dropped. Killing the original stops
concurrent execution but cannot undo a committed email/CRM write/send, and the
platform does not yet enforce action-level idempotency at the connector
boundary.

Resolution (safety over availability): refuse to auto-retry ANY run whose worker
command started, whether or not the sandbox is confirmed dead. Retry stays
enabled ONLY for create/upload-phase drops (command never started -> no worker
code ran -> no side effects possible), which still covers the common
sandbox-create / HPACK / header-block transport drops. Post-command-start drops
end terminally as sandbox_liveness_unconfirmed (operator action). The bounded
kill-and-verify still runs to stop the original promptly.

This is stricter than the original task's "confirmed-dead -> retry" test gate; it
trades some auto-recovery on mid-run transport blips for a hard guarantee that a
started worker is never re-executed. Re-enabling safe post-start retry requires
durable run_id+operation idempotency keys threaded into connector calls (the
documented residual / follow-up).

Tests updated: post-command-start drop (even confirmed dead) -> no retry,
terminal; create-phase drop -> retry proceeds to success.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codex round 3 confirmed the in-driver guard holds but found the double-execute
moved UP a layer, two P1s:

- P1: an opaque, NON-transient exception raised after the worker command started
  escaped the transport-retry handling and fell through to the driver's outer
  handler, which classified it as a RETRYABLE code (e2b_sandbox_error). The
  run_service retry scheduler then re-dispatched the whole run in a fresh
  sandbox, re-running the worker. Fix: the outer handler now forces the
  non-retryable sandbox_liveness_unconfirmed terminal whenever the command
  started (stamped on the exception).
- P1: graceful-shutdown requeue (_requeue_interrupted_run_in_place) re-queued
  every joined active run regardless of execution stage, so a run whose worker
  command started got re-run on the next drain. Fix: the driver signals
  run_service (mark_run_worker_command_started) when the command starts; the
  shutdown requeue now skips runs with worker_command_started=True (left
  cancelled for operator action). Pre-command runs still requeue safely.

The invariant is now enforced at every layer: once the worker command starts,
no path (driver transport-retry, driver outer handler, run_service retry
scheduler, graceful-shutdown requeue) re-runs it. Startup recovery was already
safe (only requeues dispatch-orphans with no sandbox logs).

Tests: opaque post-start failure -> non-retryable terminal, one execution;
shutdown does not requeue a started-worker run but does requeue a pre-command
run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…mp (Codex round 4)

Codex round 4: a failure that unwinds AFTER the worker command completed and
keep_warm was set would return the sandbox to the warm pool (pooled=True),
skipping the exception-stamping block. run()'s outer handler then fell back to a
retryable e2b_sandbox_error, which the run_service scheduler re-dispatches ->
duplicate. Fix: stamp _e2b_worker_command_started/_completed on EVERY
post-command-start unwind BEFORE the warm-pool branch, and never return a
sandbox to the warm pool while unwinding a failure (only pool cleanly-completed
runs).

Test: warm-pool enabled + a post-completion failure -> non-retryable
sandbox_liveness_unconfirmed terminal and the sandbox is NOT pooled.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… bounded cancel-loop kill

These two tests live only in the CI-blocking root `tests/` suite (not
`apps/api/tests`), so the earlier apps/api-focused runs missed them. Both are
test-only fixes to match the (correct) product behavior; no product code changed.

- test_e2b_artifact_collection.py::test_e2b_cancelled_sandbox_exception_reads_active_repository:
  the new pre-spawn create-race guard short-circuited this test (repo reported
  cancel_requested from the start), so it stopped exercising the post-spawn
  terminate + active-repo read it was written for. Moved the cancellation to
  AFTER the sandbox spawns (the FakeCancelledSandbox command hook flips the run's
  cancel flag when the worker command runs), and added an assertion that a
  sandbox actually spawned. Still asserts cancelled/user_cancel via the
  active-repository read.
- test_s35_shutdown.py::test_request_active_run_shutdown_marks_cancel_and_kills_sandbox:
  updated the mocked cancel_sandbox to accept the new bounded `request_timeout`
  kwarg the cancel loop now passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…no global monotonic patch)

test_cancel_loop_is_time_bounded_on_hung_kills patched the global
run_service.time.monotonic with a fake clock, which is fragile under full-suite
runs (a prior test's leftover state could break the fake clock, making the loop
skip all kills -> calls==[]). It passed in isolation but failed in the full
apps/api/tests serial run. Rewrote it to use REAL wall-clock: the first fake
cancel sleeps longer than the small budget, so the loop provably stops issuing
kills after the budget is spent. No product change; the bound itself is
unchanged (in prod timeout_seconds is 30-75s so the first kill always fires).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@federicodeponte
federicodeponte marked this pull request as ready for review July 24, 2026 21:15
@federicodeponte
federicodeponte merged commit c00129f into main Jul 24, 2026
6 of 7 checks passed
@federicodeponte
federicodeponte deleted the fix/e2b-double-execute-transport-retry branch July 24, 2026 21:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant