Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions wool/src/wool/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from tblib import pickling_support

from wool.runtime.cache import ReferenceCountedCache
from wool.runtime.context import RuntimeContext
from wool.runtime.discovery.base import Discovery
from wool.runtime.discovery.base import DiscoveryEvent
Expand All @@ -19,7 +20,6 @@
from wool.runtime.loadbalancer.base import LoadBalancerLike
from wool.runtime.loadbalancer.base import NoWorkersAvailable
from wool.runtime.loadbalancer.roundrobin import RoundRobinLoadBalancer
from wool.runtime.resourcepool import ResourcePool
from wool.runtime.routine.task import Serializer
from wool.runtime.routine.task import Task
from wool.runtime.routine.task import TaskException
Expand Down Expand Up @@ -51,8 +51,8 @@

__proxy__: Final[ContextVar[WorkerProxy | None]] = ContextVar("__proxy__", default=None)

__proxy_pool__: Final[ContextVar[ResourcePool[WorkerProxy] | None]] = ContextVar(
"__proxy_pool__", default=None
__proxy_pool__: Final[ContextVar[ReferenceCountedCache[WorkerProxy] | None]] = (
ContextVar("__proxy_pool__", default=None)
)

__worker_metadata__: WorkerMetadata | None = None
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,21 @@
T = TypeVar("T")


class Resource(Generic[T]):
class Reference(Generic[T]):
"""
A single-use async context manager for resource acquisition.
A single-use async context manager for reference acquisition.

This class can only be used once as an async context manager. After
acquisition, it cannot be reacquired, and after release, it cannot be
released again.

:param pool:
The :class:`ResourcePool` this resource belongs to.
The :class:`ReferenceCountedCache` this reference belongs to.
:param key:
The cache key for this resource.
The cache key for this reference.
"""

def __init__(self, pool: ResourcePool[T], key):
def __init__(self, pool: ReferenceCountedCache[T], key):
self._pool = pool
self._key = key
self._resource = None
Expand Down Expand Up @@ -91,7 +91,7 @@ async def _release(self):
await self._pool.release(self._key)


class ResourcePool(Generic[T]):
class ReferenceCountedCache(Generic[T]):
"""
An asynchronous reference-counted cache with TTL-based cleanup.

Expand Down Expand Up @@ -127,7 +127,7 @@ class CacheEntry:
@dataclass
class Stats:
"""
Statistics about the current state of the resource pool.
Statistics about the current state of the cache.

:param total_entries:
Total number of cached entries.
Expand All @@ -151,14 +151,14 @@ def __init__(
self._factory = factory
self._finalizer = finalizer
self._ttl = ttl
self._cache: dict[Any, ResourcePool.CacheEntry] = {}
self._cache: dict[Any, ReferenceCountedCache.CacheEntry] = {}
self._lock = asyncio.Lock()

async def __aenter__(self):
"""Async context manager entry.

:returns:
The ResourcePool instance itself.
The ReferenceCountedCache instance itself.
"""
return self

Expand All @@ -184,7 +184,7 @@ def stats(self) -> Stats:
when not concurrently modifying the cache.

:returns:
:class:`ResourcePool.Stats` containing current statistics.
:class:`ReferenceCountedCache.Stats` containing current statistics.
"""
pending_cleanup = sum(
1 for c in self.pending_cleanup.values() if c is not None and not c.done()
Expand All @@ -210,17 +210,17 @@ def pending_cleanup(self):
if v.cleanup is not None and not v.cleanup.done()
}

def get(self, key: Any) -> Resource[T]:
def get(self, key: Any) -> Reference[T]:
"""
Get a resource acquisition that can be awaited or used as context
Get a reference acquisition that can be awaited or used as context
manager.

:param key:
The cache key.
:returns:
:class:`Resource` that can be awaited or used with 'async with'.
:class:`Reference` that can be awaited or used with 'async with'.
"""
return Resource(self, key)
return Reference(self, key)

async def acquire(self, key: Any) -> T:
"""
Expand Down
4 changes: 2 additions & 2 deletions wool/src/wool/runtime/discovery/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
from typing import Any
from typing import Final

from wool.runtime.resourcepool import ResourcePool
from wool.runtime.cache import ReferenceCountedCache

__subscriber_pool__: Final[ContextVar[ResourcePool[Any] | None]] = ContextVar(
__subscriber_pool__: Final[ContextVar[ReferenceCountedCache[Any] | None]] = ContextVar(
"__subscriber_pool__", default=None
)
6 changes: 3 additions & 3 deletions wool/src/wool/runtime/discovery/local.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@
from watchdog.observers import Observer

from wool.protocol import WorkerMetadata as WorkerMetadataProtobuf
from wool.runtime.cache import ReferenceCountedCache
from wool.runtime.discovery.base import Discovery
from wool.runtime.discovery.base import DiscoveryEvent
from wool.runtime.discovery.base import DiscoveryEventType
from wool.runtime.discovery.base import DiscoveryPublisherLike
from wool.runtime.discovery.base import DiscoverySubscriberLike
from wool.runtime.discovery.base import PredicateFunction
from wool.runtime.discovery.pool import SubscriberMeta
from wool.runtime.resourcepool import ResourcePool
from wool.runtime.worker.metadata import WorkerMetadata
from wool.utilities.afilter import afilter

Expand Down Expand Up @@ -339,15 +339,15 @@ class Publisher:
_block_size: int
_cleanups: dict[str, Callable]
_namespace: Final[str]
_shared_memory_pool: ResourcePool[SharedMemory]
_shared_memory_pool: ReferenceCountedCache[SharedMemory]

def __init__(self, namespace: str, *, block_size: int = 512):
if block_size < 0:
raise ValueError("Block size must be positive")
self._namespace = namespace
self._block_size = block_size
self._cleanups = {}
self._shared_memory_pool = ResourcePool(
self._shared_memory_pool = ReferenceCountedCache(
factory=self._shared_memory_factory,
finalizer=self._shared_memory_finalizer,
ttl=0,
Expand Down
12 changes: 6 additions & 6 deletions wool/src/wool/runtime/discovery/pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
from typing import Callable
from typing import ClassVar

from wool.runtime.cache import ReferenceCountedCache
from wool.runtime.discovery import __subscriber_pool__
from wool.runtime.discovery.base import DiscoveryEvent
from wool.runtime.resourcepool import ResourcePool
from wool.runtime.worker.metadata import WorkerMetadata
from wool.utilities.fanout import Fanout

Expand All @@ -30,7 +30,7 @@ class _SharedSubscription:
"""Adapter bridging a discovery subscriber and a :class:`Fanout`.

Each ``__aiter__`` call returns an independent async generator
that enters a :class:`~wool.runtime.resourcepool.Resource` from
that enters a :class:`~wool.runtime.cache.Reference` from
the pool, wraps the raw subscriber in a shared
:class:`~wool.utilities.fanout.Fanout`, and iterates a
:class:`~wool.utilities.fanout.FanoutConsumer`. The ``async
Expand All @@ -42,7 +42,7 @@ class _SharedSubscription:
so they start with a consistent view of the current state.

:param key:
Cache key for the :class:`ResourcePool`.
Cache key for the :class:`ReferenceCountedCache`.
:param reduce_info:
``(cls, args, kwargs)`` tuple used by :meth:`__reduce__` for
pickle support.
Expand Down Expand Up @@ -105,7 +105,7 @@ class SubscriberMeta(type):
``__new__`` onto the subscriber class. The injected method
registers a factory that creates raw subscribers, then returns a
:class:`_SharedSubscription` whose pool
:class:`~wool.runtime.resourcepool.Resource` is entered lazily
:class:`~wool.runtime.cache.Reference` is entered lazily
on first iteration.

Subscriber classes pass a ``key`` keyword argument at class
Expand Down Expand Up @@ -135,7 +135,7 @@ def _subscriber_new(cls_arg: type, *args: Any, **kwargs: Any) -> Any:
key = cls_arg._cache_key_fn(cls_arg, *args, **kwargs) # type: ignore[attr-defined]
pool = __subscriber_pool__.get()
if pool is None:
pool = ResourcePool(
pool = ReferenceCountedCache(
factory=_pool_factory,
finalizer=_pool_finalizer,
ttl=0,
Expand All @@ -158,7 +158,7 @@ def factory(_: Any) -> Any:


async def _pool_finalizer(subscriber: Any) -> None:
"""Resource pool finalizer — clean up fanout and subscriber."""
"""Cache finalizer — clean up fanout and subscriber."""
fanout = _SharedSubscription._fanouts.pop(subscriber, None)
if fanout is not None:
await fanout.cleanup()
Expand Down
4 changes: 2 additions & 2 deletions wool/src/wool/runtime/worker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ Signal handlers map `SIGTERM` to timeout 0 (cancel immediately) and `SIGINT` to

### Nested routines

Worker subprocesses can dispatch tasks to other workers. Each subprocess is configured with a `ResourcePool` of `WorkerProxy` instances (via `wool.__proxy_pool__`), so `@wool.routine` calls within a task transparently route to the target pool. Spinning up a `WorkerProxy` is not free — it involves establishing a discovery subscription, starting a sentinel task, and opening gRPC connections — so the resource pool caches proxies with a configurable TTL (default 60 seconds, set via `proxy_pool_ttl` on `LocalWorker`). If the interval between dispatches for a given pool on a given worker is shorter than the TTL, the cached proxy is reused. If it exceeds the TTL, the proxy is finalized and must be recreated on the next dispatch. Tuning `proxy_pool_ttl` above the expected dispatch interval keeps proxies warm and avoids this cold-start overhead.
Worker subprocesses can dispatch tasks to other workers. Each subprocess is configured with a `ReferenceCountedCache` of `WorkerProxy` instances (via `wool.__proxy_pool__`), so `@wool.routine` calls within a task transparently route to the target pool. Spinning up a `WorkerProxy` is not free — it involves establishing a discovery subscription, starting a sentinel task, and opening gRPC connections — so the cache stores proxies with a configurable TTL (default 60 seconds, set via `proxy_pool_ttl` on `LocalWorker`). If the interval between dispatches for a given pool on a given worker is shorter than the TTL, the cached proxy is reused. If it exceeds the TTL, the proxy is finalized and must be recreated on the next dispatch. Tuning `proxy_pool_ttl` above the expected dispatch interval keeps proxies warm and avoids this cold-start overhead.

Proxies on worker subprocesses are lazy by default — the `WorkerPool` propagates its `lazy` flag to every `WorkerProxy` it constructs, and each task serializes the proxy (including the flag) so that workers receiving the task inherit the same laziness setting. A lazy proxy defers discovery subscription and sentinel setup until its first `dispatch()` call, so workers that never invoke nested routines pay no startup cost.

Expand Down Expand Up @@ -240,7 +240,7 @@ Workers are self-describing: each worker advertises its gRPC transport configura

### Connection pooling

`WorkerConnection` is a lightweight facade that dispatches tasks over pooled gRPC channels. Channels are cached at the module level in a `ResourcePool` keyed by `(target, credentials, options)`, with a 60-second TTL — idle channels are finalized after the TTL expires. Each channel's concurrency semaphore is sized by the worker's advertised `max_concurrent_streams`.
`WorkerConnection` is a lightweight facade that dispatches tasks over pooled gRPC channels. Channels are cached at the module level in a `ReferenceCountedCache` keyed by `(target, credentials, options)`, with a 60-second TTL — idle channels are finalized after the TTL expires. Each channel's concurrency semaphore is sized by the worker's advertised `max_concurrent_streams`.

### Transport configuration

Expand Down
4 changes: 2 additions & 2 deletions wool/src/wool/runtime/worker/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

import wool
from wool import protocol
from wool.runtime.resourcepool import ResourcePool
from wool.runtime.cache import ReferenceCountedCache
from wool.runtime.routine.task import PassthroughSerializer
from wool.runtime.routine.task import Task
from wool.runtime.worker.base import ChannelOptions
Expand Down Expand Up @@ -80,7 +80,7 @@ async def _channel_finalizer(channel: _Channel):
await channel.close()


_channel_pool: ResourcePool[_Channel] = ResourcePool(
_channel_pool: ReferenceCountedCache[_Channel] = ReferenceCountedCache(
factory=_channel_factory, finalizer=_channel_finalizer, ttl=60
)

Expand Down
12 changes: 6 additions & 6 deletions wool/src/wool/runtime/worker/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

import wool
from wool import protocol
from wool.runtime.resourcepool import ResourcePool
from wool.runtime.cache import ReferenceCountedCache
from wool.runtime.worker.auth import CredentialContext
from wool.runtime.worker.auth import WorkerCredentials
from wool.runtime.worker.base import WorkerOptions
Expand Down Expand Up @@ -223,7 +223,7 @@ def run(self) -> None:
logger.info(f"Worker process starting on {self._host}:{self._port}")

wool.__proxy_pool__.set(
ResourcePool(
ReferenceCountedCache(
factory=_proxy_factory,
finalizer=_proxy_finalizer,
ttl=self._proxy_pool_ttl,
Expand Down Expand Up @@ -413,14 +413,14 @@ def _sigint_handler(loop, service, signum, frame):


async def _proxy_factory(proxy: WorkerProxy):
"""Factory function for WorkerProxy instances in ResourcePool.
"""Factory function for WorkerProxy instances in ReferenceCountedCache.

Calls ``enter()`` on the proxy. Lazy proxies defer actual
startup until first dispatch; non-lazy proxies start eagerly.
The proxy object itself is used as the cache key.

:param proxy:
The WorkerProxy instance (passed as key from ResourcePool).
The WorkerProxy instance (passed as key from ReferenceCountedCache).
:returns:
The entered WorkerProxy instance.
"""
Expand All @@ -429,10 +429,10 @@ async def _proxy_factory(proxy: WorkerProxy):


async def _proxy_finalizer(proxy: WorkerProxy):
"""Finalizer function for WorkerProxy instances in ResourcePool.
"""Finalizer function for WorkerProxy instances in ReferenceCountedCache.

Exits the proxy context when it's being cleaned up from the
resource pool. Lazy proxies that were never started are handled
cache. Lazy proxies that were never started are handled
gracefully by the proxy's own exit method.

:param proxy:
Expand Down
8 changes: 4 additions & 4 deletions wool/src/wool/runtime/worker/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

import wool
from wool import protocol
from wool.runtime.resourcepool import ResourcePool
from wool.runtime.cache import ReferenceCountedCache
from wool.runtime.routine.task import Task
from wool.runtime.routine.task import do_dispatch

Expand Down Expand Up @@ -147,15 +147,15 @@ class WorkerService(protocol.WorkerServicer):
_stopped: asyncio.Event
_stopping: asyncio.Event
_task_completed: asyncio.Event
_loop_pool: ResourcePool[tuple[asyncio.AbstractEventLoop, threading.Thread]]
_loop_pool: ReferenceCountedCache[tuple[asyncio.AbstractEventLoop, threading.Thread]]

def __init__(self, *, backpressure: BackpressureLike | None = None):
self._stopped = asyncio.Event()
self._stopping = asyncio.Event()
self._task_completed = asyncio.Event()
self._docket = set()
self._backpressure = backpressure
self._loop_pool = ResourcePool(
self._loop_pool = ReferenceCountedCache(
factory=self._create_worker_loop,
finalizer=self._destroy_worker_loop,
ttl=0,
Expand Down Expand Up @@ -267,7 +267,7 @@ def _create_worker_loop(
"""Create a new event loop running on a dedicated daemon thread.

:param key:
The :class:`ResourcePool` cache key (unused).
The :class:`ReferenceCountedCache` cache key (unused).
:returns:
A tuple of the event loop and the thread running it.
"""
Expand Down
4 changes: 2 additions & 2 deletions wool/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import pytest

import wool
from wool.runtime.resourcepool import ResourcePool
from wool.runtime.cache import ReferenceCountedCache

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -55,7 +55,7 @@ def pytest_addoption(parser):

@pytest.fixture
def mock_worker_proxy_cache(mocker):
mock_pool = mocker.MagicMock(spec=ResourcePool)
mock_pool = mocker.MagicMock(spec=ReferenceCountedCache)
mock_proxy = mocker.MagicMock() # This will be returned by the context manager
mock_pool.acquire.return_value.__aenter__ = mocker.AsyncMock(return_value=mock_proxy)
mock_pool.acquire.return_value.__aexit__ = mocker.AsyncMock(return_value=False)
Expand Down
6 changes: 3 additions & 3 deletions wool/tests/runtime/discovery/test_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
import cloudpickle
import pytest

from wool.runtime.cache import ReferenceCountedCache
from wool.runtime.discovery import __subscriber_pool__
from wool.runtime.discovery.base import DiscoveryEvent
from wool.runtime.discovery.pool import SubscriberMeta
from wool.runtime.discovery.pool import _pool_factory
from wool.runtime.discovery.pool import _SharedSubscription
from wool.runtime.discovery.pool import _subscriber_factories
from wool.runtime.resourcepool import ResourcePool
from wool.runtime.worker.metadata import WorkerMetadata


Expand Down Expand Up @@ -53,7 +53,7 @@ def _setup_pool():
"""Ensure the subscriber pool exists for direct-construction tests."""
pool = __subscriber_pool__.get()
if pool is None:
pool = ResourcePool(factory=_pool_factory, ttl=0)
pool = ReferenceCountedCache(factory=_pool_factory, ttl=0)
__subscriber_pool__.set(pool)
return pool

Expand Down Expand Up @@ -192,7 +192,7 @@ def test___new___with_lazy_pool_init(self):
When:
A subscriber is constructed via SubscriberMeta.
Then:
The ContextVar should be set to a ResourcePool.
The ContextVar should be set to a ReferenceCountedCache.
"""
# Arrange
__subscriber_pool__.set(None)
Expand Down
Loading
Loading