CrackedRuby CrackedRuby

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