Skip to content

Commit f8c8500

Browse files
Fail fast on async loop contract runtime errors
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent 0e43836 commit f8c8500

2 files changed

Lines changed: 52 additions & 0 deletions

File tree

hyperbrowser/client/polling.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,11 +128,22 @@ def _is_reused_coroutine_runtime_error(exc: Exception) -> bool:
128128
return "coroutine" in normalized_message and "already awaited" in normalized_message
129129

130130

131+
def _is_async_loop_contract_runtime_error(exc: Exception) -> bool:
132+
if not isinstance(exc, RuntimeError):
133+
return False
134+
normalized_message = str(exc).lower()
135+
if "event loop is closed" in normalized_message:
136+
return True
137+
return "different loop" in normalized_message and "future" in normalized_message
138+
139+
131140
def _is_retryable_exception(exc: Exception) -> bool:
132141
if isinstance(exc, (StopIteration, StopAsyncIteration)):
133142
return False
134143
if _is_reused_coroutine_runtime_error(exc):
135144
return False
145+
if _is_async_loop_contract_runtime_error(exc):
146+
return False
136147
if isinstance(exc, ConcurrentCancelledError):
137148
return False
138149
if isinstance(exc, _NonRetryablePollingError):

tests/test_polling.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1184,6 +1184,47 @@ async def get_status() -> str:
11841184
asyncio.run(run())
11851185

11861186

1187+
def test_retry_operation_does_not_retry_runtime_errors_for_loop_mismatch():
1188+
attempts = {"count": 0}
1189+
1190+
def operation() -> str:
1191+
attempts["count"] += 1
1192+
raise RuntimeError("Task got Future attached to a different loop")
1193+
1194+
with pytest.raises(RuntimeError, match="different loop"):
1195+
retry_operation(
1196+
operation_name="sync retry loop-mismatch runtime error",
1197+
operation=operation,
1198+
max_attempts=5,
1199+
retry_delay_seconds=0.0001,
1200+
)
1201+
1202+
assert attempts["count"] == 1
1203+
1204+
1205+
def test_poll_until_terminal_status_async_does_not_retry_runtime_errors_for_closed_loop():
1206+
async def run() -> None:
1207+
attempts = {"count": 0}
1208+
1209+
async def get_status() -> str:
1210+
attempts["count"] += 1
1211+
raise RuntimeError("Event loop is closed")
1212+
1213+
with pytest.raises(RuntimeError, match="Event loop is closed"):
1214+
await poll_until_terminal_status_async(
1215+
operation_name="async poll closed-loop runtime error",
1216+
get_status=get_status,
1217+
is_terminal_status=lambda value: value == "completed",
1218+
poll_interval_seconds=0.0001,
1219+
max_wait_seconds=1.0,
1220+
max_status_failures=5,
1221+
)
1222+
1223+
assert attempts["count"] == 1
1224+
1225+
asyncio.run(run())
1226+
1227+
11871228
def test_async_poll_until_terminal_status_allows_immediate_terminal_on_zero_max_wait():
11881229
async def run() -> None:
11891230
status = await poll_until_terminal_status_async(

0 commit comments

Comments
 (0)