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 f14ab7a9..4228cec7 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverInstanceActor.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverInstanceActor.cs @@ -745,12 +745,19 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers /// driver exposes (nothing to inject otherwise). Self-sends the first /// tagged with the current init generation so a tick that outlives a reconnect /// is rejected by the generation guard in . - /// Generic by design: re-discovery runs for EVERY driver on each - /// (re)connect, bounded by stop-on-stable (the discovered-set signature repeats) + the attempt cap. - /// Narrowing this to opt-in for heavy network drivers (Galaxy / OpcUaClient) is a follow-up. + /// Honours the driver's : Never opts out entirely + /// (no tick scheduled); Once runs a single pass (the loop stops after the first publish in + /// ); UntilStable retries each (re)connect, bounded by + /// stop-on-stable (the discovered-set signature repeats) + the attempt cap. private void StartDiscovery() { - if (_driver is not ITagDiscovery) return; // driver doesn't expose discovery — nothing to inject + if (_driver is not ITagDiscovery discovery) return; // driver doesn't expose discovery — nothing to inject + if (discovery.RediscoverPolicy == DiscoveryRediscoverPolicy.Never) + { + // Driver opts out of post-connect discovery — don't even schedule the first tick. + _log.Debug("DriverInstance {Id}: RediscoverPolicy=Never — skipping post-connect discovery", _driverInstanceId); + return; + } Self.Tell(new RediscoverTick(_initGeneration, Attempt: 0, PreviousSignature: string.Empty)); } @@ -798,6 +805,16 @@ 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. + if (discovery.RediscoverPolicy == DiscoveryRediscoverPolicy.Once) + { + _log.Debug("DriverInstance {Id}: RediscoverPolicy=Once — single discovery pass, not scheduling another", _driverInstanceId); + return; + } + // Stop when the non-empty discovered SET has stabilised (its signature repeats), or the attempt cap // is hit. Keep retrying while empty (a FixedTree cache may still be populating). First tick carries "". var signature = string.Join('\u0001', 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 70ff8938..3673c792 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 @@ -171,6 +171,56 @@ public sealed class DriverInstanceActorDiscoveryTests : RuntimeActorTestBase driver.DiscoverCount.ShouldBe(3); } + /// + /// A driver whose is + /// opts out of post-connect discovery entirely: the + /// Connected entry's discovery kick returns before scheduling the first tick, so the driver is never + /// asked to discover and the parent receives no . + /// + [Fact] + public void Discovery_policy_Never_runs_no_passes_and_publishes_nothing() + { + var driver = new DiscoverableStubDriver(DiscoveryRediscoverPolicy.Never); + var parent = CreateTestProbe(); + var actor = parent.ChildActorOf(DriverInstanceActor.Props( + driver, rediscoverInterval: TimeSpan.FromMilliseconds(20))); + + actor.Tell(new DriverInstanceActor.InitializeRequested("{}")); + // Connect happened (the discovery decision is made on the Connected entry)... + AwaitCondition(() => driver.InitializeCount > 0, TimeSpan.FromSeconds(2)); + + // ...but policy=Never ⇒ no discovery pass is ever run and nothing is published. + parent.ExpectNoMsg(TimeSpan.FromMilliseconds(300)); + driver.DiscoverCount.ShouldBe(0); + } + + /// + /// A driver whose is + /// runs EXACTLY one post-connect pass even when its + /// discovered set would keep growing forever — under UntilStable the never-repeating signature + /// would retry to the attempt cap. Exactly one + /// is published and no further RediscoverTick is scheduled. + /// + [Fact] + public void Discovery_policy_Once_publishes_exactly_one_pass_even_when_set_keeps_growing() + { + var driver = new GrowingDiscoverableStubDriver(DiscoveryRediscoverPolicy.Once); + var parent = CreateTestProbe(); + var actor = parent.ChildActorOf(DriverInstanceActor.Props( + driver, rediscoverInterval: TimeSpan.FromMilliseconds(20))); + + actor.Tell(new DriverInstanceActor.InitializeRequested("{}")); + + // Exactly one pass is published (the first, growing set → 1 node)... + var only = parent.ExpectMsg(TimeSpan.FromSeconds(2)); + only.Nodes.Count.ShouldBe(1); + only.DriverInstanceId.ShouldBe(driver.DriverInstanceId); + + // ...and NO second tick is scheduled, even though the set would keep growing under UntilStable. + parent.ExpectNoMsg(TimeSpan.FromMilliseconds(300)); + driver.DiscoverCount.ShouldBe(1); + } + /// /// 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). @@ -206,6 +256,15 @@ public sealed class DriverInstanceActorDiscoveryTests : RuntimeActorTestBase { private int _passCount; + /// Constructs the fake reporting the given ; + /// defaults to (the interface default) so the + /// existing UntilStable tests are unaffected. + public DiscoverableStubDriver(DiscoveryRediscoverPolicy policy = DiscoveryRediscoverPolicy.UntilStable) + => RediscoverPolicy = policy; + + /// The post-connect re-discovery policy this fake reports to the actor. + public DiscoveryRediscoverPolicy RediscoverPolicy { get; } + /// Number of passes the actor has driven. public int DiscoverCount => Volatile.Read(ref _passCount); @@ -266,6 +325,16 @@ public sealed class DriverInstanceActorDiscoveryTests : RuntimeActorTestBase { private int _passCount; + /// Constructs the fake reporting the given ; + /// defaults to (the interface default) so the + /// existing attempt-cap test is unaffected. With the + /// ever-growing set proves the actor stops after a single pass (UntilStable would keep retrying). + public GrowingDiscoverableStubDriver(DiscoveryRediscoverPolicy policy = DiscoveryRediscoverPolicy.UntilStable) + => RediscoverPolicy = policy; + + /// The post-connect re-discovery policy this fake reports to the actor. + public DiscoveryRediscoverPolicy RediscoverPolicy { get; } + /// Number of passes the actor has driven. public int DiscoverCount => Volatile.Read(ref _passCount);