clockwork gemの使い方

自転車ロードレース放送予告bot[twitter:@cycletvschedule]を最近作った[twitter:@riocampos]です。
当初はtweetするRubyスクリプトを5分毎に起動するようcronというかlaunchdに5分刻みの時刻を指定する設定を行っていました。しかし(Mac miniの動作環境のせいか)5分ごとに起動してくれない。
ということで、clockworkdでdaemon化した別スクリプトを走らせ、そこからtweetするスクリプトを5分ごとに起動するようにしました。そのスクリプトを例にして説明します。
参考:

clockworkとは

cronみたいなものです。
cronの無くなったherokuでも便利に使えるようです。

本家でも。

スクリプト

当然ですが、gem install clockworkしておいてください。

clockworkのスクリプト crontweet.rb
#!/usr/bin/env ruby
# coding: utf-8

require 'clockwork'
load File.expand_path('jspocycletimetable.rb')

def make_each_5min_array(last_digit)
  last_digit = last_digit.round
  last_digit = last_digit - 5 if last_digit >= 5
  array = []
  (0..11).each do |m|
    min = "%02d" % (m * 5 + last_digit)
    array[m] = "**:#{min}"
  end
  array
end

Clockwork::every( 1.hour, 'cyclerrtvtweet', {
                  :thread => true,
                  :at => make_each_5min_array(0)}) do 
                    Jsports.jspocycletimetable
                  end

GC.start
ツイートするスクリプト jspocycletimetable.rb(大半省略m(_ _)m)
require 'twitter'

module Jsports
  def tweet
    # ツイート文作成
  end

  def jspocycletimetable
    load "./set.rb"
    tweet_jspocycle = set
    tweet_jspocycle.update(tweet)
    # その他もろもろ
  end
end

解説

Clockwork::handlerをつかうやり方が一般的かも知れませんが、各時刻条件で作業が違う場合には以下のやり方が楽だと思います。
clockworkの基本フォーマットは

Clockwork::every(時刻条件, 名前, パラメータ){ブロック}

となっています。

時刻条件

起動間隔を指定します。メソッドの表記が単数・複数形の両方ありますが、どちらも同じです。

20.seconds #20秒間隔
5.mins     #5分間隔
6.hours    #6時間間隔
2.days     #2日間隔
1.week     #1週間間隔
名前

ログに出力する名前です。
本来は、Clockwork::handlerで呼ぶときの名前です。

パラメータ

ハッシュで指定します。3種類です。指定不要の場合は省略可能です。

{:at => "23:45"                        # 23時45分に起動
 :if => if: lambda { |t| t.day == 1 }) # 日付が1日の場合に起動
 :thread => true }                     # マルチスレッドで起動

:atは配列で指定することも出来ます。また時刻部分はワイルドカード可能です。例えば

Clockwork::every(1.hour, "everyminute", :at => ["*:05", "*:20", "*:35", "*:45"]) 
# 毎時5分、20分、35分、45分に…何もしない(作業が指定されていないから)。
ブロック

起動させる作業を指定します。
別メソッドを呼ぶなり、ブロックの中でごにょごにょするなり、なんなり指定してくださいw

もう一度当初のスクリプトを見直す

時刻指定

:atで5分ごとに指定したいと思ったので、メソッドを作成しました。一の位が0から4までの自然数を指定します。
分の部分は二桁になるよう整形しています。時刻の部分はワイルドカード*にしています。

def make_each_5min_array(last_digit)
  last_digit = last_digit.round
  last_digit = last_digit - 5 if last_digit >= 5
  array = []
  (0..11).each do |m|
    min = "%02d" % (m * 5 + last_digit)
    array[m] = "*:#{min}"
  end
  array
end

make_each_5min_array()を呼ぶと5分ごとの時刻を指定した配列を返してきます。

作業

名前を'cyclerrtvtweet'としています。
末尾0分/5分にブロックを呼び出します。
1時間のうちの起動時刻をmake_each_5min_array(0)で指定しているので、時刻条件は1時間毎、つまり 1.hour を指定します。
メソッドの作業に時間を要する可能性があるのでマルチスレッド指定をしています。
ブロックでは、モジュールJsportsのメソッドJsports#jspocycletimetableを呼び出すよう指定しています。

Clockwork::every( 1.hour, 'cyclerrtvtweet', {
                  :thread => true,
                  :at => make_each_5min_array(0)}) do 
                    Jsports.jspocycletimetable
                  end

Jsports#jspocycletimetableでは必要に応じてツイートをします。

通常実行

clockworkコマンドを使います。rubyコマンドじゃないです。

$ clockwork crontweet.rb

daemonとして起動

clockworkdヘルプ

clockworkdはclockworkとは別のgemなので、別途gem install clockworkdしてください。
必要なのはdaemons gemでしたm(_ _)m

