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
67 lines
3.2 KiB
C#
67 lines
3.2 KiB
C#
using Akka.Actor;
|
|
using Akka.Event;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
|
|
using ZB.MOM.WW.OtOpcUa.Commons.Types;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
|
|
using ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;
|
|
|
|
/// <summary>
|
|
/// Review 02 H5: the <c>Stale</c> behaviour registered no receive for a driver child's value/alarm bursts (nor
|
|
/// for a late <c>ArtifactForApply</c> / <c>ApplyWedged</c> pipe-back), so any that landed there hit
|
|
/// <c>Unhandled</c> — dead-letter noise and a dropped message with no log naming why. They are now explicit
|
|
/// drops. The two private pipe-back records cannot be constructed from a test; the two public child
|
|
/// messages are pinned here, and the private pair by the same registration pattern (compile-time).
|
|
/// </summary>
|
|
public sealed class DriverHostActorStaleDropTests : RuntimeActorTestBase
|
|
{
|
|
private static readonly NodeId TestNode = NodeId.Parse("stale-drop-test");
|
|
|
|
[Fact]
|
|
public void Child_value_and_alarm_bursts_landing_in_Stale_are_dropped_not_unhandled()
|
|
{
|
|
var unhandled = CreateTestProbe();
|
|
Sys.EventStream.Subscribe(unhandled.Ref, typeof(UnhandledMessage));
|
|
|
|
// ThrowingDbFactory ⇒ Bootstrap's catch ⇒ Become(Stale).
|
|
var host = Sys.ActorOf(DriverHostActor.Props(
|
|
new ThrowingDbFactory(), TestNode, CreateTestProbe().Ref,
|
|
localRoles: new HashSet<string> { "driver" }));
|
|
// Barrier: Stale answers RouteNodeWrite synchronously, so a reply proves the host is in Stale.
|
|
var asker = CreateTestProbe();
|
|
host.Tell(new DriverHostActor.RouteNodeWrite("eq-x/speed", 0.0, AddressSpaceRealm.Uns), asker.Ref);
|
|
asker.ExpectMsg<DriverHostActor.NodeWriteResult>(PresenceBudget).Reason!.ShouldContain("stale");
|
|
|
|
// Positive control: an unknown message IS reported as unhandled by this host.
|
|
host.Tell("not a message the host knows");
|
|
unhandled.ExpectMsg<UnhandledMessage>(m => ReferenceEquals(m.Recipient, host) || m.Recipient.Equals(host), PresenceBudget);
|
|
|
|
host.Tell(new DriverInstanceActor.AttributeValueBatchPublished(
|
|
"DI-1", new[] { new DriverInstanceActor.AttributeValueItem("Plant/x/y", 1.0, OpcUaStatus.Good, DateTime.UtcNow) }));
|
|
host.Tell(new DriverInstanceActor.AttributeAlarmPublished(
|
|
"DI-1", new AlarmEventArgs(new Handle(), "Plant/x/alarm", "Plant/x/alarm", "AlarmCondition", "test",
|
|
AlarmSeverity.High, DateTime.UtcNow)));
|
|
// Barrier again, then the absence window.
|
|
host.Tell(new DriverHostActor.RouteNodeWrite("eq-x/speed", 0.0, AddressSpaceRealm.Uns), asker.Ref);
|
|
asker.ExpectMsg<DriverHostActor.NodeWriteResult>(PresenceBudget);
|
|
|
|
unhandled.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
|
|
}
|
|
|
|
private sealed class Handle : IAlarmSubscriptionHandle
|
|
{
|
|
public string DiagnosticId => "stale-test";
|
|
}
|
|
|
|
private sealed class ThrowingDbFactory : IDbContextFactory<OtOpcUaConfigDbContext>
|
|
{
|
|
public OtOpcUaConfigDbContext CreateDbContext() => throw new InvalidOperationException("db unreachable");
|
|
}
|
|
}
|