perf(comms): alarms-only seed, capped buffers, at-least-once audit pull

This commit is contained in:
Joseph Doherty
2026-08-14 21:10:19 -04:00
parent 1040dc0fcc
commit 2ce0ad7ed1
30 changed files with 1941 additions and 482 deletions
@@ -245,13 +245,21 @@ public class SiteStreamGrpcClient : IAsyncDisposable, IDisposable
/// <paramref name="onError"/>; see <see cref="ConsumeStreamAsync"/>.
/// </param>
/// <param name="ct">Cancellation token to stop the subscription.</param>
/// <param name="onConnected">
/// Optional callback invoked once when the site has ACCEPTED the subscription (response
/// headers received — the site writes them as soon as its relay actor is subscribed, so
/// no event can be missed after this point). The per-site aggregator uses it to run
/// exactly one re-seed per successful (re)connect instead of one per reconnect attempt.
/// Never invoked more than once per call, and never after <paramref name="onError"/>.
/// </param>
/// <returns>A task that represents the asynchronous operation.</returns>
public virtual async Task SubscribeSiteAsync(
string correlationId,
Action<AlarmStateChanged> onAlarmEvent,
Action<Exception> onError,
Action onCompleted,
CancellationToken ct)
CancellationToken ct,
Action? onConnected = null)
{
if (_client is null)
throw new InvalidOperationException("Cannot subscribe on a test-only client.");
@@ -275,7 +283,8 @@ public class SiteStreamGrpcClient : IAsyncDisposable, IDisposable
onAlarmEvent(alarm);
},
onError,
onCompleted);
onCompleted,
onConnected);
}
/// <summary>
@@ -301,6 +310,13 @@ public class SiteStreamGrpcClient : IAsyncDisposable, IDisposable
/// <param name="onEvent">Invoked per wire event.</param>
/// <param name="onError">Invoked once if the stream faulted.</param>
/// <param name="onCompleted">Invoked once if the server ended the stream with OK.</param>
/// <param name="onConnected">
/// Optional; invoked once when the server's response headers arrive — i.e. the site has
/// accepted the subscription and its relay actor is attached. Bounded by
/// <see cref="ConnectedHeaderTimeout"/> so a peer that defers headers (a pre-WP2.3 site,
/// which only flushes them with its first event) still reports connected instead of
/// leaving the caller waiting for a signal that may never come on a quiet site.
/// </param>
/// <returns>A task that completes when the stream has ended and its outcome been reported.</returns>
internal async Task ConsumeStreamAsync(
string correlationId,
@@ -308,13 +324,20 @@ public class SiteStreamGrpcClient : IAsyncDisposable, IDisposable
Func<AsyncServerStreamingCall<SiteStreamEvent>> openCall,
Action<SiteStreamEvent> onEvent,
Action<Exception> onError,
Action onCompleted)
Action onCompleted,
Action? onConnected = null)
{
var completedGracefully = false;
try
{
using (var call = openCall())
{
if (onConnected is not null)
{
await AwaitHeadersAsync(call, cts.Token).ConfigureAwait(false);
onConnected();
}
await foreach (var evt in call.ResponseStream.ReadAllAsync(cts.Token))
{
onEvent(evt);
@@ -348,6 +371,38 @@ public class SiteStreamGrpcClient : IAsyncDisposable, IDisposable
onCompleted();
}
/// <summary>
/// How long to wait for response headers before treating the stream as connected anyway.
/// A peer that only flushes headers with its first message would otherwise hold the
/// connected signal — and with it the aggregator's re-seed — for as long as the site
/// happens to be quiet.
/// </summary>
internal static TimeSpan ConnectedHeaderTimeout { get; set; } = TimeSpan.FromSeconds(10);
/// <summary>
/// Awaits the call's response headers, bounded by <see cref="ConnectedHeaderTimeout"/>.
/// A fault propagates (the caller reports it through <c>onError</c> like any other stream
/// fault); a timeout returns normally. On timeout the abandoned headers task is observed
/// so a later fault on it can never surface as an unobserved task exception.
/// </summary>
private static async Task AwaitHeadersAsync(
AsyncServerStreamingCall<SiteStreamEvent> call, CancellationToken ct)
{
var headers = call.ResponseHeadersAsync;
try
{
await headers.WaitAsync(ConnectedHeaderTimeout, ct).ConfigureAwait(false);
}
catch (TimeoutException)
{
_ = headers.ContinueWith(
t => _ = t.Exception,
CancellationToken.None,
TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default);
}
}
/// <summary>
/// Cancels an active subscription by correlation ID.
/// </summary>