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
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);
}
}
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);
}
}
When I debug this and cut my internet connection. The DisplayAlert
is never shown. One of two things happens:
- The execution continues to call my task over and over without completing
- 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?
UPDATE:
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.
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.
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>();
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);
or:
TodoItem item = await catcher.runTask(async () => await t);
You need to use
.RetryForeverAsync(...)
as a commentor noted. Then, since your on-retry delegate is async, you also need to useonRetryAsync:
. Thus:.RetryForeverAsync(onRetryAsync: async (e,i) => await RefreshAuthorization());
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 synchronousonRetry
delegate (returning void), but then assigning an async delegate to it.That causes the async-delegate-assigned-to-sync-param to become
async void
; calling code doesn't/can't wait for that. Since theasync void
delegate isn't waited for, your executed delegate would indeed be continuously retried.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 ofmyTask
(not available in the q at the time of posting this answer).EDIT in response to UPDATE to question and further comments:
Re:
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 theFunc<Task<R>>
throws. See the async retry implementation: it retries the user delegate afresh each time round the retry loop.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.If
RefreshAuthorization())
is blocking correctly, you will need to dismiss theConnection Lost
popup five times before theMaking retry 5
dialog is displayed.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 theConnection Lost
dialog. If this scenario holds, then dismissing theConnection Lost
popup just once, you would then see popupsMaking retry 5
,Making retry 10
(etc; possibly more), before the nextConnection Lost
popup.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 withmyTask
- we may need to know more about it, and dig deeper there.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:
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-builtwhile
orfor
loop for retries).A
Task
instance is not 're-runnable': an instance ofTask
can only ever represent a single execution. In this line:you call
LookupAsync(id)
just once, and assign intot
aTask
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 ofTask
, representing that one execution. (The value oft
never changes, and each time the func returns it, it still represents the result of that first-and-only execution ofLookupAsync(id)
.). So, if the first call fails because there is no internet connection, all you have made the Polly retry policy do is keepawait
-ing aTask
representing that first-and-only execution's failure, so the original failure indeed keeps getting rethrown.To take
Task
out of the picture to illustrate the problem, it's a bit like writing this code:and expecting it to print
12345
rather than (what it will print, the value ofj
five times)11111
.To make it work, simply:
Then each invocation of the lambda will call
.LookupAsync(id)
afresh, returning a new instance ofTask<ToDoItem>
representing that fresh call.