fix(mqtt): back off the Sparkplug primary-host claim loop after failures (#602 STAB-27)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
@@ -238,6 +238,53 @@ public sealed class SparkplugPrimaryHostTests
|
||||
await host.DisposeAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// STAB-27 — a claim the broker keeps refusing used to be redialled (and Warning-logged) on every
|
||||
/// one-second role sample, forever. It now backs off. The clock is frozen, so the real one-second
|
||||
/// loop ticks several times without the backoff window ever opening; advancing the clock opens it.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task A_failing_claim_backs_off_instead_of_redialling_every_tick()
|
||||
{
|
||||
var clock = new FrozenClock();
|
||||
var host = new SparkplugPrimaryHost(
|
||||
Unreachable(), "drv-1", new FixedRole(shouldActAsPrimary: true), timeProvider: clock);
|
||||
|
||||
await host.StartAsync(TestContext.Current.CancellationToken);
|
||||
host.ClaimAttemptsForTests.ShouldBe(1);
|
||||
|
||||
// Several real loop ticks elapse while the (frozen) backoff window stays shut.
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(3200), TestContext.Current.CancellationToken);
|
||||
host.ClaimAttemptsForTests.ShouldBe(1, "no further claim while inside the backoff window");
|
||||
|
||||
// Positive control: once the window passes, the very next tick does try again.
|
||||
clock.Advance(TimeSpan.FromSeconds(5));
|
||||
(await WaitUntilAsync(() => host.ClaimAttemptsForTests >= 2, TimeSpan.FromSeconds(10)))
|
||||
.ShouldBeTrue("the claim must resume after the backoff window");
|
||||
|
||||
await host.DisposeAsync();
|
||||
}
|
||||
|
||||
/// <summary>The ladder is exponential from 2 s and capped, so a long outage settles at one attempt per cap.</summary>
|
||||
[Theory]
|
||||
[InlineData(1, 2)]
|
||||
[InlineData(2, 4)]
|
||||
[InlineData(3, 8)]
|
||||
[InlineData(4, 16)]
|
||||
[InlineData(5, 30)]
|
||||
[InlineData(40, 30)]
|
||||
public void Claim_backoff_is_exponential_and_capped(int failures, int expectedSeconds)
|
||||
=> SparkplugPrimaryHost.ClaimBackoffFor(failures).ShouldBe(TimeSpan.FromSeconds(expectedSeconds));
|
||||
|
||||
private sealed class FrozenClock : TimeProvider
|
||||
{
|
||||
private DateTimeOffset _now = new(2026, 10, 8, 0, 0, 0, TimeSpan.Zero);
|
||||
|
||||
public override DateTimeOffset GetUtcNow() => _now;
|
||||
|
||||
public void Advance(TimeSpan by) => _now += by;
|
||||
}
|
||||
|
||||
/// <summary>A broker that refuses instantly, so a connect attempt is observable but not slow.</summary>
|
||||
private static MqttDriverOptions Unreachable() =>
|
||||
Options() with { Host = "127.0.0.1", Port = 1, ConnectTimeoutSeconds = 1 };
|
||||
|
||||
Reference in New Issue
Block a user