class
PgORM::PgAdvisoryLock
- PgORM::PgAdvisoryLock
- Reference
- Object
Overview
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_lockssystem view
Included Modules
- JSON::Serializable
- YAML::Serializable
Extended Modules
Defined in:
pg-orm/lock.crConstructors
- .new(ctx : YAML::ParseContext, node : YAML::Nodes::Node)
- .new(pull : JSON::PullParser)
-
.new(key : String, timeout : Time::Span | Nil = nil)
Creates a new advisory lock with the given key.
Class Method Summary
-
.count
Returns the number of advisory locks currently held in the database.
Instance Method Summary
-
#lock(timeout : Time::Span = self.timeout) : Nil
Acquires the lock, waiting up to the specified timeout.
- #locked? : Bool
-
#synchronize(**options, &)
Acquires the lock, executes the block, and releases the lock.
-
#timeout : Time::Span
Lock acquisition timeout (defaults to 5 seconds from settings)
-
#timeout=(timeout : Time::Span)
Lock acquisition timeout (defaults to 5 seconds from settings)
-
#try_lock
Attempts to acquire the lock without waiting.
-
#unlock : Nil
Releases the lock.
Constructor Detail
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)
Class Method Detail
Returns the number of advisory locks currently held in the database.
Useful for monitoring and debugging.
Example
puts "Active advisory locks: #{PgORM::PgAdvisoryLock.count}"
Instance Method Detail
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.
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.
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.
Releases the lock.
Example
lock.lock
begin
# Critical section
ensure
lock.unlock
end
Raises Error::LockInvalidOp if the lock is not currently held.