diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index f0fe592..32535b9 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -4,6 +4,7 @@ true + diff --git a/src/DistributedLock.GBase/AssemblyAttributes.cs b/src/DistributedLock.GBase/AssemblyAttributes.cs new file mode 100644 index 0000000..a9e3a34 --- /dev/null +++ b/src/DistributedLock.GBase/AssemblyAttributes.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("DistributedLock.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100fd3af56ccc8ed94fffe25bfd651e6a5674f8f20a76d37de800dd0f7380e04f0fde2da6fa200380b14fe398605b6f470c87e5e0a0bf39ae871f07536a4994aa7a0057c4d3bcedc8fef3eecb0c88c2024a1b3289305c2393acd9fb9f9a42d0bd7826738ce864d507575ea3a1fe1746ab19823303269f79379d767949807f494be8")] diff --git a/src/DistributedLock.GBase/DistributedLock.GBase.csproj b/src/DistributedLock.GBase/DistributedLock.GBase.csproj new file mode 100644 index 0000000..56611ad --- /dev/null +++ b/src/DistributedLock.GBase/DistributedLock.GBase.csproj @@ -0,0 +1,67 @@ + + + + netstandard2.1;net472 + Medallion.Threading.GBase + True + 4 + Latest + enable + enable + + + + 2.7.0.2 + 1.0.0.0 + Michael Adelson + Provides a distributed lock implementation based on GBase Database + Copyright © 2021 Michael Adelson + MIT + distributed lock async mutex reader writer sql GBase + https://github.com/madelson/DistributedLock + https://github.com/madelson/DistributedLock + 1.0.0.0 + See https://github.com/madelson/DistributedLock#release-notes + true + ..\DistributedLock.snk + + + + True + True + True + + + embedded + + true + true + + + + False + 1591 + TRACE;DEBUG + + + + + + + + + + + + + + + + + + true + true + + + + \ No newline at end of file diff --git a/src/DistributedLock.GBase/GBaseConnectionOptionsBuilder.cs b/src/DistributedLock.GBase/GBaseConnectionOptionsBuilder.cs new file mode 100644 index 0000000..37fb287 --- /dev/null +++ b/src/DistributedLock.GBase/GBaseConnectionOptionsBuilder.cs @@ -0,0 +1,68 @@ +using Medallion.Threading.Internal; + +namespace Medallion.Threading.GBase; + +/// +/// Specifies options for connecting to and locking against an GBase database +/// +public sealed class GBaseConnectionOptionsBuilder +{ + private TimeoutValue? _keepaliveCadence; + private bool? _useMultiplexing; + + internal GBaseConnectionOptionsBuilder() { } + + /// + /// GBase does not kill idle connections by default, so by default keepalive is disabled (set to ). + /// + /// However, if you are using the IDLE_TIME setting in GBase or if your network is dropping connections that are idle holding locks for + /// a long time, you can set a value for keepalive to prevent this from happening. + /// + /// + public GBaseConnectionOptionsBuilder KeepaliveCadence(TimeSpan keepaliveCadence) + { + this._keepaliveCadence = new TimeoutValue(keepaliveCadence, nameof(keepaliveCadence)); + return this; + } + + /// + /// This mode takes advantage of the fact that while "holding" a lock (or other synchronization primitive) + /// a connection is essentially idle. Thus, rather than creating a new connection for each held lock it is + /// often possible to multiplex a shared connection so that that connection can hold multiple locks at the same time. + /// + /// Multiplexing is on by default. + /// + /// This is implemented in such a way that releasing a lock held on such a connection will never be blocked by an + /// Acquire() call that is waiting to acquire a lock on that same connection. For this reason, the multiplexing + /// strategy is "optimistic": if the lock can't be acquired instantaneously on the shared connection, a new (shareable) + /// connection will be allocated. + /// + /// This option can improve performance and avoid connection pool starvation in high-load scenarios. It is also + /// particularly applicable to cases where + /// semantics are used with a zero-length timeout. + /// + public GBaseConnectionOptionsBuilder UseMultiplexing(bool useMultiplexing = true) + { + this._useMultiplexing = useMultiplexing; + return this; + } + + internal static (TimeoutValue keepaliveCadence, bool useMultiplexing) GetOptions(Action? optionsBuilder) + { + GBaseConnectionOptionsBuilder? options; + if (optionsBuilder != null) + { + options = new GBaseConnectionOptionsBuilder(); + optionsBuilder(options); + } + else + { + options = null; + } + + var keepaliveCadence = options?._keepaliveCadence ?? Timeout.InfiniteTimeSpan; + var useMultiplexing = options?._useMultiplexing ?? true; + + return (keepaliveCadence, useMultiplexing); + } +} diff --git a/src/DistributedLock.GBase/GBaseDatabaseConnection.cs b/src/DistributedLock.GBase/GBaseDatabaseConnection.cs new file mode 100644 index 0000000..57f442d --- /dev/null +++ b/src/DistributedLock.GBase/GBaseDatabaseConnection.cs @@ -0,0 +1,80 @@ +using GBS.Data.GBasedbt; +using Medallion.Threading.Internal.Data; +using System.Data; + +namespace Medallion.Threading.GBase; + +internal class GBaseDatabaseConnection : DatabaseConnection +{ + public const string ApplicationNameIndicatorPrefix = "__DistributedLock.ApplicationName="; + + // see SleepAsync() for why we need this + private readonly IDbConnection _innerConnection; + + public GBaseDatabaseConnection(IDbConnection connection) + : this(connection, isExternallyOwned: true) + { + } + + public GBaseDatabaseConnection(IDbTransaction transaction) + : base(transaction, isExternallyOwned: true) + { + this._innerConnection = transaction.Connection; + } + + public GBaseDatabaseConnection(string connectionString) + : this(CreateConnection(connectionString), isExternallyOwned: false) + { + } + + private GBaseDatabaseConnection(IDbConnection connection, bool isExternallyOwned) + : base(connection, isExternallyOwned) + { + this._innerConnection = connection; + } + + public override bool ShouldPrepareCommands => false; + + public override bool IsCommandCancellationException(Exception exception) => + exception is GbsException gbsException + && (gbsException.ErrorCode == 01013 || gbsException.ErrorCode == 00936 || gbsException.ErrorCode == 00604); + + public override async Task SleepAsync(TimeSpan sleepTime, CancellationToken cancellationToken, Func> executor) + { + using var sleepCommand = this.CreateCommand(); + sleepCommand.SetCommandText("dbms_lock_sleep(?)"); + sleepCommand.AddParameter("seconds", sleepTime.TotalSeconds); + + try + { + await executor(sleepCommand, cancellationToken).ConfigureAwait(false); + } + catch when (!cancellationToken.IsCancellationRequested) + { + // GBase doesn't fire StateChange unless the State is observed or the connection is explicitly opened/closed. Therefore, we observe + // the state on seeing any exception in order to for the event to fire. + _ = this._innerConnection.State; + throw; + } + } + + public static GbsConnection CreateConnection(string connectionString) + { + if (connectionString == null) { throw new ArgumentNullException(connectionString, nameof(connectionString)); } + + // The .NET GBase provider does not currently support ApplicationName natively as a connection string property. + // However, that functionality is relied on by many of our tests. As a workaround, we permit the application name + // to be included in the connection string using a custom encoding scheme. This is only intended to work in tests! + if (connectionString.StartsWith(ApplicationNameIndicatorPrefix, StringComparison.Ordinal)) + { + var firstSeparatorIndex = connectionString.IndexOf(';'); + var applicationName = connectionString.Substring(startIndex: ApplicationNameIndicatorPrefix.Length, length: firstSeparatorIndex - ApplicationNameIndicatorPrefix.Length); + // After upgrading the GBase client to 23.6.1, the connection pool sometimes seems to grow beyond what is strictly required. + // This causes issues if we're tracking connections by name. Therefore, we disable pooling on named connections + var connection = new GbsConnection(connectionString.Substring(startIndex: firstSeparatorIndex + 1)); + return connection; + } + + return new GbsConnection(connectionString); + } +} diff --git a/src/DistributedLock.GBase/GBaseDbmsLock.cs b/src/DistributedLock.GBase/GBaseDbmsLock.cs new file mode 100644 index 0000000..ca95005 --- /dev/null +++ b/src/DistributedLock.GBase/GBaseDbmsLock.cs @@ -0,0 +1,239 @@ +using GBS.Data.GBasedbt; +using Medallion.Threading.Internal; +using Medallion.Threading.Internal.Data; +using System.Data; + +namespace Medallion.Threading.GBase; + +/// +/// Implements using GBase's DBMS_LOCK package +/// +internal class GBaseDbmsLock : IDbSynchronizationStrategy +{ + private const int MaxWaitSeconds = 32767; + private const int MaxTimeoutSeconds = MaxWaitSeconds - 1; + + public static readonly GBaseDbmsLock SharedLock = new(Mode.Shared), + UpdateLock = new(Mode.Update), + ExclusiveLock = new(Mode.Exclusive), + UpgradeLock = new(Mode.Exclusive, isUpgrade: true); + + private static readonly object Cookie = new(); + + private readonly Mode _mode; + private readonly bool _isUpgrade; + + private GBaseDbmsLock(Mode mode, bool isUpgrade = false) + { + Invariant.Require(!isUpgrade || mode == Mode.Exclusive); + + this._mode = mode; + this._isUpgrade = isUpgrade; + } + + public bool IsUpgradeable => this._mode == Mode.Update; + + private int ModeSqlConstant + { + get + { + var modeCode = this._mode switch + { + Mode.Shared => 2, //SS_MODE + Mode.Update => 5, //SSX_MODE + Mode.Exclusive => 6, //X_MODE + _ => throw new InvalidOperationException(), + }; + return modeCode; + } + } + + private async Task GetAllocateLockHandle(DatabaseCommand cmd, string resourceName, CancellationToken cancellationToken) + { + var lockAllockFunction = "dbms_lock_allocate_unique"; + cmd.SetCommandText($"{lockAllockFunction}"); + cmd.SetCommandType(CommandType.StoredProcedure); + + var pAllocReturn = new GbsParameter(); + pAllocReturn.GbsType = GbsType.Integer; + pAllocReturn.ParameterName = "returnValue"; + pAllocReturn.Direction = ParameterDirection.ReturnValue; + cmd.Parameters.Add(pAllocReturn); + + var pAllocLockName = new GbsParameter(); + pAllocLockName.GbsType = GbsType.LVarChar; + pAllocLockName.ParameterName = "LockName"; + pAllocLockName.Direction = ParameterDirection.Input; + pAllocLockName.Value = resourceName; + cmd.Parameters.Add(pAllocLockName); + + var pAllocLockHandle = new GbsParameter(); + pAllocLockHandle.GbsType = GbsType.LVarChar; + pAllocLockHandle.ParameterName = "LockHandle"; + pAllocLockHandle.Direction = ParameterDirection.Output; + pAllocLockHandle.Size = 128; + cmd.Parameters.Add(pAllocLockHandle); + + var pAllocExpir = new GbsParameter(); + pAllocExpir.GbsType = GbsType.Integer; + pAllocExpir.ParameterName = "expiration_secs"; + pAllocExpir.Direction = ParameterDirection.Input; + pAllocExpir.Value = 864000; + cmd.Parameters.Add(pAllocExpir); + try + { + await cmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + if (Convert.ToInt32(pAllocReturn.Value) != 0) + { + throw new InvalidOperationException($"allocate locked handle failed. rc = {pAllocReturn.Value}"); + } + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + Console.WriteLine(ex.StackTrace); + foreach (GbsError err in ((GbsException)ex).Errors) + { + Console.WriteLine("Native Error" + err.NativeError); + } + throw ex; + } + + return (string)pAllocLockHandle!.Value; + } + + public async ValueTask ReleaseAsync(DatabaseConnection connection, string resourceName, object lockCookie) + { + // Since we we don't allow downgrading and therefore "releasing" an upgrade only happens on disposal of the + // original handle, this can safely be a noop. + if (this._isUpgrade) { return; } + + using var cmd = connection.CreateCommand(); + + string lockHandle = await this.GetAllocateLockHandle(cmd, resourceName, CancellationToken.None).ConfigureAwait(false); + var releaseFunction = "dbms_lock_release"; + + cmd.SetCommandText(releaseFunction); + cmd.SetCommandType(CommandType.StoredProcedure); + cmd.Parameters.Clear(); + + var pRequestReturnValue = new GbsParameter(); + pRequestReturnValue.GbsType = GbsType.Integer; + pRequestReturnValue.ParameterName = "returnFake"; + pRequestReturnValue.Direction = ParameterDirection.ReturnValue; + cmd.Parameters.Add(pRequestReturnValue); + + var pRequestLockHandle = new GbsParameter(); + pRequestLockHandle.GbsType = GbsType.LVarChar; + pRequestLockHandle.ParameterName = "lockHandle"; + pRequestLockHandle.Direction = ParameterDirection.Input; + pRequestLockHandle.Value = lockHandle; + cmd.Parameters.Add(pRequestLockHandle); + + var pResponseReturnOutput = new GbsParameter(); + pResponseReturnOutput.GbsType = GbsType.Integer; + pResponseReturnOutput.ParameterName = "returnValue"; + pResponseReturnOutput.Direction = ParameterDirection.Output; + cmd.Parameters.Add(pResponseReturnOutput); + + await cmd.ExecuteNonQueryAsync(CancellationToken.None).ConfigureAwait(false); + + var returnValue = (int)pResponseReturnOutput.Value; + if (returnValue != 0) + { + throw new InvalidOperationException($"dbms_lock_release returned error code :{returnValue}"); + } + } + + public async ValueTask TryAcquireAsync(DatabaseConnection connection, string resourceName, TimeoutValue timeout, CancellationToken cancellationToken) + { + using var cmd = connection.CreateCommand(); + + string lockHandle = await this.GetAllocateLockHandle(cmd, resourceName, cancellationToken).ConfigureAwait(false); + + var acquireFunction = this._isUpgrade ? "dbms_lock_convert" : "dbms_lock_request"; + cmd.SetCommandText(acquireFunction); + cmd.SetCommandType(CommandType.StoredProcedure); + cmd.SetTimeout(timeout); + cmd.Parameters.Clear(); + + var pRequestReturnFake = new GbsParameter(); + pRequestReturnFake.GbsType = GbsType.Integer; + pRequestReturnFake.ParameterName = "returnFake"; + pRequestReturnFake.Direction = ParameterDirection.ReturnValue; + cmd.Parameters.Add(pRequestReturnFake); + + var pRequestLockHandle = new GbsParameter(); + pRequestLockHandle.GbsType = GbsType.LVarChar; + pRequestLockHandle.ParameterName = "lockHandle"; + pRequestLockHandle.Direction = ParameterDirection.Input; + pRequestLockHandle.Value = lockHandle; + cmd.Parameters.Add(pRequestLockHandle); + + var pRequestReturnOutput = new GbsParameter(); + pRequestReturnOutput.GbsType = GbsType.Integer; + pRequestReturnOutput.ParameterName = "returnValue"; + pRequestReturnOutput.Direction = ParameterDirection.Output; + cmd.Parameters.Add(pRequestReturnOutput); + + var pRequestLockMode = new GbsParameter(); + pRequestLockMode.GbsType = GbsType.Integer; + pRequestLockMode.ParameterName = "lockMode"; + pRequestLockMode.Direction = ParameterDirection.Input; + pRequestLockMode.Value = this.ModeSqlConstant; + cmd.Parameters.Add(pRequestLockMode); + + var pRequestTimeOut = new GbsParameter(); + pRequestTimeOut.GbsType = GbsType.Integer; + pRequestTimeOut.ParameterName = "timeout"; + pRequestTimeOut.Direction = ParameterDirection.Input; + pRequestTimeOut.Value = timeout.IsInfinite ? MaxWaitSeconds + // we could support longer timeouts via looping lock requests, but this doesn't feel particularly valuable and isn't a true longer wait + // since by looping you fall out of the wait queue + : timeout.TimeSpan.TotalSeconds > MaxTimeoutSeconds ? throw new ArgumentOutOfRangeException($"Requested non-infinite timeout value '{timeout}' is longer than GBase's allowed max of '{TimeSpan.FromSeconds(MaxTimeoutSeconds)}'") + : timeout.TimeSpan.TotalSeconds; + cmd.Parameters.Add(pRequestTimeOut); + + if (this._isUpgrade == false) + { + var pRequestCommit = new GbsParameter(); + pRequestCommit.GbsType = GbsType.Boolean; + pRequestCommit.ParameterName = "release_on_commit"; + pRequestCommit.Direction = ParameterDirection.Input; + pRequestCommit.Value = false; + cmd.Parameters.Add(pRequestCommit); + } + + await cmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + + var returnValue = (int)pRequestReturnOutput.Value; + return returnValue switch + { + 0 => Cookie, // success + 1 => null, // timeout + 2 => throw new DeadlockException(GetErrorMessage("deadlock")), + 3 => throw new InvalidOperationException(GetErrorMessage("parameter error")), + 4 => timeout.IsZero ? null + : timeout.IsInfinite ? throw new DeadlockException("Attempted to acquire a lock that is already held on the same connection") + : await WaitThenReturnNullAsync().ConfigureAwait(false), + 5 => throw new InvalidOperationException(GetErrorMessage("illegal lock handle")), + _ => throw new InvalidOperationException(GetErrorMessage("unknown error code")), + }; + + string GetErrorMessage(string description) => + $"{acquireFunction} returned error code {returnValue} ({description})"; + + async ValueTask WaitThenReturnNullAsync() + { + await SyncViaAsync.Delay(timeout, cancellationToken).ConfigureAwait(false); + return null; + } + } + + private enum Mode + { + Shared, + Update, + Exclusive, + } +} diff --git a/src/DistributedLock.GBase/GBaseDistributedLock.IDistributedLock.cs b/src/DistributedLock.GBase/GBaseDistributedLock.IDistributedLock.cs new file mode 100644 index 0000000..308c476 --- /dev/null +++ b/src/DistributedLock.GBase/GBaseDistributedLock.IDistributedLock.cs @@ -0,0 +1,81 @@ +using Medallion.Threading.Internal; + +namespace Medallion.Threading.GBase; + +public partial class GBaseDistributedLock +{ + // AUTO-GENERATED + + IDistributedSynchronizationHandle? IDistributedLock.TryAcquire(TimeSpan timeout, CancellationToken cancellationToken) => + this.TryAcquire(timeout, cancellationToken); + IDistributedSynchronizationHandle IDistributedLock.Acquire(TimeSpan? timeout, CancellationToken cancellationToken) => + this.Acquire(timeout, cancellationToken); + ValueTask IDistributedLock.TryAcquireAsync(TimeSpan timeout, CancellationToken cancellationToken) => + this.TryAcquireAsync(timeout, cancellationToken).Convert(To.ValueTask); + ValueTask IDistributedLock.AcquireAsync(TimeSpan? timeout, CancellationToken cancellationToken) => + this.AcquireAsync(timeout, cancellationToken).Convert(To.ValueTask); + + /// + /// Attempts to acquire the lock synchronously. Usage: + /// + /// using (var handle = myLock.TryAcquire(...)) + /// { + /// if (handle != null) { /* we have the lock! */ } + /// } + /// // dispose releases the lock if we took it + /// + /// + /// How long to wait before giving up on the acquisition attempt. Defaults to 0 + /// Specifies a token by which the wait can be canceled + /// An which can be used to release the lock or null on failure + public GBaseDistributedLockHandle? TryAcquire(TimeSpan timeout = default, CancellationToken cancellationToken = default) => + DistributedLockHelpers.TryAcquire(this, timeout, cancellationToken); + + /// + /// Acquires the lock synchronously, failing with if the attempt times out. Usage: + /// + /// using (myLock.Acquire(...)) + /// { + /// /* we have the lock! */ + /// } + /// // dispose releases the lock + /// + /// + /// How long to wait before giving up on the acquisition attempt. Defaults to + /// Specifies a token by which the wait can be canceled + /// An which can be used to release the lock + public GBaseDistributedLockHandle Acquire(TimeSpan? timeout = null, CancellationToken cancellationToken = default) => + DistributedLockHelpers.Acquire(this, timeout, cancellationToken); + + /// + /// Attempts to acquire the lock asynchronously. Usage: + /// + /// await using (var handle = await myLock.TryAcquireAsync(...)) + /// { + /// if (handle != null) { /* we have the lock! */ } + /// } + /// // dispose releases the lock if we took it + /// + /// + /// How long to wait before giving up on the acquisition attempt. Defaults to 0 + /// Specifies a token by which the wait can be canceled + /// An which can be used to release the lock or null on failure + public ValueTask TryAcquireAsync(TimeSpan timeout = default, CancellationToken cancellationToken = default) => + this.As>().InternalTryAcquireAsync(timeout, cancellationToken); + + /// + /// Acquires the lock asynchronously, failing with if the attempt times out. Usage: + /// + /// await using (await myLock.AcquireAsync(...)) + /// { + /// /* we have the lock! */ + /// } + /// // dispose releases the lock + /// + /// + /// How long to wait before giving up on the acquisition attempt. Defaults to + /// Specifies a token by which the wait can be canceled + /// An which can be used to release the lock + public ValueTask AcquireAsync(TimeSpan? timeout = null, CancellationToken cancellationToken = default) => + DistributedLockHelpers.AcquireAsync(this, timeout, cancellationToken); +} \ No newline at end of file diff --git a/src/DistributedLock.GBase/GBaseDistributedLock.cs b/src/DistributedLock.GBase/GBaseDistributedLock.cs new file mode 100644 index 0000000..523b2cb --- /dev/null +++ b/src/DistributedLock.GBase/GBaseDistributedLock.cs @@ -0,0 +1,85 @@ +using Medallion.Threading.Internal; +using Medallion.Threading.Internal.Data; +using System.Data; + +namespace Medallion.Threading.GBase; + +/// +/// Implements a distributed lock for GBase databse based on the DBMS_LOCK package +/// +public sealed partial class GBaseDistributedLock : IInternalDistributedLock +{ + internal const int MaxNameLength = 128; + + private readonly IDbDistributedLock _internalLock; + + /// + /// Constructs a lock with the given that connects using the provided and + /// . + /// + /// Unless is specified, will be escaped/hashed to ensure name validity. + /// + public GBaseDistributedLock(string name, string connectionString, Action? options = null, bool exactName = false) + : this(name, exactName, n => CreateInternalLock(n, connectionString, options)) + { + } + + /// + /// Constructs a lock with the given that connects using the provided . + /// + /// Unless is specified, will be escaped/hashed to ensure name validity. + /// + public GBaseDistributedLock(string name, IDbConnection connection, bool exactName = false) + : this(name, exactName, n => CreateInternalLock(n, connection)) + { + } + + private GBaseDistributedLock(string name, bool exactName, Func internalLockFactory) + { + this.Name = GetName(name, exactName); + this._internalLock = internalLockFactory(this.Name); + } + + internal static string GetName(string name, bool exactName) + { + if (name == null) { throw new ArgumentNullException(nameof(name)); } + + if (exactName) + { + if (name.Length > MaxNameLength) { throw new FormatException($"{nameof(name)}: must be at most {MaxNameLength} characters"); } + if (name.Length == 0) { throw new FormatException($"{nameof(name)} must not be empty"); } + return name; + } + + return DistributedLockHelpers.ToSafeName(name, MaxNameLength, s => s.Length == 0 ? "EMPTY" : s); + } + + /// + /// Implements + /// + public string Name { get; } + + ValueTask IInternalDistributedLock.InternalTryAcquireAsync(TimeoutValue timeout, CancellationToken cancellationToken) => + this._internalLock.TryAcquireAsync(timeout, GBaseDbmsLock.ExclusiveLock, cancellationToken, contextHandle: null).Wrap(h => new GBaseDistributedLockHandle(h)); + + internal static IDbDistributedLock CreateInternalLock(string name, string connectionString, Action? options) + { + if (connectionString == null) { throw new ArgumentNullException(nameof(connectionString)); } + + var (keepaliveCadence, useMultiplexing) = GBaseConnectionOptionsBuilder.GetOptions(options); + + if (useMultiplexing) + { + return new OptimisticConnectionMultiplexingDbDistributedLock(name, connectionString, GBaseMultiplexedConnectionLockPool.Instance, keepaliveCadence); + } + + return new DedicatedConnectionOrTransactionDbDistributedLock(name, () => new GBaseDatabaseConnection(connectionString), useTransaction: false, keepaliveCadence); + } + + internal static IDbDistributedLock CreateInternalLock(string name, IDbConnection connection) + { + if (connection == null) { throw new ArgumentNullException(nameof(connection)); } + + return new DedicatedConnectionOrTransactionDbDistributedLock(name, () => new GBaseDatabaseConnection(connection)); + } +} diff --git a/src/DistributedLock.GBase/GBaseDistributedLockHandle.cs b/src/DistributedLock.GBase/GBaseDistributedLockHandle.cs new file mode 100644 index 0000000..ed1cb45 --- /dev/null +++ b/src/DistributedLock.GBase/GBaseDistributedLockHandle.cs @@ -0,0 +1,31 @@ +using Medallion.Threading.Internal; + +namespace Medallion.Threading.GBase; + +/// +/// Implements +/// +public sealed class GBaseDistributedLockHandle : IDistributedSynchronizationHandle +{ + private IDistributedSynchronizationHandle? _innerHandle; + + internal GBaseDistributedLockHandle(IDistributedSynchronizationHandle innerHandle) + { + this._innerHandle = innerHandle; + } + + /// + /// Implements + /// + public CancellationToken HandleLostToken => this._innerHandle?.HandleLostToken ?? throw this.ObjectDisposed(); + + /// + /// Releases the lock + /// + public void Dispose() => Interlocked.Exchange(ref this._innerHandle, null)?.Dispose(); + + /// + /// Releases the lock asynchronously + /// + public ValueTask DisposeAsync() => Interlocked.Exchange(ref this._innerHandle, null)?.DisposeAsync() ?? default; +} diff --git a/src/DistributedLock.GBase/GBaseDistributedReaderWriterLock.IDistributedUpgradeableReaderWriterLock.cs b/src/DistributedLock.GBase/GBaseDistributedReaderWriterLock.IDistributedUpgradeableReaderWriterLock.cs new file mode 100644 index 0000000..8355c0d --- /dev/null +++ b/src/DistributedLock.GBase/GBaseDistributedReaderWriterLock.IDistributedUpgradeableReaderWriterLock.cs @@ -0,0 +1,226 @@ +using Medallion.Threading.Internal; + +namespace Medallion.Threading.GBase; + +public partial class GBaseDistributedReaderWriterLock +{ + // AUTO-GENERATED + + IDistributedSynchronizationHandle? IDistributedReaderWriterLock.TryAcquireReadLock(TimeSpan timeout, CancellationToken cancellationToken) => + this.TryAcquireReadLock(timeout, cancellationToken); + IDistributedSynchronizationHandle IDistributedReaderWriterLock.AcquireReadLock(TimeSpan? timeout, CancellationToken cancellationToken) => + this.AcquireReadLock(timeout, cancellationToken); + ValueTask IDistributedReaderWriterLock.TryAcquireReadLockAsync(TimeSpan timeout, CancellationToken cancellationToken) => + this.TryAcquireReadLockAsync(timeout, cancellationToken).Convert(To.ValueTask); + ValueTask IDistributedReaderWriterLock.AcquireReadLockAsync(TimeSpan? timeout, CancellationToken cancellationToken) => + this.AcquireReadLockAsync(timeout, cancellationToken).Convert(To.ValueTask); + IDistributedLockUpgradeableHandle? IDistributedUpgradeableReaderWriterLock.TryAcquireUpgradeableReadLock(TimeSpan timeout, CancellationToken cancellationToken) => + this.TryAcquireUpgradeableReadLock(timeout, cancellationToken); + IDistributedLockUpgradeableHandle IDistributedUpgradeableReaderWriterLock.AcquireUpgradeableReadLock(TimeSpan? timeout, CancellationToken cancellationToken) => + this.AcquireUpgradeableReadLock(timeout, cancellationToken); + ValueTask IDistributedUpgradeableReaderWriterLock.TryAcquireUpgradeableReadLockAsync(TimeSpan timeout, CancellationToken cancellationToken) => + this.TryAcquireUpgradeableReadLockAsync(timeout, cancellationToken).Convert(To.ValueTask); + ValueTask IDistributedUpgradeableReaderWriterLock.AcquireUpgradeableReadLockAsync(TimeSpan? timeout, CancellationToken cancellationToken) => + this.AcquireUpgradeableReadLockAsync(timeout, cancellationToken).Convert(To.ValueTask); + IDistributedSynchronizationHandle? IDistributedReaderWriterLock.TryAcquireWriteLock(TimeSpan timeout, CancellationToken cancellationToken) => + this.TryAcquireWriteLock(timeout, cancellationToken); + IDistributedSynchronizationHandle IDistributedReaderWriterLock.AcquireWriteLock(TimeSpan? timeout, CancellationToken cancellationToken) => + this.AcquireWriteLock(timeout, cancellationToken); + ValueTask IDistributedReaderWriterLock.TryAcquireWriteLockAsync(TimeSpan timeout, CancellationToken cancellationToken) => + this.TryAcquireWriteLockAsync(timeout, cancellationToken).Convert(To.ValueTask); + ValueTask IDistributedReaderWriterLock.AcquireWriteLockAsync(TimeSpan? timeout, CancellationToken cancellationToken) => + this.AcquireWriteLockAsync(timeout, cancellationToken).Convert(To.ValueTask); + + /// + /// Attempts to acquire a READ lock synchronously. Multiple readers are allowed. Not compatible with a WRITE lock. Usage: + /// + /// using (var handle = myLock.TryAcquireReadLock(...)) + /// { + /// if (handle != null) { /* we have the lock! */ } + /// } + /// // dispose releases the lock if we took it + /// + /// + /// How long to wait before giving up on the acquisition attempt. Defaults to 0 + /// Specifies a token by which the wait can be canceled + /// An which can be used to release the lock or null on failure + public GBaseDistributedReaderWriterLockHandle? TryAcquireReadLock(TimeSpan timeout = default, CancellationToken cancellationToken = default) => + DistributedLockHelpers.TryAcquire(this, timeout, cancellationToken, isWrite: false); + + /// + /// Acquires a READ lock synchronously, failing with if the attempt times out. Multiple readers are allowed. Not compatible with a WRITE lock. Usage: + /// + /// using (myLock.AcquireReadLock(...)) + /// { + /// /* we have the lock! */ + /// } + /// // dispose releases the lock + /// + /// + /// How long to wait before giving up on the acquisition attempt. Defaults to + /// Specifies a token by which the wait can be canceled + /// An which can be used to release the lock + public GBaseDistributedReaderWriterLockHandle AcquireReadLock(TimeSpan? timeout = null, CancellationToken cancellationToken = default) => + DistributedLockHelpers.Acquire(this, timeout, cancellationToken, isWrite: false); + + /// + /// Attempts to acquire a READ lock asynchronously. Multiple readers are allowed. Not compatible with a WRITE lock. Usage: + /// + /// await using (var handle = await myLock.TryAcquireReadLockAsync(...)) + /// { + /// if (handle != null) { /* we have the lock! */ } + /// } + /// // dispose releases the lock if we took it + /// + /// + /// How long to wait before giving up on the acquisition attempt. Defaults to 0 + /// Specifies a token by which the wait can be canceled + /// An which can be used to release the lock or null on failure + public ValueTask TryAcquireReadLockAsync(TimeSpan timeout = default, CancellationToken cancellationToken = default) => + this.As>().InternalTryAcquireAsync(timeout, cancellationToken, isWrite: false); + + /// + /// Acquires a READ lock asynchronously, failing with if the attempt times out. Multiple readers are allowed. Not compatible with a WRITE lock. Usage: + /// + /// await using (await myLock.AcquireReadLockAsync(...)) + /// { + /// /* we have the lock! */ + /// } + /// // dispose releases the lock + /// + /// + /// How long to wait before giving up on the acquisition attempt. Defaults to + /// Specifies a token by which the wait can be canceled + /// An which can be used to release the lock + public ValueTask AcquireReadLockAsync(TimeSpan? timeout = null, CancellationToken cancellationToken = default) => + DistributedLockHelpers.AcquireAsync(this, timeout, cancellationToken, isWrite: false); + + /// + /// Attempts to acquire an UPGRADE lock synchronously. Not compatible with another UPGRADE lock or a WRITE lock. Usage: + /// + /// using (var handle = myLock.TryAcquireUpgradeableReadLock(...)) + /// { + /// if (handle != null) { /* we have the lock! */ } + /// } + /// // dispose releases the lock if we took it + /// + /// + /// How long to wait before giving up on the acquisition attempt. Defaults to 0 + /// Specifies a token by which the wait can be canceled + /// An which can be used to release the lock or null on failure + public GBaseDistributedReaderWriterLockUpgradeableHandle? TryAcquireUpgradeableReadLock(TimeSpan timeout = default, CancellationToken cancellationToken = default) => + DistributedLockHelpers.TryAcquireUpgradeableReadLock(this, timeout, cancellationToken); + + /// + /// Acquires an UPGRADE lock synchronously, failing with if the attempt times out. Not compatible with another UPGRADE lock or a WRITE lock. Usage: + /// + /// using (myLock.AcquireUpgradeableReadLock(...)) + /// { + /// /* we have the lock! */ + /// } + /// // dispose releases the lock + /// + /// + /// How long to wait before giving up on the acquisition attempt. Defaults to + /// Specifies a token by which the wait can be canceled + /// An which can be used to release the lock + public GBaseDistributedReaderWriterLockUpgradeableHandle AcquireUpgradeableReadLock(TimeSpan? timeout = null, CancellationToken cancellationToken = default) => + DistributedLockHelpers.AcquireUpgradeableReadLock(this, timeout, cancellationToken); + + /// + /// Attempts to acquire an UPGRADE lock asynchronously. Not compatible with another UPGRADE lock or a WRITE lock. Usage: + /// + /// await using (var handle = await myLock.TryAcquireUpgradeableReadLockAsync(...)) + /// { + /// if (handle != null) { /* we have the lock! */ } + /// } + /// // dispose releases the lock if we took it + /// + /// + /// How long to wait before giving up on the acquisition attempt. Defaults to 0 + /// Specifies a token by which the wait can be canceled + /// An which can be used to release the lock or null on failure + public ValueTask TryAcquireUpgradeableReadLockAsync(TimeSpan timeout = default, CancellationToken cancellationToken = default) => + this.As>().InternalTryAcquireUpgradeableReadLockAsync(timeout, cancellationToken); + + /// + /// Acquires an UPGRADE lock asynchronously, failing with if the attempt times out. Not compatible with another UPGRADE lock or a WRITE lock. Usage: + /// + /// await using (await myLock.AcquireUpgradeableReadLockAsync(...)) + /// { + /// /* we have the lock! */ + /// } + /// // dispose releases the lock + /// + /// + /// How long to wait before giving up on the acquisition attempt. Defaults to + /// Specifies a token by which the wait can be canceled + /// An which can be used to release the lock + public ValueTask AcquireUpgradeableReadLockAsync(TimeSpan? timeout = null, CancellationToken cancellationToken = default) => + DistributedLockHelpers.AcquireUpgradeableReadLockAsync(this, timeout, cancellationToken); + + /// + /// Attempts to acquire a WRITE lock synchronously. Not compatible with another WRITE lock or an UPGRADE lock. Usage: + /// + /// using (var handle = myLock.TryAcquireWriteLock(...)) + /// { + /// if (handle != null) { /* we have the lock! */ } + /// } + /// // dispose releases the lock if we took it + /// + /// + /// How long to wait before giving up on the acquisition attempt. Defaults to 0 + /// Specifies a token by which the wait can be canceled + /// An which can be used to release the lock or null on failure + public GBaseDistributedReaderWriterLockHandle? TryAcquireWriteLock(TimeSpan timeout = default, CancellationToken cancellationToken = default) => + DistributedLockHelpers.TryAcquire(this, timeout, cancellationToken, isWrite: true); + + /// + /// Acquires a WRITE lock synchronously, failing with if the attempt times out. Not compatible with another WRITE lock or an UPGRADE lock. Usage: + /// + /// using (myLock.AcquireWriteLock(...)) + /// { + /// /* we have the lock! */ + /// } + /// // dispose releases the lock + /// + /// + /// How long to wait before giving up on the acquisition attempt. Defaults to + /// Specifies a token by which the wait can be canceled + /// An which can be used to release the lock + public GBaseDistributedReaderWriterLockHandle AcquireWriteLock(TimeSpan? timeout = null, CancellationToken cancellationToken = default) => + DistributedLockHelpers.Acquire(this, timeout, cancellationToken, isWrite: true); + + /// + /// Attempts to acquire a WRITE lock asynchronously. Not compatible with another WRITE lock or an UPGRADE lock. Usage: + /// + /// await using (var handle = await myLock.TryAcquireWriteLockAsync(...)) + /// { + /// if (handle != null) { /* we have the lock! */ } + /// } + /// // dispose releases the lock if we took it + /// + /// + /// How long to wait before giving up on the acquisition attempt. Defaults to 0 + /// Specifies a token by which the wait can be canceled + /// An which can be used to release the lock or null on failure + public ValueTask TryAcquireWriteLockAsync(TimeSpan timeout = default, CancellationToken cancellationToken = default) => + this.As>().InternalTryAcquireAsync(timeout, cancellationToken, isWrite: true); + + /// + /// Acquires a WRITE lock asynchronously, failing with if the attempt times out. Not compatible with another WRITE lock or an UPGRADE lock. Usage: + /// + /// await using (await myLock.AcquireWriteLockAsync(...)) + /// { + /// /* we have the lock! */ + /// } + /// // dispose releases the lock + /// + /// + /// How long to wait before giving up on the acquisition attempt. Defaults to + /// Specifies a token by which the wait can be canceled + /// An which can be used to release the lock + public ValueTask AcquireWriteLockAsync(TimeSpan? timeout = null, CancellationToken cancellationToken = default) => + DistributedLockHelpers.AcquireAsync(this, timeout, cancellationToken, isWrite: true); + +} \ No newline at end of file diff --git a/src/DistributedLock.GBase/GBaseDistributedReaderWriterLock.cs b/src/DistributedLock.GBase/GBaseDistributedReaderWriterLock.cs new file mode 100644 index 0000000..3811f2b --- /dev/null +++ b/src/DistributedLock.GBase/GBaseDistributedReaderWriterLock.cs @@ -0,0 +1,68 @@ +using Medallion.Threading.Internal; +using Medallion.Threading.Internal.Data; +using System.Data; + +namespace Medallion.Threading.GBase; + +/// +/// Implements an upgradeable distributed reader-writer lock for the GBase database using the DBMS_LOCK package. +/// +public sealed partial class GBaseDistributedReaderWriterLock : IInternalDistributedUpgradeableReaderWriterLock +{ + private readonly IDbDistributedLock _internalLock; + + /// + /// Constructs a new lock using the provided . + /// + /// The provided will be used to connect to the database. + /// + /// Unless is specified, will be escaped/hashed to ensure name validity. + /// + public GBaseDistributedReaderWriterLock(string name, string connectionString, Action? options = null, bool exactName = false) + : this(name, exactName, n => GBaseDistributedLock.CreateInternalLock(n, connectionString, options)) + { + } + + /// + /// Constructs a new lock using the provided . + /// + /// The provided will be used to connect to the database and will provide lock scope. It is assumed to be externally managed and + /// will not be opened or closed. + /// + /// Unless is specified, will be escaped/hashed to ensure name validity. + /// + public GBaseDistributedReaderWriterLock(string name, IDbConnection connection, bool exactName = false) + : this(name, exactName, n => GBaseDistributedLock.CreateInternalLock(n, connection)) + { + } + + private GBaseDistributedReaderWriterLock(string name, bool exactName, Func internalLockFactory) + { + this.Name = GBaseDistributedLock.GetName(name, exactName); + this._internalLock = internalLockFactory(this.Name); + } + + /// + /// Implements + /// + public string Name { get; } + + async ValueTask IInternalDistributedUpgradeableReaderWriterLock.InternalTryAcquireUpgradeableReadLockAsync( + TimeoutValue timeout, + CancellationToken cancellationToken) + { + var innerHandle = await this._internalLock + .TryAcquireAsync(timeout, GBaseDbmsLock.UpdateLock, cancellationToken, contextHandle: null).ConfigureAwait(false); + return innerHandle != null ? new GBaseDistributedReaderWriterLockUpgradeableHandle(innerHandle, this._internalLock) : null; + } + + async ValueTask IInternalDistributedReaderWriterLock.InternalTryAcquireAsync( + TimeoutValue timeout, + CancellationToken cancellationToken, + bool isWrite) + { + var innerHandle = await this._internalLock + .TryAcquireAsync(timeout, isWrite ? GBaseDbmsLock.ExclusiveLock : GBaseDbmsLock.SharedLock, cancellationToken, contextHandle: null).ConfigureAwait(false); + return innerHandle != null ? new GBaseDistributedReaderWriterLockNonUpgradeableHandle(innerHandle) : null; + } +} diff --git a/src/DistributedLock.GBase/GBaseDistributedReaderWriterLockHandle.cs b/src/DistributedLock.GBase/GBaseDistributedReaderWriterLockHandle.cs new file mode 100644 index 0000000..dde1979 --- /dev/null +++ b/src/DistributedLock.GBase/GBaseDistributedReaderWriterLockHandle.cs @@ -0,0 +1,123 @@ +using Medallion.Threading.Internal; +using Medallion.Threading.Internal.Data; + +namespace Medallion.Threading.GBase; + +/// +/// Implements +/// +public abstract class GBaseDistributedReaderWriterLockHandle : IDistributedSynchronizationHandle +{ + // forbid external inheritors + internal GBaseDistributedReaderWriterLockHandle() { } + + /// + /// Implements + /// + public abstract CancellationToken HandleLostToken { get; } + + /// + /// Releases the lock + /// + public void Dispose() => this.DisposeSyncViaAsync(); + + /// + /// Releases the lock asynchronously + /// + public abstract ValueTask DisposeAsync(); +} + +internal sealed class GBaseDistributedReaderWriterLockNonUpgradeableHandle : GBaseDistributedReaderWriterLockHandle +{ + private IDistributedSynchronizationHandle? _innerHandle; + + internal GBaseDistributedReaderWriterLockNonUpgradeableHandle(IDistributedSynchronizationHandle? handle) + { + this._innerHandle = handle; + } + + public override CancellationToken HandleLostToken => this._innerHandle?.HandleLostToken ?? throw this.ObjectDisposed(); + + public override ValueTask DisposeAsync() => Interlocked.Exchange(ref this._innerHandle, null)?.DisposeAsync() ?? default; +} + +/// +/// Implements +/// +public sealed class GBaseDistributedReaderWriterLockUpgradeableHandle : GBaseDistributedReaderWriterLockHandle, IInternalDistributedLockUpgradeableHandle +{ + private RefBox<(IDistributedSynchronizationHandle innerHandle, IDbDistributedLock @lock, IDistributedSynchronizationHandle? upgradedHandle)>? _box; + + internal GBaseDistributedReaderWriterLockUpgradeableHandle(IDistributedSynchronizationHandle innerHandle, IDbDistributedLock @lock) + { + this._box = RefBox.Create((innerHandle, @lock, default(IDistributedSynchronizationHandle?))); + } + + /// + /// Implements + /// + public override CancellationToken HandleLostToken => (this._box ?? throw this.ObjectDisposed()).Value.innerHandle.HandleLostToken; + + /// + /// Releases the lock asynchronously + /// + public override async ValueTask DisposeAsync() + { + if (RefBox.TryConsume(ref this._box, out var contents)) + { + try { await (contents.upgradedHandle?.DisposeAsync() ?? default).ConfigureAwait(false); } + finally { await contents.innerHandle.DisposeAsync().ConfigureAwait(false); } + } + } + + /// + /// Implements + /// + public bool TryUpgradeToWriteLock(TimeSpan timeout = default, CancellationToken cancellationToken = default) => + DistributedLockHelpers.TryUpgradeToWriteLock(this, timeout, cancellationToken); + + /// + /// Implements + /// + public ValueTask TryUpgradeToWriteLockAsync(TimeSpan timeout = default, CancellationToken cancellationToken = default) => + this.As().InternalTryUpgradeToWriteLockAsync(timeout, cancellationToken); + + /// + /// Implements + /// + public void UpgradeToWriteLock(TimeSpan? timeout = null, CancellationToken cancellationToken = default) => + DistributedLockHelpers.UpgradeToWriteLock(this, timeout, cancellationToken); + + /// + /// Implements + /// + public ValueTask UpgradeToWriteLockAsync(TimeSpan? timeout = null, CancellationToken cancellationToken = default) => + DistributedLockHelpers.UpgradeToWriteLockAsync(this, timeout, cancellationToken); + + ValueTask IInternalDistributedLockUpgradeableHandle.InternalTryUpgradeToWriteLockAsync(TimeoutValue timeout, CancellationToken cancellationToken) + { + var box = this._box ?? throw this.ObjectDisposed(); + var contents = box.Value; + if (contents.upgradedHandle != null) { throw new InvalidOperationException("the lock has already been upgraded"); } + return TryPerformUpgradeAsync(); + + async ValueTask TryPerformUpgradeAsync() + { + var upgradedHandle = + await contents.@lock.TryAcquireAsync(timeout, GBaseDbmsLock.UpgradeLock, cancellationToken, contextHandle: contents.innerHandle).ConfigureAwait(false); + if (upgradedHandle == null) + { + return false; + } + + contents.upgradedHandle = upgradedHandle; + var newBox = RefBox.Create(contents); + if (Interlocked.CompareExchange(ref this._box, newBox, comparand: box) != box) + { + await upgradedHandle.DisposeAsync().ConfigureAwait(false); + } + + return true; + } + } +} diff --git a/src/DistributedLock.GBase/GBaseDistributedSynchronizationProvider.cs b/src/DistributedLock.GBase/GBaseDistributedSynchronizationProvider.cs new file mode 100644 index 0000000..4e14aef --- /dev/null +++ b/src/DistributedLock.GBase/GBaseDistributedSynchronizationProvider.cs @@ -0,0 +1,55 @@ +using System.Data; + +namespace Medallion.Threading.GBase; + +/// +/// Implements for +/// and for +/// +public sealed class GBaseDistributedSynchronizationProvider : IDistributedLockProvider, IDistributedUpgradeableReaderWriterLockProvider +{ + private readonly Func _lockFactory; + private readonly Func _readerWriterLockFactory; + + /// + /// Constructs a provider that connects with and . + /// + public GBaseDistributedSynchronizationProvider(string connectionString, Action? options = null) + { + if (connectionString == null) { throw new ArgumentNullException(nameof(connectionString)); } + + this._lockFactory = (name, exactName) => new GBaseDistributedLock(name, connectionString, options, exactName); + this._readerWriterLockFactory = (name, exactName) => new GBaseDistributedReaderWriterLock(name, connectionString, options, exactName); + } + + /// + /// Constructs a provider that connects with . + /// + public GBaseDistributedSynchronizationProvider(IDbConnection connection) + { + if (connection == null) { throw new ArgumentNullException(nameof(connection)); } + + this._lockFactory = (name, exactName) => new GBaseDistributedLock(name, connection, exactName); + this._readerWriterLockFactory = (name, exactName) => new GBaseDistributedReaderWriterLock(name, connection, exactName); + } + + /// + /// Creates a with the provided . Unless + /// is specified, invalid names will be escaped/hashed. + /// + public GBaseDistributedLock CreateLock(string name, bool exactName = false) => this._lockFactory(name, exactName); + + IDistributedLock IDistributedLockProvider.CreateLock(string name) => this.CreateLock(name); + + /// + /// Creates a with the provided . Unless + /// is specified, invalid names will be escaped/hashed. + /// + public GBaseDistributedReaderWriterLock CreateReaderWriterLock(string name, bool exactName = false) => this._readerWriterLockFactory(name, exactName); + + IDistributedUpgradeableReaderWriterLock IDistributedUpgradeableReaderWriterLockProvider.CreateUpgradeableReaderWriterLock(string name) => + this.CreateReaderWriterLock(name); + + IDistributedReaderWriterLock IDistributedReaderWriterLockProvider.CreateReaderWriterLock(string name) => + this.CreateReaderWriterLock(name); +} diff --git a/src/DistributedLock.GBase/GBaseMultiplexedConnectionLockPool.cs b/src/DistributedLock.GBase/GBaseMultiplexedConnectionLockPool.cs new file mode 100644 index 0000000..ee7de3a --- /dev/null +++ b/src/DistributedLock.GBase/GBaseMultiplexedConnectionLockPool.cs @@ -0,0 +1,8 @@ +using Medallion.Threading.Internal.Data; + +namespace Medallion.Threading.GBase; + +internal static class GBaseMultiplexedConnectionLockPool +{ + public static readonly MultiplexedConnectionLockPool Instance = new(s => new GBaseDatabaseConnection(s)); +} diff --git a/src/DistributedLock.GBase/PublicAPI.Shipped.txt b/src/DistributedLock.GBase/PublicAPI.Shipped.txt new file mode 100644 index 0000000..c8932ff --- /dev/null +++ b/src/DistributedLock.GBase/PublicAPI.Shipped.txt @@ -0,0 +1,48 @@ +#nullable enable +abstract Medallion.Threading.GBase.GBaseDistributedReaderWriterLockHandle.DisposeAsync() -> System.Threading.Tasks.ValueTask +abstract Medallion.Threading.GBase.GBaseDistributedReaderWriterLockHandle.HandleLostToken.get -> System.Threading.CancellationToken +Medallion.Threading.GBase.GBaseConnectionOptionsBuilder +Medallion.Threading.GBase.GBaseConnectionOptionsBuilder.KeepaliveCadence(System.TimeSpan keepaliveCadence) -> Medallion.Threading.GBase.GBaseConnectionOptionsBuilder! +Medallion.Threading.GBase.GBaseConnectionOptionsBuilder.UseMultiplexing(bool useMultiplexing = true) -> Medallion.Threading.GBase.GBaseConnectionOptionsBuilder! +Medallion.Threading.GBase.GBaseDistributedLock +Medallion.Threading.GBase.GBaseDistributedLock.Acquire(System.TimeSpan? timeout = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Medallion.Threading.GBase.GBaseDistributedLockHandle! +Medallion.Threading.GBase.GBaseDistributedLock.AcquireAsync(System.TimeSpan? timeout = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +Medallion.Threading.GBase.GBaseDistributedLock.Name.get -> string! +Medallion.Threading.GBase.GBaseDistributedLock.GBaseDistributedLock(string! name, string! connectionString, System.Action? options = null, bool exactName = false) -> void +Medallion.Threading.GBase.GBaseDistributedLock.GBaseDistributedLock(string! name, System.Data.IDbConnection! connection, bool exactName = false) -> void +Medallion.Threading.GBase.GBaseDistributedLock.TryAcquire(System.TimeSpan timeout = default(System.TimeSpan), System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Medallion.Threading.GBase.GBaseDistributedLockHandle? +Medallion.Threading.GBase.GBaseDistributedLock.TryAcquireAsync(System.TimeSpan timeout = default(System.TimeSpan), System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +Medallion.Threading.GBase.GBaseDistributedLockHandle +Medallion.Threading.GBase.GBaseDistributedLockHandle.Dispose() -> void +Medallion.Threading.GBase.GBaseDistributedLockHandle.DisposeAsync() -> System.Threading.Tasks.ValueTask +Medallion.Threading.GBase.GBaseDistributedLockHandle.HandleLostToken.get -> System.Threading.CancellationToken +Medallion.Threading.GBase.GBaseDistributedReaderWriterLock +Medallion.Threading.GBase.GBaseDistributedReaderWriterLock.AcquireReadLock(System.TimeSpan? timeout = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Medallion.Threading.GBase.GBaseDistributedReaderWriterLockHandle! +Medallion.Threading.GBase.GBaseDistributedReaderWriterLock.AcquireReadLockAsync(System.TimeSpan? timeout = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +Medallion.Threading.GBase.GBaseDistributedReaderWriterLock.AcquireUpgradeableReadLock(System.TimeSpan? timeout = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Medallion.Threading.GBase.GBaseDistributedReaderWriterLockUpgradeableHandle! +Medallion.Threading.GBase.GBaseDistributedReaderWriterLock.AcquireUpgradeableReadLockAsync(System.TimeSpan? timeout = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +Medallion.Threading.GBase.GBaseDistributedReaderWriterLock.AcquireWriteLock(System.TimeSpan? timeout = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Medallion.Threading.GBase.GBaseDistributedReaderWriterLockHandle! +Medallion.Threading.GBase.GBaseDistributedReaderWriterLock.AcquireWriteLockAsync(System.TimeSpan? timeout = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +Medallion.Threading.GBase.GBaseDistributedReaderWriterLock.Name.get -> string! +Medallion.Threading.GBase.GBaseDistributedReaderWriterLock.GBaseDistributedReaderWriterLock(string! name, string! connectionString, System.Action? options = null, bool exactName = false) -> void +Medallion.Threading.GBase.GBaseDistributedReaderWriterLock.GBaseDistributedReaderWriterLock(string! name, System.Data.IDbConnection! connection, bool exactName = false) -> void +Medallion.Threading.GBase.GBaseDistributedReaderWriterLock.TryAcquireReadLock(System.TimeSpan timeout = default(System.TimeSpan), System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Medallion.Threading.GBase.GBaseDistributedReaderWriterLockHandle? +Medallion.Threading.GBase.GBaseDistributedReaderWriterLock.TryAcquireReadLockAsync(System.TimeSpan timeout = default(System.TimeSpan), System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +Medallion.Threading.GBase.GBaseDistributedReaderWriterLock.TryAcquireUpgradeableReadLock(System.TimeSpan timeout = default(System.TimeSpan), System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Medallion.Threading.GBase.GBaseDistributedReaderWriterLockUpgradeableHandle? +Medallion.Threading.GBase.GBaseDistributedReaderWriterLock.TryAcquireUpgradeableReadLockAsync(System.TimeSpan timeout = default(System.TimeSpan), System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +Medallion.Threading.GBase.GBaseDistributedReaderWriterLock.TryAcquireWriteLock(System.TimeSpan timeout = default(System.TimeSpan), System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Medallion.Threading.GBase.GBaseDistributedReaderWriterLockHandle? +Medallion.Threading.GBase.GBaseDistributedReaderWriterLock.TryAcquireWriteLockAsync(System.TimeSpan timeout = default(System.TimeSpan), System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +Medallion.Threading.GBase.GBaseDistributedReaderWriterLockHandle +Medallion.Threading.GBase.GBaseDistributedReaderWriterLockHandle.Dispose() -> void +Medallion.Threading.GBase.GBaseDistributedReaderWriterLockUpgradeableHandle +Medallion.Threading.GBase.GBaseDistributedReaderWriterLockUpgradeableHandle.TryUpgradeToWriteLock(System.TimeSpan timeout = default(System.TimeSpan), System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool +Medallion.Threading.GBase.GBaseDistributedReaderWriterLockUpgradeableHandle.TryUpgradeToWriteLockAsync(System.TimeSpan timeout = default(System.TimeSpan), System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +Medallion.Threading.GBase.GBaseDistributedReaderWriterLockUpgradeableHandle.UpgradeToWriteLock(System.TimeSpan? timeout = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> void +Medallion.Threading.GBase.GBaseDistributedReaderWriterLockUpgradeableHandle.UpgradeToWriteLockAsync(System.TimeSpan? timeout = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +Medallion.Threading.GBase.GBaseDistributedSynchronizationProvider +Medallion.Threading.GBase.GBaseDistributedSynchronizationProvider.CreateLock(string! name, bool exactName = false) -> Medallion.Threading.GBase.GBaseDistributedLock! +Medallion.Threading.GBase.GBaseDistributedSynchronizationProvider.CreateReaderWriterLock(string! name, bool exactName = false) -> Medallion.Threading.GBase.GBaseDistributedReaderWriterLock! +Medallion.Threading.GBase.GBaseDistributedSynchronizationProvider.GBaseDistributedSynchronizationProvider(string! connectionString, System.Action? options = null) -> void +Medallion.Threading.GBase.GBaseDistributedSynchronizationProvider.GBaseDistributedSynchronizationProvider(System.Data.IDbConnection! connection) -> void +override Medallion.Threading.GBase.GBaseDistributedReaderWriterLockUpgradeableHandle.DisposeAsync() -> System.Threading.Tasks.ValueTask +override Medallion.Threading.GBase.GBaseDistributedReaderWriterLockUpgradeableHandle.HandleLostToken.get -> System.Threading.CancellationToken \ No newline at end of file diff --git a/src/DistributedLock.GBase/PublicAPI.Unshipped.txt b/src/DistributedLock.GBase/PublicAPI.Unshipped.txt new file mode 100644 index 0000000..5f28270 --- /dev/null +++ b/src/DistributedLock.GBase/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/DistributedLock.GBase/packages.lock.json b/src/DistributedLock.GBase/packages.lock.json new file mode 100644 index 0000000..0ab4075 --- /dev/null +++ b/src/DistributedLock.GBase/packages.lock.json @@ -0,0 +1,109 @@ +{ + "version": 2, + "dependencies": { + ".NETFramework,Version=v4.7.2": { + "GeneralData.GBase8s.DataProvider": { + "type": "Direct", + "requested": "[1.0.0.1, )", + "resolved": "1.0.0.1", + "contentHash": "yfadHeyeiUCoaOgDn/fpVTP+KkfcWLMxcd44fxN7ne2axeMW9ewkZ6ei7/0Bc9IXQTITWOX6lq8YTCMNlByhxQ==" + }, + "Microsoft.CodeAnalysis.PublicApiAnalyzers": { + "type": "Direct", + "requested": "[3.3.4, )", + "resolved": "3.3.4", + "contentHash": "kNLTfXtXUWDHVt5iaPkkiPuyHYlMgLI6SOFT4w88bfeI2vqSeGgHunFkdvlaCM8RDfcY0t2+jnesQtidRJJ/DA==" + }, + "Microsoft.SourceLink.GitHub": { + "type": "Direct", + "requested": "[8.0.0, )", + "resolved": "8.0.0", + "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==", + "dependencies": { + "Microsoft.Build.Tasks.Git": "8.0.0", + "Microsoft.SourceLink.Common": "8.0.0" + } + }, + "Microsoft.Bcl.AsyncInterfaces": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "3WA9q9yVqJp222P3x1wYIGDAkpjAku0TMUaaQV22g6L67AI0LdOIrVS7Ht2vJfLHGSPVuqN94vIr15qn+HEkHw==", + "dependencies": { + "System.Threading.Tasks.Extensions": "4.5.4" + } + }, + "Microsoft.Build.Tasks.Git": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ==" + }, + "Microsoft.SourceLink.Common": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw==" + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "4.5.3", + "contentHash": "3TIsJhD1EiiT0w2CcDMN/iSSwnNnsrnbzeVHSKkaEgV85txMprmuO+Yq2AdSbeVGcg28pdNDTPK87tJhX7VFHw==" + }, + "System.Threading.Tasks.Extensions": { + "type": "Transitive", + "resolved": "4.5.4", + "contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "4.5.3" + } + }, + "System.ValueTuple": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "okurQJO6NRE/apDIP23ajJ0hpiNmJ+f0BwOlB/cSqTLQlw5upkf+5+96+iG2Jw40G1fCVCyPz/FhIABUjMR+RQ==" + }, + "distributedlock.core": { + "type": "Project", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "[8.0.0, )", + "System.ValueTuple": "[4.5.0, )" + } + } + }, + ".NETStandard,Version=v2.1": { + "GeneralData.GBase8s.DataProvider": { + "type": "Direct", + "requested": "[1.0.0.1, )", + "resolved": "1.0.0.1", + "contentHash": "yfadHeyeiUCoaOgDn/fpVTP+KkfcWLMxcd44fxN7ne2axeMW9ewkZ6ei7/0Bc9IXQTITWOX6lq8YTCMNlByhxQ==" + }, + "Microsoft.CodeAnalysis.PublicApiAnalyzers": { + "type": "Direct", + "requested": "[3.3.4, )", + "resolved": "3.3.4", + "contentHash": "kNLTfXtXUWDHVt5iaPkkiPuyHYlMgLI6SOFT4w88bfeI2vqSeGgHunFkdvlaCM8RDfcY0t2+jnesQtidRJJ/DA==" + }, + "Microsoft.SourceLink.GitHub": { + "type": "Direct", + "requested": "[8.0.0, )", + "resolved": "8.0.0", + "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==", + "dependencies": { + "Microsoft.Build.Tasks.Git": "8.0.0", + "Microsoft.SourceLink.Common": "8.0.0" + } + }, + "Microsoft.Build.Tasks.Git": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ==" + }, + "Microsoft.SourceLink.Common": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw==" + }, + "distributedlock.core": { + "type": "Project" + } + } + } +} \ No newline at end of file diff --git a/src/DistributedLock.Tests/DistributedLock.Tests.csproj b/src/DistributedLock.Tests/DistributedLock.Tests.csproj index be82219..98c4927 100644 --- a/src/DistributedLock.Tests/DistributedLock.Tests.csproj +++ b/src/DistributedLock.Tests/DistributedLock.Tests.csproj @@ -14,9 +14,11 @@ 1591 false + net8.0 + all