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

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)


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

    function = advisory_unlock_function(options[:shared])
    execute_advisory(function, lock_keys, options[: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.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

#supports_database_timeout?Boolean

Returns:

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