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; /// /// Review 02 P7: 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 GetStatus 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. /// 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); var status = await actor.Ask( DbHealthProbeActor.GetStatus.Instance, TimeSpan.FromSeconds(3)); 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.GetStatus.Instance, TimeSpan.FromSeconds(3)); status.Reachable.ShouldBeTrue(); }, PresenceBudget); } /// Blocks every CreateDbContext until released — a connect that never answers. private sealed class HangingDbFactory : IDbContextFactory { 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"); } } }