この記事は、Railsの機能をcronから実行する方法について解説しています。
Railsの機能をcronから実行する方法はいくつかありますが、単純に実装してしまうと、サーバーのリソースを無駄に消費し、実行完了までとても時間のかかるものになってしまいます。
ある程度アクセスのあるサービスを運営している場合は、上記のリソースの無駄使いを防ぐために、Railsの機能をcronから実行するときに少し工夫が必要になります。
最終的な完成形は、「バックグラウンドジョブとして実装し、起動しているRailsサーバーからジョブをキューイングする」というものになります。順を追って解説していきます。
既存の方法の何が問題なのか
既存のよくある方法は、cronからRailsサーバーを起動するものがほとんどです。この方法だと、Railsサーバーの起動に10秒ほど時間がかかります。時間だけでなく、100MBほどのメモリが消費されます。規模が大きい場合はもっとたくさんの時間とメモリが必要になります。
cronから毎分実行する場合は、このリソースの無駄使いを避けたいところです。cronから呼び出したい機能をバックグラウンドジョブとして実装し、ジョブのキューイングをRailsサーバーから行うことで、このリソースの無駄使いを避けることができます。
バックグラウンドジョブとして実装する
まずは、cronから呼び出したいRailsの機能をバックグラウンドジョブとして実装します。ジョブキューシステムにはSidekiqを利用している想定です。
このジョブは、UpdateSomethingWorker.perform_async(user_id)
というコードで実行することができます。
class UpdateSomethingWorker
include Sidekiq::Worker
sidekiq_options queue: 'misc', retry: 0, backtrace: false
def perform(user_id, options = {})
user = User.find(user_id)
# 数秒よりも長い時間がかかると想定
user.update_something
end
end
実装したジョブの動作テスト用として、Rakeタスクもついでに用意します。ここは必須ではありません。
このRakeタスクは、rails users:update_something
というコードで実行することができます。
namespace :users do
desc 'Update something'
task update_something: :environment do
user_id = ENV['USER_ID']
UpdateSomethingWorker.perform_async(user_id)
end
end
起動しているRailsサーバーからジョブをキューイングする
Railsでアプリケーションを実装している場合、ほとんどの場合はWebサーバーが起動している思います。このWebサーバーに、ジョブをキューイングするためのエンドポイントを用意します。
必ず、リクエストの正当性チェックをする必要があります。そうしないと、第三者に意図しないタイミングでジョブをキューイングされてしまう脆弱性が残ってしまいます。
class UsersController < ApplicationController
before_action do
# !!! リクエストの正当性を必ずチェックすること !!!
end
def update_something
user_id = params[:user_id]
UpdateSomethingWorker.perform_async(user_id)
render json: {status: 'ok'}
end
end
このルーティングを追加することで、コントローラーで実装したコードが有効になります。
ビューの中では、users_update_something_path
というコードでこのルーティングに対応するパスを取得できます。
post 'users/update_something', to: 'users#update_something'
**ここがポイントです。実装したコントローラーにアクセスするためのRubyコードを書きます。**RailsではなくRuby単体で動くようにしている点が重要です。こうすることで、cronからRailsの機能を実行するときの不要なオーバーヘッドを減らすことができます。
このRubyスクリプトは、ruby bin/users_update_something.rb
というコードで実行することができます。
# !/usr/bin/env ruby
require 'net/http'
require 'uri'
if __FILE__ == $0
# users_update_something_url
url = 'https://yourweb.com/users/update_something'
puts Net::HTTP.post_form(URI.parse(url), {}).body
end
さらに、現実的にはcronから実行したときのログも記録したくなると思います。そのためのシェルスクリプトを用意します。
このシェルスクリプトは、sh bin/cron_ruby.sh [実行したいRubyファイル]
というコードで実行することができます。
# !/usr/bin/env bash
exec >>/var/yourapp/log/cron.log 2>&1
cmd="/usr/local/bin/ruby $1"
SECONDS=0
echo -e "$(date) $cmd started"
cd /var/yourapp && $cmd
echo -e "$(date) $cmd finished elapsed=${SECONDS}"
crontabを更新する
後は、上記のシェルスクリプトをcronから実行するようにすればOKです。こうすることで、Rais起動時のオーバーヘッドを避けつつ、cronからRailsの機能を呼び出すことができます。
* * * * * /bin/sh -c "/var/yourapp/bin/cron_ruby.sh /var/yourapp/bin/users_update_something.rb"
Railsの機能をcronから実行する他の方法
毎分ではなくもっと長いスパンでの実行であったり、サーバーのスペックに余裕があるのなら、今回のような複雑な仕組みは不要かもしれません。そういう場合は、下記の方法を使うことができます。
両方とも、crontabに直接書くことでcronから実行できます。
# Rubyのコードをコマンドラインから実行する方法
rails runner "実行したいRubyコード"
# Rakeタスクをコマンドラインから実行する方法
rails "実行したいRakeタスク"
質問の連絡先
質問や分からない点はお気軽に @ts_3156 までご連絡ください。