Как лучше всего уведомлять пользователя об истечении подписки
У меня есть телеграм бот, в котором у меня есть пользователи, которые имеют подписку на бота. Я хочу уведомлять пользователя, если до истечения подписки остаётся к примеру неделя. Как мне лучше поступить? Просто перебирать всю таблицу (postgresql), выбрав платных подписчиков, и проверять, что если осталось 2 недели или 1 неделя, бот сообщает об этом пользователю.
Для этого есть идея проверять через select for update skip locked
, или использовать Redis с его message stream (стримы). Затем использовать это через celerybeat.
Подскажите, с какой периодичностью мне следует делать эти проверки, какие есть практики для этого, чтобы это было всё оптимизировано. Какой вариант следует использовать?
Ответы (1 шт):
Традиционная задача Шедулера/Планировщика :)
Есть разные варианты решения, опишу парочку:
1. Простой вариант для слабого сервака
Пока число пользователей не достигает астрономических значений и скейлинг сервиса не требуется, можно работать в один экземпляр приложения на слабом серваке. По шагам примерно так:
- Фоновая джоба периодически запускается (например, посредством
Celery beat
). - Извлекает из БД небольшой набор пользователей, которым пора отправить письмо, и оно не было отправлено в последнюю неделю.
- Отправляет письма пользователям из списка, пишет в БД дату-время этой отправки.
- Циклически возвращаемся к п.1. Если запрос больше не возвращает пользователей, джоба успешно завершается.
Исходный запрос к БД примерно такой:
SELECT users.id
FROM users
WHERE
(users.paid_until - NOW()) < DAYS(14)
AND (users.last_payment_notification - NOW()) < DAYS(7)
LIMIT 10
Здесь можно поставить метрики для отслеживания длительности выполнения этого цикла, а также прерывание, если его длительность приблизилась к периодичности запуска – во избежание дублирования.
2. Вариант шустрее с масштабированием
- Фоновая джоба периодически запускается.
- Аналогично извлекает нужных пользователей из БД, но уже можно не лимитировать.
- А затем эта основная регулярная джоба создаёт задания с порциями из наборов пользователей, которые отправляются в брокер задач. На этом основная джоба-планировщик завершается.
- Затем уже брокер задач вызывает нужные функции у воркеров, которые могут быть как отдельными потоками или процессами или даже разными шардами/серверами, обеспечивая максимум производительности.
Насчёт локов, блокировки строк – это нормальная практика, но можно запариться и выстрелить себе в ногу, если где-то сервис упадёт / перезапустится/потеряется связь с БД, и какие-то строки не будут разблокированы... Здесь надо полагаться на свои компетенции в конкретной СУБД. И нужно понимать, что лочить – если пользователя целиком, то можно сломать много других операций, в т.ч другие планировщики.
3. Вариант с гарантией предотвращения дублей
Без потребности не буду расписывать детально по шагам, тем более, здесь есть разные вариации. В чём суть: дополняем 2й подход дополнительной таблицей, созданной специально для задач планирования (или одной для всех задач, тогда primary key будет user-id + job-name), и храним стейт задач по каждому пользователю в ней. Можно и колонками разными, с датами и/или bool, можно и лочить. И дополнить исходный запрос проверкой этой таблицы – выявлять случаи, когда какая-то джоба взяла пользователя в работу, но не отписалась, что задачу закончила.
Для отправки писем такие проверки не сильно важны, но весьма актуальны для финансовых транзакций, списывания абонентских плат и т.п.
Совет: хранить всё состояние (например, уже обработанных пользователей) в БД или в ином персистентном хранилище брокера задач, а не в переменных оперативной памяти – это позволит минимизировать риски дублей при падении сервиса, а также упрощает масштабирование.
Update, насчёт лимитирования в запросе: можно и 100, и 10к поставить, такие параметры вместе с частотой обновления нужно подбирать глядя на потребности бизнеса и статистику вроде числа юзеров и длительности выполнения задач. Можно и вообще не ограничивать если пользователи расти до сотен тысяч не планируют. Основная цель лимитирования – не упасть и не зависнуть в случае инцидентов, например если окажется что планировщик почему-то пару недель не работал, и пользователей накопилось так много, что БД будет долго отвечать, или питонячий процесс будет выполняться дольше периода запуска, что приведёт к дублированию.
Доводилось возиться с подобными алгоритмами в крупных компаниях, где были и спецы, у которых можно было учиться, и большие объёмы данных, клиентов, требующие готовности ко всем негативным случаям (: