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