philiprehberger-lock_kit
File-based and PID locking for process coordination with TTL expiration, stale detection, read-write locks, and lock owner identification
Requirements
- Ruby >= 3.1
Installation
Add to your Gemfile:
gem 'philiprehberger-lock_kit'
Or install directly:
gem install philiprehberger-lock_kit
Usage
require 'philiprehberger/lock_kit'
Philiprehberger::LockKit.with_file_lock('/tmp/my.lock') do
# exclusive work here
end
File Locking with Timeout
Philiprehberger::LockKit.with_file_lock('/tmp/my.lock', timeout: 5) do
# waits up to 5 seconds for the lock
end
PID File Locking
Philiprehberger::LockKit.with_pid_lock('my_worker') do
# only one process with this name can run at a time
end
Read-Write Locks
# Multiple readers can hold the lock concurrently
Philiprehberger::LockKit.with_read_lock('/tmp/data.lock', timeout: 5) do
# read shared data
end
# Write lock is exclusive — no readers or other writers allowed
Philiprehberger::LockKit.with_write_lock('/tmp/data.lock', timeout: 5) do
# write shared data
end
Retry Lock
# Retries lock acquisition with exponential backoff
Philiprehberger::LockKit.with_retry_lock('/tmp/my.lock', retries: 5, delay: 0.2, backoff: 2) do
# acquired after retrying if initially contended
end
# With timeout per attempt, TTL, and custom cleanup
Philiprehberger::LockKit.with_retry_lock('/tmp/my.lock', retries: 3, delay: 0.1, backoff: 2, timeout: 5, ttl: 30) do
# each attempt waits up to 5s; lock expires after 30s
end
Lock with TTL (Time-to-Live)
# Lock expires after 30 seconds — treated as stale once elapsed
Philiprehberger::LockKit.with_file_lock('/tmp/my.lock', ttl: 30) do
# work that should not hold the lock indefinitely
end
# PID locks also support TTL
Philiprehberger::LockKit.with_pid_lock('my_worker', ttl: 60) do
# expires after 60 seconds
end
# Check if a lock has expired
Philiprehberger::LockKit.expired?('/tmp/my.lock') # => true/false
Automatic Stale Lock Cleanup
Philiprehberger::LockKit.with_file_lock('/tmp/my.lock', auto_cleanup: true) do
# automatically removes stale locks from dead processes
end
Philiprehberger::LockKit.with_pid_lock('my_worker', auto_cleanup: true) do
# same for PID locks
end
Lock Owner Identification
info = Philiprehberger::LockKit.owner('/tmp/my.lock')
# => { pid: 12345, hostname: 'web-01', acquired_at: 2026-03-28 12:00:00 +0000 }
Lock Waiting with Callbacks
Philiprehberger::LockKit.with_file_lock('/tmp/my.lock', timeout: 10, on_wait: ->(elapsed) { puts "Waiting #{elapsed}s..." }) do
# callback fires every 0.5s while waiting
end
Force Break Lock
# Break stale locks only (raises if held by a live process)
Philiprehberger::LockKit.break!('/tmp/my.lock')
# => { broken: true, previous_owner: { pid: 12345, hostname: 'web-01', acquired_at: ... } }
# Force break any lock regardless of status
Philiprehberger::LockKit.break!('/tmp/my.lock', force: true)
Manual File Lock
lock = Philiprehberger::LockKit::FileLock.new('/tmp/my.lock')
lock.acquire(timeout: 10)
# ... do work ...
lock.release
Manual PID Lock
lock = Philiprehberger::LockKit::PidLock.new('my_worker', dir: '/var/run')
lock.acquire
# ... do work ...
lock.release
Checking Lock Status
Philiprehberger::LockKit.locked?('/tmp/my.lock') # => true/false
Philiprehberger::LockKit.stale?('/tmp/my_worker.pid') # => true/false
API
LockKit
| Method | Description |
|---|---|
.with_file_lock(path, timeout: nil, auto_cleanup: false, on_wait: nil, ttl: nil) { } |
Execute block with exclusive file lock |
.with_retry_lock(path, retries: 3, delay: 0.1, backoff: 2, timeout: nil, auto_cleanup: true, ttl: nil) { } |
Execute block with file lock, retrying with exponential backoff |
.with_pid_lock(name, dir: Dir.tmpdir, auto_cleanup: false, ttl: nil) { } |
Execute block with PID file lock |
.with_read_lock(path, timeout: nil) { } |
Execute block with shared read lock |
.with_write_lock(path, timeout: nil) { } |
Execute block with exclusive write lock |
.locked?(path) |
Check if a file is currently locked |
.stale?(pid_file) |
Check if a PID file references a dead process |
.expired?(path) |
Check if a lock has expired based on its TTL |
.owner(path) |
Get lock owner metadata (pid, hostname, acquired_at) |
.break!(path, force: false) |
Break a lock (stale only by default, any with force) |
LockKit::FileLock
| Method | Description |
|---|---|
.new(path) |
Create a file lock instance |
#acquire(timeout: nil, auto_cleanup: false, on_wait: nil, ttl: nil) |
Acquire exclusive lock with optional timeout, cleanup, wait callback, and TTL |
#release |
Release the lock and close the file handle |
#locked? |
Check if the file is currently locked (false if expired) |
#expired? |
Check if the lock has expired based on its TTL |
#owner |
Get lock owner metadata |
LockKit::PidLock
| Method | Description |
|---|---|
.new(name, dir: Dir.tmpdir) |
Create a PID lock instance |
#acquire(auto_cleanup: false, ttl: nil) |
Acquire PID lock with optional TTL, raises if held by a living process |
#release |
Release lock and remove PID file |
#locked? |
Check if lock is held by a living process (false if expired) |
#expired? |
Check if the lock has expired based on its TTL |
#stale? |
Check if PID file references a dead process |
#owner |
Get lock owner metadata |
LockKit::ReadWriteLock
| Method | Description |
|---|---|
.new(path) |
Create a read-write lock instance |
#acquire_read(timeout: nil) |
Acquire shared read lock |
#release_read |
Release the read lock |
#acquire_write(timeout: nil) |
Acquire exclusive write lock |
#release_write |
Release the write lock |
#reader_count |
Get current number of active readers |
Development
bundle install
bundle exec rspec
bundle exec rubocop
Support
If you find this project useful: