Repository navigation
fix(OMN-13658): sync event_publisher loop affinity — schedule onto kernel loop via run_coroutine_threadsafe - #2133
Merged
jonahgabriel merged 1 commit intoJun 28, 2026
Conversation
…n_coroutine_threadsafe _make_sync_event_publisher captured no owning loop and, when invoked from a ThreadPoolExecutor worker thread (the dispatch engine offloads blocking sync handlers via run_in_executor), fell back to asyncio.run() which spun a throwaway loop in the worker thread. The publish awaitable's internal Futures are bound to the kernel loop, so running them on that foreign loop raised 'got Future attached to a different loop' and delayed terminal emission 2-3 min via retry. Capture the runtime kernel loop at construction time (wire_from_manifest runs on it) and, for publishes from any non-kernel thread, schedule the coroutine back onto the kernel loop with asyncio.run_coroutine_threadsafe. Publishes already on the kernel loop keep the create_task fast path. No new asyncio.run loop is ever spawned. Adds tests/unit/runtime/auto_wiring/test_sync_event_publisher_loop_affinity.py proving a worker-thread publish completes without error, never calls asyncio.run, and runs the coroutine on the kernel loop.
📝 WalkthroughWalkthrough
Sync Event Publisher Loop Affinity Fix
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
jonahgabriel
enabled auto-merge
June 28, 2026 06:57
github-merge-queue
Bot
removed this pull request from the merge queue due to a manual request
Jun 28, 2026
jonahgabriel
enabled auto-merge
June 28, 2026 07:15
jonahgabriel
deleted the
jonah/omn-13658-runtime-effect-publish-path-asyncio-event-loop-affinity-bug
branch
June 28, 2026 08:32
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes the asyncio event-loop affinity bug in the auto-wired sync
event_publisheradapter (
src/omnibase_infra/runtime/auto_wiring/handler_wiring.py,_make_sync_event_publisher).The dispatch engine offloads blocking sync handlers (e.g.
HandlerContextRoiRunner,node_context_roi_runner) onto aThreadPoolExecutorworker thread via
run_in_executor. When such a handler published a terminalevent, the old adapter called
asyncio.get_running_loop(), hitRuntimeError(worker threads own no loop), and fell back to
asyncio.run(...)— spinning athrowaway event loop in the worker thread.
The publish awaitable returned by the event bus binds its internal Futures to the
runtime kernel loop. Running it on that foreign worker-thread loop produced
the
got Future attached to a different loopwarning and a 2-3 minuteterminal-emission retry delay (attempt 1/4 ... retry), plus duplicate delivery.
Fix
wire_from_manifestbuilds the publisher while running on the kernel loop, so
asyncio.get_running_loop()resolves to the kernel loop and is closed over.coroutine back onto the kernel loop with
asyncio.run_coroutine_threadsafe(publish_awaitable, kernel_loop). Every Futurestays on its owning loop, so the publish completes immediately.
create_taskfast path.asyncio.run(...)throwaway-loop branch is removed entirely — no new loopis ever spawned.
Tests
tests/unit/runtime/auto_wiring/test_sync_event_publisher_loop_affinity.py:test_sync_publisher_from_worker_thread_runs_on_kernel_loop— runs the kernelloop in a background thread, builds the publisher on that loop, then calls the
sync publisher from a separate worker thread. Asserts it completes without
error, never calls
asyncio.run(patched +assert_not_called), and thepublish coroutine executes on the kernel loop (
publish_loop is kernel_loop)— not a worker-thread loop.
test_sync_publisher_from_kernel_loop_thread_schedules_task— the on-loop(async handler) path still delivers via
create_task.TDD: the worker-thread test fails on the pre-fix code (
asyncio.runcalled once),passes on the fix.
Verification
uv run pytest tests/ -m unit— 21480 passed, 22 skipped.uv run pytest tests/unit/runtime/auto_wiring/ tests/integration/runtime/ ...—green (DB-backed integration tests skip locally; CI has the DB).
uv run mypy src/omnibase_infra/runtime/ --strict— clean (276 files).ruff format+ruff checkclean; pre-commit hooks pass on changed files.DoD #3 (live dev-lane repro on
node_context_roi_runnershowing no attempt-1/4retry warning + no 2-3 min terminal delay) is post-merge: this wiring-only fix is
proven pre-merge by the deterministic loop-affinity unit test; the live dev-lane
redeploy/repro is performed after merge. No prod/stability/.201 mutation.
Evidence
Evidence-Source: OCC#3238
Evidence-Ticket: OMN-13658
Closes OMN-13658.