1

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

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

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

我找到了Polly包,它设置为在 C# 中处理 try/catch 重试。我目前的代码设置如下:

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在主线程的当前页面上显示一个:

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从未显示。发生以下两种情况之一:

  1. 执行继续一遍又一遍地调用我的任务而没有完成
  2. 抛出ASystem.AggregateException并显示以下消息:

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

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

更新:

在将调用DisplayAlert放在Device.BeginInvokeOnMainThread方法内部之后,我现在找到了绕过AggregateException. 但是,现在我遇到了另一个问题。

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

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

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

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

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

或者:

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

1 回答 1

2

您需要使用.RetryForeverAsync(...)作为评论员指出。然后,由于您的重试委托是异步的,您还需要使用onRetryAsync:. 因此:

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


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

这导致 async-delegate-assigned-to-sync-param 变为async void; 调用代码不会/不能等待。由于async void没有等待委托,因此您执行的委托确实会不断重试。

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

编辑以回应更新问题和进一步评论:

回覆:

似乎 RetryForeverAsync 只是重新抛出旧异常。

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

您可以尝试对调用代码进行以下(临时、诊断)修改,以查看RefreshAuthorization()现在是否真正阻止调用策略代码继续执行,同时等待用户单击重试。

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显示对话框。

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

使用这个(临时的、诊断性的)修正还应该证明 Polly 每次都重新执行你传递的委托。如果 抛出相同的异常myTask,那可能是一个问题myTask——我们可能需要更多地了解它,并在那里更深入地挖掘。


更新以响应发起者的第二次更新,开始“这就是我打电话的方式runTask():

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

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

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);

每次遍历这些代码行时,这只会调用App.MobileService.GetTable<TodoItem>().LookupAsync(id)一次,无论 Polly 策略如何(或者如果您使用手动构建whilefor循环进行重试,则同样如此)。

一个Task实例不是“可重新运行的”:一个实例Task只能代表一次执行。在这一行:

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

LookupAsync(id)只调用一次,并分配给t一个Task实例,该实例表示 LookupAsync 正在运行以及(当它完成或出错时)该一次执行的结果。然后在第二行中构造一个() => t始终返回相同实例的lambda Task,表示该一次执行。(never 的值t不会改变,并且每次 func 返回它时,它仍然代表 . 的第一次也是唯一一次执行的结果LookupAsync(id)。)。因此,如果由于没有 Internet 连接而导致第一次调用失败,那么您所做的 Polly 重试策略所做的就是保持await-Task表示第一次也是唯一一次执行的失败,因此最初的失败确实会不断被重新抛出。

拿出Task图来说明问题,有点像写这段代码:

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

并期望它打印12345而不是(它将打印什么,j五次的值)11111

要使其工作,只需:

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

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

于 2017-09-26T08:34:21.427 回答