- お知らせ -
  • 当wikiのプログラムコードの表示を直してみました(ついでに長い行があると全体が下にぶっ飛ぶのも修正)。不具合があればBBSまでご連絡下さい。

Ruby/Ruby on Rails/cron的なこと

はじめに Edit

cron処理をRuby風DSLを使って書けるwheneverプラグイン。

crontabに処理をそのまま書かずに、このプラグインを使う利点はCapistranoと連携して、簡単にデプロイするようにできる点かと思います。

whenever 0.4.1 とRails 2.3.5とRuby 1.8.7とUbuntu 9.04で確認してます。

インストール Edit

javan's whenever at master - GitHub
公式のドキュメントを読みながら進めてみてます。

gemコマンドでまずはインストール

sudo gem install whenever

config/environment.rbに以下のようにconfig.gemの行を追加します。

1
2
3
4
5
6
7
  #config/environment.rb
  Rails::Initializer.run do |config|
    :
    # ↓これを追加
    config.gem 'whenever', :lib => false, :source => 'http://gemcutter.org/'
    :
  end

Railsアプリのディレクトリでwheneverizeコマンドを使うと、
"config/schedule.rb"が生成されます。

cd (Railsアプリのあるディレクトリ)
wheneverize .

テストしてみる Edit

あとは、config/schedule.rbを編集しますが、
その前に、ここではcronでログを吐いてみる例のためのテスト的な適当な何かを作ってみます。(注:いわゆる動作テストです)

ここでは、毎分ごとに作成したモデルのメソッドをキック(というか起動)してみます。

まずは、適当にモデル app/models/diosama.rb を新規に作成し、例えば以下のように編集してログを出力するようにしてみます。

1
2
3
4
5
6
7
#app/models/diosama.rb 
class Diosama
  # log test
  def self.urryy
    Rails.logger.info "URYYYYYY!!"
  end
end

script/consoleでDiosama.urryyがちゃんと動くか確認してみます。

Loading development environment (Rails 2.3.5)
>> Diosama.urryy
=> "2010-03-11 19:59:58 +0900 [INFO] URYYYYYY!!\n"

大丈夫ですね!ログはlog/development.logも確認してみてください
(上記でログの表示が標準と違うのはloggerの書式を弄っているだけなので気にしないでください)

それではいよいよconfig/schedule.rbに組み込んでみます。
以下を追加してみます。

1
2
3
4
5
6
7
8
9
10
11
# config/schedule.rb
 :
 :
# Automatically Detecting Environment - whenever - GitHub
# http://wiki.github.com/javan/whenever/automatically-detecting-environment
set :environment, Rails.env

# log test
every 1.minute do
  runner "Diosama.urryy"
end

ここでは毎分、Diosama.urryyを起動しログに出力してみてます。

set :environment, Rails.env はコメントのURLにも記載されてますが、そのときのモード(development or production 等)によって自動でcronのrunner等を実行するモードを設定してます。
(この自動設定のように適切に設定しとかないと、デフォルトではproductionモードで動いちゃって開発中に確認できないです ><;;)

さて、ここまで書いたら、wheneverコマンドでcrontabがどのように出力されるか確認できます。(crontab=cronの設定コマンドと設定のこと)

$ whenever 
PATH=/home/pubuntu/bin:/home/pubuntu/bin:/home/pubuntu/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/home/pubuntu/bin:/home/pubuntu/bin
* * * * * /home/oreore/my_roject/script/runner -e development "Diosama.urryy"

なるほど、よさそうですね!
(そもそもcrontabがよくわからない人は『crontab 書き方』でググルとよいかと)

さてでは、開発環境でテストしてみます。

$ whenever --update myapp     # cronにmyappというIDで登録
[write] crontab file updated
$ crontab -l     # crontabの中身を拝見


# Begin Whenever generated tasks for: myapp
PATH=/home/pubuntu/bin:/home/pubuntu/bin:/home/pubuntu/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/home/pubuntu/bin:/home/pubuntu/bin

* * * * * /home/oreore/my_roject/script/runner -e development "Diosama.urryy"


# End Whenever generated tasks for: myapp

後はtail -f してログがちゃんと毎分出力されるか確認してみます。

$ tail -f log/development.log
 :
2010-03-11 20:32:15 +0900 [INFO] URYYYYYY!!

うまく行きましたか?

