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.



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

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:



346
347
348
# File 'app/models/concerns/good_job/advisory_lockable.rb', line 346

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)


378
379
380
# File 'app/models/concerns/good_job/advisory_lockable.rb', line 378

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.



335
336
337
# File 'app/models/concerns/good_job/advisory_lockable.rb', line 335

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



416
417
418
# File 'app/models/concerns/good_job/advisory_lockable.rb', line 416

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)


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

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)


428
429
430
# File 'app/models/concerns/good_job/advisory_lockable.rb', line 428

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)


422
423
424
# File 'app/models/concerns/good_job/advisory_lockable.rb', line 422

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)


392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
# File 'app/models/concerns/good_job/advisory_lockable.rb', line 392

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)


363
364
365
366
367
368
369
370
371
372
373
# File 'app/models/concerns/good_job/advisory_lockable.rb', line 363

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