0

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 ワーカーが実行されています。更新がキューに入れられ、正常に完了したように見えますが、実際の更新は一部のWidget.name更新で行われ、他の更新では行われません。コンソールから実行するWidget.performと、100% の確率で動作します。

私の仮説は、これは競合状態であるというものでした。より多くのことが並行して発生するステージング環境では、ジョブがキューに入れられ、saveトランザクションWidgetが完了する前に実行されていました (これには 1 秒かかる場合があります。Widgets多くの複雑なオブジェクトです)。協会)。したがって、Widget.findresque ジョブでは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しかし、名前が時々更新されるという動作がまだ見られますが、常にではありません。(そして、私が知る限り、ジョブが更新ごとに複数回キューに入れられることはありません。)

2 つの質問: (1) そもそも私の問題の診断は間違っているのでしょうか? (2) 更新ジョブを毎回正常に実行するにはどうすればよいですか?

編集:これが本当に競合状態であることをますます確信しています。sleep 60前にバックグラウンド ジョブに追加すると、 Widget.find100% の確率で更新が正常に行われるようです。しかし、私はそれを受け入れられる解決策とは考えていません。

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_commitname_was. ただし、どうやらRailsは変更がコミットされた後でも変更を利用できるようにします(ただしreload、データベースからオブジェクトを ing すると変更は破棄されます)、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 に答える