Guard WorkerPool and WorkerProxy against reentrant context usage — Closes #145#146
Merged
conradbzura merged 5 commits intowool-labs:mainfrom Apr 1, 2026
Merged
Conversation
conradbzura
commented
Mar 31, 2026
ec65baa to
e6d4ed4
Compare
8efd9a1 to
fd191d7
Compare
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.
fd191d7 to
077199e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds single-use enforcement to
WorkerPoolandWorkerProxycontext managers so that any second entry — whether reentrant or after a full enter/exit cycle — raisesRuntimeErrorimmediately rather than silently corrupting state.The implementation introduces a generic
@noreentrydescriptor class insrc/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 aweakref.WeakSeton the descriptor, keeping instances clean without namespace pollution.functools.update_wrapperandinspect.markcoroutinefunctionensure the wrapper is transparent to introspection. The decorator is applied toWorkerProxy.enter()andWorkerPool.__aenter__(). Both READMEs are updated to document the single-use contract.Closes #145.
Proposed changes
src/wool/utilities/noreentry.py— new@noreentrydescriptorImplements
noreentryas 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__ -> Neverto reject bare function usage at the type-check level.Guard state is tracked via a
weakref.WeakSeton 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 raisesRuntimeError. 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
TypeErrorat runtime and is flagged as a static type error via theNeverreturn annotation on__call__.src/wool/runtime/worker/proxy.pyandpool.py— apply guard@noreentryis applied toWorkerProxy.enter()andWorkerPool.__aenter__(). The:raises RuntimeError:docstring on each method is updated to document single-use semantics.Documentation
README.mdandsrc/wool/runtime/worker/README.mdeach 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
TestNoReentry@noreentrymethodTestNoReentry@noreentrymethod called onceRuntimeErrorTestNoReentry@noreentrymethodTestNoReentry@noreentrymethod awaited onceRuntimeErrorTestNoReentry@noreentrymethod, first calledTestNoReentry@noreentrymethod called onceRuntimeErrormessage includes the method's__qualname__TestNoReentry@noreentrymethodasyncio.iscoroutinefunctionis called on the methodTrueTestNoReentry@noreentrymethod__name__is inspected on the decorated methodfunctools.wrapspreservationTestNoReentry@noreentrymethods; first called twiceTestNoReentry@noreentrymethodTestNoReentry@noreentrydescriptor accessed unboundTypeErrorTestWorkerProxyWorkerProxythat has already been enteredenter()is called a second timeRuntimeErrorTestWorkerProxyWorkerProxythat has been entered and exitedenter()is called againRuntimeErrorTestWorkerPoolWorkerPoolalready entered viaasync withasync withRuntimeErrorTestWorkerPoolWorkerPoolentered and exited viaasync withasync withRuntimeError