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 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: []