redis_queued_locks 0.0.6 → 0.0.8
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 +11 -0
- data/README.md +102 -11
- data/lib/redis_queued_locks/acquier/release.rb +1 -1
- data/lib/redis_queued_locks/acquier/try.rb +7 -1
- data/lib/redis_queued_locks/acquier.rb +115 -0
- data/lib/redis_queued_locks/client.rb +38 -0
- data/lib/redis_queued_locks/version.rb +2 -2
- data/redis_queued_locks.gemspec +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4846c7a27c9ce2108903a9b9b9250a38aa18ae34be6da85b4055bfe9c20371bb
|
4
|
+
data.tar.gz: 0346d8a0c4d43490ea0fcf79c7048e70c480285b18f26f84b06ad24fe42dc9fc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cbf5bd49d6c3b912ff8e3b8061cae4e2b1eaea00f913c8f217058168e7008f3c4daaeef0888ca22bce22d318742db1890c8524293e20734b4dea506fc3eeae10
|
7
|
+
data.tar.gz: 744519a43e3f1eb98527cbcb5495700b16a6ea94de95732baf8027debc14df7e5fa0394f804a9360383eb9168d3975794b46c4a0a6c45d1b769fd0c94e1700b5
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,16 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.0.8] - 2024-02-27
|
4
|
+
### Added
|
5
|
+
- `RedisQueuedLock::Client#locked?`
|
6
|
+
- `RedisQueuedLock::Client#queued?`
|
7
|
+
- `RedisQueuedLock::Client#lock_info`
|
8
|
+
- `RedisQueuedLock::Client#queue_info`
|
9
|
+
|
10
|
+
## [0.0.7] - 2024-02-27
|
11
|
+
### Changed
|
12
|
+
- Minor documentation updates;
|
13
|
+
|
3
14
|
## [0.0.6] - 2024-02-27
|
4
15
|
### Changed
|
5
16
|
- Major documentation updates;
|
data/README.md
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# RedisQueuedLocks
|
2
2
|
|
3
|
-
Distributed
|
3
|
+
Distributed locks with "lock acquisition queue" capabilities based on the Redis Database.
|
4
|
+
|
4
5
|
Each lock request is put into a request queue and processed in order of their priority (FIFO).
|
5
6
|
|
6
7
|
---
|
@@ -14,11 +15,15 @@ Each lock request is put into a request queue and processed in order of their pr
|
|
14
15
|
- [Usage](#usage)
|
15
16
|
- [lock](#lock---obtain-a-lock)
|
16
17
|
- [lock!](#lock---exeptional-lock-obtaining)
|
18
|
+
- [lock_info](#lock_info)
|
19
|
+
- [queue_info](#queue_info)
|
20
|
+
- [locked?](#locked)
|
21
|
+
- [queued?](#queued)
|
17
22
|
- [unlock](#unlock---release-a-lock)
|
18
23
|
- [clear_locks](#clear_locks---release-all-locks-and-lock-queues)
|
19
24
|
- [Instrumentation](#instrumentation)
|
20
25
|
- [Instrumentation Events](#instrumentation-events)
|
21
|
-
- [
|
26
|
+
- [Roadmap](#roadmap)
|
22
27
|
- [Contributing](#contributing)
|
23
28
|
- [License](#license)
|
24
29
|
- [Authors](#authors)
|
@@ -65,6 +70,7 @@ rq_lock_client = RedisQueuedLocks::Client.new(redis_client) do |config|
|
|
65
70
|
end
|
66
71
|
|
67
72
|
# Step 3: start to work with locks :)
|
73
|
+
rq_lock_client.lock("some-lock") { puts "Hello, lock!" }
|
68
74
|
```
|
69
75
|
|
70
76
|
---
|
@@ -122,6 +128,10 @@ end
|
|
122
128
|
|
123
129
|
- [lock](#lock---obtain-a-lock)
|
124
130
|
- [lock!](#lock---exeptional-lock-obtaining)
|
131
|
+
- [lock_info](#lock_info)
|
132
|
+
- [queue_info](#queue_info)
|
133
|
+
- [locked?](#locked)
|
134
|
+
- [queued?](#queued)
|
125
135
|
- [unlock](#unlock---release-a-lock)
|
126
136
|
- [clear_locks](#clear_locks---release-all-locks-and-lock-queues)
|
127
137
|
|
@@ -166,9 +176,11 @@ def lock(
|
|
166
176
|
- `instrumenter` - `[#notify]`
|
167
177
|
- See RedisQueuedLocks::Instrument::ActiveSupport for example.
|
168
178
|
- `raise_errors` - `[Boolean]`
|
169
|
-
- Raise errors on library-related limits such as timeout or
|
179
|
+
- Raise errors on library-related limits such as timeout or retry count limit.
|
170
180
|
- `block` - `[Block]`
|
171
181
|
- A block of code that should be executed after the successfully acquired lock.
|
182
|
+
- If block is **passed** the obtained lock will be released after the block execution or it's ttl (what will happen first);
|
183
|
+
- If block is **not passed** the obtained lock will be released after it's ttl;
|
172
184
|
|
173
185
|
Return value:
|
174
186
|
- `[Hash<Symbol,Any>]` Format: `{ ok: true/false, result: Symbol/Hash }`;
|
@@ -191,7 +203,6 @@ Return value:
|
|
191
203
|
{ ok: false, result: :unknown }
|
192
204
|
```
|
193
205
|
|
194
|
-
|
195
206
|
---
|
196
207
|
|
197
208
|
#### #lock! - exceptional lock obtaining
|
@@ -219,6 +230,83 @@ See `#lock` method [documentation](#lock---obtain-a-lock).
|
|
219
230
|
|
220
231
|
---
|
221
232
|
|
233
|
+
#### #lock_info
|
234
|
+
|
235
|
+
- get the lock information;
|
236
|
+
- returns `nil` if lock does not exist;
|
237
|
+
- lock data (`Hash<Symbol,String|Integer>`):
|
238
|
+
- `lock_key` - `string` - lock key in redis;
|
239
|
+
- `acq_id` - `string` - acquier identifier (process_id/thread_id by default);
|
240
|
+
- `ts` - `integer`/`epoch` - the time lock was obtained;
|
241
|
+
- `init_ttl` - `integer` - (milliseconds) initial lock key ttl;
|
242
|
+
- `rem_ttl` - `integer` - (milliseconds) remaining lock key ttl;
|
243
|
+
|
244
|
+
```ruby
|
245
|
+
rql.lock_info("your_lock_name")
|
246
|
+
|
247
|
+
# =>
|
248
|
+
{
|
249
|
+
lock_key: "rql:lock:your_lock_name",
|
250
|
+
acq_id: "rql:acq:123/456",
|
251
|
+
ts: 123456789,
|
252
|
+
ini_ttl: 123456789,
|
253
|
+
rem_ttl: 123456789
|
254
|
+
}
|
255
|
+
```
|
256
|
+
|
257
|
+
---
|
258
|
+
|
259
|
+
#### #queue_info
|
260
|
+
|
261
|
+
- get the lock queue information;
|
262
|
+
- queue represents the ordered set of lock key reqests:
|
263
|
+
- set is ordered by score in ASC manner (inside the Redis Set);
|
264
|
+
- score is represented as a timestamp when the lock request was made;
|
265
|
+
- represents the acquier identifier and their score as an array of hashes;
|
266
|
+
- returns `nil` if lock queue does not exist;
|
267
|
+
- lock queue data (`Hash<Symbol,String|Array<Hash<Symbol,String|Numeric>>`):
|
268
|
+
- `lock_queue` - `string` - lock queue key in redis;
|
269
|
+
- `queue` - `array` - an array of lock requests (array of hashes):
|
270
|
+
- `acq_id` - `string` - acquier identifier (process_id/thread_id by default);
|
271
|
+
- `score` - `float`/`epoch` - time when the lock request was made (epoch);
|
272
|
+
|
273
|
+
```ruby
|
274
|
+
rql.queue_info("your_lock_name")
|
275
|
+
|
276
|
+
# =>
|
277
|
+
{
|
278
|
+
lock_queue: "rql:lock_queue:your_lock_name",
|
279
|
+
queue: [
|
280
|
+
{ acq_id: "rql:acq:123/456", score: 1},
|
281
|
+
{ acq_id: "rql:acq:123/567", score: 2},
|
282
|
+
{ acq_id: "rql:acq:555/329", score: 3},
|
283
|
+
# ...etc
|
284
|
+
]
|
285
|
+
}
|
286
|
+
```
|
287
|
+
|
288
|
+
---
|
289
|
+
|
290
|
+
#### #locked?
|
291
|
+
|
292
|
+
- is the lock obtaied or not?
|
293
|
+
|
294
|
+
```ruby
|
295
|
+
rql.locked?("your_lock_name") # => true/false
|
296
|
+
```
|
297
|
+
|
298
|
+
---
|
299
|
+
|
300
|
+
#### #queued?
|
301
|
+
|
302
|
+
- is the lock queued for obtain / has requests for obtain?
|
303
|
+
|
304
|
+
```ruby
|
305
|
+
rql.queued?("your_lock_name") # => true/false
|
306
|
+
```
|
307
|
+
|
308
|
+
---
|
309
|
+
|
222
310
|
#### #unlock - release a lock
|
223
311
|
|
224
312
|
- release the concrete lock with lock request queue;
|
@@ -276,9 +364,9 @@ Return:
|
|
276
364
|
|
277
365
|
## Instrumentation
|
278
366
|
|
279
|
-
An instrumentation layer is incapsulated in `instrumenter` object stored in
|
367
|
+
An instrumentation layer is incapsulated in `instrumenter` object stored in [config](#configuration) (`RedisQueuedLocks::Client#config[:instrumenter]`).
|
280
368
|
|
281
|
-
Instrumenter object should provide `notify(event, payload)` method with following signarue:
|
369
|
+
Instrumenter object should provide `notify(event, payload)` method with the following signarue:
|
282
370
|
|
283
371
|
- `event` - `string`;
|
284
372
|
- `payload` - `hash<Symbol,Any>`;
|
@@ -286,7 +374,7 @@ Instrumenter object should provide `notify(event, payload)` method with followin
|
|
286
374
|
`redis_queued_locks` provides two instrumenters:
|
287
375
|
|
288
376
|
- `RedisQueuedLocks::Instrument::ActiveSupport` - `ActiveSupport::Notifications` instrumenter
|
289
|
-
that instrument events via `ActiveSupport::Notifications`
|
377
|
+
that instrument events via `ActiveSupport::Notifications` API;
|
290
378
|
- `RedisQueuedLocks::Instrument::VoidNotifier` - instrumenter that does nothing;
|
291
379
|
|
292
380
|
By default `RedisQueuedLocks::Client` is configured with the void notifier (which means "instrumentation is disabled").
|
@@ -338,11 +426,14 @@ Detalized event semantics and payload structure:
|
|
338
426
|
|
339
427
|
---
|
340
428
|
|
341
|
-
##
|
429
|
+
## Roadmap
|
342
430
|
|
343
|
-
-
|
344
|
-
-
|
345
|
-
-
|
431
|
+
- **Major**
|
432
|
+
- Semantic Error objects for unexpected Redis errors;
|
433
|
+
- `100%` test coverage;
|
434
|
+
- **Minor**
|
435
|
+
- `RedisQueuedLocks::Acquier::Try.try_to_lock` - detailed successful result analization;
|
436
|
+
- better code stylization and interesting refactorings;
|
346
437
|
|
347
438
|
---
|
348
439
|
|
@@ -38,7 +38,7 @@ module RedisQueuedLocks::Acquier::Release
|
|
38
38
|
count: batch_size
|
39
39
|
) do |lock_queue|
|
40
40
|
pipeline.call('ZREMRANGEBYSCORE', lock_queue, '-inf', '+inf')
|
41
|
-
pipeline.call('EXPIRE', RedisQueuedLocks::Resource.lock_key_from_queue(lock_queue),
|
41
|
+
pipeline.call('EXPIRE', RedisQueuedLocks::Resource.lock_key_from_queue(lock_queue), '0')
|
42
42
|
end
|
43
43
|
|
44
44
|
# Step B: release all locks
|
@@ -95,7 +95,13 @@ module RedisQueuedLocks::Acquier::Try
|
|
95
95
|
)
|
96
96
|
|
97
97
|
# Step 6.2: acquire a lock and store an info about the acquier
|
98
|
-
transact.call(
|
98
|
+
transact.call(
|
99
|
+
'HSET',
|
100
|
+
lock_key,
|
101
|
+
'acq_id', acquier_id,
|
102
|
+
'ts', (timestamp = Time.now.to_i),
|
103
|
+
'ini_ttl', ttl
|
104
|
+
)
|
99
105
|
|
100
106
|
# Step 6.3: set the lock expiration time in order to prevent "infinite locks"
|
101
107
|
transact.call('PEXPIRE', lock_key, ttl) # NOTE: in seconds
|
@@ -271,6 +271,121 @@ module RedisQueuedLocks::Acquier
|
|
271
271
|
{ ok: true, result: { rel_key_cnt: result[:rel_keys], rel_time: rel_time } }
|
272
272
|
end
|
273
273
|
|
274
|
+
# @param redis_client [RedisClient]
|
275
|
+
# @param lock_name [String]
|
276
|
+
# @return [Boolean]
|
277
|
+
#
|
278
|
+
# @api private
|
279
|
+
# @since 0.1.0
|
280
|
+
def locked?(redis_client, lock_name)
|
281
|
+
lock_key = RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
|
282
|
+
redis_client.call('EXISTS', lock_key) == 1
|
283
|
+
end
|
284
|
+
|
285
|
+
# @param redis_client [RedisClient]
|
286
|
+
# @param lock_name [String]
|
287
|
+
# @return [Boolean]
|
288
|
+
#
|
289
|
+
# @api private
|
290
|
+
# @since 0.1.0
|
291
|
+
def queued?(redis_client, lock_name)
|
292
|
+
lock_key_queue = RedisQueuedLocks::Resource.prepare_lock_queue(lock_name)
|
293
|
+
redis_client.call('EXISTS', lock_key_queue) == 1
|
294
|
+
end
|
295
|
+
|
296
|
+
# @param redis_client [RedisClient]
|
297
|
+
# @param lock_name [String]
|
298
|
+
# @return [Hash<Symbol,String|Numeric>,NilClass]
|
299
|
+
# - `nil` is returned when lock key does not exist or expired;
|
300
|
+
# - result format: {
|
301
|
+
# lock_key: "rql:lock:your_lockname", # acquired lock key
|
302
|
+
# acq_id: "rql:acq:process_id/thread_id", # lock acquier identifier
|
303
|
+
# ts: 123456789, # <locked at> time stamp (epoch)
|
304
|
+
# ini_ttl: 123456789, # initial lock key ttl (milliseconds),
|
305
|
+
# rem_ttl: 123456789, # remaining lock key ttl (milliseconds)
|
306
|
+
# }
|
307
|
+
#
|
308
|
+
# @api private
|
309
|
+
# @since 0.1.0
|
310
|
+
def lock_info(redis_client, lock_name)
|
311
|
+
lock_key = RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
|
312
|
+
|
313
|
+
result = redis_client.multi(watch: [lock_key]) do |transact|
|
314
|
+
transact.call('HGETALL', lock_key)
|
315
|
+
transact.call('PTTL', lock_key)
|
316
|
+
end
|
317
|
+
|
318
|
+
if result == nil
|
319
|
+
# NOTE:
|
320
|
+
# - nil result means that during transaction invocation the lock is changed (CAS):
|
321
|
+
# - lock is expired;
|
322
|
+
# - lock is released;
|
323
|
+
# - lock is expired + re-obtained;
|
324
|
+
nil
|
325
|
+
else
|
326
|
+
hget_cmd_res = result[0]
|
327
|
+
pttl_cmd_res = result[1]
|
328
|
+
|
329
|
+
if hget_cmd_res == {} || pttl_cmd_res == -2 # NOTE: key does not exist
|
330
|
+
nil
|
331
|
+
else
|
332
|
+
# NOTE: the result of MULTI-command is an array of results of each internal command
|
333
|
+
# - result[0] (HGETALL) (Hash<String,String>)
|
334
|
+
# - result[1] (PTTL) (Integer)
|
335
|
+
{
|
336
|
+
lock_key: lock_key,
|
337
|
+
acq_id: hget_cmd_res['acq_id'],
|
338
|
+
ts: Integer(hget_cmd_res['ts']),
|
339
|
+
ini_ttl: Integer(hget_cmd_res['ini_ttl']),
|
340
|
+
rem_ttl: ((pttl_cmd_res == -1) ? Infinity : pttl_cmd_res)
|
341
|
+
}
|
342
|
+
end
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
# Returns an information about the required lock queue by the lock name. The result
|
347
|
+
# represnts the ordered lock request queue that is ordered by score (Redis sets) and shows
|
348
|
+
# lock acquirers and their position in queue. Async nature with redis communcation can lead
|
349
|
+
# the sitation when the queue becomes empty during the queue data extraction. So sometimes
|
350
|
+
# you can receive the lock queue info with empty queue.
|
351
|
+
#
|
352
|
+
# @param redis_client [RedisClient]
|
353
|
+
# @param lock_name [String]
|
354
|
+
# @return [Hash<Symbol,String|Array<Hash<Symbol,String|Float>>,NilClass]
|
355
|
+
# - `nil` is returned when lock queue does not exist;
|
356
|
+
# - result format: {
|
357
|
+
# lock_queue: "rql:lock_queue:your_lock_name", # lock queue key in redis,
|
358
|
+
# queue: [
|
359
|
+
# { acq_id: "rql:acq:process_id/thread_id", score: 123 },
|
360
|
+
# { acq_id: "rql:acq:process_id/thread_id", score: 456 },
|
361
|
+
# ] # ordered set (by score) with information about an acquier and their position in queue
|
362
|
+
# }
|
363
|
+
#
|
364
|
+
# @api private
|
365
|
+
# @since 0.1.0
|
366
|
+
def queue_info(redis_client, lock_name)
|
367
|
+
lock_key_queue = RedisQueuedLocks::Resource.prepare_lock_queue(lock_name)
|
368
|
+
|
369
|
+
result = redis_client.pipelined do |pipeline|
|
370
|
+
pipeline.call('EXISTS', lock_key_queue)
|
371
|
+
pipeline.call('ZRANGE', lock_key_queue, '0', '-1', 'WITHSCORES')
|
372
|
+
end
|
373
|
+
|
374
|
+
exists_cmd_res = result[0]
|
375
|
+
zrange_cmd_res = result[1]
|
376
|
+
|
377
|
+
if exists_cmd_res == 1
|
378
|
+
# NOTE: queue existed during the piepline invocation
|
379
|
+
{
|
380
|
+
lock_queue: lock_key_queue,
|
381
|
+
queue: zrange_cmd_res.map { |val| { acq_id: val[0], score: val[1] } }
|
382
|
+
}
|
383
|
+
else
|
384
|
+
# NOTE: queue did not exist during the pipeline invocation
|
385
|
+
nil
|
386
|
+
end
|
387
|
+
end
|
388
|
+
|
274
389
|
private
|
275
390
|
|
276
391
|
# @param timeout [NilClass,Integer]
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
# @api public
|
4
4
|
# @since 0.1.0
|
5
|
+
# rubocop:disable Metrics/ClassLength
|
5
6
|
class RedisQueuedLocks::Client
|
6
7
|
# @since 0.1.0
|
7
8
|
include Qonfig::Configurable
|
@@ -154,6 +155,42 @@ class RedisQueuedLocks::Client
|
|
154
155
|
)
|
155
156
|
end
|
156
157
|
|
158
|
+
# @param lock_name [String]
|
159
|
+
# @return [Boolean]
|
160
|
+
#
|
161
|
+
# @api public
|
162
|
+
# @since 0.1.0
|
163
|
+
def locked?(lock_name)
|
164
|
+
RedisQueuedLocks::Acquier.locked?(redis_client, lock_name)
|
165
|
+
end
|
166
|
+
|
167
|
+
# @param lock_name [String]
|
168
|
+
# @return [Boolean]
|
169
|
+
#
|
170
|
+
# @api public
|
171
|
+
# @since 0.1.0
|
172
|
+
def queued?(lock_name)
|
173
|
+
RedisQueuedLocks::Acquier.queued?(redis_client, lock_name)
|
174
|
+
end
|
175
|
+
|
176
|
+
# @param lock_name [String]
|
177
|
+
# @return [Hash,NilClass]
|
178
|
+
#
|
179
|
+
# @api public
|
180
|
+
# @since 0.1.0
|
181
|
+
def lock_info(lock_name)
|
182
|
+
RedisQueuedLocks::Acquier.lock_info(redis_client, lock_name)
|
183
|
+
end
|
184
|
+
|
185
|
+
# @param lock_name [String]
|
186
|
+
# @return [Hash,NilClass]
|
187
|
+
#
|
188
|
+
# @api public
|
189
|
+
# @since 0.1.0
|
190
|
+
def queue_info(lock_name)
|
191
|
+
RedisQueuedLocks::Acquier.queue_info(redis_client, lock_name)
|
192
|
+
end
|
193
|
+
|
157
194
|
# @option batch_size [Integer]
|
158
195
|
# @return [Hash<Symbol,Any>] Format: { ok: true/false, result: Symbol/Hash }.
|
159
196
|
#
|
@@ -167,3 +204,4 @@ class RedisQueuedLocks::Client
|
|
167
204
|
)
|
168
205
|
end
|
169
206
|
end
|
207
|
+
# rubocop:enable Metrics/ClassLength
|
data/redis_queued_locks.gemspec
CHANGED
@@ -11,7 +11,7 @@ Gem::Specification.new do |spec|
|
|
11
11
|
spec.email = ['iamdaiver@gmail.com']
|
12
12
|
|
13
13
|
spec.summary = 'Queued distributed locks based on Redis.'
|
14
|
-
spec.description = 'Distributed
|
14
|
+
spec.description = 'Distributed locks with "lock acquisition queue" ' \
|
15
15
|
'capabilities based on the Redis Database.'
|
16
16
|
spec.homepage = 'https://github.com/0exp/redis_queued_locks'
|
17
17
|
spec.license = 'MIT'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: redis_queued_locks
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rustam Ibragimov
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-02-
|
11
|
+
date: 2024-02-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis-client
|
@@ -38,8 +38,8 @@ dependencies:
|
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0.28'
|
41
|
-
description: Distributed
|
42
|
-
|
41
|
+
description: Distributed locks with "lock acquisition queue" capabilities based on
|
42
|
+
the Redis Database.
|
43
43
|
email:
|
44
44
|
- iamdaiver@gmail.com
|
45
45
|
executables: []
|