我正在开发一个Rails 3.0.19
应用程序 ( ruby 1.9.2
),使用MySQL 5.1
. 从实际代码中抽象一点,我得到的是这样的:
Widgets
并且它们Parts
具有name
属性,并且它们的名称Parts
有时是从关联的名称派生的Widget
。所以很自然地,当 aWidget
的名称更新时,我也想更新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 工作人员在与应用服务器不同的盒子上运行。现在更新排队,并且似乎成功完成,但实际更新发生在某些Widget.name
更新而不是其他更新上。如果我Widget.perform
从控制台运行,它会 100% 工作。
我的假设是这是一个竞争条件——在有更多并行发生的暂存环境中,作业正在排队,然后在save
事务Widget
完成之前执行(这可能需要一秒钟;Widgets
是具有许多协会)。因此,Widget.find
在 resque 工作中加载了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
尚未提交对名称的更新,这只会继续重新排队作业,然后它就会成功。但是我仍然看到part
名称有时会更新的行为,但并非总是如此。(据我所知,每次更新该作业的排队次数永远不会超过一次。)
所以有两个问题:(1)我对问题的诊断一开始就错了吗?(2) 如何让我的更新作业每次都能成功运行?
编辑:越来越确定这确实是一种竞争条件;在之前添加sleep 60
到后台作业Widget.find
似乎可以使更新成功地发生 100% 的时间。但我不认为这是一个可以接受的解决方案。