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

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

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

Урок 11 из 25 ~16 мин +50 XP

Таймауты и дедлайн задачи

Задача, которая висит в active полчаса, держит слот воркера и тормозит всю очередь — против неё и существуют asynq Timeout: опция задаёт длительность обработки, а Deadline назначает крайний момент, и дедлайн задач go останавливает бессмысленную работу. Разберём механику истечения, связь с ретраями и выбор значений.

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

Ситуация из тикета: очередь стоит, в asynqmon одна задача report:build живёт в active уже двадцать минут. Внутри — HTTP-вызов к партнёрскому API без таймаута: тот молчит, хендлер ждёт, воркер занят. Asynq по умолчанию не разрешает задачам вечность: если не задать пределы, постановка сама назначит дедлайн контекста в тридцать минут, но для большинства задач это не лимит, а алиби. Нужен способ сказать: «эта обработка должна уложиться в две минуты» или «после девяти утра результат никому не нужен». Для этого существуют две опции постановки: Timeout с длительностью и Deadline с точным моментом. Работают обе одинаково честно: они не убивают хендлер, а ставят дедлайн на его контекст — дальше всё зависит от того, слушает ли ваш код отмену. В уроке: механика истечения, связь с ретраями и выбор значений, которые не бьются с бизнесом. Терминологию зафиксируем сразу: таймаут — про длительность попытки, дедлайн — про крайний момент; их часто путают, и путаница стоит ночных инцидентов, когда отчёт поставили на полночь и удивляются, почему он «сам собой» ретраится к обеду.

Asynq Timeout: сколько живёт задача#

Asynq Timeout — опция постановки: client.Enqueue(task, asynq.Timeout(10time.Minute)) обещает, что обработка уложится в десять минут. Спустя этот срок истекает дедлайн контекста хендлера: если ProcessTask ещё не вернулся, asynq засчитывает неудачу, и задача ретраится по обычной политике. Значение — любой duration: секунды для вебхуков, минуты для генерации PDF, часы для ночных выгрузок. Правило чтения простое: Timeout отвечает на вопрос «сколько живёт задача после того, как воркер её взял». Если задан только Deadline, таймаут-компонент не участвует, и обработку ограничивает сам дедлайн; если не задано ни то ни другое, постановка ставит дефолт — тридцать минут, о котором документация Enqueue говорит прямо: by default, max retry is set to 25 and timeout is set to 30 minutes. Тридцать минут — не забота, а молчаливое согласие: зависший вызов съест треть часа слота воркера прежде, чем кто-то заметит. Поле info.Timeout из TaskInfo возвращает то, что реально записалось в задачу: удобно логировать при постановке и сверять с тем, что вы думали о лимите. Ещё одна граница, которую Timeout не двигает: время ожидания в очереди. Задача с ProcessAt на полночь и Timeout(10time.Minute) будет ждать в scheduled хоть сутки — таймаут про исполнение, а не про ожидание; отсчёт начинается, когда воркер возьмёт задачу в руки.

Постановка с таймаутом

GO
info, err := client.Enqueue(task,
    asynq.Queue("reports"),
    asynq.Timeout(10*time.Minute),
)
if err != nil {
    log.Fatal(err)
}
log.Printf("task=%s timeout=%v", info.ID, info.Timeout)

Постановка отличается одной опцией: Timeout(10*time.Minute) записывает предел в сообщение задачи. Если хендлер не уложится, asynq отменяет ожидание и засчитывает неудачу — задача уйдёт в retry по обычной политике. info.Timeout возвращает записанную длительность: логируйте её рядом с ID, чтобы при разборе инцидента видеть фактический лимит.

Дедлайн задач go: опция Deadline#

Дедлайн задач go — это опция Deadline: client.Enqueue(task, asynq.Deadline(time.Now().Add(24*time.Hour))) назначает крайний момент не от начала обработки, а абсолютный. Разница в точке отсчёта определяет сценарии. Timeout описывает длительность работы: «генерация отчёта занимает не больше десяти минут». Deadline описывает смысл результата: «отчёт нужен к девяти утра; сделанный в полдень, он бесполезен, сколько бы воркер ни старался». Если заданы обе опции, победит та, что наступит раньше: попытка может успеть за таймаут, но после дедлайна всё равно провалится. Точку дедлайна считают от бизнес-события: конец рабочего дня, закрытие регистрации, момент, когда купон сгорает. Ночному отчёту с лимитом «до начала рабочего дня» ставят Deadline на девять утра по таймзоне бизнеса — с теми же правилами LoadLocation, что и у ProcessAt из пятого урока. Поле info.Deadline возвращает записанный момент; в дашборде он виден в карточке задачи, и при разборе жалоб сразу ясно, работала ли задача на живом дедлайне или его проигнорировали. Полезно развести оси и с ProcessAt из пятого урока: ProcessAt отвечает за «когда начнём», Deadline — за «когда уже поздно начинать». Для «напомнить за сутки до дедлайна заказа» обе опции встречаются в одной постановке: старт считается от даты из базы, а Deadline — сама дата заказа, после которой напоминание стало бы вредным.

Таймаут и дедлайн вместе

GO
loc, err := time.LoadLocation("Europe/Moscow")
if err != nil {
    log.Fatal(err)
}

now := time.Now().In(loc)
usefulUntil := time.Date(now.Year(), now.Month(), now.Day(), 9, 0, 0, 0, loc)

info, err := client.Enqueue(reportTask,
    asynq.Timeout(30*time.Minute),
    asynq.Deadline(usefulUntil),
)

Обе опции вместе: таймаут ограничивает длительность одной попытки, дедлайн — момент, после которого результат никому не нужен. Если попытка начнётся перед девятью и не уложится, провалится она по дедлайну; а ретрай, назначенный уже после девяти, сгорит мгновенно — контекст к его старту истёк. Победа всегда за более ранним пределом.

Проверка: конфликт пределов

Задача поставлена с asynq.Timeout(30*time.Minute) и asynq.Deadline через пять минут. Какой предел остановит обработку?

Истечение времени: контекст, а не пуля#

Главное, что стоит понимать про механику: asynq не убивает хендлер. Обе опции превращаются в дедлайн контекста, с которым вызывается ProcessTask. Когда срок истёк, воркер перестаёт ждать результата: задача засчитывается неудачей с ошибкой context deadline exceeded и уходит в retry. Горутина хендлера при этом продолжает жить, пока не вернётся сама: HTTP-вызов без таймаута внутри всё равно доделает свою тихую работу, и результат её уже никому не нужен. Отсюда дисциплина: длинные операции обязаны слушать контекст. Внешние вызовы принимают ctx первым аргументом и обрывают соединение при отмене; циклы проверяют ctx.Err() или select по ctx.Done() на каждой итерации; вместо time.Sleep — таймер и select по тому же каналу. Общий шаблон: каждое долгое действие внутри ProcessTask либо само отменяется по контексту, либо явно проверяет его между шагами. Тогда «истёк таймаут» означает, что работа действительно прекратилась, а не что асинхронный призрак догоняет ретрай. Контекст asynq создаёт, а уважать его — работа хендлера. Текст ошибки при этом предсказуем: ctx.Err() вернёт context.DeadlineExceeded, и именно он ляжет в LastErr задачи — по нему истечение узнаётся в логах мгновенно, без раскопок.

После истечения: ретрай по обычной политике#

Истечение — обычная ошибка в глазах очереди. Воркер получает context deadline exceeded, счётчик Retried растёт, паузу перед следующей попыткой считает формула из шестого урока — или ваша RetryDelayFunc, если вы её настроили. После исчерпания MaxRetry задача попадает в архив, как любая другая неудачная. Эта честность удобна: отдельный код «для задач, которые не успели» не нужен — работает вся привычная механика повторов и архива. Обратная сторона: задачам, которым повтор не поможет, истечение жжёт попытки зря. Зависший партнёрский API вернёт то же молчание и на второй, и на двадцать пятой попытке — уместнее SkipRetry или меньший MaxRetry на постановке, о чём говорит четырнадцатый урок. Показания смотреть просто: в списке retry-задач поле LastErr хранит текст последней ошибки, и если «context deadline exceeded» собирается у одного типа задач пачками, пересмотра требует лимит или зависимый сервис, а не ретраи. Планируйте и худший случай по слотам: MaxRetry(3) с Timeout(10*time.Minute) означает до четырёх попыток по десять минут — сорок минут воркерного времени на одну упорную задачу. Если такое число для типа задач неприемлемо, уменьшайте либо таймаут, либо число попыток, либо разбирайтесь, почему задача вообще не успевает.

Пределы задачи и их следствия#

ПределЧто ограничиваетЧто будет при превышении
asynq.Timeout(d)длительность одной обработкинеудача с context deadline exceeded и ретрай
asynq.Deadline(t)крайний момент обработкинеудача в момент дедлайна; попытка после него сгорает сразу
MaxRetryчисло попытокархив после исчерпания
30 минутумолчание, когда не задано ни то ни другоенеудача и ретрай, как у явного таймаута

Где хендлер обязан слушать контекст#

  • HTTP-вызовы: передавайте ctx в клиент — отмена закроет соединение.
  • Запросы к базе: контекст в Query и Exec, чтобы ретрай не конкурировал с прошлой попыткой.
  • Циклы и батчи: проверяйте ctx.Err() на каждой итерации.
  • Ожидания: вместо time.Sleep — таймер и select по ctx.Done().
  • Длинные вычисления: режьте на шаги с проверками между ними.
  • Логи: фиксируйте, на каком шаге контекст истёк, — это половина диагностики.

Проверка: судьба горутины

Дедлайн контекста истёк, а хендлер ещё не вернулся. Что делает asynq?

Что выйдет из хендлера без проверки?

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

func handleSlow(ctx context.Context, t *asynq.Task) error {
    time.Sleep(5 * time.Second) // select по ctx.Done() забыли
    return nil
}
// постановка: asynq.Timeout(2 * time.Second)

Соберите постановку с пределами

Допишите опции: длительность обработки и абсолютный крайний момент.

info, err := client.Enqueue(task, asynq.(10 * time.Minute), asynq.(time.Now().Add(24*time.Hour)), )

Соберите постановку с бизнес-дедлайном

Расставьте строки: расчёт момента, задача, срез опций, постановка, лог.

Строки

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

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

Сколько ставить: выбор значений#

Значения выбирают от обратного: не «сколько даём», а «сколько нужно, чтобы успеть, и чем рискуем, дав больше». Для обёртки над внешним вызовом таймаут равен p99 здорового ответа плюс запас: если платёжный шлюз отвечает за двести миллисекунд, двум минутам нечего делать в опции — ставьте пять секунд и ретрай. Для задач-конвейеров меряйте реальную длительность по логам хендлера, а не на глаз. Ночным задачам вместо большого таймаута часто честнее Deadline «до начала рабочего дня»: попытка успеет, а после девяти работать уже незачем. Дедлайны от событий — купон горит в полночь, регистрация закрывается в пятницу — только через Deadline: таймаут тут бессилен. Отложенная постановка не меняет пределы: ProcessIn сдвигает старт, но отсчёт таймаута всё равно начинается, когда воркер взял задачу. И помните про очередь в целом: короткие таймауты не спасут её от тысяч зависших задач, если зависимый сервис умер целиком, — тут нужны пауза очереди и алерт, о которых говорит пятнадцатый урок. Практический приём: считайте таймаут от измерений, а не от ощущений — возьмите из логов или метрик девяносто девятый перцентиль длительности, умножьте на два-три запаса и округлите до человеческого числа. Для батчей внутри одной задачи складывайте шаги: таймаут «двести писем по сто миллисекунд» — это двадцать секунд, а не сто миллисекунд от одиночной отправки.

