Clear EventLoopThread.loop before closing it - #739
Merged
Conversation
`_thread_main`'s `finally` closed the loop and only then set `self.loop = None`, so between those two statements `self.loop` named an already-closed loop — the window #727 had to defend against with an `is_closed()` guard plus a suppressed `RuntimeError` in `force_stop`. Publishing `None` first makes the invariant "`self.loop` is either `None` or an open loop" hold for every reader, and it also means the exiting thread never writes `self.loop` again after that point — previously the trailing `self.loop = None` could clobber the loop a concurrent `start()` had just installed. #727's `suppress(RuntimeError)` in `force_stop` stays: a caller can still snapshot the loop just before `close()` runs.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## dev #739 +/- ##
=======================================
Coverage 99.55% 99.55%
=======================================
Files 64 64
Lines 4263 4263
=======================================
Hits 4244 4244
Misses 19 19 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
puddly
approved these changes
Jul 28, 2026
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.
Follow-up to the review thread on #727 (#727 (comment)).
#727 fixed the
RuntimeError: Event loop is closedflake on the read side, inforce_stop. The write side that creates the window is still in_thread_main:Between those two statements
self.loopnames an already-closed loop. This swaps them, so the loop reference is published asNonebefore the loop is closed:Two things follow:
self.loopis eitherNoneor an open loop, for every reader — not just the oneforce_stopguards.run_coroutine_threadsafe()(bellows/thread.py:17-20) passesself.loopstraight toasyncio.run_coroutine_threadsafe, which raises the sameRuntimeErroron a closed loop, andstart()s restart guard reads it too. ANoneloop is already handled correctly bystart(), which falls through to creating a new one.self.loopafter it clears it. Previously the trailingself.loop = Noneran afterclose(), so astart()racing the shutdown could have its freshly installed loop clobbered back toNoneby the thread that was on its way out. With the swap there is no write after the handoff.#727s
contextlib.suppress(RuntimeError)inforce_stopis still needed and stays — a caller can snapshot the loop just before the worker reachesclose(), and no ordering here can prevent that.Test
test_thread_clears_loop_before_closingwraps the worker loopsclose()and records whatself.loopis at the moment close is called, asserting it is alreadyNone. It fails ondev(assert <_UnixSelectorEventLoop ...> is None) and passes with the change, so it pins the ordering rather than just the end state.Verification
pytest tests/→ 441 passed (Python 3.13.11)pytest tests/test_thread.py::test_thread_clears_loop_before_closingwithbellows/thread.pyreverted todev→ 1 failed, confirming the test is not vacuousautoflake,black,flake8,isort,codespell,ruffpass.pyupgradeandmypyfail identically on untouched files (ast.Strremoved in 3.14 / missingtyped-ast), so those are local hook-env breakage, not this change — CI is the real check.