具有多个线程的Ruby with_advisory_lock测试间歇性地失败

时间:2019-05-15 09:39:16

标签: ruby-on-rails ruby multithreading

我正在使用with_advisory_lock宝石来尝试确保仅创建一次记录。这是宝石的github url

我有以下代码,位于我编写的用于处理创建用户订阅的操作类中:

def create_subscription_for user
  subscription = UserSubscription.with_advisory_lock("lock_%d" % user.id) do
    UserSubscription.where({ user_id: user.id }).first_or_create
  end

  # do more stuff on that subscription
end

及其伴随的测试:

threads = []
user = FactoryBot.create(:user)

rand(5..10).times do
  threads << Thread.new do
    subject.create_subscription_for(user)
  end
end

threads.each(&:join)

expect(UserSubscription.count).to eq(1)

我希望发生的事情:

  • 第一个到达该块的线程获取锁并创建一条记录。
  • 在被另一个线程waits indefinitely until the lock is released保留的同时到达该块的任何其他线程(根据文档)
  • 一旦创建记录的第一个线程释放了锁,另一个线程就获取了该锁,现在发现了记录,因为该记录已经由第一个线程创建了。

实际发生的事情:

  • 第一个到达该块的线程获取锁并创建一条记录。
  • 在被另一个线程持有的同时到达该块的任何其他线程仍会去执行该块中的代码,因此,在运行测试时,它有时会失败并出现ActiveRecord::RecordNotUnique错误(我在表上具有唯一索引,该索引允许单个user_subscription与相同的user_id

更奇怪的是,如果我在sleep方法之前的方法中添加find_or_create几百毫秒,则测试永远不会失败:

def create_subscription_for user
  subscription = UserSubscription.with_advisory_lock("lock_%d" % user.id) do
    sleep 0.2
    UserSubscription.where({ user_id: user.id }).first_or_create
  end

  # do more stuff on that subscription
end

我的问题是:“为什么添加sleep 0.2才能使测试始终通过?”和“我要在哪里调试?”

谢谢!

更新:稍微调整测试会导致它们始终失败:

threads = []
user = FactoryBot.create(:user)

rand(5..10).times do
  threads << Thread.new do
    sleep
    subject.create_subscription_for(user)
  end
end

until threads.all? { |t| t.status == 'sleep' }
  sleep 0.1
end

threads.each(&:wakeup)
threads.each(&:join)

expect(UserSubscription.count).to eq(1)

我也将first_or_create包装在一个事务中,这使测试通过,一切都能按预期工作:

def create_subscription_for user
  subscription = UserSubscription.with_advisory_lock("lock_%d" % user.id) do
    UserSubscription.transaction do
      UserSubscription.where({ user_id: user.id }).first_or_create
    end
  end

  # do more stuff on that subscription
end

那为什么要在事务中包装first_or_create才能使事情正常进行?

1 个答案:

答案 0 :(得分:3)

您是否要为此测试案例关闭事务测试?我正在研究类似的东西,事实证明这对实际模拟并发非常重要。

请参见uses_transaction https://api.rubyonrails.org/classes/ActiveRecord/TestFixtures/ClassMethods.html

如果未关闭事务,Rails会将整个测试包装在一个事务中,这将导致所有线程共享一个数据库连接。此外,在Postgres中,始终可以在同一会话中重新获取会话级咨询锁。从文档中:

如果会话已经拥有给定的咨询锁,则其他请求 这样,即使其他会话正在等待 锁;无论现有锁是否存在,此语句都是正确的 保留和新请求处于会话级别或事务级别。

基于这种情况,我怀疑您始终可以获取您的锁,因此始终执行.first_or_create调用,这会导致间歇性的RecordNotUnique异常。