Rails、理论和生产中的 find_or_create 和race-condition

发布于 2024-10-17 01:55:59 字数 641 浏览 3 评论 0原文

你好,我有这段代码

class Place < ActiveRecord::Base
  def self.find_or_create_by_latlon(lat, lon)
    place_id = call_external_webapi
    result = Place.where(:place_id => place_id).limit(1)
    result = Place.create(:place_id => place_id, ... ) if result.empty? #!
    result
  end
end

然后我想在另一个模型或控制器中执行

p = Post.new
p.place = Place.find_or_create_by_latlon(XXXXX, YYYYY) # race-condition
p.save

但是如果执行的操作是创建,Place.find_or_create_by_latlon需要太多时间来获取数据 em> 有时在生产中 p.place 为零。

如何在执行 p.save 之前强制等待响应? 谢谢你的建议

Hi I've this piece of code

class Place < ActiveRecord::Base
  def self.find_or_create_by_latlon(lat, lon)
    place_id = call_external_webapi
    result = Place.where(:place_id => place_id).limit(1)
    result = Place.create(:place_id => place_id, ... ) if result.empty? #!
    result
  end
end

Then I'd like to do in another model or controller

p = Post.new
p.place = Place.find_or_create_by_latlon(XXXXX, YYYYY) # race-condition
p.save

But Place.find_or_create_by_latlon takes too much time to get the data if the action executed is create and sometimes in production p.place is nil.

How can I force to wait for the response before execute p.save ?
thanks for your advices

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

眼泪都笑了 2024-10-24 01:55:59

你是对的,这是一个竞争条件,它通常可以由双击表单上的提交按钮的人触发。如果遇到错误,您可能会做的就是循环返回。

result = Place.find_by_place_id(...) ||
  Place.create(...) ||
  Place.find_by_place_id(...)

有更优雅的方法可以做到这一点,但基本方法就在这里。

You're right that this is a race condition and it can often be triggered by people who double click submit buttons on forms. What you might do is loop back if you encounter an error.

result = Place.find_by_place_id(...) ||
  Place.create(...) ||
  Place.find_by_place_id(...)

There are more elegant ways of doing this, but the basic method is here.

池木 2024-10-24 01:55:59

我不得不处理类似的问题。在我们的后端,如果用户不存在,则会根据令牌创建用户。创建用户记录后,将发送一个缓慢的 API 调用来更新用户信息。

def self.find_or_create_by_facebook_id(facebook_id)
  User.find_by_facebook_id(facebook_id) || User.create(facebook_id: facebook_id)
rescue ActiveRecord::RecordNotUnique => e
  User.find_by_facebook_id(facebook_id)
end

def self.find_by_token(token)
  facebook_id = get_facebook_id_from_token(token)

  user = User.find_or_create_by_facebook_id(facebook_id)

  if user.unregistered?
    user.update_profile_from_facebook
    user.mark_as_registered
    user.save
  end

  return user
end

该策略的步骤是首先从 create 方法中删除缓慢的 API 调用(在我的例子中为 update_profile_from_facebook)。由于该操作需要很长的时间,因此当您将该操作作为 create 调用的一部分时,会显着增加重复插入操作的机会。

第二步是向数据库列添加唯一约束,以确保不会创建重复项。

最后一步是创建一个函数,在重复插入操作发送到数据库的罕见情况下捕获 RecordNotUnique 异常。

这可能不是最优雅的解决方案,但它对我们有用。

I had to deal with a similar problem. In our backend a user is is created from a token if the user doesn't exist. AFTER a user record is already created, a slow API call gets sent to update the users information.

def self.find_or_create_by_facebook_id(facebook_id)
  User.find_by_facebook_id(facebook_id) || User.create(facebook_id: facebook_id)
rescue ActiveRecord::RecordNotUnique => e
  User.find_by_facebook_id(facebook_id)
end

def self.find_by_token(token)
  facebook_id = get_facebook_id_from_token(token)

  user = User.find_or_create_by_facebook_id(facebook_id)

  if user.unregistered?
    user.update_profile_from_facebook
    user.mark_as_registered
    user.save
  end

  return user
end

The step of the strategy is to first remove the slow API call (in my case update_profile_from_facebook) from the create method. Because the operation takes so long, you are significantly increasing the chance of duplicate insert operations when you include the operation as part of the call to create.

The second step is to add a unique constraint to your database column to ensure duplicates aren't created.

The final step is to create a function that will catch the RecordNotUnique exception in the rare case where duplicate insert operations are sent to the database.

This may not be the most elegant solution but it worked for us.

肩上的翅膀 2024-10-24 01:55:59

我在一个伙伴作业中遇到了这个问题,该作业会重试并反复出现错误,并最终自行清除。我找到的最好的解释是在博客文章此处。要点是 postgres 保留一个内部存储的值,用于增加主键,但主键会以某种方式弄乱。这对我来说是正确的,因为我正在设置主键,而不仅仅是使用递增的值,所以这很可能是这样出现的。上面链接中评论中的解决方案似乎是调用 ActiveRecord::Base.connection.reset_pk_sequence!(table_name) 这为我解决了这个问题。

begin
   result = Place.where(:place_id => place_id).limit(1)
   result = Place.create(:place_id => place_id, ... ) if result.empty? #!
rescue ActiveRecord::StatementInvalid => error
   @save_retry_count =  (@save_retry_count || 1)
   ActiveRecord::Base.connection.reset_pk_sequence!(:place)
   retry if( (@save_retry_count -= 1) >= 0 )
   raise error
end

I hit this inside a sidekick job that retries and gets the error repeatedly and eventually clears itself. The best explanation I've found is on a blog post here. The gist is that postgres keeps an internally stored value for incrementing the primary key that gets messed up somehow. This rings true for me because I'm setting the primary key and not just using an incremented value so that's likely how this cropped up. The solution from the comments in the link above appears to be to call ActiveRecord::Base.connection.reset_pk_sequence!(table_name) This cleared up the issue for me.

begin
   result = Place.where(:place_id => place_id).limit(1)
   result = Place.create(:place_id => place_id, ... ) if result.empty? #!
rescue ActiveRecord::StatementInvalid => error
   @save_retry_count =  (@save_retry_count || 1)
   ActiveRecord::Base.connection.reset_pk_sequence!(:place)
   retry if( (@save_retry_count -= 1) >= 0 )
   raise error
end
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文