fix(abcip): native runtime STRING capacity, mistype churn, malformed-name config error, LWORD type check; non-blocking lease release (#570, #579)
#570 - STRING: the built-in 88-byte Logix STRING holds 82 characters (LEN + DATA[82] + 2 pad), not 84; the write still sends the whole 88-byte structure and a read never decodes the pad as characters. - Mistype churn: a type refusal (CIP 0xFF/0x2107) on the existence read no longer throws from InitializeAsync, so the runtime is not disposed/re-created (and re-Forward-Opened) every poll; the driver also keeps a runtime that throws TypeMismatch from init and records the device as reachable. - Malformed tag name: a TagPath that does not parse, or a name the CIP symbolic encoder refuses (now also non-ASCII, empty member, >255-char member, unterminated subscript), is AbCipTagConfigurationException -> per-tag BadConfigurationError (named const in AbCipStatusMapper), warned once, never feeding the device backoff or health; siblings keep reading. A malformed probe tag no longer feeds the backoff either. - Micro800 BYTE/WORD/LWORD (0xD1/0xD2/0xD4) are type-checked as integers of their width: REAL/LREAL or a narrower integer over LWORD is TypeMismatch, not decoded garbage. #579 - ConnectionLease.ReleaseInBackground: one non-blocking release shape for both native AB runtimes. The AbCip runtime's Dispose no longer waits up to 2 s under the per-tag lock; the pool still registers the close synchronously so a re-create cannot overlap it. Offline soak tests (25 cycles, AbCip + AbLegacy) prove no leaked connection; FakeEipServer gains OpenConnections. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
@@ -267,6 +267,43 @@ public sealed class DeviceSessionPoolTests
|
||||
public void Dispose() { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gitea #579: <see cref="ConnectionLease.ReleaseInBackground"/> returns before the last release's Forward
|
||||
/// Close completes, yet a successor acquired that same instant still cannot open until the close is over —
|
||||
/// the closing chain is registered synchronously, inside the call.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Release_in_background_returns_before_the_close_and_a_successor_still_waits_for_it()
|
||||
{
|
||||
await using var target = new FakeCipTarget();
|
||||
var closeHeld = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
target.ForwardCloseGate = () => closeHeld.Task;
|
||||
var time = new ManualTimeProvider();
|
||||
var pool = new DeviceSessionPool(time);
|
||||
var key = new DeviceKey("127.0.0.1", target.Port, "1,0", "ControlLogix");
|
||||
|
||||
var a = pool.Acquire(key, Options());
|
||||
await a.Scheduler.SendAsync(ReadTag, TestCt);
|
||||
var teardown = a.ReleaseInBackground();
|
||||
pool.EntryCount.ShouldBe(0); // bookkeeping done before it returned
|
||||
await FakeCipTarget.Until(() => target.ForwardCloses == 1, Guard, "the Forward Close");
|
||||
teardown.IsCompleted.ShouldBeFalse(); // ...but the close is still pending
|
||||
|
||||
time.Advance(TimeSpan.FromSeconds(5)); // the throttle is no obstacle
|
||||
await using var b = pool.Acquire(key, Options());
|
||||
var bSend = b.Scheduler.SendAsync(ReadTag, TestCt);
|
||||
await Task.Delay(300, TestCt);
|
||||
target.ForwardOpens.ShouldBe(1); // held behind A's close
|
||||
bSend.IsCompleted.ShouldBeFalse();
|
||||
|
||||
closeHeld.SetResult();
|
||||
await teardown.WaitAsync(Guard, TestCt);
|
||||
(await bSend.WaitAsync(Guard, TestCt)).GeneralStatus.ShouldBe((byte)0);
|
||||
target.ForwardOpens.ShouldBe(2);
|
||||
await a.ReleaseInBackground().WaitAsync(Guard, TestCt); // idempotent: b's reference is untouched
|
||||
pool.EntryCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Shared_is_one_process_wide_instance() =>
|
||||
DeviceSessionPool.Shared.ShouldBeSameAs(DeviceSessionPool.Shared);
|
||||
|
||||
Reference in New Issue
Block a user