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.Actor;
|
||||||
using Akka.Cluster;
|
using Akka.Cluster;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||||
|
|
||||||
|
[assembly: InternalsVisibleTo("ZB.MOM.WW.Health.Akka.Tests")]
|
||||||
|
|
||||||
namespace ZB.MOM.WW.Health.Akka;
|
namespace ZB.MOM.WW.Health.Akka;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -10,7 +13,7 @@ namespace ZB.MOM.WW.Health.Akka;
|
|||||||
/// <see cref="ActiveNodeHealthCheck"/> so the role-less and role-filtered matrices are exhaustively
|
/// <see cref="ActiveNodeHealthCheck"/> so the role-less and role-filtered matrices are exhaustively
|
||||||
/// table-testable without forming a real cluster.
|
/// table-testable without forming a real cluster.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class ActiveNodeDecision
|
internal static class ActiveNodeDecision
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Maps the resolved cluster facts to a <see cref="HealthStatus"/>.
|
/// 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)
|
public ActiveNodeHealthCheck(IServiceProvider serviceProvider, string role)
|
||||||
{
|
{
|
||||||
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
|
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
|
||||||
_role = role ?? throw new ArgumentNullException(nameof(role));
|
ArgumentException.ThrowIfNullOrWhiteSpace(role);
|
||||||
|
_role = role;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -119,15 +123,26 @@ public sealed class ActiveNodeHealthCheck : IHealthCheck
|
|||||||
}
|
}
|
||||||
|
|
||||||
var health = ActiveNodeDecision.Evaluate(selfUp, isLeader, hasRole, _role);
|
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)
|
if (_role is null)
|
||||||
return health == HealthStatus.Healthy
|
{
|
||||||
? "Active node (cluster leader)."
|
if (health == HealthStatus.Healthy)
|
||||||
: $"Standby node (status: {status}).";
|
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
|
return health switch
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -45,7 +45,12 @@ public sealed class AkkaClusterHealthCheck : IHealthCheck
|
|||||||
var status = Cluster.Get(system).SelfMember.Status;
|
var status = Cluster.Get(system).SelfMember.Status;
|
||||||
var health = _policy.Evaluate(status);
|
var health = _policy.Evaluate(status);
|
||||||
var description = $"Akka cluster member status: {status}";
|
var description = $"Akka cluster member status: {status}";
|
||||||
|
var result = health switch
|
||||||
return Task.FromResult(new HealthCheckResult(health, description));
|
{
|
||||||
|
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.
|
// Role-filtered: requiredRole != null.
|
||||||
// lacks role -> Healthy (probe irrelevant for this node)
|
// 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
|
// has role & not leader -> Degraded
|
||||||
public static IEnumerable<object[]> RoleFilteredCases() => new[]
|
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, true, false, "admin", HealthStatus.Healthy },
|
||||||
new object[] { true, false, false, "admin", HealthStatus.Healthy },
|
new object[] { true, false, false, "admin", HealthStatus.Healthy },
|
||||||
new object[] { false, 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 },
|
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
|
// node carries the role but is not leader -> Degraded
|
||||||
new object[] { true, false, true, "admin", HealthStatus.Degraded },
|
new object[] { true, false, true, "admin", HealthStatus.Degraded },
|
||||||
new object[] { false, 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.WeaklyUp, HealthStatus.Unhealthy },
|
||||||
new object[] { MemberStatus.Down, HealthStatus.Unhealthy },
|
new object[] { MemberStatus.Down, HealthStatus.Unhealthy },
|
||||||
new object[] { MemberStatus.Removed, HealthStatus.Unhealthy },
|
new object[] { MemberStatus.Removed, HealthStatus.Unhealthy },
|
||||||
|
new object[] { (MemberStatus)99, HealthStatus.Unhealthy }, // unknown / future status
|
||||||
};
|
};
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
@@ -41,6 +42,7 @@ public sealed class AkkaClusterStatusPolicyTests
|
|||||||
new object[] { MemberStatus.WeaklyUp, HealthStatus.Degraded },
|
new object[] { MemberStatus.WeaklyUp, HealthStatus.Degraded },
|
||||||
new object[] { MemberStatus.Down, HealthStatus.Degraded },
|
new object[] { MemberStatus.Down, HealthStatus.Degraded },
|
||||||
new object[] { MemberStatus.Removed, HealthStatus.Degraded },
|
new object[] { MemberStatus.Removed, HealthStatus.Degraded },
|
||||||
|
new object[] { (MemberStatus)99, HealthStatus.Degraded }, // unknown / future status
|
||||||
};
|
};
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||||
<PackageReference Include="xunit" />
|
<PackageReference Include="xunit" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" />
|
<PackageReference Include="xunit.runner.visualstudio" />
|
||||||
<PackageReference Include="Akka.TestKit.Xunit2" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<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
|
`IActiveNodeGate` is a single-property interface (`bool IsActiveNode { get; }`) that expresses
|
||||||
whether the current node should accept write / active-role requests. The default implementation,
|
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`
|
`IEndpointConventionBuilder` attaches a policy that short-circuits with `503 Service Unavailable`
|
||||||
on standby nodes.
|
on standby nodes.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user