Example #1
0
        public async Task TestExecuteNonQueryCanCancel([Values] bool isAsync, [Values] bool isSystemDataSqlClient)
        {
            using var cancellationTokenSource = new CancellationTokenSource();

            await using var connection = CreateConnection(isSystemDataSqlClient);
            await connection.OpenAsync(CancellationToken.None);

            using var command = connection.CreateCommand();
            command.SetCommandText(@"
                WHILE 1 = 1
                BEGIN
                    DECLARE @x INT = 1
                END"
                                   );

            var task = Task.Run(async() =>
            {
                if (isAsync)
                {
                    await command.ExecuteNonQueryAsync(cancellationTokenSource.Token, disallowAsyncCancellation: true);
                }
                else
                {
                    SyncViaAsync.Run(_ => command.ExecuteNonQueryAsync(cancellationTokenSource.Token), 0);
                }
            });

            Assert.IsFalse(task.Wait(TimeSpan.FromSeconds(.1)));

            cancellationTokenSource.Cancel();
            Assert.IsTrue(task.ContinueWith(_ => { }).Wait(TimeSpan.FromSeconds(5)));
            task.Status.ShouldEqual(TaskStatus.Canceled);
        }
Example #2
0
        public async Task TestExecuteNonQueryAlreadyCanceled(
            [Values] bool isAsync,
            [Values] bool isSystemDataSqlClient,
            [Values] bool isFastQuery)
        {
            using var cancellationTokenSource = new CancellationTokenSource();
            cancellationTokenSource.Cancel();

            await using var connection = CreateConnection(isSystemDataSqlClient);
            await connection.OpenAsync(CancellationToken.None);

            using var command = connection.CreateCommand();
            command.SetCommandText(
                isFastQuery
                    ? "SELECT 1"
                    : @"WHILE 1 = 1
                        BEGIN
                            DECLARE @x INT = 1
                        END"
                );

            if (isAsync)
            {
                Assert.CatchAsync <OperationCanceledException>(() => command.ExecuteNonQueryAsync(cancellationTokenSource.Token).AsTask());
            }
            else
            {
                Assert.Catch <OperationCanceledException>(() => SyncViaAsync.Run(_ => command.ExecuteNonQueryAsync(cancellationTokenSource.Token), 0));
            }
        }
        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);
            }
        }
Example #4
0
        public async Task TestLockOnDifferentBlobClientTypes(
            [Values] BlobClientType type,
            [Values] bool isAsync)
        {
            if (isAsync)
            {
                await TestAsync();
            }
            else
            {
                SyncViaAsync.Run(_ => TestAsync(), default(object));
            }

            async ValueTask TestAsync()
            {
                using var provider = new TestingAzureBlobLeaseDistributedLockProvider();
                var name   = provider.GetUniqueSafeName();
                var client = CreateClient(type, name);

                if (client is AppendBlobClient appendClient)
                {
                    Assert.That(
                        Assert.Throws <RequestFailedException>(() => appendClient.CreateIfNotExists()).ToString(),
                        Does.Contain("This feature is not currently supported by the Storage Emulator")
                        );
                    return;
                }
                if (client.GetType() == typeof(BlobBaseClient))
                {
                    // work around inability to do CreateIfNotExists for the base client
                    new BlobClient(AzureCredentials.ConnectionString, AzureCredentials.DefaultBlobContainerName, name).Upload(Stream.Null);
                }

                var @lock = new AzureBlobLeaseDistributedLock(client);

                await using var handle = await @lock.TryAcquireAsync();

                Assert.IsNotNull(handle);
                await using var nestedHandle = await @lock.TryAcquireAsync();

                Assert.IsNull(nestedHandle);
            }
        }
        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);
                }
            }
        }
Example #6
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}");
                }
            }
        }