Skip to content

Commit fc4122d

Browse files
Treat generator reentrancy errors as non-retryable
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent def01cf commit fc4122d

2 files changed

Lines changed: 137 additions & 0 deletions

File tree

hyperbrowser/client/polling.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,12 @@ def _is_async_generator_reuse_runtime_error(exc: Exception) -> bool:
138138
)
139139

140140

141+
def _is_generator_reentrancy_error(exc: Exception) -> bool:
142+
if not isinstance(exc, ValueError):
143+
return False
144+
return "generator already executing" in str(exc).lower()
145+
146+
141147
def _is_async_loop_contract_runtime_error(exc: Exception) -> bool:
142148
if not isinstance(exc, RuntimeError):
143149
return False
@@ -156,6 +162,8 @@ def _is_async_loop_contract_runtime_error(exc: Exception) -> bool:
156162
def _is_retryable_exception(exc: Exception) -> bool:
157163
if isinstance(exc, (StopIteration, StopAsyncIteration)):
158164
return False
165+
if _is_generator_reentrancy_error(exc):
166+
return False
159167
if _is_reused_coroutine_runtime_error(exc):
160168
return False
161169
if _is_async_generator_reuse_runtime_error(exc):

tests/test_polling.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,26 @@ def get_status() -> str:
143143
assert attempts["count"] == 1
144144

145145

146+
def test_poll_until_terminal_status_does_not_retry_generator_reentrancy_errors():
147+
attempts = {"count": 0}
148+
149+
def get_status() -> str:
150+
attempts["count"] += 1
151+
raise ValueError("generator already executing")
152+
153+
with pytest.raises(ValueError, match="generator already executing"):
154+
poll_until_terminal_status(
155+
operation_name="sync poll generator-reentrancy passthrough",
156+
get_status=get_status,
157+
is_terminal_status=lambda value: value == "completed",
158+
poll_interval_seconds=0.0001,
159+
max_wait_seconds=1.0,
160+
max_status_failures=5,
161+
)
162+
163+
assert attempts["count"] == 1
164+
165+
146166
def test_poll_until_terminal_status_does_not_retry_timeout_or_polling_errors():
147167
timeout_attempts = {"count": 0}
148168

@@ -514,6 +534,24 @@ def operation() -> str:
514534
assert attempts["count"] == 1
515535

516536

537+
def test_retry_operation_does_not_retry_generator_reentrancy_errors():
538+
attempts = {"count": 0}
539+
540+
def operation() -> str:
541+
attempts["count"] += 1
542+
raise ValueError("generator already executing")
543+
544+
with pytest.raises(ValueError, match="generator already executing"):
545+
retry_operation(
546+
operation_name="sync retry generator-reentrancy passthrough",
547+
operation=operation,
548+
max_attempts=5,
549+
retry_delay_seconds=0.0001,
550+
)
551+
552+
assert attempts["count"] == 1
553+
554+
517555
def test_retry_operation_does_not_retry_timeout_or_polling_errors():
518556
timeout_attempts = {"count": 0}
519557

@@ -806,6 +844,29 @@ async def get_status() -> str:
806844
asyncio.run(run())
807845

808846

847+
def test_poll_until_terminal_status_async_does_not_retry_generator_reentrancy_errors():
848+
async def run() -> None:
849+
attempts = {"count": 0}
850+
851+
async def get_status() -> str:
852+
attempts["count"] += 1
853+
raise ValueError("generator already executing")
854+
855+
with pytest.raises(ValueError, match="generator already executing"):
856+
await poll_until_terminal_status_async(
857+
operation_name="async poll generator-reentrancy passthrough",
858+
get_status=get_status,
859+
is_terminal_status=lambda value: value == "completed",
860+
poll_interval_seconds=0.0001,
861+
max_wait_seconds=1.0,
862+
max_status_failures=5,
863+
)
864+
865+
assert attempts["count"] == 1
866+
867+
asyncio.run(run())
868+
869+
809870
def test_poll_until_terminal_status_async_does_not_retry_timeout_or_polling_errors():
810871
async def run() -> None:
811872
timeout_attempts = {"count": 0}
@@ -962,6 +1023,27 @@ async def operation() -> str:
9621023
asyncio.run(run())
9631024

9641025

1026+
def test_retry_operation_async_does_not_retry_generator_reentrancy_errors():
1027+
async def run() -> None:
1028+
attempts = {"count": 0}
1029+
1030+
async def operation() -> str:
1031+
attempts["count"] += 1
1032+
raise ValueError("generator already executing")
1033+
1034+
with pytest.raises(ValueError, match="generator already executing"):
1035+
await retry_operation_async(
1036+
operation_name="async retry generator-reentrancy passthrough",
1037+
operation=operation,
1038+
max_attempts=5,
1039+
retry_delay_seconds=0.0001,
1040+
)
1041+
1042+
assert attempts["count"] == 1
1043+
1044+
asyncio.run(run())
1045+
1046+
9651047
def test_retry_operation_async_does_not_retry_timeout_or_polling_errors():
9661048
async def run() -> None:
9671049
timeout_attempts = {"count": 0}
@@ -1981,6 +2063,28 @@ def get_next_page(page: int) -> dict:
19812063
assert attempts["count"] == 1
19822064

19832065

2066+
def test_collect_paginated_results_does_not_retry_generator_reentrancy_errors():
2067+
attempts = {"count": 0}
2068+
2069+
def get_next_page(page: int) -> dict:
2070+
attempts["count"] += 1
2071+
raise ValueError("generator already executing")
2072+
2073+
with pytest.raises(ValueError, match="generator already executing"):
2074+
collect_paginated_results(
2075+
operation_name="sync paginated generator-reentrancy passthrough",
2076+
get_next_page=get_next_page,
2077+
get_current_page_batch=lambda response: response["current"],
2078+
get_total_page_batches=lambda response: response["total"],
2079+
on_page_success=lambda response: None,
2080+
max_wait_seconds=1.0,
2081+
max_attempts=5,
2082+
retry_delay_seconds=0.0001,
2083+
)
2084+
2085+
assert attempts["count"] == 1
2086+
2087+
19842088
def test_collect_paginated_results_does_not_retry_timeout_errors():
19852089
attempts = {"count": 0}
19862090

@@ -2316,6 +2420,31 @@ async def get_next_page(page: int) -> dict:
23162420
asyncio.run(run())
23172421

23182422

2423+
def test_collect_paginated_results_async_does_not_retry_generator_reentrancy_errors():
2424+
async def run() -> None:
2425+
attempts = {"count": 0}
2426+
2427+
async def get_next_page(page: int) -> dict:
2428+
attempts["count"] += 1
2429+
raise ValueError("generator already executing")
2430+
2431+
with pytest.raises(ValueError, match="generator already executing"):
2432+
await collect_paginated_results_async(
2433+
operation_name="async paginated generator-reentrancy passthrough",
2434+
get_next_page=get_next_page,
2435+
get_current_page_batch=lambda response: response["current"],
2436+
get_total_page_batches=lambda response: response["total"],
2437+
on_page_success=lambda response: None,
2438+
max_wait_seconds=1.0,
2439+
max_attempts=5,
2440+
retry_delay_seconds=0.0001,
2441+
)
2442+
2443+
assert attempts["count"] == 1
2444+
2445+
asyncio.run(run())
2446+
2447+
23192448
def test_collect_paginated_results_async_does_not_retry_timeout_errors():
23202449
async def run() -> None:
23212450
attempts = {"count": 0}

0 commit comments

Comments
 (0)