Как я могу пессимистически заблокировать несколько строк в Rails? - PullRequest
1 голос
/ 12 февраля 2020

Я пытаюсь пессимистически заблокировать подмножество таблицы (Postgres) для условных вставок, и у меня есть куча времени, чтобы найти синтаксис, который работает. Вот в основном то, что я пытаюсь сделать:

ActiveRecord::Base.transaction do
  if consumer.purchases.lock.sum(&:amount) < some_threshold
    consumer.purchases.create!(amount: amount)
  end
end

К сожалению, вышеприведенное не работает. Но такое чувство, что должно. Мне просто нужно заблокировать все строки для определенного потребителя c без блокировки всей таблицы. И, к сожалению, я имею дело с реальными деньгами, это своего рода бухгалтерская книга, поэтому она должна быть пуленепробиваемой.

consumer.purchases.lock.to_sql приводит к SELECT "purchases".* FROM "purchases" WHERE "purchases"."consumer_id" = ? FOR UPDATE, как я и ожидал, но по какой-то причине цепочка .create! заставляет конструктор запросов удалить блокировку FOR UPDATE.

Хорошо, поэтому я разобрал ее и попробовал различные вещи, которые, как мне кажется, должны работать, но НЕ :

# Process 1
ActiveRecord::Base.transaction do
  consumer.purchases.tap{ |p| p.lock! }.create!(amount: amount)
  sleep 20
end

# Process 2
ActiveRecord::Base.transaction do
  consumer.purchases.tap{ |p| p.lock! }.create!(amount: amount)
  # Should wait but doesn't
end
ActiveRecord::Base.transaction do
  purchases = Consumer.find(3).purchases
  purchases.lock
  purchase = purchases.new(amount: amount)
  purchase.save!
  sleep 20
end

... Other process doesn't wait...

Единственный способ, которым я CAN могу заставить его работать, - это если я блокирую строки итеративно; это РАБОТАЕТ работа:

# DOES WORK!
# Process 1
ActiveRecord::Base.transaction do
  purchases = Consumer.where(id: 3).first.purchases
  purchases.each(&:lock!)
  purchase = purchases.new(amount: amount)
  purchase.save!
  sleep 20
end

# Process 2
ActiveRecord::Base.transaction do
  purchases = Consumer.where(id: 3).first.purchases
  purchases.each(&:lock!)
  purchase = purchases.new(amount: amount)
  purchase.save!
  # waits as it should
end

Но меня не могут попросить заблокировать их итеративно, это безумие :) Так что я подумал, может быть, это странная Postgres странность? (Я намного лучше знаком с MySQL), поэтому я сделал это вручную в Postgres, и это без проблем работает:

BEGIN;
SELECT * FROM purchases WHERE consumer_id = 3 FOR UPDATE;
SELECT pg_sleep(30);
INSERT INTO purchases (name, amount) VALUES ('shouldBlock30Seconds', '1000');
END;

BEGIN;
SELECT * FROM purchases WHERE consumer_id = 3 FOR UPDATE;
INSERT INTO purchases (name, amount) VALUES ('shouldWait30Seconds', '1000');
END;

BEGIN;
SELECT * FROM purchases WHERE consumer_id = 24839992 FOR UPDATE;
INSERT INTO purchases (name, amount) VALUES ('shouldInsertImmediately', '1000');
END;

shouldInsertImmediately вставляется сразу, shouldBlock30Seconds получает вставляется через 30 с, а shouldWait30Seconds вставляется сразу после.

Я выдергиваю волосы :) Кто-нибудь сталкивался с этим раньше или я просто очень устал и упустил что-то очевидное?

(Rails 5.1.7, Ruby 2.4.1, Postgres 11.6)

1 Ответ

1 голос
/ 12 февраля 2020

purchases.lock ничего не блокирует, возвращает только новое отношение, которое блокирует записи при извлечении.

Попробуйте принудительно выбрать: purchases.lock.to_a

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