Skip to content

Instantly share code, notes, and snippets.

@treby
Last active November 30, 2016 03:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save treby/78a1c6bf9c145970ea8f1e4af2cbc73d to your computer and use it in GitHub Desktop.
Save treby/78a1c6bf9c145970ea8f1e4af2cbc73d to your computer and use it in GitHub Desktop.
Shinjuku.rb#43 research -- javan/whenever

Wheneverの調査

Shinjuku.rb #43

Wheneverとは、明快なシンタックスとcron jobを提供するgemである。

使い方

インストール

グローバルにinstall

$ gem install whenever

or Gemfileに書く

+ gem 'whenever', require: false

DSLを記述

$ wheneverize .

config/schedule.rb に設定ファイルができる

書き方

set :output, "/path/to/my/cron_log.log"

every 2.hours do
  command "/usr/bin/some_great_command"
  runner "MyModel.some_method"
  rake "some:great:rake:task"
end

みたいなDSLで書ける。

cronの記述に変換 / 実際にcrontabを更新する

$ wheneverize --update-crontab

直感的なDSLでcronを書けるよ!

実装

大体以下のようにできている

DSLの解釈

(A)の部分にて、 https://github.com/javan/whenever/blob/c7675c81d34131f7107b2930c207c9b994895a08/lib/whenever/job_list.rb#L24

instance_eval(setup, setup_file)

とある。

読み込んだ中身をそのままevalしている -> lib/whenever/job_list.rbのメソッドがそのまま、DSLとして使える

  • set
  • env
  • every
  • job_type

この中でもset, job_typeはメソッドの中でさらにメソッドを定義するというメタプロなことをしている

job_type はどういうテンプレートを使うか定義できるものだが、defaultのDSLsetup.rb内で下記4つが動的に定義されている

  • command
  • rake
  • script
  • runner

everyメソッドはブロックをスコープのように扱うことでcronの時間情報を閉じ込めている(@current_time_scopeに時間情報を持っている) つまり、

every 2.days do
  command 'echo "hello, world!!"'
end

のようなDSLは、@current_time_scope = 2.daysとして、その後のparseが行われる。

crontabの更新

  • lib/whenever/command_line.rb内でやっている  - 特定のマジックコメントでどの範囲を更新するか制御している

wheneverの使い所

Schedulingツールを使う上で考慮すべきこと:処理の重複(ロック)

  • プロセス内ロック  - 単一のプロセス内では重複しないことが保証されている
  • プロセス間ロック・サーバ内ロック  - 同一のサーバ内では処理が重複しないことが保証されている
  • サーバ間ロック  - サーバ群の中で処理が重複しないことが保証されている

wheneverはあくまでcrontabを更新するためのgem。上の内では「プロセス間ロック・サーバ内ロック」にあたる。単一のサーバでのみ稼働するシステムでは有用だが、複数サーバを使うようなケースでは、どのサーバで稼働させるかを考慮しなければならない。

@treby
Copy link
Author

treby commented Nov 27, 2016

Whenever::Job#initialize

    def initialize(options = {})
      @options = options
      @at                               = options.delete(:at)
      @template                         = options.delete(:template)
      @job_template                     = options.delete(:job_template) || ":job"
      @roles                            = Array(options.delete(:roles))
      @options[:output]                 = options.has_key?(:output) ? Whenever::Output::Redirection.new(options[:output]).to_s : ''
      @options[:environment_variable] ||= "RAILS_ENV"
      @options[:environment]          ||= :production
      @options[:path]                   = Shellwords.shellescape(@options[:path] || Whenever.path)
    end

@treby
Copy link
Author

treby commented Nov 27, 2016

一旦戻ってWhenever::Output::Cron を見る (lib/whenever/cron.rb)

@treby
Copy link
Author

treby commented Nov 27, 2016

outputメソッド / timesにはcurrent_time_scopeが入る

def self.output(times, job)
  enumerate(times).each do |time|
    enumerate(job.at, false).each do |at|
      yield new(time, job.output, at).output
    end
  end
end

@treby
Copy link
Author

treby commented Nov 27, 2016

まず、enumerateから / timeを解釈しているのかな

def self.enumerate(item, detect_cron = true)
  if item and item.is_a?(String)
    items =
      if detect_cron && item =~ REGEX
        [item]
      else
        item.split(',')
      end
  else
    items = item
    items = [items] unless items and items.respond_to?(:each)
  end
  items
end

@treby
Copy link
Author

treby commented Nov 27, 2016

次に#initializeをみる

def initialize(time = nil, task = nil, at = nil)
  @at_given = at
  @time = time
  @task = task
  @at     = at.is_a?(String) ? (Chronic.parse(at) || 0) : (at || 0)
end

つまり、少なくとも組み込みクラスの範囲において、===演算子が==演算子よりも厳密な同値判定であるケースは皆無というわけです。Rubyにおける===演算子は、他の言語における===演算子とは全く別物と割り切るべきである、と言えます。

へー

@treby
Copy link
Author

treby commented Nov 27, 2016

timeはcurrent_time_scopeが、atはjobが持っているパラメータ

@treby
Copy link
Author

treby commented Nov 27, 2016

Whenever::Output::Cron#outputをみる

def output
  [time_in_cron_syntax, task].compact.join(' ').strip
end

@treby
Copy link
Author

treby commented Nov 27, 2016

      def time_in_cron_syntax
        @time = @time.to_i if @time.is_a?(Numeric) # Compatibility with `1.day` format using ruby 2.3 and activesupport
        case @time
          when REGEX  then @time # raw cron syntax given
          when Symbol then parse_symbol
          when String then parse_as_string
          else parse_time
        end
      end

こんなcase文の書き方できるんや
http://qiita.com/giiko_/items/c75833cd47e41c9b8605

@treby
Copy link
Author

treby commented Nov 27, 2016

@treby
Copy link
Author

treby commented Nov 27, 2016

基本的には、Whenever::Output::Cronで組み立てられているのか

@treby
Copy link
Author

treby commented Nov 27, 2016

Whenever::JobList#generate_cron_output

cron_jobs

@treby
Copy link
Author

treby commented Nov 27, 2016

shortcut_jobs / regular_jobsの違い?

@treby
Copy link
Author

treby commented Nov 30, 2016

  • プロセス内ロック
    • それぞれのプロセスで実行したい
  • プロセス間ロック
    • サーバ内でユニークにしたい(crontab)
    • file lock
  • サーバ間ロック
    • サーバ群でユニークにしたい

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment