Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 6 из 25 ~18 мин +50 XP

Ретраи: сколько раз и с каким интервалом

Ретрай — обещание очереди повторить задачу самой: asynq retry после сбоя возвращает задачу с растущей паузой, по умолчанию до 25 попыток с экспоненциальным интервалом. Разберём, где живёт повторяющаяся задача, как считается пауза и что происходит, когда попытки кончились.

Содержание урока · 10 разделов

Два часа ночи, SMTP-шлюз партнёра лёг на двадцать минут. Если письмо отправлялось в HTTP-хендлере — оно потеряно: пользователь уже увидел ошибку, повторять никто не будет. В asynq всё происходит иначе. Хендлер возвращает ошибку — и задача не исчезает, а уезжает в повторы: через полминуты первая повторная попытка, потом вторая, потом третья. Шлюз ожил — письма ушли, и никто среди ночи не просыпался. Это режим по умолчанию, а не опция, которую надо не забыть включить: очередь задач отличается от «просто вызова функции» ровно этим поведением при сбое. Обратная сторона той же механики: если воркеры остановлены на час, повторяющаяся задача просто ждёт в очереди — попытка не сгорает, а откладывается. В уроке — три вопроса: сколько раз повторяется задача, как asynq выбирает паузу между попытками и куда попадает задача, у которой попытки исчерпаны. Формат по-прежнему честный: asynq — сетевая библиотека, код запускается локально с Go и Redis в Docker, а песочница сайта показывает только тематическую программу на чистом Go.

Asynq retry: 25 попыток по умолчанию#

Пока вы не попросили иного, asynq retry делает до 25 попыток для каждой задачи: это умолчание зашито в постановку, и его же вы видите в TaskInfo в поле MaxRetry. Счётчик сделанных повторов живёт рядом — поле Retried растёт с каждой неудачей, а NextProcessAt показывает расчётное время следующей попытки. Изменить потолок можно опцией постановки: client.Enqueue(task, asynq.MaxRetry(5)) — и все три числа переедут в дашборд asynqmon, где retry-задачи сидят отдельной вкладкой. Сколько выбирать? Практика такая: уведомления и письма — три-пять попыток, потому что поздний успех там почти ничего не стоит; задачи вроде выгрузки в CRM или вебхука в партнёрскую систему — чаще с алертом на исчерпание, потому что потеря видна деньгами. Умолчание в 25 заточено под задачи, чей успех важен в любой день, — и оно же ловит тех, кто не думал о числе попыток вовсе. Между прочим, повторы задач go в asynq считаются на стороне воркера: постановка один раз записала потолок, дальше библиотека ведёт счёт сама, клиент в процесс не вмешивается. Ещё одна деталь дисциплины: потолок по умолчанию применяется ко всем постановкам без опции — поэтому в код-ревью полезно спрашивать не «сколько попыток здесь стоит», а «почему здесь умолчание годится». Задача с неявным потолком в двадцать пять выглядит безобидно ровно до первой недели живого сбоя внешнего сервиса.

Постановка с MaxRetry и хендлер, который падает

GO
task := asynq.NewTask("email:welcome", payload)

info, err := client.Enqueue(task, asynq.MaxRetry(5))
if err != nil {
    log.Fatal(err)
}
log.Printf("потолок попыток: %d", info.MaxRetry)

func handleWelcomeEmail(ctx context.Context, t *asynq.Task) error {
    if err := mailer.Send(parsePayload(t)); err != nil {
        return fmt.Errorf("отправка не удалась: %w", err)
    }
    return nil
}

Постановка отличается одной опцией: MaxRetry(5) записывает в задачу потолок повторов, и то же число вернёт info.MaxRetry. Сигнал «повторить» — ошибка хендлера: возвращённый err asynq посчитает сбоем, увеличит счётчик и отложит задачу. В LastErr попадёт именно ваше сообщение — оборачивайте оригинальную ошибку, не теряя деталей.

Экспоненциальная задержка DefaultRetryDelayFunc#

Паузу между попытками считает DefaultRetryDelayFunc — формула, взятая у Sidekiq: n в четвёртой степени плюс пятнадцать секунд плюс случайная добавка rand(30) на (n+1), где n — сколько повторов задача уже сделала. Первый ретрай при n=0 назначается через 15–44 секунды: случайная добавка разводит волну одновременных сбоев, чтобы полсотни задач, упавших от одного упавшего сервиса, не вернулись в обработку одной волной. Дальше экспонента набирает силу быстро: пятая повторная попытка ждёт уже четыре с половиной — семь минут, одиннадцатая — почти три часа, а пауза перед двадцать пятой — почти четверо суток: 24 в четвёртой степени секунд — это три дня и двадцать часов. Номер попытки здесь и ниже означает число уже сделанных повторов: «перед двадцать пятой» значит при n=24, после двадцати четырёх неудач. Суммарно от первого сбоя до исчерпания всех 25 попыток проходит порядка двадцати суток — цифра, которая многих удивляет: экспонента честно говорит «если не сработало за час, будем пробовать редко». Сигнатура функции — func(n int, e error, t *Task) time.Duration: на вход номер попытки, ошибка и сама задача, на выходе пауза. Свою функцию можно поставить в Config.RetryDelayFunc — например, чтобы считать интервалы по типу ошибки, — но это отдельное ремесло, ему посвящён тринадцатый урок. Пока достаточно понимать арифметику умолчания: она объясняет, почему задача «висит» в retry то минуты, то сутки. И почему пауза — не таймер в памяти процесса: срок следующей попытки записан в Redis баллом, и пока воркеры живы, время текёт одинаково для всех реплик — двойных повторов из-за конкурирующих воркеров ждать не надо.

Проверка: первая пауза

Задача впервые вернула ошибку, своя RetryDelayFunc не задана. Через сколько asynq назначит первую повторную попытку?

Номер попытки в хендлере: GetRetryCount

GO
func handleWebhook(ctx context.Context, t *asynq.Task) error {
    retried, _ := asynq.GetRetryCount(ctx)
    maxRetry, _ := asynq.GetMaxRetry(ctx)
    log.Printf("попытка %d из %d", retried+1, maxRetry+1)

    err := partner.Push(t.Payload())
    if errors.Is(err, partner.ErrBadRequest) {
        log.Printf("повтор не поможет: %v", err)
    }
    return err
}

GetRetryCount и GetMaxRetry читают счётчики из контекста — сервер кладёт их туда перед каждым вызовом хендлера. Номер попытки в логе сразу показывает, гаснет ли сбой сам: одни ошибки исчезают на второй попытке, другие повторяются слово в слово. Плохой запрос выделен сознательно: повтор такой задачи бессмыслен, и в четырнадцатом уроке появится способ сказать это очереди явно.

Состояние retry: где живёт повторяющаяся задача#

Повторяющаяся задача переходит в состояние retry и живёт в сортированном множестве Redis с баллом-временем следующей попытки — вы уже видели это устройство в пятом уроке про отложенные задачи: retry и scheduled внутри библиотеки близнецы, разница только в том, кто назначил время. Дальше всё то же самое: воркеры опрашивают расписание с интервалом в несколько секунд, созревшая задача переходит в pending и забирается свободным горутинам. Поскольку множество живёт в Redis, повторы переживают рестарты воркеров, клиентов и самого контейнера Redis — упавшая задача не зависит от жизни процесса. В asynqmon retry-задачи видны отдельной вкладкой с последней ошибкой и временем следующей попытки — шестнадцатый урок покажет, что там смотреть в первую очередь. Практический вывод для мониторинга: длина retry-вкладки — симптом внешних проблем. Десяток задач с одинаковым LastErr — упал внешний сервис, а не ваш код. И наоборот: разброс ошибок на одной вкладке — повод смотреть в собственный код. Для дашборда полезно помнить, что вкладки scheduled и retry читаются вместе: длина первой — ваши обещания, длина второй — чужие сбои, и смешивать их в одном алерте — верный способ перестать понимать, что происходит.

Анатомия попытки: где что смотреть#

Поле или функцияГде доступноЧто даёт
MaxRetryопция постановки и TaskInfoпотолок повторов конкретной задачи
RetriedTaskInfoсколько повторов уже сделано
LastErr, LastFailedAtTaskInfo, asynqmonпоследняя ошибка и время сбоя
NextProcessAtTaskInfoрасчётное время следующей попытки
GetRetryCount(ctx)внутри хендлераномер попытки для логов и ветвлений
GetMaxRetry(ctx)внутри хендлератот же потолок, что и в TaskInfo

