Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
50ad68b
Eliminate Rx Merge gate in queue-serialized operators
May 27, 2026
7bfe343
Fix UnsynchronizedMerge dropping terminal notifications
May 27, 2026
860adb5
Introduce DeliveryQueueMerge to combine SDQ routing and gate-free merge
May 27, 2026
f4e983a
Scope DeliveryQueueMerge to AutoRefresh's same-type case
May 27, 2026
0cab88e
Exercise subject-driven branches in DeadlockTortureTest
May 27, 2026
9d526a3
Use typed DeliveryQueue<T> for DeliveryQueueMerge
May 27, 2026
b362db9
Trim AllDangerous_Stacked pusher load to fit per-iteration timeout
May 27, 2026
07e202f
Raise DeadlockTortureTest per-iteration timeout to 60s
May 27, 2026
33df67d
Merge branch 'main' into fix/operator-merge-gate-deadlock
dwcullop May 28, 2026
1b3a03d
Apply UnsynchronizedMerge to SortAndPage/SortAndVirtualize and add fo…
May 28, 2026
d007b2b
Merge branch 'main' into fix/operator-merge-gate-deadlock
dwcullop May 29, 2026
0af69ea
Merge branch 'main' into fix/operator-merge-gate-deadlock
dwcullop May 30, 2026
07a3823
Refactor comments and improve observer handling
dwcullop May 30, 2026
30d5400
Extend Rx gate elimination to 6 additional cache operators
dwcullop May 30, 2026
2aaceb5
Merge branch 'main' into fix/operator-merge-gate-deadlock
dwcullop Jun 14, 2026
baa049e
Add canonical Rx contract rules reference
dwcullop Jun 14, 2026
103f851
Document citation format for Rx contract rule IDs
dwcullop Jun 14, 2026
9383308
Link to original Rx Design Guidelines PDF in rx-contract reference
dwcullop Jun 14, 2026
d9c6a4e
Dedupe section-structure blurb in rx-contract reference
dwcullop Jun 14, 2026
2fc623a
Rename "Quick reference by consumer type" to "Document structure"
dwcullop Jun 14, 2026
a243b18
Restore PDF section order in rx-contract reference
dwcullop Jun 14, 2026
4eb91eb
Rename rx-contract to rx-design-guide; complete PDF distillation
dwcullop Jun 14, 2026
a4df581
Tighten rx-design-guide.instructions.md from 798 to 301 lines
dwcullop Jun 14, 2026
1d77314
Drop CacheParentSubscription mention from rx-design-guide
dwcullop Jun 14, 2026
faf6b05
Remove DynamicData references from rx-design-guide
dwcullop Jun 14, 2026
31bdda9
Virtualise: collapse per-branch null filters into single post-merge f…
dwcullop Jun 14, 2026
78fc364
GroupOn/GroupOnImmutable: collapse per-branch Where into post-merge
dwcullop Jun 14, 2026
5efb0d0
Merge branch 'main' into fix/operator-merge-gate-deadlock
dwcullop Jun 15, 2026
146c6e9
Trim PR-narration from gate-elimination comments
Jun 15, 2026
638f566
Merge branch 'main' into fix/operator-merge-gate-deadlock
dwcullop Jun 19, 2026
e792263
Merge branch 'main' into fix/operator-merge-gate-deadlock
dwcullop Jul 2, 2026
77a9eff
Merge branch 'main' into fix/operator-merge-gate-deadlock
dwcullop Jul 9, 2026
af11b41
Merge branch 'main' into fix/operator-merge-gate-deadlock
dwcullop Jul 27, 2026
1abc2d6
Serialize TransformAsync's forced-transform chain on the shared queue
dwcullop Jul 27, 2026
713bcd4
Add concurrent-load coverage for TransformAsync's forced-transform chain
dwcullop Jul 27, 2026
f98f22d
Move the gate-holding operator audit into repo instructions
dwcullop Jul 29, 2026
1e6ef3f
Merge upstream/main into the merge-gate elimination work
dwcullop Jul 31, 2026
b54caf6
Merge branch 'main' into fix/operator-merge-gate-deadlock
dwcullop Aug 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ When optimizing, measure allocation rates and lock contention, not just wall-clo

DynamicData operators compose — the output of one is the input of the next. If any operator violates the Rx contract (e.g., concurrent `OnNext` calls, calls after `OnCompleted`), every downstream operator can corrupt its internal state. This is not a crash — it's silent data corruption that manifests as wrong results, missing items, or phantom entries. In a reactive UI, this means the user sees stale or incorrect data with no error message.

See `.github/instructions/rx.instructions.md` for comprehensive Rx contract rules, scheduler usage, disposable patterns, and a complete standard Rx operator reference.
See `.github/instructions/rx.instructions.md` for the DynamicData-flavored practical guide (hot/cold, schedulers, disposable helpers, custom operator patterns, common pitfalls), and `.github/instructions/rx-design-guide.instructions.md` for the canonical rule reference (a complete distillation of the Microsoft Rx Design Guidelines, with stable `§X.Y` IDs to cite in PRs, code reviews, and commit messages, e.g. "§6.6", "§5.2").

