fix(inbound): background active-node-only method warm-up service (review 2026-10-21 05/U3)

This commit is contained in:
Joseph Doherty
2026-10-22 08:41:55 -04:00
parent 4921ebb0c8
commit fa73ddb453
4 changed files with 163 additions and 2 deletions
@@ -0,0 +1,69 @@
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;
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);
}
}