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
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Drivers;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Hubs;
|
||||
@@ -10,6 +11,12 @@ namespace ZB.MOM.WW.OtOpcUa.AdminUI.Hubs;
|
||||
public sealed class InMemoryDriverStatusSnapshotStore : IDriverStatusSnapshotStore
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, DriverHealthChanged> _byInstance = new();
|
||||
private readonly ILogger<InMemoryDriverStatusSnapshotStore>? _logger;
|
||||
|
||||
/// <summary>Creates the store.</summary>
|
||||
/// <param name="logger">Optional logger for a subscriber that throws during a raise.</param>
|
||||
public InMemoryDriverStatusSnapshotStore(ILogger<InMemoryDriverStatusSnapshotStore>? logger = null)
|
||||
=> _logger = logger;
|
||||
|
||||
/// <summary>Raised whenever a driver status snapshot is stored or updated.</summary>
|
||||
public event Action<DriverHealthChanged>? SnapshotChanged;
|
||||
@@ -18,8 +25,22 @@ public sealed class InMemoryDriverStatusSnapshotStore : IDriverStatusSnapshotSto
|
||||
public void Upsert(DriverHealthChanged snapshot)
|
||||
{
|
||||
_byInstance[snapshot.DriverInstanceId] = snapshot;
|
||||
// Capture-then-invoke so a concurrent unsubscribe can't null the delegate mid-raise.
|
||||
SnapshotChanged?.Invoke(snapshot);
|
||||
// Capture-then-invoke so a concurrent unsubscribe can't null the delegate mid-raise; one
|
||||
// subscriber at a time so a throwing one cannot starve the rest (or escape onto the bridge actor).
|
||||
SafeMulticast.Invoke(SnapshotChanged, snapshot,
|
||||
ex => _logger?.LogDebug(ex, "Driver status subscriber threw for {DriverInstanceId}", snapshot.DriverInstanceId));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int RetainOnly(IReadOnlySet<string> configuredInstanceIds)
|
||||
{
|
||||
var removed = 0;
|
||||
foreach (var id in _byInstance.Keys)
|
||||
{
|
||||
if (!configuredInstanceIds.Contains(id) && _byInstance.TryRemove(id, out _)) removed++;
|
||||
}
|
||||
|
||||
return removed;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
Reference in New Issue
Block a user