dc2df628e3
The remediation reviews approved every task but left a tail of small notes. This lands the gateway-side half of them. Hardening (behavior changes, all narrow): - BuildFilteredWriteBulkCommand's unreachable default case failed OPEN: a fifth bulk-write kind added upstream without a filter case here would have shipped the DENIED entries to the worker while reporting them denied to the caller. It now throws UnreachableException. - SqliteCanonicalAuditStore.ListRecentAsync no longer throws on a row it cannot date. The retention sweep deliberately preserves such rows (SQLite's datetime() yields NULL, so the DELETE never matches), which guaranteed the dashboard's recent-audit view would meet one eventually and lose the whole page to it. The row is now reported at DateTimeOffset.MinValue with every other column intact, behind an optional logger. - The audit drain loop's finally now completes the channel writer alongside detaching the drain, so a producer that raced past the attached check takes the write-through branch instead of stranding its event in a buffer nobody reads until shutdown. TryComplete is idempotent, so StopAsync is unaffected. Tests: - MapCommandReply ownership (Assert.Same on the inner reply), mirroring the existing MapEvent ownership test. - Redactor key-id length boundary at exactly 64 and 65 characters, pinning which way it fails. Nothing validates key-id length at creation, so docs/Diagnostics.md's "which no issued key id does" is now stated as the heuristic it is. - ApiKeyFailureLimiter.Reset with a PartitionResolution whose partition was evicted between the Check and the Reset: inert, and clears nobody else's block. - Constraint-cache concurrency stress: the cap is enforced by the inserting thread, so overshoot must be transient and proportional to the in-flight inserters, and the cache must settle at or under the cap. - ListRecentAsync against a raw-SQL undateable row. Comment/doc accuracy: - EventsHubViewerRegistry.ReleaseConnection records that it relies on SignalR's default sequential per-connection dispatch (MaximumParallelInvocationsPerClient = 1). - A PERF(followup) note on Invoke's double session resolve and why removing it needs a SessionManager overload. - SessionEventDistributor: the volatile-field comment named the pump as the lock-free reader, but the pump's single capture point is inside _replayLock; the genuinely lock-free reader is SubscriberCount. OnSubscriberOverflow's "cannot be observed here" now excepts the DisposeAsync abandon path. The churn test names its ConcurrentDictionary bucket-order assumption and that a violation surfaces as a read timeout, not a silent pass. - The two "restores the sequential drain's behavior" claims (SessionManager, docs/Sessions.md) were wrong: the sequential drain leaked too, because KillWorkerAsync's entry ThrowIfCancellationRequested aborted the whole loop on the first session for zero kills. Reworded to "fixes a leak the sequential drain also had", with the sweep-bound/shutdown-unbound ParallelOptions asymmetry explained. - ISessionManager.ShutdownAsync's token doc: it degrades the drain to a kill sweep rather than cancelling it, with the bounded overrun stated. SessionShutdownHostedService.StopAsync records that its cancellation-logging branch is now unreachable.
93 lines
4.8 KiB
C#
93 lines
4.8 KiB
C#
using System.Diagnostics.CodeAnalysis;
|
|
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
|
|
|
namespace ZB.MOM.WW.MxGateway.Server.Sessions;
|
|
|
|
public interface ISessionManager
|
|
{
|
|
/// <summary>Opens a new gateway session and launches a worker process.</summary>
|
|
/// <param name="request">Request payload.</param>
|
|
/// <param name="clientIdentity">Client identity string.</param>
|
|
/// <param name="ownerKeyId">API key identifier of the caller creating the session.</param>
|
|
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
|
|
/// <returns>The newly opened session.</returns>
|
|
Task<GatewaySession> OpenSessionAsync(
|
|
SessionOpenRequest request,
|
|
string? clientIdentity,
|
|
string? ownerKeyId,
|
|
CancellationToken cancellationToken);
|
|
|
|
/// <summary>Attempts to retrieve a session by ID.</summary>
|
|
/// <param name="sessionId">Identifier of the session.</param>
|
|
/// <param name="session">The retrieved session, if found.</param>
|
|
/// <returns>True if the session exists; otherwise false.</returns>
|
|
bool TryGetSession(
|
|
string sessionId,
|
|
[MaybeNullWhen(false)] out GatewaySession session);
|
|
|
|
/// <summary>Invokes a command on the worker for the specified session.</summary>
|
|
/// <param name="sessionId">Identifier of the session.</param>
|
|
/// <param name="command">Command to invoke.</param>
|
|
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
|
|
/// <returns>The command reply from the worker.</returns>
|
|
Task<WorkerCommandReply> InvokeAsync(
|
|
string sessionId,
|
|
WorkerCommand command,
|
|
CancellationToken cancellationToken);
|
|
|
|
/// <summary>Reads events streamed from the worker for the specified session.</summary>
|
|
/// <param name="sessionId">Identifier of the session.</param>
|
|
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
|
|
/// <returns>Events emitted by the worker.</returns>
|
|
IAsyncEnumerable<WorkerEvent> ReadEventsAsync(
|
|
string sessionId,
|
|
CancellationToken cancellationToken);
|
|
|
|
/// <summary>Closes a session and terminates its worker process.</summary>
|
|
/// <param name="sessionId">Identifier of the session to close.</param>
|
|
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
|
|
/// <returns>The result of closing the session.</returns>
|
|
Task<SessionCloseResult> CloseSessionAsync(
|
|
string sessionId,
|
|
CancellationToken cancellationToken);
|
|
|
|
/// <summary>
|
|
/// Forcefully terminates a session's worker without attempting graceful shutdown,
|
|
/// transitions the session to Closed, and removes it from the registry.
|
|
/// </summary>
|
|
/// <param name="sessionId">Identifier of the session whose worker to kill.</param>
|
|
/// <param name="reason">Reason for killing the worker (recorded in logs/audit).</param>
|
|
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
|
|
/// <returns>The result of closing the session.</returns>
|
|
Task<SessionCloseResult> KillWorkerAsync(
|
|
string sessionId,
|
|
string reason,
|
|
CancellationToken cancellationToken);
|
|
|
|
/// <summary>Closes all sessions with expired leases at the specified time.</summary>
|
|
/// <param name="now">The current time to evaluate expiration against.</param>
|
|
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
|
|
/// <returns>The number of sessions closed.</returns>
|
|
/// <remarks>
|
|
/// A close that fails does not abandon the rest of the pass: every session selected by this
|
|
/// sweep is attempted, and the first failure is then rethrown so the caller still observes
|
|
/// (and logs) that the sweep failed. Which failure surfaces is nondeterministic when several
|
|
/// closes fail in the same pass, because the closes run concurrently.
|
|
/// </remarks>
|
|
Task<int> CloseExpiredLeasesAsync(
|
|
DateTimeOffset now,
|
|
CancellationToken cancellationToken);
|
|
|
|
/// <summary>Shuts down all sessions and the session manager.</summary>
|
|
/// <param name="cancellationToken">
|
|
/// Token that <em>degrades</em> the drain rather than cancelling it. It is passed only to each
|
|
/// session's graceful close; the drain loop and the kill fallback are not bound to it, so
|
|
/// cancelling turns the drain into a kill sweep instead of abandoning the untried sessions as
|
|
/// leaked workers. The call therefore overruns a cancelled token by a bounded amount —
|
|
/// roughly <c>ceil(sessionCount / 4)</c> batches of the worker shutdown timeout in the worst
|
|
/// case, where 4 is <c>MaxParallelSessionCloses</c>.
|
|
/// </param>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
Task ShutdownAsync(CancellationToken cancellationToken);
|
|
}
|