Skip to content

Commit

Permalink
Introduce AsyncLock, AsyncSemaphore, Disposable and AsyncDisposable
Browse files Browse the repository at this point in the history
  • Loading branch information
NimaAra committed Jun 11, 2023
1 parent 21468b4 commit 40073c8
Show file tree
Hide file tree
Showing 5 changed files with 212 additions and 0 deletions.
61 changes: 61 additions & 0 deletions Easy.Common.Tests.Unit/AsyncSemaphore/AsyncSemaphoreTests.cs
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));
}
}
}
29 changes: 29 additions & 0 deletions Easy.Common.Tests.Unit/Disposable/AsyncDisposableTests.cs
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();
}
}
25 changes: 25 additions & 0 deletions Easy.Common.Tests.Unit/Disposable/DisposableTests.cs
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();
}
}
47 changes: 47 additions & 0 deletions Easy.Common/AsyncLock.cs
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());
}
}
50 changes: 50 additions & 0 deletions Easy.Common/Disposable.cs
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;
}

0 comments on commit 40073c8

Please sign in to comment.