やること
例えば「毎日0時に、売り上げデータをリセットする」、「1時間おきに株価を取得してDBに書き込む」といった定期実行をRailsで実現します。
ツール
基本的に定期的に何かのプログラムを自動で実行するにはcronというものを使います。
「cronとは、多くのUNIX系OSで標準的に利用される常駐プログラム(デーモン)の一種で、利用者の設定したスケジュールに従って指定されたプログラムを定期的に起動してくれるもの。」です。
詳細はこちらのサイトにあります。
このcronに「どういった頻度で実行するのか」と「何のプログラムを実行するのか」の2点を記述します。このcronを直接自分で書くのもいいですが、独特の記述方法があり素人にはなかなか書けるものではありません。
そういう人向けに「Whenever」というgemがあります。
wheneverを使うと、cronに記述するためのファイルの生成や、それをcronに転記するといった作業をやってくれます(いわゆるcronへの翻訳者といった感じです)
ちなみに↓がcronの中身です。「一分毎に実行」 と「実行ファイル名」を書いてありますが、自分で書ける気がしません。
* * * * * /bin/bash -l -c 'cd /Users/username/Desktop/rails/appname && RAILS_ENV=development bundle exec rake midnight_reset:reset_flag --silent >> log/crontab.log 2>&1'
前提
macOS catalina
場合によってはcronやiTermにフルディスクアクセスを許可する必要があるかもしれません。
[設定]→[セキュリティとプライバシー]よりプライバシータグを開き、フルディスクアクセスに追加してやってください
Rails 5.2を使用しています。
Rails5以降はrakeで実行するコマンドはrailsでも実行できるようになっていますので、ここではrailsコマンドを使います。
今回やること
メンバー参加を登録するプログラムを作っています。
このプログラムには、Memberというモデルがあり、メンバーが参加するとvisitがTRUEになり、paymentやtaskを記載するようなプログラムです。
日付が変わる時に、参加記録をリセット(visitをFALSEに、taskをnilに)します。
手順
wheneverの導入
Gemfileにwheneverのgemを使うことを書き入れ、bundle installします。
Gemfile
gem 'whenever', require: false
$ bundle install
タスクの登録
冒頭で述べたとおり、最終的にはcronに「どういった頻度で実行するのか」と「何のプログラムを実行するのか」の2点を記述するのが定期実行プログラムのキモです。
まずは、「何のプログラムを実行するのか」のプログラム部分を書いていきます。
今回は、Member一人一人のレコードを呼び出し、mode, visit, task, paymentをnilにするプログラムを書きます。
処理したい内容は↓これ
Member.find_each do |member|
member.update!(mode:nil,visit:nil,task:nil,payment:nil)
end
さてRailsでは、このプログラム(= 処理のかたまり)をタスクといいます。
タスクファイルはlib/tasks以下にrakeファイルとして作成します。
ここではmidnight_reset.rakeという名前でrakeファイルを作成し、内容を以下のように記述しました。
app/lib/tasks/midnight_reset.rake
desc "mode、visit, task, paymentをリセット"
task :reset_flag => :environment do
#ログ
logger = Logger.new 'log/midnight_reset.log'
#ここから処理を書いていく
Member.find_each {|member| member.update!(mode:nil, visit:nil, task:nil,payment=nil)}
#デバッグのため
p "ここまでOK"
end
desk
はこのタスクのメモ書きです。
task :reset_flag => :environment do
〜end
の間に今回実行したい内容を記載します。
:reset_flagというのがタスク名です。タスク名は好きに付けれます(シンボル形式です)。省略することはできません。(省略するとエラーになります)
=> :environmentも省略できませんので、これはこのまま記載してください。
logger = Logger.new 'log/recover_user_life.log'
wheneverもcronも、何も設定しなければ自動でログをとってくれません。
ログはなくてもrake taskを実行できますが、エラー起きた時に確認する術がありません。
なので、ログファイルを作成しておきましょう
rake taskの確認と単発実行
まずは定期実行ではなく、railsから呼び出して実行します
Rakeタスクの一覧を見ましょう。
※Rails5以降はrakeコマンドはrailsコマンドと統合されていますので、railsでもrakeでもどちら使っても構いません。
rails -T
ずらずらーっと出てくると思いますが、その中の1つに、先ほど定義したタスクも以下のように出てくると思います。
rails reset_flag # mode、visit, task, paymentをリセット
タスクを確認できたら、単発で実行してみましょう。
$ rails reset_flag
タスクが実行できたらひとまず成功です。
rakeファイルの記載方法
先ほどはrakeファイルにひとつのタスクのみ記載しましたが、タスクを複数記載したい場合もあると思います。
rakeファイルを新しく作ってもいいですが、ひとつのrakeファイルにタスクを複数記載することも可能です。
desc "mode、visit, task, paymentをリセット"
task :reset_flag => :environment do
#ログ
logger = Logger.new 'log/midnight_reset.log'
#ここから処理を書いていく
Member.find_each {|member| member.update!(mode:nil, visit:nil, task:nil,payment=nil)}
#デバッグのため
p "ここまでOK"
end
desc "二つ目のタスクを追加してみる"
task :test_flag => :environment do
p "テストフラグ"
end
さて2つのタスクを記載しましたが、このままrails -T
を実行すると50音順に出てくるため、タスクがバラバラになってしまって管理しづらいです。
そこで、同一rakeファイルにあるタスクはグループでまとめてあげましょう。(rakeファイルがメインメニューとしたらタスクはサブメニューみたいな感じです)
やり方は2つのタスクをnamespace:rakeファイル名〜end
で囲ってやるだけです。
namespace :midnight_reset do
desc "mode、visit, task, paymentをリセット"
task :reset_flag => :environment do
#ログ
logger = Logger.new 'log/midnight_reset.log'
#ここから処理を書いていく
Member.find_each {|member| member.update!(mode:nil, visit:nil, task:nil,payment=nil)}
#デバッグのため
p "ここまでOK"
end
desc "二つ目のタスクを追加してみる"
task :test_flag => :environment do
p "テストフラグ"
end
end
ちなみにnamespaceの下に書く名称はrakeファイル名と同じにする必要があります。
この状態でrails -T
を実行すると、midnight_reset以下にタスクが表示されます。
rails midnight_reset:reset_flag # mode、visit, task, paymentをリセット
rails midnight_reset:test_flag # 二つ目のタスクを追加してみる
cronへの書き込みと定期実行
ここからが本題
定期実行プログラムの本質であるcronに、実行タイミングとタスクを記述します。
・・・と言いたいところですが、冒頭でも述べた通り、cronは独特の記述方式なため直接cronに記述するのは難しいです。
そこで、人間が読みやすい形式で実行タイミングとタスクを記述し、それをwheneverが翻訳してcronに書いてあげるという流れをとります。
まず、実行タイミングとタスクを記述するファイルですが、これは以下のコマンドで作成できます。
$ wheneverize .
config/schedule.rbというファイルが生成されます。
このschedule.rbに、定期実行する間隔と、どのプログラムを定期実行するのかということを書いていきます。
schedule.rb
set :output, 'log/crontab.log'
set :environment, ENV['RAILS_ENV']
every 1.day do
rake "recover_user_life:recover"
end
内容はほぼ見たままです。
上2行はset :output, 'log/crontab.log'
ログをlog/crontab.logに出力
set :environment, ENV['RAILS_ENV']
開発環境で動く
every 1.day do
rake "midnight_reset:reset_flag"
end
1日に1回midnight_reset:reset_flag
を実行するための記述です。
ちなみにここはrakeと書く必要があります(railsではエラーになります)
rakeファイルの実行はrakeですが、他にクラスメソッドやコマンドを実行する方法もあります。
また頻度も1時間に一回とか、毎日12時といった書き方ができます。
別記コラムを参考してください。
cronへの反映
schedule.rbが作成できたら、cronに反映させましょう。
現状のcrontabの記述内容は以下のコマンドで確認できます。
crontab -e
また、実際にschedule.rbを使った場合にどのように設定されるのかも、以下のコマンドであらかじめ確認できます。
whenever
* * * * * /bin/bash -l -c 'cd /Users/yutaueda/Desktop/rails/kiiiyaLine && RAILS_ENV=development bundle exec rake midnight_reset:reset_flag --silent >> log/crontab.log 2>&1'
問題なければ、実際にcronに反映させます
whenever --update-crontab
もしcrontabの中身を消し去りたいときは、以下のコマンドで消せます
whenever --clear-crontab
これでローカル上では定期実行できるはずです。
rails sで確認してみましょう。
重要 macOS catalinaではcronにフルアクセス権限を付与してやらないと、cronが動かせません。システム環境設定で設定しましょう。
コラム
実行頻度に関する記述
一時間ごと、1日ごと、一年ごと
# every 1.day, :at => '4:30 am' do
# every :hour do # Many shortcuts available: :hour, :day, :month, :year, :reboot
# every :sunday, :at => '12pm' do # Use any day of the week or :weekend, :weekday
# every '0 0 27-31 * *' do
実行する内容について
runner "MyModel.some_process"
rake "my:rake:task"
command "/usr/bin/my_great_command"