C# Polly async-await: Wait for user confirmation b

2019-06-10 23:52发布

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:

  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?

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

1条回答
混吃等死
2楼-- · 2019-06-11 00:34

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


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.

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.

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:

Re:

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

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.

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

If RefreshAuthorization()) is blocking correctly, you will need to dismiss the Connection Lost popup five times before the Making 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 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.

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.


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

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

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

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.

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

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

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

查看更多
登录 后发表回答