## Breaking Changes

Expand Down
300 changes: 300 additions & 0 deletions .github/instructions/rx-design-guide.instructions.md

Large diffs are not rendered by default.

52 changes: 52 additions & 0 deletions .github/instructions/rx-gate-holding-operators.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
applyTo: "**/*.cs"
---
# Rx Gate-Holding Operators

Several Rx combinators install a private `_gate` and hold it for the full duration of every downstream `OnNext`. This file records which ones matter to DynamicData, what we use instead, and the audit of every cache-side usage.

## The rule

**Never hold a lock while emitting downstream.**

Any operator that holds a lock while emitting adds an edge to the lock ordering. Two of them is all it takes for ABBA: A holds its gate and emits into B where it needs B's gate, while a second path has B holding its gate and emitting into A. Downstream of a DynamicData pipeline is consumer code whose locks we cannot audit, so the only reliable defence is not to hold one across the callout.

`SharedDeliveryQueue` is the mechanism: enqueue, release, drain outside the lock. Nothing of ours is held while a downstream observer runs.

## Helpers

Defined in `Internal/SynchronizeSafeExtensions.cs` and `Internal/DeliveryQueueMergeExtensions.cs`.

| Helper | Use when |
|---|---|
| `UnsynchronizedMerge<T>` | Drop-in for `Observable.Merge` where every input is already serialized. Preserves Merge's terminal semantics without installing a gate. |
| `UnsynchronizedCombineLatest<TFirst, TSecond, TResult>` | Two-input drop-in for `Observable.CombineLatest`, same precondition. |
| `DeliveryQueueMerge<T>` | Same-type merge that owns its own `DeliveryQueue<T>`, so the call site never mentions queue plumbing. |

**Precondition for the unsynchronized variants:** every input must already be serialized, which in this library means routing each one through the same `SharedDeliveryQueue` via `SynchronizeSafe(queue)` before the merge.

The precondition is a property of how a pipeline is wired, not of the operator, and it breaks silently. `TransformAsync`'s `forced` path applied `SynchronizeSafe(queue)` and then ran an async `Select(...).Concat()` after it, so results came back off-gate; stock `Observable.Merge` serialized them anyway and hid the defect. Route every input through the queue before merging, and put the `SynchronizeSafe` call last in the chain so nothing escapes it.

## Audit

Every cache-side usage of an Rx combinator that holds a gate during downstream delivery.

| Rx operator | DynamicData cache usage | Verdict |
|---|---|---|
| `Merge` | `AutoRefresh`, `Page`, `Virtualise`, `Sort`, `SortAndPage`, `SortAndVirtualize`, `GroupOnImmutable`, `QueryWhenChanged`, `TransformWithForcedTransform`, `GroupOn`, `GroupOnDynamic`, `TransformAsync`, `TransformMany`, `Switch` | Replaced with `UnsynchronizedMerge` / `DeliveryQueueMerge` |
| `Merge` | `FullJoin` / `InnerJoin` / `LeftJoin` / `RightJoin` | Left alone: inputs come from independently materialized caches that share no queue, so Merge's gate IS the serializer |
| `Merge` | `AsyncDisposeMany` disposal fan-in, `ToObservableOptional` initial-value branch, `TransformAsync.Merge(maxConcurrency)` | Left alone: not in queue-drain context |
| `Merge(int)` | `ObservablePropertyFactory`, property-chain plumbing | Left alone: not in queue-drain context |
| `CombineLatest` | `TreeBuilder` | Replaced with `UnsynchronizedCombineLatest` |
| `CombineLatest` | `TrueFor`, `Binding/NotifyPropertyChangedEx` | Left alone: not in queue-drain context |
| `Switch` | `Cache/Internal/Switch.cs` and the `IObservableCache` overload that delegates to it | Refactored to an inline `SerialDisposable` |
| `Switch` | `ObservableCache` subscription plumbing, `AggregationEx` | Left alone: one-shot or aggregation, not queue-drain |
| `Synchronize` | Previous `Synchronize(lock)` usages | Migrated to `SynchronizeSafe` |
| `Synchronize` | `EditDiffChangeSetOptional` defensive serialize | Removed: `§6.8` / `§5.8` anti-pattern, the source already guarantees serialization by `§4.2` |
| `Buffer` (time-based) | `AutoRefresh` change buffering, consumer-facing `Buffer` overloads | Left alone: single input, the gate protects internal buffer state |
| `Throttle` | `WhenChanged` / `AutoRefresh` throttle, `GroupOnProperty` regrouper throttle | Left alone: single input, the gate protects internal throttle state |
| `Zip`, `WithLatestFrom`, `Sample`, `Window`, `Join`, `GroupJoin`, observable-`SelectMany` | Not used in cache pipelines | n/a. All `Zip` and `SelectMany` matches are LINQ-over-`IEnumerable`, not the Rx operators |

## Maintenance

When an operator starts or stops using one of these combinators, update the table. When a new gate-holding Rx combinator comes into use, add a row for it and state the verdict.
Loading
Loading