Consul, Lusnoc, Rufus и фоновые задачи

В прошлой статье я обещал рассказать о конкретном использовании распределенных блокировок с использованием нашей собственной Ruby-библиотеки Lusnoc. Про это и поговорим.

Солдат спит - служба идет

О чем речь?

В большом количестве проектов часто требуется выполнять "регулярные задачи". Вот небольшой список регулярных задач, который я собрал по нескольким нашим проектам:

  • обновление курсов валют, каких-то статических списков, баз из чужих источников или проверка обновлений
  • регулярная очистка старых данных в базе или какая либо их пометка
  • ежедневная/еженедельная/ежемесячная рассылка
  • всё что раньше принято было класть в cron и управлять через whenever

Некоторые задачи можно выполнить несколько раз подряд безо всякого вреда, но такие как рассылка еженедельных дайджестов должны выполняться строго один раз.

Для наших любимых "бэкграунд процессоров" sidekiq и delayed job(❤️) уже написаны гемы решающие эту проблему тем или иным способом. Но, как обычно, хочется собственного 🚲 обобщенного решения ведь у нас есть как проекты с sidekiq так и с delayed_job и ходят легенды про обычные скрипты, оставшиеся в заброшенных уголках крона...

Структура стандартного проекта

Для того чтобы всё это гармонично работало у нас есть типовая структура информационной системы:

  • web - сервис, исполняющий Rails приложение
  • jobs - сервис который крутит sidekiq или delyaed job или что-то подобное
  • assistant - сервис, который "живет постоянно" и держит соединение с RabbitMQ или Kafka
  • 👉scheduler - сервис, который ставит регулярные задачи
  • вокруг еще есть редисы, брокеры и базы, но они вроде как не входят в "проект"

Для реализации сервиса schedulerмы используем гем Rufus который исправно выполняет свои функции на нескольких проектах.

Обновялем курсы валют 1 раз в час

У Rufus::Scheduler есть куча параметров таких как управление многопоточностью, обработка ошибок и многое другое, но мы его используем не для выполнения задач, а для постановки - выполнение происходит в других сервисах.

Находим себе проблемы и побеждаем их!

В RNDSOFT все сервисы (по крайней мере в современных проектах) уже запакованы в контейнеры и запущены в нескольких экземплярах для обеспечения отказоустойчивости. И именно тут кроется проблема - если мы запустим 2 контейнера scheduler то все задачи будут запущены 2 раза...

И здесь нам на помощь приходит мьютекс, да не простой, а межпроцессный! О его реализации я рассказывал в прошлой статье. Идея простая - назовем право ставить задачи ресурсом и тогда можно применить классическое эксклюзивное владение:

Чуть что - сразу в могилу

Работаем пока владеем ресурсом. В случае любых ошибок, таймаутов, потери владения - завершаемся и другой процесс, ожидающий блокировку, берёт на себя работу. Если при запуске не смогли завладеть ресурсом - тоже завершаемся. При любом завершении наш движок контейнеризации Dockerперезапустит контейнер.

Заключение

В жизни скрипт немного сложнее, лучше затюнен Rufus, есть обработка сигналов семейства SIGTERM и надежнее обработка ошибок - ведь тут самое важное быстрее завершиться при любом отклонении от нормы.

Задачи тоже бывают разные - мы рассмотрели одну из самых очевидных, но у меня есть гораздо более извращенный пример, когда рабочий поток порождался в каждом (КАЖДОМ, Карл!) процессе рельсы (имеется ввиду prefork модель как в Unicorn и Passenger) и эти процессы через Lusnoc боролись за право выполнять эксклюзивное действие. Так себе идея, но работает 💪