redis_queued_locks 0.0.4 → 0.0.6
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 +4 -4
- data/CHANGELOG.md +10 -0
- data/README.md +276 -11
- data/lib/redis_queued_locks/acquier/release.rb +0 -2
- data/lib/redis_queued_locks/acquier.rb +22 -9
- data/lib/redis_queued_locks/client.rb +2 -2
- data/lib/redis_queued_locks/errors.rb +1 -1
- data/lib/redis_queued_locks/version.rb +2 -2
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9e98a72e1519c54d21a6273176a7d8a3bd9fa3033b7f30ca73fdca5a4335341d
|
4
|
+
data.tar.gz: b2cf62db6016bd456a379feb3703989cc4a481f2eed4103c5c27a60bec624d46
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d90c848b5fb00614c3f61719d470d7a2dae008d3b6349c395674fe7bf262feea09795dafc74756a16426f499652e9cb0dcb4eef8b0dc9e2c1cc8abff2a1345d1
|
7
|
+
data.tar.gz: ccc6e73175a2c41988303e3ea73feeacf95833b8d318dc1318c91f800d656ea157aeca62d7eba491ca489253a01ba4489f6a4c254c32f54ef3a540c9312ca179
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,15 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.0.6] - 2024-02-27
|
4
|
+
### Changed
|
5
|
+
- Major documentation updates;
|
6
|
+
- `RedisQueuedLock#release_lock!` now returns detaield semantic result;
|
7
|
+
- `RediSQueuedLock#release_all_locks!` now returns detailed semantic result;
|
8
|
+
|
9
|
+
## [0.0.5] - 2024-02-26
|
10
|
+
### Changed
|
11
|
+
- Minor gem update with documentation and configuration updates inside.
|
12
|
+
|
3
13
|
## [0.0.4] - 2024-02-26
|
4
14
|
### Changed
|
5
15
|
- changed default configuration values of `RedisQueuedLocks::Client` config;
|
data/README.md
CHANGED
@@ -1,8 +1,75 @@
|
|
1
1
|
# RedisQueuedLocks
|
2
2
|
|
3
3
|
Distributed lock implementation with "lock acquisition queue" capabilities based on the Redis Database.
|
4
|
+
Each lock request is put into a request queue and processed in order of their priority (FIFO).
|
4
5
|
|
5
|
-
|
6
|
+
---
|
7
|
+
|
8
|
+
## Table of Contents
|
9
|
+
|
10
|
+
- [Algorithm](#algorithm)
|
11
|
+
- [Installation](#installation)
|
12
|
+
- [Setup](#setup)
|
13
|
+
- [Configuration](#configuration)
|
14
|
+
- [Usage](#usage)
|
15
|
+
- [lock](#lock---obtain-a-lock)
|
16
|
+
- [lock!](#lock---exeptional-lock-obtaining)
|
17
|
+
- [unlock](#unlock---release-a-lock)
|
18
|
+
- [clear_locks](#clear_locks---release-all-locks-and-lock-queues)
|
19
|
+
- [Instrumentation](#instrumentation)
|
20
|
+
- [Instrumentation Events](#instrumentation-events)
|
21
|
+
- [TODO](#todo)
|
22
|
+
- [Contributing](#contributing)
|
23
|
+
- [License](#license)
|
24
|
+
- [Authors](#authors)
|
25
|
+
|
26
|
+
---
|
27
|
+
|
28
|
+
### Algorithm
|
29
|
+
|
30
|
+
- soon;
|
31
|
+
|
32
|
+
---
|
33
|
+
|
34
|
+
### Installation
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
gem 'redis_queued_locks'
|
38
|
+
```
|
39
|
+
|
40
|
+
```shell
|
41
|
+
bundle install
|
42
|
+
# --- or ---
|
43
|
+
gem install redis_queued_locks
|
44
|
+
```
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
require 'redis_queued_locks'
|
48
|
+
```
|
49
|
+
|
50
|
+
---
|
51
|
+
|
52
|
+
### Setup
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
require 'redis_queued_locks'
|
56
|
+
|
57
|
+
# Step 1: initialize RedisClient instance
|
58
|
+
redis_clinet = RedisClient.config.new_pool # NOTE: provide your ounw RedisClient instance
|
59
|
+
|
60
|
+
# Step 2: initialize RedisQueuedLock::Client instance
|
61
|
+
rq_lock_client = RedisQueuedLocks::Client.new(redis_client) do |config|
|
62
|
+
# NOTE:
|
63
|
+
# - some your application-related configs;
|
64
|
+
# - for documentation see <Configuration> section in readme;
|
65
|
+
end
|
66
|
+
|
67
|
+
# Step 3: start to work with locks :)
|
68
|
+
```
|
69
|
+
|
70
|
+
---
|
71
|
+
|
72
|
+
### Configuration
|
6
73
|
|
7
74
|
```ruby
|
8
75
|
redis_client = RedisClient.config.new_pool # NOTE: provide your own RedisClient instance
|
@@ -49,21 +116,193 @@ clinet = RedisQueuedLocks::Client.new(redis_client) do |config|
|
|
49
116
|
end
|
50
117
|
```
|
51
118
|
|
52
|
-
|
119
|
+
---
|
120
|
+
|
121
|
+
### Usage
|
122
|
+
|
123
|
+
- [lock](#lock---obtain-a-lock)
|
124
|
+
- [lock!](#lock---exeptional-lock-obtaining)
|
125
|
+
- [unlock](#unlock---release-a-lock)
|
126
|
+
- [clear_locks](#clear_locks---release-all-locks-and-lock-queues)
|
127
|
+
|
128
|
+
---
|
129
|
+
|
130
|
+
#### #lock - obtain a lock
|
53
131
|
|
54
132
|
```ruby
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
133
|
+
def lock(
|
134
|
+
lock_name,
|
135
|
+
process_id: RedisQueuedLocks::Resource.get_process_id,
|
136
|
+
thread_id: RedisQueuedLocks::Resource.get_thread_id,
|
137
|
+
ttl: config[:default_lock_ttl],
|
138
|
+
queue_ttl: config[:default_queue_ttl],
|
139
|
+
timeout: config[:try_to_lock_timeout],
|
140
|
+
retry_count: config[:retry_count],
|
141
|
+
retry_delay: config[:retry_delay],
|
142
|
+
retry_jitter: config[:retry_jitter],
|
143
|
+
raise_errors: false,
|
144
|
+
&block
|
145
|
+
)
|
146
|
+
```
|
147
|
+
|
148
|
+
- `lock_name` - `[String]`
|
149
|
+
- Lock name to be obtained.
|
150
|
+
- `process_id` - `[Integer,String]`
|
151
|
+
- The process that want to acquire the lock.
|
152
|
+
- `thread_id` - `[Integer,String]`
|
153
|
+
- The process's thread that want to acquire the lock.
|
154
|
+
- `ttl` [Integer]
|
155
|
+
- Lock's time to live (in milliseconds).
|
156
|
+
- `queue_ttl` - `[Integer]`
|
157
|
+
- Lifetime of the acuier's lock request. In seconds.
|
158
|
+
- `timeout` - `[Integer,NilClass]`
|
159
|
+
- Time period whe should try to acquire the lock (in seconds). Nil means "without timeout".
|
160
|
+
- `retry_count` - `[Integer,NilClass]`
|
161
|
+
- How many times we should try to acquire a lock. Nil means "infinite retries".
|
162
|
+
- `retry_delay` - `[Integer]`
|
163
|
+
- A time-interval between the each retry (in milliseconds).
|
164
|
+
- `retry_jitter` - `[Integer]`
|
165
|
+
- Time-shift range for retry-delay (in milliseconds).
|
166
|
+
- `instrumenter` - `[#notify]`
|
167
|
+
- See RedisQueuedLocks::Instrument::ActiveSupport for example.
|
168
|
+
- `raise_errors` - `[Boolean]`
|
169
|
+
- Raise errors on library-related limits such as timeout or failed lock obtain.
|
170
|
+
- `block` - `[Block]`
|
171
|
+
- A block of code that should be executed after the successfully acquired lock.
|
172
|
+
|
173
|
+
Return value:
|
174
|
+
- `[Hash<Symbol,Any>]` Format: `{ ok: true/false, result: Symbol/Hash }`;
|
175
|
+
- for successful lock obtaining:
|
176
|
+
```ruby
|
177
|
+
{
|
178
|
+
ok: true,
|
179
|
+
result: {
|
180
|
+
lock_key: String, # acquierd lock key ("rql:lock:your_lock_name")
|
181
|
+
acq_id: String, # acquier identifier ("your_process_id/your_thread_id")
|
182
|
+
ts: Integer, # time (epoch) when lock was obtained (integer)
|
183
|
+
ttl: Integer # lock's time to live in milliseconds (integer)
|
184
|
+
}
|
185
|
+
}
|
186
|
+
```
|
187
|
+
- for failed lock obtaining:
|
188
|
+
```ruby
|
189
|
+
{ ok: false, result: :timeout_reached }
|
190
|
+
{ ok: false, result: :retry_count_reached }
|
191
|
+
{ ok: false, result: :unknown }
|
192
|
+
```
|
193
|
+
|
194
|
+
|
195
|
+
---
|
196
|
+
|
197
|
+
#### #lock! - exceptional lock obtaining
|
198
|
+
|
199
|
+
- fails when (and with):
|
200
|
+
- (`RedisQueuedLocks::LockAcquiermentTimeoutError`) `timeout` limit reached before lock is obtained;
|
201
|
+
- (`RedisQueuedLocks::LockAcquiermentRetryLimitError`) `retry_count` limit reached before lock is obtained;
|
202
|
+
|
203
|
+
```ruby
|
204
|
+
def lock!(
|
205
|
+
lock_name,
|
206
|
+
process_id: RedisQueuedLocks::Resource.get_process_id,
|
207
|
+
thread_id: RedisQueuedLocks::Resource.get_thread_id,
|
208
|
+
ttl: config[:default_lock_ttl],
|
209
|
+
queue_ttl: config[:default_queue_ttl],
|
210
|
+
timeout: config[:default_timeout],
|
211
|
+
retry_count: config[:retry_count],
|
212
|
+
retry_delay: config[:retry_delay],
|
213
|
+
retry_jitter: config[:retry_jitter],
|
214
|
+
&block
|
215
|
+
)
|
216
|
+
```
|
217
|
+
|
218
|
+
See `#lock` method [documentation](#lock---obtain-a-lock).
|
219
|
+
|
220
|
+
---
|
221
|
+
|
222
|
+
#### #unlock - release a lock
|
223
|
+
|
224
|
+
- release the concrete lock with lock request queue;
|
225
|
+
- queue will be relased first;
|
226
|
+
|
227
|
+
```ruby
|
228
|
+
def unlock(lock_name)
|
229
|
+
```
|
230
|
+
|
231
|
+
- `lock_name` - `[String]`
|
232
|
+
- the lock name that should be released.
|
233
|
+
|
234
|
+
Return:
|
235
|
+
- `[Hash<Symbol,Any>]` - Format: `{ ok: true/false, result: Hash<Symbol,Numeric|String> }`;
|
236
|
+
|
237
|
+
```ruby
|
238
|
+
{
|
239
|
+
ok: true,
|
240
|
+
result: {
|
241
|
+
rel_time: 0.02, # time spent to lock release (in seconds)
|
242
|
+
rel_key: "rql:lock:your_lock_name", # released lock key
|
243
|
+
rel_queue: "rql:lock_queue:your_lock_name" # released lock key queue
|
244
|
+
}
|
245
|
+
}
|
246
|
+
```
|
247
|
+
|
248
|
+
---
|
249
|
+
|
250
|
+
#### #clear_locks - release all locks and lock queues
|
251
|
+
|
252
|
+
- release all obtained locks and related lock request queues;
|
253
|
+
- queues will be released first;
|
254
|
+
|
255
|
+
```ruby
|
256
|
+
def clear_locks(batch_size: config[:lock_release_batch_size])
|
257
|
+
```
|
258
|
+
|
259
|
+
- `batch_size` - `[Integer]`
|
260
|
+
- batch of cleared locks and lock queus unde the one pipelined redis command;
|
261
|
+
|
262
|
+
Return:
|
263
|
+
- `[Hash<Symbol,Any>]` - Format: `{ ok: true/false, result: Hash<Symbol,Numeric> }`;
|
264
|
+
|
265
|
+
```ruby
|
266
|
+
{
|
267
|
+
ok: true,
|
268
|
+
result: {
|
269
|
+
rel_time: 3.07, # time spent to release all locks and related lock queues
|
270
|
+
rel_key_cnt: 100_500 # released redis keys (released locks + released lock queues)
|
271
|
+
}
|
272
|
+
}
|
59
273
|
```
|
60
274
|
|
61
|
-
|
62
|
-
- `#lock!`
|
63
|
-
- `#unlock`
|
64
|
-
- `#clear_locks`
|
275
|
+
---
|
65
276
|
|
66
|
-
## Instrumentation
|
277
|
+
## Instrumentation
|
278
|
+
|
279
|
+
An instrumentation layer is incapsulated in `instrumenter` object stored in configurations.
|
280
|
+
|
281
|
+
Instrumenter object should provide `notify(event, payload)` method with following signarue:
|
282
|
+
|
283
|
+
- `event` - `string`;
|
284
|
+
- `payload` - `hash<Symbol,Any>`;
|
285
|
+
|
286
|
+
`redis_queued_locks` provides two instrumenters:
|
287
|
+
|
288
|
+
- `RedisQueuedLocks::Instrument::ActiveSupport` - `ActiveSupport::Notifications` instrumenter
|
289
|
+
that instrument events via `ActiveSupport::Notifications` api;
|
290
|
+
- `RedisQueuedLocks::Instrument::VoidNotifier` - instrumenter that does nothing;
|
291
|
+
|
292
|
+
By default `RedisQueuedLocks::Client` is configured with the void notifier (which means "instrumentation is disabled").
|
293
|
+
|
294
|
+
---
|
295
|
+
|
296
|
+
### Instrumentation Events
|
297
|
+
|
298
|
+
List of instrumentation events
|
299
|
+
|
300
|
+
- **redis_queued_locks.lock_obtained**
|
301
|
+
- **redis_queued_locks.lock_hold_and_release**
|
302
|
+
- **redis_queued_locks.explicit_lock_release**
|
303
|
+
- **redis_queued_locks.explicit_all_locks_release**
|
304
|
+
|
305
|
+
Detalized event semantics and payload structure:
|
67
306
|
|
68
307
|
- `"redis_queued_locks.lock_obtained"`
|
69
308
|
- a moment when the lock was obtained;
|
@@ -96,3 +335,29 @@ end
|
|
96
335
|
- `rel_time` - `float`/`milliseconds` - time spent on "realese all locks" operation;
|
97
336
|
- `at` - `integer`/`epoch` - the time when the operation has ended;
|
98
337
|
- `rel_keys` - `integer` - released redis keys count (`released queu keys` + `released lock keys`);
|
338
|
+
|
339
|
+
---
|
340
|
+
|
341
|
+
## TODO
|
342
|
+
|
343
|
+
- `RedisQueuedLocks::Acquier::Try.try_to_lock` - detailed successful result analization;
|
344
|
+
- `100%` test coverage;
|
345
|
+
- better code stylization and interesting refactorings :)
|
346
|
+
|
347
|
+
---
|
348
|
+
|
349
|
+
## Contributing
|
350
|
+
|
351
|
+
- Fork it ( https://github.com/0exp/redis_queued_locks )
|
352
|
+
- Create your feature branch (`git checkout -b feature/my-new-feature`)
|
353
|
+
- Commit your changes (`git commit -am '[feature_context] Add some feature'`)
|
354
|
+
- Push to the branch (`git push origin feature/my-new-feature`)
|
355
|
+
- Create new Pull Request
|
356
|
+
|
357
|
+
## License
|
358
|
+
|
359
|
+
Released under MIT License.
|
360
|
+
|
361
|
+
## Authors
|
362
|
+
|
363
|
+
[Rustam Ibragimov](https://github.com/0exp)
|
@@ -37,7 +37,6 @@ module RedisQueuedLocks::Acquier::Release
|
|
37
37
|
RedisQueuedLocks::Resource::LOCK_QUEUE_PATTERN,
|
38
38
|
count: batch_size
|
39
39
|
) do |lock_queue|
|
40
|
-
puts "RELEASE (lock_queue): #{lock_queue}"
|
41
40
|
pipeline.call('ZREMRANGEBYSCORE', lock_queue, '-inf', '+inf')
|
42
41
|
pipeline.call('EXPIRE', RedisQueuedLocks::Resource.lock_key_from_queue(lock_queue), "0")
|
43
42
|
end
|
@@ -48,7 +47,6 @@ module RedisQueuedLocks::Acquier::Release
|
|
48
47
|
RedisQueuedLocks::Resource::LOCK_PATTERN,
|
49
48
|
count: batch_size
|
50
49
|
) do |lock_key|
|
51
|
-
puts "RELEASE (lock_key): #{lock_key}"
|
52
50
|
pipeline.call('EXPIRE', lock_key, '0')
|
53
51
|
end
|
54
52
|
end
|
@@ -60,7 +60,7 @@ module RedisQueuedLocks::Acquier
|
|
60
60
|
#
|
61
61
|
# @api private
|
62
62
|
# @since 0.1.0
|
63
|
-
# rubocop:disable Metrics/MethodLength
|
63
|
+
# rubocop:disable Metrics/MethodLength, Metrics/BlockNesting
|
64
64
|
def acquire_lock!(
|
65
65
|
redis,
|
66
66
|
lock_name,
|
@@ -147,12 +147,19 @@ module RedisQueuedLocks::Acquier
|
|
147
147
|
if retry_count != nil && acq_process[:tries] >= retry_count
|
148
148
|
# NOTE: reached the retry limit => quit from the loop
|
149
149
|
acq_process[:should_try] = false
|
150
|
+
acq_process[:result] = :retry_limit_reached
|
150
151
|
# NOTE: reached the retry limit => dequeue from the lock queue
|
151
152
|
acq_dequeue.call
|
152
|
-
|
153
|
+
# NOTE: check and raise an error
|
154
|
+
raise(LockAcquiermentRetryLimitError, <<~ERROR_MESSAGE.strip) if raise_errors
|
155
|
+
Failed to acquire the lock "#{lock_key}"
|
156
|
+
for the given retry_count limit (#{retry_count} times).
|
157
|
+
ERROR_MESSAGE
|
158
|
+
else
|
153
159
|
# NOTE:
|
154
160
|
# delay the exceution in order to prevent chaotic attempts
|
155
161
|
# and to allow other processes and threads to obtain the lock too.
|
162
|
+
delay_execution(retry_delay, retry_jitter)
|
156
163
|
end
|
157
164
|
end
|
158
165
|
end
|
@@ -186,12 +193,18 @@ module RedisQueuedLocks::Acquier
|
|
186
193
|
{ ok: true, result: acq_process[:lock_info] }
|
187
194
|
end
|
188
195
|
else
|
189
|
-
|
190
|
-
|
196
|
+
unless acq_process[:result] == :retry_limit_reached
|
197
|
+
# NOTE: we have only two situations if lock is not acquired:
|
198
|
+
# - time limit is reached
|
199
|
+
# - retry count limit is reached
|
200
|
+
# In other cases the lock obtaining time and tries count are infinite.
|
201
|
+
acq_process[:result] = :timeout_reached
|
202
|
+
end
|
203
|
+
# Step 3.b: lock is not acquired (acquier is dequeued by timeout callback)
|
191
204
|
{ ok: false, result: acq_process[:result] }
|
192
205
|
end
|
193
206
|
end
|
194
|
-
# rubocop:enable Metrics/MethodLength
|
207
|
+
# rubocop:enable Metrics/MethodLength, Metrics/BlockNesting
|
195
208
|
|
196
209
|
# Release the concrete lock:
|
197
210
|
# - 1. clear lock queue: al; related processes released
|
@@ -203,7 +216,7 @@ module RedisQueuedLocks::Acquier
|
|
203
216
|
# @param redis [RedisClient] Redis connection client.
|
204
217
|
# @param lock_name [String] The lock name that should be released.
|
205
218
|
# @param isntrumenter [#notify] See RedisQueuedLocks::Instrument::ActiveSupport for example.
|
206
|
-
# @return [Hash<Symbol,Any>] Format: { ok: true/false, result:
|
219
|
+
# @return [Hash<Symbol,Any>] Format: { ok: true/false, result: Hash<Symbil,Numeric|String> }
|
207
220
|
#
|
208
221
|
# @api private
|
209
222
|
# @since 0.1.0
|
@@ -226,7 +239,7 @@ module RedisQueuedLocks::Acquier
|
|
226
239
|
})
|
227
240
|
end
|
228
241
|
|
229
|
-
{ ok: true, result:
|
242
|
+
{ ok: true, result: { rel_time: rel_time, rel_key: lock_key, rel_queue: lock_key_queue } }
|
230
243
|
end
|
231
244
|
|
232
245
|
# Release all locks:
|
@@ -236,7 +249,7 @@ module RedisQueuedLocks::Acquier
|
|
236
249
|
# @param redis [RedisClient] Redis connection client.
|
237
250
|
# @param batch_size [Integer] The number of lock keys that should be released in a time.
|
238
251
|
# @param isntrumenter [#notify] See RedisQueuedLocks::Instrument::ActiveSupport for example.
|
239
|
-
# @return [Hash<Symbol,Any>] Format: { ok: true/false, result:
|
252
|
+
# @return [Hash<Symbol,Any>] Format: { ok: true/false, result: Hash<Symbol,Numeric> }
|
240
253
|
#
|
241
254
|
# @api private
|
242
255
|
# @since 0.1.0
|
@@ -255,7 +268,7 @@ module RedisQueuedLocks::Acquier
|
|
255
268
|
})
|
256
269
|
end
|
257
270
|
|
258
|
-
{ ok: true, result: result }
|
271
|
+
{ ok: true, result: { rel_key_cnt: result[:rel_keys], rel_time: rel_time } }
|
259
272
|
end
|
260
273
|
|
261
274
|
private
|
@@ -52,7 +52,7 @@ class RedisQueuedLocks::Client
|
|
52
52
|
end
|
53
53
|
|
54
54
|
# @param lock_name [String]
|
55
|
-
# Lock name to be
|
55
|
+
# Lock name to be obtained.
|
56
56
|
# @option process_id [Integer,String]
|
57
57
|
# The process that want to acquire the lock.
|
58
58
|
# @option thread_id [Integer,String]
|
@@ -73,7 +73,7 @@ class RedisQueuedLocks::Client
|
|
73
73
|
# See RedisQueuedLocks::Instrument::ActiveSupport for example.
|
74
74
|
# @option raise_errors [Boolean]
|
75
75
|
# Raise errors on library-related limits such as timeout or failed lock obtain.
|
76
|
-
# @param [Block]
|
76
|
+
# @param block [Block]
|
77
77
|
# A block of code that should be executed after the successfully acquired lock.
|
78
78
|
# @return [Hash<Symbol,Any>]
|
79
79
|
# Format: { ok: true/false, result: Symbol/Hash }.
|