C#Polly async-await:在重试之前等待用户确认

C#Polly async-await:在重试之前等待用户确认

问题描述:

我正在为iOS和Android创建Xamarin.Forms应用程序,在其中我将数据和本地sqlite数据库保存在Azure服务器中并在线保存.尽管我的应用程序需要Internet连接,并且始终使用Connectivity插件进行检查,但我发现,如果用户在接收中途请求时丢失了小区接收信号,有时会抛出异常.

I'm creating a Xamarin.Forms app for iOS and Android where I am keeping data and a local sqlite database and online in an Azure server. Although my app requires an internet connection which it is always checking with the Connectivity plugin, I've found that I will sometimes have an exception thrown if a user loses cell reception mid request.

我想要一个可以调用我所有服务器请求的方法,如果发生错误,该方法将重试该请求.我还希望能够在重试之前要求用户输入.流程如下所示:

I want to have a method I can call all my server requests to that will retry the request if an error occurs. I would also like the ability to ask the user for input before retrying. The flow would look like this:

呼叫服务器->捕获到异常->询问用户是否要重试->重试

Call to server --> Exception Caught --> Ask user if they want to retry --> Retry

我找到了 Polly 程序包,该程序包可以处理C#中的try/catch重试.我目前的代码设置如下:

I found the Polly package which is setup to handle try/catch retries in C#. I currently have my code setup like this:

public class WebExceptionCatcher<T, R> where T : Task<R>
{      
    public async Task<R> runTask(Func<T> myTask)
    {
        Policy p = Policy.Handle<WebException>()
        .Or<MobileServiceInvalidOperationException>()
        .Or<HttpRequestException>()
        .RetryForeverAsync(onRetryAsync: async (e,i) => await RefreshAuthorization());

        return await p.ExecuteAsync<R>(myTask);
    }
}

我的RefreshAuthorization()方法仅在主线程的当前页面上显示DisplayAlert:

My RefreshAuthorization() method simply displays a DisplayAlert on the current page on the main thread:

private async Task RefreshAuthorization()
{
    bool loop = true;
    Device.BeginInvokeOnMainThread(async () =>
    {
        await DisplayAlert("Connection Lost", "Please re-connect to the internet and try again", "Retry");
        loop = false;
    });

    while (loop)
    {
        await Task.Delay(100); 
    }
}

当我调试它并切断我的互联网连接时. DisplayAlert从不显示.发生两件事之一:

When I debug this and cut my internet connection. The DisplayAlert is never shown. One of two things happens:

  1. 执行继续不停地反复调用我的任务
  2. 抛出System.AggregateException并显示以下消息:
  1. The execution continues to call my task over and over without completing
  2. A System.AggregateException is thrown with the following message:

System.AggregateException: A Task's exception(s) were not observed either by Waiting on the Task or accessing its Exception property. As a result, the unobserved exception was rethrown by the finalizer thread. ---> System.Net.Http.HttpRequestException: An error occurred while sending the request

有人知道如何在任务失败时成功暂停执行并等待用户恢复吗?

Does anyone know how to successfully pause execution when a Task fails, and wait for the user to resume?

更新:

在对Device.BeginInvokeOnMainThread方法中的DisplayAlert进行调用之后,我现在找到了解决AggregateException的方法.但是,现在我还有另一个问题.

After putting the call to DisplayAlert inside of the Device.BeginInvokeOnMainThread method, I now have found a way around the AggregateException. However, now I have another problem.

一旦我断开与Internet的连接,DisplayAlert就会弹出,如预期的那样.该程序在完成onRetry功能之前等待我单击重试,因此RetryForeverAsync等待正在正常工作.问题是,如果我重新连接到互联网然后点击重试,它将一次又一次失败.因此,即使我已连接到Internet,也陷入了被要求重新连接的无限循环中.看来RetryForeverAsync只是在抛出旧的异常.

Once I disconnect from the internet, the DisplayAlert pops up like it is supposed to. The program waits for me to click retry before finishing the onRetry function so the RetryForeverAsync waiting is working correctly. The issue is that if I reconnect to the internet and then hit retry, it fails again, and again, and again. So even though I'm connected to the internet, I'm stuck in an infinite loop of being asked to reconnect. It seems that RetryForeverAsync is simply re-throwing the old exception.

这是我给runTask()打电话的方式:

