Module: Philiprehberger::LockKit
- Defined in:
- lib/philiprehberger/lock_kit.rb,
lib/philiprehberger/lock_kit/version.rb,
lib/philiprehberger/lock_kit/pid_lock.rb,
lib/philiprehberger/lock_kit/file_lock.rb,
lib/philiprehberger/lock_kit/read_write_lock.rb
Defined Under Namespace
Classes: Error, FileLock, PidLock, ReadWriteLock
Constant Summary collapse
- VERSION =
'0.4.0'
Class Method Summary collapse
-
.break!(path, force: false) ⇒ Hash
Force break a lock.
-
.expired?(path) ⇒ Boolean
Check if a lock has expired based on its TTL.
-
.locked?(path) ⇒ Boolean
Check if a file is currently locked by another process.
-
.owner(path) ⇒ Hash?
Get lock owner metadata.
-
.stale?(pid_file) ⇒ Boolean
Check if a PID file references a dead process.
-
.with_file_lock(path, timeout: nil, auto_cleanup: false, on_wait: nil, ttl: nil) { ... } ⇒ Object
Execute a block while holding an exclusive file lock.
-
.with_pid_lock(name, dir: Dir.tmpdir, auto_cleanup: false, ttl: nil) { ... } ⇒ Object
Execute a block while holding a PID file lock.
-
.with_read_lock(path, timeout: nil) { ... } ⇒ Object
Execute a block while holding a shared read lock.
-
.with_retry_lock(path, retries: 3, delay: 0.1, backoff: 2, timeout: nil, auto_cleanup: true, ttl: nil) { ... } ⇒ Object
Execute a block with a file lock, retrying with exponential backoff on failure.
-
.with_write_lock(path, timeout: nil) { ... } ⇒ Object
Execute a block while holding an exclusive write lock.
Class Method Details
.break!(path, force: false) ⇒ Hash
Force break a lock
227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 |
# File 'lib/philiprehberger/lock_kit.rb', line 227 def self.break!(path, force: false) = "#{path}.meta" # Determine the previous owner previous_owner = owner(path) unless previous_owner || File.exist?(path) return { broken: false, reason: 'not locked' } end # Check if the lock holder is alive if previous_owner && previous_owner[:pid] alive = begin Process.kill(0, previous_owner[:pid]) true rescue Errno::ESRCH false rescue Errno::EPERM true end if alive && !force raise Error, "Lock on #{path} is held by living process #{previous_owner[:pid]}" end end # Break the lock FileUtils.rm_f(path) FileUtils.rm_f() if previous_owner { broken: true, previous_owner: previous_owner } else { broken: true, previous_owner: nil } end end |
.expired?(path) ⇒ Boolean
Check if a lock has expired based on its TTL
161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 |
# File 'lib/philiprehberger/lock_kit.rb', line 161 def self.expired?(path) # Check file lock metadata = "#{path}.meta" target = if File.exist?() elsif File.exist?(path) path end return false unless target content = File.read(target).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?(path) ⇒ Boolean
Check if a file is currently locked by another process
125 126 127 |
# File 'lib/philiprehberger/lock_kit.rb', line 125 def self.locked?(path) FileLock.new(path).locked? end |
.owner(path) ⇒ Hash?
Get lock owner metadata
188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 |
# File 'lib/philiprehberger/lock_kit.rb', line 188 def self.owner(path) # Check for file lock metadata = "#{path}.meta" if File.exist?() content = File.read().strip unless content.empty? data = JSON.parse(content) return { pid: data['pid'], hostname: data['hostname'], acquired_at: data['acquired_at'] ? Time.parse(data['acquired_at']) : nil } end end # Check for PID lock (JSON format) if File.exist?(path) content = File.read(path).strip unless content.empty? data = JSON.parse(content) return { pid: data['pid'], hostname: data['hostname'], acquired_at: data['acquired_at'] ? Time.parse(data['acquired_at']) : nil } end end nil rescue JSON::ParserError, Errno::ENOENT nil end |
.stale?(pid_file) ⇒ Boolean
Check if a PID file references a dead process
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 |
# File 'lib/philiprehberger/lock_kit.rb', line 133 def self.stale?(pid_file) return false unless File.exist?(pid_file) content = File.read(pid_file).strip return true if content.empty? # Try JSON format first pid = begin data = JSON.parse(content) data.is_a?(Hash) ? data['pid'] : Integer(data) rescue JSON::ParserError Integer(content) end Process.kill(0, pid) false rescue ArgumentError true rescue Errno::ESRCH true rescue Errno::EPERM false end |
.with_file_lock(path, timeout: nil, auto_cleanup: false, on_wait: nil, ttl: nil) { ... } ⇒ Object
Execute a block while holding an exclusive file lock
24 25 26 27 28 29 30 31 32 |
# File 'lib/philiprehberger/lock_kit.rb', line 24 def self.with_file_lock(path, timeout: nil, auto_cleanup: false, on_wait: nil, ttl: nil, &block) lock = FileLock.new(path) lock.acquire(timeout: timeout, auto_cleanup: auto_cleanup, on_wait: on_wait, ttl: ttl) begin block.call ensure lock.release end end |
.with_pid_lock(name, dir: Dir.tmpdir, auto_cleanup: false, ttl: nil) { ... } ⇒ Object
Execute a block while holding a PID file lock
43 44 45 46 47 48 49 50 51 |
# File 'lib/philiprehberger/lock_kit.rb', line 43 def self.with_pid_lock(name, dir: Dir.tmpdir, auto_cleanup: false, ttl: nil, &block) lock = PidLock.new(name, dir: dir) lock.acquire(auto_cleanup: auto_cleanup, ttl: ttl) begin block.call ensure lock.release end end |
.with_read_lock(path, timeout: nil) { ... } ⇒ Object
Execute a block while holding a shared read lock
Multiple readers can hold the lock concurrently. Blocks if a write lock is held.
63 64 65 66 67 68 69 70 71 |
# File 'lib/philiprehberger/lock_kit.rb', line 63 def self.with_read_lock(path, timeout: nil, &block) lock = ReadWriteLock.new(path) lock.acquire_read(timeout: timeout) begin block.call ensure lock.release_read end end |
.with_retry_lock(path, retries: 3, delay: 0.1, backoff: 2, timeout: nil, auto_cleanup: true, ttl: nil) { ... } ⇒ Object
Execute a block with a file lock, retrying with exponential backoff on failure
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
# File 'lib/philiprehberger/lock_kit.rb', line 104 def self.with_retry_lock(path, retries: 3, delay: 0.1, backoff: 2, timeout: nil, auto_cleanup: true, ttl: nil, &block) attempts = 0 current_delay = delay loop do attempts += 1 begin return with_file_lock(path, timeout: timeout, auto_cleanup: auto_cleanup, ttl: ttl, &block) rescue Error raise if attempts >= retries sleep current_delay current_delay *= backoff end end end |
.with_write_lock(path, timeout: nil) { ... } ⇒ Object
Execute a block while holding an exclusive write lock
No readers or other writers are allowed while the write lock is held.
82 83 84 85 86 87 88 89 90 |
# File 'lib/philiprehberger/lock_kit.rb', line 82 def self.with_write_lock(path, timeout: nil, &block) lock = ReadWriteLock.new(path) lock.acquire_write(timeout: timeout) begin block.call ensure lock.release_write end end |