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

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

Returns:

  • (Boolean)


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, options = args
  return if options[:transaction]

  function = advisory_unlock_function(options[:shared])
  begin
    execute_advisory(function, lock_keys, options[: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.message =~ /PG::ConnectionBad/

    raise unless e.message =~ ERROR_MESSAGE_REGEX

    begin
      rollback_db_transaction
      execute_advisory(function, lock_keys, options[:lock_name])
    ensure
      begin_db_transaction
    end
  end
end

#supports_database_timeout?Boolean

Returns:

  • (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.message.include?('deadlock detected') ||
                  e.message =~ ERROR_MESSAGE_REGEX)
    false
  else
    raise
  end
end