Немедленная повторная попытка, отложенная повторная попытка и повторная попытка с отсрочкой

AngularInDepth уходит от Medium. Более свежие статьи размещаются на новой платформе inDepth.dev. Спасибо за то, что участвуете в глубоком движении!

Доступ к данным из серверной части - это основа почти каждого одностраничного приложения. Весь динамический контент загружается с сервера.

В большинстве случаев эти HTTP-запросы работают хорошо и возвращают желаемый результат. Однако есть сценарии, в которых запрос может пойти не так.

Представьте, что кто-то использует ваш сайт через точку доступа в поезде, который мчится по стране со скоростью 200 км / ч. 🚅 Что ж, сеть может быть медленной, а HTTP-запрос по-прежнему возвращает желаемый результат.

Но что, если поезд войдет в туннель? Велика вероятность того, что пользователь потерял соединение и ваше веб-приложение не сможет связаться с сервером. В этом случае пользователь вынужден обновить ваше приложение, как только он выйдет из туннеля и снова подключится к сети.

Обновление приложения может повлиять на текущее состояние приложения. Пользователь может, например, потерять данные, которые он ввел в форму.

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

Повторная попытка неудачных запросов

Давайте смоделируем ситуацию с поездом на примере бэкэнда, который терпит неудачу при первых трех попытках и возвращает данные при четвертой попытке.

Поэтому обычно в Angular мы создаем службу, вводим HttpClient и используем его для получения данных из бэкэнда.

Здесь ничего особенного. Мы вводим Angulars HTTPClient и выполняем простой get Request. Если этот запрос возвращает ошибку, мы выполняем некоторую логику ошибки и возвращаем пустой наблюдаемый объект, чтобы сообщить вызывающему; Произошла ошибка, но все в порядке, я понял.

Именно так большинство приложений выполняет свои HTTP-запросы. Приведенная выше реализация выполняет запрос один раз. Затем он либо возвращает данные из серверной части, либо терпит неудачу.

Итак, как мы собираемся повторить наш запрос, если конечная точка приветствия недоступна или возвращает ошибку? Может быть, RxJS предоставляет оператора? Конечно, есть, у RxJS есть оператор для всего 😉

Первое, что может прийти в голову, - это оператор retry. Давайте посмотрим на его определение.

Возвращает Observable, который отражает исходный Observable, за исключением error. Если источник Observable вызывает error, этот метод повторно подпишется на источник Observable максимум на count повторных подписок (заданных как числовой параметр), а не распространит вызов error.

Это очень похоже на оператора, который нам нужен. Так что давайте включим это в нашу цепочку.

Мы успешно применили оператор retry. Давайте посмотрим, как он повлиял на поведение HTTP-запроса, когда он используется внутри примера приложения.

Приложение очень простое. Он просто выполняет HTTP-вызов, как только мы нажимаем кнопку «PING THE SERVER».

Как упоминалось ранее, серверная часть возвращает ошибку при первых трех попытках и доставляет ответ при четвертой попытке.

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

Это круто. Итак, мы получили поведение повтора. 🤘

Но все еще есть возможности для улучшения. Обратите внимание, что повторные попытки выполняются немедленно. Это не очень полезно, если мы подумаем о нашем примере с туннелем. 🤔

Отложенные повторные попытки

Мы не выходим из туннеля сразу после того, как вошли в него. Мы проводим в нем некоторое время. Итак, нам нужно продлить период повтора, откладывая каждую попытку.

Для этого нам нужен более детальный контроль над поведением повтора. Нам нужно иметь возможность решать, когда следует выполнять повторные попытки. Оператора retry недостаточно. Давайте снова обратимся за помощью к документации RxJs.

Есть оператор retryWhen, который, кажется, подходит для нашего случая. Официальные документы описывают этот оператор следующим образом:

Возвращает Observable, который отражает исходный Observable, за исключением error. Если исходный Observable вызывает error, этот метод выдаст объект Throwable, который вызвал ошибку, в Observable, возвращенном из notifier. Если этот Observable вызывает complete или error, тогда этот метод вызовет complete или error для дочерней подписки. В противном случае этот метод повторно подпишется на источник Observable.

Какие? 😶 Звучит довольно сложно. Попробуем объяснить это более прямолинейно.

Оператор retryWhen принимает обратный вызов, который возвращает Observable. Возвращенный Observable определяет фактическое поведение оператора retryWhen на основе следующих правил:

Оператор retryWhen

  • повторяет исходную наблюдаемую, если возвращенная наблюдаемая излучает успешно
  • сдается и выдает ошибку, как только возвращаемый наблюдаемый выдает ошибку
  • завершается, если возвращенный наблюдаемый завершается

Сам обратный вызов вызывается только тогда, когда наблюдаемый источник дает сбой в первый раз.

