public static async ValueTask <bool> ParseExitCodeAsync(int exitCode, TimeoutValue timeout, CancellationToken cancellationToken)
        {
            // sp_getapplock exit codes documented at
            // https://msdn.microsoft.com/en-us/library/ms189823.aspx

            switch (exitCode)
            {
            case 0:
            case 1:
                return(true);

            case TimeoutExitCode:
                return(false);

            case -2:     // canceled
                throw new OperationCanceledException(GetErrorMessage(exitCode, "canceled"));

            case -3:     // deadlock
                throw new DeadlockException(GetErrorMessage(exitCode, "deadlock"));

            case -999:     // parameter / unknown
                throw new ArgumentException(GetErrorMessage(exitCode, "parameter validation or other error"));

            case InvalidUpgradeExitCode:
                // should never happen unless something goes wrong (e. g. user manually releases the lock on an externally-owned connection)
                throw new InvalidOperationException("Cannot upgrade to an exclusive lock because the update lock is not held");

            case AlreadyHeldExitCode:
                return(timeout.IsZero ? false
                        : timeout.IsInfinite ? throw new DeadlockException("Attempted to acquire a lock that is already held on the same connection")
                        : await WaitThenReturnFalseAsync().ConfigureAwait(false));

            default:
                if (exitCode <= 0)
                {
                    throw new InvalidOperationException(GetErrorMessage(exitCode, "unknown"));
                }
                return(true);    // unknown success code
            }

            async ValueTask <bool> WaitThenReturnFalseAsync()
            {
                await SyncViaAsync.Delay(timeout, cancellationToken).ConfigureAwait(false);

                return(false);
            }
        }
        public async ValueTask <object?> TryAcquireAsync(DatabaseConnection connection, string resourceName, TimeoutValue timeout, CancellationToken cancellationToken)
        {
            const string SavePointName = "medallion_threading_postgres_advisory_lock_acquire";

            var key = new PostgresAdvisoryLockKey(resourceName);

            var hasTransaction = await HasTransactionAsync(connection).ConfigureAwait(false);

            if (hasTransaction)
            {
                // Our acquire command will use SET LOCAL to set up statement timeouts. This lasts until the end
                // of the current transaction instead of just the current batch if we're in a transaction. To make sure
                // we don't leak those settings, in the case of a transaction we first set up a save point which we can
                // later roll back (taking the settings changes with it but NOT the lock). Because we can't confidently
                // roll back a save point without knowing that it has been set up, we start the save point in its own
                // query before we try-catch
                using var setSavePointCommand = connection.CreateCommand();
                setSavePointCommand.SetCommandText("SAVEPOINT " + SavePointName);
                await setSavePointCommand.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
            }

            using var acquireCommand = this.CreateAcquireCommand(connection, key, timeout);

            int acquireCommandResult;

            try
            {
                acquireCommandResult = (int)await acquireCommand.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                await RollBackTransactionTimeoutVariablesIfNeededAsync().ConfigureAwait(false);

                if (ex is PostgresException postgresException)
                {
                    switch (postgresException.SqlState)
                    {
                    // lock_timeout error code from https://www.postgresql.org/docs/10/errcodes-appendix.html
                    case "55P03":
                        return(null);

                    // deadlock_detected error code from https://www.postgresql.org/docs/10/errcodes-appendix.html
                    case "40P01":
                        throw new DeadlockException($"The request for the distributed lock failed with exit code '{postgresException.SqlState}' (deadlock_detected)", ex);
                    }
                }

                if (ex is OperationCanceledException && cancellationToken.IsCancellationRequested)
                {
                    // if we bailed in the middle of an acquire, make sure we didn't leave a lock behind
                    await this.ReleaseAsync(connection, key, isTry : true).ConfigureAwait(false);
                }

                throw;
            }

            await RollBackTransactionTimeoutVariablesIfNeededAsync().ConfigureAwait(false);

            switch (acquireCommandResult)
            {
            case 0: return(null);

            case 1: return(Cookie);

            case AlreadyHeldReturnCode:
                if (timeout.IsZero)
                {
                    return(null);
                }
                if (timeout.IsInfinite)
                {
                    throw new DeadlockException("Attempted to acquire a lock that is already held on the same connection");
                }
                await SyncViaAsync.Delay(timeout, cancellationToken).ConfigureAwait(false);

                return(null);

            default:
                throw new InvalidOperationException($"Unexpected return code {acquireCommandResult}");
            }

            async ValueTask RollBackTransactionTimeoutVariablesIfNeededAsync()
            {
                if (hasTransaction)
                {
                    // attempt to clear the timeout variables we set
                    using var rollBackSavePointCommand = connection.CreateCommand();
                    rollBackSavePointCommand.SetCommandText("ROLLBACK TO SAVEPOINT " + SavePointName);
                    await rollBackSavePointCommand.ExecuteNonQueryAsync(CancellationToken.None).ConfigureAwait(false);
                }
            }
        }
Esempio n. 3
0
        private static ValueTask <bool> ProcessAcquireResultAsync(
            IDataParameterCollection parameters,
            TimeoutValue timeout,
            CancellationToken cancellationToken,
            out string?markerTableName,
            out string?ticketLockName)
        {
            var resultCode = (int)((IDbDataParameter)parameters[ResultCodeParameter]).Value;

            switch (resultCode)
            {
            case SuccessCode:
                ticketLockName  = (string)((IDbDataParameter)parameters[TicketLockNameParameter]).Value;
                markerTableName = (string)((IDbDataParameter)parameters[MarkerTableNameParameter]).Value;
                return(true.AsValueTask());

            case FinishedPreambleWithoutAcquiringCode:
                ticketLockName  = null;
                markerTableName = (string)((IDbDataParameter)parameters[MarkerTableNameParameter]).Value;
                return(false.AsValueTask());

            case FailedToAcquireWithSpaceRemainingCode:
                throw new InvalidOperationException($"An internal semaphore algorithm error ({resultCode}) occurred: failed to acquire a ticket despite indication that tickets are available");

            case BusyWaitTimeoutCode:
                ticketLockName = markerTableName = null;
                return(false.AsValueTask());

            case AllTicketsHeldByCurrentSessionCode:
                // whenever we hit this case, it's a deadlock. If the user asked us to wait forever, we just throw. However,
                // if the user asked us to wait a specified amount of time we will wait in C#. There are other justifiable policies
                // but this one seems relatively safe and likely to do what you want. It seems reasonable that no one intends to hang
                // forever but also reasonable that someone should be able to test for lock acquisition without getting a throw
                if (timeout.IsInfinite)
                {
                    throw new DeadlockException("Deadlock detected: attempt to acquire the semaphore cannot succeed because all tickets are held by the current connection");
                }

                ticketLockName = markerTableName = null;

                async ValueTask <bool> DelayFalseAsync()
                {
                    await SyncViaAsync.Delay(timeout, cancellationToken).ConfigureAwait(false);

                    return(false);
                }

                return(DelayFalseAsync());

            case SqlApplicationLock.TimeoutExitCode:
                ticketLockName = markerTableName = null;
                return(false.AsValueTask());

            default:
                ticketLockName = markerTableName = null;
                return(FailAsync());

                async ValueTask <bool> FailAsync()
                {
                    if (resultCode < 0)
                    {
                        await SqlApplicationLock.ParseExitCodeAsync(resultCode, timeout, cancellationToken).ConfigureAwait(false);
                    }
                    throw new InvalidOperationException($"Unexpected semaphore algorithm result code {resultCode}");
                }
            }
        }