a04196d725
The driver-status and resilience stores had no removal path, so a deleted driver stayed on /hosts as a frozen row until restart. Both stores gain RetainOnly, which /hosts calls after each successful config load (never on a failed one). The stores and InProcessBroadcaster now raise their events one subscriber at a time, so a throwing handler cannot starve later subscribers or escape onto the bridge actor. The alert/script-log bridges publish inside their try, and the resilience bridge catches a store failure instead of restarting. Refs #745 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
40 lines
1.7 KiB
C#
40 lines
1.7 KiB
C#
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Hubs;
|
|
|
|
/// <summary>
|
|
/// Raises a multicast delegate one subscriber at a time, so a subscriber that throws cannot stop
|
|
/// delivery to the subscribers after it. The in-process stores and broadcasters raise their events on
|
|
/// the bridge actor's thread; a plain <c>handler?.Invoke(x)</c> stops at the first throwing handler (for
|
|
/// example a Blazor circuit whose renderer is mid-teardown), so the remaining panels miss the update and
|
|
/// the exception escapes into the bridge actor, which restarts it.
|
|
/// </summary>
|
|
internal static class SafeMulticast
|
|
{
|
|
/// <summary>Invokes every subscriber of <paramref name="handler"/> with <paramref name="arg"/>, isolating failures.</summary>
|
|
/// <typeparam name="T">The event payload type.</typeparam>
|
|
/// <param name="handler">The captured multicast delegate; null means no subscribers.</param>
|
|
/// <param name="arg">The payload passed to each subscriber.</param>
|
|
/// <param name="onError">Optional sink for a subscriber's exception; it must not throw.</param>
|
|
/// <returns>The number of subscribers that threw.</returns>
|
|
public static int Invoke<T>(Action<T>? handler, T arg, Action<Exception>? onError = null)
|
|
{
|
|
if (handler is null) return 0;
|
|
|
|
var failures = 0;
|
|
foreach (var subscriber in handler.GetInvocationList())
|
|
{
|
|
try
|
|
{
|
|
((Action<T>)subscriber)(arg);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
failures++;
|
|
try { onError?.Invoke(ex); }
|
|
catch { /* the error sink must never take delivery down with it */ }
|
|
}
|
|
}
|
|
|
|
return failures;
|
|
}
|
|
}
|