feat(health): IActiveNodeGate seam + RequireActiveNode filter
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace ZB.MOM.WW.Health;
|
||||
|
||||
/// <summary>
|
||||
/// Endpoint filter that gates a route to the active node. Resolves <see cref="IActiveNodeGate"/>
|
||||
/// from request services; when it is registered and reports a standby
|
||||
/// (<see cref="IActiveNodeGate.IsActiveNode"/> is <c>false</c>) the request is short-circuited with
|
||||
/// HTTP 503 and a <c>Retry-After</c> header. When no gate is registered (non-clustered host / tests)
|
||||
/// the request is served, preserving prior behaviour.
|
||||
/// </summary>
|
||||
public sealed class ActiveNodeGateEndpointFilter : IEndpointFilter
|
||||
{
|
||||
/// <summary>Default <c>Retry-After</c> value (seconds) advertised on a standby 503 response.</summary>
|
||||
private const int RetryAfterSeconds = 5;
|
||||
|
||||
/// <summary>
|
||||
/// Returns 503 (with <c>Retry-After</c>) when the resolved <see cref="IActiveNodeGate"/> reports
|
||||
/// a standby node; otherwise delegates to the next filter or endpoint handler.
|
||||
/// </summary>
|
||||
/// <param name="context">The endpoint filter invocation context.</param>
|
||||
/// <param name="next">The next filter or endpoint handler in the pipeline.</param>
|
||||
public async ValueTask<object?> InvokeAsync(
|
||||
EndpointFilterInvocationContext context,
|
||||
EndpointFilterDelegate next)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
ArgumentNullException.ThrowIfNull(next);
|
||||
|
||||
var httpContext = context.HttpContext;
|
||||
var gate = httpContext.RequestServices.GetService<IActiveNodeGate>();
|
||||
|
||||
if (gate is { IsActiveNode: false })
|
||||
{
|
||||
httpContext.Response.Headers.RetryAfter = RetryAfterSeconds.ToString();
|
||||
return Results.StatusCode(StatusCodes.Status503ServiceUnavailable);
|
||||
}
|
||||
|
||||
return await next(context);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Route convention that gates endpoint(s) to the active node, returning 503 on standby nodes.
|
||||
/// </summary>
|
||||
public static class ActiveNodeGateExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies <see cref="ActiveNodeGateEndpointFilter"/> to the decorated endpoint(s): the route is
|
||||
/// served only when the DI-resolved <see cref="IActiveNodeGate"/> reports the node active, and
|
||||
/// returns 503 with a <c>Retry-After</c> header when the node is a standby.
|
||||
/// </summary>
|
||||
/// <param name="builder">The endpoint convention builder to decorate.</param>
|
||||
/// <returns>The same <paramref name="builder"/> for chaining.</returns>
|
||||
public static IEndpointConventionBuilder RequireActiveNode(this IEndpointConventionBuilder builder)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(builder);
|
||||
return builder.AddEndpointFilter(new ActiveNodeGateEndpointFilter());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace ZB.MOM.WW.Health;
|
||||
|
||||
/// <summary>
|
||||
/// Single-property seam: is this node the active / leader node?
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Attach to endpoints or route groups via
|
||||
/// <see cref="ActiveNodeGateExtensions.RequireActiveNode"/>. A standby node must not serve the
|
||||
/// gated routes, so the filter returns HTTP 503 when <see cref="IsActiveNode"/> is <c>false</c>.
|
||||
/// The implementation is supplied by the consumer — the <c>ZB.MOM.WW.Health.Akka</c> package ships
|
||||
/// <c>AkkaActiveNodeGate</c> for clustered nodes; non-Akka hosts provide their own.
|
||||
/// </remarks>
|
||||
public interface IActiveNodeGate
|
||||
{
|
||||
/// <summary>
|
||||
/// <c>true</c> when this node is the active node and may serve gated routes;
|
||||
/// <c>false</c> on a standby node.
|
||||
/// </summary>
|
||||
bool IsActiveNode { get; }
|
||||
}
|
||||
Reference in New Issue
Block a user