Skip to content

Commit ffdd21b

Browse files
committed
fix(devbox): reconcile on_evict with generated stream and 2-arg callback
- Use the real generated event type DevboxEvictionEventView (the endpoint's view) in place of the assumed DevboxEvictionEvent. - Invoke the callback as callback(devbox, eviction_deadline_ms): the Devbox object and the Unix millisecond deadline, instead of passing the raw event. Requires the main-repo stainless config change that generates watch_evictions as Stream[DevboxEvictionEventView] (runloopai/runloop#10404). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 0117490 commit ffdd21b

6 files changed

Lines changed: 49 additions & 47 deletions

File tree

src/runloop_api_client/sdk/async_.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@
4545
from .async_agent import AsyncAgent
4646
from .async_devbox import AsyncDevbox
4747
from .async_scorer import AsyncScorer
48-
from .async_eviction import shutdown_monitor_for as shutdown_eviction_monitor
4948
from .async_secret import AsyncSecret
49+
from .async_eviction import shutdown_monitor_for as shutdown_eviction_monitor
5050
from .async_scenario import AsyncScenario
5151
from .async_snapshot import AsyncSnapshot
5252
from .async_benchmark import AsyncBenchmark

src/runloop_api_client/sdk/async_devbox.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@
3333
from .._types import omit
3434
from .._client import AsyncRunloop
3535
from ._helpers import filter_params
36-
from .async_eviction import AsyncEvictionCallback, monitor_for as _eviction_monitor_for
3736
from .._streaming import AsyncStream
3837
from ..lib.polling import PollingConfig
38+
from .async_eviction import AsyncEvictionCallback, monitor_for as _eviction_monitor_for
3939
from ..types.devboxes import ExecutionUpdateChunk
4040
from .async_execution import AsyncExecution, _AsyncStreamingGroup
4141
from .async_execution_result import AsyncExecutionResult
@@ -222,14 +222,15 @@ async def on_evict(self, callback: AsyncEvictionCallback) -> None:
222222
223223
The first ``on_evict`` across any devbox on this client opens a single
224224
account-wide notification stream; it closes automatically once every
225-
registered devbox has been notified. The callback runs at most once and
226-
receives the eviction event (with its ``eviction_deadline_ms``) — use it to
227-
run cleanup before the devbox is suspended. The callback may be sync or async.
225+
registered devbox has been notified. The callback runs at most once and is
226+
invoked as ``callback(devbox, eviction_deadline_ms)`` — the devbox itself and
227+
the Unix millisecond deadline by which it will be suspended. Use it to run
228+
cleanup before the devbox is suspended. The callback may be sync or async.
228229
229-
:param callback: Callable invoked with the eviction event for this devbox.
230+
:param callback: Callable invoked with this devbox and its eviction deadline (ms).
230231
:type callback: AsyncEvictionCallback
231232
"""
232-
await _eviction_monitor_for(self._client).register(self._id, callback)
233+
await _eviction_monitor_for(self._client).register(self, callback)
233234

234235
async def cancel_on_evict(self) -> None:
235236
"""Withdraw this devbox's eviction interest registered via :meth:`on_evict`."""

src/runloop_api_client/sdk/async_eviction.py

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,17 @@
99
import asyncio
1010
import inspect
1111
import logging
12-
from typing import TYPE_CHECKING, Dict, Union, Callable, Optional, Awaitable
12+
from typing import TYPE_CHECKING, Dict, Tuple, Union, Callable, Optional, Awaitable
1313
from weakref import WeakKeyDictionary
1414

1515
if TYPE_CHECKING:
16+
from ..types import DevboxEvictionEventView
1617
from .._client import AsyncRunloop
1718
from .._streaming import AsyncStream
19+
from .async_devbox import AsyncDevbox
1820

19-
# Assumed Stainless-generated SSE event type for ``watch_evictions``; carries
20-
# ``devbox_id`` and ``eviction_deadline_ms``. Reconcile the import once the
21-
# generated code lands.
22-
from ..types import DevboxEvictionEvent
23-
24-
AsyncEvictionCallback = Callable[["DevboxEvictionEvent"], Union[None, Awaitable[None]]]
25-
"""Sync or async callable invoked once with the eviction event for its devbox."""
21+
AsyncEvictionCallback = Callable[["AsyncDevbox", int], Union[None, Awaitable[None]]]
22+
"""Sync or async callable invoked once with the devbox and its eviction deadline (ms)."""
2623

2724
_logger = logging.getLogger(__name__)
2825

@@ -33,14 +30,14 @@ class AsyncEvictionMonitor:
3330
def __init__(self, client: "AsyncRunloop") -> None:
3431
self._client = client
3532
self._lock = asyncio.Lock()
36-
self._callbacks: Dict[str, AsyncEvictionCallback] = {}
33+
self._callbacks: Dict[str, Tuple["AsyncDevbox", AsyncEvictionCallback]] = {}
3734
self._task: Optional["asyncio.Task[None]"] = None
38-
self._stream: Optional["AsyncStream[DevboxEvictionEvent]"] = None
35+
self._stream: Optional["AsyncStream[DevboxEvictionEventView]"] = None
3936

40-
async def register(self, devbox_id: str, callback: AsyncEvictionCallback) -> None:
41-
"""Add ``devbox_id`` to the interest set, starting the stream task if idle."""
37+
async def register(self, devbox: "AsyncDevbox", callback: AsyncEvictionCallback) -> None:
38+
"""Add ``devbox`` to the interest set, starting the stream task if idle."""
4239
async with self._lock:
43-
self._callbacks[devbox_id] = callback
40+
self._callbacks[devbox.id] = (devbox, callback)
4441
if self._task is None or self._task.done():
4542
self._task = asyncio.create_task(self._run(), name="runloop-eviction-monitor")
4643

@@ -78,13 +75,14 @@ async def _run(self) -> None:
7875
self._stream = None
7976
self._task = None
8077

81-
async def _dispatch(self, event: "DevboxEvictionEvent") -> None:
78+
async def _dispatch(self, event: "DevboxEvictionEventView") -> None:
8279
async with self._lock:
83-
callback = self._callbacks.pop(event.devbox_id, None)
84-
if callback is None:
80+
entry = self._callbacks.pop(event.devbox_id, None)
81+
if entry is None:
8582
return
83+
devbox, callback = entry
8684
try:
87-
result = callback(event)
85+
result = callback(devbox, event.eviction_deadline_ms)
8886
if inspect.isawaitable(result):
8987
await result
9088
except Exception:

src/runloop_api_client/sdk/devbox.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@
3333
)
3434
from .._types import omit
3535
from .._client import Runloop
36-
from .eviction import EvictionCallback, monitor_for as _eviction_monitor_for
3736
from ._helpers import filter_params
37+
from .eviction import EvictionCallback, monitor_for as _eviction_monitor_for
3838
from .execution import Execution, _StreamingGroup
3939
from .._streaming import Stream
4040
from ..lib.polling import PollingConfig
@@ -225,14 +225,15 @@ def on_evict(self, callback: EvictionCallback) -> None:
225225
226226
The first ``on_evict`` across any devbox on this client opens a single
227227
account-wide notification stream; it closes automatically once every
228-
registered devbox has been notified. The callback runs at most once and
229-
receives the eviction event (with its ``eviction_deadline_ms``) — use it to
230-
run cleanup before the devbox is suspended.
228+
registered devbox has been notified. The callback runs at most once and is
229+
invoked as ``callback(devbox, eviction_deadline_ms)`` — the devbox itself and
230+
the Unix millisecond deadline by which it will be suspended. Use it to run
231+
cleanup before the devbox is suspended.
231232
232-
:param callback: Callable invoked with the eviction event for this devbox.
233+
:param callback: Callable invoked with this devbox and its eviction deadline (ms).
233234
:type callback: EvictionCallback
234235
"""
235-
_eviction_monitor_for(self._client).register(self._id, callback)
236+
_eviction_monitor_for(self._client).register(self, callback)
236237

237238
def cancel_on_evict(self) -> None:
238239
"""Withdraw this devbox's eviction interest registered via :meth:`on_evict`."""

