fix(mesh-phase5): telemetry hub — plain Dictionary under gate, concurrency stress test, priming/eviction notes
Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
This commit is contained in:
@@ -75,6 +75,13 @@ public interface ITelemetryLocalHub
|
||||
/// <see cref="TelemetryItem.Health"/>, then all cached <see cref="TelemetryItem.Resilience"/>),
|
||||
/// then live deltas — with no delta lost across the attach boundary. Dispose to detach.
|
||||
/// </summary>
|
||||
/// <param name="boundedCapacity">Per-subscriber channel capacity (must be positive).</param>
|
||||
/// <param name="boundedCapacity">
|
||||
/// Per-subscriber channel capacity (must be positive). The snapshot prelude is written into this
|
||||
/// same bounded/DropOldest channel before <see cref="Subscribe"/> returns, so
|
||||
/// <paramref name="boundedCapacity"/> should comfortably exceed this node's live driver-instance +
|
||||
/// (instance, host) count — otherwise the earliest cached snapshots are silently evicted during
|
||||
/// priming before the reader ever drains them. The node-local cache holds only THIS node's
|
||||
/// instances (a handful), so a normal large capacity leaves ample headroom.
|
||||
/// </param>
|
||||
ITelemetrySubscription Subscribe(int boundedCapacity);
|
||||
}
|
||||
|
||||
@@ -28,10 +28,18 @@ namespace ZB.MOM.WW.OtOpcUa.Runtime.Telemetry;
|
||||
public sealed class TelemetryLocalHub : ITelemetryLocalHub
|
||||
{
|
||||
private readonly object _gate = new();
|
||||
private readonly ConcurrentDictionary<Guid, Channel<TelemetryItem>> _subscribers = new();
|
||||
|
||||
// Plain Dictionary, NOT ConcurrentDictionary: every access is already under _gate, and
|
||||
// ConcurrentDictionary.Values would materialize a fresh List (and take its own internal lock) on
|
||||
// every Emit — the hot node-actor-thread path. Under the gate a plain Dictionary is safe and
|
||||
// allocation-free to iterate.
|
||||
private readonly Dictionary<Guid, Channel<TelemetryItem>> _subscribers = new();
|
||||
|
||||
// Snapshot last-value caches. Only ever mutated under _gate (kept ConcurrentDictionary so a
|
||||
// Subscribe reading them under the gate is trivially safe even against any future lock-free read).
|
||||
// TODO(mesh-phase5+): evict snapshot cache entries on driver-instance removal (needs a
|
||||
// driver-lifecycle hook); pre-production, accepted for now. Until then a decommissioned driver
|
||||
// instance's last-known Health/Resilience state replays to new subscribers forever.
|
||||
private readonly ConcurrentDictionary<string, TelemetryItem.Health> _healthCache = new();
|
||||
private readonly ConcurrentDictionary<(string InstanceId, string HostName), TelemetryItem.Resilience> _resilienceCache = new();
|
||||
|
||||
@@ -91,7 +99,7 @@ public sealed class TelemetryLocalHub : ITelemetryLocalHub
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_subscribers.TryRemove(id, out var channel))
|
||||
if (_subscribers.Remove(id, out var channel))
|
||||
channel.Writer.TryComplete();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user