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>
144 lines
5.8 KiB
C#
144 lines
5.8 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Logging;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Server;
|
|
|
|
/// <summary>
|
|
/// Walks every registered driver once per heartbeat interval, asks each
|
|
/// <see cref="IHostConnectivityProbe"/>-capable driver for its current
|
|
/// <see cref="HostConnectivityStatus"/> list, and upserts one
|
|
/// <see cref="DriverHostStatus"/> row per (NodeId, DriverInstanceId, HostName) into the
|
|
/// central config DB. Powers the Admin UI's per-host drill-down page (LMX follow-up #7).
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// Polling rather than event-driven: simpler, and matches the cadence the Admin UI
|
|
/// consumes. An event-subscription optimization (push on <c>OnHostStatusChanged</c> for
|
|
/// immediate reflection) is a straightforward follow-up but adds lifecycle complexity
|
|
/// — drivers can be registered after the publisher starts, and subscribing to each
|
|
/// one's event on register + unsubscribing on unregister requires DriverHost to expose
|
|
/// lifecycle events it doesn't today.
|
|
/// </para>
|
|
/// <para>
|
|
/// <see cref="DriverHostStatus.LastSeenUtc"/> advances every heartbeat so the Admin UI
|
|
/// can flag stale rows from a crashed Server process independent of
|
|
/// <see cref="DriverHostStatus.State"/> — a Faulted publisher that stops heartbeating
|
|
/// stays Faulted in the DB but its LastSeenUtc ages out, which is the signal
|
|
/// operators actually want.
|
|
/// </para>
|
|
/// <para>
|
|
/// If the DB is unreachable on a given tick, the publisher logs and moves on — it
|
|
/// does not retry or buffer. The next heartbeat picks up the current-state snapshot,
|
|
/// which is more useful than replaying stale transitions after a long outage.
|
|
/// </para>
|
|
/// </remarks>
|
|
public sealed class HostStatusPublisher(
|
|
DriverHost driverHost,
|
|
NodeOptions nodeOptions,
|
|
IServiceScopeFactory scopeFactory,
|
|
ILogger<HostStatusPublisher> logger) : BackgroundService
|
|
{
|
|
internal static readonly TimeSpan HeartbeatInterval = TimeSpan.FromSeconds(10);
|
|
|
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|
{
|
|
// Wait a short moment at startup so NodeBootstrap's RegisterAsync calls have had a
|
|
// chance to land. First tick runs immediately after so a freshly-started Server
|
|
// surfaces its host topology in the Admin UI without waiting a full interval.
|
|
try { await Task.Delay(TimeSpan.FromSeconds(2), stoppingToken); }
|
|
catch (OperationCanceledException) { return; }
|
|
|
|
while (!stoppingToken.IsCancellationRequested)
|
|
{
|
|
try { await PublishOnceAsync(stoppingToken); }
|
|
catch (OperationCanceledException) { return; }
|
|
catch (Exception ex)
|
|
{
|
|
// Never take down the Server on a publisher failure. Log and continue —
|
|
// stale-row detection on the Admin side will surface the outage.
|
|
logger.LogWarning(ex, "Host-status publisher tick failed — will retry next heartbeat");
|
|
}
|
|
|
|
try { await Task.Delay(HeartbeatInterval, stoppingToken); }
|
|
catch (OperationCanceledException) { return; }
|
|
}
|
|
}
|
|
|
|
internal async Task PublishOnceAsync(CancellationToken ct)
|
|
{
|
|
var driverIds = driverHost.RegisteredDriverIds;
|
|
if (driverIds.Count == 0) return;
|
|
|
|
var now = DateTime.UtcNow;
|
|
using var scope = scopeFactory.CreateScope();
|
|
var db = scope.ServiceProvider.GetRequiredService<OtOpcUaConfigDbContext>();
|
|
|
|
foreach (var driverId in driverIds)
|
|
{
|
|
var driver = driverHost.GetDriver(driverId);
|
|
if (driver is not IHostConnectivityProbe probe) continue;
|
|
|
|
IReadOnlyList<HostConnectivityStatus> statuses;
|
|
try { statuses = probe.GetHostStatuses(); }
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogWarning(ex, "Driver {DriverId} GetHostStatuses threw — skipping this tick", driverId);
|
|
continue;
|
|
}
|
|
|
|
foreach (var status in statuses)
|
|
{
|
|
await UpsertAsync(db, driverId, status, now, ct);
|
|
}
|
|
}
|
|
|
|
await db.SaveChangesAsync(ct);
|
|
}
|
|
|
|
private async Task UpsertAsync(OtOpcUaConfigDbContext db, string driverId,
|
|
HostConnectivityStatus status, DateTime now, CancellationToken ct)
|
|
{
|
|
var mapped = MapState(status.State);
|
|
var existing = await db.DriverHostStatuses.SingleOrDefaultAsync(r =>
|
|
r.NodeId == nodeOptions.NodeId
|
|
&& r.DriverInstanceId == driverId
|
|
&& r.HostName == status.HostName, ct);
|
|
|
|
if (existing is null)
|
|
{
|
|
db.DriverHostStatuses.Add(new DriverHostStatus
|
|
{
|
|
NodeId = nodeOptions.NodeId,
|
|
DriverInstanceId = driverId,
|
|
HostName = status.HostName,
|
|
State = mapped,
|
|
StateChangedUtc = status.LastChangedUtc,
|
|
LastSeenUtc = now,
|
|
});
|
|
return;
|
|
}
|
|
|
|
existing.LastSeenUtc = now;
|
|
if (existing.State != mapped)
|
|
{
|
|
existing.State = mapped;
|
|
existing.StateChangedUtc = status.LastChangedUtc;
|
|
}
|
|
}
|
|
|
|
internal static DriverHostState MapState(HostState state) => state switch
|
|
{
|
|
HostState.Running => DriverHostState.Running,
|
|
HostState.Stopped => DriverHostState.Stopped,
|
|
HostState.Faulted => DriverHostState.Faulted,
|
|
_ => DriverHostState.Unknown,
|
|
};
|
|
}
|