与后台工作的竞争条件的解决方法

时间:2013-01-10 15:56:38

标签: ruby-on-rails resque race-condition

我正在使用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%的时间内成功完成。但我不认为这是一个可以接受的解决方案。

2 个答案:

答案 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

如果您不必发送旧名称也很好,您是否可以使用现有方法重新创建部件名称?