我正在使用Rails 3.0.19
处理ruby 1.9.2
应用(MySQL 5.1
)。从实际代码中抽象出一点,我得到的是这样的:
Widgets
及其Parts
具有name
个属性,Parts
的名称有时来自关联Widget
的名称。很自然地,当更新Widget
的名称时,我还想更新Parts
的名称。这可能需要一段合理的时间(约60秒),所以我想在后台工作中完成。因此:
class Widget < ActiveRecord::Base
has_many :parts
after_save :update_part_names
def update_part_names
if name_was && name_changed?
Resque.enqueue Widget, { 'widget' => self.id, 'old_name' => name_was }
end
end
def self.perform(args)
widget = Widget.find(args['widget'])
widget.parts.each do |part|
new_name = part.name.sub(args['old_name'], widget.name)
part.name = new_name
part.save!
end
end
end
现在,在我的开发环境中,这非常有用。但后来我将此代码推送到我们的暂存环境中,在该环境中,我们有许多resque worker在与app服务器分开的盒子上运行。现在,更新排队等待,并且似乎已成功完成,但实际更新发生在某些Widget.name
更新而非其他更新。如果我从控制台运行Widget.perform
,它可以100%的时间运行。
我的假设是这是一种竞争条件 - 在暂存环境中并行发生更多事情,作业正在排队,然后在save
的{{1}}交易之前执行已完成(这可能需要一秒钟; Widget
是具有许多关联的复杂对象)。因此,resque作业中的Widgets
正在加载仍具有旧名称的Widget.find
记录,因此Widget
无效。
我尝试将以下代码添加到作业方法中:
part.name.sub(args['old_name'], self.name)
我们的想法是,只要尚未提交def self.perform(args)
widget = Widget.find(args['widget'])
if widget.name == args['old_name']
Resque.enqueue Widget, args
else
# run as before
名称的更新,这只会继续重新排队,然后就会成功。但我仍然看到Widget
名称有时会更新的行为,但并非总是如此。 (据我所知,每次更新时,工作不会排队多次。)
所以有两个问题:(1)我的诊断问题是错误的吗? (2)如何让我的更新作业每次都成功运行?
编辑:越来越确定这真的是一种竞争条件;在part
之前将sleep 60
添加到后台作业似乎会使得更新在100%的时间内成功完成。但我不认为这是一个可以接受的解决方案。
答案 0 :(得分:1)
在http://logicalfriday.com/2012/08/21/rails-callbacks-workers-and-the-race-you-never-expected-to-lose/
的帮助下找到了解决方案我之前曾考虑使用after_commit
回调而不是after_save
,但拒绝了这一想法,理由是after_commit
我们无法再访问name_was
。但是,显然Rails即使在提交后也可以进行更改(尽管reload
数据库中的对象将丢弃它们),通过previous_changes
哈希。如,
after_commit :update_part_names
def update_part_names
return unless self.previous_changes['name'].try(:first)
Resque.enqueue Widget,
{ 'widget' => self.id, 'old_name' => self.previous_changes['name'].first }
end
previous_changes
看起来像:
{ "name" => ['old_name', 'updated_name'] }
答案 1 :(得分:0)
一些想法 - 你可以做一些事情来减少竞争条件的可能性。首先 - 为零件排队作业,而不是小部件。失败只会影响失败的部分。然后,在处理作业时,执行update_column而不是保存! - 它会更快,你不会触发其他回调。
class Part
belongs_to :widget
def self.perform(args)
part = Part.find(args['part'])
part.update_column(:name, part.name.sub(args['old_name'], self.name))
end
end
如果您不必发送旧名称也很好,您是否可以使用现有方法重新创建部件名称?