Skip to content

Commit

Permalink
Allowing actionable options, bug fixes with regards to timeouts, bett…
Browse files Browse the repository at this point in the history
…er error handling, minor optimizations, improved documentation, added considerably more tests.
  • Loading branch information
MarkCiliaVincenti committed Dec 17, 2022
1 parent f89c81d commit edc6f00
Show file tree
Hide file tree
Showing 6 changed files with 215 additions and 4 deletions.
32 changes: 32 additions & 0 deletions AsyncKeyedLock.Tests/OriginalTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,38 @@ namespace AsyncKeyedLock.Tests
{
public class OriginalTests
{
[Fact]
public async Task TestTimeout()
{
var asyncKeyedLocker = new AsyncKeyedLocker<string>();
using (var myFirstLock = await asyncKeyedLocker.LockAsync("test"))
{
using (var myLock = await asyncKeyedLocker.LockAsync("test", 0))
{
var myTimeoutReleaser = (AsyncKeyedLockTimeoutReleaser<string>)myLock;
Assert.False(myTimeoutReleaser.EnteredSemaphore);
}
Assert.True(asyncKeyedLocker.IsInUse("test"));
}
Assert.False(asyncKeyedLocker.IsInUse("test"));
}

[Fact]
public async Task TestTimeoutWithTimeSpan()
{
var asyncKeyedLocker = new AsyncKeyedLocker<string>();
using (var myFirstLock = await asyncKeyedLocker.LockAsync("test"))
{
using (var myLock = await asyncKeyedLocker.LockAsync("test", TimeSpan.Zero))
{
var myTimeoutReleaser = (AsyncKeyedLockTimeoutReleaser<string>)myLock;
Assert.False(myTimeoutReleaser.EnteredSemaphore);
}
Assert.True(asyncKeyedLocker.IsInUse("test"));
}
Assert.False(asyncKeyedLocker.IsInUse("test"));
}

[Fact]
public async Task BasicTest()
{
Expand Down
6 changes: 3 additions & 3 deletions AsyncKeyedLock/AsyncKeyedLock.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@
<PackageProjectUrl>https://github.com/MarkCiliaVincenti/AsyncKeyedLock</PackageProjectUrl>
<Copyright>MIT</Copyright>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<Version>6.0.0</Version>
<Version>6.0.1</Version>
<PackageIcon>logo.png</PackageIcon>
<PackageReleaseNotes>Allowing actionable options, bug fixes with regards to timeouts, better error handling, minor optimizations, improved documentation, added considerably more tests.</PackageReleaseNotes>
<Description>An asynchronous .NET Standard 2.0 library that allows you to lock based on a key (keyed semaphores), limiting concurrent threads sharing the same key to a specified number.</Description>
<Copyright>© 2022 Mark Cilia Vincenti</Copyright>
<PackageTags>async,lock,key,keyed,semaphore,dictionary,pooling,duplicate,synchronization</PackageTags>
<RepositoryType>git</RepositoryType>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
<AssemblyVersion>6.0.0.0</AssemblyVersion>
<FileVersion>6.0.0.0</FileVersion>
<AssemblyVersion>6.0.1.0</AssemblyVersion>
<FileVersion>6.0.1.0</FileVersion>
<PackageReadmeFile>README.md</PackageReadmeFile>
<IsPackable>true</IsPackable>
<IsTrimmable>true</IsTrimmable>
Expand Down
12 changes: 12 additions & 0 deletions AsyncKeyedLock/AsyncKeyedLockReleaser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,5 +66,17 @@ public void Dispose()
{
_dictionary.Release(this);
}

internal void Dispose(bool enteredSemaphore)
{
if (enteredSemaphore)
{
_dictionary.Release(this);
}
else
{
_dictionary.ReleaseWithoutSemaphoreRelease(this);
}
}
}
}
33 changes: 33 additions & 0 deletions AsyncKeyedLock/AsyncKeyedLockTimeoutReleaser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System;
using System.Runtime.CompilerServices;
using System.Threading;

namespace AsyncKeyedLock
{
/// <summary>
/// Represents an <see cref="IDisposable"/> for AsyncKeyedLock with timeouts.
/// </summary>
public sealed class AsyncKeyedLockTimeoutReleaser<TKey> : IDisposable
{
/// <summary>
/// True if the timeout was reached, false if not.
/// </summary>
public bool EnteredSemaphore { get; internal set; }
internal readonly AsyncKeyedLockReleaser<TKey> _releaser;

internal AsyncKeyedLockTimeoutReleaser(bool enteredSemaphore, AsyncKeyedLockReleaser<TKey> releaser)
{
EnteredSemaphore = enteredSemaphore;
_releaser = releaser;
}

/// <summary>
/// Releases the <see cref="SemaphoreSlim"/> object once, depending on whether or not the semaphore was entered.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Dispose()
{
_releaser.Dispose(EnteredSemaphore);
}
}
}
132 changes: 132 additions & 0 deletions AsyncKeyedLock/AsyncKeyedLocker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,72 @@ public IDisposable Lock(TKey key, CancellationToken cancellationToken)
}
return releaser;
}

/// <summary>
/// Synchronously lock based on a key, setting a limit for the number of milliseconds to wait.
/// </summary>
/// <param name="key">The key to lock on.</param>
/// <param name="millisecondsTimeout">The number of milliseconds to wait, <see cref="Timeout.Infinite"/> (-1) to wait indefinitely, or zero to test the state of the wait handle and return immediately.</param>
/// <returns>A disposable value.</returns>
public IDisposable Lock(TKey key, int millisecondsTimeout)
{
var releaser = GetOrAdd(key);
return new AsyncKeyedLockTimeoutReleaser<TKey>(releaser.SemaphoreSlim.Wait(millisecondsTimeout), releaser);
}

/// <summary>
/// Synchronously lock based on a key, setting a limit for the <see cref="TimeSpan"/> to wait.
/// </summary>
/// <param name="key">The key to lock on.</param>
/// <param name="timeout">A <see cref="TimeSpan"/> that represents the number of milliseconds to wait, a <see cref="TimeSpan"/> that represents -1 milliseconds to wait indefinitely, or a <see cref="TimeSpan"/> that represents 0 milliseconds to test the wait handle and return immediately.</param>
/// <returns>A disposable value.</returns>
public IDisposable Lock(TKey key, TimeSpan timeout)
{
var releaser = GetOrAdd(key);
return new AsyncKeyedLockTimeoutReleaser<TKey>(releaser.SemaphoreSlim.Wait(timeout), releaser);
}

/// <summary>
/// Synchronously lock based on a key, setting a limit for the number of milliseconds to wait, while observing a <see cref="CancellationToken"/>.
/// </summary>
/// <param name="key">The key to lock on.</param>
/// <param name="millisecondsTimeout">The number of milliseconds to wait, <see cref="Timeout.Infinite"/> (-1) to wait indefinitely, or zero to test the state of the wait handle and return immediately.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
/// <returns>A disposable value.</returns>
public IDisposable Lock(TKey key, int millisecondsTimeout, CancellationToken cancellationToken)
{
var releaser = GetOrAdd(key);
try
{
return new AsyncKeyedLockTimeoutReleaser<TKey>(releaser.SemaphoreSlim.Wait(millisecondsTimeout, cancellationToken), releaser);
}
catch (OperationCanceledException)
{
Release(releaser);
throw;
}
}

/// <summary>
/// Synchronously lock based on a key, setting a limit for the <see cref="System.TimeSpan"/> to wait, while observing a <see cref="CancellationToken"/>.
/// </summary>
/// <param name="key">The key to lock on.</param>
/// <param name="timeout">A <see cref="TimeSpan"/> that represents the number of milliseconds to wait, a <see cref="TimeSpan"/> that represents -1 milliseconds to wait indefinitely, or a <see cref="TimeSpan"/> that represents 0 milliseconds to test the wait handle and return immediately.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
/// <returns>A disposable value.</returns>
public IDisposable Lock(TKey key, TimeSpan timeout, CancellationToken cancellationToken)
{
var releaser = GetOrAdd(key);
try
{
return new AsyncKeyedLockTimeoutReleaser<TKey>(releaser.SemaphoreSlim.Wait(timeout, cancellationToken), releaser);
}
catch (OperationCanceledException)
{
Release(releaser);
throw;
}
}
#endregion Synchronous

#region SynchronousTry
Expand Down Expand Up @@ -779,6 +845,72 @@ public async ValueTask<IDisposable> LockAsync(TKey key, CancellationToken cancel
}
return releaser;
}

