docs(src): add missing XML docs and strip tracking-ID comments
Sweep of 203 source files resolving CommentChecker findings: add <summary>/<param>/<returns>/<inheritdoc> where missing, and remove resolved task/issue tracking markers (Tests-NNN, Worker-NNN, Server-NNN, Task N) from code comments. Comment/doc-only — no logic changes. Server+Tests build clean under TreatWarningsAsErrors.
This commit is contained in:
@@ -4,7 +4,7 @@ namespace ZB.MOM.WW.MxGateway.Server.Sessions;
|
||||
|
||||
/// <summary>
|
||||
/// The result of a reconnect/resume attach
|
||||
/// (<see cref="GatewaySession.AttachEventSubscriberWithReplay"/>, Task 12): the live
|
||||
/// (<see cref="GatewaySession.AttachEventSubscriberWithReplay"/>): the live
|
||||
/// subscriber lease plus the replay batch and resume watermarks snapshotted atomically
|
||||
/// with the registration, so the replay→live handoff has no gap and no duplicate.
|
||||
/// </summary>
|
||||
|
||||
@@ -28,8 +28,7 @@ public sealed class GatewaySession
|
||||
// True once at least one external subscriber attached SUCCESSFULLY. Detach-grace's
|
||||
// "last subscriber dropped" stamp (see DetachEventSubscriber) is gated on this so a
|
||||
// FAILED first attach — which still runs the rollback DetachEventSubscriber from the
|
||||
// attach catch path — does not push a never-subscribed session into the grace window
|
||||
// (Server-055).
|
||||
// attach catch path — does not push a never-subscribed session into the grace window.
|
||||
private bool _everHadEventSubscriber;
|
||||
private SessionEventDistributor? _eventDistributor;
|
||||
private bool _eventDistributorStarted;
|
||||
@@ -115,7 +114,7 @@ public sealed class GatewaySession
|
||||
/// </param>
|
||||
/// <param name="detachGrace">
|
||||
/// Retention window kept after the last external (gRPC) event subscriber drops, so a
|
||||
/// client can reconnect (Task 12). When the window is positive and the active external
|
||||
/// client can reconnect. When the window is positive and the active external
|
||||
/// subscriber count falls to zero, the session stays <see cref="SessionState.Ready"/>
|
||||
/// and records a detached timestamp; the lease monitor closes it once the window
|
||||
/// elapses with no subscriber having re-attached. <see cref="TimeSpan.Zero"/> (the
|
||||
@@ -389,7 +388,7 @@ public sealed class GatewaySession
|
||||
/// session by walking it back to <see cref="SessionState.Ready"/> or any earlier
|
||||
/// state. Both close-related writes (<c>Closing</c> and <c>Closed</c>) go through
|
||||
/// <c>_syncRoot</c> just like every other state read/write, closing the split-lock
|
||||
/// race called out in Server-015.
|
||||
/// race.
|
||||
/// </remarks>
|
||||
public void TransitionTo(SessionState nextState)
|
||||
{
|
||||
@@ -420,13 +419,13 @@ public sealed class GatewaySession
|
||||
/// Transitions the session to the Ready state.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// On becoming Ready the session starts its internal dashboard mirror (Task 6) when a
|
||||
/// On becoming Ready the session starts its internal dashboard mirror when a
|
||||
/// dashboard broadcaster was supplied. The mirror registers an internal subscriber on
|
||||
/// the distributor and starts the pump <em>before</em> any gRPC client attaches, so the
|
||||
/// dashboard EventsHub receives session events even with no gRPC subscriber streaming —
|
||||
/// fixing the "dark feed" where the dashboard only saw events while a gRPC client was
|
||||
/// actively streaming. Registering the internal subscriber BEFORE
|
||||
/// <see cref="SessionEventDistributor.StartAsync"/> also avoids the Task 4 hazard where
|
||||
/// <see cref="SessionEventDistributor.StartAsync"/> also avoids the hazard where
|
||||
/// starting the pump at Ready with zero subscribers drained a fast-completing worker
|
||||
/// stream into nothing and left a later subscriber hanging: there is now always a
|
||||
/// subscriber (the dashboard one) registered before the pump starts.
|
||||
@@ -444,7 +443,7 @@ public sealed class GatewaySession
|
||||
// single drain to first-attach preserves that "events start flowing on subscribe"
|
||||
// behavior and avoids draining a fast-completing source into the void before any
|
||||
// subscriber exists. The source factory mirrors the mapping/ordering/start that
|
||||
// EventStreamService.ProduceEventsAsync used before Task 4: it drains the worker event
|
||||
// EventStreamService.ProduceEventsAsync previously used: it drains the worker event
|
||||
// stream in source order and maps each WorkerEvent to the public MxEvent with the same
|
||||
// mapper, with no skip/filter — per-RPC filtering (e.g. AfterWorkerSequence) stays at the
|
||||
// subscriber boundary in EventStreamService. Returns a registered lease atomically with
|
||||
@@ -462,7 +461,7 @@ public sealed class GatewaySession
|
||||
return lease;
|
||||
}
|
||||
|
||||
// Reconnect/resume variant of StartDistributorAndRegister (Task 12). Snapshots the replay
|
||||
// Reconnect/resume variant of StartDistributorAndRegister. Snapshots the replay
|
||||
// ring for events newer than afterSequence AND registers the live subscriber atomically
|
||||
// under the distributor's replay lock, so the replay→live handoff has no gap and no
|
||||
// duplicate (see SessionEventDistributor.RegisterWithReplay). The pump is started after
|
||||
@@ -542,7 +541,7 @@ public sealed class GatewaySession
|
||||
// once when the session becomes Ready (idempotent). The internal subscriber is registered
|
||||
// BEFORE the pump starts (see StartDistributorAndRegister / EnsureDistributorCreated), so
|
||||
// a subscriber is always present at pump start — the dashboard receives events with no
|
||||
// gRPC subscriber attached, and the Task 4 "zero-subscriber drain into the void" hang
|
||||
// gRPC subscriber attached, and the "zero-subscriber drain into the void" hang
|
||||
// cannot occur. No-op when no dashboard broadcaster was supplied (unit tests).
|
||||
//
|
||||
// Race-safety (Issue 1): _dashboardMirrorLease and _dashboardMirrorTask are published
|
||||
@@ -605,14 +604,14 @@ public sealed class GatewaySession
|
||||
}
|
||||
|
||||
// Reads the internal dashboard subscriber's channel and publishes each RAW fanned event
|
||||
// to the dashboard broadcaster. The dashboard is a first-class distributor subscriber
|
||||
// (Task 6), so it sees the session's full raw event activity — NOT the per-gRPC-subscriber
|
||||
// to the dashboard broadcaster. The dashboard is a first-class distributor subscriber,
|
||||
// so it sees the session's full raw event activity — NOT the per-gRPC-subscriber
|
||||
// AfterWorkerSequence filtering that EventStreamService applies at its own boundary. This
|
||||
// is intentional: the dashboard is a separate LDAP-authenticated monitoring view (per-
|
||||
// session dashboard ACL is the separate Task 18). Publish is best-effort / never-throw, so
|
||||
// session dashboard ACL is a separate concern). Publish is best-effort / never-throw, so
|
||||
// a slow or broken dashboard cannot fault the session or stall the pump; the bounded
|
||||
// internal subscriber channel (Task 5 per-subscriber isolation) only disconnects THIS
|
||||
// mirror on overflow, leaving the session and other subscribers untouched.
|
||||
// internal subscriber channel only disconnects THIS mirror on overflow, leaving the
|
||||
// session and other subscribers untouched.
|
||||
private async Task RunDashboardMirrorAsync(
|
||||
IDashboardEventBroadcaster broadcaster,
|
||||
IEventSubscriberLease lease,
|
||||
@@ -757,6 +756,7 @@ public sealed class GatewaySession
|
||||
/// Determines whether the session lease has expired.
|
||||
/// </summary>
|
||||
/// <param name="now">Current timestamp for comparison.</param>
|
||||
/// <returns><see langword="true"/> if the lease has expired with no active event subscriber; otherwise <see langword="false"/>.</returns>
|
||||
public bool IsLeaseExpired(DateTimeOffset now)
|
||||
{
|
||||
lock (_syncRoot)
|
||||
@@ -778,6 +778,7 @@ public sealed class GatewaySession
|
||||
/// window).
|
||||
/// </summary>
|
||||
/// <param name="now">Current timestamp for comparison.</param>
|
||||
/// <returns><see langword="true"/> if the detach-grace window has elapsed with no re-attached subscriber; otherwise <see langword="false"/>.</returns>
|
||||
public bool IsDetachGraceExpired(DateTimeOffset now)
|
||||
{
|
||||
lock (_syncRoot)
|
||||
@@ -813,6 +814,7 @@ public sealed class GatewaySession
|
||||
/// succeed past it. On distributor-register failure the count is rolled back (see the
|
||||
/// catch below).
|
||||
/// </remarks>
|
||||
/// <returns>A lease that reads the fanned public events for this subscriber.</returns>
|
||||
public IEventSubscriberLease AttachEventSubscriber(int maxSubscribers)
|
||||
{
|
||||
// Derive the mode from the same source the distributor uses so the two can never
|
||||
@@ -868,7 +870,7 @@ public sealed class GatewaySession
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reconnect/resume variant of <see cref="AttachEventSubscriber"/> (Task 12). Attaches
|
||||
/// Reconnect/resume variant of <see cref="AttachEventSubscriber"/>. Attaches
|
||||
/// an event subscriber AND atomically snapshots the session replay ring for events newer
|
||||
/// than <paramref name="afterSequence"/>, so a resuming client can replay what it missed
|
||||
/// before live delivery resumes — with no gap and no duplicate across the handoff.
|
||||
@@ -939,7 +941,7 @@ public sealed class GatewaySession
|
||||
|
||||
// Records that an external subscriber attached successfully. Gates the detach-grace
|
||||
// "last subscriber dropped" stamp so a FAILED first attach (which still rolls back via
|
||||
// DetachEventSubscriber) never pushes a never-subscribed session into grace (Server-055).
|
||||
// DetachEventSubscriber) never pushes a never-subscribed session into grace.
|
||||
private void MarkEventSubscriberAttached()
|
||||
{
|
||||
lock (_syncRoot)
|
||||
@@ -953,6 +955,7 @@ public sealed class GatewaySession
|
||||
/// </summary>
|
||||
/// <param name="command">Worker command to invoke.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
|
||||
/// <returns>The worker's reply to the command.</returns>
|
||||
public async Task<WorkerCommandReply> InvokeAsync(
|
||||
WorkerCommand command,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -969,7 +972,7 @@ public sealed class GatewaySession
|
||||
return await workerClient.InvokeAsync(command, CommandTimeout, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Single outbound choke point for the two array-write ergonomics shims (Task 3):
|
||||
// Single outbound choke point for the two array-write ergonomics shims:
|
||||
// 1. AddItem/AddItem2 array addresses gain the writable "[]" suffix when Galaxy metadata
|
||||
// reports them as arrays, so the worker registers a write-capable handle. The mutation
|
||||
// lands on the same MxCommand instance forwarded to the worker.
|
||||
@@ -1063,6 +1066,7 @@ public sealed class GatewaySession
|
||||
/// <param name="serverHandle">The MXAccess server handle.</param>
|
||||
/// <param name="itemHandle">The MXAccess item handle.</param>
|
||||
/// <param name="registration">The item registration if found.</param>
|
||||
/// <returns><see langword="true"/> if a registration was found for the handle pair; otherwise <see langword="false"/>.</returns>
|
||||
public bool TryGetItemRegistration(
|
||||
int serverHandle,
|
||||
int itemHandle,
|
||||
@@ -1136,6 +1140,7 @@ public sealed class GatewaySession
|
||||
/// <param name="serverHandle">Server handle returned by the worker.</param>
|
||||
/// <param name="tagAddresses">Tag addresses to add.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
|
||||
/// <returns>The per-address subscribe results.</returns>
|
||||
public Task<IReadOnlyList<SubscribeResult>> AddItemBulkAsync(
|
||||
int serverHandle,
|
||||
IReadOnlyList<string> tagAddresses,
|
||||
@@ -1161,6 +1166,7 @@ public sealed class GatewaySession
|
||||
/// <param name="serverHandle">Server handle returned by the worker.</param>
|
||||
/// <param name="itemHandles">Item handles to advise.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
|
||||
/// <returns>The per-handle subscribe results.</returns>
|
||||
public Task<IReadOnlyList<SubscribeResult>> AdviseItemBulkAsync(
|
||||
int serverHandle,
|
||||
IReadOnlyList<int> itemHandles,
|
||||
@@ -1186,6 +1192,7 @@ public sealed class GatewaySession
|
||||
/// <param name="serverHandle">Server handle returned by the worker.</param>
|
||||
/// <param name="itemHandles">Item handles to remove.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
|
||||
/// <returns>The per-handle subscribe results.</returns>
|
||||
public Task<IReadOnlyList<SubscribeResult>> RemoveItemBulkAsync(
|
||||
int serverHandle,
|
||||
IReadOnlyList<int> itemHandles,
|
||||
@@ -1211,6 +1218,7 @@ public sealed class GatewaySession
|
||||
/// <param name="serverHandle">Server handle returned by the worker.</param>
|
||||
/// <param name="itemHandles">Item handles to un-advise.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
|
||||
/// <returns>The per-handle subscribe results.</returns>
|
||||
public Task<IReadOnlyList<SubscribeResult>> UnAdviseItemBulkAsync(
|
||||
int serverHandle,
|
||||
IReadOnlyList<int> itemHandles,
|
||||
@@ -1236,6 +1244,7 @@ public sealed class GatewaySession
|
||||
/// <param name="serverHandle">Server handle returned by the worker.</param>
|
||||
/// <param name="tagAddresses">Tag addresses to subscribe to.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
|
||||
/// <returns>The per-address subscribe results.</returns>
|
||||
public Task<IReadOnlyList<SubscribeResult>> SubscribeBulkAsync(
|
||||
int serverHandle,
|
||||
IReadOnlyList<string> tagAddresses,
|
||||
@@ -1261,6 +1270,7 @@ public sealed class GatewaySession
|
||||
/// <param name="serverHandle">Server handle returned by the worker.</param>
|
||||
/// <param name="itemHandles">Item handles to unsubscribe from.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
|
||||
/// <returns>The per-handle subscribe results.</returns>
|
||||
public Task<IReadOnlyList<SubscribeResult>> UnsubscribeBulkAsync(
|
||||
int serverHandle,
|
||||
IReadOnlyList<int> itemHandles,
|
||||
@@ -1284,6 +1294,7 @@ public sealed class GatewaySession
|
||||
/// <param name="serverHandle">Server handle returned by the worker.</param>
|
||||
/// <param name="entries">Write entries to execute.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
|
||||
/// <returns>The per-entry write results.</returns>
|
||||
public Task<IReadOnlyList<BulkWriteResult>> WriteBulkAsync(
|
||||
int serverHandle,
|
||||
IReadOnlyList<WriteBulkEntry> entries,
|
||||
@@ -1307,6 +1318,7 @@ public sealed class GatewaySession
|
||||
/// <param name="serverHandle">Server handle returned by the worker.</param>
|
||||
/// <param name="entries">Write entries to execute.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
|
||||
/// <returns>The per-entry write results.</returns>
|
||||
public Task<IReadOnlyList<BulkWriteResult>> Write2BulkAsync(
|
||||
int serverHandle,
|
||||
IReadOnlyList<Write2BulkEntry> entries,
|
||||
@@ -1330,6 +1342,7 @@ public sealed class GatewaySession
|
||||
/// <param name="serverHandle">Server handle returned by the worker.</param>
|
||||
/// <param name="entries">Write entries to execute.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
|
||||
/// <returns>The per-entry write results.</returns>
|
||||
public Task<IReadOnlyList<BulkWriteResult>> WriteSecuredBulkAsync(
|
||||
int serverHandle,
|
||||
IReadOnlyList<WriteSecuredBulkEntry> entries,
|
||||
@@ -1353,6 +1366,7 @@ public sealed class GatewaySession
|
||||
/// <param name="serverHandle">Server handle returned by the worker.</param>
|
||||
/// <param name="entries">Write entries to execute.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
|
||||
/// <returns>The per-entry write results.</returns>
|
||||
public Task<IReadOnlyList<BulkWriteResult>> WriteSecured2BulkAsync(
|
||||
int serverHandle,
|
||||
IReadOnlyList<WriteSecured2BulkEntry> entries,
|
||||
@@ -1380,6 +1394,7 @@ public sealed class GatewaySession
|
||||
/// <param name="tagAddresses">Tag addresses to read.</param>
|
||||
/// <param name="timeout">Timeout for the read operation.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
|
||||
/// <returns>The per-address read results.</returns>
|
||||
public Task<IReadOnlyList<BulkReadResult>> ReadBulkAsync(
|
||||
int serverHandle,
|
||||
IReadOnlyList<string> tagAddresses,
|
||||
@@ -1437,6 +1452,7 @@ public sealed class GatewaySession
|
||||
/// <see cref="TransitionTo"/> / <see cref="MarkFaulted"/> and a concurrent
|
||||
/// <c>TransitionTo(Ready)</c> cannot race past a <c>Closing</c> write.
|
||||
/// </remarks>
|
||||
/// <returns>The outcome of the close operation.</returns>
|
||||
public async Task<SessionCloseResult> CloseAsync(
|
||||
string reason,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -1613,7 +1629,7 @@ public sealed class GatewaySession
|
||||
/// Mirrors <see cref="CloseAsync"/>'s use of <c>_closeLock</c> so that
|
||||
/// a Close in flight from one caller and a Kill from another do not
|
||||
/// race on the "was the session already closed" observation that
|
||||
/// drives metric increments (Server-045).
|
||||
/// drives metric increments.
|
||||
/// </remarks>
|
||||
/// <param name="reason">Reason for killing the worker.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
@@ -1652,6 +1668,7 @@ public sealed class GatewaySession
|
||||
/// The acquire is best-effort: a non-cancellable wait that swallows
|
||||
/// <see cref="ObjectDisposedException"/> so double-dispose still completes.
|
||||
/// </remarks>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
try
|
||||
@@ -1991,7 +2008,7 @@ public sealed class GatewaySession
|
||||
// session instead of letting it linger only on the (long) lease: stamp the detached
|
||||
// time so the lease monitor can close it once the grace window elapses. The session
|
||||
// stays in its current (Ready) state and remains usable, so a reconnecting subscriber
|
||||
// (Task 12) re-attaches normally. The gateway-owned internal dashboard subscriber is
|
||||
// re-attaches normally. The gateway-owned internal dashboard subscriber is
|
||||
// NOT counted in _activeEventSubscriberCount (it registers on the distributor with
|
||||
// isInternal: true), so a session whose only remaining subscriber is the dashboard
|
||||
// mirror still enters grace. Only stamp while the session is alive — once
|
||||
@@ -2001,7 +2018,7 @@ public sealed class GatewaySession
|
||||
// Only stamp a detach that mirrors a prior SUCCESSFUL attach. The attach catch path
|
||||
// calls this same method to roll back a reserved slot when the FIRST attach failed
|
||||
// before any subscriber registered; that never-subscribed session must not enter the
|
||||
// grace window (Server-055).
|
||||
// grace window.
|
||||
if (_everHadEventSubscriber
|
||||
&& _detachGrace > TimeSpan.Zero
|
||||
&& _activeEventSubscriberCount == 0
|
||||
|
||||
@@ -74,5 +74,6 @@ public interface ISessionManager
|
||||
|
||||
/// <summary>Shuts down all sessions and the session manager.</summary>
|
||||
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
Task ShutdownAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -14,12 +14,12 @@ namespace ZB.MOM.WW.MxGateway.Server.Sessions;
|
||||
/// </summary>
|
||||
/// <param name="isOnlySubscriber">
|
||||
/// <see langword="true"/> when FailFast is allowed to fault the whole session for this
|
||||
/// overflow. As of Task 8 this is gated on the SESSION MODE, not a live count: it is
|
||||
/// overflow. This is gated on the SESSION MODE, not a live count: it is
|
||||
/// <see langword="true"/> only for an external subscriber in single-subscriber mode
|
||||
/// (<c>AllowMultipleEventSubscribers == false</c>), where at most one external subscriber
|
||||
/// can ever exist. In multi-subscriber mode it is always <see langword="false"/>, so
|
||||
/// FailFast degrades to a per-subscriber disconnect and one slow consumer never faults a
|
||||
/// session shared by others; gating on the fixed mode also removes the Task 5 race where a
|
||||
/// session shared by others; gating on the fixed mode also removes the race where a
|
||||
/// concurrent registration could make a count snapshot falsely report a sole subscriber.
|
||||
/// Always <see langword="false"/> for internal subscribers (the dashboard mirror) so a
|
||||
/// slow/broken dashboard can never fault the session.
|
||||
@@ -38,15 +38,14 @@ public delegate void SubscriberOverflowHandler(bool isOnlySubscriber, bool isInt
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Introduced by Task 2 of the Session Resilience epic; the bounded replay ring
|
||||
/// buffer was added by Task 3, it was wired into <c>GatewaySession</c> and
|
||||
/// <c>EventStreamService</c> by Task 4, and the per-subscriber backpressure-isolation
|
||||
/// policy (Task 5) is implemented here: a slow subscriber overflows only its own
|
||||
/// bounded channel and the pump applies the policy to that subscriber alone (see
|
||||
/// The bounded replay ring buffer is wired into <c>GatewaySession</c> and
|
||||
/// <c>EventStreamService</c>. The per-subscriber backpressure-isolation policy is
|
||||
/// implemented here: a slow subscriber overflows only its own bounded channel and the
|
||||
/// pump applies the policy to that subscriber alone (see
|
||||
/// <see cref="SubscriberOverflowHandler"/> and <c>OnSubscriberOverflow</c>), leaving
|
||||
/// the pump, the session, and other subscribers running. Task 8 made the
|
||||
/// FailFast-faults-session decision mode-gated: it fires only in single-subscriber
|
||||
/// mode (<c>singleSubscriberMode</c>), so multi-subscriber FailFast always degrades to
|
||||
/// the pump, the session, and other subscribers running. The FailFast-faults-session
|
||||
/// decision is mode-gated: it fires only in single-subscriber mode
|
||||
/// (<c>singleSubscriberMode</c>), so multi-subscriber FailFast always degrades to
|
||||
/// a per-subscriber disconnect — see <c>OnSubscriberOverflow</c>. The ring buffer supports capacity
|
||||
/// eviction (oldest entry dropped when the count exceeds
|
||||
/// <c>replayBufferCapacity</c>) and age eviction (entries older than
|
||||
@@ -58,7 +57,7 @@ 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 for Task 4: it can pass
|
||||
/// 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
|
||||
/// channel reader's <c>ReadAllAsync</c> with no real session. The pump owns the
|
||||
@@ -133,7 +132,7 @@ public sealed class SessionEventDistributor : IAsyncDisposable
|
||||
/// <param name="sessionId">Owning session id, used only for logging context.</param>
|
||||
/// <param name="eventSourceFactory">
|
||||
/// Factory producing the session's event stream given a cancellation token.
|
||||
/// The pump consumes this exactly once. See the type remarks for the seam Task 4
|
||||
/// The pump consumes this exactly once. See the type remarks for the seam this
|
||||
/// plugs into.
|
||||
/// </param>
|
||||
/// <param name="subscriberQueueCapacity">
|
||||
@@ -141,10 +140,18 @@ public sealed class SessionEventDistributor : IAsyncDisposable
|
||||
/// queue capacity shape used today.
|
||||
/// </param>
|
||||
/// <param name="logger">Logger for pump lifecycle diagnostics.</param>
|
||||
/// <param name="overflowHandler">
|
||||
/// Optional per-subscriber backpressure handler invoked when a subscriber's bounded
|
||||
/// channel is full. See the primary constructor overload for the full contract.
|
||||
/// </param>
|
||||
/// <param name="singleSubscriberMode">
|
||||
/// <see langword="true"/> when the owning session is in single-subscriber mode. See
|
||||
/// the primary constructor overload for the full contract.
|
||||
/// </param>
|
||||
/// <remarks>
|
||||
/// This overload disables the replay ring buffer (capacity 0). Use the overload
|
||||
/// taking replay parameters to retain events for reconnect/reattach replay.
|
||||
/// Kept <c>internal</c> so production wiring (Task 4) cannot accidentally use
|
||||
/// Kept <c>internal</c> so production wiring cannot accidentally use
|
||||
/// the no-replay path; tests reach it via <c>InternalsVisibleTo</c>.
|
||||
/// </remarks>
|
||||
internal SessionEventDistributor(
|
||||
@@ -173,7 +180,7 @@ public sealed class SessionEventDistributor : IAsyncDisposable
|
||||
/// <param name="sessionId">Owning session id, used only for logging context.</param>
|
||||
/// <param name="eventSourceFactory">
|
||||
/// Factory producing the session's event stream given a cancellation token.
|
||||
/// The pump consumes this exactly once. See the type remarks for the seam Task 4
|
||||
/// The pump consumes this exactly once. See the type remarks for the seam this
|
||||
/// plugs into.
|
||||
/// </param>
|
||||
/// <param name="subscriberQueueCapacity">
|
||||
@@ -208,9 +215,9 @@ public sealed class SessionEventDistributor : IAsyncDisposable
|
||||
/// overflows reports <c>isOnlySubscriber == true</c> (legacy FailFast faults the
|
||||
/// session) ONLY in single-subscriber mode. In multi-subscriber mode it is always
|
||||
/// <see langword="false"/>, so FailFast degrades to a per-subscriber disconnect and a
|
||||
/// transient registration race can never falsely fault a shared session (Task 8;
|
||||
/// resolves the Task 5 REVISIT race). Defaults to <see langword="true"/> so existing
|
||||
/// call sites and unit tests keep legacy single-subscriber FailFast behavior.
|
||||
/// transient registration race can never falsely fault a shared session. Defaults to
|
||||
/// <see langword="true"/> so existing call sites and unit tests keep legacy
|
||||
/// single-subscriber FailFast behavior.
|
||||
/// </param>
|
||||
public SessionEventDistributor(
|
||||
string sessionId,
|
||||
@@ -259,6 +266,7 @@ public sealed class SessionEventDistributor : IAsyncDisposable
|
||||
/// Starts the background pump. Idempotent — a second call is a no-op.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Token observed only while starting.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
@@ -285,7 +293,7 @@ public sealed class SessionEventDistributor : IAsyncDisposable
|
||||
/// subscribers.
|
||||
/// </summary>
|
||||
/// <param name="isInternal">
|
||||
/// <see langword="true"/> for a gateway-owned internal subscriber (Task 6: the
|
||||
/// <see langword="true"/> for a gateway-owned internal subscriber (the
|
||||
/// session's dashboard mirror) that must NOT participate in the single-subscriber
|
||||
/// overflow accounting. An internal subscriber is excluded from the
|
||||
/// <c>isOnlySubscriber</c> count, so a lone external gRPC subscriber still reports
|
||||
@@ -296,6 +304,7 @@ public sealed class SessionEventDistributor : IAsyncDisposable
|
||||
/// disconnected from the mirror. Defaults to <see langword="false"/> (external
|
||||
/// subscriber) so every existing call site is unchanged.
|
||||
/// </param>
|
||||
/// <returns>The lease for the newly-registered subscriber.</returns>
|
||||
public IEventSubscriberLease Register(bool isInternal = false)
|
||||
{
|
||||
Channel<MxEvent> channel = CreateSubscriberChannel();
|
||||
@@ -355,7 +364,7 @@ public sealed class SessionEventDistributor : IAsyncDisposable
|
||||
/// <summary>
|
||||
/// Atomically snapshots the replay ring for events newer than
|
||||
/// <paramref name="afterSequence"/> AND registers a live subscriber, so the
|
||||
/// replay→live handoff has no gap and no duplicate (Task 12 reconnect/resume).
|
||||
/// replay→live handoff has no gap and no duplicate (reconnect/resume).
|
||||
/// </summary>
|
||||
/// <param name="afterSequence">
|
||||
/// The last worker sequence the reconnecting client already observed. Replay returns
|
||||
@@ -389,6 +398,7 @@ public sealed class SessionEventDistributor : IAsyncDisposable
|
||||
/// <see langword="true"/> for a gateway-owned internal subscriber. See
|
||||
/// <see cref="Register"/>.
|
||||
/// </param>
|
||||
/// <returns>The lease for the newly-registered subscriber.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <b>Why this is atomic and the handoff is correct.</b> The replay snapshot and the
|
||||
@@ -488,6 +498,7 @@ public sealed class SessionEventDistributor : IAsyncDisposable
|
||||
/// <summary>
|
||||
/// Stops the pump and completes all subscriber channels. Idempotent.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
Task? pumpTask;
|
||||
@@ -603,22 +614,6 @@ public sealed class SessionEventDistributor : IAsyncDisposable
|
||||
{
|
||||
// Decide whether FailFast may fault the whole session for this overflow. This is the
|
||||
// "isOnlySubscriber" signal the legacy single-subscriber FailFast path keys on.
|
||||
//
|
||||
// Task 8 resolution of the Task 5/7 REVISIT race: gate this on the SESSION MODE
|
||||
// (_singleSubscriberMode), NOT on a live count snapshot. The old
|
||||
// `CountExternalSubscribers() == 1` snapshot raced once multi-subscriber became real —
|
||||
// a concurrent second registration/unregistration could make the count read as 1 with
|
||||
// two subscribers actually present, producing a false FailFast that faults a shared
|
||||
// session. The mode is fixed for the session's lifetime, so reading it is race-free:
|
||||
// - single-subscriber mode: at most one external subscriber can ever exist (the
|
||||
// AttachEventSubscriber guard enforces it), so an overflowing external subscriber
|
||||
// IS the sole subscriber — preserve the legacy FailFast session-fault behavior.
|
||||
// - multi-subscriber mode: never fault the shared session; FailFast degrades to a
|
||||
// per-subscriber disconnect so one slow consumer cannot punish healthy ones.
|
||||
//
|
||||
// Task 6: the gateway-owned internal dashboard subscriber is excluded — an internal
|
||||
// subscriber that overflows is NEVER the "only subscriber", so a slow/broken dashboard
|
||||
// can only disconnect its own mirror and never fault the session.
|
||||
bool isOnlySubscriber = !subscriber.IsInternal && _singleSubscriberMode;
|
||||
|
||||
_logger.LogDebug(
|
||||
@@ -816,12 +811,17 @@ public sealed class SessionEventDistributor : IAsyncDisposable
|
||||
|
||||
private sealed class Subscriber(long id, Channel<MxEvent> channel, bool isInternal)
|
||||
{
|
||||
/// <summary>Gets the subscriber's monotonic id, assigned at registration.</summary>
|
||||
public long Id { get; } = id;
|
||||
|
||||
/// <summary>Gets the subscriber's own bounded event channel, written by the pump.</summary>
|
||||
public Channel<MxEvent> Channel { get; } = channel;
|
||||
|
||||
// True for the gateway-owned internal dashboard subscriber. Excluded from the
|
||||
// single-subscriber overflow accounting so it cannot fault the session.
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this is the gateway-owned internal dashboard
|
||||
/// subscriber. Excluded from the single-subscriber overflow accounting so it cannot
|
||||
/// fault the session.
|
||||
/// </summary>
|
||||
public bool IsInternal { get; } = isInternal;
|
||||
}
|
||||
|
||||
@@ -830,8 +830,13 @@ public sealed class SessionEventDistributor : IAsyncDisposable
|
||||
{
|
||||
private int _leaseDisposed;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ChannelReader<MxEvent> Reader => subscriber.Channel.Reader;
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters the subscriber from the distributor and completes its channel.
|
||||
/// Safe to call more than once; only the first call takes effect.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
// Atomic check-and-set so concurrent Dispose calls unregister at most once.
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace ZB.MOM.WW.MxGateway.Server.Sessions;
|
||||
/// <param name="Mapper">
|
||||
/// Maps worker IPC <c>WorkerEvent</c> frames to public <c>MxEvent</c>s. The distributor
|
||||
/// pump applies this once per event in worker order, mirroring the mapping
|
||||
/// <c>EventStreamService.ProduceEventsAsync</c> used before Task 4.
|
||||
/// <c>EventStreamService.ProduceEventsAsync</c> used previously.
|
||||
/// </param>
|
||||
/// <param name="EventOptions">
|
||||
/// Supplies the distributor's per-subscriber queue capacity and replay ring-buffer
|
||||
@@ -32,20 +32,20 @@ namespace ZB.MOM.WW.MxGateway.Server.Sessions;
|
||||
/// preserving the observability the pre-epic per-RPC overflow path emitted.
|
||||
/// </param>
|
||||
/// <param name="DashboardBroadcaster">
|
||||
/// Sink the session's internal dashboard mirror loop (Task 6) publishes raw session
|
||||
/// Sink the session's internal dashboard mirror loop publishes raw session
|
||||
/// <c>MxEvent</c>s to. When non-null the session registers an internal distributor
|
||||
/// subscriber on becoming Ready and mirrors every fanned event to the dashboard
|
||||
/// EventsHub group regardless of whether a gRPC client is streaming. When null
|
||||
/// (unit tests that don't exercise the dashboard mirror) no mirror is started.
|
||||
/// </param>
|
||||
/// <param name="AllowMultipleEventSubscribers">
|
||||
/// The session's effective multi-subscriber mode (Task 8). Carried here so the session
|
||||
/// The session's effective multi-subscriber mode. Carried here so the session
|
||||
/// can pass it to its <see cref="SessionEventDistributor"/> at construction — the
|
||||
/// distributor is created at <c>MarkReady</c> (for the dashboard mirror) before any gRPC
|
||||
/// subscriber attaches, so the mode cannot be learned from a later
|
||||
/// <c>AttachEventSubscriber</c> call. The distributor gates its FailFast session-fault
|
||||
/// decision on this mode (single-subscriber only) instead of a live count snapshot,
|
||||
/// closing the Task 5 false-FailFast race. Defaults to <see langword="false"/>
|
||||
/// closing the false-FailFast race. Defaults to <see langword="false"/>
|
||||
/// (single-subscriber) so existing call sites and unit tests are unchanged.
|
||||
/// </param>
|
||||
public sealed record SessionEventStreaming(
|
||||
|
||||
@@ -44,7 +44,7 @@ public sealed class SessionManager : ISessionManager
|
||||
/// <param name="distributorLogger">Logger passed to each session's event distributor pump.</param>
|
||||
/// <param name="dashboardEventBroadcaster">
|
||||
/// Dashboard SignalR fan-out sink. Each session registers an internal distributor
|
||||
/// subscriber (Task 6) that mirrors raw session events to this broadcaster, so the
|
||||
/// subscriber that mirrors raw session events to this broadcaster, so the
|
||||
/// dashboard receives events regardless of whether a gRPC client is streaming. Null in
|
||||
/// unit tests that do not exercise the dashboard mirror.
|
||||
/// </param>
|
||||
@@ -79,14 +79,7 @@ public sealed class SessionManager : ISessionManager
|
||||
_sessionSlots = new SemaphoreSlim(_options.Sessions.MaxSessions, _options.Sessions.MaxSessions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens a new gateway session and connects to the worker.
|
||||
/// </summary>
|
||||
/// <param name="request">Session open request.</param>
|
||||
/// <param name="clientIdentity">Client authentication identity.</param>
|
||||
/// <param name="ownerKeyId">API key identifier of the caller creating the session.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Opened gateway session.</returns>
|
||||
/// <inheritdoc />
|
||||
public async Task<GatewaySession> OpenSessionAsync(
|
||||
SessionOpenRequest request,
|
||||
string? clientIdentity,
|
||||
@@ -151,12 +144,7 @@ public sealed class SessionManager : ISessionManager
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to retrieve a session by ID.
|
||||
/// </summary>
|
||||
/// <param name="sessionId">Session identifier.</param>
|
||||
/// <param name="session">The session if found.</param>
|
||||
/// <returns>True if session found; otherwise false.</returns>
|
||||
/// <inheritdoc />
|
||||
public bool TryGetSession(
|
||||
string sessionId,
|
||||
[MaybeNullWhen(false)] out GatewaySession session)
|
||||
@@ -164,13 +152,7 @@ public sealed class SessionManager : ISessionManager
|
||||
return _registry.TryGet(sessionId, out session);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes a worker command on a session asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="sessionId">Session identifier.</param>
|
||||
/// <param name="command">Worker command.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Command reply.</returns>
|
||||
/// <inheritdoc />
|
||||
public async Task<WorkerCommandReply> InvokeAsync(
|
||||
string sessionId,
|
||||
WorkerCommand command,
|
||||
@@ -197,12 +179,7 @@ public sealed class SessionManager : ISessionManager
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads events from a session's event stream asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="sessionId">Session identifier.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Async enumerable of worker events.</returns>
|
||||
/// <inheritdoc />
|
||||
public IAsyncEnumerable<WorkerEvent> ReadEventsAsync(
|
||||
string sessionId,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -212,12 +189,7 @@ public sealed class SessionManager : ISessionManager
|
||||
return session.ReadEventsAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes a gateway session asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="sessionId">Session identifier.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Session close result.</returns>
|
||||
/// <inheritdoc />
|
||||
public async Task<SessionCloseResult> CloseSessionAsync(
|
||||
string sessionId,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -231,16 +203,12 @@ public sealed class SessionManager : ISessionManager
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forcefully terminates a session's worker without attempting graceful shutdown.
|
||||
/// <inheritdoc />
|
||||
/// <remarks>
|
||||
/// Mirrors the registry/metrics cleanup that <see cref="CloseSessionCoreAsync"/>
|
||||
/// performs after a successful close, but skips the <c>WorkerClient.ShutdownAsync</c>
|
||||
/// step that <see cref="GatewaySession.CloseAsync"/> would otherwise attempt.
|
||||
/// </summary>
|
||||
/// <param name="sessionId">Session identifier.</param>
|
||||
/// <param name="reason">Reason recorded for the kill.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Session close result.</returns>
|
||||
/// </remarks>
|
||||
public async Task<SessionCloseResult> KillWorkerAsync(
|
||||
string sessionId,
|
||||
string reason,
|
||||
@@ -251,10 +219,6 @@ public sealed class SessionManager : ISessionManager
|
||||
|
||||
GatewaySession session = GetRequiredSession(sessionId);
|
||||
|
||||
// Serialize concurrent kill/close attempts on this session by routing through the
|
||||
// per-session close lock (Server-045). Returns whether the session was already in
|
||||
// Closed state when the lock was acquired so the metric counter is incremented at
|
||||
// most once across concurrent callers.
|
||||
bool wasClosed;
|
||||
try
|
||||
{
|
||||
@@ -265,10 +229,6 @@ public sealed class SessionManager : ISessionManager
|
||||
session.MarkFaulted(exception.Message);
|
||||
_metrics.Fault(SessionManagerErrorCode.CloseFailed.ToString());
|
||||
|
||||
// Server-044: the open-session gauge was incremented in OpenSessionAsync;
|
||||
// every session reaching KillWorkerAsync had SessionOpened recorded. If the
|
||||
// kill path throws, decrement the gauge here so mxgateway.sessions.open
|
||||
// does not leak — mirroring the Server-006 fix on OpenSessionAsync.
|
||||
_metrics.SessionRemoved();
|
||||
await RemoveSessionAsync(session).ConfigureAwait(false);
|
||||
throw new SessionManagerException(
|
||||
@@ -291,12 +251,7 @@ public sealed class SessionManager : ISessionManager
|
||||
return new SessionCloseResult(sessionId, SessionState.Closed, AlreadyClosed: wasClosed);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes all sessions with expired leases asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="now">Current time for lease expiration check.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Count of sessions closed.</returns>
|
||||
/// <inheritdoc />
|
||||
public async Task<int> CloseExpiredLeasesAsync(
|
||||
DateTimeOffset now,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -341,11 +296,7 @@ public sealed class SessionManager : ISessionManager
|
||||
return closedCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shuts down all active sessions gracefully asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Completed task.</returns>
|
||||
/// <inheritdoc />
|
||||
public async Task ShutdownAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (GatewaySession session in _registry.Snapshot())
|
||||
@@ -361,10 +312,6 @@ public sealed class SessionManager : ISessionManager
|
||||
"Graceful shutdown failed for session {SessionId}; killing worker.",
|
||||
session.SessionId);
|
||||
|
||||
// Defensive fallback: CloseSessionCoreAsync's inner SessionCloseStartedException
|
||||
// catch normally removes the session and accounts the close (Server-046). The
|
||||
// outer fallback only fires for sessions still in the registry — route through
|
||||
// KillWorkerAsync so the bookkeeping is identical to the dashboard kill path.
|
||||
if (_registry.TryGet(session.SessionId, out _))
|
||||
{
|
||||
try
|
||||
@@ -409,11 +356,6 @@ public sealed class SessionManager : ISessionManager
|
||||
session.MarkFaulted(exception.Message);
|
||||
if (!wasClosed)
|
||||
{
|
||||
// Server-046: account the close as a SessionClosed (decrements the open-session
|
||||
// gauge AND increments the sessions.closed counter), not just SessionRemoved.
|
||||
// The session is being removed from the registry below; treating this as a
|
||||
// half-finished close that only decremented the gauge under-counted the closed
|
||||
// counter.
|
||||
_metrics.SessionClosed();
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ public sealed record SessionOpenRequest(
|
||||
{
|
||||
/// <summary>Creates a SessionOpenRequest from a gRPC OpenSessionRequest contract.</summary>
|
||||
/// <param name="request">Request payload.</param>
|
||||
/// <returns>The equivalent <see cref="SessionOpenRequest"/>.</returns>
|
||||
public static SessionOpenRequest FromContract(OpenSessionRequest request)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
@@ -11,20 +11,13 @@ public sealed class SessionRegistry : ISessionRegistry
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, GatewaySession> _sessions = new(StringComparer.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total count of sessions in the registry.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public int Count => _sessions.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the count of non-closed sessions.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public int ActiveCount => _sessions.Values.Count(session => session.State is not SessionState.Closed);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a session to the registry.
|
||||
/// </summary>
|
||||
/// <param name="session">Gateway session to add.</param>
|
||||
/// <inheritdoc />
|
||||
public bool TryAdd(GatewaySession session)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(session);
|
||||
@@ -32,11 +25,7 @@ public sealed class SessionRegistry : ISessionRegistry
|
||||
return _sessions.TryAdd(session.SessionId, session);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a session by identifier.
|
||||
/// </summary>
|
||||
/// <param name="sessionId">Identifier of the session.</param>
|
||||
/// <param name="session">The retrieved session if found.</param>
|
||||
/// <inheritdoc />
|
||||
public bool TryGet(
|
||||
string sessionId,
|
||||
[MaybeNullWhen(false)] out GatewaySession session)
|
||||
@@ -44,11 +33,7 @@ public sealed class SessionRegistry : ISessionRegistry
|
||||
return _sessions.TryGetValue(sessionId, out session);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a session from the registry by identifier.
|
||||
/// </summary>
|
||||
/// <param name="sessionId">Identifier of the session.</param>
|
||||
/// <param name="session">The removed session if found.</param>
|
||||
/// <inheritdoc />
|
||||
public bool TryRemove(
|
||||
string sessionId,
|
||||
[MaybeNullWhen(false)] out GatewaySession session)
|
||||
@@ -56,9 +41,7 @@ public sealed class SessionRegistry : ISessionRegistry
|
||||
return _sessions.TryRemove(sessionId, out session);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a snapshot of all sessions in the registry.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyCollection<GatewaySession> Snapshot()
|
||||
{
|
||||
return _sessions.Values.ToArray();
|
||||
|
||||
@@ -8,13 +8,17 @@ public sealed class SessionShutdownHostedService(
|
||||
ISessionManager sessionManager,
|
||||
ILogger<SessionShutdownHostedService> logger) : IHostedService
|
||||
{
|
||||
/// <inheritdoc />
|
||||
/// <summary>No-op startup hook; this service only has work to do on shutdown.</summary>
|
||||
/// <param name="cancellationToken">Token that signals startup should be aborted.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>Shuts down all gateway sessions as the host stops, logging (without throwing) if the host's shutdown timeout cancels the operation first.</summary>
|
||||
/// <param name="cancellationToken">Token that signals the host's shutdown timeout has elapsed.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -39,10 +39,7 @@ public sealed class SessionWorkerClientFactory : ISessionWorkerClientFactory
|
||||
_options = options.Value;
|
||||
}
|
||||
|
||||
/// <summary>Creates a worker client and launches the worker process.</summary>
|
||||
/// <param name="session">The gateway session.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The created worker client.</returns>
|
||||
/// <inheritdoc />
|
||||
public async Task<IWorkerClient> CreateAsync(
|
||||
GatewaySession session,
|
||||
CancellationToken cancellationToken)
|
||||
|
||||
Reference in New Issue
Block a user