Locker::Room
Traditional data modelling has it that there are two patterns - the optimistic and the pessimistic locking. The optimistic is the Rails default; one in which "we get away with it - if we get away with it" whereas the pessimistic is a design choice available to only MySQL and PostgreSQL even though some like oldmoe are working on bettering this.
No matter the importance of this work most concurrency issues arise at a far earlier point on the transaction timeline - when the data which later on will conflict on writes are read; like the sales_order
and sales_order_lines
that really hangs on product.quantity
(transposable to any domain - flights and flight_seats, theatre and theatre_seats, more). It all comes down to a conflict of resource allocation - to whom does this seat go?
Allowing Ruby/Rails developers an easy way out of this predicament would validate Rails as a lot more than just a "toy" and send SQLite on a stratospheric course!
Locker Room refers to rooms where you put stuff/clothes while doing something in a different state
like playing a game, working out, working on a shop floor, more. Once done - you return to the locker room and recover your other "stuff". Should you forget/decide not to recover your other stuff, the locks in the locker room are preset to open automagically.
Workflow:
- find scarce resource and reserve it
- allow user UI to process data
- possibly broadcast inquiry to user asking for intent to keep locking resource
- update scarce resource (if appropriate) and release
Imagine
resource = Resource.reserve(
id: params.expect(:resource)[:id],
lock: [ quantity: params.expect(:resource)[:quantity_to_reserve] ],
duration: params.expect(:resource)[:how_long_to_reserve]
)
Using SELECT...FOR UPDATE
is not the right tool 'cause hits are scarse (how many browse handbags as opposed to the number actually buying them 😎 ) and postpone the issue until the write - what we need is a polymorphic table with a background job that do house cleaning on Locker::Room.where( release_at: ..Time.current-1.second).delete_all
class ApplicationRecord < ActiveRecord::Base
def self.reserve(id:, lock:, duration:)
release = Time.current + duration
return find( :id) if Locker::Room.create(
id: id,
type: self.name,
lock: lock,
reserved_by: Current.user&.id
release_at: release )
false
end
def self.release(id:, lock:)
Locker::Room.where( id: id, lock: lock).delete
end
end
class Locker::Room < ActiveRecord::Base
#
# composite primary key = [ record_id, record_type, lock ]
# attr :release_at, :reserved_by
end
Thoughts?