Module: GoodJob::AdvisoryLockable

Extended by:
ActiveSupport::Concern
Included in:
BaseExecution, BatchRecord, Process
Defined in:
app/models/concerns/good_job/advisory_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_lock(key: lockable_key, function: advisory_lockable_function) ⇒ Boolean

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).

Parameters:

  • key (String, Symbol) (defaults to: lockable_key)

    Key to Advisory Lock against

  • function (String, Symbol) (defaults to: advisory_lockable_function)

    Postgres Advisory Lock function name to use

Returns:

  • (Boolean)

    whether the lock was acquired.



316
317
318
# File 'app/models/concerns/good_job/advisory_lockable.rb', line 316

def advisory_lock(key: lockable_key, function: advisory_lockable_function)
  self.class.advisory_lock_key(key, function: function)
end

#advisory_lock!(key: lockable_key, function: advisory_lockable_function) ⇒ Boolean

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

Parameters:

  • key (String, Symbol) (defaults to: lockable_key)

    Key to lock against

  • function (String, Symbol) (defaults to: advisory_lockable_function)

    Postgres Advisory Lock function name to use

Returns:

  • (Boolean)

    true

Raises:



337
338
339
# File 'app/models/concerns/good_job/advisory_lockable.rb', line 337

def advisory_lock!(key: lockable_key, function: advisory_lockable_function)
  self.class.advisory_lock_key(key, function: function) || raise(RecordAlreadyAdvisoryLockedError)
end

#advisory_locked?(key: lockable_key) ⇒ Boolean

Tests whether this record has an advisory lock on it.

Parameters:

  • key (String, Symbol) (defaults to: lockable_key)

    Key to test lock against

Returns:

  • (Boolean)


368
369
370
# File 'app/models/concerns/good_job/advisory_lockable.rb', line 368

def advisory_locked?(key: lockable_key)
  self.class.advisory_locked_key?(key)
end

#advisory_unlock(key: lockable_key, function: self.class.advisory_unlockable_function(advisory_lockable_function)) ⇒ Boolean

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.

Parameters:

  • key (String, Symbol) (defaults to: lockable_key)

    Key to lock against

  • function (String, Symbol) (defaults to: self.class.advisory_unlockable_function(advisory_lockable_function))

    Postgres Advisory Lock function name to use

Returns:

  • (Boolean)

    whether the lock was released.



326
327
328
# File 'app/models/concerns/good_job/advisory_lockable.rb', line 326

def advisory_unlock(key: lockable_key, function: self.class.advisory_unlockable_function(advisory_lockable_function))
  self.class.advisory_unlock_key(key, function: function)
end

#advisory_unlock!(key: lockable_key, function: self.class.advisory_unlockable_function(advisory_lockable_function)) ⇒ void

This method returns an undefined value.

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

Parameters:

  • key (String, Symbol) (defaults to: lockable_key)

    Key to lock against

  • function (String, Symbol) (defaults to: self.class.advisory_unlockable_function(advisory_lockable_function))

    Postgres Advisory Lock function name to use



406
407
408
# File 'app/models/concerns/good_job/advisory_lockable.rb', line 406

def advisory_unlock!(key: lockable_key, function: self.class.advisory_unlockable_function(advisory_lockable_function))
  advisory_unlock(key: key, function: function) while advisory_locked?
end

#advisory_unlocked?(key: lockable_key) ⇒ Boolean

Tests whether this record does not have an advisory lock on it.

Parameters:

  • key (String, Symbol) (defaults to: lockable_key)

    Key to test lock against

Returns:

  • (Boolean)


375
376
377
# File 'app/models/concerns/good_job/advisory_lockable.rb', line 375

def advisory_unlocked?(key: lockable_key)
  !advisory_locked?(key: key)
end

#lockable_column_key(column: self.class._advisory_lockable_column) ⇒ String

Default Advisory Lock key for column-based locking

Returns:

  • (String)


418
419
420
# File 'app/models/concerns/good_job/advisory_lockable.rb', line 418

def lockable_column_key(column: self.class._advisory_lockable_column)
  "#{self.class.table_name}-#{self[column]}"
end

#lockable_keyString

Default Advisory Lock key

Returns:

  • (String)


412
413
414
# File 'app/models/concerns/good_job/advisory_lockable.rb', line 412

def lockable_key
  lockable_column_key
end

#owns_advisory_lock?(key: lockable_key) ⇒ Boolean

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

Parameters:

  • key (String, Symbol) (defaults to: lockable_key)

    Key to test lock against

Returns:

  • (Boolean)


382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
# File 'app/models/concerns/good_job/advisory_lockable.rb', line 382

def owns_advisory_lock?(key: lockable_key)
  self.class.owns_advisory_lock_key?(key)
  query = <<~SQL.squish
    SELECT 1 AS one
    FROM pg_locks
    WHERE pg_locks.locktype = 'advisory'
      AND pg_locks.objsubid = 1
      AND pg_locks.classid = ('x' || substr(md5($1::text), 1, 16))::bit(32)::int
      AND pg_locks.objid = (('x' || substr(md5($2::text), 1, 16))::bit(64) << 32)::bit(32)::int
      AND pg_locks.pid = pg_backend_pid()
    LIMIT 1
  SQL
  binds = [
    ActiveRecord::Relation::QueryAttribute.new('key', key, ActiveRecord::Type::String.new),
    ActiveRecord::Relation::QueryAttribute.new('key', key, ActiveRecord::Type::String.new),
  ]
  self.class.connection.exec_query(pg_or_jdbc_query(query), 'GoodJob::Lockable Owns Advisory Lock?', binds).any?
end

#with_advisory_lock(key: lockable_key, function: advisory_lockable_function) { ... } ⇒ 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

Parameters:

  • key (String, Symbol) (defaults to: lockable_key)

    Key to lock against

  • function (String, Symbol) (defaults to: advisory_lockable_function)

    Postgres Advisory Lock function name to use

Yields:

  • Nothing

Returns:

  • (Object)

    The result of the block.

Raises:

  • (ArgumentError)


354
355
356
357
358
359
360
361
362
363
# File 'app/models/concerns/good_job/advisory_lockable.rb', line 354

def with_advisory_lock(key: lockable_key, function: advisory_lockable_function)
  raise ArgumentError, "Must provide a block" unless block_given?

  advisory_lock!(key: key, function: function)
  begin
    yield
  ensure
    advisory_unlock(key: key, function: self.class.advisory_unlockable_function(function))
  end
end