我有一个过程,通常需要几秒钟才能完成,因此我尝试使用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])
我是否应该费心改变这一点,并推迟删除完整记录?我不知道我怎么能得到它的状态通知。或者正在查看死记录作为完成证明吗?其他人面对类似的事情吗?
答案 0 :(得分:45)
让我们从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
到目前为止一切顺利。我们有一份工作。现在让我们编写将其排列的逻辑。由于可用是负责这项工作的模型,让我们教它如何开始这项工作。
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
。
虽然我们正在努力,但我们怎么知道这项工作因为已经完成而无法工作,或者因为它还没有开始?一种方法是实际检查作业实际上做了什么。如果它创建了一个文件,请检查文件是否存在。如果计算了一个值,请检查结果是否已写入。有些工作并不容易检查,因为他们的工作可能没有明确的可验证结果。对于这种情况,您可以在模型中使用标志或时间戳。假设这是我们的情况,让我们添加一个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
好的。所以,现在让我们通过修改Available
方法将start_working!
与作业排队后立即关联。
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
传递给作业(作为其中一个选项)并在作业中使用它。为此,我们需要修改start_working!
方法以传递id。
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
来了解为您的可用作业创建的确切作业。您可以通过belongs_to :job
轻松将其转换为真正的关联。
答案 1 :(得分:14)
我最终使用了Delayed_Job和after(job)回调的组合,它使用与创建的作业相同的ID填充memcached对象。这样,我最小化了数据库询问作业状态的次数,而不是轮询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
答案 2 :(得分:13)
我认为最好的方法是使用delayed_job中可用的回调。 这些是: :成功,:错误和:之后。 所以你可以使用after:
在模型中放入一些代码class ToBeDelayed
def perform
# do something
end
def after(job)
# do something
end
end
因为如果你坚持使用obj.delayed.method,那么你将不得不修补Delayed :: PerformableMethod并在那里添加after
方法。
恕我直言,它远比轮询某些可能甚至特定于后端的值更好(例如ActiveRecord vs. Mongoid)。
答案 3 :(得分:5)
实现此目的的最简单方法是将您的轮询操作更改为类似于以下内容:
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
属性设置为遇到的错误。使用上述两项功能,您可以检查已删除的记录,看看它们是否成功。
上述方法的好处是:
您可以通过执行以下操作将此功能封装在模型方法中:
# 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
答案 4 :(得分:1)
我建议如果获得作业已完成的通知很重要,那么编写一个自定义作业对象并排队 而不是依赖于在您调用时排队的默认作业{ {1}}。创建一个类似的对象:
Available.delay.dosomething
并将其排队:
class DoSomethingAvailableJob
attr_accessor options
def initialize(options = {})
@options = options
end
def perform
Available.dosomething(@options)
# Do some sort of notification here
# ...
end
end
答案 5 :(得分:1)
应用程序中的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
有很多方法可以做到这一点,但我发现这种方法很简单,对我来说效果很好。