Files
ware_house/async_loop_singleton.py

73 lines
2.3 KiB
Python

"""Shared asyncio loop lifecycle helpers for the desktop application.
The GUI runs on Tk's main thread, while database calls are executed on a
dedicated background event loop. These helpers expose that loop as a lazy
singleton so every module can schedule work on the same async runtime.
"""
import asyncio
import threading
import contextlib
class _LoopHolder:
"""Store the global loop instance and its worker thread."""
def __init__(self):
self.loop = None
self.thread = None
self.closing = False
_GLOBAL = _LoopHolder()
def get_global_loop() -> asyncio.AbstractEventLoop:
"""Return the shared background event loop.
The loop is created lazily the first time the function is called and kept
alive for the lifetime of the application.
"""
if _GLOBAL.loop and not _GLOBAL.closing:
return _GLOBAL.loop
ready = threading.Event()
def _run():
_GLOBAL.loop = asyncio.new_event_loop()
asyncio.set_event_loop(_GLOBAL.loop)
ready.set()
try:
_GLOBAL.loop.run_forever()
finally:
loop = _GLOBAL.loop
if loop is not None:
pending = [task for task in asyncio.all_tasks(loop) if not task.done()]
for task in pending:
task.cancel()
if pending:
with contextlib.suppress(Exception):
loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True))
with contextlib.suppress(Exception):
loop.run_until_complete(loop.shutdown_asyncgens())
with contextlib.suppress(Exception):
loop.run_until_complete(loop.shutdown_default_executor())
with contextlib.suppress(Exception):
loop.close()
_GLOBAL.thread = threading.Thread(target=_run, name="asyncio-bg-loop", daemon=True)
_GLOBAL.thread.start()
ready.wait()
return _GLOBAL.loop
def stop_global_loop():
"""Stop the shared loop and join the background thread if present."""
if _GLOBAL.loop and _GLOBAL.loop.is_running():
_GLOBAL.closing = True
_GLOBAL.loop.call_soon_threadsafe(_GLOBAL.loop.stop)
_GLOBAL.thread.join(timeout=2)
_GLOBAL.loop = None
_GLOBAL.thread = None
_GLOBAL.closing = False