1
votes

I have a wrapper to the Task.Factory.StartNew, called TaskManager, that I needed to implement in order to add exception handling in the new thread. Here is my class:

public static class TaskManager
{
    public static Task StartNew(Action action, IUserContextBase userContext, Action<Task> onCompletedCallback = null)
    {
        return AddContinueWithToTask(Task.Factory.StartNew(action), userContext, null, onCompletedCallback);
    }

    public static Task StartNew(Action action, IUserContextBase userContext, CancellationToken cancellationToken, TaskCreationOptions taskCreationOptions, TaskScheduler taskScheduler, Action<Task> onCompletedCallback = null)
    {
        return AddContinueWithToTask(Task.Factory.StartNew(action, cancellationToken, taskCreationOptions, taskScheduler), userContext, null, onCompletedCallback);
    }

    public static Task StartNew(Action action, IUserContextBase userContext, TaskCreationOptions taskCreationOptions, Action<Task> onCompletedCallback = null)
    {
        return AddContinueWithToTask(Task.Factory.StartNew(action, taskCreationOptions), userContext, null, onCompletedCallback);
    }

    public static Task StartNew(Action<object> action, IUserContextBase userContext, object state, Action<Task> onCompletedCallback = null)
    {
        return AddContinueWithToTask(Task.Factory.StartNew(action, state), userContext, null, onCompletedCallback);
    }

    public static Task StartNew(Action<object> action, object state, CancellationToken cancellationToken, TaskCreationOptions taskCreationOptions, TaskScheduler taskScheduler, Action<Task> onCompletedCallback = null)
    {
        return AddContinueWithToTask(Task.Factory.StartNew(action, null, cancellationToken, taskCreationOptions, taskScheduler), null, null, onCompletedCallback);
    }

    public static Task StartNew(Action<object> action, object state, Action<System.Exception> onExceptionCallback = null, Action<Task> onCompletedCallback = null)
    {
        return AddContinueWithToTask(Task.Factory.StartNew(action, state), null, onExceptionCallback, onCompletedCallback);
    }

    public static Task StartNew(Action action, Action<System.Exception> onExceptionCallback = null)
    {
        return AddContinueWithToTask(Task.Factory.StartNew(action), null, onExceptionCallback);
    }

    public static Task<TResult> StartNew<TResult>(Func<TResult> function, IUserContextBase userContext, CancellationToken cancellationToken)
    {
        return Task.Factory.StartNew(function, cancellationToken).ContinueWith(t =>
        {
            ManageException(t, userContext);
            return t.Result;
        }, TaskContinuationOptions.OnlyOnFaulted);
    }

    private static void ManageException(Task t, IUserContextBase userContext, Action<System.Exception> onExceptionCallback = null)
    {
        if (t.Exception != null)
            if (onExceptionCallback != null)
                onExceptionCallback(t.Exception);
            else
                t.Exception.Handle(ex =>
                {
                    GlobalContainer.Unity.Resolve<IDiagnosticLogHandler>().LogError(userContext, ex, ErrorSeverity.Normal);
                    return true;
                });          
    }

    private static Task AddContinueWithToTask(Task task, IUserContextBase userContext, Action<System.Exception> onExceptionCallback = null, Action<Task> onCompletedCallback = null)
    {
        return task
            .ContinueWith(t => ManageException(t, userContext, onExceptionCallback), TaskContinuationOptions.OnlyOnFaulted)
            .ContinueWith(t2 => { if (onCompletedCallback != null) onCompletedCallback(t2); })
            .ContinueWith(t => ManageException(t, userContext, onExceptionCallback), TaskContinuationOptions.OnlyOnFaulted);
    }

However, whenever someone uses my class (it returns a Task) and then calls task.Wait() or task.WaitAll() afterwards, they seem to get an exception saying that the tasks have (all) been cancelled. The error occurs at the Wait() or WaitAll(). What could be wrong with my TaskManager class?

2
Why would you butcher the TPL like that? The whole point of using the TPL is to not require the caller to define the exception behavior or continuation behavior when starting task, instead allowing the operations to be added at any time through the Task. You're trapping all exceptions, preventing the users from actually using the Task to deal with them properly or having the appropriate continuation behavior. As for your error, without knowing how you're using these methods there's no way to reproduce the problem or see what the problem is. - Servy
@Servy If you believe that exceptions should be managed as a policy... Not saying that's a good thing. - Peter Ritchie
After applying Ian's suggestion, it solved my problem and it also prevents trapping the exceptions. We have a requirement that all exceptions be caught and managed, including those in Tasks. - Ray

2 Answers

2
votes

You're actually returning the continuation of the original task to the caller not the actual task.

Your continuation has TaskContinuationOptions.OnlyOnFaulted, which means run it only when antecedent is faulted, otherwise cancel it. And hence you see the caller gets TaskCancelledException.

In other words, When actual task Run to completion, Continuation will be cancelled. When actual task is faulted, Continuation will Run to completion given that TaskContinuationOptions.OnlyOnFaulted is used.

To fix it, you need to return the actual task to the caller but add the continuation and keep it with you.

Update: To return the original task, just use a local variable.

public static Task StartNew(Action action, Action<System.Exception> onExceptionCallback = null)
{
    var actualTask = Task.Factory.StartNew(action);
    AddContinueWithToTask(actualTask, null, onExceptionCallback);
    return actualTask;
}
...
0
votes

The problem is that you are chaining your t2 task onto the end of a task which never runs because it is set to OnlyOnFaulted.

Try this:-

        private static Task AddContinueWithToTask(Task task, object userContext, Action<System.Exception> onExceptionCallback = null, Action<Task> onCompletedCallback = null)
        {
            return task
                .ContinueWith(t => {
                    if (t.IsFaulted)
                        ManageException(t, userContext, onExceptionCallback);
                })
                .ContinueWith(t2 => 
                {
                    if (onCompletedCallback != null) onCompletedCallback(t2); 
                })
                .ContinueWith(t => {
                    if (t.IsFaulted)
                        ManageException(t, userContext, onExceptionCallback);
                })
                ;
        }

From the MSDN on Task.Continuewith

The returned Task will not be scheduled for execution until the current task has completed. If the criteria specified through the continuationOptions parameter are not met, the continuation task will be canceled instead of scheduled.