redis_queued_locks 0.0.6 → 0.0.8

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9e98a72e1519c54d21a6273176a7d8a3bd9fa3033b7f30ca73fdca5a4335341d
4
- data.tar.gz: b2cf62db6016bd456a379feb3703989cc4a481f2eed4103c5c27a60bec624d46
3
+ metadata.gz: 4846c7a27c9ce2108903a9b9b9250a38aa18ae34be6da85b4055bfe9c20371bb
4
+ data.tar.gz: 0346d8a0c4d43490ea0fcf79c7048e70c480285b18f26f84b06ad24fe42dc9fc
5
5
  SHA512:
6
- metadata.gz: d90c848b5fb00614c3f61719d470d7a2dae008d3b6349c395674fe7bf262feea09795dafc74756a16426f499652e9cb0dcb4eef8b0dc9e2c1cc8abff2a1345d1
7
- data.tar.gz: ccc6e73175a2c41988303e3ea73feeacf95833b8d318dc1318c91f800d656ea157aeca62d7eba491ca489253a01ba4489f6a4c254c32f54ef3a540c9312ca179
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 lock implementation with "lock acquisition queue" capabilities based on the Redis Database.
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
- - [TODO](#todo)
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 failed lock obtain.
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 configurations.
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` api;
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
- ## TODO
429
+ ## Roadmap
342
430
 
343
- - `RedisQueuedLocks::Acquier::Try.try_to_lock` - detailed successful result analization;
344
- - `100%` test coverage;
345
- - better code stylization and interesting refactorings :)
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), "0")
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('HSET', lock_key, 'acq_id', acquier_id, 'ts', (timestamp = Time.now.to_i))
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
@@ -5,6 +5,6 @@ module RedisQueuedLocks
5
5
  #
6
6
  # @api public
7
7
  # @since 0.0.1
8
- # @version 0.0.6
9
- VERSION = '0.0.6'
8
+ # @version 0.0.8
9
+ VERSION = '0.0.8'
10
10
  end
@@ -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 lock implementation with "lock acquisition queue" ' \
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.6
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-26 00:00:00.000000000 Z
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 lock implementation with "lock acquisition queue" capabilities
42
- based on the Redis Database.
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: []