Skip to content

Commit 8c8e5d2

Browse files
Treat invalid-state errors as non-retryable
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent f57991b commit 8c8e5d2

File tree

2 files changed

+245
-2
lines changed

2 files changed

+245
-2
lines changed

hyperbrowser/client/polling.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import asyncio
2-
from concurrent.futures import CancelledError as ConcurrentCancelledError
32
from concurrent.futures import BrokenExecutor as ConcurrentBrokenExecutor
3+
from concurrent.futures import CancelledError as ConcurrentCancelledError
4+
from concurrent.futures import InvalidStateError as ConcurrentInvalidStateError
45
import inspect
56
import math
67
from numbers import Real
@@ -206,6 +207,8 @@ def _normalize_status_code_for_retry(status_code: object) -> Optional[int]:
206207
def _is_retryable_exception(exc: Exception) -> bool:
207208
if isinstance(exc, ConcurrentBrokenExecutor):
208209
return False
210+
if isinstance(exc, (asyncio.InvalidStateError, ConcurrentInvalidStateError)):
211+
return False
209212
if isinstance(exc, (StopIteration, StopAsyncIteration)):
210213
return False
211214
if _is_generator_reentrancy_error(exc):

tests/test_polling.py

Lines changed: 241 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import asyncio
2-
from concurrent.futures import CancelledError as ConcurrentCancelledError
32
from concurrent.futures import BrokenExecutor as ConcurrentBrokenExecutor
3+
from concurrent.futures import CancelledError as ConcurrentCancelledError
4+
from concurrent.futures import InvalidStateError as ConcurrentInvalidStateError
45
import math
56
from fractions import Fraction
67

@@ -287,6 +288,26 @@ def get_status() -> str:
287288
assert attempts["count"] == 1
288289

289290

291+
def test_poll_until_terminal_status_does_not_retry_invalid_state_errors():
292+
attempts = {"count": 0}
293+
294+
def get_status() -> str:
295+
attempts["count"] += 1
296+
raise asyncio.InvalidStateError("invalid async state")
297+
298+
with pytest.raises(asyncio.InvalidStateError, match="invalid async state"):
299+
poll_until_terminal_status(
300+
operation_name="sync poll invalid-state passthrough",
301+
get_status=get_status,
302+
is_terminal_status=lambda value: value == "completed",
303+
poll_interval_seconds=0.0001,
304+
max_wait_seconds=1.0,
305+
max_status_failures=5,
306+
)
307+
308+
assert attempts["count"] == 1
309+
310+
290311
def test_poll_until_terminal_status_does_not_retry_executor_shutdown_runtime_errors():
291312
attempts = {"count": 0}
292313

@@ -754,6 +775,24 @@ def operation() -> str:
754775
assert attempts["count"] == 1
755776

756777

778+
def test_retry_operation_does_not_retry_invalid_state_errors():
779+
attempts = {"count": 0}
780+
781+
def operation() -> str:
782+
attempts["count"] += 1
783+
raise ConcurrentInvalidStateError("invalid executor state")
784+
785+
with pytest.raises(ConcurrentInvalidStateError, match="invalid executor state"):
786+
retry_operation(
787+
operation_name="sync retry invalid-state passthrough",
788+
operation=operation,
789+
max_attempts=5,
790+
retry_delay_seconds=0.0001,
791+
)
792+
793+
assert attempts["count"] == 1
794+
795+
757796
def test_retry_operation_retries_server_errors():
758797
attempts = {"count": 0}
759798

@@ -1158,6 +1197,29 @@ async def get_status() -> str:
11581197
asyncio.run(run())
11591198

11601199

1200+
def test_poll_until_terminal_status_async_does_not_retry_invalid_state_errors():
1201+
async def run() -> None:
1202+
attempts = {"count": 0}
1203+
1204+
async def get_status() -> str:
1205+
attempts["count"] += 1
1206+
raise asyncio.InvalidStateError("invalid async state")
1207+
1208+
with pytest.raises(asyncio.InvalidStateError, match="invalid async state"):
1209+
await poll_until_terminal_status_async(
1210+
operation_name="async poll invalid-state passthrough",
1211+
get_status=get_status,
1212+
is_terminal_status=lambda value: value == "completed",
1213+
poll_interval_seconds=0.0001,
1214+
max_wait_seconds=1.0,
1215+
max_status_failures=5,
1216+
)
1217+
1218+
assert attempts["count"] == 1
1219+
1220+
asyncio.run(run())
1221+
1222+
11611223
def test_poll_until_terminal_status_async_retries_server_errors():
11621224
async def run() -> None:
11631225
attempts = {"count": 0}
@@ -1402,6 +1464,27 @@ async def operation() -> str:
14021464
asyncio.run(run())
14031465

14041466

1467+
def test_retry_operation_async_does_not_retry_invalid_state_errors():
1468+
async def run() -> None:
1469+
attempts = {"count": 0}
1470+
1471+
async def operation() -> str:
1472+
attempts["count"] += 1
1473+
raise ConcurrentInvalidStateError("invalid executor state")
1474+
1475+
with pytest.raises(ConcurrentInvalidStateError, match="invalid executor state"):
1476+
await retry_operation_async(
1477+
operation_name="async retry invalid-state passthrough",
1478+
operation=operation,
1479+
max_attempts=5,
1480+
retry_delay_seconds=0.0001,
1481+
)
1482+
1483+
assert attempts["count"] == 1
1484+
1485+
asyncio.run(run())
1486+
1487+
14051488
def test_retry_operation_async_does_not_retry_executor_shutdown_runtime_errors():
14061489
async def run() -> None:
14071490
attempts = {"count": 0}
@@ -2519,6 +2602,28 @@ def get_next_page(page: int) -> dict:
25192602
assert attempts["count"] == 1
25202603

25212604

2605+
def test_collect_paginated_results_does_not_retry_invalid_state_errors():
2606+
attempts = {"count": 0}
2607+
2608+
def get_next_page(page: int) -> dict:
2609+
attempts["count"] += 1
2610+
raise asyncio.InvalidStateError("invalid async state")
2611+
2612+
with pytest.raises(asyncio.InvalidStateError, match="invalid async state"):
2613+
collect_paginated_results(
2614+
operation_name="sync paginated invalid-state passthrough",
2615+
get_next_page=get_next_page,
2616+
get_current_page_batch=lambda response: response["current"],
2617+
get_total_page_batches=lambda response: response["total"],
2618+
on_page_success=lambda response: None,
2619+
max_wait_seconds=1.0,
2620+
max_attempts=5,
2621+
retry_delay_seconds=0.0001,
2622+
)
2623+
2624+
assert attempts["count"] == 1
2625+
2626+
25222627
def test_collect_paginated_results_does_not_retry_executor_shutdown_runtime_errors():
25232628
attempts = {"count": 0}
25242629

@@ -2934,6 +3039,31 @@ async def get_next_page(page: int) -> dict:
29343039
asyncio.run(run())
29353040

29363041

3042+
def test_collect_paginated_results_async_does_not_retry_invalid_state_errors():
3043+
async def run() -> None:
3044+
attempts = {"count": 0}
3045+
3046+
async def get_next_page(page: int) -> dict:
3047+
attempts["count"] += 1
3048+
raise ConcurrentInvalidStateError("invalid executor state")
3049+
3050+
with pytest.raises(ConcurrentInvalidStateError, match="invalid executor state"):
3051+
await collect_paginated_results_async(
3052+
operation_name="async paginated invalid-state passthrough",
3053+
get_next_page=get_next_page,
3054+
get_current_page_batch=lambda response: response["current"],
3055+
get_total_page_batches=lambda response: response["total"],
3056+
on_page_success=lambda response: None,
3057+
max_wait_seconds=1.0,
3058+
max_attempts=5,
3059+
retry_delay_seconds=0.0001,
3060+
)
3061+
3062+
assert attempts["count"] == 1
3063+
3064+
asyncio.run(run())
3065+
3066+
29373067
def test_collect_paginated_results_async_does_not_retry_executor_shutdown_runtime_errors():
29383068
async def run() -> None:
29393069
attempts = {"count": 0}
@@ -3209,6 +3339,35 @@ def fetch_result() -> dict:
32093339
assert fetch_attempts["count"] == 0
32103340

32113341

3342+
def test_wait_for_job_result_does_not_retry_invalid_state_status_errors():
3343+
status_attempts = {"count": 0}
3344+
fetch_attempts = {"count": 0}
3345+
3346+
def get_status() -> str:
3347+
status_attempts["count"] += 1
3348+
raise asyncio.InvalidStateError("invalid async state")
3349+
3350+
def fetch_result() -> dict:
3351+
fetch_attempts["count"] += 1
3352+
return {"ok": True}
3353+
3354+
with pytest.raises(asyncio.InvalidStateError, match="invalid async state"):
3355+
wait_for_job_result(
3356+
operation_name="sync wait helper status invalid-state",
3357+
get_status=get_status,
3358+
is_terminal_status=lambda value: value == "completed",
3359+
fetch_result=fetch_result,
3360+
poll_interval_seconds=0.0001,
3361+
max_wait_seconds=1.0,
3362+
max_status_failures=5,
3363+
fetch_max_attempts=5,
3364+
fetch_retry_delay_seconds=0.0001,
3365+
)
3366+
3367+
assert status_attempts["count"] == 1
3368+
assert fetch_attempts["count"] == 0
3369+
3370+
32123371
def test_wait_for_job_result_does_not_retry_executor_shutdown_status_errors():
32133372
status_attempts = {"count": 0}
32143373
fetch_attempts = {"count": 0}
@@ -3472,6 +3631,29 @@ def fetch_result() -> dict:
34723631
assert fetch_attempts["count"] == 1
34733632

