Railsの機能をcronから実行する

この記事は、Railsの機能をcronから実行する方法について解説しています。

Railsの機能をcronから実行する方法はいくつかありますが、単純に実装してしまうと、サーバーのリソースを無駄に消費し、実行完了までとても時間のかかるものになってしまいます。

ある程度アクセスのあるサービスを運営している場合は、上記のリソースの無駄使いを防ぐために、Railsの機能をcronから実行するときに少し工夫が必要になります。

最終的な完成形は、「バックグラウンドジョブとして実装し、起動しているRailsサーバーからジョブをキューイングする」というものになります。順を追って解説していきます。




## 既存の方法の何が問題なのか

既存のよくある方法は、cronからRailsサーバーを起動するものがほとんどです。この方法だと、Railsサーバーの起動に10秒ほど時間がかかります。時間だけでなく、100MBほどのメモリが消費されます。規模が大きい場合はもっとたくさんの時間とメモリが必要になります。

cronから毎分実行する場合は、このリソースの無駄使いを避けたいところです。cronから呼び出したい機能をバックグラウンドジョブとして実装し、ジョブのキューイングをRailsサーバーから行うことで、このリソースの無駄使いを避けることができます。




## バックグラウンドジョブとして実装する

まずは、cronから呼び出したいRailsの機能をバックグラウンドジョブとして実装します。ジョブキューシステムにはSidekiqを利用している想定です。

このジョブは、`UpdateSomethingWorker.perform_async(user_id)`というコードで実行することができます。

```ruby:app/workers/update_something_worker.rb
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`というコードで実行することができます。

```ruby:lib/tasks/users.rake
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サーバーに、ジョブをキューイングするためのエンドポイントを用意します。

**必ず、リクエストの正当性チェックをする必要があります。そうしないと、第三者に意図しないタイミングでジョブをキューイングされてしまう脆弱性が残ってしまいます。**

```ruby:app/controllers/users_controller.rb
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`というコードでこのルーティングに対応するパスを取得できます。

```ruby:config/routes.rb
post 'users/update_something', to: 'users#update_something'
```

**ここがポイントです。実装したコントローラーにアクセスするためのRubyコードを書きます。**RailsではなくRuby単体で動くようにしている点が重要です。こうすることで、cronからRailsの機能を実行するときの不要なオーバーヘッドを減らすことができます。

このRubyスクリプトは、`ruby bin/users_update_something.rb`というコードで実行することができます。

```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ファイル]`というコードで実行することができます。


```sh:bin/cron_ruby.sh
#!/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の機能を呼び出すことができます。


```sh:crontab
* * * * * /bin/sh -c "/var/yourapp/bin/cron_ruby.sh /var/yourapp/bin/users_update_something.rb"
```



## Railsの機能をcronから実行する他の方法

毎分ではなくもっと長いスパンでの実行であったり、サーバーのスペックに余裕があるのなら、今回のような複雑な仕組みは不要かもしれません。そういう場合は、下記の方法を使うことができます。

両方とも、crontabに直接書くことでcronから実行できます。

```ruby
# Rubyのコードをコマンドラインから実行する方法
rails runner "実行したいRubyコード"

# Rakeタスクをコマンドラインから実行する方法
rails "実行したいRakeタスク"
```

## 質問の連絡先

質問や分からない点はお気軽に [@ts_3156](https://twitter.com/ts_3156) までご連絡ください。