Class: Philiprehberger::LockKit::PidLock

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

Overview

PID-file based lock with stale process detection

Writes the current process ID, hostname, and timestamp to a file in JSON format. Other processes can check the file to determine if the lock holder is still alive, enabling automatic recovery from crashed processes.

Instance Method Summary collapse

Constructor Details

#initialize(name, dir: Dir.tmpdir) ⇒ PidLock

Returns a new instance of PidLock.

Parameters:

  • name (String)

    lock name (used as the PID file basename)

  • dir (String) (defaults to: Dir.tmpdir)

    directory for the PID file (defaults to system tmpdir)



17
18
19
20
21
22
# File 'lib/philiprehberger/lock_kit/pid_lock.rb', line 17

def initialize(name, dir: Dir.tmpdir)
  @name = name
  @dir = dir
  @pid_path = File.join(dir, "#{name}.pid")
  @acquired = false
end

Instance Method Details

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

Acquire the PID lock

Creates a PID file containing metadata (PID, hostname, timestamp) in JSON format. If a PID file already exists, checks whether the owning process is still alive. Stale PID files from dead processes are automatically cleaned up.

Parameters:

  • auto_cleanup (Boolean) (defaults to: false)

    when true, automatically remove stale locks

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

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

Returns:

  • (true)

    when the lock is acquired

Raises:



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
# File 'lib/philiprehberger/lock_kit/pid_lock.rb', line 35

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

  if File.exist?(@pid_path)
    # Check TTL expiration first
    if lock_expired?
      FileUtils.rm_f(@pid_path)
    else
      existing_pid = read_pid

      if existing_pid && process_alive?(existing_pid)
        raise Error, "Lock '#{@name}' is held by process #{existing_pid}"
      end

      # Stale PID file — remove it
      FileUtils.rm_f(@pid_path)
    end
  end

   = {
    'pid' => Process.pid,
    'hostname' => Socket.gethostname,
    'acquired_at' => Time.now.iso8601
  }
  ['expires_at'] = (Time.now + ttl).iso8601 if ttl
  File.write(@pid_path, JSON.generate())
  @acquired = true
  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



108
109
110
# File 'lib/philiprehberger/lock_kit/pid_lock.rb', line 108

def expired?
  lock_expired?
end

#locked?Boolean

Check whether the lock is currently held by a living process

Returns false if the lock has expired (TTL elapsed).

Returns:

  • (Boolean)


83
84
85
86
87
88
89
90
91
# File 'lib/philiprehberger/lock_kit/pid_lock.rb', line 83

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

  pid = read_pid
  return false unless pid

  process_alive?(pid)
end

#ownerHash?

Read lock owner metadata from the PID file

Returns:

  • (Hash, nil)

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



115
116
117
118
119
120
121
# File 'lib/philiprehberger/lock_kit/pid_lock.rb', line 115

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

  
rescue Errno::ENOENT
  nil
end

#releasevoid

This method returns an undefined value.

Release the lock by removing the PID file

Only removes the file if it was written by this process.



70
71
72
73
74
75
76
# File 'lib/philiprehberger/lock_kit/pid_lock.rb', line 70

def release
  return unless @acquired

  File.delete(@pid_path) if File.exist?(@pid_path) && read_pid == Process.pid

  @acquired = false
end

#stale?Boolean

Check whether the PID file references a dead process

Returns:

  • (Boolean)


96
97
98
99
100
101
102
103
# File 'lib/philiprehberger/lock_kit/pid_lock.rb', line 96

def stale?
  return false unless File.exist?(@pid_path)

  pid = read_pid
  return true unless pid

  !process_alive?(pid)
end