/// <summary>Returns the connection to the pool for subsequent reuse.</summary> /// <param name="connection">The connection to return.</param> public void ReturnConnection(HttpConnection connection) { TimeSpan lifetime = _poolManager.Settings._pooledConnectionLifetime; bool lifetimeExpired = lifetime != Timeout.InfiniteTimeSpan && (lifetime == TimeSpan.Zero || connection.CreationTime + lifetime <= DateTime.UtcNow); if (!lifetimeExpired) { 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 and this one's still valid, simply transfer this one to them rather than pooling it. // Note that while we checked connection lifetime above, we don't check idle timeout, as even if idle timeout // is zero, we consider a connection that's just handed from one use to another to never actually be idle. bool receivedUnexpectedData = false; if (HasWaiter()) { receivedUnexpectedData = connection.EnsureReadAheadAndPollRead(); if (!receivedUnexpectedData && TransferConnection(connection)) { if (NetEventSource.IsEnabled) { connection.Trace("Transferred connection to waiter."); } return; } } // If the connection is still valid, add it to the list. // 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. We also dispose of connections if connection // timeouts are such that the connection would immediately expire, anyway, as // well as for connections that have unexpectedly received extraneous data / EOF. if (!receivedUnexpectedData && !_disposed && _poolManager.Settings._pooledConnectionIdleTimeout != TimeSpan.Zero) { // Pool the connection by adding it to the list. list.Add(new CachedConnection(connection)); if (NetEventSource.IsEnabled) { connection.Trace("Stored connection in pool."); } return; } } } // The connection could be not be reused. Dispose of it. // Disposing it will alert any waiters that a connection slot has become available. if (NetEventSource.IsEnabled) { connection.Trace( lifetimeExpired ? "Disposing connection return to pool. Connection lifetime expired." : _poolManager.Settings._pooledConnectionIdleTimeout == TimeSpan.Zero ? "Disposing connection returned to pool. Zero idle timeout." : _disposed ? "Disposing connection returned to pool. Pool was disposed." : "Disposing connection returned to pool. Read-ahead unexpectedly completed."); } connection.Dispose(); }
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; 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(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 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(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. } }
private ValueTask <HttpConnection> GetOrReserveHttp11ConnectionAsync(CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) { return(new ValueTask <HttpConnection>(Task.FromCanceled <HttpConnection>(cancellationToken))); } TimeSpan pooledConnectionLifetime = _poolManager.Settings._pooledConnectionLifetime; TimeSpan pooledConnectionIdleTimeout = _poolManager.Settings._pooledConnectionIdleTimeout; DateTimeOffset now = DateTimeOffset.UtcNow; List <CachedConnection> list = _idleConnections; // Try to find a usable cached connection. // If we can't find one, we will either wait for one to become available (if at the connection limit) // or just increment the connection count and return null so the caller can create a new connection. TaskCompletionSourceWithCancellation <HttpConnection> waiter; while (true) { CachedConnection cachedConnection; lock (SyncObj) { if (list.Count > 0) { // We have a cached connection that we can attempt to use. // Test it below outside 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. if (_associatedConnectionCount < _maxConnections) { // We are under the connection limit, so just increment the count and return null // to indicate to the caller that they should create a new connection. IncrementConnectionCountNoLock(); return(new ValueTask <HttpConnection>((HttpConnection)null)); } else { // We've reached the connection limit and need to wait for an existing connection // to become available, or to be closed so that we can create a new connection. // Enqueue a waiter that will be signalled when this happens. // Break out of the loop and then do the actual wait below. waiter = EnqueueWaiter(); break; } // 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>(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 loop around to try again. if (NetEventSource.IsEnabled) { conn.Trace("Found invalid connection in pool."); } conn.Dispose(); } // We are at the connection limit. Wait for an available connection or connection count (indicated by null). if (NetEventSource.IsEnabled) { Trace("Connection limit reached, waiting for available connection."); } return(new ValueTask <HttpConnection>(waiter.WaitWithCancellationAsync(cancellationToken))); }