C # Polly async-await: дождитесь подтверждения пользователя перед повторной попыткой

Я создаю приложение Xamarin.Forms для iOS и Android, в котором я храню данные и локальную базу данных sqlite и в сети на сервере Azure. Хотя моему приложению требуется подключение к Интернету, которое оно всегда проверяет с помощью подключаемого модуля Connectivity, я обнаружил, что иногда возникает исключение, если пользователь теряет прием ячейки в середине запроса.

Я хочу иметь метод, с помощью которого я могу вызывать все запросы моего сервера, который будет повторять запрос в случае возникновения ошибки. Я также хотел бы иметь возможность запрашивать у пользователя ввод перед повторной попыткой. Поток будет выглядеть так:

Вызов на сервер -> Обнаружено исключение -> Спросить пользователя, хотят ли они повторить попытку -> Повторить

Я нашел пакет 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

Кто-нибудь знает, как успешно приостановить выполнение при сбое задачи и дождаться возобновления работы пользователя?

ОБНОВЛЕНИЕ:

После вызова 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);

or:

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

person cvanbeek    schedule 25.09.2017    source источник
comment
Спасибо, вы пробовали RetryForeverAsync?   -  person JSteward    schedule 26.09.2017
comment
Да, именно это я и звонил, когда получил исключение AggregateException   -  person cvanbeek    schedule 26.09.2017


Ответы (1)


Вы должны использовать .RetryForeverAsync(...), как заметил комментатор. Затем, поскольку ваш делегат при повторной попытке является асинхронным, вам также необходимо использовать onRetryAsync:. Таким образом:

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


Чтобы объяснить ошибки, которые вы видели: в примере кода в вопросе, используя onRetry:, вы указываете, что хотите использовать синхронный onRetry делегат (возвращающий void), но затем назначаете ему асинхронный делегат.

Это приводит к тому, что параметр async-delegate-assign-to-sync-param становится async void; вызывающий код не может / не может ждать этого. Поскольку делегат async void не ожидается, ваш выполненный делегат действительно будет непрерывно повторяться.

System.AggregateException: A Task's exception(s) were not observed может быть вызвано этим или может быть вызвано некоторым несоответствием в подписи myTask (недоступно в q на момент публикации этого ответа).

ИЗМЕНИТЬ в ответ на ОБНОВЛЕНИЕ на вопрос и дальнейшие комментарии:

Re:

Кажется, что 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 5, Making retry 10 (и т. Д., Возможно, больше) перед следующим Connection 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 (или в равной степени, если вы использовали ручной while или for цикл для повторных попыток).

Экземпляр Task не может быть повторно запущен: экземпляр Task может всегда представлять только одно выполнение. В этой строке:

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

вы вызываете LookupAsync(id) только один раз и назначаете t экземпляр Task, который представляет запущенный LookupAsync и (когда он завершается или дает сбой) результат этого одного выполнения. Во второй строке вы затем создаете лямбда () => t, которая всегда возвращает тот же экземпляр Task, представляющий это одно выполнение. (Значение t никогда не изменяется, и каждый раз, когда функция возвращает его, оно по-прежнему представляет результат первого и единственного выполнения LookupAsync(id).). Итак, если первый вызов завершился неудачей из-за отсутствия подключения к Интернету, все, что вы сделали для политики повторных попыток Polly, - это сохранить await-ing 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));

Затем каждый вызов лямбды будет вызывать .LookupAsync(id) заново, возвращая новый экземпляр Task<ToDoItem>, представляющий этот свежий вызов.

person mountain traveller    schedule 26.09.2017
comment
Это все еще бросает System.AggregateException. Я обновлю вопрос, чтобы включить весь мой класс - person cvanbeek; 26.09.2017
comment
A запустил его в другой раз и получил другое исключение: System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---> Java.Lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare() - person cvanbeek; 26.09.2017
comment
@cvanbeek Не знаком с этим конкретным исключением Android. Сообщите мне, однако, если переключатели на использование async Polly обеспечивают необходимое ожидание (когда другие исключения исправлены) - person mountain traveller; 26.09.2017
comment
Я обошел AggregateException, которое возникло, потому что я вызвал DisplayAlert из основного потока. Теперь у меня возникла проблема, заключающаяся в том, что даже при повторном подключении к Интернету повторная попытка не удалась. См. Обновление выше для получения дополнительной информации. - person cvanbeek; 27.09.2017
comment
Вы переключились на onRetryAsync:, как подсказал мой ответ? В противном случае многие эффекты, описанные в ОБНОВЛЕНИИ к q, могут быть отнесены к этому (я могу объяснить подробно). Вы должны использовать onRetryAsync:, чтобы RefreshAuthorization() блокировал продолжение потока политики до RefreshAuthorization() выхода. Если это неясно, прочтите об опасностях async void в обработчиках / делегатах событий. Многие блоги, например: blogs.msdn.microsoft.com/andy_wigley/2013/07/31/ - person mountain traveller; 27.09.2017
comment
Да, я использую OnRetryAsync. Прошу прощения, я забыл отредактировать эту часть своего вопроса. - person cvanbeek; 28.09.2017
comment
Теперь метод ожидает, пока я нажму «Повторить попытку», но даже после восстановления подключения к Интернету вызов моей веб-службы не работает. - person cvanbeek; 28.09.2017
comment
Если RefreshAuthorization() правильно блокирует вызывающий код, может ли myTask, который вы передали, не обнаружить переподключенное интернет-соединение? Или переданный экземпляр myTask не подлежит повторному вызову? - просто возвращать тот же исходный результат? - person mountain traveller; 28.09.2017
comment
Примечание. В опубликованном коде вы не используете переменную t, а myTask не определен: это одно и то же? Также может быть полезно увидеть характер обычно переданных Func<Task<R>>. - person mountain traveller; 28.09.2017
comment
да, я понимаю, t и myTask были одним и тем же, я изменил одно, чтобы сделать вопрос более ясным, и забыл изменить другое. Я исправил этот вопрос, а также показал оба способа вызова runTask(). Кроме того, RefreshAuthorization() правильно блокирует вызывающий код. - person cvanbeek; 28.09.2017
comment
Вы также можете рассмотреть возможность использования Device.InvokeOnMainThread() вместо Device.BeginInvokeOnMainThread(), если таковая имеется. По крайней мере, в документации monotouch это указывает, что Device.InvokeOnMainThread() блокируется до тех пор, пока действие в основном потоке не завершится, тогда как Device.BeginInvokeOnMainThread только ставит его в очередь для основного потока и немедленно возвращается вызывающему (не блокирует). С Device.InvokeOnMainThread() вы можете опустить все, что вы вокруг, используя переменную bool loop, чтобы гарантировать блокировку. - person mountain traveller; 30.09.2017