Files
lmxopcua/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverHostActorReprovisionStampTests.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

64 lines
3.6 KiB
C#

using Akka.Actor;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Commons.Types;
using ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness;
namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;
/// <summary>
/// Review 02 L2: <c>DriverHostActor._lastReprovisionUtc</c> was never pruned — a driver removed by a deploy
/// after a supervision crash kept its stamp, so the map grew with every id ever reprovisioned-then-removed,
/// and (the observable half) a driver removed and later re-added inherited the OLD stamp: its first crash
/// after the re-add was deferred by the backoff as if it were crash-looping, although the new incarnation had
/// never crashed. <c>StopChild</c> now drops the stamp.
/// </summary>
public sealed class DriverHostActorReprovisionStampTests : RuntimeActorTestBase
{
private static readonly RevisionHash RevA = RevisionHash.Parse(new string('a', 64));
private static readonly RevisionHash RevB = RevisionHash.Parse(new string('b', 64));
private static readonly RevisionHash RevC = RevisionHash.Parse(new string('c', 64));
private static readonly NodeId TestNode = NodeId.Parse("node-1:4053");
private const string DriverId = "DI-1";
private const string SpeedPath = "Plant/Modbus/dev1/speed";
[Fact]
public void A_driver_removed_and_re_added_does_not_inherit_the_old_incarnations_reprovision_stamp()
{
var db = NewInMemoryDbFactory();
// Far longer than the presence budget below: an inherited stamp would defer the re-added driver's
// reprovision past it, which is the failure this test reports.
var backoff = TimeSpan.FromSeconds(60);
var factory = new RecordingSubscribableDriverFactory("Modbus");
var withDriver = new[] { (DriverId, "Modbus", "dev1", "speed", "Float", false) };
var none = Array.Empty<(string, string, string, string, string?)>();
var revA = V3ArtifactSeeder.SeedV3Deployment(db, RevA, withDriver, none, enabled: true);
var (host, _, _, coordinator) = V3ArtifactSeeder.SpawnHostAndApplyWithCoordinator(
this, db, TestNode, revA, RevA, factory, reprovisionBackoff: backoff);
AwaitAssert(() => factory.Created.Count.ShouldBe(1), PresenceBudget);
AwaitAssert(() => factory.Created[0].SubscribeCount.ShouldBe(1), PresenceBudget);
// Crash 1 on the first incarnation: reprovisioned immediately, which stamps the id.
factory.Created[0].FireMalformedDataChange(SpeedPath);
AwaitAssert(() => factory.Created[0].InitializeCount.ShouldBe(2), PresenceBudget);
// Deploy B removes the driver (StopChild), deploy C re-adds it: a brand-new driver object.
var revB = V3ArtifactSeeder.SeedV3Deployment(
db, RevB, Array.Empty<(string, string, string, string, string, bool)>(), none, enabled: true);
V3ArtifactSeeder.Dispatch(host, coordinator, revB, RevB);
var revC = V3ArtifactSeeder.SeedV3Deployment(db, RevC, withDriver, none, enabled: true);
V3ArtifactSeeder.Dispatch(host, coordinator, revC, RevC);
AwaitAssert(() => factory.Created.Count.ShouldBe(2), PresenceBudget);
var reAdded = factory.Created[1];
AwaitAssert(() => reAdded.SubscribeCount.ShouldBe(1), PresenceBudget);
// Its FIRST crash must reprovision immediately — it has no history of its own.
reAdded.FireMalformedDataChange(SpeedPath);
AwaitAssert(
() => reAdded.InitializeCount.ShouldBe(
2, "the re-added driver's first reprovision was deferred by the REMOVED incarnation's stamp"),
duration: TimeSpan.FromSeconds(5));
}
}