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);
                    }
                }
            }
Example #2
0
 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);
                }
            }
Example #4
0
 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));
 }
Example #13
0
 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.");
Example #14
0
 public abstract void Release(PooledSession session, ByteString transactionToRollback, bool deleteSession);
 public abstract void Release(PooledSession session, bool deleteSession);
Example #16
0
 public abstract Task <PooledSession> WithFreshTransactionOrNewAsync(PooledSession session, TransactionOptions transactionOptions, CancellationToken cancellationToken);
 private void EvictSession(PooledSession session)
 {
     Interlocked.Decrement(ref _liveOrRequestedSessionCount);
     Parent.DeleteSessionFireAndForget(session);
 }