Merge #572/#575: native browse paging/lease/pacing, differential equal-refusal + budget isolation
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe # Conflicts: # docs/drivers/AbCip.md
This commit is contained in:
@@ -26,18 +26,19 @@ public static class SymbolObjectServices
|
||||
}
|
||||
|
||||
/// <summary>Entries are <c>u32 instance, u16 type, u16 elemLen, u32×3 dims, u16 nameLen, name</c>.
|
||||
/// <c>LastInstance</c> is the last entry's instance id (next start = LastInstance + 1).
|
||||
/// <c>MaxInstance</c> is the HIGHEST instance id in the page, not the last entry's: the next start is
|
||||
/// MaxInstance + 1, which does not rely on the controller listing a page in ascending order.
|
||||
/// <c>More</c> is true on general status 0x06 (partial transfer).</summary>
|
||||
public static (byte[] Entries, uint LastInstance, bool More) ParseListingReply(CipReply reply)
|
||||
public static (byte[] Entries, uint MaxInstance, bool More) ParseListingReply(CipReply reply)
|
||||
{
|
||||
if (reply.GeneralStatus is not (0x00 or 0x06))
|
||||
throw new AbTransportException(reply.Status, $"Symbol listing failed: general 0x{reply.GeneralStatus:X2}.");
|
||||
var span = reply.Data.Span;
|
||||
var r = new CipReader(span);
|
||||
uint last = 0;
|
||||
uint max = 0;
|
||||
while (r.Remaining > 0)
|
||||
{
|
||||
last = r.U32();
|
||||
max = Math.Max(max, r.U32());
|
||||
r.Bytes(2 + 2 + 12);
|
||||
var nameLen = r.U16();
|
||||
r.Bytes(nameLen);
|
||||
@@ -45,6 +46,6 @@ public static class SymbolObjectServices
|
||||
var more = reply.GeneralStatus == 0x06;
|
||||
if (more && span.IsEmpty)
|
||||
throw new AbTransportException(AbStatus.BadReply, "Symbol listing reported more data but returned no entries.");
|
||||
return (span.ToArray(), last, more);
|
||||
return (span.ToArray(), max, more);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,15 @@ public sealed class AbCipDriverOptions
|
||||
/// </summary>
|
||||
public bool EnableControllerBrowse { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Pause between the pages of a symbol listing, NATIVE transport only (libplctag pages inside its
|
||||
/// own library). A large controller's listing is hundreds of pages; unpaced they are a back-to-back
|
||||
/// burst on the controller's message-processing slice. Default 5 ms — small next to a page's own
|
||||
/// round-trip, but it breaks up the burst. <see cref="TimeSpan.Zero"/> restores back-to-back paging.
|
||||
/// Config key <c>browsePageDelayMs</c>; a negative value is treated as zero.
|
||||
/// </summary>
|
||||
public TimeSpan BrowsePageDelay { get; init; } = TimeSpan.FromMilliseconds(5);
|
||||
|
||||
/// <summary>
|
||||
/// When <c>true</c>, declared ALMD tags are surfaced as alarm conditions
|
||||
/// via <c>IAlarmSource</c>; the driver polls each subscribed
|
||||
|
||||
@@ -40,6 +40,13 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
private readonly IAbCipTagFactory _tagFactory;
|
||||
private readonly IAbCipTagEnumeratorFactory _enumeratorFactory;
|
||||
private readonly IAbCipTemplateReaderFactory _templateReaderFactory;
|
||||
|
||||
/// <summary>
|
||||
/// The batch template reader of the discovery run in progress on this async flow, if any. While set,
|
||||
/// <see cref="FetchUdtShapeAsync"/> reads through it so a browse that fetches many templates holds ONE
|
||||
/// device lease across them instead of opening a connection per template (Gitea #572).
|
||||
/// </summary>
|
||||
private readonly AsyncLocal<IAbCipTemplateReader?> _batchTemplateReader = new();
|
||||
private readonly AbCipTemplateCache _templateCache = new();
|
||||
|
||||
/// <summary>
|
||||
@@ -158,7 +165,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
var nodeDefault = defaultTransport ?? AbCipTransportResolver.Default;
|
||||
_transport = AbCipTransportResolver.Resolve(options, nodeDefault);
|
||||
_transportDescription = AbCipTransportResolver.Describe(options.Transport, nodeDefault);
|
||||
var defaults = AbCipTransportFactories.For(_transport);
|
||||
var defaults = AbCipTransportFactories.For(_transport, options.BrowsePageDelay);
|
||||
_tagFactory = tagFactory ?? defaults.Tags;
|
||||
_enumeratorFactory = enumeratorFactory ?? defaults.Enumerators;
|
||||
_templateReaderFactory = templateReaderFactory ?? defaults.Templates;
|
||||
@@ -247,7 +254,9 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
|
||||
try
|
||||
{
|
||||
using var reader = _templateReaderFactory.Create();
|
||||
var batch = _batchTemplateReader.Value;
|
||||
using var own = batch is null ? _templateReaderFactory.Create() : null;
|
||||
var reader = batch ?? own!;
|
||||
var buffer = await reader.ReadAsync(deviceParams, templateInstanceId, cancellationToken).ConfigureAwait(false);
|
||||
var shape = CipTemplateObjectDecoder.Decode(buffer);
|
||||
if (shape is not null)
|
||||
@@ -1411,6 +1420,10 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
{
|
||||
using var enumerator = _enumeratorFactory.Create();
|
||||
var deviceParams = state.BuildCreateParams("@tags", _options.Timeout);
|
||||
// One template-reader lease across every UDT shape this device's browse fetches.
|
||||
using var batchReader = _templateReaderFactory.CreateBatch();
|
||||
_batchTemplateReader.Value = batchReader;
|
||||
using var clearBatch = new ClearOnDispose(_batchTemplateReader);
|
||||
|
||||
IAddressSpaceBuilder? discoveredFolder = null;
|
||||
// Dedup discovered member full-names within this device's Discovered folder.
|
||||
@@ -1488,6 +1501,12 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Clears an <see cref="AsyncLocal{T}"/> when the scope that set it ends.</summary>
|
||||
private sealed class ClearOnDispose(AsyncLocal<IAbCipTemplateReader?> slot) : IDisposable
|
||||
{
|
||||
public void Dispose() => slot.Value = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <c>true</c> for a UDT member the controller generates rather than the user declares: the
|
||||
/// hidden SINT/DINT hosts Logix packs BOOL members into (<c>ZZZZZZZZZZAlarms0</c>) and AOI
|
||||
|
||||
@@ -109,6 +109,7 @@ public static class AbCipDriverFactoryExtensions
|
||||
? null
|
||||
: ParseEnum<AbCipTransport>(dto.Transport, tagName: null, driverInstanceId, "Transport"),
|
||||
EnableControllerBrowse = dto.EnableControllerBrowse ?? false,
|
||||
BrowsePageDelay = TimeSpan.FromMilliseconds(Math.Max(0, dto.BrowsePageDelayMs ?? 5)),
|
||||
EnableAlarmProjection = dto.EnableAlarmProjection ?? false,
|
||||
AlarmPollInterval = TimeSpan.FromMilliseconds(dto.AlarmPollIntervalMs ?? 1_000),
|
||||
EnableDeclarationOnlyUdtGrouping = dto.EnableDeclarationOnlyUdtGrouping ?? false,
|
||||
@@ -176,6 +177,11 @@ public static class AbCipDriverFactoryExtensions
|
||||
/// </summary>
|
||||
public bool? EnableControllerBrowse { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the pause in milliseconds between symbol-listing pages (native transport); absent means 5.
|
||||
/// </summary>
|
||||
public int? BrowsePageDelayMs { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether alarm projection is enabled.
|
||||
/// </summary>
|
||||
|
||||
@@ -4,10 +4,13 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||
internal static class AbCipTransportFactories
|
||||
{
|
||||
/// <summary>Returns the tag, enumerator and template-reader factories for <paramref name="transport"/>.</summary>
|
||||
/// <param name="transport">The transport.</param>
|
||||
/// <param name="browsePageDelay">Pause between native symbol-listing pages; null is the compiled default.</param>
|
||||
public static (IAbCipTagFactory Tags, IAbCipTagEnumeratorFactory Enumerators, IAbCipTemplateReaderFactory Templates)
|
||||
For(AbCipTransport transport) => transport switch
|
||||
For(AbCipTransport transport, TimeSpan? browsePageDelay = null) => transport switch
|
||||
{
|
||||
AbCipTransport.Native => (new NativeAbCipTagFactory(), new NativeAbCipTagEnumeratorFactory(),
|
||||
AbCipTransport.Native => (new NativeAbCipTagFactory(),
|
||||
new NativeAbCipTagEnumeratorFactory(browsePageDelay ?? NativeListingReader.DefaultPageDelay),
|
||||
new NativeAbCipTemplateReaderFactory()),
|
||||
_ => (new LibplctagTagFactory(), new LibplctagTagEnumeratorFactory(), new LibplctagTemplateReaderFactory()),
|
||||
};
|
||||
|
||||
@@ -29,4 +29,13 @@ public interface IAbCipTemplateReaderFactory
|
||||
/// <summary>Creates a new template reader instance.</summary>
|
||||
/// <returns>A new <see cref="IAbCipTemplateReader"/> instance.</returns>
|
||||
IAbCipTemplateReader Create();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a reader meant to serve MANY <see cref="IAbCipTemplateReader.ReadAsync"/> calls: the native
|
||||
/// reader then holds one device lease from its first read until it is disposed, so a caller fetching
|
||||
/// N templates opens one connection rather than one per template. Dispose it when the batch ends.
|
||||
/// The default is <see cref="Create"/> (a transport whose reader has no per-read connection cost).
|
||||
/// </summary>
|
||||
/// <returns>A new <see cref="IAbCipTemplateReader"/> instance.</returns>
|
||||
IAbCipTemplateReader CreateBatch() => Create();
|
||||
}
|
||||
|
||||
@@ -17,27 +17,34 @@ internal sealed class NativeAbCipTagEnumerator : IAbCipTagEnumerator
|
||||
{
|
||||
private readonly Func<AbCipTagCreateParams, (RequestScheduler Scheduler, Func<ValueTask> Release)> _acquire;
|
||||
private readonly ILogger _logger;
|
||||
private readonly TimeSpan _pageDelay;
|
||||
private readonly TimeProvider _time;
|
||||
|
||||
/// <summary>Production constructor: leases from <paramref name="pool"/>.</summary>
|
||||
/// <param name="pool">The connection pool (the factory passes <see cref="DeviceSessionPool.Shared"/>).</param>
|
||||
public NativeAbCipTagEnumerator(DeviceSessionPool pool)
|
||||
/// <param name="pageDelay">Pause between listing pages (see <see cref="NativeListingReader.DefaultPageDelay"/>).</param>
|
||||
public NativeAbCipTagEnumerator(DeviceSessionPool pool, TimeSpan pageDelay = default)
|
||||
: this(p =>
|
||||
{
|
||||
var lease = pool.Acquire(NativeAbCipDevice.Key(p), NativeAbCipDevice.Options(p));
|
||||
return (lease.Scheduler, lease.DisposeAsync);
|
||||
})
|
||||
}, pageDelay: pageDelay)
|
||||
{
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// <param name="pageDelay">Pause between listing pages; zero is back-to-back.</param>
|
||||
/// <param name="time">Clock for the pause; null is the system clock.</param>
|
||||
internal NativeAbCipTagEnumerator(
|
||||
Func<AbCipTagCreateParams, (RequestScheduler Scheduler, Func<ValueTask> Release)> acquire,
|
||||
ILogger? logger = null)
|
||||
ILogger? logger = null, TimeSpan pageDelay = default, TimeProvider? time = null)
|
||||
{
|
||||
_acquire = acquire;
|
||||
_logger = logger ?? NullLogger.Instance;
|
||||
_pageDelay = pageDelay;
|
||||
_time = time ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -49,7 +56,7 @@ internal sealed class NativeAbCipTagEnumerator : IAbCipTagEnumerator
|
||||
try
|
||||
{
|
||||
using var inner = new SymbolListingEnumerator(
|
||||
(_, name, ct) => NativeListingReader.ReadAsync(scheduler, name, ct));
|
||||
(_, name, ct) => NativeListingReader.ReadAsync(scheduler, name, ct, _pageDelay, _time));
|
||||
await foreach (var tag in inner.EnumerateAsync(deviceParams, cancellationToken).ConfigureAwait(false))
|
||||
yield return tag;
|
||||
}
|
||||
@@ -75,13 +82,25 @@ internal static class NativeListingReader
|
||||
/// <summary>Byte cap for one listing (64 MiB).</summary>
|
||||
internal const long MaxListingBytes = 64L * 1024 * 1024;
|
||||
|
||||
/// <summary>
|
||||
/// Default pause between listing pages. A big controller's listing is hundreds of pages and, unpaced,
|
||||
/// a back-to-back burst (one request in flight, roughly a Studio 5000 online browse). 5 ms is small
|
||||
/// against a page's own round-trip (tens of ms on a real network), so a 50k-symbol browse gains a few
|
||||
/// seconds, yet it yields the controller's message-processing slice between pages. Zero disables it.
|
||||
/// Operator knob: <c>browsePageDelayMs</c> on the driver config.
|
||||
/// </summary>
|
||||
internal static readonly TimeSpan DefaultPageDelay = TimeSpan.FromMilliseconds(5);
|
||||
|
||||
/// <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>
|
||||
/// <param name="cancellationToken">Cancels the read.</param>
|
||||
/// <param name="pageDelay">Pause before each page after the first; zero is back-to-back.</param>
|
||||
/// <param name="time">Clock for the pause; null is the system clock.</param>
|
||||
/// <returns>The raw entries, byte for byte what <see cref="CipSymbolObjectDecoder"/> expects.</returns>
|
||||
public static async Task<byte[]> ReadAsync(
|
||||
RequestScheduler scheduler, string listingName, CancellationToken cancellationToken)
|
||||
RequestScheduler scheduler, string listingName, CancellationToken cancellationToken,
|
||||
TimeSpan pageDelay = default, TimeProvider? time = null)
|
||||
{
|
||||
string? program = null;
|
||||
if (listingName != "@tags")
|
||||
@@ -96,21 +115,24 @@ internal static class NativeListingReader
|
||||
uint start = 0;
|
||||
for (var page = 0; page < MaxPages; page++)
|
||||
{
|
||||
if (page > 0 && pageDelay > TimeSpan.Zero)
|
||||
await Task.Delay(pageDelay, time ?? TimeProvider.System, cancellationToken).ConfigureAwait(false);
|
||||
var request = SymbolObjectServices.GetInstanceAttributeList(program, start);
|
||||
// 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);
|
||||
var (entries, pageMax, 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)
|
||||
// Continue past the HIGHEST instance seen so far, so the order a controller lists a page in does
|
||||
// not matter. A page whose highest instance is still below its start made no progress, and a
|
||||
// highest id that would wrap cannot be continued: both would loop forever against the controller.
|
||||
if (pageMax < start || pageMax == uint.MaxValue)
|
||||
throw new AbTransportException(AbStatus.BadReply,
|
||||
$"Symbol listing '{listingName}' made no progress (start {start}, last {last}).");
|
||||
start = last + 1;
|
||||
$"Symbol listing '{listingName}' made no progress (start {start}, highest instance {pageMax}).");
|
||||
start = pageMax + 1;
|
||||
}
|
||||
throw new AbTransportException(AbStatus.BadReply,
|
||||
$"Symbol listing '{listingName}' exceeded {MaxPages} pages.");
|
||||
@@ -120,8 +142,17 @@ internal static class NativeListingReader
|
||||
/// <summary>Factory for <see cref="NativeAbCipTagEnumerator"/>.</summary>
|
||||
internal sealed class NativeAbCipTagEnumeratorFactory : IAbCipTagEnumeratorFactory
|
||||
{
|
||||
private readonly TimeSpan _pageDelay;
|
||||
|
||||
/// <summary>Factory with the compiled default pacing.</summary>
|
||||
public NativeAbCipTagEnumeratorFactory() : this(NativeListingReader.DefaultPageDelay) { }
|
||||
|
||||
/// <summary>Factory pacing listing pages by <paramref name="pageDelay"/>.</summary>
|
||||
/// <param name="pageDelay">Pause between pages; zero is back-to-back.</param>
|
||||
public NativeAbCipTagEnumeratorFactory(TimeSpan pageDelay) => _pageDelay = pageDelay;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IAbCipTagEnumerator Create() => new NativeAbCipTagEnumerator(DeviceSessionPool.Shared);
|
||||
public IAbCipTagEnumerator Create() => new NativeAbCipTagEnumerator(DeviceSessionPool.Shared, _pageDelay);
|
||||
}
|
||||
|
||||
/// <summary>Bounded lease release shared by the native enumerator and template reader.</summary>
|
||||
|
||||
@@ -9,7 +9,9 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||
/// <summary>
|
||||
/// Native <see cref="IAbCipTemplateReader"/>: Get Attribute List then chunked Read Template on the
|
||||
/// device's shared scheduler, returning the 14-byte-header buffer <see cref="CipTemplateObjectDecoder"/>
|
||||
/// parses. The lease is held for one read and released when it completes.
|
||||
/// parses. By default the lease is held for one read and released when it completes; a reader built for a
|
||||
/// batch (<see cref="IAbCipTemplateReaderFactory.CreateBatch"/>) instead keeps one lease per device from its
|
||||
/// first read until it is disposed.
|
||||
/// </summary>
|
||||
internal sealed class NativeAbCipTemplateReader : IAbCipTemplateReader
|
||||
{
|
||||
@@ -24,27 +26,52 @@ internal sealed class NativeAbCipTemplateReader : IAbCipTemplateReader
|
||||
|
||||
private readonly Func<AbCipTagCreateParams, (RequestScheduler Scheduler, ICipChannel Channel, Func<ValueTask> Release)> _acquire;
|
||||
private readonly ILogger _logger;
|
||||
private readonly bool _holdLeases;
|
||||
private readonly object _heldLock = new();
|
||||
private readonly Dictionary<DeviceKey, (RequestScheduler Scheduler, ICipChannel Channel, Func<ValueTask> Release)> _held = new();
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>Production constructor: leases from <paramref name="pool"/>.</summary>
|
||||
/// <param name="pool">The connection pool (the factory passes <see cref="DeviceSessionPool.Shared"/>).</param>
|
||||
public NativeAbCipTemplateReader(DeviceSessionPool pool)
|
||||
/// <param name="holdLeases">True for a batch reader: keep each device's lease until disposed.</param>
|
||||
public NativeAbCipTemplateReader(DeviceSessionPool pool, bool holdLeases = false)
|
||||
: this(p =>
|
||||
{
|
||||
var lease = pool.Acquire(NativeAbCipDevice.Key(p), NativeAbCipDevice.Options(p));
|
||||
return (lease.Scheduler, lease.Channel, lease.DisposeAsync);
|
||||
})
|
||||
}, holdLeases: holdLeases)
|
||||
{
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// <param name="holdLeases">True for a batch reader: keep each device's lease until disposed.</param>
|
||||
internal NativeAbCipTemplateReader(
|
||||
Func<AbCipTagCreateParams, (RequestScheduler Scheduler, ICipChannel Channel, Func<ValueTask> Release)> acquire,
|
||||
ILogger? logger = null)
|
||||
ILogger? logger = null, bool holdLeases = false)
|
||||
{
|
||||
_acquire = acquire;
|
||||
_logger = logger ?? NullLogger.Instance;
|
||||
_holdLeases = holdLeases;
|
||||
}
|
||||
|
||||
/// <summary>The connection parts for one read, and whether this read owns (must release) them.</summary>
|
||||
private (RequestScheduler Scheduler, ICipChannel Channel, Func<ValueTask> Release, bool Owned) Lease(AbCipTagCreateParams p)
|
||||
{
|
||||
if (!_holdLeases)
|
||||
{
|
||||
var one = _acquire(p);
|
||||
return (one.Scheduler, one.Channel, one.Release, true);
|
||||
}
|
||||
var key = NativeAbCipDevice.Key(p);
|
||||
lock (_heldLock)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (!_held.TryGetValue(key, out var held))
|
||||
_held[key] = held = _acquire(p);
|
||||
return (held.Scheduler, held.Channel, held.Release, false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -55,7 +82,7 @@ internal sealed class NativeAbCipTemplateReader : IAbCipTemplateReader
|
||||
throw new ArgumentOutOfRangeException(nameof(templateInstanceId), "Template ids are 16-bit.");
|
||||
var id = (ushort)templateInstanceId;
|
||||
|
||||
var (scheduler, channel, release) = _acquire(deviceParams);
|
||||
var (scheduler, channel, release, owned) = Lease(deviceParams);
|
||||
try
|
||||
{
|
||||
var attrReply = await scheduler.SendAsync(TemplateObjectServices.GetAttributes(id), cancellationToken)
|
||||
@@ -91,12 +118,22 @@ internal sealed class NativeAbCipTemplateReader : IAbCipTemplateReader
|
||||
}
|
||||
finally
|
||||
{
|
||||
NativeLease.ReleaseBounded(release, _logger);
|
||||
if (owned) NativeLease.ReleaseBounded(release, _logger);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Nothing to release — each read owns and releases its own lease.</summary>
|
||||
public void Dispose() { }
|
||||
/// <summary>Releases the leases a batch reader held; a per-read reader has none.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
List<Func<ValueTask>> releases;
|
||||
lock (_heldLock)
|
||||
{
|
||||
_disposed = true;
|
||||
releases = [.. _held.Values.Select(h => h.Release)];
|
||||
_held.Clear();
|
||||
}
|
||||
foreach (var release in releases) NativeLease.ReleaseBounded(release, _logger);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Factory for <see cref="NativeAbCipTemplateReader"/>.</summary>
|
||||
@@ -104,4 +141,7 @@ internal sealed class NativeAbCipTemplateReaderFactory : IAbCipTemplateReaderFac
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public IAbCipTemplateReader Create() => new NativeAbCipTemplateReader(DeviceSessionPool.Shared);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IAbCipTemplateReader CreateBatch() => new NativeAbCipTemplateReader(DeviceSessionPool.Shared, holdLeases: true);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user