feat(sessions): replay-on-reconnect with ReplayGap sentinel
This commit is contained in:
@@ -433,6 +433,32 @@ public sealed class GatewaySession
|
||||
return lease;
|
||||
}
|
||||
|
||||
// Reconnect/resume variant of StartDistributorAndRegister (Task 12). Snapshots the replay
|
||||
// ring for events newer than afterSequence AND registers the live subscriber atomically
|
||||
// under the distributor's replay lock, so the replay→live handoff has no gap and no
|
||||
// duplicate (see SessionEventDistributor.RegisterWithReplay). The pump is started after
|
||||
// registration, exactly as the fresh-attach path, so the very first subscriber on a
|
||||
// freshly-Ready session still sees the stream from its beginning.
|
||||
private IEventSubscriberLease StartDistributorAndRegisterWithReplay(
|
||||
ulong afterSequence,
|
||||
out IReadOnlyList<MxEvent> replayedEvents,
|
||||
out bool gap,
|
||||
out ulong oldestAvailableSequence,
|
||||
out ulong liveResumeSequence)
|
||||
{
|
||||
SessionEventDistributor distributor = EnsureDistributorCreated(out bool startNow);
|
||||
|
||||
IEventSubscriberLease lease = distributor.RegisterWithReplay(
|
||||
afterSequence,
|
||||
out replayedEvents,
|
||||
out gap,
|
||||
out oldestAvailableSequence,
|
||||
out liveResumeSequence);
|
||||
StartPumpIfRequested(distributor, startNow);
|
||||
|
||||
return lease;
|
||||
}
|
||||
|
||||
// Constructs the distributor exactly once and reports whether THIS caller is the one
|
||||
// that should start the pump (i.e. it observed the unstarted state and claimed the
|
||||
// start). Both the construction and the started-flag flip happen under _syncRoot so two
|
||||
@@ -811,6 +837,75 @@ public sealed class GatewaySession
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reconnect/resume variant of <see cref="AttachEventSubscriber"/> (Task 12). Attaches
|
||||
/// an event subscriber AND atomically snapshots the session replay ring for events newer
|
||||
/// than <paramref name="afterSequence"/>, so a resuming client can replay what it missed
|
||||
/// before live delivery resumes — with no gap and no duplicate across the handoff.
|
||||
/// </summary>
|
||||
/// <param name="maxSubscribers">See <see cref="AttachEventSubscriber"/>.</param>
|
||||
/// <param name="afterSequence">
|
||||
/// The last worker sequence the resuming client already observed. Replay returns events
|
||||
/// strictly newer than this; the caller must filter the live channel to events strictly
|
||||
/// newer than <see cref="EventSubscriberReplayAttachment.LiveResumeSequence"/>.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// The lease plus the replay batch, gap flag, and resume watermarks. See
|
||||
/// <see cref="SessionEventDistributor.RegisterWithReplay"/> for the no-gap/no-duplicate
|
||||
/// guarantee.
|
||||
/// </returns>
|
||||
public EventSubscriberReplayAttachment AttachEventSubscriberWithReplay(int maxSubscribers, ulong afterSequence)
|
||||
{
|
||||
bool allowMultipleSubscribers = _eventStreaming.AllowMultipleEventSubscribers;
|
||||
int effectiveCap = allowMultipleSubscribers ? Math.Max(1, maxSubscribers) : 1;
|
||||
|
||||
lock (_syncRoot)
|
||||
{
|
||||
if (_state != SessionState.Ready || _workerClient?.State != WorkerClientState.Ready)
|
||||
{
|
||||
throw new SessionManagerException(
|
||||
SessionManagerErrorCode.SessionNotReady,
|
||||
$"Session {SessionId} is not ready for event streaming. Current state is {_state}.");
|
||||
}
|
||||
|
||||
if (_activeEventSubscriberCount >= effectiveCap)
|
||||
{
|
||||
throw allowMultipleSubscribers
|
||||
? new SessionManagerException(
|
||||
SessionManagerErrorCode.EventSubscriberLimitReached,
|
||||
$"Session {SessionId} has reached its maximum of {effectiveCap} concurrent event stream subscribers.")
|
||||
: new SessionManagerException(
|
||||
SessionManagerErrorCode.EventSubscriberAlreadyActive,
|
||||
$"Session {SessionId} already has an active event stream subscriber.");
|
||||
}
|
||||
|
||||
_activeEventSubscriberCount++;
|
||||
_detachedAtUtc = null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
IEventSubscriberLease distributorLease = StartDistributorAndRegisterWithReplay(
|
||||
afterSequence,
|
||||
out IReadOnlyList<MxEvent> replayedEvents,
|
||||
out bool gap,
|
||||
out ulong oldestAvailableSequence,
|
||||
out ulong liveResumeSequence);
|
||||
|
||||
return new EventSubscriberReplayAttachment(
|
||||
new EventSubscriberLease(this, distributorLease),
|
||||
replayedEvents,
|
||||
gap,
|
||||
oldestAvailableSequence,
|
||||
liveResumeSequence);
|
||||
}
|
||||
catch
|
||||
{
|
||||
DetachEventSubscriber();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes a worker command synchronously and returns the reply.
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user