@@ -235,6 +235,63 @@ def _wait_for_terminal(db_path: Path, task_ids: list[str], process: subprocess.P
235235 raise ExperimentFailure ("sandbox_tasks_did_not_reach_terminal_state" )
236236
237237
238+ def _sandbox_log_tail (* paths : Path , max_bytes : int = 4096 ) -> str :
239+ chunks : list [str ] = []
240+ for path in paths :
241+ if not path .is_file ():
242+ continue
243+ data = path .read_bytes ()
244+ if not data :
245+ continue
246+ chunks .append (data [- max_bytes :].decode ("utf-8" , errors = "replace" ))
247+ return "\n " .join (chunks )
248+
249+
250+ def _wait_for_sandbox_api (
251+ process : subprocess .Popen ,
252+ * ,
253+ base_url : str ,
254+ token : str ,
255+ log_paths : tuple [Path , ...],
256+ timeout_seconds : float = 30.0 ,
257+ interval_seconds : float = 0.2 ,
258+ ) -> None :
259+ parsed = urlparse (base_url )
260+ deadline = time .monotonic () + timeout_seconds
261+ last_error = "no_probe_attempt"
262+ while time .monotonic () < deadline :
263+ if process .poll () is not None :
264+ tail = _sandbox_log_tail (* log_paths )
265+ raise ExperimentFailure (
266+ f"sandbox_api_failed_to_start returncode={ process .returncode } log_tail={ tail !r} "
267+ )
268+ try :
269+ connection = http .client .HTTPConnection (parsed .hostname , parsed .port , timeout = 1 )
270+ try :
271+ connection .request (
272+ "GET" ,
273+ "/api/uploads/capabilities" ,
274+ headers = {"Authorization" : f"Bearer { token } " },
275+ )
276+ readiness = connection .getresponse ()
277+ body = readiness .read ()
278+ finally :
279+ connection .close ()
280+ if readiness .status == 200 and json .loads (body ).get ("enabled" ) is True :
281+ return
282+ last_error = f"http_{ readiness .status } "
283+ except (OSError , http .client .HTTPException , json .JSONDecodeError ) as exc :
284+ last_error = f"{ type (exc ).__name__ } :{ exc } "
285+ remaining = deadline - time .monotonic ()
286+ if remaining <= 0 :
287+ break
288+ time .sleep (min (interval_seconds , remaining ))
289+ tail = _sandbox_log_tail (* log_paths )
290+ raise ExperimentFailure (
291+ f"sandbox_api_not_ready timeout_seconds={ timeout_seconds } last_error={ last_error } log_tail={ tail !r} "
292+ )
293+
294+
238295def run_experiment (duration_seconds : int ) -> int :
239296 if duration_seconds < 1 or duration_seconds > 30 :
240297 raise ExperimentFailure ("duration_seconds_must_be_between_1_and_30" )
@@ -305,23 +362,19 @@ def __init__(self, *args, **kwargs):
305362 stderr = subprocess .STDOUT ,
306363 )
307364 cleanup .callback (_stop_process , process )
308- time .sleep (2 )
309- if process .poll () is not None :
310- raise ExperimentFailure ("sandbox_api_failed_to_start" )
311365
312366 base_url = f"http://127.0.0.1:{ api_port } "
313- parsed = urlparse (base_url )
314- connection = http .client .HTTPConnection (parsed .hostname , parsed .port , timeout = 5 )
315- connection .request (
316- "GET" ,
317- "/api/uploads/capabilities" ,
318- headers = {"Authorization" : "Bearer sandbox-token-never-use-outside-this-run" },
367+ _wait_for_sandbox_api (
368+ process ,
369+ base_url = base_url ,
370+ token = "sandbox-token-never-use-outside-this-run" ,
371+ log_paths = (
372+ service_log_path ,
373+ root / "data" / "logs" / "sandbox-api.log" ,
374+ ),
375+ timeout_seconds = 30.0 ,
376+ interval_seconds = 0.2 ,
319377 )
320- readiness = connection .getresponse ()
321- capabilities = json .loads (readiness .read ())
322- connection .close ()
323- if readiness .status != 200 or capabilities .get ("enabled" ) is not True :
324- raise ExperimentFailure ("sandbox_upload_receiver_not_ready" )
325378
326379 starts = threading .Barrier (3 )
327380 data_temp = root / "data" / "temp"
0 commit comments