$ clockworkd --help
Usage: clockworkd -c FILE [options] start|stop|restart|run

        --pid-dir=DIR                Alternate directory in which to store the process ids. Default is /.../clockwork/tmp.
    -i, --identifier=STR             An identifier for the process. Default is clock file name.
    -l, --log                        Redirect both STDOUT and STDERR to a logfile named clockworkd[.<identifier>].output in the pid-file directory.
        --log-dir=DIR                A specific directory to put the log files into (default location is pid directory).
    -m, --monitor                    Start monitor process.
    -c, --clock=FILE                 Clock .rb file. Default is /.../clockwork/clock.rb.
    -h, --help                       Show this message
logを出力させる設定で起動
$ clockworkd -c crontweet.rb --log start

logは tmp/clockworkd.(スクリプト名crontweet).output に出力されます。
tail -Fを使うと自動更新してくれます。

clockworkd.crontweet: process with pid 74526 started.
I, [2013-06-25T13:45:02.774780 #74526]  INFO -- : Starting clock for 12 events: [ cyclerrtvtweet cyclerrtvtweet cyclerrtvtweet cyclerrtvtweet cyclerrtvtweet cyclerrtvtweet cyclerrtvtweet cyclerrtvtweet cyclerrtvtweet cyclerrtvtweet cyclerrtvtweet cyclerrtvtweet  ]
I, [2013-06-25T13:45:02.775175 #74526]  INFO -- : Triggering 'cyclerrtvtweet'
 :
logディレクトリ指定(Macの場合)

なお、Macのユーザプロセスのlogディレクトリは~/Library/Logsなので

$ clockworkd -c crontweet.rb --log start --log-dir=~/Library/Logs

としておくとコンソールでlogが見れて便利です。

daemonで稼働しているclockwork(ruby)プロセスのPIDはtmp/clockworkd.(スクリプト名crontweet).pidに出力されます。もちろん、そのプロセスが終了すればそのファイルも削除されます。

なにか設定を変えたらrestartで再読込
$ clockworkd -c crontweet.rb --log restart
clockworkd.crontweet: pid file: /.../tmp/clockworkd.crontweet.pid
clockworkd.crontweet: output log file: /.../tmp/clockworkd.crontweet.output
clockworkd.crontweet: trying to stop process with pid 71129...
clockworkd.crontweet: process with pid 71129 successfully stopped.

上記のようにlogの位置を変更している場合にはlogディレクトリを指定しましょう。log出力を指定しながらディレクトリを指定し忘れるとデフォルトディレクトリにlog出力されます。

$ clockworkd -c crontweet.rb --log --log-dir=~/Library/Logs restart

launchdで自動起動Macの場合)

Mac起動時にclockworkdを起動させ、且つ異常終了した場合に再起動するように設定しておきます。
plistに実行させるコマンドは

/Users/hoge/.rbenv/shims/clockworkd -c /.../crontweet.rb --log --log-dir=/Users/hoge/Library/Logs --pid-dir=/.../tmp restart

です(詳細なパスは省略していますので、ご自身の環境に合わせてください)。restartなのは、clockworkdスクリプトやそのスクリプトで指定しているrubyスクリプトを修正した後にこのplistを読み込ませたいからです(startだと重複起動してしまう)。
これをplistに変換すると以下のようになります。
名前をcrontweet_bootup.plistとしています。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>KeepAlive</key>
	<dict>
		<key>SuccessfulExit</key>
		<false/>
	</dict>
	<key>Label</key>
	<string>crontweet_bootup</string>
	<key>ProgramArguments</key>
	<array>
		<string>/Users/hoge/.rbenv/shims/clockworkd</string>
		<string>-c</string>
		<string>/.../crontweet.rb</string>
		<string>--log</string>
		<string>--log-dir=/Users/hoge/Library/Logs</string>
		<string>--pid-dir=/.../tmp</string>
		<string>restart</string>
	</array>
	<key>RunAtLoad</key>
	<true/>
</dict>
</plist>
ファイル出力先指定を忘れると

--pid-dirを指定していないと、logファイルやpidファイルが/tmpへ出力されます(Linuxでcronする場合でも同様でしょう)。
--log-dirだけ指定して--pid-dirを指定し忘れた例:

$ ls -l /tmp | grep crontweet
-rw-r--r--  1 hoge      wheel     25  6  4 02:18 clockworkd.crontweet.pid
なにか設定を変えたらrestartで再読込

lunchy gemを使って再起動させます。
もちろんlaunchctl unloadしてからlaunchctl loadしても同じですが、フルパス指定しなければいけないのは面倒なので。

$ lunchy restart crontweet_bootup
stopped crontweet_bootup
started crontweet_bootup

大満足

このclockworkdのおかげで、botのツイート時刻ズレが全く無くなりました。大感謝。
残念ながら数十秒単位のずれはあります。他のスケジューラgemを検討中。