fix: a render waiting on the lock must not interleave with close()#53
Merged
Conversation
The close() teardown added in #52 (breaking reference cycles so closed render contexts free by refcounting) runs its tail - including self.context = None - after the thread_lock block ends. A render that was blocked on that lock (a disconnect can close the kernel while an update waits to render; solara closes kernels promptly at disconnect since 1.60) then proceeds into the torn-down tree and races the tail: seen in production as "assert rc.context is not None" in Element.__exit__, and reproducible as AttributeError on a None context. Two guards: render() becomes a no-op when the context is closing or closed (rendering a torn-down tree is meaningless), and the close() teardown tail moves inside the lock so it cannot interleave with anything. The race test fails on master and passes with the fix; full suite green in both renderers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.
Seen in production (a large solara application, hours after deploying reacton 1.10.1 + solara 1.60.1):
AssertionErroratElement.__exit__(assert rc.context is not None), raised mid-render of a component whose__enter__had just passed the same assert.The race
The
close()teardown added in #52 runs its tail — includingself.context = None— after thethread_lockblock ends. Meanwhilerender()waits for the lock but never re-checks_closingafter acquiring it. So:Two changes made this go from theoretical to observed: #52 added the context-nulling tail (1.10.1), and solara closes kernels promptly at disconnect since 1.60 (widgetti/solara#1176) — so close-during-queued-render now actually happens.
Fix
render(): after acquiring the lock, no-op (return the container) whenself._closing or self.context is None— rendering a torn-down tree is meaningless.close()teardown tail moves inside the lock, so it cannot interleave with anything. (Closing widgets under the lock is precedented —_remove_elementalready did that.) Theexceptionsre-raise stays outside the lock.Tests
test_render_after_close_is_noop— the distilled production scenario.test_close_render_race— 20 iterations of close-vs-render from two threads behind a barrier; fails on master (AttributeError: 'NoneType' object has no attribute 'exception_handler'— the same race, different symptom), passes with the fix.REACTON_FAST=1renderers.🤖 Generated with Claude Code