Skip to content

Fix CombinedUsage lock-order inversion with the SignalTree lock - #25178

Merged
mshabarov merged 3 commits into
mainfrom
fix/combinedusage-lock-order-inversion
Aug 12, 2026
Merged

Fix CombinedUsage lock-order inversion with the SignalTree lock#25178
mshabarov merged 3 commits into
mainfrom
fix/combinedusage-lock-order-inversion

Conversation

@totally-not-ai

@totally-not-ai totally-not-ai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Problem

UsageTracker.CombinedUsage held its per-registration monitor while calling into signal trees, which can deadlock (ABBA):

  • When a tracked read spans two or more usages on the same tree (e.g. a cached/computed signal reading two nodes of one shared signal), registering a listener held the monitor while onNextChange acquired the dependency's SignalTree lock.
  • A concurrent thread committing to that tree holds the tree lock and, from notifyObservers, invokes the first registered listener → onChange, which then needs the monitor.

The same inversion applied to onChange invoking the downstream listener while holding the monitor, since that listener may itself read signals or register usages.

This is a second, independent inversion from the CachedSignal one fixed for #25166: it survives that fix and triggers on the common multi-dependency case.

Change

  • Register the per-usage listeners and invoke the downstream listener without holding the monitor, matching what close() already does for removal. The monitor now only guards the small closed/cleanups state.
  • If the usage was closed while a registration was in flight, the cleanup is removed outside the monitor (removal also takes the tree lock).
  • onChange no longer serializes concurrent listener invocations, which the downstream listeners (Effect, CachedSignal, nested CombinedUsage) already tolerate.

Test

Added a deterministic regression test in UsageTrackerTest. Rather than relying on winning a narrow timing race across many iterations, a control usage placed between two real usages on the same tree starts a committer mid-registration and waits (via thread state) until that committer holds the tree lock and is blocked entering the monitor, then lets registration proceed to the second usage, which needs that same tree lock.

Without the fix this deadlocks on a single run (detected via a join timeout / ThreadMXBean); with the fix it passes.

Fixes #25166

#25166)

UsageTracker.CombinedUsage held its per-registration monitor while calling into
signal trees, which can deadlock. When a tracked read spans two or more usages
on the same tree (e.g. a cached/computed signal reading two nodes of one shared
signal), registering a listener held the monitor while onNextChange acquired the
dependency's SignalTree lock. A concurrent thread committing to that tree holds
the tree lock and, from notifyObservers, invokes the first registered listener
-> onChange, which then needs the monitor. The opposite acquire orders form an
ABBA deadlock. The same applied to onChange invoking the downstream listener
(which can read signals / register usages) while holding the monitor.

This is a second, independent inversion from the CachedSignal one fixed for
#25166: it survives that fix and triggers on the common multi-dependency case.

Register the per-usage listeners and invoke the downstream listener without
holding the monitor, matching what close() already does for removal. The monitor
now only guards the small closed/cleanups state. onChange no longer serializes
concurrent listener invocations, which the downstream listeners (Effect,
CachedSignal, nested CombinedUsage) already tolerate.

Add a deterministic regression test that races registration against commits; it
deadlocks (ThreadMXBean-detected) on the old code and passes on the fix.
@totally-not-ai totally-not-ai Bot changed the title fix: avoid CombinedUsage lock-order inversion with the SignalTree lock (#25166) fix: avoid CombinedUsage lock-order inversion with the SignalTree lock Aug 11, 2026
@Artur-
Artur- requested review from mshabarov and tltv August 11, 2026 13:14
@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Test Results

 1 450 files  ±0   1 451 suites  ±0   1h 27m 37s ⏱️ + 1m 49s
10 395 tests +1  10 328 ✅ +1  67 💤 ±0  0 ❌ ±0 
10 831 runs  +1  10 763 ✅ +1  68 💤 ±0  0 ❌ ±0 

Results for commit 3c15a14. ± Comparison against base commit a1e3b17.

♻️ This comment has been updated with latest results.

Comment thread flow-server/src/test/java/com/vaadin/flow/signals/impl/UsageTrackerTest.java Outdated
The previous regression test relied on winning a narrow timing race across many
iterations, so it did not reliably fail without the fix on all machines. Replace
it with a deterministic reproduction: a control usage placed between two real
usages on the same tree starts a committer mid-registration and waits (via
thread state) until that committer holds the tree lock and is blocked entering
the monitor, then lets registration proceed to the second usage, which needs
that same tree lock. Without the fix this deadlocks on a single run (detected
via a join timeout / ThreadMXBean); with the fix it passes.
@totally-not-ai totally-not-ai Bot changed the title fix: avoid CombinedUsage lock-order inversion with the SignalTree lock Fix CombinedUsage lock-order inversion with the SignalTree lock Aug 11, 2026
@sonarqubecloud

Copy link
Copy Markdown

@mshabarov
mshabarov added this pull request to the merge queue Aug 12, 2026
Merged via the queue into main with commit 1936de4 Aug 12, 2026
35 checks passed
@mshabarov
mshabarov deleted the fix/combinedusage-lock-order-inversion branch August 12, 2026 08:42
vaadin-bot added a commit that referenced this pull request Aug 12, 2026
…) (CP: 25.1) (#25190)

This PR cherry-picks changes from the original PR #25178 to branch 25.1.
---
#### Original PR description
> ## Problem
> 
> `UsageTracker.CombinedUsage` held its per-registration monitor while
calling into signal trees, which can deadlock (ABBA):
> 
> - When a tracked read spans two or more usages on the same tree (e.g.
a cached/computed signal reading two nodes of one shared signal),
registering a listener held the monitor while `onNextChange` acquired
the dependency's `SignalTree` lock.
> - A concurrent thread committing to that tree holds the tree lock and,
from `notifyObservers`, invokes the first registered listener →
`onChange`, which then needs the monitor.
> 
> The same inversion applied to `onChange` invoking the downstream
listener while holding the monitor, since that listener may itself read
signals or register usages.
> 
> This is a second, independent inversion from the `CachedSignal` one
fixed for #25166: it survives that fix and triggers on the common
multi-dependency case.
> 
> ## Change
> 
> - Register the per-usage listeners and invoke the downstream listener
**without** holding the monitor, matching what `close()` already does
for removal. The monitor now only guards the small `closed`/`cleanups`
state.
> - If the usage was closed while a registration was in flight, the
cleanup is removed outside the monitor (removal also takes the tree
lock).
> - `onChange` no longer serializes concurrent listener invocations,
which the downstream listeners (`Effect`, `CachedSignal`, nested
`CombinedUsage`) already tolerate.
> 
> ## Test
> 
> Added a deterministic regression test in `UsageTrackerTest`. Rather
than relying on winning a narrow timing race across many iterations, a
control usage placed between two real usages on the same tree starts a
committer mid-registration and waits (via thread state) until that
committer holds the tree lock and is blocked entering the monitor, then
lets registration proceed to the second usage, which needs that same
tree lock.
> 
> Without the fix this deadlocks on a single run (detected via a join
timeout / `ThreadMXBean`); with the fix it passes.
> 
> Fixes #25166

Co-authored-by: totally-not-ai[bot] <290682512+totally-not-ai[bot]@users.noreply.github.com>
Co-authored-by: Tomi Virtanen <tltv@vaadin.com>
vaadin-bot added a commit that referenced this pull request Aug 12, 2026
…) (CP: 25.2) (#25189)

This PR cherry-picks changes from the original PR #25178 to branch 25.2.
---
#### Original PR description
> ## Problem
> 
> `UsageTracker.CombinedUsage` held its per-registration monitor while
calling into signal trees, which can deadlock (ABBA):
> 
> - When a tracked read spans two or more usages on the same tree (e.g.
a cached/computed signal reading two nodes of one shared signal),
registering a listener held the monitor while `onNextChange` acquired
the dependency's `SignalTree` lock.
> - A concurrent thread committing to that tree holds the tree lock and,
from `notifyObservers`, invokes the first registered listener →
`onChange`, which then needs the monitor.
> 
> The same inversion applied to `onChange` invoking the downstream
listener while holding the monitor, since that listener may itself read
signals or register usages.
> 
> This is a second, independent inversion from the `CachedSignal` one
fixed for #25166: it survives that fix and triggers on the common
multi-dependency case.
> 
> ## Change
> 
> - Register the per-usage listeners and invoke the downstream listener
**without** holding the monitor, matching what `close()` already does
for removal. The monitor now only guards the small `closed`/`cleanups`
state.
> - If the usage was closed while a registration was in flight, the
cleanup is removed outside the monitor (removal also takes the tree
lock).
> - `onChange` no longer serializes concurrent listener invocations,
which the downstream listeners (`Effect`, `CachedSignal`, nested
`CombinedUsage`) already tolerate.
> 
> ## Test
> 
> Added a deterministic regression test in `UsageTrackerTest`. Rather
than relying on winning a narrow timing race across many iterations, a
control usage placed between two real usages on the same tree starts a
committer mid-registration and waits (via thread state) until that
committer holds the tree lock and is blocked entering the monitor, then
lets registration proceed to the second usage, which needs that same
tree lock.
> 
> Without the fix this deadlocks on a single run (detected via a join
timeout / `ThreadMXBean`); with the fix it passes.
> 
> Fixes #25166

Co-authored-by: totally-not-ai[bot] <290682512+totally-not-ai[bot]@users.noreply.github.com>
Co-authored-by: Tomi Virtanen <tltv@vaadin.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Deadlock: lock-order inversion between Signal.cached monitor and SignalTree lock

3 participants