philiprehberger-lock_kit 0.3.0 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f82a00dac3cb9f7457b0d6e98d97a14a86502b1d2b2fe688b5bef0c07dcdbda1
4
- data.tar.gz: 88e1f0ac7f71c7585e7730b3984a9d4646c63ec7324f23731d2c088806257a3a
3
+ metadata.gz: fd5f40b2057061170fc5913d9ac238f7070d3fb05025e9f5bffe2e321e3b240b
4
+ data.tar.gz: ece6023899b7329973758c48b3f7da96937e9cdc700ec4a11f0e572a7f9d4246
5
5
  SHA512:
6
- metadata.gz: ba54abec814f3c18e17f0942f6954b3d108f59ea302ab8ae82efa21afaf118d911b9cc1c6ad1b089ad15b89faacb1c586372cf0b85ac5fe9a2843c4d89ce5f19
7
- data.tar.gz: 45248d0994c40fb782df7ce7185992c7a440267ad7fbc5bd43a27d2b0dc18b10172885b4b4c86aa9ddad550993a1ca661a43f979b7338d289fa0feffe128c5a9
6
+ metadata.gz: 1b57abbeb1216f641fbe8d386425df778fd50a9aadc64be51f98df41e3f47409e6886c0bfe588cde71223b73a684c7da5c7e4c14996c30e66f70961d0fd8bec3
7
+ data.tar.gz: 4e85ef77b8c6c6bdb50f327a8ddf0841f19754e40ec20b1cfa52e69b7b5ef1127cf8426ed4c4f5e4584c2ff3e54bc13ed8905ab6cd5a15b68da83d7925966d81
data/CHANGELOG.md CHANGED
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.5.0] - 2026-04-21
11
+
12
+ ### Added
13
+ - `ReadWriteLock#write_locked?` promoted to a public non-blocking query method
14
+ - `ReadWriteLock#stats` returning `{ readers:, write_locked: }` snapshot
15
+ - `LockKit.rw_stats(path)` module-level helper for the same snapshot without an instance
16
+
17
+ ## [0.4.0] - 2026-04-15
18
+
19
+ ### Added
20
+ - `with_retry_lock(path, retries: 3, delay: 0.1, backoff: 2, timeout: nil, auto_cleanup: true, ttl: nil)` module method for lock acquisition with exponential backoff retries
21
+
10
22
  ## [0.3.0] - 2026-04-10
11
23
 
12
24
  ### Added
data/README.md CHANGED
@@ -64,6 +64,20 @@ Philiprehberger::LockKit.with_write_lock('/tmp/data.lock', timeout: 5) do
64
64
  end
65
65
  ```
66
66
 
67
+ ### Retry Lock
68
+
69
+ ```ruby
70
+ # Retries lock acquisition with exponential backoff
71
+ Philiprehberger::LockKit.with_retry_lock('/tmp/my.lock', retries: 5, delay: 0.2, backoff: 2) do
72
+ # acquired after retrying if initially contended
73
+ end
74
+
75
+ # With timeout per attempt, TTL, and custom cleanup
76
+ Philiprehberger::LockKit.with_retry_lock('/tmp/my.lock', retries: 3, delay: 0.1, backoff: 2, timeout: 5, ttl: 30) do
77
+ # each attempt waits up to 5s; lock expires after 30s
78
+ end
79
+ ```
80
+
67
81
  ### Lock with TTL (Time-to-Live)
68
82
 
69
83
  ```ruby
@@ -151,6 +165,7 @@ Philiprehberger::LockKit.stale?('/tmp/my_worker.pid') # => true/false
151
165
  | Method | Description |
152
166
  |--------|-------------|
153
167
  | `.with_file_lock(path, timeout: nil, auto_cleanup: false, on_wait: nil, ttl: nil) { }` | Execute block with exclusive file lock |
168
+ | `.with_retry_lock(path, retries: 3, delay: 0.1, backoff: 2, timeout: nil, auto_cleanup: true, ttl: nil) { }` | Execute block with file lock, retrying with exponential backoff |
154
169
  | `.with_pid_lock(name, dir: Dir.tmpdir, auto_cleanup: false, ttl: nil) { }` | Execute block with PID file lock |
155
170
  | `.with_read_lock(path, timeout: nil) { }` | Execute block with shared read lock |
156
171
  | `.with_write_lock(path, timeout: nil) { }` | Execute block with exclusive write lock |
@@ -159,6 +174,7 @@ Philiprehberger::LockKit.stale?('/tmp/my_worker.pid') # => true/false
159
174
  | `.expired?(path)` | Check if a lock has expired based on its TTL |
