/// <summary>Removes the specified waiter from the waiters list as part of a cancellation request.</summary> /// <param name="waiter">The waiter to remove.</param> /// <returns>true if the waiter was in the list; otherwise, false.</returns> private bool RemoveWaiterForCancellation(ConnectionWaiter waiter) { Debug.Assert(Monitor.IsEntered(SyncObj)); Debug.Assert(waiter != null); Debug.Assert(waiter._cancellationToken.IsCancellationRequested); bool inList = waiter._next != null || waiter._prev != null || _waitersHead == waiter || _waitersTail == waiter; if (waiter._next != null) { waiter._next._prev = waiter._prev; } if (waiter._prev != null) { waiter._prev._next = waiter._next; } if (_waitersHead == waiter && _waitersTail == waiter) { _waitersHead = _waitersTail = null; } else if (_waitersHead == waiter) { _waitersHead = waiter._next; } else if (_waitersTail == waiter) { _waitersTail = waiter._prev; } waiter._next = null; waiter._prev = null; return(inList); }
public ValueTask <HttpConnection> GetConnectionAsync <TState>(Func <TState, ValueTask <HttpConnection> > createConnection, TState state) { List <CachedConnection> list = _idleConnections; lock (SyncObj) { // Try to return a cached connection. We need to loop in case the connection // we get from the list is unusable. while (list.Count > 0) { CachedConnection cachedConnection = list[list.Count - 1]; list.RemoveAt(list.Count - 1); if (cachedConnection.IsUsable()) { // We found a valid collection. Return it. return(new ValueTask <HttpConnection>(cachedConnection._connection)); } // We got a connection, but it was already closed by the server or the // server sent unexpected data or the connection is too old. In any case, // we can't use the connection, so get rid of it and try again. cachedConnection._connection.Dispose(); } // No valid cached connections, so we need to create a new one. If // there's no limit on the number of connections associated with this // pool, or if we haven't reached such a limit, simply create a new // connection. if (_waiters == null || _associatedConnectionCount < _maxConnections) { IncrementConnectionCountNoLock(); return(WaitForCreatedConnectionAsync(createConnection(state))); } else { // There is a limit, and we've reached it, which means we need to // wait for a connection to be returned to the pool or for a connection // associated with the pool to be dropped before we can create a // new one. Create a waiter object and register it with the pool; it'll // be signaled with the created connection when one is returned or // space is available and the provided creation func has successfully // created the connection to be used. var waiter = new ConnectionWaiter <TState>(this, createConnection, state); _waiters.Enqueue(waiter); return(new ValueTask <HttpConnection>(waiter.Task)); } // Note that we don't check for _disposed. We may end up disposing the // created connection when it's returned, but we don't want to block use // of the pool if it's already been disposed, as there's a race condition // between getting a pool and someone disposing of it, and we don't want // to complicate the logic about trying to get a different pool when the // retrieved one has been disposed of. In the future we could alternatively // try returning such connections to whatever pool is currently considered // current for that endpoint, if there is one. } }
/// <summary>Returns the connection to the pool for subsequent reuse.</summary> /// <param name="connection">The connection to return.</param> public void ReturnConnection(HttpConnection connection) { List <CachedConnection> list = _idleConnections; lock (SyncObj) { Debug.Assert(list.Count <= _maxConnections, $"Expected {list.Count} <= {_maxConnections}"); // Mark the pool as still being active. _usedSinceLastCleanup = true; // If there's someone waiting for a connection, simply // transfer this one to them rather than pooling it. if (_waitersTail != null) { ConnectionWaiter waiter = DequeueWaiter(); waiter._cancellationTokenRegistration.Dispose(); if (NetEventSource.IsEnabled) { connection.Trace("Transferring connection returned to pool."); } waiter.SetResult(connection); return; } // If the pool has been disposed of, dispose the connection being returned, // as the pool is being deactivated. We do this after the above in order to // use pooled connections to satisfy any requests that pended before the // the pool was disposed of. if (_disposed) { if (NetEventSource.IsEnabled) { connection.Trace("Disposing connection returned to disposed pool."); } connection.Dispose(); return; } // Pool the connection by adding it to the list. list.Add(new CachedConnection(connection)); if (NetEventSource.IsEnabled) { connection.Trace("Stored connection in pool."); } } }
/// <summary>Enqueues a waiter to the waiters list.</summary> /// <param name="waiter">The waiter to add.</param> private void EnqueueWaiter(ConnectionWaiter waiter) { Debug.Assert(Monitor.IsEntered(SyncObj)); Debug.Assert(waiter != null); Debug.Assert(waiter._next == null); Debug.Assert(waiter._prev == null); waiter._next = _waitersHead; if (_waitersHead != null) { _waitersHead._prev = waiter; } else { Debug.Assert(_waitersTail == null); _waitersTail = waiter; } _waitersHead = waiter; }
/// <summary>Dequeues a waiter from the waiters list. The list must not be empty.</summary> /// <returns>The dequeued waiter.</returns> private ConnectionWaiter DequeueWaiter() { Debug.Assert(Monitor.IsEntered(SyncObj)); Debug.Assert(_waitersTail != null); ConnectionWaiter waiter = _waitersTail; _waitersTail = waiter._prev; if (_waitersTail != null) { _waitersTail._next = null; } else { Debug.Assert(_waitersHead == waiter); _waitersHead = null; } waiter._next = null; waiter._prev = null; return(waiter); }
public ValueTask <HttpConnection> GetConnectionAsync <TState>( Func <TState, CancellationToken, ValueTask <HttpConnection> > createConnection, TState state, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) { return(new ValueTask <HttpConnection>(Task.FromCanceled <HttpConnection>(cancellationToken))); } List <CachedConnection> list = _idleConnections; lock (SyncObj) { // Try to return a cached connection. We need to loop in case the connection // we get from the list is unusable. while (list.Count > 0) { CachedConnection cachedConnection = list[list.Count - 1]; HttpConnection conn = cachedConnection._connection; list.RemoveAt(list.Count - 1); if (cachedConnection.IsUsable()) { // We found a valid collection. Return it. if (NetEventSource.IsEnabled) { conn.Trace("Found usable connection in pool."); } return(new ValueTask <HttpConnection>(conn)); } // We got a connection, but it was already closed by the server or the // server sent unexpected data or the connection is too old. In any case, // we can't use the connection, so get rid of it and try again. if (NetEventSource.IsEnabled) { conn.Trace("Found invalid connection in pool."); } conn.Dispose(); } // No valid cached connections, so we need to create a new one. If // there's no limit on the number of connections associated with this // pool, or if we haven't reached such a limit, simply create a new // connection. if (_associatedConnectionCount < _maxConnections) { if (NetEventSource.IsEnabled) { Trace("Creating new connection for pool."); } IncrementConnectionCountNoLock(); return(WaitForCreatedConnectionAsync(createConnection(state, cancellationToken))); } else { // There is a limit, and we've reached it, which means we need to // wait for a connection to be returned to the pool or for a connection // associated with the pool to be dropped before we can create a // new one. Create a waiter object and register it with the pool; it'll // be signaled with the created connection when one is returned or // space is available and the provided creation func has successfully // created the connection to be used. if (NetEventSource.IsEnabled) { Trace("Limit reached. Waiting to create new connection."); } var waiter = new ConnectionWaiter <TState>(this, createConnection, state, cancellationToken); EnqueueWaiter(waiter); if (cancellationToken.CanBeCanceled) { // If cancellation could be requested, register a callback for it that'll cancel // the waiter and remove the waiter from the queue. Note that this registration needs // to happen under the reentrant lock and after enqueueing the waiter. waiter._cancellationTokenRegistration = cancellationToken.Register(s => { var innerWaiter = (ConnectionWaiter)s; lock (innerWaiter._pool.SyncObj) { // If it's in the list, remove it and cancel it. if (innerWaiter._pool.RemoveWaiterForCancellation(innerWaiter)) { bool canceled = innerWaiter.TrySetCanceled(innerWaiter._cancellationToken); Debug.Assert(canceled); } } }, waiter); } return(new ValueTask <HttpConnection>(waiter.Task)); } // Note that we don't check for _disposed. We may end up disposing the // created connection when it's returned, but we don't want to block use // of the pool if it's already been disposed, as there's a race condition // between getting a pool and someone disposing of it, and we don't want // to complicate the logic about trying to get a different pool when the // retrieved one has been disposed of. In the future we could alternatively // try returning such connections to whatever pool is currently considered // current for that endpoint, if there is one. } }
/// <summary> /// Decrements the number of connections associated with the pool. /// If there are waiters on the pool due to having reached the maximum, /// this will instead try to transfer the count to one of them. /// </summary> public void DecrementConnectionCount() { if (NetEventSource.IsEnabled) { Trace(null); } lock (SyncObj) { Debug.Assert(_associatedConnectionCount > 0 && _associatedConnectionCount <= _maxConnections, $"Expected 0 < {_associatedConnectionCount} <= {_maxConnections}"); // Mark the pool as not being stale. _usedSinceLastCleanup = true; if (_waitersHead == null) { // There are no waiters to which the count should logically be transferred, // so simply decrement the count. _associatedConnectionCount--; } else { // There's at least one waiter to which we should try to logically transfer // the associated count. Get the waiter. Debug.Assert(_idleConnections.Count == 0, $"With {_idleConnections} connections, we shouldn't have a waiter."); ConnectionWaiter waiter = DequeueWaiter(); Debug.Assert(waiter != null, "Expected non-null waiter"); Debug.Assert(waiter.Task.Status == TaskStatus.WaitingForActivation, $"Expected {waiter.Task.Status} == {nameof(TaskStatus.WaitingForActivation)}"); waiter._cancellationTokenRegistration.Dispose(); // Having a waiter means there must not be any idle connections, so we need to create // one, and we do so using the logic associated with the waiter. ValueTask <HttpConnection> connectionTask = waiter.CreateConnectionAsync(); if (connectionTask.IsCompletedSuccessfully) { // We synchronously and successfully created a connection (this is rare). // Transfer the connection to the waiter. Since we already have a count // that's inflated due to the connection being disassociated, we don't // need to change the count here. waiter.SetResult(connectionTask.Result); } else { // We initiated a connection creation. When it completes, transfer the result to the waiter. connectionTask.AsTask().ContinueWith((innerConnectionTask, state) => { var innerWaiter = (ConnectionWaiter)state; try { // Get the resulting connection. HttpConnection result = innerConnectionTask.GetAwaiter().GetResult(); // Store the resulting connection into the waiter. As in the synchronous case, // since we already have a count that's inflated due to the connection being // disassociated, we don't need to change the count here. innerWaiter.SetResult(innerConnectionTask.Result); } catch (Exception e) { // The creation operation failed. Store the exception into the waiter. innerWaiter.SetException(e); // At this point, a connection was dropped and we failed to replace it, // which means our connection count still needs to be decremented. innerWaiter._pool.DecrementConnectionCount(); } }, waiter, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); } } } }
private ValueTask <(HttpConnection, HttpResponseMessage)> GetConnectionAsync(HttpRequestMessage request, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) { return(new ValueTask <(HttpConnection, HttpResponseMessage)>(Task.FromCanceled <(HttpConnection, HttpResponseMessage)>(cancellationToken))); } TimeSpan pooledConnectionLifetime = _poolManager.Settings._pooledConnectionLifetime; TimeSpan pooledConnectionIdleTimeout = _poolManager.Settings._pooledConnectionIdleTimeout; DateTimeOffset now = DateTimeOffset.UtcNow; List <CachedConnection> list = _idleConnections; // Try to get a cached connection. If we can and if it's usable, return it. If we can but it's not usable, // try again. And if we can't because there aren't any valid ones, create a new one and return it. while (true) { CachedConnection cachedConnection; lock (SyncObj) { if (list.Count > 0) { // Pop off the next connection to try. We'll test it outside of the lock // to avoid doing expensive validation while holding the lock. cachedConnection = list[list.Count - 1]; list.RemoveAt(list.Count - 1); } else { // No valid cached connections, so we need to create a new one. If // there's no limit on the number of connections associated with this // pool, or if we haven't reached such a limit, simply create a new // connection. if (_associatedConnectionCount < _maxConnections) { if (NetEventSource.IsEnabled) { Trace("Creating new connection for pool."); } IncrementConnectionCountNoLock(); return(WaitForCreatedConnectionAsync(CreateConnectionAsync(request, cancellationToken))); } else { // There is a limit, and we've reached it, which means we need to // wait for a connection to be returned to the pool or for a connection // associated with the pool to be dropped before we can create a // new one. Create a waiter object and register it with the pool; it'll // be signaled with the created connection when one is returned or // space is available and the provided creation func has successfully // created the connection to be used. if (NetEventSource.IsEnabled) { Trace("Limit reached. Waiting to create new connection."); } var waiter = new ConnectionWaiter(this, request, cancellationToken); EnqueueWaiter(waiter); if (cancellationToken.CanBeCanceled) { // If cancellation could be requested, register a callback for it that'll cancel // the waiter and remove the waiter from the queue. Note that this registration needs // to happen under the reentrant lock and after enqueueing the waiter. waiter._cancellationTokenRegistration = cancellationToken.Register(s => { var innerWaiter = (ConnectionWaiter)s; lock (innerWaiter._pool.SyncObj) { // If it's in the list, remove it and cancel it. if (innerWaiter._pool.RemoveWaiterForCancellation(innerWaiter)) { bool canceled = innerWaiter.TrySetCanceled(innerWaiter._cancellationToken); Debug.Assert(canceled); } } }, waiter); } return(new ValueTask <(HttpConnection, HttpResponseMessage)>(waiter.Task)); } // Note that we don't check for _disposed. We may end up disposing the // created connection when it's returned, but we don't want to block use // of the pool if it's already been disposed, as there's a race condition // between getting a pool and someone disposing of it, and we don't want // to complicate the logic about trying to get a different pool when the // retrieved one has been disposed of. In the future we could alternatively // try returning such connections to whatever pool is currently considered // current for that endpoint, if there is one. } } HttpConnection conn = cachedConnection._connection; if (cachedConnection.IsUsable(now, pooledConnectionLifetime, pooledConnectionIdleTimeout) && !conn.EnsureReadAheadAndPollRead()) { // We found a valid connection. Return it. if (NetEventSource.IsEnabled) { conn.Trace("Found usable connection in pool."); } return(new ValueTask <(HttpConnection, HttpResponseMessage)>((conn, null))); } // We got a connection, but it was already closed by the server or the // server sent unexpected data or the connection is too old. In any case, // we can't use the connection, so get rid of it and loop around to try again. if (NetEventSource.IsEnabled) { conn.Trace("Found invalid connection in pool."); } conn.Dispose(); } }