154 lines
6.3 KiB
C#
154 lines
6.3 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Pure decision function for the active / leader probe, factored out of
|
|
/// <see cref="ActiveNodeHealthCheck"/> so the role-less and role-filtered matrices are exhaustively
|
|
/// table-testable without forming a real cluster.
|
|
/// </summary>
|
|
internal static class ActiveNodeDecision
|
|
{
|
|
/// <summary>
|
|
/// Maps the resolved cluster facts to a <see cref="HealthStatus"/>.
|
|
/// </summary>
|
|
/// <param name="selfUp">Whether the local node's member status is <c>Up</c>.</param>
|
|
/// <param name="isLeader">
|
|
/// Whether the local node is the leader: the cluster leader in role-less mode, or the
|
|
/// role-singleton leader in role-filtered mode.
|
|
/// </param>
|
|
/// <param name="hasRole">
|
|
/// Whether the local node carries <paramref name="requiredRole"/>. Ignored when
|
|
/// <paramref name="requiredRole"/> is <c>null</c>.
|
|
/// </param>
|
|
/// <param name="requiredRole">
|
|
/// The role to scope the check to, or <c>null</c> for the role-less (whole-cluster-leader) mode.
|
|
/// </param>
|
|
/// <returns>
|
|
/// 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.
|
|
/// </returns>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// <see cref="ZbHealthTags.Active"/> tag.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The <see cref="ActorSystem"/> is resolved lazily from the service provider. If it is not yet
|
|
/// available — e.g. during startup before Akka is initialised — the check returns
|
|
/// <see cref="HealthStatus.Degraded"/> rather than throwing, so it is startup-safe.
|
|
/// </remarks>
|
|
public sealed class ActiveNodeHealthCheck : IHealthCheck
|
|
{
|
|
private readonly IServiceProvider _serviceProvider;
|
|
private readonly string? _role;
|
|
|
|
/// <summary>
|
|
/// Role-less constructor: Healthy when the node is <c>Up</c> and the cluster leader
|
|
/// (ScadaBridge ActiveNode pattern); Unhealthy otherwise. Degraded when the ActorSystem /
|
|
/// cluster is not yet ready.
|
|
/// </summary>
|
|
/// <param name="serviceProvider">
|
|
/// The application service provider. The <see cref="ActorSystem"/> is resolved lazily so the
|
|
/// check is startup-safe: if no <see cref="ActorSystem"/> is registered yet the result is Degraded.
|
|
/// </param>
|
|
public ActiveNodeHealthCheck(IServiceProvider serviceProvider)
|
|
{
|
|
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
|
|
_role = null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Role-filtered constructor: Healthy when the node lacks <paramref name="role"/> 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.
|
|
/// </summary>
|
|
/// <param name="serviceProvider">
|
|
/// The application service provider. The <see cref="ActorSystem"/> is resolved lazily so the
|
|
/// check is startup-safe: if no <see cref="ActorSystem"/> is registered yet the result is Degraded.
|
|
/// </param>
|
|
/// <param name="role">The Akka cluster role to scope the check to.</param>
|
|
public ActiveNodeHealthCheck(IServiceProvider serviceProvider, string role)
|
|
{
|
|
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(role);
|
|
_role = role;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<HealthCheckResult> CheckHealthAsync(
|
|
HealthCheckContext context,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var system = _serviceProvider.GetService<ActorSystem>();
|
|
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.",
|
|
};
|
|
}
|
|
}
|