redis_queued_locks 0.0.38 → 0.0.40

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,16 +1,17 @@
1
- # RedisQueuedLocks
1
+ # RedisQueuedLocks · [![Gem Version](https://badge.fury.io/rb/redis_queued_locks.svg)](https://badge.fury.io/rb/redis_queued_locks)
2
2
 
3
- Distributed locks with "lock acquisition queue" capabilities based on the Redis Database.
3
+ <a href="https://redis.io/docs/manual/patterns/distributed-locks/">Distributed locks</a> with "lock acquisition queue" capabilities based on the Redis Database.
4
4
 
5
5
  Provides flexible invocation flow, parametrized limits (lock request ttl, lock ttls, queue ttls, fast failing, etc), logging and instrumentation.
6
6
 
7
- Each lock request is put into the request queue (each lock is hosted by it's own queue separately from other queues) and processed in order of their priority (FIFO). Each lock request lives some period of time (RTTL) which guarantees the request queue will never be stacked.
7
+ Each lock request is put into the request queue (each lock is hosted by it's own queue separately from other queues) and processed in order of their priority (FIFO). Each lock request lives some period of time (RTTL) (with requeue capabilities) which guarantees the request queue will never be stacked.
8
8
 
9
9
  ---
10
10
 
11
11
  ## Table of Contents
12
12
 
13
13
  - [Requirements](#requirements)
14
+ - [Experience](#experience)
14
15
  - [Algorithm](#algorithm)
15
16
  - [Installation](#installation)
16
17
  - [Setup](#setup)
@@ -30,6 +31,8 @@ Each lock request is put into the request queue (each lock is hosted by it's own
30
31
  - [keys](#keys---get-list-of-taken-locks-and-queues)
31
32
  - [locks_info](#locks_info---get-list-of-locks-with-their-info)
32
33
  - [queues_info](#queues_info---get-list-of-queues-with-their-info)
34
+ - [clear_dead_requests](#clear_dead_requests)
35
+ - [Logging](#logging)
33
36
  - [Instrumentation](#instrumentation)
34
37
  - [Instrumentation Events](#instrumentation-events)
35
38
  - [Roadmap](#roadmap)
@@ -41,13 +44,28 @@ Each lock request is put into the request queue (each lock is hosted by it's own
41
44
 
42
45
  ### Requirements
43
46
 
47
+ <sup>\[[back to top](#table-of-contents)\]</sup>
48
+
44
49
  - Redis Version: `~> 7.x`;
45
50
  - Redis Protocol: `RESP3`;
51
+ - gem `redis-client`: `~> 0.20`;
52
+ - Ruby: `>= 3.1`;
53
+
54
+ ---
55
+
56
+ ### Experience
57
+
58
+ <sup>\[[back to top](#table-of-contents)\]</sup>
59
+
60
+ - Battle-tested on huge ruby projects in production: `~1500` locks-per-second are obtained and released on an ongoing basis;
61
+ - Works well with `hiredis` driver enabled (it is enabled by default on our projects where `redis_queued_locks` are used);
46
62
 
47
63
  ---
48
64
 
49
65
  ### Algorithm
50
66
 
67
+ <sup>\[[back to top](#table-of-contents)\]</sup>
68
+
51
69
  > Each lock request is put into the request queue (each lock is hosted by it's own queue separately from other queues) and processed in order of their priority (FIFO). Each lock request lives some period of time (RTTL) which guarantees that the request queue will never be stacked.
52
70
 
53
71
  **Soon**: detailed explanation.
@@ -56,6 +74,8 @@ Each lock request is put into the request queue (each lock is hosted by it's own
56
74
 
57
75
  ### Installation
58
76
 
77
+ <sup>\[[back to top](#table-of-contents)\]</sup>
78
+
59
79
  ```ruby
60
80
  gem 'redis_queued_locks'
61
81
  ```
@@ -74,6 +94,8 @@ require 'redis_queued_locks'
74
94
 
75
95
  ### Setup
76
96
 
97
+ <sup>\[[back to top](#table-of-contents)\]</sup>
98
+
77
99
  ```ruby
78
100
  require 'redis_queued_locks'
79
101
 
@@ -95,6 +117,8 @@ rq_lock_client.lock("some-lock") { puts "Hello, lock!" }
95
117
 
96
118
  ### Configuration
97
119
 
120
+ <sup>\[[back to top](#table-of-contents)\]</sup>
121
+
98
122
  ```ruby
99
123
  redis_client = RedisClient.config.new_pool # NOTE: provide your own RedisClient instance
100
124
 
@@ -121,6 +145,10 @@ clinet = RedisQueuedLocks::Client.new(redis_client) do |config|
121
145
  # - lock request timeout. after this timeout your lock request in queue will be requeued with new position (at the end of the queue);
122
146
  config.default_queue_ttl = 15
123
147
 
148
+ # (boolean) (default: false)
149
+ # - should be all blocks of code are timed by default;
150
+ config.is_timed_by_default = false
151
+
124
152
  # (default: 100)
125
153
  # - how many items will be released at a time in RedisQueuedLocks::Client#clear_locks logic (uses SCAN);
126
154
  # - affects the performancs of your Redis and Ruby Application (configure thoughtfully);
@@ -156,6 +184,8 @@ clinet = RedisQueuedLocks::Client.new(redis_client) do |config|
156
184
  # - "[redis_queued_locks.start_try_to_lock_cycle]" (logs "lock_key", "queue_ttl", "acq_id");
157
185
  # - "[redis_queued_locks.dead_score_reached__reset_acquier_position]" (logs "lock_key", "queue_ttl", "acq_id");
158
186
  # - "[redis_queued_locks.lock_obtained]" (logs "lockkey", "queue_ttl", "acq_id", "acq_time");
187
+ # - "[redis_queued_locks.fail_fast_or_limits_reached__dequeue] (logs "lock_key", "queue_ttl", "acq_id");
188
+ # - "[redis_queued_locks.expire_lock]" # (logs "lock_key", "queue_ttl", "acq_id");
159
189
  # - by default uses VoidLogger that does nothing;
160
190
  config.logger = RedisQueuedLocks::Logging::VoidLogger
161
191
 
@@ -170,8 +200,8 @@ clinet = RedisQueuedLocks::Client.new(redis_client) do |config|
170
200
  # - "[redis_queued_locks.try_lock.get_first_from_queue]" (logs "lock_key", "queue_ttl", "acq_id", "first_acq_id_in_queue");
171
201
  # - "[redis_queued_locks.try_lock.exit__queue_ttl_reached]" (logs "lock_key", "queue_ttl", "acq_id");
172
202
  # - "[redis_queued_locks.try_lock.exit__no_first]" (logs "lock_key", "queue_ttl", "acq_id", "first_acq_id_in_queue", "<current_lock_data>");
173
- # - "[redis_queued_locks.try_lock.exit__still_obtained]" (logs "lock_key", "queue_ttl", "acq_id", "first_acq_id_in_queue", "locked_by_acq_id", "<current_lock_data>");
174
- # - "[redis_queued_locks.try_lock.run__free_to_acquire]" (logs "lock_key", "queue_ttl", "acq_id");
203
+ # - "[redis_queued_locks.try_lock.exit__lock_still_obtained]" (logs "lock_key", "queue_ttl", "acq_id", "first_acq_id_in_queue", "locked_by_acq_id", "<current_lock_data>");
204
+ # - "[redis_queued_locks.try_lock.obtain__free_to_acquire]" (logs "lock_key", "queue_ttl", "acq_id");
175
205
  config.log_lock_try = false
176
206
  end
177
207
  ```
@@ -180,6 +210,8 @@ end
180
210
 
181
211
  ### Usage
182
212
 
213
+ <sup>\[[back to top](#table-of-contents)\]</sup>
214
+
183
215
  - [lock](#lock---obtain-a-lock)
184
216
  - [lock!](#lock---exeptional-lock-obtaining)
185
217
  - [lock_info](#lock_info)
@@ -194,11 +226,14 @@ end
194
226
  - [keys](#keys---get-list-of-taken-locks-and-queues)
195
227
  - [locks_info](#locks_info---get-list-of-locks-with-their-info)
196
228
  - [queues_info](#queues_info---get-list-of-queues-with-their-info)
229
+ - [clear_dead_requests](#clear_dead_requests)
197
230
 
198
231
  ---
199
232
 
200
233
  #### #lock - obtain a lock
201
234
 
235
+ <sup>\[[back to top](#usage)\]</sup>
236
+
202
237
  - If block is passed the obtained lock will be released after the block execution or the lock's ttl (what will happen first);
203
238
  - If block is not passed the obtained lock will be released after lock's ttl;
204
239
  - If block is passed the block's yield result will be returned;
@@ -225,48 +260,64 @@ def lock(
225
260
  )
226
261
  ```
227
262
 
228
- - `lock_name` - `[String]`
263
+ - `lock_name` - (required) `[String]`
229
264
  - Lock name to be obtained.
230
- - `ttl` [Integer]
231
- - Lock's time to live (in milliseconds).
232
- - `queue_ttl` - `[Integer]`
265
+ - `ttl` - (optional) - [Integer]
266
+ - Lock's time to live (in milliseconds);
267
+ - pre-configured in `config[:default_lock_ttl]`;
268
+ - `queue_ttl` - (optional) `[Integer]`
233
269
  - Lifetime of the acuier's lock request. In seconds.
234
- - `timeout` - `[Integer,NilClass]`
235
- - Time period whe should try to acquire the lock (in seconds). Nil means "without timeout".
236
- - `timed` - `[Boolean]`
270
+ - pre-configured in `config[:default_queue_ttl]`;
271
+ - `timeout` - (optional) `[Integer,NilClass]`
272
+ - Time period a client should try to acquire the lock (in seconds). Nil means "without timeout".
273
+ - pre-configured in `config[:try_to_lock_timeout]`;
274
+ - `timed` - (optiona) `[Boolean]`
237
275
  - Limit the invocation time period of the passed block of code by the lock's TTL.
238
- - `retry_count` - `[Integer,NilClass]`
276
+ - pre-configured in `config[:is_timed_by_default]`;
277
+ - `false` by default;
278
+ - `retry_count` - (optional) `[Integer,NilClass]`
239
279
  - How many times we should try to acquire a lock. Nil means "infinite retries".
240
- - `retry_delay` - `[Integer]`
280
+ - pre-configured in `config[:retry_count]`;
281
+ - `retry_delay` - (optional) `[Integer]`
241
282
  - A time-interval between the each retry (in milliseconds).
242
- - `retry_jitter` - `[Integer]`
243
- - Time-shift range for retry-delay (in milliseconds).
244
- - `instrumenter` - `[#notify]`
245
- - See RedisQueuedLocks::Instrument::ActiveSupport for example.
246
- - `raise_errors` - `[Boolean]`
247
- - Raise errors on library-related limits such as timeout or retry count limit.
248
- - `fail_fast` - `[Boolean]`
283
+ - pre-configured in `config[:retry_delay]`;
284
+ - `retry_jitter` - (optional) `[Integer]`
285
+ - Time-shift range for retry-delay (in milliseconds);
286
+ - pre-configured in `config[:retry_jitter]`;
287
+ - `instrumenter` - (optional) `[#notify]`
288
+ - See RedisQueuedLocks::Instrument::ActiveSupport for example;
289
+ - See [Instrumentation](#instrumentation) section of docs;
290
+ - pre-configured in `config[:isntrumenter]` with void notifier (`RedisQueuedLocks::Instrumenter::VoidNotifier`);
291
+ - `raise_errors` - (optional) `[Boolean]`
292
+ - Raise errors on library-related limits such as timeout or retry count limit;
293
+ - `false` by default;
294
+ - `fail_fast` - (optional) `[Boolean]`
249
295
  - Should the required lock to be checked before the try and exit immidietly if lock is
250
296
  already obtained;
251
297
  - Should the logic exit immidietly after the first try if the lock was obtained
252
298
  by another process while the lock request queue was initially empty;
253
- - `identity` - `[String]`
299
+ - `false` by default;
300
+ - `identity` - (optional) `[String]`
254
301
  - An unique string that is unique per `RedisQueuedLock::Client` instance. Resolves the
255
302
  collisions between the same process_id/thread_id/fiber_id/ractor_id identifiers on different
256
303
  pods or/and nodes of your application;
257
304
  - It is calculated once during `RedisQueuedLock::Client` instantiation and stored in `@uniq_identity`
258
305
  ivar (accessed via `uniq_dentity` accessor method);
259
- - `meta` - `[NilClass,Hash<String|Symbol,Any>]`
306
+ - Identity calculator is pre-configured in `config[:uniq_identifier]`;
307
+ - `meta` - (optional) `[NilClass,Hash<String|Symbol,Any>]`
260
308
  - A custom metadata wich will be passed to the lock data in addition to the existing data;
261
309
  - Custom metadata can not contain reserved lock data keys (such as `lock_key`, `acq_id`, `ts`, `ini_ttl`, `rem_ttl`);
262
- - `instrument` - `[NilClass,Any]`
310
+ - `nil` by default (means "no metadata");
311
+ - `instrument` - (optional) `[NilClass,Any]`
263
312
  - Custom instrumentation data wich will be passed to the instrumenter's payload with :instrument key;
264
- - `logger` - `[::Logger,#debug]`
265
- - Logger object used from the configuration layer (see config[:logger]);
266
- - See `RedisQueuedLocks::Logging::VoidLogger` for example;
267
- - `log_lock_try` - `[Boolean]`
313
+ - `nil` by default (means "no custom instrumentation data");
314
+ - `logger` - (optional) `[::Logger,#debug]`
315
+ - Logger object used for loggin internal mutation oeprations and opertioan results / process progress;
316
+ - pre-configured in `config[:logger]` with void logger `RedisQueuedLocks::Logging::VoidLogger`;
317
+ - `log_lock_try` - (optional) `[Boolean]`
268
318
  - should be logged the each try of lock acquiring (a lot of logs can be generated depending on your retry configurations);
269
- - see `config[:log_lock_try]`;
319
+ - pre-configured in `config[:log_lock_try]`;
320
+ - `false` by default;
270
321
  - `block` - `[Block]`
271
322
  - A block of code that should be executed after the successfully acquired lock.
272
323
  - If block is **passed** the obtained lock will be released after the block execution or it's ttl (what will happen first);
@@ -274,8 +325,25 @@ def lock(
274
325
 
275
326
  Return value:
276
327
 
277
- - If block is passed the block's yield result will be returned;
278
- - If block is not passed the lock information will be returned;
328
+ - If block is passed the block's yield result will be returned:
329
+ ```ruby
330
+ result = rql.lock("my_lock") { 1 + 1 }
331
+ result # => 2
332
+ ```
333
+ - If block is not passed the lock information will be returned:
334
+ ```ruby
335
+ result = rql.lock("my_lock")
336
+ result # =>
337
+ {
338
+ ok: true,
339
+ result: {
340
+ lock_key: "rql:lock:my_lock",
341
+ acq_id: "rql:acq:26672/2280/2300/2320/70ea5dbf10ea1056",
342
+ ts: 1711909612.653696,
343
+ ttl: 10000
344
+ }
345
+ }
346
+ ```
279
347
  - Lock information result:
280
348
  - Signature: `[yield, Hash<Symbol,Boolean|Hash<Symbol,Numeric|String>>]`
281
349
  - Format: `{ ok: true/false, result: <Symbol|Hash<Symbol,Hash>> }`;
@@ -286,11 +354,23 @@ Return value:
286
354
  result: {
287
355
  lock_key: String, # acquierd lock key ("rql:lock:your_lock_name")
288
356
  acq_id: String, # acquier identifier ("process_id/thread_id/fiber_id/ractor_id/identity")
289
- ts: Integer, # time (epoch) when lock was obtained (integer)
357
+ ts: Float, # time (epoch) when lock was obtained (float, Time#to_f)
290
358
  ttl: Integer # lock's time to live in milliseconds (integer)
291
359
  }
292
360
  }
293
361
  ```
362
+ ```ruby
363
+ # example:
364
+ {
365
+ ok: true,
366
+ result: {
367
+ lock_key: "rql:lock:my_lock",
368
+ acq_id: "rql:acq:26672/2280/2300/2320/70ea5dbf10ea1056",
369
+ ts: 1711909612.653696,
370
+ ttl: 10000
371
+ }
372
+ }
373
+ ```
294
374
  - for failed lock obtaining:
295
375
  ```ruby
296
376
  { ok: false, result: :timeout_reached }
@@ -300,10 +380,61 @@ Return value:
300
380
  { ok: false, result: :unknown }
301
381
  ```
302
382
 
383
+ Examples:
384
+
385
+ - obtain a lock:
386
+
387
+ ```ruby
388
+ rql.lock("my_lock") { print "Hello!" }
389
+ ```
390
+
391
+ - obtain a lock with custom lock TTL:
392
+
393
+ ```ruby
394
+ rql.lock("my_lock", ttl: 5_000) { print "Hello!" } # for 5 seconds
395
+ ```
396
+
397
+ - obtain a lock and limit the passed block of code TTL with lock's TTL:
398
+
399
+ ```ruby
400
+ rql.lock("my_lock", ttl: 5_000, timed: true) { sleep(4) }
401
+ # => OK
402
+
403
+ rql.lock("my_lock", ttl: 5_000, timed: true) { sleep(6) }
404
+ # => fails with RedisQueuedLocks::TimedLockTimeoutError
405
+ ```
406
+
407
+ - infinite lock obtaining (no retry limit, no timeout limit):
408
+
409
+ ```ruby
410
+ rql.lock("my_lock", retry_count: nil, timeout: nil)
411
+ ```
412
+
413
+ - try to obtain with a custom waiting timeout:
414
+
415
+ ```ruby
416
+ # First Ruby Process:
417
+ rql.lock("my_lock", ttl: 5_000) { sleep(4) } # acquire a long living lock
418
+
419
+ # Another Ruby Process:
420
+ rql.lock("my_lock", timeout: 2) # try to acquire but wait for a 2 seconds maximum
421
+ # =>
422
+ { ok: false, result: :timeout_reached }
423
+ ```
424
+
425
+ - obtain a lock and immediatly continue working (the lock will live in the background in Redis with the passed ttl)
426
+
427
+ ```ruby
428
+ rql.lock("my_lock", ttl: 6_500) # blocks execution until the lock is obtained
429
+ puts "Let's go" # will be called immediately after the lock is obtained
430
+ ```
431
+
303
432
  ---
304
433
 
305
434
  #### #lock! - exceptional lock obtaining
306
435
 
436
+ <sup>\[[back to top](#usage)\]</sup>
437
+
307
438
  - fails when (and with):
308
439
  - (`RedisQueuedLocks::LockAlreadyObtainedError`) when `fail_fast` is `true` and lock is already obtained;
309
440
  - (`RedisQueuedLocks::LockAcquiermentTimeoutError`) `timeout` limit reached before lock is obtained;
@@ -334,9 +465,11 @@ See `#lock` method [documentation](#lock---obtain-a-lock).
334
465
 
335
466
  #### #lock_info
336
467
 
468
+ <sup>\[[back to top](#usage)\]</sup>
469
+
337
470
  - get the lock information;
338
471
  - returns `nil` if lock does not exist;
339
- - lock data (`Hash<Symbol,String|Integer>`):
472
+ - lock data (`Hash<String,String|Integer>`):
340
473
  - `"lock_key"` - `string` - lock key in redis;
341
474
  - `"acq_id"` - `string` - acquier identifier (process_id/thread_id/fiber_id/ractor_id/identity);
342
475
  - `"ts"` - `integer`/`epoch` - the time lock was obtained;
@@ -379,26 +512,26 @@ rql.lock_info("your_lock_name")
379
512
 
380
513
  #### #queue_info
381
514
 
515
+ <sup>\[[back to top](#usage)\]</sup>
516
+
517
+ Returns an information about the required lock queue by the lock name. The result
518
+ represnts the ordered lock request queue that is ordered by score (Redis Sets) and shows
519
+ lock acquirers and their position in queue. Async nature with redis communcation can lead
520
+ the situation when the queue becomes empty during the queue data extraction. So sometimes
521
+ you can receive the lock queue info with empty queue value (an empty array).
522
+
382
523
  - get the lock queue information;
383
524
  - queue represents the ordered set of lock key reqests:
384
525
  - set is ordered by score in ASC manner (inside the Redis Set);
385
526
  - score is represented as a timestamp when the lock request was made;
386
527
  - represents the acquier identifier and their score as an array of hashes;
387
528
  - returns `nil` if lock queue does not exist;
388
- - lock queue data (`Hash<Symbol,String|Array<Hash<Symbol,String|Numeric>>`):
529
+ - lock queue data (`Hash<String,String|Array<Hash<String|Numeric>>`):
389
530
  - `"lock_queue"` - `string` - lock queue key in redis;
390
531
  - `"queue"` - `array` - an array of lock requests (array of hashes):
391
532
  - `"acq_id"` - `string` - acquier identifier (process_id/thread_id/fiber_id/ractor_id/identity by default);
392
533
  - `"score"` - `float`/`epoch` - time when the lock request was made (epoch);
393
534
 
394
- ```
395
- | Returns an information about the required lock queue by the lock name. The result
396
- | represnts the ordered lock request queue that is ordered by score (Redis sets) and shows
397
- | lock acquirers and their position in queue. Async nature with redis communcation can lead
398
- | the situation when the queue becomes empty during the queue data extraction. So sometimes
399
- | you can receive the lock queue info with empty queue value (an empty array).
400
- ```
401
-
402
535
  ```ruby
403
536
  rql.queue_info("your_lock_name")
404
537
 
@@ -418,6 +551,8 @@ rql.queue_info("your_lock_name")
418
551
 
419
552
  #### #locked?
420
553
 
554
+ <sup>\[[back to top](#usage)\]</sup>
555
+
421
556
  - is the lock obtaied or not?
422
557
 
423
558
  ```ruby
@@ -438,26 +573,47 @@ rql.queued?("your_lock_name") # => true/false
438
573
 
439
574
  #### #unlock - release a lock
440
575
 
576
+ <sup>\[[back to top](#usage)\]</sup>
577
+
441
578
  - release the concrete lock with lock request queue;
442
579
  - queue will be relased first;
443
-
444
- ```ruby
445
- def unlock(lock_name)
446
- ```
447
-
448
- - `lock_name` - `[String]`
449
- - the lock name that should be released.
580
+ - accepts:
581
+ - `lock_name` - (required) `[String]` - the lock name that should be released.
582
+ - `:logger` - (optional) `[::Logger,#debug]`
583
+ - custom logger object;
584
+ - pre-configured in `config[:logger]`;
585
+ - `:instrumenter` - (optional) `[#notify]`
586
+ - custom instrumenter object;
587
+ - pre-configured in `config[:instrumetner]`;
588
+ - `:instrument` - (optional) `[NilClass,Any]`;
589
+ - custom instrumentation data wich will be passed to the instrumenter's payload with :instrument key;
590
+ - `nil` by default (no additional data);
591
+ - if you try to unlock non-existent lock you will receive `ok: true` result with operation timings
592
+ and `:nothing_to_release` result factor inside;
450
593
 
451
594
  Return:
452
- - `[Hash<Symbol,Numeric|String>]` - Format: `{ ok: true/false, result: Hash<Symbol,Numeric|String> }`;
595
+ - `[Hash<Symbol,Boolean|Hash<Symbol,Numeric|String|Symbol>>]` (`{ ok: true/false, result: Hasn }`);
596
+ - `:result` format;
597
+ - `:rel_time` - `Float` - time spent to process redis commands (in seconds);
598
+ - `:rel_key` - `String` - released lock key (RedisQueudLocks-internal lock key name from Redis);
599
+ - `:rel_queue` - `String` - released lock queue key (RedisQueuedLocks-internal queue key name from Redis);
600
+ - `:queue_res` - `Symbol` - `:released` (or `:nothing_to_release` if the required queue does not exist);
601
+ - `:lock_res` - `Symbol` - `:released` (or `:nothing_to_release` if the required lock does not exist);
602
+
603
+ Consider that `lock_res` and `queue_res` can have different value because of the async nature of invoked Redis'es commands.
453
604
 
454
605
  ```ruby
606
+ rql.unlock("your_lock_name")
607
+
608
+ # =>
455
609
  {
456
610
  ok: true,
457
611
  result: {
458
612
  rel_time: 0.02, # time spent to lock release (in seconds)
459
613
  rel_key: "rql:lock:your_lock_name", # released lock key
460
614
  rel_queue: "rql:lock_queue:your_lock_name" # released lock key queue
615
+ queue_res: :released, # or :nothing_to_release
616
+ lock_res: :released # or :nothing_to_release
461
617
  }
462
618
  }
463
619
  ```
@@ -466,25 +622,38 @@ Return:
466
622
 
467
623
  #### #clear_locks - release all locks and lock queues
468
624
 
625
+ <sup>\[[back to top](#usage)\]</sup>
626
+
469
627
  - release all obtained locks and related lock request queues;
470
628
  - queues will be released first;
629
+ - accepts:
630
+ - `:batch_size` - (optional) `[Integer]`
631
+ - the size of batch of locks and lock queus that should be cleared under the one pipelined redis command at once;
632
+ - pre-configured in `config[:lock_release_batch_size]`;
633
+ - `:logger` - (optional) `[::Logger,#debug]`
634
+ - custom logger object;
635
+ - has a preconfigured value in `config[:logger]`;
636
+ - `:instrumenter` - (optional) `[#notify]`
637
+ - custom instrumenter object;
638
+ - has a preconfigured value in `config[:isntrumenter]`;
639
+ - `:instrument` - (optional) `[NilClass,Any]`
640
+ - custom instrumentation data wich will be passed to the instrumenter's payload with :instrument key;
471
641
 
472
- ```ruby
473
- def clear_locks(batch_size: config[:lock_release_batch_size])
474
- ```
475
-
476
- - `batch_size` - `[Integer]`
477
- - the size of batch of locks and lock queus that should be cleared under the one pipelined redis command at once;
478
-
479
- Return:
480
- - `[Hash<Symbol,Numeric>]` - Format: `{ ok: true/false, result: Hash<Symbol,Numeric> }`;
642
+ - returns:
643
+ - `[Hash<Symbol,Numeric>]` - Format: `{ ok: true, result: Hash<Symbol,Numeric> }`;
644
+ - result data:
645
+ - `:rel_time` - `Numeric` - time spent to release all locks and related queus;
646
+ - `:rel_key_cnt` - `Integer` - the number of released Redis keys (queues+locks);
481
647
 
482
648
  ```ruby
649
+ rql.clear_locks
650
+
651
+ # =>
483
652
  {
484
653
  ok: true,
485
654
  result: {
486
- rel_time: 3.07, # time spent to release all locks and related lock queues
487
- rel_key_cnt: 100_500 # released redis keys (released locks + released lock queues)
655
+ rel_time: 3.07,
656
+ rel_key_cnt: 1234
488
657
  }
489
658
  }
490
659
  ```
@@ -493,19 +662,56 @@ Return:
493
662
 
494
663
  #### #extend_lock_ttl
495
664
 
496
- - soon
665
+ <sup>\[[back to top](#usage)\]</sup>
666
+
667
+ - extends lock ttl by the required number of milliseconds;
668
+ - expects the lock name and the number of milliseconds;
669
+ - accepts:
670
+ - `lock_name` - (required) `[String]`
671
+ - the lock name which ttl should be extended;
672
+ - `milliseconds` - (required) `[Integer]`
673
+ - how many milliseconds should be added to the lock's TTL;
674
+ - `:logger` - (optional) `[::Logger,#debug]`
675
+ - custom logger object;
676
+ - pre-configured in `config[:logger]`;
677
+ - returns `{ ok: true, result: :ttl_extended }` when ttl is extended;
678
+ - returns `{ ok: false, result: :async_expire_or_no_lock }` when lock not found or lock is expired during
679
+ some steps of invocation (see **Important** section below);
680
+ - **Important**:
681
+ - the method is non-atomic cuz redis does not provide an atomic function for TTL/PTTL extension;
682
+ - the method consists of two commands:
683
+ - (1) read current pttl;
684
+ - (2) set new ttl that is calculated as "current pttl + additional milliseconds";
685
+ - what can happen during these steps:
686
+ - lock is expired between commands or before the first command;
687
+ - lock is expired before the second command;
688
+ - lock is expired AND newly acquired by another process (so you will extend the
689
+ totally new lock with fresh PTTL);
690
+ - use it at your own risk and consider the async nature when calling this method;
691
+
692
+ ```ruby
693
+ rql.extend_lock_ttl("my_lock", 5_000) # NOTE: add 5_000 milliseconds
694
+
695
+ # => `ok` case
696
+ { ok: true, result: :ttl_extended }
697
+
698
+ # => `failed` case
699
+ { ok: false, result: :async_expire_or_no_lock }
700
+ ```
497
701
 
498
702
  ---
499
703
 
500
704
  #### #locks - get list of obtained locks
501
705
 
706
+ <sup>\[[back to top](#usage)\]</sup>
707
+
502
708
  - uses redis `SCAN` under the hood;
503
709
  - accepts:
504
710
  - `:scan_size` - `Integer` - (`config[:key_extraction_batch_size]` by default);
505
711
  - `:with_info` - `Boolean` - `false` by default (for details see [#locks_info](#locks_info---get-list-of-locks-with-their-info));
506
712
  - returns:
507
713
  - `Set<String>` (for `with_info: false`);
508
- - `Set<Hash<Symbol,Any>>` (for `with_info: true`). See `#locks_info` for details;
714
+ - `Set<Hash<Symbol,Any>>` (for `with_info: true`). See [#locks_info](#locks_info---get-list-of-locks-with-their-info) for details;
509
715
 
510
716
  ```ruby
511
717
  rql.locks # or rql.locks(scan_size: 123)
@@ -529,13 +735,15 @@ rql.locks # or rql.locks(scan_size: 123)
529
735
 
530
736
  #### #queues - get list of lock request queues
531
737
 
738
+ <sup>\[[back to top](#usage)\]</sup>
739
+
532
740
  - uses redis `SCAN` under the hood;
533
741
  - accepts
534
742
  - `:scan_size` - `Integer` - (`config[:key_extraction_batch_size]` by default);
535
- - `:with_info` - `Boolean` - `false` by default (for details see [queues_info](#queues_info---get-list-of-queues-with-their-info));
743
+ - `:with_info` - `Boolean` - `false` by default (for details see [#queues_info](#queues_info---get-list-of-queues-with-their-info));
536
744
  - returns:
537
745
  - `Set<String>` (for `with_info: false`);
538
- - `Set<Hash<Symbol,Any>>` (for `with_info: true`). See `#locks_info` for details;
746
+ - `Set<Hash<Symbol,Any>>` (for `with_info: true`). See [#locks_info](#locks_info---get-list-of-locks-with-their-info) for details;
539
747
 
540
748
  ```ruby
541
749
  rql.queues # or rql.queues(scan_size: 123)
@@ -559,6 +767,8 @@ rql.queues # or rql.queues(scan_size: 123)
559
767
 
560
768
  #### #keys - get list of taken locks and queues
561
769
 
770
+ <sup>\[[back to top](#usage)\]</sup>
771
+
562
772
  - uses redis `SCAN` under the hood;
563
773
  - accepts:
564
774
  `:scan_size` - `Integer` - (`config[:key_extraction_batch_size]` by default);
@@ -590,6 +800,8 @@ rql.keys # or rql.keys(scan_size: 123)
590
800
 
591
801
  #### #locks_info - get list of locks with their info
592
802
 
803
+ <sup>\[[back to top](#usage)\]</sup>
804
+
593
805
  - uses redis `SCAN` under the hod;
594
806
  - accepts `scan_size:`/`Integer` option (`config[:key_extraction_batch_size]` by default);
595
807
  - returns `Set<Hash<Symbol,Any>>` (see [#lock_info](#lock_info) and examples below for details).
@@ -622,6 +834,8 @@ rql.locks_info # or rql.locks_info(scan_size: 123)
622
834
 
623
835
  #### #queues_info - get list of queues with their info
624
836
 
837
+ <sup>\[[back to top](#usage)\]</sup>
838
+
625
839
  - uses redis `SCAN` under the hod;
626
840
  - accepts `scan_size:`/`Integer` option (`config[:key_extraction_batch_size]` by default);
627
841
  - returns `Set<Hash<Symbol,Any>>` (see [#queue_info](#queue_info) and examples below for details).
@@ -645,11 +859,97 @@ rql.queues_info # or rql.qeuues_info(scan_size: 123)
645
859
  {"acq_id"=>"rql:acq:38529/4460/4480/4360/66093702f24a3129", "score"=>1711606640.540808}]},
646
860
  ...}>
647
861
  ```
862
+ ---
863
+
864
+ #### #clear_dead_requests
865
+
866
+ <sup>\[[back to top](#usage)\]</sup>
867
+
868
+ In some cases your lock requests may become "dead". It can happen when your processs
869
+ that are enqueeud to the lock queue is failed unexpectedly (for some reason) before the lock acquire moment
870
+ and when no any other process does not need this lock anymore. For this case your lock will be cleared only when any process
871
+ will try to acquire this lock again (cuz lock acquirement triggers the removement of expired requests).
872
+
873
+ In order to help with these dead requests you may periodically call `#clear_dead_requests`
874
+ with corresponding `dead_ttl` option, that is pre-configured by default via `config[:dead_request_ttl]`.
875
+
876
+ An option is required because of it is no any **fast** way to understand which request
877
+ is dead now and is it really dead cuz each request queue can host their requests with
878
+ a custom queue ttl for each request differently.
879
+
880
+ Accepts:
881
+ - `:dead_ttl` - (optional) `[Integer]`
882
+ - lock request ttl after which a lock request is considered dead;
883
+ - has a preconfigured value in `config[:dead_request_ttl]` (1 day by default);
884
+ - `:sacn_size` - (optional) `[Integer]`
885
+ - the batch of scanned keys for Redis'es SCAN command;
886
+ - has a preconfigured valie in `config[:key_extraction_batch_size]`;
887
+ - `:logger` - (optional) `[::Logger,#debug]`
888
+ - custom logger object;
889
+ - pre-configured in `config[:logger]`;
890
+ - `:instrumenter` - (optional) `[#notify]`
891
+ - custom instrumenter object;
892
+ - pre-configured in `config[:isntrumenter]`;
893
+ - `:instrument` - (optional) `[NilClass,Any]`
894
+ - custom instrumentation data wich will be passed to the instrumenter's payload with :instrument key;
895
+ - `nil` by default (no additional data);
896
+
897
+ Returns: `{ ok: true, processed_queues: Set<String> }` returns the list of processed lock queues;
898
+
899
+ ```ruby
900
+ rql.clear_dead_requests(dead_ttl: 60 * 60 * 1000) # 1 hour in milliseconds
901
+
902
+ # =>
903
+ {
904
+ ok: true,
905
+ processed_queues: [
906
+ "rql:lock_queue:some-lock-123",
907
+ "rql:lock_queue:some-lock-456",
908
+ "rql:lock_queue:your-other-lock",
909
+ ...
910
+ ]
911
+ }
912
+ ```
913
+
914
+ ---
915
+
916
+ ## Logging
917
+
918
+ <sup>\[[back to top](#table-of-contents)\]</sup>
919
+
920
+ - default logs (raised from `#lock`/`#lock!`):
921
+
922
+ ```ruby
923
+ "[redis_queued_locks.start_lock_obtaining]" # (logs "lock_key", "queue_ttl", "acq_id");
924
+ "[redis_queued_locks.start_try_to_lock_cycle]" # (logs "lock_key", "queue_ttl", "acq_id");
925
+ "[redis_queued_locks.dead_score_reached__reset_acquier_position]" # (logs "lock_key", "queue_ttl", "acq_id");
926
+ "[redis_queued_locks.lock_obtained]" # (logs "lockkey", "queue_ttl", "acq_id", "acq_time");
927
+ "[redis_queued_locks.fail_fast_or_limits_reached__dequeue]" # (logs "lock_key", "queue_ttl", "acq_id");
928
+ "[redis_queued_locks.expire_lock]" # (logs "lock_key", "queue_ttl", "acq_id");
929
+ ```
930
+
931
+ - additional logs (raised from `#lock`/`#lock!` with `confg[:log_lock_try] == true`):
932
+
933
+ ```ruby
934
+ "[redis_queued_locks.try_lock.start]" # (logs "lock_key", "queue_ttl", "acq_id");
935
+ "[redis_queued_locks.try_lock.rconn_fetched]" # (logs "lock_key", "queue_ttl", "acq_id");
936
+ "[redis_queued_locks.try_lock.acq_added_to_queue]" # (logs "lock_key", "queue_ttl", "acq_id)";
937
+ "[redis_queued_locks.try_lock.remove_expired_acqs]" # (logs "lock_key", "queue_ttl", "acq_id");
938
+ "[redis_queued_locks.try_lock.get_first_from_queue]" # (logs "lock_key", "queue_ttl", "acq_id", "first_acq_id_in_queue");
939
+ "[redis_queued_locks.try_lock.exit__queue_ttl_reached]" # (logs "lock_key", "queue_ttl", "acq_id");
940
+ "[redis_queued_locks.try_lock.exit__no_first]" # (logs "lock_key", "queue_ttl", "acq_id", "first_acq_id_in_queue", "<current_lock_data>");
941
+ "[redis_queued_locks.try_lock.exit__lock_still_obtained]" # (logs "lock_key", "queue_ttl", "acq_id", "first_acq_id_in_queue", "locked_by_acq_id", "<current_lock_data>");
942
+ "[redis_queued_locks.try_lock.obtain__free_to_acquire]" # (logs "lock_key", "queue_ttl", "acq_id");
943
+ ```
648
944
 
649
945
  ---
650
946
 
651
947
  ## Instrumentation
652
948
 
949
+ <sup>\[[back to top](#table-of-contents)\]</sup>
950
+
951
+ - [Instrumentation Events](#instrumentation-events)
952
+
653
953
  An instrumentation layer is incapsulated in `instrumenter` object stored in [config](#configuration) (`RedisQueuedLocks::Client#config[:instrumenter]`).
654
954
 
655
955
  Instrumenter object should provide `notify(event, payload)` method with the following signarue:
@@ -659,8 +959,8 @@ Instrumenter object should provide `notify(event, payload)` method with the foll
659
959
 
660
960
  `redis_queued_locks` provides two instrumenters:
661
961
 
662
- - `RedisQueuedLocks::Instrument::ActiveSupport` - `ActiveSupport::Notifications` instrumenter
663
- that instrument events via `ActiveSupport::Notifications` API;
962
+ - `RedisQueuedLocks::Instrument::ActiveSupport` - **ActiveSupport::Notifications** instrumenter
963
+ that instrument events via **ActiveSupport::Notifications** API;
664
964
  - `RedisQueuedLocks::Instrument::VoidNotifier` - instrumenter that does nothing;
665
965
 
666
966
  By default `RedisQueuedLocks::Client` is configured with the void notifier (which means "instrumentation is disabled").
@@ -669,17 +969,20 @@ By default `RedisQueuedLocks::Client` is configured with the void notifier (whic
669
969
 
670
970
  ### Instrumentation Events
671
971
 
972
+ <sup>\[[back to top](#instrumentation-events)\]</sup>
973
+
672
974
  List of instrumentation events
673
975
 
674
- - `redis_queued_locks.lock_obtained`
675
- - `redis_queued_locks.lock_hold_and_release`
676
- - `redis_queued_locks.explicit_lock_release`
677
- - `redis_queued_locks.explicit_all_locks_release`
976
+ - `redis_queued_locks.lock_obtained`;
977
+ - `redis_queued_locks.lock_hold_and_release`;
978
+ - `redis_queued_locks.explicit_lock_release`;
979
+ - `redis_queued_locks.explicit_all_locks_release`;
678
980
 
679
981
  Detalized event semantics and payload structure:
680
982
 
681
983
  - `"redis_queued_locks.lock_obtained"`
682
984
  - a moment when the lock was obtained;
985
+ - raised from `#lock`/`#lock!`;
683
986
  - payload:
684
987
  - `:ttl` - `integer`/`milliseconds` - lock ttl;
685
988
  - `:acq_id` - `string` - lock acquier identifier;
@@ -687,9 +990,10 @@ Detalized event semantics and payload structure:
687
990
  - `:ts` - `integer`/`epoch` - the time when the lock was obtaiend;
688
991
  - `:acq_time` - `float`/`milliseconds` - time spent on lock acquiring;
689
992
  - `:instrument` - `nil`/`Any` - custom data passed to the `lock`/`lock!` method as `:instrument` attribute;
993
+
690
994
  - `"redis_queued_locks.lock_hold_and_release"`
691
- - an event signalizes about the "hold+and+release" process
692
- when the lock obtained and hold by the block of logic;
995
+ - an event signalizes about the "hold+and+release" process is finished;
996
+ - raised from `#lock`/`#lock!` when invoked with a block of code;
693
997
  - payload:
694
998
  - `:hold_time` - `float`/`milliseconds` - lock hold time;
695
999
  - `:ttl` - `integer`/`milliseconds` - lock ttl;
@@ -698,26 +1002,32 @@ Detalized event semantics and payload structure:
698
1002
  - `:ts` - `integer`/`epoch` - the time when lock was obtained;
699
1003
  - `:acq_time` - `float`/`milliseconds` - time spent on lock acquiring;
700
1004
  - `:instrument` - `nil`/`Any` - custom data passed to the `lock`/`lock!` method as `:instrument` attribute;
1005
+
701
1006
  - `"redis_queued_locks.explicit_lock_release"`
702
1007
  - an event signalizes about the explicit lock release (invoked via `RedisQueuedLock#unlock`);
1008
+ - raised from `#unlock`;
703
1009
  - payload:
704
- - `:at` - `integer`/`epoch` - the time when the lock was released;
1010
+ - `:at` - `float`/`epoch` - the time when the lock was released;
705
1011
  - `:rel_time` - `float`/`milliseconds` - time spent on lock releasing;
706
1012
  - `:lock_key` - `string` - released lock (lock name);
707
1013
  - `:lock_key_queue` - `string` - released lock queue (lock queue name);
1014
+
708
1015
  - `"redis_queued_locks.explicit_all_locks_release"`
709
1016
  - an event signalizes about the explicit all locks release (invoked via `RedisQueuedLock#clear_locks`);
1017
+ - raised from `#clear_locks`;
710
1018
  - payload:
711
1019
  - `:rel_time` - `float`/`milliseconds` - time spent on "realese all locks" operation;
712
- - `:at` - `integer`/`epoch` - the time when the operation has ended;
1020
+ - `:at` - `float`/`epoch` - the time when the operation has ended;
713
1021
  - `:rel_keys` - `integer` - released redis keys count (`released queue keys` + `released lock keys`);
714
1022
 
715
1023
  ---
716
1024
 
717
1025
  ## Roadmap
718
1026
 
1027
+ <sup>\[[back to top](#table-of-contents)\]</sup>
1028
+
719
1029
  - Semantic Error objects for unexpected Redis errors;
720
- - `100%` test coverage;
1030
+ - better specs with 100% test coverage;
721
1031
  - per-block-holding-the-lock sidecar `Ractor` and `in progress queue` in RedisDB that will extend
722
1032
  the acquired lock for long-running blocks of code (that invoked "under" the lock
723
1033
  whose ttl may expire before the block execution completes). It only makes sense for non-`timed` locks;
@@ -726,14 +1036,15 @@ Detalized event semantics and payload structure:
726
1036
  - structured logging (separated docs);
727
1037
  - GitHub Actions CI;
728
1038
  - `RedisQueuedLocks::Acquier::Try.try_to_lock` - detailed successful result analization;
729
- - better code stylization and interesting refactorings;
730
- - dead queue keys cleanup (empty queues);
1039
+ - better code stylization (+ some refactorings);
731
1040
  - statistics with UI;
732
1041
 
733
1042
  ---
734
1043
 
735
1044
  ## Contributing
736
1045
 
1046
+ <sup>\[[back to top](#table-of-contents)\]</sup>
1047
+
737
1048
  - Fork it ( https://github.com/0exp/redis_queued_locks )
738
1049
  - Create your feature branch (`git checkout -b feature/my-new-feature`)
739
1050
  - Commit your changes (`git commit -am '[feature_context] Add some feature'`)
@@ -742,8 +1053,12 @@ Detalized event semantics and payload structure:
742
1053
 
743
1054
  ## License
744
1055
 
1056
+ <sup>\[[back to top](#table-of-contents)\]</sup>
1057
+
745
1058
  Released under MIT License.
746
1059
 
747
1060
  ## Authors
748
1061
 
1062
+ <sup>\[[back to top](#table-of-contents)\]</sup>
1063
+
749
1064
  [Rustam Ibragimov](https://github.com/0exp)