perf(comms): alarms-only seed, capped buffers, at-least-once audit pull
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user