fix(dashboard): bound EventsHub delivery with one drop-oldest pump per session
Publish fired a group SendAsync per event and returned, so a slow remote viewer accumulated unbounded pending sends plus cloned MxEvents. Hub delivery now goes through a 256-deep DropOldest channel per hub-watched session with a single drain task and time-bounded sends. The registry counts hub viewers separately and raises HubViewersReleased so the pump is torn down with the last remote viewer; in-process-only sessions start no pump.
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace ZB.MOM.WW.MxGateway.Server.Dashboard.Hubs;
|
||||
|
||||
/// <summary>
|
||||
/// Time-bounded SignalR send shared by the dashboard hub publishers. A group or
|
||||
/// broadcast send completes only once every target connection has accepted the
|
||||
/// frame, so one remote viewer with a full transport buffer would otherwise hold
|
||||
/// the publisher's single drain loop for as long as that viewer stays slow.
|
||||
/// </summary>
|
||||
internal static class BoundedHubSend
|
||||
{
|
||||
/// <summary>
|
||||
/// Sends <paramref name="argument"/> as <paramref name="method"/> and waits at most
|
||||
/// <paramref name="timeout"/> for it. The timeout cancels the send's own token, so
|
||||
/// the lifetime manager abandons the pending write to the slow connection, and the
|
||||
/// wait itself is bounded too, so a proxy that ignores the token cannot stall the
|
||||
/// caller either.
|
||||
/// </summary>
|
||||
/// <param name="proxy">Target client proxy (a group or all connections).</param>
|
||||
/// <param name="method">Client method name.</param>
|
||||
/// <param name="argument">The single message argument.</param>
|
||||
/// <param name="timeout">Upper bound on the send.</param>
|
||||
/// <param name="stoppingToken">Host shutdown token; its cancellation propagates.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> when the send completed in time; <see langword="false"/>
|
||||
/// when it was abandoned at the timeout.
|
||||
/// </returns>
|
||||
/// <exception cref="OperationCanceledException"><paramref name="stoppingToken"/> was cancelled.</exception>
|
||||
public static async Task<bool> SendAsync(
|
||||
IClientProxy proxy,
|
||||
string method,
|
||||
object argument,
|
||||
TimeSpan timeout,
|
||||
CancellationToken stoppingToken)
|
||||
{
|
||||
using CancellationTokenSource bounded = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
|
||||
bounded.CancelAfter(timeout);
|
||||
|
||||
try
|
||||
{
|
||||
await proxy.SendAsync(method, argument, bounded.Token)
|
||||
.WaitAsync(bounded.Token)
|
||||
.ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
catch (OperationCanceledException) when (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user