Files
lmxopcua/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Health/HostStatusAggregator.cs
Joseph Doherty a25593a9c6 chore: organize solution into module folders (Core/Server/Drivers/Client/Tooling)
Group all 69 projects into category subfolders under src/ and tests/ so the
Rider Solution Explorer mirrors the module structure. Folders: Core, Server,
Drivers (with a nested Driver CLIs subfolder), Client, Tooling.

- Move every project folder on disk with git mv (history preserved as renames).
- Recompute relative paths in 57 .csproj files: cross-category ProjectReferences,
  the lib/ HintPath+None refs in Driver.Historian.Wonderware, and the external
  mxaccessgw refs in Driver.Galaxy and its test project.
- Rebuild ZB.MOM.WW.OtOpcUa.slnx with nested solution folders.
- Re-prefix project paths in functional scripts (e2e, compliance, smoke SQL,
  integration, install).

Build green (0 errors); unit tests pass. Docs left for a separate pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 01:55:28 -04:00

99 lines
3.8 KiB
C#

using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Health;
/// <summary>
/// Pure-logic merger for the per-host connectivity entries that
/// <see cref="IHostConnectivityProbe"/> surfaces. Holds the current set of host
/// statuses (one synthetic top-level transport entry plus one entry per
/// <c>$WinPlatform</c>/<c>$AppEngine</c> probe) and emits
/// <see cref="OnHostStatusChanged"/> only when an upsert actually changes a host's
/// <see cref="HostState"/> — re-asserting the same state is a no-op so a stable
/// <c>ScanState=Running</c> burst doesn't fan out duplicate transitions.
/// </summary>
/// <remarks>
/// This class owns the de-dup + diff logic that lived in
/// <c>GalaxyProxyDriver.OnHostConnectivityUpdate</c> in v1. The watcher
/// (<see cref="PerPlatformProbeWatcher"/>) and the transport forwarder
/// (<see cref="HostConnectivityForwarder"/>) both feed this aggregator; the
/// <see cref="GalaxyDriver"/> consumes <see cref="Snapshot"/> from
/// <c>IHostConnectivityProbe.GetHostStatuses()</c> and re-raises
/// <see cref="OnHostStatusChanged"/> as the driver-level event (wired in PR 4.W).
/// </remarks>
public sealed class HostStatusAggregator
{
private readonly object _lock = new();
private readonly Dictionary<string, HostConnectivityStatus> _byHost =
new(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Fires when an <see cref="Update"/> call either introduces a new host or
/// transitions an existing host's <see cref="HostState"/>. Handlers run
/// outside the internal lock so they can safely re-enter the aggregator
/// (e.g. the driver re-broadcasting through <c>IHostConnectivityProbe</c>).
/// </summary>
public event EventHandler<HostStatusChangedEventArgs>? OnHostStatusChanged;
/// <summary>
/// Snapshot the current host set. Suitable as the body of
/// <c>IHostConnectivityProbe.GetHostStatuses()</c>.
/// </summary>
public IReadOnlyList<HostConnectivityStatus> Snapshot()
{
lock (_lock)
{
return [.. _byHost.Values];
}
}
/// <summary>
/// Upsert the supplied status by <see cref="HostConnectivityStatus.HostName"/>.
/// Raises <see cref="OnHostStatusChanged"/> when the host is newly tracked
/// (previous state reported as <see cref="HostState.Unknown"/>) or when its
/// state value differs from the last cached entry. Re-asserting the same
/// state is silent.
/// </summary>
public void Update(HostConnectivityStatus status)
{
ArgumentNullException.ThrowIfNull(status);
HostState previous;
bool changed;
lock (_lock)
{
if (_byHost.TryGetValue(status.HostName, out var existing))
{
previous = existing.State;
changed = existing.State != status.State;
}
else
{
previous = HostState.Unknown;
changed = true;
}
_byHost[status.HostName] = status;
}
if (changed)
{
OnHostStatusChanged?.Invoke(this,
new HostStatusChangedEventArgs(status.HostName, previous, status.State));
}
}
/// <summary>
/// Drop a host entirely (e.g. after a redeploy removes a Platform). No event
/// is fired — observers only react to live transitions, not topology
/// reductions. Returns <c>true</c> when the host was tracked.
/// </summary>
public bool Remove(string hostName)
{
ArgumentException.ThrowIfNullOrWhiteSpace(hostName);
lock (_lock)
{
return _byHost.Remove(hostName);
}
}
}