redis_queued_locks 1.21.0 → 1.23.0

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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/project-overview.md +294 -51
  3. data/.claude/rules/acquirer.md +16 -11
  4. data/.claude/rules/arguments.md +2 -2
  5. data/.claude/rules/changelog.md +27 -0
  6. data/.claude/rules/git.md +1 -1
  7. data/.claude/rules/logic.md +14 -15
  8. data/.claude/rules/swarm.md +3 -4
  9. data/.claude/rules/tests.md +9 -9
  10. data/.claude/rules/type-checking.md +14 -16
  11. data/.claude/rules/visitors.md +11 -9
  12. data/.claude/skills/rql-code-review/SKILL.md +129 -47
  13. data/.ruby-version +1 -1
  14. data/CHANGELOG.md +122 -245
  15. data/CLAUDE.md +12 -10
  16. data/README.md +516 -175
  17. data/Rakefile +11 -0
  18. data/lib/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue.rb +4 -4
  19. data/lib/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rb +28 -12
  20. data/lib/redis_queued_locks/acquirer/acquire_lock/log_visitor.rb +38 -16
  21. data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock/log_visitor.rb +92 -15
  22. data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rb +945 -272
  23. data/lib/redis_queued_locks/acquirer/acquire_lock.rb +82 -32
  24. data/lib/redis_queued_locks/acquirer/clear_dead_requests.rb +240 -45
  25. data/lib/redis_queued_locks/acquirer/extend_lock_ttl.rb +15 -13
  26. data/lib/redis_queued_locks/acquirer/keys.rb +6 -2
  27. data/lib/redis_queued_locks/acquirer/lock_series/instr_visitor.rb +243 -0
  28. data/lib/redis_queued_locks/acquirer/lock_series/log_visitor.rb +472 -0
  29. data/lib/redis_queued_locks/acquirer/lock_series/release_lock_series.rb +181 -0
  30. data/lib/redis_queued_locks/acquirer/lock_series/try_to_lock_series.rb +1051 -0
  31. data/lib/redis_queued_locks/acquirer/lock_series.rb +948 -0
  32. data/lib/redis_queued_locks/acquirer/locks.rb +6 -2
  33. data/lib/redis_queued_locks/acquirer/queue_info.rb +30 -7
  34. data/lib/redis_queued_locks/acquirer/queues.rb +17 -6
  35. data/lib/redis_queued_locks/acquirer/release_all_locks.rb +6 -2
  36. data/lib/redis_queued_locks/acquirer/release_locks_of.rb +6 -2
  37. data/lib/redis_queued_locks/acquirer.rb +1 -1
  38. data/lib/redis_queued_locks/client.rb +116 -44
  39. data/lib/redis_queued_locks/config.rb +93 -1
  40. data/lib/redis_queued_locks/resource.rb +124 -9
  41. data/lib/redis_queued_locks/swarm/swarm_element/threaded.rb +6 -5
  42. data/lib/redis_queued_locks/swarm/zombie_info.rb +6 -2
  43. data/lib/redis_queued_locks/version.rb +2 -2
  44. data/rbs_collection.lock.yaml +4 -0
  45. data/rbs_collection.yaml +2 -1
  46. data/redis_queued_locks.gemspec +21 -14
  47. data/sig/manifest.yml +1 -0
  48. data/sig/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue/log_visitor.rbs +2 -2
  49. data/sig/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue.rbs +2 -2
  50. data/sig/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rbs +12 -6
  51. data/sig/redis_queued_locks/acquirer/acquire_lock/log_visitor.rbs +21 -14
  52. data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock/log_visitor.rbs +61 -40
  53. data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rbs +160 -11
  54. data/sig/redis_queued_locks/acquirer/acquire_lock/{yield_with_expire → yield_expire}/log_visitor.rbs +4 -4
  55. data/sig/redis_queued_locks/acquirer/acquire_lock/yield_expire.rbs +2 -2
  56. data/sig/redis_queued_locks/acquirer/acquire_lock.rbs +5 -3
  57. data/sig/redis_queued_locks/acquirer/clear_dead_requests.rbs +14 -1
  58. data/sig/redis_queued_locks/acquirer/extend_lock_ttl.rbs +2 -2
  59. data/sig/redis_queued_locks/acquirer/lock_series/instr_visitor.rbs +102 -0
  60. data/sig/redis_queued_locks/acquirer/lock_series/log_visitor.rbs +154 -0
  61. data/sig/redis_queued_locks/acquirer/lock_series/release_lock_series.rbs +37 -0
  62. data/sig/redis_queued_locks/acquirer/lock_series/try_to_lock_series.rbs +198 -0
  63. data/sig/redis_queued_locks/acquirer/lock_series.rbs +106 -0
  64. data/sig/redis_queued_locks/acquirer/queues.rbs +1 -1
  65. data/sig/redis_queued_locks/client.rbs +77 -7
  66. data/sig/redis_queued_locks/config.rbs +54 -1
  67. data/sig/redis_queued_locks/instrument/sampler.rbs +3 -1
  68. data/sig/redis_queued_locks/instrument/void_notifier.rbs +3 -1
  69. data/sig/redis_queued_locks/logging/sampler.rbs +3 -1
  70. data/sig/redis_queued_locks/resource.rbs +22 -12
  71. data/sig/redis_queued_locks/swarm/swarm_element/threaded.rbs +1 -1
  72. data/sig/redis_queued_locks/utilities.rbs +2 -1
  73. data/sig/redis_queued_locks.rbs +7 -0
  74. metadata +35 -13
  75. data/lib/redis_queued_locks/acquirer/lock_series_poc/instr_visitor.rb +0 -56
  76. data/lib/redis_queued_locks/acquirer/lock_series_poc/log_visitor.rb +0 -95
  77. data/lib/redis_queued_locks/acquirer/lock_series_poc.rb +0 -484
  78. /data/sig/redis_queued_locks/{acquier.rbs → acquirer.rbs} +0 -0
data/README.md CHANGED
@@ -4,11 +4,11 @@
4
4
 
5
5
  **RedisQueuedLocks** (aka **RQL**, aka **RQL-lock**) - <a href="https://redis.io/docs/latest/develop/clients/patterns/distributed-locks/">distributed locks</a> with "prioritized lock acquisition queue" capabilities based on the Redis Database.
6
6
 
