Skip to content

fix: конечный retry с backoff и AbortSignal для attachment.not.ready#235

Open
smelukov wants to merge 1 commit intomax-messenger:mainfrom
smelukov:fix/attachment-not-ready-retry
Open

fix: конечный retry с backoff и AbortSignal для attachment.not.ready#235
smelukov wants to merge 1 commit intomax-messenger:mainfrom
smelukov:fix/attachment-not-ready-retry

Conversation

@smelukov
Copy link
Copy Markdown

Проблема

При отправке сообщения с вложением сервер может вернуть ошибку attachment.not.ready, если файл ещё обрабатывается. Текущая реализация MessagesApi.send обрабатывает это бесконечной рекурсией без лимита попыток:

if (err.code === 'attachment.not.ready') {
  console.log('Attachment not ready');
  await setTimeout(1000);
  return this.send({ ... }); // бесконечная рекурсия
}

Это приводит к трём проблемам:

  1. Утечка ресурсов. Если потребитель прерывает ожидание (например, через таймаут), исходный промис send() продолжает рекурсивно вызывать API бесконечно. Эти «зомби»-промисы невозможно остановить снаружи, и они живут до завершения процесса.

  2. console.log в библиотеке. Прямой вывод в console.log ломает вывод потребителям (прогресс-бары, структурированные логи). Остальные модули SDK используют debug.

  3. Отсутствие backoff. Фиксированная задержка 1 с при каждой попытке не снижает нагрузку на сервер при длительной обработке тяжёлых файлов.

Пример

Бот загружает видео через upload.video(), затем отправляет сообщение с полученным токеном. Сервер отвечает attachment.not.ready - возможно видео ещё обрабатывается. SDK начинает бесконечный retry-цикл. Бот, не дождавшись ответа, прерывает ожидание по таймауту и перезагружает видео. Повторная отправка проходит успешно. Но первый send() продолжает рекурсивно стучаться в API в фоне - бесконечно, без возможности остановки. В логах после завершения импорта видны сотни строк Attachment not ready, генерируемые «зомби»-промисами.

То, что повторная загрузка того же видео завершается быстро (а первая может зависнуть навсегда), может указывать на проблему на стороне сервера — возможно, первый запрос «застревает» в обработке, а повторный уже использует закешированный результат. Тем не менее, клиент не должен зависать бесконечно в любом случае.

Решение

Конечный цикл с backoff

Рекурсия заменена на for-цикл с лимитом 10 попыток и экспоненциальной задержкой (1 с → 2 с → 4 с → ... → 512 с). При исчерпании попыток выбрасывается MaxError с кодом attachment.not.ready.

debug вместо console.log

Логирование через debug('one-me:messages'), как во всех остальных модулях SDK.

Поддержка AbortSignal

sendMessageToChat и sendMessageToUser теперь принимают опциональный signal: AbortSignal в extra. Signal:

  • Проверяется перед каждой итерацией (signal.throwIfAborted())
  • Отменяет задержку между попытками (node:timers/promises setTimeout с { signal })
  • Передаётся в fetch для реальной отмены HTTP-запроса

Это позволяет потребителю гарантированно прервать ожидание без утечки ресурсов.

Обратная совместимость

Все изменения полностью обратно совместимы:

  • signal опционален - без него поведение не меняется (кроме конечного цикла вместо бесконечного)
  • Публичные API-сигнатуры расширены, не изменены
  • При исчерпании попыток выбрасывается тот же MaxError с тем же кодом

@smelukov smelukov requested a review from a team as a code owner March 15, 2026 17:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant