mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-20 11:42:23 +02:00
40 lines
1.4 KiB
Python
40 lines
1.4 KiB
Python
"""One asyncio primitive per event loop, so module-level state can never outlive the loop it used."""
|
|
|
|
import asyncio
|
|
from typing import Callable, Optional, TypeVar
|
|
|
|
T = TypeVar("T")
|
|
|
|
|
|
def loop_local(factory: Callable[[], T]) -> Callable[[], T]:
|
|
"""Wrap an asyncio primitive so it is rebuilt whenever the running event loop changes.
|
|
|
|
A module-level ``asyncio.Lock()`` outlives the loop that used it. If that loop dies while the
|
|
lock is HELD, the flag stays set forever and the next loop waits on a release that can never
|
|
come: no error, no log line, just a process that stops. That is what wedged the entire backend
|
|
test suite (ENG-219), and after a ``uvicorn --reload`` it is the same silent hang in the app.
|
|
A Semaphore loses its count the same way; an Event raises "bound to a different event loop"
|
|
and kills whatever loop was driving it.
|
|
|
|
Pass the CLASS, not an instance, and call the result::
|
|
|
|
p_boot_lock = loop_local(asyncio.Lock)
|
|
|
|
async with p_boot_lock():
|
|
...
|
|
|
|
Needs a running loop, which is the whole point: nothing else can say which loop to build for.
|
|
"""
|
|
held: Optional[T] = None
|
|
held_loop: Optional[asyncio.AbstractEventLoop] = None
|
|
|
|
def get() -> T:
|
|
nonlocal held, held_loop
|
|
running = asyncio.get_running_loop()
|
|
if held is None or held_loop is not running:
|
|
held = factory()
|
|
held_loop = running
|
|
return held
|
|
|
|
return get
|