ラベル ActiveJob の投稿を表示しています。 すべての投稿を表示
ラベル ActiveJob の投稿を表示しています。 すべての投稿を表示

2015/07/23

録画予約をActiveJob with sidekiq で

前回の記事で、録画ジョブをActiveJobでやる話を書きました。
実際にやってみて、これまた色々ハマったので今回はそのへんの話を書きたいと思います。

要件
1.EPG情報から直近7日間中の何時何分に実行という予約ができること。
その間、システムリブートしても予約が消滅しないこと。キューの永続化。
2.ジョブが失敗してもリトライしない。スケジュールした時間に意味あるので。
4.予約したものを途中キャンセルできること。


RecordJob という ActiveJob::Baseの派生クラスを作成
$ rails g job record
すると、app/jobs/record_job.rbファイルが作られます。最初は以下の様な感じ。
class RecordJob < ActiveJob::Base
  queue_as :default

  def perform(*args)
    # Do something later
  end
end
このperform関数内に録画処理をずらずらと書いておくわけですが、その内容とは関係ない周辺処理でいくつかハマってしまいました。

1.指定日時分に実行するジョブ登録と実行(perform)

登録自体は簡単です。
  # 予約
  def reserve
    start = self.program.start_at - 30
    job = RecordJob.set(wait_until: start).perform_later(self)
    update_attribute(:job_id, job.job_id)
  end
このなメソッドを、ビデオレコードモデル models/video.rb に書いておいて、予約時に video.reserveを呼ぶだけです。
RecordJob.set(wait_until: start).perform_later(self) がActiveJobを通してSidekiqにstartで指定した日時分に実行するスケジュールが登録されます。redisにスケジュールは永続化するので、途中システムリブートしても大丈夫。

なぜ perform_later(video.id)ではなく、perform_later(self) なのか?

ネット記事を読みながらやると、オブジェクト渡しはシリアライズが入ってよろしくないからID渡しをするのだ的な内容が多かったので、最初は自分もそうしてみました。
 ところが!
最初のperform実行ではちゃんとvideo.idが渡って滞り無く処理されるんですが、不思議なことに2回めのジョブ(もちろんIDは異なる)でも最初のIDでperformが実行されてしまいました。
perform(video_id)で受けるのをやめてみて perform(*args) で受けてみても結果同じでした。

↑後で気づいたんだけど、whereではなくfind_byを使ったためと思われる。perform(id)ができないというわけではないようだ。

その後知りましたが、現在のActiveJobのperformへはオブジェクト渡しは悪い方法ではないらしい。シリアライズ負荷がどうのという懸念は解消しているらしい。

ただオブジェクト渡しだと、ジョブ実行前にそのオブジェクトレコードが削除された場合は事前に調べることが出来ず、例外エラーとなってしまう。これはちゃんとレコード削除とジョブキャンセルを同期させれば問題ないか。

2.失敗してもリトライしないように

ネット上は色々なソリューションがあるようです。

ActiveJobオプションをジョブクラスに記述する。
job_options retry: false

Sidekiqオプションを記述する。
sidekiq_options :retry => false

ふむふむ? どっちも効きませんけど・・・
結局自分は、以下のコードを、initializers/sidekiq.rb に加えました。
Sidekiq.configure_server do |config|
   config.server_middleware do |chain|
     chain.add Sidekiq::Middleware::Server::RetryJobs, :max_retries => 0
   end
end

3.SidekiqスケジュールをRailsアプリからキャンセルする

放っておいてもジョブ失敗するだけでシステムが死ぬわけじゃないんで支障はない
のだけれど、ログが汚くなるし、sidekiq_webで見た時にわけわからなくなるんで、
予約キャンセルでジョブスケジュールも削除したいわけです。

探してみましたが、ActiveJobからキュー状態を調べたり削除したりすることが出来ない?

perform_laterが返すJobのjob_idは、SidekiqのjobIDとは異なる? むむぅ。調べた結果、
Sidekiqスケジュールのアーギュメント内にActiveJobのjob_idが記録されている。
ということで以下のコードをレコード削除メッソド内に記述することで解決。
jobs = Sidekiq::ScheduledSet.new.select do |job|
  job.args[0]['job_id'] == @video.job_id
end
jobs.each(&:delete)
冒頭のreserveメソッドでActiveJobのjob_idをレコード上に覚えておくのは、このため。
以上が、やりたいこと以外のやらなくてはならなかったこと。結構苦労した。

2015/07/19

録画処理をActiveJobで

実際の録画ジョブをどこでどうやって動かすか?

過去のアプローチとしては、Linuxの'at'コマンドでOS的なジョブ登録で回していた。
'at'は、お手軽でいいんだけれど、ジョブ管理が結構面倒だった。

その後、Jenkinsでジョブ管理するようにした。これはこれで良かったが、Jenkins自体がメモリとCPU食いなので、1台で全部やらせようとするとキャパ的にきつかった。

今回完全リニューアルということで、調べなおしてみたところ、Rails4以降では、ActiveJob というものが標準化したらしい。ならば、使ってみようじゃないですかと。

ActiveJobはただのフロントAPIであって、実際のジョブは別のジョブキューイングシステムに任せるようだ。その辺の解説は、http://blog.chopschips.net/blog/2015/02/26/active-job/がいいです。

Sucker Punch

バックエンドに簡単そうな、Sucker Punch を使ってやってみることにした。作者の記事もなかなか面白いです → Why I Wrote the Sucker Punch Gem

いつものようにGemfileに、gem 'sucker_punch' を追記して、$ bundle しておく。
config/initializers/sucker_punch.rbファイル作成して
Rails.application.configure do
  config.active_job.queue_adapter = :sucker_punch
end
と書いて、ActiveJobのバックエンドとしてSuckerPunchを使うようにして準備完了。
簡単にできるはずが・・・

実は色々ハマりました
SuckerPunchは実は Celluloid のラッパーに等しい。このCelluloidの挙動がむずい。

1.予約録画モデルのVideoモデルがジョブキューを持てばいいんじゃないか?
と、いきなりSuckerPunchを直接使ってしまおうと思った。そこで、Videoモデルに
include SuckerPunch::Job
を入れてみたら、Videoモデル全体がCelluloidでラップされてしまい、.createとか、.first_or_createとかが全然動かなくなってしまった。newしてsaveすれば?と書き方を変えて粘ってもみたが、 全く太刀打ち出来無かった・・・

気持ちを新たに、rails g job RecordJob で、RecordJobというActiveJobを作ってからのこと・・・

2.perform_laterと10秒制限?
RecordJob.perform_laterでキュー登録してみた。performメソッド内で録画タイミングを図るためにsleepをさせていた。perform_laterからperformはコールされるが、perform内は10秒以内の処理じゃないと強制的にアボートしてしまうようだ。はぁ?って感じ。

 RecordJob.set(wait_until: video.program.start_at).perform_later(video)
気持ちとしては、上記の1行で予約時にキュー登録したいのだが、試しにやってみたら、wait_untilはSuckerPuchでは実装されていなかった・・・

残念ながら、SuckerPunchは目的には合致しなかったようだ。撃沈。

やっぱり普通にSidekiqか

sidekiqをバックエンドに使用することにしたので redis 入れる。


$ apt-get install redis-server

Gemfileにgem 'sidekiq'を追加して、$ bundle実行。
sidekiqは、$ sidekiq で直接動かしてテスト実行。

今度は、
 RecordJob.set(wait_until: 録画開始時間).perform_later(video)
が普通に動いた。
後は、sidekiqをサーバ自動起動化して、タスク環境はよしとしよう。