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 ebb77b8d..f14ab7a9 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverInstanceActor.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverInstanceActor.cs
@@ -38,6 +38,10 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
/// Default cap on the number of post-connect re-discovery passes.
public const int DefaultRediscoverMaxAttempts = 15;
+ /// Default per-pass timeout for during
+ /// bounded post-connect re-discovery. Bounds the mailbox suspension time; production default 30 s.
+ public static readonly TimeSpan DefaultRediscoverDiscoverTimeout = TimeSpan.FromSeconds(30);
+
public sealed record InitializeRequested(string DriverConfigJson);
public sealed record InitializeSucceeded(int Generation);
public sealed record InitializeFailed(string Reason, int Generation);
@@ -135,6 +139,11 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
/// Cap on the number of post-connect re-discovery passes — a backstop so a never-stabilising
/// (or perpetually-empty) discovered set cannot spin the loop forever. Production default 15.
private readonly int _rediscoverMaxAttempts;
+
+ /// Per-pass timeout for during bounded post-connect
+ /// re-discovery. Bounds the mailbox suspension time. Production default 30 s; tests may inject a shorter
+ /// value. Stored to allow injection rather than hardcoding.
+ private readonly TimeSpan _rediscoverDiscoverTimeout;
private readonly ILoggingAdapter _log = Context.GetLogger();
private string? _currentConfigJson;
@@ -192,6 +201,7 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
/// defaults to an empty string when not provided (e.g. in unit tests).
/// Optional interval between post-connect re-discovery passes; defaults to 2 seconds.
/// Optional cap on re-discovery passes; defaults to 15.
+ /// Optional per-pass timeout for ; defaults to 30 seconds.
public static Props Props(
IDriver driver,
TimeSpan? reconnectInterval = null,
@@ -199,7 +209,8 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
IDriverHealthPublisher? healthPublisher = null,
string? clusterId = null,
TimeSpan? rediscoverInterval = null,
- int rediscoverMaxAttempts = DefaultRediscoverMaxAttempts) =>
+ int rediscoverMaxAttempts = DefaultRediscoverMaxAttempts,
+ TimeSpan? rediscoverDiscoverTimeout = null) =>
Akka.Actor.Props.Create(() => new DriverInstanceActor(
driver,
reconnectInterval ?? DefaultReconnectInterval,
@@ -207,7 +218,8 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
healthPublisher ?? NullDriverHealthPublisher.Instance,
clusterId ?? string.Empty,
rediscoverInterval,
- rediscoverMaxAttempts));
+ rediscoverMaxAttempts,
+ rediscoverDiscoverTimeout));
///
/// Returns true when the driver should boot in DEV-STUB mode based on host platform and
@@ -241,6 +253,7 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
/// Cluster identifier forwarded in health snapshots.
/// Interval between post-connect re-discovery passes; defaults to 2 seconds.
/// Cap on the number of re-discovery passes; defaults to 15.
+ /// Per-pass timeout for ; defaults to 30 seconds.
public DriverInstanceActor(
IDriver driver,
TimeSpan reconnectInterval,
@@ -248,7 +261,8 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
IDriverHealthPublisher? healthPublisher = null,
string? clusterId = null,
TimeSpan? rediscoverInterval = null,
- int rediscoverMaxAttempts = DefaultRediscoverMaxAttempts)
+ int rediscoverMaxAttempts = DefaultRediscoverMaxAttempts,
+ TimeSpan? rediscoverDiscoverTimeout = null)
{
_driver = driver;
_driverInstanceId = driver.DriverInstanceId;
@@ -257,6 +271,7 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
_reconnectInterval = reconnectInterval;
_rediscoverInterval = rediscoverInterval ?? DefaultRediscoverInterval;
_rediscoverMaxAttempts = rediscoverMaxAttempts;
+ _rediscoverDiscoverTimeout = rediscoverDiscoverTimeout ?? DefaultRediscoverDiscoverTimeout;
OtOpcUaTelemetry.DriverInstanceLifecycle.Add(1,
new KeyValuePair("event", startStubbed ? "spawn_stub" : "spawn"),
new KeyValuePair("driver_type", driver.DriverType));
@@ -762,7 +777,7 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
var builder = new CapturingAddressSpaceBuilder();
// Bound the browse — ReceiveAsync suspends the mailbox for the whole handler, so an unbounded
// DiscoverAsync would block DisconnectObserved / ForceReconnect / writes / health-poll behind it.
- using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
+ using var cts = new CancellationTokenSource(_rediscoverDiscoverTimeout);
// NO ConfigureAwait(false): a genuinely-async DiscoverAsync (Galaxy / OpcUaClient / TwinCAT) must
// resume on the actor task scheduler so the Context.Parent.Tell + Timers calls below run with a
// live ActorContext. ConfigureAwait(false) would resume off-context and throw
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 e319cc66..70ff8938 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,32 @@ public sealed class DriverInstanceActorDiscoveryTests : RuntimeActorTestBase
driver.DiscoverCount.ShouldBe(3);
}
+ ///
+ /// 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).
+ /// Wiring is verified by constructing via Props with a custom value and confirming the actor starts
+ /// and begins discovery normally.
+ ///
+ [Fact]
+ public void Discovery_timeout_default_constant_is_30s_and_Props_accepts_custom_value()
+ {
+ // The constant must exist and preserve the pre-refactor 30 s literal.
+ DriverInstanceActor.DefaultRediscoverDiscoverTimeout.ShouldBe(TimeSpan.FromSeconds(30));
+
+ // Props must accept the new optional parameter — no throw and actor starts normally.
+ var driver = new DiscoverableStubDriver();
+ var parent = CreateTestProbe();
+ var actor = parent.ChildActorOf(DriverInstanceActor.Props(
+ driver,
+ rediscoverInterval: TimeSpan.FromMilliseconds(20),
+ rediscoverDiscoverTimeout: TimeSpan.FromSeconds(5)));
+
+ actor.Tell(new DriverInstanceActor.InitializeRequested("{}"));
+
+ // Actor starts and discovery publishes — confirms the custom timeout was wired without error.
+ parent.ExpectMsg(TimeSpan.FromSeconds(2));
+ }
+
///
/// A that also exposes . Each DiscoverAsync
/// pass is counted; passes 1–2 yield nothing (cache warming), passes 3+ yield a stable 3-node set —