Skip to content

Instantly share code, notes, and snippets.

@liorksh
Created April 25, 2020 06:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save liorksh/a9ad687957411d58e2dae71ab3032d6a to your computer and use it in GitHub Desktop.
Save liorksh/a9ad687957411d58e2dae71ab3032d6a to your computer and use it in GitHub Desktop.
TaskCancellation example
/* 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