fix(sessions): tidy replay filter/comment; zero OldestAvailableSequence when no gap

- EventStreamService: remove dead per-item sequence guard in the replay
  loop (RegisterWithReplay already returns only events > afterSequence)
  and correct the comment that falsely claimed a "per-item constraint
  filter" is applied; the event stream has no per-event constraint
  filtering today.
- SessionEventDistributor.RegisterWithReplay: set oldestAvailableSequence=0
  when gap==false so the implementation matches the documented contract
  (OldestAvailableSequence is meaningful only when Gap is true).
  Update the two RegisterWithReplay tests that asserted the old non-zero
  value in the no-gap path.
- RegisterSubscriber: remove stray blank line at method entry.
- SessionEventDistributorTests: add RegisterWithReplay_AfterDispose_
  ThrowsObjectDisposedException to pin nested-lock disposal behavior.
This commit is contained in:
Joseph Doherty
2026-06-16 07:28:37 -04:00
parent 36ab8d15f1
commit c7a7cd1e5e
3 changed files with 36 additions and 16 deletions
@@ -113,6 +113,27 @@ public sealed class SessionEventDistributorTests
Assert.Throws<ObjectDisposedException>(() => distributor.Register());
}
[Fact]
public async Task RegisterWithReplay_AfterDispose_ThrowsObjectDisposedException()
{
// Pins the nested-lock disposal behavior in RegisterWithReplay: the inner
// _lifecycleLock check must surface ObjectDisposedException even when the outer
// _replayLock snapshot succeeds on a disposed distributor.
Channel<MxEvent> source = Channel.CreateUnbounded<MxEvent>();
SessionEventDistributor distributor = CreateDistributor(source.Reader);
await distributor.StartAsync(CancellationToken.None);
await distributor.DisposeAsync().AsTask().WaitAsync(ReadTimeout);
Assert.Throws<ObjectDisposedException>(() =>
distributor.RegisterWithReplay(
0,
out _,
out _,
out _,
out _));
}
[Fact]
public async Task ReplayBuffer_OverCapacity_EvictsOldestFirst_AndReportsGap()
{
@@ -601,7 +622,8 @@ public sealed class SessionEventDistributorTests
Assert.False(gap);
Assert.Equal(new ulong[] { 3, 4, 5 }, replay.Select(e => e.WorkerSequence));
Assert.Equal(5ul, liveResume);
Assert.Equal(1ul, oldestAvailable);
// OldestAvailableSequence is 0 when gap == false (meaningful only when gap is true).
Assert.Equal(0ul, oldestAvailable);
// A subsequent live event flows to the resumed subscriber's channel.
source.Writer.TryWrite(Event(6));
@@ -669,7 +691,8 @@ public sealed class SessionEventDistributorTests
Assert.False(gap);
Assert.Empty(replay);
Assert.Equal(3ul, liveResume);
Assert.Equal(1ul, oldestAvailable);
// OldestAvailableSequence is 0 when gap == false (meaningful only when gap is true).
Assert.Equal(0ul, oldestAvailable);
source.Writer.TryWrite(Event(4));
MxEvent live = await ReadOneAsync(resume.Reader);