Files
lmxopcua/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Health/DbHealthProbeOffThreadTests.cs
T
Joseph Doherty 79d6b0a4b2 fix(runtime): lifecycle housekeeping — reprovision stamp, Stale drops, off-thread DB probe, tombstone sweep (A4)
- DriverHostActor.StopChild removes the driver's _lastReprovisionUtc stamp
  (and its deferred-reprovision timer): the map grew per removed id and a
  re-added driver inherited a backoff it never earned (02/L2).
- Stale now registers explicit drops for AttributeValueBatchPublished,
  AttributeAlarmPublished, ArtifactForApply and ApplyWedged instead of
  letting them hit Unhandled (02/H5).
- DbHealthProbeActor runs its SELECT on the thread pool and pipes the
  outcome back (single-flight), with a 2 s command timeout on relational
  providers, so a hung SQL Server no longer pins the actor thread and the
  cached GetStatus answer stays reachable (02/P7).
- TelemetryLocalHub eviction tombstones are timestamped and swept past a
  10 min TTL on the next Evict; a TimeProvider ctor is the test seam (02/L4).
- Tests: ValuePublishes.Single helper replaces the per-value
  AttributeValuePublished in CalculationDependencyFlowTests (02/U4, test
  half; the production receive is owned by Lane E).
- R05/8 audit: PollGroupEngine.Unsubscribe has no production caller; every
  driver already uses UnsubscribeAsync (no change).

Refs #731

Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
2026-10-22 06:36:12 -04:00

74 lines
2.7 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);
var status = await actor.Ask<DbHealthProbeActor.DbHealthStatus>(
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.DbHealthStatus>(
DbHealthProbeActor.GetStatus.Instance, TimeSpan.FromSeconds(3));
status.Reachable.ShouldBeTrue();
}, PresenceBudget);
}
/// <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");
}
}
}