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>
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Admin.Services;
|
||||
|
||||
/// <summary>
|
||||
/// One row per <see cref="DriverHostStatus"/> record, enriched with the owning
|
||||
/// <c>ClusterNode.ClusterId</c> (left-join) + the per-<c>(DriverInstanceId, HostName)</c>
|
||||
/// <see cref="DriverInstanceResilienceStatus"/> counters (also left-join) so the Admin
|
||||
/// <c>/hosts</c> page renders the resilience surface inline with host state.
|
||||
/// </summary>
|
||||
public sealed record HostStatusRow(
|
||||
string NodeId,
|
||||
string? ClusterId,
|
||||
string DriverInstanceId,
|
||||
string HostName,
|
||||
DriverHostState State,
|
||||
DateTime StateChangedUtc,
|
||||
DateTime LastSeenUtc,
|
||||
string? Detail,
|
||||
int ConsecutiveFailures,
|
||||
DateTime? LastCircuitBreakerOpenUtc,
|
||||
int CurrentBulkheadDepth,
|
||||
DateTime? LastRecycleUtc);
|
||||
|
||||
/// <summary>
|
||||
/// Read-side service for the Admin UI's per-host drill-down. Loads
|
||||
/// <see cref="DriverHostStatus"/> rows (written by the Server process's
|
||||
/// <c>HostStatusPublisher</c>) and left-joins <c>ClusterNode</c> so each row knows which
|
||||
/// cluster it belongs to — the Admin UI groups by cluster for the fleet-wide view.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The publisher heartbeat is 10s (<c>HostStatusPublisher.HeartbeatInterval</c>). The
|
||||
/// Admin page also polls every ~10s and treats rows with <c>LastSeenUtc</c> older than
|
||||
/// <c>StaleThreshold</c> (30s) as stale — covers a missed heartbeat tolerance plus
|
||||
/// a generous buffer for clock skew and publisher GC pauses.
|
||||
/// </remarks>
|
||||
public sealed class HostStatusService(OtOpcUaConfigDbContext db)
|
||||
{
|
||||
public static readonly TimeSpan StaleThreshold = TimeSpan.FromSeconds(30);
|
||||
|
||||
/// <summary>Consecutive-failure threshold at which <see cref="IsFlagged"/> returns <c>true</c>
|
||||
/// so the Admin UI can paint a red badge. Matches Phase 6.1 decision #143's conservative
|
||||
/// half-of-breaker-threshold convention — flags before the breaker actually opens.</summary>
|
||||
public const int FailureFlagThreshold = 3;
|
||||
|
||||
public async Task<IReadOnlyList<HostStatusRow>> ListAsync(CancellationToken ct = default)
|
||||
{
|
||||
// Two LEFT JOINs:
|
||||
// 1. ClusterNodes on NodeId — row persists even when its owning ClusterNode row
|
||||
// hasn't been created yet (first-boot bootstrap case).
|
||||
// 2. DriverInstanceResilienceStatuses on (DriverInstanceId, HostName) — resilience
|
||||
// counters haven't been sampled yet for brand-new hosts, so a missing row means
|
||||
// zero failures + never-opened breaker.
|
||||
var rows = await (from s in db.DriverHostStatuses.AsNoTracking()
|
||||
join n in db.ClusterNodes.AsNoTracking()
|
||||
on s.NodeId equals n.NodeId into nodeJoin
|
||||
from n in nodeJoin.DefaultIfEmpty()
|
||||
join r in db.DriverInstanceResilienceStatuses.AsNoTracking()
|
||||
on new { s.DriverInstanceId, s.HostName } equals new { r.DriverInstanceId, r.HostName } into resilJoin
|
||||
from r in resilJoin.DefaultIfEmpty()
|
||||
orderby s.NodeId, s.DriverInstanceId, s.HostName
|
||||
select new HostStatusRow(
|
||||
s.NodeId,
|
||||
n != null ? n.ClusterId : null,
|
||||
s.DriverInstanceId,
|
||||
s.HostName,
|
||||
s.State,
|
||||
s.StateChangedUtc,
|
||||
s.LastSeenUtc,
|
||||
s.Detail,
|
||||
r != null ? r.ConsecutiveFailures : 0,
|
||||
r != null ? r.LastCircuitBreakerOpenUtc : null,
|
||||
r != null ? r.CurrentBulkheadDepth : 0,
|
||||
r != null ? r.LastRecycleUtc : null)).ToListAsync(ct);
|
||||
return rows;
|
||||
}
|
||||
|
||||
public static bool IsStale(HostStatusRow row) =>
|
||||
DateTime.UtcNow - row.LastSeenUtc > StaleThreshold;
|
||||
|
||||
/// <summary>
|
||||
/// Red-badge predicate — <c>true</c> when the host has accumulated enough consecutive
|
||||
/// failures that an operator should take notice before the breaker trips.
|
||||
/// </summary>
|
||||
public static bool IsFlagged(HostStatusRow row) =>
|
||||
row.ConsecutiveFailures >= FailureFlagThreshold;
|
||||
}
|
||||
Reference in New Issue
Block a user