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); }