如果任务中发生异常,则根据用户输入多次重试任务

时间:2022-05-15 05:26:28

All the service calls in my application are implemented as tasks.When ever a task is faulted ,I need to present the user with a dialog box to retry the last operation failed.If the user chooses retry the program should retry the task ,else the execution of the program should continue after logging the exception.Any one has got a high level idea on how to implement this functionality ?

我的应用程序中的所有服务调用都是作为任务实现的。当任务出现故障时,我需要向用户显示一个对话框以重试上一次操作失败。如果用户选择重试,程序应该重试该任务,否则在记录异常后,程序的执行应该继续。任何人都对如何实现这个功能有了很高的想法?

3 个解决方案

#1


31  

UPDATE 5/2017

更新时间2017年5月

C# 6 exception filters make the catch clause a lot simpler :

C#6异常过滤器使catch子句更简单:

    private static async Task<T> Retry<T>(Func<T> func, int retryCount)
    {
        while (true)
        {
            try
            {
                var result = await Task.Run(func);
                return result;
            }
            catch when (retryCount-- > 0){}
        }
    }

and a recursive version:

和递归版本:

    private static async Task<T> Retry<T>(Func<T> func, int retryCount)
    {
        try
        {
            var result = await Task.Run(func);
            return result;
        }
        catch when (retryCount-- > 0){}
        return await Retry(func, retryCount);
    }

ORIGINAL

原版的

There are many ways to code a Retry function: you can use recursion or task iteration. There was a discussion in the Greek .NET User group a while back on the different ways to do exactly this.
If you are using F# you can also use Async constructs. Unfortunately, you can't use the async/await constructs at least in the Async CTP, because the code generated by the compiler doesn't like multiple awaits or possible rethrows in catch blocks.

编写重试函数的方法有很多种:您可以使用递归或任务迭代。希腊.NET用户小组曾讨论过不同的方法。如果您使用F#,您也可以使用Async构造。不幸的是,至少在Async CTP中你不能使用async / await结构,因为编译器生成的代码不喜欢catch块中的多个等待或可能的重新抛出。

The recursive version is perhaps the simplest way to build a Retry in C#. The following version doesn't use Unwrap and adds an optional delay before retries :

递归版本可能是在C#中构建重试的最简单方法。以下版本不使用Unwrap并在重试之前添加可选延迟:

private static Task<T> Retry<T>(Func<T> func, int retryCount, int delay, TaskCompletionSource<T> tcs = null)
    {
        if (tcs == null)
            tcs = new TaskCompletionSource<T>();
        Task.Factory.StartNew(func).ContinueWith(_original =>
        {
            if (_original.IsFaulted)
            {
                if (retryCount == 0)
                    tcs.SetException(_original.Exception.InnerExceptions);
                else
                    Task.Factory.StartNewDelayed(delay).ContinueWith(t =>
                    {
                        Retry(func, retryCount - 1, delay,tcs);
                    });
            }
            else
                tcs.SetResult(_original.Result);
        });
        return tcs.Task;
    } 

The StartNewDelayed function comes from the ParallelExtensionsExtras samples and uses a timer to trigger a TaskCompletionSource when the timeout occurs.

StartNewDelayed函数来自ParallelExtensionsExtras示例,并使用计时器在超时发生时触发TaskCompletionSource。

The F# version is a lot simpler:

F#版本更简单:

