Я пытаюсь пессимистически заблокировать подмножество таблицы (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)