Skip to content

fix: a render waiting on the lock must not interleave with close()#53

Merged
maartenbreddels merged 1 commit into
masterfrom
fix/render-close-race
Jul 7, 2026
Merged

fix: a render waiting on the lock must not interleave with close()#53
maartenbreddels merged 1 commit into
masterfrom
fix/render-close-race

Conversation

@maartenbreddels

Copy link
Copy Markdown
Contributor

Seen in production (a large solara application, hours after deploying reacton 1.10.1 + solara 1.60.1): AssertionError at Element.__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 — including self.context = Noneafter the thread_lock block ends. Meanwhile render() waits for the lock but never re-checks _closing after acquiring it. So:

render thread: update → render() → lock held by close → waits
disconnect:    close() → locked phase (remove elements, _closing=True) → lock RELEASED
render thread: acquires lock → renders into the torn-down tree
close tail:    (unlocked) self.context = None
render thread: Element.__exit__ → assert rc.context is not None → AssertionError

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

  1. render(): after acquiring the lock, no-op (return the container) when self._closing or self.context is None — rendering a torn-down tree is meaningless.
  2. Belt-and-braces: the whole close() teardown tail moves inside the lock, so it cannot interleave with anything. (Closing widgets under the lock is precedented — _remove_element already did that.) The exceptions re-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.
  • Full suite: 178 passed in both stock and REACTON_FAST=1 renderers.

🤖 Generated with Claude Code

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>
@maartenbreddels maartenbreddels merged commit e16ab2c into master Jul 7, 2026
22 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