From 7a50e3a52b1debdfa0ebb1c9438d44620fee1003 Mon Sep 17 00:00:00 2001 From: Conrad Date: Mon, 6 Apr 2026 22:02:40 -0400 Subject: [PATCH 1/3] refactor: Rename ResourcePool to ReferenceCountedCache The class name now accurately describes the data structure's behavior: a reference-counted cache with TTL-based cleanup, not a generic resource pool. The module is also renamed from resourcepool.py to cache.py to match. Closes #151 --- wool/src/wool/__init__.py | 6 +- .../runtime/{resourcepool.py => cache.py} | 14 +-- wool/src/wool/runtime/discovery/__init__.py | 4 +- wool/src/wool/runtime/discovery/local.py | 6 +- wool/src/wool/runtime/discovery/pool.py | 12 +-- wool/src/wool/runtime/worker/README.md | 4 +- wool/src/wool/runtime/worker/connection.py | 4 +- wool/src/wool/runtime/worker/process.py | 12 +-- wool/src/wool/runtime/worker/service.py | 8 +- wool/tests/conftest.py | 4 +- wool/tests/runtime/discovery/test_pool.py | 6 +- .../{test_resourcepool.py => test_cache.py} | 88 +++++++++---------- wool/tests/runtime/worker/test_process.py | 31 +++---- 13 files changed, 100 insertions(+), 99 deletions(-) rename wool/src/wool/runtime/{resourcepool.py => cache.py} (96%) rename wool/tests/runtime/{test_resourcepool.py => test_cache.py} (90%) diff --git a/wool/src/wool/__init__.py b/wool/src/wool/__init__.py index 35405807..b1ce3d15 100644 --- a/wool/src/wool/__init__.py +++ b/wool/src/wool/__init__.py @@ -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 @@ -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 @@ -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 diff --git a/wool/src/wool/runtime/resourcepool.py b/wool/src/wool/runtime/cache.py similarity index 96% rename from wool/src/wool/runtime/resourcepool.py rename to wool/src/wool/runtime/cache.py index 6517087c..f32e48d0 100644 --- a/wool/src/wool/runtime/resourcepool.py +++ b/wool/src/wool/runtime/cache.py @@ -24,12 +24,12 @@ class Resource(Generic[T]): released again. :param pool: - The :class:`ResourcePool` this resource belongs to. + The :class:`ReferenceCountedCache` this resource belongs to. :param key: The cache key for this resource. """ - def __init__(self, pool: ResourcePool[T], key): + def __init__(self, pool: ReferenceCountedCache[T], key): self._pool = pool self._key = key self._resource = None @@ -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. @@ -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. @@ -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 @@ -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() diff --git a/wool/src/wool/runtime/discovery/__init__.py b/wool/src/wool/runtime/discovery/__init__.py index a8589fc0..052543e7 100644 --- a/wool/src/wool/runtime/discovery/__init__.py +++ b/wool/src/wool/runtime/discovery/__init__.py @@ -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 ) diff --git a/wool/src/wool/runtime/discovery/local.py b/wool/src/wool/runtime/discovery/local.py index ebb31fb0..18eefb35 100644 --- a/wool/src/wool/runtime/discovery/local.py +++ b/wool/src/wool/runtime/discovery/local.py @@ -23,6 +23,7 @@ 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 @@ -30,7 +31,6 @@ 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 @@ -339,7 +339,7 @@ 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: @@ -347,7 +347,7 @@ def __init__(self, namespace: str, *, block_size: int = 512): 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, diff --git a/wool/src/wool/runtime/discovery/pool.py b/wool/src/wool/runtime/discovery/pool.py index 56bb22f5..27658c80 100644 --- a/wool/src/wool/runtime/discovery/pool.py +++ b/wool/src/wool/runtime/discovery/pool.py @@ -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 @@ -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.Resource` 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 @@ -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. @@ -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.Resource` is entered lazily on first iteration. Subscriber classes pass a ``key`` keyword argument at class @@ -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, @@ -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() diff --git a/wool/src/wool/runtime/worker/README.md b/wool/src/wool/runtime/worker/README.md index 13b3af25..dbf6bf06 100644 --- a/wool/src/wool/runtime/worker/README.md +++ b/wool/src/wool/runtime/worker/README.md @@ -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. @@ -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 diff --git a/wool/src/wool/runtime/worker/connection.py b/wool/src/wool/runtime/worker/connection.py index aad9f65f..144cb6c6 100644 --- a/wool/src/wool/runtime/worker/connection.py +++ b/wool/src/wool/runtime/worker/connection.py @@ -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 @@ -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 ) diff --git a/wool/src/wool/runtime/worker/process.py b/wool/src/wool/runtime/worker/process.py index 3ff5a8e5..387c1385 100644 --- a/wool/src/wool/runtime/worker/process.py +++ b/wool/src/wool/runtime/worker/process.py @@ -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 @@ -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, @@ -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. """ @@ -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: diff --git a/wool/src/wool/runtime/worker/service.py b/wool/src/wool/runtime/worker/service.py index e76743a1..fc89e631 100644 --- a/wool/src/wool/runtime/worker/service.py +++ b/wool/src/wool/runtime/worker/service.py @@ -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 @@ -147,7 +147,7 @@ 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() @@ -155,7 +155,7 @@ def __init__(self, *, backpressure: BackpressureLike | None = None): 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, @@ -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. """ diff --git a/wool/tests/conftest.py b/wool/tests/conftest.py index 05016959..25c16780 100644 --- a/wool/tests/conftest.py +++ b/wool/tests/conftest.py @@ -5,7 +5,7 @@ import pytest import wool -from wool.runtime.resourcepool import ResourcePool +from wool.runtime.cache import ReferenceCountedCache logger = logging.getLogger(__name__) @@ -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) diff --git a/wool/tests/runtime/discovery/test_pool.py b/wool/tests/runtime/discovery/test_pool.py index 49b362cd..a1b3bd43 100644 --- a/wool/tests/runtime/discovery/test_pool.py +++ b/wool/tests/runtime/discovery/test_pool.py @@ -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 @@ -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 @@ -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) diff --git a/wool/tests/runtime/test_resourcepool.py b/wool/tests/runtime/test_cache.py similarity index 90% rename from wool/tests/runtime/test_resourcepool.py rename to wool/tests/runtime/test_cache.py index 0b533480..17de034b 100644 --- a/wool/tests/runtime/test_resourcepool.py +++ b/wool/tests/runtime/test_cache.py @@ -10,9 +10,9 @@ from hypothesis import given from hypothesis import strategies -import wool.runtime.resourcepool as rp -from wool.runtime.resourcepool import Resource -from wool.runtime.resourcepool import ResourcePool +import wool.runtime.cache as cache_module +from wool.runtime.cache import ReferenceCountedCache +from wool.runtime.cache import Resource # Global tracking for factory and finalizer calls using function names as keys call_tracker = defaultdict(lambda: {"factory_calls": [], "finalizer_calls": []}) @@ -195,13 +195,13 @@ def mock_finalizer(): @pytest.fixture def resource_pool_with_ttl(mock_resource_factory, mock_finalizer): """Create a resource pool configured for TTL testing.""" - return ResourcePool(factory=mock_resource_factory, finalizer=mock_finalizer, ttl=0.1) + return ReferenceCountedCache(factory=mock_resource_factory, finalizer=mock_finalizer, ttl=0.1) @pytest.fixture def resource_pool_immediate_cleanup(mock_resource_factory, mock_finalizer): """Create a resource pool with TTL=0 for immediate cleanup testing.""" - return ResourcePool(factory=mock_resource_factory, finalizer=mock_finalizer, ttl=0) + return ReferenceCountedCache(factory=mock_resource_factory, finalizer=mock_finalizer, ttl=0) @pytest.fixture @@ -219,11 +219,11 @@ def __call__(self, _key): return CountingFactory() -class TestResourcePool: +class TestReferenceCountedCache: @staticmethod @strategies.composite def setup(draw, *, max_key_count=5): - """Generate a ResourcePool with varied initial resource states. + """Generate a ReferenceCountedCache with varied initial resource states. Creates a pool with 0-max_key_count resources using the public API to create realistic pool states for property-based testing. @@ -234,11 +234,11 @@ def setup(draw, *, max_key_count=5): Maximum number of keys to create resources for. :returns: An async function that when called returns a tuple of - (ResourcePool, factory, list of resources, list of keys). + (ReferenceCountedCache, factory, list of resources, list of keys). """ factory = draw(factory_functions()) finalizer = draw(finalizer_functions()) - pool = ResourcePool(factory=factory, finalizer=finalizer, ttl=0) + pool = ReferenceCountedCache(factory=factory, finalizer=finalizer, ttl=0) created_resources = [] keys = [] @@ -283,7 +283,7 @@ async def test_get_returns_resource_instance(self, setup): assert isinstance(resource_acquisition, Resource) @pytest.mark.asyncio - @pytest.mark.dependency("TestResourcePool::test_get_returns_resource_instance") + @pytest.mark.dependency("TestReferenceCountedCache::test_get_returns_resource_instance") async def test_release_decrements_reference_counts(self): """Test releasing resources decrements reference counts properly. @@ -296,7 +296,7 @@ async def test_release_decrements_reference_counts(self): """ # Arrange - Create pool with TTL to keep resources after context exit mock_factory = Mock() - pool = ResourcePool(factory=mock_factory, ttl=60) + pool = ReferenceCountedCache(factory=mock_factory, ttl=60) # Create some test resources test_keys = ["key1", "key2", "key3"] @@ -323,7 +323,7 @@ async def test_release_decrements_reference_counts(self): assert pool.stats.referenced_entries == 0 @pytest.mark.asyncio - @pytest.mark.dependency("TestResourcePool::test_release_decrements_reference_counts") + @pytest.mark.dependency("TestReferenceCountedCache::test_release_decrements_reference_counts") async def test_release_nonexistent_key_raises_error(self, counting_factory): """Test releasing nonexistent key raises KeyError. @@ -335,7 +335,7 @@ async def test_release_nonexistent_key_raises_error(self, counting_factory): Should exit without affecting existing resources """ # Arrange - pool = ResourcePool(factory=counting_factory, ttl=1.0) + pool = ReferenceCountedCache(factory=counting_factory, ttl=1.0) # Create some resources to establish initial state keys = ["key1", "key2"] @@ -355,7 +355,7 @@ async def test_release_nonexistent_key_raises_error(self, counting_factory): assert pool.stats.referenced_entries == 0 @pytest.mark.asyncio - @pytest.mark.dependency("TestResourcePool::test_release_decrements_reference_counts") + @pytest.mark.dependency("TestReferenceCountedCache::test_release_decrements_reference_counts") async def test_release_zero_reference_count_raises_error(self): """Test releasing key with zero ref count raises ValueError. @@ -372,7 +372,7 @@ async def test_release_zero_reference_count_raises_error(self): # so the resource stays in cache after release mock_factory = Mock() mock_finalizer = AsyncMock() - ttl_pool = ResourcePool(factory=mock_factory, finalizer=mock_finalizer, ttl=60) + ttl_pool = ReferenceCountedCache(factory=mock_factory, finalizer=mock_finalizer, ttl=60) unique_key = "test-zero-ref-count" mock_resource = Mock() @@ -392,7 +392,7 @@ async def test_release_zero_reference_count_raises_error(self): await ttl_pool.release(unique_key) @pytest.mark.asyncio - @pytest.mark.dependency("TestResourcePool::test_get_returns_resource_instance") + @pytest.mark.dependency("TestReferenceCountedCache::test_get_returns_resource_instance") async def test_clear_finalizes_all_resources(self): """Test clearing the pool calls finalizer on all resources. @@ -406,7 +406,7 @@ async def test_clear_finalizes_all_resources(self): # Arrange - Create pool with TTL to keep resources after context exit mock_factory = Mock() mock_finalizer = AsyncMock() - pool = ResourcePool(factory=mock_factory, finalizer=mock_finalizer, ttl=60) + pool = ReferenceCountedCache(factory=mock_factory, finalizer=mock_finalizer, ttl=60) # Create some resources test_resources = [] @@ -431,7 +431,7 @@ async def test_clear_finalizes_all_resources(self): assert mock_finalizer.call_count == 3 @pytest.mark.asyncio - @pytest.mark.dependency("TestResourcePool::test_get_returns_resource_instance") + @pytest.mark.dependency("TestReferenceCountedCache::test_get_returns_resource_instance") async def test_clear_key_removes_specific_resource(self, mock_finalizer): """Test clearing a specific key from the pool. @@ -444,7 +444,7 @@ async def test_clear_key_removes_specific_resource(self, mock_finalizer): """ # Arrange mock_factory = Mock() - pool = ResourcePool(factory=mock_factory, finalizer=mock_finalizer, ttl=60) + pool = ReferenceCountedCache(factory=mock_factory, finalizer=mock_finalizer, ttl=60) # Create multiple resources mock_resource1 = Mock() @@ -490,7 +490,7 @@ async def test_clear_nonexistent_key_raises_error(self): # Arrange mock_factory = Mock() mock_finalizer = AsyncMock() - pool = ResourcePool(factory=mock_factory, finalizer=mock_finalizer, ttl=60) + pool = ReferenceCountedCache(factory=mock_factory, finalizer=mock_finalizer, ttl=60) # Create one resource mock_resource = Mock() @@ -513,7 +513,7 @@ async def test_clear_nonexistent_key_raises_error(self): mock_finalizer.assert_not_called() @pytest.mark.asyncio - @pytest.mark.dependency("TestResourcePool::test_get_returns_resource_instance") + @pytest.mark.dependency("TestReferenceCountedCache::test_get_returns_resource_instance") async def test_ttl_cleanup_schedules_resource_removal(self): """Test TTL-based cleanup schedules and executes properly. @@ -527,7 +527,7 @@ async def test_ttl_cleanup_schedules_resource_removal(self): # Arrange mock_factory = Mock() mock_finalizer = AsyncMock() - pool = ResourcePool(factory=mock_factory, finalizer=mock_finalizer, ttl=0.1) + pool = ReferenceCountedCache(factory=mock_factory, finalizer=mock_finalizer, ttl=0.1) mock_resource = Mock() mock_factory.return_value = mock_resource @@ -541,7 +541,7 @@ async def mock_sleep(_delay): # Wait for the test to signal that sleep should complete await sleep_event.wait() - with patch.object(rp.asyncio, "sleep", side_effect=mock_sleep): + with patch.object(cache_module.asyncio, "sleep", side_effect=mock_sleep): # Act # Acquire and immediately release async with pool.get(key) as resource: @@ -573,7 +573,7 @@ async def mock_sleep(_delay): mock_finalizer.assert_called_once_with(mock_resource) @pytest.mark.asyncio - @pytest.mark.dependency("TestResourcePool::test_get_returns_resource_instance") + @pytest.mark.dependency("TestReferenceCountedCache::test_get_returns_resource_instance") async def test_ttl_cleanup_cancelled_on_reacquire(self): """Test TTL cleanup is cancelled when resource is reacquired. @@ -587,7 +587,7 @@ async def test_ttl_cleanup_cancelled_on_reacquire(self): # Arrange mock_factory = Mock() mock_finalizer = AsyncMock() - pool = ResourcePool(factory=mock_factory, finalizer=mock_finalizer, ttl=0.1) + pool = ReferenceCountedCache(factory=mock_factory, finalizer=mock_finalizer, ttl=0.1) mock_resource = Mock() mock_factory.return_value = mock_resource @@ -619,7 +619,7 @@ async def test_ttl_cleanup_cancelled_on_reacquire(self): assert pool.stats.referenced_entries == 0 @pytest.mark.asyncio - @pytest.mark.dependency("TestResourcePool::test_get_returns_resource_instance") + @pytest.mark.dependency("TestReferenceCountedCache::test_get_returns_resource_instance") async def test_stats_returns_accurate_counts(self): """Test stats method returns accurate cache statistics. @@ -634,7 +634,7 @@ async def test_stats_returns_accurate_counts(self): # Arrange mock_factory = Mock() mock_finalizer = AsyncMock() - pool = ResourcePool(factory=mock_factory, finalizer=mock_finalizer, ttl=0.1) + pool = ReferenceCountedCache(factory=mock_factory, finalizer=mock_finalizer, ttl=0.1) # Start with empty pool stats = pool.stats @@ -656,12 +656,12 @@ async def test_stats_returns_accurate_counts(self): assert stats.pending_cleanup == 0 # None scheduled yet @pytest.mark.asyncio - @pytest.mark.dependency("TestResourcePool::test_get_returns_resource_instance") + @pytest.mark.dependency("TestReferenceCountedCache::test_get_returns_resource_instance") async def test_async_context_manager_clears_resources(self): - """Test ResourcePool as async context manager clears all on exit. + """Test ReferenceCountedCache as async context manager clears all on exit. Given: - A ResourcePool with resources + A ReferenceCountedCache with resources When: Used as async context manager and then exited Then: @@ -672,7 +672,7 @@ async def test_async_context_manager_clears_resources(self): mock_finalizer = AsyncMock() # Act & assert - async with ResourcePool(factory=mock_factory, finalizer=mock_finalizer) as pool: + async with ReferenceCountedCache(factory=mock_factory, finalizer=mock_finalizer) as pool: mock_resource = Mock() mock_factory.return_value = mock_resource @@ -718,7 +718,7 @@ def mock_finalizer_func(obj): mock_finalizer = Mock(side_effect=mock_finalizer_func) - pool = ResourcePool(factory=mock_factory, finalizer=mock_finalizer, ttl=ttl) + pool = ReferenceCountedCache(factory=mock_factory, finalizer=mock_finalizer, ttl=ttl) async with pool.get("test-key"): pass @@ -770,7 +770,7 @@ async def test_finalizer_exception_handling_catches_errors(self): async def failing_finalizer(_): raise ValueError("Finalizer failed") - pool = ResourcePool(factory=mock_factory, finalizer=failing_finalizer) + pool = ReferenceCountedCache(factory=mock_factory, finalizer=failing_finalizer) mock_resource = Mock() mock_factory.return_value = mock_resource @@ -798,7 +798,7 @@ async def test_clear_with_nonexistent_key_raises_error(self): """ # Arrange mock_factory = Mock(return_value=Mock()) - pool = ResourcePool(factory=mock_factory, ttl=0) + pool = ReferenceCountedCache(factory=mock_factory, ttl=0) # Create one resource first async with pool.get("valid-key"): @@ -821,7 +821,7 @@ async def test_concurrent_acquire_release_same_key(self, counting_factory): Resource pool should maintain consistency and not leak resources """ # Arrange - pool = ResourcePool(factory=counting_factory, ttl=0.1) + pool = ReferenceCountedCache(factory=counting_factory, ttl=0.1) # Act async def acquire_release_worker(): @@ -888,7 +888,7 @@ async def test_get_with_none_key_handles_gracefully(self): mock_factory = Mock() mock_resource = Mock() mock_factory.return_value = mock_resource - pool = ResourcePool(factory=mock_factory, ttl=0) + pool = ReferenceCountedCache(factory=mock_factory, ttl=0) # Act & assert # None should be treated as a valid key @@ -919,7 +919,7 @@ async def test_context_manager_auto_releases(self): mock_resource.name = "context-resource" mock_factory.return_value = mock_resource - pool = ResourcePool(factory=mock_factory, ttl=0) + pool = ReferenceCountedCache(factory=mock_factory, ttl=0) # Act & assert # Use Resource as context manager @@ -947,7 +947,7 @@ async def test_resource_has_no_manual_release_method(self): mock_resource = Mock() mock_factory.return_value = mock_resource - pool = ResourcePool(factory=mock_factory, ttl=0) + pool = ReferenceCountedCache(factory=mock_factory, ttl=0) resource_acquisition = pool.get("test-key") @@ -971,7 +971,7 @@ async def test_resource_lifecycle_with_ttl(self): mock_resource = Mock() mock_factory.return_value = mock_resource - pool = ResourcePool(factory=mock_factory, ttl=60) # Use TTL to keep resource + pool = ReferenceCountedCache(factory=mock_factory, ttl=60) # Use TTL to keep resource resource_acquisition = pool.get("test-key") @@ -1000,7 +1000,7 @@ async def test_context_manager_only_usage_handles_lifecycle(self): mock_resource = Mock() mock_factory.return_value = mock_resource - pool = ResourcePool(factory=mock_factory, ttl=0) + pool = ReferenceCountedCache(factory=mock_factory, ttl=0) # Act & assert # Use only as context manager @@ -1026,7 +1026,7 @@ async def test_acquire_twice(self): mock_resource = Mock() mock_factory.return_value = mock_resource - pool = ResourcePool(factory=mock_factory, ttl=0) + pool = ReferenceCountedCache(factory=mock_factory, ttl=0) resource_acquisition = pool.get("test-key") # First use as context manager @@ -1053,7 +1053,7 @@ async def test_resource_context_acquire_exception(self): mock_pool = AsyncMock() mock_pool.acquire.side_effect = RuntimeError("Acquire failed") - from wool.runtime.resourcepool import Resource + from wool.runtime.cache import Resource resource = Resource(pool=mock_pool, key="test-key") @@ -1078,7 +1078,7 @@ async def test_resource_context_release_not_acquired(self): """ # Arrange mock_pool = AsyncMock() - from wool.runtime.resourcepool import Resource + from wool.runtime.cache import Resource resource = Resource(pool=mock_pool, key="test-key") @@ -1104,7 +1104,7 @@ async def test_resource_context_release_already_released(self): mock_resource = Mock() mock_pool.acquire.return_value = mock_resource - from wool.runtime.resourcepool import Resource + from wool.runtime.cache import Resource resource = Resource(pool=mock_pool, key="test-key") diff --git a/wool/tests/runtime/worker/test_process.py b/wool/tests/runtime/worker/test_process.py index b84a6ebd..79311d5d 100644 --- a/wool/tests/runtime/worker/test_process.py +++ b/wool/tests/runtime/worker/test_process.py @@ -762,7 +762,8 @@ def test_run_sets_up_proxy_pool_and_starts_server(self, mocker): mock_resource_pool = mocker.MagicMock() mock_resource_pool_class = mocker.patch( - "wool.runtime.worker.process.ResourcePool", return_value=mock_resource_pool + "wool.runtime.worker.process.ReferenceCountedCache", + return_value=mock_resource_pool, ) mock_server = mocker.MagicMock() @@ -821,7 +822,7 @@ def test_run_sends_port_through_pipe(self, mocker): process = WorkerProcess(host="0.0.0.0", port=8080) mocker.patch("wool.runtime.worker.process.wool.__proxy_pool__") - mocker.patch("wool.runtime.worker.process.ResourcePool") + mocker.patch("wool.runtime.worker.process.ReferenceCountedCache") mock_server = mocker.MagicMock() mock_server.add_insecure_port = mocker.MagicMock(return_value=8080) @@ -872,7 +873,7 @@ def test_run_with_extra_and_tags_serializes_complete_metadata(self, mocker): ) mocker.patch("wool.runtime.worker.process.wool.__proxy_pool__") - mocker.patch("wool.runtime.worker.process.ResourcePool") + mocker.patch("wool.runtime.worker.process.ReferenceCountedCache") mock_server = mocker.MagicMock() mock_server.add_insecure_port = mocker.MagicMock(return_value=50051) @@ -921,7 +922,7 @@ def test_run_with_dict_extra_produces_mapping_proxy(self, mocker): process = WorkerProcess(extra={"key": "value"}) mocker.patch.object(process_module.wool, "__proxy_pool__") - mocker.patch.object(process_module, "ResourcePool") + mocker.patch.object(process_module, "ReferenceCountedCache") mock_server = mocker.MagicMock() mock_server.add_insecure_port = mocker.MagicMock(return_value=50051) @@ -969,7 +970,7 @@ def test_run_closes_pipe_even_on_error(self, mocker): process = WorkerProcess() mocker.patch("wool.runtime.worker.process.wool.__proxy_pool__") - mocker.patch("wool.runtime.worker.process.ResourcePool") + mocker.patch("wool.runtime.worker.process.ReferenceCountedCache") mock_server = mocker.MagicMock() mock_server.add_insecure_port = mocker.MagicMock(return_value=50051) @@ -1012,7 +1013,7 @@ def test_run_stops_server_even_on_service_error(self, mocker): process = WorkerProcess(shutdown_grace_period=45.0) mocker.patch("wool.runtime.worker.process.wool.__proxy_pool__") - mocker.patch("wool.runtime.worker.process.ResourcePool") + mocker.patch("wool.runtime.worker.process.ReferenceCountedCache") mock_server = mocker.MagicMock() mock_server.add_insecure_port = mocker.MagicMock(return_value=50051) @@ -1465,7 +1466,7 @@ def test_run_with_default_options_passes_grpc_options(self, mocker): process = WorkerProcess() mocker.patch("wool.runtime.worker.process.wool.__proxy_pool__") - mocker.patch("wool.runtime.worker.process.ResourcePool") + mocker.patch("wool.runtime.worker.process.ReferenceCountedCache") mock_server = mocker.MagicMock() mock_server.add_insecure_port = mocker.MagicMock(return_value=50051) @@ -1525,7 +1526,7 @@ def test_run_with_custom_options_passes_grpc_options(self, mocker): process = WorkerProcess(options=opts) mocker.patch("wool.runtime.worker.process.wool.__proxy_pool__") - mocker.patch("wool.runtime.worker.process.ResourcePool") + mocker.patch("wool.runtime.worker.process.ReferenceCountedCache") mock_server = mocker.MagicMock() mock_server.add_insecure_port = mocker.MagicMock(return_value=50051) @@ -1588,7 +1589,7 @@ def test_run_with_all_keepalive_options(self, mocker): process = WorkerProcess(options=opts) mocker.patch("wool.runtime.worker.process.wool.__proxy_pool__") - mocker.patch("wool.runtime.worker.process.ResourcePool") + mocker.patch("wool.runtime.worker.process.ReferenceCountedCache") mock_server = mocker.MagicMock() mock_server.add_insecure_port = mocker.MagicMock(return_value=50051) @@ -1637,7 +1638,7 @@ def test_run_with_default_keepalive_options(self, mocker): process = WorkerProcess() mocker.patch("wool.runtime.worker.process.wool.__proxy_pool__") - mocker.patch("wool.runtime.worker.process.ResourcePool") + mocker.patch("wool.runtime.worker.process.ReferenceCountedCache") mock_server = mocker.MagicMock() mock_server.add_insecure_port = mocker.MagicMock(return_value=50051) @@ -1691,7 +1692,7 @@ def test_run_with_lifecycle_options(self, mocker): process = WorkerProcess(options=opts) mocker.patch("wool.runtime.worker.process.wool.__proxy_pool__") - mocker.patch("wool.runtime.worker.process.ResourcePool") + mocker.patch("wool.runtime.worker.process.ReferenceCountedCache") mock_server = mocker.MagicMock() mock_server.add_insecure_port = mocker.MagicMock(return_value=50051) @@ -1737,7 +1738,7 @@ def test_run_without_lifecycle_options(self, mocker): process = WorkerProcess() mocker.patch("wool.runtime.worker.process.wool.__proxy_pool__") - mocker.patch("wool.runtime.worker.process.ResourcePool") + mocker.patch("wool.runtime.worker.process.ReferenceCountedCache") mock_server = mocker.MagicMock() mock_server.add_insecure_port = mocker.MagicMock(return_value=50051) @@ -1790,7 +1791,7 @@ def test_run_serializes_channel_options_in_metadata(self, mocker): process = WorkerProcess(options=opts) mocker.patch("wool.runtime.worker.process.wool.__proxy_pool__") - mocker.patch("wool.runtime.worker.process.ResourcePool") + mocker.patch("wool.runtime.worker.process.ReferenceCountedCache") mock_server = mocker.MagicMock() mock_server.add_insecure_port = mocker.MagicMock(return_value=50051) @@ -1847,7 +1848,7 @@ def test_run_sets_worker_credentials_contextvar(self, mocker): captured_current = [] mocker.patch("wool.runtime.worker.process.wool.__proxy_pool__") - mocker.patch.object(process_module, "ResourcePool") + mocker.patch.object(process_module, "ReferenceCountedCache") mock_server = mocker.MagicMock() mock_server.add_secure_port = mocker.MagicMock(return_value=50051) @@ -1892,7 +1893,7 @@ def test_run_does_not_set_contextvar_when_credentials_none(self, mocker): captured_current = [] mocker.patch("wool.runtime.worker.process.wool.__proxy_pool__") - mocker.patch.object(process_module, "ResourcePool") + mocker.patch.object(process_module, "ReferenceCountedCache") mock_server = mocker.MagicMock() mock_server.add_insecure_port = mocker.MagicMock(return_value=50051) From 3fc672302bf144bfa542cca1c7170b14d81f0e08 Mon Sep 17 00:00:00 2001 From: Conrad Date: Tue, 7 Apr 2026 13:54:07 -0400 Subject: [PATCH 2/3] refactor: Rename Resource to Reference --- wool/src/wool/runtime/cache.py | 16 ++++++++-------- wool/src/wool/runtime/discovery/pool.py | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/wool/src/wool/runtime/cache.py b/wool/src/wool/runtime/cache.py index f32e48d0..f4ff216e 100644 --- a/wool/src/wool/runtime/cache.py +++ b/wool/src/wool/runtime/cache.py @@ -15,18 +15,18 @@ 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:`ReferenceCountedCache` 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: ReferenceCountedCache[T], key): @@ -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: """ diff --git a/wool/src/wool/runtime/discovery/pool.py b/wool/src/wool/runtime/discovery/pool.py index 27658c80..76b9de24 100644 --- a/wool/src/wool/runtime/discovery/pool.py +++ b/wool/src/wool/runtime/discovery/pool.py @@ -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.cache.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 @@ -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.cache.Resource` is entered lazily + :class:`~wool.runtime.cache.Reference` is entered lazily on first iteration. Subscriber classes pass a ``key`` keyword argument at class From c1bfe94ae6b95d89ee6c92586fb59793c3995a2e Mon Sep 17 00:00:00 2001 From: Conrad Date: Tue, 7 Apr 2026 13:54:11 -0400 Subject: [PATCH 3/3] test: Update tests for Resource to Reference rename --- wool/tests/runtime/test_cache.py | 158 +++++++++++++++++++------------ 1 file changed, 100 insertions(+), 58 deletions(-) diff --git a/wool/tests/runtime/test_cache.py b/wool/tests/runtime/test_cache.py index 17de034b..9a3da4a9 100644 --- a/wool/tests/runtime/test_cache.py +++ b/wool/tests/runtime/test_cache.py @@ -11,8 +11,8 @@ from hypothesis import strategies import wool.runtime.cache as cache_module +from wool.runtime.cache import Reference from wool.runtime.cache import ReferenceCountedCache -from wool.runtime.cache import Resource # Global tracking for factory and finalizer calls using function names as keys call_tracker = defaultdict(lambda: {"factory_calls": [], "finalizer_calls": []}) @@ -195,13 +195,17 @@ def mock_finalizer(): @pytest.fixture def resource_pool_with_ttl(mock_resource_factory, mock_finalizer): """Create a resource pool configured for TTL testing.""" - return ReferenceCountedCache(factory=mock_resource_factory, finalizer=mock_finalizer, ttl=0.1) + return ReferenceCountedCache( + factory=mock_resource_factory, finalizer=mock_finalizer, ttl=0.1 + ) @pytest.fixture def resource_pool_immediate_cleanup(mock_resource_factory, mock_finalizer): """Create a resource pool with TTL=0 for immediate cleanup testing.""" - return ReferenceCountedCache(factory=mock_resource_factory, finalizer=mock_finalizer, ttl=0) + return ReferenceCountedCache( + factory=mock_resource_factory, finalizer=mock_finalizer, ttl=0 + ) @pytest.fixture @@ -263,15 +267,15 @@ async def setup(): @pytest.mark.asyncio @given(setup=setup()) - async def test_get_returns_resource_instance(self, setup): - """Test that get returns a Resource instance. + async def test_get_returns_reference_instance(self, setup): + """Test that get returns a Reference instance. Given: A pool with various initial resource states When: get() is called with a test key Then: - Should return a Resource instance + Should return a Reference instance """ # Arrange pool, _, _, _ = await setup() @@ -280,10 +284,12 @@ async def test_get_returns_resource_instance(self, setup): resource_acquisition = pool.get("test-key") # Assert - assert isinstance(resource_acquisition, Resource) + assert isinstance(resource_acquisition, Reference) @pytest.mark.asyncio - @pytest.mark.dependency("TestReferenceCountedCache::test_get_returns_resource_instance") + @pytest.mark.dependency( + "TestReferenceCountedCache::test_get_returns_reference_instance" + ) async def test_release_decrements_reference_counts(self): """Test releasing resources decrements reference counts properly. @@ -323,7 +329,9 @@ async def test_release_decrements_reference_counts(self): assert pool.stats.referenced_entries == 0 @pytest.mark.asyncio - @pytest.mark.dependency("TestReferenceCountedCache::test_release_decrements_reference_counts") + @pytest.mark.dependency( + "TestReferenceCountedCache::test_release_decrements_reference_counts" + ) async def test_release_nonexistent_key_raises_error(self, counting_factory): """Test releasing nonexistent key raises KeyError. @@ -355,7 +363,9 @@ async def test_release_nonexistent_key_raises_error(self, counting_factory): assert pool.stats.referenced_entries == 0 @pytest.mark.asyncio - @pytest.mark.dependency("TestReferenceCountedCache::test_release_decrements_reference_counts") + @pytest.mark.dependency( + "TestReferenceCountedCache::test_release_decrements_reference_counts" + ) async def test_release_zero_reference_count_raises_error(self): """Test releasing key with zero ref count raises ValueError. @@ -372,7 +382,9 @@ async def test_release_zero_reference_count_raises_error(self): # so the resource stays in cache after release mock_factory = Mock() mock_finalizer = AsyncMock() - ttl_pool = ReferenceCountedCache(factory=mock_factory, finalizer=mock_finalizer, ttl=60) + ttl_pool = ReferenceCountedCache( + factory=mock_factory, finalizer=mock_finalizer, ttl=60 + ) unique_key = "test-zero-ref-count" mock_resource = Mock() @@ -392,7 +404,9 @@ async def test_release_zero_reference_count_raises_error(self): await ttl_pool.release(unique_key) @pytest.mark.asyncio - @pytest.mark.dependency("TestReferenceCountedCache::test_get_returns_resource_instance") + @pytest.mark.dependency( + "TestReferenceCountedCache::test_get_returns_reference_instance" + ) async def test_clear_finalizes_all_resources(self): """Test clearing the pool calls finalizer on all resources. @@ -406,7 +420,9 @@ async def test_clear_finalizes_all_resources(self): # Arrange - Create pool with TTL to keep resources after context exit mock_factory = Mock() mock_finalizer = AsyncMock() - pool = ReferenceCountedCache(factory=mock_factory, finalizer=mock_finalizer, ttl=60) + pool = ReferenceCountedCache( + factory=mock_factory, finalizer=mock_finalizer, ttl=60 + ) # Create some resources test_resources = [] @@ -431,7 +447,9 @@ async def test_clear_finalizes_all_resources(self): assert mock_finalizer.call_count == 3 @pytest.mark.asyncio - @pytest.mark.dependency("TestReferenceCountedCache::test_get_returns_resource_instance") + @pytest.mark.dependency( + "TestReferenceCountedCache::test_get_returns_reference_instance" + ) async def test_clear_key_removes_specific_resource(self, mock_finalizer): """Test clearing a specific key from the pool. @@ -444,7 +462,9 @@ async def test_clear_key_removes_specific_resource(self, mock_finalizer): """ # Arrange mock_factory = Mock() - pool = ReferenceCountedCache(factory=mock_factory, finalizer=mock_finalizer, ttl=60) + pool = ReferenceCountedCache( + factory=mock_factory, finalizer=mock_finalizer, ttl=60 + ) # Create multiple resources mock_resource1 = Mock() @@ -490,7 +510,9 @@ async def test_clear_nonexistent_key_raises_error(self): # Arrange mock_factory = Mock() mock_finalizer = AsyncMock() - pool = ReferenceCountedCache(factory=mock_factory, finalizer=mock_finalizer, ttl=60) + pool = ReferenceCountedCache( + factory=mock_factory, finalizer=mock_finalizer, ttl=60 + ) # Create one resource mock_resource = Mock() @@ -513,7 +535,9 @@ async def test_clear_nonexistent_key_raises_error(self): mock_finalizer.assert_not_called() @pytest.mark.asyncio - @pytest.mark.dependency("TestReferenceCountedCache::test_get_returns_resource_instance") + @pytest.mark.dependency( + "TestReferenceCountedCache::test_get_returns_reference_instance" + ) async def test_ttl_cleanup_schedules_resource_removal(self): """Test TTL-based cleanup schedules and executes properly. @@ -527,7 +551,9 @@ async def test_ttl_cleanup_schedules_resource_removal(self): # Arrange mock_factory = Mock() mock_finalizer = AsyncMock() - pool = ReferenceCountedCache(factory=mock_factory, finalizer=mock_finalizer, ttl=0.1) + pool = ReferenceCountedCache( + factory=mock_factory, finalizer=mock_finalizer, ttl=0.1 + ) mock_resource = Mock() mock_factory.return_value = mock_resource @@ -573,7 +599,9 @@ async def mock_sleep(_delay): mock_finalizer.assert_called_once_with(mock_resource) @pytest.mark.asyncio - @pytest.mark.dependency("TestReferenceCountedCache::test_get_returns_resource_instance") + @pytest.mark.dependency( + "TestReferenceCountedCache::test_get_returns_reference_instance" + ) async def test_ttl_cleanup_cancelled_on_reacquire(self): """Test TTL cleanup is cancelled when resource is reacquired. @@ -587,7 +615,9 @@ async def test_ttl_cleanup_cancelled_on_reacquire(self): # Arrange mock_factory = Mock() mock_finalizer = AsyncMock() - pool = ReferenceCountedCache(factory=mock_factory, finalizer=mock_finalizer, ttl=0.1) + pool = ReferenceCountedCache( + factory=mock_factory, finalizer=mock_finalizer, ttl=0.1 + ) mock_resource = Mock() mock_factory.return_value = mock_resource @@ -619,7 +649,9 @@ async def test_ttl_cleanup_cancelled_on_reacquire(self): assert pool.stats.referenced_entries == 0 @pytest.mark.asyncio - @pytest.mark.dependency("TestReferenceCountedCache::test_get_returns_resource_instance") + @pytest.mark.dependency( + "TestReferenceCountedCache::test_get_returns_reference_instance" + ) async def test_stats_returns_accurate_counts(self): """Test stats method returns accurate cache statistics. @@ -634,7 +666,9 @@ async def test_stats_returns_accurate_counts(self): # Arrange mock_factory = Mock() mock_finalizer = AsyncMock() - pool = ReferenceCountedCache(factory=mock_factory, finalizer=mock_finalizer, ttl=0.1) + pool = ReferenceCountedCache( + factory=mock_factory, finalizer=mock_finalizer, ttl=0.1 + ) # Start with empty pool stats = pool.stats @@ -656,7 +690,9 @@ async def test_stats_returns_accurate_counts(self): assert stats.pending_cleanup == 0 # None scheduled yet @pytest.mark.asyncio - @pytest.mark.dependency("TestReferenceCountedCache::test_get_returns_resource_instance") + @pytest.mark.dependency( + "TestReferenceCountedCache::test_get_returns_reference_instance" + ) async def test_async_context_manager_clears_resources(self): """Test ReferenceCountedCache as async context manager clears all on exit. @@ -672,7 +708,9 @@ async def test_async_context_manager_clears_resources(self): mock_finalizer = AsyncMock() # Act & assert - async with ReferenceCountedCache(factory=mock_factory, finalizer=mock_finalizer) as pool: + async with ReferenceCountedCache( + factory=mock_factory, finalizer=mock_finalizer + ) as pool: mock_resource = Mock() mock_factory.return_value = mock_resource @@ -718,7 +756,9 @@ def mock_finalizer_func(obj): mock_finalizer = Mock(side_effect=mock_finalizer_func) - pool = ReferenceCountedCache(factory=mock_factory, finalizer=mock_finalizer, ttl=ttl) + pool = ReferenceCountedCache( + factory=mock_factory, finalizer=mock_finalizer, ttl=ttl + ) async with pool.get("test-key"): pass @@ -899,15 +939,15 @@ async def test_get_with_none_key_handles_gracefully(self): assert pool.stats.total_entries == 0 -class TestResource: - """Test suite for the Resource class.""" +class TestReference: + """Test suite for the Reference class.""" @pytest.mark.asyncio async def test_context_manager_auto_releases(self): - """Test Resource as async context manager. + """Test Reference as async context manager. Given: - A Resource instance from a pool + A Reference instance from a pool When: Used as async context manager Then: @@ -922,7 +962,7 @@ async def test_context_manager_auto_releases(self): pool = ReferenceCountedCache(factory=mock_factory, ttl=0) # Act & assert - # Use Resource as context manager + # Use Reference as context manager async with pool.get("test-key") as resource: assert resource is mock_resource assert pool.stats.total_entries == 1 @@ -932,11 +972,11 @@ async def test_context_manager_auto_releases(self): assert pool.stats.total_entries == 0 @pytest.mark.asyncio - async def test_resource_has_no_manual_release_method(self): - """Test Resource has no manual release method. + async def test_reference_has_no_manual_release_method(self): + """Test Reference has no manual release method. Given: - A Resource instance + A Reference instance When: Checking for release method Then: @@ -956,11 +996,11 @@ async def test_resource_has_no_manual_release_method(self): assert not hasattr(resource_acquisition, "release") @pytest.mark.asyncio - async def test_resource_lifecycle_with_ttl(self): - """Test Resource lifecycle with TTL keeps resource in cache. + async def test_reference_lifecycle_with_ttl(self): + """Test Reference lifecycle with TTL keeps resource in cache. Given: - A Resource instance with TTL pool + A Reference instance with TTL pool When: Used as context manager Then: @@ -971,7 +1011,9 @@ async def test_resource_lifecycle_with_ttl(self): mock_resource = Mock() mock_factory.return_value = mock_resource - pool = ReferenceCountedCache(factory=mock_factory, ttl=60) # Use TTL to keep resource + pool = ReferenceCountedCache( + factory=mock_factory, ttl=60 + ) # Use TTL to keep resource resource_acquisition = pool.get("test-key") @@ -986,10 +1028,10 @@ async def test_resource_lifecycle_with_ttl(self): @pytest.mark.asyncio async def test_context_manager_only_usage_handles_lifecycle(self): - """Test using Resource only as context manager. + """Test using Reference only as context manager. Given: - A Resource instance + A Reference instance When: Used only as context manager Then: @@ -1013,10 +1055,10 @@ async def test_context_manager_only_usage_handles_lifecycle(self): @pytest.mark.asyncio async def test_acquire_twice(self): - """Test that re-acquiring the same Resource instance raises error. + """Test that re-acquiring the same Reference instance raises error. Given: - A Resource that has been used as context manager once + A Reference that has been used as context manager once When: Attempting to use it as context manager again Then: @@ -1039,11 +1081,11 @@ async def test_acquire_twice(self): pass @pytest.mark.asyncio - async def test_resource_context_acquire_exception(self): - """Test Resource context manager handles acquire exceptions properly. + async def test_reference_context_acquire_exception(self): + """Test Reference context manager handles acquire exceptions properly. Given: - A Resource instance from a pool that fails during acquire + A Reference instance from a pool that fails during acquire When: Entering the context manager Then: @@ -1053,9 +1095,9 @@ async def test_resource_context_acquire_exception(self): mock_pool = AsyncMock() mock_pool.acquire.side_effect = RuntimeError("Acquire failed") - from wool.runtime.cache import Resource + from wool.runtime.cache import Reference - resource = Resource(pool=mock_pool, key="test-key") + resource = Reference(pool=mock_pool, key="test-key") # Act & assert with pytest.raises(RuntimeError, match="Acquire failed"): @@ -1066,21 +1108,21 @@ async def test_resource_context_acquire_exception(self): assert resource._acquired is False @pytest.mark.asyncio - async def test_resource_context_release_not_acquired(self): - """Test Resource release when not acquired raises RuntimeError. + async def test_reference_context_release_not_acquired(self): + """Test Reference release when not acquired raises RuntimeError. Given: - A Resource instance that was never acquired + A Reference instance that was never acquired When: Attempting to exit context without entering properly Then: - Should raise RuntimeError indicating resource was not acquired + Should raise RuntimeError indicating reference was not acquired """ # Arrange mock_pool = AsyncMock() - from wool.runtime.cache import Resource + from wool.runtime.cache import Reference - resource = Resource(pool=mock_pool, key="test-key") + resource = Reference(pool=mock_pool, key="test-key") # Act & assert - manually call __aexit__ without calling __aenter__ with pytest.raises( @@ -1089,24 +1131,24 @@ async def test_resource_context_release_not_acquired(self): await resource.__aexit__(None, None, None) @pytest.mark.asyncio - async def test_resource_context_release_already_released(self): - """Test Resource release when already released raises RuntimeError. + async def test_reference_context_release_already_released(self): + """Test Reference release when already released raises RuntimeError. Given: - A Resource instance that was already released + A Reference instance that was already released When: Attempting to exit context again after normal usage Then: - Should raise RuntimeError indicating resource was already released + Should raise RuntimeError indicating reference was already released """ # Arrange mock_pool = AsyncMock() mock_resource = Mock() mock_pool.acquire.return_value = mock_resource - from wool.runtime.cache import Resource + from wool.runtime.cache import Reference - resource = Resource(pool=mock_pool, key="test-key") + resource = Reference(pool=mock_pool, key="test-key") # Use normally once (which sets _released = True) async with resource: