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:
@@ -19,8 +19,21 @@ public sealed class RequestScheduler : IAsyncDisposable
|
||||
private const byte MultipleServiceCode = 0x0A;
|
||||
private const byte EmbeddedServiceError = 0x1E;
|
||||
|
||||
private sealed class Pending(byte[] request, bool exclusive)
|
||||
/// <summary>Smallest possible CIP reply: service, reserved, general status, additional-status size.</summary>
|
||||
public const int ReplyHeaderBytes = 4;
|
||||
|
||||
/// <summary>The <c>expectedReplyBytes</c> default: the reply carries no data beyond its header (a
|
||||
/// write, a status-only service). Every caller whose reply carries data must pass its real bound.</summary>
|
||||
public const int StatusOnlyReply = ReplyHeaderBytes;
|
||||
|
||||
/// <summary>Pass as <c>expectedReplyBytes</c> when the reply size cannot be bounded: the request is
|
||||
/// then sent alone, never packed, because its reply could fill the whole connection.</summary>
|
||||
public const int UnknownReply = -1;
|
||||
|
||||
private sealed class Pending(byte[] request, bool exclusive, int expectedReplyBytes)
|
||||
{
|
||||
// Private lock object: lock(this) would let any holder of the Pending reference contend.
|
||||
private readonly object _gate = new();
|
||||
private bool _completed;
|
||||
private CancellationTokenRegistration _registration;
|
||||
private bool _hasRegistration;
|
||||
@@ -29,13 +42,16 @@ public sealed class RequestScheduler : IAsyncDisposable
|
||||
|
||||
/// <summary>True when this request must never share a Multiple Service Packet with another.</summary>
|
||||
public bool Exclusive { get; } = exclusive;
|
||||
|
||||
/// <summary>Expected whole-reply size (header included), or <see cref="UnknownReply"/>.</summary>
|
||||
public int ExpectedReplyBytes { get; } = expectedReplyBytes;
|
||||
public TaskCompletionSource<CipReply> Tcs { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
public LinkedListNode<Pending>? Node { get; set; }
|
||||
|
||||
/// <summary>Stores the waiter's registration; disposes it at once if the waiter already completed.</summary>
|
||||
public void SetRegistration(CancellationTokenRegistration registration, RequestScheduler owner)
|
||||
{
|
||||
lock (this)
|
||||
lock (_gate)
|
||||
{
|
||||
if (!_completed)
|
||||
{
|
||||
@@ -54,7 +70,7 @@ public sealed class RequestScheduler : IAsyncDisposable
|
||||
{
|
||||
CancellationTokenRegistration registration;
|
||||
bool hasRegistration;
|
||||
lock (this)
|
||||
lock (_gate)
|
||||
{
|
||||
if (_completed) return false;
|
||||
_completed = true;
|
||||
@@ -125,13 +141,19 @@ public sealed class RequestScheduler : IAsyncDisposable
|
||||
/// <param name="request">The CIP request.</param>
|
||||
/// <param name="ct">Cancels this waiter only.</param>
|
||||
/// <param name="exclusive">When true the request is sent alone, never packed into a Multiple Service
|
||||
/// Packet: for requests whose reply can fill the whole connection (the packer budgets request bytes only).</param>
|
||||
public Task<CipReply> SendAsync(byte[] request, CancellationToken ct, bool exclusive = false)
|
||||
/// Packet: for requests whose reply can fill the whole connection.</param>
|
||||
/// <param name="expectedReplyBytes">Upper bound on this request's whole reply (header included) as
|
||||
/// it would sit inside a Multiple Service Packet reply. The packer budgets this against the
|
||||
/// connection size as well as the request bytes (#582), so a batch of small requests with large
|
||||
/// replies (STRINGs, arrays) is split instead of overflowing. Defaults to <see cref="StatusOnlyReply"/>;
|
||||
/// <see cref="UnknownReply"/> sends the request alone.</param>
|
||||
public Task<CipReply> SendAsync(byte[] request, CancellationToken ct, bool exclusive = false,
|
||||
int expectedReplyBytes = StatusOnlyReply)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
if (ct.IsCancellationRequested) return Task.FromCanceled<CipReply>(ct);
|
||||
|
||||
var p = new Pending(request, exclusive);
|
||||
var p = new Pending(request, exclusive || expectedReplyBytes < 0, Math.Max(expectedReplyBytes, UnknownReply));
|
||||
lock (_lock)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
@@ -168,19 +190,27 @@ public sealed class RequestScheduler : IAsyncDisposable
|
||||
lock (_lock)
|
||||
{
|
||||
var sizes = new List<int>();
|
||||
var replySizes = new List<int>();
|
||||
var replyBudget = _channel.MaxRequestBytes; // connections are symmetric: one size bounds both directions
|
||||
while (_queue.First is { } n)
|
||||
{
|
||||
if (batch.Count > 0)
|
||||
{
|
||||
if (!_allowPacking || n.Value.Exclusive) break;
|
||||
sizes.Add(n.Value.Request.Length);
|
||||
if (MultipleServicePacket.BuildSize(sizes) > _channel.MaxRequestBytes) break;
|
||||
replySizes.Add(n.Value.ExpectedReplyBytes);
|
||||
if (MultipleServicePacket.BuildSize(sizes) > _channel.MaxRequestBytes
|
||||
|| ReplyEnvelopeSize(replySizes) > replyBudget)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// The first request always goes, even when it alone exceeds the limit: it is sent
|
||||
// bare, and callers that must fit (LogixTagClient) fragment before they get here.
|
||||
sizes.Add(n.Value.Request.Length);
|
||||
replySizes.Add(n.Value.ExpectedReplyBytes);
|
||||
}
|
||||
batch.Add(n.Value);
|
||||
_queue.RemoveFirst();
|
||||
@@ -189,6 +219,15 @@ public sealed class RequestScheduler : IAsyncDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Expected size of the MSP reply for the given embedded reply sizes: reply header, count,
|
||||
/// one offset per reply, then the replies themselves.</summary>
|
||||
internal static int ReplyEnvelopeSize(IReadOnlyList<int> replySizes)
|
||||
{
|
||||
var total = ReplyHeaderBytes + 2 + 2 * replySizes.Count;
|
||||
foreach (var r in replySizes) total += r;
|
||||
return total;
|
||||
}
|
||||
|
||||
private async Task PumpAsync()
|
||||
{
|
||||
while (true)
|
||||
|
||||
Reference in New Issue
Block a user