После последней попытки: архив#

Когда счётчик догоняет потолок — Retried стал равен MaxRetry и попытка снова не удалась — asynq отправляет задачу в состояние archived. Архив — не кладбище, а досье: задача остаётся в Redis со всей историей, включая LastErr и LastFailedAt, и видна в asynqmon отдельной вкладкой. Разобрать её можно двумя руками: Inspector умеет запускать задачу заново и удалять — им занимается пятнадцатый урок; в дашборде те же действия есть кнопками. Важно не путать архив с удалением: данные на месте, исполнение прекратилось. Есть и второй путь в архив — ошибка SkipRetry, с которой хендлер говорит «не повторять вообще, неси сразу в архив»; это инструмент для ошибок, которые не исправятся повтором, и его разбор — в четырнадцатом уроке. Держите в голове симптом: растущая вкладка archived почти всегда означает, что где-то сломалось то, чинить должен человек, — очередь честно всё это время повторяла, а потом призналась. Ручной запуск из архива — не ритуал прощения, а инструмент: починили endpoint, запустили задачу заново, проверили результат — и удалили, не оставляя хвостов.

Правила ретраев одним списком#

  • По умолчанию — до 25 попыток; для конкретной задачи потолок задаёт MaxRetry(n) на постановке.
  • Счётчик повторов — Retried, потолок — MaxRetry: оба видны в TaskInfo и в дашборде.
  • Паузу считает формула Sidekiq: n в четвёртой степени плюс 15 секунд плюс случайная добавка.
  • Первая пауза 15–44 секунды, к 25-й попытке — почти четверо суток: экспонента прощает долгие аварии.
  • Retry-задача живёт в сортированном множестве Redis и переживает рестарты всех участников.
  • Попытки кончились — задача уходит в archived с LastErr: разбирается Inspector-ом или вручную в asynqmon.
  • Ретрай — не бесплатный: каждая попытка занимает слот Concurrency и дёргает внешние системы.

Проверка: исчерпание попыток

Задача поставлена с MaxRetry(3) и вернула ошибку в третий раз, Retried стал равен трём. Что с ней будет?

Соберите постановку и лог попытки

Допишите опцию потолка попыток и два чтения счётчиков из контекста в хендлере.

info, err := client.Enqueue(task, asynq.(5), // потолок повторов ) retried, _ := asynq.(ctx) // сколько раз уже повторяли limit, _ := asynq.(ctx) // сколько всего разрешено log.Printf("попытка %d из %d", retried+1, limit+1)

Соберите жизнь неудачной задачи

Расставьте этапы в порядке наступления: от постановки до архива при постоянном сбое.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Повторы задач go: цена и границы#

Повторы задач go не бесплатны, и считать цену стоит в двух валютах. Первая — слоты воркера: каждая повторная попытка занимает тот же пул Concurrency, что и свежие задачи, поэтому тысячный ретрай бессмысленного вебхука конкурирует с живыми письмами. Вторая — внешние системы: SMTP, платёжные шлюзы и партнёрские API сами устают от повторов, и волна ретраев после аварии способна положить их повторно. Есть в конфиге и редкий, но полезный винтик: функция IsFailure решает, считать ли возвращённую ошибку сбоем; если она вернёт false, счётчик повторов не вырастет и статистика очереди не испортится. Классический случай — ошибка валидации payload: задача без обязательного поля не исправится повтором, и засчитывать её сбоем значит портить метрики и жечь попытки впустую. Спорное мнение в тему: я не люблю ретраи с бесконечным потолком «на всякий случай» — бесконечный повтор означает, что где-то нет алерта. Потолок и ручной запуск из архива дают предсказуемую точку, где задача требует человека. О том, как считать интервалы по своей логике — например, длиннее для платёжных ошибок и короче для сетевых, — говорит тринадцатый урок; он заменяет формулу целиком через Config.RetryDelayFunc.

Где окажется задача после двух сбоев?

Прочитайте код и выберите, что он выведет:

client.Enqueue(task,
    asynq.MaxRetry(1),
    asynq.TaskID("webhook-7"),
)

// хендлер возвращал ошибку при каждой попытке

Карточки: ретраи

Термин

MaxRetry

нажмите, чтобы перевернуть
Определение

Опция постановки: потолок повторов конкретной задачи. Умолчание — 25; то же число возвращают TaskInfo и GetMaxRetry.

1 / 6

Ретраи разобраны: потолок в 25 по умолчанию, экспонента с рандомом от Sidekiq, retry-множество в Redis и архив в конце пути. Главная мысль урока: политика повторов — это решение о стоимости позднего успеха, и умолчание может быть дороже, чем кажется на глазах дашборда. Мини-эксперимент на локальной машине: поставьте задачу с хендлером, который всегда возвращает ошибку, MaxRetry(3) и ProcessIn на минуту — и проследите в asynqmon, как она гуляет active → retry → archived, сверяя тайминги с LastFailedAt. Дальше по курсу: седьмой урок научит делить задачи на очереди critical/default/low, чтобы тяжёлая работа не задерживала срочную; восьмой разберёт маршрутизацию ServeMux по типам задач. Сверьте после эксперимента карточку самой задачи в asynqmon: там виден полный путь попыток, и это самый наглядный способ почувствовать экспоненту. Ретраи вернутся в курсе дважды — как настраиваемая функция паузы и как ошибки, которые повторять нельзя; к обоим урокам вы теперь подходите с готовой механикой внутри.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • Хендлер вернул ошибку — asynq повторяет задачу сам: по умолчанию до 25 попыток.
  • MaxRetry(n) на постановке задаёт потолок для конкретной задачи; счётчик — Retried в TaskInfo.
  • Паузу считает DefaultRetryDelayFunc: n в четвёртой степени плюс 15 секунд плюс случайная добавка.
  • Первая пауза 15–44 секунды, к 25-й попытке — почти четверо суток: экспонента сильно растягивает хвост.
  • Retry-задача живёт в сортированном множестве Redis с баллом-временем и переживает рестарты.
  • Попытки кончились — задача уходит в archived с LastErr и LastFailedAt для разбора.
  • GetRetryCount и GetMaxRetry в хендлере дают номер попытки — логируйте его.
  • Ретраи не бесплатны: каждая попытка занимает слот Concurrency и дёргает внешние системы.

Домашнее задание

Соберите эксперимент урока: задача с MaxRetry(3), хендлер всегда возвращает ошибку — проследите в asynqmon путь active → retry → archived и сверьте паузы с формулой по LastFailedAt. Затем научите хендлер возвращать nil после второй неудачи (переменная-счётчик в тестовом коде) и убедитесь, что задача завершилась успехом и не доехала до архива. Для всего этого нужен локальный Go и Redis из Docker — песочница сайта сетевой код asynq не исполняет.

Личные заметкиПусто
Хранятся только в вашем браузере (Asynq · Ретраи: сколько раз и с каким интервалом)

Вопросы и ответы по уроку

Почему 25 попыток, а не 3?

Умолчание рассчитано на задачи, чей успех важен в любой день: экспонента растягивает попытки на недели, и задача доживает до починки внешнего сервиса. Для уведомлений ставьте три-пять явно — MaxRetry применяется на каждой постановке, и 25 лишь стартовая точка, а не рекомендация.

Можно ли задать свой интервал ретраев?

Да: Config.RetryDelayFunc — функция вида func(n int, e error, t *Task) time.Duration, она заменяет формулу для всех задач сервера. Внутри можно ветвиться по типу ошибки и по самой задаче; подробный разбор с примерами — тринадцатый урок курса.

Чем retry отличается от scheduled?

Механика хранения одна — сортированное множество с баллом-временем; отличается причина попадания. Scheduled — вы сами попросили отложить задачу при постановке, retry — задача уже падала и вернулась с экспоненциальной паузой. В asynqmon это разные вкладки, и длина второй — симптом внешних сбоев.

Термины урока

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

Следующая ступень учебного пути

Kafka25 уроков~12 чпрактика25 уроков уже доступны

Kafka в Go: продюсеры и консьюмеры на kafka-go

Курс Kafka для Go: топики и партиции, producer и consumer, группы, оффсеты, гарантии доставки, rebalance, Admin API и мониторинг. 25 уроков.

  • Понимать топики, партиции, оффсеты и группы консьюмеров
  • Писать продюсеры с ключами, батчами и ретраями
  • Читать сообщения группой с коммитами оффсетов
  • Выбирать гарантии: at-most-once, at-least-once