find_or_create竞争条件

时间:2011-05-06 21:48:05

标签: ruby-on-rails activerecord callback

我正在尝试使用ActiveRecord的find_or_create_by_*column*,但我从Postgres收到错误,让我知道它偶尔找不到该模型,并试图插入一个。我保持这个表的独特性非常重要,因此我在其迁移中添加了:unique => true属性,以便Postgres知道我对此非常认真。

而且,失败:

ActiveRecord::StatementInvalid: PGError: ERROR: duplicate key value violates unique constraint "index_marketo_leads_on_person_id" DETAIL: Key (person_id)=(9968932) already exists. : INSERT INTO "marketo_leads" ("mkt_person_id", "synced_at", "person_updated_at", "person_id") VALUES(NULL, NULL, '2011-05-06 12:57:02.447018', 9968932) RETURNING "id"

我有这样的模型:

class User < AR::Base
  has_one :marketo_lead

  before_save :update_marketo_lead

  def update_marketo_lead
    if marketo_lead
      if (User.marketo_columns & self.changes.keys).any?  
        marketo_lead.touch(:person_updated_at) 
      end
    elsif self.id
      marketo_lead = MarketoLead.find_or_create_by_person_id(:person_updated_at => Time.now, :person_id => self.id) 
    end
  end
end

class MarketoLead
  belongs_to :user, :foreign_key => 'person_id'
end

第二个模型用于将我们的用户帐户链接到Marketo电子邮件服务器,并保留上次更改用户的某些字段的记录,以便我们可以在批量后台任务中推送更改的记录。

我想不出这个回调的任何原因,update_marketo_lead失败,除了某种我无法想象的竞争条件。

(请忽略'用户'与'人'共享主键的可怕性) (使用Rails 2.3.11,Postgres 9.0.3)

2 个答案:

答案 0 :(得分:5)

很可能当执行find_or_create时,找不到匹配的person_id,因此使用了create logic,但是它可能在find_or_create和实际user.save之间,另一个请求设法完成保存事务,此时你的数据库约束引起了这个例外。

我建议捕获StatementInvalid异常并重试保存(最多有限次数......

begin
   user.save!
rescue ActiveRecord::StatementInvalid => error
  @save_retry_count =  (@save_retry_count || 5)
  retry if( (@save_retry_count -= 1) > 0 )
  raise error
end

请注意,只要您尝试保存用户,就应该执行此操作。所有回调和验证都在保存中发生!交易

P.S。我假设您的rails版本支持事务:)在Rails 3中,它不需要包装保存!在事务中,因为它已经在内部使用了一个

答案 1 :(得分:0)

我在一个重复的工作中遇到这个问题,重复并重复获取错误并最终清除。我不相信它的竞争条件来自另一个请求,或者它真的很少见,只发生一次或两次但不是连续11次,就像我看到的那样。我发现的最佳解释是在博文here上。要点是postgres保留了一个内部存储的值,用于递增以某种方式搞砸的主键。这对我来说是正确的,因为我设置主键而不仅仅是使用递增的值,所以可能会出现这种情况。上述链接中的评论解决方案似乎是致电ActiveRecord::Base.connection.reset_pk_sequence!(table_name)

我无法对此进行验证,因为我无法重现这个问题,但我从上面修改弗拉基米尔修正后的尝试修复是:

begin
   user.save!
rescue ActiveRecord::StatementInvalid => error
   @save_retry_count =  (@save_retry_count || 1)
   ActiveRecord::Base.connection.reset_pk_sequence!(:user)
   retry if( (@save_retry_count -= 1) >= 0 )
   raise error
end

因此,如果在第一次尝试时没有修复它,我会看到引发的错误