Skip to content

Commit c74a652

Browse files
AHarmlessPyroclaude
andcommitted
Drop the sync client's socket-level wait deadlines
Interrupting an in-flight HTTPX request needed a timer thread that shut down sockets through trace hooks, with special cases for shared HTTP/2 connections, TLS stream swaps and trickled responses, plus deadline-aware copies of the token refresh and control request paths. The sync secret process wait now checks its limit between requests and caps each request's HTTP timeouts at the time left, like the async client's transport. A trickled response or a token refresh can overrun the limit; the README points to the async client for a strict one. The control request path is back to master's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 97c3418 commit c74a652

5 files changed

Lines changed: 28 additions & 601 deletions

File tree

‎README.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -331,9 +331,9 @@ Things to know:
331331
the total local wait and raises `TimeoutError` (`asyncio.TimeoutError` for
332332
async handles); the process continues and the handle can be used to wait
333333
again. The start's timeout limits the command's execution separately.
334-
For strict cancellation timing, use the async client: blocking DNS, custom
335-
transports or hooks, shared HTTP/2 control connections, and trickled headers
336-
on a reused control connection can delay cancellation in the sync client.
334+
The sync client checks the limit between requests and caps each request's
335+
HTTP timeouts at the time left, so a slow response or a runtime token
336+
refresh can overrun it; use the async client for a strict limit.
337337
- A process start with secrets that times out, or whose result the API can't
338338
confirm, raises a non-retryable error with code
339339
`secret_process_outcome_unknown`: the command may be running, so check

‎hyperbrowser/client/managers/sync_manager/sandbox.py‎

Lines changed: 13 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
from pathlib import Path
33
from typing import Dict, Optional, Union
44

5-
import httpx
6-
75
from ..._request import coerce_request, dump_request
86
from ....exceptions import HyperbrowserError
97
from ....models.sandbox import (
@@ -115,7 +113,7 @@
115113
SandboxTerminalConnection,
116114
SandboxTerminalHandle,
117115
)
118-
from .sandboxes.sandbox_transport import RuntimeTransport, _RequestDeadline
116+
from .sandboxes.sandbox_transport import RuntimeTransport
119117

120118
__all__ = [
121119
"DEFAULT_PROCESS_KILL_WAIT_SECONDS",
@@ -144,7 +142,6 @@ def __init__(self, service: "SandboxManager", detail: SandboxDetail):
144142
self._resolve_runtime_connection,
145143
service.runtime_timeout,
146144
service.runtime_proxy_override,
147-
self._resolve_runtime_connection_with_deadline,
148145
)
149146
self.processes = SandboxProcessesApi(
150147
self._transport,
@@ -294,11 +291,6 @@ def get_exposed_url(self, port: int) -> str:
294291

295292
def create_runtime_session(
296293
self, force_refresh: bool = False
297-
) -> SandboxRuntimeSession:
298-
return self._create_runtime_session(force_refresh)
299-
300-
def _create_runtime_session(
301-
self, force_refresh: bool, deadline: Optional[float] = None
302294
) -> SandboxRuntimeSession:
303295
self._assert_runtime_available()
304296
if (
@@ -308,11 +300,7 @@ def _create_runtime_session(
308300
):
309301
return _copy_model(self._runtime_session)
310302

311-
detail = (
312-
self._service.get_detail(self.id)
313-
if deadline is None
314-
else self._service._get_detail_with_deadline(self.id, deadline)
315-
)
303+
detail = self._service.get_detail(self.id)
316304
self._hydrate(detail)
317305
if self._runtime_session is None:
318306
raise HyperbrowserError(
@@ -383,16 +371,6 @@ def _resolve_runtime_socket_info(self) -> RuntimeConnection:
383371
token=session.token,
384372
)
385373

386-
def _resolve_runtime_connection_with_deadline(
387-
self, force_refresh: bool, deadline: float
388-
) -> RuntimeConnection:
389-
session = self._create_runtime_session(force_refresh, deadline)
390-
return RuntimeConnection(
391-
sandbox_id=self.id,
392-
base_url=session.runtime.base_url,
393-
token=session.token,
394-
)
395-
396374
def _apply_runtime_session(self, session: SandboxRuntimeSession) -> None:
397375
self._runtime_session = _copy_model(session)
398376
self._detail = self._detail.model_copy(
@@ -1092,14 +1070,6 @@ def get_detail(self, sandbox_id: str) -> SandboxDetail:
10921070
payload = self._request("GET", f"/sandbox/{sandbox_id}")
10931071
return SandboxDetail(**payload)
10941072

1095-
def _get_detail_with_deadline(
1096-
self, sandbox_id: str, deadline: float
1097-
) -> SandboxDetail:
1098-
payload = self._request(
1099-
"GET", f"/sandbox/{sandbox_id}", request_deadline=deadline
1100-
)
1101-
return SandboxDetail(**payload)
1102-
11031073
def attach(self, detail: SandboxDetail) -> SandboxHandle:
11041074
return SandboxHandle(self, detail)
11051075

@@ -1203,38 +1173,22 @@ def _request(
12031173
*,
12041174
params: Optional[Dict[str, object]] = None,
12051175
data: Optional[Dict[str, object]] = None,
1206-
request_deadline: Optional[float] = None,
12071176
):
12081177
failed_attempt = 1
12091178
while True:
12101179
try:
1211-
request_params = {
1212-
key: value
1213-
for key, value in (params or {}).items()
1214-
if value is not None
1215-
}
1216-
if request_deadline is None:
1217-
response = self._client.transport.client.request(
1218-
method,
1219-
self._client._build_url(path),
1220-
params=request_params,
1221-
json=data,
1222-
)
1223-
else:
1224-
response = self._request_with_deadline(
1225-
method, path, request_params, data, request_deadline
1226-
)
1180+
response = self._client.transport.client.request(
1181+
method,
1182+
self._client._build_url(path),
1183+
params={
1184+
key: value
1185+
for key, value in (params or {}).items()
1186+
if value is not None
1187+
},
1188+
json=data,
1189+
)
12271190
ensure_response_ok(response, "control")
12281191
except BaseException as cause:
1229-
if not isinstance(cause, Exception):
1230-
raise
1231-
if (
1232-
request_deadline is not None
1233-
and time.monotonic() >= request_deadline
1234-
):
1235-
raise TimeoutError(
1236-
"Timed out waiting for command output"
1237-
) from cause
12381192
error = normalize_network_error(
12391193
cause,
12401194
"control",
@@ -1243,39 +1197,8 @@ def _request(
12431197
)
12441198
if not should_retry_get(method, error, failed_attempt):
12451199
raise error
1246-
delay = get_retry_delay_seconds(failed_attempt)
1247-
if request_deadline is not None:
1248-
delay = min(delay, max(0, request_deadline - time.monotonic()))
1249-
time.sleep(delay)
1200+
time.sleep(get_retry_delay_seconds(failed_attempt))
12501201
failed_attempt += 1
12511202
continue
12521203

12531204
return parse_json_response(response, "control")
1254-
1255-
def _request_with_deadline(self, method, path, params, data, deadline):
1256-
budget = _RequestDeadline(deadline, shared_connection=True)
1257-
response = None
1258-
try:
1259-
client = self._client.transport.client
1260-
remaining = budget.remaining()
1261-
timeouts = {
1262-
key: min(value, remaining) if value is not None else remaining
1263-
for key, value in client.timeout.as_dict().items()
1264-
}
1265-
request = client.build_request(
1266-
method,
1267-
self._client._build_url(path),
1268-
params=params,
1269-
json=data,
1270-
timeout=httpx.Timeout(**timeouts),
1271-
extensions={"trace": budget.trace},
1272-
)
1273-
response = client.send(request, stream=True)
1274-
budget.capture_response(response)
1275-
response.read()
1276-
budget.remaining()
1277-
return response
1278-
finally:
1279-
budget.close()
1280-
if response is not None:
1281-
response.close()

‎hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_processes.py‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ def _wait_with_secrets(
109109
chunk_ms, request_timeout, _ = wait.request_budget()
110110
started_at = time.monotonic()
111111
try:
112-
result = self._wait_once(chunk_ms, None, request_timeout, wait.deadline)
112+
result = self._wait_once(chunk_ms, None, request_timeout)
113113
wait.remaining()
114114
return result
115115
except TimeoutError as error:
@@ -133,13 +133,10 @@ def _wait_once(
133133
timeout_ms: Optional[int],
134134
timeout_sec: Optional[int],
135135
request_timeout: Optional[float] = None,
136-
request_deadline: Optional[float] = None,
137136
) -> SandboxProcessResult:
138137
request_options: Dict[str, Any] = {}
139138
if request_timeout is not None:
140139
request_options["request_timeout"] = request_timeout
141-
if request_deadline is not None:
142-
request_options["request_deadline"] = request_deadline
143140
payload = self._transport.request_json(
144141
f"/sandbox/processes/{self.id}/wait",
145142
method="POST",

0 commit comments

Comments
 (0)