Рассылка оповещений по расписанию при помощи sidekiq и cron

Illustration of a person sitting on the floor leaning against a wall, absorbed in reading a book, with a spotlight effect highlighting them. Illustration of a person sitting on the floor leaning against a wall, absorbed in reading a book, with a spotlight effect highlighting them.

Не всегда удаётся сфокусировано учиться каждый день. Периодически фокус теряется аж на несколько дней и начинается этап активной прокрастинации -- к занятиям никак не получается вернуться. Начинается рационализация безделья -- нет времени, куча дела и т.д. В mkdev менторы пинают прокрастинаторов при любой возможности. Но, признаюсь честно, не всегда получается вовремя заметить, что тот или иной ученик отбился от рук.

Я очень люблю автоматизировать всё, что можно автоматизировать, поэтому в этой статьей мы автоматизируем пинки для дорогих учеников при помощи cron, гема whenever. Прежде чем продолжить чтение, убедитесь что вводная статья по whenever, написанная Евгением Бурмакиным для mkdev уже изучена и общее представление о том, что мы сейчас будем делать в пределах кода mkdev уже имеется.

Когда был активен пользователь?

Итак, нам нужно отправлять оповещения тем ученикам, которые не появлялись на mkdev больше двух дней. В Devise, используемый нами гем для аутенфикации, уже встроено отслеживание даты последнего логина, которая хранится в колонке last_sign_in_at. Тем не менее, есть вероятность что ученик просто напросто не выходил с сайта с тех пор, поэтому основываться на этой дате будет не разумно.

Придётся отслеживать самим. Для этого добавим поле last_active_at, которое будем обновлять каждый раз, когда ученик открывает страницу, связанную с заданиями. Решается такая "задача" элементарно -- добавлением before_action в UserTasksController:

  def update_last_active
    current_user.touch :last_active_at
  end

Настраиваем whenever

Теперь нам необходимо добавить новую cron задачу: раз в день брать всех учеников, которые неактивны больше двух дней и отправлять им письмо. Следуя инструкциям из нашей статьи про whenever, добавляем гем в Gemfile и выполняем в папке с проектом wheneverize .. В свежесозданном файле config/schedule.rb пропишем следующее правило:

every 1.day do
  runner "User.notify_inactive"
end

Рассылаем оповещения

Теперь осталось лишь написать метод notify_inactive в модели User. Код простейший:

# app/models/user.rb

# ...
  def self.notify_inactive
    customers.where("last_active_at < ?", 2.days.ago).each do |user|
      NotificationsMailer.delay.inactive_notice(user)
    end
  end
# ...

Конечно, необходимо теперь добавить NotificationsMailer, реализовать в нём метод inactive_notice, который и будет рассылать оповещения. Обратите внимание на метод delay: мы используем sidekiq для рассылки почты на фоне, который предоставляет этот удобный метод.

Не буду приводить здесь код Mailer'a -- там нет ничего особенного (честно). Лучше расскажу ещё, как деплоить этот новый код.

Деплоим

Внимание! Текст дальше будет сложен для понимания, если у вас нет представления о том, что такое chef. Не волнуйтесь. Это очень advanced случай, который, тем не менее, нельзя было совсем опустить в данной статье.

whenever очень удобно деплоить при помощи capistrano. Но мы не используем capistrano в mkdev, мы используем chef. Это значит, что нам необходимо обновить наш кукбук для деплоя приложения. Я сделал это добавлением в блок before_restart нужной команды, теперь он выглядит так:


# ...
  before_restart do
    execute "cd #{release_path} && bundle exec whenever --update-crontab '#{node['app']['name']}_production'"
  end
# ...

before_restart это часть ресурса deploy, включённого в Chef по-умолчанию и деплоящего приложения по той же схеме что и capistrano.

Чему мы научились

Теперь вы знаете, насколько просто создавать периодические задачи в Rails при помощи whenever. Пожалуй, делать это даже слишком просто и в отдельной статье, возможно, не было необходимости. Тем не менее, я хотел показать пример использования в конкретном приложении, а так же показать все задействованные в процессе технологии.