-
Notifications
You must be signed in to change notification settings - Fork 62
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Introduce AsyncLock, AsyncSemaphore, Disposable and AsyncDisposable
- Loading branch information
Showing
5 changed files
with
212 additions
and
0 deletions.
There are no files selected for viewing
61 changes: 61 additions & 0 deletions
61
Easy.Common.Tests.Unit/AsyncSemaphore/AsyncSemaphoreTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
namespace Easy.Common.Tests.Unit.AsyncSemaphore; | ||
|
||
using System; | ||
using System.Threading.Tasks; | ||
using NUnit.Framework; | ||
using Shouldly; | ||
using AsyncSemaphore = Easy.Common.AsyncSemaphore; | ||
|
||
[TestFixture] | ||
internal sealed class AsyncSemaphoreTests | ||
{ | ||
[Test] | ||
public async Task GivenAnAsyncSemaphore_WhenMultipleThreadsAttemptToAcquireLock_ThenOnlyThoseAllowedSucceed() | ||
{ | ||
TimeSpan timeoutDuration = TimeSpan.FromMilliseconds(50); | ||
|
||
AsyncSemaphore sem = new(2); | ||
|
||
Task worker1 = GetWorker(); | ||
Task worker2 = GetWorker(); | ||
Task worker3 = GetWorker(); | ||
|
||
TimeoutException ex = await Should.ThrowAsync<TimeoutException>(Task.WhenAll(worker1, worker2, worker3)); | ||
ex.Message.ShouldBe("The request to semaphore timed out after: " + timeoutDuration); | ||
|
||
worker1.Status.ShouldBe(TaskStatus.RanToCompletion); | ||
worker2.Status.ShouldBe(TaskStatus.RanToCompletion); | ||
worker3.Status.ShouldBe(TaskStatus.Faulted); | ||
|
||
async Task GetWorker() | ||
{ | ||
using IDisposable _ = await sem.Acquire(timeoutDuration); | ||
await Task.Delay(TimeSpan.FromMilliseconds(70)); | ||
} | ||
} | ||
|
||
[Test] | ||
public async Task GivenAnAsyncLock_WhenMultipleThreadsAttemptToAcquireLock_ThenOnlyOneSucceeds() | ||
{ | ||
TimeSpan timeoutDuration = TimeSpan.FromMilliseconds(50); | ||
|
||
AsyncLock locker = new(); | ||
|
||
Task worker1 = GetWorker(); | ||
Task worker2 = GetWorker(); | ||
Task worker3 = GetWorker(); | ||
|
||
TimeoutException ex = await Should.ThrowAsync<TimeoutException>(Task.WhenAll(worker1, worker2, worker3)); | ||
ex.Message.ShouldBe("The request to semaphore timed out after: " + timeoutDuration); | ||
|
||
worker1.Status.ShouldBe(TaskStatus.RanToCompletion); | ||
worker2.Status.ShouldBe(TaskStatus.Faulted); | ||
worker3.Status.ShouldBe(TaskStatus.Faulted); | ||
|
||
async Task GetWorker() | ||
{ | ||
using IDisposable _ = await locker.Acquire(timeoutDuration); | ||
await Task.Delay(TimeSpan.FromMilliseconds(70)); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
namespace Easy.Common.Tests.Unit.Disposable; | ||
|
||
using System; | ||
using System.Threading.Tasks; | ||
using NUnit.Framework; | ||
using Shouldly; | ||
|
||
[TestFixture] | ||
internal sealed class AsyncDisposableTests | ||
{ | ||
[Test] | ||
public async Task GivenADisposable_WhenDisposing_ThenHandlerShouldBeCalled() | ||
{ | ||
bool handled = false; | ||
Func<ValueTask> someHandler = () => | ||
{ | ||
handled = true; | ||
return ValueTask.CompletedTask; | ||
}; | ||
|
||
IAsyncDisposable sut = AsyncDisposable.Create(someHandler); | ||
|
||
handled.ShouldBeFalse(); | ||
|
||
await sut.DisposeAsync(); | ||
|
||
handled.ShouldBeTrue(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
namespace Easy.Common.Tests.Unit.Disposable; | ||
|
||
using System; | ||
using NUnit.Framework; | ||
using Shouldly; | ||
using Disposable = Easy.Common.Disposable; | ||
|
||
[TestFixture] | ||
internal sealed class DisposableTests | ||
{ | ||
[Test] | ||
public void GivenADisposable_WhenDisposing_ThenHandlerShouldBeCalled() | ||
{ | ||
bool handled = false; | ||
Action someHandler = () => handled = true; | ||
|
||
IDisposable sut = Disposable.Create(someHandler); | ||
|
||
handled.ShouldBeFalse(); | ||
|
||
sut.Dispose(); | ||
|
||
handled.ShouldBeTrue(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
namespace Easy.Common; | ||
|
||
using System; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
|
||
/// <summary> | ||
/// An abstraction for a lock which can be used in an asynchronous scenarios. | ||
/// </summary> | ||
public sealed class AsyncLock : AsyncSemaphore | ||
{ | ||
/// <summary> | ||
/// Creates an instance of <see cref="AsyncLock"/>. | ||
/// </summary> | ||
public AsyncLock() : base(maxConcurrency: 1) { } | ||
} | ||
|
||
/// <summary> | ||
/// An abstraction for a semaphore which can be used in asynchronous scenarios. | ||
/// </summary> | ||
public class AsyncSemaphore | ||
{ | ||
private readonly SemaphoreSlim _semaphore; | ||
|
||
/// <summary> | ||
/// Creates an instance of <see cref="AsyncSemaphore"/>. | ||
/// </summary> | ||
public AsyncSemaphore(int maxConcurrency = 1) => | ||
_semaphore = new(maxConcurrency, maxConcurrency); | ||
|
||
/// <summary> | ||
/// Attempts to acquire a lock within the given <paramref name="timeout"/> period; If succeeds an <see cref="IDisposable"/> is | ||
/// returned which on disposal releases the lock. | ||
/// </summary> | ||
/// <exception cref="TimeoutException">Thrown in case of a failure to acquire the lock.</exception> | ||
public async Task<IDisposable> Acquire(TimeSpan timeout) | ||
{ | ||
bool timedOut = !await _semaphore.WaitAsync(timeout).ConfigureAwait(false); | ||
|
||
if (timedOut) | ||
{ | ||
throw new TimeoutException("The request to semaphore timed out after: " + timeout); | ||
} | ||
|
||
return Disposable.Create(() => _semaphore.Release()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
namespace Easy.Common; | ||
|
||
using System; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
|
||
/// <summary> | ||
/// An abstraction representing an <see cref="IDisposable"/> object which executes an action on disposal. | ||
/// </summary> | ||
public sealed class Disposable : IDisposable | ||
{ | ||
/// <summary> | ||
/// Creates a disposable that invokes the specified <paramref name="onDispose"/> upon disposal. | ||
/// </summary> | ||
/// <param name="onDispose">The action to execute during <see cref="IDisposable.Dispose"/>.</param> | ||
/// <returns>An <see cref="IDisposable"/> which represents the scope.</returns> | ||
public static IDisposable Create(Action onDispose) => new Disposable(onDispose); | ||
|
||
private Action? _onDispose; | ||
|
||
private Disposable(Action onDispose) => Interlocked.Exchange(ref _onDispose, onDispose); | ||
|
||
/// <summary> | ||
/// Disposes the instance and executes the provided logic. | ||
/// </summary> | ||
public void Dispose() => Interlocked.Exchange(ref _onDispose, null)?.Invoke(); | ||
} | ||
|
||
/// <summary> | ||
/// An abstraction representing an <see cref="IAsyncDisposable"/> object which executes an action on disposal. | ||
/// </summary> | ||
public sealed class AsyncDisposable : IAsyncDisposable | ||
{ | ||
/// <summary> | ||
/// Creates a disposable that invokes the specified <paramref name="onDispose"/> asynchronously upon disposal. | ||
/// </summary> | ||
/// <param name="onDispose">The action to execute during <see cref="IAsyncDisposable.DisposeAsync"/>.</param> | ||
/// <returns>An <see cref="IAsyncDisposable"/> which represents the scope.</returns> | ||
public static IAsyncDisposable Create(Func<ValueTask> onDispose) => new AsyncDisposable(onDispose); | ||
|
||
private Func<ValueTask>? _onDispose; | ||
|
||
private AsyncDisposable(Func<ValueTask> onDispose) => Interlocked.Exchange(ref _onDispose, onDispose); | ||
|
||
/// <summary> | ||
/// Disposes the instance and executes the provided logic. | ||
/// </summary> | ||
public ValueTask DisposeAsync() => | ||
Interlocked.Exchange(ref _onDispose, null)?.Invoke() ?? ValueTask.CompletedTask; | ||
} |