443 lines
18 KiB
C#
443 lines
18 KiB
C#
using System;
|
|
using System.Data;
|
|
using System.Diagnostics;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Threading;
|
|
using System.Threading.Channels;
|
|
using System.Threading.Tasks;
|
|
using System.Transactions;
|
|
using Npgsql.Internal;
|
|
using Npgsql.Logging;
|
|
using Npgsql.Util;
|
|
|
|
namespace Npgsql;
|
|
|
|
class ConnectorPool : ConnectorSource
|
|
{
|
|
#region Fields and properties
|
|
|
|
static readonly NpgsqlLogger Log = NpgsqlLogManager.CreateLogger(nameof(ConnectorPool));
|
|
|
|
readonly int _max;
|
|
readonly int _min;
|
|
readonly TimeSpan _connectionLifetime;
|
|
|
|
volatile int _numConnectors;
|
|
|
|
volatile int _idleCount;
|
|
|
|
/// <summary>
|
|
/// Tracks all connectors currently managed by this pool, whether idle or busy.
|
|
/// Only updated rarely - when physical connections are opened/closed - but is read in perf-sensitive contexts.
|
|
/// </summary>
|
|
private protected readonly NpgsqlConnector?[] Connectors;
|
|
|
|
readonly MultiHostConnectorPool? _parentPool;
|
|
|
|
/// <summary>
|
|
/// Reader side for the idle connector channel. Contains nulls in order to release waiting attempts after
|
|
/// a connector has been physically closed/broken.
|
|
/// </summary>
|
|
readonly ChannelReader<NpgsqlConnector?> _idleConnectorReader;
|
|
internal ChannelWriter<NpgsqlConnector?> IdleConnectorWriter { get; }
|
|
|
|
/// <summary>
|
|
/// Incremented every time this pool is cleared via <see cref="NpgsqlConnection.ClearPool"/> or
|
|
/// <see cref="NpgsqlConnection.ClearAllPools"/>. Allows us to identify connections which were
|
|
/// created before the clear.
|
|
/// </summary>
|
|
volatile int _clearCounter;
|
|
|
|
static readonly TimerCallback PruningTimerCallback = PruneIdleConnectors;
|
|
readonly Timer _pruningTimer;
|
|
readonly TimeSpan _pruningSamplingInterval;
|
|
readonly int _pruningSampleSize;
|
|
readonly int[] _pruningSamples;
|
|
readonly int _pruningMedianIndex;
|
|
volatile bool _pruningTimerEnabled;
|
|
int _pruningSampleIndex;
|
|
|
|
volatile int _isClearing;
|
|
|
|
static readonly SingleThreadSynchronizationContext SingleThreadSynchronizationContext = new("NpgsqlRemainingAsyncSendWorker");
|
|
|
|
#endregion
|
|
|
|
internal sealed override (int Total, int Idle, int Busy) Statistics
|
|
{
|
|
get
|
|
{
|
|
var numConnectors = _numConnectors;
|
|
var idleCount = _idleCount;
|
|
return (numConnectors, idleCount, numConnectors - idleCount);
|
|
}
|
|
}
|
|
|
|
internal sealed override bool OwnsConnectors => true;
|
|
|
|
internal ConnectorPool(NpgsqlConnectionStringBuilder settings, string connString, MultiHostConnectorPool? parentPool = null)
|
|
: base(settings, connString)
|
|
{
|
|
if (settings.MaxPoolSize < settings.MinPoolSize)
|
|
throw new ArgumentException($"Connection can't have 'Max Pool Size' {settings.MaxPoolSize} under 'Min Pool Size' {settings.MinPoolSize}");
|
|
|
|
_parentPool = parentPool;
|
|
|
|
// We enforce Max Pool Size, so no need to to create a bounded channel (which is less efficient)
|
|
// On the consuming side, we have the multiplexing write loop but also non-multiplexing Rents
|
|
// On the producing side, we have connections being released back into the pool (both multiplexing and not)
|
|
var idleChannel = Channel.CreateUnbounded<NpgsqlConnector?>();
|
|
_idleConnectorReader = idleChannel.Reader;
|
|
IdleConnectorWriter = idleChannel.Writer;
|
|
|
|
_max = settings.MaxPoolSize;
|
|
_min = settings.MinPoolSize;
|
|
|
|
if (settings.ConnectionPruningInterval == 0)
|
|
throw new ArgumentException("ConnectionPruningInterval can't be 0.");
|
|
var connectionIdleLifetime = TimeSpan.FromSeconds(settings.ConnectionIdleLifetime);
|
|
var pruningSamplingInterval = TimeSpan.FromSeconds(settings.ConnectionPruningInterval);
|
|
if (connectionIdleLifetime < pruningSamplingInterval)
|
|
throw new ArgumentException($"Connection can't have ConnectionIdleLifetime {connectionIdleLifetime} under ConnectionPruningInterval {_pruningSamplingInterval}");
|
|
|
|
_pruningTimer = new Timer(PruningTimerCallback, this, Timeout.Infinite, Timeout.Infinite);
|
|
_pruningSampleSize = DivideRoundingUp(settings.ConnectionIdleLifetime, settings.ConnectionPruningInterval);
|
|
_pruningMedianIndex = DivideRoundingUp(_pruningSampleSize, 2) - 1; // - 1 to go from length to index
|
|
_pruningSamplingInterval = pruningSamplingInterval;
|
|
_pruningSamples = new int[_pruningSampleSize];
|
|
_pruningTimerEnabled = false;
|
|
|
|
_connectionLifetime = TimeSpan.FromSeconds(settings.ConnectionLifetime);
|
|
Connectors = new NpgsqlConnector[_max];
|
|
}
|
|
|
|
internal sealed override ValueTask<NpgsqlConnector> Get(
|
|
NpgsqlConnection conn, NpgsqlTimeout timeout, bool async, CancellationToken cancellationToken)
|
|
{
|
|
return TryGetIdleConnector(out var connector)
|
|
? new ValueTask<NpgsqlConnector>(connector)
|
|
: RentAsync(conn, timeout, async, cancellationToken);
|
|
|
|
async ValueTask<NpgsqlConnector> RentAsync(
|
|
NpgsqlConnection conn, NpgsqlTimeout timeout, bool async, CancellationToken cancellationToken)
|
|
{
|
|
// First, try to open a new physical connector. This will fail if we're at max capacity.
|
|
var connector = await OpenNewConnector(conn, timeout, async, cancellationToken);
|
|
if (connector != null)
|
|
return connector;
|
|
|
|
// We're at max capacity. Block on the idle channel with a timeout.
|
|
// Note that Channels guarantee fair FIFO behavior to callers of ReadAsync (first-come first-
|
|
// served), which is crucial to us.
|
|
using var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
|
var finalToken = linkedSource.Token;
|
|
linkedSource.CancelAfter(timeout.CheckAndGetTimeLeft());
|
|
|
|
while (true)
|
|
{
|
|
try
|
|
{
|
|
if (async)
|
|
{
|
|
connector = await _idleConnectorReader.ReadAsync(finalToken);
|
|
if (CheckIdleConnector(connector))
|
|
return connector;
|
|
}
|
|
else
|
|
{
|
|
// Channels don't have a sync API. To avoid sync-over-async issues, we use a special single-
|
|
// thread synchronization context which ensures that callbacks are executed on a dedicated
|
|
// thread.
|
|
// Note that AsTask isn't safe here for getting the result, since it still causes some continuation code
|
|
// to get executed on the TP (which can cause deadlocks).
|
|
using (SingleThreadSynchronizationContext.Enter())
|
|
using (var mre = new ManualResetEventSlim())
|
|
{
|
|
_idleConnectorReader.WaitToReadAsync(finalToken).GetAwaiter().OnCompleted(() => mre.Set());
|
|
mre.Wait(finalToken);
|
|
}
|
|
}
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
Debug.Assert(finalToken.IsCancellationRequested);
|
|
throw new NpgsqlException(
|
|
$"The connection pool has been exhausted, either raise 'Max Pool Size' (currently {_max}) " +
|
|
$"or 'Timeout' (currently {Settings.Timeout} seconds) in your connection string.",
|
|
new TimeoutException());
|
|
}
|
|
catch (ChannelClosedException)
|
|
{
|
|
throw new NpgsqlException("The connection pool has been shut down.");
|
|
}
|
|
|
|
// If we're here, our waiting attempt on the idle connector channel was released with a null
|
|
// (or bad connector), or we're in sync mode. Check again if a new idle connector has appeared since we last checked.
|
|
if (TryGetIdleConnector(out connector))
|
|
return connector;
|
|
|
|
// We might have closed a connector in the meantime and no longer be at max capacity
|
|
// so try to open a new connector and if that fails, loop again.
|
|
connector = await OpenNewConnector(conn, timeout, async, cancellationToken);
|
|
if (connector != null)
|
|
return connector;
|
|
}
|
|
}
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
internal sealed override bool TryGetIdleConnector([NotNullWhen(true)] out NpgsqlConnector? connector)
|
|
{
|
|
while (_idleConnectorReader.TryRead(out var nullableConnector))
|
|
{
|
|
if (CheckIdleConnector(nullableConnector))
|
|
{
|
|
connector = nullableConnector;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
connector = null;
|
|
return false;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
bool CheckIdleConnector([NotNullWhen(true)] NpgsqlConnector? connector)
|
|
{
|
|
if (connector is null)
|
|
return false;
|
|
|
|
// Only decrement when the connector has a value.
|
|
Interlocked.Decrement(ref _idleCount);
|
|
|
|
// An connector could be broken because of a keepalive that occurred while it was
|
|
// idling in the pool
|
|
// TODO: Consider removing the pool from the keepalive code. The following branch is simply irrelevant
|
|
// if keepalive isn't turned on.
|
|
if (connector.IsBroken)
|
|
{
|
|
CloseConnector(connector);
|
|
return false;
|
|
}
|
|
|
|
if (_connectionLifetime != TimeSpan.Zero && DateTime.UtcNow > connector.OpenTimestamp + _connectionLifetime)
|
|
{
|
|
Log.Debug("Connection has exceeded its maximum lifetime and will be closed.", connector.Id);
|
|
CloseConnector(connector);
|
|
return false;
|
|
}
|
|
|
|
Debug.Assert(connector.State == ConnectorState.Ready,
|
|
$"Got idle connector but {nameof(connector.State)} is {connector.State}");
|
|
Debug.Assert(connector.CommandsInFlightCount == 0,
|
|
$"Got idle connector but {nameof(connector.CommandsInFlightCount)} is {connector.CommandsInFlightCount}");
|
|
Debug.Assert(connector.MultiplexAsyncWritingLock == 0,
|
|
$"Got idle connector but {nameof(connector.MultiplexAsyncWritingLock)} is 1");
|
|
|
|
return true;
|
|
}
|
|
|
|
internal sealed override async ValueTask<NpgsqlConnector?> OpenNewConnector(
|
|
NpgsqlConnection conn, NpgsqlTimeout timeout, bool async, CancellationToken cancellationToken)
|
|
{
|
|
// As long as we're under max capacity, attempt to increase the connector count and open a new connection.
|
|
for (var numConnectors = _numConnectors; numConnectors < _max; numConnectors = _numConnectors)
|
|
{
|
|
// Note that we purposefully don't use SpinWait for this: https://github.com/dotnet/coreclr/pull/21437
|
|
if (Interlocked.CompareExchange(ref _numConnectors, numConnectors + 1, numConnectors) != numConnectors)
|
|
continue;
|
|
|
|
try
|
|
{
|
|
// We've managed to increase the open counter, open a physical connections.
|
|
var connector = new NpgsqlConnector(this, conn) { ClearCounter = _clearCounter };
|
|
await connector.Open(timeout, async, cancellationToken);
|
|
|
|
var i = 0;
|
|
for (; i < _max; i++)
|
|
if (Interlocked.CompareExchange(ref Connectors[i], connector, null) == null)
|
|
break;
|
|
|
|
Debug.Assert(i < _max, $"Could not find free slot in {Connectors} when opening.");
|
|
if (i == _max)
|
|
throw new NpgsqlException($"Could not find free slot in {Connectors} when opening. Please report a bug.");
|
|
|
|
// Only start pruning if we've incremented open count past _min.
|
|
// Note that we don't do it only once, on equality, because the thread which incremented open count past _min might get exception
|
|
// on NpgsqlConnector.Open due to timeout, CancellationToken or other reasons.
|
|
if (numConnectors >= _min)
|
|
UpdatePruningTimer();
|
|
|
|
return connector;
|
|
}
|
|
catch
|
|
{
|
|
// Physical open failed, decrement the open and busy counter back down.
|
|
Interlocked.Decrement(ref _numConnectors);
|
|
|
|
// In case there's a waiting attempt on the channel, we write a null to the idle connector channel
|
|
// to wake it up, so it will try opening (and probably throw immediately)
|
|
// Statement order is important since we have synchronous completions on the channel.
|
|
IdleConnectorWriter.TryWrite(null);
|
|
|
|
// Just in case we always call UpdatePruningTimer for failed physical open
|
|
UpdatePruningTimer();
|
|
|
|
throw;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
internal sealed override void Return(NpgsqlConnector connector)
|
|
{
|
|
Debug.Assert(!connector.InTransaction);
|
|
Debug.Assert(connector.MultiplexAsyncWritingLock == 0 || connector.IsBroken || connector.IsClosed,
|
|
$"About to return multiplexing connector to the pool, but {nameof(connector.MultiplexAsyncWritingLock)} is {connector.MultiplexAsyncWritingLock}");
|
|
|
|
// If Clear/ClearAll has been been called since this connector was first opened,
|
|
// throw it away. The same if it's broken (in which case CloseConnector is only
|
|
// used to update state/perf counter).
|
|
if (connector.ClearCounter != _clearCounter || connector.IsBroken)
|
|
{
|
|
CloseConnector(connector);
|
|
return;
|
|
}
|
|
|
|
// Statement order is important since we have synchronous completions on the channel.
|
|
Interlocked.Increment(ref _idleCount);
|
|
var written = IdleConnectorWriter.TryWrite(connector);
|
|
Debug.Assert(written);
|
|
}
|
|
|
|
internal override void Clear()
|
|
{
|
|
Interlocked.Increment(ref _clearCounter);
|
|
|
|
if (Interlocked.CompareExchange(ref _isClearing, 1, 0) == 1)
|
|
return;
|
|
|
|
try
|
|
{
|
|
var count = _idleCount;
|
|
while (count > 0 && _idleConnectorReader.TryRead(out var connector))
|
|
{
|
|
if (CheckIdleConnector(connector))
|
|
{
|
|
CloseConnector(connector);
|
|
count--;
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_isClearing = 0;
|
|
}
|
|
}
|
|
|
|
void CloseConnector(NpgsqlConnector connector)
|
|
{
|
|
try
|
|
{
|
|
connector.Close();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Log.Warn("Exception while closing connector", e, connector.Id);
|
|
}
|
|
|
|
var i = 0;
|
|
for (; i < _max; i++)
|
|
if (Interlocked.CompareExchange(ref Connectors[i], null, connector) == connector)
|
|
break;
|
|
|
|
Debug.Assert(i < _max, $"Could not find free slot in {Connectors} when closing.");
|
|
if (i == _max)
|
|
throw new NpgsqlException($"Could not find free slot in {Connectors} when closing. Please report a bug.");
|
|
|
|
var numConnectors = Interlocked.Decrement(ref _numConnectors);
|
|
Debug.Assert(numConnectors >= 0);
|
|
|
|
// If a connector has been closed for any reason, we write a null to the idle connector channel to wake up
|
|
// a waiter, who will open a new physical connection
|
|
// Statement order is important since we have synchronous completions on the channel.
|
|
IdleConnectorWriter.TryWrite(null);
|
|
|
|
// Only turn off the timer one time, when it was this Close that brought Open back to _min.
|
|
if (numConnectors == _min)
|
|
UpdatePruningTimer();
|
|
}
|
|
|
|
internal override bool TryRemovePendingEnlistedConnector(NpgsqlConnector connector, Transaction transaction)
|
|
=> _parentPool is null
|
|
? base.TryRemovePendingEnlistedConnector(connector, transaction)
|
|
: _parentPool.TryRemovePendingEnlistedConnector(connector, transaction);
|
|
|
|
#region Pruning
|
|
|
|
void UpdatePruningTimer()
|
|
{
|
|
lock (_pruningTimer)
|
|
{
|
|
var numConnectors = _numConnectors;
|
|
if (numConnectors > _min && !_pruningTimerEnabled)
|
|
{
|
|
_pruningTimerEnabled = true;
|
|
_pruningTimer.Change(_pruningSamplingInterval, Timeout.InfiniteTimeSpan);
|
|
}
|
|
else if (numConnectors <= _min && _pruningTimerEnabled)
|
|
{
|
|
_pruningTimer.Change(Timeout.Infinite, Timeout.Infinite);
|
|
_pruningSampleIndex = 0;
|
|
_pruningTimerEnabled = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void PruneIdleConnectors(object? state)
|
|
{
|
|
var pool = (ConnectorPool)state!;
|
|
var samples = pool._pruningSamples;
|
|
int toPrune;
|
|
lock (pool._pruningTimer)
|
|
{
|
|
// Check if we might have been contending with DisablePruning.
|
|
if (!pool._pruningTimerEnabled)
|
|
return;
|
|
|
|
var sampleIndex = pool._pruningSampleIndex;
|
|
samples[sampleIndex] = pool._idleCount;
|
|
if (sampleIndex != pool._pruningSampleSize - 1)
|
|
{
|
|
pool._pruningSampleIndex = sampleIndex + 1;
|
|
pool._pruningTimer.Change(pool._pruningSamplingInterval, Timeout.InfiniteTimeSpan);
|
|
return;
|
|
}
|
|
|
|
// Calculate median value for pruning, reset index and timer, and release the lock.
|
|
Array.Sort(samples);
|
|
toPrune = samples[pool._pruningMedianIndex];
|
|
pool._pruningSampleIndex = 0;
|
|
pool._pruningTimer.Change(pool._pruningSamplingInterval, Timeout.InfiniteTimeSpan);
|
|
}
|
|
|
|
while (toPrune > 0 &&
|
|
pool._numConnectors > pool._min &&
|
|
pool._idleConnectorReader.TryRead(out var connector) &&
|
|
connector != null)
|
|
{
|
|
if (pool.CheckIdleConnector(connector))
|
|
{
|
|
pool.CloseConnector(connector);
|
|
toPrune--;
|
|
}
|
|
}
|
|
}
|
|
|
|
static int DivideRoundingUp(int value, int divisor) => 1 + (value - 1) / divisor;
|
|
|
|
#endregion
|
|
} |