duck.utils.asyncio.eventloop¶
Module Contents¶
Classes¶
A manager that runs an asyncio event loop in a background thread, with optional task type protection. |
Functions¶
Retrieve or create an |
Data¶
API¶
- class duck.utils.asyncio.eventloop.AsyncioLoopManager(creator_thread: Optional[threading.Thread] = None, _id: Optional[Union[str, int]] = None)[source]¶
A manager that runs an asyncio event loop in a background thread, with optional task type protection.
Notes:
This starts an asyncio event loop in a single global thread.
Allows submitting coroutines from synchronous code.
Supports synchronous result blocking via SyncFuture.
Can restrict tasks by type to prevent inappropriate coroutine submission to protected loops.
Using the event loop directly is not recommended and it seems not to work, use
submit_taskinstead.
Initialization
Initialize the threadpool.
- Parameters:
creator_thread – This is the thread responsible for this manager.’
_id – A custom Unique Identifier for the manager.
- __instances¶
[]
This is the list of created instances.
- classmethod all_instances() List[duck.utils.asyncio.eventloop.AsyncioLoopManager][source]¶
Returns a list of all created instances so far.
- get_event_loop() asyncio.AbstractEventLoop[source]¶
Returns the running asyncio event loop.
- Returns:
The currently running event loop.
- Return type:
asyncio.AbstractEventLoop
- Raises:
RuntimeError – If the loop isn’t running.
- classmethod registry() Dict[int, Dict[Any, duck.utils.asyncio.eventloop.AsyncioLoopManager]][source]¶
Returns the registry for created instances. Useful for tracking.
- start(task_type: Optional[str] = None)[source]¶
Starts the event loop in a background thread if it’s not already running.
- Parameters:
task_type – The expected type of async tasks for this loop. If set, only tasks with matching type will be accepted.
- Raises:
RuntimeError – If event loop is not None and loop’s thread is alive.
- stop()[source]¶
Stops the event loop and waits for the thread to finish.
… admonition:: Notes
This should be called to clean up the background loop thread.
- submit_task(coro: Coroutine, return_sync_future: bool = False, task_type: Optional[str] = None) Union[duck.utils.threading.SyncFuture, asyncio.Future][source]¶
Submit an asynchronous coroutine to the event loop, with optional task type protection.
- Parameters:
coro – The coroutine to schedule.
return_sync_future – If True, wraps the result in a SyncFuture for blocking use.
task_type – The type of this async task; must match manager’s required type if set.
- Returns:
A future representing the scheduled coroutine.
- Return type:
Union[SyncFuture, asyncio.Future]
- Raises:
UnknownAsyncTaskError – If the provided type doesn’t match the loop’s protected type.
RuntimeError – If the event loop is not running.
- exception duck.utils.asyncio.eventloop.ManagerNotFound[source]¶
Bases:
ExceptionRaised if manager cannot be resolved and user strictly wants to get the manager and user doesn’t allow creating it if it doesn’t exist.
Initialization
Initialize self. See help(type(self)) for accurate signature.
- duck.utils.asyncio.eventloop.REGISTRY¶
None
- exception duck.utils.asyncio.eventloop.UnknownAsyncTaskError(given_type, expected_type)[source]¶
Bases:
ExceptionRaised when attempting to submit a coroutine of a disallowed or unexpected type to a protected asyncio loop.
Initialization
Initialize self. See help(type(self)) for accurate signature.
- duck.utils.asyncio.eventloop.get_or_create_loop_manager(id: Optional[Any] = None, force_create: bool = False, strictly_get: bool = False) AsyncioLoopManager[source]¶
Retrieve or create an
AsyncioLoopManagerinstance scoped to the current thread lineage, with optional namespace isolation.This function provides a hierarchical registry: each thread may own multiple managers, identified by user-supplied
idnamespaces. If a manager is not found in the current thread, its parent thread is searched recursively, allowing thread families to inherit managers unless isolation is explicitly requested.Thread-scoped behavior
Without
force_create, the manager resolution walks upward through parent threads until it finds a matching manager for the givenid.With
force_create=True, a new manager is always created for the current thread, regardless of parent managers.
Use cases
Worker threads that must run async tasks in their own protected loop.
Request-scoped or component-scoped corruption-free async execution.
Shared background async loop across a thread tree, unless isolation is desired.
- Parameters:
id –
Optional namespace key. Managers with different
idvalues can coexist within the same thread. Using the sameidretrieves the same manager.Examples:
default_mgr = get_or_create_loop_manager() io_mgr = get_or_create_loop_manager(id="io") job_mgr = get_or_create_loop_manager(id="job-executor")force_create – If True, bypasses lineage resolution and creates a fresh manager bound to the current thread. This is required inside worker threads where isolated loops are needed.
strictly_get – Whether to strictly get the loop manager or raise an exception if manager not found.
- Raises:
ManagerNotFound – Raised if manager not found and
strictly_get=True.TypeError – If arguments
strictly_getandforce_createare both True.
Notes:
Calling this function in the main thread makes the returned manager automatically inherited by all descendant threads unless they specify
force_create=True.The lookup chain stops at the root thread; if no manager is found, a new one is created and registered.
If you already have created manager, use
strictly_getargument to strictly get your created manager or raise an exception if manager not found without creating new one.
- Returns:
The resolved manager or a newly created instance.
- Return type:
Asyncio Loop Manager With Thread-Based Context Propagation¶
This module provides a structured system for managing asyncio event loops that run inside dedicated background threads. It enables synchronous code (including worker threads, component systems, WSGI-like environments, and blocking server logic) to safely submit and await asyncio coroutines without requiring a running loop in the current thread.
Unlike globally shared event loops—which cause cross-context contamination, unbounded concurrency, and invalid reentrancy—each loop manager created by this module is thread-scoped and optional-task-type-protected, ensuring strong isolation for component rendering, per-request async operations, and specialized background jobs.
Core Components¶
AsyncioLoopManager A manager that:
initializes a dedicated asyncio event loop,
runs it inside a background thread,
allows synchronous code to submit async tasks,
optionally restricts allowed task types (e.g.,
"update","render"),supports blocking behavior via
SyncFuture.
This bridges synchronous and asynchronous subsystems safely, without requiring callers to understand asyncio internals or manage loops manually.
SyncFuture A thread-safe future that lets synchronous callers block until an async coroutine completes. This provides an ergonomic way to mix async/sync code without deadlocks or event-loop mismanagement.
Thread-Scoped Manager Registry Loop managers are registered per-thread (and per-ID), and automatically inherited by child threads via a lineage-resolution system. This ensures:
consistent async context for worker threads,
isolation of async operations across independent request handlers,
avoidance of global-state conflicts.
Task-Type Protection Optional enforcement that allows only a specific
task_typeto be submitted into a given loop. This prevents misrouting workloads—for example:keeping component-rendering coroutines separate from background updates,
preventing slow tasks from being scheduled into latency-critical loops,
enforcing strict execution domains for sensitive operations.
Motivation¶
Frameworks that must bridge the sync/async boundary—such as servers using thread-per-request models, component rendering systems, hybrid blocking/ non-blocking architectures, or environments where loops cannot be run directly—need a safe, isolated way to schedule coroutines.
Typical pitfalls solved by this module:¶
Running async code inside random threads: leads to
RuntimeError: no running event loop.Global event loop sharing: creates race conditions and cross-context pollution.
Worker threads needing async helpers without owning an event loop.
Component systems generating async work inside sync call stacks.
Preventing unrelated tasks from entering specialized async domains.
This module eliminates these pitfalls by providing predictable, isolated event loops that behave like mini-ASGI environments local to each thread.
Thread Lineage Model¶
Loop managers are resolved using a hierarchical lookup:
Start with the current thread.
If it has a manager for the requested ID, return it.
If not, recursively check its parent thread.
If none exist in the lineage, create and attach a new manager.
This mirrors context inheritance in modern frameworks and avoids global state entirely.
Multiple Manager Namespaces¶
Managers are identified by an optional id:
Calling
get_or_create_loop_manager(id="render-loop")creates or retrieves a loop dedicated to rendering operations.Calling
get_or_create_loop_manager(id="background-io")yields an entirely separate loop for I/O-heavy async tasks.
This encourages clean separation of execution domains without rewriting infrastructure.
Typical Usage
def worker():
loop_mgr = get_or_create_loop_manager(id="component")
loop_mgr.start(task_type="render")
# Submit coroutine and block for result
value = loop_mgr.submit_task(
some_async_function(),
return_sync_future=True,
task_type="render",
)
value = value.result()
return value
Best Practices¶
Always resolve loop managers inside worker threads to avoid sharing the main thread’s manager unintentionally.
Use task-type protection to ensure coroutine routing guarantees.
Use unique IDs for logically distinct async domains.
Never directly interact with the internal REGISTRY.
This module provides a safe, controlled, extensible foundation for mixing async and synchronous workloads in complex frameworks such as the Duck Lively Component system.