using System.Runtime.CompilerServices; using Akka.Actor; using Akka.Cluster; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; [assembly: InternalsVisibleTo("ZB.MOM.WW.Health.Akka.Tests")] namespace ZB.MOM.WW.Health.Akka; /// /// Pure decision function for the active / leader probe, factored out of /// so the role-less and role-filtered matrices are exhaustively /// table-testable without forming a real cluster. /// internal static class ActiveNodeDecision { /// /// Maps the resolved cluster facts to a . /// /// Whether the local node's member status is Up. /// /// Whether the local node is the leader: the cluster leader in role-less mode, or the /// role-singleton leader in role-filtered mode. /// /// /// Whether the local node carries . Ignored when /// is null. /// /// /// The role to scope the check to, or null for the role-less (whole-cluster-leader) mode. /// /// /// Role-less: Healthy iff the node is Up and the cluster leader, otherwise Unhealthy. /// Role-filtered: Healthy when the node lacks the role (probe irrelevant) or carries the role and /// is the role-singleton leader; Degraded when it carries the role but is not the leader. /// public static HealthStatus Evaluate(bool selfUp, bool isLeader, bool hasRole, string? requiredRole) { if (requiredRole is null) return selfUp && isLeader ? HealthStatus.Healthy : HealthStatus.Unhealthy; if (!hasRole) return HealthStatus.Healthy; return isLeader ? HealthStatus.Healthy : HealthStatus.Degraded; } } /// /// Health check that reports whether this node is the designated active / leader node. /// An optional role scopes the check to nodes carrying that role. Register to the /// tag. /// /// /// The is resolved lazily from the service provider. If it is not yet /// available — e.g. during startup before Akka is initialised — the check returns /// rather than throwing, so it is startup-safe. /// public sealed class ActiveNodeHealthCheck : IHealthCheck { private readonly IServiceProvider _serviceProvider; private readonly string? _role; /// /// Role-less constructor: Healthy when the node is Up and the cluster leader /// (ScadaBridge ActiveNode pattern); Unhealthy otherwise. Degraded when the ActorSystem / /// cluster is not yet ready. /// /// /// The application service provider. The is resolved lazily so the /// check is startup-safe: if no is registered yet the result is Degraded. /// public ActiveNodeHealthCheck(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); _role = null; } /// /// Role-filtered constructor: Healthy when the node lacks or carries it /// and is the role-singleton leader; Degraded when it carries the role but is not the leader /// (OtOpcUa AdminRoleLeader pattern). Degraded when the ActorSystem / cluster is not yet ready. /// /// /// The application service provider. The is resolved lazily so the /// check is startup-safe: if no is registered yet the result is Degraded. /// /// The Akka cluster role to scope the check to. public ActiveNodeHealthCheck(IServiceProvider serviceProvider, string role) { _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); ArgumentException.ThrowIfNullOrWhiteSpace(role); _role = role; } /// public Task CheckHealthAsync( HealthCheckContext context, CancellationToken cancellationToken = default) { var system = _serviceProvider.GetService(); if (system is null) return Task.FromResult(HealthCheckResult.Degraded("ActorSystem not yet available.")); var cluster = Cluster.Get(system); var self = cluster.SelfMember; var selfUp = self.Status == MemberStatus.Up; bool hasRole; bool isLeader; if (_role is null) { hasRole = false; var leader = cluster.State.Leader; isLeader = leader is not null && leader == self.Address; } else { hasRole = self.HasRole(_role); var roleLeader = cluster.State.RoleLeader(_role); isLeader = roleLeader is not null && roleLeader == self.Address; } var health = ActiveNodeDecision.Evaluate(selfUp, isLeader, hasRole, _role); var description = DescribeResult(health, self.Status, selfUp, isLeader); var result = health switch { HealthStatus.Healthy => HealthCheckResult.Healthy(description), HealthStatus.Degraded => HealthCheckResult.Degraded(description), _ => HealthCheckResult.Unhealthy(description), }; return Task.FromResult(result); } private string DescribeResult(HealthStatus health, MemberStatus status, bool selfUp, bool isLeader) { if (_role is null) { if (health == HealthStatus.Healthy) return "Active node (cluster leader)."; return selfUp && !isLeader ? "Standby: node is Up but not the cluster leader." : $"Standby: node is not Up (status: {status})."; } return health switch { HealthStatus.Healthy => $"Active for role '{_role}' (or not a role member).", _ => $"Role '{_role}' member but not leader.", }; } }