feat(otopcua): make FixedTree re-discovery per-pass timeout injectable (follow-up A)
This commit is contained in:
@@ -38,6 +38,10 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
|
|||||||
/// <summary>Default cap on the number of post-connect re-discovery passes.</summary>
|
/// <summary>Default cap on the number of post-connect re-discovery passes.</summary>
|
||||||
public const int DefaultRediscoverMaxAttempts = 15;
|
public const int DefaultRediscoverMaxAttempts = 15;
|
||||||
|
|
||||||
|
/// <summary>Default per-pass timeout for <see cref="ITagDiscovery.DiscoverAsync"/> during
|
||||||
|
/// bounded post-connect re-discovery. Bounds the mailbox suspension time; production default 30 s.</summary>
|
||||||
|
public static readonly TimeSpan DefaultRediscoverDiscoverTimeout = TimeSpan.FromSeconds(30);
|
||||||
|
|
||||||
public sealed record InitializeRequested(string DriverConfigJson);
|
public sealed record InitializeRequested(string DriverConfigJson);
|
||||||
public sealed record InitializeSucceeded(int Generation);
|
public sealed record InitializeSucceeded(int Generation);
|
||||||
public sealed record InitializeFailed(string Reason, int Generation);
|
public sealed record InitializeFailed(string Reason, int Generation);
|
||||||
@@ -135,6 +139,11 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
|
|||||||
/// <summary>Cap on the number of post-connect re-discovery passes — a backstop so a never-stabilising
|
/// <summary>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.</summary>
|
/// (or perpetually-empty) discovered set cannot spin the loop forever. Production default 15.</summary>
|
||||||
private readonly int _rediscoverMaxAttempts;
|
private readonly int _rediscoverMaxAttempts;
|
||||||
|
|
||||||
|
/// <summary>Per-pass timeout for <see cref="ITagDiscovery.DiscoverAsync"/> 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.</summary>
|
||||||
|
private readonly TimeSpan _rediscoverDiscoverTimeout;
|
||||||
private readonly ILoggingAdapter _log = Context.GetLogger();
|
private readonly ILoggingAdapter _log = Context.GetLogger();
|
||||||
private string? _currentConfigJson;
|
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).</param>
|
/// defaults to an empty string when not provided (e.g. in unit tests).</param>
|
||||||
/// <param name="rediscoverInterval">Optional interval between post-connect re-discovery passes; defaults to 2 seconds.</param>
|
/// <param name="rediscoverInterval">Optional interval between post-connect re-discovery passes; defaults to 2 seconds.</param>
|
||||||
/// <param name="rediscoverMaxAttempts">Optional cap on re-discovery passes; defaults to 15.</param>
|
/// <param name="rediscoverMaxAttempts">Optional cap on re-discovery passes; defaults to 15.</param>
|
||||||
|
/// <param name="rediscoverDiscoverTimeout">Optional per-pass timeout for <see cref="ITagDiscovery.DiscoverAsync"/>; defaults to 30 seconds.</param>
|
||||||
public static Props Props(
|
public static Props Props(
|
||||||
IDriver driver,
|
IDriver driver,
|
||||||
TimeSpan? reconnectInterval = null,
|
TimeSpan? reconnectInterval = null,
|
||||||
@@ -199,7 +209,8 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
|
|||||||
IDriverHealthPublisher? healthPublisher = null,
|
IDriverHealthPublisher? healthPublisher = null,
|
||||||
string? clusterId = null,
|
string? clusterId = null,
|
||||||
TimeSpan? rediscoverInterval = null,
|
TimeSpan? rediscoverInterval = null,
|
||||||
int rediscoverMaxAttempts = DefaultRediscoverMaxAttempts) =>
|
int rediscoverMaxAttempts = DefaultRediscoverMaxAttempts,
|
||||||
|
TimeSpan? rediscoverDiscoverTimeout = null) =>
|
||||||
Akka.Actor.Props.Create(() => new DriverInstanceActor(
|
Akka.Actor.Props.Create(() => new DriverInstanceActor(
|
||||||
driver,
|
driver,
|
||||||
reconnectInterval ?? DefaultReconnectInterval,
|
reconnectInterval ?? DefaultReconnectInterval,
|
||||||
@@ -207,7 +218,8 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
|
|||||||
healthPublisher ?? NullDriverHealthPublisher.Instance,
|
healthPublisher ?? NullDriverHealthPublisher.Instance,
|
||||||
clusterId ?? string.Empty,
|
clusterId ?? string.Empty,
|
||||||
rediscoverInterval,
|
rediscoverInterval,
|
||||||
rediscoverMaxAttempts));
|
rediscoverMaxAttempts,
|
||||||
|
rediscoverDiscoverTimeout));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns true when the driver should boot in DEV-STUB mode based on host platform and
|
/// 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
|
|||||||
/// <param name="clusterId">Cluster identifier forwarded in health snapshots.</param>
|
/// <param name="clusterId">Cluster identifier forwarded in health snapshots.</param>
|
||||||
/// <param name="rediscoverInterval">Interval between post-connect re-discovery passes; defaults to 2 seconds.</param>
|
/// <param name="rediscoverInterval">Interval between post-connect re-discovery passes; defaults to 2 seconds.</param>
|
||||||
/// <param name="rediscoverMaxAttempts">Cap on the number of re-discovery passes; defaults to 15.</param>
|
/// <param name="rediscoverMaxAttempts">Cap on the number of re-discovery passes; defaults to 15.</param>
|
||||||
|
/// <param name="rediscoverDiscoverTimeout">Per-pass timeout for <see cref="ITagDiscovery.DiscoverAsync"/>; defaults to 30 seconds.</param>
|
||||||
public DriverInstanceActor(
|
public DriverInstanceActor(
|
||||||
IDriver driver,
|
IDriver driver,
|
||||||
TimeSpan reconnectInterval,
|
TimeSpan reconnectInterval,
|
||||||
@@ -248,7 +261,8 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
|
|||||||
IDriverHealthPublisher? healthPublisher = null,
|
IDriverHealthPublisher? healthPublisher = null,
|
||||||
string? clusterId = null,
|
string? clusterId = null,
|
||||||
TimeSpan? rediscoverInterval = null,
|
TimeSpan? rediscoverInterval = null,
|
||||||
int rediscoverMaxAttempts = DefaultRediscoverMaxAttempts)
|
int rediscoverMaxAttempts = DefaultRediscoverMaxAttempts,
|
||||||
|
TimeSpan? rediscoverDiscoverTimeout = null)
|
||||||
{
|
{
|
||||||
_driver = driver;
|
_driver = driver;
|
||||||
_driverInstanceId = driver.DriverInstanceId;
|
_driverInstanceId = driver.DriverInstanceId;
|
||||||
@@ -257,6 +271,7 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
|
|||||||
_reconnectInterval = reconnectInterval;
|
_reconnectInterval = reconnectInterval;
|
||||||
_rediscoverInterval = rediscoverInterval ?? DefaultRediscoverInterval;
|
_rediscoverInterval = rediscoverInterval ?? DefaultRediscoverInterval;
|
||||||
_rediscoverMaxAttempts = rediscoverMaxAttempts;
|
_rediscoverMaxAttempts = rediscoverMaxAttempts;
|
||||||
|
_rediscoverDiscoverTimeout = rediscoverDiscoverTimeout ?? DefaultRediscoverDiscoverTimeout;
|
||||||
OtOpcUaTelemetry.DriverInstanceLifecycle.Add(1,
|
OtOpcUaTelemetry.DriverInstanceLifecycle.Add(1,
|
||||||
new KeyValuePair<string, object?>("event", startStubbed ? "spawn_stub" : "spawn"),
|
new KeyValuePair<string, object?>("event", startStubbed ? "spawn_stub" : "spawn"),
|
||||||
new KeyValuePair<string, object?>("driver_type", driver.DriverType));
|
new KeyValuePair<string, object?>("driver_type", driver.DriverType));
|
||||||
@@ -762,7 +777,7 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
|
|||||||
var builder = new CapturingAddressSpaceBuilder();
|
var builder = new CapturingAddressSpaceBuilder();
|
||||||
// Bound the browse — ReceiveAsync suspends the mailbox for the whole handler, so an unbounded
|
// Bound the browse — ReceiveAsync suspends the mailbox for the whole handler, so an unbounded
|
||||||
// DiscoverAsync would block DisconnectObserved / ForceReconnect / writes / health-poll behind it.
|
// 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
|
// 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
|
// 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
|
// live ActorContext. ConfigureAwait(false) would resume off-context and throw
|
||||||
|
|||||||
+26
@@ -171,6 +171,32 @@ public sealed class DriverInstanceActorDiscoveryTests : RuntimeActorTestBase
|
|||||||
driver.DiscoverCount.ShouldBe(3);
|
driver.DiscoverCount.ShouldBe(3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The per-pass discovery timeout is injectable via <see cref="DriverInstanceActor.Props"/> so tests
|
||||||
|
/// can control it without real-time delays. The default constant must be 30 seconds (behaviour-preserving).
|
||||||
|
/// Wiring is verified by constructing via <c>Props</c> with a custom value and confirming the actor starts
|
||||||
|
/// and begins discovery normally.
|
||||||
|
/// </summary>
|
||||||
|
[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<DriverInstanceActor.DiscoveredNodesReady>(TimeSpan.FromSeconds(2));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A <see cref="StubDriver"/> that also exposes <see cref="ITagDiscovery"/>. Each <c>DiscoverAsync</c>
|
/// A <see cref="StubDriver"/> that also exposes <see cref="ITagDiscovery"/>. Each <c>DiscoverAsync</c>
|
||||||
/// pass is counted; passes 1–2 yield nothing (cache warming), passes 3+ yield a stable 3-node set —
|
/// pass is counted; passes 1–2 yield nothing (cache warming), passes 3+ yield a stable 3-node set —
|
||||||
|
|||||||
Reference in New Issue
Block a user