Skip to content

fix(runtime): create background job tasks on the loop thread - #4

Merged
tickernelz merged 3 commits into
mainfrom
fix/zha2358-threadsafe-job-scheduling
Oct 18, 2026
Merged

tickernelz merged 3 commits into
mainfrom
fix/zha2358-threadsafe-job-scheduling

Conversation

@tickernelz

Copy link
Copy Markdown
Owner

ZHA-2358. post_tool_call runs on a worker thread, so _schedule_background_job_poll and _schedule_background_job_cleanup called loop.create_task off the loop thread: the task sat in _ready without waking the selector (poll deferred until an unrelated wakeup) and asyncio debug mode raised RuntimeError: Non-thread-safe operation invoked on an event loop other than the current one.

  • utils/scheduling.py: new schedule_task_on_loop(loop, start) runs start inline when already on the loop, otherwise hands it to loop.call_soon_threadsafe; on_loop_thread is now the shared thread check (clarify._on_loop removed).
  • runtime/tool_events.py: both background-job schedulers use it; the poll_task dedupe check and claim moved into the loop-side callback so the guard stays single-threaded.
  • Sweep: delivery.schedule_delayed_live_flush, delivery.schedule_auto_delete, renderer._schedule_delegate_cleanup and the clarify bridges only run inside coroutines already on the loop (reachable through submit_or_close/run_coroutine_threadsafe), so they keep the plain create_task_or_close.
  • Regression tests: real debug-mode loop on its own thread, poll/cleanup/_on_post_tool_call driven from a foreign thread. All three fail on 875a561 with the non-thread-safe RuntimeError.

post_tool_call runs on a worker thread, so _schedule_background_job_poll and _schedule_background_job_cleanup called loop.create_task off-loop: the task sat in _ready without waking the selector, and asyncio debug mode raised RuntimeError. Both now go through schedule_task_on_loop, which hops to the loop with call_soon_threadsafe; the poll dedupe check moved into the loop-side callback. Regression tests drive a real debug-mode loop on its own thread; all three fail on 875a561 with the non-thread-safe RuntimeError.
…allback

The loop-side _start callback races the started render, so the job record often does not exist yet and poll_task stayed empty for the poll lifetime (3/40 and 1/40 in review probes). _poll now claims its own task once the record exists. Regression assert: poll_task is set while the job is still running; it fails with this block removed.
@tickernelz
tickernelz merged commit 001bf98 into main Oct 18, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant