diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverInstanceActor.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverInstanceActor.cs index 4228cec7..1b134937 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverInstanceActor.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverInstanceActor.cs @@ -805,10 +805,11 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers Context.Parent.Tell(new DiscoveredNodesReady(_driverInstanceId, nodes)); - // Honour the driver's re-discovery policy. A Once driver discovers synchronously in its single - // DiscoverAsync, so a single published pass is complete — do not schedule another tick. (Never never - // reaches here — StartDiscovery returns before the first tick.) UntilStable falls through to the - // stop-on-stable + attempt-cap logic below. + // Honour the driver's re-discovery policy. A Once driver runs a single post-connect pass per + // (re)connect regardless of whether DiscoverAsync is synchronous or async — one published pass is + // complete, so the retry loop is skipped (no further tick scheduled). (Never never reaches here — + // StartDiscovery returns before the first tick.) UntilStable falls through to the stop-on-stable + + // attempt-cap logic below. if (discovery.RediscoverPolicy == DiscoveryRediscoverPolicy.Once) { _log.Debug("DriverInstance {Id}: RediscoverPolicy=Once — single discovery pass, not scheduling another", _driverInstanceId); diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverInstanceActorDiscoveryTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverInstanceActorDiscoveryTests.cs index 3673c792..11784ca0 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverInstanceActorDiscoveryTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverInstanceActorDiscoveryTests.cs @@ -221,6 +221,51 @@ public sealed class DriverInstanceActorDiscoveryTests : RuntimeActorTestBase driver.DiscoverCount.ShouldBe(1); } + /// + /// means one pass PER (re)connect cycle — not one pass + /// ever. After the initial single pass settles, a + /// drives the actor through Reconnecting and back to Connected (via the auto retry-connect timer), and + /// StartDiscovery re-kicks discovery — which must run EXACTLY ONE more pass, not the full attempt + /// cap. Uses the ever-growing fake with a small cap (3): under a (wrong) policy-ignoring loop the + /// never-stabilising set would publish 3 passes per connect, so a single post-reconnect pass proves + /// Once is honoured on the reconnect path too. Guards the exact StartDiscovery-on-reconnect path + /// the follow-on TriggerRediscovery task touches. + /// + [Fact] + public void Discovery_policy_Once_reruns_one_pass_on_reconnect() + { + var driver = new GrowingDiscoverableStubDriver(DiscoveryRediscoverPolicy.Once); + var parent = CreateTestProbe(); + // Small reconnect + rediscover intervals so the cycle runs fast; cap 3 so a (wrong) full loop is + // visibly more than the one pass Once must run per (re)connect. + var actor = parent.ChildActorOf(DriverInstanceActor.Props( + driver, + reconnectInterval: TimeSpan.FromMilliseconds(50), + rediscoverInterval: TimeSpan.FromMilliseconds(20), + rediscoverMaxAttempts: 3)); + + actor.Tell(new DriverInstanceActor.InitializeRequested("{}")); + + // Initial connect: Once ⇒ exactly one pass (growing set → 1 node), then no more. + var first = parent.ExpectMsg(TimeSpan.FromSeconds(2)); + first.Nodes.Count.ShouldBe(1); + parent.ExpectNoMsg(TimeSpan.FromMilliseconds(200)); + driver.DiscoverCount.ShouldBe(1); + + // Force a reconnect: Connected → Reconnecting → (auto retry-connect) → Connected again. + actor.Tell(new DriverInstanceActor.ForceReconnect()); + + // Once = one pass PER (re)connect: exactly ONE additional pass after the reconnect, NOT the full cap. + // The set keeps growing across the reconnect (same driver instance), so this pass yields 2 nodes. + var afterReconnect = parent.ExpectMsg(TimeSpan.FromSeconds(3)); + afterReconnect.Nodes.Count.ShouldBe(2); + afterReconnect.DriverInstanceId.ShouldBe(driver.DriverInstanceId); + + // No further passes — Once did NOT run the attempt cap on reconnect; one pass per connect cycle. + parent.ExpectNoMsg(TimeSpan.FromMilliseconds(300)); + driver.DiscoverCount.ShouldBe(2); + } + /// /// The per-pass discovery timeout is injectable via so tests /// can control it without real-time delays. The default constant must be 30 seconds (behaviour-preserving).