160
175
  | `.owner(path)` | Get lock owner metadata (pid, hostname, acquired_at) |
161
176
  | `.break!(path, force: false)` | Break a lock (stale only by default, any with force) |
177
+ | `.rw_stats(path)` | Snapshot of a read-write lock as `{ readers:, write_locked: }` |
162
178
 
163
179
  ### `LockKit::FileLock`
164
180
 
@@ -193,6 +209,8 @@ Philiprehberger::LockKit.stale?('/tmp/my_worker.pid') # => true/false
193
209
  | `#acquire_write(timeout: nil)` | Acquire exclusive write lock |
194
210
  | `#release_write` | Release the write lock |
195
211
  | `#reader_count` | Get current number of active readers |
212
+ | `#write_locked?` | Non-blocking check for an active write lock |
213
+ | `#stats` | Snapshot as `{ readers:, write_locked: }` |
196
214
 
197
215
  ## Development
198
216
 
@@ -126,8 +126,9 @@ module Philiprehberger
126
126
  0
127
127
  end
128
128
 
129
- private
130
-
129
+ # Non-blocking check for whether a write lock is currently held.
130
+ #
131
+ # @return [Boolean]
131
132
  def write_locked?
132
133
  return false unless File.exist?(@write_lock_path)
133
134
 
@@ -143,6 +144,13 @@ module Philiprehberger
143
144
  end
144
145
  end
145
146
 
147
+ # Snapshot of the current lock state: readers and writer presence.
148
+ #
149
+ # @return [Hash] `{ readers: Integer, write_locked: Boolean }`
150
+ def stats
151
+ { readers: reader_count, write_locked: write_locked? }
152
+ end
153
+
146
154
  def increment_readers
147
155
  update_reader_count(1)
148
156
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Philiprehberger
4
4
  module LockKit
5
- VERSION = '0.3.0'
5
+ VERSION = '0.5.0'
6
6
  end
7
7
  end
@@ -89,6 +89,35 @@ module Philiprehberger
89
89
  end
90
90
  end
91
91
 
92
+ # Execute a block with a file lock, retrying with exponential backoff on failure
93
+ #
94
+ # @param path [String] path to the lock file
95
+ # @param retries [Integer] maximum number of attempts
96
+ # @param delay [Numeric] initial delay in seconds between retries
97
+ # @param backoff [Numeric] multiplier applied to delay after each failed attempt
98
+ # @param timeout [Numeric, nil] seconds to wait per acquire attempt
99
+ # @param auto_cleanup [Boolean] automatically remove stale locks before acquiring
100
+ # @param ttl [Numeric, nil] time-to-live in seconds; lock expires after this duration
101
+ # @yield block to execute while the lock is held
102
+ # @return [Object] the return value of the block
103
+ # @raise [Error] if all retry attempts are exhausted
104
+ def self.with_retry_lock(path, retries: 3, delay: 0.1, backoff: 2, timeout: nil, auto_cleanup: true, ttl: nil, &block)
105
+ attempts = 0
106
+ current_delay = delay
107
+
108
+ loop do
109
+ attempts += 1
110
+ begin
111
+ return with_file_lock(path, timeout: timeout, auto_cleanup: auto_cleanup, ttl: ttl, &block)
112
+ rescue Error
113
+ raise if attempts >= retries
114
+
115
+ sleep current_delay
116
+ current_delay *= backoff
117
+ end
118
+ end
119
+ end
120
+
92
121
  # Check if a file is currently locked by another process
93
122
  #
94
123
  # @param path [String] path to the lock file
@@ -97,6 +126,14 @@ module Philiprehberger
97
126
  FileLock.new(path).locked?
98
127
  end
99
128
 
129
+ # Snapshot of read-write lock state for `path` as `{ readers:, write_locked: }`.
130
+ #
131
+ # @param path [String] base path used with `with_read_lock` / `with_write_lock`
132
+ # @return [Hash] `{ readers: Integer, write_locked: Boolean }`
133
+ def self.rw_stats(path)
134
+ ReadWriteLock.new(path).stats
135
+ end
136
+
100
137
  # Check if a PID file references a dead process
101
138
  #
102
139
  # @param pid_file [String] path to the PID file
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: philiprehberger-lock_kit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Philip Rehberger
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-04-11 00:00:00.000000000 Z
11
+ date: 2026-04-21 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: File locks using flock and PID file locks with stale detection for coordinating
14
14
  between processes. Timeout support and automatic cleanup.