Skip to content

Commit 26e409a

Browse files
authored
fix: additional polling updates (#794)
1 parent 6d69e3c commit 26e409a

3 files changed

Lines changed: 72 additions & 25 deletions

File tree

src/runloop_api_client/lib/polling.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import time
2-
from typing import Any, TypeVar, Callable, Optional
2+
from typing import Any, Union, TypeVar, Callable, Optional
33
from dataclasses import dataclass
44

55
T = TypeVar("T")
@@ -73,3 +73,46 @@ def poll_until(
7373
raise PollingTimeout(f"Exceeded timeout of {config.timeout_seconds} seconds", last_result)
7474

7575
time.sleep(config.interval_seconds)
76+
77+
78+
def retry_server_poll_until(
79+
retriever: Callable[[float], T],
80+
is_terminal: Callable[[T], bool],
81+
timeout_seconds: float = 30.0,
82+
on_error: Optional[Callable[[Exception], T]] = None,
83+
) -> T:
84+
"""
85+
Retry a server-side long-poll until a condition is met or max timeout is reached.
86+
87+
Args:
88+
retriever: Callable that takes the remaining timeout (seconds) and
89+
returns the object to check.
90+
is_terminal: Callable that returns True when polling should stop
91+
timeout_seconds: Total time to wait. Must be > 0
92+
on_error: Optional error handler that can return a value to continue polling
93+
or re-raise the exception to stop polling
94+
95+
Returns:
96+
The final state of the polled object
97+
98+
Raises:
99+
PollingTimeout: When max attempts or timeout is reached
100+
"""
101+
last_result: Union[T, None] = None
102+
start_time = time.time()
103+
104+
while True:
105+
remaining_time = timeout_seconds - (time.time() - start_time)
106+
if remaining_time <= 0:
107+
raise PollingTimeout(f"Exceeded timeout of {timeout_seconds} seconds", last_result)
108+
109+
try:
110+
last_result = retriever(remaining_time)
111+
except Exception as e:
112+
if on_error is not None:
113+
last_result = on_error(e)
114+
else:
115+
raise
116+
117+
if is_terminal(last_result):
118+
return last_result

src/runloop_api_client/lib/polling_async.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ async def async_poll_until(
6060
await asyncio.sleep(config.interval_seconds)
6161

6262

63-
async def retry_server_poll_until(
63+
async def async_retry_server_poll_until(
6464
retriever: Callable[[float], Awaitable[T]],
6565
is_terminal: Callable[[T], bool],
6666
timeout_seconds: float = 30.0,

src/runloop_api_client/resources/devboxes/devboxes.py

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
AsyncDiskSnapshotsCursorIDPage,
7373
)
7474
from ..._exceptions import RunloopError, APIStatusError, APITimeoutError
75-
from ...lib.polling import PollingConfig, poll_until
75+
from ...lib.polling import PollingConfig, poll_until, retry_server_poll_until as sync_retry_server_poll_until
7676
from ..._base_client import AsyncPaginator, make_request_options
7777
from .disk_snapshots import (
7878
DiskSnapshotsResource,
@@ -82,7 +82,7 @@
8282
DiskSnapshotsResourceWithStreamingResponse,
8383
AsyncDiskSnapshotsResourceWithStreamingResponse,
8484
)
85-
from ...lib.polling_async import async_poll_until, retry_server_poll_until
85+
from ...lib.polling_async import async_poll_until, async_retry_server_poll_until
8686
from ...types.devbox_view import DevboxView
8787
from ...types.tunnel_view import TunnelView
8888
from ...types.shared_params.mount import Mount
@@ -397,30 +397,31 @@ def await_running(
397397
RunloopError: If devbox enters a non-running terminal state
398398
"""
399399

400-
def wait_for_devbox_status() -> DevboxView:
401-
# This wait_for_status endpoint polls the devbox status for 10 seconds until it reaches either running or failure.
402-
# If it's neither, it will throw an error.
403-
return self._post(
404-
f"/v1/devboxes/{id}/wait_for_status",
405-
body={"statuses": ["running", "failure", "shutdown"]},
406-
cast_to=DevboxView,
407-
)
408-
409-
def handle_timeout_error(error: Exception) -> DevboxView:
410-
# Handle timeout errors by returning current devbox state to continue polling
411-
if isinstance(error, APITimeoutError) or (
412-
isinstance(error, APIStatusError) and error.response.status_code == 408
413-
):
414-
# Return a placeholder result to continue polling
415-
return placeholder_devbox_view(id)
416-
417-
# Re-raise other errors to stop polling
418-
raise error
400+
def wait_for_devbox_status(remaining_timeout_seconds: float) -> DevboxView:
401+
try:
402+
return self._post(
403+
f"/v1/devboxes/{id}/wait_for_status",
404+
body={"statuses": ["running", "failure", "shutdown"], "timeout_seconds": remaining_timeout_seconds},
405+
cast_to=DevboxView,
406+
options={"max_retries": 0},
407+
)
408+
except (APITimeoutError, APIStatusError) as error:
409+
if isinstance(error, APITimeoutError) or error.response.status_code == 408:
410+
return placeholder_devbox_view(id)
411+
raise
419412

420413
def is_done_booting(devbox: DevboxView) -> bool:
421414
return devbox.status not in DEVBOX_BOOTING_STATES
422415

423-
devbox = poll_until(wait_for_devbox_status, is_done_booting, polling_config, handle_timeout_error)
416+
config = polling_config
417+
if not config:
418+
config = PollingConfig()
419+
420+
timeout = config.interval_seconds * config.max_attempts
421+
if config.timeout_seconds is not None and config.timeout_seconds > 0:
422+
timeout = min(config.timeout_seconds, timeout)
423+
424+
devbox = sync_retry_server_poll_until(wait_for_devbox_status, is_done_booting, timeout)
424425

425426
if devbox.status != "running":
426427
raise RunloopError(f"Devbox entered non-running terminal state: {devbox.status}")
@@ -452,6 +453,7 @@ def wait_for_devbox_status() -> DevboxView:
452453
f"/v1/devboxes/{id}/wait_for_status",
453454
body={"statuses": list(DEVBOX_TERMINAL_STATES)},
454455
cast_to=DevboxView,
456+
options={"max_retries": 0},
455457
)
456458

457459
def handle_timeout_error(error: Exception) -> DevboxView:
@@ -2063,6 +2065,7 @@ async def wait_for_devbox_status(remaining_timeout_seconds: float) -> DevboxView
20632065
f"/v1/devboxes/{id}/wait_for_status",
20642066
body={"statuses": ["running", "failure", "shutdown"], "timeout_seconds": remaining_timeout_seconds},
20652067
cast_to=DevboxView,
2068+
options={"max_retries": 0},
20662069
)
20672070
except (APITimeoutError, APIStatusError) as error:
20682071
# Handle timeout errors by returning current devbox state to continue polling
@@ -2088,7 +2091,7 @@ def is_done_booting(devbox: DevboxView) -> bool:
20882091
if config.timeout_seconds is not None and config.timeout_seconds > 0:
20892092
timeout = min(config.timeout_seconds, timeout)
20902093

2091-
devbox = await retry_server_poll_until(wait_for_devbox_status, is_done_booting, timeout)
2094+
devbox = await async_retry_server_poll_until(wait_for_devbox_status, is_done_booting, timeout)
20922095

20932096
if devbox.status != "running":
20942097
raise RunloopError(f"Devbox entered non-running terminal state: {devbox.status}")
@@ -2121,6 +2124,7 @@ async def wait_for_devbox_status() -> DevboxView:
21212124
f"/v1/devboxes/{id}/wait_for_status",
21222125
body={"statuses": list(DEVBOX_TERMINAL_STATES)},
21232126
cast_to=DevboxView,
2127+
options={"max_retries": 0},
21242128
)
21252129
except (APITimeoutError, APIStatusError) as error:
21262130
if isinstance(error, APITimeoutError) or error.response.status_code == 408:

0 commit comments

Comments
 (0)