2017-09-25 43 views
0

我正在为iOS和Android创建一个Xamarin.Forms应用程序,用于保存数据和本地sqlite数据库并在Azure服务器中联机。虽然我的应用程序需要互联网连接,而且它始终使用Connectivity插件进行检查,但我发现如果用户丢失了信元接收请求,我有时会发生异常。C#Polly async-await:在重试前等待用户确认

我想要一个方法,我可以调用我所有的服务器请求,以便在发生错误时重试请求。我还希望能够在重试之前询问用户输入。流程是这样的:

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

我发现Polly包,它是建立处理的try/catch在C#中重试。目前,我有我的代码的设置是这样的:

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. 一个System.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

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

UPDATE:

把呼叫DisplayAlertDevice.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); 

或:

+0

谢谢,你尝试过'RetryForeverAsync'吗? – JSteward

+0

是的,这实际上是我在收到AggregateException时调用的那个 – cvanbeek

回答

0

您需要使用.RetryForeverAsync(...)作为COM导师指出。然后,由于您的on-retry委托是异步的,您还需要使用onRetryAsync:。因此:

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


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

这会导致异步代理分配的同步参数变为async void;呼叫代码不会/ 不能等待。由于没有等待async void代表,您的执行代表确实会不断重试。

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

编辑响应UPDATE质疑和进一步的评论:

回复:

似乎RetryForeverAsync仅仅是重新抛出旧的例外。

我知道(如波利撰文/维护),其波利肯定调用传递Func<Task<R>>每轮循环,只会再次引发任何异常的Func<Task<R>>的新鲜执行罚球。请参阅async retry implementation:它retries the user delegate afresh每次围绕retry loop

您可以对您的调用代码尝试类似以下(临时性,诊断性)修订,以查看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())正确阻挡,你将需要显示Making retry 5对话框之前关闭该Connection Lost弹出五倍。

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

使用此(临时性,诊断性)修正还应证明Polly每次都重新执行通过的委托。如果myTask引发同样的例外,那么这可能与myTask有关 - 我们可能需要了解更多信息,并在此处深入挖掘。


UPDATE响应发起者的第二次更新首发“这是我如何我打电话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)%的代码行遍历一次,无论波利政策(或者,如果同样你曾用一只手用于重试的构建的whilefor循环)。

A Task实例不是'可重新运行的':Task的实例只能表示一次执行。在这一行:

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

你打电话LookupAsync(id)只有一次,并分配到一个t例如Task,表示LookupAsync运行,(它完成或故障时)是一个执行的结果。然后在第二行中构建一个总是返回Task的相同实例的lambda () => t,表示该执行。 (t的值永远不会改变,每次func返回时,它仍然表示首次且唯一的执行结果LookupAsync(id)。)。因此,如果第一次通话因为没有互联网连接而失败,那么您所做的波利重试策略所做的只是保持Task表示首次且唯一的执行失败,因此最初的失败确实不断重演。

采取Task出来的画面来说明问题,这是一个有点像编写这些代码:

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

,并期待它来打印12345,而不是(它会打印的j五值次)​​。

要使其工作,简单地说:

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

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

+0

这仍会抛出'System.AggregateException'。我将更新问题以包括我的整个班级 – cvanbeek

+0

再次运行它并得到了一个不同的异常:System.Reflection.TargetInvocationException:Exception被调用的目标抛出。 ---> Java.Lang.RuntimeException:无法在未调用Looper.prepare()的线程内创建处理程序' – cvanbeek

+0

@cvanbeek不熟悉该特定的Android异常。然而,让我知道,如果切换到'async' Polly用法提供了必要的等待(当其他异常被修复时) –