perf(events): copy-on-write subscriber snapshot in fan-out pump

This commit is contained in:
Joseph Doherty
2026-08-15 12:21:42 -04:00
parent 75e3dc2794
commit e04b1c9199
2 changed files with 155 additions and 26 deletions
@@ -1057,6 +1057,63 @@ public sealed class SessionEventDistributorTests
Assert.False(lateCts.IsCancellationRequested);
}
/// <summary>
/// Guards the copy-on-write fan-out snapshot: registrations and unregistrations churn on
/// another thread while the pump is actively fanning events, and the stable subscriber
/// must still receive every event exactly once and in order. The pump captures the
/// subscriber array once per event instead of enumerating the dictionary, so a mutation
/// racing the fan-out must never drop, duplicate, or reorder an event for a subscriber
/// registered throughout — nor leave the array and the dictionary disagreeing on the
/// subscriber count once the churn stops.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task RegistrationChurnDuringFanOut_StableSubscriberStillReceivesEveryEventInOrder()
{
// Below the 64-event per-subscriber queue capacity, so the stable subscriber cannot
// overflow and be disconnected while the writes race the churn — the assertion stays
// deterministic no matter how the threads interleave.
const int EventCount = 50;
Channel<MxEvent> source = Channel.CreateUnbounded<MxEvent>();
await using SessionEventDistributor distributor = CreateDistributor(source.Reader);
await distributor.StartAsync(CancellationToken.None);
using IEventSubscriberLease stable = distributor.Register();
using CancellationTokenSource churnCts = new();
Task churn = Task.Run(async () =>
{
while (!churnCts.IsCancellationRequested)
{
// Register then immediately unregister: every iteration rebuilds the fan-out
// snapshot twice, maximizing the chance of landing inside a fan-out pass.
distributor.Register().Dispose();
await Task.Yield();
}
});
for (ulong sequence = 1; sequence <= EventCount; sequence++)
{
source.Writer.TryWrite(Event(sequence));
}
List<ulong> received = [];
for (int i = 0; i < EventCount; i++)
{
received.Add((await ReadOneAsync(stable.Reader)).WorkerSequence);
}
await churnCts.CancelAsync();
await churn.WaitAsync(ReadTimeout);
Assert.Equal(Enumerable.Range(1, EventCount).Select(sequence => (ulong)sequence), received);
// Only the stable subscriber remains: the snapshot the count is read from tracked every
// add and remove the churn performed.
Assert.Equal(1, distributor.SubscriberCount);
}
private static SessionEventDistributor CreateDistributor(ChannelReader<MxEvent> source)
=> CreateDistributor(source, replayBufferCapacity: 1024, replayRetentionSeconds: 300);