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



210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/good_job/lockable.rb', line 210

def advisory_lock(key: lockable_key, function: advisory_lockable_function)
  query = if function.include? "_try_"
            "              SELECT \#{function}(('x'||substr(md5($1::text), 1, 16))::bit(64)::bigint) AS locked\n            SQL\n          else\n            <<~SQL.squish\n              SELECT \#{function}(('x'||substr(md5($1::text), 1, 16))::bit(64)::bigint)::text AS locked\n            SQL\n          end\n\n  binds = [[nil, key]]\n  self.class.connection.exec_query(pg_or_jdbc_query(query), 'GoodJob::Lockable Advisory Lock', binds).first['locked']\nend\n".squish

#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:



246
247
248
249
# File 'lib/good_job/lockable.rb', line 246

def advisory_lock!(key: lockable_key, function: advisory_lockable_function)
  result = advisory_lock(key: key, function: function)
  result || 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)


276
277
278
279
280
281
282
283
284
285
286
287
# File 'lib/good_job/lockable.rb', line 276

def advisory_locked?(key: lockable_key)
  query = "    SELECT 1 AS one\n    FROM pg_locks\n    WHERE pg_locks.locktype = 'advisory'\n      AND pg_locks.objsubid = 1\n      AND pg_locks.classid = ('x' || substr(md5($1::text), 1, 16))::bit(32)::int\n      AND pg_locks.objid = (('x' || substr(md5($2::text), 1, 16))::bit(64) << 32)::bit(32)::int\n  SQL\n  binds = [[nil, key], [nil, key]]\n  self.class.connection.exec_query(pg_or_jdbc_query(query), 'GoodJob::Lockable Advisory Locked?', binds).any?\nend\n".squish

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



231
232
233
234
235
236
237
# File 'lib/good_job/lockable.rb', line 231

def advisory_unlock(key: lockable_key, function: self.class.advisory_unlockable_function(advisory_lockable_function))
  query = "    SELECT \#{function}(('x'||substr(md5($1::text), 1, 16))::bit(64)::bigint) AS unlocked\n  SQL\n  binds = [[nil, key]]\n  self.class.connection.exec_query(pg_or_jdbc_query(query), 'GoodJob::Lockable Advisory Unlock', binds).first['unlocked']\nend\n".squish

#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



311
312
313
# File 'lib/good_job/lockable.rb', line 311

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

#lockable_keyString

Default Advisory Lock key

Returns:

  • (String)


317
318
319
# File 'lib/good_job/lockable.rb', line 317

def lockable_key
  "#{self.class.table_name}-#{self[self.class._advisory_lockable_column]}"
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)


292
293
294
295
296
297
298
299
300
301
302
303
304
# File 'lib/good_job/lockable.rb', line 292

def owns_advisory_lock?(key: lockable_key)
  query = "    SELECT 1 AS one\n    FROM pg_locks\n    WHERE pg_locks.locktype = 'advisory'\n      AND pg_locks.objsubid = 1\n      AND pg_locks.classid = ('x' || substr(md5($1::text), 1, 16))::bit(32)::int\n      AND pg_locks.objid = (('x' || substr(md5($2::text), 1, 16))::bit(64) << 32)::bit(32)::int\n      AND pg_locks.pid = pg_backend_pid()\n  SQL\n  binds = [[nil, key], [nil, key]]\n  self.class.connection.exec_query(pg_or_jdbc_query(query), 'GoodJob::Lockable Owns Advisory Lock?', binds).any?\nend\n".squish

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



264
265
266
267
268
269
270
271
# File 'lib/good_job/lockable.rb', line 264

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)
  yield
ensure
  advisory_unlock(key: key, function: self.class.advisory_unlockable_function(function)) unless $ERROR_INFO.is_a? RecordAlreadyAdvisoryLockedError
end