web-dev-qa-db-ja.com

delayd_jobによるポーリング

完了するまでに通常数秒かかるプロセスがあるため、delayed_jobを使用して非同期で処理しようとしています。ジョブ自体は正常に機能します。私の質問は、ジョブをポーリングして、完了したかどうかを確認する方法です。

変数に割り当てるだけでdelayed_jobからIDを取得できます。

job = Available.delay.dosomething(:var => 1234)

+------+----------+----------+------------+------------+-------------+-----------+-----------+-----------+------------+-------------+
| id   | priority | attempts | handler    | last_error | run_at      | locked_at | failed_at | locked_by | created_at | updated_at  |
+------+----------+----------+------------+------------+-------------+-----------+-----------+-----------+------------+-------------+
| 4037 | 0        | 0        | --- !ru... |            | 2011-04-... |           |           |           | 2011-04... | 2011-04-... |
+------+----------+----------+------------+------------+-------------+-----------+-----------+-----------+------------+-------------+

ただし、ジョブが完了するとすぐに削除され、完了したレコードを検索するとエラーが返されます。

@job=Delayed::Job.find(4037)

ActiveRecord::RecordNotFound: Couldn't find Delayed::Backend::ActiveRecord::Job with ID=4037

@job= Delayed::Job.exists?(params[:id])

わざわざこれを変更し、完全なレコードの削除を延期する必要がありますか?他にどのようにしてステータスの通知を受け取ることができるかわかりません。それとも、完了の証拠としてデッドレコードをポーリングしても大丈夫ですか?他の誰かが似たようなことに直面していますか?

33
holden

最終的に、Delayed_Jobとafter(job)コールバックを組み合わせて使用​​し、memcachedオブジェクトに作成されたジョブと同じIDを設定しました。このようにして、memcachedオブジェクトをポーリングする代わりに、データベースにアクセスしてジョブのステータスを要求する回数を最小限に抑えます。また、完了したジョブから必要なオブジェクト全体が含まれているため、ラウンドトリップリクエストもありません。ほぼ同じことをしたgithubの人たちの記事からアイデアを得ました。

https://github.com/blog/467-smart-js-polling

ポーリングにjqueryプラグインを使用しました。これは、ポーリングの頻度が少なく、一定の回数の再試行後にあきらめます。

https://github.com/jeremyw/jquery-smart-poll

うまく機能しているようです。

 def after(job)
    prices = Room.prices.where("space_id = ? AND bookdate BETWEEN ? AND ?", space_id.to_i, date_from, date_to).to_a
    Rails.cache.fetch(job.id) do
      bed = Bed.new(:space_id => space_id, :date_from => date_from, :date_to => date_to, :prices => prices)
    end
  end
14
holden

APIから始めましょう。次のようなものが欲しいのですが。

@available.working? # => true or false, so we know it's running
@available.finished? # => true or false, so we know it's finished (already ran)

それでは、仕事を書いてみましょう。

class AwesomeJob < Struct.new(:options)

  def perform
    do_something_with(options[:var])
  end

end

ここまでは順調ですね。私たちには仕事があります。それをエンキューするロジックを書いてみましょう。 Availableはこのジョブを担当するモデルなので、このジョブの開始方法を教えましょう。

class Available < ActiveRecord::Base

  def start_working!
    Delayed::Job.enqueue(AwesomeJob.new(options))
  end

  def working?
    # not sure what to put here yet
  end

  def finished?
    # not sure what to put here yet
  end

end

では、仕事が機能しているかどうかをどうやって知るのでしょうか?いくつかの方法がありますが、Railsでは、モデルが何かを作成するときに、通常はその何かに関連付けられていると感じます。どのように関連付けるのですか?データベースでIDを使用します。job_idを追加しましょう。利用可能なモデル。

その間、ジョブがすでに終了しているため、またはまだ開始されていないために、ジョブが機能していないことをどのようにして知ることができますか? 1つの方法は、ジョブが実際に何をしたかを実際に確認することです。ファイルを作成した場合は、ファイルが存在するかどうかを確認してください。値を計算した場合は、結果が書き込まれていることを確認してください。ただし、一部のジョブは、その作業の明確な検証可能な結果がない可能性があるため、チェックが簡単ではありません。このような場合、モデルでフラグまたはタイムスタンプを使用できます。これが私たちの場合であると仮定して、job_finished_atタイムスタンプを追加して、まだ実行されていないジョブとすでに終了しているジョブを区別しましょう。

class AddJobIdToAvailable < ActiveRecord::Migration
  def self.up
    add_column :available, :job_id, :integer
    add_column :available, :job_finished_at, :datetime
  end

  def self.down
    remove_column :available, :job_id
    remove_column :available, :job_finished_at
  end
end

了解しました。それでは、start_working!メソッドを変更して、ジョブをキューに入れるとすぐに、実際にAvailableをそのジョブに関連付けましょう。

def start_working!
  job = Delayed::Job.enqueue(AwesomeJob.new(options))
  update_attribute(:job_id, job.id)
end

すごい。この時点でbelongs_to :jobを書くこともできましたが、実際には必要ありません。

これで、working?メソッドの記述方法がわかりました。とても簡単です。

def working?
  job_id.present?
