Содержание урока · 10 разделов
- Asynq retry: 25 попыток по умолчанию
- Экспоненциальная задержка DefaultRetryDelayFunc
- Состояние retry: где живёт повторяющаяся задача
- Анатомия попытки: где что смотреть
- После последней попытки: архив
- Правила ретраев одним списком
- Повторы задач go: цена и границы
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Два часа ночи, 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 и хендлер, который падает
GOtask := 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
GOfunc 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 | потолок повторов конкретной задачи |
| Retried | TaskInfo | сколько повторов уже сделано |
| LastErr, LastFailedAt | TaskInfo, asynqmon | последняя ошибка и время сбоя |
| NextProcessAt | TaskInfo | расчётное время следующей попытки |
| GetRetryCount(ctx) | внутри хендлера | номер попытки для логов и ветвлений |
| GetMaxRetry(ctx) | внутри хендлера | тот же потолок, что и в TaskInfo |
Вебхук партнёру с умолчаниями упал из-за смены их endpoint — а мы узнали об этом через три недели, когда Partner выслал счёт за недоставленные уведомления. Задача всё это время болталась в retry: паузы к тому моменту были по несколько суток, и в логах каждые пару дней всплывала одна и та же ошибка. Правило, которое я вывожу из кейса: MaxRetry подбирается под ответ на вопрос «сколько стоит поздний успех». Для уведомлений — 3–5 и честная сдача после них; 25 оправдано там, где успех важен в любой день, и вы готовы чистить архив от призраков.
После последней попытки: архив#
Когда счётчик догоняет потолок — 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 стал равен трём. Что с ней будет?
Соберите постановку и лог попытки
Допишите опцию потолка попыток и два чтения счётчиков из контекста в хендлере.
Соберите жизнь неудачной задачи
Расставьте этапы в порядке наступления: от постановки до архива при постоянном сбое.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Повторы задач go: цена и границы#
Повторы задач go не бесплатны, и считать цену стоит в двух валютах. Первая — слоты воркера: каждая повторная попытка занимает тот же пул Concurrency, что и свежие задачи, поэтому тысячный ретрай бессмысленного вебхука конкурирует с живыми письмами. Вторая — внешние системы: SMTP, платёжные шлюзы и партнёрские API сами устают от повторов, и волна ретраев после аварии способна положить их повторно. Есть в конфиге и редкий, но полезный винтик: функция IsFailure решает, считать ли возвращённую ошибку сбоем; если она вернёт false, счётчик повторов не вырастет и статистика очереди не испортится. Классический случай — ошибка валидации payload: задача без обязательного поля не исправится повтором, и засчитывать её сбоем значит портить метрики и жечь попытки впустую. Спорное мнение в тему: я не люблю ретраи с бесконечным потолком «на всякий случай» — бесконечный повтор означает, что где-то нет алерта. Потолок и ручной запуск из архива дают предсказуемую точку, где задача требует человека. О том, как считать интервалы по своей логике — например, длиннее для платёжных ошибок и короче для сетевых, — говорит тринадцатый урок; он заменяет формулу целиком через Config.RetryDelayFunc.
Любимая самодеятельность — цикл с тремя попытками SMTP внутри ProcessTask. Умножьте на 25 ретраев очереди и получите до семидесяти пяти реальных вызовов на одну задачу, а счётчик Retried при этом покажет скромное единицу. Пусть паузами и подсчётом занимается asynq; внутренние короткие повторы оправданы разве что против миллисекундных гонок вроде блокировки строки в базе — и не больше пары попыток.
Где окажется задача после двух сбоев?
Прочитайте код и выберите, что он выведет:
client.Enqueue(task,
asynq.MaxRetry(1),
asynq.TaskID("webhook-7"),
)
// хендлер возвращал ошибку при каждой попыткеКарточки: ретраи
Ретраи разобраны: потолок в 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 не исполняет.
Личные заметкиПусто
Вопросы и ответы по уроку
Почему 25 попыток, а не 3?
Умолчание рассчитано на задачи, чей успех важен в любой день: экспонента растягивает попытки на недели, и задача доживает до починки внешнего сервиса. Для уведомлений ставьте три-пять явно — MaxRetry применяется на каждой постановке, и 25 лишь стартовая точка, а не рекомендация.
Можно ли задать свой интервал ретраев?
Да: Config.RetryDelayFunc — функция вида func(n int, e error, t *Task) time.Duration, она заменяет формулу для всех задач сервера. Внутри можно ветвиться по типу ошибки и по самой задаче; подробный разбор с примерами — тринадцатый урок курса.
Чем retry отличается от scheduled?
Механика хранения одна — сортированное множество с баллом-временем; отличается причина попадания. Scheduled — вы сами попросили отложить задачу при постановке, retry — задача уже падала и вернулась с экспоненциальной паузой. В asynqmon это разные вкладки, и длина второй — симптом внешних сбоев.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Kafka в Go: продюсеры и консьюмеры на kafka-go
Курс Kafka для Go: топики и партиции, producer и consumer, группы, оффсеты, гарантии доставки, rebalance, Admin API и мониторинг. 25 уроков.
- Понимать топики, партиции, оффсеты и группы консьюмеров
- Писать продюсеры с ключами, батчами и ретраями
- Читать сообщения группой с коммитами оффсетов
- Выбирать гарантии: at-most-once, at-least-once