From 61fec653cfd753cf5b1f4955ae81992189de780e Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Wed, 25 Sep 2024 14:07:55 +1000 Subject: [PATCH] Abstract OrderLocker for re-use --- app/services/current_order_locker.rb | 29 +----------------------- app/services/order_locker.rb | 33 ++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 28 deletions(-) create mode 100644 app/services/order_locker.rb diff --git a/app/services/current_order_locker.rb b/app/services/current_order_locker.rb index 1584701d1a..602ee1f15d 100644 --- a/app/services/current_order_locker.rb +++ b/app/services/current_order_locker.rb @@ -10,33 +10,6 @@ class CurrentOrderLocker # https://guides.rubyonrails.org/action_controller_overview.html#filters # def self.around(controller, &) - lock_order_and_variants(controller.current_order, &) + OrderLocker.lock_order_and_variants(controller.current_order, &) end - - # Locking will not prevent all access to these rows. Other processes are - # only waiting if they try to lock one of these rows as well. - # - # https://api.rubyonrails.org/classes/ActiveRecord/Locking/Pessimistic.html - # - def self.lock_order_and_variants(order) - return yield if order.nil? - - order.with_lock do - lock_variants_of(order) - yield - end - end - private_class_method :lock_order_and_variants - - # There are many places in which stock is stored in the database. Row locking - # on variant level ensures that there are no conflicts even when an item is - # sold through multiple shops. - def self.lock_variants_of(order) - variant_ids = order.line_items.select(:variant_id) - - # Ordering the variants by id prevents deadlocks. Plucking the ids sends - # the locking query without building Spree::Variant objects. - Spree::Variant.where(id: variant_ids).order(:id).lock.pluck(:id) - end - private_class_method :lock_variants_of end diff --git a/app/services/order_locker.rb b/app/services/order_locker.rb new file mode 100644 index 0000000000..4f864ca769 --- /dev/null +++ b/app/services/order_locker.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +# Locks an order including its variants. +# +# It should be used when making major changes like checking out the order. +# It can keep stock checking in sync and prevent overselling of an item. +class OrderLocker + # Locking will not prevent all access to these rows. Other processes are + # only waiting if they try to lock one of these rows as well. + # + # https://api.rubyonrails.org/classes/ActiveRecord/Locking/Pessimistic.html + # + def self.lock_order_and_variants(order) + return yield if order.nil? + + order.with_lock do + lock_variants_of(order) + yield + end + end + + # There are many places in which stock is stored in the database. Row locking + # on variant level ensures that there are no conflicts even when an item is + # sold through multiple shops. + def self.lock_variants_of(order) + variant_ids = order.line_items.select(:variant_id) + + # Ordering the variants by id prevents deadlocks. Plucking the ids sends + # the locking query without building Spree::Variant objects. + Spree::Variant.where(id: variant_ids).order(:id).lock.pluck(:id) + end + private_class_method :lock_variants_of +end