feat(sessions): detach-grace retention window for reconnect

This commit is contained in:
Joseph Doherty
2026-06-16 06:15:46 -04:00
parent 85e4334bb7
commit db95f8644f
10 changed files with 346 additions and 6 deletions
@@ -1,5 +1,6 @@
using Google.Protobuf.WellKnownTypes;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Time.Testing;
using ZB.MOM.WW.MxGateway.Contracts.Proto;
using ZB.MOM.WW.MxGateway.Server.Configuration;
using ZB.MOM.WW.MxGateway.Server.Metrics;
@@ -752,6 +753,42 @@ public sealed class SessionManagerTests
Assert.Equal(0, workerClient.ShutdownCount);
}
/// <summary>
/// Task 11. With detach-grace enabled, a session whose last external subscriber dropped
/// and whose detach-grace window has elapsed is closed by the lease sweep exactly like an
/// expired-lease session — even though its normal lease is still far in the future.
/// </summary>
[Fact]
public async Task CloseExpiredLeasesAsync_ClosesSessionWhoseDetachGraceWindowExpired()
{
FakeWorkerClient workerClient = new();
FakeTimeProvider clock = new(DateTimeOffset.UtcNow);
SessionManager manager = CreateManager(
new FakeSessionWorkerClientFactory(workerClient),
options: CreateOptions(defaultLeaseSeconds: 1800, detachGraceSeconds: 30),
timeProvider: clock);
GatewaySession session = await manager.OpenSessionAsync(CreateOpenRequest(), "client-1", ownerKeyId: null, CancellationToken.None);
// Attach and drop an external subscriber to enter detach-grace. The normal lease is
// still 30 minutes out, so only the detach-grace window can close this session.
IDisposable subscriber = session.AttachEventSubscriber(maxSubscribers: 1);
subscriber.Dispose();
Assert.NotNull(session.DetachedAtUtc);
// Before the window elapses: not closed.
clock.Advance(TimeSpan.FromSeconds(29));
int closedBefore = await manager.CloseExpiredLeasesAsync(clock.GetUtcNow(), CancellationToken.None);
Assert.Equal(0, closedBefore);
Assert.Equal(SessionState.Ready, session.State);
// After the window elapses: the sweep closes it.
clock.Advance(TimeSpan.FromSeconds(1));
int closedAfter = await manager.CloseExpiredLeasesAsync(clock.GetUtcNow(), CancellationToken.None);
Assert.Equal(1, closedAfter);
Assert.Equal(SessionState.Closed, session.State);
Assert.Equal(1, workerClient.ShutdownCount);
}
/// <summary>Verifies that shutdown closes all registered sessions.</summary>
[Fact]
public async Task ShutdownAsync_ClosesAllRegisteredSessions()
@@ -797,7 +834,8 @@ public sealed class SessionManagerTests
private static GatewayOptions CreateOptions(
int maxSessions = 64,
int defaultLeaseSeconds = 1800)
int defaultLeaseSeconds = 1800,
int detachGraceSeconds = 0)
{
return new GatewayOptions
{
@@ -806,6 +844,7 @@ public sealed class SessionManagerTests
DefaultCommandTimeoutSeconds = 30,
MaxSessions = maxSessions,
DefaultLeaseSeconds = defaultLeaseSeconds,
DetachGraceSeconds = detachGraceSeconds,
},
Worker = new WorkerOptions
{