private static async Task ExecuteNonQueryAsync(IDbCommand command, CancellationToken cancellationToken, bool isSyncOverAsync)
        {
            if (isSyncOverAsync)
            {
                command.ExecuteNonQuery();
                return;
            }

            // note: we can't call ExecuteNonQueryAsync(cancellationToken) here because of
            // what appears to be a .NET bug (see https://github.com/dotnet/corefx/issues/26623,
            // https://stackoverflow.com/questions/48461567/canceling-query-with-while-loop-hangs-forever)
            // The workaround is to fall back to multi-threaded async querying in the case where we
            // have a live cancellation token (less efficient but at least it works)

            if (!cancellationToken.CanBeCanceled)
            {
                await command.ExecuteNonQueryAsync(CancellationToken.None).ConfigureAwait(false);

                return;
            }

            var commandTask = Task.Run(() => command.ExecuteNonQuery());

            using (cancellationToken.Register(() =>
            {
                // we call cancel in a loop here until the command task completes. This is because
                // when cancellation triggers it's possible the task hasn't even run yet. Therefore
                // we want to keep trying until we know cancellation has worked
                var spinWait = new SpinWait();
                while (true)
                {
                    try { command.Cancel(); }
                    catch { /* just ignore errors here */ }

                    if (commandTask.IsCompleted)
                    {
                        break;
                    }
                    spinWait.SpinOnce();
                }
            }))
            {
                try { await commandTask.ConfigureAwait(false); }
                catch (DbException ex)
                    // MA: canceled SQL operations throw SqlException instead of OCE.
                    // That means that downstream operations end up faulted instead of canceled. We
                    // wrap with OCE here to correctly propagate cancellation
                    when(cancellationToken.IsCancellationRequested && SqlHelpers.IsCancellationException(ex))
                    {
                        throw new OperationCanceledException("Command was canceled", ex, cancellationToken);
                    }
            }
        }
        public async Task <IDisposable?> TryAcquireAsync <TLockCookie>(int timeoutMillis, ISqlSynchronizationStrategy <TLockCookie> strategy, CancellationToken cancellationToken, IDisposable?contextHandle)
            where TLockCookie : class
        {
            if (contextHandle != null)
            {
                cancellationToken.ThrowIfCancellationRequested(); // if already canceled, exit immediately

                // if we are taking a nested lock, we don't want to start another keepalive on the same connection.
                // However, we do need to stop our current keepalive while we take the nested lock to avoid threading issues
                var lockScope = (LockScope)contextHandle;
                await lockScope.Keepalive !.StopAsync().ConfigureAwait(false);
                try
                {
                    var internalHandle = await lockScope.InternalLock !.TryAcquireAsync(timeoutMillis, strategy, cancellationToken, contextHandle: lockScope.InternalHandle).ConfigureAwait(false);
                    return(internalHandle != null
                        ? new LockScope(internalHandle, lockScope.InternalLock, lockScope.Keepalive, connection: null)
                        : null);
                }
                finally
                {
                    // always restart, even if the acquisition fails
                    lockScope.Keepalive.Start();
                }
            }

            var       connection = SqlHelpers.CreateConnection(this.connectionString);
            LockScope?result     = null;

            try
            {
                await connection.OpenAsync(cancellationToken).ConfigureAwait(false);

                var internalLock   = new ExternalConnectionOrTransactionSqlDistributedLock(this.lockName, connection);
                var internalHandle = await internalLock.TryAcquireAsync(timeoutMillis, strategy, cancellationToken, contextHandle : null).ConfigureAwait(false);

                if (internalHandle != null)
                {
                    var keepalive = new KeepaliveHelper(connection);
                    keepalive.Start();
                    result = new LockScope(internalHandle, internalLock, keepalive, connection);
                }
            }
            finally
            {
                if (result == null)
                {
                    connection.Dispose();
                }
            }

            return(result);
        }
        public IDisposable?TryAcquire <TLockCookie>(int timeoutMillis, ISqlSynchronizationStrategy <TLockCookie> strategy, IDisposable?contextHandle)
            where TLockCookie : class
        {
            if (contextHandle != null)
            {
                // if we are taking a nested lock, we don't want to start another keepalive on the same connection.
                // However, we do need to stop our current keepalive while we take the nested lock to avoid threading issues
                var lockScope = (LockScope)contextHandle;
                lockScope.Keepalive !.Stop();
                try
                {
                    var internalHandle = lockScope.InternalLock !.TryAcquire(timeoutMillis, strategy, contextHandle: lockScope.InternalHandle);
                    return(internalHandle != null
                        ? new LockScope(internalHandle, lockScope.InternalLock, lockScope.Keepalive, connection: null)
                        : null);
                }
                finally
                {
                    // always restart, even if the acquisition fails
                    lockScope.Keepalive.Start();
                }
            }

            var       connection = SqlHelpers.CreateConnection(this.connectionString);
            LockScope?result     = null;

            try
            {
                connection.Open();
                var internalLock   = new ExternalConnectionOrTransactionSqlDistributedLock(this.lockName, connection);
                var internalHandle = internalLock.TryAcquire(timeoutMillis, strategy, contextHandle: null);
                if (internalHandle != null)
                {
                    var keepalive = new KeepaliveHelper(connection);
                    keepalive.Start();
                    result = new LockScope(internalHandle, internalLock, keepalive, connection);
                }
            }
            finally
            {
                if (result == null)
                {
                    connection.Dispose();
                }
            }

            return(result);
        }
        public async Task <IDisposable?> TryAcquireAsync <TLockCookie>(int timeoutMillis, ISqlSynchronizationStrategy <TLockCookie> strategy, CancellationToken cancellationToken, IDisposable?contextHandle = null)
            where TLockCookie : class
        {
            if (contextHandle != null)
            {
                return(await this.CreateContextLock(contextHandle).TryAcquireAsync(timeoutMillis, strategy, cancellationToken, contextHandle: null).ConfigureAwait(false));
            }

            IDisposable?  result      = null;
            var           connection  = SqlHelpers.CreateConnection(this.connectionString);
            DbTransaction?transaction = null;

            try
            {
                await connection.OpenAsync(cancellationToken).ConfigureAwait(false);

                // when creating a transaction, the isolation level doesn't matter, since we're using sp_getapplock
                transaction = connection.BeginTransaction();
                var lockCookie = await strategy.TryAcquireAsync(transaction, this.lockName, timeoutMillis, cancellationToken).ConfigureAwait(false);

                if (lockCookie != null)
                {
                    result = new LockScope(transaction);
                }
            }
            finally
            {
                // if we fail to acquire or throw, make sure to clean up
                if (result == null)
                {
                    transaction?.Dispose();
                    connection.Dispose();
                }
            }

            return(result);
        }