/// <summary>
/// Asynchronously lock based on a key, setting a limit for the number of milliseconds to wait.
/// </summary>
/// <param name="key">The key to lock on.</param>
/// <param name="millisecondsTimeout">The number of milliseconds to wait, <see cref="Timeout.Infinite"/> (-1) to wait indefinitely, or zero to test the state of the wait handle and return immediately.</param>
/// <returns>A disposable value.</returns>
public async ValueTask<IDisposable> LockAsync(TKey key, int millisecondsTimeout)
{
var releaser = GetOrAdd(key);
return new AsyncKeyedLockTimeoutReleaser<TKey>(await releaser.SemaphoreSlim.WaitAsync(millisecondsTimeout).ConfigureAwait(false), releaser);
}

/// <summary>
/// Asynchronously lock based on a key, setting a limit for the <see cref="TimeSpan"/> to wait.
/// </summary>
/// <param name="key">The key to lock on.</param>
/// <param name="timeout">A <see cref="TimeSpan"/> that represents the number of milliseconds to wait, a <see cref="TimeSpan"/> that represents -1 milliseconds to wait indefinitely, or a <see cref="TimeSpan"/> that represents 0 milliseconds to test the wait handle and return immediately.</param>
/// <returns>A disposable value.</returns>
public async ValueTask<IDisposable> LockAsync(TKey key, TimeSpan timeout)
{
var releaser = GetOrAdd(key);
return new AsyncKeyedLockTimeoutReleaser<TKey>(await releaser.SemaphoreSlim.WaitAsync(timeout).ConfigureAwait(false), releaser);
}

/// <summary>
/// Asynchronously lock based on a key, setting a limit for the number of milliseconds to wait, while observing a <see cref="CancellationToken"/>.
/// </summary>
/// <param name="key">The key to lock on.</param>
/// <param name="millisecondsTimeout">The number of milliseconds to wait, <see cref="Timeout.Infinite"/> (-1) to wait indefinitely, or zero to test the state of the wait handle and return immediately.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
/// <returns>A disposable value.</returns>
public async ValueTask<IDisposable> LockAsync(TKey key, int millisecondsTimeout, CancellationToken cancellationToken)
{
var releaser = GetOrAdd(key);
try
{
return new AsyncKeyedLockTimeoutReleaser<TKey>(await releaser.SemaphoreSlim.WaitAsync(millisecondsTimeout, cancellationToken).ConfigureAwait(false), releaser);
}
catch (OperationCanceledException)
{
Release(releaser);
throw;
}
}

/// <summary>
/// Asynchronously lock based on a key, setting a limit for the <see cref="System.TimeSpan"/> to wait, while observing a <see cref="CancellationToken"/>.
/// </summary>
/// <param name="key">The key to lock on.</param>
/// <param name="timeout">A <see cref="TimeSpan"/> that represents the number of milliseconds to wait, a <see cref="TimeSpan"/> that represents -1 milliseconds to wait indefinitely, or a <see cref="TimeSpan"/> that represents 0 milliseconds to test the wait handle and return immediately.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
/// <returns>A disposable value.</returns>
public async ValueTask<IDisposable> LockAsync(TKey key, TimeSpan timeout, CancellationToken cancellationToken)
{
var releaser = GetOrAdd(key);
try
{
return new AsyncKeyedLockTimeoutReleaser<TKey>(await releaser.SemaphoreSlim.WaitAsync(timeout, cancellationToken).ConfigureAwait(false), releaser);
}
catch (OperationCanceledException)
{
Release(releaser);
throw;
}
}
#endregion

/// <summary>
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,9 @@ using (var lockObj = await asyncKeyedLocker.LockAsync(myObject, cancellationToke
}
```

In the case you need to use timeouts, you can also use `TryLockAsync` methods which will call a `Func<Task>` or `Action` if the timeout is not expired, whilst returning a boolean representing whether or not it waited successfully.
You can also use timeouts with overloaded methods to set the maximum time to wait, either in milliseconds or as a `TimeSpan`.

In the case you need to use timeouts to instead give up if unable to obtain a lock by a certain amount of time, you can also use `TryLockAsync` methods which will call a `Func<Task>` or `Action` if the timeout is not expired, whilst returning a boolean representing whether or not it waited successfully.

There are also synchronous `Lock` and `TryLock` methods available.

Expand Down

0 comments on commit edc6f00

Please sign in to comment.