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);