7
- Each lock request is put into the request queue (each lock is hosted by its 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.
7
+ Each lock request is put into the request queue (each lock is hosted by its own queue separately from other queues) and processed in order of their priority: requests of a higher [priority](#lock-request-prioritization) go first, requests of the same priority are processed in FIFO order. 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
  In addition to the classic `queued` (FIFO) strategy RQL supports `random` (RANDOM) lock obtaining strategy when any acquirer from the lock queue can obtain the lock regardless the position in the queue.
10
10
 
11
- Supports `Read`/`Write` locks and `Reentrant` locks.
11
+ Supports [lock request prioritization](#lock-request-prioritization), `Read`/`Write` locks and `Reentrant` locks.
12
12
 
13
13
  Provides flexible invocation flow, parametrized limits (lock request ttl, lock ttl, queue ttl, lock attempts limit, fast failing, etc), **zombie locks elimination**, **[lock indexing](#lock-index)**, logging and instrumentation (and much more).
14
14
 
@@ -29,8 +29,8 @@ Provides flexible invocation flow, parametrized limits (lock request ttl, lock t
29
29
  - [Usage](#usage)
30
30
  - [lock](#lock---obtain-a-lock)
31
31
  - [lock!](#lock---exceptional-lock-obtaining)
32
- - [lock_series](#lock_series---poc-acquire-a-series-of-locks) (PoC)
33
- - [lock_series!](#lock_series---poc-exceptional-lock_series) (PoC)
32
+ - [lock_series](#lock_series---acquire-a-series-of-locks)
33
+ - [lock_series!](#lock_series---exceptional-lock_series)
34
34
  - [lock_info](#lock_info)
35
35
  - [queue_info](#queue_info)
36
36
  - [locked?](#locked)
@@ -71,6 +71,7 @@ Provides flexible invocation flow, parametrized limits (lock request ttl, lock t
71
71
  - [Lock Access Strategies](#lock-access-strategies)
72
72
  - [queued](#lock-access-strategies)
73
73
  - [random](#lock-access-strategies)
74
+ - [Lock Request Prioritization](#lock-request-prioritization)
74
75
  - [Read/Write Locks](#readwrite-locks)
75
76
  - [Deadlocks and Reentrant locks](#deadlocks-and-reentrant-locks)
76
77
  - [Logging](#logging)
@@ -84,8 +85,12 @@ Provides flexible invocation flow, parametrized limits (lock request ttl, lock t
84
85
  - ["redis_queued_locks.reentrant_lock_obtained"](#redis_queued_locksreentrant_lock_obtained)
85
86
  - ["redis_queued_locks.lock_hold_and_release"](#redis_queued_lockslock_hold_and_release)
86
87
  - ["redis_queued_locks.reentrant_lock_hold_completes"](#redis_queued_locksreentrant_lock_hold_completes)
88
+ - ["redis_queued_locks.lock_series_intermediate_lock_obtained"](#redis_queued_lockslock_series_intermediate_lock_obtained)
89
+ - ["redis_queued_locks.lock_series_lock_obtained"](#redis_queued_lockslock_series_lock_obtained)
90
+ - ["redis_queued_locks.lock_series_lock_obtaining_failed"](#redis_queued_lockslock_series_lock_obtaining_failed)
87
91
  - ["redis_queued_locks.lock_series_obtained"](#redis_queued_lockslock_series_obtained)
88
92
  - ["redis_queued_locks.lock_series_hold_and_release"](#redis_queued_lockslock_series_hold_and_release)
93
+ - ["redis_queued_locks.lock_series_obtaining_failed"](#redis_queued_lockslock_series_obtaining_failed)
89
94
  - ["redis_queued_locks.explicit_lock_release"](#redis_queued_locksexplicit_lock_release)
90
95
  - ["redis_queued_locks.explicit_read_lock_release"](#redis_queued_locksexplicit_read_lock_release)
91
96
  - ["redis_queued_locks.explicit_all_locks_release"](#redis_queued_locksexplicit_all_locks_release)
@@ -125,7 +130,7 @@ Provides flexible invocation flow, parametrized limits (lock request ttl, lock t
125
130
 
126
131
  <sup>\[[back to top](#table-of-contents)\]</sup>
127
132
 
128
- > Each lock request is put into the request queue (each lock is hosted by its 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.
133
+ > Each lock request is put into the request queue (each lock is hosted by its own queue separately from other queues) and processed in order of their priority: requests of a higher [priority](#lock-request-prioritization) go first, requests of the same priority are processed in FIFO order. Each lock request lives some period of time (RTTL) which guarantees that the request queue will never be stacked.
129
134
 
130
135
  > In addition to the classic "queued" (FIFO) strategy RQL supports "random" (RANDOM) lock obtaining strategy when any acquirer from the lock queue can obtain the lock regardless the position in the queue.
131
136
 
@@ -205,7 +210,8 @@ client = RedisQueuedLocks::Client.new(redis_client) do |config|
205
210
  config['default_lock_ttl'] = 5_000
206
211
 
207
212
  # (seconds) (default: 15)
208
- # - lock request timeout. after this timeout your lock request in queue will be requeued with new position (at the end of the queue);
213
+ # - lock request timeout. after this timeout your lock request in queue will be requeued with new position (at the end of the
214
+ # lock requests of its priority);
209
215
  # - (the enabled lock index) the lock request queue lives as long as its latest lock request (the queue TTL is refreshed with
210
216
  # the queue ttl on each lock request): queues of dead acquirers expire by themselves;
211
217
  # - should be a positive Integer
@@ -249,6 +255,30 @@ client = RedisQueuedLocks::Client.new(redis_client) do |config|
249
255
  # retries, timeouts and so on). if lock is free to obtain - it will be obtained;
250
256
  config['default_access_strategy'] = :queued
251
257
 
258
+ # (hash) (default: { lowest: -100, low: -50, normal: 0, high: 50, highest: 100 })
259
+ # - Lock request priority names: a priority name (Symbol) => its priority (Integer from `-100` to `100`);
260
+ # - The names can be used instead of the Integer priorities in the `:priority` attribute and in
261
+ # `config['default_lock_priority']`: `rql.lock('name', priority: :high) { ... }`;
262
+ # - Define the names of your application (keep the default names with `merge`):
263
+ # `config['lock_priorities'] = config['lock_priorities'].merge(payment: 90, email: -10)`;
264
+ # - The default value is frozen (it is shared by all clients): assign a new hash, don't change it in place;
265
+ # - See [Lock Request Prioritization](#lock-request-prioritization) documentation section for details;
266
+ config['lock_priorities'] = { lowest: -100, low: -50, normal: 0, high: 50, highest: 100 }
267
+
268
+ # (integer/symbol) (default: 0)
269
+ # - The default lock request priority: an Integer from `-100` (the lowest) to `100` (the highest)
270
+ # or a priority name of `config['lock_priorities']`;
271
+ # - The priority name should be a key of `config['lock_priorities']` (`RedisQueuedLocks::ConfigValidationError` otherwise):
272
+ # it is checked once after the configuration block (`Client.new { ... }`, `#configure { ... }`: the keys can be assigned
273
+ # in any order; an inconsistent pair is never applied: both keys keep their previous values, also when the block fails)
274
+ # and on a direct `config[...] =` assignment (before the value is applied: change the priority names and the default
275
+ # name together in a configuration block);
276
+ # - Can be customized in methods `#lock`, `#lock!`, `#lock_series` and `#lock_series!` via `:priority` attribute;
277
+ # - `:queued` access strategy: the lock request is placed in the lock request queue after all requests of the same
278
+ # or higher priority and before all requests of the lower priority (FIFO inside the same priority);
279
+ # - See [Lock Request Prioritization](#lock-request-prioritization) documentation section for details;
280
+ config['default_lock_priority'] = 0
281
+
252
282
  # (symbol) (default: :wait_for_lock)
253
283
  # - Global default conflict strategy mode;
254
284
  # - Can be customized in methods `#lock` and `#lock!` via `:conflict_strategy` attribute (see method signatures of #lock and #lock! methods);
@@ -339,19 +369,27 @@ client = RedisQueuedLocks::Client.new(redis_client) do |config|
339
369
  # - should implement `debug(progname = nil, &block)` (minimal requirement) or be an instance of Ruby's `::Logger` class/subclass;
340
370
  # - supports `SemanticLogger::Logger` (see "semantic_logger" gem)
341
371
  # - at this moment the only debug logs are realised in following cases:
342
- # - "[redis_queued_locks.start_lock_obtaining]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
343
- # - "[redis_queued_locks.start_try_to_lock_cycle]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
344
- # - "[redis_queued_locks.dead_score_reached__reset_acquirer_position]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
345
- # - "[redis_queued_locks.lock_obtained]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "acq_time");
346
- # - "[redis_queued_locks.lock_obtaining_failed]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "acq_time", "result");
347
- # - "[redis_queued_locks.extendable_reentrant_lock_obtained]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "acq_time");
348
- # - "[redis_queued_locks.reentrant_lock_obtained]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "acq_time");
372
+ # - "[redis_queued_locks.start_lock_obtaining]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority");
373
+ # - "[redis_queued_locks.start_try_to_lock_cycle]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority");
374
+ # - "[redis_queued_locks.dead_score_reached__reset_acquirer_position]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority");
375
+ # - "[redis_queued_locks.lock_obtained]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority", "acq_time");
376
+ # - "[redis_queued_locks.lock_obtaining_failed]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority", "acq_time", "result");
377
+ # - "[redis_queued_locks.extendable_reentrant_lock_obtained]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority", "acq_time");
378
+ # - "[redis_queued_locks.reentrant_lock_obtained]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority", "acq_time");
349
379
  # - "[redis_queued_locks.fail_fast_or_limits_reached_or_deadlock__dequeue]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
350
380
  # - "[redis_queued_locks.expire_lock]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
351
381
  # - "[redis_queued_locks.decrease_lock]" (logs "lock_key", "decreased_ttl", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
352
- # - "[redis_queued_locks.start_lock_series_obtaining]" (logs "lock_keys", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
353
- # - "[redis_queued_locks.lock_series_obtained]" (logs "lock_keys", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "acq_time");
354
- # - "[redis_queued_locks.expire_lock_series]" (logs "lock_keys", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
382
+ # - "[redis_queued_locks.start_lock_series_obtaining]" (logs "lock_keys", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority");
383
+ # - "[redis_queued_locks.start_try_to_lock_series_cycle]" (logs "lock_keys", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority");
384
+ # - "[redis_queued_locks.dead_score_reached__reset_series_position]" (logs "lock_keys", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority");
385
+ # - "[redis_queued_locks.lock_series_intermediate_lock_obtained]" (logs "lock_key", "lock_keys", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority", "acq_time");
386
+ # - "[redis_queued_locks.lock_series_lock_obtained]" (logs "lock_key", "lock_keys", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority", "acq_time");
387
+ # - "[redis_queued_locks.lock_series_lock_obtaining_failed]" (logs "lock_key", "lock_keys", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority", "acq_time", "result");
388
+ # - "[redis_queued_locks.lock_series_obtained]" (logs "lock_keys", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority", "acq_time", "reentrant_lock_keys");
389
+ # - "[redis_queued_locks.lock_series_obtaining_failed]" (logs "lock_keys", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority", "acq_time", "result", "failed_lock_key");
390
+ # - "[redis_queued_locks.dequeue_from_lock_series_queues]" (logs "lock_keys", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority");
391
+ # - "[redis_queued_locks.expire_lock_series]" (logs "lock_keys", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority");
392
+ # - "[redis_queued_locks.decrease_lock_series]" (logs "lock_keys", "decreased_ttl", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority");
355
393
  # - "[redis_queued_locks.explicit_lock_release]" (logs "lock_key", "lock_key_queue", "rel_time", "at");
356
394
  # - "[redis_queued_locks.explicit_read_lock_release]" (logs "lock_key", "acq_id", "lock_res", "rel_time", "at");
357
395
  # - "[redis_queued_locks.explicit_all_locks_release]" (logs "at", "rel_time", "rel_key_cnt");
@@ -376,15 +414,16 @@ client = RedisQueuedLocks::Client.new(redis_client) do |config|
376
414
  # - "[redis_queued_locks.try_lock.reentrant_lock__work_through]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "spc_status", "last_spc_ts");
377
415
  # - "[redis_queued_locks.try_lock.single_process_lock_conflict__dead_lock]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "spc_status", "last_spc_ts");
378
416
  # - "[redis_queued_locks.try_lock.single_process_lock_conflict__lock_upgrade]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "spc_status", "last_spc_ts");
379
- # - "[redis_queued_locks.try_lock.acq_added_to_queue]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
417
+ # - "[redis_queued_locks.try_lock.acq_added_to_queue]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority");
380
418
  # - "[redis_queued_locks.try_lock.remove_expired_acqs]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
381
- # - "[redis_queued_locks.try_lock.get_first_from_queue]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "first_acq_id_in_queue");
419
+ # - "[redis_queued_locks.try_lock.dead_request_removed]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "dead_acq_id", "dead_priority");
420
+ # - "[redis_queued_locks.try_lock.get_first_from_queue]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "first_acq_id_in_queue", "first_priority_in_queue");
382
421
  # - "[redis_queued_locks.try_lock.exit__queue_ttl_reached]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
383
- # - "[redis_queued_locks.try_lock.exit__no_first]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "first_acq_id_in_queue", "<current_lock_data>");
384
- # - "[redis_queued_locks.try_lock.exit__lock_still_obtained]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "first_acq_id_in_queue", "locked_by_acq_id", "<current_lock_data>");
385
- # - "[redis_queued_locks.try_lock.exit__write_request_ahead]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "ahead_acq_id");
386
- # - "[redis_queued_locks.try_lock.exit__read_request_ahead]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "ahead_acq_id");
387
- # - "[redis_queued_locks.try_lock.exit__read_lock_still_obtained]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "first_acq_id_in_queue");
422
+ # - "[redis_queued_locks.try_lock.exit__no_first]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "first_acq_id_in_queue", "first_priority_in_queue", "<current_lock_data>");
423
+ # - "[redis_queued_locks.try_lock.exit__lock_still_obtained]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "first_acq_id_in_queue", "first_priority_in_queue", "locked_by_acq_id", "<current_lock_data>");
424
+ # - "[redis_queued_locks.try_lock.exit__write_request_ahead]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "ahead_acq_id", "ahead_priority");
425
+ # - "[redis_queued_locks.try_lock.exit__read_request_ahead]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "ahead_acq_id", "ahead_priority");
426
+ # - "[redis_queued_locks.try_lock.exit__read_lock_still_obtained]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "first_acq_id_in_queue", "first_priority_in_queue");
388
427
  # - "[redis_queued_locks.try_lock.obtain__free_to_acquire]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
389
428
  # - "rw_mode" is the requested lock mode ('write'/'read');
390
429
  config['log_lock_try'] = false
@@ -441,8 +480,8 @@ end
441
480
 
442
481
  - [lock](#lock---obtain-a-lock)
443
482
  - [lock!](#lock---exceptional-lock-obtaining)
444
- - [lock_series](#lock_series---poc-acquire-a-series-of-locks) (PoC)
445
- - [lock_series!](#lock_series---poc-exceptional-lock_series) (PoC)
483
+ - [lock_series](#lock_series---acquire-a-series-of-locks)
484
+ - [lock_series!](#lock_series---exceptional-lock_series)
446
485
  - [lock_info](#lock_info)
447
486
  - [queue_info](#queue_info)
448
487
  - [locked?](#locked)
@@ -501,8 +540,9 @@ def lock(
501
540
  raise_errors: false,
502
541
  fail_fast: false,
503
542
  conflict_strategy: config['default_conflict_strategy'],
504
- read_write_mode: :write,
543
+ rw_mode: :write,
505
544
  access_strategy: config['default_access_strategy'],
545
+ priority: config['default_lock_priority'],
506
546
  identity: uniq_identity, # (attr_accessor) calculated during client instantiation via config['uniq_identifier'] proc;
507
547
  meta: nil,
508
548
  detailed_acq_timeout_error: config['detailed_acq_timeout_error'],
@@ -563,7 +603,7 @@ def lock(
563
603
  - `instrumenter` - (optional) `[#notify]`
564
604
  - See RedisQueuedLocks::Instrument::ActiveSupport for example;
565
605
  - See [Instrumentation](#instrumentation) section of docs;
566
- - pre-configured in `config['isntrumenter']` with void notifier (`RedisQueuedLocks::Instrumenter::VoidNotifier`);
606
+ - pre-configured in `config['instrumenter']` with void notifier (`RedisQueuedLocks::Instrument::VoidNotifier`);
567
607
  - `instrument` - (optional) `[NilClass,Any]`
568
608
  - Custom instrumentation data wich will be passed to the instrumenter's payload with :instrument key;
569
609
  - `nil` by default (means "no custom instrumentation data");
@@ -585,6 +625,19 @@ def lock(
585
625
  if lock is free to obtain - it will be obtained;
586
626
  - pre-configured in `config['default_access_strategy']`;
587
627
  - See [Lock Access Strategies](#lock-access-strategies) documentation section for details;
628
+ - `priority` - (optional) - `[Integer,Symbol]`
629
+ - The lock request priority: an Integer from `-100` (the lowest) to `100` (the highest), `0` by default,
630
+ or a priority name of `config['lock_priorities']` (`:lowest`, `:low`, `:normal`, `:high`, `:highest` by default,
631
+ the names of your application can be configured);
632
+ - `:queued` access strategy: the lock request is placed in the lock request queue after all requests of the same or
633
+ higher priority and before all requests of the lower priority (strict priority, FIFO inside the same priority;
634
+ read and write lock requests too);
635
+ - `:random` access strategy: the lock request does not wait for its turn (it ignores the order of the lock request queues
636
+ and the priorities of other requests), but it is queued at the position of its priority like a `:queued` one: `:queued`
637
+ lock requests wait for it;
638
+ - the obtained lock is never taken away: the priority orders the waiting lock requests only;
639
+ - pre-configured in `config['default_lock_priority']`;
640
+ - See [Lock Request Prioritization](#lock-request-prioritization) documentation section for details;
588
641
  - `conflict_strategy` - (optional) - `[Symbol]`
589
642
  - The conflict strategy mode for cases when the process that obtained the lock
590
643
  want to acquire this lock again;
@@ -596,7 +649,7 @@ def lock(
596
649
  - `:wait_for_lock` - (default) - work in classic way (with timeouts, retry delays, retry limits, etc - in classic way :));
597
650
  - `:dead_locking` - fail with deadlock exception;
598
651
  - See [Deadlocks and Reentrant locks](#deadlocks-and-reentrant-locks) documentation section for details;
599
- - `read_write_mode` - (optional) - `[Symbol]`
652
+ - `rw_mode` - (optional) - `[Symbol]`
600
653
  - The lock mode (read/write lock semantics);
601
654
  - `:write` by default;
602
655
  - Modes:
@@ -758,7 +811,7 @@ Return value:
758
811
  { ok: false, result: :timeout_reached } # see <timeout> option
759
812
  { ok: false, result: :retry_limit_reached } # see <retry_count> option
760
813
  { ok: false, result: :conflict_dead_lock } # see <conflict_strategy> option for details (:dead_locking strategy)
761
- { ok: false, result: :conflict_lock_upgrade } # read-to-write lock upgrade (see <read_write_mode> and <conflict_strategy> options)
814
+ { ok: false, result: :conflict_lock_upgrade } # read-to-write lock upgrade (see <rw_mode> and <conflict_strategy> options)
762
815
  { ok: false, result: :fail_fast_no_try } # see <fail_fast> option
763
816
  { ok: false, result: :fail_fast_after_try } # see <fail_fast> option
764
817
  ```
@@ -834,7 +887,7 @@ rql.lock_info("my_lock")
834
887
 
835
888
  - (`:queue_ttl`) setting a short limit of time to the lock request queue position (if a process fails to acquire
836
889
  the lock within this period of time (and before timeout/retry_count limits occurs of course) -
837
- its lock request will be moved to the end of queue):
890
+ its lock request will be moved to the end of queue: the end of the lock requests of its priority):
838
891
 
839
892
  ```ruby
840
893
  rql.lock("my_lock", queue_ttl: 5, timeout: 10_000, retry_count: nil)
@@ -946,8 +999,9 @@ def lock!(
946
999
  instrument: nil,
947
1000
  instrumenter: config['instrumenter'],
948
1001
  access_strategy: config['default_access_strategy'],
1002
+ priority: config['default_lock_priority'],
949
1003
  conflict_strategy: config['default_conflict_strategy'],
950
- read_write_mode: :write,
1004
+ rw_mode: :write,
951
1005
  log_sampling_enabled: config['log_sampling_enabled'],
952
1006
  log_sampling_percent: config['log_sampling_percent'],
953
1007
  log_sampler: config['log_sampler'],
@@ -964,38 +1018,56 @@ See `#lock` method [documentation](#lock---obtain-a-lock).
964
1018
 
965
1019
  ---
966
1020
 
967
- #### #lock_series - (PoC) acquire a series of locks
1021
+ #### #lock_series - acquire a series of locks
968
1022
 
969
1023
  <sup>\[[back to top](#usage)\]</sup>
970
1024
 
971
- > (IMPORTANT!) "Proof of Concept" realization. Will be reworked in the future.
972
-
973
- `lock_series` - acquire a series of locks simultaniously and hold them all while your block of code is executing
974
- or (if block is not passed) while all locks will not expire.
975
-
976
- If it is not possible to obtain all locks at the same time (taking into queue ttl, retry attempts count for each lock, etc) no any lock will be acquired.
977
-
978
- Method options is the same as for `lock` method: `retry_count`, `retry_jitter`, `ttl`, `queue_ttl`, etc. Each option will be
979
- applied with the same value to the each lock acquirement process (for each lock in a series separately).
980
-
981
- **IMPORTANT DETAILS AND LIMITATIONS**:
982
- - (this behavior will be reworked with another algorithm in future);
983
- - each lock will be released with the next formula:
984
- - `lock position with reverse numbering` * `ttl`
985
- - (positions exmaple: ["a", "b", "c"] => "a" position is 3, "b" position is 2, "c" position is 1);
986
- - this means that in case of 3 locks (`lock_series("x", "y", "z", ttl: 5_000`) with `5 seconds TTL` your locks will be with the next TTL:
987
- - `"x"` - 15 seconds
988
- - `"y"` - 10 seconds
989
- - `"z"` - 5 seconds
990
- - when your block of code has completed their execution - all locks will be released immediatly;
991
- - when the series fails (any lock can not be obtained, with or without an exception) - the locks that are already
992
- obtained by the series are released immediatly (`acquired_locks` of the failure result lists them);
1025
+ `lock_series` - obtain a group of locks atomically: all locks of the series are obtained at the same moment with the same TTL
1026
+ and are held together while your block of code is executing (or, if a block is not passed, while the locks live).
1027
+
1028
+ **Guarantees**:
1029
+ - **all or nothing**: one transaction obtains all locks of the series; an attempt that can not obtain some lock obtains nothing,
1030
+ so a failed series leaves no obtained locks (and no lock requests in the lock queues);
1031
+ - **all locks at the same time**: all locks are obtained with the same TTL (`ttl:`) at the same moment, so the block of code
1032
+ never works with a partially held series and `timed: true` limits it by the real lifetime of the locks;
1033
+ - **no hold-and-wait**: nothing is held while the series waits for its locks: the free locks of the series are not locked
1034
+ and their TTL is not spent on the waiting;
1035
+ - **no deadlocks between lock series**: the order of the lock names does not matter (`lock_series("a", "b")` and
1036
+ `lock_series("b", "a")` work in parallel threads): the lock request of the series has one position in all its lock queues,
1037
+ so the lock requests wait only for earlier lock requests. Your own nested logic is out of it: code that holds a lock and
1038
+ then waits for a series still can wait for a lock that is needed by another acquirer (the waiting is limited by the limits
1039
+ of the series);
1040
+
1041
+ **How the options work**:
1042
+ - method options are the same as for the `#lock` method: `ttl`, `queue_ttl`, `retry_count`, `retry_delay`, `retry_jitter`, etc;
1043
+ - limits are the limits of **the whole series**, not of each lock: `timeout:` is one time period for all locks, `retry_count:` is
1044
+ the number of attempts to obtain all locks, `queue_ttl:` is the lifetime of the lock request of the series;
1045
+ - `ttl:` is the TTL of each lock of the series (the same value for all locks);
1046
+ - `rw_mode:` is the same for all locks of the series (see [Read/Write Locks](#readwrite-locks));
1047
+ - `priority:` is the same for all locks of the series (see [Lock Request Prioritization](#lock-request-prioritization)):
1048
+ the lock request of the series has this priority in all its lock queues;
1049
+ - `conflict_strategy:` works per lock: reentrant locks (already obtained by your outer logic, `:work_through`/`:extendable_work_through`)
1050
+ are not obtained and not released by the series: the extended ones (`:extendable_work_through`) get their TTL back after the block,
1051
+ like in the `#lock` method; a lock with a failed conflict (`:dead_locking`, read-to-write lock upgrade) fails the whole series;
1052
+ - `fail_fast: true` fails the series on the first lock that is already obtained;
1053
+ - a duplicated lock name is the same lock (`lock_series("a", "b", "a")` works with two locks); at least one lock name is required
1054
+ (`RedisQueuedLocks::ArgumentError`);
1055
+
1056
+ **Details and limitations**:
1057
+ - when your block of code has completed its execution (with or without an exception) all locks obtained by the series are released;
1058
+ - an interrupted series (`Timeout.timeout` of your logic, `Thread#kill`, a Redis error) releases its locks and drops its lock requests,
1059
+ when it is possible (a dead process or an unavailable Redis: the locks live till their TTL, the lock requests till their `queue_ttl`);
993
1060
  - the series releases only locks obtained by itself and only if they are still owned by the current acquirer:
994
- - reentrant locks (already obtained by your outer logic, `conflict_strategy: :work_through`/`:extendable_work_through`) are kept;
995
- - a lock that is expired during your block and obtained by another acquirer is kept;
996
- - `read_write_mode:` is the same for all locks of the series (see [Read/Write Locks](#readwrite-locks));
997
- - if you did not pass a block - all locks will be released with their forumla-based TTL described above;
998
- - **HOW WILL THIS CASE WORK IN FUTURE RELESAE**: all locks will have the same TTL (`5 seconds` for our example, not `15/10/5 seconds`);
1061
+ a lock that is expired during your block and obtained by another acquirer is kept;
1062
+ - if you did not pass a block - all locks live till their TTL;
1063
+ - locks of the series are regular locks with regular keys: `#lock_info`, `#unlock`, `#extend_lock_ttl`, `#clear_locks` and the swarm
1064
+ work with each of them separately;
1065
+ - `:queued` access strategy: a series that is ahead of other lock requests makes them wait even for its free locks (the waiting
1066
+ series is the next obtainer of these locks): one long-waiting series delays the lock requests of all its locks for its waiting time
1067
+ (no longer than its `queue_ttl`, `timeout` and `retry_count` limits allow);
1068
+ - `:random` access strategy: the series is obtained only when all its locks are free at the same moment, so a big series can
1069
+ wait for a long time under load (lock requests of the `:random` strategy have no reserved turn);
1070
+ - all processes that work with the same locks should use the same RQL version: older versions obtain the series lock by lock;
999
1071
 
1000
1072
  ##### How to use:
1001
1073
 
@@ -1006,12 +1078,27 @@ applied with the same value to the each lock acquirement process (for each lock
1006
1078
  client.lock_series("a", "b", "c") { ...some_code... }
1007
1079
 
1008
1080
  # result-oriented way:
1009
- # - (block is passed)
1081
+ # - (block is not passed)
1082
+ client.lock_series('x', 'y', 'z', ttl: 5_000)
1083
+ # =>
1084
+ {
1085
+ ok: true,
1086
+ result: {
1087
+ lock_series: ["x", "y", "z"], # your locks
1088
+ rql_lock_series: ["rql:lock:x", "rql:lock:y", "rql:lock:z"], # internal RQL lock-keys of your locks
1089
+ acq_id: "rql:acq:4486/1696/1704/1712/e6fd0da7991e4303", # the acquirer of the locks
1090
+ hst_id: "rql:hst:4486/1696/1712/e6fd0da7991e4303", # the host of the locks
1091
+ ts: 1769506100.676884, # the time when the locks were obtained, epoch
1092
+ ttl: 5000 # the ttl of each lock, milliseconds
1093
+ }
1094
+ }
1095
+
1096
+ # - (detailed result, block is passed)
1010
1097
  detailed_result = client.lock_series('x', 'y', 'z', detailed_result: true) { 1 + 2 }
1011
1098
  detailed_result # =>
1012
1099
  {
1013
1100
  yield_result: 3, # result of your block of code
1014
- lock_release_strategy: :immediate_release_after_yield, # (block is passed)
1101
+ locks_release_strategy: :immediate_release_after_yield, # (block is passed)
1015
1102
  locks_released_at: 1769506100.676884, # (instrumentation) release time, epoch
1016
1103
  locks_acq_time: 7.0, # (instrumentation) time spent to acquire all locks, milliseconds
1017
1104
  locks_hold_time: 0.65, # (instrumentation) lock series hold period (hold preiod of all required locks), milliseconds
@@ -1019,12 +1106,12 @@ detailed_result # =>
1019
1106
  rql_lock_series: ["rql:lock:x", "rql:lock:y", "rql:lock:z"] # internal RQL lock-keys of your locks
1020
1107
  }
1021
1108
 
1022
- # - (block is not passed)
1109
+ # - (detailed result, block is not passed)
1023
1110
  detailed_result = client.lock_series('x', 'y', 'z', detailed_result: true)
1024
1111
  detailed_result # =>
1025
1112
  {
1026
1113
  yield_result: nil, # block is not given - nothing to return
1027
- lock_release_strategy: :redis_key_ttl, # block is not given - locks are living in redis with their TTL
1114
+ locks_release_strategy: :redis_key_ttl, # block is not given - locks are living in redis with their TTL
1028
1115
  locks_released_at: nil, # (instrumentation) # block is not given - locks are living in redis with their TTL
1029
1116
  locks_acq_time: 7.0, # (instrumentation) time spent to acquire all locks, milliseconds
1030
1117
  locks_hold_time: nil, # (instrumentation) # block is not given - locks are living in redis with their TTL
@@ -1041,53 +1128,71 @@ detailed_result # =>
1041
1128
  client.lock('a', ttl: 10_000_000)
1042
1129
 
1043
1130
  # - try to bobtain series of locks with `a` inside the series (`a` can not be obtained for now)
1044
- client.lock_seires('c', 'a', 'b') # ... `a` is still obtained
1131
+ client.lock_series('c', 'a', 'b') # ... `a` is still obtained
1045
1132
  # =>
1046
1133
  {
1047
1134
  ok: false,
1048
1135
  result: {
1049
1136
  error: :failed_to_acquire_lock_series,
1050
- detailed_errors: { 'a' => { ok: false, result: :retry_limit_reached } },
1137
+ detailed_errors: { 'a' => { ok: false, result: :retry_limit_reached } }, # (empty without `failed_on_lock`)
1051
1138
  lock_series: ["c", "a", "b"],
1052
- acquired_locks: ["c"],
1053
- missing_locks: ["a", "b"],
1054
- failed_on_lock: ["a"],
1139
+ acquired_locks: [], # always empty: nothing is obtained by the failed series
1140
+ missing_locks: ["c", "a", "b"],
1141
+ failed_on_lock: "a", # the first lock that blocked the last completed attempt (`nil`: unknown)
1055
1142
  }
1056
1143
  }
1057
1144
  ```
1058
1145
 
1059
- ###### New instrumentaiton events:
1060
-
1061
- - `rw_mode` is the mode of the series (`read_write_mode`, the same for all locks of the series);
1146
+ ###### Instrumentation events:
1147
+
1148
+ - `rw_mode` is the mode of the series (the same for all locks of the series);
1149
+ - `#lock` events (`lock_obtained`, `lock_obtaining_failed`, ...) are not raised for the locks of the series, each lock has its own series events
1150
+ with the series context in the name (`lock_key` is the lock, `lock_keys` are all locks of the series):
1151
+ - `lock_series_intermediate_lock_obtained` - intermediate obtaining: the lock passed all checks of an attempt of the series (the lock is
1152
+ free and the lock request of the series is next). Nothing is held yet: the series can still wait for its other locks or fail.
1153
+ Raised once per lock of the series (not per attempt), in the order of the lock names up to the lock that blocks the series;
1154
+ - `lock_series_lock_obtained` - full obtaining: the lock is really obtained, all locks of the series are held;
1155
+ - `lock_series_lock_obtaining_failed` - the lock that blocked the failed series, once per series (not raised when the blocking lock
1156
+ is unknown: no attempt found it, see `failed_lock_key` of `lock_series_obtaining_failed`);
1157
+ - reentrant locks are not obtained by the series: they have no lock events (see `reentrant_lock_keys` of the series events);
1062
1158
  - see [Instrumentation Events](#instrumentation-events) for the payload details;
1063
1159
 
1064
1160
  ```ruby
1065
- # NEW instrumentation events (examples)
1066
- "redis_queued_locks.lock_series_obtained" => # {lock_keys: ["rql:lock:s", "rql:lock:t", "rql:lock:u"], rw_mode: :write, ttl: 5000, acq_id: "rql:acq:4486/1696/1704/1712/e6fd0da7991e4303", hst_id: "rql:hst:4486/1696/1712/e6fd0da7991e4303", ts: 1769627106.4688692, acq_time: 2.61, instrument: nil}
1067
- "redis_queued_locks.lock_series_hold_and_release" => # {lock_keys: ["rql:lock:x", "rql:lock:y", "rql:lock:z"], rw_mode: :write, hold_time: 0.29, ttl: 5000, acq_id: "rql:acq:4486/1696/1704/1712/e6fd0da7991e4303", hst_id: "rql:hst:4486/1696/1712/e6fd0da7991e4303", ts: 1769627106.4717052, acq_time: 4.26, instrument: nil}
1161
+ # instrumentation events (examples)
1162
+ "redis_queued_locks.lock_series_intermediate_lock_obtained" => # {lock_key: "rql:lock:s", lock_keys: ["rql:lock:s", "rql:lock:t"], rw_mode: :write, priority: 0, ttl: 5000, acq_id: "rql:acq:4486/1696/1704/1712/e6fd0da7991e4303", hst_id: "rql:hst:4486/1696/1712/e6fd0da7991e4303", ts: 1769627106.4688692, acq_time: 2.61, instrument: nil}
1163
+ "redis_queued_locks.lock_series_lock_obtained" => # {lock_key: "rql:lock:s", lock_keys: ["rql:lock:s", "rql:lock:t"], rw_mode: :write, priority: 0, ttl: 5000, acq_id: "rql:acq:4486/1696/1704/1712/e6fd0da7991e4303", hst_id: "rql:hst:4486/1696/1712/e6fd0da7991e4303", ts: 1769627106.4688692, acq_time: 2.61, instrument: nil}
1164
+ "redis_queued_locks.lock_series_lock_obtaining_failed" => # {lock_key: "rql:lock:y", lock_keys: ["rql:lock:x", "rql:lock:y"], rw_mode: :write, priority: 0, ttl: 5000, acq_id: "rql:acq:4486/1696/1704/1712/e6fd0da7991e4303", hst_id: "rql:hst:4486/1696/1712/e6fd0da7991e4303", ts: 1769627106.4717052, acq_time: 4.26, result: :retry_limit_reached, instrument: nil}
1165
+ "redis_queued_locks.lock_series_obtained" => # {lock_keys: ["rql:lock:s", "rql:lock:t", "rql:lock:u"], rw_mode: :write, priority: 0, ttl: 5000, acq_id: "rql:acq:4486/1696/1704/1712/e6fd0da7991e4303", hst_id: "rql:hst:4486/1696/1712/e6fd0da7991e4303", ts: 1769627106.4688692, acq_time: 2.61, reentrant_lock_keys: [], instrument: nil}
1166
+ "redis_queued_locks.lock_series_hold_and_release" => # {lock_keys: ["rql:lock:x", "rql:lock:y", "rql:lock:z"], rw_mode: :write, priority: 0, hold_time: 0.29, ttl: 5000, acq_id: "rql:acq:4486/1696/1704/1712/e6fd0da7991e4303", hst_id: "rql:hst:4486/1696/1712/e6fd0da7991e4303", ts: 1769627106.4717052, acq_time: 4.26, reentrant_lock_keys: [], instrument: nil}
1167
+ "redis_queued_locks.lock_series_obtaining_failed" => # {lock_keys: ["rql:lock:x", "rql:lock:y"], rw_mode: :write, priority: 0, ttl: 5000, acq_id: "rql:acq:4486/1696/1704/1712/e6fd0da7991e4303", hst_id: "rql:hst:4486/1696/1712/e6fd0da7991e4303", ts: 1769627106.4717052, acq_time: 4.26, result: :retry_limit_reached, failed_lock_key: "rql:lock:y", instrument: nil}
1068
1168
  ```
1069
1169
 
1070
- ###### New logs:
1170
+ ###### Logs:
1071
1171
 
1072
- - `rw_mode` is the mode of the series (`read_write_mode`, the same for all locks of the series);
1172
+ - `rw_mode` is the mode of the series (the same for all locks of the series);
1073
1173
  - see [Logging](#logging) for the logged keys;
1074
1174
 
1075
1175
  ```shell
1076
- # NEW logs (examples)
1077
- [redis_queued_locks.start_lock_series_obtaining] lock_keys => '["rql:lock:a", "rql:lock:b"]' queue_ttl => 15 acq_id => 'rql:acq:4486/1696/1704/1712/e6fd0da7991e4303' hst_id => 'rql:hst:4486/1696/1712/e6fd0da7991e4303' acs_strat => 'queued' rw_mode => 'write'
1078
- [redis_queued_locks.lock_series_obtained] lock_keys => '["rql:lock:a", "rql:lock:b"]' queue_ttl => 15 acq_id => 'rql:acq:4486/1696/1704/1712/e6fd0da7991e4303' hst_id => 'rql:hst:4486/1696/1712/e6fd0da7991e4303' acs_strat => 'queued' rw_mode => 'write' acq_time => 2.55 (ms)
1079
- [redis_queued_locks.expire_lock_series] lock_keys => '["rql:lock:x", "rql:lock:y", "rql:lock:z"]' queue_ttl => 15 acq_id => 'rql:acq:4486/1696/1704/1712/e6fd0da7991e4303' hst_id => 'rql:hst:4486/1696/1712/e6fd0da7991e4303' acs_strat => 'queued' rw_mode => 'write'
1176
+ # logs (examples)
1177
+ [redis_queued_locks.start_lock_series_obtaining] lock_keys => '["rql:lock:a", "rql:lock:b"]' queue_ttl => 15 acq_id => 'rql:acq:4486/1696/1704/1712/e6fd0da7991e4303' hst_id => 'rql:hst:4486/1696/1712/e6fd0da7991e4303' acs_strat => 'queued' rw_mode => 'write' priority => 0
1178
+ [redis_queued_locks.start_try_to_lock_series_cycle] lock_keys => '["rql:lock:a", "rql:lock:b"]' queue_ttl => 15 acq_id => 'rql:acq:4486/1696/1704/1712/e6fd0da7991e4303' hst_id => 'rql:hst:4486/1696/1712/e6fd0da7991e4303' acs_strat => 'queued' rw_mode => 'write' priority => 0
1179
+ [redis_queued_locks.lock_series_intermediate_lock_obtained] lock_key => 'rql:lock:a' lock_keys => '["rql:lock:a", "rql:lock:b"]' queue_ttl => 15 acq_id => 'rql:acq:4486/1696/1704/1712/e6fd0da7991e4303' hst_id => 'rql:hst:4486/1696/1712/e6fd0da7991e4303' acs_strat => 'queued' rw_mode => 'write' priority => 0 acq_time => 2.55 (ms)
1180
+ [redis_queued_locks.lock_series_lock_obtained] lock_key => 'rql:lock:a' lock_keys => '["rql:lock:a", "rql:lock:b"]' queue_ttl => 15 acq_id => 'rql:acq:4486/1696/1704/1712/e6fd0da7991e4303' hst_id => 'rql:hst:4486/1696/1712/e6fd0da7991e4303' acs_strat => 'queued' rw_mode => 'write' priority => 0 acq_time => 2.55 (ms)
1181
+ [redis_queued_locks.lock_series_obtained] lock_keys => '["rql:lock:a", "rql:lock:b"]' queue_ttl => 15 acq_id => 'rql:acq:4486/1696/1704/1712/e6fd0da7991e4303' hst_id => 'rql:hst:4486/1696/1712/e6fd0da7991e4303' acs_strat => 'queued' rw_mode => 'write' priority => 0 acq_time => 2.55 (ms) reentrant_lock_keys => '[]'
1182
+ [redis_queued_locks.expire_lock_series] lock_keys => '["rql:lock:a", "rql:lock:b"]' queue_ttl => 15 acq_id => 'rql:acq:4486/1696/1704/1712/e6fd0da7991e4303' hst_id => 'rql:hst:4486/1696/1712/e6fd0da7991e4303' acs_strat => 'queued' rw_mode => 'write' priority => 0
1080
1183
  ```
1081
1184
 
1082
1185
  ----
1083
1186
 
1084
- #### #lock_series! - (PoC) exceptional `lock_series`
1187
+ #### #lock_series! - exceptional `lock_series`
1085
1188
 
1086
1189
  <sup>\[[back to top](#usage)\]</sup>
1087
1190
 
1088
- > (IMPORTANT!) "Proof of Concept" realization. Will be reworked in the future (very-very soon)
1191
+ Raises the same errors as `#lock!` when the series can not be obtained (`RedisQueuedLocks::LockAlreadyObtainedError`,
1192
+ `RedisQueuedLocks::ConflictLockObtainError`, `RedisQueuedLocks::LockAcquirementRetryLimitError`,
1193
+ `RedisQueuedLocks::LockAcquirementTimeoutError`).
1089
1194
 
1090
- For more details see `lock_series` [docs](#lock_series---poc-acquire-a-series-of-locks)
1195
+ For more details see `lock_series` [docs](#lock_series---acquire-a-series-of-locks)
1091
1196
 
1092
1197
  ```ruby
1093
1198
  client.lock_series!("x", "y", "z") { ...some_code... }
@@ -1186,18 +1291,18 @@ rql.lock_info("your_lock_name")
1186
1291
 
1187
1292
  ```ruby
1188
1293
  # <for read/write locks>
1189
- # (see `read_write_mode:` kwarg attribute of #lock/#lock! methods and "Read/Write Locks" documentation section)
1294
+ # (see `rw_mode:` kwarg attribute of #lock/#lock! methods and "Read/Write Locks" documentation section)
1190
1295
  # (lock requests are not included: use #queue_info to see the queued readers and writers)
1191
1296
 
1192
1297
  # -> case 1: the lock is held by readers, writers and readers are waiting in the lock queues;
1193
1298
 
1194
1299
  # process#1 and process#2 (readers, work in parallel):
1195
- rql.lock("your_lock_name", read_write_mode: :read, ttl: 5_000, meta: { "kek" => "pek" }) { ... }
1196
- rql.lock("your_lock_name", read_write_mode: :read, ttl: 3_000) { ... }
1300
+ rql.lock("your_lock_name", rw_mode: :read, ttl: 5_000, meta: { "kek" => "pek" }) { ... }
1301
+ rql.lock("your_lock_name", rw_mode: :read, ttl: 3_000) { ... }
1197
1302
  # process#3 (writer, waits for all readers in the write lock queue):
1198
- rql.lock("your_lock_name", read_write_mode: :write) { ... }
1303
+ rql.lock("your_lock_name", rw_mode: :write) { ... }
1199
1304
  # process#4 (reader, waits for the earlier writer in the read lock queue):
1200
- rql.lock("your_lock_name", read_write_mode: :read) { ... }
1305
+ rql.lock("your_lock_name", rw_mode: :read) { ... }
1201
1306
 
1202
1307
  rql.lock_info("your_lock_name")
1203
1308
 
@@ -1256,15 +1361,18 @@ lock acquirers and their position in queue. The queue of write lock requests is
1256
1361
  - get the lock queue information;
1257
1362
  - queue represents the ordered set of lock key reqests:
1258
1363
  - set is ordered by score in ASC manner (inside the Redis Set);
1259
- - score is represented as a timestamp when the lock request was made;
1364
+ - score is represented as a timestamp when the lock request was made shifted by the
1365
+ [priority](#lock-request-prioritization) band (`-priority * 2**32` seconds: the default priority `0` keeps the timestamp);
1260
1366
  - represents the acquirer identifier and their score as an array of hashes;
1261
1367
  - returns `nil` if lock queues (write and read) do not exist;
1262
1368
  - lock queue data (`Hash<String,String|Array<Hash<String|Numeric>>`):
1263
1369
  - `"lock_queue"` - `string` - lock queue key in redis (write lock requests);
1264
1370
  - `"queue"` - `array` - an array of lock requests (array of hashes):
1265
1371
  - `"acq_id"` - `string` - acquirer identifier (process_id/thread_id/fiber_id/ractor_id/identity by default);
1266
- - `"score"` - `float`/`epoch` - time when the lock request was made (epoch);
1372
+ - `"score"` - `float`/`epoch` - the position of the lock request: time when the lock request was made (epoch) shifted by
1373
+ `-priority * 2**32` seconds (the time itself for the default priority `0`);
1267
1374
  - `"rw_mode"` - `string` - the requested lock mode (`"write"`);
1375
+ - `"priority"` - `integer` - the lock request priority (from `-100` to `100`, see [Lock Request Prioritization](#lock-request-prioritization));
1268
1376
  - `"read_lock_queue"`, `"read_queue"` - (only when read lock requests exist) the read lock queue key and read lock requests
1269
1377
  with the same structure (`"rw_mode"` - `"read"`), see [Read/Write Locks](#readwrite-locks);
1270
1378
 
@@ -1275,15 +1383,16 @@ rql.queue_info("your_lock_name")
1275
1383
  {
1276
1384
  "lock_queue" => "rql:lock_queue:your_lock_name",
1277
1385
  "queue" => [
1278
- { "acq_id" => "rql:acq:123/456/567/678/fa76df9cc2", "score" => 1711606640.540842, "rw_mode" => "write" },
1279
- { "acq_id" => "rql:acq:123/567/456/679/c7bfcaf4f9", "score" => 1711606640.540906, "rw_mode" => "write" },
1280
- { "acq_id" => "rql:acq:555/329/523/127/7329553b11", "score" => 1711606640.540963, "rw_mode" => "write" },
1386
+ # (a lock request of the high priority: ahead of the requests of the default priority)
1387
+ { "acq_id" => "rql:acq:123/456/567/678/fa76df9cc2", "score" => -2583360655.459158, "rw_mode" => "write", "priority" => 1 },
1388
+ { "acq_id" => "rql:acq:123/567/456/679/c7bfcaf4f9", "score" => 1711606640.540906, "rw_mode" => "write", "priority" => 0 },
1389
+ { "acq_id" => "rql:acq:555/329/523/127/7329553b11", "score" => 1711606640.540963, "rw_mode" => "write", "priority" => 0 },
1281
1390
  # ...etc
1282
1391
  ],
1283
1392
  # (when read lock requests exist)
1284
1393
  "read_lock_queue" => "rql:lock_read_queue:your_lock_name",
1285
1394
  "read_queue" => [
1286
- { "acq_id" => "rql:acq:123/456/567/700/fa76df9cc2", "score" => 1711606640.541011, "rw_mode" => "read" },
1395
+ { "acq_id" => "rql:acq:123/456/567/700/fa76df9cc2", "score" => 1711606640.541011, "rw_mode" => "read", "priority" => 0 },
1287
1396
  # ...etc
1288
1397
  ]
1289
1398
  }
@@ -1398,7 +1507,7 @@ rql.unlock("your_lock_name")
1398
1507
 
1399
1508
  - releases the read lock of the current acquirer only (see [Read/Write Locks](#readwrite-locks)):
1400
1509
  other read locks, the write lock and lock requests are not affected (`#unlock` releases the lock entirely);
1401
- - suitable for read locks obtained without a block of code (`lock(..., read_write_mode: :read)` without a block);
1510
+ - suitable for read locks obtained without a block of code (`lock(..., rw_mode: :read)` without a block);
1402
1511
  - the current acquirer is calculated in the same way as for `#lock` (`#current_acquirer_id`: process, thread, fiber, ractor, identity),
1403
1512
  so call it from the same thread/fiber that obtained the read lock;
1404
1513
  - the read lock of a different acquirer can be released via `:acquirer_id` (for example, extracted from `#lock_info`/`#locks_info`);
@@ -1416,7 +1525,7 @@ rql.unlock("your_lock_name")
1416
1525
  - raises the `"redis_queued_locks.explicit_read_lock_release"` instrumentation event;
1417
1526
 
1418
1527
  ```ruby
1419
- rql.lock('report', read_write_mode: :read, ttl: 30_000) # => obtained without a block
1528
+ rql.lock('report', rw_mode: :read, ttl: 30_000) # => obtained without a block
1420
1529
  # ...
1421
1530
  rql.unlock_read('report')
1422
1531
  # =>
@@ -1471,7 +1580,7 @@ rql.unlock_read('report', acquirer_id: reader['acq_id'])
1471
1580
  - pre-configured value in `config['logger']`;
1472
1581
  - `:instrumenter` - (optional) `[#notify]`
1473
1582
  - custom instrumenter object;
1474
- - pre-configured value in `config['isntrumenter']`;
1583
+ - pre-configured value in `config['instrumenter']`;
1475
1584
  - `:instrument` - (optional) `[NilClass,Any]`
1476
1585
  - custom instrumentation data wich will be passed to the instrumenter's payload with `:instrument` key;
1477
1586
  - `:log_sampling_enabled` - (optional) `[Boolean]`
@@ -1554,7 +1663,7 @@ rql.clear_locks
1554
1663
  - pre-configured value in `config['logger']`;
1555
1664
  - `:instrumenter` - (optional) `[#notify]`
1556
1665
  - custom instrumenter object;
1557
- - pre-configured value in `config['isntrumenter']`;
1666
+ - pre-configured value in `config['instrumenter']`;
1558
1667
  - `:instrument` - (optional) `[NilClass,Any]`
1559
1668
  - custom instrumentation data wich will be passed to the instrumenter's payload with `:instrument` key;
1560
1669
  - `:log_sampling_enabled` - (optional) `[Boolean]`
@@ -1662,7 +1771,7 @@ rql.clear_current_locks
1662
1771
  - pre-configured value in `config['logger']`;
1663
1772
  - `:instrumenter` - (optional) `[#notify]`
1664
1773
  - custom instrumenter object;
1665
- - pre-configured value in `config['isntrumenter']`;
1774
+ - pre-configured value in `config['instrumenter']`;
1666
1775
  - `:instrument` - (optional) `[NilClass,Any]`
1667
1776
  - custom instrumentation data wich will be passed to the instrumenter's payload with `:instrument` key;
1668
1777
  - `:log_sampling_enabled` - (optional) `[Boolean]`
@@ -1732,7 +1841,7 @@ rql.release_locks_of(host_id: rql.current_host_id, acquirer_id: rql.current_acqu
1732
1841
  - `milliseconds` - (required) `[Integer]`
1733
1842
  - how many milliseconds should be added to the lock's TTL;
1734
1843
  - should be a positive `Integer`;
1735
- - `:read_write_mode` - (optional) `[Symbol]`
1844
+ - `:rw_mode` - (optional) `[Symbol]`
1736
1845
  - `:write` (default) - extends the write lock (the behavior described below);
1737
1846
  - `:read` - extends the read lock of the current acquirer only (see [Read/Write Locks](#readwrite-locks)):
1738
1847
  - the read lock expiration, the read locks registry TTL and the read lock data TTL are extended;
@@ -1972,8 +2081,8 @@ rql.locks_info # or rql.locks_info(scan_size: 123)
1972
2081
  - returns `Set<Hash<Symbol,Any>>` (see [#queue_info](#queue_info) and examples below for details).
1973
2082
  - contained data: `{ queue: String, requests: Array<Hash<String,Any>> }`
1974
2083
  - `:queue` - `String` - lock key queue in Redis;
1975
- - `:requests` - `Array<Hash<String,Any>>` - lock requests in the que with their acquirer id, score and requested lock mode
1976
- (`"rw_mode"` - `"write"` for `rql:lock_queue:*`, `"read"` for read lock queues `rql:lock_read_queue:*`).
2084
+ - `:requests` - `Array<Hash<String,Any>>` - lock requests in the que with their acquirer id, score, requested lock mode
2085
+ (`"rw_mode"` - `"write"` for `rql:lock_queue:*`, `"read"` for read lock queues `rql:lock_read_queue:*`) and priority.
1977
2086
 
1978
2087
  ```ruby
1979
2088
  rql.queues_info # or rql.qeuues_info(scan_size: 123)
@@ -1981,13 +2090,13 @@ rql.queues_info # or rql.qeuues_info(scan_size: 123)
1981
2090
  => #<Set:
1982
2091
  {{:queue=>"rql:lock_queue:some-lock-123",
1983
2092
  :requests=>
1984
- [{"acq_id"=>"rql:acq:38529/4500/4520/4360/66093702f24a3129", "score"=>1711606640.540842, "rw_mode"=>"write"},
1985
- {"acq_id"=>"rql:acq:38529/4580/4600/4360/66093702f24a3129", "score"=>1711606640.540906, "rw_mode"=>"write"},
1986
- {"acq_id"=>"rql:acq:38529/4620/4640/4360/66093702f24a3129", "score"=>1711606640.5409632, "rw_mode"=>"write"}]},
2093
+ [{"acq_id"=>"rql:acq:38529/4500/4520/4360/66093702f24a3129", "score"=>1711606640.540842, "rw_mode"=>"write", "priority"=>0},
2094
+ {"acq_id"=>"rql:acq:38529/4580/4600/4360/66093702f24a3129", "score"=>1711606640.540906, "rw_mode"=>"write", "priority"=>0},
2095
+ {"acq_id"=>"rql:acq:38529/4620/4640/4360/66093702f24a3129", "score"=>1711606640.5409632, "rw_mode"=>"write", "priority"=>0}]},
1987
2096
  {:queue=>"rql:lock_read_queue:some-lock-456",
1988
2097
  :requests=>
1989
- [{"acq_id"=>"rql:acq:38529/4380/4400/4360/66093702f24a3129", "score"=>1711606640.540722, "rw_mode"=>"read"},
1990
- {"acq_id"=>"rql:acq:38529/4420/4440/4360/66093702f24a3129", "score"=>1711606640.5407748, "rw_mode"=>"read"}]},
2098
+ [{"acq_id"=>"rql:acq:38529/4380/4400/4360/66093702f24a3129", "score"=>1711606640.540722, "rw_mode"=>"read", "priority"=>0},
2099
+ {"acq_id"=>"rql:acq:38529/4420/4440/4360/66093702f24a3129", "score"=>1711606640.5407748, "rw_mode"=>"read", "priority"=>0}]},
1991
2100
  ...}>
1992
2101
  ```
1993
2102
  ---
@@ -1999,8 +2108,10 @@ rql.queues_info # or rql.qeuues_info(scan_size: 123)
1999
2108
  In some cases your lock requests may become "dead". It means that your lock request lives in lock queue in Redis without
2000
2109
  any processing. It can happen when your processs that are enqueeud to the lock queue is failed unexpectedly (for some reason)
2001
2110
  before the lock acquire moment occurs and when no any other process does not need this lock anymore.
2002
- For this case your lock reuquest will be cleared only when any process will try
2003
- to acquire this lock again (cuz lock acquirement triggers the removement of expired requests).
2111
+ For this case your lock request will be cleared only when another lock request meets it: a lock request of the same
2112
+ [priority](#lock-request-prioritization) (lock acquirement drops the expired requests of its priority) or a `:queued` lock request
2113
+ it blocks (the head of the queue, an earlier request of the other read/write mode); the dead requests of other priorities stay
2114
+ in the queue.
2004
2115
 
2005
2116
  NOTE: with the enabled [Lock Index](#lock-index) the lock request queue lives as long as its latest lock request (the queue TTL
2006
2117
  is refreshed with the request `queue_ttl` on each lock request), so the queue of dead requests expires by itself (queues of older
@@ -2009,7 +2120,12 @@ RQL versions and queues created with the disabled lock index have no TTL: see [#
2009
2120
  In order to help with these dead requests you may periodically call `#clear_dead_requests`
2010
2121
  with corresponding `:dead_ttl` option, that is pre-configured by default via `config['dead_request_ttl']`.
2011
2122
  Lock queues are found via the redis `SCAN` (one pass) or via the [Lock Index](#lock-index) (one `ZSCAN` pass, the keyspace is
2012
- never scanned: the enabled lock index).
2123
+ never scanned: the enabled lock index). Each batch of lock queues takes two round trips: the lock requests are read, then the dead
2124
+ requests of each [priority](#lock-request-prioritization) found in the queues are dropped (no second round trip without dead requests).
2125
+ With `by_priority: true` the lock requests are not read: only the first lock request of each priority is read (the dead requests
2126
+ of a priority are found by its first lock request): a batch takes one round trip per priority of its lock queue with the most
2127
+ priorities + 1 (the dead requests of a priority are dropped in the round trip that reads the next priority), so the transfer does not
2128
+ depend on the length of the lock queues: use it for long lock queues with few priorities.
2013
2129
 
2014
2130
  `:dead_ttl` option is required because of it is no any **fast** and **resource-free** way to understand which request
2015
2131
  is dead now and is it really dead cuz each request queue can host their requests with
@@ -2022,14 +2138,20 @@ Accepts:
2022
2138
  - `:scan_size` - (optional) `[Integer]`
2023
2139
  - the batch of scanned keys for Redis'es SCAN command;
2024
2140
  - (the enabled lock index) the `ZSCAN` count of the [Lock Index](#lock-index) pass and the number of locks (both lock queues
2025
- of each lock) processed in one round trip;
2141
+ of each lock) processed in one batch;
2026
2142
  - has a preconfigured valie in `config['lock_release_batch_size']`;
2143
+ - `:by_priority` - (optional) `[Boolean]`
2144
+ - read only the first lock request of each [priority](#lock-request-prioritization) in the lock queues instead of all lock
2145
+ requests: a batch of lock queues takes one round trip per priority of its lock queue with the most priorities + 1 (instead
2146
+ of two round trips), the transfer does not depend on the length of the lock queues (for long lock queues with few
2147
+ priorities);
2148
+ - `false` by default (all lock requests are read);
2027
2149
  - `:logger` - (optional) `[::Logger,#debug]`
2028
2150
  - custom logger object;
2029
2151
  - pre-configured in `config['logger']`;
2030
2152
  - `:instrumenter` - (optional) `[#notify]`
2031
2153
  - custom instrumenter object;
2032
- - pre-configured in `config['isntrumenter']`;
2154
+ - pre-configured in `config['instrumenter']`;
2033
2155
  - `:instrument` - (optional) `[NilClass,Any]`
2034
2156
  - custom instrumentation data wich will be passed to the instrumenter's payload with :instrument key;
2035
2157
  - `nil` by default (no additional data);
@@ -2079,6 +2201,9 @@ rql.clear_dead_requests(dead_ttl: 60 * 60 * 1000) # 1 hour in milliseconds
2079
2201
  }>
2080
2202
  }
2081
2203
  }
2204
+
2205
+ # long lock queues with few priorities: only the first lock request of each priority is read
2206
+ rql.clear_dead_requests(dead_ttl: 60 * 60 * 1000, by_priority: true)
2082
2207
  ```
2083
2208
 
2084
2209
  ---
@@ -2598,37 +2723,142 @@ rql.possible_host_ids
2598
2723
  - can be customized in methods `#lock` and `#lock!` via `:access_strategy` attribute (see method signatures of #lock and #lock! methods);
2599
2724
  - supports different strategies:
2600
2725
  - `:queued` (FIFO): the classic queued behavior (default), your lock will be obitaned if you are first in queue and the required lock is free;
2726
+ the queue is ordered by the [lock request priorities](#lock-request-prioritization) (FIFO inside the same priority);
2601
2727
  - `:random` (RANDOM): obtain a lock without checking the positions in the queue (but with checking the limist, retries, timeouts and so on). if lock is free to obtain - it will be obtained;
2728
+ (the priorities of other lock requests are ignored too; the lock request itself is queued at the position of its
2729
+ [priority](#lock-request-prioritization) like a `:queued` one: `:queued` lock requests wait for it);
2602
2730
  - for current implementation detalis check:
2603
2731
  - [Configuration](#configuration) documentation: see `config['default_access_strategy']` config docs;
2604
2732
  - [#lock](#lock---obtain-a-lock) method documentation: see `access_strategy` attribute docs;
2605
2733
 
2606
2734
  ---
2607
2735
 
2736
+ ## Lock Request Prioritization
2737
+
2738
+ <sup>\[[back to top](#table-of-contents)\]</sup>
2739
+
2740
+ - `priority:` attribute of `#lock`, `#lock!`, `#lock_series` and `#lock_series!` (`config['default_lock_priority']` by default):
2741
+ an Integer from `-100` (the lowest priority) to `100` (the highest priority), `0` - (**default**) normal priority,
2742
+ or a priority name (Symbol) of `config['lock_priorities']`;
2743
+ - priority names (`config['lock_priorities']`, a priority name => its Integer priority):
2744
+ - default names: `:lowest` (`-100`), `:low` (`-50`), `:normal` (`0`), `:high` (`50`), `:highest` (`100`);
2745
+ - name the priorities of your application (keep the default names with `merge`; the default value is frozen: assign
2746
+ a new hash);
2747
+ - a name is resolved to its Integer priority on each lock acquirement: the lock request queues, `#queue_info`/`#queues_info`,
2748
+ logs and instrumentation have the Integer priority; an unknown name fails with `RedisQueuedLocks::ArgumentError`;
2749
+ - `config['default_lock_priority']` can be a priority name too: an unknown one fails the configuration with
2750
+ `RedisQueuedLocks::ConfigValidationError` (checked after the configuration block, the keys can be assigned in any order,
2751
+ and on a direct `config[...] =` assignment);
2752
+ - `:queued` access strategy (default): the lock request is placed in the lock request queue after all requests of the same
2753
+ or higher priority and before all requests of the lower priority: strict priority between priorities, FIFO inside the same
2754
+ priority (read and write lock requests too, see [Read/Write Locks](#readwrite-locks));
2755
+
2756
+ ```ruby
2757
+ # a balance operation goes ahead of the email notifications that are already waiting for the lock
2758
+ rql.lock("user_#{user.id}", priority: 50) { process_balance }
2759
+
2760
+ # a payment goes ahead of the balance operations
2761
+ rql.lock("user_#{user.id}", priority: 90) { process_payment }
2762
+
2763
+ # email notifications wait for all lock requests of higher priorities
2764
+ rql.lock("user_#{user.id}", priority: -10) { send_email }
2765
+
2766
+ # normal priority by default
2767
+ rql.lock("user_#{user.id}") { update_profile } # priority: 0
2768
+
2769
+ # the default priority of the client (for example, in background workers)
2770
+ rql.configure { |config| config['default_lock_priority'] = -10 }
2771
+
2772
+ # priority names: the default ones (:lowest, :low, :normal, :high, :highest) and the names of your application
2773
+ rql.configure do |config|
2774
+ config['lock_priorities'] = config['lock_priorities'].merge(payment: 90, balance: 50, email: -10)
2775
+ end
2776
+ rql.lock("user_#{user.id}", priority: :payment) { process_payment } # priority: 90
2777
+ rql.lock("user_#{user.id}", priority: :email) { send_email } # priority: -10
2778
+ rql.lock("user_#{user.id}", priority: :high) { update_profile } # priority: 50
2779
+ ```
2780
+
2781
+ - how it works:
2782
+ - the position of the lock request in the queue (the score) is the request time shifted by the priority band:
2783
+ `request time - priority * 2**32 seconds` (each priority has its own range of positions; the default priority `0` keeps
2784
+ the request time: the positions of RQL versions without priorities);
2785
+ - every lock request drops dead requests (older than `queue_ttl`) of its own priority in both lock queues (in the same round
2786
+ trip as before priorities); dead requests of other priorities that block it (the head of the queue and the earlier request
2787
+ of the other read/write mode: the priority and the request time are decoded from the position) are dropped with all dead
2788
+ requests of their priorities in one more round trip of the same attempt, so a dead request of any priority never blocks the
2789
+ queue and the cost of an attempt does not depend on the number of priorities in use;
2790
+ - dead requests of lower priorities never block: they are dropped by the lock requests of their priority (or lower ones)
2791
+ and by `#clear_dead_requests` (it drops the dead requests of every priority found in the queues);
2792
+ - lock requests at infinite positions (`-inf`/`+inf`: manual writes of the lock queues, RQL never writes them) have the
2793
+ bound priorities (`100`/`-100`) in `#queue_info`/`#queues_info` and logs; a request at `-inf` is a dead one (like before
2794
+ priorities): it is dropped when it blocks a lock request and by `#clear_dead_requests`; a request at `+inf` is behind all
2795
+ requests and is never dropped as a dead one;
2796
+ - `#queue_info`/`#queues_info` return the priority of each lock request (`"priority"`); the lock acquirement logs and
2797
+ instrumentation events (classic, reentrant and lock series ones) have the `priority` of the lock request, the `log_lock_try`
2798
+ logs also have the priorities of the requests ahead (`first_priority_in_queue`, `ahead_priority`, `dead_priority`;
2799
+ `first_priority_in_queue => nil` when the lock queue has no requests);
2800
+ - limitations and recommendations:
2801
+ - the priority orders the waiting lock requests only: the obtained lock is never taken away (no preemption);
2802
+ - strict priority: a continuous flow of requests of a higher priority can starve the requests of lower priorities (they
2803
+ reach their `timeout`/`retry_count` limits): give them longer limits;
2804
+ - the released lock is passed by polling: the next lock request notices the released lock on its next attempt
2805
+ (`retry_delay`): use a shorter `retry_delay` for the important logic;
2806
+ - the default `retry_count` (`3`) gives a short waiting time: the priority matters for the requests that wait longer
2807
+ (`retry_count: nil` with `timeout`);
2808
+ - requests of the same priority made within ~61 µs of each other are ordered by their acquirer ids (the precision of
2809
+ the shifted positions);
2810
+ - each lock request drops dead requests with its own `queue_ttl` (the requests of its priority and the requests that block it):
2811
+ a request that waits longer than the shorter `queue_ttl` of such requests is dropped by their attempts and comes back at its
2812
+ old position on its own attempts (its own `queue_ttl` is not reached yet), so the requests behind it with a shorter
2813
+ `queue_ttl` don't wait for it: it loses its place (and the advantage of its priority) to them, the lock goes to the request
2814
+ whose attempt finds the lock free first; use the same `queue_ttl` for the lock requests of a lock (a longer `queue_ttl` does
2815
+ not keep a request in the queue);
2816
+ - `:random` access strategy: the lock request does not wait for its turn (the order of the queues and the priorities of other
2817
+ requests are ignored), but it is queued at the position of its priority: `:queued` lock requests wait for it (a `:random`
2818
+ request of a high priority holds back the waiting `:queued` requests of lower priorities until it obtains the lock or
2819
+ leaves the queue);
2820
+ - dead lock requests are dropped by the lock requests that meet them (the requests of their priority and the requests they
2821
+ block): the dead requests of the priorities that no lock request uses any more (and the dead requests met only by `:random`
2822
+ lock requests) stay in the queues; they never block (an accepted trade-off of the head-driven cleanup: the cost of an
2823
+ attempt does not depend on the number of priorities); without the [Lock Index](#lock-index) (its queues expire) and the
2824
+ [swarm](#swarm-mode-and-zombie-locks) (it drops the requests of dead processes) call `#clear_dead_requests` periodically
2825
+ (e.g. from a scheduled job) to drop them (`by_priority: true` for long lock queues: only the first lock request of each
2826
+ priority is read);
2827
+ - all processes that share locks should use RQL versions with priorities before positive priorities are used (`:high`,
2828
+ `:highest`, any priority above `0`): older versions drop such lock requests as dead ones (the order is broken; the mutual
2829
+ exclusion is not affected: lock queues never take part in it); the default priority (`0`) and negative priorities
2830
+ (`:low`, `:lowest`) work with older versions too: older versions keep such requests behind their own ones and never drop
2831
+ them as dead ones (the dead ones are dropped by the upgraded processes);
2832
+
2833
+ ---
2834
+
2608
2835
  ## Read/Write Locks
2609
2836
 
2610
2837
  <sup>\[[back to top](#table-of-contents)\]</sup>
2611
2838
 
2612
- - `read_write_mode:` attribute of `#lock` and `#lock!`:
2839
+ - `rw_mode:` attribute of `#lock` and `#lock!`:
2613
2840
  - `:write` (**default**) - exclusive lock (the classic RQL lock): waits for the write lock and for all read locks;
2614
2841
  - `:read` - shared lock: waits for the write lock only (read locks of different acquirers work in parallel);
2615
2842
 
2616
2843
  ```ruby
2617
2844
  # readers work in parallel
2618
- rql.lock('report', read_write_mode: :read) { read_report }
2845
+ rql.lock('report', rw_mode: :read) { read_report }
2619
2846
 
2620
2847
  # writer waits for all readers (and readers wait for the writer)
2621
- rql.lock('report', read_write_mode: :write) { rebuild_report }
2848
+ rql.lock('report', rw_mode: :write) { rebuild_report }
2622
2849
 
2623
2850
  # write mode is default:
2624
- rql.lock('report') { rebuild_report } # read_write_mode: :write by default
2851
+ rql.lock('report') { rebuild_report } # rw_mode: :write by default
2625
2852
  ```
2626
2853
 
2627
2854
  - lock request ordering (`access_strategy`):
2628
2855
  - `:queued` (default) - FIFO between modes: read lock request waits for every earlier write lock request,
2629
2856
  write lock request waits for every earlier read and write lock request (so writers are not starved by a
2630
- continuous flow of readers and consecutive readers obtain the lock together);
2631
- - `:random` - lock queues are ignored: any free lock is obtained (a continuous flow of readers can starve writers);
2857
+ continuous flow of readers and consecutive readers obtain the lock together); with
2858
+ [lock request priorities](#lock-request-prioritization) "earlier" means "ahead in the queue": a request of a higher
2859
+ priority is ahead of all requests of lower priorities in both modes (FIFO inside the same priority);
2860
+ - `:random` - the order of the lock queues is ignored: any free lock is obtained (a continuous flow of readers can starve
2861
+ writers); the lock request itself is queued like any lock request (`:queued` lock requests wait for it);
2632
2862
  - read locks:
2633
2863
  - each read lock has its own `ttl` and is released (after the block execution) or expired independently of other read locks;
2634
2864
  - `ttl` should be a positive `Integer`;
@@ -2639,11 +2869,11 @@ rql.lock('report') { rebuild_report } # read_write_mode: :write by default
2639
2869
  - `#unlock` releases the lock entirely: the write lock, all read locks (with their data) and all lock requests;
2640
2870
  - `#unlock_read` (aka `#release_read_lock`) releases the read lock of the current acquirer
2641
2871
  (suitable for read locks obtained without a block), or the read lock of the acquirer passed via `acquirer_id:`;
2642
- - `#extend_lock_ttl(lock_name, milliseconds, read_write_mode: :read)` extends the read lock of the current acquirer only;
2872
+ - `#extend_lock_ttl(lock_name, milliseconds, rw_mode: :read)` extends the read lock of the current acquirer only;
2643
2873
  `all_read_locks: true` extends all live read locks of the lock;
2644
- - `#lock_series` with `read_write_mode: :read` obtains a series of read locks (the mode is the same for all locks of the series):
2874
+ - `#lock_series` with `rw_mode: :read` obtains a series of read locks (the mode is the same for all locks of the series):
2645
2875
  - read lock series of different acquirers work in parallel, writers wait for them;
2646
- - after the block (or when the series fails) only the read locks of the current acquirer are released;
2876
+ - after the block only the read locks of the current acquirer are released;
2647
2877
  - reentrant read locks of the series (already obtained by the outer logic of the same acquirer) are not released by the series;
2648
2878
  - reentrant locks (the same acquirer, see [Deadlocks and Reentrant locks](#deadlocks-and-reentrant-locks)):
2649
2879
  - read lock request under the obtained read lock or write lock is processed as a reentrant lock of the obtained lock
@@ -2727,13 +2957,13 @@ rql.lock('report') { rebuild_report } # read_write_mode: :write by default
2727
2957
  - default logs (raised from `#lock`/`#lock!`):
2728
2958
 
2729
2959
  ```ruby
2730
- "[redis_queued_locks.start_lock_obtaining]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2731
- "[redis_queued_locks.start_try_to_lock_cycle]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2732
- "[redis_queued_locks.dead_score_reached__reset_acquirer_position]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2733
- "[redis_queued_locks.lock_obtained]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "acq_time");
2734
- "[redis_queued_locks.lock_obtaining_failed]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "acq_time", "result");
2735
- "[redis_queued_locks.extendable_reentrant_lock_obtained]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "acq_time");
2736
- "[redis_queued_locks.reentrant_lock_obtained]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "acq_time");
2960
+ "[redis_queued_locks.start_lock_obtaining]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority");
2961
+ "[redis_queued_locks.start_try_to_lock_cycle]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority");
2962
+ "[redis_queued_locks.dead_score_reached__reset_acquirer_position]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority");
2963
+ "[redis_queued_locks.lock_obtained]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority", "acq_time");
2964
+ "[redis_queued_locks.lock_obtaining_failed]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority", "acq_time", "result");
2965
+ "[redis_queued_locks.extendable_reentrant_lock_obtained]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority", "acq_time");
2966
+ "[redis_queued_locks.reentrant_lock_obtained]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority", "acq_time");
2737
2967
  "[redis_queued_locks.fail_fast_or_limits_reached_or_deadlock__dequeue]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2738
2968
  "[redis_queued_locks.expire_lock]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2739
2969
  "[redis_queued_locks.decrease_lock]" # (logs "lock_key", "decreased_ttl", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
@@ -2750,24 +2980,34 @@ rql.lock('report') { rebuild_report } # read_write_mode: :write by default
2750
2980
  "[redis_queued_locks.try_lock.reentrant_lock__work_through]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "spc_status", "last_spc_ts");
2751
2981
  "[redis_queued_locks.try_lock.single_process_lock_conflict__dead_lock]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "spc_status", "last_spc_ts");
2752
2982
  "[redis_queued_locks.try_lock.single_process_lock_conflict__lock_upgrade]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "spc_status", "last_spc_ts");
2753
- "[redis_queued_locks.try_lock.acq_added_to_queue]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2983
+ "[redis_queued_locks.try_lock.acq_added_to_queue]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority");
2754
2984
  "[redis_queued_locks.try_lock.remove_expired_acqs]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2755
- "[redis_queued_locks.try_lock.get_first_from_queue]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "first_acq_id_in_queue");
2985
+ "[redis_queued_locks.try_lock.dead_request_removed]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "dead_acq_id", "dead_priority");
2986
+ "[redis_queued_locks.try_lock.get_first_from_queue]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "first_acq_id_in_queue", "first_priority_in_queue");
2756
2987
  "[redis_queued_locks.try_lock.exit__queue_ttl_reached]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2757
- "[redis_queued_locks.try_lock.exit__no_first]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "first_acq_id_in_queue", "<current_lock_data>");
2758
- "[redis_queued_locks.try_lock.exit__lock_still_obtained]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "first_acq_id_in_queue", "locked_by_acq_id", "<current_lock_data>");
2759
- "[redis_queued_locks.try_lock.exit__write_request_ahead]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "ahead_acq_id");
2760
- "[redis_queued_locks.try_lock.exit__read_request_ahead]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "ahead_acq_id");
2761
- "[redis_queued_locks.try_lock.exit__read_lock_still_obtained]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "first_acq_id_in_queue");
2988
+ "[redis_queued_locks.try_lock.exit__no_first]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "first_acq_id_in_queue", "first_priority_in_queue", "<current_lock_data>");
2989
+ "[redis_queued_locks.try_lock.exit__lock_still_obtained]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "first_acq_id_in_queue", "first_priority_in_queue", "locked_by_acq_id", "<current_lock_data>");
2990
+ "[redis_queued_locks.try_lock.exit__write_request_ahead]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "ahead_acq_id", "ahead_priority");
2991
+ "[redis_queued_locks.try_lock.exit__read_request_ahead]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "ahead_acq_id", "ahead_priority");
2992
+ "[redis_queued_locks.try_lock.exit__read_lock_still_obtained]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "first_acq_id_in_queue", "first_priority_in_queue");
2762
2993
  "[redis_queued_locks.try_lock.obtain__free_to_acquire]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2763
2994
  ```
2764
2995
 
2765
- - lock series logs (raised from `#lock_series`/`#lock_series!`, PoC; `rw_mode` is the mode of the series):
2996
+ - lock series logs (raised from `#lock_series`/`#lock_series!`; `rw_mode` is the mode of the series; with `log_lock_try` each
2997
+ attempt of the series writes the `try_lock.*` logs of the list above with its own `lock_key`, one log per lock of the series):
2766
2998
 
2767
2999
  ```ruby
2768
- "[redis_queued_locks.start_lock_series_obtaining]" # (logs "lock_keys", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2769
- "[redis_queued_locks.lock_series_obtained]" # (logs "lock_keys", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "acq_time");
2770
- "[redis_queued_locks.expire_lock_series]" # (logs "lock_keys", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
3000
+ "[redis_queued_locks.start_lock_series_obtaining]" # (logs "lock_keys", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority");
3001
+ "[redis_queued_locks.start_try_to_lock_series_cycle]" # (logs "lock_keys", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority");
3002
+ "[redis_queued_locks.dead_score_reached__reset_series_position]" # (logs "lock_keys", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority");
3003
+ "[redis_queued_locks.lock_series_intermediate_lock_obtained]" # (logs "lock_key", "lock_keys", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority", "acq_time");
3004
+ "[redis_queued_locks.lock_series_lock_obtained]" # (logs "lock_key", "lock_keys", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority", "acq_time");
3005
+ "[redis_queued_locks.lock_series_lock_obtaining_failed]" # (logs "lock_key", "lock_keys", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority", "acq_time", "result");
3006
+ "[redis_queued_locks.lock_series_obtained]" # (logs "lock_keys", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority", "acq_time", "reentrant_lock_keys");
3007
+ "[redis_queued_locks.lock_series_obtaining_failed]" # (logs "lock_keys", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority", "acq_time", "result", "failed_lock_key");
3008
+ "[redis_queued_locks.dequeue_from_lock_series_queues]" # (logs "lock_keys", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority");
3009
+ "[redis_queued_locks.expire_lock_series]" # (logs "lock_keys", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority");
3010
+ "[redis_queued_locks.decrease_lock_series]" # (logs "lock_keys", "decreased_ttl", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority");
2771
3011
  ```
2772
3012
 
2773
3013
  - operation logs (one log line per operation that changes Redis keys, with the time of the whole operation; the same data as
@@ -2808,19 +3048,27 @@ rql.lock('report') { rebuild_report } # read_write_mode: :write by default
2808
3048
  # - should implement `debug(progname = nil, &block)` (minimal requirement) or be an instance of Ruby's `::Logger` class/subclass;
2809
3049
  # - supports `SemanticLogger::Logger` (see "semantic_logger" gem)
2810
3050
  # - at this moment the only debug logs are realised in following cases:
2811
- # - "[redis_queued_locks.start_lock_obtaining]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2812
- # - "[redis_queued_locks.start_try_to_lock_cycle]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2813
- # - "[redis_queued_locks.dead_score_reached__reset_acquirer_position]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2814
- # - "[redis_queued_locks.lock_obtained]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "acq_time");
2815
- # - "[redis_queued_locks.lock_obtaining_failed]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "acq_time", "result");
2816
- # - "[redis_queued_locks.extendable_reentrant_lock_obtained]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "acq_time");
2817
- # - "[redis_queued_locks.reentrant_lock_obtained]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "acq_time");
3051
+ # - "[redis_queued_locks.start_lock_obtaining]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority");
3052
+ # - "[redis_queued_locks.start_try_to_lock_cycle]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority");
3053
+ # - "[redis_queued_locks.dead_score_reached__reset_acquirer_position]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority");
3054
+ # - "[redis_queued_locks.lock_obtained]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority", "acq_time");
3055
+ # - "[redis_queued_locks.lock_obtaining_failed]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority", "acq_time", "result");
3056
+ # - "[redis_queued_locks.extendable_reentrant_lock_obtained]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority", "acq_time");
3057
+ # - "[redis_queued_locks.reentrant_lock_obtained]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority", "acq_time");
2818
3058
  # - "[redis_queued_locks.fail_fast_or_limits_reached_or_deadlock__dequeue]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2819
3059
  # - "[redis_queued_locks.expire_lock]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2820
3060
  # - "[redis_queued_locks.decrease_lock]" (logs "lock_key", "decreased_ttl", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2821
- # - "[redis_queued_locks.start_lock_series_obtaining]" (logs "lock_keys", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2822
- # - "[redis_queued_locks.lock_series_obtained]" (logs "lock_keys", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "acq_time");
2823
- # - "[redis_queued_locks.expire_lock_series]" (logs "lock_keys", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
3061
+ # - "[redis_queued_locks.start_lock_series_obtaining]" (logs "lock_keys", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority");
3062
+ # - "[redis_queued_locks.start_try_to_lock_series_cycle]" (logs "lock_keys", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority");
3063
+ # - "[redis_queued_locks.dead_score_reached__reset_series_position]" (logs "lock_keys", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority");
3064
+ # - "[redis_queued_locks.lock_series_intermediate_lock_obtained]" (logs "lock_key", "lock_keys", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority", "acq_time");
3065
+ # - "[redis_queued_locks.lock_series_lock_obtained]" (logs "lock_key", "lock_keys", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority", "acq_time");
3066
+ # - "[redis_queued_locks.lock_series_lock_obtaining_failed]" (logs "lock_key", "lock_keys", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority", "acq_time", "result");
3067
+ # - "[redis_queued_locks.lock_series_obtained]" (logs "lock_keys", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority", "acq_time", "reentrant_lock_keys");
3068
+ # - "[redis_queued_locks.lock_series_obtaining_failed]" (logs "lock_keys", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority", "acq_time", "result", "failed_lock_key");
3069
+ # - "[redis_queued_locks.dequeue_from_lock_series_queues]" (logs "lock_keys", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority");
3070
+ # - "[redis_queued_locks.expire_lock_series]" (logs "lock_keys", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority");
3071
+ # - "[redis_queued_locks.decrease_lock_series]" (logs "lock_keys", "decreased_ttl", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority");
2824
3072
  # - "[redis_queued_locks.explicit_lock_release]" (logs "lock_key", "lock_key_queue", "rel_time", "at");
2825
3073
  # - "[redis_queued_locks.explicit_read_lock_release]" (logs "lock_key", "acq_id", "lock_res", "rel_time", "at");
2826
3074
  # - "[redis_queued_locks.explicit_all_locks_release]" (logs "at", "rel_time", "rel_key_cnt");
@@ -2845,15 +3093,16 @@ config['logger'] = RedisQueuedLocks::Logging::VoidLogger
2845
3093
  # - "[redis_queued_locks.try_lock.reentrant_lock__work_through]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "spc_status", "last_spc_ts");
2846
3094
  # - "[redis_queued_locks.try_lock.single_process_lock_conflict__dead_lock]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "spc_status", "last_spc_ts");
2847
3095
  # - "[redis_queued_locks.try_lock.single_process_lock_conflict__lock_upgrade]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "spc_status", "last_spc_ts");
2848
- # - "[redis_queued_locks.try_lock.acq_added_to_queue]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
3096
+ # - "[redis_queued_locks.try_lock.acq_added_to_queue]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "priority");
2849
3097
  # - "[redis_queued_locks.try_lock.remove_expired_acqs]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2850
- # - "[redis_queued_locks.try_lock.get_first_from_queue]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "first_acq_id_in_queue");
3098
+ # - "[redis_queued_locks.try_lock.dead_request_removed]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "dead_acq_id", "dead_priority");
3099
+ # - "[redis_queued_locks.try_lock.get_first_from_queue]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "first_acq_id_in_queue", "first_priority_in_queue");
2851
3100
  # - "[redis_queued_locks.try_lock.exit__queue_ttl_reached]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2852
- # - "[redis_queued_locks.try_lock.exit__no_first]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "first_acq_id_in_queue", "<current_lock_data>");
2853
- # - "[redis_queued_locks.try_lock.exit__lock_still_obtained]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "first_acq_id_in_queue", "locked_by_acq_id", "<current_lock_data>");
2854
- # - "[redis_queued_locks.try_lock.exit__write_request_ahead]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "ahead_acq_id");
2855
- # - "[redis_queued_locks.try_lock.exit__read_request_ahead]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "ahead_acq_id");
2856
- # - "[redis_queued_locks.try_lock.exit__read_lock_still_obtained]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "first_acq_id_in_queue");
3101
+ # - "[redis_queued_locks.try_lock.exit__no_first]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "first_acq_id_in_queue", "first_priority_in_queue", "<current_lock_data>");
3102
+ # - "[redis_queued_locks.try_lock.exit__lock_still_obtained]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "first_acq_id_in_queue", "first_priority_in_queue", "locked_by_acq_id", "<current_lock_data>");
3103
+ # - "[redis_queued_locks.try_lock.exit__write_request_ahead]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "ahead_acq_id", "ahead_priority");
3104
+ # - "[redis_queued_locks.try_lock.exit__read_request_ahead]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "ahead_acq_id", "ahead_priority");
3105
+ # - "[redis_queued_locks.try_lock.exit__read_lock_still_obtained]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "first_acq_id_in_queue", "first_priority_in_queue");
2857
3106
  # - "[redis_queued_locks.try_lock.obtain__free_to_acquire]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2858
3107
  # - "rw_mode" is the requested lock mode ('write'/'read');
2859
3108
  config['log_lock_try'] = false
@@ -2961,8 +3210,12 @@ List of instrumentation events
2961
3210
  - ["redis_queued_locks.reentrant_lock_obtained"](#redis_queued_locksreentrant_lock_obtained)
2962
3211
  - ["redis_queued_locks.lock_hold_and_release"](#redis_queued_lockslock_hold_and_release)
2963
3212
  - ["redis_queued_locks.reentrant_lock_hold_completes"](#redis_queued_locksreentrant_lock_hold_completes)
3213
+ - ["redis_queued_locks.lock_series_intermediate_lock_obtained"](#redis_queued_lockslock_series_intermediate_lock_obtained)
3214
+ - ["redis_queued_locks.lock_series_lock_obtained"](#redis_queued_lockslock_series_lock_obtained)
3215
+ - ["redis_queued_locks.lock_series_lock_obtaining_failed"](#redis_queued_lockslock_series_lock_obtaining_failed)
2964
3216
  - ["redis_queued_locks.lock_series_obtained"](#redis_queued_lockslock_series_obtained)
2965
3217
  - ["redis_queued_locks.lock_series_hold_and_release"](#redis_queued_lockslock_series_hold_and_release)
3218
+ - ["redis_queued_locks.lock_series_obtaining_failed"](#redis_queued_lockslock_series_obtaining_failed)
2966
3219
  - ["redis_queued_locks.explicit_lock_release"](#redis_queued_locksexplicit_lock_release)
2967
3220
  - ["redis_queued_locks.explicit_read_lock_release"](#redis_queued_locksexplicit_read_lock_release)
2968
3221
  - ["redis_queued_locks.explicit_all_locks_release"](#redis_queued_locksexplicit_all_locks_release)
@@ -2972,8 +3225,8 @@ List of instrumentation events
2972
3225
  - ["redis_queued_locks.reindex_locks"](#redis_queued_locksreindex_locks)
2973
3226
 
2974
3227
  What is instrumented (each event has a log line with the same name, see [Logging](#logging)):
2975
- - the lock acquirement (`#lock`/`#lock!`, `#lock_series`/`#lock_series!`): each meaningful outcome (the lock is obtained,
2976
- not obtained, held and released);
3228
+ - the lock acquirement (`#lock`/`#lock!`, `#lock_series`/`#lock_series!`): each meaningful outcome (the lock or the whole
3229
+ series is obtained, not obtained, held and released);
2977
3230
  - each operation that changes Redis keys (`#unlock`, `#unlock_read`, `#clear_locks`, `#clear_locks_of`/`#clear_current_locks`,
2978
3231
  `#extend_lock_ttl`, `#clear_dead_requests`, `#reindex_locks`): one event with the time of the whole operation;
2979
3232
  - lock information queries (`#lock_info`, `#locks`, `#queues`, `#keys`, ...) are not instrumented;
@@ -2991,6 +3244,7 @@ Detalized event semantics and payload structure:
2991
3244
  - `:hst_id` - `string` - lock's host identifier;
2992
3245
  - `:lock_key` - `string` - lock name;
2993
3246
  - `:rw_mode` - `symbol` - requested lock mode: `:write` or `:read` (see [Read/Write Locks](#readwrite-locks));
3247
+ - `:priority` - `integer` - lock request priority: from `-100` to `100` (a priority name is reported as its integer priority, see [Lock Request Prioritization](#lock-request-prioritization));
2994
3248
  - `:ts` - `numeric`/`epoch` - the time when the lock was obtaiend;
2995
3249
  - `:acq_time` - `float`/`milliseconds` - time spent on lock acquiring;
2996
3250
  - `:instrument` - `nil`/`Any` - custom data passed to the `#lock`/`#lock!` method as `:instrument` attribute;
@@ -2998,11 +3252,11 @@ Detalized event semantics and payload structure:
2998
3252
  ##### `"redis_queued_locks.lock_obtaining_failed"`
2999
3253
  - <sup>\[[back to the list](#instrumentation-events)\]</sup>
3000
3254
  - a moment when the lock acquirement is failed (the lock is not obtained);
3001
- - raised from `#lock`/`#lock!` (with `raise_errors: true` - before the error is raised) and from `#lock_series`/`#lock_series!`
3002
- (for the lock of the series that is not obtained);
3255
+ - raised from `#lock`/`#lock!` (with `raise_errors: true` - before the error is raised);
3003
3256
  - payload:
3004
3257
  - `:lock_key` - `string` - lock name;
3005
3258
  - `:rw_mode` - `symbol` - requested lock mode: `:write` or `:read` (see [Read/Write Locks](#readwrite-locks));
3259
+ - `:priority` - `integer` - lock request priority: from `-100` to `100` (a priority name is reported as its integer priority, see [Lock Request Prioritization](#lock-request-prioritization));
3006
3260
  - `:ttl` - `integer`/`milliseconds` - requested lock ttl;
3007
3261
  - `:acq_id` - `string` - lock acquirer identifier;
3008
3262
  - `:hst_id` - `string` - lock's host identifier;
@@ -3020,6 +3274,7 @@ Detalized event semantics and payload structure:
3020
3274
  - `:lock_key` - `string` - lock name;
3021
3275
  - `:rw_mode` - `symbol` - requested lock mode: `:write` or `:read` (see [Read/Write Locks](#readwrite-locks));
3022
3276
  a read lock request under the write lock of the same acquirer works through the write lock (`:rw_mode` is `:read`);
3277
+ - `:priority` - `integer` - requested lock request priority: from `-100` to `100` (a priority name is reported as its integer priority; a reentrant lock request is not queued, see [Lock Request Prioritization](#lock-request-prioritization));
3023
3278
  - `:ttl` - `integer`/`milliseconds` - last lock ttl by reentrant locking;
3024
3279
  - `:acq_id` - `string` - lock acquirer identifier;
3025
3280
  - `:hst_id` - `string` - lock's host identifier;
@@ -3035,6 +3290,7 @@ Detalized event semantics and payload structure:
3035
3290
  - `:lock_key` - `string` - lock name;
3036
3291
  - `:rw_mode` - `symbol` - requested lock mode: `:write` or `:read` (see [Read/Write Locks](#readwrite-locks));
3037
3292
  a read lock request under the write lock of the same acquirer works through the write lock (`:rw_mode` is `:read`);
3293
+ - `:priority` - `integer` - requested lock request priority: from `-100` to `100` (a priority name is reported as its integer priority; a reentrant lock request is not queued, see [Lock Request Prioritization](#lock-request-prioritization));
3038
3294
  - `:ttl` - `integer`/`milliseconds` - last lock ttl by reentrant locking;
3039
3295
  - `:acq_id` - `string` - lock acquirer identifier;
3040
3296
  - `:hst_id` - `string` - lock's host identifier;
@@ -3053,6 +3309,7 @@ Detalized event semantics and payload structure:
3053
3309
  - `:hst_id` - `string` - lock's host identifier;
3054
3310
  - `:lock_key` - `string` - lock name;
3055
3311
  - `:rw_mode` - `symbol` - requested lock mode: `:write` or `:read` (see [Read/Write Locks](#readwrite-locks));
3312
+ - `:priority` - `integer` - lock request priority: from `-100` to `100` (a priority name is reported as its integer priority, see [Lock Request Prioritization](#lock-request-prioritization));
3056
3313
  - `:ts` - `numeric`/`epoch` - the time when lock was obtained;
3057
3314
  - `:acq_time` - `float`/`milliseconds` - time spent on lock acquiring;
3058
3315
  - `:instrument` - `nil`/`Any` - custom data passed to the `#lock`/`#lock!` method as `:instrument` attribute;
@@ -3071,37 +3328,114 @@ Detalized event semantics and payload structure:
3071
3328
  - `:lock_key` - `string` - lock name;
3072
3329
  - `:rw_mode` - `symbol` - requested lock mode: `:write` or `:read` (see [Read/Write Locks](#readwrite-locks));
3073
3330
  a read lock request under the write lock of the same acquirer works through the write lock (`:rw_mode` is `:read`);
3331
+ - `:priority` - `integer` - requested lock request priority: from `-100` to `100` (a priority name is reported as its integer priority; a reentrant lock request is not queued, see [Lock Request Prioritization](#lock-request-prioritization));
3074
3332
  - `:acq_time` - `float`/`milliseconds` - time spent on lock acquiring;
3075
3333
  - `:instrument` - `nil`/`Any` - custom data passed to the `#lock`/`#lock!` method as `:instrument` attribute;
3076
3334
 
3335
+ ##### `"redis_queued_locks.lock_series_intermediate_lock_obtained"`
3336
+ - <sup>\[[back to the list](#instrumentation-events)\]</sup>
3337
+ - intermediate obtaining: a moment when the lock of the series passed all checks of an attempt of the series (the lock is free and the lock
3338
+ request of the series is next). Nothing is held yet: the series can still wait for its other locks or fail;
3339
+ - raised from `#lock_series`/`#lock_series!` once per lock of the series (not per attempt), in the order of the lock names up to the lock
3340
+ that blocks the series; reentrant locks of the series are not reported;
3341
+ - payload:
3342
+ - `:lock_key` - `string` - the lock of the series;
3343
+ - `:lock_keys` - `array<string>` - all locks of the series;
3344
+ - `:rw_mode` - `symbol` - the mode of the series: `:write` or `:read` (see [Read/Write Locks](#readwrite-locks));
3345
+ - `:priority` - `integer` - the lock request priority of the series (`priority`): from `-100` to `100` (a priority name is reported as its integer priority, see [Lock Request Prioritization](#lock-request-prioritization));
3346
+ - `:ttl` - `integer`/`milliseconds` - the ttl of each lock of the series;
3347
+ - `:acq_id` - `string` - lock acquirer identifier;
3348
+ - `:hst_id` - `string` - lock's host identifier;
3349
+ - `:ts` - `float`/`epoch` - the time of the event;
3350
+ - `:acq_time` - `float`/`milliseconds` - time spent on the series acquiring up to this moment;
3351
+ - `:instrument` - `nil`/`Any` - custom data passed to the `#lock_series`/`#lock_series!` method as `:instrument` attribute;
3352
+
3353
+ ##### `"redis_queued_locks.lock_series_lock_obtained"`
3354
+ - <sup>\[[back to the list](#instrumentation-events)\]</sup>
3355
+ - full obtaining: a moment when the lock of the series was really obtained (all locks of the series are held);
3356
+ - raised from `#lock_series`/`#lock_series!` for each lock obtained by the series, right before `lock_series_obtained`; reentrant locks of
3357
+ the series are not reported (see `:reentrant_lock_keys` of `lock_series_obtained`);
3358
+ - payload:
3359
+ - `:lock_key` - `string` - the lock of the series;
3360
+ - `:lock_keys` - `array<string>` - all locks of the series;
3361
+ - `:rw_mode` - `symbol` - the mode of the series: `:write` or `:read` (see [Read/Write Locks](#readwrite-locks));
3362
+ - `:priority` - `integer` - the lock request priority of the series (`priority`): from `-100` to `100` (a priority name is reported as its integer priority, see [Lock Request Prioritization](#lock-request-prioritization));
3363
+ - `:ttl` - `integer`/`milliseconds` - the ttl of each lock of the series;
3364
+ - `:acq_id` - `string` - lock acquirer identifier;
3365
+ - `:hst_id` - `string` - lock's host identifier;
3366
+ - `:ts` - `float`/`epoch` - the time when the series was obtained;
3367
+ - `:acq_time` - `float`/`milliseconds` - time spent on the series acquiring;
3368
+ - `:instrument` - `nil`/`Any` - custom data passed to the `#lock_series`/`#lock_series!` method as `:instrument` attribute;
3369
+
3370
+ ##### `"redis_queued_locks.lock_series_lock_obtaining_failed"`
3371
+ - <sup>\[[back to the list](#instrumentation-events)\]</sup>
3372
+ - a moment when the series acquirement is failed: the lock of the series that blocked it;
3373
+ - raised from `#lock_series`/`#lock_series!` once per failed series (with `raise_errors: true` - before the error is raised), right before
3374
+ `lock_series_obtaining_failed`; it is not raised when the blocking lock is unknown (no attempt found it);
3375
+ - payload:
3376
+ - `:lock_key` - `string` - the lock that blocked the series;
3377
+ - `:lock_keys` - `array<string>` - all locks of the series;
3378
+ - `:rw_mode` - `symbol` - the mode of the series: `:write` or `:read` (see [Read/Write Locks](#readwrite-locks));
3379
+ - `:priority` - `integer` - the lock request priority of the series (`priority`): from `-100` to `100` (a priority name is reported as its integer priority, see [Lock Request Prioritization](#lock-request-prioritization));
3380
+ - `:ttl` - `integer`/`milliseconds` - the ttl of each lock of the series;
3381
+ - `:acq_id` - `string` - lock acquirer identifier;
3382
+ - `:hst_id` - `string` - lock's host identifier;
3383
+ - `:ts` - `float`/`epoch` - the time when the series acquirement was failed;
3384
+ - `:acq_time` - `float`/`milliseconds` - time spent on the series acquiring;
3385
+ - `:result` - `symbol` - the failure result (see `lock_series_obtaining_failed`);
3386
+ - `:instrument` - `nil`/`Any` - custom data passed to the `#lock_series`/`#lock_series!` method as `:instrument` attribute;
3387
+
3077
3388
  ##### `"redis_queued_locks.lock_series_obtained"`
3078
3389
  - <sup>\[[back to the list](#instrumentation-events)\]</sup>
3079
- - (PoC) a moment when all locks of the series were obtained;
3390
+ - a moment when all locks of the series were obtained;
3080
3391
  - raised from `#lock_series`/`#lock_series!`;
3081
3392
  - payload:
3082
3393
  - `:lock_keys` - `array<string>` - lock names of the series;
3083
- - `:rw_mode` - `symbol` - the mode of the series (`read_write_mode`): `:write` or `:read` (see [Read/Write Locks](#readwrite-locks));
3084
- - `:ttl` - `integer`/`milliseconds` - the `ttl` option of the series (each lock of the series gets its own ttl,
3085
- see [lock_series](#lock_series---poc-acquire-a-series-of-locks));
3394
+ - `:rw_mode` - `symbol` - the mode of the series: `:write` or `:read` (see [Read/Write Locks](#readwrite-locks));
3395
+ - `:priority` - `integer` - the lock request priority of the series (`priority`): from `-100` to `100` (a priority name is reported as its integer priority, see [Lock Request Prioritization](#lock-request-prioritization));
3396
+ - `:ttl` - `integer`/`milliseconds` - the ttl of each lock of the series (see [lock_series](#lock_series---acquire-a-series-of-locks));
3086
3397
  - `:acq_id` - `string` - lock acquirer identifier;
3087
3398
  - `:hst_id` - `string` - lock's host identifier;
3088
3399
  - `:ts` - `numeric`/`epoch` - the time when the series was obtained;
3089
3400
  - `:acq_time` - `float`/`milliseconds` - time spent on the series acquiring;
3401
+ - `:reentrant_lock_keys` - `array<string>` - the locks of the series that are obtained by your outer logic (reentrant locks:
3402
+ they are not obtained and not released by the series);
3090
3403
  - `:instrument` - `nil`/`Any` - custom data passed to the `#lock_series`/`#lock_series!` method as `:instrument` attribute;
3091
3404
 
3092
3405
  ##### `"redis_queued_locks.lock_series_hold_and_release"`
3093
3406
  - <sup>\[[back to the list](#instrumentation-events)\]</sup>
3094
- - (PoC) an event signalizes about the "hold+and+release" process of the series is finished;
3407
+ - an event signalizes about the "hold+and+release" process of the series is finished;
3095
3408
  - raised from `#lock_series`/`#lock_series!` when invoked with a block of code;
3096
3409
  - payload:
3097
3410
  - `:lock_keys` - `array<string>` - lock names of the series;
3098
- - `:rw_mode` - `symbol` - the mode of the series (`read_write_mode`): `:write` or `:read` (see [Read/Write Locks](#readwrite-locks));
3411
+ - `:rw_mode` - `symbol` - the mode of the series: `:write` or `:read` (see [Read/Write Locks](#readwrite-locks));
3412
+ - `:priority` - `integer` - the lock request priority of the series (`priority`): from `-100` to `100` (a priority name is reported as its integer priority, see [Lock Request Prioritization](#lock-request-prioritization));
3099
3413
  - `:hold_time` - `float`/`milliseconds` - the series hold time;
3100
- - `:ttl` - `integer`/`milliseconds` - the `ttl` option of the series;
3414
+ - `:ttl` - `integer`/`milliseconds` - the ttl of each lock of the series;
3101
3415
  - `:acq_id` - `string` - lock acquirer identifier;
3102
3416
  - `:hst_id` - `string` - lock's host identifier;
3103
3417
  - `:ts` - `float`/`epoch` - the time when the series was released;
3104
3418
  - `:acq_time` - `float`/`milliseconds` - time spent on the series acquiring;
3419
+ - `:reentrant_lock_keys` - `array<string>` - the locks of the series that are obtained by your outer logic (reentrant locks:
3420
+ they are not obtained and not released by the series);
3421
+ - `:instrument` - `nil`/`Any` - custom data passed to the `#lock_series`/`#lock_series!` method as `:instrument` attribute;
3422
+
3423
+ ##### `"redis_queued_locks.lock_series_obtaining_failed"`
3424
+ - <sup>\[[back to the list](#instrumentation-events)\]</sup>
3425
+ - a moment when the series acquirement is failed (no lock of the series is obtained);
3426
+ - raised from `#lock_series`/`#lock_series!` (with `raise_errors: true` - before the error is raised);
3427
+ - payload:
3428
+ - `:lock_keys` - `array<string>` - lock names of the series;
3429
+ - `:rw_mode` - `symbol` - the mode of the series: `:write` or `:read` (see [Read/Write Locks](#readwrite-locks));
3430
+ - `:priority` - `integer` - the lock request priority of the series (`priority`): from `-100` to `100` (a priority name is reported as its integer priority, see [Lock Request Prioritization](#lock-request-prioritization));
3431
+ - `:ttl` - `integer`/`milliseconds` - the ttl of each lock of the series;
3432
+ - `:acq_id` - `string` - lock acquirer identifier;
3433
+ - `:hst_id` - `string` - lock's host identifier;
3434
+ - `:ts` - `float`/`epoch` - the time when the series acquirement was failed;
3435
+ - `:acq_time` - `float`/`milliseconds` - time spent on the series acquiring;
3436
+ - `:result` - `symbol` - the failure result: `:timeout_reached`, `:retry_limit_reached`, `:fail_fast_no_try`, `:fail_fast_after_try`, `:conflict_dead_lock`, `:conflict_lock_upgrade`;
3437
+ - `:failed_lock_key` - `string`/`nil` - the first lock that blocked the last completed attempt of the series (`nil`: unknown:
3438
+ no attempt was completed or some lock state was changed during the attempt);
3105
3439
  - `:instrument` - `nil`/`Any` - custom data passed to the `#lock_series`/`#lock_series!` method as `:instrument` attribute;
3106
3440
 
3107
3441
  ##### `"redis_queued_locks.explicit_lock_release"`
@@ -3203,13 +3537,7 @@ Detalized event semantics and payload structure:
3203
3537
  - isolated timeouts which are independent of internal Ruby's timeout implementation (where all timeouts are hostend inside
3204
3538
  the global "timeout request" queue and managed by a single global "timeout wathcer" thread). it should prevent any logic and timeout intersections,
3205
3539
  some GVL-related things and problem situations when the global watcher thread is "dead";
3206
- - lock request prioritization;
3207
3540
  - **strict redlock algorithm support** (support for many `RedisClient` instances that are fully independent (distributed redis instances));
3208
- - #lock_series` - acquire a series of locks:
3209
- - (NOTE: "Proof of Concept" implementation is ready to use);
3210
- ```ruby
3211
- rql.lock_series('lock_a', 'lock_b', 'lock_c') { puts 'locked' }
3212
- ```
3213
3541
  - an ability to release all locks and all requests of the concrete acquirer id or host id (or both in validation-orianted combination);
3214
3542
  - detailed lock informotion inside the error object in cases of exceptions (at the moment we have this info inside the error message only that hard to analyze in work);
3215
3543
  - a convenient way to mark any `lock` invocation as "non-instrumentable" / "non-loggable" (as an alternative to `VoidNotifier` and to `VoidLogger`);
@@ -3221,6 +3549,7 @@ Detalized event semantics and payload structure:
3221
3549
  Mutex (instantiated inside the main Ractor) from the our non-main Ractor (swar-mode works under the non-main ractor) and this process fails\
3222
3550
  on non-sharable memory error);
3223
3551
  - **Minor**:
3552
+ - `*_status` aliases for `*_info` methods;
3224
3553
  - an ability to return all insturmentation metrics from the `lock` invocation (and after block `yield`ing);
3225
3554
  - think about a PG-like `stats`-data/table, that should be helpful for lock history acquirement analyzation :thinking: (suitable for cases when we want
3226
3555
  to check if the lock was acquired by some different parallel process durign the some period of time in the past or not);
@@ -3257,7 +3586,7 @@ Detalized event semantics and payload structure:
3257
3586
  - add a library-level exception, when RQL-related key in Redis (required for its logic) has incompatible type (means: some other program uses our key with their own type and logic and RQL can't work properly);
3258
3587
  - yardoc docs with CI check (full doc coverage check);
3259
3588
  - split *exception* inheritance to the groups: `lock obtaining errors`, `block invocation errors`, `swarm errors`, and other groups (research possible groups):
3260
- - in some cases we need to intercept "Lock Obtaining Process Erros", in other cases: "Block Invocation Errors", and so on (in `Lock Serirs PoC` for example);
3589
+ - in some cases we need to intercept "Lock Obtaining Process Erros", in other cases: "Block Invocation Errors", and so on;
3261
3590
  - think about `#debounce` and `#throttle` mechanisms;
3262
3591
 
3263
3592
  ---
@@ -3269,6 +3598,17 @@ Detalized event semantics and payload structure:
3269
3598
  bundle exec rake rspec
3270
3599
  ```
3271
3600
 
3601
+ - Tests in parallel processes (`RSpec`, ~6x faster): each process (worker) works with its own pair of Redis databases
3602
+ (the specs use db 0 and db 1), so `WORKERS=n` flushes db `0..2n-1` of the local Redis (the default number of workers:
3603
+ the Redis `databases` / 2, up to the CPU count); the examples are distributed by their durations of the previous runs
3604
+ (`tmp/rspec_parallel/durations.json`), the output of each worker is in `tmp/rspec_parallel/worker-<n>.log`, the coverage
3605
+ report of the whole run is collated into `coverage/` after all workers:
3606
+ ```shell
3607
+ bundle exec rake rspec:parallel
3608
+ WORKERS=4 bundle exec rake rspec:parallel
3609
+ bundle exec rake "rspec:parallel[spec/redis_queued_locks_spec.rb]" # spec files/filters
3610
+ ```
3611
+
3272
3612
  - Linting (`Rubocop`+`RBS`):
3273
3613
  ```shell
3274
3614
  bundle exec rake rubocop
@@ -3281,7 +3621,8 @@ bundle exec rake steep:check
3281
3621
 
3282
3622
  - `Runtime` TypeChecking (`RBS`):
3283
3623
  ```shell
3284
- bundle exec rbs collection install && RBS_TEST_RAISE=true RUBYOPT='-rrbs/test/setup' RBS_TEST_OPT='-I sig' RBS_TEST_LOGLEVEL=error RBS_TEST_TARGET='RedisQueuedLocks::*' bundle exec rspec --failure-exit-code=0
3624
+ bundle exec rbs collection install && RBS_TEST_RAISE=true RUBYOPT='-rbundler/setup -rrbs/test/setup' RBS_TEST_OPT='-I sig' RBS_TEST_LOGLEVEL=error RBS_TEST_TARGET='RedisQueuedLocks::*' RBS_TEST_SKIP='::RedisQueuedLocks::Swarm::FlushZombies,::RedisQueuedLocks::Swarm::SwarmElement::Isolated,::RedisQueuedLocks::Swarm::RedisClientBuilder' bundle exec rspec
3625
+ # or in parallel processes: the same environment with `bundle exec rake rspec:parallel` instead of `bundle exec rspec`
3285
3626
  ```
3286
3627
 
3287
3628
  ---