refactor(sessions): derive subscriber mode from session config; close Task 8 review nits
Remove the per-call allowMultipleSubscribers param from AttachEventSubscriber and derive the mode internally from _eventStreaming.AllowMultipleEventSubscribers — the same source SessionEventDistributor uses for singleSubscriberMode — so the two can never structurally diverge. The maxSubscribers cap param is kept because MaxEventSubscribersPerSession lives in SessionOptions, which the session does not hold directly (only EventOptions flows through SessionEventStreaming). Other nits: - SubscriberCount XML doc clarifies it includes internal subscribers and differs from GatewaySession.ActiveEventSubscriberCount (external/gRPC only). - SingleSubscriberMode_LoneExternalOverflow test: add Assert.Equal(1, observedSet) guard before the value assertion so the test cannot pass vacuously if the handler never fired. - GatewayOptionsValidator.ValidateSessions: add explanatory code comment documenting why !AllowMultipleEventSubscribers && MaxEventSubscribersPerSession > 1 is NOT rejected as a hard error (the default config ships with this combination; the cap is simply unused in single-subscriber mode, not a behavior bug). - GatewaySession.DetachEventSubscriber: add Debug.Assert before the clamp so a genuine double-decrement surfaces in debug builds.
This commit is contained in:
+1
-1
@@ -151,7 +151,7 @@ public sealed class GatewaySessionDashboardMirrorTests
|
||||
session.MarkReady();
|
||||
|
||||
Assert.Equal(0, session.ActiveEventSubscriberCount);
|
||||
using IEventSubscriberLease lease = session.AttachEventSubscriber(allowMultipleSubscribers: false, maxSubscribers: 1);
|
||||
using IEventSubscriberLease lease = session.AttachEventSubscriber(maxSubscribers: 1);
|
||||
Assert.Equal(1, session.ActiveEventSubscriberCount);
|
||||
}
|
||||
|
||||
|
||||
@@ -168,8 +168,8 @@ public sealed class GatewaySessionTests
|
||||
/// completion and a client cancellation both fire at the same time — must
|
||||
/// decrement <c>_activeEventSubscriberCount</c> exactly once, never to −1.
|
||||
/// A negative count permanently blocks future subscribers because
|
||||
/// <c>AttachEventSubscriber(allowMultipleSubscribers:false)</c> gates on
|
||||
/// <c>_activeEventSubscriberCount > 0</c>. After both racing disposes finish,
|
||||
/// <c>AttachEventSubscriber</c> gates on <c>_activeEventSubscriberCount >= effectiveCap</c>.
|
||||
/// After both racing disposes finish,
|
||||
/// the count must be exactly 0 and a subsequent single-subscriber attach must
|
||||
/// succeed.
|
||||
/// </summary>
|
||||
@@ -186,8 +186,7 @@ public sealed class GatewaySessionTests
|
||||
for (int i = 0; i < Iterations; i++)
|
||||
{
|
||||
// Attach one subscriber; this increments _activeEventSubscriberCount to 1.
|
||||
IEventSubscriberLease lease = session.AttachEventSubscriber(
|
||||
allowMultipleSubscribers: false, maxSubscribers: 1);
|
||||
IEventSubscriberLease lease = session.AttachEventSubscriber(maxSubscribers: 1);
|
||||
|
||||
// Race Concurrency threads all calling Dispose() on the same lease.
|
||||
// Only one must actually run DetachEventSubscriber.
|
||||
@@ -212,8 +211,7 @@ public sealed class GatewaySessionTests
|
||||
|
||||
// Observable contract: a fresh single subscriber must now be attachable
|
||||
// (i.e., the guard _activeEventSubscriberCount > 0 is false).
|
||||
IEventSubscriberLease next = session.AttachEventSubscriber(
|
||||
allowMultipleSubscribers: false, maxSubscribers: 1);
|
||||
IEventSubscriberLease next = session.AttachEventSubscriber(maxSubscribers: 1);
|
||||
next.Dispose();
|
||||
Assert.Equal(0, session.ActiveEventSubscriberCount);
|
||||
}
|
||||
@@ -233,11 +231,10 @@ public sealed class GatewaySessionTests
|
||||
FakeWorkerClient workerClient = new();
|
||||
GatewaySession session = CreateReadySessionWithEventStreaming(workerClient);
|
||||
|
||||
using IEventSubscriberLease first = session.AttachEventSubscriber(
|
||||
allowMultipleSubscribers: false, maxSubscribers: 8);
|
||||
using IEventSubscriberLease first = session.AttachEventSubscriber(maxSubscribers: 8);
|
||||
|
||||
SessionManagerException exception = Assert.Throws<SessionManagerException>(
|
||||
() => session.AttachEventSubscriber(allowMultipleSubscribers: false, maxSubscribers: 8));
|
||||
() => session.AttachEventSubscriber(maxSubscribers: 8));
|
||||
Assert.Equal(SessionManagerErrorCode.EventSubscriberAlreadyActive, exception.ErrorCode);
|
||||
Assert.Equal(1, session.ActiveEventSubscriberCount);
|
||||
|
||||
@@ -262,13 +259,13 @@ public sealed class GatewaySessionTests
|
||||
List<IEventSubscriberLease> leases = [];
|
||||
for (int i = 0; i < Cap; i++)
|
||||
{
|
||||
leases.Add(session.AttachEventSubscriber(allowMultipleSubscribers: true, maxSubscribers: Cap));
|
||||
leases.Add(session.AttachEventSubscriber(maxSubscribers: Cap));
|
||||
}
|
||||
|
||||
Assert.Equal(Cap, session.ActiveEventSubscriberCount);
|
||||
|
||||
SessionManagerException exception = Assert.Throws<SessionManagerException>(
|
||||
() => session.AttachEventSubscriber(allowMultipleSubscribers: true, maxSubscribers: Cap));
|
||||
() => session.AttachEventSubscriber(maxSubscribers: Cap));
|
||||
Assert.Equal(SessionManagerErrorCode.EventSubscriberLimitReached, exception.ErrorCode);
|
||||
Assert.Equal(Cap, session.ActiveEventSubscriberCount);
|
||||
|
||||
@@ -304,14 +301,14 @@ public sealed class GatewaySessionTests
|
||||
List<IEventSubscriberLease> leases = [];
|
||||
for (int i = 0; i < Cap; i++)
|
||||
{
|
||||
leases.Add(session.AttachEventSubscriber(allowMultipleSubscribers: true, maxSubscribers: Cap));
|
||||
leases.Add(session.AttachEventSubscriber(maxSubscribers: Cap));
|
||||
}
|
||||
|
||||
Assert.Equal(Cap, session.ActiveEventSubscriberCount);
|
||||
|
||||
// The (cap+1)-th still fails: the dashboard mirror did not eat a slot.
|
||||
SessionManagerException exception = Assert.Throws<SessionManagerException>(
|
||||
() => session.AttachEventSubscriber(allowMultipleSubscribers: true, maxSubscribers: Cap));
|
||||
() => session.AttachEventSubscriber(maxSubscribers: Cap));
|
||||
Assert.Equal(SessionManagerErrorCode.EventSubscriberLimitReached, exception.ErrorCode);
|
||||
|
||||
foreach (IEventSubscriberLease lease in leases)
|
||||
@@ -356,8 +353,7 @@ public sealed class GatewaySessionTests
|
||||
await gate.WaitAsync(testTimeout);
|
||||
try
|
||||
{
|
||||
IEventSubscriberLease lease = session.AttachEventSubscriber(
|
||||
allowMultipleSubscribers: true, maxSubscribers: Cap);
|
||||
IEventSubscriberLease lease = session.AttachEventSubscriber(maxSubscribers: Cap);
|
||||
leases[index] = lease;
|
||||
Interlocked.Increment(ref successCount);
|
||||
}
|
||||
@@ -411,13 +407,13 @@ public sealed class GatewaySessionTests
|
||||
workerClient,
|
||||
allowMultipleEventSubscribers: true);
|
||||
|
||||
IEventSubscriberLease a = session.AttachEventSubscriber(allowMultipleSubscribers: true, maxSubscribers: Cap);
|
||||
IEventSubscriberLease b = session.AttachEventSubscriber(allowMultipleSubscribers: true, maxSubscribers: Cap);
|
||||
IEventSubscriberLease a = session.AttachEventSubscriber(maxSubscribers: Cap);
|
||||
IEventSubscriberLease b = session.AttachEventSubscriber(maxSubscribers: Cap);
|
||||
Assert.Equal(Cap, session.ActiveEventSubscriberCount);
|
||||
|
||||
// At cap: next attach is rejected.
|
||||
Assert.Throws<SessionManagerException>(
|
||||
() => session.AttachEventSubscriber(allowMultipleSubscribers: true, maxSubscribers: Cap));
|
||||
() => session.AttachEventSubscriber(maxSubscribers: Cap));
|
||||
|
||||
// Dispose one — and dispose it twice. The second dispose must not double-free.
|
||||
a.Dispose();
|
||||
@@ -425,10 +421,10 @@ public sealed class GatewaySessionTests
|
||||
Assert.Equal(1, session.ActiveEventSubscriberCount);
|
||||
|
||||
// Exactly one slot is free, so exactly one fresh attach succeeds.
|
||||
using IEventSubscriberLease c = session.AttachEventSubscriber(allowMultipleSubscribers: true, maxSubscribers: Cap);
|
||||
using IEventSubscriberLease c = session.AttachEventSubscriber(maxSubscribers: Cap);
|
||||
Assert.Equal(Cap, session.ActiveEventSubscriberCount);
|
||||
Assert.Throws<SessionManagerException>(
|
||||
() => session.AttachEventSubscriber(allowMultipleSubscribers: true, maxSubscribers: Cap));
|
||||
() => session.AttachEventSubscriber(maxSubscribers: Cap));
|
||||
|
||||
b.Dispose();
|
||||
|
||||
|
||||
@@ -565,6 +565,9 @@ public sealed class SessionEventDistributorTests
|
||||
}
|
||||
});
|
||||
|
||||
// Guard: ensure the handler actually fired before asserting its observed value.
|
||||
// Without this the test could pass vacuously if the overflow never triggered.
|
||||
Assert.Equal(1, Volatile.Read(ref observedSet));
|
||||
Assert.True(Volatile.Read(ref observedValue),
|
||||
"isOnlySubscriber must be true for a lone external subscriber in single-subscriber mode.");
|
||||
}
|
||||
|
||||
@@ -743,7 +743,7 @@ public sealed class SessionManagerTests
|
||||
GatewaySession session = await manager.OpenSessionAsync(CreateOpenRequest(), "client-1", ownerKeyId: null, CancellationToken.None);
|
||||
DateTimeOffset now = DateTimeOffset.UtcNow;
|
||||
session.ExtendLease(now.AddSeconds(-1));
|
||||
using IDisposable eventSubscriber = session.AttachEventSubscriber(allowMultipleSubscribers: false, maxSubscribers: 1);
|
||||
using IDisposable eventSubscriber = session.AttachEventSubscriber(maxSubscribers: 1);
|
||||
|
||||
int closedCount = await manager.CloseExpiredLeasesAsync(now, CancellationToken.None);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user