79d6b0a4b2
- 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
74 lines
2.7 KiB
C#
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");
|
|
}
|
|
}
|
|
}
|