Теперь мы можем использовать эти знания для записи отложенного повтора с помощью оператора Rx retryWhen.

Если исходный Observable, которым является наш HTTP-запрос, не работает, вызывается оператор retryWhen. Внутри обратного вызова мы получаем доступ к ошибке, вызвавшей сбой. Мы задерживаем errors, уменьшаем количество повторных попыток и возвращаем новый Observable, который выдает ошибку.

Основываясь на правилах оператора retryWhen, этот Observable запускает фактическую повторную попытку, потому что он выдает значение. Если повторная попытка не удалась несколько раз и переменная retries опустилась до 0, мы, наконец, сдаемся и выдаем точную ошибку.

🤠 Круто! Итак, теперь мы можем взять приведенный выше фрагмент и заменить его оператором повтора в нашей цепочке. Но ждать.

А что насчет переменной повторных попыток? Эта переменная содержит текущее состояние повторных попыток. Где это заявлено? Когда сбрасывается состояние? Состояние должно управляться внутри вашего потока, а не снаружи.

Создание настраиваемого оператора с именем delayedRetry

Извлекая приведенный выше код в отдельный оператор RxJS, мы решаем эту проблему состояния и, кроме того, улучшаем читаемость нашего кода.

Есть разные способы реализовать собственный оператор RxJS. Методы того, как это сделать, сильно зависят от того, как построен ваш оператор.

Наш оператор построен с использованием существующих операторов RxJS. Поэтому мы можем пойти по более легкому пути создания нашего оператора RxJS. В нашем случае оператор RxJS - это просто функция со следующей сигнатурой.

const customOperator = (src: Observable<A>) => Observable<B>

Оператор принимает наблюдаемый источник и возвращает другое наблюдаемое.

Поскольку наш пользовательский оператор позволяет пользователю решать, в каком интервале и как часто должны происходить повторные попытки, нам нужно заключить определение функции, приведенное выше, в фабричную функцию, которая принимает delayMs и maxRetry в качестве параметров.

const customOperator = (delayMs: number, maxRetry: number) => {
   return (src: Observable<A>) => Observable<B>
}

⚠️ Если вы хотите создать оператор, не состоящий из существующих операторов, вам следует обратить внимание на обработку ошибок и подписку. Кроме того, вам следует расширить класс Observable и реализовать функцию lift. Узнайте больше, если вам это интересно.

Итак, основываясь на приведенном выше фрагменте, давайте напишем наш собственный оператор Rx.

Аккуратный. Этот оператор теперь доступен и может быть импортирован. Давайте воспользуемся нашим новым оператором в нашем HTTP-запросе.

Мы помещаем оператор delayedRetry в нашу цепочку и передаем 1000 и 3 в качестве параметров. Первый параметр определяет задержку в миллисекундах между каждой повторной попыткой. Второй определяет максимальное количество попыток.

Давайте перезапустим наше приложение и посмотрим, как работает наш оператор.

Мы видим, что каждая повторная попытка откладывается, и правильный ответ отображается пользователю после его получения.

Подписывайтесь на меня в Twitter или канал, чтобы получать уведомления о моих новых сообщениях в блоге! 🐥

Повторить, отступить, повторить!

Давайте сделаем еще один шаг вперед, если откладывать запросы. В предыдущей попытке мы всегда откладывали каждый запрос в одно и то же время.

В этом сценарии мы увеличиваем задержку после каждой попытки. Первая попытка происходит через одну секунду, вторая - через две секунды, а третья - через три секунды.

Давайте создадим новый оператор retryWithBackoff, который реализует это поведение.

Если мы сейчас запустим наше приложение, мы увидим, как увеличивается задержка повторных запросов.

После каждой попытки мы отступаем и увеличиваем задержку. Как только источник вернет правильный ответ, мы отобразим его в пользовательском интерфейсе.

Заключение

Повторная попытка HTTP-запросов делает наше приложение более стабильным.

Его особенно полезно применять в критических запросах, которые получают данные, необходимые для работы вашего приложения. Например, данные конфигурации, которые включают URL-адреса бэкэндов, нам нужно вызвать.

В большинстве сценариев оператора retry из RxJS недостаточно, поскольку он немедленно повторяет неудачные запросы. Оператор retryWhen дает нам более точный контроль над повторными попытками. Это позволяет нам определить, когда должна произойти повторная попытка. Благодаря этой возможности мы можем реализовать отложенные попытки или повторные попытки с отсрочкой.

При реализации многоразового поведения в вашей цепочке RxJS рекомендуется извлекать логику в новом операторе.

🧞‍ 🙏 Если вам понравился этот пост, поделитесь им и дайте, нажав несколько раз на кнопку хлопка слева.

Не стесняйтесь ознакомиться с некоторыми из моих других статей о фронтенд-разработке.