fix(deploy): hold the Applied ack until the address-space apply outcome is known (#601)
DriverHostActor no longer acks Applied the moment the revision advances. It asks OpcUaPublishActor for the outcome (RebuildAddressSpace.ReportOutcome) and acks Applied+Degraded(reason) when the apply was degraded; a missing outcome times out (60 s, inside the coordinator's 2 min deadline) to a degraded ack, never a clean one. Re-dispatching a revision whose apply was degraded or is still unconfirmed re-applies instead of taking the clean idempotent short-circuit - that re-apply is also what repairs the degraded nodes. Tests with a bare publish probe use a clean-outcome auto-pilot probe. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
+2
-2
@@ -67,7 +67,7 @@ public sealed class DriverHostActorBootFromCacheTests : RuntimeActorTestBase
|
||||
var blob = EmptyArtifact();
|
||||
var cached = new CachedDeploymentArtifact(
|
||||
DeploymentId.NewId().ToString(), CachedRev.Value, blob, DateTimeOffset.UtcNow.AddHours(-1));
|
||||
var publishProbe = CreateTestProbe();
|
||||
var publishProbe = this.CreateCleanOutcomePublishProbe();
|
||||
|
||||
Sys.ActorOf(DriverHostActor.Props(
|
||||
new ThrowingDbFactory(), TestNode, ackRouter: null,
|
||||
@@ -92,7 +92,7 @@ public sealed class DriverHostActorBootFromCacheTests : RuntimeActorTestBase
|
||||
var cached = new CachedDeploymentArtifact(
|
||||
DeploymentId.NewId().ToString(), CachedRev.Value,
|
||||
System.Text.Encoding.Latin1.GetBytes(text), DateTimeOffset.UtcNow.AddHours(-1));
|
||||
var publishProbe = CreateTestProbe();
|
||||
var publishProbe = this.CreateCleanOutcomePublishProbe();
|
||||
|
||||
var actor = Sys.ActorOf(DriverHostActor.Props(
|
||||
new ThrowingDbFactory(), TestNode, ackRouter: null,
|
||||
|
||||
@@ -112,7 +112,7 @@ public sealed class DriverHostActorDbLessTests : RuntimeActorTestBase
|
||||
var cache = new RecordingArtifactCache();
|
||||
var fetcher = new RecordingFetcher(ArtifactBytes());
|
||||
var deploymentId = DeploymentId.NewId();
|
||||
var publish = CreateTestProbe();
|
||||
var publish = this.CreateCleanOutcomePublishProbe();
|
||||
var coordinator = CreateTestProbe();
|
||||
|
||||
var actor = Sys.ActorOf(DriverHostActor.Props(
|
||||
@@ -141,7 +141,7 @@ public sealed class DriverHostActorDbLessTests : RuntimeActorTestBase
|
||||
[Fact]
|
||||
public void DbLessNode_WithLocalDbStore_SpawnsScriptedAlarmHost()
|
||||
{
|
||||
var publish = CreateTestProbe();
|
||||
var publish = this.CreateCleanOutcomePublishProbe();
|
||||
var actor = Sys.ActorOf(DriverHostActor.Props(
|
||||
dbFactory: null,
|
||||
TestNode,
|
||||
@@ -162,7 +162,7 @@ public sealed class DriverHostActorDbLessTests : RuntimeActorTestBase
|
||||
[Fact]
|
||||
public void DbLessNode_WithNoStoreAndNoDbFactory_SkipsScriptedAlarmHost()
|
||||
{
|
||||
var publish = CreateTestProbe();
|
||||
var publish = this.CreateCleanOutcomePublishProbe();
|
||||
var actor = Sys.ActorOf(DriverHostActor.Props(
|
||||
dbFactory: null,
|
||||
TestNode,
|
||||
@@ -185,7 +185,7 @@ public sealed class DriverHostActorDbLessTests : RuntimeActorTestBase
|
||||
[Fact]
|
||||
public void DbBackedNode_WithNoStore_SkipsScriptedAlarmHost()
|
||||
{
|
||||
var publish = CreateTestProbe();
|
||||
var publish = this.CreateCleanOutcomePublishProbe();
|
||||
var actor = Sys.ActorOf(DriverHostActor.Props(
|
||||
dbFactory: NewInMemoryDbFactory(),
|
||||
TestNode,
|
||||
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
using Akka.Actor;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Deploy;
|
||||
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.OpcUaServer;
|
||||
using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
|
||||
using ZB.MOM.WW.OtOpcUa.Runtime.OpcUa;
|
||||
using ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;
|
||||
|
||||
/// <summary>
|
||||
/// Gitea #601 (archreview 01/S-9) — the deploy ack must be honest about the address-space apply. It used
|
||||
/// to be sent the moment the revision advanced, before <c>OpcUaPublishActor</c> had even dequeued the
|
||||
/// rebuild, so a degraded apply (swallowed sink faults; <c>_lastApplied</c> deliberately not advanced)
|
||||
/// still acked a clean <see cref="ApplyAckOutcome.Applied"/> and the deployment sealed green fleet-wide.
|
||||
/// Now the Applied ack waits for the publish actor's <see cref="OpcUaPublishActor.RebuildCompleted"/> and
|
||||
/// carries its <see cref="ApplyAck.Degraded"/> verdict; a missing verdict times out to degraded, never clean.
|
||||
/// </summary>
|
||||
[Collection("opcua-publish-meters")]
|
||||
public sealed class DriverHostActorDegradedAckTests : RuntimeActorTestBase
|
||||
{
|
||||
private static readonly NodeId TestNode = NodeId.Parse("driver-test");
|
||||
private static readonly RevisionHash RevA = RevisionHash.Parse(new string('a', 64));
|
||||
|
||||
[Fact]
|
||||
public void The_Applied_ack_waits_for_the_address_space_outcome_and_a_clean_one_acks_clean()
|
||||
{
|
||||
var (db, deploymentId) = SeedOneTagDeployment();
|
||||
var coordinator = CreateTestProbe();
|
||||
var publish = CreateTestProbe();
|
||||
var actor = Sys.ActorOf(DriverHostActor.Props(
|
||||
db, TestNode, coordinator.Ref, localRoles: new HashSet<string> { "driver" },
|
||||
opcUaPublishActor: publish.Ref));
|
||||
|
||||
actor.Tell(new DispatchDeployment(deploymentId, RevA, CorrelationId.NewId()));
|
||||
|
||||
var rebuild = publish.FishForMessage<OpcUaPublishActor.RebuildAddressSpace>(_ => true, PresenceBudget);
|
||||
rebuild.ReportOutcome.ShouldBeTrue();
|
||||
// The ack is HELD until the outcome lands — the pre-#601 code had already sent it by now.
|
||||
coordinator.ExpectNoMsg(TimeSpan.FromMilliseconds(300));
|
||||
|
||||
publish.Reply(new OpcUaPublishActor.RebuildCompleted(rebuild.Correlation, rebuild.DeploymentId, false, null));
|
||||
|
||||
var ack = coordinator.ExpectMsg<ApplyAck>(PresenceBudget);
|
||||
ack.Outcome.ShouldBe(ApplyAckOutcome.Applied);
|
||||
ack.Degraded.ShouldBeFalse();
|
||||
ack.DegradedReason.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_degraded_address_space_outcome_acks_Applied_but_degraded_with_the_reason()
|
||||
{
|
||||
var (db, deploymentId) = SeedOneTagDeployment();
|
||||
var coordinator = CreateTestProbe();
|
||||
var publish = CreateTestProbe();
|
||||
var actor = Sys.ActorOf(DriverHostActor.Props(
|
||||
db, TestNode, coordinator.Ref, localRoles: new HashSet<string> { "driver" },
|
||||
opcUaPublishActor: publish.Ref));
|
||||
|
||||
actor.Tell(new DispatchDeployment(deploymentId, RevA, CorrelationId.NewId()));
|
||||
var rebuild = publish.FishForMessage<OpcUaPublishActor.RebuildAddressSpace>(_ => true, PresenceBudget);
|
||||
publish.Reply(new OpcUaPublishActor.RebuildCompleted(
|
||||
rebuild.Correlation, rebuild.DeploymentId, Degraded: true, DegradedReason: "failedNodes=2"));
|
||||
|
||||
var ack = coordinator.ExpectMsg<ApplyAck>(PresenceBudget);
|
||||
ack.Outcome.ShouldBe(ApplyAckOutcome.Applied);
|
||||
ack.Degraded.ShouldBeTrue();
|
||||
ack.DegradedReason.ShouldBe("failedNodes=2");
|
||||
ack.DeploymentId.ShouldBe(deploymentId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_real_publish_actor_whose_sink_faults_produces_a_degraded_ack_end_to_end()
|
||||
{
|
||||
// No hand-written verdict: the real OpcUaPublishActor + AddressSpaceApplier over a sink that faults on
|
||||
// every node. This is the production shape of the defect — a degraded apply that sealed green.
|
||||
var (db, deploymentId) = SeedOneTagDeployment();
|
||||
var coordinator = CreateTestProbe();
|
||||
var sink = new FaultingSink();
|
||||
var publish = Sys.ActorOf(OpcUaPublishActor.PropsForTests(
|
||||
sink: sink, applier: new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance)));
|
||||
var actor = Sys.ActorOf(DriverHostActor.Props(
|
||||
db, TestNode, coordinator.Ref, localRoles: new HashSet<string> { "driver" },
|
||||
opcUaPublishActor: publish));
|
||||
|
||||
actor.Tell(new DispatchDeployment(deploymentId, RevA, CorrelationId.NewId()));
|
||||
|
||||
var ack = coordinator.ExpectMsg<ApplyAck>(PresenceBudget);
|
||||
ack.Outcome.ShouldBe(ApplyAckOutcome.Applied);
|
||||
ack.Degraded.ShouldBeTrue();
|
||||
ack.DegradedReason.ShouldNotBeNull();
|
||||
ack.DegradedReason.ShouldContain("failedNodes=");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_real_publish_actor_with_a_healthy_sink_produces_a_clean_ack_end_to_end()
|
||||
{
|
||||
// The control for the test above: same wiring, a sink that does not fault ⇒ clean.
|
||||
var (db, deploymentId) = SeedOneTagDeployment();
|
||||
var coordinator = CreateTestProbe();
|
||||
var sink = new FaultingSink(fault: false);
|
||||
var publish = Sys.ActorOf(OpcUaPublishActor.PropsForTests(
|
||||
sink: sink, applier: new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance)));
|
||||
var actor = Sys.ActorOf(DriverHostActor.Props(
|
||||
db, TestNode, coordinator.Ref, localRoles: new HashSet<string> { "driver" },
|
||||
opcUaPublishActor: publish));
|
||||
|
||||
actor.Tell(new DispatchDeployment(deploymentId, RevA, CorrelationId.NewId()));
|
||||
|
||||
var ack = coordinator.ExpectMsg<ApplyAck>(PresenceBudget);
|
||||
ack.Outcome.ShouldBe(ApplyAckOutcome.Applied);
|
||||
ack.Degraded.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void No_outcome_within_the_timeout_acks_degraded_never_clean()
|
||||
{
|
||||
var (db, deploymentId) = SeedOneTagDeployment();
|
||||
var coordinator = CreateTestProbe();
|
||||
var publish = CreateTestProbe(); // never replies — a hung / lost apply
|
||||
var actor = Sys.ActorOf(DriverHostActor.Props(
|
||||
db, TestNode, coordinator.Ref, localRoles: new HashSet<string> { "driver" },
|
||||
opcUaPublishActor: publish.Ref, applyOutcomeTimeout: TimeSpan.FromMilliseconds(300)));
|
||||
|
||||
actor.Tell(new DispatchDeployment(deploymentId, RevA, CorrelationId.NewId()));
|
||||
|
||||
var ack = coordinator.ExpectMsg<ApplyAck>(PresenceBudget);
|
||||
ack.Outcome.ShouldBe(ApplyAckOutcome.Applied);
|
||||
ack.Degraded.ShouldBeTrue();
|
||||
ack.DegradedReason.ShouldNotBeNull();
|
||||
ack.DegradedReason.ShouldContain("did not report an outcome");
|
||||
|
||||
// A late outcome after the timeout must not produce a second ack.
|
||||
var rebuild = publish.FishForMessage<OpcUaPublishActor.RebuildAddressSpace>(_ => true, PresenceBudget);
|
||||
publish.Reply(new OpcUaPublishActor.RebuildCompleted(rebuild.Correlation, rebuild.DeploymentId, false, null));
|
||||
coordinator.ExpectNoMsg(TimeSpan.FromMilliseconds(300));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Redispatching_a_degraded_revision_reapplies_instead_of_acking_clean()
|
||||
{
|
||||
var (db, deploymentId) = SeedOneTagDeployment();
|
||||
var coordinator = CreateTestProbe();
|
||||
var publish = CreateTestProbe();
|
||||
var actor = Sys.ActorOf(DriverHostActor.Props(
|
||||
db, TestNode, coordinator.Ref, localRoles: new HashSet<string> { "driver" },
|
||||
opcUaPublishActor: publish.Ref));
|
||||
|
||||
actor.Tell(new DispatchDeployment(deploymentId, RevA, CorrelationId.NewId()));
|
||||
var first = publish.FishForMessage<OpcUaPublishActor.RebuildAddressSpace>(_ => true, PresenceBudget);
|
||||
publish.Reply(new OpcUaPublishActor.RebuildCompleted(first.Correlation, first.DeploymentId, true, "failedNodes=1"));
|
||||
coordinator.ExpectMsg<ApplyAck>(PresenceBudget).Degraded.ShouldBeTrue();
|
||||
|
||||
// Same revision again: the old idempotent short-circuit would ack a CLEAN Applied right here. It must
|
||||
// re-apply instead (a second rebuild request), and ack whatever THAT apply reports.
|
||||
actor.Tell(new DispatchDeployment(deploymentId, RevA, CorrelationId.NewId()));
|
||||
var second = publish.FishForMessage<OpcUaPublishActor.RebuildAddressSpace>(_ => true, PresenceBudget);
|
||||
coordinator.ExpectNoMsg(TimeSpan.FromMilliseconds(200));
|
||||
publish.Reply(new OpcUaPublishActor.RebuildCompleted(second.Correlation, second.DeploymentId, false, null));
|
||||
coordinator.ExpectMsg<ApplyAck>(PresenceBudget).Degraded.ShouldBeFalse();
|
||||
|
||||
// Now clean: a third dispatch of the same revision takes the short-circuit again (no rebuild).
|
||||
actor.Tell(new DispatchDeployment(deploymentId, RevA, CorrelationId.NewId()));
|
||||
var third = coordinator.ExpectMsg<ApplyAck>(PresenceBudget);
|
||||
third.Degraded.ShouldBeFalse();
|
||||
publish.ExpectNoMsg(TimeSpan.FromMilliseconds(200));
|
||||
}
|
||||
|
||||
private (IDbContextFactory<OtOpcUaConfigDbContext> Db, DeploymentId Id) SeedOneTagDeployment()
|
||||
{
|
||||
var db = NewInMemoryDbFactory();
|
||||
var id = V3ArtifactSeeder.SeedV3Deployment(db, RevA,
|
||||
rawTags: new[] { (Driver: "drv-1", DriverName: "Modbus", Device: "dev1", Tag: "speed", DataType: "Double", Writable: false) },
|
||||
unsRefs: new[] { (Area: "filling", Line: "line1", Equip: "station1", BackingTag: "speed", Effective: (string?)null) });
|
||||
return (db, id);
|
||||
}
|
||||
|
||||
/// <summary>A sink that throws from every node-creating call when <c>fault</c> is set.</summary>
|
||||
private sealed class FaultingSink(bool fault = true) : IOpcUaAddressSpaceSink
|
||||
{
|
||||
private void MaybeFault()
|
||||
{
|
||||
if (fault) throw new InvalidOperationException("simulated sink fault");
|
||||
}
|
||||
|
||||
public void WriteValue(string nodeId, object? value, uint statusCode, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void WriteAlarmQuality(string alarmNodeId, uint statusCode, DateTime sourceTimestampUtc, AddressSpaceRealm realm) { }
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime occurredUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, AddressSpaceRealm realm, bool isNative = false) => MaybeFault();
|
||||
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName, AddressSpaceRealm realm = AddressSpaceRealm.Uns) => MaybeFault();
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, AddressSpaceRealm realm, string? historianTagname = null, bool isArray = false, uint? arrayLength = null, bool refuseArrayOnScalar = false) => MaybeFault();
|
||||
public void RebuildAddressSpace() { }
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void WireAlarmNotifiers(string alarmNodeId, AddressSpaceRealm alarmRealm, IReadOnlyList<string> notifierFolderNodeIds, AddressSpaceRealm notifierFolderRealm) { }
|
||||
public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, string targetNodeId, AddressSpaceRealm targetRealm, string referenceType = "Organizes") { }
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -42,7 +42,7 @@ public sealed class DriverHostActorFetchAndCacheBootstrapTests : RuntimeActorTes
|
||||
var cached = new CachedDeploymentArtifact(
|
||||
DeploymentId.NewId().ToString(), CachedRev.Value, blob, DateTimeOffset.UtcNow.AddHours(-1));
|
||||
var cache = new StubArtifactCache(cached);
|
||||
var publishProbe = CreateTestProbe();
|
||||
var publishProbe = this.CreateCleanOutcomePublishProbe();
|
||||
|
||||
var actor = Sys.ActorOf(DriverHostActor.Props(
|
||||
new ThrowingDbFactory(), TestNode, ackRouter: null,
|
||||
|
||||
+3
-3
@@ -42,7 +42,7 @@ public sealed class DriverHostActorFetchAndCacheUnreadableTests : RuntimeActorTe
|
||||
[RevA.Value] = Artifact(),
|
||||
[RevB.Value] = null,
|
||||
});
|
||||
var publishProbe = CreateTestProbe();
|
||||
var publishProbe = this.CreateCleanOutcomePublishProbe();
|
||||
var coordinator = CreateTestProbe();
|
||||
|
||||
var actor = Sys.ActorOf(DriverHostActor.Props(
|
||||
@@ -72,7 +72,7 @@ public sealed class DriverHostActorFetchAndCacheUnreadableTests : RuntimeActorTe
|
||||
// empty blob handed to it directly: ReconcileDriversFromBlob's #485 guard fails the apply.
|
||||
var db = NewInMemoryDbFactory();
|
||||
var fetcher = new MapFetcher(new Dictionary<string, byte[]?> { [RevA.Value] = Array.Empty<byte>() });
|
||||
var publishProbe = CreateTestProbe();
|
||||
var publishProbe = this.CreateCleanOutcomePublishProbe();
|
||||
var coordinator = CreateTestProbe();
|
||||
|
||||
var actor = Sys.ActorOf(DriverHostActor.Props(
|
||||
@@ -95,7 +95,7 @@ public sealed class DriverHostActorFetchAndCacheUnreadableTests : RuntimeActorTe
|
||||
// A truncated/corrupt cached artifact surfaces as GetCurrentUnkeyedAsync == null (the cache's
|
||||
// integrity check is deliberately indistinguishable from a miss). Boot must land
|
||||
// Steady-no-revision and apply NOTHING — never a partial address space.
|
||||
var publishProbe = CreateTestProbe();
|
||||
var publishProbe = this.CreateCleanOutcomePublishProbe();
|
||||
|
||||
var actor = Sys.ActorOf(DriverHostActor.Props(
|
||||
new ThrowingDbFactory(), TestNode, ackRouter: null,
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@ public sealed class DriverHostActorHistoryWriterTests : RuntimeActorTestBase
|
||||
// A publish actor MUST be wired for SpawnVirtualTagHost to spawn the real host (its ctor
|
||||
// requires a non-null sink). No virtualTagHostOverride ⇒ the real VirtualTagHostActor is
|
||||
// spawned with _historyWriter threaded in.
|
||||
var publish = CreateTestProbe();
|
||||
var publish = this.CreateCleanOutcomePublishProbe();
|
||||
var writer = new CapturingHistoryWriter();
|
||||
|
||||
var actor = Sys.ActorOf(DriverHostActor.Props(
|
||||
|
||||
+1
-1
@@ -441,7 +441,7 @@ public sealed class DriverHostActorNativeAlarmTests : RuntimeActorTestBase
|
||||
IDbContextFactory<OtOpcUaConfigDbContext> db, DeploymentId deploymentId)
|
||||
{
|
||||
var coordinator = CreateTestProbe();
|
||||
var publish = CreateTestProbe();
|
||||
var publish = this.CreateCleanOutcomePublishProbe();
|
||||
var mux = CreateTestProbe();
|
||||
var vtHost = CreateTestProbe();
|
||||
|
||||
|
||||
+1
-1
@@ -251,7 +251,7 @@ public sealed class DriverHostActorPrimaryGateTests : RuntimeActorTestBase
|
||||
IDbContextFactory<OtOpcUaConfigDbContext> db, DeploymentId dep, int driverMemberCount)
|
||||
{
|
||||
var coordinator = CreateTestProbe();
|
||||
var publish = CreateTestProbe();
|
||||
var publish = this.CreateCleanOutcomePublishProbe();
|
||||
var mux = CreateTestProbe();
|
||||
var vtHost = CreateTestProbe();
|
||||
|
||||
|
||||
@@ -226,7 +226,7 @@ public sealed class DriverHostActorTests : RuntimeActorTestBase
|
||||
seeded.Length.ShouldBeGreaterThan(0);
|
||||
|
||||
var coordinator = CreateTestProbe();
|
||||
var publish = CreateTestProbe();
|
||||
var publish = this.CreateCleanOutcomePublishProbe();
|
||||
var actor = Sys.ActorOf(DriverHostActor.Props(
|
||||
db, TestNode, coordinator.Ref,
|
||||
localRoles: new HashSet<string> { "driver" },
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
using Akka.Actor;
|
||||
using Akka.TestKit;
|
||||
using ZB.MOM.WW.OtOpcUa.Runtime.OpcUa;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness;
|
||||
|
||||
/// <summary>
|
||||
/// Gitea #601 — since the deploy ack waits for <see cref="OpcUaPublishActor"/>'s
|
||||
/// <see cref="OpcUaPublishActor.RebuildCompleted"/> reply, a bare <see cref="TestProbe"/> standing in for the
|
||||
/// publish actor would hold every Applied ack until the outcome timeout (and then ack it degraded). Tests
|
||||
/// that are not ABOUT the ack's honesty use this probe instead: it still records every message exactly
|
||||
/// like a plain probe (the auto-pilot runs in addition to queueing), and answers each
|
||||
/// <see cref="OpcUaPublishActor.RebuildAddressSpace"/> that asks for an outcome with a CLEAN one — the
|
||||
/// reply the real publish actor gives a clean apply.
|
||||
/// </summary>
|
||||
public static class PublishProbes
|
||||
{
|
||||
/// <summary>Creates a publish-actor probe that reports a clean address-space outcome for every rebuild
|
||||
/// carrying <see cref="OpcUaPublishActor.RebuildAddressSpace.ReportOutcome"/>.</summary>
|
||||
/// <param name="kit">The calling TestKit.</param>
|
||||
/// <returns>The probe.</returns>
|
||||
public static TestProbe CreateCleanOutcomePublishProbe(this TestKitBase kit)
|
||||
{
|
||||
var probe = kit.CreateTestProbe();
|
||||
probe.SetAutoPilot(new DelegateAutoPilot((sender, message) =>
|
||||
{
|
||||
if (message is OpcUaPublishActor.RebuildAddressSpace { ReportOutcome: true } rebuild)
|
||||
sender.Tell(new OpcUaPublishActor.RebuildCompleted(
|
||||
rebuild.Correlation, rebuild.DeploymentId, Degraded: false, DegradedReason: null));
|
||||
return AutoPilot.KeepRunning;
|
||||
}));
|
||||
return probe;
|
||||
}
|
||||
}
|
||||
@@ -151,7 +151,7 @@ public static class V3ArtifactSeeder
|
||||
TimeSpan? reprovisionBackoff = null,
|
||||
TimeSpan? commsRecoveryAuditDelay = null)
|
||||
{
|
||||
var publish = kit.CreateTestProbe();
|
||||
var publish = kit.CreateCleanOutcomePublishProbe();
|
||||
var (actor, mux, coordinator) =
|
||||
Spawn(kit, db, localNode, publish.Ref, driverFactory, reprovisionBackoff, commsRecoveryAuditDelay);
|
||||
|
||||
|
||||
+1
-1
@@ -232,7 +232,7 @@ public sealed class PublishSeamEmitsToHubTests : RuntimeActorTestBase
|
||||
IDbContextFactory<OtOpcUaConfigDbContext> db, DeploymentId deploymentId, ITelemetryLocalHub hub)
|
||||
{
|
||||
var coordinator = CreateTestProbe();
|
||||
var publish = CreateTestProbe();
|
||||
var publish = this.CreateCleanOutcomePublishProbe();
|
||||
var mux = CreateTestProbe();
|
||||
var vtHost = CreateTestProbe();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user