fix(abcip): budget expected reply size in RequestScheduler.TakeBatch (#582); private lock + struct-handle check across read fragments (#567)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
+44
@@ -437,4 +437,48 @@ public sealed class RequestSchedulerTests
|
||||
ch.Sent[1][^1].ShouldBe((byte)2);
|
||||
FakeCipChannel.SplitMsp(ch.Sent[2]).Select(e => e[^1]).ShouldBe(new byte[] { 3, 4 });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Mixed_scalar_string_and_array_replies_are_packed_within_the_reply_budget()
|
||||
{
|
||||
// Every request is 8 bytes, so the REQUEST budget would pack all of them into one MSP; only the
|
||||
// expected-reply budget (#582) splits the batch. Reply envelope = 6 + 2n + sum(replies) <= 504.
|
||||
const int scalar = 8, str = 96, array = 200;
|
||||
var ch = new FakeCipChannel(maxRequestBytes: 504);
|
||||
await using var s = new RequestScheduler(ch, allowPacking: true);
|
||||
var a = await HoldFirstAsync(s, ch, Req(8, 1));
|
||||
|
||||
var sizes = new[] { scalar, str, str, str, str, array, scalar, RequestScheduler.UnknownReply };
|
||||
var tasks = sizes.Select((size, i) => s.SendAsync(Req(8, (byte)(10 + i)), Ct, expectedReplyBytes: size)).ToArray();
|
||||
ch.Gate!.SetResult();
|
||||
await Task.WhenAll(tasks.Append(a)).WaitAsync(Timeout, Ct);
|
||||
|
||||
// [sc, S, S, S, S] = 6+10+392 = 408; the array (+200) would make 608 > 504, so it starts batch 2
|
||||
// with the trailing scalar; the unknown-size read goes alone.
|
||||
ch.Sent.Count.ShouldBe(4);
|
||||
FakeCipChannel.SplitMsp(ch.Sent[1]).Select(e => e[^1]).ShouldBe(new byte[] { 10, 11, 12, 13, 14 });
|
||||
FakeCipChannel.SplitMsp(ch.Sent[2]).Select(e => e[^1]).ShouldBe(new byte[] { 15, 16 });
|
||||
IsMsp(ch.Sent[3]).ShouldBeFalse();
|
||||
ch.Sent[3][^1].ShouldBe((byte)17);
|
||||
ReplyEnvelopeFor(scalar, str, str, str, str).ShouldBeLessThanOrEqualTo(504);
|
||||
ReplyEnvelopeFor(scalar, str, str, str, str, array).ShouldBeGreaterThan(504);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Status_only_replies_still_pack_to_the_request_limit()
|
||||
{
|
||||
// Positive control for the reply budget: the default (status-only) reply never splits a batch
|
||||
// the request budget allows, so existing write / probe coalescing is unchanged.
|
||||
var ch = new FakeCipChannel(maxRequestBytes: 504);
|
||||
await using var s = new RequestScheduler(ch, allowPacking: true);
|
||||
var a = await HoldFirstAsync(s, ch, Req(8, 1));
|
||||
var rest = Enumerable.Range(0, 20).Select(i => s.SendAsync(Req(8, (byte)(2 + i)), Ct)).ToArray();
|
||||
ch.Gate!.SetResult();
|
||||
await Task.WhenAll(rest.Append(a)).WaitAsync(Timeout, Ct);
|
||||
|
||||
ch.Sent.Count.ShouldBe(2);
|
||||
FakeCipChannel.SplitMsp(ch.Sent[1]).Count.ShouldBe(20);
|
||||
}
|
||||
|
||||
private static int ReplyEnvelopeFor(params int[] replies) => RequestScheduler.ReplyEnvelopeSize(replies);
|
||||
}
|
||||
|
||||
+25
@@ -279,4 +279,29 @@ public sealed class LogixTagClientTests
|
||||
() => client.WriteAsync(Path, LogixTypeCode.DInt, null, 40, Bytes(160, 0), Ct));
|
||||
ch.Sent.Count.ShouldBe(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_struct_handle_that_changes_between_fragments_fails_the_read()
|
||||
{
|
||||
var (ch, client, s) = Create();
|
||||
await using var _ = s;
|
||||
ch.RespondPerService = r => ReadOffset(r) == 0
|
||||
? FakeCipChannel.Reply(r[0], 0x06, TypedData(LogixTypeCode.Struct, Bytes(8, 0), handle: 0x0FCE))
|
||||
: FakeCipChannel.Reply(r[0], 0x00, TypedData(LogixTypeCode.Struct, Bytes(4, 8), handle: 0x1234));
|
||||
|
||||
var ex = await Should.ThrowAsync<AbTransportException>(() => client.ReadAsync(Path, 1, Ct));
|
||||
|
||||
ex.Status.ShouldBe(AbStatus.BadReply);
|
||||
ex.Message.ShouldContain("0x0FCE");
|
||||
ex.Message.ShouldContain("0x1234");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Expected_read_reply_bytes_budgets_elements_times_width_and_is_unknown_without_a_width()
|
||||
{
|
||||
// header 4 + type/handle 4 + 3 STRING elements.
|
||||
LogixTagClient.ExpectedReadReplyBytes(3, LogixTypeCode.StringElementBytes, 0).ShouldBe(4 + 4 + 3 * 88);
|
||||
LogixTagClient.ExpectedReadReplyBytes(3, LogixTypeCode.StringElementBytes, 100).ShouldBe(4 + 4 + 3 * 88 - 100);
|
||||
LogixTagClient.ExpectedReadReplyBytes(3, 0, 0).ShouldBe(RequestScheduler.UnknownReply);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user