-
-
Save liorksh/a9ad687957411d58e2dae71ab3032d6a to your computer and use it in GitHub Desktop.
TaskCancellation example
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
/* See below the definitions of the test scenarios*/ | |
[Theory] | |
[MemberData(nameof(CancellationTestScenarios))] | |
public void TasksCancellation_Test(int booksAllowance, int booksToWithdraw, string scenarioDescription, TestScenario testScenario, TaskStatus expectedStatus) | |
{ | |
string testParameters = $@"Initial books allowance: { booksAllowance}, withdraw: { booksToWithdraw} | |
test scenario: { scenarioDescription}, expected result: { expectedStatus}"; | |
Exception caughtException = null; | |
Logger.Log($@">>> Starting a new test --> {testParameters}"); | |
LibraryAccount libraryAccount = new LibraryAccount(booksAllowance); | |
using (CancellationTokenSource source = new CancellationTokenSource()) | |
{ | |
CancellationToken cancellationToken = source.Token; | |
cancellationToken.Register(OnCancellationOccurred, null); | |
// Start the task after 5 seconds delay | |
Task taskWithdrawBooks = Task.Delay(5000).ContinueWith(_ => | |
{ | |
// if the thread wasn't canceled, the execution will continue normally. | |
cancellationToken.ThrowIfCancellationRequested(); | |
while (libraryAccount.IsWithdrawAllowed(booksToWithdraw)) | |
{ | |
//Trace.WriteLine($"Working: {DateTime.Now.ToString("dd/MM/yyyy hh:mm:ssss")}"); | |
libraryAccount.WithdrawBooks(booksToWithdraw); | |
//Trace.WriteLine($"Current books inventory count {libraryAccount.BorrowedBooksCount}"); | |
// check if a cancellation request was made. | |
if (cancellationToken.IsCancellationRequested) | |
{ | |
Logger.Log("Received a cancellation request"); | |
// Throws OperationCancelException and stops the thread; | |
// thus, better to clean here all the thread's resources before the throwing the exception. | |
if ((testScenario & TestScenario.ThrowErrorAfterCancellation) != 0) | |
{ | |
cancellationToken.ThrowIfCancellationRequested(); | |
} | |
// Can throw other exception too, however the thread's state won't be set to Cancel, but Faulted. | |
if ((testScenario & TestScenario.ThrowGenerarExceptionAfterCancellation) != 0) | |
{ | |
throw new InvalidDataException("a random exception"); | |
} | |
} | |
Thread.Sleep(1000); | |
} | |
}, cancellationToken); | |
// Another task to print the status of the Withdraw books task. | |
Task printTaskStatus = Task.Factory.StartNew(() => | |
{ | |
TaskStatus prev = taskWithdrawBooks.Status; | |
Logger.Log($"+++ The task status: {taskWithdrawBooks.Status}"); | |
while (taskWithdrawBooks.Status != TaskStatus.RanToCompletion) | |
{ | |
if (taskWithdrawBooks.Status != prev) | |
{ | |
prev = taskWithdrawBooks.Status; | |
Logger.Log($"+++ The task status has changed to: {taskWithdrawBooks.Status}"); | |
} | |
} | |
}); | |
// Changing the token's status to Cancel. | |
//Task.Delay(10000).ContinueWith(_=> { source.Cancel(false); }); | |
source.CancelAfter(15000); | |
try | |
{ | |
// This method returns if the task was canceled (calling Cancel method) or the thread has ended (gracefully or not). | |
if ((testScenario & TestScenario.WaitWithCancellationToken) != 0) | |
taskWithdrawBooks.Wait(cancellationToken); | |
// By calling Wait method without the CancellationToken, the Wait method won't return even if the thread was canceled. | |
// Although the thread was canceled, it will continue to run until completion. | |
// The method returns if the thread ended gracefully or not. | |
if ((testScenario & TestScenario.WaitWithoutCancellationToken) != 0) | |
taskWithdrawBooks.Wait(); | |
} | |
catch (AggregateException e) | |
{ | |
caughtException = e; | |
Logger.Log($"Exception #1: {nameof(e)} was thrown; error message: {e.Message}"); | |
} | |
catch (OperationCanceledException e) | |
{ | |
caughtException = e; | |
Logger.Log($"Exception #2: {nameof(OperationCanceledException)} was thrown; error message: {e.Message}"); | |
} | |
// Wait for ensuring the thread's state is set. | |
Thread.Sleep(20000); | |
Logger.Log($@">>> Ending test --> {testParameters}"); | |
// The status of the task will be set to Cancel only is a OperationCanceledException is thrown. | |
// Otherwise, the task status will remain Running or Faulted (if an other exception was thrown) | |
Assert.Equal(expectedStatus, taskWithdrawBooks.Status); | |
if ((testScenario & TestScenario.WaitWithCancellationToken) != 0) | |
{ | |
// When calling a Wait method with the cancellation token, the exception will be caught regardless if the exception is thrown in the thread. | |
Assert.IsType<OperationCanceledException>(caughtException); | |
} | |
else if (((testScenario & TestScenario.WaitWithoutCancellationToken) != 0) && | |
((testScenario & TestScenario.ThrowGenerarExceptionAfterCancellation) != 0 || | |
((testScenario & TestScenario.ThrowErrorAfterCancellation) != 0))) | |
{ | |
Assert.IsType<AggregateException>(caughtException); | |
} | |
else | |
{ | |
// If no Wait method was called, even if an exception is thrown, it will not be caught; the main thread will continue without waiting for any returned status. | |
Assert.Null(caughtException); | |
} | |
} | |
// A local method for demonstration purposes. | |
void OnCancellationOccurred(object state) | |
{ | |
Logger.Log($"The task was canceled, current books balance: {libraryAccount.BorrowedBooksCount}"); | |
} | |
} | |
// Define an Enum to facilitate the various test scenarios | |
public enum TestScenario | |
{ | |
ThrowErrorAfterCancellation = 1, | |
ThrowGenerarExceptionAfterCancellation = 2, | |
WaitWithCancellationToken = 4, | |
WaitWithoutCancellationToken = 8 | |
} | |
// Define test scenarios to be passed into the test class | |
public static IEnumerable<object[]> CancellationTestScenarios() | |
{ | |
// The structure of each test scenario is: | |
// (initial number of items), (number of items to reduce), (Scenario description), (TestScenario code), (Expected result) | |
yield return new object[] { 1550, 34, "Not calling wait, throw a cancellation exception", TestScenario.ThrowErrorAfterCancellation, TaskStatus.Canceled }; | |
yield return new object[] { 1550, 34, "Not calling wait, throw a general exception", TestScenario.ThrowGenerarExceptionAfterCancellation, TaskStatus.Faulted }; | |
yield return new object[] { 1550, 34, "Wait with cancellation, without any exception", TestScenario.WaitWithCancellationToken, TaskStatus.Running }; | |
yield return new object[] { 1550, 34, "Wait with cancellation, throw a cancellation exception", TestScenario.WaitWithCancellationToken | TestScenario.ThrowErrorAfterCancellation, TaskStatus.Canceled }; | |
yield return new object[] { 1550, 34, "Wait with cancellation, throw a general exception", TestScenario.WaitWithCancellationToken | TestScenario.ThrowGenerarExceptionAfterCancellation, TaskStatus.Faulted }; | |
yield return new object[] { 200, 34, "Wait without cancellation, without any exception", TestScenario.WaitWithoutCancellationToken, TaskStatus.RanToCompletion }; | |
yield return new object[] { 1550, 34, "Wait without cancellation, throw a cancellation exception", TestScenario.WaitWithoutCancellationToken | TestScenario.ThrowErrorAfterCancellation, TaskStatus.Canceled }; | |
yield return new object[] { 1550, 34, "Wait without cancellation, throw a general exception", TestScenario.WaitWithoutCancellationToken | TestScenario.ThrowGenerarExceptionAfterCancellation, TaskStatus.Faulted }; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment