feat(ab-transport): process-wide open-attempt budget so the differential never retries an open (Task 18 review r1)

This commit is contained in:
Joseph Doherty
2026-10-07 18:10:25 -04:00
parent 0e20a8e5a9
commit 1d6501f427
2 changed files with 46 additions and 0 deletions
@@ -106,6 +106,35 @@ public sealed class CipConnectionTests
target.ForwardOpens.ShouldBe(2); // Large, then exactly one regular
}
[Fact]
public async Task A_spent_open_attempt_budget_refuses_the_reconnect_before_any_io()
{
// Task 18 review r1 (C1): the differential harness caps the process at one open attempt, so a press
// that refuses the Forward Open sees one TCP connect and one Forward Open, never one per remaining tag.
await using var target = new FakeCipTarget { ForwardOpenReply = FakeCipTarget.Refuse(0x01, 0x0100) };
var time = new ManualTimeProvider();
await using var conn = Create(target, Options(size: 504), time);
var before = Volatile.Read(ref CipConnection.OpenAttempts);
Volatile.Write(ref CipConnection.OpenAttemptBudget, before + 1);
try
{
await Should.ThrowAsync<AbTransportException>(() => conn.SendAsync(ReadTag, TestCt));
var retry = conn.SendAsync(ReadTag, TestCt);
await time.WaitForTimerDueAtAsync(time.Start + TimeSpan.FromSeconds(1), Guard); // parked in the throttle
time.Advance(TimeSpan.FromSeconds(1));
var refused = await Should.ThrowAsync<AbTransportException>(() => retry.WaitAsync(Guard, TestCt));
refused.Status.ShouldBe(AbStatus.Abort);
refused.Message.ShouldContain("budget");
target.Server.Connections.ShouldBe(1);
target.ForwardOpens.ShouldBe(1);
}
finally
{
Volatile.Write(ref CipConnection.OpenAttemptBudget, -1);
}
}
[Fact]
public async Task Connection_in_use_does_not_fall_back()
{