diff --git a/DistributedLock.Core/AssemblyAttributes.cs b/DistributedLock.Core/AssemblyAttributes.cs index d2e9bff3..951bcc01 100644 --- a/DistributedLock.Core/AssemblyAttributes.cs +++ b/DistributedLock.Core/AssemblyAttributes.cs @@ -15,4 +15,5 @@ [assembly: InternalsVisibleTo("DistributedLock.ZooKeeper, PublicKey=0024000004800000940000000602000000240000525341310004000001000100fd3af56ccc8ed94fffe25bfd651e6a5674f8f20a76d37de800dd0f7380e04f0fde2da6fa200380b14fe398605b6f470c87e5e0a0bf39ae871f07536a4994aa7a0057c4d3bcedc8fef3eecb0c88c2024a1b3289305c2393acd9fb9f9a42d0bd7826738ce864d507575ea3a1fe1746ab19823303269f79379d767949807f494be8")] [assembly: InternalsVisibleTo("DistributedLock.MySql, PublicKey=0024000004800000940000000602000000240000525341310004000001000100fd3af56ccc8ed94fffe25bfd651e6a5674f8f20a76d37de800dd0f7380e04f0fde2da6fa200380b14fe398605b6f470c87e5e0a0bf39ae871f07536a4994aa7a0057c4d3bcedc8fef3eecb0c88c2024a1b3289305c2393acd9fb9f9a42d0bd7826738ce864d507575ea3a1fe1746ab19823303269f79379d767949807f494be8")] [assembly: InternalsVisibleTo("DistributedLock.Oracle, PublicKey=0024000004800000940000000602000000240000525341310004000001000100fd3af56ccc8ed94fffe25bfd651e6a5674f8f20a76d37de800dd0f7380e04f0fde2da6fa200380b14fe398605b6f470c87e5e0a0bf39ae871f07536a4994aa7a0057c4d3bcedc8fef3eecb0c88c2024a1b3289305c2393acd9fb9f9a42d0bd7826738ce864d507575ea3a1fe1746ab19823303269f79379d767949807f494be8")] +[assembly: InternalsVisibleTo("DistributedLock.ProcessScoped, PublicKey=0024000004800000940000000602000000240000525341310004000001000100fd3af56ccc8ed94fffe25bfd651e6a5674f8f20a76d37de800dd0f7380e04f0fde2da6fa200380b14fe398605b6f470c87e5e0a0bf39ae871f07536a4994aa7a0057c4d3bcedc8fef3eecb0c88c2024a1b3289305c2393acd9fb9f9a42d0bd7826738ce864d507575ea3a1fe1746ab19823303269f79379d767949807f494be8")] #endif diff --git a/DistributedLock.Core/DistributedLock.Core.csproj b/DistributedLock.Core/DistributedLock.Core.csproj index 2bb6ba2d..daa97fee 100644 --- a/DistributedLock.Core/DistributedLock.Core.csproj +++ b/DistributedLock.Core/DistributedLock.Core.csproj @@ -10,7 +10,7 @@ - 1.0.5 + 1.0.6 1.0.0.0 Michael Adelson Core interfaces and utilities that support the DistributedLock.* family of packages diff --git a/DistributedLock.Core/Internal/AsyncLock.cs b/DistributedLock.Core/Internal/AsyncLock.cs index 8377e9a5..8353f3de 100644 --- a/DistributedLock.Core/Internal/AsyncLock.cs +++ b/DistributedLock.Core/Internal/AsyncLock.cs @@ -11,7 +11,12 @@ namespace Medallion.Threading.Internal /// method because does not require disposal unless its /// is accessed /// - internal readonly struct AsyncLock +#if DEBUG + public +#else + internal +#endif + readonly struct AsyncLock { private readonly SemaphoreSlim _semaphore; diff --git a/DistributedLock.ProcessScoped/AssemblyAttributes.cs b/DistributedLock.ProcessScoped/AssemblyAttributes.cs new file mode 100644 index 00000000..e54310ec --- /dev/null +++ b/DistributedLock.ProcessScoped/AssemblyAttributes.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("DistributedLock.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100fd3af56ccc8ed94fffe25bfd651e6a5674f8f20a76d37de800dd0f7380e04f0fde2da6fa200380b14fe398605b6f470c87e5e0a0bf39ae871f07536a4994aa7a0057c4d3bcedc8fef3eecb0c88c2024a1b3289305c2393acd9fb9f9a42d0bd7826738ce864d507575ea3a1fe1746ab19823303269f79379d767949807f494be8")] \ No newline at end of file diff --git a/DistributedLock.ProcessScoped/AsyncReaderWriterLock.cs b/DistributedLock.ProcessScoped/AsyncReaderWriterLock.cs new file mode 100644 index 00000000..00c617e8 --- /dev/null +++ b/DistributedLock.ProcessScoped/AsyncReaderWriterLock.cs @@ -0,0 +1,371 @@ +using Medallion.Threading.Internal; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Medallion.Threading +{ + /// + /// A version of which supports both + /// synchronous (via ) and asynchronous locking. + /// + internal sealed class AsyncReaderWriterLock + { + private readonly AsyncLock _upgradeableReadLock = AsyncLock.Create(); + /// + /// Fires when the last reader releases OR the writer releases + /// + private readonly AsyncManualResetEvent _releasedEvent = new(set: true); + + /// + /// is the number of readers who have acquired or are waiting to acquire the read lock. + /// is the number of writers who have acquired or are waiting to acquire the write lock. + /// Neither counter includes the upgradeable read lock. + /// + private ulong _readerCount, _writerCount; + /// + /// Whether the lock is held by a reader or a writer or neither. Ignores the upgradeable read lock. + /// + private HeldState _state; + + private object Lock => this._releasedEvent; + + public async ValueTask TryAcquireReadLockAsync(TimeoutValue timeout, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + lock (this.Lock) + { + checked { ++this._readerCount; } + if (this._writerCount == 0) { return SetStateAndCreateHandleNoLock(); } + } + try + { + TimeoutTracker timeoutTracker = new(timeout); + while (true) + { + if (!await this._releasedEvent.WaitAsync(timeoutTracker.Remaining, cancellationToken).ConfigureAwait(false)) + { + this.ReleaseReadLock(); + return null; + } + + lock (this.Lock) + { + if (this._writerCount == 0) { return SetStateAndCreateHandleNoLock(); } + } + } + } + catch + { + this.ReleaseReadLock(); + throw; + } + + Handle SetStateAndCreateHandleNoLock() + { + Invariant.Require(this._state is HeldState.None or HeldState.Reader); + this._state = HeldState.Reader; + this._releasedEvent.Reset(); // writers will now block + return new Handle(this, state: null, static (l, _) => l.ReleaseReadLock()); + } + } + + private void ReleaseReadLock() + { + lock (this.Lock) + { + if (checked(--this._readerCount) == 0 && this._state == HeldState.Reader) + { + this._state = HeldState.None; + this._releasedEvent.Set(); // must be last in the lock block in case continuations run inline + } + } + } + + public async ValueTask TryAcquireUpgradeableReadLockAsync(TimeoutValue timeout, CancellationToken cancellationToken) + { + var upgradeableReadLockHandle = await this._upgradeableReadLock.TryAcquireAsync(timeout, cancellationToken).ConfigureAwait(false); + return upgradeableReadLockHandle is null ? null : new(this, upgradeableReadLockHandle); + } + + private async ValueTask TryUpgradeToWriteLockAsync(TimeoutValue timeout, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + lock (this.Lock) + { + this.BeginAcquiringWriteLockNoLock(); + + if (this._readerCount == 0) + { + this.SetStateOnWriteLockAcquiredNoLock(); + return true; + } + } + try + { + TimeoutTracker timeoutTracker = new(timeout); + while (true) + { + if (!await this._releasedEvent.WaitAsync(timeoutTracker.Remaining, cancellationToken).ConfigureAwait(false)) + { + lock (this.Lock) + { + this.ReleaseWriteLock(null); + } + return false; + } + + lock (this.Lock) + { + if (this._readerCount == 0) + { + this.SetStateOnWriteLockAcquiredNoLock(); + return true; + } + } + } + } + catch + { + this.ReleaseWriteLock(null); + throw; + } + } + + public async ValueTask TryAcquireWriteLockAsync(TimeoutValue timeout, CancellationToken cancellationToken) + { + lock (this.Lock) + { + this.BeginAcquiringWriteLockNoLock(); + } + + IDisposable? upgradeableReadLockHandle = null; + try + { + TimeoutTracker timeoutTracker = new(timeout); + upgradeableReadLockHandle = await this._upgradeableReadLock.TryAcquireAsync(timeoutTracker.Remaining, cancellationToken).ConfigureAwait(false); + if (upgradeableReadLockHandle is null) + { + this.ReleaseWriteLock(upgradeableReadLockHandle); + return null; + } + + while (true) + { + lock (this.Lock) + { + if (this._readerCount == 0) + { + this.SetStateOnWriteLockAcquiredNoLock(); + return new Handle(this, upgradeableReadLockHandle, static (l, s) => l.ReleaseWriteLock((IDisposable?)s)); + } + } + + if (!await this._releasedEvent.WaitAsync(timeoutTracker.Remaining, cancellationToken).ConfigureAwait(false)) + { + this.ReleaseWriteLock(upgradeableReadLockHandle); + return null; + } + } + } + catch + { + this.ReleaseWriteLock(upgradeableReadLockHandle); + throw; + } + } + + private void BeginAcquiringWriteLockNoLock() + { + checked { ++this._writerCount; } + this._releasedEvent.Reset(); // readers will now block + } + + private void SetStateOnWriteLockAcquiredNoLock() + { + Invariant.Require(this._state == HeldState.None); + this._state = HeldState.Writer; + } + + private void ReleaseWriteLock(IDisposable? upgradeableReadLockHandle) + { + lock (this.Lock) + { + if (checked(--this._writerCount) == 0 && this._state == HeldState.Writer) + { + this._state = HeldState.None; + this._releasedEvent.Set(); // must be last in the lock block in case continuations run inline + } + } + upgradeableReadLockHandle?.Dispose(); + } + + private enum HeldState { None, Reader, Writer, } + + private sealed class Handle : IDisposable + { + private AsyncReaderWriterLock? _lock; + private readonly object? _state; + private readonly Action _release; + + public Handle(AsyncReaderWriterLock @lock, object? state, Action release) + { + this._lock = @lock; + this._state = state; + this._release = release; + } + + public void Dispose() + { + if (Interlocked.Exchange(ref this._lock, null) is { } @lock) + { + this._release(@lock, this._state); + } + } + } + + public sealed class UpgradeableHandle : IDisposable + { + private AsyncReaderWriterLock? _lock; + private readonly IDisposable _upgradeableReadLockHandle; + private UpgradeState _state; + + public UpgradeableHandle(AsyncReaderWriterLock @lock, IDisposable upgradeableReadLockHandle) + { + this._lock = @lock; + this._upgradeableReadLockHandle = upgradeableReadLockHandle; + } + + private object Mutex => this._upgradeableReadLockHandle; + + public async ValueTask TryUpgradeToWriteLockAsync(TimeoutValue timeout, CancellationToken cancellationToken) + { + lock (this.Mutex) + { + if (this._lock is null) { throw new ObjectDisposedException(this.GetType().ToString()); } + this._state = this._state switch + { + UpgradeState.NotUpgraded => UpgradeState.Upgrading, + UpgradeState.Upgrading => throw new InvalidOperationException("Already upgrading to write lock"), + UpgradeState.Upgraded => throw new InvalidOperationException("Already upgraded to write lock"), + _ => throw new InvalidOperationException("Should never get here"), + }; + } + var state = UpgradeState.NotUpgraded; + try + { + if (await this._lock.TryUpgradeToWriteLockAsync(timeout, cancellationToken).ConfigureAwait(false)) + { + state = UpgradeState.Upgraded; + return true; + } + return false; + } + finally + { + lock (this.Mutex) + { + this._state = state; + } + } + } + + public void Dispose() + { + lock (this.Mutex) + { + if (this._lock is { } @lock) + { + if (this._state == UpgradeState.Upgrading) + { + throw new InvalidOperationException("Cannot dispose during an upgrade operation"); + } + + if (this._state == UpgradeState.Upgraded) { @lock.ReleaseWriteLock(this._upgradeableReadLockHandle); } + else { this._upgradeableReadLockHandle.Dispose(); } + this._lock = null; + } + } + } + + private enum UpgradeState { NotUpgraded, Upgrading, Upgraded } + } + + private sealed class AsyncManualResetEvent + { + private static readonly Task TrueTask = Task.FromResult(true); + + private readonly object _lock = new(); + private TaskCompletionSource? _task; + private bool _set; + + public AsyncManualResetEvent(bool set) + { + this._set = set; + } + + public bool IsSet => Volatile.Read(ref this._set); + + public void Set() + { + TaskCompletionSource? taskToComplete; + lock (this._lock) + { + if (this._set) { return; } + + taskToComplete = this._task; + this._task = null; + this._set = false; + } + + // true is important for allowing us to directly return this task in InternalWaitAsync + taskToComplete?.SetResult(true); + } + + public void Reset() + { + lock (this._lock) { this._set = false; } + } + + private Task GetTask() + { + lock (this._lock) + { + return this._set + ? TrueTask + : (this._task ??= new()).Task; + } + } + + public ValueTask WaitAsync(TimeoutValue timeout, CancellationToken cancellationToken) => + SyncViaAsync.IsSynchronous + ? this.InternalWait(timeout, cancellationToken).AsValueTask() + : this.InternalWaitAsync(timeout, cancellationToken).AsValueTask(); + + private bool InternalWait(TimeoutValue timeout, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + return this.GetTask().Wait(timeout.InMilliseconds, cancellationToken); + } + + private Task InternalWaitAsync(TimeoutValue timeout, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) { return Task.FromCanceled(cancellationToken); } + var task = this.GetTask(); + if (task.IsCompleted || (timeout.IsInfinite && !cancellationToken.CanBeCanceled)) { return task; } + return WaitHelperAsync(task, timeout, cancellationToken); + + static async Task WaitHelperAsync(Task task, TimeoutValue timeout, CancellationToken cancellationToken) + { + var completed = await Task.WhenAny(task, Task.Delay(timeout.InMilliseconds, cancellationToken)).ConfigureAwait(false); + await completed.ConfigureAwait(false); // propagate OperationCanceledException + return completed == task; + } + } + } + } +} diff --git a/DistributedLock.ProcessScoped/DistributedLock.ProcessScoped.csproj b/DistributedLock.ProcessScoped/DistributedLock.ProcessScoped.csproj new file mode 100644 index 00000000..60b8b8cc --- /dev/null +++ b/DistributedLock.ProcessScoped/DistributedLock.ProcessScoped.csproj @@ -0,0 +1,59 @@ + + + + netstandard2.0;netstandard2.1;net461 + Medallion.Threading + True + 4 + Latest + enable + + + + 1.0.0 + 1.0.0.0 + Michael Adelson + Provides a process-scoped implementation of named locks + Copyright © 2022 Michael Adelson + MIT + distributed lock async mutex reader writer semaphore + 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 + + + + + all + + + + + + + + + + + \ No newline at end of file diff --git a/DistributedLock.ProcessScoped/NamedObjectPool.cs b/DistributedLock.ProcessScoped/NamedObjectPool.cs new file mode 100644 index 00000000..efb02889 --- /dev/null +++ b/DistributedLock.ProcessScoped/NamedObjectPool.cs @@ -0,0 +1,92 @@ +using Medallion.Threading.Internal; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Text; +using System.Threading; + +namespace Medallion.Threading +{ + /// + /// Manages a unique instance of an object of type referred to by a name. + /// Callers can acquire a reference to the instance via the method. + /// When all leases are relinquished, the pooled instance becomes eligible for garbage collection. + /// + internal sealed class NamedObjectPool where T : class + { + private readonly Dictionary _instances = new(); + + private readonly Func _factory; + + public NamedObjectPool(Func factory) + { + this._factory = factory; + } + + private object Lock => this._instances; + + public ILease LeaseObject(string name) => Lease.Acquire(this, name); + + private sealed record State + { + public State(T instance) { this.Instance = instance; } + + public T Instance { get; } + public ulong LeaseCount { get; set; } + } + + public interface ILease : IDisposable + { + T Value { get; } + } + + private sealed class Lease : ILease + { + private T? _value; + private NamedObjectPool _pool; + private string _name; + + private Lease(NamedObjectPool pool, string name) + { + this._pool = pool; + this._name = name; + } + + public T Value => this._value ?? throw new ObjectDisposedException(this.GetType().ToString()); + + public static Lease Acquire(NamedObjectPool pool, string name) + { + Lease lease = new(pool, name); + lock (pool.Lock) + { + if (!pool._instances.TryGetValue(name, out var state)) + { + pool._instances.Add(name, state = new(pool._factory(name))); + } + checked { ++state.LeaseCount; } + lease._value = state.Instance; + } + return lease; + } + + public void Dispose() + { + if (Interlocked.Exchange(ref this._value, null) is { } value) + { + lock (this._pool.Lock) + { + var state = this._pool._instances[this._name]; + Invariant.Require(ReferenceEquals(state.Instance, value)); + checked { --state.LeaseCount; } + if (state.LeaseCount == 0) + { + this._pool._instances.Remove(this._name); + } + } + this._pool = null!; + this._name = null!; + } + } + } + } +} diff --git a/DistributedLock.ProcessScoped/ProcessScopedNamedLock.IDistributedLock.cs b/DistributedLock.ProcessScoped/ProcessScopedNamedLock.IDistributedLock.cs new file mode 100644 index 00000000..7f6cbd84 --- /dev/null +++ b/DistributedLock.ProcessScoped/ProcessScopedNamedLock.IDistributedLock.cs @@ -0,0 +1,85 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Medallion.Threading.Internal; + +namespace Medallion.Threading +{ + public partial class ProcessScopedNamedLock + { + // 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 + /// A which can be used to release the lock or null on failure + public ProcessScopedNamedLockHandle? 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 + /// A which can be used to release the lock + public ProcessScopedNamedLockHandle 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 + /// A 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 + /// A 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/DistributedLock.ProcessScoped/ProcessScopedNamedLock.cs b/DistributedLock.ProcessScoped/ProcessScopedNamedLock.cs new file mode 100644 index 00000000..16684286 --- /dev/null +++ b/DistributedLock.ProcessScoped/ProcessScopedNamedLock.cs @@ -0,0 +1,60 @@ +using Medallion.Threading.Internal; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Medallion.Threading +{ + /// + /// An implementation of which is SCOPED TO JUST THE CURRENT PROCESS and therefore + /// is NOT TRULY DISTRIBUTED. Therefore, this implementation is intended primarily for testing or scenarios where + /// name-based locking is useful (e.g. when frequently creating and destroying fine-grained locks). + /// + public sealed partial class ProcessScopedNamedLock : IInternalDistributedLock + { + private static readonly NamedObjectPool NamedObjectPool = new(_ => new()); + + /// + /// Constructs a lock with . + /// + public ProcessScopedNamedLock(string name) + { + this.Name = name ?? throw new ArgumentNullException(nameof(name)); + } + + /// + /// Implements + /// + public string Name { get; } + + async ValueTask IInternalDistributedLock.InternalTryAcquireAsync( + TimeoutValue timeout, + CancellationToken cancellationToken) + { + var acquired = false; + var lease = NamedObjectPool.LeaseObject(this.Name); + try + { + var handle = await lease.Value.Lock.TryAcquireAsync(timeout, cancellationToken).ConfigureAwait(false); + if (handle is null) { return null; } + + acquired = true; + return new(handle, lease); + } + finally + { + if (!acquired) + { + lease.Dispose(); + } + } + } + + private sealed class AsyncLockWrapper + { + internal readonly AsyncLock Lock = AsyncLock.Create(); + } + } +} diff --git a/DistributedLock.ProcessScoped/ProcessScopedNamedLockHandle.cs b/DistributedLock.ProcessScoped/ProcessScopedNamedLockHandle.cs new file mode 100644 index 00000000..a8220b0a --- /dev/null +++ b/DistributedLock.ProcessScoped/ProcessScopedNamedLockHandle.cs @@ -0,0 +1,61 @@ +using Medallion.Threading.Internal; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Medallion.Threading +{ + /// + /// Implements + /// + public sealed class ProcessScopedNamedLockHandle : IDistributedSynchronizationHandle + { + private HandleAndLease? _handleAndLease; + private IDisposable? _finalizerRegistration; + + internal ProcessScopedNamedLockHandle(IDisposable handle, IDisposable lease) + { + this._handleAndLease = new(handle, lease); + this._finalizerRegistration = ManagedFinalizerQueue.Instance.Register(this, this._handleAndLease); + } + + CancellationToken IDistributedSynchronizationHandle.HandleLostToken => + this._handleAndLease is null ? throw this.ObjectDisposed() : CancellationToken.None; + + /// + /// Releases the lock + /// + public void Dispose() + { + Interlocked.Exchange(ref this._finalizerRegistration, null)?.Dispose(); + Interlocked.Exchange(ref this._handleAndLease, null)?.Dispose(); + } + + /// + /// Releases the lock + /// + public ValueTask DisposeAsync() + { + this.Dispose(); + return default; + } + } + + internal sealed record HandleAndLease(TDisposable LockHandle, IDisposable Lease) : IAsyncDisposable, IDisposable + where TDisposable : IDisposable + { + public void Dispose() + { + this.LockHandle.Dispose(); + this.Lease.Dispose(); + } + + public ValueTask DisposeAsync() + { + this.Dispose(); + return default; + } + } +} diff --git a/DistributedLock.ProcessScoped/ProcessScopedNamedReaderWriterLock.IDistributedUpgradeableReaderWriterLock.cs b/DistributedLock.ProcessScoped/ProcessScopedNamedReaderWriterLock.IDistributedUpgradeableReaderWriterLock.cs new file mode 100644 index 00000000..a425ab3c --- /dev/null +++ b/DistributedLock.ProcessScoped/ProcessScopedNamedReaderWriterLock.IDistributedUpgradeableReaderWriterLock.cs @@ -0,0 +1,230 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Medallion.Threading.Internal; + +namespace Medallion.Threading +{ + public partial class ProcessScopedNamedReaderWriterLock + { + // 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 + /// A which can be used to release the lock or null on failure + public ProcessScopedNamedReaderWriterLockHandle? 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 + /// A which can be used to release the lock + public ProcessScopedNamedReaderWriterLockHandle 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 + /// A 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 + /// A 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 + /// A which can be used to release the lock or null on failure + public ProcessScopedNamedReaderWriterLockUpgradeableHandle? 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 + /// A which can be used to release the lock + public ProcessScopedNamedReaderWriterLockUpgradeableHandle 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 + /// A 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 + /// A 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 + /// A which can be used to release the lock or null on failure + public ProcessScopedNamedReaderWriterLockHandle? 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 + /// A which can be used to release the lock + public ProcessScopedNamedReaderWriterLockHandle 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 + /// A 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 + /// A 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/DistributedLock.ProcessScoped/ProcessScopedNamedReaderWriterLock.cs b/DistributedLock.ProcessScoped/ProcessScopedNamedReaderWriterLock.cs new file mode 100644 index 00000000..179e3bbd --- /dev/null +++ b/DistributedLock.ProcessScoped/ProcessScopedNamedReaderWriterLock.cs @@ -0,0 +1,85 @@ +using Medallion.Threading.Internal; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Medallion.Threading +{ + /// + /// An implementation of which is SCOPED TO JUST THE CURRENT PROCESS and therefore + /// is NOT TRULY DISTRIBUTED. Therefore, this implementation is intended primarily for testing or scenarios where + /// name-based locking is useful (e.g. when frequently creating and destroying fine-grained locks). + /// + public sealed partial class ProcessScopedNamedReaderWriterLock + : IInternalDistributedUpgradeableReaderWriterLock + { + private static readonly NamedObjectPool NamedObjectPool = new(static _ => new()); + + /// + /// Constructs a lock with + /// + public ProcessScopedNamedReaderWriterLock(string name) + { + this.Name = name ?? throw new ArgumentNullException(nameof(name)); + } + + /// + /// Implements + /// + public string Name { get; } + + async ValueTask IInternalDistributedReaderWriterLock.InternalTryAcquireAsync( + TimeoutValue timeout, + CancellationToken cancellationToken, + bool isWrite) + { + var result = await ( + isWrite ? this.TryAcquireAsync(static (l, t, c) => l.TryAcquireWriteLockAsync(t, c), timeout, cancellationToken) + : this.TryAcquireAsync(static (l, t, c) => l.TryAcquireReadLockAsync(t, c), timeout, cancellationToken) + ).ConfigureAwait(false); + return result is var (handle, lease) + ? new ProcessScopedNamedReaderWriterLockNonUpgradeableHandle(handle, lease) + : null; + } + + async ValueTask IInternalDistributedUpgradeableReaderWriterLock.InternalTryAcquireUpgradeableReadLockAsync( + TimeoutValue timeout, + CancellationToken cancellationToken) + { + var result = await this.TryAcquireAsync(static (l, t, c) => l.TryAcquireUpgradeableReadLockAsync(t, c), timeout, cancellationToken).ConfigureAwait(false); + return result is var (handle, lease) + ? new(handle, lease) + : null; + } + + private async ValueTask<(THandle Handle, IDisposable Lease)?> TryAcquireAsync( + Func> tryAcquireAsync, + TimeoutValue timeout, + CancellationToken cancellationToken) + where THandle : class + { + var acquired = false; + var lease = NamedObjectPool.LeaseObject(this.Name); + try + { + var handle = await tryAcquireAsync(lease.Value, timeout, cancellationToken).ConfigureAwait(false); + if (handle is null) + { + return null; + } + + acquired = true; + return (handle, lease); + } + finally + { + if (!acquired) + { + lease.Dispose(); + } + } + } + } +} diff --git a/DistributedLock.ProcessScoped/ProcessScopedNamedReaderWriterLockHandle.cs b/DistributedLock.ProcessScoped/ProcessScopedNamedReaderWriterLockHandle.cs new file mode 100644 index 00000000..fa4892ee --- /dev/null +++ b/DistributedLock.ProcessScoped/ProcessScopedNamedReaderWriterLockHandle.cs @@ -0,0 +1,114 @@ +using Medallion.Threading.Internal; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Medallion.Threading +{ + /// + /// Implements + /// + public abstract class ProcessScopedNamedReaderWriterLockHandle : IDistributedSynchronizationHandle + { + // forbid external inheritors + internal ProcessScopedNamedReaderWriterLockHandle() { } + + CancellationToken IDistributedSynchronizationHandle.HandleLostToken => this.IsDisposed ? throw this.ObjectDisposed() : CancellationToken.None; + + private protected abstract bool IsDisposed { get; } + + /// + /// Releases the lock + /// + public abstract void Dispose(); + + /// + /// Releases the lock + /// + public ValueTask DisposeAsync() + { + this.Dispose(); + return default; + } + } + + internal sealed class ProcessScopedNamedReaderWriterLockNonUpgradeableHandle : ProcessScopedNamedReaderWriterLockHandle + { + private HandleAndLease? _handleAndLease; + private IDisposable? _finalizerRegistration; + + public ProcessScopedNamedReaderWriterLockNonUpgradeableHandle(IDisposable handle, IDisposable lease) + { + this._handleAndLease = new(handle, lease); + this._finalizerRegistration = ManagedFinalizerQueue.Instance.Register(this, this._handleAndLease); + } + + private protected override bool IsDisposed => Volatile.Read(ref this._finalizerRegistration) is null; + + public override void Dispose() + { + Interlocked.Exchange(ref this._finalizerRegistration, null)?.Dispose(); + Interlocked.Exchange(ref this._handleAndLease, null)?.Dispose(); + } + } + + /// + /// Implements + /// + public sealed class ProcessScopedNamedReaderWriterLockUpgradeableHandle : ProcessScopedNamedReaderWriterLockHandle, IInternalDistributedLockUpgradeableHandle + { + private HandleAndLease? _handleAndLease; + private IDisposable? _finalizerRegistration; + + internal ProcessScopedNamedReaderWriterLockUpgradeableHandle(AsyncReaderWriterLock.UpgradeableHandle handle, IDisposable lease) + { + this._handleAndLease = new(handle, lease); + this._finalizerRegistration = ManagedFinalizerQueue.Instance.Register(this, this._handleAndLease); + } + + private protected override bool IsDisposed => Volatile.Read(ref this._finalizerRegistration) is null; + + /// + /// Releases the lock + /// + public override void Dispose() + { + Interlocked.Exchange(ref this._handleAndLease, null)?.Dispose(); // call this first because it can throw if upgrading + Interlocked.Exchange(ref this._finalizerRegistration, null)?.Dispose(); + } + + /// + /// 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) + { + // Note: we avoid locking here because we don't want to call TryUpgradeToWriteLockAsync inside the lock. + // If the handle is disposed out from under us, then it will properly throw ObjectDisposedException. + var handle = (Volatile.Read(ref this._handleAndLease) ?? throw this.ObjectDisposed()).LockHandle; + return handle.TryUpgradeToWriteLockAsync(timeout, cancellationToken); + } + } +} diff --git a/DistributedLock.ProcessScoped/ProcessScopedNamedSemaphore.IDistributedSemaphore.cs b/DistributedLock.ProcessScoped/ProcessScopedNamedSemaphore.IDistributedSemaphore.cs new file mode 100644 index 00000000..0ab4a3df --- /dev/null +++ b/DistributedLock.ProcessScoped/ProcessScopedNamedSemaphore.IDistributedSemaphore.cs @@ -0,0 +1,85 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Medallion.Threading.Internal; + +namespace Medallion.Threading +{ + public partial class ProcessScopedNamedSemaphore + { + // AUTO-GENERATED + + IDistributedSynchronizationHandle? IDistributedSemaphore.TryAcquire(TimeSpan timeout, CancellationToken cancellationToken) => + this.TryAcquire(timeout, cancellationToken); + IDistributedSynchronizationHandle IDistributedSemaphore.Acquire(TimeSpan? timeout, CancellationToken cancellationToken) => + this.Acquire(timeout, cancellationToken); + ValueTask IDistributedSemaphore.TryAcquireAsync(TimeSpan timeout, CancellationToken cancellationToken) => + this.TryAcquireAsync(timeout, cancellationToken).Convert(To.ValueTask); + ValueTask IDistributedSemaphore.AcquireAsync(TimeSpan? timeout, CancellationToken cancellationToken) => + this.AcquireAsync(timeout, cancellationToken).Convert(To.ValueTask); + + /// + /// Attempts to acquire a semaphore ticket synchronously. Usage: + /// + /// using (var handle = mySemaphore.TryAcquire(...)) + /// { + /// if (handle != null) { /* we have the ticket! */ } + /// } + /// // dispose releases the ticket 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 + /// A which can be used to release the ticket or null on failure + public ProcessScopedNamedSemaphoreHandle? TryAcquire(TimeSpan timeout = default, CancellationToken cancellationToken = default) => + DistributedLockHelpers.TryAcquire(this, timeout, cancellationToken); + + /// + /// Acquires a semaphore ticket synchronously, failing with if the attempt times out. Usage: + /// + /// using (mySemaphore.Acquire(...)) + /// { + /// /* we have the ticket! */ + /// } + /// // dispose releases the ticket + /// + /// + /// How long to wait before giving up on the acquisition attempt. Defaults to + /// Specifies a token by which the wait can be canceled + /// A which can be used to release the ticket + public ProcessScopedNamedSemaphoreHandle Acquire(TimeSpan? timeout = null, CancellationToken cancellationToken = default) => + DistributedLockHelpers.Acquire(this, timeout, cancellationToken); + + /// + /// Attempts to acquire a semaphore ticket asynchronously. Usage: + /// + /// await using (var handle = await mySemaphore.TryAcquireAsync(...)) + /// { + /// if (handle != null) { /* we have the ticket! */ } + /// } + /// // dispose releases the ticket 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 + /// A which can be used to release the ticket or null on failure + public ValueTask TryAcquireAsync(TimeSpan timeout = default, CancellationToken cancellationToken = default) => + this.As>().InternalTryAcquireAsync(timeout, cancellationToken); + + /// + /// Acquires a semaphore ticket asynchronously, failing with if the attempt times out. Usage: + /// + /// await using (await mySemaphore.AcquireAsync(...)) + /// { + /// /* we have the ticket! */ + /// } + /// // dispose releases the ticket + /// + /// + /// How long to wait before giving up on the acquisition attempt. Defaults to + /// Specifies a token by which the wait can be canceled + /// A which can be used to release the ticket + public ValueTask AcquireAsync(TimeSpan? timeout = null, CancellationToken cancellationToken = default) => + DistributedLockHelpers.AcquireAsync(this, timeout, cancellationToken); + } +} \ No newline at end of file diff --git a/DistributedLock.ProcessScoped/ProcessScopedNamedSemaphore.cs b/DistributedLock.ProcessScoped/ProcessScopedNamedSemaphore.cs new file mode 100644 index 00000000..c5fb5441 --- /dev/null +++ b/DistributedLock.ProcessScoped/ProcessScopedNamedSemaphore.cs @@ -0,0 +1,79 @@ +using Medallion.Threading.Internal; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Medallion.Threading +{ + /// + /// An implementation of which is SCOPED TO JUST THE CURRENT PROCESS and therefore + /// is NOT TRULY DISTRIBUTED. Therefore, this implementation is intended primarily for testing or scenarios where + /// name-based locking is useful (e.g. when frequently creating and destroying fine-grained locks). + /// + public sealed partial class ProcessScopedNamedSemaphore : IInternalDistributedSemaphore + { + private static readonly NamedObjectPool NamedObjectPool = new(_ => new()); + + /// + /// Constructs a semaphore with and . + /// + public ProcessScopedNamedSemaphore(string name, int maxCount) + { + if (maxCount < 1) { throw new ArgumentOutOfRangeException(nameof(maxCount), maxCount, "must be positive"); } + + this.Name = name ?? throw new ArgumentNullException(nameof(name)); + this.MaxCount = maxCount; + } + + /// + /// Implements + /// + public string Name { get; } + /// + /// Implements + /// + public int MaxCount { get; } + + async ValueTask IInternalDistributedSemaphore.InternalTryAcquireAsync( + TimeoutValue timeout, + CancellationToken cancellationToken) + { + var acquired = false; + var lease = NamedObjectPool.LeaseObject(this.Name); + try + { + var semaphore = lease.Value.GetSemaphore(this.MaxCount); + acquired = SyncViaAsync.IsSynchronous + ? semaphore.Wait(timeout.InMilliseconds, cancellationToken) + : await semaphore.WaitAsync(timeout.InMilliseconds, cancellationToken).ConfigureAwait(false); + + return acquired ? new(semaphore, lease) : null; + } + finally + { + if (!acquired) + { + lease.Dispose(); + } + } + } + + private sealed class SemaphoreBox + { + private SemaphoreSlim? _semaphore; + + public SemaphoreSlim GetSemaphore(int maxCount) + { + if (this._semaphore is { } semaphore) + { + return semaphore; + } + + SemaphoreSlim created = new(maxCount, maxCount); + return Interlocked.CompareExchange(ref this._semaphore, value: created, comparand: null) ?? created; + } + } + } +} diff --git a/DistributedLock.ProcessScoped/ProcessScopedNamedSemaphoreHandle.cs b/DistributedLock.ProcessScoped/ProcessScopedNamedSemaphoreHandle.cs new file mode 100644 index 00000000..83526b60 --- /dev/null +++ b/DistributedLock.ProcessScoped/ProcessScopedNamedSemaphoreHandle.cs @@ -0,0 +1,60 @@ +using Medallion.Threading.Internal; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Medallion.Threading +{ + /// + /// Implements + /// + public sealed class ProcessScopedNamedSemaphoreHandle : IDistributedSynchronizationHandle + { + private SemaphoreAndLease? _semaphoreAndLease; + private IDisposable? _finalizerRegistration; + + internal ProcessScopedNamedSemaphoreHandle(SemaphoreSlim semaphore, IDisposable lease) + { + this._semaphoreAndLease = new(semaphore, lease); + this._finalizerRegistration = ManagedFinalizerQueue.Instance.Register(this, this._semaphoreAndLease); + } + + CancellationToken IDistributedSynchronizationHandle.HandleLostToken => + this._semaphoreAndLease is null ? throw this.ObjectDisposed() : CancellationToken.None; + + /// + /// Releases the semaphore + /// + public void Dispose() + { + Interlocked.Exchange(ref this._semaphoreAndLease, null)?.Dispose(); + Interlocked.Exchange(ref this._finalizerRegistration, null)?.Dispose(); + } + + /// + /// Releases the semaphore + /// + public ValueTask DisposeAsync() + { + this.Dispose(); + return default; + } + + private sealed record SemaphoreAndLease(SemaphoreSlim Semaphore, IDisposable Lease) : IAsyncDisposable, IDisposable + { + public void Dispose() + { + this.Semaphore.Release(); + this.Lease.Dispose(); + } + + public ValueTask DisposeAsync() + { + this.Dispose(); + return default; + } + } + } +} diff --git a/DistributedLock.ProcessScoped/ProcessScopedNamedSynchronizationProvider.cs b/DistributedLock.ProcessScoped/ProcessScopedNamedSynchronizationProvider.cs new file mode 100644 index 00000000..39cccf18 --- /dev/null +++ b/DistributedLock.ProcessScoped/ProcessScopedNamedSynchronizationProvider.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Medallion.Threading +{ + /// + /// Implements for , + /// for , + /// and for . + /// + /// Note that these implementations are SCOPED TO JUST THE CURRENT PROCESS and therefore are NOT TRULY DISTRIBUTED. + /// Therefore, they is intended primarily for testing or scenarios where name-based locking is useful (e.g. when frequently + /// creating and destroying fine-grained locks). + /// + public sealed class ProcessScopedNamedSynchronizationProvider : IDistributedLockProvider, IDistributedUpgradeableReaderWriterLockProvider, IDistributedSemaphoreProvider + { + /// + /// Constructs a with the provided . + /// + public ProcessScopedNamedLock CreateLock(string name) => new(name); + + IDistributedLock IDistributedLockProvider.CreateLock(string name) => this.CreateLock(name); + + /// + /// Constructs a with the provided . + /// + public ProcessScopedNamedReaderWriterLock CreateReaderWriterLock(string name) => new(name); + + IDistributedReaderWriterLock IDistributedReaderWriterLockProvider.CreateReaderWriterLock(string name) => + this.CreateReaderWriterLock(name); + + IDistributedUpgradeableReaderWriterLock IDistributedUpgradeableReaderWriterLockProvider.CreateUpgradeableReaderWriterLock(string name) => + this.CreateReaderWriterLock(name); + + /// + /// Constructs a with the provided and . + /// + public ProcessScopedNamedSemaphore CreateSemaphore(string name, int maxCount) => new(name, maxCount); + + IDistributedSemaphore IDistributedSemaphoreProvider.CreateSemaphore(string name, int maxCount) => this.CreateSemaphore(name, maxCount); + } +} diff --git a/DistributedLock.ProcessScoped/TimeoutTracker.cs b/DistributedLock.ProcessScoped/TimeoutTracker.cs new file mode 100644 index 00000000..2ecbd41f --- /dev/null +++ b/DistributedLock.ProcessScoped/TimeoutTracker.cs @@ -0,0 +1,29 @@ +using Medallion.Threading.Internal; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Medallion.Threading +{ + /// + /// Tracks how much of a timeout value has elapsed. + /// + internal readonly struct TimeoutTracker + { + private readonly TimeoutValue _timeout; + private readonly int _initialTickCountMillis; + + public TimeoutTracker(TimeoutValue timeout) + { + this._timeout = timeout; + this._initialTickCountMillis = NeedsTracking(timeout) ? Environment.TickCount : 0; + } + + public TimeoutValue Remaining => + NeedsTracking(this._timeout) + ? new(TimeSpan.FromMilliseconds(Math.Max(this._timeout.InMilliseconds - (Environment.TickCount - this._initialTickCountMillis), 0))) + : this._timeout; + + private static bool NeedsTracking(TimeoutValue timeout) => !(timeout.IsInfinite || timeout.IsZero); + } +} diff --git a/DistributedLock.Tests/AbstractTestCases/DistributedLockCoreTestCases.cs b/DistributedLock.Tests/AbstractTestCases/DistributedLockCoreTestCases.cs index 05d28947..f19ad0df 100644 --- a/DistributedLock.Tests/AbstractTestCases/DistributedLockCoreTestCases.cs +++ b/DistributedLock.Tests/AbstractTestCases/DistributedLockCoreTestCases.cs @@ -395,6 +395,8 @@ public async Task TestLockAbandonment() [Test] public void TestCrossProcess() { + if (!this._lockProvider.Strategy.SupportsCrossProcess) { Assert.Pass(); } + var lockName = this._lockProvider.GetUniqueSafeName(); var command = this.RunLockTaker(this._lockProvider, this._lockProvider.GetCrossProcessLockType(), lockName); Assert.IsTrue(command.StandardOutput.ReadLineAsync().Wait(TimeSpan.FromSeconds(10))); @@ -426,6 +428,8 @@ public void TestCrossProcessAbandonmentWithKill() private void CrossProcessAbandonmentHelper(bool asyncWait, bool kill) { + if (!this._lockProvider.Strategy.SupportsCrossProcess) { Assert.Pass(); } + var name = this._lockProvider.GetUniqueSafeName($"cpl-{asyncWait}-{kill}"); var command = this.RunLockTaker(this._lockProvider, this._lockProvider.GetCrossProcessLockType(), name); Assert.IsTrue(command.StandardOutput.ReadLineAsync().Wait(TimeSpan.FromSeconds(10))); diff --git a/DistributedLock.Tests/Infrastructure/ProcessScoped/TestingProcessScopedProviders.cs b/DistributedLock.Tests/Infrastructure/ProcessScoped/TestingProcessScopedProviders.cs new file mode 100644 index 00000000..3427a69a --- /dev/null +++ b/DistributedLock.Tests/Infrastructure/ProcessScoped/TestingProcessScopedProviders.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Medallion.Threading.Tests.ProcessScoped +{ + [SupportsContinuousIntegration] + public sealed class TestingProcessScopedNamedLockProvider : TestingLockProvider + { + public override IDistributedLock CreateLockWithExactName(string name) => new ProcessScopedNamedLock(name); + + public override string GetSafeName(string name) => new ProcessScopedNamedLock(name).Name; + } + + [SupportsContinuousIntegration] + public sealed class TestingProcessScopedNamedSemaphoreProvider : TestingSemaphoreProvider + { + public override IDistributedSemaphore CreateSemaphoreWithExactName(string name, int maxCount) => new ProcessScopedNamedSemaphore(name, maxCount); + + public override string GetSafeName(string name) => new ProcessScopedNamedSemaphore(name, 1).Name; + } + + [SupportsContinuousIntegration] + public sealed class TestingProcessScopedNamedReaderWriterLockProvider : TestingUpgradeableReaderWriterLockProvider + { + public override IDistributedUpgradeableReaderWriterLock CreateUpgradeableReaderWriterLockWithExactName(string name) => + new ProcessScopedNamedReaderWriterLock(name); + + public override string GetSafeName(string name) => new ProcessScopedNamedReaderWriterLock(name).Name; + } +} diff --git a/DistributedLock.Tests/Infrastructure/ProcessScoped/TestingProcessScopedSynchronizationStrategy.cs b/DistributedLock.Tests/Infrastructure/ProcessScoped/TestingProcessScopedSynchronizationStrategy.cs new file mode 100644 index 00000000..45730881 --- /dev/null +++ b/DistributedLock.Tests/Infrastructure/ProcessScoped/TestingProcessScopedSynchronizationStrategy.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Medallion.Threading.Tests.ProcessScoped +{ + [SupportsContinuousIntegration] + public sealed class TestingProcessScopedSynchronizationStrategy : TestingSynchronizationStrategy + { + public override bool SupportsCrossProcess => false; // since we're scoped to a single process + } +} diff --git a/DistributedLock.Tests/Infrastructure/TestingReaderWriterLockAsMutexProvider.cs b/DistributedLock.Tests/Infrastructure/TestingReaderWriterLockAsMutexProvider.cs index 9abf9b05..bb755274 100644 --- a/DistributedLock.Tests/Infrastructure/TestingReaderWriterLockAsMutexProvider.cs +++ b/DistributedLock.Tests/Infrastructure/TestingReaderWriterLockAsMutexProvider.cs @@ -12,6 +12,7 @@ public interface ITestingReaderWriterLockAsMutexProvider public bool DisableUpgradeLock { get; set; } } + [SupportsContinuousIntegration] public sealed class TestingReaderWriterLockAsMutexProvider : TestingLockProvider, ITestingReaderWriterLockAsMutexProvider where TReaderWriterLockProvider : TestingReaderWriterLockProvider, new() where TStrategy : TestingSynchronizationStrategy, new() diff --git a/DistributedLock.Tests/Infrastructure/TestingSynchronizationStrategy.cs b/DistributedLock.Tests/Infrastructure/TestingSynchronizationStrategy.cs index 4b5a5a2d..ffe367ea 100644 --- a/DistributedLock.Tests/Infrastructure/TestingSynchronizationStrategy.cs +++ b/DistributedLock.Tests/Infrastructure/TestingSynchronizationStrategy.cs @@ -10,11 +10,13 @@ namespace Medallion.Threading.Tests /// public abstract class TestingSynchronizationStrategy : IDisposable { + public virtual bool SupportsCrossProcess => true; + /// /// Whether or not abandoning a ticket held in another process will cause that ticket /// to be released if tickets are still held elsewhere /// - public virtual bool SupportsCrossProcessSingleSemaphoreTicketAbandonment => true; + public virtual bool SupportsCrossProcessSingleSemaphoreTicketAbandonment => this.SupportsCrossProcess; public virtual void PrepareForHandleAbandonment() { } public virtual void PerformAdditionalCleanupForHandleAbandonment() { } diff --git a/DistributedLock.Tests/Tests/ApiTest.cs b/DistributedLock.Tests/Tests/ApiTest.cs index 9488c8a2..b908115e 100644 --- a/DistributedLock.Tests/Tests/ApiTest.cs +++ b/DistributedLock.Tests/Tests/ApiTest.cs @@ -22,7 +22,8 @@ public class ApiTest public void TestPublicNamespaces(AssemblyName assemblyName) { var expectedNamespace = assemblyName.Name!.Replace("DistributedLock", "Medallion.Threading") - .Replace(".Core", string.Empty); + .Replace(".Core", string.Empty) + .Replace(".ProcessScoped", string.Empty); foreach (var type in GetPublicTypes(Assembly.Load(assemblyName))) { type.Namespace.ShouldEqual(expectedNamespace, $"{type} in {assemblyName}"); @@ -68,7 +69,11 @@ public void TestProviderApisAreAvailable(AssemblyName assemblyName) foreach (var provider in providers) { - Assert.That(provider.Name, Does.EndWith("DistributedSynchronizationProvider")); + Assert.That( + provider.Name, + Does.EndWith("DistributedSynchronizationProvider") + .Or.EqualTo("ProcessScopedNamedSynchronizationProvider") + ); } } } diff --git a/DistributedLock.Tests/Tests/CombinatorialTests.cs b/DistributedLock.Tests/Tests/CombinatorialTests.cs index 5db0de62..235c7976 100644 --- a/DistributedLock.Tests/Tests/CombinatorialTests.cs +++ b/DistributedLock.Tests/Tests/CombinatorialTests.cs @@ -136,6 +136,17 @@ public class ReaderWriterCore_PostgresReaderWriter_OwnedConnectionSynchronizatio public class ReaderWriterCore_PostgresReaderWriter_OwnedTransactionSynchronizationStrategy_PostgresDb_OwnedTransactionSynchronizationStrategy_PostgresDbTest : DistributedReaderWriterLockCoreTestCases>, TestingOwnedTransactionSynchronizationStrategy> { } } +namespace Medallion.Threading.Tests.ProcessScoped +{ + [Category("CI")] public class Core_ProcessScoped_ProcessScopedSynchronizationStrategyTest : DistributedLockCoreTestCases { } + [Category("CI")] public class Core_ReaderWriterAsMutex_ProcessScopedReaderWriter_ProcessScopedSynchronizationStrategy_ProcessScopedSynchronizationStrategyTest : DistributedLockCoreTestCases, TestingProcessScopedSynchronizationStrategy> { } + [Category("CI")] public class Core_Semaphore1AsMutex_ProcessScopedSemaphore_ProcessScopedSynchronizationStrategy_ProcessScopedSynchronizationStrategyTest : DistributedLockCoreTestCases, TestingProcessScopedSynchronizationStrategy> { } + [Category("CI")] public class Core_Semaphore5AsMutex_ProcessScopedSemaphore_ProcessScopedSynchronizationStrategy_ProcessScopedSynchronizationStrategyTest : DistributedLockCoreTestCases, TestingProcessScopedSynchronizationStrategy> { } + [Category("CI")] public class ReaderWriterCore_ProcessScopedReaderWriter_ProcessScopedSynchronizationStrategyTest : DistributedReaderWriterLockCoreTestCases { } + [Category("CI")] public class SemaphoreCore_ProcessScopedSemaphore_ProcessScopedSynchronizationStrategyTest : DistributedSemaphoreCoreTestCases { } + [Category("CI")] public class UpgradeableReaderWriterCore_ProcessScopedReaderWriter_ProcessScopedSynchronizationStrategyTest : DistributedUpgradeableReaderWriterLockCoreTestCases { } +} + namespace Medallion.Threading.Tests.Redis { public class Core_ReaderWriterAsMutex_RedisReaderWriter_Redis2x1Database_RedisSynchronizationStrategy_Redis2x1Database_RedisSynchronizationStrategy_Redis2x1DatabaseTest : DistributedLockCoreTestCases, TestingRedisSynchronizationStrategy>, TestingRedisSynchronizationStrategy> { } diff --git a/DistributedLock.Tests/Tests/ProcessScoped/ProcessScopedNamedSynchronizationProviderTest.cs b/DistributedLock.Tests/Tests/ProcessScoped/ProcessScopedNamedSynchronizationProviderTest.cs new file mode 100644 index 00000000..53b72c3e --- /dev/null +++ b/DistributedLock.Tests/Tests/ProcessScoped/ProcessScopedNamedSynchronizationProviderTest.cs @@ -0,0 +1,21 @@ +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Medallion.Threading.Tests.ProcessScoped +{ + public class ProcessScopedNamedSynchronizationProviderTest + { + [Test] + public void TestCanCreatePrimitives() + { + ProcessScopedNamedSynchronizationProvider provider = new(); + provider.CreateLock("abc").Name.ShouldEqual("abc"); + provider.CreateReaderWriterLock("123").Name.ShouldEqual("123"); + Assert.IsTrue(provider.CreateSemaphore("x", 37) is { Name: "x", MaxCount: 37 }); + } + } +} diff --git a/DistributedLock.Tests/Tests/TestSetupTest.cs b/DistributedLock.Tests/Tests/TestSetupTest.cs index 47e06a3b..08ee15f8 100644 --- a/DistributedLock.Tests/Tests/TestSetupTest.cs +++ b/DistributedLock.Tests/Tests/TestSetupTest.cs @@ -92,8 +92,7 @@ static string GetCSharpName(Type type) } // remove words that are very common and therefore don't add much to the name - var testClassName = Regex.Replace(GetTestClassName(testClassType), "Distributed|Lock|Testing|TestCases", string.Empty) + "Test"; - + var testClassName = Regex.Replace(GetTestClassName(testClassType), "Distributed|Named|Lock|Testing|TestCases", string.Empty) + "Test"; var supportsContinuousIntegrationAttributes = TraverseDepthFirst(testClassType, t => t.GetGenericArguments()) .Where(t => t != testClassType) .Select(a => a.GetCustomAttribute()) diff --git a/DistributedLock.ZooKeeper/DistributedLock.ZooKeeper.csproj b/DistributedLock.ZooKeeper/DistributedLock.ZooKeeper.csproj index 67cbf653..25eddc28 100644 --- a/DistributedLock.ZooKeeper/DistributedLock.ZooKeeper.csproj +++ b/DistributedLock.ZooKeeper/DistributedLock.ZooKeeper.csproj @@ -49,7 +49,7 @@ all - + all diff --git a/DistributedLock.sln b/DistributedLock.sln index 699cbe67..a53bb07b 100644 --- a/DistributedLock.sln +++ b/DistributedLock.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29613.14 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32616.157 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DistributedLock", "DistributedLock\DistributedLock.csproj", "{C1F56B68-C2EE-48E5-A99B-B40D397AE34F}" EndProject @@ -35,7 +35,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DistributedLock.ZooKeeper", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DistributedLock.MySql", "DistributedLock.MySql\DistributedLock.MySql.csproj", "{6C13E55C-51A7-47CD-88A5-7C8564EBCB3C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DistributedLock.Oracle", "DistributedLock.Oracle\DistributedLock.Oracle.csproj", "{1CAB9A1D-0C02-459C-A90E-47819832BD58}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DistributedLock.Oracle", "DistributedLock.Oracle\DistributedLock.Oracle.csproj", "{1CAB9A1D-0C02-459C-A90E-47819832BD58}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DistributedLock.ProcessScoped", "DistributedLock.ProcessScoped\DistributedLock.ProcessScoped.csproj", "{78EFB915-5E25-41BA-AB82-FD227E61B1D6}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -99,6 +101,10 @@ Global {1CAB9A1D-0C02-459C-A90E-47819832BD58}.Debug|Any CPU.Build.0 = Debug|Any CPU {1CAB9A1D-0C02-459C-A90E-47819832BD58}.Release|Any CPU.ActiveCfg = Release|Any CPU {1CAB9A1D-0C02-459C-A90E-47819832BD58}.Release|Any CPU.Build.0 = Release|Any CPU + {78EFB915-5E25-41BA-AB82-FD227E61B1D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {78EFB915-5E25-41BA-AB82-FD227E61B1D6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {78EFB915-5E25-41BA-AB82-FD227E61B1D6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {78EFB915-5E25-41BA-AB82-FD227E61B1D6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/DistributedLock/DistributedLock.csproj b/DistributedLock/DistributedLock.csproj index 536c39fd..5692b16b 100644 --- a/DistributedLock/DistributedLock.csproj +++ b/DistributedLock/DistributedLock.csproj @@ -10,7 +10,7 @@ - 2.3.1 + 2.4.0 2.0.0.0 Michael Adelson Provides easy-to-use mutexes, reader-writer locks, and semaphores that can synchronize across processes and machines. This is an umbrella package that brings in the entire family of DistributedLock.* packages (e. g. DistributedLock.SqlServer) as references. Those packages can also be installed individually. @@ -54,6 +54,7 @@ + diff --git a/DistributedLockCodeGen/GenerateIDistributedLockImplementations.cs b/DistributedLockCodeGen/GenerateIDistributedLockImplementations.cs index 1b868375..2210db4d 100644 --- a/DistributedLockCodeGen/GenerateIDistributedLockImplementations.cs +++ b/DistributedLockCodeGen/GenerateIDistributedLockImplementations.cs @@ -16,7 +16,7 @@ public void GenerateForIDistributedLockAndSemaphore([Values("Lock", "Semaphore") { var files = CodeGenHelpers.EnumerateSolutionFiles() .Where(f => !f.Contains($"Distributed{name}.Core", StringComparison.OrdinalIgnoreCase)) - .Where(f => f.EndsWith($"Distributed{name}.cs", StringComparison.OrdinalIgnoreCase) && Path.GetFileName(f)[0] != 'I'); + .Where(f => Regex.IsMatch(f, $"(Distributed|Named){name}.cs$", RegexOptions.IgnoreCase) && Path.GetFileName(f)[0] != 'I'); var errors = new List(); foreach (var file in files) @@ -98,7 +98,7 @@ public void GenerateForIDistributedReaderWriterLock() { var files = CodeGenHelpers.EnumerateSolutionFiles() .Where(f => f.IndexOf("DistributedLock.Core", StringComparison.OrdinalIgnoreCase) < 0) - .Where(f => Regex.IsMatch(Path.GetFileName(f), @"Distributed.*?ReaderWriterLock\.cs$", RegexOptions.IgnoreCase)); + .Where(f => Regex.IsMatch(Path.GetFileName(f), @"(Distributed|Named).*?ReaderWriterLock\.cs$", RegexOptions.IgnoreCase)); var errors = new List(); foreach (var file in files) diff --git a/appveyor.yml b/appveyor.yml index b3cbaac9..f819209e 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,7 +1,7 @@ version: 1.0.{build} image: - - Visual Studio 2019 + - Visual Studio 2022 - Ubuntu build_script: