Skip to content

Commit c795c3c

Browse files
Fail fast on reused coroutine callback errors
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent 39d8aff commit c795c3c

2 files changed

Lines changed: 88 additions & 0 deletions

File tree

hyperbrowser/client/polling.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,10 @@ def _invoke_non_retryable_callback(
122122

123123

124124
def _is_retryable_exception(exc: Exception) -> bool:
125+
if isinstance(
126+
exc, RuntimeError
127+
) and "cannot reuse already awaited coroutine" in str(exc):
128+
return False
125129
if isinstance(exc, ConcurrentCancelledError):
126130
return False
127131
if isinstance(exc, _NonRetryablePollingError):

tests/test_polling.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1007,6 +1007,60 @@ async def operation() -> str:
10071007
asyncio.run(run())
10081008

10091009

1010+
def test_poll_until_terminal_status_async_does_not_retry_reused_coroutines():
1011+
async def run() -> None:
1012+
attempts = {"count": 0}
1013+
shared_status_coroutine = asyncio.sleep(0, result="running")
1014+
1015+
async def get_status() -> str:
1016+
attempts["count"] += 1
1017+
return await shared_status_coroutine
1018+
1019+
with pytest.raises(
1020+
RuntimeError, match="cannot reuse already awaited coroutine"
1021+
):
1022+
await poll_until_terminal_status_async(
1023+
operation_name="async poll reused coroutine",
1024+
get_status=get_status,
1025+
is_terminal_status=lambda value: value == "completed",
1026+
poll_interval_seconds=0.0001,
1027+
max_wait_seconds=1.0,
1028+
max_status_failures=5,
1029+
)
1030+
1031+
assert attempts["count"] == 2
1032+
1033+
asyncio.run(run())
1034+
1035+
1036+
def test_retry_operation_async_does_not_retry_reused_coroutines():
1037+
async def run() -> None:
1038+
attempts = {"count": 0}
1039+
1040+
async def shared_operation() -> str:
1041+
raise ValueError("transient")
1042+
1043+
shared_operation_coroutine = shared_operation()
1044+
1045+
async def operation() -> str:
1046+
attempts["count"] += 1
1047+
return await shared_operation_coroutine
1048+
1049+
with pytest.raises(
1050+
RuntimeError, match="cannot reuse already awaited coroutine"
1051+
):
1052+
await retry_operation_async(
1053+
operation_name="async retry reused coroutine",
1054+
operation=operation,
1055+
max_attempts=5,
1056+
retry_delay_seconds=0.0001,
1057+
)
1058+
1059+
assert attempts["count"] == 2
1060+
1061+
asyncio.run(run())
1062+
1063+
10101064
def test_async_poll_until_terminal_status_allows_immediate_terminal_on_zero_max_wait():
10111065
async def run() -> None:
10121066
status = await poll_until_terminal_status_async(
@@ -2056,6 +2110,36 @@ async def get_next_page(page: int) -> dict:
20562110
asyncio.run(run())
20572111

20582112

2113+
def test_collect_paginated_results_async_does_not_retry_reused_coroutines():
2114+
async def run() -> None:
2115+
attempts = {"count": 0}
2116+
shared_page_coroutine = asyncio.sleep(
2117+
0, result={"current": 1, "total": 2, "items": []}
2118+
)
2119+
2120+
async def get_next_page(page: int) -> dict:
2121+
attempts["count"] += 1
2122+
return await shared_page_coroutine
2123+
2124+
with pytest.raises(
2125+
RuntimeError, match="cannot reuse already awaited coroutine"
2126+
):
2127+
await collect_paginated_results_async(
2128+
operation_name="async paginated reused coroutine",
2129+
get_next_page=get_next_page,
2130+
get_current_page_batch=lambda response: response["current"],
2131+
get_total_page_batches=lambda response: response["total"],
2132+
on_page_success=lambda response: None,
2133+
max_wait_seconds=1.0,
2134+
max_attempts=5,
2135+
retry_delay_seconds=0.0001,
2136+
)
2137+
2138+
assert attempts["count"] == 2
2139+
2140+
asyncio.run(run())
2141+
2142+
20592143
def test_wait_for_job_result_returns_fetched_value():
20602144
status_values = iter(["running", "completed"])
20612145

0 commit comments

Comments
 (0)