Я использую гем with_advisory_lock
, чтобы убедиться, что запись создается только один раз.Вот URL-адрес github для гема.
У меня есть следующий код, который находится в классе операций, который я написал для обработки создания пользовательских подписок:
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
в транзакции необходимо сделать вещиработа