Skip to content

Commit daa57f2

Browse files
committed
Retry PiSessionFactory.create() with backoff
Mirror of the OpenCodeSessionFactory retry: session creation (sandbox + Pi install + proxy + agent) is the flakiest step, and a single transient failure dropped the rollout as unscorable. create() now retries create_attempts times with exponential backoff; the per-attempt logic moved to _create_once, which already tears its own sandbox down on failure, so a retry never leaks.
1 parent 9bbec18 commit daa57f2

2 files changed

Lines changed: 73 additions & 3 deletions

File tree

envs/pi_env/harness.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,8 @@ def __init__(
206206
verifier: Verifier | None = None,
207207
install_timeout_s: int = 300,
208208
setup_timeout_s: int = 300,
209+
create_attempts: int = 3,
210+
create_backoff_s: float = 2.0,
209211
) -> None:
210212
if mode not in {"black_box", "transparent_proxy"}:
211213
raise ValueError(f"Unknown mode: {mode!r}")
@@ -215,12 +217,45 @@ def __init__(
215217
self._verifier = verifier
216218
self._install_timeout_s = install_timeout_s
217219
self._setup_timeout_s = setup_timeout_s
220+
self._create_attempts = create_attempts
221+
self._create_backoff_s = create_backoff_s
218222

219223
def create(
220224
self,
221225
task: Any,
222226
seed: int | None = None,
223227
episode_id: str | None = None,
228+
) -> PiSession:
229+
"""Create one session, retrying with exponential backoff.
230+
231+
Session creation spins up a sandbox, installs Pi, and starts the proxy +
232+
agent, the flakiest step in a rollout. Each failed attempt tears its own
233+
sandbox down (see :meth:`_create_once`), so a retry never leaks.
234+
"""
235+
import logging
236+
import time
237+
238+
_log = logging.getLogger(__name__)
239+
last_exc: Exception = RuntimeError("create_attempts must be >= 1")
240+
for i in range(self._create_attempts):
241+
try:
242+
return self._create_once(task, seed=seed, episode_id=episode_id)
243+
except Exception as exc: # noqa: BLE001
244+
last_exc = exc
245+
if i + 1 < self._create_attempts:
246+
backoff = self._create_backoff_s * (2**i)
247+
_log.warning(
248+
"factory.create attempt %d/%d failed (%r); retrying in %.1fs",
249+
i + 1, self._create_attempts, exc, backoff,
250+
)
251+
time.sleep(backoff)
252+
raise last_exc
253+
254+
def _create_once(
255+
self,
256+
task: Any,
257+
seed: int | None = None,
258+
episode_id: str | None = None,
224259
) -> PiSession:
225260
import logging
226261
_log = logging.getLogger(__name__)

tests/envs/test_pi_factory_lifecycle.py

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,17 @@ def create(self, **kwargs):
3535
return self._sandbox
3636

3737

38-
def _factory(sandbox):
38+
def _factory(sandbox, **overrides):
3939
return PiSessionFactory(
4040
config=PiConfig(base_url="http://localhost:8000/v1"),
4141
sandbox_backend=_FakeBackend(sandbox),
42+
**overrides,
4243
)
4344

4445

4546
def test_create_kills_sandbox_when_bootstrap_fails(monkeypatch):
4647
sandbox = _FakeSandbox()
47-
factory = _factory(sandbox)
48+
factory = _factory(sandbox, create_attempts=1)
4849
monkeypatch.setattr(
4950
factory,
5051
"_bootstrap_sandbox",
@@ -61,7 +62,7 @@ def kill(self):
6162
raise RuntimeError("kill failed")
6263

6364
sandbox = _KillRaises()
64-
factory = _factory(sandbox)
65+
factory = _factory(sandbox, create_attempts=1)
6566
monkeypatch.setattr(
6667
factory,
6768
"_bootstrap_sandbox",
@@ -70,3 +71,37 @@ def kill(self):
7071
# The original bootstrap failure must surface, not the cleanup error.
7172
with pytest.raises(RuntimeError, match="boom"):
7273
factory.create("write a function")
74+
75+
76+
def test_create_retries_then_succeeds(monkeypatch):
77+
# create() retries a flaky _create_once and returns once it succeeds.
78+
sandbox = _FakeSandbox()
79+
factory = _factory(sandbox, create_attempts=3, create_backoff_s=0)
80+
calls = {"n": 0}
81+
session = object()
82+
83+
def _flaky(task, seed=None, episode_id=None):
84+
calls["n"] += 1
85+
if calls["n"] < 3:
86+
raise RuntimeError("transient create failure")
87+
return session
88+
89+
monkeypatch.setattr(factory, "_create_once", _flaky)
90+
assert factory.create("write a function") is session
91+
assert calls["n"] == 3
92+
93+
94+
def test_create_raises_after_exhausting_attempts(monkeypatch):
95+
# A persistent failure is re-raised after create_attempts tries.
96+
sandbox = _FakeSandbox()
97+
factory = _factory(sandbox, create_attempts=3, create_backoff_s=0)
98+
calls = {"n": 0}
99+
100+
def _always_fails(task, seed=None, episode_id=None):
101+
calls["n"] += 1
102+
raise RuntimeError("persistent create failure")
103+
104+
monkeypatch.setattr(factory, "_create_once", _always_fails)
105+
with pytest.raises(RuntimeError, match="persistent create failure"):
106+
factory.create("write a function")
107+
assert calls["n"] == 3

0 commit comments

Comments
 (0)