0

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

4

2 回答 2

2

在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'] }
于 2013-01-10T20:20:24.897 回答
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

如果您不必发送旧名称也很好,您可以使用现有方法简单地重新创建零件名称吗?

于 2013-01-10T17:42:32.700 回答