refactor: rename ScadaLink → ZB.MOM.WW.ScadaBridge (code + projects + namespaces)

Solution + 23 src projects + 26 test projects renamed; folders, csproj,
namespaces, and ScadaLinkDbContext/ScadaBridgeDbContext class updated.
ActorSystem "scadalink" → "scadabridge", Akka seed-node URLs migrated.
SQL roles/logins, LDAP domains, CLI command name, and CLI config dir
(~/.scadalink → ~/.scadabridge) also renamed.

Build green; 5 Host.Tests fail awaiting SQL login rename in next commit.
Pre-existing StaleTagMonitor timing flakes unchanged.

Rename script committed at tools/rename-to-scadabridge.sh.
This commit is contained in:
Joseph Doherty
2026-05-28 09:37:45 -04:00
parent 6d87ee3c3b
commit 7b0b9c7365
1531 changed files with 11180 additions and 11054 deletions
@@ -0,0 +1,57 @@
using Akka.Cluster;
using ZB.MOM.WW.ScadaBridge.Host.Actors;
using ZB.MOM.WW.ScadaBridge.InboundAPI;
namespace ZB.MOM.WW.ScadaBridge.Host.Health;
/// <summary>
/// InboundAPI-008 / InboundAPI-022: production implementation of
/// <see cref="IActiveNodeGate"/> backed by the running Akka.NET cluster.
///
/// The inbound API is "Central cluster only (active node)" — a standby central
/// node must not execute method scripts or <c>Route.To()</c> calls. This gate
/// mirrors the leadership check in <see cref="ActiveNodeHealthCheck"/> (the
/// node is the cluster leader, <see cref="MemberStatus.Up"/>), so
/// <see cref="InboundApiEndpointFilter"/> can return HTTP 503 on a standby.
///
/// Registered only in the Central-role branch of <c>Program.cs</c>. The gate
/// is resolved per request from <c>HttpContext.RequestServices</c>; while the
/// <c>AkkaHostedService</c> is still warming up (<c>ActorSystem == null</c>)
/// or the node has not yet reached <see cref="MemberStatus.Up"/>, this
/// implementation reports <c>IsActiveNode == false</c> — the safe-by-default
/// answer matching the standby case.
/// </summary>
public sealed class ActiveNodeGate : IActiveNodeGate
{
private readonly AkkaHostedService _akkaService;
/// <summary>Initializes a new <see cref="ActiveNodeGate"/> bound to the given Akka hosted service.</summary>
/// <param name="akkaService">The Akka hosted service exposing the cluster's <see cref="Akka.Actor.ActorSystem"/>.</param>
public ActiveNodeGate(AkkaHostedService akkaService)
{
_akkaService = akkaService;
}
/// <summary>
/// <c>true</c> only when this node has joined the cluster (<see cref="MemberStatus.Up"/>)
/// AND is the current cluster leader; <c>false</c> in every other state
/// (actor system not yet started, node still joining, node is a standby).
/// </summary>
public bool IsActiveNode
{
get
{
var system = _akkaService.ActorSystem;
if (system == null)
return false;
var cluster = Cluster.Get(system);
var self = cluster.SelfMember;
if (self.Status != MemberStatus.Up)
return false;
var leader = cluster.State.Leader;
return leader != null && leader == self.Address;
}
}
}
@@ -0,0 +1,45 @@
using Akka.Cluster;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using ZB.MOM.WW.ScadaBridge.Host.Actors;
namespace ZB.MOM.WW.ScadaBridge.Host.Health;
/// <summary>
/// Health check that returns healthy only if this node is the active (leader) node
/// in the Akka.NET cluster. Used by Traefik to route traffic to the active node.
/// </summary>
public class ActiveNodeHealthCheck : IHealthCheck
{
private readonly AkkaHostedService _akkaService;
/// <summary>Initializes a new <see cref="ActiveNodeHealthCheck"/> with the given Akka hosted service.</summary>
/// <param name="akkaService">The Akka hosted service providing access to the actor system and cluster state.</param>
public ActiveNodeHealthCheck(AkkaHostedService akkaService)
{
_akkaService = akkaService;
}
/// <summary>Returns healthy if this node is the cluster leader (active node); otherwise returns unhealthy.</summary>
/// <param name="context">Health check context providing registration details.</param>
/// <param name="cancellationToken">Cancellation token.</param>
public Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
var system = _akkaService.ActorSystem;
if (system == null)
return Task.FromResult(HealthCheckResult.Unhealthy("ActorSystem not yet available."));
var cluster = Cluster.Get(system);
var self = cluster.SelfMember;
if (self.Status != MemberStatus.Up)
return Task.FromResult(HealthCheckResult.Unhealthy($"Node not Up (status: {self.Status})."));
var leader = cluster.State.Leader;
if (leader != null && leader == self.Address)
return Task.FromResult(HealthCheckResult.Healthy("Active node (cluster leader)."));
return Task.FromResult(HealthCheckResult.Unhealthy("Standby node (not cluster leader)."));
}
}
@@ -0,0 +1,52 @@
using Akka.Cluster;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using ZB.MOM.WW.ScadaBridge.Host.Actors;
namespace ZB.MOM.WW.ScadaBridge.Host.Health;
/// <summary>
/// Health check that verifies this node is an active member of the Akka.NET cluster.
/// Returns healthy only if the node's self-member status is Up or Joining.
/// </summary>
public class AkkaClusterHealthCheck : IHealthCheck
{
private readonly AkkaHostedService _akkaService;
/// <summary>
/// Initializes the health check with the Akka hosted service.
/// </summary>
/// <param name="akkaService">The hosted service providing access to the Akka actor system.</param>
public AkkaClusterHealthCheck(AkkaHostedService akkaService)
{
_akkaService = akkaService;
}
/// <summary>
/// Checks that this node is an active member of the Akka.NET cluster.
/// </summary>
/// <param name="context">Health check context.</param>
/// <param name="cancellationToken">Cancellation token.</param>
public Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
var system = _akkaService.ActorSystem;
if (system == null)
return Task.FromResult(HealthCheckResult.Degraded("ActorSystem not yet available."));
var cluster = Cluster.Get(system);
var status = cluster.SelfMember.Status;
var result = status switch
{
MemberStatus.Up or MemberStatus.Joining =>
HealthCheckResult.Healthy($"Akka cluster member status: {status}"),
MemberStatus.Leaving or MemberStatus.Exiting =>
HealthCheckResult.Degraded($"Akka cluster member status: {status}"),
_ =>
HealthCheckResult.Unhealthy($"Akka cluster member status: {status}")
};
return Task.FromResult(result);
}
}
@@ -0,0 +1,82 @@
using Akka.Actor;
using Akka.Cluster;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Health;
using ZB.MOM.WW.ScadaBridge.HealthMonitoring;
using ZB.MOM.WW.ScadaBridge.Host.Actors;
namespace ZB.MOM.WW.ScadaBridge.Host.Health;
/// <summary>
/// Provides cluster node statuses from Akka.NET cluster membership for health reporting.
/// </summary>
public class AkkaClusterNodeProvider : IClusterNodeProvider
{
private readonly AkkaHostedService _akkaService;
private readonly string _siteRole;
/// <summary>
/// Initializes a new <see cref="AkkaClusterNodeProvider"/>.
/// </summary>
/// <param name="akkaService">The Akka hosted service providing access to the actor system.</param>
/// <param name="siteRole">The Akka cluster role used to filter relevant member nodes.</param>
public AkkaClusterNodeProvider(AkkaHostedService akkaService, string siteRole)
{
_akkaService = akkaService;
_siteRole = siteRole;
}
/// <inheritdoc />
public bool SelfIsPrimary
{
get
{
var system = _akkaService.ActorSystem;
if (system == null) return false;
var cluster = Cluster.Get(system);
if (cluster.SelfMember.Status != MemberStatus.Up) return false;
var leader = cluster.State.Leader;
return leader != null && leader.Equals(cluster.SelfAddress);
}
}
/// <inheritdoc />
public IReadOnlyList<NodeStatus> GetClusterNodes()
{
var system = _akkaService.ActorSystem;
if (system == null) return [];
var cluster = Cluster.Get(system);
var selfAddress = cluster.SelfAddress;
var leader = cluster.State.Leader;
var nodes = new List<NodeStatus>();
foreach (var member in cluster.State.Members)
{
if (!member.HasRole(_siteRole))
continue;
var hostname = member.Address.Host ?? member.Address.ToString();
var isOnline = member.Status == MemberStatus.Up;
var isLeader = member.Address.Equals(leader);
var role = isLeader ? "Primary" : "Standby";
nodes.Add(new NodeStatus(hostname, isOnline, role));
}
// If we have unreachable members, add them as offline
foreach (var unreachable in cluster.State.Unreachable)
{
if (!unreachable.HasRole(_siteRole))
continue;
// Don't duplicate if already in members list
if (nodes.Any(n => n.Hostname == (unreachable.Address.Host ?? unreachable.Address.ToString())))
continue;
var hostname = unreachable.Address.Host ?? unreachable.Address.ToString();
nodes.Add(new NodeStatus(hostname, false, "Standby"));
}
return nodes;
}
}
@@ -0,0 +1,43 @@
using Microsoft.Extensions.Diagnostics.HealthChecks;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase;
namespace ZB.MOM.WW.ScadaBridge.Host.Health;
/// <summary>
/// Health check that verifies database connectivity for Central nodes.
/// </summary>
public class DatabaseHealthCheck : IHealthCheck
{
private readonly ScadaBridgeDbContext _dbContext;
/// <summary>
/// Initializes a new <see cref="DatabaseHealthCheck"/>.
/// </summary>
/// <param name="dbContext">The EF Core database context used to test connectivity.</param>
public DatabaseHealthCheck(ScadaBridgeDbContext dbContext)
{
_dbContext = dbContext;
}
/// <summary>
/// Checks database connectivity by attempting to open a connection.
/// </summary>
/// <param name="context">Health check context providing failure status information.</param>
/// <param name="cancellationToken">Cancellation token for the check.</param>
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
try
{
var canConnect = await _dbContext.Database.CanConnectAsync(cancellationToken);
return canConnect
? HealthCheckResult.Healthy("Database connection is available.")
: HealthCheckResult.Unhealthy("Database connection failed.");
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy("Database connection failed.", ex);
}
}
}