Compare commits

..
Author SHA1 Message Date
Alex Schapiro d31d6fb6b8 fix(runtime): retry transient sandbox startup failures 2026-07-15 02:59:55 +00:00
2 changed files with 7 additions and 33 deletions
+6 -7
View File
@@ -38,21 +38,21 @@ _TRANSIENT_CONNECTION_NAMES = {
def _start_attempts() -> int:
raw = os.environ.get("STRIX_SANDBOX_START_ATTEMPTS")
raw = os.environ.get("STRIX_E2B_BOOTSTRAP_ATTEMPTS")
if raw is None:
return _DEFAULT_START_ATTEMPTS
try:
attempts = int(raw)
except ValueError:
logger.warning(
"Invalid STRIX_SANDBOX_START_ATTEMPTS=%r; using %d",
"Invalid STRIX_E2B_BOOTSTRAP_ATTEMPTS=%r; using %d",
raw,
_DEFAULT_START_ATTEMPTS,
)
return _DEFAULT_START_ATTEMPTS
if attempts < 1:
logger.warning(
"STRIX_SANDBOX_START_ATTEMPTS must be positive; using %d",
"STRIX_E2B_BOOTSTRAP_ATTEMPTS must be positive; using %d",
_DEFAULT_START_ATTEMPTS,
)
return _DEFAULT_START_ATTEMPTS
@@ -88,7 +88,7 @@ def _is_transient_start_error(error: BaseException) -> bool:
if name in _TRANSIENT_TIMEOUT_NAMES:
return True
if name in _TRANSIENT_CONNECTION_NAMES and (
module.startswith(("httpcore", "httpx", "agents"))
module.startswith(("httpcore", "httpx", "e2b", "agents"))
or name in {"ConnectionError", "ConnectionResetError"}
):
return True
@@ -119,12 +119,11 @@ async def start_session_with_retry(
if session is not None:
try:
await client.delete(session)
except Exception as teardown_error:
except Exception: # noqa: BLE001
logger.warning(
"Failed to tear down sandbox after start failure; aborting retry",
"Failed to tear down sandbox after start failure",
exc_info=True,
)
raise exc from teardown_error
transient = _is_transient_start_error(exc)
if not transient or attempt == max_attempts:
raise
+1 -26
View File
@@ -33,10 +33,9 @@ class _FakeSession:
class _FakeClient:
def __init__(self, *, delete_error: BaseException | None = None) -> None:
def __init__(self) -> None:
self.created = 0
self.deleted: list[_FakeSession] = []
self.delete_error = delete_error
async def create(self) -> _FakeSession:
self.created += 1
@@ -55,8 +54,6 @@ class _FakeClient:
async def delete(self, session: _FakeSession) -> None:
self.deleted.append(session)
if self.delete_error is not None:
raise self.delete_error
async def test_transient_workspace_failure_retries_and_tears_down(
@@ -93,28 +90,6 @@ async def test_non_transient_workspace_failure_does_not_retry() -> None:
assert client.deleted == [session]
async def test_teardown_failure_raises_original_error_without_retry() -> None:
start_error = WorkspaceStartError(
path=Path("/workspace"),
cause=TimeoutError("transient transport timeout"),
)
teardown_error = RuntimeError("teardown failed")
client = _FakeClient(delete_error=teardown_error)
session = _FakeSession([start_error])
async def create_session() -> _FakeSession:
client.created += 1
return session
with pytest.raises(WorkspaceStartError) as caught:
await start_session_with_retry(client, create_session, attempts=3)
assert caught.value is start_error
assert caught.value.__cause__ is teardown_error
assert client.created == 1
assert client.deleted == [session]
async def test_each_transient_attempt_is_torn_down(monkeypatch: pytest.MonkeyPatch) -> None:
client = _FakeClient()
client.created = 0