Module: GoodJob::AdvisoryLockable

Extended by:
ActiveSupport::Concern
Included in:
BatchRecord, Job, 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.



333
334
335
# File 'app/models/concerns/good_job/advisory_lockable.rb', line 333

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:



354
355
356
# File 'app/models/concerns/good_job/advisory_lockable.rb', line 354

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)


386
387
388
# File 'app/models/concerns/good_job/advisory_lockable.rb', line 386

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.



343
344
345
# File 'app/models/concerns/good_job/advisory_lockable.rb', line 343

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

    Postgres Advisory Lock function name to use



424
425
426
# File 'app/models/concerns/good_job/advisory_lockable.rb', line 424

def advisory_unlock!(key: lockable_key, function: self.class.advisory_unlockable_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)


393
394
395
# File 'app/models/concerns/good_job/advisory_lockable.rb', line 393

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)


436
437
438
# File 'app/models/concerns/good_job/advisory_lockable.rb', line 436

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)


430
431
432
# File 'app/models/concerns/good_job/advisory_lockable.rb', line 430

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)


400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
# File 'app/models/concerns/good_job/advisory_lockable.rb', line 400

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)


371
372
373
374
375
376
377
378
379
380
381
# File 'app/models/concerns/good_job/advisory_lockable.rb', line 371

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
    unlock_function = self.class.advisory_unlockable_function(function)
    advisory_unlock(key: key, function: unlock_function) if unlock_function
  end
end