fix(briefing): deadlock in scheduler startup
start_briefing_scheduler was called from before_serving (event loop thread) and used run_coroutine_threadsafe(...).result() which blocks the calling thread waiting for the coroutine to complete — but since the calling thread IS the event loop, the coroutine could never run, causing a 10s timeout and zero jobs scheduled. Fix: make start_briefing_scheduler async and await _get_briefing_enabled_users() directly. Also use asyncio.create_task for the catch-up rather than run_coroutine_threadsafe. The background thread jobs (_run_user_slot_sync) continue to use run_coroutine_threadsafe correctly since they run on the APScheduler thread, not the event loop thread. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -246,7 +246,7 @@ def create_app() -> Quart:
|
|||||||
|
|
||||||
# Start briefing scheduler
|
# Start briefing scheduler
|
||||||
from fabledassistant.services.briefing_scheduler import start_briefing_scheduler
|
from fabledassistant.services.briefing_scheduler import start_briefing_scheduler
|
||||||
start_briefing_scheduler(asyncio.get_running_loop())
|
await start_briefing_scheduler(asyncio.get_running_loop())
|
||||||
|
|
||||||
@app.after_serving
|
@app.after_serving
|
||||||
async def shutdown():
|
async def shutdown():
|
||||||
|
|||||||
@@ -296,10 +296,10 @@ async def _catchup_missed_slots(loop: asyncio.AbstractEventLoop) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def start_briefing_scheduler(loop: asyncio.AbstractEventLoop) -> None:
|
async def start_briefing_scheduler(loop: asyncio.AbstractEventLoop) -> None:
|
||||||
"""
|
"""
|
||||||
Start the APScheduler background scheduler with per-user timezone-aware jobs.
|
Start the APScheduler background scheduler with per-user timezone-aware jobs.
|
||||||
Must be called from the app's before_serving hook with the running event loop.
|
Must be awaited from the app's before_serving hook (async context).
|
||||||
"""
|
"""
|
||||||
global _scheduler, _loop
|
global _scheduler, _loop
|
||||||
if _scheduler is not None:
|
if _scheduler is not None:
|
||||||
@@ -308,10 +308,10 @@ def start_briefing_scheduler(loop: asyncio.AbstractEventLoop) -> None:
|
|||||||
_loop = loop
|
_loop = loop
|
||||||
_scheduler = BackgroundScheduler(timezone="UTC")
|
_scheduler = BackgroundScheduler(timezone="UTC")
|
||||||
|
|
||||||
# Schedule jobs synchronously: run the async query in the provided loop
|
# Await directly — we're already on the event loop, so run_coroutine_threadsafe
|
||||||
future = asyncio.run_coroutine_threadsafe(_get_briefing_enabled_users(), loop)
|
# would deadlock (it blocks the calling thread, which IS the event loop thread).
|
||||||
try:
|
try:
|
||||||
users = future.result(timeout=10)
|
users = await _get_briefing_enabled_users()
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Failed to load briefing users at startup")
|
logger.exception("Failed to load briefing users at startup")
|
||||||
users = []
|
users = []
|
||||||
@@ -325,7 +325,7 @@ def start_briefing_scheduler(loop: asyncio.AbstractEventLoop) -> None:
|
|||||||
len(users), len(users) * len(SLOTS),
|
len(users), len(users) * len(SLOTS),
|
||||||
)
|
)
|
||||||
|
|
||||||
asyncio.run_coroutine_threadsafe(_catchup_missed_slots(loop), loop)
|
asyncio.create_task(_catchup_missed_slots(loop))
|
||||||
|
|
||||||
|
|
||||||
def stop_briefing_scheduler() -> None:
|
def stop_briefing_scheduler() -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user