简体   繁体   中英

C#/Tasks Error being logged twice with Task.Wait after wait timeout

Error being thrown twice with Task.Wait after wait timeout

This problem seems to stem from the fact that I'm using Task.Wait for a timeout. The issue is that either the exception thrown after the task timeout is logged twice or an error thrown before the timeout isn't logged. I've added the code and test I used to try to better understand the scenario.

The idea behind this test is that we are forcing the timeout to occur (at 2 seconds) BEFORE the exception is thrown (at 3 seconds). What happens with the exception in this situation? Below is the result. The “boom” exception is never reported. It remains as an unobserved exception in the Task.

         [MassUpdateEngine.cs]
        // Package the collection of statements that need to be run as a Task.
        // The Task can then be given a cancellation token and a timeout.
        Task task = Task.Run(async () =>
        {
            try
            {
                Thread.Sleep(3000);
                throw new Exception("boom");

                // Checking whether the task was cancelled at each step in the task gives us finer grained control over when we should bail out.
                token.ThrowIfCancellationRequested();
                Guid id = SubmitPreview();
                results.JobId = id;

                token.ThrowIfCancellationRequested();
                bool previewStatus = await GetPreviewStatus(id, token);
                Logger.Log("Preview status: " + previewStatus);

                token.ThrowIfCancellationRequested();
                ExecuteUpdate(id);

                token.ThrowIfCancellationRequested();
                bool updateStatus = await GetUpdateStatus(id, token);
                Logger.Log("Update status: " + updateStatus);

                token.ThrowIfCancellationRequested();
                string value = GetUpdateResults(id);
                results.NewValue = value;
            }
            // It appears that awaited methods will throw exceptions on when cancelled.
            catch (OperationCanceledException)
            {
                Logger.Log("***An operation was cancelled.***");
            }
        }, token);

        task.ContinueWith(antecedent =>
        {
            //Logger.Log(antecedent.Exception.ToString());
            throw new CustomException();
        }, TaskContinuationOptions.OnlyOnFaulted);



         [Program.cs]
        try
        {
            MassUpdateEngine engine = new MassUpdateEngine();

            // This call simulates calling the MassUpdate.Execute method that will handle preview + update all in one "synchronous" call.
            //Results results = engine.Execute();

            // This call simulates calling the MassUpdate.Execute method that will handle preview + update all in one "synchronous" call along with a timeout value.
            // Note: PreviewProcessor and UpdateProcessor both sleep for 3 seconds each.  The timeout needs to be > 6 seconds for the call to complete successfully.
            int timeout = 2000;
            Results results = engine.Execute(timeout);

            Logger.Log("Results: " + results.NewValue);
        }
        catch (TimeoutException ex)
        {
            Logger.Log("***Timeout occurred.***");
        }
        catch (AggregateException ex)
        {
            Logger.Log("***Aggregate exception occurred.***\n" + ex.ToString());
        }
        catch (CustomException ex)
        {
            Logger.Log("A custom exception was caught and handled.\n" + ex.ToString());
        }

Because the exception isn't ever observed and therefore not appropriately logged this won't work.

This information leads to the following rules:

  1. Don't throw from .ContinueWith. Exceptions thrown from here aren't marshaled back to the calling thread. These exceptions remain as unobserved exceptions and are effectively eaten.
  2. An exception from a Task may or may not be marshalled back to the calling thread when using Wait with a timeout. If the exception occurs BEFORE the timeout for the Wait call, then the exception gets marshalled back to the calling thread. If the exception occurs AFTER the timeout for the Wait call, then the exception remains as an unobserved exception of the task.

Rule #2 is pretty ugly. How do we reliably log exceptions in this scenario? We could use the .ContinueWith / OnlyOnFaulted to log the exception (see below).

        task.ContinueWith(antecedent =>
        {
            Logger.Log(antecedent.Exception.ToString());
            //throw new CustomException();
        }, TaskContinuationOptions.OnlyOnFaulted);

However, if the exception occurs BEFORE the timeout for the Wait call, then the exception will get marshalled back to the calling thread and handled by the global unhandled exception handler (and logged) AND will then be passed to the .ContinueWith task (and logged), resulting in two error log entries for the same exception.

There must be something that I'm missing here. Any help would be appreciated.

Don't throw from .ContinueWith. Exceptions thrown from here aren't marshaled back to the calling thread. These exceptions remain as unobserved exceptions and are effectively eaten.

The cause of the unobserved exception is because the task is not observed (not because it was created using ContinueWith ). The task returned from ContinueWith is just being ignored.

I'd say the more appropriate advice is to not use ContinueWith at all (see my blog for more details). Once you change to use await , the answer becomes clearer:

public static async Task LogExceptions(Func<Task> func)
{
  try
  {
    await func();
  }
  catch (Exception ex)
  {
    Logger.Log(ex.ToString());
  }
}

public static async Task<T> LogExceptions<T>(Func<Task<T>> func)
{
  try
  {
    return await func();
  }
  catch (Exception ex)
  {
    Logger.Log(ex.ToString());
  }
}

The async / await pattern more clearly illustrates what's really going on here: the code is creating a wrapper task that must be used as a replacement for the original task:

Task task = LogExceptions(() => Task.Run(() => ...

Now the wrapper task will always observe any exception of its inner task.

An exception from a Task may or may not be marshalled back to the calling thread when using Wait with a timeout. If the exception occurs BEFORE the timeout for the Wait call, then the exception gets marshalled back to the calling thread. If the exception occurs AFTER the timeout for the Wait call, then the exception remains as an unobserved exception of the task.

I'd think of this in terms of where the exception is instead of "marshalling" or "calling threads". That just confuses the issue. When a task faults, the exception is placed on the task (and the task is completed). So if the exception is placed on the task before the wait completes, then the wait is satisfied by the task completing, and raises the exception. If the wait times out before the task completes, then the wait is no longer waiting, so of course it doesn't see the exception, and the exception can possibly be unobserved.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM