Skip to content

Fix: WhenAnyValue subscribes to PropertyChanged before reading the initial value#4381

Merged
glennawatson merged 1 commit into
reactiveui:mainfrom
dwcullop:bugfix/whenanyvalue_races
Jun 14, 2026
Merged

Fix: WhenAnyValue subscribes to PropertyChanged before reading the initial value#4381
glennawatson merged 1 commit into
reactiveui:mainfrom
dwcullop:bugfix/whenanyvalue_races

Conversation

@dwcullop

@dwcullop dwcullop commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Fixes #4380.

ExpressionChainSink.Level.SetParent reads the property and emits the kicker value before subscribing to PropertyChanged. Any mutation that fires PropertyChanged between the two is raised against an empty handler list and silently lost, leaving downstream observers pinned to the pre-write value until the next change. The race fires non-deterministically when the property can be written from a thread other than the one calling Subscribe (typical MVVM pattern: a view model aggregating data from background services).

This swaps the order to subscribe-then-read. A racing handler emit that arrives queued behind the chain sink's gate then delivers the same post-mutation value the kicker just emitted, so a one-shot _suppressNextIfSameAsLast guard collapses the single racing duplicate. The guard fires independently of the user-facing isDistinct flag, so existing distinct semantics are unchanged.

Scope

The same defect exists in the released 23.2.28 code (via NestedObservedChanges.StartWith(kicker) then Subscribe(NotifyForProperty)), but that code path is being replaced by the custom-reactive-sinks rework on main. This PR fixes the defect in the rework's new location so the next release ships with the fix. Backporting to a 23.2.x hotfix branch (if one is maintained) would be a separate change.

Tests

In WhenAnyValueSubscribeRaceTests:

  • WhenAnyValue_MutationBetweenInitialEmitAndHandlerAttach_IsLost — deterministic single-thread repro on ReactiveObject.
  • ..._IsLost_PlainInpc — same shape with a hand-rolled INotifyPropertyChanged source, to rule out ReactiveObject specifics.
  • WhenAnyValue_ConcurrentMutationDuringSubscribe_NeverLosesFinalValue_Stress — 2 000-iteration multi-threaded stress (~7.5% failure rate without the fix; 0% with it).

All three fail without the fix and pass with it. Full ReactiveUI.Tests suite (1 899 pre-existing + 3 new) passes locally on net8.0.

…itial value

ExpressionChainSink.Level.SetParent read the property and emitted the kicker value
before subscribing to PropertyChanged. Any mutation that fired PropertyChanged
between the two was raised against an empty handler list and silently lost, leaving
downstream observers pinned to the pre-write value until the next change.

Subscribe first, then read. A racing handler emit that arrives queued behind the
chain sink's gate then delivers the same post-mutation value the kicker just emitted,
so add a one-shot "suppress next emission if equal to the kicker" guard that fires
independently of the user-facing isDistinct flag.

Repro tests cover deterministic single-thread and multi-threaded stress paths.

Fixes reactiveui#4380
@dwcullop dwcullop changed the title fix: WhenAnyValue subscribes to PropertyChanged before reading the initial value Fix: WhenAnyValue subscribes to PropertyChanged before reading the initial value Jun 13, 2026
@glennawatson

Copy link
Copy Markdown
Contributor

Thanks for the PR. I got a major refactor incoming (mostly all internal) where we swap over internally to using ReactiveUI.Primitives, basically still keeps IObservable but swaps out a lot of the internals.

So would like to get this fix in before that refactor.

We also have a source generated version of the WhenAny pipeline in the project in ReactiveUI.Binding.SourceGenerators mind having a peak and seeing if the issue is also there @dwcullop

@glennawatson glennawatson merged commit 69586a4 into reactiveui:main Jun 14, 2026
7 of 9 checks passed
@glennawatson

Copy link
Copy Markdown
Contributor

The fails are false negatives. Going to get your fixes in as part of the refactor.

@dwcullop

Copy link
Copy Markdown
Contributor Author

Happy to help. 😄 Let me know if you want a fix for the 23.2.28 version. It's very similar.

I'll take a look at the Binding SourceGen repo as well and see if I find any issues.

@dwcullop dwcullop deleted the bugfix/whenanyvalue_races branch June 19, 2026 21:21
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.

Race condition: WhenAnyValue loses PropertyChanged events fired between its initial read and its handler attach

2 participants