Module: WithAdvisoryLock::PostgreSQLAdvisory
- Extended by:
- ActiveSupport::Concern
- Defined in:
- lib/with_advisory_lock/postgresql_advisory.rb
Constant Summary collapse
- LOCK_RESULT_VALUES =
['t', true].freeze
- ERROR_MESSAGE_REGEX =
/ ERROR: +current transaction is aborted,/
Instance Method Summary collapse
-
#advisory_lock_exists_for?(lock_name, shared: false) ⇒ Boolean
Non-blocking check for advisory lock existence to avoid race conditions This queries pg_locks directly instead of trying to acquire the lock.
- #lock_keys_for(lock_name) ⇒ Object
- #release_advisory_lock(*args) ⇒ Object
- #supports_database_timeout? ⇒ Boolean
- #try_advisory_lock(lock_keys, lock_name:, shared:, transaction:, timeout_seconds: nil, blocking: false) ⇒ Object
Instance Method Details
#advisory_lock_exists_for?(lock_name, shared: false) ⇒ Boolean
Non-blocking check for advisory lock existence to avoid race conditions This queries pg_locks directly instead of trying to acquire the lock
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
# File 'lib/with_advisory_lock/postgresql_advisory.rb', line 84 def advisory_lock_exists_for?(lock_name, shared: false) lock_keys = lock_keys_for(lock_name) query = <<~SQL.squish SELECT 1 FROM pg_locks WHERE locktype = 'advisory' AND database = (SELECT oid FROM pg_database WHERE datname = CURRENT_DATABASE()) AND classid = #{lock_keys.first} AND objid = #{lock_keys.last} AND mode = '#{shared ? 'ShareLock' : 'ExclusiveLock'}' AND granted LIMIT 1 SQL query_value(query).present? rescue ActiveRecord::StatementInvalid # If pg_locks is not accessible, fall back to nil to indicate we should use the default method nil end |
#lock_keys_for(lock_name) ⇒ Object
71 72 73 74 75 76 |
# File 'lib/with_advisory_lock/postgresql_advisory.rb', line 71 def lock_keys_for(lock_name) [ stable_hashcode(lock_name), ENV.fetch(CoreAdvisory::LOCK_PREFIX_ENV, nil) ].map { |ea| ea.to_i & 0x7fffffff } end |
#release_advisory_lock(*args) ⇒ Object
44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
# File 'lib/with_advisory_lock/postgresql_advisory.rb', line 44 def release_advisory_lock(*args) # ActiveRecord's built-in signature: release_advisory_lock(lock_id) return super if args.length == 1 && args[0].is_a?(Integer) # Our signature: release_advisory_lock(lock_keys, lock_name:, shared:, transaction:) lock_keys, = args return if [:transaction] function = advisory_unlock_function([:shared]) begin execute_advisory(function, lock_keys, [:lock_name]) rescue ActiveRecord::StatementInvalid => e # If the connection is broken, the lock is automatically released by PostgreSQL # No need to fail the release operation return if e.cause.is_a?(PG::ConnectionBad) || e. =~ /PG::ConnectionBad/ raise unless e. =~ ERROR_MESSAGE_REGEX begin rollback_db_transaction execute_advisory(function, lock_keys, [:lock_name]) ensure begin_db_transaction end end end |
#supports_database_timeout? ⇒ Boolean
78 79 80 |
# File 'lib/with_advisory_lock/postgresql_advisory.rb', line 78 def supports_database_timeout? false end |
#try_advisory_lock(lock_keys, lock_name:, shared:, transaction:, timeout_seconds: nil, blocking: false) ⇒ Object
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
# File 'lib/with_advisory_lock/postgresql_advisory.rb', line 12 def try_advisory_lock(lock_keys, lock_name:, shared:, transaction:, timeout_seconds: nil, blocking: false) function = if blocking advisory_lock_function(transaction, shared) else advisory_try_lock_function(transaction, shared) end if blocking && timeout_seconds&.positive? blocking_lock_with_timeout(function, lock_keys, lock_name, timeout_seconds) else execute_advisory(function, lock_keys, lock_name, blocking: blocking) end rescue ActiveRecord::Deadlocked # Rails 8.2+ raises ActiveRecord::Deadlocked directly for PostgreSQL deadlocks # When using blocking locks, treat deadlocks as lock acquisition failure return false if blocking raise rescue ActiveRecord::StatementInvalid => e # PostgreSQL deadlock detection raises PG::TRDeadlockDetected (SQLSTATE 40P01) # When using blocking locks, treat deadlocks as lock acquisition failure. # Rails 8.2+ may also retry after deadlock and get "current transaction is aborted" # when the transaction was rolled back by PostgreSQL's deadlock detection. if blocking && (e.cause.is_a?(PG::TRDeadlockDetected) || e..include?('deadlock detected') || e. =~ ERROR_MESSAGE_REGEX) false else raise end end |