b211911f98
v2-ci / unit-tests (push) Failing after 13m22s
Akka.TestKit.Xunit2 -> Akka.TestKit.Xunit 1.5.62 (xunit.v3 TestKit; needs xunit.v3 3.2.2, set via VersionOverride on the four projects). Removes the xunit 2.9.3 pin. IAsyncLifetime returns ValueTask, SqlServerFactAttribute gains the source-info ctor, and xUnit1051 call sites pass TestContext.Current.CancellationToken. Test counts unchanged. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
74 lines
2.9 KiB
C#
74 lines
2.9 KiB
C#
using Akka.Actor;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration;
|
|
using ZB.MOM.WW.OtOpcUa.Runtime.Health;
|
|
using ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Health;
|
|
|
|
/// <summary>
|
|
/// Review 02 P7: <see cref="DbHealthProbeActor"/> ran its SELECT synchronously on the actor thread, so a
|
|
/// hanging SQL connect/command held the dispatcher thread for the implicit 15 s / 30 s SqlClient timeouts and
|
|
/// queued every later tick (and every <c>GetStatus</c> Ask) behind it. The probe now runs off the mailbox
|
|
/// and pipes its outcome back, so the cached answer stays available while a probe hangs.
|
|
/// </summary>
|
|
public sealed class DbHealthProbeOffThreadTests : RuntimeActorTestBase
|
|
{
|
|
[Fact]
|
|
public async Task GetStatus_answers_from_cache_while_a_probe_is_hung()
|
|
{
|
|
var factory = new HangingDbFactory();
|
|
try
|
|
{
|
|
var actor = Sys.ActorOf(DbHealthProbeActor.Props(factory));
|
|
AwaitCondition(() => factory.Entered > 0, PresenceBudget, TestContext.Current.CancellationToken);
|
|
|
|
var status = await actor.Ask<DbHealthProbeActor.DbHealthStatus>(
|
|
DbHealthProbeActor.GetStatus.Instance, TimeSpan.FromSeconds(3), TestContext.Current.CancellationToken);
|
|
|
|
status.Reachable.ShouldBeFalse();
|
|
status.LastError.ShouldBe("not probed yet");
|
|
factory.Entered.ShouldBe(1, "a second tick must not stack a second probe behind a hung one");
|
|
}
|
|
finally
|
|
{
|
|
factory.Release();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_probe_that_completes_updates_the_cached_status()
|
|
{
|
|
// Positive control for the off-thread path: the piped-back outcome lands in the cache.
|
|
var db = NewInMemoryDbFactory();
|
|
var actor = Sys.ActorOf(DbHealthProbeActor.Props(db));
|
|
|
|
await AwaitAssertAsync(async () =>
|
|
{
|
|
var status = await actor.Ask<DbHealthProbeActor.DbHealthStatus>(
|
|
DbHealthProbeActor.GetStatus.Instance, TimeSpan.FromSeconds(3));
|
|
status.Reachable.ShouldBeTrue();
|
|
}, PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
|
|
}
|
|
|
|
/// <summary>Blocks every <c>CreateDbContext</c> until released — a connect that never answers.</summary>
|
|
private sealed class HangingDbFactory : IDbContextFactory<OtOpcUaConfigDbContext>
|
|
{
|
|
private readonly ManualResetEventSlim _release = new(false);
|
|
private int _entered;
|
|
|
|
public int Entered => Volatile.Read(ref _entered);
|
|
|
|
public void Release() => _release.Set();
|
|
|
|
public OtOpcUaConfigDbContext CreateDbContext()
|
|
{
|
|
Interlocked.Increment(ref _entered);
|
|
_release.Wait();
|
|
throw new InvalidOperationException("released without a database");
|
|
}
|
|
}
|
|
}
|