config/schedule.rbの書き方の詳細は公式のwikiにもあります

※【注】動かない場合は、script/runner -e development "Diosama.urryy" のように直接起動してみてエラーが出ないか原因を探ってみてください。

【注】wheneverは script/runner が実行属性がついているのが前提なので注意 Edit

wheneverは script/runner が実行属性がついているのが前提のようでして、開発にPortableUbuntu使っていて(=coLinux)でcofsでソース等がWindowsファイルシステムである領域をマウントしていて script/runner に実行属性がついていないよヽ(`Д´)ノ ウワァァン!!な場合は、実行属性がつくように設定してあげましょう

困ったことにcofs だとchmod +x script/runner じゃ駄目みたいなんで、具体的には /etc/fstabでマウントするときにオプションで強制的に実行属性(exec,fmask=777みたいな感じで)をつけてマウントする設定にしてcoLinuxを再起動してください、ということになります。

以下/etc/fstabの該当行の例:

cofs2          /mnt/D           cofs    rw,user,exec,dmask=0777,fmask=0777 0       0

この辺はcoLinuxやPortableUbuntuのバージョンや状況により方法が変わるかもなので注意。

Capistranoで簡単デプロイできるように Edit

以下はCapistranoを使ってデプロイしている場合の設定について。

config/deploy.rbの最後尾にでも以下を追加します。
これでcapコマンドでのデプロイ時に自動でcrontabを更新してくれるはず!

1
2
3
4
5
6
7
8
9
10
11
#config/deploy.rb
 :
after "deploy:symlink", "deploy:update_crontab"

namespace :deploy do
  desc "Update the crontab file"
  task :update_crontab, :roles => :db do
    run "cd #{current_path} && whenever --update-crontab #{application}",
      :env => {"PATH" => "~/.gem/ruby/1.8/bin/:$PATH" }
  end
end

cron + wget駆動させる方法 Edit

上記では wheneverで script/runnerをキックする方法を試しました。
しかしながら、すごく簡単ではあるのですが、上記で使っているscript/runnerは起動処理が重いという問題があります。

script/runnerが重い原因はRails環境を毎回毎回、全部(現在は一部らしい)読み込むためです
試しに、開発環境や動作環境のコンソールで

time ruby script/runner "puts 'hello'"

と打ってみてどの程度時間がかかるか見てみて下さい。

(例えば動作環境で5秒ほど占有していて毎分キックするようなケースの場合、おいおい全CPU時間の1/12もその処理の起動に費やすのか!まずくない!?という試算ができます)

もちろん、マシンスペックが問題ない場合やマルチコアCPUならさして問題がなかったりしますが、例えば、毎分のように頻繁に処理したいモノがあったり、VPSや昔のマシンの使い回し等スペックが低い場合には負荷が問題になってきます。(逆に言うと負荷が問題になってから対処してもいい気もします)

この項では上記を解決するために、(地味ではあるのですが古典的で有効な方法かと思われる)ローカルにwgetして処理を起動するという方法を提案してみます。

前提条件 Edit

ここでは、以下の条件を想定しています。

  • CronController::minutelyに毎分処理したい処理(ここでは毎分ログに文字列を出す処理)を書く
  • /cron/minutely つまり開発環境では、 http://localhost:3000/cron/minutely というURLを毎分cronでキックする
  • cronへ書く際はwheneverを使う

まずは起動用のコントローラーとキック用のメソッドを作成 Edit

最初に起動用のコントローラーとキック用のメソッドを作成してみます。

以下のように、CronControllerを生成します。

ruby script/generate controller cron

以下のようにapp/controllers/cron_controller.rbを記述します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# app/controllers/cron_controller.rb
class CronController < ApplicationController
  before_filter :check_cron

  # 毎分ごと
  def minutely
    # ここにしたい処理(ここではテスト的にログに出力してみている) 
    Rails.logger.info "TESTTT!!"   

    render :text=>"OK"   # ただ、OKを表示
  end

  private
  def check_cron
    # developmentモードなら実行する
    return if Rails.env == "development"
    # ローカルからじゃないなら、エラーる
    render :text=>"not Cron" unless local_request?
  end
end

まずはテスト Edit

wgetコマンドがないと話にならないのでインストールしておきます。
(ここではUbuntuなのでaptitudeを使っていますが、環境に合わせて下さい)

sudo aptitude install wget

開発サーバーでテストしてみます。開発サーバーを起動した状態で、以下を打ってみます。

wget --spider http://localhost:3000/cron/minutely

log/development.logに以下の様に "TESTTT!!" の文字列が出ていますか?出ていればOK。

TESTTT!!

wgetで起動させる Edit

wheneverのインストールは済ませておいて下さい。

あとは、wheneverの設定ファイルである config/schedule.rb を変更します。

1
2
3
4
5
6
7
8
9
10
11
12
# config/schedule.rb
 :
 :
# Automatically Detecting Environment - whenever - GitHub
# http://wiki.github.com/javan/whenever/automatically-detecting-environment
set :environment, Rails.env

# log test
set :output, nil
every 1.minute do
  command "wget --spider http://localhost:3000/cron/minutely"
end

として、

whenever --update myapp

してcrontabに反映させてみます。

これで毎分ごとにlog/development.rbに以下のようにログが書き込まれていればOK!!

TESTTT!!

あとはデプロイ時の考慮 Edit

script/runner駆動との違いですが、ここが問題です。
(さすがにproductionサーバーは http://localhost:3000/ じゃないだろう的な)

指針としては、capistranoでデプロイ時に環境ごとにwgetするURLを指定してwheneverコマンドを叩こうということにします。

まずは、config/schedule.rb を変更してみます。

1
2
3
4
5
6
7
8
9
10
11
12
# config/schedule.rb
  :
# Automatically Detecting Environment - whenever - GitHub
# http://wiki.github.com/javan/whenever/automatically-detecting-environment
set :environment, Rails.env

# log test
set :output, nil
every 1.minute do   # 下記を変更
  @url = "http://localhost:3000" unless @url
  command "wget --spider #{@url + '/cron/minutely'}"
end

every 1.minute do 以下を変更しています。
これで、wheneverコマンドでのcrontab生成時に、"url"という変数を反映できるようになりました。

試しにコマンドを打ってみます。

$ whenever
PATH=/home/pubuntu/bin:/home/pubuntu/bin:/home/pubuntu/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/home/pubuntu/bin:/home/pubuntu/bin
* * * * * wget --spider http://localhost:3000/cron/minutely >> /dev/null 2>&1
$ whenever --set 'url=http://example.com'
PATH=/home/pubuntu/bin:/home/pubuntu/bin:/home/pubuntu/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/home/pubuntu/bin:/home/pubuntu/bin

* * * * * wget --spider http://example.com/cron/minutely >> /dev/null 2>&1

OKっぽいですね(--spiderの次のURLが変わっていますよね?)

あとは、capistrano用にconfig/deploy.rbのwheneverの項を修正します。

1
2
3
4
5
6
7
8
9
# for whenever
after "deploy:symlink", "deploy:update_crontab"
namespace :deploy do
  desc "Update the crontab file"
  task :update_crontab, :roles => :db do
    run "cd #{current_path} && whenever --update-crontab #{application} --set 'url=#{url}'",
      :env => {"PATH" => "~/.gem/ruby/1.8/bin/:$PATH" }
  end
end

上記では、 --set 'url=#{url}' を wheneverコマンドに渡している部分が変更点です。

そしてあとは、config/deploy.rbなどで、

set :url, "http://test.example.com:11111"

のようにwheneverに渡すURLを変数で設定してやればOKです。

capistrano-extを使っている場合で、例えばstaring用の設定とproduction用の設定を分けてある場合は、
config/delpoy/staring.rbとconfig/delpoy/production.rbにそれぞれ異なる上記の set :url の設定を書いてやればOKかと思います。
(urlで渡しているのはそのための考慮だったりします)

もっと、環境ごとに分けるいい実現方法があればコメントで教えてください!!

参考リンク Edit


Show recent 10 comments. Go to the comment page.

  • deploy.rbのデプロイ設定がミスってたので修正。これでちゃんとデプロイできるはず。 -- TOBY 2010-03-31 (Wed) 20:35:38
  • デプロイしたら"not Cron"が帰って来てるみたいで上手く動いていない orz local_request? で判定だと駄目かも… -- TOBY 2010-04-05 (Mon) 19:22:45
Name:

Front page   Edit Freeze Diff Backup Upload Copy Rename Reload   New Pages Search Recent changes   Help   RSS of recent changes
Last-modified: 2010-03-31 Wed 20:36:10 JST (3428d)