docs: deferred-table closure, stale-comment sweep, as-built execution record
This commit is contained in:
@@ -136,7 +136,11 @@ cost stays bounded: a pure-event pass — the event hot path — still pays
|
||||
exactly one flush however many frames drain together, a run of control
|
||||
frames still pays one for the whole run (never one per heartbeat, the
|
||||
syscall-per-frame cost the coalescing removed), and only a pass that actually
|
||||
mixes both classes pays a second.
|
||||
mixes both classes pays a second. Mixed passes are not exotic: command replies
|
||||
travel at Control priority alongside heartbeats and faults, so a session under
|
||||
sustained command traffic concurrent with event streaming can hit them
|
||||
routinely. The cost stays bounded either way — one extra flush per class
|
||||
transition present in the pass, not per frame.
|
||||
|
||||
One consequence of the boundary flush is worth stating: a control frame whose
|
||||
run has already been flushed and completed is out of the drain's
|
||||
|
||||
@@ -308,3 +308,67 @@ Push the branch to origin, then on windev (`ssh windev`, clone `C:\build\mxacces
|
||||
| Structural alarm-truncation degraded-status signal | Contract-level design (proto change candidate) — separate effort. |
|
||||
| SEC-25 per-session dashboard event ACL | Security roadmap item; Task 6 deliberately preserves the current posture. |
|
||||
| `MxAccessWriteCompletionCache` clone | Different lifecycle than the value cache; consciously kept (Task 12.5). |
|
||||
|
||||
---
|
||||
|
||||
## As-built notes (execution record)
|
||||
|
||||
Where the delivered work differs from the task text above, or where the route to it
|
||||
is worth keeping, this is the record.
|
||||
|
||||
**Task 3 — `ReadEventsAsync` retained.** The method was not removed after
|
||||
`MapWorkerEventsAsync` inlined the read-then-map chain: a second caller reaches it
|
||||
through `ISessionManager.ReadEventsAsync`. That interface member itself has zero
|
||||
production call sites — only test fakes implement and exercise it. Deleting it is a
|
||||
mechanical but wide change (~15 test-fake touches), so it is recorded as a follow-up
|
||||
rather than done here.
|
||||
|
||||
**Task 5 — dashboard event feed, two review rounds.** Review caught two races that
|
||||
the first cut did not have. First, subscription lifetime: subscriptions are now
|
||||
generation-tagged, a generation ends at the time the fault is *observed* (not when it
|
||||
is raised), `Reset` is scoped to the dying generation so it cannot cancel its
|
||||
successor, and a backstop restart covers the case where no subscriber is left to
|
||||
drive recovery. Second, `UnsubscribeAsync` needed a generation-scoped idle gate so a
|
||||
teardown for an old generation cannot tear down the new one. Both fixes are pinned by
|
||||
tests verified against mutations of the fixed code.
|
||||
|
||||
**Task 6 — subscribe API placement.** The subscribe surface lives on
|
||||
`IDashboardSessionEventSubscriber`, with DI forwarding to a single instance so every
|
||||
consumer shares one feed. Batches that arrive for a session the renderer has already
|
||||
moved off are dropped by a subscription identity check inside the renderer dispatch,
|
||||
which is what makes a stale batch harmless rather than a cross-session leak.
|
||||
|
||||
**Task 10 — the delivered property is the delivery point, not awaited latency.** The
|
||||
spec asked for control-frame completion to be observable before the pass's event
|
||||
writes. That is unachievable in the enqueue-then-contend shape: a caller that loses
|
||||
the write-lock race does not run again until the winning drainer releases the lock,
|
||||
so its `await` cannot return early no matter when its frame completes. What shipped
|
||||
is the honest half: control frames are written *and flushed* at the class-transition
|
||||
boundary, so the priority class governs the frame's delivery point rather than only
|
||||
its byte order. Getting the awaited-latency win too requires unparking the lock-race
|
||||
loser from the winner's pass — a change to the write-lock shape, recorded as a
|
||||
follow-up. One extra `FlushFileBuffers` per mixed pass is the accepted cost.
|
||||
|
||||
**Task 11 — teardown ordering and unconditional fault observation.** Teardown disposes
|
||||
the session-owned transport first, then observes the read that dispose abandoned.
|
||||
Fault observation is unconditional — a `ContinueWith(..., TaskContinuationOptions.OnlyOnFaulted)`
|
||||
continuation, so the budget that bounds the wait is diagnostics-only and can never be
|
||||
the reason a fault goes unobserved. The same continuation covers heartbeat and drain
|
||||
overrun. `DisposeTransportStream` is exception-total: no dispose path can throw out of
|
||||
teardown.
|
||||
|
||||
**Task 12 — three clones removed, plan rationale corrected in-code.** All three
|
||||
`OnDataChange` value-cache clones are gone. The plan's stated reason for keeping the
|
||||
`MxAccessWriteCompletionCache` clone ("different lifecycle than the value cache") is
|
||||
wrong and was corrected where the code documents it: the clone is kept on
|
||||
*provenance* grounds — the cached payload comes from a caller-supplied object the
|
||||
worker does not own — not on lifecycle grounds.
|
||||
|
||||
**Task 13 — windev verification.** Solution build 0 warnings / 0 errors after
|
||||
clearing stale `Contracts` `obj` artifacts (an infrastructure problem on the box, not
|
||||
a regression from this branch). Worker x86: 509/509 (+8 new). Gateway: 1059/1059,
|
||||
including `SecretsStorePathGuardTests` — the first fully green Windows gateway run,
|
||||
that suite having been red before this branch. One load flake
|
||||
(`InvokeAsync_WhenWorkerHandshakingThenReadyWithinTimeout_Succeeds`) passed in
|
||||
isolation and on re-run, consistent with the load-sensitivity caveat documented in
|
||||
`docs/GatewayTesting.md`.
|
||||
|
||||
@@ -619,6 +619,11 @@ Commit anything found: `docs: remediation plan doc sweep`
|
||||
| Event-path triple async-iterator flattening | LOW-rated; touches the most invariant-dense code in the gateway for two `MoveNextAsync` hops per event. Reconsider after Tasks 3/4 land and if profiling still shows it. |
|
||||
| `SessionEventDistributor._subscribers` `ConcurrentDictionary` → plain `Dictionary` (Task 25 / Task 3 review) | Every mutation is already inside `_lifecycleLock`, so the concurrent type buys nothing. Behavior-neutral refactor with no measurable win, proposed after the windev gate was already green — not worth re-running the verification matrix for. Comment cleanups from the same review landed; this swap did not. |
|
||||
|
||||
All six rows above were resolved on 2026-08-15 by the follow-up plan
|
||||
[`docs/plans/2026-08-15-deferred-remediation.md`](2026-08-15-deferred-remediation.md)
|
||||
(branch `perf/deferred-remediation`), which also fixed the pre-existing Windows
|
||||
`SecretsStorePathGuardTests` failure.
|
||||
|
||||
## Execution notes for the orchestrator
|
||||
|
||||
- Branch: `git checkout -b perf/review-remediation` before Task 1.
|
||||
|
||||
@@ -56,9 +56,9 @@ public delegate void SubscriberOverflowHandler(bool isOnlySubscriber, bool isInt
|
||||
/// <see cref="Func{T, TResult}"/> producing an
|
||||
/// <see cref="IAsyncEnumerable{T}"/> of already-mapped public
|
||||
/// <see cref="MxEvent"/>s, given a <see cref="CancellationToken"/>. This is the
|
||||
/// cleanest seam: it can pass
|
||||
/// <c>ct => session.ReadEventsAsync(ct).Select(mapper.MapEvent)</c> (or a
|
||||
/// channel reader's <c>ReadAllAsync</c>), while unit tests pass a plain
|
||||
/// cleanest seam: production passes <c>GatewaySession.MapWorkerEventsAsync</c>,
|
||||
/// which reads the worker event channel and maps each frame in one iterator
|
||||
/// (inlining what used to be a read-then-<c>Select</c> chain), while unit tests pass a plain
|
||||
/// channel reader's <c>ReadAllAsync</c> with no real session. The pump owns the
|
||||
/// single consumption of this enumerable; fan-out happens on the public
|
||||
/// <see cref="MxEvent"/> after mapping, mirroring today's
|
||||
@@ -751,9 +751,9 @@ public sealed class SessionEventDistributor : IAsyncDisposable
|
||||
}
|
||||
|
||||
// Disconnect ONLY this subscriber: it is already out of the fan-out set (removed above),
|
||||
// so complete its channel with the overflow fault. Its gRPC reader's MoveNextAsync then
|
||||
// throws the SessionManagerException, which EventStreamService surfaces to the client
|
||||
// exactly as the pre-epic per-RPC overflow did. The pump and every other subscriber are
|
||||
// so complete its channel with the overflow fault. EventStreamService consumes the channel
|
||||
// directly, so its next WaitToReadAsync throws the SessionManagerException, which it surfaces
|
||||
// to the client exactly as the pre-epic per-RPC overflow did. The pump and every other subscriber are
|
||||
// untouched. This runs even when the handler above threw — the subscriber must never be
|
||||
// left attached with an un-completed channel.
|
||||
subscriber.Channel.Writer.TryComplete(new SessionManagerException(
|
||||
|
||||
@@ -179,7 +179,9 @@ public sealed class WorkerFrameWriter
|
||||
/// remaining events are written, so a batch in flight does not delay a control frame's delivery.
|
||||
/// Every frame's "written and flushed before completion" contract is unchanged. An event batch that
|
||||
/// a control frame cuts into therefore pays one extra flush; an uninterrupted batch still pays
|
||||
/// exactly one.
|
||||
/// exactly one. Mixed passes are not exotic — command replies are Control priority too, so
|
||||
/// sustained command traffic concurrent with event streaming can hit them routinely; the cost
|
||||
/// stays bounded at one extra flush per class transition present in the pass.
|
||||
/// </summary>
|
||||
/// <param name="envelopes">Envelopes to write, in order.</param>
|
||||
/// <param name="priority">Scheduling priority for the whole batch.</param>
|
||||
|
||||
Reference in New Issue
Block a user