refactor(health.akka): review polish (internal decision helper, role guard, factory results, test coverage) + fix SPEC §4 gate description
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
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>
|
||||
@@ -10,7 +13,7 @@ namespace ZB.MOM.WW.Health.Akka;
|
||||
/// <see cref="ActiveNodeHealthCheck"/> so the role-less and role-filtered matrices are exhaustively
|
||||
/// table-testable without forming a real cluster.
|
||||
/// </summary>
|
||||
public static class ActiveNodeDecision
|
||||
internal static class ActiveNodeDecision
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps the resolved cluster facts to a <see cref="HealthStatus"/>.
|
||||
@@ -87,7 +90,8 @@ public sealed class ActiveNodeHealthCheck : IHealthCheck
|
||||
public ActiveNodeHealthCheck(IServiceProvider serviceProvider, string role)
|
||||
{
|
||||
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
|
||||
_role = role ?? throw new ArgumentNullException(nameof(role));
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(role);
|
||||
_role = role;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -119,15 +123,26 @@ public sealed class ActiveNodeHealthCheck : IHealthCheck
|
||||
}
|
||||
|
||||
var health = ActiveNodeDecision.Evaluate(selfUp, isLeader, hasRole, _role);
|
||||
return Task.FromResult(new HealthCheckResult(health, DescribeResult(health, self.Status)));
|
||||
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)
|
||||
private string DescribeResult(HealthStatus health, MemberStatus status, bool selfUp, bool isLeader)
|
||||
{
|
||||
if (_role is null)
|
||||
return health == HealthStatus.Healthy
|
||||
? "Active node (cluster leader)."
|
||||
: $"Standby node (status: {status}).";
|
||||
{
|
||||
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
|
||||
{
|
||||
|
||||
@@ -45,7 +45,12 @@ public sealed class AkkaClusterHealthCheck : IHealthCheck
|
||||
var status = Cluster.Get(system).SelfMember.Status;
|
||||
var health = _policy.Evaluate(status);
|
||||
var description = $"Akka cluster member status: {status}";
|
||||
|
||||
return Task.FromResult(new HealthCheckResult(health, description));
|
||||
var result = health switch
|
||||
{
|
||||
HealthStatus.Healthy => HealthCheckResult.Healthy(description),
|
||||
HealthStatus.Degraded => HealthCheckResult.Degraded(description),
|
||||
_ => HealthCheckResult.Unhealthy(description),
|
||||
};
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ public sealed class ActiveNodeDecisionTests
|
||||
|
||||
// Role-filtered: requiredRole != null.
|
||||
// lacks role -> Healthy (probe irrelevant for this node)
|
||||
// has role & is leader -> Healthy
|
||||
// has role & is leader -> Healthy (selfUp is ignored — role-filtered mode only cares about leadership)
|
||||
// has role & not leader -> Degraded
|
||||
public static IEnumerable<object[]> RoleFilteredCases() => new[]
|
||||
{
|
||||
@@ -39,8 +39,10 @@ public sealed class ActiveNodeDecisionTests
|
||||
new object[] { true, true, false, "admin", HealthStatus.Healthy },
|
||||
new object[] { true, false, false, "admin", HealthStatus.Healthy },
|
||||
new object[] { false, false, false, "admin", HealthStatus.Healthy },
|
||||
// node carries the role and is leader -> Healthy
|
||||
// node carries the role and is leader -> Healthy (selfUp=true)
|
||||
new object[] { true, true, true, "admin", HealthStatus.Healthy },
|
||||
// node carries the role and is leader -> Healthy (selfUp=false: role-filtered mode ignores selfUp)
|
||||
new object[] { false, true, true, "admin", HealthStatus.Healthy },
|
||||
// node carries the role but is not leader -> Degraded
|
||||
new object[] { true, false, true, "admin", HealthStatus.Degraded },
|
||||
new object[] { false, false, true, "admin", HealthStatus.Degraded },
|
||||
|
||||
@@ -23,6 +23,7 @@ public sealed class AkkaClusterStatusPolicyTests
|
||||
new object[] { MemberStatus.WeaklyUp, HealthStatus.Unhealthy },
|
||||
new object[] { MemberStatus.Down, HealthStatus.Unhealthy },
|
||||
new object[] { MemberStatus.Removed, HealthStatus.Unhealthy },
|
||||
new object[] { (MemberStatus)99, HealthStatus.Unhealthy }, // unknown / future status
|
||||
};
|
||||
|
||||
[Theory]
|
||||
@@ -41,6 +42,7 @@ public sealed class AkkaClusterStatusPolicyTests
|
||||
new object[] { MemberStatus.WeaklyUp, HealthStatus.Degraded },
|
||||
new object[] { MemberStatus.Down, HealthStatus.Degraded },
|
||||
new object[] { MemberStatus.Removed, HealthStatus.Degraded },
|
||||
new object[] { (MemberStatus)99, HealthStatus.Degraded }, // unknown / future status
|
||||
};
|
||||
|
||||
[Theory]
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" />
|
||||
<PackageReference Include="Akka.TestKit.Xunit2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -150,7 +150,10 @@ plug it into `MapHealthChecks` options and also call it from custom endpoints.
|
||||
|
||||
`IActiveNodeGate` is a single-property interface (`bool IsActiveNode { get; }`) that expresses
|
||||
whether the current node should accept write / active-role requests. The default implementation,
|
||||
`AkkaActiveNodeGate`, delegates to `ActiveNodeHealthCheck`. A `RequireActiveNode()` extension on
|
||||
`AkkaActiveNodeGate`, reads cluster state **directly**: `IsActiveNode` returns `true` iff the
|
||||
`ActorSystem` is available, `SelfMember.Status == Up`, and the node is the cluster leader. It is
|
||||
null-guarded and returns `false` when the `ActorSystem` is not yet ready (safe default during
|
||||
startup). It does **not** resolve `ActiveNodeHealthCheck` from DI. A `RequireActiveNode()` extension on
|
||||
`IEndpointConventionBuilder` attaches a policy that short-circuits with `503 Service Unavailable`
|
||||
on standby nodes.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user