f1db002b87
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.
53 lines
2.2 KiB
C#
53 lines
2.2 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|