Here is how I'm calling runTask():

Task<TodoItem> t = App.MobileService.GetTable<TodoItem>().LookupAsync(id);
WebExceptionCatcher<Task<TodoItem>, TodoItem> catcher = new WebExceptionCatcher<Task<TodoItem>, TodoItem>();

然后,我尝试了两种不同的调用runTask的方法,两种方法都具有在重新建立连接时重试失败的相同结果:

I've then tried two different ways of calling runTask, both with the same result of failing the retry when connection is re-established:

TodoItem item = await catcher.runTask(() => t);

或:

TodoItem item = await catcher.runTask(async () => await t);

您需要使用.RetryForeverAsync(...)作为注释者.然后,由于您的重试委托是异步的,因此您还需要使用onRetryAsync:.因此:

You need to use .RetryForeverAsync(...) as a commentor noted. Then, since your on-retry delegate is async, you also need to use onRetryAsync:. Thus:

.RetryForeverAsync(onRetryAsync: async (e,i) => await RefreshAuthorization());

要解释所看到的错误:在问题的代码示例中,通过使用onRetry:,您指定要使用同步的onRetry委托(返回void),然后将异步委托分配给它.

To explain the errors you saw: In the code sample in the question, by using onRetry:, you are specifying that you want to use a synchronous onRetry delegate (returning void), but then assigning an async delegate to it.

这将导致异步委托分配给同步参数成为async void;调用代码不能/不能等待.由于不等待async void委托,因此确实会不断地重试您执行的委托.

That causes the async-delegate-assigned-to-sync-param to become async void; calling code doesn't/can't wait for that. Since the async void delegate isn't waited for, your executed delegate would indeed be continuously retried.

System.AggregateException: A Task's exception(s) were not observed可能是由于此原因引起的,也可能是由于myTask的签名中的某些不匹配(发布此答案时在q中不可用)引起的.

The System.AggregateException: A Task's exception(s) were not observed could be caused by that, or could be caused by some mismatch in the signature of myTask (not available in the q at the time of posting this answer).

编辑,以回复更新,以提问和进一步评论:

EDIT in response to UPDATE to question and further comments:

回复:

RetryForeverAsync似乎只是在重新抛出旧的异常.

It seems that RetryForeverAsync is simply re-throwing the old exception.

我知道(作为Polly的作者/维护者),Polly肯定在每次循环时都调用传递的Func<Task<R>>,并且只会抛出Func<Task<R>>的新执行抛出的异常.请参见异步重试实现:它重试用户每次重试循环.

I know (as the Polly author/maintainer) that Polly certainly calls the passed Func<Task<R>> each time round the loop, and will only rethrow whatever exception that fresh execution of the Func<Task<R>> throws. See the async retry implementation: it retries the user delegate afresh each time round the retry loop.

您可以尝试对呼叫代码进行以下类似的(临时的,诊断的)修订,以查看RefreshAuthorization()现在是否真的在阻止用户继续尝试呼叫策略代码,同时等待用户单击重试.

You could try something like the following (temporary, diagnostic) amendment to your calling code, to see if RefreshAuthorization() is now genuinely blocking the calling policy code from continuing execution, while it waits for the user to click retry.

public class WebExceptionCatcher<T, R> where T : Task<R>
{      
    public async Task<R> runTask(Func<T> t)
    {
        int j = 0;
        Policy p = Policy.Handle<WebException>()
        .Or<MobileServiceInvalidOperationException>()
        .Or<HttpRequestException>()
        .RetryForeverAsync(onRetryAsync: async (e,i) => await RefreshAuthorization());

        return await p.ExecuteAsync<R>( async () => 
        {
            j++;
            if ((j % 5) == 0) Device.BeginInvokeOnMainThread(async () =>
            {
                 await DisplayAlert("Making retry "+ i, "whatever", "Ok");
            });
            await myTask;
        });
    }
}

如果RefreshAuthorization())正确阻止,您将需要关闭五次Connection Lost弹出窗口,然后显示Making retry 5对话框.

If RefreshAuthorization()) is blocking correctly, you will need to dismiss the Connection Lost popup five times before the Making retry 5 dialog is displayed.