34743633

3634+
def test_wait_for_job_result_does_not_retry_invalid_state_fetch_errors():
3635+
fetch_attempts = {"count": 0}
3636+
3637+
def fetch_result() -> dict:
3638+
fetch_attempts["count"] += 1
3639+
raise ConcurrentInvalidStateError("invalid executor state")
3640+
3641+
with pytest.raises(ConcurrentInvalidStateError, match="invalid executor state"):
3642+
wait_for_job_result(
3643+
operation_name="sync wait helper fetch invalid-state",
3644+
get_status=lambda: "completed",
3645+
is_terminal_status=lambda value: value == "completed",
3646+
fetch_result=fetch_result,
3647+
poll_interval_seconds=0.0001,
3648+
max_wait_seconds=1.0,
3649+
max_status_failures=5,
3650+
fetch_max_attempts=5,
3651+
fetch_retry_delay_seconds=0.0001,
3652+
)
3653+
3654+
assert fetch_attempts["count"] == 1
3655+
3656+
34753657
def test_wait_for_job_result_does_not_retry_executor_shutdown_fetch_errors():
34763658
fetch_attempts = {"count": 0}
34773659

@@ -3819,6 +4001,38 @@ async def fetch_result() -> dict:
38194001
asyncio.run(run())
38204002

38214003

4004+
def test_wait_for_job_result_async_does_not_retry_invalid_state_status_errors():
4005+
async def run() -> None:
4006+
status_attempts = {"count": 0}
4007+
fetch_attempts = {"count": 0}
4008+
4009+
async def get_status() -> str:
4010+
status_attempts["count"] += 1
4011+
raise ConcurrentInvalidStateError("invalid executor state")
4012+
4013+
async def fetch_result() -> dict:
4014+
fetch_attempts["count"] += 1
4015+
return {"ok": True}
4016+
4017+
with pytest.raises(ConcurrentInvalidStateError, match="invalid executor state"):
4018+
await wait_for_job_result_async(
4019+
operation_name="async wait helper status invalid-state",
4020+
get_status=get_status,
4021+
is_terminal_status=lambda value: value == "completed",
4022+
fetch_result=fetch_result,
4023+
poll_interval_seconds=0.0001,
4024+
max_wait_seconds=1.0,
4025+
max_status_failures=5,
4026+
fetch_max_attempts=5,
4027+
fetch_retry_delay_seconds=0.0001,
4028+
)
4029+
4030+
assert status_attempts["count"] == 1
4031+
assert fetch_attempts["count"] == 0
4032+
4033+
asyncio.run(run())
4034+
4035+
38224036
def test_wait_for_job_result_async_does_not_retry_executor_shutdown_status_errors():
38234037
async def run() -> None:
38244038
status_attempts = {"count": 0}
@@ -4106,6 +4320,32 @@ async def fetch_result() -> dict:
41064320
asyncio.run(run())
41074321

41084322

4323+
def test_wait_for_job_result_async_does_not_retry_invalid_state_fetch_errors():
4324+
async def run() -> None:
4325+
fetch_attempts = {"count": 0}
4326+
4327+
async def fetch_result() -> dict:
4328+
fetch_attempts["count"] += 1
4329+
raise asyncio.InvalidStateError("invalid async state")
4330+
4331+
with pytest.raises(asyncio.InvalidStateError, match="invalid async state"):
4332+
await wait_for_job_result_async(
4333+
operation_name="async wait helper fetch invalid-state",
4334+
get_status=lambda: asyncio.sleep(0, result="completed"),
4335+
is_terminal_status=lambda value: value == "completed",
4336+
fetch_result=fetch_result,
4337+
poll_interval_seconds=0.0001,
4338+
max_wait_seconds=1.0,
4339+
max_status_failures=5,
4340+
fetch_max_attempts=5,
4341+
fetch_retry_delay_seconds=0.0001,
4342+
)
4343+
4344+
assert fetch_attempts["count"] == 1
4345+
4346+
asyncio.run(run())
4347+
4348+
41094349
def test_wait_for_job_result_async_does_not_retry_executor_shutdown_fetch_errors():
41104350
async def run() -> None:
41114351
fetch_attempts = {"count": 0}

0 commit comments

Comments
 (0)