Overview
File locking provides synchronization mechanisms that prevent multiple processes from simultaneously modifying the same file. When a process acquires a lock on a file, other processes attempting to access that file must wait until the lock releases. This coordination prevents data corruption, ensures consistency, and maintains atomicity of file operations.
Operating systems implement file locking through kernel-level primitives that track which processes hold locks on which files. The kernel enforces these locks by blocking operations that would violate the locking state. Applications invoke these primitives through system calls, typically wrapped by higher-level programming language APIs.
File locking addresses the fundamental problem of coordinating access to shared resources in concurrent systems. Without locking, two processes writing to the same file simultaneously could interleave their writes, producing corrupted data. Similarly, a process reading while another writes could see inconsistent partial updates.
# Without locking - potential corruption
file = File.open('counter.txt', 'r+')
count = file.read.to_i
count += 1
file.rewind
file.write(count.to_s)
file.close
# => Race condition if multiple processes execute this
Two primary locking modes exist: advisory and mandatory. Advisory locks require cooperating processes to check for and respect locks. The operating system tracks advisory locks but does not prevent processes from ignoring them. Mandatory locks, in contrast, force the operating system to block operations that conflict with existing locks, regardless of process cooperation.
File locking appears in numerous contexts: database systems use it for transaction isolation, web servers use it to coordinate log file writes, backup systems use it to prevent modification during backup, and daemon processes use lock files to ensure single-instance execution.
Key Principles
File locking operates through two fundamental lock types: shared locks and exclusive locks. A shared lock allows multiple processes to hold simultaneous read access to a file. An exclusive lock grants write access to exactly one process while blocking all other access. Multiple shared locks can coexist, but an exclusive lock conflicts with any other lock.
The lock acquisition process follows specific rules. When a process requests a lock, the kernel checks existing locks on that file. If no conflicting locks exist, the kernel grants the lock immediately. If conflicts exist, the process either blocks until conflicts clear or receives an immediate error, depending on whether blocking or non-blocking mode was specified.
Lock scope defines the portion of a file covered by a lock. Whole-file locks apply to the entire file from the first byte to the end. Byte-range locks apply to specific portions, allowing different processes to lock different regions simultaneously. Byte-range locking provides finer-grained concurrency but increases complexity.
Advisory locking relies on process cooperation. The operating system tracks which processes hold advisory locks but permits processes to read or write locked files without checking locks. Applications must explicitly test for locks before accessing files. This design prioritizes performance and flexibility but requires disciplined programming.
# Advisory lock checking
file = File.open('data.txt', 'r+')
if file.flock(File::LOCK_EX | File::LOCK_NB)
# Lock acquired, safe to modify
file.write("data")
file.flock(File::LOCK_UN)
else
# Lock held by another process
puts "Could not acquire lock"
end
file.close
Mandatory locking enforces locks at the operating system level. When a process attempts to read or write a file with conflicting mandatory locks, the operating system blocks the operation regardless of whether the process checked for locks. Mandatory locking provides stronger guarantees but comes with performance overhead and limited portability.
Lock inheritance and file descriptor behavior affect how locks propagate. In Unix-like systems, locks associate with file descriptions (not file descriptors), meaning that duplicate file descriptors created through dup share locks. When a process forks, the child inherits file descriptors but not locks. Closing any file descriptor pointing to a locked file description releases all locks held through that description.
Deadlock occurs when two or more processes each hold locks while waiting for locks held by others, creating a circular dependency. Process A holds lock on file X and waits for file Y. Process B holds lock on file Y and waits for file X. Neither can proceed. Preventing deadlock requires establishing lock acquisition ordering or using timeout mechanisms.
Lock release happens explicitly through unlock operations or implicitly when processes close files or terminate. Implicit release creates the risk of stale locks when processes crash before releasing locks. Applications must handle crash recovery by detecting and removing stale locks.
Network file systems introduce additional complexity. NFS (Network File System) historically implemented file locking through a separate lock daemon (lockd), which could fail independently of file operations. Modern NFS versions improve locking support, but network partitions and server failures still create challenging edge cases.
Ruby Implementation
Ruby exposes file locking through the File#flock method, which wraps the underlying POSIX flock() system call on Unix-like systems and similar mechanisms on Windows. The method operates on whole files rather than byte ranges, trading granularity for simplicity and portability.
file = File.open('resource.dat', 'r+')
file.flock(File::LOCK_EX)
# Exclusive lock acquired, perform operations
data = file.read
modified = process(data)
file.rewind
file.write(modified)
file.flock(File::LOCK_UN)
file.close
The flock method accepts lock type constants that specify the desired locking mode. File::LOCK_SH requests a shared lock for read operations. File::LOCK_EX requests an exclusive lock for write operations. File::LOCK_UN releases any held locks. These constants can combine with File::LOCK_NB using bitwise OR to specify non-blocking behavior.
file = File.open('data.txt', 'r')
if file.flock(File::LOCK_SH | File::LOCK_NB)
# Shared lock acquired without blocking
content = file.read
file.flock(File::LOCK_UN)
puts content
else
# Lock unavailable
puts "File locked by another process"
end
file.close
By default, flock blocks until the lock becomes available. When combined with File::LOCK_NB, the method returns immediately, returning false if the lock cannot be acquired or 0 if successful. This non-blocking mode allows applications to implement timeouts or alternative logic when locks are unavailable.
file = File.open('queue.dat', 'r+')
locked = file.flock(File::LOCK_EX | File::LOCK_NB)
if locked
# Process queue
process_queue(file)
file.flock(File::LOCK_UN)
else
# Queue locked, try later
schedule_retry
end
file.close
Ruby's file locking integrates with the block form of File.open, allowing automatic lock release when the block exits. This pattern ensures locks release even if exceptions occur within the block.
File.open('counter.txt', 'r+') do |file|
file.flock(File::LOCK_EX)
count = file.read.to_i
count += 1
file.rewind
file.write(count.to_s)
file.truncate(file.pos)
# Lock automatically releases when block exits
end
Lock upgrade and downgrade operations convert between shared and exclusive locks. Converting from shared to exclusive (upgrade) requires exclusive access, so other shared lock holders must release first. Converting from exclusive to shared (downgrade) always succeeds immediately.
file = File.open('config.txt', 'r+')
file.flock(File::LOCK_SH)
config = parse_config(file.read)
if needs_update?(config)
# Upgrade to exclusive lock
file.flock(File::LOCK_EX)
file.rewind
file.write(updated_config)
file.truncate(file.pos)
end
file.flock(File::LOCK_UN)
file.close
Platform differences affect flock behavior. On Unix-like systems, flock provides advisory locking by default. Windows implements mandatory locking semantics. NFS behavior varies by version and configuration. Applications requiring consistent cross-platform behavior must account for these differences.
def safe_flock(file, mode)
file.flock(mode)
rescue Errno::ENOLCK
# NFS or system limit reached
false
rescue Errno::EWOULDBLOCK
# Non-blocking lock unavailable (some platforms)
false
end
Ruby also provides access to fcntl for more granular locking control on systems supporting POSIX record locking. This interface allows byte-range locking but requires platform-specific code.
require 'fcntl'
file = File.open('database.dat', 'r+')
# Lock bytes 100-199 for exclusive access
lock = [Fcntl::F_WRLCK, 0, 100, 100, 0].pack('ssqqi')
file.fcntl(Fcntl::F_SETLKW, lock)
# Perform operations on locked region
file.close
Practical Examples
Coordinating concurrent writers demonstrates the fundamental use case for file locking. Multiple processes append entries to a shared log file. Without locking, interleaved writes produce corrupted log entries.
# Log writer with proper locking
def append_log(message)
File.open('application.log', 'a') do |file|
file.flock(File::LOCK_EX)
file.puts "[#{Time.now}] #{message}"
# Lock releases when block exits
end
end
# Safe for concurrent use
append_log("User logged in")
append_log("Transaction completed")
Implementing a shared counter requires read-modify-write atomicity. Multiple processes increment a counter stored in a file. Locking ensures each process reads the current value, increments it, and writes it back without interference.
def increment_counter(filename)
File.open(filename, File::RDWR | File::CREAT, 0644) do |file|
file.flock(File::LOCK_EX)
# Read current value
current = file.read.to_i
# Increment
new_value = current + 1
# Write back
file.rewind
file.write(new_value.to_s)
file.truncate(file.pos)
new_value
end
end
# Multiple processes can safely increment
puts increment_counter('visitor_count.txt')
# => 42
A daemon process ensures single-instance execution using a lock file. The daemon attempts to acquire an exclusive lock on a specific file at startup. If the lock fails, another instance already runs.
class DaemonLock
def initialize(lockfile)
@lockfile = lockfile
@file = nil
end
def acquire
@file = File.open(@lockfile, File::RDWR | File::CREAT, 0644)
unless @file.flock(File::LOCK_EX | File::LOCK_NB)
@file.close
raise "Daemon already running"
end
# Write PID to lock file
@file.rewind
@file.write(Process.pid.to_s)
@file.truncate(@file.pos)
@file.flush
true
end
def release
return unless @file
@file.flock(File::LOCK_UN)
@file.close
File.delete(@lockfile) rescue nil
end
end
# Usage in daemon
lock = DaemonLock.new('/var/run/mydaemon.lock')
begin
lock.acquire
# Run daemon operations
loop do
perform_work
sleep 1
end
ensure
lock.release
end
Reader-writer coordination optimizes for multiple concurrent readers while ensuring exclusive writer access. Readers acquire shared locks, allowing simultaneous reads. Writers acquire exclusive locks, blocking all other access.
class SharedResource
def initialize(filename)
@filename = filename
end
def read
File.open(@filename, 'r') do |file|
file.flock(File::LOCK_SH)
content = file.read
# Shared lock allows concurrent readers
content
end
end
def write(data)
File.open(@filename, 'w') do |file|
file.flock(File::LOCK_EX)
# Exclusive lock blocks all other access
file.write(data)
end
end
end
resource = SharedResource.new('shared.dat')
# Multiple readers can execute concurrently
reader1 = Thread.new { puts resource.read }
reader2 = Thread.new { puts resource.read }
# Writer blocks until readers complete
writer = Thread.new { resource.write("new data") }
[reader1, reader2, writer].each(&:join)
Processing a job queue with multiple workers requires lock coordination. Workers acquire locks to claim jobs, preventing duplicate processing.
class FileQueue
def initialize(queue_file)
@queue_file = queue_file
end
def push(job)
File.open(@queue_file, 'a') do |file|
file.flock(File::LOCK_EX)
file.puts(job.to_json)
end
end
def pop
File.open(@queue_file, 'r+') do |file|
file.flock(File::LOCK_EX)
lines = file.readlines
return nil if lines.empty?
job = JSON.parse(lines.shift)
file.rewind
file.write(lines.join)
file.truncate(file.pos)
job
end
rescue JSON::ParserError
nil
end
end
# Multiple workers safely process jobs
queue = FileQueue.new('jobs.queue')
workers = 4.times.map do |i|
Thread.new do
while job = queue.pop
puts "Worker #{i} processing: #{job}"
process_job(job)
end
end
end
workers.each(&:join)
Implementation Approaches
Advisory locking strategies rely on process cooperation and offer maximum performance. Applications must explicitly check for locks before accessing files. This approach works well in controlled environments where all processes follow the locking protocol. The operating system tracks locks but does not enforce them, allowing non-cooperating processes to bypass locks.
Advisory locking minimizes overhead because the kernel only updates lock tables rather than checking every file operation. This design suits applications where performance matters and all code paths properly acquire locks. Database systems often use advisory locking internally because they control all file access through a single process.
# Advisory locking pattern
class AdvisoryFile
def self.with_lock(filename, mode)
File.open(filename, mode) do |file|
file.flock(File::LOCK_EX)
yield file
end
end
end
# All code must use this pattern
AdvisoryFile.with_lock('data.txt', 'r+') do |file|
data = file.read
file.rewind
file.write(transform(data))
end
Mandatory locking strategies enforce locks at the operating system level. When enabled, the kernel blocks read and write operations that conflict with existing locks, preventing non-cooperating processes from corrupting data. This approach provides stronger safety guarantees but comes with performance costs and platform-specific behavior.
Enabling mandatory locking typically requires special file permission bits or filesystem mount options. On Linux, setting the set-gid bit without the group execute bit enables mandatory locking. Windows uses different mechanisms. Mandatory locking works poorly with memory-mapped files and certain system operations.
Lock file strategies use separate files to coordinate access rather than locking the resource itself. A process creates or locks a specific lock file to indicate resource ownership. This approach works across network filesystems that poorly support traditional file locking and allows additional metadata storage in the lock file.
# Lock file pattern
class LockFile
def initialize(resource_path)
@lock_path = "#{resource_path}.lock"
end
def with_lock
lock_file = File.open(@lock_path, File::CREAT | File::RDWR, 0644)
begin
lock_file.flock(File::LOCK_EX)
yield
ensure
lock_file.flock(File::LOCK_UN)
lock_file.close
end
end
end
# Usage
lock = LockFile.new('/var/data/resource.dat')
lock.with_lock do
# Access resource
end
Database-backed locking strategies store lock state in a database rather than using filesystem locks. This approach provides distributed locking across multiple servers and survives application crashes. Lock acquisition becomes a database transaction, and lock state queries use SQL.
Database locking works well for distributed systems and microservices where multiple servers access shared resources. The database provides ACID properties for lock operations and handles failure scenarios through timeouts and connection monitoring. Performance becomes dependent on database latency.
# Database-backed lock pattern
class DBLock
def initialize(db, resource_name)
@db = db
@resource_name = resource_name
end
def acquire(timeout: 10)
deadline = Time.now + timeout
loop do
result = @db.execute(
"INSERT INTO locks (resource, owner, acquired_at)
VALUES (?, ?, ?)
ON CONFLICT DO NOTHING",
@resource_name, Process.pid, Time.now
)
return true if result.changes > 0
raise "Lock timeout" if Time.now >= deadline
sleep 0.1
end
end
def release
@db.execute(
"DELETE FROM locks WHERE resource = ? AND owner = ?",
@resource_name, Process.pid
)
end
end
Timeout-based strategies add time limits to lock acquisition attempts. Rather than blocking indefinitely, processes wait for a specified duration before giving up. This approach prevents deadlocks and allows applications to implement fallback behavior when locks remain unavailable.
def with_timeout_lock(file, timeout: 5)
deadline = Time.now + timeout
loop do
if file.flock(File::LOCK_EX | File::LOCK_NB)
begin
yield file
ensure
file.flock(File::LOCK_UN)
end
return true
end
if Time.now >= deadline
return false
end
sleep 0.01
end
end
File.open('resource.dat', 'r+') do |file|
if with_timeout_lock(file, timeout: 3)
# Lock acquired
else
# Timeout occurred
end
end
Common Patterns
The lock-modify-unlock pattern forms the basic structure for most file locking operations. Acquire the lock, perform operations on the file, then release the lock. This pattern appears in nearly all locking scenarios and provides the foundation for more complex patterns.
def atomic_update(filename)
File.open(filename, 'r+') do |file|
file.flock(File::LOCK_EX)
# Modify
data = file.read
updated = yield data
file.rewind
file.write(updated)
file.truncate(file.pos)
file.flock(File::LOCK_UN)
end
end
atomic_update('config.json') do |content|
config = JSON.parse(content)
config['updated_at'] = Time.now.to_i
JSON.generate(config)
end
The try-lock pattern attempts lock acquisition without blocking. If the lock is unavailable, the application executes alternative logic rather than waiting. This pattern prevents contention in high-throughput scenarios and allows graceful degradation.
def try_process(filename)
File.open(filename, 'r+') do |file|
if file.flock(File::LOCK_EX | File::LOCK_NB)
begin
process_file(file)
return :processed
ensure
file.flock(File::LOCK_UN)
end
else
return :busy
end
end
end
result = try_process('queue.dat')
case result
when :processed
puts "Processing completed"
when :busy
puts "File busy, skipping"
end
The double-check pattern combines shared and exclusive locks to optimize read-modify-write operations. First acquire a shared lock and read the file. If modification is needed, upgrade to an exclusive lock. This pattern reduces contention when modifications are infrequent.
def conditional_update(filename)
File.open(filename, 'r+') do |file|
# Check with shared lock
file.flock(File::LOCK_SH)
data = file.read
if needs_update?(data)
# Upgrade to exclusive lock
file.flock(File::LOCK_EX)
# Re-read after acquiring exclusive lock
file.rewind
data = file.read
if needs_update?(data)
updated = perform_update(data)
file.rewind
file.write(updated)
file.truncate(file.pos)
end
end
file.flock(File::LOCK_UN)
end
end
The lock file with PID pattern stores the process ID in a lock file, enabling stale lock detection. When a process acquires the lock, it writes its PID to the lock file. Other processes can read the PID and check if that process still exists.
class PIDLockFile
def initialize(path)
@path = path
end
def acquire
@file = File.open(@path, File::CREAT | File::RDWR, 0644)
if @file.flock(File::LOCK_EX | File::LOCK_NB)
@file.truncate(0)
@file.write(Process.pid)
@file.flush
return true
end
# Check for stale lock
@file.rewind
pid = @file.read.to_i
if pid > 0 && !process_exists?(pid)
# Stale lock, force acquisition
@file.flock(File::LOCK_UN)
@file.flock(File::LOCK_EX)
@file.truncate(0)
@file.write(Process.pid)
@file.flush
return true
end
@file.close
false
end
def release
return unless @file
@file.flock(File::LOCK_UN)
@file.close
File.delete(@path) rescue nil
end
private
def process_exists?(pid)
Process.kill(0, pid)
true
rescue Errno::ESRCH
false
rescue Errno::EPERM
true
end
end
The lock hierarchy pattern establishes an ordering for acquiring multiple locks to prevent deadlock. When operations require multiple files, always acquire locks in the same order. Sort filenames and acquire locks sequentially.
def multi_file_operation(filenames)
files = filenames.sort.map { |name| File.open(name, 'r+') }
begin
# Acquire locks in sorted order
files.each { |f| f.flock(File::LOCK_EX) }
# Perform operation across all files
yield files
ensure
# Release locks
files.each do |f|
f.flock(File::LOCK_UN)
f.close
end
end
end
multi_file_operation(['a.txt', 'b.txt', 'c.txt']) do |files|
# All locks acquired, safe to operate
data = files.map(&:read)
results = process(data)
files.zip(results).each do |file, result|
file.rewind
file.write(result)
end
end
The lock-free read pattern optimizes for read-heavy workloads. Writers create new files atomically rather than locking existing files. Readers always see complete, consistent files without acquiring locks. This pattern uses atomic rename operations.
def atomic_write(filename, data)
temp_file = "#{filename}.tmp.#{Process.pid}"
File.open(temp_file, 'w') do |file|
file.write(data)
file.flush
file.fsync
end
# Atomic rename
File.rename(temp_file, filename)
ensure
File.delete(temp_file) rescue nil
end
def lock_free_read(filename)
File.read(filename)
end
# Writes don't block reads
atomic_write('data.txt', 'new content')
content = lock_free_read('data.txt')
Error Handling & Edge Cases
Lock acquisition failures occur when another process holds a conflicting lock. Non-blocking operations return immediately with a false value. Blocking operations raise exceptions if interrupted by signals or timeout mechanisms.
def safe_lock_acquire(file)
file.flock(File::LOCK_EX | File::LOCK_NB)
rescue Errno::EWOULDBLOCK, Errno::EAGAIN
# Lock held by another process (platform-dependent errno)
false
rescue Errno::ENOLCK
# System lock table full or NFS error
false
rescue Errno::EBADF
# Invalid file descriptor
false
end
File.open('resource.dat', 'r+') do |file|
if safe_lock_acquire(file)
# Process file
file.flock(File::LOCK_UN)
else
# Handle lock failure
retry_later
end
end
Stale locks occur when processes crash or terminate abnormally without releasing locks. On most systems, the kernel automatically releases locks when processes exit, but lock files persist. Applications must detect and clean up stale lock files.
def acquire_with_stale_detection(lockfile)
file = File.open(lockfile, File::CREAT | File::RDWR, 0644)
# Try non-blocking lock
if file.flock(File::LOCK_EX | File::LOCK_NB)
file.truncate(0)
file.write("#{Process.pid}:#{Time.now.to_i}")
file.flush
return file
end
# Check if lock is stale
file.rewind
lock_info = file.read.split(':')
lock_pid = lock_info[0].to_i
lock_time = lock_info[1].to_i
# Lock is stale if process doesn't exist or lock is very old
stale = false
if lock_pid > 0
begin
Process.kill(0, lock_pid)
# Process exists, check age
stale = (Time.now.to_i - lock_time) > 3600
rescue Errno::ESRCH
# Process doesn't exist
stale = true
end
end
if stale
file.flock(File::LOCK_UN)
file.flock(File::LOCK_EX)
file.truncate(0)
file.write("#{Process.pid}:#{Time.now.to_i}")
file.flush
return file
end
file.close
nil
end
Signal interruption can cause lock operations to raise exceptions. When a process blocks on lock acquisition and receives a signal, the system call fails with EINTR. Applications must decide whether to retry or propagate the error.
def lock_with_retry(file, max_attempts: 3)
attempts = 0
begin
file.flock(File::LOCK_EX)
rescue Errno::EINTR
attempts += 1
if attempts < max_attempts
retry
else
raise "Lock acquisition interrupted repeatedly"
end
end
end
Network filesystem issues introduce complex failure modes. NFS clients may lose connection to the lock server, causing locks to fail or appear held indefinitely. Lock state can become inconsistent across clients during network partitions.
def nfs_safe_lock(file, timeout: 30)
start_time = Time.now
loop do
begin
return true if file.flock(File::LOCK_EX | File::LOCK_NB)
rescue Errno::ENOLCK
# NFS lock daemon failure
if Time.now - start_time > timeout
raise "NFS lock service unavailable"
end
sleep 1
next
end
if Time.now - start_time > timeout
return false
end
sleep 0.1
end
end
File descriptor leak prevention requires careful lock cleanup. Using blocks ensures automatic cleanup, but explicit file management needs ensure clauses. Leaked locks can cause resource exhaustion and prevent other processes from acquiring locks.
class ManagedLock
def initialize(filename)
@filename = filename
@file = nil
end
def acquire
@file = File.open(@filename, File::CREAT | File::RDWR, 0644)
@file.flock(File::LOCK_EX)
self
rescue => e
@file.close if @file
@file = nil
raise
end
def release
return unless @file
@file.flock(File::LOCK_UN)
@file.close
rescue IOError
# File already closed
ensure
@file = nil
end
def with_lock
acquire
yield
ensure
release
end
end
# Ensures cleanup even on exceptions
lock = ManagedLock.new('resource.lock')
lock.with_lock do
perform_operations
end
Race conditions between lock checking and file operations require atomic approaches. The time-of-check-to-time-of-use (TOCTOU) problem occurs when a process checks lock status, then attempts file operations, but another process modifies state between these steps.
# Vulnerable to TOCTOU
def vulnerable_read(filename)
file = File.open(filename, 'r')
if file.flock(File::LOCK_SH | File::LOCK_NB)
content = file.read # Another process could lock here
file.flock(File::LOCK_UN)
return content
end
nil
end
# Atomic approach
def safe_read(filename)
File.open(filename, 'r') do |file|
file.flock(File::LOCK_SH)
file.read
end
end
Permission errors prevent lock acquisition when processes lack necessary file permissions. The operation fails before attempting lock acquisition. Applications should verify file permissions during initialization rather than during critical operations.
def check_lock_permissions(filename)
begin
File.open(filename, File::CREAT | File::RDWR, 0644) do |file|
file.flock(File::LOCK_EX | File::LOCK_NB)
file.flock(File::LOCK_UN)
end
true
rescue Errno::EACCES, Errno::EPERM
false
end
end
if check_lock_permissions('/var/lock/myapp.lock')
# Proceed with application
else
abort "Insufficient permissions for lock file"
end
Reference
Lock Type Constants
| Constant | Value | Description |
|---|---|---|
| File::LOCK_SH | Platform-specific | Shared lock for read access |
| File::LOCK_EX | Platform-specific | Exclusive lock for write access |
| File::LOCK_UN | Platform-specific | Unlock the file |
| File::LOCK_NB | Platform-specific | Non-blocking mode flag |
File#flock Method Reference
| Operation | Syntax | Behavior |
|---|---|---|
| Acquire shared lock | file.flock(File::LOCK_SH) | Blocks until lock acquired |
| Acquire exclusive lock | file.flock(File::LOCK_EX) | Blocks until lock acquired |
| Non-blocking shared | file.flock(File::LOCK_SH | File::LOCK_NB) | Returns false if unavailable |
| Non-blocking exclusive | file.flock(File::LOCK_EX | File::LOCK_NB) | Returns false if unavailable |
| Release lock | file.flock(File::LOCK_UN) | Releases immediately |
Return Values
| Condition | Blocking Mode | Non-Blocking Mode |
|---|---|---|
| Lock acquired | Returns 0 | Returns 0 |
| Lock unavailable | Blocks | Returns false |
| Error condition | Raises exception | Raises exception |
Common Error Codes
| Error | Constant | Meaning |
|---|---|---|
| ENOLCK | Errno::ENOLCK | Lock table full or NFS failure |
| EBADF | Errno::EBADF | Invalid file descriptor |
| EINTR | Errno::EINTR | Interrupted by signal |
| EWOULDBLOCK | Errno::EWOULDBLOCK | Lock unavailable (non-blocking) |
| EAGAIN | Errno::EAGAIN | Lock unavailable (some platforms) |
| EACCES | Errno::EACCES | Permission denied |
Lock Compatibility Matrix
| Current Lock | Shared Request | Exclusive Request |
|---|---|---|
| None | Granted | Granted |
| Shared | Granted | Blocked |
| Exclusive | Blocked | Blocked |
Platform Differences
| Platform | Lock Type | Inheritance | NFS Support |
|---|---|---|---|
| Linux | Advisory | Not inherited | Requires lockd |
| macOS | Advisory | Not inherited | Requires lockd |
| FreeBSD | Advisory | Not inherited | Requires lockd |
| Windows | Mandatory | Not inherited | CIFS dependent |
Best Practices Checklist
| Practice | Rationale |
|---|---|
| Always use block form of File.open | Ensures automatic cleanup |
| Check return values for non-blocking locks | Prevents silent failures |
| Use timeout mechanisms for critical operations | Prevents indefinite blocking |
| Sort filenames when locking multiple files | Prevents deadlock |
| Store PID in lock files | Enables stale lock detection |
| Handle signal interruption | Improves reliability |
| Test lock behavior on target filesystems | Catches NFS and network issues |
| Document lock acquisition order | Maintains consistency |
Lock File Format Examples
| Format | Example | Use Case |
|---|---|---|
| PID only | 12345 | Simple process detection |
| PID with timestamp | 12345:1609459200 | Stale lock detection |
| JSON metadata | {"pid":12345,"host":"server1"} | Distributed systems |
| Multiple fields | 12345 server1 2021-01-01T00:00:00Z | Detailed tracking |