internal static IDbDistributedLock CreateInternalLock(PostgresAdvisoryLockKey key, IDbConnection connection) { if (connection == null) { throw new ArgumentNullException(nameof(connection)); } return(new DedicatedConnectionOrTransactionDbDistributedLock(key.ToString(), () => new PostgresDatabaseConnection(connection))); }
private async ValueTask ReleaseAsync(DatabaseConnection connection, PostgresAdvisoryLockKey key, bool isTry) { using var command = connection.CreateCommand(); command.SetCommandText($"SELECT pg_catalog.pg_advisory_unlock{(this._isShared ? "_shared" : string.Empty)}({AddKeyParametersAndGetKeyArguments(command, key)})"); var result = (bool)await command.ExecuteScalarAsync(CancellationToken.None).ConfigureAwait(false); if (!isTry && !result) { throw new InvalidOperationException("Attempted to release a lock that was not held"); } }
internal static IDbDistributedLock CreateInternalLock(PostgresAdvisoryLockKey key, string connectionString, Action <PostgresConnectionOptionsBuilder>?options) { if (connectionString == null) { throw new ArgumentNullException(nameof(connectionString)); } var(keepaliveCadence, useMultiplexing) = PostgresConnectionOptionsBuilder.GetOptions(options); if (useMultiplexing) { return(new OptimisticConnectionMultiplexingDbDistributedLock(key.ToString(), connectionString, PostgresMultiplexedConnectionLockPool.Instance, keepaliveCadence)); } return(new DedicatedConnectionOrTransactionDbDistributedLock(key.ToString(), () => new PostgresDatabaseConnection(connectionString), useTransaction: false, keepaliveCadence)); }
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 string AddPGLocksFilterParametersAndGetFilterExpression(DatabaseCommand command, PostgresAdvisoryLockKey key) { // From https://www.postgresql.org/docs/12/view-pg-locks.html // Advisory locks can be acquired on keys consisting of either a single bigint value or two integer values. // A bigint key is displayed with its high-order half in the classid column, its low-order half in the objid column, // and objsubid equal to 1. The original bigint value can be reassembled with the expression (classid::bigint << 32) | objid::bigint. // Integer keys are displayed with the first key in the classid column, the second key in the objid column, and objsubid equal to 2. string classIdParameter, objIdParameter, objSubId; if (key.HasSingleKey) { // since Postgres seems to lack unchecked int conversions, it is simpler to just generate extra // parameters to carry the split key info in this case var(keyUpper32, keyLower32) = key.Keys; command.AddParameter(classIdParameter = "keyUpper32", keyUpper32, DbType.Int32); command.AddParameter(objIdParameter = "keyLower32", keyLower32, DbType.Int32); objSubId = "1"; } else { classIdParameter = "key1"; objIdParameter = "key2"; objSubId = "2"; } return($"(l.classid = @{classIdParameter} AND l.objid = @{objIdParameter} AND l.objsubid = {objSubId})"); }
private static string AddKeyParametersAndGetKeyArguments(DatabaseCommand command, PostgresAdvisoryLockKey key) { if (key.HasSingleKey) { command.AddParameter("key", key.Key, DbType.Int64); return("@key"); } else { var(key1, key2) = key.Keys; command.AddParameter("key1", key1, DbType.Int32); command.AddParameter("key2", key2, DbType.Int32); return("@key1, @key2"); } }
private DatabaseCommand CreateAcquireCommand(DatabaseConnection connection, PostgresAdvisoryLockKey key, TimeoutValue timeout) { var command = connection.CreateCommand(); var commandText = new StringBuilder(); commandText.AppendLine("SET LOCAL statement_timeout = 0;"); commandText.AppendLine($"SET LOCAL lock_timeout = {(timeout.IsZero || timeout.IsInfinite ? 0 : timeout.InMilliseconds)};"); if (connection.IsExernallyOwned) { commandText.Append($@" SELECT CASE WHEN EXISTS( SELECT * FROM pg_catalog.pg_locks l JOIN pg_catalog.pg_database d ON d.oid = l.database WHERE l.locktype = 'advisory' AND {AddPGLocksFilterParametersAndGetFilterExpression(command, key)} AND l.pid = pg_catalog.pg_backend_pid() AND d.datname = pg_catalog.current_database() ) THEN {AlreadyHeldReturnCode} ELSE " ); AppendAcquireFunctionCall(); commandText.AppendLine().Append("END"); } else { commandText.Append("SELECT "); AppendAcquireFunctionCall(); } commandText.Append(" AS result"); command.SetCommandText(commandText.ToString()); command.SetTimeout(timeout); return(command); void AppendAcquireFunctionCall() { // creates an expression like // pg_try_advisory_lock(@key1, @key2)::int // OR (SELECT 1 FROM (SELECT pg_advisory_lock(@key)) f) var isTry = timeout.IsZero; if (!isTry) { commandText.Append("(SELECT 1 FROM (SELECT "); } commandText.Append("pg_catalog.pg"); if (isTry) { commandText.Append("_try"); } commandText.Append("_advisory"); commandText.Append("_lock"); if (this._isShared) { commandText.Append("_shared"); } commandText.Append('(').Append(AddKeyParametersAndGetKeyArguments(command, key)).Append(')'); if (isTry) { commandText.Append("::int"); } else { commandText.Append(") f)"); } } }
private PostgresDistributedLock(PostgresAdvisoryLockKey key, IDbDistributedLock internalLock) { this.Key = key; this._internalLock = internalLock; }
/// <summary> /// Constructs a lock with the given <paramref name="key"/> (effectively the lock name) and <paramref name="connection"/>. /// </summary> public PostgresDistributedLock(PostgresAdvisoryLockKey key, IDbConnection connection) : this(key, CreateInternalLock(key, connection)) { }
/// <summary> /// Constructs a lock with the given <paramref name="key"/> (effectively the lock name), <paramref name="connectionString"/>, /// and <paramref name="options"/> /// </summary> public PostgresDistributedLock(PostgresAdvisoryLockKey key, string connectionString, Action <PostgresConnectionOptionsBuilder>?options = null) : this(key, CreateInternalLock(key, connectionString, options)) { }
/// <summary> /// Creates a <see cref="PostgresDistributedReaderWriterLock"/> with the provided <paramref name="key"/>. /// </summary> public PostgresDistributedReaderWriterLock CreateReaderWriterLock(PostgresAdvisoryLockKey key) => this._readerWriterLockFactory(key);
/// <summary> /// Creates a <see cref="PostgresDistributedLock"/> with the provided <paramref name="key"/>. /// </summary> public PostgresDistributedLock CreateLock(PostgresAdvisoryLockKey key) => this._lockFactory(key);