Problem
E2BSandboxProvider.acquire() holds a per-(user_id, thread_id) lock. release() does not use that lock.
Release removes the active mappings before it syncs outputs, refreshes the remote timeout, closes the local client, and adds the sandbox to the warm pool. A concurrent acquire for the same thread can observe neither an active nor a warm sandbox.
It can then reconnect to the same remote VM through discovery, or create a new VM while release still operates on the old client.
Expected behavior
For one Gateway process, acquire must wait until release finishes the thread state transition. It must then reclaim the warm sandbox or create a replacement after release completes.
Proposed direction
- Use the existing per-thread lock for the full release transition.
- Do not hold the provider-wide lock across remote IO.
- Add an explicit releasing state or clear state-transition logs.
- Add a concurrency regression test that blocks output sync and asserts acquire does not discover or create early.
Scope
This fixes the in-process race. Multi-process Gateway deployments still need a distributed lease or an E2B-side atomic claim.
Problem
E2BSandboxProvider.acquire()holds a per-(user_id, thread_id)lock.release()does not use that lock.Release removes the active mappings before it syncs outputs, refreshes the remote timeout, closes the local client, and adds the sandbox to the warm pool. A concurrent acquire for the same thread can observe neither an active nor a warm sandbox.
It can then reconnect to the same remote VM through discovery, or create a new VM while release still operates on the old client.
Expected behavior
For one Gateway process, acquire must wait until release finishes the thread state transition. It must then reclaim the warm sandbox or create a replacement after release completes.
Proposed direction
Scope
This fixes the in-process race. Multi-process Gateway deployments still need a distributed lease or an E2B-side atomic claim.