Module: GoodJob::Lockable

Extended by:
ActiveSupport::Concern
Included in:
Job
Defined in:
lib/good_job/lockable.rb

Overview

Adds Postgres advisory locking capabilities to an ActiveRecord record. For details on advisory locks, see the Postgres documentation:

Examples:

Add this concern to a MyRecord class:

class MyRecord < ActiveRecord::Base
  include Lockable

  def my_method
    ...
  end
end

Constant Summary collapse

RecordAlreadyAdvisoryLockedError =

Indicates an advisory lock is already held on a record by another database session.

Class.new(StandardError)

Instance Method Summary collapse

Instance Method Details

#advisory_lockBoolean

Acquires an advisory lock on this record if it is not already locked by another database session. Be careful to ensure you release the lock when you are done with #advisory_unlock (or #advisory_unlock! to release all remaining locks).

Returns:

  • (Boolean)

    whether the lock was acquired.



142
143
144
145
146
147
# File 'lib/good_job/lockable.rb', line 142

def advisory_lock
  where_sql = <<~SQL.squish
    pg_try_advisory_lock(('x' || substr(md5(:table_name || :id::text), 1, 16))::bit(64)::bigint)
  SQL
  self.class.unscoped.exists?([where_sql, { table_name: self.class.table_name, id: send(self.class.primary_key) }])
end

#advisory_lock!Boolean

Acquires an advisory lock on this record or raises RecordAlreadyAdvisoryLockedError if it is already locked by another database session.

Returns:

  • (Boolean)

    true

Raises:



165
166
167
168
# File 'lib/good_job/lockable.rb', line 165

def advisory_lock!
  result = advisory_lock
  result || raise(RecordAlreadyAdvisoryLockedError)
end

#advisory_locked?Boolean

Tests whether this record has an advisory lock on it.

Returns:

  • (Boolean)


193
194
195
# File 'lib/good_job/lockable.rb', line 193

def advisory_locked?
  self.class.unscoped.advisory_locked.exists?(id: send(self.class.primary_key))
end

#advisory_unlockBoolean

Releases an advisory lock on this record if it is locked by this database session. Note that advisory locks stack, so you must call #advisory_unlock and #advisory_lock the same number of times.

Returns:

  • (Boolean)

    whether the lock was released.



153
154
155
156
157
158
# File 'lib/good_job/lockable.rb', line 153

def advisory_unlock
  where_sql = <<~SQL.squish
    pg_advisory_unlock(('x' || substr(md5(:table_name || :id::text), 1, 16))::bit(64)::bigint)
  SQL
  self.class.unscoped.exists?([where_sql, { table_name: self.class.table_name, id: send(self.class.primary_key) }])
end

#advisory_unlock!void

This method returns an undefined value.

Releases all advisory locks on the record that are held by the current database session.



206
207
208
# File 'lib/good_job/lockable.rb', line 206

def advisory_unlock!
  advisory_unlock while advisory_locked?
end

#owns_advisory_lock?Boolean

Tests whether this record is locked by the current database session.

Returns:

  • (Boolean)


199
200
201
# File 'lib/good_job/lockable.rb', line 199

def owns_advisory_lock?
  self.class.unscoped.owns_advisory_locked.exists?(id: send(self.class.primary_key))
end

#with_advisory_lock { ... } ⇒ Object

Acquires an advisory lock on this record and safely releases it after the passed block is completed. If the record is locked by another database session, this raises RecordAlreadyAdvisoryLockedError.

Examples:

record = MyLockableRecord.first
record.with_advisory_lock do
  do_something_with record
end

Yields:

  • Nothing

Returns:

  • (Object)

    The result of the block.



182
183
184
185
186
187
188
189
# File 'lib/good_job/lockable.rb', line 182

def with_advisory_lock
  raise ArgumentError, "Must provide a block" unless block_given?

  advisory_lock!
  yield
ensure
  advisory_unlock unless $ERROR_INFO.is_a? RecordAlreadyAdvisoryLockedError
end