Skip to content

Commit 6b1f5b3

Browse files
Cover executor-shutdown runtime errors as non-retryable
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent c914fb1 commit 6b1f5b3

File tree

2 files changed

+59
-0
lines changed

2 files changed

+59
-0
lines changed

hyperbrowser/client/polling.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,13 +152,25 @@ def _is_async_loop_contract_runtime_error(exc: Exception) -> bool:
152152
return True
153153
if "event loop other than the current one" in normalized_message:
154154
return True
155+
if "attached to a different loop" in normalized_message:
156+
return True
155157
if "different event loop" in normalized_message:
156158
return True
157159
return "different loop" in normalized_message and any(
158160
marker in normalized_message for marker in ("future", "task")
159161
)
160162

161163

164+
def _is_executor_shutdown_runtime_error(exc: Exception) -> bool:
165+
if not isinstance(exc, RuntimeError):
166+
return False
167+
normalized_message = str(exc).lower()
168+
return (
169+
"cannot schedule new futures after" in normalized_message
170+
and "shutdown" in normalized_message
171+
)
172+
173+
162174
def _is_retryable_exception(exc: Exception) -> bool:
163175
if isinstance(exc, (StopIteration, StopAsyncIteration)):
164176
return False
@@ -170,6 +182,8 @@ def _is_retryable_exception(exc: Exception) -> bool:
170182
return False
171183
if _is_async_loop_contract_runtime_error(exc):
172184
return False
185+
if _is_executor_shutdown_runtime_error(exc):
186+
return False
173187
if isinstance(exc, ConcurrentCancelledError):
174188
return False
175189
if isinstance(exc, _NonRetryablePollingError):

tests/test_polling.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1365,6 +1365,26 @@ def operation() -> str:
13651365
assert attempts["count"] == 1
13661366

13671367

1368+
def test_retry_operation_does_not_retry_executor_shutdown_runtime_errors():
1369+
attempts = {"count": 0}
1370+
1371+
def operation() -> str:
1372+
attempts["count"] += 1
1373+
raise RuntimeError("cannot schedule new futures after interpreter shutdown")
1374+
1375+
with pytest.raises(
1376+
RuntimeError, match="cannot schedule new futures after interpreter shutdown"
1377+
):
1378+
retry_operation(
1379+
operation_name="sync retry executor-shutdown runtime error",
1380+
operation=operation,
1381+
max_attempts=5,
1382+
retry_delay_seconds=0.0001,
1383+
)
1384+
1385+
assert attempts["count"] == 1
1386+
1387+
13681388
def test_poll_until_terminal_status_async_does_not_retry_runtime_errors_for_closed_loop():
13691389
async def run() -> None:
13701390
attempts = {"count": 0}
@@ -1388,6 +1408,31 @@ async def get_status() -> str:
13881408
asyncio.run(run())
13891409

13901410

1411+
def test_poll_until_terminal_status_async_does_not_retry_executor_shutdown_runtime_errors():
1412+
async def run() -> None:
1413+
attempts = {"count": 0}
1414+
1415+
async def get_status() -> str:
1416+
attempts["count"] += 1
1417+
raise RuntimeError("cannot schedule new futures after shutdown")
1418+
1419+
with pytest.raises(
1420+
RuntimeError, match="cannot schedule new futures after shutdown"
1421+
):
1422+
await poll_until_terminal_status_async(
1423+
operation_name="async poll executor-shutdown runtime error",
1424+
get_status=get_status,
1425+
is_terminal_status=lambda value: value == "completed",
1426+
poll_interval_seconds=0.0001,
1427+
max_wait_seconds=1.0,
1428+
max_status_failures=5,
1429+
)
1430+
1431+
assert attempts["count"] == 1
1432+
1433+
asyncio.run(run())
1434+
1435+
13911436
def test_poll_until_terminal_status_async_does_not_retry_runtime_errors_for_non_thread_safe_loop_operation():
13921437
async def run() -> None:
13931438
attempts = {"count": 0}

0 commit comments

Comments
 (0)