Skip to content

Guard WorkerPool and WorkerProxy against reentrant context usage — Closes #145#146

Merged
conradbzura merged 5 commits intowool-labs:mainfrom
conradbzura:145-reentry-guards-pool-proxy
Apr 1, 2026
Merged

Guard WorkerPool and WorkerProxy against reentrant context usage — Closes #145#146
conradbzura merged 5 commits intowool-labs:mainfrom
conradbzura:145-reentry-guards-pool-proxy

Conversation

@conradbzura
Copy link
Copy Markdown
Contributor

@conradbzura conradbzura commented Mar 31, 2026

Summary

Adds single-use enforcement to WorkerPool and WorkerProxy context managers so that any second entry — whether reentrant or after a full enter/exit cycle — raises RuntimeError immediately rather than silently corrupting state.

The implementation introduces a generic @noreentry descriptor class in src/wool/utilities/noreentry.py. The descriptor exclusively decorates instance methods (bare functions and unbound calls are static errors caught at the type-check level via __call__ -> Never). Guard state for each instance is tracked via a weakref.WeakSet on the descriptor, keeping instances clean without namespace pollution. functools.update_wrapper and inspect.markcoroutinefunction ensure the wrapper is transparent to introspection. The decorator is applied to WorkerProxy.enter() and WorkerPool.__aenter__(). Both READMEs are updated to document the single-use contract.

Closes #145.

Proposed changes

src/wool/utilities/noreentry.py — new @noreentry descriptor

Implements noreentry as a descriptor class for single-use method guards. The decorator uses the descriptor protocol's __get__ to return a bound wrapper on instance access, and __call__ -> Never to reject bare function usage at the type-check level.

Guard state is tracked via a weakref.WeakSet on the descriptor instance — when an instance's guarded method is called, that instance is added to the WeakSet. Any subsequent call to the same method on the same instance detects the instance in the set and raises RuntimeError. This approach avoids polluting instance namespaces and automatically cleans up references when instances are garbage collected.

The decorator works with both sync and async methods. Attempting to use it on bare functions or call it directly (unbound style) raises TypeError at runtime and is flagged as a static type error via the Never return annotation on __call__.

src/wool/runtime/worker/proxy.py and pool.py — apply guard

@noreentry is applied to WorkerProxy.enter() and WorkerPool.__aenter__(). The :raises RuntimeError: docstring on each method is updated to document single-use semantics.

Documentation

README.md and src/wool/runtime/worker/README.md each receive a note on the single-use contract with a brief code example showing the correct pattern (create a new instance for a new context).

Test cases

# Test Suite Given When Then Coverage Target
1 TestNoReentry A class with a sync @noreentry method Method is called for the first time Returns normally First-call success for bound sync method
2 TestNoReentry A class with a sync @noreentry method called once Method is called a second time on the same instance Raises RuntimeError Guard on second sync call
3 TestNoReentry A class with an async @noreentry method Method is awaited for the first time Returns normally First-call success for bound async method
4 TestNoReentry A class with an async @noreentry method awaited once Method is awaited a second time Raises RuntimeError Guard on second async call
5 TestNoReentry Two instances of a class with a @noreentry method, first called Method is called on the second instance Returns normally Per-instance isolation
6 TestNoReentry A sync @noreentry method called once Method is called a second time RuntimeError message includes the method's __qualname__ Error message content
7 TestNoReentry A class with an async @noreentry method asyncio.iscoroutinefunction is called on the method Returns True Coroutine function introspection transparency
8 TestNoReentry A class with a sync @noreentry method __name__ is inspected on the decorated method Equals the original function name functools.wraps preservation
9 TestNoReentry A class with two @noreentry methods; first called twice Second method is called Returns normally Per-method dict key independence
10 TestNoReentry A class with a @noreentry method Method is accessed through the class (unbound) Returns the descriptor itself Descriptor protocol for unbound access
11 TestNoReentry A @noreentry descriptor accessed unbound Descriptor is called directly Raises TypeError Rejection of bare function usage
12 TestWorkerProxy A WorkerProxy that has already been entered enter() is called a second time Raises RuntimeError Reentrant entry guard on proxy
13 TestWorkerProxy A WorkerProxy that has been entered and exited enter() is called again Raises RuntimeError Post-exit re-entry guard on proxy
14 TestWorkerPool A WorkerPool already entered via async with Pool is entered again via async with Raises RuntimeError Reentrant entry guard on pool
15 TestWorkerPool A WorkerPool entered and exited via async with Pool is entered again via async with Raises RuntimeError Post-exit re-entry guard on pool

@conradbzura conradbzura marked this pull request as ready for review March 31, 2026 15:52
@conradbzura conradbzura force-pushed the 145-reentry-guards-pool-proxy branch 2 times, most recently from ec65baa to e6d4ed4 Compare April 1, 2026 13:58
@conradbzura conradbzura self-assigned this Apr 1, 2026
@conradbzura conradbzura force-pushed the 145-reentry-guards-pool-proxy branch 3 times, most recently from 8efd9a1 to fd191d7 Compare April 1, 2026 15:25
Implements a generic @noreentry descriptor class that prevents instance
methods from being invoked more than once. Guard state is tracked via a
weakref.WeakSet on the descriptor instance — when a method is called on
an instance, that instance is added to the set. Any subsequent call raises
RuntimeError. This approach keeps instance namespaces clean and auto-cleans
references when instances are garbage collected.

The descriptor works with both sync and async methods. Attempting to use it
on bare functions or call it directly raises TypeError at runtime and is
flagged as a static error via the Never return annotation on __call__.
…usage

Apply the @noreentry decorator to WorkerProxy.enter() and WorkerPool.__aenter__()
to enforce single-use semantics. Both context managers now reject any attempt
to re-enter (whether reentrant or after a full enter/exit cycle) with RuntimeError.

Update docstrings to document the single-use contract and explain that users
must create a new instance for a new context.
Add and update tests to verify single-use enforcement on WorkerPool and
WorkerProxy. New test cases cover:

- Reentrant entry (async with pool within pool context)
- Post-exit re-entry (entering pool again after full cycle)
- RuntimeError is raised with appropriate message

These tests validate that the noreentry guard works correctly across both
context managers and complements the comprehensive unit tests for the
noreentry descriptor itself.
Add documentation to the worker README explaining the single-use contract
for WorkerPool and WorkerProxy. Include examples of correct usage patterns
and clarify that both context managers must be re-created for each context.

This complements the updated docstrings in pool.py and proxy.py and ensures
users understand the single-use semantics enforced by the @noreentry guard.
Conditionally set the asyncio.coroutines._is_coroutine marker when
Python version is 3.11. This ensures that asyncio.iscoroutinefunction()
returns True for async noreentry methods on Python 3.11 and earlier.
@conradbzura conradbzura force-pushed the 145-reentry-guards-pool-proxy branch from fd191d7 to 077199e Compare April 1, 2026 15:27
@conradbzura conradbzura merged commit b9369d7 into wool-labs:main Apr 1, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Guard WorkerPool and WorkerProxy against reentrant context usage

1 participant