Как решить проблему отправки пушей пользователям в разных часовых поясах в удобное время
Что проверяют
Умеете ли вы правильно работать со временем и спроектировать планировщик на миллионы пользователей, а не на один print. Следом обычно спрашивают про переход на летнее время и про то, как не положить сервис рассылкой в одну минуту.
Как отвечать
Базовые правила хранения:
- часовой пояс пользователя: IANA-идентификатор вроде
Europe/MoscowилиAmerica/New_York, а не смещение+3. Смещение меняется при переходе на летнее время, идентификатор нет; - моменты в базе только в UTC: колонка
timestamptz, в коде толькоdatetimeс поясом; - библиотека: стандартный
zoneinfo(Python 3.9+).pytzустарел, и его главная ловушка:datetime(..., tzinfo=pytz.timezone(...))даёт историческое смещение LMT вместо нужного.
Схема отправки: для каждого уведомления считаете локальное «завтра в 10:00» в поясе пользователя, переводите в UTC и сохраняете как send_at. Воркер раз в минуту забирает созревшие записи через SELECT ... WHERE send_at <= now() LIMIT 1000 FOR UPDATE SKIP LOCKED, так несколько воркеров не возьмут одно и то же. Периодический запуск делает Celery beat, APScheduler или cron.
Детали, которые показывают опыт:
- переход на летнее время: 02:30 в ночь перевода может не существовать или случиться дважды,
zoneinfoразличает это через атрибутfold; - тихие часы: если вычисленное время попало на ночь, сдвиг на утро;
- размазывание нагрузки: случайный разброс в пределах 10-15 минут и пакетная отправка в FCM и APNs, чтобы не упереться в их лимиты;
- выбор времени: для начала по активности пользователя, дальше A/B-тест по открываемости.
Где спотыкаются
Ставят каждому пользователю задачу Celery с eta на завтра. Такие задачи висят в памяти воркера, а с Redis-брокером при превышении visibility_timeout выполняются повторно.
Используют datetime.now() без пояса. Результат зависит от пояса сервера, и при переезде в контейнер с UTC все пуши сдвигаются.
Хранят только смещение от UTC. Полгода всё работает, а после перевода часов в одной из стран уведомления приходят на час раньше.