Module: GoodJob::Lockable
Overview
Adds Postgres advisory locking capabilities to an ActiveRecord record. For details on advisory locks, see the Postgres documentation:
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
-
#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.
-
#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.
-
#advisory_locked?(key: lockable_key) ⇒ Boolean
Tests whether this record has an advisory lock on it.
-
#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.
-
#advisory_unlock!(key: lockable_key, function: self.class.advisory_unlockable_function(advisory_lockable_function)) ⇒ void
Releases all advisory locks on the record that are held by the current database session.
-
#lockable_key ⇒ String
Default Advisory Lock key.
-
#owns_advisory_lock?(key: lockable_key) ⇒ Boolean
Tests whether this record is locked by the current database session.
-
#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.
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).
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.
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.
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.
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.
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_key ⇒ String
Default Advisory Lock key
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.
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.
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 |