Skip to content

Commit bc470ba

Browse files
committed
Use new wait for status endpoint in await_running
1 parent 8e559a9 commit bc470ba

3 files changed

Lines changed: 89 additions & 32 deletions

File tree

src/runloop_api_client/lib/polling.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ def poll_until(
2121
retriever: Callable[[], T],
2222
is_terminal: Callable[[T], bool],
2323
config: Optional[PollingConfig] = None,
24+
on_error: Optional[Callable[[Exception], T]] = None,
2425
) -> T:
2526
"""
2627
Poll until a condition is met or timeout/max attempts are reached.
@@ -29,6 +30,8 @@ def poll_until(
2930
retriever: Callable that returns the object to check
3031
is_terminal: Callable that returns True when polling should stop
3132
config: Optional polling configuration
33+
on_error: Optional error handler that can return a value to continue polling
34+
or re-raise the exception to stop polling
3235
3336
Returns:
3437
The final state of the polled object
@@ -44,7 +47,13 @@ def poll_until(
4447
last_result = None
4548

4649
while True:
47-
last_result = retriever()
50+
try:
51+
last_result = retriever()
52+
except Exception as e:
53+
if on_error is not None:
54+
last_result = on_error(e)
55+
else:
56+
raise
4857

4958
if is_terminal(last_result):
5059
return last_result

src/runloop_api_client/lib/polling_async.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ async def async_poll_until(
1010
retriever: Callable[[], Awaitable[T]],
1111
is_terminal: Callable[[T], bool],
1212
config: Optional[PollingConfig] = None,
13+
on_error: Optional[Callable[[Exception], T]] = None,
1314
) -> T:
1415
"""
1516
Poll until a condition is met or timeout/max attempts are reached.
@@ -18,6 +19,8 @@ async def async_poll_until(
1819
retriever: Async or sync callable that returns the object to check
1920
is_terminal: Callable that returns True when polling should stop
2021
config: Optional polling configuration
22+
on_error: Optional error handler that can return a value to continue polling
23+
or re-raise the exception to stop polling
2124
2225
Returns:
2326
The final state of the polled object
@@ -33,7 +36,13 @@ async def async_poll_until(
3336
last_result: Union[T, None] = None
3437

3538
while True:
36-
last_result = await retriever()
39+
try:
40+
last_result = await retriever()
41+
except Exception as e:
42+
if on_error is not None:
43+
last_result = on_error(e)
44+
else:
45+
raise
3746

3847
if is_terminal(last_result):
3948
return last_result

src/runloop_api_client/resources/devboxes/devboxes.py

Lines changed: 69 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,12 @@
108108
from ...types.shared_params.launch_parameters import LaunchParameters
109109
from ...types.devbox_async_execution_detail_view import DevboxAsyncExecutionDetailView
110110
from ...types.shared_params.code_mount_parameters import CodeMountParameters
111+
from ...types.shared.launch_parameters import LaunchParameters as SharedLaunchParameters
111112

112113
__all__ = ["DevboxesResource", "AsyncDevboxesResource"]
113114

114-
DEVBOX_BOOTING_STATES = frozenset(('provisioning', 'initializing'))
115+
DEVBOX_BOOTING_STATES = frozenset(("provisioning", "initializing"))
116+
115117

116118
class DevboxesResource(SyncAPIResource):
117119
@cached_property
@@ -357,7 +359,7 @@ def update(
357359
def await_running(
358360
self,
359361
id: str,
360-
*,
362+
*,
361363
polling_config: PollingConfig | None = None,
362364
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
363365
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -367,7 +369,7 @@ def await_running(
367369
timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
368370
) -> DevboxView:
369371
"""Wait for a devbox to be in running state.
370-
372+
371373
Args:
372374
id: The ID of the devbox to wait for
373375
config: Optional polling configuration
@@ -383,24 +385,43 @@ def await_running(
383385
PollingTimeout: If polling times out before devbox is running
384386
RunloopError: If devbox enters a non-running terminal state
385387
"""
386-
def retrieve_devbox() -> DevboxView:
387-
return self.retrieve(
388-
id,
389-
extra_headers=extra_headers,
390-
extra_query=extra_query,
391-
extra_body=extra_body,
392-
timeout=timeout
388+
389+
def wait_for_devbox_status() -> DevboxView:
390+
# This wait_for_status endpoint polls the devbox status for 10 seconds until it reaches either running or failure.
391+
# If it's neither, it will throw an error.
392+
return self._post(
393+
f"/v1/devboxes/{id}/wait_for_status",
394+
body={"statuses": ["running", "failure"]},
395+
options=make_request_options(
396+
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
397+
),
398+
cast_to=DevboxView,
393399
)
394400

401+
def handle_timeout_error(error: Exception) -> DevboxView:
402+
# Handle 408 timeout errors by returning current devbox state to continue polling
403+
if isinstance(error, httpx.HTTPStatusError) and error.response.status_code == 408:
404+
# Return a placeholder result to continue polling
405+
return DevboxView(
406+
id=id,
407+
status="provisioning",
408+
capabilities=[],
409+
create_time_ms=0,
410+
launch_parameters=SharedLaunchParameters(),
411+
metadata={},
412+
state_transitions=[],
413+
)
414+
else:
415+
# Re-raise other errors to stop polling
416+
raise error
417+
395418
def is_done_booting(devbox: DevboxView) -> bool:
396419
return devbox.status not in DEVBOX_BOOTING_STATES
397420

398-
devbox = poll_until(retrieve_devbox, is_done_booting, polling_config)
421+
devbox = poll_until(wait_for_devbox_status, is_done_booting, polling_config, handle_timeout_error)
399422

400423
if devbox.status != "running":
401-
raise RunloopError(
402-
f"Devbox entered non-running terminal state: {devbox.status}"
403-
)
424+
raise RunloopError(f"Devbox entered non-running terminal state: {devbox.status}")
404425

405426
return devbox
406427

@@ -427,7 +448,7 @@ def create_and_await_running(
427448
timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
428449
) -> DevboxView:
429450
"""Create a new devbox and wait for it to be in running state.
430-
451+
431452
Args:
432453
blueprint_id: The ID of the blueprint to use
433454
blueprint_name: The name of the blueprint to use
@@ -1660,7 +1681,7 @@ async def await_running(
16601681
timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
16611682
) -> DevboxView:
16621683
"""Wait for a devbox to be in running state.
1663-
1684+
16641685
Args:
16651686
id: The ID of the devbox to wait for
16661687
config: Optional polling configuration
@@ -1676,27 +1697,45 @@ async def await_running(
16761697
PollingTimeout: If polling times out before devbox is running
16771698
RunloopError: If devbox enters a non-running terminal state
16781699
"""
1679-
async def retrieve_devbox() -> DevboxView:
1680-
return await self.retrieve(
1681-
id,
1682-
extra_headers=extra_headers,
1683-
extra_query=extra_query,
1684-
extra_body=extra_body,
1685-
timeout=timeout
1686-
)
1687-
1700+
1701+
async def wait_for_devbox_status() -> DevboxView:
1702+
# This wait_for_status endpoint polls the devbox status for 10 seconds until it reaches either running or failure.
1703+
# If it's neither, it will throw an error.
1704+
try:
1705+
return await self._post(
1706+
f"/v1/devboxes/{id}/wait_for_status",
1707+
body={"statuses": ["running", "failure"]},
1708+
options=make_request_options(
1709+
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
1710+
),
1711+
cast_to=DevboxView,
1712+
)
1713+
except httpx.HTTPStatusError as error:
1714+
if error.response.status_code == 408:
1715+
# Handle 408 timeout errors by returning a placeholder result to continue polling
1716+
return DevboxView(
1717+
id=id,
1718+
status="provisioning",
1719+
capabilities=[],
1720+
create_time_ms=0,
1721+
launch_parameters=SharedLaunchParameters(),
1722+
metadata={},
1723+
state_transitions=[],
1724+
)
1725+
else:
1726+
# Re-raise other errors to stop polling
1727+
raise
1728+
16881729
def is_done_booting(devbox: DevboxView) -> bool:
16891730
return devbox.status not in DEVBOX_BOOTING_STATES
16901731

1691-
devbox = await async_poll_until(retrieve_devbox, is_done_booting, polling_config)
1732+
devbox = await async_poll_until(wait_for_devbox_status, is_done_booting, polling_config)
16921733

16931734
if devbox.status != "running":
1694-
raise RunloopError(
1695-
f"Devbox entered non-running terminal state: {devbox.status}"
1696-
)
1735+
raise RunloopError(f"Devbox entered non-running terminal state: {devbox.status}")
16971736

16981737
return devbox
1699-
1738+
17001739
async def update(
17011740
self,
17021741
id: str,

0 commit comments

Comments
 (0)