feat(otopcua): DriverInstanceActor.TriggerRediscovery message (follow-up C)
This commit is contained in:
@@ -113,6 +113,16 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
|
|||||||
/// the parent dedups and injection is idempotent.</summary>
|
/// the parent dedups and injection is idempotent.</summary>
|
||||||
public sealed record DiscoveredNodesReady(string DriverInstanceId, IReadOnlyList<DiscoveredNode> Nodes);
|
public sealed record DiscoveredNodesReady(string DriverInstanceId, IReadOnlyList<DiscoveredNode> Nodes);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sent by <see cref="DriverHostActor"/> to ask this driver child to re-run post-connect discovery
|
||||||
|
/// after the host rebinds the driver to a new equipment. Handled only in <c>Connected</c>, where it
|
||||||
|
/// re-kicks <see cref="StartDiscovery"/> — which already honours the driver's
|
||||||
|
/// <see cref="ITagDiscovery.RediscoverPolicy"/> and the <see cref="ITagDiscovery"/> guard, tagging the
|
||||||
|
/// fresh pass with the current init generation. In any non-Connected state it is a deliberate no-op:
|
||||||
|
/// the driver's eventual (re)connect re-discovers anyway, so there is nothing to do and nothing to log.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record TriggerRediscovery;
|
||||||
|
|
||||||
/// <summary>Internal self-tick driving bounded post-connect re-discovery (FixedTree populates ~0–2s after connect).
|
/// <summary>Internal self-tick driving bounded post-connect re-discovery (FixedTree populates ~0–2s after connect).
|
||||||
/// <paramref name="PreviousSignature"/> is the ordered-distinct full-reference signature of the prior pass's
|
/// <paramref name="PreviousSignature"/> is the ordered-distinct full-reference signature of the prior pass's
|
||||||
/// captured set (empty string on the first tick); re-discovery stops once a non-empty set repeats it.</summary>
|
/// captured set (empty string on the first tick); re-discovery stops once a non-empty set repeats it.</summary>
|
||||||
@@ -312,6 +322,8 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
|
|||||||
// Stubbed drivers never enter Connected, so they never kick discovery; swallow defensively in case a
|
// Stubbed drivers never enter Connected, so they never kick discovery; swallow defensively in case a
|
||||||
// re-discovery self-tick is ever routed here so it doesn't surface as an Akka Unhandled message.
|
// re-discovery self-tick is ever routed here so it doesn't surface as an Akka Unhandled message.
|
||||||
Receive<RediscoverTick>(_ => { });
|
Receive<RediscoverTick>(_ => { });
|
||||||
|
// A TriggerRediscovery is meaningless to a stubbed (never-Connected) driver — silently ignore it.
|
||||||
|
Receive<TriggerRediscovery>(_ => { });
|
||||||
Receive<HealthPollTick>(_ => PublishHealthSnapshot());
|
Receive<HealthPollTick>(_ => PublishHealthSnapshot());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,6 +380,9 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
|
|||||||
// Likewise the attempt-0 re-discovery self-tick (sent on Connected entry) can be overtaken by an
|
// Likewise the attempt-0 re-discovery self-tick (sent on Connected entry) can be overtaken by an
|
||||||
// already-queued disconnect; swallow it — the next Connected entry re-kicks discovery.
|
// already-queued disconnect; swallow it — the next Connected entry re-kicks discovery.
|
||||||
Receive<RediscoverTick>(_ => { });
|
Receive<RediscoverTick>(_ => { });
|
||||||
|
// A TriggerRediscovery arriving while not Connected is a deliberate no-op — the (re)connect path
|
||||||
|
// re-runs discovery anyway. Swallow it so it stays a clean silent no-op (no Unhandled event).
|
||||||
|
Receive<TriggerRediscovery>(_ => { });
|
||||||
Receive<HealthPollTick>(_ => PublishHealthSnapshot());
|
Receive<HealthPollTick>(_ => PublishHealthSnapshot());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,6 +408,10 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
|
|||||||
PublishHealthSnapshot();
|
PublishHealthSnapshot();
|
||||||
});
|
});
|
||||||
ReceiveAsync<RediscoverTick>(HandleRediscoverAsync);
|
ReceiveAsync<RediscoverTick>(HandleRediscoverAsync);
|
||||||
|
// The host asks for a fresh discovery pass after rebinding the driver to a new equipment. Re-kick the
|
||||||
|
// bounded loop via StartDiscovery (honours RediscoverPolicy + the ITagDiscovery guard, tagged with the
|
||||||
|
// current _initGeneration). Only handled here in Connected — non-Connected states no-op it below.
|
||||||
|
Receive<TriggerRediscovery>(_ => StartDiscovery());
|
||||||
ReceiveAsync<WriteAttribute>(HandleWriteAsync);
|
ReceiveAsync<WriteAttribute>(HandleWriteAsync);
|
||||||
ReceiveAsync<RouteAlarmAck>(HandleAcknowledgeAsync);
|
ReceiveAsync<RouteAlarmAck>(HandleAcknowledgeAsync);
|
||||||
ReceiveAsync<Subscribe>(HandleSubscribeAsync);
|
ReceiveAsync<Subscribe>(HandleSubscribeAsync);
|
||||||
@@ -476,6 +495,9 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
|
|||||||
// Likewise the attempt-0 re-discovery self-tick (sent on Connected entry) can be overtaken by an
|
// Likewise the attempt-0 re-discovery self-tick (sent on Connected entry) can be overtaken by an
|
||||||
// already-queued disconnect; swallow it — the next Connected entry re-kicks discovery.
|
// already-queued disconnect; swallow it — the next Connected entry re-kicks discovery.
|
||||||
Receive<RediscoverTick>(_ => { });
|
Receive<RediscoverTick>(_ => { });
|
||||||
|
// A TriggerRediscovery arriving while not Connected is a deliberate no-op — the (re)connect path
|
||||||
|
// re-runs discovery anyway. Swallow it so it stays a clean silent no-op (no Unhandled event).
|
||||||
|
Receive<TriggerRediscovery>(_ => { });
|
||||||
Receive<HealthPollTick>(_ => PublishHealthSnapshot());
|
Receive<HealthPollTick>(_ => PublishHealthSnapshot());
|
||||||
Timers.StartPeriodicTimer("retry-connect", RetryConnect.Instance, _reconnectInterval);
|
Timers.StartPeriodicTimer("retry-connect", RetryConnect.Instance, _reconnectInterval);
|
||||||
}
|
}
|
||||||
|
|||||||
+87
@@ -292,6 +292,93 @@ public sealed class DriverInstanceActorDiscoveryTests : RuntimeActorTestBase
|
|||||||
parent.ExpectMsg<DriverInstanceActor.DiscoveredNodesReady>(TimeSpan.FromSeconds(2));
|
parent.ExpectMsg<DriverInstanceActor.DiscoveredNodesReady>(TimeSpan.FromSeconds(2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <see cref="DriverInstanceActor.TriggerRediscovery"/> received while Connected re-kicks the
|
||||||
|
/// post-connect discovery loop: after the initial discovery has settled, sending the message drives a
|
||||||
|
/// FRESH discovery pass — the driver's <c>DiscoverCount</c> advances and a new
|
||||||
|
/// <see cref="DriverInstanceActor.DiscoveredNodesReady"/> is published. This is the message
|
||||||
|
/// <see cref="DriverHostActor"/> uses to re-run discovery after rebinding the driver to a new equipment.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void TriggerRediscovery_when_Connected_reruns_discovery()
|
||||||
|
{
|
||||||
|
var driver = new DiscoverableStubDriver();
|
||||||
|
var parent = CreateTestProbe();
|
||||||
|
var actor = parent.ChildActorOf(DriverInstanceActor.Props(
|
||||||
|
driver, rediscoverInterval: TimeSpan.FromMilliseconds(20)));
|
||||||
|
|
||||||
|
actor.Tell(new DriverInstanceActor.InitializeRequested("{}"));
|
||||||
|
|
||||||
|
// Let the initial post-connect loop settle (passes 0,0,3,3) and confirm it stopped.
|
||||||
|
for (var i = 0; i < 4; i++)
|
||||||
|
parent.ExpectMsg<DriverInstanceActor.DiscoveredNodesReady>(TimeSpan.FromSeconds(2));
|
||||||
|
parent.ExpectNoMsg(TimeSpan.FromMilliseconds(200));
|
||||||
|
var passesBeforeTrigger = driver.DiscoverCount; // 4
|
||||||
|
|
||||||
|
// Re-kick discovery via the new message — the cache is warm, so the fresh pass sees the 3-node set.
|
||||||
|
actor.Tell(new DriverInstanceActor.TriggerRediscovery());
|
||||||
|
|
||||||
|
var afterTrigger = parent.ExpectMsg<DriverInstanceActor.DiscoveredNodesReady>(TimeSpan.FromSeconds(2));
|
||||||
|
afterTrigger.Nodes.Count.ShouldBe(3);
|
||||||
|
afterTrigger.DriverInstanceId.ShouldBe(driver.DriverInstanceId);
|
||||||
|
|
||||||
|
// A fresh pass genuinely ran — DiscoverCount advanced past the settled count.
|
||||||
|
driver.DiscoverCount.ShouldBeGreaterThan(passesBeforeTrigger);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <see cref="DriverInstanceActor.TriggerRediscovery"/> on a driver whose
|
||||||
|
/// <see cref="ITagDiscovery.RediscoverPolicy"/> is <see cref="DiscoveryRediscoverPolicy.Never"/> does
|
||||||
|
/// NOT re-discover: the handler calls <c>StartDiscovery</c>, which returns early for <c>Never</c>, so
|
||||||
|
/// no pass runs and nothing is published — mirroring the Connected-entry Never opt-out.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void TriggerRediscovery_with_policy_Never_does_not_rediscover()
|
||||||
|
{
|
||||||
|
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("{}"));
|
||||||
|
AwaitCondition(() => driver.InitializeCount > 0, TimeSpan.FromSeconds(2));
|
||||||
|
|
||||||
|
// Connected, but policy=Never — the trigger is honoured by StartDiscovery's early return.
|
||||||
|
actor.Tell(new DriverInstanceActor.TriggerRediscovery());
|
||||||
|
|
||||||
|
parent.ExpectNoMsg(TimeSpan.FromMilliseconds(300));
|
||||||
|
driver.DiscoverCount.ShouldBe(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <see cref="DriverInstanceActor.TriggerRediscovery"/> received while NOT Connected (still Connecting,
|
||||||
|
/// before init completes) is a clean silent no-op: no discovery pass runs, nothing is published, and
|
||||||
|
/// the actor neither crashes nor dies (the driver's eventual reconnect re-discovers anyway). A
|
||||||
|
/// follow-up connect then discovers normally, proving the actor is unharmed.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void TriggerRediscovery_when_not_Connected_is_a_silent_noop()
|
||||||
|
{
|
||||||
|
var driver = new DiscoverableStubDriver();
|
||||||
|
var parent = CreateTestProbe();
|
||||||
|
var actor = parent.ChildActorOf(DriverInstanceActor.Props(
|
||||||
|
driver, rediscoverInterval: TimeSpan.FromMilliseconds(20)));
|
||||||
|
Watch(actor);
|
||||||
|
|
||||||
|
// The actor boots into Connecting; send the trigger BEFORE InitializeRequested so it is handled
|
||||||
|
// in a non-Connected state.
|
||||||
|
actor.Tell(new DriverInstanceActor.TriggerRediscovery());
|
||||||
|
|
||||||
|
// No discovery resulted, and the actor is unharmed (no Terminated arrives at the watching test actor).
|
||||||
|
parent.ExpectNoMsg(TimeSpan.FromMilliseconds(300));
|
||||||
|
ExpectNoMsg(TimeSpan.FromMilliseconds(100));
|
||||||
|
driver.DiscoverCount.ShouldBe(0);
|
||||||
|
|
||||||
|
// Sanity: the actor still works — driving it to Connected discovers normally afterwards.
|
||||||
|
actor.Tell(new DriverInstanceActor.InitializeRequested("{}"));
|
||||||
|
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