Class: Philiprehberger::LockKit::FileLock

Inherits:
Object
  • Object
show all
Defined in:
lib/philiprehberger/lock_kit/file_lock.rb

Overview

Exclusive file lock using flock(2)

Provides process-level mutual exclusion via the filesystem. The lock is advisory and relies on all participants using the same lock file path.

Instance Method Summary collapse

Constructor Details

#initialize(path) ⇒ FileLock

Returns a new instance of FileLock.

Parameters:

  • path (String)

    path to the lock file



14
15
16
17
18
# File 'lib/philiprehberger/lock_kit/file_lock.rb', line 14

def initialize(path)
  @path = path
  @meta_path = "#{path}.meta"
  @file = nil
end

Instance Method Details

#acquire(timeout: nil, auto_cleanup: false, on_wait: nil, ttl: nil) ⇒ true

Acquire an exclusive lock on the file

Parameters:

  • timeout (Numeric, nil) (defaults to: nil)

    seconds to wait before raising; nil means non-blocking single attempt

  • auto_cleanup (Boolean) (defaults to: false)

    when true, check for stale locks and remove them

  • on_wait (Proc, nil) (defaults to: nil)

    callback invoked every 0.5s while waiting; receives elapsed seconds

  • ttl (Numeric, nil) (defaults to: nil)

    time-to-live in seconds; lock expires after this duration

Returns:

  • (true)

    when the lock is acquired

Raises:



28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/philiprehberger/lock_kit/file_lock.rb', line 28

def acquire(timeout: nil, auto_cleanup: false, on_wait: nil, ttl: nil)
  @ttl = ttl

  if auto_cleanup
    cleanup_stale_lock
  end

  @file = File.open(@path, File::CREAT | File::RDWR)

  if timeout.nil?
    unless @file.flock(File::LOCK_EX | File::LOCK_NB)
      close_file
      raise Error, "Could not acquire lock on #{@path}"
    end
  else
    deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
    start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
    last_callback = start_time

    until @file.flock(File::LOCK_EX | File::LOCK_NB)
      now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
      remaining = deadline - now

      if remaining <= 0
        close_file
        raise Error, "Timeout acquiring lock on #{@path} after #{timeout}s"
      end

      if on_wait && (now - last_callback) >= 0.5
        elapsed = (now - start_time).round(1)
        on_wait.call(elapsed)
        last_callback = now
      end

      sleep [0.05, remaining].min
    end
  end

  
  true
end

#expired?Boolean

Check whether the lock has expired based on its TTL

Returns:

  • (Boolean)

    true if the lock metadata contains an expires_at time that has passed



107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/philiprehberger/lock_kit/file_lock.rb', line 107

def expired?
  return false unless File.exist?(@meta_path)

  content = File.read(@meta_path).strip
  return false if content.empty?

  data = JSON.parse(content)
  expires_at = data['expires_at']
  return false unless expires_at

  Time.parse(expires_at) <= Time.now
rescue JSON::ParserError, Errno::ENOENT
  false
end

#locked?Boolean

Check whether the file is currently locked by another process

Opens the file, attempts a non-blocking exclusive lock, and immediately releases it. Returns true if the lock attempt fails (file is locked). Returns false if the lock has expired (TTL elapsed).

Returns:

  • (Boolean)


88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/philiprehberger/lock_kit/file_lock.rb', line 88

def locked?
  return false unless File.exist?(@path)
  return false if expired?

  f = File.open(@path, File::CREAT | File::RDWR)
  got_lock = f.flock(File::LOCK_EX | File::LOCK_NB)
  if got_lock
    f.flock(File::LOCK_UN)
    f.close
    false
  else
    f.close
    true
  end
end

#ownerHash?

Read lock owner metadata

Returns:

  • (Hash, nil)

    hash with :pid, :hostname, :acquired_at keys or nil



125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/philiprehberger/lock_kit/file_lock.rb', line 125

def owner
  return nil unless File.exist?(@meta_path)

  content = File.read(@meta_path).strip
  return nil if content.empty?

  data = JSON.parse(content)
  {
    pid: data['pid'],
    hostname: data['hostname'],
    acquired_at: data['acquired_at'] ? Time.parse(data['acquired_at']) : nil
  }
rescue JSON::ParserError, Errno::ENOENT
  nil
end

#releasevoid

This method returns an undefined value.

Release the lock and close the file handle



73
74
75
76
77
78
79
# File 'lib/philiprehberger/lock_kit/file_lock.rb', line 73

def release
  return unless @file

  
  @file.flock(File::LOCK_UN)
  close_file
end