-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathasync_eviction.py
More file actions
119 lines (98 loc) · 4.38 KB
/
Copy pathasync_eviction.py
File metadata and controls
119 lines (98 loc) · 4.38 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
"""Async client-side eviction notification monitor.
Async counterpart of :mod:`runloop_api_client.sdk.eviction`; see that module for the
delivery contract. Callbacks may be plain functions or coroutines.
"""
from __future__ import annotations
import asyncio
import inspect
import logging
from typing import TYPE_CHECKING, Dict, Tuple, Union, Callable, Optional, Awaitable
from weakref import WeakKeyDictionary
if TYPE_CHECKING:
from ..types import DevboxEvictionEventView
from .._client import AsyncRunloop
from .._streaming import AsyncStream
from .async_devbox import AsyncDevbox
AsyncEvictionCallback = Callable[["AsyncDevbox", int], Union[None, Awaitable[None]]]
"""Sync or async callable invoked once with the devbox and its eviction deadline (ms)."""
_logger = logging.getLogger(__name__)
class AsyncEvictionMonitor:
"""Async fan-out of account-wide eviction notifications to per-devbox callbacks."""
def __init__(self, client: "AsyncRunloop") -> None:
self._client = client
self._lock = asyncio.Lock()
self._callbacks: Dict[str, Tuple["AsyncDevbox", AsyncEvictionCallback]] = {}
self._task: Optional["asyncio.Task[None]"] = None
self._stream: Optional["AsyncStream[DevboxEvictionEventView]"] = None
async def register(self, devbox: "AsyncDevbox", callback: AsyncEvictionCallback) -> None:
"""Add ``devbox`` to the interest set, starting the stream task if idle."""
async with self._lock:
self._callbacks[devbox.id] = (devbox, callback)
if self._task is None or self._task.done():
self._task = asyncio.create_task(self._run(), name="runloop-eviction-monitor")
async def unregister(self, devbox_id: str) -> None:
"""Drop ``devbox_id``; close the stream if it was the last interested devbox."""
async with self._lock:
self._callbacks.pop(devbox_id, None)
empty = not self._callbacks
if empty:
await self._close_stream()
async def close(self) -> None:
"""Clear all interest and tear down the stream."""
async with self._lock:
self._callbacks.clear()
await self._close_stream()
async def _run(self) -> None:
try:
stream = await self._client.devboxes.watch_evictions()
async with self._lock:
self._stream = stream
async with stream:
async for event in stream:
await self._dispatch(event)
async with self._lock:
if not self._callbacks:
break
except asyncio.CancelledError:
raise
except Exception:
_logger.exception("async eviction monitor stream failed")
finally:
async with self._lock:
self._stream = None
self._task = None
async def _dispatch(self, event: "DevboxEvictionEventView") -> None:
async with self._lock:
entry = self._callbacks.pop(event.devbox_id, None)
if entry is None:
return
devbox, callback = entry
try:
result = callback(devbox, event.eviction_deadline_ms)
if inspect.isawaitable(result):
await result
except Exception:
_logger.exception("error in eviction callback for devbox %s", event.devbox_id)
async def _close_stream(self) -> None:
async with self._lock:
stream = self._stream
self._stream = None
if stream is not None:
try:
await stream.close()
except Exception:
_logger.debug("error closing eviction stream", exc_info=True)
# asyncio is single-threaded, so the registry needs no lock.
_monitors: "WeakKeyDictionary[AsyncRunloop, AsyncEvictionMonitor]" = WeakKeyDictionary()
def monitor_for(client: "AsyncRunloop") -> AsyncEvictionMonitor:
"""Return the shared :class:`AsyncEvictionMonitor` for ``client``, creating it once."""
monitor = _monitors.get(client)
if monitor is None:
monitor = AsyncEvictionMonitor(client)
_monitors[client] = monitor
return monitor
async def shutdown_monitor_for(client: "AsyncRunloop") -> None:
"""Tear down the shared monitor for ``client`` if one exists."""
monitor = _monitors.pop(client, None)
if monitor is not None:
await monitor.close()