feat(site-runtime): tag-subscribe failure reply frees the in-flight slot, backs off (×2 ±20%, capped) and Success resets to floor (Lever 1)
Also from the Task 3 review: FailedSubscribeResponse_IsRetried guards the failure path again (large ceiling); HandleSubscribeTagsResponse reads state instead of get-or-create; SubscribeToDcl prunes stale retry state on re-entry; doc comments refreshed. Claude-Session: https://claude.ai/code/session_019ZxXT5sxZs5XrtdwYzJDPV
This commit is contained in:
@@ -1290,7 +1290,7 @@ public class InstanceActorTests : TestKit, IDisposable
|
||||
[Fact]
|
||||
public void FailedSubscribeResponse_IsRetried()
|
||||
{
|
||||
var options = new SiteRuntimeOptions { TagSubscribeRetryIntervalMs = 200, TagSubscribeInFlightCeilingMs = 200 };
|
||||
var options = new SiteRuntimeOptions { TagSubscribeRetryIntervalMs = 200, TagSubscribeInFlightCeilingMs = 60_000 };
|
||||
var (instance, dclProbe) = CreateInstanceWithDataSourcedAttribute(options);
|
||||
|
||||
var first = dclProbe.ExpectMsg<SubscribeTagsRequest>();
|
||||
@@ -1370,7 +1370,8 @@ public class InstanceActorTests : TestKit, IDisposable
|
||||
var first = dclProbe.ExpectMsg<SubscribeTagsRequest>();
|
||||
var second = dclProbe.ExpectMsg<SubscribeTagsRequest>(TimeSpan.FromSeconds(3));
|
||||
|
||||
// A late Success for the FIRST id must not cancel the watchdog guarding the SECOND.
|
||||
// A late Success for the FIRST id is dropped at the pending-map check (its id was
|
||||
// removed at the ceiling); the watchdog guarding the SECOND is untouched.
|
||||
instance.Tell(new SubscribeTagsResponse(first.CorrelationId, first.InstanceUniqueName,
|
||||
true, null, DateTimeOffset.UtcNow), dclProbe.Ref);
|
||||
var third = dclProbe.ExpectMsg<SubscribeTagsRequest>(TimeSpan.FromSeconds(3)); // second eventually re-sent
|
||||
@@ -1381,4 +1382,60 @@ public class InstanceActorTests : TestKit, IDisposable
|
||||
true, null, DateTimeOffset.UtcNow), dclProbe.Ref);
|
||||
dclProbe.ExpectNoMsg(TimeSpan.FromMilliseconds(800));
|
||||
}
|
||||
|
||||
// --- Lever 1 (2026-08-28): backoff on failure, reset on success ---
|
||||
|
||||
[Fact]
|
||||
public void RepeatedFailures_BackOff_AndSuccessResetsToFloor()
|
||||
{
|
||||
// Floor 200 ms, max 800 ms. Jitter is ±20 % so bound the assertions, don't pin them.
|
||||
var options = new SiteRuntimeOptions
|
||||
{
|
||||
TagSubscribeRetryIntervalMs = 200,
|
||||
TagSubscribeRetryMaxIntervalMs = 800,
|
||||
TagSubscribeInFlightCeilingMs = 60_000, // guard never expires; only replies drive retries
|
||||
};
|
||||
var (instance, dclProbe) = CreateInstanceWithDataSourcedAttribute(options);
|
||||
|
||||
var req = dclProbe.ExpectMsg<SubscribeTagsRequest>();
|
||||
var gaps = new List<TimeSpan>();
|
||||
for (var i = 0; i < 3; i++)
|
||||
{
|
||||
instance.Tell(new SubscribeTagsResponse(req.CorrelationId, req.InstanceUniqueName,
|
||||
false, "gateway busy", DateTimeOffset.UtcNow), dclProbe.Ref);
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
req = dclProbe.ExpectMsg<SubscribeTagsRequest>(TimeSpan.FromSeconds(5));
|
||||
gaps.Add(sw.Elapsed);
|
||||
}
|
||||
|
||||
// Expected nominal gaps: 400, 800(cap), 800(cap) — each ±20 % jitter and scheduler slack.
|
||||
Assert.InRange(gaps[0].TotalMilliseconds, 300, 560);
|
||||
Assert.InRange(gaps[1].TotalMilliseconds, 600, 1100);
|
||||
Assert.InRange(gaps[2].TotalMilliseconds, 600, 1100);
|
||||
|
||||
// Success resets: no further request follows a confirmed subscription.
|
||||
instance.Tell(new SubscribeTagsResponse(req.CorrelationId, req.InstanceUniqueName,
|
||||
true, null, DateTimeOffset.UtcNow), dclProbe.Ref);
|
||||
dclProbe.ExpectNoMsg(TimeSpan.FromMilliseconds(1200));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FailureReply_ReissuesEvenThoughGuardHasNotExpired()
|
||||
{
|
||||
var options = new SiteRuntimeOptions
|
||||
{
|
||||
TagSubscribeRetryIntervalMs = 200,
|
||||
TagSubscribeRetryMaxIntervalMs = 200,
|
||||
TagSubscribeInFlightCeilingMs = 60_000,
|
||||
};
|
||||
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);
|
||||
|
||||
// A definite failure clears the in-flight slot, so the watchdog re-sends.
|
||||
var retry = dclProbe.ExpectMsg<SubscribeTagsRequest>(TimeSpan.FromSeconds(3));
|
||||
Assert.NotEqual(first.CorrelationId, retry.CorrelationId);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user