cron на fly.io

В будь-якому проекті є задача періодично запускати процеси. Наприклад, в @Donate1024Bot це щоденна ранкова розсилка всім підписникам нового збору, щоденна розсилка звітів, оноволення статистики всіх монобанок щопівгодини та інше.

В Heroku для цього є супер-зручна штука що називається Scheduler. В інтерфейсі наклікуєш коли запускати задачу, команду яку потрібно виконати і все. Далі Heroku сам в потрібний час підніме інстанс та запустить джобу. Я постійно користувався цією функцією.

Коли переїхав на fly, то виявилося, що там такого немає. В кінці вересня вони додали схожу функціональність, але вона не є повноцінною заміною крону.

Тому довелося педалити милиці власноруч. В більшості базових образів cron вже встановлено, у мене це ruby:3.1.2-alpine. Далі робимо текстовий файл з нашими джобами:

# Send new post MON to SUN at 9:00
0 7 * * * cd /app && ./bin/bundle exec rake donate:daily

# update mono jars every half of a hour
0,30 * * * * cd /app && ./bin/bundle exec rake donate:update_mono_jars

В Dockerfile ініціалізуємо цей крон:

COPY crontab.txt /app/crontab.txt
RUN crontab /app/crontab.txt

Потім потрібно зробити на fly.toml процес власне крону:

[processes]
  web = "bin/rails server -p 8080"
  worker = "bundle exec sidekiq -c 3"
  cron = "crond -f -d 0"

Все. Після деплою з'явиться новий інстанс всередині якого буде бігти крон.

З таким підходом у нас постійно буде крутитись окрема машина для цього, це звичайно недолік. Щоб оптимізувати, можна запускати крон разом з, наприклад, вебом через стартовий скрипт або systemd, але я не хотів цим запарюватися.

Також потрібно зауважити що всі джоби мають коректно відпрацьовувати повторні або паралельні запуски. Конкретно я для цього використовую локи в базі даних: Lock.find_by(key: "daily_post").with_lock do, щоб гарантувати єдиноразовий запуск, та не трапилося такого, що користувачі два рази отримують одне й те саме повідомлення.

Також сама крон джоба не містить коду який працює, а просто кладе в чергу Sidekiq джобу яка вже буде оброблятися на іншому інстансі. Таким чином ми можемо виділити крон-процесу мінімум пам'яті, бо все що він робить—це ставить завдання у чергу.

В проектах з мікросервісами крон це проблема, тому що там теж треба якимось чином гарантувати єдиноразовий запуск та коректно обробляти повторні запуски однієї й тієї ж самої джоби. На попередньому проекті де був AWS для розподіленого крону ми використовували CloudWatch Event які тригерили лямбду, яка клала джобу в чергу або тригерила якийсь ендпоїнт. Чесно кажучи це цирк на дроті, а не нормальне рішення. Можливо зараз амазон дає щось краще. Також в кубернетісі та номаді є крон джоби, тому можливо ця задача вирішується простіше, аніж встановлення спеціального рішення типу Quartz або Sidikiq Enterprise, де є функціональність розподіленого крону. Впевнений, що читачі підкажуть.


Сподобалось? Долучайтеся до мого телеграм каналу: https://t.me/full_of_hatred