Skip to content

Commit 5a60355

Browse files
committed
Clean up error handling in long polling
1 parent 2a9eeb5 commit 5a60355

3 files changed

Lines changed: 54 additions & 55 deletions

File tree

src/runloop_api_client/resources/devboxes/devboxes.py

Lines changed: 29 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@
8888
SyncDiskSnapshotsCursorIDPage,
8989
AsyncDiskSnapshotsCursorIDPage,
9090
)
91-
from ..._exceptions import RunloopError, APIStatusError
91+
from ..._exceptions import RunloopError, APIStatusError, APITimeoutError
9292
from ...lib.polling import PollingConfig, poll_until
9393
from ..._base_client import AsyncPaginator, make_request_options
9494
from .disk_snapshots import (
@@ -115,6 +115,18 @@
115115
DEVBOX_BOOTING_STATES = frozenset(("provisioning", "initializing"))
116116

117117

118+
def placeholder_devbox_view(id: str) -> DevboxView:
119+
return DevboxView(
120+
id=id,
121+
status="provisioning",
122+
capabilities=[],
123+
create_time_ms=0,
124+
launch_parameters=SharedLaunchParameters(),
125+
metadata={},
126+
state_transitions=[],
127+
)
128+
129+
118130
class DevboxesResource(SyncAPIResource):
119131
@cached_property
120132
def disk_snapshots(self) -> DiskSnapshotsResource:
@@ -391,21 +403,15 @@ def wait_for_devbox_status() -> DevboxView:
391403
)
392404

393405
def handle_timeout_error(error: Exception) -> DevboxView:
394-
# Handle 408 timeout errors by returning current devbox state to continue polling
395-
if isinstance(error, APIStatusError) and error.response.status_code == 408:
406+
# Handle timeout errors by returning current devbox state to continue polling
407+
if isinstance(error, APITimeoutError) or (
408+
isinstance(error, APIStatusError) and error.response.status_code == 408
409+
):
396410
# Return a placeholder result to continue polling
397-
return DevboxView(
398-
id=id,
399-
status="provisioning",
400-
capabilities=[],
401-
create_time_ms=0,
402-
launch_parameters=SharedLaunchParameters(),
403-
metadata={},
404-
state_transitions=[],
405-
)
406-
else:
407-
# Re-raise other errors to stop polling
408-
raise error
411+
return placeholder_devbox_view(id)
412+
413+
# Re-raise other errors to stop polling
414+
raise error
409415

410416
def is_done_booting(devbox: DevboxView) -> bool:
411417
return devbox.status not in DEVBOX_BOOTING_STATES
@@ -1686,21 +1692,14 @@ async def wait_for_devbox_status() -> DevboxView:
16861692
body={"statuses": ["running", "failure"]},
16871693
cast_to=DevboxView,
16881694
)
1689-
except APIStatusError as error:
1690-
if error.response.status_code == 408:
1691-
# Handle 408 timeout errors by returning a placeholder result to continue polling
1692-
return DevboxView(
1693-
id=id,
1694-
status="provisioning",
1695-
capabilities=[],
1696-
create_time_ms=0,
1697-
launch_parameters=SharedLaunchParameters(),
1698-
metadata={},
1699-
state_transitions=[],
1700-
)
1701-
else:
1702-
# Re-raise other errors to stop polling
1703-
raise
1695+
except (APITimeoutError, APIStatusError) as error:
1696+
# Handle timeout errors by returning current devbox state to continue polling
1697+
if isinstance(error, APITimeoutError) or error.response.status_code == 408:
1698+
# Return a placeholder result to continue polling
1699+
return placeholder_devbox_view(id)
1700+
1701+
# Re-raise other errors to stop polling
1702+
raise
17041703

17051704
def is_done_booting(devbox: DevboxView) -> bool:
17061705
return devbox.status not in DEVBOX_BOOTING_STATES

src/runloop_api_client/resources/devboxes/executions.py

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
async_to_streamed_response_wrapper,
1818
)
1919
from ..._constants import DEFAULT_TIMEOUT
20-
from ..._exceptions import APIStatusError
20+
from ..._exceptions import APIStatusError, APITimeoutError
2121
from ...lib.polling import PollingConfig, poll_until
2222
from ..._base_client import make_request_options
2323
from ...types.devboxes import execution_retrieve_params, execution_execute_sync_params, execution_execute_async_params
@@ -28,6 +28,16 @@
2828
__all__ = ["ExecutionsResource", "AsyncExecutionsResource"]
2929

3030

31+
def placeholder_execution_detail_view(devbox_id: str, execution_id: str) -> DevboxAsyncExecutionDetailView:
32+
return DevboxAsyncExecutionDetailView(
33+
devbox_id=devbox_id,
34+
execution_id=execution_id,
35+
status="queued",
36+
stdout="",
37+
stderr="",
38+
)
39+
40+
3141
class ExecutionsResource(SyncAPIResource):
3242
@cached_property
3343
def with_raw_response(self) -> ExecutionsResourceWithRawResponse:
@@ -127,16 +137,12 @@ def wait_for_execution_status() -> DevboxAsyncExecutionDetailView:
127137
)
128138

129139
def handle_timeout_error(error: Exception) -> DevboxAsyncExecutionDetailView:
130-
# Handle 408 timeout errors by returning current execution state to continue polling
131-
if isinstance(error, APIStatusError) and error.response.status_code == 408:
140+
# Handle timeout errors by returning current execution state to continue polling
141+
if isinstance(error, APITimeoutError) or (
142+
isinstance(error, APIStatusError) and error.response.status_code == 408
143+
):
132144
# Return a placeholder result to continue polling
133-
return DevboxAsyncExecutionDetailView(
134-
devbox_id=devbox_id,
135-
execution_id=execution_id,
136-
status="queued",
137-
stdout="",
138-
stderr="",
139-
)
145+
return placeholder_execution_detail_view(devbox_id, execution_id)
140146
else:
141147
# Re-raise other errors to stop polling
142148
raise error
@@ -410,19 +416,13 @@ async def wait_for_execution_status() -> DevboxAsyncExecutionDetailView:
410416
body={"statuses": ["completed"]},
411417
cast_to=DevboxAsyncExecutionDetailView,
412418
)
413-
except APIStatusError as error:
414-
if error.response.status_code == 408:
415-
# Handle 408 timeout errors by returning current execution state to continue polling
416-
return DevboxAsyncExecutionDetailView(
417-
devbox_id=devbox_id,
418-
execution_id=execution_id,
419-
status="queued",
420-
stdout="",
421-
stderr="",
422-
)
423-
else:
424-
# Re-raise other errors to stop polling
425-
raise
419+
except (APITimeoutError, APIStatusError) as error:
420+
# Handle timeout errors by returning placeholder to continue polling
421+
if isinstance(error, APITimeoutError) or error.response.status_code == 408:
422+
return placeholder_execution_detail_view(devbox_id, execution_id)
423+
424+
# Re-raise other errors to stop polling
425+
raise
426426

427427
def is_done(execution: DevboxAsyncExecutionDetailView) -> bool:
428428
return execution.status == "completed"

tests/api_resources/devboxes/test_executions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from tests.utils import assert_matches_type
1212
from runloop_api_client import Runloop, AsyncRunloop
1313
from runloop_api_client.types import DevboxExecutionDetailView, DevboxAsyncExecutionDetailView
14-
from runloop_api_client._exceptions import APIStatusError
14+
from runloop_api_client._exceptions import APIStatusError, APITimeoutError
1515
from runloop_api_client.lib.polling import PollingConfig, PollingTimeout
1616

1717
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -665,7 +665,7 @@ async def test_method_await_completed_timeout_handling(self, async_client: Async
665665
# Create a mock 408 response
666666
mock_response = Mock()
667667
mock_response.status_code = 408
668-
mock_408_error = APIStatusError("Request timeout", response=mock_response, body=None)
668+
mock_408_error = APITimeoutError(request=mock_response.request)
669669

670670
mock_execution_completed = DevboxAsyncExecutionDetailView(
671671
devbox_id="devbox_id",

0 commit comments

Comments
 (0)