fix(sessions): account SessionClosed for sweep-driven closes
TryBeginCloseIfExpired set _closeStarted when it reserved a session, so the sweep's own CloseAsync reported AlreadyClosed: true and CloseSessionCoreAsync skipped SessionClosed: lease-expired, faulted-reaped and detach-grace-expired closes never left the sessions.open gauge and under-counted sessions.closed. The reservation now writes Closing only; the first CloseAsync to take the close lock does the accounting exactly once.
This commit is contained in:
@@ -1095,6 +1095,59 @@ public sealed class SessionManagerTests
|
||||
Assert.Equal(0, activeClient.ShutdownCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Every sweep-driven close — lease expiry, faulted reap and detach-grace expiry — must
|
||||
/// account <c>SessionClosed</c> exactly once. The sweep begins the close itself
|
||||
/// (<c>TryBeginCloseIfExpired</c>) before delegating to the shared close path, and that
|
||||
/// reservation must not make the delegated close look like a second, already-running close
|
||||
/// whose accounting is skipped: the open-session gauge would then never drop for swept
|
||||
/// sessions and the closed counter would under-count them.
|
||||
/// </summary>
|
||||
/// <param name="sweepKind">Which sweep condition selects the session.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Theory]
|
||||
[InlineData(SessionManager.LeaseExpiredReason)]
|
||||
[InlineData(SessionManager.FaultedReason)]
|
||||
[InlineData(SessionManager.DetachGraceExpiredReason)]
|
||||
public async Task CloseExpiredLeasesAsync_SweptClose_AccountsSessionClosedExactlyOnce(string sweepKind)
|
||||
{
|
||||
FakeWorkerClient workerClient = new();
|
||||
FakeTimeProvider clock = new(DateTimeOffset.UtcNow);
|
||||
using GatewayMetrics metrics = new();
|
||||
SessionManager manager = CreateManager(
|
||||
new FakeSessionWorkerClientFactory(workerClient),
|
||||
metrics: metrics,
|
||||
options: CreateOptions(detachGraceSeconds: 30),
|
||||
timeProvider: clock);
|
||||
GatewaySession session = await manager.OpenSessionAsync(CreateOpenRequest(), "client-1", ownerKeyId: null, CancellationToken.None);
|
||||
Assert.Equal(1, metrics.GetSnapshot().OpenSessions);
|
||||
|
||||
switch (sweepKind)
|
||||
{
|
||||
case SessionManager.LeaseExpiredReason:
|
||||
SetLeaseExpiry(session, clock.GetUtcNow().AddSeconds(-1));
|
||||
break;
|
||||
case SessionManager.FaultedReason:
|
||||
session.MarkFaulted("test-fault");
|
||||
break;
|
||||
case SessionManager.DetachGraceExpiredReason:
|
||||
session.AttachEventSubscriber(maxSubscribers: 1).Dispose();
|
||||
clock.Advance(TimeSpan.FromSeconds(31));
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(sweepKind), sweepKind, "Unknown sweep kind.");
|
||||
}
|
||||
|
||||
int closedCount = await manager.CloseExpiredLeasesAsync(clock.GetUtcNow(), CancellationToken.None);
|
||||
|
||||
Assert.Equal(1, closedCount);
|
||||
Assert.Equal(SessionState.Closed, session.State);
|
||||
Assert.False(manager.TryGetSession(session.SessionId, out _));
|
||||
GatewayMetricsSnapshot snapshot = metrics.GetSnapshot();
|
||||
Assert.Equal(1, snapshot.SessionsClosed);
|
||||
Assert.Equal(0, snapshot.OpenSessions);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an expired-lease sweep leaves a session with an active event subscriber open.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
@@ -1436,6 +1489,17 @@ public sealed class SessionManagerTests
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves a session's lease expiry by back-dating its last client activity, which is the only
|
||||
/// production path that moves the lease (<c>_leaseExpiresAt = activityAt + LeaseDuration</c>).
|
||||
/// </summary>
|
||||
/// <param name="session">Session whose lease to move.</param>
|
||||
/// <param name="leaseExpiresAt">Wanted lease expiry.</param>
|
||||
private static void SetLeaseExpiry(GatewaySession session, DateTimeOffset leaseExpiresAt)
|
||||
{
|
||||
session.TouchClientActivity(leaseExpiresAt - session.LeaseDuration);
|
||||
}
|
||||
|
||||
private static SessionOpenRequest CreateOpenRequest()
|
||||
{
|
||||
return new SessionOpenRequest(
|
||||
|
||||
Reference in New Issue
Block a user