ある時間になったらジョブを起動するスケジューラを実装したgemは、世の中に色々とあります。
- javan/whenever: Cron jobs in Ruby
- adamwiggins/clockwork: A scheduler process to replace cron
- moove-it/sidekiq-scheduler: Lightweight job scheduler extension for Sidekiq
しかし、いまいち自分の用途に合致するものがなかったので自作しました。
基本的に上記のgemは、事前に設定ファイルを書いてあるジョブクラスをトリガーするスケジュールを登録するスタイルです。
システムの運用に関わるバッチのトリガーとかならそれでも便利なんですが、サービスの機能としてユーザーにスケジューラを提供したい場合に困ります。
例えば、ユーザーが毎日12時に広告メールを配信したい、とかそういう機能をサービスとして提供したい場合です。
この場合、ActiveRecordで扱っているモデルそのものがスケジュールの定義になって欲しい。
collectiveidea/delayed_job は割とそれに近いし、そういう使い方もできる。
ただ、あれはテーブルが基本的に一つであくまでスケジュールの定義だけを扱うことになる。
というわけで、微妙に用途が噛み合わない。
そこで自作したのが joker1007/crono_trigger です。
次に実行する時間を保存するカラムと、実行しているレコードをロックするカラムが最低要件で、それらのカラムがあれば任意のActiveRecordモデルをスケジュール定義として利用できます。
その他にオプションでカラムを追加することで、cron形式で繰り返しの実行時間を定義したり、スケジュール自体の有効期間を設定することができます。
典型的なモデルは以下の様な定義になります。
class CreateNotifications < ActiveRecord::Migration[5.0]
def change
create_table :notifications do |t|
t.string :name
# used by crono_trigger
t.string :cron
t.datetime :next_execute_at
t.datetime :last_executed_at
t.string :timezone
t.integer :execute_lock, limit: 8, default: 0, null: false
t.datetime :started_at, null: false
t.datetime :finished_at
t.string :last_error_name
t.string :last_error_reason
t.datetime :last_error_time
t.integer :retry_count, default: 0, null: false
t.timestamps null: false
end
add_index :notifications, [:next_execute_at, :execute_lock, :started_at, :finished_at], name: "crono_trigger_index_on_notifications"
end
end
class Notification < ApplicationRecord
include CronoTrigger::Schedulable
self.crono_trigger_options = {
retry_limit: 5,
retry_interval: 10,
exponential_backoff: true,
execute_lock_timeout: 300,
}
# `execute` callback is defined
# can use `before_execute`, `after_execute`, `around_execute`
# If execute method raise Exception, worker retry task until reach `retry_limit`
# If `retry_count` reaches `retry_limit`, task schedule is reset.
#
# If record has cron value, reset process set next execution time by cron definition
# If record has no cron value, reset process clear next execution time
def execute
send_mail
throw :retry # break execution and retry task
throw :abort # break execution and raise AbortExecution. AbortExecution is not retried
throw :ok # break execution and handle task as success
throw :ok_without_reset # break execution and handle task as success but without schedule reseting and unlocking
end
end
この様にモジュールをincludeしてexecuteメソッドを定義します。
execute_lockだけちょっと特殊なので解説しておくと、これはあるレコードを実行しようとした時のunixtimeが入ります。
0だと誰も実行していないと判断してスケジューリングの対象に含まれます。もし0でなくて、現在時刻がロックしてから指定したタイムアウト時間を過ぎてない場合はスケジュール対象から除外します。
ちなみに、用途的に噛み合わない感じがしたのでActiveJobのインターフェースは実装していません。
実際にジョブを実行するためにはワーカープロセスが必要です。
ワーカープロセスは、serverengineとconcurrent-rubyを利用しています。
$ crono_trigger MailNotification
$ crono_trigger --help
Usage: crono_trigger [options] MODEL [MODEL..]
-f, --config-file=CONFIG Config file (ex. ./crono_trigger.rb)
-e, --envornment=ENV Set environment name (ex. development, production)
-p, --polling-thread=SIZE Polling thread size (Default: 1)
-i, --polling-interval=SECOND Polling interval seconds (Default: 5)
-c, --concurrency=SIZE Execute thread size (Default: 25)
-l, --log=LOGFILE Set log output destination (Default: STDOUT or ./crono_trigger.log if daemonize is true)
--log-level=LEVEL Set log level (Default: info)
-d, --daemonize Daemon mode
--pid=PIDFILE Set pid file
-h, --help Prints this help
こんな感じで、引数にスケジュール定義として利用したいモデルを指定します。
コンフィグファイルを書いてそこで指定することもできます。
CronoTrigger.configure do |c|
c.executor_thread = 10
c.model_names = ["MailNotification", "OtherNotification"]
end
まだ、0.1.0って感じで、自分でも使いこめていないので、バグが残ってるかもしれませんが、用途があったら触ってみてください。