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)


61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/with_advisory_lock/postgresql_advisory.rb', line 61

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'}'
    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



48
49
50
51
52
53
# File 'lib/with_advisory_lock/postgresql_advisory.rb', line 48

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



20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/with_advisory_lock/postgresql_advisory.rb', line 20

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)


55
56
57
# File 'lib/with_advisory_lock/postgresql_advisory.rb', line 55

def supports_database_timeout?
  false
end

#try_advisory_lock(lock_keys, lock_name:, shared:, transaction:, timeout_seconds: nil) ⇒ Object



13
14
15
16
17
18
# File 'lib/with_advisory_lock/postgresql_advisory.rb', line 13

def try_advisory_lock(lock_keys, lock_name:, shared:, transaction:, timeout_seconds: nil)
  # 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 = advisory_try_lock_function(transaction, shared)
  execute_advisory(function, lock_keys, lock_name)
end