Skip to content

Commit fdf8197

Browse files
Treat iterator exhaustion callback errors as non-retryable
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent a147f93 commit fdf8197

File tree

2 files changed

+131
-0
lines changed

2 files changed

+131
-0
lines changed

hyperbrowser/client/polling.py

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

123123

124124
def _is_retryable_exception(exc: Exception) -> bool:
125+
if isinstance(exc, (StopIteration, StopAsyncIteration)):
126+
return False
125127
if isinstance(
126128
exc, RuntimeError
127129
) and "cannot reuse already awaited coroutine" in str(exc):

tests/test_polling.py

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

125125

126+
def test_poll_until_terminal_status_does_not_retry_stop_iteration_errors():
127+
attempts = {"count": 0}
128+
129+
def get_status() -> str:
130+
attempts["count"] += 1
131+
raise StopIteration("callback exhausted")
132+
133+
with pytest.raises(StopIteration, match="callback exhausted"):
134+
poll_until_terminal_status(
135+
operation_name="sync poll stop-iteration passthrough",
136+
get_status=get_status,
137+
is_terminal_status=lambda value: value == "completed",
138+
poll_interval_seconds=0.0001,
139+
max_wait_seconds=1.0,
140+
max_status_failures=5,
141+
)
142+
143+
assert attempts["count"] == 1
144+
145+
126146
def test_poll_until_terminal_status_does_not_retry_timeout_or_polling_errors():
127147
timeout_attempts = {"count": 0}
128148

@@ -476,6 +496,24 @@ def operation() -> str:
476496
assert attempts["count"] == 1
477497

478498

499+
def test_retry_operation_does_not_retry_stop_iteration_errors():
500+
attempts = {"count": 0}
501+
502+
def operation() -> str:
503+
attempts["count"] += 1
504+
raise StopIteration("callback exhausted")
505+
506+
with pytest.raises(StopIteration, match="callback exhausted"):
507+
retry_operation(
508+
operation_name="sync retry stop-iteration passthrough",
509+
operation=operation,
510+
max_attempts=5,
511+
retry_delay_seconds=0.0001,
512+
)
513+
514+
assert attempts["count"] == 1
515+
516+
479517
def test_retry_operation_does_not_retry_timeout_or_polling_errors():
480518
timeout_attempts = {"count": 0}
481519

@@ -745,6 +783,29 @@ async def get_status() -> str:
745783
asyncio.run(run())
746784

747785

786+
def test_poll_until_terminal_status_async_does_not_retry_stop_async_iteration_errors():
787+
async def run() -> None:
788+
attempts = {"count": 0}
789+
790+
async def get_status() -> str:
791+
attempts["count"] += 1
792+
raise StopAsyncIteration("callback exhausted")
793+
794+
with pytest.raises(StopAsyncIteration, match="callback exhausted"):
795+
await poll_until_terminal_status_async(
796+
operation_name="async poll stop-async-iteration passthrough",
797+
get_status=get_status,
798+
is_terminal_status=lambda value: value == "completed",
799+
poll_interval_seconds=0.0001,
800+
max_wait_seconds=1.0,
801+
max_status_failures=5,
802+
)
803+
804+
assert attempts["count"] == 1
805+
806+
asyncio.run(run())
807+
808+
748809
def test_poll_until_terminal_status_async_does_not_retry_timeout_or_polling_errors():
749810
async def run() -> None:
750811
timeout_attempts = {"count": 0}
@@ -880,6 +941,27 @@ async def operation() -> str:
880941
asyncio.run(run())
881942

882943

944+
def test_retry_operation_async_does_not_retry_stop_async_iteration_errors():
945+
async def run() -> None:
946+
attempts = {"count": 0}
947+
948+
async def operation() -> str:
949+
attempts["count"] += 1
950+
raise StopAsyncIteration("callback exhausted")
951+
952+
with pytest.raises(StopAsyncIteration, match="callback exhausted"):
953+
await retry_operation_async(
954+
operation_name="async retry stop-async-iteration passthrough",
955+
operation=operation,
956+
max_attempts=5,
957+
retry_delay_seconds=0.0001,
958+
)
959+
960+
assert attempts["count"] == 1
961+
962+
asyncio.run(run())
963+
964+
883965
def test_retry_operation_async_does_not_retry_timeout_or_polling_errors():
884966
async def run() -> None:
885967
timeout_attempts = {"count": 0}
@@ -1666,6 +1748,28 @@ def get_next_page(page: int) -> dict:
16661748
assert attempts["count"] == 1
16671749

16681750

1751+
def test_collect_paginated_results_does_not_retry_stop_iteration_errors():
1752+
attempts = {"count": 0}
1753+
1754+
def get_next_page(page: int) -> dict:
1755+
attempts["count"] += 1
1756+
raise StopIteration("callback exhausted")
1757+
1758+
with pytest.raises(StopIteration, match="callback exhausted"):
1759+
collect_paginated_results(
1760+
operation_name="sync paginated stop-iteration passthrough",
1761+
get_next_page=get_next_page,
1762+
get_current_page_batch=lambda response: response["current"],
1763+
get_total_page_batches=lambda response: response["total"],
1764+
on_page_success=lambda response: None,
1765+
max_wait_seconds=1.0,
1766+
max_attempts=5,
1767+
retry_delay_seconds=0.0001,
1768+
)
1769+
1770+
assert attempts["count"] == 1
1771+
1772+
16691773
def test_collect_paginated_results_does_not_retry_timeout_errors():
16701774
attempts = {"count": 0}
16711775

@@ -1976,6 +2080,31 @@ async def get_next_page(page: int) -> dict:
19762080
asyncio.run(run())
19772081

19782082

2083+
def test_collect_paginated_results_async_does_not_retry_stop_async_iteration_errors():
2084+
async def run() -> None:
2085+
attempts = {"count": 0}
2086+
2087+
async def get_next_page(page: int) -> dict:
2088+
attempts["count"] += 1
2089+
raise StopAsyncIteration("callback exhausted")
2090+
2091+
with pytest.raises(StopAsyncIteration, match="callback exhausted"):
2092+
await collect_paginated_results_async(
2093+
operation_name="async paginated stop-async-iteration passthrough",
2094+
get_next_page=get_next_page,
2095+
get_current_page_batch=lambda response: response["current"],
2096+
get_total_page_batches=lambda response: response["total"],
2097+
on_page_success=lambda response: None,
2098+
max_wait_seconds=1.0,
2099+
max_attempts=5,
2100+
retry_delay_seconds=0.0001,
2101+
)
2102+
2103+
assert attempts["count"] == 1
2104+
2105+
asyncio.run(run())
2106+
2107+
19792108
def test_collect_paginated_results_async_does_not_retry_timeout_errors():
19802109
async def run() -> None:
19812110
attempts = {"count": 0}

0 commit comments

Comments
 (0)