let retry (asyncComputation : Async<'T>) (retryCount : int) : Async<'T> = 
let rec retry' retryCount = 
    async {
        try
            let! result = asyncComputation  
            return result
        with exn ->
            if retryCount = 0 then
                return raise exn
            else
                return! retry' (retryCount - 1)
    }
retry' retryCount

Unfortunatley, it isn't possible to write something similar in C# using async/await from the Async CTP because the compiler doesn't like await statements inside a catch block. The following attempt also fails silenty, because the runtime doesn't like encountering an await after an exception:

不幸的是,不可能使用Async CTP中的async / await在C#中编写类似的东西,因为编译器不喜欢catch块中的await语句。以下尝试也会使静默失败,因为运行时不喜欢在异常后遇到等待:

private static async Task<T> Retry<T>(Func<T> func, int retryCount)
    {
        while (true)
        {
            try
            {
                var result = await TaskEx.Run(func);
                return result;
            }
            catch 
            {
                if (retryCount == 0)
                    throw;
                retryCount--;
            }
        }
    }

As for asking the user, you can modify Retry to call a function that asks the user and returns a task through a TaskCompletionSource to trigger the next step when the user answers, eg:

至于询问用户,您可以修改重试以调用询问用户的函数,并通过TaskCompletionSource返回任务,以在用户回答时触发下一步,例如:

 private static Task<bool> AskUser()
    {
        var tcs = new TaskCompletionSource<bool>();
        Task.Factory.StartNew(() =>
        {
            Console.WriteLine(@"Error Occured, continue? Y\N");
            var response = Console.ReadKey();
            tcs.SetResult(response.KeyChar=='y');

        });
        return tcs.Task;
    }

    private static Task<T> RetryAsk<T>(Func<T> func, int retryCount,  TaskCompletionSource<T> tcs = null)
    {
        if (tcs == null)
            tcs = new TaskCompletionSource<T>();
        Task.Factory.StartNew(func).ContinueWith(_original =>
        {
            if (_original.IsFaulted)
            {
                if (retryCount == 0)
                    tcs.SetException(_original.Exception.InnerExceptions);
                else
                    AskUser().ContinueWith(t =>
                    {
                        if (t.Result)
                            RetryAsk(func, retryCount - 1, tcs);
                    });
            }
            else
                tcs.SetResult(_original.Result);
        });
        return tcs.Task;
    } 

With all the continuations, you can see why an async version of Retry is so desirable.

通过所有延续,您可以了解为什么异步版本的Retry如此理想。

UPDATE:

更新:

In Visual Studio 2012 Beta the following two versions work:

在Visual Studio 2012 Beta中,以下两个版本可以工作:

A version with a while loop:

带有while循环的版本:

    private static async Task<T> Retry<T>(Func<T> func, int retryCount)
    {
        while (true)
        {
            try
            {
                var result = await Task.Run(func);
                return result;
            }
            catch
            {
                if (retryCount == 0)
                    throw;
                retryCount--;
            }
        }
    }

and a recursive version:

和递归版本:

    private static async Task<T> Retry<T>(Func<T> func, int retryCount)
    {
        try
        {
            var result = await Task.Run(func);
            return result;
        }
        catch
        {
            if (retryCount == 0)
                throw;
        }
        return await Retry(func, --retryCount);
    }

#2


4  

Here's a riffed version of Panagiotis Kanavos's excellent answer which I've tested and am using in production.

这是Panagiotis Kanavos的优秀答案,我已经测试过,并且正在制作中使用。

It addresses some things that were important to me:

它解决了一些对我很重要的事情:

  • Want to be able to decide whether to retry based on number of preceding attempts and exception from current attempt
  • 希望能够根据先前尝试的次数和当前尝试的异常来决定是否重试
  • Don't want to rely on async (less environment constraints)
  • 不想依赖异步(减少环境约束)
  • Want to have the resulting Exception in the case of failure include details from each attempt
  • 希望在失败的情况下产生的异常包括每次尝试的详细信息


static Task<T> RetryWhile<T>(
    Func<int, Task<T>> func, 
    Func<Exception, int, bool> shouldRetry )
{
    return RetryWhile<T>( func, shouldRetry, new TaskCompletionSource<T>(), 0, Enumerable.Empty<Exception>() );
}

static Task<T> RetryWhile<T>( 
    Func<int, Task<T>> func, 
    Func<Exception, int, bool> shouldRetry, 
    TaskCompletionSource<T> tcs, 
    int previousAttempts, IEnumerable<Exception> previousExceptions )
{
    func( previousAttempts ).ContinueWith( antecedent =>
    {
        if ( antecedent.IsFaulted )
        {
            var antecedentException = antecedent.Exception;
            var allSoFar = previousExceptions
                .Concat( antecedentException.Flatten().InnerExceptions );
            if ( shouldRetry( antecedentException, previousAttempts ) )
                RetryWhile( func,shouldRetry,previousAttempts+1, tcs, allSoFar);
            else
                tcs.SetException( allLoggedExceptions );
        }
        else
            tcs.SetResult( antecedent.Result );
    }, TaskContinuationOptions.ExecuteSynchronously );
    return tcs.Task;
}

#3


2  

When at the high level, I find it helps to make a function signature from what you have and what you want.

在高级别时,我发现根据您拥有的内容和您想要的内容制作功能签名会很有帮助。

You have:

你有:

  • A function that gives you a task (Func<Task>). We'll use the function because tasks themselves are not retryable in general.
  • 一个为您提供任务的函数(Func )。我们将使用该函数,因为任务本身通常不可重试。
  • A function that determines if the overall task is completed or should be retried (Func<Task, bool>)
  • 确定整个任务是完成还是应该重试的函数(Func ,bool>

You want:

你要:

  • An overall Task
  • 整体任务

So you'll have a function like:

所以你将拥有如下功能:

Task Retry(Func<Task> action, Func<Task, bool> shouldRetry);

Extending the practice inside the function, tasks pretty much have 2 operations to do with them, read their state and ContinueWith. To make your own tasks, TaskCompletionSource is a good starting point. A first try might look something like:

扩展函数内部的实践,任务几乎有2个操作与它们相关,读取它们的状态和ContinueWith。要完成自己的任务,TaskCompletionSource是一个很好的起点。第一次尝试可能看起来像:

//error checking
var result = new TaskCompletionSource<object>();
action().ContinueWith((t) => 
  {
    if (shouldRetry(t))
        action();
    else
    {
        if (t.IsFaulted)
            result.TrySetException(t.Exception);
        //and similar for Canceled and RunToCompletion
    }
  });

The obvious problem here is that only 1 retry will ever happen. To get around that, you need to make a way for the function to call itself. The usual way to do this with lambdas is something like this:

这里显而易见的问题是只会发生1次重试。要解决这个问题,你需要为函数调用自己。使用lambdas执行此操作的常用方法是这样的:

//error checking
var result = new TaskCompletionSource<object>();

Func<Task, Task> retryRec = null; //declare, then assign
retryRec = (t) => { if (shouldRetry(t))
                        return action().ContinueWith(retryRec).Unwrap();
                    else
                    {
                        if (t.IsFaulted) 
                            result.TrySetException(t.Exception);
                        //and so on
                        return result.Task; //need to return something
                     }
                  };
 action().ContinueWith(retryRec);
 return result.Task;

#1


31  

UPDATE 5/2017

更新时间2017年5月

C# 6 exception filters make the catch clause a lot simpler :

C#6异常过滤器使catch子句更简单:

    private static async Task<T> Retry<T>(Func<T> func, int retryCount)
    {
        while (true)
        {
            try
            {
                var result = await Task.Run(func);
                return result;
            }
            catch when (retryCount-- > 0){}
        }
    }

and a recursive version:

和递归版本:

    private static async Task<T> Retry<T>(Func<T> func, int retryCount)
    {
        try
        {
            var result = await Task.Run(func);
            return result;
        }
        catch when (retryCount-- > 0){}
        return await Retry(func, retryCount);
    }

ORIGINAL

原版的

There are many ways to code a Retry function: you can use recursion or task iteration. There was a discussion in the Greek .NET User group a while back on the different ways to do exactly this.
If you are using F# you can also use Async constructs. Unfortunately, you can't use the async/await constructs at least in the Async CTP, because the code generated by the compiler doesn't like multiple awaits or possible rethrows in catch blocks.

编写重试函数的方法有很多种:您可以使用递归或任务迭代。希腊.NET用户小组曾讨论过不同的方法。如果您使用F#,您也可以使用Async构造。不幸的是,至少在Async CTP中你不能使用async / await结构,因为编译器生成的代码不喜欢catch块中的多个等待或可能的重新抛出。

The recursive version is perhaps the simplest way to build a Retry in C#. The following version doesn't use Unwrap and adds an optional delay before retries :

递归版本可能是在C#中构建重试的最简单方法。以下版本不使用Unwrap并在重试之前添加可选延迟:

private static Task<T> Retry<T>(Func<T> func, int retryCount, int delay, TaskCompletionSource<T> tcs = null)
    {
        if (tcs == null)
            tcs = new TaskCompletionSource<T>();
        Task.Factory.StartNew(func).ContinueWith(_original =>
        {
            if (_original.IsFaulted)
            {
                if (retryCount == 0)
                    tcs.SetException(_original.Exception.InnerExceptions);
                else
                    Task.Factory.StartNewDelayed(delay).ContinueWith(t =>
                    {
                        Retry(func, retryCount - 1, delay,tcs);
                    });
            }
            else
                tcs.SetResult(_original.Result);
        });
        return tcs.Task;
    } 

The StartNewDelayed function comes from the ParallelExtensionsExtras samples and uses a timer to trigger a TaskCompletionSource when the timeout occurs.

StartNewDelayed函数来自ParallelExtensionsExtras示例,并使用计时器在超时发生时触发TaskCompletionSource。

The F# version is a lot simpler:

F#版本更简单:

let retry (asyncComputation : Async<'T>) (retryCount : int) : Async<'T> = 
let rec retry' retryCount = 
    async {
        try
            let! result = asyncComputation  
            return result
        with exn ->
            if retryCount = 0 then
                return raise exn
            else
                return! retry' (retryCount - 1)
    }
retry' retryCount

Unfortunatley, it isn't possible to write something similar in C# using async/await from the Async CTP because the compiler doesn't like await statements inside a catch block. The following attempt also fails silenty, because the runtime doesn't like encountering an await after an exception:

不幸的是,不可能使用Async CTP中的async / await在C#中编写类似的东西,因为编译器不喜欢catch块中的await语句。以下尝试也会使静默失败,因为运行时不喜欢在异常后遇到等待:

private static async Task<T> Retry<T>(Func<T> func, int retryCount)
    {
        while (true)
        {
            try
            {
                var result = await TaskEx.Run(func);
                return result;
            }
            catch 
            {
                if (retryCount == 0)
                    throw;
                retryCount--;
            }
        }
    }

As for asking the user, you can modify Retry to call a function that asks the user and returns a task through a TaskCompletionSource to trigger the next step when the user answers, eg:

至于询问用户,您可以修改重试以调用询问用户的函数,并通过TaskCompletionSource返回任务,以在用户回答时触发下一步,例如:

 private static Task<bool> AskUser()
    {
        var tcs = new TaskCompletionSource<bool>();
        Task.Factory.StartNew(() =>
        {
            Console.WriteLine(@"Error Occured, continue? Y\N");
            var response = Console.ReadKey();
            tcs.SetResult(response.KeyChar=='y');

        });
        return tcs.Task;
    }

    private static Task<T> RetryAsk<T>(Func<T> func, int retryCount,  TaskCompletionSource<T> tcs = null)
    {
        if (tcs == null)
            tcs = new TaskCompletionSource<T>();
        Task.Factory.StartNew(func).ContinueWith(_original =>
        {
            if (_original.IsFaulted)
            {
                if (retryCount == 0)
                    tcs.SetException(_original.Exception.InnerExceptions);
                else
                    AskUser().ContinueWith(t =>
                    {
                        if (t.Result)
                            RetryAsk(func, retryCount - 1, tcs);
                    });
            }
            else
                tcs.SetResult(_original.Result);
        });
        return tcs.Task;
    } 

With all the continuations, you can see why an async version of Retry is so desirable.

通过所有延续,您可以了解为什么异步版本的Retry如此理想。

UPDATE:

更新:

In Visual Studio 2012 Beta the following two versions work:

在Visual Studio 2012 Beta中,以下两个版本可以工作:

A version with a while loop:

带有while循环的版本:

    private static async Task<T> Retry<T>(Func<T> func, int retryCount)
    {
        while (true)
        {
            try
            {
                var result = await Task.Run(func);
                return result;
            }
            catch
            {
                if (retryCount == 0)
                    throw;
                retryCount--;
            }
        }
    }

and a recursive version:

和递归版本:

    private static async Task<T> Retry<T>(Func<T> func, int retryCount)
    {
        try
        {
            var result = await Task.Run(func);
            return result;
        }
        catch
        {
            if (retryCount == 0)
                throw;
        }
        return await Retry(func, --retryCount);
    }

#2


4  

Here's a riffed version of Panagiotis Kanavos's excellent answer which I've tested and am using in production.

这是Panagiotis Kanavos的优秀答案,我已经测试过,并且正在制作中使用。

It addresses some things that were important to me:

它解决了一些对我很重要的事情:

  • Want to be able to decide whether to retry based on number of preceding attempts and exception from current attempt
  • 希望能够根据先前尝试的次数和当前尝试的异常来决定是否重试
  • Don't want to rely on async (less environment constraints)
  • 不想依赖异步(减少环境约束)
  • Want to have the resulting Exception in the case of failure include details from each attempt
  • 希望在失败的情况下产生的异常包括每次尝试的详细信息


static Task<T> RetryWhile<T>(
    Func<int, Task<T>> func, 
    Func<Exception, int, bool> shouldRetry )
{
    return RetryWhile<T>( func, shouldRetry, new TaskCompletionSource<T>(), 0, Enumerable.Empty<Exception>() );
}

static Task<T> RetryWhile<T>( 
    Func<int, Task<T>> func, 
    Func<Exception, int, bool> shouldRetry, 
    TaskCompletionSource<T> tcs, 
    int previousAttempts, IEnumerable<Exception> previousExceptions )
{
    func( previousAttempts ).ContinueWith( antecedent =>
    {
        if ( antecedent.IsFaulted )
        {
            var antecedentException = antecedent.Exception;
            var allSoFar = previousExceptions
                .Concat( antecedentException.Flatten().InnerExceptions );
            if ( shouldRetry( antecedentException, previousAttempts ) )
                RetryWhile( func,shouldRetry,previousAttempts+1, tcs, allSoFar);
            else
                tcs.SetException( allLoggedExceptions );
        }
        else
            tcs.SetResult( antecedent.Result );
    }, TaskContinuationOptions.ExecuteSynchronously );
    return tcs.Task;
}

#3


2  

When at the high level, I find it helps to make a function signature from what you have and what you want.

在高级别时,我发现根据您拥有的内容和您想要的内容制作功能签名会很有帮助。

You have:

你有:

  • A function that gives you a task (Func<Task>). We'll use the function because tasks themselves are not retryable in general.
  • 一个为您提供任务的函数(Func )。我们将使用该函数,因为任务本身通常不可重试。
  • A function that determines if the overall task is completed or should be retried (Func<Task, bool>)
  • 确定整个任务是完成还是应该重试的函数(Func ,bool>

You want:

你要:

  • An overall Task
  • 整体任务

So you'll have a function like:

所以你将拥有如下功能:

Task Retry(Func<Task> action, Func<Task, bool> shouldRetry);

Extending the practice inside the function, tasks pretty much have 2 operations to do with them, read their state and ContinueWith. To make your own tasks, TaskCompletionSource is a good starting point. A first try might look something like:

扩展函数内部的实践,任务几乎有2个操作与它们相关,读取它们的状态和ContinueWith。要完成自己的任务,TaskCompletionSource是一个很好的起点。第一次尝试可能看起来像:

//error checking
var result = new TaskCompletionSource<object>();
action().ContinueWith((t) => 
  {
    if (shouldRetry(t))
        action();
    else
    {
        if (t.IsFaulted)
            result.TrySetException(t.Exception);
        //and similar for Canceled and RunToCompletion
    }
  });

The obvious problem here is that only 1 retry will ever happen. To get around that, you need to make a way for the function to call itself. The usual way to do this with lambdas is something like this:

这里显而易见的问题是只会发生1次重试。要解决这个问题,你需要为函数调用自己。使用lambdas执行此操作的常用方法是这样的:

//error checking
var result = new TaskCompletionSource<object>();

Func<Task, Task> retryRec = null; //declare, then assign
retryRec = (t) => { if (shouldRetry(t))
                        return action().ContinueWith(retryRec).Unwrap();
                    else
                    {
                        if (t.IsFaulted) 
                            result.TrySetException(t.Exception);
                        //and so on
                        return result.Task; //need to return something
                     }
                  };
 action().ContinueWith(retryRec);
 return result.Task;