我正在使用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)
我希望发生的事情:
实际发生的事情:
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
才能使事情正常进行?
答案 0 :(得分:3)
您是否要为此测试案例关闭事务测试?我正在研究类似的东西,事实证明这对实际模拟并发非常重要。
请参见uses_transaction
https://api.rubyonrails.org/classes/ActiveRecord/TestFixtures/ClassMethods.html
如果未关闭事务,Rails会将整个测试包装在一个事务中,这将导致所有线程共享一个数据库连接。此外,在Postgres中,始终可以在同一会话中重新获取会话级咨询锁。从文档中:
如果会话已经拥有给定的咨询锁,则其他请求 这样,即使其他会话正在等待 锁;无论现有锁是否存在,此语句都是正确的 保留和新请求处于会话级别或事务级别。
基于这种情况,我怀疑您始终可以获取您的锁,因此始终执行.first_or_create
调用,这会导致间歇性的RecordNotUnique
异常。