如果RefreshAuthorization())没有阻止调用代码,则在重新连接并首先关闭Connection Lost对话框之前,该策略将继续在后台进行多次(失败)尝试.如果这种情况成立,那么只需关闭一次Connection Lost弹出窗口,然后您会在下一个Connection Lost弹出窗口之前看到弹出窗口Making retry 5Making retry 10(等等;可能还会更多).

If RefreshAuthorization()) is not blocking the calling code, the policy would have continued making multiple (failing) tries in the background before you reconnected and first dismissed the Connection Lost dialog. If this scenario holds, then dismissing the Connection Lost popup just once, you would then see popups Making retry 5, Making retry 10 (etc; possibly more), before the next Connection Lost popup.

使用此(临时的,诊断的)修订还应该证明Polly每次都在重新执行您通过的委托.如果myTask引发了相同的异常,则myTask可能会遇到问题-我们可能需要更多地了解它,并在此进行更深入的研究.

Use this (temporary, diagnostic) amendment should also demonstrate that Polly is executing your passed delegate afresh each time. If the same exceptions are being thrown by myTask, that may be a problem with myTask - we may need to know more about it, and dig deeper there.

更新,以响应创建者的第二次更新,开始于这是我呼叫runTask():的方式"

UPDATE in response to originator's second update starting "Here is how I'm calling runTask():"

所以:您一直假设重试失败,但是您构建的代码实际上并未进行任何重试.

So: you have been assuming retries are failing, but you have constructed code that doesn't actually make any retries.

剩下的问题的根源是这两行:

The source of the remaining problem is these two lines:

Task<TodoItem> t = App.MobileService.GetTable<TodoItem>().LookupAsync(id);
TodoItem item = await catcher.runTask(() => t); // Or same effect: TodoItem item = await catcher.runTask(async () => await t);

无论Polly策略如何(或者如果您使用手工构建的whilefor循环进行重试,那么)每次遍历这些代码行一次都只会调用App.MobileService.GetTable<TodoItem>().LookupAsync(id).

This only ever calls App.MobileService.GetTable<TodoItem>().LookupAsync(id) once per traversal of these lines of code, regardless of the Polly policy (or equally if you had used a hand-built while or for loop for retries).

Task实例不可重新运行":Task实例只能代表一次执行.在这一行:

A Task instance is not 're-runnable': an instance of Task can only ever represent a single execution. In this line:

Task<TodoItem> t = App.MobileService.GetTable<TodoItem>().LookupAsync(id);

您只需调用一次LookupAsync(id),并将一个Task实例分配给t实例,该实例表示LookupAsync正在运行以及(一次完成或出错)该执行的结果.然后在第二行中构造一个lambda () => t,该lambda () => t始终返回相同的Task实例,表示该执行. (t的值从不改变,并且每次func返回它时,它仍然表示LookupAsync(id)的首次执行结果.)因此,如果由于没有互联网连接而导致第一个呼叫失败,则您使Polly重试策略所做的只是保持await -ing一个Task来表示首次唯一执行的失败,因此原始失败的确被抛弃.

you call LookupAsync(id) just once, and assign into t a Task instance that represents that LookupAsync running and (when it completes or faults) the outcome of that one execution. In the second line you then construct a lambda () => t that always returns that same instance of Task, representing that one execution. (The value of t never changes, and each time the func returns it, it still represents the result of that first-and-only execution of LookupAsync(id).). So, if the first call fails because there is no internet connection, all you have made the Polly retry policy do is keep await-ing a Task representing that first-and-only execution's failure, so the original failure indeed keeps getting rethrown.

要从图片中删除Task来说明问题,这有点像编写以下代码:

To take Task out of the picture to illustrate the problem, it's a bit like writing this code:

int i = 0;
int j = i++;
Func<int> myFunc = () => j;
for (k=0; k<5; k++) Console.Write(myFunc());

并期望它打印12345,而不是(将打印j值的五倍)11111.

and expecting it to print 12345 rather than (what it will print, the value of j five times) 11111.

要使其正常运行,只需:

To make it work, simply:

TodoItem item = await catcher.runTask(() => App.MobileService.GetTable<TodoItem>().LookupAsync(id));

然后,每次调用lambda都会重新调用.LookupAsync(id),并返回表示该新调用的Task<ToDoItem>的新实例.

Then each invocation of the lambda will call .LookupAsync(id) afresh, returning a new instance of Task<ToDoItem> representing that fresh call.