Files
lmxopcua/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/SafeMulticast.cs
T
Joseph Doherty a04196d725 fix(adminui): evict unconfigured drivers from status stores; isolate throwing subscribers (R04 L1, L5)
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
2026-10-22 17:24:04 -04:00

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;
}
}