public async Task <PooledSession> AcquireSessionAsync(TransactionOptions transactionOptions, CancellationToken cancellationToken) { bool success = false; try { int sessionCount = Interlocked.Increment(ref _activeSessionCount); if (sessionCount > Options.MaximumActiveSessions && Options.WaitOnResourcesExhausted == ResourcesExhaustedBehavior.Fail) { // Not really an RpcException, but the cleanest way of representing it. // (The ADO.NET provider will convert it to a SpannerException with the same code.) throw new RpcException(new Status(StatusCode.ResourceExhausted, "Local maximum number of active sessions exceeded. Possibly resource leak in client code?")); } // We check for shutdown after incrementing ActiveSessionCount, and we *set* shutdown before checking ActiveSessionCount, // so it shouldn't be possible for us to miss this check but still end up with an acquisition task which waits forever because // the shutdown loop starts and finishes too quickly. if (Shutdown) { throw new InvalidOperationException("Session pool has already been shut down"); } PooledSession session = await AcquireSessionImplAsync(transactionOptions, cancellationToken).ConfigureAwait(false); success = true; return(session); } finally { if (!success) { Interlocked.Decrement(ref _activeSessionCount); } } }
public override void Release(PooledSession session, bool deleteSession) { if (deleteSession) { Parent.DeleteSessionFireAndForget(session); } }
private async Task <PooledSession> CreatePooledSessionAsync(CancellationToken cancellationToken) { bool success = false; bool canceled = false; Interlocked.Increment(ref _inFlightSessionCreationCount); Interlocked.Increment(ref _liveOrRequestedSessionCount); try { var callSettings = Client.Settings.CreateSessionSettings .WithExpiration(Expiration.FromTimeout(Options.Timeout)) .WithCancellationToken(cancellationToken); Session sessionProto; bool acquiredSemaphore = false; try { await Parent._sessionAcquisitionSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); acquiredSemaphore = true; sessionProto = await Client.CreateSessionAsync(_createSessionRequest, callSettings).ConfigureAwait(false); success = true; return(PooledSession.FromSessionName(this, sessionProto.SessionName)); } catch (OperationCanceledException) { canceled = true; throw; } finally { if (acquiredSemaphore) { Parent._sessionAcquisitionSemaphore.Release(); } } } finally { // Atomically set _healthy and determine whether we were previously healthy, but only if either we've succeeded, // or we failed for a reason other than cancellation. We don't want to go unhealthy just because a caller cancelled // a cancellation token before we had chance to create the session. if (success || !canceled) { bool wasHealthy = Interlocked.Exchange(ref _healthy, success ? 1 : 0) == 1; if (wasHealthy != success) { Parent._logger.Info(() => $"Session pool for {_databaseName} is now {(success ? "healthy" : "unhealthy")}."); } } // If this call failed, we can make another attempt. if (!success) { Interlocked.Decrement(ref _liveOrRequestedSessionCount); } Interlocked.Decrement(ref _inFlightSessionCreationCount); } }
public override void Release(PooledSession session, ByteString transactionToRollback, bool deleteSession) { // Note: we never roll back the transaction in a detached session. if (deleteSession) { Parent.DeleteSessionFireAndForget(session); } }
private async Task DeleteSessionAsync(PooledSession session) { try { await Client.DeleteSessionAsync(new DeleteSessionRequest { SessionName = session.SessionName }).ConfigureAwait(false); } catch (RpcException e) { _logger.Warn("Failed to delete session. Session will be abandoned to garbage collection.", e); } }
/// <summary> /// Release a session back to the pool (or refresh or evict it) and decrement the number of active sessions. /// </summary> public override void Release(PooledSession session, bool deleteSession) { Interlocked.Decrement(ref _activeSessionCount); if (deleteSession) { EvictSession(session); } else { ReleaseInactiveSession(session, maybeCreateReadWriteTransaction: true); } }
private async Task TryCreateReadWriteTransactionAndReturnToPool(PooledSession session) { try { session = await BeginTransactionAsync(session, s_readWriteOptions, CancellationToken.None).ConfigureAwait(false); } catch (RpcException e) { // Failed to create a read/write transaction; release this back to the pool, but making // sure we don't come back here. Parent._logger.Warn("Failed to create read/write transaction for pooled session", e); } ReleaseInactiveSession(session, maybeCreateReadWriteTransaction: false); }
private async Task <PooledSession> BeginTransactionAsync(PooledSession session, TransactionOptions options, CancellationToken cancellationToken) { // While we're creating a transaction, it's as if we're preparing a new session - it's a period of time // where there's already an RPC in flight, and when it completes a session will be available. Interlocked.Increment(ref _inFlightSessionCreationCount); var request = new BeginTransactionRequest { Options = options }; try { var transaction = await session.BeginTransactionAsync(request, Options.Timeout, cancellationToken).ConfigureAwait(false); return(session.WithTransaction(transaction.Id, options.ModeCase)); } finally { Interlocked.Decrement(ref _inFlightSessionCreationCount); } }
/// <summary> /// Refreshes a session by setting executing a trivial SELECT SQL statement. /// This is performed via the client session itself so it can update its next refresh time. /// </summary> private async Task RefreshAsync(PooledSession session) { // While we're refreshing a session, it's as if we're creating a new one - it's a period of time // where there's already an RPC in flight, and when it completes a session will be available. Interlocked.Increment(ref _inFlightSessionCreationCount); try { await session.ExecuteSqlAsync(new ExecuteSqlRequest { Sql = "SELECT 1" }, Options.Timeout, CancellationToken.None).ConfigureAwait(false); } catch (RpcException e) { Parent._logger.Warn("Failed to refresh session. Session will be deleted.", e); EvictSession(session); return; } finally { Interlocked.Decrement(ref _inFlightSessionCreationCount); } // We now definitely don't have a transaction. ReleaseInactiveSession(session.WithTransaction(null, ModeOneofCase.None), maybeCreateReadWriteTransaction: true); }
/// <summary> /// Release a session back to the pool (or refresh) but don't change the number of active sessions. /// </summary> /// <param name="session">The session to queue. Should be "active" (i.e. not disposed)</param> /// <param name="maybeCreateReadWriteTransaction">Whether to allow the session to go through a cycle of acquiring a read/write transaction. /// This is true unless we've just come from attempting to create a read/write transaction, in which case either we succeeded (no need /// to create a new one) or failed (in which case we should just keep it read-only). /// </param> private void ReleaseInactiveSession(PooledSession session, bool maybeCreateReadWriteTransaction) { if (Shutdown) { EvictSession(session); return; } if (session.RequiresRefresh) { // RefreshAsync will then release the refreshed session itself - which // may trigger a transaction request as well. But eventually, it'll get // back to the pool (or a waiting consumer). Parent.ConsumeBackgroundTask(RefreshAsync(session), "session refresh"); return; } // There are a couple of cases where we need to take an action outside the lock after breaking // out of the loop. It's simplest to remember that in a delegate. Action outsideLockAction = null; // We need to atomically (within the lock) decide between: // - Adding the session to a pool queue (adding performed within the lock) // - Providing the session to a waiting caller (setting the result peformed outside the lock) // - If it's currently not got a transaction but we need more read/write transactions, starting a transaction // In the last case, we will come back to this code to make another decision later. while (true) { TaskCompletionSource <PooledSession> pendingAquisition; lock (_lock) { // Only add a session to a queue if there are no pending acquisitions. if (!_pendingAcquisitions.TryDequeue(out pendingAquisition)) { // Options: // - Decide to create a new read/write transaction (will get back here later) // - Enqueue the current session as read-only or read/write depending on its mode ConcurrentQueue <PooledSession> queue; // If the session already has a read/write transaction, add it to the read/write pool immediately. // Otherwise, work out whether we *want* it to be read/write. if (session.TransactionMode == ModeOneofCase.ReadWrite) { queue = _readWriteSessions; } else { var readCount = _readOnlySessions.Count; var writeCount = _readWriteSessions.Count; // Avoid division by zero by including the new session in the denominator. var writeProportion = writeCount / (writeCount + readCount + 1.0); bool createReadWriteTransaction = maybeCreateReadWriteTransaction && writeProportion < Options.WriteSessionsFraction; if (createReadWriteTransaction) { // Exit the loop, and acquire a read/write transaction outsideLockAction = () => Parent.ConsumeBackgroundTask(TryCreateReadWriteTransactionAndReturnToPool(session), "transaction creation"); break; } else { // At this point we didn't already have a r/w transaction, and we don't want to // create one, so add it to the pool of read-only sessions. queue = _readOnlySessions; } } // We definitely have a queue now, so add the session to it, and // potentially release tasks waiting for the pool to reach minimum size. queue.Enqueue(session); int poolSize = _readOnlySessions.Count + _readWriteSessions.Count; if (poolSize >= Options.MinimumPooledSessions && _minimumSizeWaiters.Count > 0) { var minimumSizeWaiters = _minimumSizeWaiters.ToList(); outsideLockAction = () => minimumSizeWaiters.ForEach(tcs => tcs.TrySetResult(0)); } break; } } // We perform TrySetResult outside the lock, to avoid executing any synchronous continuations inside the lock. if (pendingAquisition.TrySetResult(session)) { return; } // The task had been cancelled by the caller. Go round the loop again. (There may or may not be more pending acquisitions.) } // If we've got anything to execute outside the lock, do so now. outsideLockAction?.Invoke(); }
private void DeleteSessionFireAndForget(PooledSession session) { ConsumeBackgroundTask(DeleteSessionAsync(session), "session deletion"); }
/// <summary> /// Creates a <see cref="PooledSession"/> with a known name and transaction ID/mode, with the client associated /// with this pool, but is otherwise not part of this pool. This method does not query the server for the session state. /// When the returned <see cref="PooledSession"/> is released, it will not become part of this pool, and the transaction /// will not be rolled back. /// </summary> /// <remarks> /// This is typically used for partitioned queries, where the same session is used across multiple machines, so should /// not be reused by the pool. /// </remarks> /// <param name="sessionName">The name of the transaction. Must not be null.</param> /// <param name="transactionId">The ID of the transaction. Must not be null.</param> /// <param name="transactionMode">The mode of the transaction.</param> /// <returns>A <see cref="PooledSession"/> for the given session and transaction.</returns> public PooledSession CreateDetachedSession(SessionName sessionName, ByteString transactionId, ModeOneofCase transactionMode) { GaxPreconditions.CheckNotNull(sessionName, nameof(sessionName)); GaxPreconditions.CheckNotNull(transactionId, nameof(transactionId)); return(PooledSession.FromSessionName(_detachedSessionPool, sessionName).WithTransaction(transactionId, transactionMode)); }
public override Task <PooledSession> WithFreshTransactionOrNewAsync(PooledSession session, TransactionOptions transactionOptions, CancellationToken cancellationToken) => throw new InvalidOperationException( $"{nameof(session)} is a detached session. Its transaction can't be refreshed and it can't be substituted by a new session.");
public abstract void Release(PooledSession session, ByteString transactionToRollback, bool deleteSession);
public abstract void Release(PooledSession session, bool deleteSession);
public abstract Task <PooledSession> WithFreshTransactionOrNewAsync(PooledSession session, TransactionOptions transactionOptions, CancellationToken cancellationToken);
private void EvictSession(PooledSession session) { Interlocked.Decrement(ref _liveOrRequestedSessionCount); Parent.DeleteSessionFireAndForget(session); }