feat(otopcua): DriverInstanceActor honors RediscoverPolicy (Never/Once/UntilStable) (follow-up B)
This commit is contained in:
@@ -745,12 +745,19 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
|
||||
/// driver exposes <see cref="ITagDiscovery"/> (nothing to inject otherwise). Self-sends the first
|
||||
/// <see cref="RediscoverTick"/> tagged with the current init generation so a tick that outlives a reconnect
|
||||
/// is rejected by the generation guard in <see cref="HandleRediscoverAsync"/>.
|
||||
/// <para>Generic by design: re-discovery runs for EVERY <see cref="ITagDiscovery"/> 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.</para></summary>
|
||||
/// <para>Honours the driver's <see cref="ITagDiscovery.RediscoverPolicy"/>: <c>Never</c> opts out entirely
|
||||
/// (no tick scheduled); <c>Once</c> runs a single pass (the loop stops after the first publish in
|
||||
/// <see cref="HandleRediscoverAsync"/>); <c>UntilStable</c> retries each (re)connect, bounded by
|
||||
/// stop-on-stable (the discovered-set signature repeats) + the attempt cap.</para></summary>
|
||||
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',
|
||||
|
||||
+69
@@ -171,6 +171,56 @@ public sealed class DriverInstanceActorDiscoveryTests : RuntimeActorTestBase
|
||||
driver.DiscoverCount.ShouldBe(3);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A driver whose <see cref="ITagDiscovery.RediscoverPolicy"/> is
|
||||
/// <see cref="DiscoveryRediscoverPolicy.Never"/> 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 <see cref="DriverInstanceActor.DiscoveredNodesReady"/>.
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A driver whose <see cref="ITagDiscovery.RediscoverPolicy"/> is
|
||||
/// <see cref="DiscoveryRediscoverPolicy.Once"/> runs EXACTLY one post-connect pass even when its
|
||||
/// discovered set would keep growing forever — under <c>UntilStable</c> the never-repeating signature
|
||||
/// would retry to the attempt cap. Exactly one <see cref="DriverInstanceActor.DiscoveredNodesReady"/>
|
||||
/// is published and no further <c>RediscoverTick</c> is scheduled.
|
||||
/// </summary>
|
||||
[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<DriverInstanceActor.DiscoveredNodesReady>(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);
|
||||
}
|
||||
|
||||
/// <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).
|
||||
@@ -206,6 +256,15 @@ public sealed class DriverInstanceActorDiscoveryTests : RuntimeActorTestBase
|
||||
{
|
||||
private int _passCount;
|
||||
|
||||
/// <summary>Constructs the fake reporting the given <see cref="DiscoveryRediscoverPolicy"/>;
|
||||
/// defaults to <see cref="DiscoveryRediscoverPolicy.UntilStable"/> (the interface default) so the
|
||||
/// existing UntilStable tests are unaffected.</summary>
|
||||
public DiscoverableStubDriver(DiscoveryRediscoverPolicy policy = DiscoveryRediscoverPolicy.UntilStable)
|
||||
=> RediscoverPolicy = policy;
|
||||
|
||||
/// <summary>The post-connect re-discovery policy this fake reports to the actor.</summary>
|
||||
public DiscoveryRediscoverPolicy RediscoverPolicy { get; }
|
||||
|
||||
/// <summary>Number of <see cref="DiscoverAsync"/> passes the actor has driven.</summary>
|
||||
public int DiscoverCount => Volatile.Read(ref _passCount);
|
||||
|
||||
@@ -266,6 +325,16 @@ public sealed class DriverInstanceActorDiscoveryTests : RuntimeActorTestBase
|
||||
{
|
||||
private int _passCount;
|
||||
|
||||
/// <summary>Constructs the fake reporting the given <see cref="DiscoveryRediscoverPolicy"/>;
|
||||
/// defaults to <see cref="DiscoveryRediscoverPolicy.UntilStable"/> (the interface default) so the
|
||||
/// existing attempt-cap test is unaffected. With <see cref="DiscoveryRediscoverPolicy.Once"/> the
|
||||
/// ever-growing set proves the actor stops after a single pass (UntilStable would keep retrying).</summary>
|
||||
public GrowingDiscoverableStubDriver(DiscoveryRediscoverPolicy policy = DiscoveryRediscoverPolicy.UntilStable)
|
||||
=> RediscoverPolicy = policy;
|
||||
|
||||
/// <summary>The post-connect re-discovery policy this fake reports to the actor.</summary>
|
||||
public DiscoveryRediscoverPolicy RediscoverPolicy { get; }
|
||||
|
||||
/// <summary>Number of <see cref="DiscoverAsync"/> passes the actor has driven.</summary>
|
||||
public int DiscoverCount => Volatile.Read(ref _passCount);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user