fix(site-runtime): InstanceActor retries failed/lost tag subscriptions per connection (S4/UA6) — closes the unknown-connection ordering race

This commit is contained in:
Joseph Doherty
2026-07-09 00:03:44 -04:00
parent bd89c1474e
commit feb6f34e90
3 changed files with 162 additions and 17 deletions
@@ -1171,4 +1171,69 @@ public class InstanceActorTests : TestKit, IDisposable
Assert.Single(overrides);
Assert.Equal("[\"a\"", overrides["Counts"]);
}
// ── S4/UA6: tag-subscription retry (failed AND lost responses) ──
/// <summary>
/// Builds an Instance Actor with a single data-sourced attribute and the given
/// options (so the retry interval is short), returning it plus the DCL probe.
/// The initial SubscribeTagsRequest is left in the probe's queue for the caller.
/// </summary>
private (IActorRef Instance, TestProbe Dcl) CreateInstanceWithDataSourcedAttribute(SiteRuntimeOptions options)
{
var config = new FlattenedConfiguration
{
InstanceUniqueName = "Sub-1",
Attributes =
[
new ResolvedAttribute
{
CanonicalName = "Motor.Temp", Value = "0", DataType = "Int",
DataSourceReference = "ns=2;s=Motor.Temp", BoundDataConnectionName = "conn-1"
}
]
};
var dcl = CreateTestProbe();
var actor = ActorOf(Props.Create(() => new InstanceActor(
"Sub-1",
JsonSerializer.Serialize(config),
_storage,
_compilationService,
_sharedScriptLibrary,
null,
options,
NullLogger<InstanceActor>.Instance,
dcl.Ref)));
return (actor, dcl);
}
[Fact]
public void FailedSubscribeResponse_IsRetried()
{
var options = new SiteRuntimeOptions { TagSubscribeRetryIntervalMs = 200 };
var (instance, dclProbe) = CreateInstanceWithDataSourcedAttribute(options);
var first = dclProbe.ExpectMsg<SubscribeTagsRequest>();
instance.Tell(new SubscribeTagsResponse(first.CorrelationId, first.InstanceUniqueName,
false, "Unknown connection: conn-1", DateTimeOffset.UtcNow), dclProbe.Ref);
var retry = dclProbe.ExpectMsg<SubscribeTagsRequest>(TimeSpan.FromSeconds(5)); // S4 core
Assert.Equal(first.ConnectionName, retry.ConnectionName);
Assert.Equal(first.TagPaths, retry.TagPaths);
}
[Fact]
public void LostSubscribeResponse_IsRetried_AndSuccessStopsRetrying()
{
var options = new SiteRuntimeOptions { TagSubscribeRetryIntervalMs = 200 };
var (instance, dclProbe) = CreateInstanceWithDataSourcedAttribute(options);
dclProbe.ExpectMsg<SubscribeTagsRequest>();
// No reply at all (dead-lettered response) -> must re-send.
var retry = dclProbe.ExpectMsg<SubscribeTagsRequest>(TimeSpan.FromSeconds(5));
instance.Tell(new SubscribeTagsResponse(retry.CorrelationId, retry.InstanceUniqueName,
true, null, DateTimeOffset.UtcNow), dclProbe.Ref);
dclProbe.ExpectNoMsg(TimeSpan.FromMilliseconds(600)); // retry timer cancelled
}
}