Карточки: таймауты и дедлайны

Термин

asynq.Timeout

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

Опция постановки: длительность обработки. Истёк срок — asynq перестаёт ждать и засчитывает неудачу; задача ретраится.

1 / 6

Таймаут и дедлайн закрыты: Timeout ограничивает длительность попытки, Deadline — момент, после которого результат не нужен; оба работают через контекст хендлера и ретраятся по обычной политике. Мини-эксперимент: поставьте задачу с Timeout(2*time.Second) и хендлером, который спит пять секунд без проверки контекста, — в asynqmon она уйдёт в retry с context deadline exceeded, а лог покажет, что горутина доспала свои пять секунд; затем тот же хендлер с таймером и select по ctx.Done() — сравните логи, где горутина завершилась сразу. Привычку логировать info.Timeout и info.Deadline при постановке приклейте к постановщику раз и навсегда: при разборе инцидента эти две строки экономят час. Формат прежний: код запускается локально с Go и Redis в Docker, песочница сайта показывает тематическую программу на чистом Go без сети. Дальше по курсу: двенадцатый урок научит не ставить одну задачу дважды, шестой останется опорой при выборе числа попыток для задач с жёстким дедлайном, а ручную отмену зависшей задачи разберёт пятнадцатый.

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

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

Вывод

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

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

  • Asynq Timeout задаёт длительность обработки, Deadline — абсолютный момент; при конфликте работает более ранний предел.
  • Оба предела превращаются в дедлайн контекста хендлера: asynq перестаёт ждать, но не убивает горутину.
  • Истечение — обычная ошибка: Retried растёт, паузу считает формула ретраев, после исчерпания попыток — архив.
  • Без явных опций постановка ставит дедлайн контекста в тридцать минут.
  • Хендлер обязан слушать контекст: вызовы с ctx, проверки в циклах, таймеры вместо Sleep.
  • Попытка, начавшаяся после абсолютного Deadline, сгорает мгновенно до запуска хендлера.
  • info.Timeout и info.Deadline из TaskInfo показывают записанные пределы — логируйте их при постановке.
  • Таймаут не ограничивает ожидание в очереди: отсчёт начинается, когда воркер взял задачу.
  • Худший случай по слотам — попытки умножить на таймаут: планируйте бюджет воркерного времени.
  • Значения выбирают от p99 внешних вызовов и бизнес-смысла результата, а не «с запасом».

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

Возьмите хендлер из прошлых уроков и поставьте задачу с Timeout(3*time.Second) и Deadline на пять минут вперёд. Разбейте работу на шаги с проверкой ctx.Err() и ожиданием через таймер с select по ctx.Done(). Убедитесь по логам: при коротком таймауте задача уходит в retry с context deadline exceeded, а горутина завершается по контексту. Затем поставьте задачу с Deadline в прошлом и посмотрите, как попытка сгорает мгновенно.

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

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

Что будет, если задать только Deadline без Timeout?

Ограничением станет сам дедлайн: контекст истечёт в назначенный момент, сколько бы задача ни работала. Таймаут-компонент при этом не добавляется: если дедлайн через сутки, обработка теоретически может длиться сутки — для долгих задач это способ сказать «живи до полуночи и ни минутой дольше». Если нужны и длительность, и крайний момент — задавайте обе опции: более ранний предел сработает сам.

Отменяет ли asynq операцию в базе при истечении таймаута?

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

Можно ли поменять таймаут у уже поставленной задачи?

У клиента такой опции нет: пределы записываются при постановке. Практичный путь — Inspector: задачу можно удалить и поставить заново с новыми пределами, а активную — отменить сигналом, после которого сработает обычный ретрай. Оба приёма разбирает пятнадцатый урок курса.

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

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

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

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

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

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

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