class

PgORM::PgAdvisoryLock

Inherits YAML::Serializable < JSON::Serializable < Reference < Object

PostgreSQL advisory locks for distributed locking and synchronization.

Advisory locks are application-level locks that use PostgreSQL's locking infrastructure. They're useful for:

  • Preventing concurrent execution of critical sections
  • Distributed job processing (ensure only one worker processes a job)
  • Rate limiting
  • Preventing duplicate operations

Features

  • Session-level locks: Automatically released when connection closes
  • Timeout support: Configurable timeout with exponential backoff
  • Named locks: Use string keys instead of numeric IDs
  • Fiber-safe: Works correctly with Crystal's concurrency model

Basic Usage

# Create a lock with a string key
lock = PgORM::PgAdvisoryLock.new("process_payments")

# Synchronize a block of code
lock.synchronize do
  # Only one fiber/process can execute this at a time
  process_payments()
end

# Manual lock/unlock
lock.lock
begin
  process_payments()
ensure
  lock.unlock
end

With Timeout

lock = PgORM::PgAdvisoryLock.new("critical_section")
lock.timeout = 10.seconds

begin
  lock.synchronize do
    # Critical section
  end
rescue PgORM::Error::LockUnavailable
  puts "Could not acquire lock within 10 seconds"
end

Try Lock (Non-blocking)

lock = PgORM::PgAdvisoryLock.new("optional_task")

if lock.try_lock
  begin
    # Got the lock, do work
    perform_task()
  ensure
    lock.unlock
  end
else
  puts "Lock already held, skipping task"
end

How It Works

The lock key (string) is hashed using SHA1 and converted to a 64-bit integer that PostgreSQL's advisory lock functions can use. This means:

  • Same key always produces the same lock ID
  • Different keys are extremely unlikely to collide
  • Locks are visible in pg_locks system view

Constructors

new(ctx : YAML::ParseContext, node : YAML::Nodes::Node)
Source
new(pull : JSON::PullParser)
Source
new(key : String, timeout : Time::Span | Nil = nil)

Creates a new advisory lock with the given key.

Parameters

  • key: String identifier for the lock (will be hashed to Int64)
  • timeout: Optional timeout for lock acquisition (defaults to 5 seconds)

Example

lock = PgORM::PgAdvisoryLock.new("process_payments")
lock = PgORM::PgAdvisoryLock.new("critical_section", timeout: 10.seconds)
Source
new(*, __pull_for_json_serializable pull : JSON::PullParser)
Source
new(*, __context_for_yaml_serializable ctx : YAML::ParseContext, __node_for_yaml_serializable node : YAML::Nodes::Node)
Source

Class methods

count

Returns the number of advisory locks currently held in the database.

Useful for monitoring and debugging.

Example

puts "Active advisory locks: #{PgORM::PgAdvisoryLock.count}"
Source

Instance methods

lock(timeout : Time::Span = self.timeout) : Nil

Acquires the lock, waiting up to the specified timeout.

Uses exponential backoff (starting at 0.1s, doubling up to 1s) to reduce database load while waiting.

Example

lock = PgORM::PgAdvisoryLock.new("my_lock")
lock.lock(timeout: 10.seconds)
begin
  # Critical section
ensure
  lock.unlock
end

Raises Error::LockUnavailable if the lock cannot be acquired within the timeout.

Source
locked?
Source
synchronize

Acquires the lock, executes the block, and releases the lock.

This is the recommended way to use advisory locks as it ensures the lock is always released, even if an exception occurs.

Example

lock = PgORM::PgAdvisoryLock.new("process_payments")
lock.synchronize do
  # Only one fiber/process can execute this at a time
  Payment.process_pending
end

Raises Error::LockUnavailable if the lock cannot be acquired within the timeout.

Source
timeout

Lock acquisition timeout (defaults to 5 seconds from settings)

Source
timeout=(timeout : Time::Span)

Lock acquisition timeout (defaults to 5 seconds from settings)

Source
try_lock

Attempts to acquire the lock without waiting.

Returns true if the lock was acquired, false if it's already held. This is useful when you want to skip work if the lock is unavailable.

Example

lock = PgORM::PgAdvisoryLock.new("optional_task")
if lock.try_lock
  begin
    perform_task()
  ensure
    lock.unlock
  end
else
  puts "Lock already held, skipping"
end

Raises Error::LockInvalidOp if the lock is already held by this instance.

Source
unlock

Releases the lock.

Example

lock.lock
begin
  # Critical section
ensure
  lock.unlock
end

Raises Error::LockInvalidOp if the lock is not currently held.

Source