Module: OpenLoam::RecordLocks

Defined in:
lib/open_loam/record_locks.rb

Overview

Advisory record locks (OpenLoam::RecordLock) — a "who's editing this" courtesy for the multi-user admin.

OpenLoam::RecordLocks.acquire(record, by: current_actor)  # take/refresh, or nil if held
OpenLoam::RecordLocks.holder(record)                      # the user holding it, or nil
OpenLoam::RecordLocks.release(record, by: current_actor)  # give it up
OpenLoam::RecordLocks.force_release(record)               # manager override

THIS IS ADVISORY: a held lock warns, it does not hard-block — optimistic locking (lock_version) is the actual guarantee that two edits can't clobber. Every query is tenant-scoped by OpenLoam::RecordLock, so a lock is invisible and unaffectable from another tenant.

Constant Summary collapse

DEFAULT_TTL =
5.minutes

Class Method Summary collapse

Class Method Details

.acquire(record, by:, ttl: DEFAULT_TTL) ⇒ Object

Take or refresh the lock if it is free (or already yours). The same holder re-acquiring extends the TTL (heartbeat). Returns the lock, or nil when someone else holds a live one. A record that no longer exists (hard- or soft-deleted) cannot be locked.



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/open_loam/record_locks.rb', line 22

def acquire(record, by:, ttl: DEFAULT_TTL)
  return nil unless present?(record)

  lock = lock_row(record)
  return nil if lock && !lock.expired? && lock.locked_by_id != by.id

  lock ||= OpenLoam::RecordLock.new(lockable_type: type_of(record), lockable_id: record.id)
  # A fresh session token when the lock is newly created or taken over from
  # an expired holder; kept as-is on a heartbeat by the same user.
  lock.token = SecureRandom.hex(16) if lock.new_record? || lock.locked_by_id != by.id
  lock.locked_by_id = by.id
  lock.expires_at = Time.current + ttl
  lock.save!
  lock
rescue ActiveRecord::RecordNotUnique
  # Lost the create race — someone else got it first, which is exactly the
  # nil contract. Advisory honesty over cleverness.
  nil
end

.active_lock(record) ⇒ Object

The live lock on a record, or nil — cleaning up a lock that is expired or whose record is gone (an auto-free), so orphaned rows don't linger.



44
45
46
47
48
49
50
51
52
53
# File 'lib/open_loam/record_locks.rb', line 44

def active_lock(record)
  lock = lock_row(record)
  return nil unless lock

  if lock.expired? || !present?(record)
    lock.destroy
    return nil
  end
  lock
end

.force_release(record) ⇒ Object

Manager override: drop whoever holds it. The controller gates the role.



66
67
68
69
# File 'lib/open_loam/record_locks.rb', line 66

def force_release(record)
  lock_row(record)&.destroy
  true
end

.holder(record) ⇒ Object



55
56
57
# File 'lib/open_loam/record_locks.rb', line 55

def holder(record)
  active_lock(record)&.locked_by
end

.release(record, by:) ⇒ Object



59
60
61
62
63
# File 'lib/open_loam/record_locks.rb', line 59

def release(record, by:)
  lock = lock_row(record)
  lock.destroy if lock && lock.locked_by_id == by.id
  true
end