Module: WithAdvisoryLock::PostgreSQLAdvisory
- Extended by:
- ActiveSupport::Concern
- Defined in:
- lib/with_advisory_lock/postgresql_advisory.rb
Constant Summary collapse
- LOCK_PREFIX_ENV =
'WITH_ADVISORY_LOCK_PREFIX'- 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
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
# File 'lib/with_advisory_lock/postgresql_advisory.rb', line 83 def advisory_lock_exists_for?(lock_name, shared: false) lock_keys = lock_keys_for(lock_name) query = " SELECT 1 FROM pg_locks\n WHERE locktype = 'advisory'\n AND database = (SELECT oid FROM pg_database WHERE datname = CURRENT_DATABASE())\n AND classid = \#{lock_keys.first}\n AND objid = \#{lock_keys.last}\n AND mode = '\#{shared ? 'ShareLock' : 'ExclusiveLock'}'\n LIMIT 1\n SQL\n\n query_value(query).present?\nrescue ActiveRecord::StatementInvalid\n # If pg_locks is not accessible, fall back to nil to indicate we should use the default method\n nil\nend\n".squish |
#lock_keys_for(lock_name) ⇒ Object
70 71 72 73 74 75 |
# File 'lib/with_advisory_lock/postgresql_advisory.rb', line 70 def lock_keys_for(lock_name) [ stable_hashcode(lock_name), ENV.fetch(LOCK_PREFIX_ENV, nil) ].map { |ea| ea.to_i & 0x7fffffff } end |
#release_advisory_lock(*args) ⇒ Object
42 43 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 |
# File 'lib/with_advisory_lock/postgresql_advisory.rb', line 42 def release_advisory_lock(*args) # Handle both signatures - ActiveRecord's built-in and ours if args.length == 1 && args[0].is_a?(Integer) # ActiveRecord's built-in signature: release_advisory_lock(lock_id) super else # Our signature: release_advisory_lock(lock_keys, lock_name:, shared:, transaction:) lock_keys, = args return if [:transaction] function = advisory_unlock_function([:shared]) execute_advisory(function, lock_keys, [:lock_name]) end 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 |
#supports_database_timeout? ⇒ Boolean
77 78 79 |
# File 'lib/with_advisory_lock/postgresql_advisory.rb', line 77 def supports_database_timeout? false end |
#try_advisory_lock(lock_keys, lock_name:, shared:, transaction:, timeout_seconds: nil, blocking: false) ⇒ Object
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 |
# File 'lib/with_advisory_lock/postgresql_advisory.rb', line 13 def try_advisory_lock(lock_keys, lock_name:, shared:, transaction:, timeout_seconds: nil, blocking: false) # timeout_seconds is accepted for compatibility but ignored - PostgreSQL doesn't support # native timeouts with pg_try_advisory_lock, requiring Ruby-level polling instead function = if blocking advisory_lock_function(transaction, shared) else advisory_try_lock_function(transaction, shared) end execute_advisory(function, lock_keys, lock_name, blocking: blocking) 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 |