// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System.Diagnostics; using MQTTnet.Internal; namespace MQTTnet.Tests.Internal; // ReSharper disable InconsistentNaming [TestClass] public sealed class AsyncLock_Tests { [TestMethod] public async Task Cancellation_Of_Awaiter() { var @lock = new AsyncLock(); // This call will not yet "release" the lock due to missing _using_. var releaser = await @lock.EnterAsync(); var counter = 0; Debug.WriteLine("Prepared locked lock."); _ = Task.Run(async () => { // SHOULD GET TIMEOUT! using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(1)); using (await @lock.EnterAsync(timeout.Token)) { Debug.WriteLine("Task 1 incremented"); counter++; } }); _ = Task.Run(async () => { // SHOULD GET TIMEOUT! using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(2)); using (await @lock.EnterAsync(timeout.Token)) { Debug.WriteLine("Task 2 incremented"); counter++; } }); _ = Task.Run(async () => { // SHOULD GET TIMEOUT! using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(3)); using (await @lock.EnterAsync(timeout.Token)) { Debug.WriteLine("Task 3 incremented"); counter++; } }); _ = Task.Run(async () => { using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(4)); using (await @lock.EnterAsync(timeout.Token)) { Debug.WriteLine("Task 4 incremented"); counter++; } }); _ = Task.Run(async () => { using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); using (await @lock.EnterAsync(timeout.Token)) { Debug.WriteLine("Task 5 incremented"); counter++; } }); _ = Task.Run(async () => { using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(6)); using (await @lock.EnterAsync(timeout.Token)) { Debug.WriteLine("Task 6 incremented"); counter++; } }); Debug.WriteLine("Delay before release..."); await Task.Delay(TimeSpan.FromSeconds(3.1)); releaser.Dispose(); Debug.WriteLine("Wait for all tasks..."); await Task.Delay(TimeSpan.FromSeconds(6.1)); Assert.AreEqual(3, counter); } [TestMethod] public void Lock_10_Parallel_Tasks_With_Dispose_Doesnt_Lockup() { const int ThreadsCount = 10; var threads = new Task[ThreadsCount]; var @lock = new AsyncLock(); var globalI = 0; for (var i = 0; i < ThreadsCount; i++) { #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed threads[i] = Task.Run(async () => { using (await @lock.EnterAsync()) { var localI = globalI; await Task.Delay(10); // Increase the chance for wrong data. localI++; globalI = localI; } }) .ContinueWith(x => { if (globalI == 5) { @lock.Dispose(); @lock = new AsyncLock(); } if (x.Exception != null) { Debug.WriteLine(x.Exception.GetBaseException().GetType().Name); } }); #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed } Task.WaitAll(threads); // Expect only 6 because the others are failing due to disposal (if (globalI == 5)). Assert.AreEqual(6, globalI); } [TestMethod] public void Lock_Parallel_Tasks() { const int taskCount = 50; var @lock = new AsyncLock(); var tasks = new Task[taskCount]; var globalI = 0; for (var i = 0; i < taskCount; i++) { #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed tasks[i] = Task.Run(async () => { using (await @lock.EnterAsync()) { var localI = globalI; await Task.Delay(5); // Increase the chance for wrong data. localI++; globalI = localI; } }); #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed } Task.WaitAll(tasks); Assert.AreEqual(taskCount, globalI); } [TestMethod] public async Task Lock_Serial_Calls() { var sum = 0; var @lock = new AsyncLock(); for (var i = 0; i < 100; i++) { using (await @lock.EnterAsync()) { sum++; } } Assert.AreEqual(100, sum); } [TestMethod] public Task Test_Cancellation() { return Assert.ThrowsExactlyAsync(async () => { var @lock = new AsyncLock(); // This call will never "release" the lock due to missing _using_. await @lock.EnterAsync(); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3)); await @lock.EnterAsync(cts.Token); }); } [TestMethod] public async Task Test_Cancellation_With_Later_Access() { var asyncLock = new AsyncLock(); var releaser = await asyncLock.EnterAsync(); try { using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(3))) { await asyncLock.EnterAsync(timeout.Token); } Assert.Fail("Exception should be thrown!"); } catch (OperationCanceledException) { } releaser.Dispose(); using (await asyncLock.EnterAsync(CancellationToken.None)) { // When the method finished, the thread got access. } } [TestMethod] public async Task Use_After_Cancellation() { var @lock = new AsyncLock(); // This call will not yet "release" the lock due to missing _using_. var releaser = await @lock.EnterAsync(); try { using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3)); await @lock.EnterAsync(cts.Token); } catch (OperationCanceledException) { // Expected exception! } releaser.Dispose(); // Regular usage after cancellation. using (await @lock.EnterAsync()) { } using (await @lock.EnterAsync()) { } using (await @lock.EnterAsync()) { } } }