philiprehberger-lock_kit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4323796d385deb9f9a2280dbd05aad5a442738b62fd126f2a9309ded2009d4e7
4
+ data.tar.gz: a552375e35331d9bcbeb02d7ffd09a1c27c9edd26114eb76a299cae9ff54f0b9
5
+ SHA512:
6
+ metadata.gz: cd045a2839a2885aa218b11146c2af5c43ee04de2c13934ba1c4dbb02e587bd9f6d1491a7f0cce7d31c8e01f6646cd54e0a45780623e5af0cc8631b1d5b2002c
7
+ data.tar.gz: 5f309ce7a1b7d03c4c86686e23766264e51118b3f8abc13c3c8e7cebd5ac9ef0bac66f07ebc544f06f753b0bf4cd6e33173278341cd71c79a56a1c9c948d473a
data/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ # Changelog
2
+
3
+ All notable changes to this gem will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-03-26
11
+
12
+ ### Added
13
+ - Initial release
14
+ - `FileLock` class with exclusive file locking via `flock(2)`
15
+ - `PidLock` class with PID file locking and stale process detection
16
+ - `with_file_lock` convenience method with optional timeout
17
+ - `with_pid_lock` convenience method with automatic cleanup
18
+ - `locked?` and `stale?` module-level query methods
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 philiprehberger
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,119 @@
1
+ # philiprehberger-lock_kit
2
+
3
+ [![Tests](https://github.com/philiprehberger/rb-lock-kit/actions/workflows/ci.yml/badge.svg)](https://github.com/philiprehberger/rb-lock-kit/actions/workflows/ci.yml)
4
+ [![Gem Version](https://badge.fury.io/rb/philiprehberger-lock_kit.svg)](https://rubygems.org/gems/philiprehberger-lock_kit)
5
+ [![License](https://img.shields.io/github/license/philiprehberger/rb-lock-kit)](LICENSE)
6
+ [![Sponsor](https://img.shields.io/badge/sponsor-GitHub%20Sponsors-ec6cb9)](https://github.com/sponsors/philiprehberger)
7
+
8
+ File-based and PID locking for process coordination
9
+
10
+ ## Requirements
11
+
12
+ - Ruby >= 3.1
13
+
14
+ ## Installation
15
+
16
+ Add to your Gemfile:
17
+
18
+ ```ruby
19
+ gem "philiprehberger-lock_kit"
20
+ ```
21
+
22
+ Or install directly:
23
+
24
+ ```bash
25
+ gem install philiprehberger-lock_kit
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ ```ruby
31
+ require "philiprehberger/lock_kit"
32
+
33
+ Philiprehberger::LockKit.with_file_lock("/tmp/my.lock") do
34
+ # exclusive work here
35
+ end
36
+ ```
37
+
38
+ ### File Locking with Timeout
39
+
40
+ ```ruby
41
+ Philiprehberger::LockKit.with_file_lock("/tmp/my.lock", timeout: 5) do
42
+ # waits up to 5 seconds for the lock
43
+ end
44
+ ```
45
+
46
+ ### PID File Locking
47
+
48
+ ```ruby
49
+ Philiprehberger::LockKit.with_pid_lock("my_worker") do
50
+ # only one process with this name can run at a time
51
+ end
52
+ ```
53
+
54
+ ### Manual File Lock
55
+
56
+ ```ruby
57
+ lock = Philiprehberger::LockKit::FileLock.new("/tmp/my.lock")
58
+ lock.acquire(timeout: 10)
59
+ # ... do work ...
60
+ lock.release
61
+ ```
62
+
63
+ ### Manual PID Lock
64
+
65
+ ```ruby
66
+ lock = Philiprehberger::LockKit::PidLock.new("my_worker", dir: "/var/run")
67
+ lock.acquire
68
+ # ... do work ...
69
+ lock.release
70
+ ```
71
+
72
+ ### Checking Lock Status
73
+
74
+ ```ruby
75
+ Philiprehberger::LockKit.locked?("/tmp/my.lock") # => true/false
76
+ Philiprehberger::LockKit.stale?("/tmp/my_worker.pid") # => true/false
77
+ ```
78
+
79
+ ## API
80
+
81
+ ### `LockKit`
82
+
83
+ | Method | Description |
84
+ |--------|-------------|
85
+ | `.with_file_lock(path, timeout: nil) { }` | Execute block with exclusive file lock |
86
+ | `.with_pid_lock(name, dir: Dir.tmpdir) { }` | Execute block with PID file lock |
87
+ | `.locked?(path)` | Check if a file is currently locked |
88
+ | `.stale?(pid_file)` | Check if a PID file references a dead process |
89
+
90
+ ### `LockKit::FileLock`
91
+
92
+ | Method | Description |
93
+ |--------|-------------|
94
+ | `.new(path)` | Create a file lock instance |
95
+ | `#acquire(timeout: nil)` | Acquire exclusive lock, optional timeout in seconds |
96
+ | `#release` | Release the lock and close the file handle |
97
+ | `#locked?` | Check if the file is currently locked |
98
+
99
+ ### `LockKit::PidLock`
100
+
101
+ | Method | Description |
102
+ |--------|-------------|
103
+ | `.new(name, dir: Dir.tmpdir)` | Create a PID lock instance |
104
+ | `#acquire` | Acquire PID lock, raises if held by a living process |
105
+ | `#release` | Release lock and remove PID file |
106
+ | `#locked?` | Check if lock is held by a living process |
107
+ | `#stale?` | Check if PID file references a dead process |
108
+
109
+ ## Development
110
+
111
+ ```bash
112
+ bundle install
113
+ bundle exec rspec
114
+ bundle exec rubocop
115
+ ```
116
+
117
+ ## License
118
+
119
+ [MIT](LICENSE)
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Philiprehberger
4
+ module LockKit
5
+ # Exclusive file lock using flock(2)
6
+ #
7
+ # Provides process-level mutual exclusion via the filesystem. The lock is
8
+ # advisory and relies on all participants using the same lock file path.
9
+ class FileLock
10
+ # @param path [String] path to the lock file
11
+ def initialize(path)
12
+ @path = path
13
+ @file = nil
14
+ end
15
+
16
+ # Acquire an exclusive lock on the file
17
+ #
18
+ # @param timeout [Numeric, nil] seconds to wait before raising; nil means non-blocking single attempt
19
+ # @return [true] when the lock is acquired
20
+ # @raise [LockKit::Error] if the lock cannot be acquired
21
+ def acquire(timeout: nil)
22
+ @file = File.open(@path, File::CREAT | File::RDWR)
23
+
24
+ if timeout.nil?
25
+ unless @file.flock(File::LOCK_EX | File::LOCK_NB)
26
+ close_file
27
+ raise Error, "Could not acquire lock on #{@path}"
28
+ end
29
+ else
30
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
31
+
32
+ until @file.flock(File::LOCK_EX | File::LOCK_NB)
33
+ remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
34
+ if remaining <= 0
35
+ close_file
36
+ raise Error, "Timeout acquiring lock on #{@path} after #{timeout}s"
37
+ end
38
+
39
+ sleep [0.05, remaining].min
40
+ end
41
+ end
42
+
43
+ true
44
+ end
45
+
46
+ # Release the lock and close the file handle
47
+ #
48
+ # @return [void]
49
+ def release
50
+ return unless @file
51
+
52
+ @file.flock(File::LOCK_UN)
53
+ close_file
54
+ end
55
+
56
+ # Check whether the file is currently locked by another process
57
+ #
58
+ # Opens the file, attempts a non-blocking exclusive lock, and immediately
59
+ # releases it. Returns true if the lock attempt fails (file is locked).
60
+ #
61
+ # @return [Boolean]
62
+ def locked?
63
+ return false unless File.exist?(@path)
64
+
65
+ f = File.open(@path, File::CREAT | File::RDWR)
66
+ got_lock = f.flock(File::LOCK_EX | File::LOCK_NB)
67
+ if got_lock
68
+ f.flock(File::LOCK_UN)
69
+ f.close
70
+ false
71
+ else
72
+ f.close
73
+ true
74
+ end
75
+ end
76
+
77
+ private
78
+
79
+ def close_file
80
+ @file&.close
81
+ @file = nil
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tmpdir'
4
+
5
+ module Philiprehberger
6
+ module LockKit
7
+ # PID-file based lock with stale process detection
8
+ #
9
+ # Writes the current process ID to a file. Other processes can check the
10
+ # file to determine if the lock holder is still alive, enabling automatic
11
+ # recovery from crashed processes.
12
+ class PidLock
13
+ # @param name [String] lock name (used as the PID file basename)
14
+ # @param dir [String] directory for the PID file (defaults to system tmpdir)
15
+ def initialize(name, dir: Dir.tmpdir)
16
+ @name = name
17
+ @dir = dir
18
+ @pid_path = File.join(dir, "#{name}.pid")
19
+ @acquired = false
20
+ end
21
+
22
+ # Acquire the PID lock
23
+ #
24
+ # Creates a PID file containing the current process ID. If a PID file
25
+ # already exists, checks whether the owning process is still alive. Stale
26
+ # PID files from dead processes are automatically cleaned up.
27
+ #
28
+ # @return [true] when the lock is acquired
29
+ # @raise [LockKit::Error] if the lock is held by a living process
30
+ def acquire
31
+ if File.exist?(@pid_path)
32
+ existing_pid = read_pid
33
+
34
+ if existing_pid && process_alive?(existing_pid)
35
+ raise Error, "Lock '#{@name}' is held by process #{existing_pid}"
36
+ end
37
+
38
+ # Stale PID file — remove it
39
+ FileUtils.rm_f(@pid_path)
40
+ end
41
+
42
+ File.write(@pid_path, Process.pid.to_s)
43
+ @acquired = true
44
+ true
45
+ end
46
+
47
+ # Release the lock by removing the PID file
48
+ #
49
+ # Only removes the file if it was written by this process.
50
+ #
51
+ # @return [void]
52
+ def release
53
+ return unless @acquired
54
+
55
+ File.delete(@pid_path) if File.exist?(@pid_path) && read_pid == Process.pid
56
+
57
+ @acquired = false
58
+ end
59
+
60
+ # Check whether the lock is currently held by a living process
61
+ #
62
+ # @return [Boolean]
63
+ def locked?
64
+ return false unless File.exist?(@pid_path)
65
+
66
+ pid = read_pid
67
+ return false unless pid
68
+
69
+ process_alive?(pid)
70
+ end
71
+
72
+ # Check whether the PID file references a dead process
73
+ #
74
+ # @return [Boolean]
75
+ def stale?
76
+ return false unless File.exist?(@pid_path)
77
+
78
+ pid = read_pid
79
+ return true unless pid
80
+
81
+ !process_alive?(pid)
82
+ end
83
+
84
+ private
85
+
86
+ # @return [Integer, nil]
87
+ def read_pid
88
+ content = File.read(@pid_path).strip
89
+ return nil if content.empty?
90
+
91
+ Integer(content)
92
+ rescue ArgumentError, Errno::ENOENT
93
+ nil
94
+ end
95
+
96
+ # @param pid [Integer]
97
+ # @return [Boolean]
98
+ def process_alive?(pid)
99
+ Process.kill(0, pid)
100
+ true
101
+ rescue Errno::ESRCH
102
+ false
103
+ rescue Errno::EPERM
104
+ # Process exists but we don't have permission to signal it
105
+ true
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Philiprehberger
4
+ module LockKit
5
+ VERSION = '0.1.0'
6
+ end
7
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lock_kit/version'
4
+ require_relative 'lock_kit/file_lock'
5
+ require_relative 'lock_kit/pid_lock'
6
+
7
+ module Philiprehberger
8
+ module LockKit
9
+ class Error < StandardError; end
10
+
11
+ # Execute a block while holding an exclusive file lock
12
+ #
13
+ # @param path [String] path to the lock file
14
+ # @param timeout [Numeric, nil] seconds to wait before raising
15
+ # @yield block to execute while the lock is held
16
+ # @return [Object] the return value of the block
17
+ # @raise [Error] if the lock cannot be acquired
18
+ def self.with_file_lock(path, timeout: nil, &block)
19
+ lock = FileLock.new(path)
20
+ lock.acquire(timeout: timeout)
21
+ begin
22
+ block.call
23
+ ensure
24
+ lock.release
25
+ end
26
+ end
27
+
28
+ # Execute a block while holding a PID file lock
29
+ #
30
+ # @param name [String] lock name
31
+ # @param dir [String] directory for the PID file
32
+ # @yield block to execute while the lock is held
33
+ # @return [Object] the return value of the block
34
+ # @raise [Error] if the lock is held by another process
35
+ def self.with_pid_lock(name, dir: Dir.tmpdir, &block)
36
+ lock = PidLock.new(name, dir: dir)
37
+ lock.acquire
38
+ begin
39
+ block.call
40
+ ensure
41
+ lock.release
42
+ end
43
+ end
44
+
45
+ # Check if a file is currently locked by another process
46
+ #
47
+ # @param path [String] path to the lock file
48
+ # @return [Boolean]
49
+ def self.locked?(path)
50
+ FileLock.new(path).locked?
51
+ end
52
+
53
+ # Check if a PID file references a dead process
54
+ #
55
+ # @param pid_file [String] path to the PID file
56
+ # @return [Boolean]
57
+ def self.stale?(pid_file)
58
+ return false unless File.exist?(pid_file)
59
+
60
+ content = File.read(pid_file).strip
61
+ return true if content.empty?
62
+
63
+ pid = Integer(content)
64
+ Process.kill(0, pid)
65
+ false
66
+ rescue ArgumentError
67
+ true
68
+ rescue Errno::ESRCH
69
+ true
70
+ rescue Errno::EPERM
71
+ false
72
+ end
73
+ end
74
+ end
metadata ADDED
@@ -0,0 +1,56 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: philiprehberger-lock_kit
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Philip Rehberger
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-03-27 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: File locks using flock and PID file locks with stale detection for coordinating
14
+ between processes. Timeout support and automatic cleanup.
15
+ email:
16
+ - me@philiprehberger.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - CHANGELOG.md
22
+ - LICENSE
23
+ - README.md
24
+ - lib/philiprehberger/lock_kit.rb
25
+ - lib/philiprehberger/lock_kit/file_lock.rb
26
+ - lib/philiprehberger/lock_kit/pid_lock.rb
27
+ - lib/philiprehberger/lock_kit/version.rb
28
+ homepage: https://github.com/philiprehberger/rb-lock-kit
29
+ licenses:
30
+ - MIT
31
+ metadata:
32
+ homepage_uri: https://github.com/philiprehberger/rb-lock-kit
33
+ source_code_uri: https://github.com/philiprehberger/rb-lock-kit
34
+ changelog_uri: https://github.com/philiprehberger/rb-lock-kit/blob/main/CHANGELOG.md
35
+ bug_tracker_uri: https://github.com/philiprehberger/rb-lock-kit/issues
36
+ rubygems_mfa_required: 'true'
37
+ post_install_message:
38
+ rdoc_options: []
39
+ require_paths:
40
+ - lib
41
+ required_ruby_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: 3.1.0
46
+ required_rubygems_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: '0'
51
+ requirements: []
52
+ rubygems_version: 3.5.22
53
+ signing_key:
54
+ specification_version: 4
55
+ summary: File-based and PID locking for process coordination
56
+ test_files: []