fix(abcip): Task 13 review r1 - bounded listing pager, exclusive large reads, template bound, safe lease release
This commit is contained in:
@@ -19,13 +19,16 @@ public sealed class RequestScheduler : IAsyncDisposable
|
||||
private const byte MultipleServiceCode = 0x0A;
|
||||
private const byte EmbeddedServiceError = 0x1E;
|
||||
|
||||
private sealed class Pending(byte[] request)
|
||||
private sealed class Pending(byte[] request, bool exclusive)
|
||||
{
|
||||
private bool _completed;
|
||||
private CancellationTokenRegistration _registration;
|
||||
private bool _hasRegistration;
|
||||
|
||||
public byte[] Request { get; } = request;
|
||||
|
||||
/// <summary>True when this request must never share a Multiple Service Packet with another.</summary>
|
||||
public bool Exclusive { get; } = exclusive;
|
||||
public TaskCompletionSource<CipReply> Tcs { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
public LinkedListNode<Pending>? Node { get; set; }
|
||||
|
||||
@@ -119,12 +122,16 @@ public sealed class RequestScheduler : IAsyncDisposable
|
||||
/// status is not an exception here. Faults with <see cref="AbTransportException"/> when the channel
|
||||
/// or the MSP envelope fails, or the scheduler is disposed; cancels only when <paramref name="ct"/> fires.
|
||||
/// </summary>
|
||||
public Task<CipReply> SendAsync(byte[] request, CancellationToken ct)
|
||||
/// <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)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
if (ct.IsCancellationRequested) return Task.FromCanceled<CipReply>(ct);
|
||||
|
||||
var p = new Pending(request);
|
||||
var p = new Pending(request, exclusive);
|
||||
lock (_lock)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
@@ -165,7 +172,7 @@ public sealed class RequestScheduler : IAsyncDisposable
|
||||
{
|
||||
if (batch.Count > 0)
|
||||
{
|
||||
if (!_allowPacking) break;
|
||||
if (!_allowPacking || n.Value.Exclusive) break;
|
||||
sizes.Add(n.Value.Request.Length);
|
||||
if (MultipleServicePacket.BuildSize(sizes) > _channel.MaxRequestBytes) break;
|
||||
}
|
||||
@@ -177,6 +184,7 @@ public sealed class RequestScheduler : IAsyncDisposable
|
||||
}
|
||||
batch.Add(n.Value);
|
||||
_queue.RemoveFirst();
|
||||
if (n.Value.Exclusive) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Cip;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Logix;
|
||||
@@ -14,6 +16,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||
internal sealed class NativeAbCipTagEnumerator : IAbCipTagEnumerator
|
||||
{
|
||||
private readonly Func<AbCipTagCreateParams, (RequestScheduler Scheduler, Func<ValueTask> Release)> _acquire;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <summary>Production constructor: leases from <paramref name="pool"/>.</summary>
|
||||
/// <param name="pool">The connection pool (the factory passes <see cref="DeviceSessionPool.Shared"/>).</param>
|
||||
@@ -28,9 +31,14 @@ internal sealed class NativeAbCipTagEnumerator : IAbCipTagEnumerator
|
||||
|
||||
/// <summary>Test seam: supplies the scheduler and its release directly.</summary>
|
||||
/// <param name="acquire">Acquires the scheduler for a device, with the callback that releases it.</param>
|
||||
/// <param name="logger">Receives release failures; null discards them.</param>
|
||||
internal NativeAbCipTagEnumerator(
|
||||
Func<AbCipTagCreateParams, (RequestScheduler Scheduler, Func<ValueTask> Release)> acquire) =>
|
||||
Func<AbCipTagCreateParams, (RequestScheduler Scheduler, Func<ValueTask> Release)> acquire,
|
||||
ILogger? logger = null)
|
||||
{
|
||||
_acquire = acquire;
|
||||
_logger = logger ?? NullLogger.Instance;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async IAsyncEnumerable<AbCipDiscoveredTag> EnumerateAsync(
|
||||
@@ -47,7 +55,7 @@ internal sealed class NativeAbCipTagEnumerator : IAbCipTagEnumerator
|
||||
}
|
||||
finally
|
||||
{
|
||||
NativeLease.ReleaseBounded(release);
|
||||
NativeLease.ReleaseBounded(release, _logger);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +69,12 @@ internal static class NativeListingReader
|
||||
private const string TagsSuffix = ".@tags";
|
||||
private const string ProgramPrefix = "Program:";
|
||||
|
||||
/// <summary>Page cap for one listing; a real controller needs a small fraction of this.</summary>
|
||||
internal const int MaxPages = 10_000;
|
||||
|
||||
/// <summary>Byte cap for one listing (64 MiB).</summary>
|
||||
internal const long MaxListingBytes = 64L * 1024 * 1024;
|
||||
|
||||
/// <summary>Reads <c>@tags</c> or <c>Program:X.@tags</c> in full.</summary>
|
||||
/// <param name="scheduler">The device's shared scheduler.</param>
|
||||
/// <param name="listingName">The listing pseudo-tag name.</param>
|
||||
@@ -80,15 +94,26 @@ internal static class NativeListingReader
|
||||
|
||||
using var all = new MemoryStream();
|
||||
uint start = 0;
|
||||
while (true)
|
||||
for (var page = 0; page < MaxPages; page++)
|
||||
{
|
||||
var request = SymbolObjectServices.GetInstanceAttributeList(program, start);
|
||||
var reply = await scheduler.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
// Exclusive: the reply can fill the connection, and the packer budgets request bytes only.
|
||||
var reply = await scheduler.SendAsync(request, cancellationToken, exclusive: true).ConfigureAwait(false);
|
||||
var (entries, last, more) = SymbolObjectServices.ParseListingReply(reply);
|
||||
all.Write(entries);
|
||||
if (all.Length > MaxListingBytes)
|
||||
throw new AbTransportException(AbStatus.BadReply,
|
||||
$"Symbol listing '{listingName}' exceeded {MaxListingBytes} bytes.");
|
||||
if (!more) return all.ToArray();
|
||||
// Instances ascend; a page that does not advance past its start, or a last id that would
|
||||
// wrap, is a misbehaving reply and would otherwise loop forever against the controller.
|
||||
if (last < start || last == uint.MaxValue)
|
||||
throw new AbTransportException(AbStatus.BadReply,
|
||||
$"Symbol listing '{listingName}' made no progress (start {start}, last {last}).");
|
||||
start = last + 1;
|
||||
}
|
||||
throw new AbTransportException(AbStatus.BadReply,
|
||||
$"Symbol listing '{listingName}' exceeded {MaxPages} pages.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,10 +130,13 @@ internal static class NativeLease
|
||||
/// <summary>Upper bound on waiting for the lease release (and its Forward Close).</summary>
|
||||
private static readonly TimeSpan DisposeWait = TimeSpan.FromSeconds(2);
|
||||
|
||||
/// <summary>Releases, waiting at most 2 s; never throws.</summary>
|
||||
public static void ReleaseBounded(Func<ValueTask> release)
|
||||
/// <summary>Releases, waiting at most 2 s; never throws, whatever the release delegate does.</summary>
|
||||
public static void ReleaseBounded(Func<ValueTask> release, ILogger logger)
|
||||
{
|
||||
try { release().AsTask().Wait(DisposeWait); }
|
||||
catch (AggregateException) { /* a faulted close is the pool's to log */ }
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Releasing the native AbCip device lease failed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Cip;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Logix;
|
||||
@@ -17,7 +19,11 @@ internal sealed class NativeAbCipTemplateReader : IAbCipTemplateReader
|
||||
/// <summary>Bytes of the template definition header not counted in the Read Template length.</summary>
|
||||
private const int DefinitionHeaderBytes = 23;
|
||||
|
||||
/// <summary>Upper bound on a template's Read Template length; Logix templates are far smaller.</summary>
|
||||
internal const long MaxTemplateBytes = 64 * 1024;
|
||||
|
||||
private readonly Func<AbCipTagCreateParams, (RequestScheduler Scheduler, ICipChannel Channel, Func<ValueTask> Release)> _acquire;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <summary>Production constructor: leases from <paramref name="pool"/>.</summary>
|
||||
/// <param name="pool">The connection pool (the factory passes <see cref="DeviceSessionPool.Shared"/>).</param>
|
||||
@@ -32,9 +38,14 @@ internal sealed class NativeAbCipTemplateReader : IAbCipTemplateReader
|
||||
|
||||
/// <summary>Test seam: supplies the scheduler, channel and release directly.</summary>
|
||||
/// <param name="acquire">Acquires the connection parts for a device.</param>
|
||||
/// <param name="logger">Receives release failures; null discards them.</param>
|
||||
internal NativeAbCipTemplateReader(
|
||||
Func<AbCipTagCreateParams, (RequestScheduler Scheduler, ICipChannel Channel, Func<ValueTask> Release)> acquire) =>
|
||||
Func<AbCipTagCreateParams, (RequestScheduler Scheduler, ICipChannel Channel, Func<ValueTask> Release)> acquire,
|
||||
ILogger? logger = null)
|
||||
{
|
||||
_acquire = acquire;
|
||||
_logger = logger ?? NullLogger.Instance;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<byte[]> ReadAsync(
|
||||
@@ -53,7 +64,7 @@ internal sealed class NativeAbCipTemplateReader : IAbCipTemplateReader
|
||||
|
||||
// The CIP-documented Read Template length: definition words * 4 - 23.
|
||||
var total = checked((long)attrs.DefinitionWords * 4 - DefinitionHeaderBytes);
|
||||
if (total < 0)
|
||||
if (total < 0 || total > MaxTemplateBytes)
|
||||
throw new AbTransportException(AbStatus.BadReply,
|
||||
$"Template {id} reports an impossible definition size of {attrs.DefinitionWords} words.");
|
||||
|
||||
@@ -63,7 +74,7 @@ internal sealed class NativeAbCipTemplateReader : IAbCipTemplateReader
|
||||
{
|
||||
var chunk = (ushort)Math.Min(total - offset, channel.MaxRequestBytes - RequestOverhead);
|
||||
var reply = await scheduler
|
||||
.SendAsync(TemplateObjectServices.ReadTemplate(id, (uint)offset, chunk), cancellationToken)
|
||||
.SendAsync(TemplateObjectServices.ReadTemplate(id, (uint)offset, chunk), cancellationToken, exclusive: true)
|
||||
.ConfigureAwait(false);
|
||||
if (reply.GeneralStatus is not (0x00 or 0x06))
|
||||
throw new AbTransportException(reply.Status,
|
||||
@@ -80,7 +91,7 @@ internal sealed class NativeAbCipTemplateReader : IAbCipTemplateReader
|
||||
}
|
||||
finally
|
||||
{
|
||||
NativeLease.ReleaseBounded(release);
|
||||
NativeLease.ReleaseBounded(release, _logger);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user