2017-09-25 42 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用法提供了必要的等待(當其他異常被修復時) –