src/runloop_api_client/sdk/eviction.py

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,21 @@
1818

1919
import logging
2020
import threading
21-
from typing import TYPE_CHECKING, Dict, Callable, Optional
21+
from typing import TYPE_CHECKING, Dict, Tuple, Callable, Optional
2222
from weakref import WeakKeyDictionary
2323

2424
if TYPE_CHECKING:
25+
from ..types import DevboxEvictionEventView
26+
from .devbox import Devbox
2527
from .._client import Runloop
2628
from .._streaming import Stream
2729

28-
# Assumed Stainless-generated SSE event type for ``watch_evictions``; carries
29-
# ``devbox_id`` and ``eviction_deadline_ms``. Reconcile the import once the
30-
# generated code lands.
31-
from ..types import DevboxEvictionEvent
30+
EvictionCallback = Callable[["Devbox", int], None]
31+
"""Invoked once when the devbox it was registered for has a pending eviction.
3232
33-
EvictionCallback = Callable[["DevboxEvictionEvent"], None]
34-
"""Invoked once with the eviction event for the devbox it was registered for."""
33+
Receives the :class:`~runloop_api_client.sdk.devbox.Devbox` and the eviction
34+
deadline as a Unix timestamp in milliseconds.
35+
"""
3536

3637
_logger = logging.getLogger(__name__)
3738

