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: