Тест Ruby with_advisory_lock с несколькими потоками периодически прерывается - PullRequest
0 голосов
/ 15 мая 2019

Я использую гем 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 в транзакции необходимо сделать вещиработа

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...