end

しかし、どのようにしてジョブを終了としてマークするのでしょうか。仕事が仕事そのものよりもうまく終わったことを誰も知りません。それでは、available_idを(オプションの1つとして)ジョブに渡して、ジョブで使用しましょう。そのためには、IDを渡すためにstart_working!メソッドを変更する必要があります。

def start_working!
  job = Delayed::Job.enqueue(AwesomeJob.new(options.merge(:available_id => id))
  update_attribute(:job_id, job.id)
end

そして、ジョブにロジックを追加して、完了時にjob_finished_atタイムスタンプを更新する必要があります。

class AwesomeJob < Struct.new(:options)

  def perform
    available = Available.find(options[:available_id])
    do_something_with(options[:var])

    # Depending on whether you consider an error'ed job to be finished
    # you may want to put this under an ensure. This way the job
    # will be deemed finished even if it error'ed out.
    available.update_attribute(:job_finished_at, Time.current)
  end

end

このコードを配置すると、finished?メソッドの記述方法がわかります。

def finished?
  job_finished_at.present?
end

これで完了です。これで、@available.working?@available.finished?に対して簡単にポーリングできます。また、@available.job_idをチェックすることで、Available用に作成された正確なジョブを知ることができます。 belongs_to :jobと言うことで、簡単に実際の関連付けに変えることができます。

45
Max Chernyak

最善の方法は、delayed_jobで利用可能なコールバックを使用することだと思います。これらは、:success、:error、および:afterです。したがって、次のようにモデルにコードを配置できます。

class ToBeDelayed
  def perform
    # do something
  end

  def after(job)
    # do something
  end
end

Obj.delayed.methodの使用を主張する場合は、Delayed :: PerformableMethodにモンキーパッチを適用し、そこにafterメソッドを追加する必要があるためです。私見では、バックエンド固有の値(ActiveRecordとMongoidなど)をポーリングするよりもはるかに優れています。

13
Roman

これを実現する最も簡単な方法は、ポーリングアクションを次のようなものに変更することです。

def poll
  @job = Delayed::Job.find_by_id(params[:job_id])

  if @job.nil?
    # The job has completed and is no longer in the database.
  else
    if @job.last_error.nil?
      # The job is still in the queue and has not been run.
    else
      # The job has encountered an error.
    end
  end
end

なぜこれが機能するのですか?いつ Delayed::Jobキューからジョブを実行し、データベースからジョブを削除します成功した場合。ジョブが失敗した場合、レコードはキューに残り、後で再度実行されます。last_error属性は発生したエラーに設定されます。上記の2つの機能を使用して、can削除されたレコードをチェックし、それらが成功したかどうかを確認します。

上記の方法の利点は次のとおりです。

  • 元の投稿で探していたポーリング効果が得られます
  • 単純なロジックブランチを使用して、ジョブの処理でエラーが発生した場合にユーザーにフィードバックを提供できます

次のような操作を行うことで、この機能をモデルメソッドにカプセル化できます。

# Include this in your initializers somewhere
class Queue < Delayed::Job
  def self.status(id)
    self.find_by_id(id).nil? ? "success" : (job.last_error.nil? ? "queued" : "failure")
  end
end

# Use this method in your poll method like so:
def poll
    status = Queue.status(params[:id])
    if status == "success"
      # Success, notify the user!
    elsif status == "failure"
      # Failure, notify the user!
    end
end
5
Mike Trpcic

ジョブが完了したという通知を受け取ることが重要な場合は、Available.delay.dosomethingを呼び出したときにキューに入れられるデフォルトのジョブに依存するのではなく、カスタムジョブオブジェクトを記述してキューに入れることをお勧めしますthat。次のようなオブジェクトを作成します。

class DoSomethingAvailableJob

  attr_accessor options

  def initialize(options = {})
    @options = options
  end

  def perform
    Available.dosomething(@options)
    # Do some sort of notification here
    # ...
  end
end

そしてそれを次のようにキューに入れます:

Delayed::Job.enqueue DoSomethingAvailableJob.new(:var => 1234)
1
nickgrim

アプリケーションのdelayed_jobsテーブルは、実行中のジョブとキューに入れられたジョブのステータスのみを提供することを目的としています。これは永続的なテーブルではなく、パフォーマンス上の理由から実際にはできるだけ小さくする必要があります。そのため、ジョブは完了後すぐに削除されます。

代わりに、ジョブが完了したことを示すフィールドをAvailableモデルに追加する必要があります。通常、ジョブの処理にかかる時間に関心があるため、start_timeフィールドとend_timeフィールドを追加します。次に、私のdosomethingメソッドは次のようになります。

def self.dosomething(model_id)

 model = Model.find(model_id)

  begin
    model.start!

    # do some long work ...

    rescue Exception => e
      # ...
    ensure
      model.finish!
  end
end

開始!そして終了!メソッドは、現在の時刻を記録してモデルを保存するだけです。それなら私はcompleted? AJAXがポーリングして、ジョブが終了したかどうかを確認できるメソッド。

def completed?
  return true if start_time and end_time
  return false
end

これを行うには多くの方法がありますが、この方法は簡単で、私にとってはうまく機能します。

1
adamlamar