redis_queued_locks 0.0.4 → 0.0.6
Sign up to get free protection for your applications and to get access to all the features.
- 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 }.
|