Class: Postburner::AdvisoryLock

Inherits:
Object
  • Object
show all
Defined in:
lib/postburner/advisory_lock.rb

Overview

PostgreSQL advisory lock support for coordinating distributed processes.

Advisory locks allow multiple scheduler processes to coordinate without race conditions. Only one process can hold a specific lock at a time.

Examples:

Using with a block

Postburner::AdvisoryLock.with_lock('postburner_scheduler') do
  # Only one process executes this block at a time
  process_due_schedules
end

Manual lock/unlock

lock = Postburner::AdvisoryLock.new('postburner_scheduler')
if lock.acquire
  begin
    process_due_schedules
  ensure
    lock.release
  end
end

Constant Summary collapse

SCHEDULER_LOCK_KEY =

Lock key for the scheduler process

'postburner_scheduler'

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(key, connection = nil) ⇒ AdvisoryLock

Initialize a new advisory lock

Parameters:

  • key (String)

    Unique lock identifier

  • connection (ActiveRecord::ConnectionAdapters::AbstractAdapter) (defaults to: nil)

    Database connection



35
36
37
38
39
# File 'lib/postburner/advisory_lock.rb', line 35

def initialize(key, connection = nil)
  @key = key
  @connection = connection || ActiveRecord::Base.connection
  @acquired = false
end

Instance Attribute Details

#connectionObject (readonly)

Returns the value of attribute connection.



29
30
31
# File 'lib/postburner/advisory_lock.rb', line 29

def connection
  @connection
end

#keyObject (readonly)

Returns the value of attribute key.



29
30
31
# File 'lib/postburner/advisory_lock.rb', line 29

def key
  @key
end

Class Method Details

.with_lock(key, blocking: true) { ... } ⇒ Object?

Execute a block with an advisory lock

Parameters:

  • key (String)

    Lock identifier

  • blocking (Boolean) (defaults to: true)

    If true, wait for lock. If false, return immediately if unavailable.

Yields:

  • Block to execute while holding lock

Returns:

  • (Object, nil)

    Returns block result if lock acquired, nil otherwise



94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/postburner/advisory_lock.rb', line 94

def self.with_lock(key, blocking: true)
  lock = new(key)

  acquired = blocking ? lock.acquire : lock.try_acquire
  return nil unless acquired

  begin
    yield
  ensure
    lock.release
  end
end

Instance Method Details

#acquireBoolean

Acquire the advisory lock (blocking)

This will wait until the lock is available.

Returns:

  • (Boolean)

    true if lock acquired



46
47
48
49
50
51
52
# File 'lib/postburner/advisory_lock.rb', line 46

def acquire
  return true if @acquired

  lock_id = generate_lock_id(key)
  result = connection.execute("SELECT pg_advisory_lock(#{lock_id})")
  @acquired = true
end

#acquired?Boolean

Check if this lock instance has acquired the lock

Returns:

  • (Boolean)


84
85
86
# File 'lib/postburner/advisory_lock.rb', line 84

def acquired?
  @acquired
end

#releaseBoolean

Release the advisory lock

Returns:

  • (Boolean)

    true if lock was held and released



71
72
73
74
75
76
77
78
79
# File 'lib/postburner/advisory_lock.rb', line 71

def release
  return false unless @acquired

  lock_id = generate_lock_id(key)
  result = connection.execute("SELECT pg_advisory_unlock(#{lock_id})").first
  @acquired = false
  value = result['pg_advisory_unlock']
  value == true || value == 't'
end

#try_acquireBoolean

Try to acquire the advisory lock (non-blocking)

Returns immediately, indicating whether lock was acquired.

Returns:

  • (Boolean)

    true if lock acquired, false if already held



59
60
61
62
63
64
65
66
# File 'lib/postburner/advisory_lock.rb', line 59

def try_acquire
  return true if @acquired

  lock_id = generate_lock_id(key)
  result = connection.execute("SELECT pg_try_advisory_lock(#{lock_id})").first
  value = result['pg_try_advisory_lock']
  @acquired = value == true || value == 't'
end