Какие альтернативы есть для отложенного запуска задач в Celery
Что проверяют
Знаете ли вы, как Celery исполняет задачи с countdown и eta и чем это грозит при задержке в часы и дни. Следом часто дают задачу: напомнить пользователю о чём-то через трое суток.
Как отвечать
Внутри Celery есть apply_async(countdown=60), apply_async(eta=...) и периодические задачи через celery beat. Важная деталь: задача с ETA сразу уходит воркеру и ждёт своего времени у него в памяти. Для минут это нормально, для суток нет. С Redis и SQS при задержке больше visibility_timeout (у Redis по умолчанию час) сообщение вернётся в очередь и выполнится повторно, у RabbitMQ похожие ограничения даёт consumer_timeout.
Альтернативы:
- срок в базе: таблица с полем
run_at, периодическая задача раз в минуту забирает созревшие записи черезSELECT ... FOR UPDATE SKIP LOCKED. Переживает рестарты, видна в админке, отменяется удалением строки; - RQ:
queue.enqueue_in(timedelta(...), func)иenqueue_at, воркер запускается с флагом--with-scheduler; - Dramatiq:
send_with_options(delay=...)с задержкой в миллисекундах, отложенные сообщения идут через отдельную очередь; - Huey:
task.schedule(delay=...)или сeta, есть@periodic_task, работает на Redis или SQLite; - arq и Taskiq: для asyncio-проектов, в arq это параметры
_defer_byи_defer_until; - Procrastinate: очередь прямо в PostgreSQL, если отдельный брокер заводить не хочется;
- APScheduler: планировщик внутри процесса, а не распределённая очередь.
Где спотыкаются
Ставят countdown на сутки и не знают про повторную доставку после visibility_timeout. В итоге пользователь получает одно и то же письмо дважды.
Запускают APScheduler внутри Gunicorn с несколькими воркерами. Каждый процесс поднимает свой планировщик, и задача выполняется столько раз, сколько воркеров.
Не делают задачу идемпотентной. Любой механизм отложенного запуска гарантирует доставку «хотя бы один раз», поэтому задача сама проверяет, не выполнена ли она уже.