Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 src/MeshWeaver.Documentation/Data/Architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ The catalog below lists every page; these are the load-bearing reads per theme.

| Theme | Start here | Go deeper |
|---|---|---|
| **Reactive core** | [Asynchronous Calls](AsynchronousCalls) — the no-`await` rulebook | [Actor Model](ActorModel) · [Controlled I/O Pooling](ControlledIoPooling) · [Initialization Gates](InitializationGates) · [Aggregating Providers](AggregatingProviders) · [Hub Disposal Model](HubDisposalModel) |
| **Reactive core** | [Asynchronous Calls](AsynchronousCalls) — the no-`await` rulebook | [Actor Model](ActorModel) · [Controlled I/O Pooling](ControlledIoPooling) · [Subscription Ownership](SubscriptionOwnership) — a pending timer is a GC root · [Initialization Gates](InitializationGates) · [Aggregating Providers](AggregatingProviders) · [Hub Disposal Model](HubDisposalModel) |
| **Reading & writing nodes** | [CQRS — Queries vs. Content Access](CqrsAndContentAccess) | [MeshNode Stream Cache](MeshNodeStreamCache) · [Request via Stream Update](RequestViaStreamUpdate) · [Data Access Patterns](DataAccessPatterns) · [Workspace References](WorkspaceReferences) · [Content Chunk Navigation](ContentChunkNavigation) |
| **Storage & partitions** | [Postgres Schema Architecture](PostgresSchemaArchitecture) | [Partition Storage Routing](PartitionStorageRouting) · [Partition Storage Hubs](PartitionStorageHubs) · [Partitioned Persistence](PartitionedPersistence) · [Static Repo Import](StaticRepoImport) · [Vector Search](VectorSearch) |
| **Security** | [Access Control](AccessControl) | [Granting Access](GrantingAccess) · [AccessContext Propagation](AccessContextPropagation) · [Owner Injection](OwnerInjection) · [Permission API](PermissionApi) · [Invitation-Only Onboarding](InvitationOnlyOnboarding) |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,23 @@ The legacy `workspace.UpdateMeshNode(update)` extension is `[Obsolete]` and poin
- `meshService.MoveNode(...)` / `meshService.CopyNode(...)` — cold; subscribe.
- `remoteStream.Update(current => updated, ex => …)` — the `ex` callback fires on the stream's hub; the returned `void` IS the subscription.

### …but a subscription that stays PENDING needs an owner

Discarding the `IDisposable` from `stream.Update(...).Subscribe(...)` is fine — those observables complete promptly, and the handle of a *completed* sequence roots nothing (Rx detaches its observer on completion). A **timer** subscription is the opposite case: `Observable.Timer` / `Observable.Interval` / `Task.Delay` park an entry on the process-wide `TimerQueue`, which is a **strong GC root**, so while the timer is pending it holds the tick closure and everything that closure captured — past its owner's disposal.

```csharp
// ❌ The timer's IDisposable goes nowhere — nothing can cancel a flush still pending
// when the hub tears down, so the closure roots the hub for the whole delay.
Observable.Timer(TimeSpan.FromMilliseconds(100)).Subscribe(_ => Publish());

// ✅ The pending timer is the HUB's: teardown disposes the composite, cancelling it.
pendingFlush.Disposable = Observable.Timer(TimeSpan.FromMilliseconds(100))
.Subscribe(_ => Publish()); // pendingFlush: a SerialDisposable passed to
// hub.RegisterForDisposal(...) at construction
```

Holding it in a field is **not** the same as owning it — the field has to be disposed by the owner's teardown, on every path. Full treatment, with both leak shapes, the measured truth table of which primitives actually root, and why there is deliberately no analyzer: [Subscription Ownership](/Doc/Architecture/SubscriptionOwnership).

---

## 🚨 `MeshNode.Content` is always typed at the `GetMeshNodeStream` boundary
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,17 @@ GC-handle** root (a true leak that accumulates) from a **stack** root — a disp
continuation the snapshot froze mid-flight, which clears on resume. Assert on the
former; tolerate the latter.

🚨 **A PASS HERE PINS NOTHING.** This probe *samples* (a root live only for a bounded
window — 1 s, 100 ms — is caught only if the forced GC lands inside it), it *cannot
attribute* (it names whatever chain it happened to walk), and it *SKIPs on macOS*
(#674). It is for **discovery** — naming a root nobody knew about. Once you have found
one, pin the fix with a targeted, timing-free **ownership** test next to the code that
owns the subscription, and prove it with a **negative control** (revert only the
ownership line; watch that test fail). See
[Subscription Ownership](/Doc/Architecture/SubscriptionOwnership) for the convention
these leaks keep violating, and for the measured table of which primitives actually
root.

### Common disposal pins and their fixes

| Pin (ClrMD chain) | Cause | Fix |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,12 @@ change-feed resubscribe latch **stay armed** and the subscriber rehydrates after
terminal callback, exactly as the phases above do.
- **Tempted to add a `TaskCompletionSource` to signal "done"?** That is the smell
the `ReplaySubject` replaced. Use a subject and a CAS-guarded `Signal…` helper.
- **Armed a timer, interval or debounce?** Its subscription must reach this disposal
chain — `hub.RegisterForDisposal(serialDisposable)`, then arm into the
`SerialDisposable`. A pending `TimerQueue` entry is a strong GC root, so an
unregistered one keeps the hub alive past its own teardown, and holding it in a
field is not the same as owning it. See
[Subscription Ownership](/Doc/Architecture/SubscriptionOwnership).

Canonical implementation: `MessageHub.HandleShutdownCore` /
`MessageHub.Dispose` / `HostedHubsCollection.DisposeHubsReactive` in
Expand Down
Loading
Loading