Подписка уже продлена, а бот всё равно просит оплатить
У отложенных уведомлений есть неприятный сценарий: задача выполняет решение, принятое несколько минут назад, хотя состояние подписки уже изменилось.
Рассмотрим Telegram-бота, который сам хранит сроки доступа и отправляет уведомление после их окончания. Условный пример:
— 12:00 — доступ закончился, планировщик создал задачу «предложить продление»;
— 12:01 — отправка получила 429 с retry_after=120 и перенесена на 12:03;
— 12:02 — пользователь оплатил следующий период, обработчик платежа зафиксировал новый access_until в БД;
— 12:03 — воркер повторил отправку старого сообщения: «Ваш доступ закончился. Продлите подписку».
В документации Telegram retry_after означает, сколько секунд нужно подождать до повторного запроса. Успех следующей попытки он не гарантирует. За время ожидания основание для уведомления может исчезнуть.
Поэтому в очереди я бы хранил контекст задачи:
subscription_id — идентификатор конкретной подписки
expected_access_until — срок доступа, для которого создано уведомление
not_before — время, раньше которого отправку не предпринимаем
valid_until — момент, после которого напоминание теряет смысл по правилам продукта
Перед каждой попыткой воркер читает состояние из источника, где видно уже зафиксированное продление. Например, с primary в новой короткой транзакции. Отстающая реплика или старый снимок транзакции могут вернуть прежний срок доступа, даже если платёж уже обработан.
Для нашего примера условия отправки такие:
— not_before <= now < valid_until;
— текущий access_until совпадает с expected_access_until;
— текущий access_until <= now;
— правила продукта разрешают предложить продление этой подписки.
Время сравниваем в одной временной шкале, например UTC.
Если срок доступа изменился, задача создана для прежнего состояния. Её отменяем, а необходимость и время нового уведомления определяем по текущим данным. Изменение access_until не обязательно означает новый период: это может быть компенсация или административная корректировка.
Если valid_until уже наступил, задачу пропускаем. Если БД недоступна, отправку откладываем с учётом этого дедлайна. Ошибка чтения сама по себе не означает, что доступ закончился.
Удаление старой задачи из очереди может сократить лишнюю работу, но проверку в воркере я бы всё равно оставил: задачу уже могли забрать на выполнение.
Актуальность и защиту от дублей нужно проверять отдельно. Даже напоминание, отправленное один раз, может содержать устаревшую информацию. Ключ вроде subscription_id + expected_access_until + notification_type помогает группировать повторы задачи для одного срока доступа, но сам по себе не гарантирует единственную отправку в Telegram.
Остаётся окно между финальным чтением БД и sendMessage: продление может быть обработано именно в этот момент. Проверка уменьшает окно гонки, но не закрывает его. Координация операций по подписке позволяет упорядочить решения внутри системы; одна транзакция БД внешний вызов Telegram не охватывает. И даже корректное на момент отправки сообщение может устареть до прочтения.
Для проверки я бы взял продление до запуска воркера, во время retry_after и между чтением БД и отправкой. Отдельно — наступление valid_until и недоступность БД.
Пропуски с причинами access_until_changed и deadline_expired стоит учитывать отдельно от ошибок отправки: они показывают, сколько работы устарело в очереди.
Поделитесь как вы разделяете продление между проверкой и отправкой? Координируете операции по subscription_id или допускаете это окно и делаете текст уведомления менее категоричным?