fix(ab-transport): harden Task 9 pool and connection (close chaining, uncancellable shared open, monotonic throttle, canonical CIP path, internal ctors, shared-pool logger)
This commit is contained in:
@@ -16,9 +16,12 @@ public sealed class DeviceSessionPool
|
||||
/// <summary>The process-wide pool the drivers use. Tests build their own.</summary>
|
||||
public static DeviceSessionPool Shared { get; } = new();
|
||||
|
||||
internal sealed class Entry(CipConnection connection, RequestScheduler scheduler, ConnectionOptions options)
|
||||
internal sealed class Entry(CipConnection connection, RequestScheduler scheduler, ConnectionOptions options,
|
||||
Task predecessorClosed)
|
||||
{
|
||||
public CipConnection Connection { get; } = connection;
|
||||
/// <summary>Completes when every earlier connection for this key has finished closing.</summary>
|
||||
public Task PredecessorClosed { get; } = predecessorClosed;
|
||||
public RequestScheduler Scheduler { get; } = scheduler;
|
||||
public ConnectionOptions Options { get; } = options;
|
||||
public int RefCount; // under the pool lock
|
||||
@@ -30,7 +33,7 @@ public sealed class DeviceSessionPool
|
||||
private readonly Dictionary<DeviceKey, ForwardOpenThrottle> _throttles = new();
|
||||
private readonly Dictionary<DeviceKey, Task> _closing = new();
|
||||
private readonly TimeProvider _time;
|
||||
private readonly ILogger _logger;
|
||||
private volatile ILogger _logger;
|
||||
|
||||
public DeviceSessionPool(TimeProvider? time = null, ILogger? logger = null)
|
||||
{
|
||||
@@ -38,6 +41,14 @@ public sealed class DeviceSessionPool
|
||||
_logger = logger ?? NullLogger.Instance;
|
||||
}
|
||||
|
||||
/// <summary>Give <see cref="Shared"/> a logger (it is built before any host exists), so its
|
||||
/// options-mismatch and close-failure warnings reach the service log. Call once at startup.</summary>
|
||||
public static void ConfigureSharedLogger(ILoggerFactory loggerFactory)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(loggerFactory);
|
||||
Shared._logger = loggerFactory.CreateLogger<DeviceSessionPool>();
|
||||
}
|
||||
|
||||
/// <summary>Test hook: keys with a live entry.</summary>
|
||||
internal int EntryCount { get { lock (_lock) return _entries.Count; } }
|
||||
|
||||
@@ -54,9 +65,9 @@ public sealed class DeviceSessionPool
|
||||
{
|
||||
if (!_throttles.TryGetValue(key, out var throttle))
|
||||
_throttles[key] = throttle = new ForwardOpenThrottle(options.MinForwardOpenInterval, _time);
|
||||
var predecessor = _closing.GetValueOrDefault(key);
|
||||
var predecessor = _closing.GetValueOrDefault(key) ?? Task.CompletedTask;
|
||||
var connection = new CipConnection(key, options, throttle, _time, predecessor);
|
||||
entry = new Entry(connection, new RequestScheduler(connection, options.AllowPacking), options);
|
||||
entry = new Entry(connection, new RequestScheduler(connection, options.AllowPacking), options, predecessor);
|
||||
_entries[key] = entry;
|
||||
}
|
||||
else if (!entry.Options.Equals(options) && !entry.MismatchLogged)
|
||||
@@ -75,27 +86,45 @@ public sealed class DeviceSessionPool
|
||||
internal async ValueTask ReleaseAsync(DeviceKey key, Entry entry)
|
||||
{
|
||||
TaskCompletionSource closed;
|
||||
Task chain;
|
||||
lock (_lock)
|
||||
{
|
||||
if (--entry.RefCount > 0) return;
|
||||
_entries.Remove(key);
|
||||
closed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
_closing[key] = closed.Task;
|
||||
// The successor must wait for THIS close and for every earlier one still running. An entry
|
||||
// that never opened closes instantly; without the chain it would hide a slow predecessor
|
||||
// (A closing, B acquired+released unopened, C opens while A's Forward Close is still pending).
|
||||
chain = Task.WhenAll(entry.PredecessorClosed, closed.Task);
|
||||
_closing[key] = chain;
|
||||
}
|
||||
|
||||
// Outside the lock: scheduler first (fails its waiters, stops sending), then Forward Close +
|
||||
// UnRegisterSession via the connection.
|
||||
try
|
||||
{
|
||||
await entry.Scheduler.DisposeAsync().ConfigureAwait(false);
|
||||
await entry.Connection.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
_ = chain.ContinueWith(_ =>
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_closing.TryGetValue(key, out var t) && t == closed.Task) _closing.Remove(key);
|
||||
if (_closing.TryGetValue(key, out var t) && t == chain) _closing.Remove(key);
|
||||
}
|
||||
}, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
|
||||
|
||||
// Outside the lock: scheduler first (fails its waiters, stops sending), then Forward Close +
|
||||
// UnRegisterSession via the connection. The connection is disposed even if the scheduler's
|
||||
// dispose throws: otherwise its keep-alive would hold the press connection open forever.
|
||||
try
|
||||
{
|
||||
try { await entry.Scheduler.DisposeAsync().ConfigureAwait(false); }
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Disposing the request scheduler for {Host}:{Port} failed; closing the connection anyway.",
|
||||
key.Host, key.Port);
|
||||
}
|
||||
try { await entry.Connection.DisposeAsync().ConfigureAwait(false); }
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Closing the connection to {Host}:{Port} failed.", key.Host, key.Port);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
closed.TrySetResult(); // never faults: a successor only needs to know the close is over
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user