Railsで定期処理を回す(rakeタスク, cron)

ブログアプリでは、記事の状態を「下書き」「公開待ち」「公開」などで分けて管理していることが多いと思います。
今回はこの「公開待ち」を指定時間になったら「公開」する、いわゆる予約投稿の処理についてまとめます。

予約投稿は、「公開待ち」状態の記事のうち、「公開日が現在の日時より過去になっている記事」を「公開」状態にする処理を定期的に実行することによって実現します。

これは以下の2つの要素で実装することになります。

  • 現在時刻と公開日を比較して状態を変更する処理(バッチ)
  • バッチを定期的に実行する仕組み(cron)

railsバッチ処理を書く際によく使われるものにrake taskとRails runnerがあります。
今回はrake taskで実装します。

Rakeタスクとは

まずrakeとはrubyで書いたビルドツールのことです。
makeでいうMakefileの代わりに、Rakefileというファイルを作成して実行することができます。 そしてRakefileで定義される処理を「Rakeタスク」と呼びます。

cronとは

UNIX系のOSに標準で備わっていて、定期的にコマンドを実行するデーモンプロセスです。 このcronに対して命令を行うには、crontabというコマンドを実行します。

そして、このcronの命令の記述を簡単に行うのがwheneverというgemになります。

Rakeタスクの実装

1.rakefileを作成する

 # rails g task [namespace] [task_name]
 $ rails g task article_state update

2. rakefileにタスクを記述する

lib/tasks/article_state.rake

# 名前空間
namespace :article_state do
  # タスクの説明
  desc '公開日時を過ぎた「公開待ち」状態の記事を「公開」状態にする'
  #タスクの名前
  task update: :environment do
    # 処理内容
    Article.publish_wait.find_each do |article|
      if article.published_at <= Time.current(
        article&.published!
    end
  end
end 

task update: :environment はupdateがタスク名になります。
:environment はupdateタスクを実行される前にrailsアプリケーションを呼び出してくれます。 これがないとRails側のモデルやクラスを扱えません。

3. タスクが登録されているかを確認する。

$ bundle exec rake -T

rake article_state:update        #公開日時を過ぎた「公開待ち」状態の記事を「公開」状態にする

#これで表示される中に作成したタスクが表示されていればOK
#パイプライン処理で検索かけるが吉

4. Rakeタスクを実行できるか確認

$ bundle exec rake article_state:update

エラーが出なければとりあえずOK
実際の処理内容は目視なりRSpecなりで確認する。

Rakeタスクをcronで定期実行する

cronの管理にwheneverを使用するのでそのインストールからやっていきます。

1. whenever インストール

Gemfile
gem 'whenever', require: false
$ bundle install

以下のコマンドで、初期のconfig/schedule.rbファイルが作成されます。

$ bundle exec wheneverize .

2. schedule.rbで定期処理を設定する

今回は30分毎に先程のタスクを走らせたいので

config/schedule.rb
# Rails.rootを使用するために必要
require File.expand_path(File.dirname(__FILE__) + '/environment')
# cronを実行する環境変数
rails_env = ENV['RAILS_ENV'] || :development
# cronを実行する環境変数をセット
set :environment, rails_env
# cronのログの吐き出し場所
set :output, "#{Rails.root}/log/cron.log"

#30分ごとにrakeタスクを実行する
every 30.minutes do
  rake "article_state:update"
end

require File. ~~Rails.rootを使用するための記述です。
wheneverはあくまでrubyの機能なので使いたい場合はこれが必要になります。

実行する処理(job)については3つ標準で用意されています。

rake:  #rake taskを実行する
runner:  #rails runnerを実行する(rails内のメソッドを実行できる)
command: #シェルコマンドを実行する。

3. crontabを更新する

$bundle exec whenever --updete-crontab 
[write] crontab file updated

このコマンドはconfig/schedule.rbを探すためプロジェクトのルートで実行する必要があります。
これでcrontabが更新されました。
確認は以下

$crontab -l


# Begin Whenever generated tasks for: (project_root)/config/schedule.rb at: 2021-06-14 01:47:15 +0900
0,30 * * * * /bin/bash -l -c 'cd ( :path ) && RAILS_ENV=development bundle exec rake article_state:update --silent >> (Rails.root)/log/cron.log 2>&1'

# End Whenever generated tasks for: (project_root)/config/schedule.rb at: 2021-06-14 01:47:15 +0900

:pathには特に指定しない限りwheneverを実行した時のカレントパスが入ります。 つまりRails.rootと同じになります。

余談

cron これは、「クロン」または「クーロン」と呼ばれます日本では。
英語圏の方は「クローン」と発音してますね。
lとrの区別ができない日本人にはcloneと区別できないので「クーロン」になったとかなんですかね。