23c35df234
The InboundAPI carried its own IActiveNodeGate (implemented by Host/Health/ActiveNodeGate) whose rule already matched the shared Health 0.3.0 AkkaActiveNodeGate — both route to ClusterActiveNode (oldest Up member). The private interface and implementation are deleted; InboundAPI references ZB.MOM.WW.Health (0.3.0, already pinned) and InboundApiEndpointFilter, AuditWriteMiddleware, InboundMethodWarmupService and FlowDebugRunner consume ZB.MOM.WW.Health.IActiveNodeGate. The Central branch of Program.cs registers AkkaActiveNodeGate (unscoped, like the active-node health check). Warm-up behaviour is kept: the shared gate answers false while no ActorSystem resolves, while this member is not Up, and on any cluster-init fault. It resolves the ActorSystem through the existing DI singleton bridge (GetOrCreateActorSystem), the same path the health probes use. Tests: CompositionRoot asserts the shared gate type (fails with the registration removed); the evaluator wiring test now drives AkkaActiveNodeGate; new warm-up test (no ActorSystem -> not active). Nothing was missing from the Health 0.3.0 package. (ScadaBridge#80) Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
71 lines
3.0 KiB
C#
71 lines
3.0 KiB
C#
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using NSubstitute;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.InboundApi;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
|
|
using ZB.MOM.WW.Health;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.InboundAPI.Tests;
|
|
|
|
/// <summary>Tests for <see cref="InboundMethodWarmupService"/> (review 2026-10-21 05/U3).</summary>
|
|
public class InboundMethodWarmupServiceTests
|
|
{
|
|
private sealed class Lifetime : IHostApplicationLifetime
|
|
{
|
|
public CancellationTokenSource StartedCts { get; } = new();
|
|
public CancellationToken ApplicationStarted => StartedCts.Token;
|
|
public CancellationToken ApplicationStopping => CancellationToken.None;
|
|
public CancellationToken ApplicationStopped => CancellationToken.None;
|
|
public void StopApplication() { }
|
|
}
|
|
|
|
private sealed class Gate(bool active) : IActiveNodeGate
|
|
{
|
|
public bool IsActiveNode => active;
|
|
}
|
|
|
|
private static (InboundMethodWarmupService svc, InboundScriptExecutor exec, Lifetime life) Build(bool active)
|
|
{
|
|
var repo = Substitute.For<IInboundApiRepository>();
|
|
repo.GetAllApiMethodsAsync(Arg.Any<CancellationToken>())
|
|
.Returns(new List<ApiMethod> { new("warm", "return 1;") { Id = 1, TimeoutSeconds = 10 } });
|
|
var provider = new ServiceCollection().AddSingleton(repo).BuildServiceProvider();
|
|
var exec = new InboundScriptExecutor(NullLogger<InboundScriptExecutor>.Instance, Substitute.For<IServiceProvider>());
|
|
var life = new Lifetime();
|
|
var svc = new InboundMethodWarmupService(
|
|
provider.GetRequiredService<IServiceScopeFactory>(), exec, life,
|
|
NullLogger<InboundMethodWarmupService>.Instance, new Gate(active));
|
|
return (svc, exec, life);
|
|
}
|
|
|
|
/// <summary>Verifies the warm-up waits for ApplicationStarted and then compiles on an active node.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task ActiveNode_CompilesAfterApplicationStarted_NotBefore()
|
|
{
|
|
var (svc, exec, life) = Build(active: true);
|
|
await svc.StartAsync(CancellationToken.None);
|
|
await Task.Delay(200);
|
|
Assert.False(exec.HasHandler("warm"));
|
|
|
|
life.StartedCts.Cancel();
|
|
for (var i = 0; i < 100 && !exec.HasHandler("warm"); i++) await Task.Delay(50);
|
|
Assert.True(exec.HasHandler("warm"));
|
|
await svc.StopAsync(CancellationToken.None);
|
|
}
|
|
|
|
/// <summary>Verifies a standby node does not warm up.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task StandbyNode_DoesNotCompile()
|
|
{
|
|
var (svc, exec, life) = Build(active: false);
|
|
await svc.StartAsync(CancellationToken.None);
|
|
life.StartedCts.Cancel();
|
|
await Task.Delay(500);
|
|
Assert.False(exec.HasHandler("warm"));
|
|
await svc.StopAsync(CancellationToken.None);
|
|
}
|
|
}
|