@@ -47,14 +48,14 @@ class EvictionMonitor:
4748
def __init__(self, client: "Runloop") -> None:
4849
self._client = client
4950
self._lock = threading.Lock()
50-
self._callbacks: Dict[str, EvictionCallback] = {}
51+
self._callbacks: Dict[str, Tuple["Devbox", EvictionCallback]] = {}
5152
self._thread: Optional[threading.Thread] = None
52-
self._stream: Optional["Stream[DevboxEvictionEvent]"] = None
53+
self._stream: Optional["Stream[DevboxEvictionEventView]"] = None
5354

54-
def register(self, devbox_id: str, callback: EvictionCallback) -> None:
55-
"""Add ``devbox_id`` to the interest set, starting the stream if idle."""
55+
def register(self, devbox: "Devbox", callback: EvictionCallback) -> None:
56+
"""Add ``devbox`` to the interest set, starting the stream if idle."""
5657
with self._lock:
57-
self._callbacks[devbox_id] = callback
58+
self._callbacks[devbox.id] = (devbox, callback)
5859
if self._thread is None or not self._thread.is_alive():
5960
self._thread = threading.Thread(
6061
target=self._run,
@@ -95,13 +96,14 @@ def _run(self) -> None:
9596
self._stream = None
9697
self._thread = None
9798

98-
def _dispatch(self, event: "DevboxEvictionEvent") -> None:
99+
def _dispatch(self, event: "DevboxEvictionEventView") -> None:
99100
with self._lock:
100-
callback = self._callbacks.pop(event.devbox_id, None)
101-
if callback is None:
101+
entry = self._callbacks.pop(event.devbox_id, None)
102+
if entry is None:
102103
return
104+
devbox, callback = entry
103105
try:
104-
callback(event)
106+
callback(devbox, event.eviction_deadline_ms)
105107
except Exception:
106108
_logger.exception("error in eviction callback for devbox %s", event.devbox_id)
107109

src/runloop_api_client/sdk/sync.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,11 @@
4141
)
4242
from .devbox import Devbox
4343
from .scorer import Scorer
44-
from .eviction import shutdown_monitor_for as shutdown_eviction_monitor
4544
from .secret import Secret
4645
from .._types import Timeout, NotGiven, not_given
4746
from .._client import DEFAULT_MAX_RETRIES, Runloop
4847
from ._helpers import detect_content_type
48+
from .eviction import shutdown_monitor_for as shutdown_eviction_monitor
4949
from .scenario import Scenario
5050
from .snapshot import Snapshot
5151
from .benchmark import Benchmark

0 commit comments

Comments
 (0)