#213 Postgres: Add support for transaction-scoped advisory locks with external transactions by Tzachi009 · Pull Request #222 · madelson/DistributedLock · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 18 additions & 15 deletions src/DistributedLock.Postgres/PostgresAdvisoryLock.cs
137 changes: 137 additions & 0 deletions src/DistributedLock.Postgres/PostgresDistributedLock.Extensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
using Medallion.Threading.Internal;
using System.Data;

namespace Medallion.Threading.Postgres;

public partial class PostgresDistributedLock
{
/// <summary>
/// Attempts to acquire a transaction-scoped advisory lock synchronously with an externally owned transaction. Usage:
/// <code>
/// var transaction = /* create a DB transaction */
///
/// var isLockAcquired = myLock.TryAcquireWithTransaction(..., transaction, ...)
///
/// if (isLockAcquired != null)
/// {
/// /* we have the lock! */
///
/// // Commit or Rollback the transaction, which in turn will release the lock
/// }
/// </code>
///
/// NOTE: The owner of the transaction is the responsible party for it - the owner must commit or rollback the transaction in order to release the acquired lock.
/// </summary>
/// <param name="key">The postgres advisory lock key which will be used to acquire the lock.</param>
/// <param name="transaction">The externally owned transaction which will be used to acquire the lock. The owner of the transaction must commit or rollback it for the lock to be released.</param>
/// <param name="timeout">How long to wait before giving up on the acquisition attempt. Defaults to 0.</param>
/// <param name="cancellationToken">Specifies a token by which the wait can be canceled</param>
/// <returns>Whether the lock has been acquired</returns>
public static bool TryAcquireWithTransaction(PostgresAdvisoryLockKey key, IDbTransaction transaction, TimeSpan timeout = default, CancellationToken cancellationToken = default) =>
SyncViaAsync.Run(state => TryAcquireWithTransactionAsyncInternal(state.key, state.transaction, state.timeout, state.cancellationToken), (key, transaction, timeout, cancellationToken));

/// <summary>
/// Acquires a transaction-scoped advisory lock synchronously, failing with <see cref="TimeoutException"/> if the attempt times out. Usage:
/// <code>
/// var transaction = /* create a DB transaction */
///
/// myLock.AcquireWithTransaction(..., transaction, ...)
///
/// /* we have the lock! */
///
/// // Commit or Rollback the transaction, which in turn will release the lock
/// </code>
///
/// NOTE: The owner of the transaction is the responsible party for it - the owner must commit or rollback the transaction in order to release the acquired lock.
/// </summary>
/// <param name="key">The postgres advisory lock key which will be used to acquire the lock.</param>
/// <param name="transaction">The externally owned transaction which will be used to acquire the lock. The owner of the transaction must commit or rollback it for the lock to be released.</param>
/// <param name="timeout">How long to wait before giving up on the acquisition attempt. Defaults to <see cref="Timeout.InfiniteTimeSpan"/></param>
/// <param name="cancellationToken">Specifies a token by which the wait can be canceled</param>
public static void AcquireWithTransaction(PostgresAdvisoryLockKey key, IDbTransaction transaction, TimeSpan? timeout = null, CancellationToken cancellationToken = default) =>
SyncViaAsync.Run(state => AcquireWithTransactionAsyncInternal(state.key, state.transaction, state.timeout, state.cancellationToken), (key, transaction, timeout, cancellationToken));

/// <summary>
/// Attempts to acquire a transaction-scoped advisory lock asynchronously with an externally owned transaction. Usage:
/// <code>
/// var transaction = /* create a DB transaction */
///
/// var isLockAcquired = await myLock.TryAcquireWithTransactionAsync(..., transaction, ...)
///
/// if (isLockAcquired != null)
/// {
/// /* we have the lock! */
///
/// // Commit or Rollback the transaction, which in turn will release the lock
/// }
/// </code>
///
/// NOTE: The owner of the transaction is the responsible party for it - the owner must commit or rollback the transaction in order to release the acquired lock.
/// </summary>
/// <param name="key">The postgres advisory lock key which will be used to acquire the lock.</param>
/// <param name="transaction">The externally owned transaction which will be used to acquire the lock. The owner of the transaction must commit or rollback it for the lock to be released.</param>
/// <param name="timeout">How long to wait before giving up on the acquisition attempt. Defaults to 0.</param>
/// <param name="cancellationToken">Specifies a token by which the wait can be canceled</param>
/// <returns>Whether the lock has been acquired</returns>
public static ValueTask<bool> TryAcquireWithTransactionAsync(PostgresAdvisoryLockKey key, IDbTransaction transaction, TimeSpan timeout = default, CancellationToken cancellationToken = default) =>
TryAcquireWithTransactionAsyncInternal(key, transaction, timeout, cancellationToken);

/// <summary>
/// Acquires a transaction-scoped advisory lock asynchronously, failing with <see cref="TimeoutException"/> if the attempt times out. Usage:
/// <code>
/// var transaction = /* create a DB transaction */
///
/// await myLock.AcquireWithTransaction(..., transaction, ...)
///
/// /* we have the lock! */
///
/// // Commit or Rollback the transaction, which in turn will release the lock
/// </code>
///
/// NOTE: The owner of the transaction is the responsible party for it - the owner must commit or rollback the transaction in order to release the acquired lock.
/// </summary>
/// <param name="key">The postgres advisory lock key which will be used to acquire the lock.</param>
/// <param name="transaction">The externally owned transaction which will be used to acquire the lock. The owner of the transaction must commit or rollback it for the lock to be released.</param>
/// <param name="timeout">How long to wait before giving up on the acquisition attempt. Defaults to <see cref="Timeout.InfiniteTimeSpan"/></param>
/// <param name="cancellationToken">Specifies a token by which the wait can be canceled</param>
public static ValueTask AcquireWithTransactionAsync(PostgresAdvisoryLockKey key, IDbTransaction transaction, TimeSpan? timeout = null, CancellationToken cancellationToken = default) =>
AcquireWithTransactionAsyncInternal(key, transaction, timeout, cancellationToken);

internal static ValueTask<bool> TryAcquireWithTransactionAsyncInternal(PostgresAdvisoryLockKey key, IDbTransaction transaction, TimeSpan timeout, CancellationToken cancellationToken)
{
if (key == null) { throw new ArgumentNullException(nameof(key)); }
if (transaction == null) { throw new ArgumentNullException(nameof(transaction)); }

return TryAcquireAsync();

async ValueTask<bool> TryAcquireAsync()
{
var connection = new PostgresDatabaseConnection(transaction);

await using (connection.ConfigureAwait(false))
{
var lockAcquiredCookie = await PostgresAdvisoryLock.ExclusiveLock.TryAcquireAsync(connection, key.ToString(), timeout, cancellationToken).ConfigureAwait(false);

return lockAcquiredCookie != null;
}
}
}

internal static ValueTask AcquireWithTransactionAsyncInternal(PostgresAdvisoryLockKey key, IDbTransaction transaction, TimeSpan? timeout, CancellationToken cancellationToken)
{
if (key == null) { throw new ArgumentNullException(nameof(key)); }
if (transaction == null) { throw new ArgumentNullException(nameof(transaction)); }

return AcquireAsync();

async ValueTask AcquireAsync()
{
var connection = new PostgresDatabaseConnection(transaction);

await using (connection.ConfigureAwait(false))
{
await PostgresAdvisoryLock.ExclusiveLock.TryAcquireAsync(connection, key.ToString(), timeout, cancellationToken).ThrowTimeoutIfNull().ConfigureAwait(false);
}
}
}
}
5 changes: 5 additions & 0 deletions src/DistributedLock.Postgres/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#nullable enable
static Medallion.Threading.Postgres.PostgresDistributedLock.AcquireWithTransaction(Medallion.Threading.Postgres.PostgresAdvisoryLockKey key, System.Data.IDbTransaction! transaction, System.TimeSpan? timeout = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> void
static Medallion.Threading.Postgres.PostgresDistributedLock.AcquireWithTransactionAsync(Medallion.Threading.Postgres.PostgresAdvisoryLockKey key, System.Data.IDbTransaction! transaction, System.TimeSpan? timeout = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask
static Medallion.Threading.Postgres.PostgresDistributedLock.TryAcquireWithTransaction(Medallion.Threading.Postgres.PostgresAdvisoryLockKey key, System.Data.IDbTransaction! transaction, System.TimeSpan timeout = default(System.TimeSpan), System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
static Medallion.Threading.Postgres.PostgresDistributedLock.TryAcquireWithTransactionAsync(Medallion.Threading.Postgres.PostgresAdvisoryLockKey key, System.Data.IDbTransaction! transaction, System.TimeSpan timeout = default(System.TimeSpan), System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask<bool>