redis_queued_locks 1.17.0 → 1.19.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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/project-overview.md +87 -10
  3. data/.claude/rules/acquirer.md +31 -11
  4. data/.claude/rules/arguments.md +2 -1
  5. data/.claude/rules/git.md +46 -0
  6. data/.claude/rules/logic.md +9 -3
  7. data/.claude/rules/swarm.md +4 -3
  8. data/.claude/rules/tests.md +9 -3
  9. data/.claude/rules/type-checking.md +10 -2
  10. data/.claude/rules/visitors.md +12 -7
  11. data/CHANGELOG.md +68 -0
  12. data/CLAUDE.md +7 -2
  13. data/README.md +427 -79
  14. data/lib/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue/log_visitor.rb +6 -3
  15. data/lib/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue.rb +9 -7
  16. data/lib/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rb +20 -10
  17. data/lib/redis_queued_locks/acquirer/acquire_lock/log_visitor.rb +34 -16
  18. data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock/log_visitor.rb +240 -28
  19. data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rb +398 -129
  20. data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire/log_visitor.rb +12 -6
  21. data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire.rb +32 -8
  22. data/lib/redis_queued_locks/acquirer/acquire_lock.rb +73 -20
  23. data/lib/redis_queued_locks/acquirer/clear_dead_requests.rb +9 -1
  24. data/lib/redis_queued_locks/acquirer/extend_lock_ttl.rb +182 -3
  25. data/lib/redis_queued_locks/acquirer/is_locked.rb +19 -2
  26. data/lib/redis_queued_locks/acquirer/is_queued.rb +6 -2
  27. data/lib/redis_queued_locks/acquirer/lock_info.rb +96 -6
  28. data/lib/redis_queued_locks/acquirer/lock_series_poc/instr_visitor.rb +8 -2
  29. data/lib/redis_queued_locks/acquirer/lock_series_poc/log_visitor.rb +18 -6
  30. data/lib/redis_queued_locks/acquirer/lock_series_poc.rb +126 -27
  31. data/lib/redis_queued_locks/acquirer/locks.rb +103 -6
  32. data/lib/redis_queued_locks/acquirer/queue_info.rb +38 -9
  33. data/lib/redis_queued_locks/acquirer/queues.rb +28 -4
  34. data/lib/redis_queued_locks/acquirer/release_all_locks.rb +35 -1
  35. data/lib/redis_queued_locks/acquirer/release_lock.rb +40 -9
  36. data/lib/redis_queued_locks/acquirer/release_locks_of.rb +43 -3
  37. data/lib/redis_queued_locks/acquirer/release_read_lock.rb +143 -0
  38. data/lib/redis_queued_locks/acquirer.rb +1 -0
  39. data/lib/redis_queued_locks/client.rb +143 -14
  40. data/lib/redis_queued_locks/resource.rb +117 -9
  41. data/lib/redis_queued_locks/swarm/flush_zombies.rb +38 -0
  42. data/lib/redis_queued_locks/swarm/zombie_info.rb +42 -0
  43. data/lib/redis_queued_locks/version.rb +2 -2
  44. data/rbs_collection.lock.yaml +10 -2
  45. data/rbs_collection.yaml +0 -2
  46. data/sig/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue/log_visitor.rbs +2 -1
  47. data/sig/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue.rbs +0 -1
  48. data/sig/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rbs +5 -0
  49. data/sig/redis_queued_locks/acquirer/acquire_lock/log_visitor.rbs +12 -6
  50. data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock/log_visitor.rbs +74 -7
  51. data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rbs +4 -2
  52. data/sig/redis_queued_locks/acquirer/acquire_lock/yield_expire.rbs +3 -0
  53. data/sig/redis_queued_locks/acquirer/acquire_lock/yield_with_expire/log_visitor.rbs +4 -2
  54. data/sig/redis_queued_locks/acquirer/extend_lock_ttl.rbs +19 -1
  55. data/sig/redis_queued_locks/acquirer/lock_info.rbs +9 -1
  56. data/sig/redis_queued_locks/acquirer/locks.rbs +10 -1
  57. data/sig/redis_queued_locks/acquirer/queue_info.rbs +2 -4
  58. data/sig/redis_queued_locks/acquirer/queues.rbs +1 -1
  59. data/sig/redis_queued_locks/acquirer/release_lock.rbs +8 -1
  60. data/sig/redis_queued_locks/acquirer/release_read_lock.rbs +44 -0
  61. data/sig/redis_queued_locks/client.rbs +23 -2
  62. data/sig/redis_queued_locks/resource.rbs +9 -2
  63. data/sig/redis_queued_locks/swarm/zombie_info.rbs +5 -0
  64. metadata +4 -1
data/README.md CHANGED
@@ -8,7 +8,9 @@ Each lock request is put into the request queue (each lock is hosted by its own
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
- Provides flexible invocation flow, parametrized limits (lock request ttl, lock ttl, queue ttl, lock attempts limit, fast failing, etc), **zombie locks elimination**, support for **reentrant locks**, logging and instrumentation (and much more).
11
+ Supports `Read`/`Write` locks and `Reentrant` locks.
12
+
13
+ Provides flexible invocation flow, parametrized limits (lock request ttl, lock ttl, queue ttl, lock attempts limit, fast failing, etc), **zombie locks elimination**, logging and instrumentation (and much more).
12
14
 
13
15
  ---
14
16
 
@@ -34,6 +36,7 @@ Provides flexible invocation flow, parametrized limits (lock request ttl, lock t
34
36
  - [locked?](#locked)
35
37
  - [queued?](#queued)
36
38
  - [unlock](#unlock---release-a-lock) (aka `release_lock`)
39
+ - [unlock_read](#unlock_read---release-the-read-lock-of-the-current-acquirer) (aka `release_read_lock`)
37
40
  - [clear_locks](#clear_locks---release-all-locks-and-lock-queues) (aka `release_locks`)
38
41
  - [clear_locks_of](#clear_locks_of) (aka `release_locks_of`)
39
42
  - [clear_current_locks](#clear_current_locks) (aka `release_current_locks`)
@@ -64,18 +67,22 @@ Provides flexible invocation flow, parametrized limits (lock request ttl, lock t
64
67
  - [Lock Access Strategies](#lock-access-strategies)
65
68
  - [queued](#lock-access-strategies)
66
69
  - [random](#lock-access-strategies)
70
+ - [Read/Write Locks](#readwrite-locks)
67
71
  - [Deadlocks and Reentrant locks](#deadlocks-and-reentrant-locks)
68
72
  - [Logging](#logging)
69
73
  - [Logging Configuration](#logging-configuration)
70
74
  - [Instrumentation](#instrumentation)
71
75
  - [Instrumentation Configuration](#instrumentation-configuration)
72
76
  - [Instrumentation Events](#instrumentation-events)
73
- - ["redis_queued_locks.lock_obtained"](#redis_queued_lockslock_hold_and_release)
77
+ - ["redis_queued_locks.lock_obtained"](#redis_queued_lockslock_obtained)
74
78
  - ["redis_queued_locks.extendable_reentrant_lock_obtained"](#redis_queued_locksextendable_reentrant_lock_obtained)
75
79
  - ["redis_queued_locks.reentrant_lock_obtained"](#redis_queued_locksreentrant_lock_obtained)
76
80
  - ["redis_queued_locks.lock_hold_and_release"](#redis_queued_lockslock_hold_and_release)
77
81
  - ["redis_queued_locks.reentrant_lock_hold_completes"](#redis_queued_locksreentrant_lock_hold_completes)
82
+ - ["redis_queued_locks.lock_series_obtained"](#redis_queued_lockslock_series_obtained)
83
+ - ["redis_queued_locks.lock_series_hold_and_release"](#redis_queued_lockslock_series_hold_and_release)
78
84
  - ["redis_queued_locks.explicit_lock_release"](#redis_queued_locksexplicit_lock_release)
85
+ - ["redis_queued_locks.explicit_read_lock_release"](#redis_queued_locksexplicit_read_lock_release)
79
86
  - ["redis_queued_locks.explicit_all_locks_release"](#redis_queued_locksexplicit_all_locks_release)
80
87
  - ["redis_queued_locks.release_locks_of"](#redis_queued_locksrelease_locks_of)
81
88
  - [Roadmap](#roadmap)
@@ -422,6 +429,7 @@ def lock(
422
429
  raise_errors: false,
423
430
  fail_fast: false,
424
431
  conflict_strategy: config['default_conflict_strategy'],
432
+ read_write_mode: :write,
425
433
  access_strategy: config['default_access_strategy'],
426
434
  identity: uniq_identity, # (attr_accessor) calculated during client instantiation via config['uniq_identifier'] proc;
427
435
  meta: nil,
@@ -502,6 +510,13 @@ def lock(
502
510
  - `:wait_for_lock` - (default) - work in classic way (with timeouts, retry delays, retry limits, etc - in classic way :));
503
511
  - `:dead_locking` - fail with deadlock exception;
504
512
  - See [Deadlocks and Reentrant locks](#deadlocks-and-reentrant-locks) documentation section for details;
513
+ - `read_write_mode` - (optional) - `[Symbol]`
514
+ - The lock mode (read/write lock semantics);
515
+ - `:write` by default;
516
+ - Modes:
517
+ - `:write` - exclusive lock: waits for the write lock and for all read locks;
518
+ - `:read` - shared lock: waits for the write lock only (read locks do not block each other);
519
+ - See [Read/Write Locks](#readwrite-locks) documentation section for details;
505
520
  - `identity` - (optional) `[String]`
506
521
  - An unique string that is unique per `RedisQueuedLock::Client` instance. Resolves the
507
522
  collisions between the same process_id/thread_id/fiber_id/ractor_id identifiers on different
@@ -511,7 +526,7 @@ def lock(
511
526
  - Identity calculator is pre-configured in `config['uniq_identifier']`;
512
527
  - `meta` - (optional) `[NilClass,Hash<String|Symbol,Any>]`
513
528
  - A custom metadata wich will be passed to the lock data in addition to the existing data;
514
- - Custom metadata can not contain reserved lock data keys (such as `lock_key`, `acq_id`, `ts`, `ini_ttl`, `rem_ttl`);
529
+ - Custom metadata can not contain reserved lock data keys (such as `lock_key`, `acq_id`, `ts`, `ini_ttl`, `rem_ttl`, `rw_mode`);
515
530
  - `nil` by default (means "no metadata");
516
531
  - `detailed_acq_timeout_error` - (optional) `[Boolean]`
517
532
  - When the lock acquirement try reached the acquirement time limit (:timeout option) the
@@ -842,6 +857,7 @@ def lock!(
842
857
  instrumenter: config['instrumenter'],
843
858
  access_strategy: config['default_access_strategy'],
844
859
  conflict_strategy: config['default_conflict_strategy'],
860
+ read_write_mode: :write,
845
861
  log_sampling_enabled: config['log_sampling_enabled'],
846
862
  log_sampling_percent: config['log_sampling_percent'],
847
863
  log_sampler: config['log_sampler'],
@@ -882,6 +898,12 @@ applied with the same value to the each lock acquirement process (for each lock
882
898
  - `"y"` - 10 seconds
883
899
  - `"z"` - 5 seconds
884
900
  - when your block of code has completed their execution - all locks will be released immediatly;
901
+ - when the series fails (any lock can not be obtained, with or without an exception) - the locks that are already
902
+ obtained by the series are released immediatly (`acquired_locks` of the failure result lists them);
903
+ - the series releases only locks obtained by itself and only if they are still owned by the current acquirer:
904
+ - reentrant locks (already obtained by your outer logic, `conflict_strategy: :work_through`/`:extendable_work_through`) are kept;
905
+ - a lock that is expired during your block and obtained by another acquirer is kept;
906
+ - `read_write_mode:` is the same for all locks of the series (see [Read/Write Locks](#readwrite-locks));
885
907
  - if you did not pass a block - all locks will be released with their forumla-based TTL described above;
886
908
  - **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`);
887
909
 
@@ -946,19 +968,25 @@ client.lock_seires('c', 'a', 'b') # ... `a` is still obtained
946
968
 
947
969
  ###### New instrumentaiton events:
948
970
 
971
+ - `rw_mode` is the mode of the series (`read_write_mode`, the same for all locks of the series);
972
+ - see [Instrumentation Events](#instrumentation-events) for the payload details;
973
+
949
974
  ```ruby
950
975
  # NEW instrumentation events (examples)
951
- "redis_queued_locks.lock_series_obtained" => # {lock_keys: ["rql:lock:s", "rql:lock:t", "rql:lock:u"], ttl: 5000, acq_id: "rql:acq:4486/1696/1704/1712/e6fd0da7991e4303", hst_id: "rql:hst:4486/1696/1712/e6fd0da7991e4303", ts: "2026-01-28 22:05:06 +0300", acq_time: 2.61, instrument: nil}
952
- "redis_queued_locks.lock_series_hold_and_release" => # {lock_keys: ["rql:lock:x", "rql:lock:y", "rql:lock:z"], 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}
976
+ "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}
977
+ "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}
953
978
  ```
954
979
 
955
980
  ###### New logs:
956
981
 
982
+ - `rw_mode` is the mode of the series (`read_write_mode`, the same for all locks of the series);
983
+ - see [Logging](#logging) for the logged keys;
984
+
957
985
  ```shell
958
986
  # NEW logs (examples)
959
- [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'
960
- [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' acq_time => 2.55 (ms)
961
- [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'
987
+ [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'
988
+ [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)
989
+ [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'
962
990
  ```
963
991
 
964
992
  ----
@@ -985,6 +1013,7 @@ client.lock_series!("x", "y", "z") { ...some_code... }
985
1013
  - returns `nil` if lock does not exist;
986
1014
  - lock data (`Hash<String,String|Integer>`):
987
1015
  - `"lock_key"` - `string` - lock key in redis;
1016
+ - `"rw_mode"` - `string` - lock mode: `"write"` (or `"read"` for read locks info, see [Read/Write Locks](#readwrite-locks));
988
1017
  - `"acq_id"` - `string` - acquirer identifier (process_id/thread_id/fiber_id/ractor_id/identity);
989
1018
  - `"hst_id"` - `string` - host identifier (process_id/thread_id/ractor_id/identity);
990
1019
  - `"ts"` - `numeric`/`epoch` - the time when lock was obtained;
@@ -1008,6 +1037,7 @@ rql.lock_info("your_lock_name")
1008
1037
  # =>
1009
1038
  {
1010
1039
  "lock_key" => "rql:lock:your_lock_name",
1040
+ "rw_mode" => "write",
1011
1041
  "acq_id" => "rql:acq:123/456/567/678/374dd74324",
1012
1042
  "hst_id" => "rql:acq:123/456/678/374dd74324",
1013
1043
  "ts" => 123456789.12345,
@@ -1024,6 +1054,7 @@ rql.lock_info("your_lock_name")
1024
1054
  # =>
1025
1055
  {
1026
1056
  "lock_key" => "rql:lock:your_lock_name",
1057
+ "rw_mode" => "write",
1027
1058
  "acq_id" => "rql:acq:123/456/567/678/374dd74324",
1028
1059
  "hst_id" => "rql:acq:123/456/678/374dd74324",
1029
1060
  "ts" => 123456789.12345,
@@ -1046,6 +1077,7 @@ rql.lock_info("your_lock_name")
1046
1077
  # =>
1047
1078
  {
1048
1079
  "lock_key" => "rql:lock:your_lock_name",
1080
+ "rw_mode" => "write",
1049
1081
  "acq_id" => "rql:acq:123/456/567/678/374dd74324",
1050
1082
  "hst_id" => "rql:acq:123/456/678/374dd74324",
1051
1083
  "ts" => 123456789.12345,
@@ -1062,6 +1094,64 @@ rql.lock_info("your_lock_name")
1062
1094
  }
1063
1095
  ```
1064
1096
 
1097
+ ```ruby
1098
+ # <for read/write locks>
1099
+ # (see `read_write_mode:` kwarg attribute of #lock/#lock! methods and "Read/Write Locks" documentation section)
1100
+ # (lock requests are not included: use #queue_info to see the queued readers and writers)
1101
+
1102
+ # -> case 1: the lock is held by readers, writers and readers are waiting in the lock queues;
1103
+
1104
+ # process#1 and process#2 (readers, work in parallel):
1105
+ rql.lock("your_lock_name", read_write_mode: :read, ttl: 5_000, meta: { "kek" => "pek" }) { ... }
1106
+ rql.lock("your_lock_name", read_write_mode: :read, ttl: 3_000) { ... }
1107
+ # process#3 (writer, waits for all readers in the write lock queue):
1108
+ rql.lock("your_lock_name", read_write_mode: :write) { ... }
1109
+ # process#4 (reader, waits for the earlier writer in the read lock queue):
1110
+ rql.lock("your_lock_name", read_write_mode: :read) { ... }
1111
+
1112
+ rql.lock_info("your_lock_name")
1113
+
1114
+ # => (read locks info: process#1 and process#2)
1115
+ {
1116
+ "lock_key" => "rql:lock:your_lock_name",
1117
+ "rw_mode" => "read",
1118
+ "rem_ttl" => 4_985, # remaining ttl of the longest read lock
1119
+ "readers" => [
1120
+ {
1121
+ "acq_id" => "rql:acq:123/456/567/678/374dd74324",
1122
+ "hst_id" => "rql:hst:123/456/678/374dd74324",
1123
+ "ts" => 123456789.12345,
1124
+ "ini_ttl" => 5_000,
1125
+ "rem_ttl" => 4_985,
1126
+ "kek" => "pek" # custom metadata of the read lock
1127
+ },
1128
+ {
1129
+ "acq_id" => "rql:acq:124/457/568/679/a83cb4f9d1",
1130
+ "hst_id" => "rql:hst:124/457/679/a83cb4f9d1",
1131
+ "ts" => 123456789.54321,
1132
+ "ini_ttl" => 3_000,
1133
+ "rem_ttl" => 2_412
1134
+ }
1135
+ ]
1136
+ }
1137
+
1138
+ # -> case 2: (some time later) readers are released, the lock is held by the writer (process#3),
1139
+ # the reader (process#4) is still waiting in the read lock queue;
1140
+
1141
+ rql.lock_info("your_lock_name")
1142
+
1143
+ # => (write lock info: process#3, read lock requests are not included)
1144
+ {
1145
+ "lock_key" => "rql:lock:your_lock_name",
1146
+ "rw_mode" => "write",
1147
+ "acq_id" => "rql:acq:125/458/569/680/c7bfcaf4f9",
1148
+ "hst_id" => "rql:hst:125/458/680/c7bfcaf4f9",
1149
+ "ts" => 123456795.12345,
1150
+ "ini_ttl" => 5_000,
1151
+ "rem_ttl" => 4_870
1152
+ }
1153
+ ```
1154
+
1065
1155
  ---
1066
1156
 
1067
1157
  #### #queue_info
@@ -1079,12 +1169,15 @@ you can receive the lock queue info with empty queue value (an empty array).
1079
1169
  - set is ordered by score in ASC manner (inside the Redis Set);
1080
1170
  - score is represented as a timestamp when the lock request was made;
1081
1171
  - represents the acquirer identifier and their score as an array of hashes;
1082
- - returns `nil` if lock queue does not exist;
1172
+ - returns `nil` if lock queues (write and read) do not exist;
1083
1173
  - lock queue data (`Hash<String,String|Array<Hash<String|Numeric>>`):
1084
- - `"lock_queue"` - `string` - lock queue key in redis;
1174
+ - `"lock_queue"` - `string` - lock queue key in redis (write lock requests);
1085
1175
  - `"queue"` - `array` - an array of lock requests (array of hashes):
1086
1176
  - `"acq_id"` - `string` - acquirer identifier (process_id/thread_id/fiber_id/ractor_id/identity by default);
1087
1177
  - `"score"` - `float`/`epoch` - time when the lock request was made (epoch);
1178
+ - `"rw_mode"` - `string` - the requested lock mode (`"write"`);
1179
+ - `"read_lock_queue"`, `"read_queue"` - (only when read lock requests exist) the read lock queue key and read lock requests
1180
+ with the same structure (`"rw_mode"` - `"read"`), see [Read/Write Locks](#readwrite-locks);
1088
1181
 
1089
1182
  ```ruby
1090
1183
  rql.queue_info("your_lock_name")
@@ -1093,9 +1186,15 @@ rql.queue_info("your_lock_name")
1093
1186
  {
1094
1187
  "lock_queue" => "rql:lock_queue:your_lock_name",
1095
1188
  "queue" => [
1096
- { "acq_id" => "rql:acq:123/456/567/678/fa76df9cc2", "score" => 1711606640.540842},
1097
- { "acq_id" => "rql:acq:123/567/456/679/c7bfcaf4f9", "score" => 1711606640.540906},
1098
- { "acq_id" => "rql:acq:555/329/523/127/7329553b11", "score" => 1711606640.540963},
1189
+ { "acq_id" => "rql:acq:123/456/567/678/fa76df9cc2", "score" => 1711606640.540842, "rw_mode" => "write" },
1190
+ { "acq_id" => "rql:acq:123/567/456/679/c7bfcaf4f9", "score" => 1711606640.540906, "rw_mode" => "write" },
1191
+ { "acq_id" => "rql:acq:555/329/523/127/7329553b11", "score" => 1711606640.540963, "rw_mode" => "write" },
1192
+ # ...etc
1193
+ ],
1194
+ # (when read lock requests exist)
1195
+ "read_lock_queue" => "rql:lock_read_queue:your_lock_name",
1196
+ "read_queue" => [
1197
+ { "acq_id" => "rql:acq:123/456/567/700/fa76df9cc2", "score" => 1711606640.541011, "rw_mode" => "read" },
1099
1198
  # ...etc
1100
1199
  ]
1101
1200
  }
@@ -1203,6 +1302,63 @@ rql.unlock("your_lock_name")
1203
1302
 
1204
1303
  ---
1205
1304
 
1305
+ #### #unlock_read - release the read lock of the current acquirer
1306
+
1307
+ <sup>\[[back to top](#usage)\]</sup>
1308
+
1309
+ - releases the read lock of the current acquirer only (see [Read/Write Locks](#readwrite-locks)):
1310
+ other read locks, the write lock and lock requests are not affected (`#unlock` releases the lock entirely);
1311
+ - suitable for read locks obtained without a block of code (`lock(..., read_write_mode: :read)` without a block);
1312
+ - the current acquirer is calculated in the same way as for `#lock` (`#current_acquirer_id`: process, thread, fiber, ractor, identity),
1313
+ so call it from the same thread/fiber that obtained the read lock;
1314
+ - the read lock of a different acquirer can be released via `:acquirer_id` (for example, extracted from `#lock_info`/`#locks_info`);
1315
+ `:identity` is ignored in this case (the acquirer id already contains its own identity);
1316
+ - has an alias: `#release_read_lock`;
1317
+ - accepts:
1318
+ - `lock_name` - (required) `[String]` - the lock name whose read lock should be released;
1319
+ - `:acquirer_id` - (optional) `[String, NilClass]` - the acquirer whose read lock should be released
1320
+ (`nil` by default: the current acquirer is used);
1321
+ - `:identity` - (optional) `[String]` - the same identity as for `#lock` (`uniq_identity` by default);
1322
+ ignored when `:acquirer_id` is passed: the acquirer id already contains its own identity;
1323
+ - `:logger`, `:instrumenter`, `:instrument`, `:log_sampling_enabled`, `:log_sampling_percent`, `:log_sampler`, `:log_sample_this`,
1324
+ `:instr_sampling_enabled`, `:instr_sampling_percent`, `:instr_sampler`, `:instr_sample_this` - (optional) - the same as for `#unlock`;
1325
+ - returns `{ ok: true, result: { rel_time:, rel_key:, rel_acq_id:, lock_res: } }` (`lock_res` - `:released` or `:nothing_to_release`);
1326
+ - raises the `"redis_queued_locks.explicit_read_lock_release"` instrumentation event;
1327
+
1328
+ ```ruby
1329
+ rql.lock('report', read_write_mode: :read, ttl: 30_000) # => obtained without a block
1330
+ # ...
1331
+ rql.unlock_read('report')
1332
+ # =>
1333
+ {
1334
+ ok: true,
1335
+ result: {
1336
+ rel_time: 0.32, # milliseconds
1337
+ rel_key: 'rql:lock:report',
1338
+ rel_acq_id: 'rql:acq:17580/2260/2380/2280/3f16b93973612580',
1339
+ lock_res: :released # or :nothing_to_release
1340
+ }
1341
+ }
1342
+ ```
1343
+
1344
+ ```ruby
1345
+ # release the read lock of a different acquirer
1346
+ reader = rql.lock_info('report')['readers'].first
1347
+ rql.unlock_read('report', acquirer_id: reader['acq_id'])
1348
+ # =>
1349
+ {
1350
+ ok: true,
1351
+ result: {
1352
+ rel_time: 0.29, # milliseconds
1353
+ rel_key: 'rql:lock:report',
1354
+ rel_acq_id: 'rql:acq:17580/2260/2380/2280/3f16b93973612580', # reader['acq_id']
1355
+ lock_res: :released # or :nothing_to_release
1356
+ }
1357
+ }
1358
+ ```
1359
+
1360
+ ---
1361
+
1206
1362
  #### #clear_locks - release all locks and lock queues
1207
1363
 
1208
1364
  <sup>\[[back to top](#usage)\]</sup>
@@ -1471,6 +1627,25 @@ rql.release_locks_of(host_id: rql.current_host_id, acquirer_id: rql.current_acqu
1471
1627
  - the lock name which ttl should be extended;
1472
1628
  - `milliseconds` - (required) `[Integer]`
1473
1629
  - how many milliseconds should be added to the lock's TTL;
1630
+ - `:read_write_mode` - (optional) `[Symbol]`
1631
+ - `:write` (default) - extends the write lock (the behavior described below);
1632
+ - `:read` - extends the read lock of the current acquirer only (see [Read/Write Locks](#readwrite-locks)):
1633
+ - the read lock expiration, the read locks registry TTL and the read lock data TTL are extended;
1634
+ - an expired read lock (or a read lock replaced by a writer) is never extended ("revived"):
1635
+ `{ ok: false, result: :async_expire_or_no_lock }` is returned;
1636
+ - the extension is atomic (it is checked and applied inside a transaction that watches the write lock);
1637
+ - `:all_read_locks` - (optional) `[Boolean]`
1638
+ - `false` by default;
1639
+ - `:read` mode only (ignored in `:write` mode): `true` extends **all live read locks** of the lock
1640
+ (of all acquirers) instead of the read lock of the current acquirer:
1641
+ - each live read lock (its expiration and its data TTL) is extended by `milliseconds`,
1642
+ the read locks registry TTL is extended to the longest read lock;
1643
+ - expired read locks are not extended ("revived"); nothing is extended while the write lock is held;
1644
+ - read locks obtained concurrently with the extension (after it reads the read locks) are not extended;
1645
+ - returns `{ ok: true, result: { extended_locks_count: <the number of extended read locks> } }`
1646
+ when at least one read lock is extended;
1647
+ - `:identity` - (optional) `[String]`
1648
+ - the same identity as for `#lock` (`uniq_identity` by default); used for the current acquirer calculation in `:read` mode;
1474
1649
  - `:instrumenter` - (optional) `[#notify]`
1475
1650
  - custom instrumenter object;
1476
1651
  - pre-configured in `config['instrumetner']`;
@@ -1506,7 +1681,9 @@ rql.release_locks_of(host_id: rql.current_host_id, acquirer_id: rql.current_acqu
1506
1681
  - marks the method that everything should be instrumneted despite the enabled instrumentation sampling;
1507
1682
  - makes sense when instrumentation sampling is enabled;
1508
1683
  - `false` by default;
1509
- - returns `{ ok: true, result: :ttl_extended }` when ttl is extended;
1684
+ - returns `{ ok: true, result: { extended_locks_count: Integer } }` when ttl is extended:
1685
+ - `extended_locks_count` - the number of locks whose TTL is extended: `1` for the write lock
1686
+ and for the read lock of the current acquirer, the number of extended read locks for `all_read_locks: true`;
1510
1687
  - returns `{ ok: false, result: :async_expire_or_no_lock }` when a lock not found or a lock is already expired during
1511
1688
  some steps of invocation (see **Important** section below);
1512
1689
  - **Important**:
@@ -1526,7 +1703,7 @@ rql.release_locks_of(host_id: rql.current_host_id, acquirer_id: rql.current_acqu
1526
1703
  rql.extend_lock_ttl("my_lock", 5_000) # NOTE: add 5_000 milliseconds
1527
1704
 
1528
1705
  # => `ok` case
1529
- { ok: true, result: :ttl_extended }
1706
+ { ok: true, result: { extended_locks_count: 1 } }
1530
1707
 
1531
1708
  # => `failed` case
1532
1709
  { ok: false, result: :async_expire_or_no_lock }
@@ -1682,7 +1859,8 @@ rql.locks_info # or rql.locks_info(scan_size: 123)
1682
1859
  - returns `Set<Hash<Symbol,Any>>` (see [#queue_info](#queue_info) and examples below for details).
1683
1860
  - contained data: `{ queue: String, requests: Array<Hash<String,Any>> }`
1684
1861
  - `:queue` - `String` - lock key queue in Redis;
1685
- - `:requests` - `Array<Hash<String,Any>>` - lock requests in the que with their acquirer id and score.
1862
+ - `:requests` - `Array<Hash<String,Any>>` - lock requests in the que with their acquirer id, score and requested lock mode
1863
+ (`"rw_mode"` - `"write"` for `rql:lock_queue:*`, `"read"` for read lock queues `rql:lock_read_queue:*`).
1686
1864
 
1687
1865
  ```ruby
1688
1866
  rql.queues_info # or rql.qeuues_info(scan_size: 123)
@@ -1690,14 +1868,13 @@ rql.queues_info # or rql.qeuues_info(scan_size: 123)
1690
1868
  => #<Set:
1691
1869
  {{:queue=>"rql:lock_queue:some-lock-123",
1692
1870
  :requests=>
1693
- [{"acq_id"=>"rql:acq:38529/4500/4520/4360/66093702f24a3129", "score"=>1711606640.540842},
1694
- {"acq_id"=>"rql:acq:38529/4580/4600/4360/66093702f24a3129", "score"=>1711606640.540906},
1695
- {"acq_id"=>"rql:acq:38529/4620/4640/4360/66093702f24a3129", "score"=>1711606640.5409632}]},
1696
- {:queue=>"rql:lock_queue:some-lock-456",
1871
+ [{"acq_id"=>"rql:acq:38529/4500/4520/4360/66093702f24a3129", "score"=>1711606640.540842, "rw_mode"=>"write"},
1872
+ {"acq_id"=>"rql:acq:38529/4580/4600/4360/66093702f24a3129", "score"=>1711606640.540906, "rw_mode"=>"write"},
1873
+ {"acq_id"=>"rql:acq:38529/4620/4640/4360/66093702f24a3129", "score"=>1711606640.5409632, "rw_mode"=>"write"}]},
1874
+ {:queue=>"rql:lock_read_queue:some-lock-456",
1697
1875
  :requests=>
1698
- [{"acq_id"=>"rql:acq:38529/4380/4400/4360/66093702f24a3129", "score"=>1711606640.540722},
1699
- {"acq_id"=>"rql:acq:38529/4420/4440/4360/66093702f24a3129", "score"=>1711606640.5407748},
1700
- {"acq_id"=>"rql:acq:38529/4460/4480/4360/66093702f24a3129", "score"=>1711606640.540808}]},
1876
+ [{"acq_id"=>"rql:acq:38529/4380/4400/4360/66093702f24a3129", "score"=>1711606640.540722, "rw_mode"=>"read"},
1877
+ {"acq_id"=>"rql:acq:38529/4420/4440/4360/66093702f24a3129", "score"=>1711606640.5407748, "rw_mode"=>"read"}]},
1701
1878
  ...}>
1702
1879
  ```
1703
1880
  ---
@@ -2110,6 +2287,92 @@ rql.possible_host_ids
2110
2287
 
2111
2288
  ---
2112
2289
 
2290
+ ## Read/Write Locks
2291
+
2292
+ <sup>\[[back to top](#table-of-contents)\]</sup>
2293
+
2294
+ - `read_write_mode:` attribute of `#lock` and `#lock!`:
2295
+ - `:write` (default) - exclusive lock (the classic RQL lock): waits for the write lock and for all read locks;
2296
+ - `:read` - shared lock: waits for the write lock only (read locks of different acquirers work in parallel);
2297
+
2298
+ ```ruby
2299
+ # readers work in parallel
2300
+ rql.lock('report', read_write_mode: :read) { read_report }
2301
+
2302
+ # writer waits for all readers (and readers wait for the writer)
2303
+ rql.lock('report', read_write_mode: :write) { rebuild_report }
2304
+ rql.lock('report') { rebuild_report } # (write mode is the default mode)
2305
+ ```
2306
+
2307
+ - lock request ordering (`access_strategy`):
2308
+ - `:queued` (default) - FIFO between modes: read lock request waits for every earlier write lock request,
2309
+ write lock request waits for every earlier read and write lock request (so writers are not starved by a
2310
+ continuous flow of readers and consecutive readers obtain the lock together);
2311
+ - `:random` - lock queues are ignored: any free lock is obtained (a continuous flow of readers can starve writers);
2312
+ - read locks:
2313
+ - each read lock has its own `ttl` and is released (after the block execution) or expired independently of other read locks;
2314
+ - `ttl` should be a positive `Integer` (`RedisQueuedLocks::ArgumentError`);
2315
+ - each read lock has its own data with the same structure as the write lock data: acquirer, host, timestamp,
2316
+ initial ttl, `:meta` (with the same reserved keys) and reentrant lock data (`spc_cnt`, `spc_ext_ttl`, ...);
2317
+ the read lock data lives as long as the read lock and is returned by `#lock_info`/`#locks_info` for each reader;
2318
+ - `#unlock` releases the lock entirely: the write lock, all read locks (with their data) and all lock requests;
2319
+ - `#unlock_read` (aka `#release_read_lock`) releases the read lock of the current acquirer
2320
+ (suitable for read locks obtained without a block), or the read lock of the acquirer passed via `acquirer_id:`;
2321
+ - `#extend_lock_ttl(lock_name, milliseconds, read_write_mode: :read)` extends the read lock of the current acquirer only;
2322
+ `all_read_locks: true` extends all live read locks of the lock;
2323
+ - `#lock_series` with `read_write_mode: :read` obtains a series of read locks (the mode is the same for all locks of the series):
2324
+ - read lock series of different acquirers work in parallel, writers wait for them;
2325
+ - after the block (or when the series fails) only the read locks of the current acquirer are released;
2326
+ - reentrant read locks of the series (already obtained by the outer logic of the same acquirer) are not released by the series;
2327
+ - reentrant locks (the same acquirer, see [Deadlocks and Reentrant locks](#deadlocks-and-reentrant-locks)):
2328
+ - read lock request under the obtained read lock or write lock is processed as a reentrant lock of the obtained lock
2329
+ in accordance with `conflict_strategy`;
2330
+ - write lock request under the obtained read lock (read-to-write lock upgrade) fails with
2331
+ `RedisQueuedLocks::ConflictLockObtainError` (`{ ok: false, result: :conflict_lock_upgrade }`) for
2332
+ `:work_through` and `:extendable_work_through` (`:dead_locking` fails as usual, `:wait_for_lock` waits for the read lock expiration);
2333
+ - info methods:
2334
+ - `#locked?` - the lock is obtained by a writer or by any reader;
2335
+ - `#queued?` - there are write or read lock requests;
2336
+ - `#lock_info` - write lock info (`'rw_mode' => 'write'`), or read locks info when there is no write lock:
2337
+ ```ruby
2338
+ {
2339
+ 'lock_key' => 'rql:lock:report',
2340
+ 'rw_mode' => 'read',
2341
+ 'rem_ttl' => 4985, # remaining ttl of the longest read lock (milliseconds)
2342
+ 'readers' => [
2343
+ {
2344
+ 'acq_id' => 'rql:acq:...',
2345
+ 'hst_id' => 'rql:hst:...',
2346
+ 'ts' => 1791582966.125299,
2347
+ 'ini_ttl' => 5000,
2348
+ 'rem_ttl' => 4985,
2349
+ 'some_meta_key' => 'some_meta_value' # (custom `:meta`)
2350
+ },
2351
+ { 'acq_id' => 'rql:acq:...', 'hst_id' => 'rql:hst:...', 'ts' => 1791582967.5, 'ini_ttl' => 5000, 'rem_ttl' => 3120 }
2352
+ ]
2353
+ }
2354
+ ```
2355
+ - `#queue_info` - additional `"read_lock_queue"`/`"read_queue"` keys when read lock requests exist;
2356
+ each request of `#queue_info`/`#queues_info` has `"rw_mode"` (`"write"`/`"read"`);
2357
+ - `#locks`/`#locks_info`, `#queues`/`#queues_info`, `#keys` - include read locks and read lock queues
2358
+ (`#locks_info` info has `'rw_mode'` - `'write'` or `'read'`);
2359
+ - logs and instrumentation: `rw_mode` is added to the lock logs and to the instrumentation payloads
2360
+ (lock series logs and events: the mode of the series), see [Logging](#logging) and [Instrumentation Events](#instrumentation-events);
2361
+ - data layout:
2362
+ - `rql:lock:<lock_name>` - write lock (`HASH`, the classic RQL lock);
2363
+ - `rql:lock_queue:<lock_name>` - write lock requests (`ZSET`, the classic RQL lock queue);
2364
+ - `rql:lock_readers:<lock_name>` - read locks (`ZSET`: acquirer id => read lock expiration time in Redis time, milliseconds);
2365
+ - `rql:lock_read_queue:<lock_name>` - read lock requests (`ZSET`);
2366
+ - `rql:lock_reader:<lock_name>:<acquirer_id>` - read lock data (`HASH`, the same structure as the write lock, read lock ttl);
2367
+ - implementation notes:
2368
+ - optimistic transactions only (`WATCH`/`MULTI`): readers watch the write lock key, writers watch the write lock key and the read locks key,
2369
+ so readers never invalidate transactions of each other;
2370
+ - lock queues are responsible for the ordering only, mutual exclusion is guaranteed by the lock state check;
2371
+ - **all your processes should use the RQL version with read/write locks** before you start to use read locks:
2372
+ write locks of older RQL versions do not check read locks;
2373
+
2374
+ ---
2375
+
2113
2376
  ## Deadlocks and Reentrant locks
2114
2377
 
2115
2378
  <sup>\[[back to top](#table-of-contents)\]</sup>
@@ -2135,39 +2398,64 @@ rql.possible_host_ids
2135
2398
  - [Logging Configuration](#logging-configuration)
2136
2399
 
2137
2400
 
2401
+ - each lock log contains `rw_mode` - the lock mode (`'write'` or `'read'`, see [Read/Write Locks](#readwrite-locks)):
2402
+ - the **requested** mode in lock obtaining logs, try-lock logs (`try_lock.*`) and `fail_fast_or_limits_reached_or_deadlock__dequeue`;
2403
+ - the **held** mode in `expire_lock` and `decrease_lock` (the lock that is released or decreased: a reentrant read lock request
2404
+ under the write lock of the same acquirer holds the write lock);
2405
+
2138
2406
  - default logs (raised from `#lock`/`#lock!`):
2139
2407
 
2140
2408
  ```ruby
2141
- "[redis_queued_locks.start_lock_obtaining]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat");
2142
- "[redis_queued_locks.start_try_to_lock_cycle]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat");
2143
- "[redis_queued_locks.dead_score_reached__reset_acquirer_position]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat");
2144
- "[redis_queued_locks.lock_obtained]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acq_time");
2145
- "[redis_queued_locks.extendable_reentrant_lock_obtained]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "acq_time");
2146
- "[redis_queued_locks.reentrant_lock_obtained]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "acq_time");
2147
- "[redis_queued_locks.fail_fast_or_limits_reached_or_deadlock__dequeue]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat");
2148
- "[redis_queued_locks.expire_lock]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat");
2149
- "[redis_queued_locks.decrease_lock]" # (logs "lock_key", "decreased_ttl", "queue_ttl", "acq_id", "hst_id", "acs_strat");
2409
+ "[redis_queued_locks.start_lock_obtaining]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2410
+ "[redis_queued_locks.start_try_to_lock_cycle]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2411
+ "[redis_queued_locks.dead_score_reached__reset_acquirer_position]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2412
+ "[redis_queued_locks.lock_obtained]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "acq_time");
2413
+ "[redis_queued_locks.extendable_reentrant_lock_obtained]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "acq_time");
2414
+ "[redis_queued_locks.reentrant_lock_obtained]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "acq_time");
2415
+ "[redis_queued_locks.fail_fast_or_limits_reached_or_deadlock__dequeue]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2416
+ "[redis_queued_locks.expire_lock]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2417
+ "[redis_queued_locks.decrease_lock]" # (logs "lock_key", "decreased_ttl", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2150
2418
  ```
2151
2419
 
2152
2420
  - additional logs (raised from `#lock`/`#lock!` with `confg[:log_lock_try] == true`):
2153
2421
 
2154
2422
  ```ruby
2155
- "[redis_queued_locks.try_lock.start]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat");
2156
- "[redis_queued_locks.try_lock.rconn_fetched]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat");
2157
- "[redis_queued_locks.try_lock.same_process_conflict_detected]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat");
2158
- "[redis_queued_locks.try_lock.same_process_conflict_analyzed]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "spc_status");
2159
- "[redis_queued_locks.try_lock.reentrant_lock__extend_and_work_through]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "spc_status", "last_ext_ttl", "last_ext_ts");
2160
- "[redis_queued_locks.try_lock.reentrant_lock__work_through]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "spc_status", last_spc_ts);
2161
- "[redis_queued_locks.try_lock.single_process_lock_conflict__dead_lock]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "spc_status", "last_spc_ts");
2162
- "[redis_queued_locks.try_lock.acq_added_to_queue]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat");
2163
- "[redis_queued_locks.try_lock.remove_expired_acqs]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat");
2164
- "[redis_queued_locks.try_lock.get_first_from_queue]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "first_acq_id_in_queue");
2165
- "[redis_queued_locks.try_lock.exit__queue_ttl_reached]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat");
2166
- "[redis_queued_locks.try_lock.exit__no_first]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "first_acq_id_in_queue", "<current_lock_data>");
2167
- "[redis_queued_locks.try_lock.exit__lock_still_obtained]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "first_acq_id_in_queue", "locked_by_acq_id", "<current_lock_data>");
2168
- "[redis_queued_locks.try_lock.obtain__free_to_acquire]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat");
2423
+ "[redis_queued_locks.try_lock.start]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2424
+ "[redis_queued_locks.try_lock.rconn_fetched]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2425
+ "[redis_queued_locks.try_lock.same_process_conflict_detected]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2426
+ "[redis_queued_locks.try_lock.same_process_conflict_analyzed]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "spc_status");
2427
+ "[redis_queued_locks.try_lock.reentrant_lock__extend_and_work_through]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "spc_status", "last_ext_ttl", "last_ext_ts");
2428
+ "[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");
2429
+ "[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");
2430
+ "[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");
2431
+ "[redis_queued_locks.try_lock.acq_added_to_queue]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2432
+ "[redis_queued_locks.try_lock.remove_expired_acqs]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2433
+ "[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");
2434
+ "[redis_queued_locks.try_lock.exit__queue_ttl_reached]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2435
+ "[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>");
2436
+ "[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>");
2437
+ "[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");
2438
+ "[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");
2439
+ "[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");
2440
+ "[redis_queued_locks.try_lock.obtain__free_to_acquire]" # (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2169
2441
  ```
2170
2442
 
2443
+ - lock series logs (raised from `#lock_series`/`#lock_series!`, PoC; `rw_mode` is the mode of the series):
2444
+
2445
+ ```ruby
2446
+ "[redis_queued_locks.start_lock_series_obtaining]" # (logs "lock_keys", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2447
+ "[redis_queued_locks.lock_series_obtained]" # (logs "lock_keys", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "acq_time");
2448
+ "[redis_queued_locks.expire_lock_series]" # (logs "lock_keys", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2449
+ ```
2450
+
2451
+ - read/write try-lock logs (see [Read/Write Locks](#readwrite-locks)):
2452
+ - `single_process_lock_conflict__lock_upgrade` - write lock request of the acquirer that holds the read lock
2453
+ (read-to-write lock upgrade is not supported for `:work_through` and `:extendable_work_through` conflict strategies);
2454
+ - `exit__write_request_ahead` - read lock request waits for an earlier write lock request (`ahead_acq_id`, `:queued` access strategy);
2455
+ - `exit__read_request_ahead` - write lock request waits for an earlier read lock request (`ahead_acq_id`, `:queued` access strategy);
2456
+ - `exit__read_lock_still_obtained` - write lock request waits for live read locks
2457
+ (read lock request: waits for the own read lock with `:wait_for_lock` conflict strategy);
2458
+
2171
2459
  ---
2172
2460
 
2173
2461
  ### Logging Configuration
@@ -2185,15 +2473,20 @@ rql.possible_host_ids
2185
2473
  # - should implement `debug(progname = nil, &block)` (minimal requirement) or be an instance of Ruby's `::Logger` class/subclass;
2186
2474
  # - supports `SemanticLogger::Logger` (see "semantic_logger" gem)
2187
2475
  # - at this moment the only debug logs are realised in following cases:
2188
- # - "[redis_queued_locks.start_lock_obtaining]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat");
2189
- # - "[redis_queued_locks.start_try_to_lock_cycle]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat");
2190
- # - "[redis_queued_locks.dead_score_reached__reset_acquirer_position]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat");
2191
- # - "[redis_queued_locks.lock_obtained]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acq_time", "acs_strat");
2192
- # - "[redis_queued_locks.extendable_reentrant_lock_obtained]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acq_time", "acs_strat");
2193
- # - "[redis_queued_locks.reentrant_lock_obtained]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acq_time", "acs_strat");
2194
- # - "[redis_queued_locks.fail_fast_or_limits_reached_or_deadlock__dequeue]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat");
2195
- # - "[redis_queued_locks.expire_lock]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat");
2196
- # - "[redis_queued_locks.decrease_lock]" (logs "lock_key", "decreased_ttl", "queue_ttl", "acq_id", "hst_id", "acs_strat");
2476
+ # - "[redis_queued_locks.start_lock_obtaining]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2477
+ # - "[redis_queued_locks.start_try_to_lock_cycle]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2478
+ # - "[redis_queued_locks.dead_score_reached__reset_acquirer_position]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2479
+ # - "[redis_queued_locks.lock_obtained]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "acq_time");
2480
+ # - "[redis_queued_locks.extendable_reentrant_lock_obtained]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "acq_time");
2481
+ # - "[redis_queued_locks.reentrant_lock_obtained]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "acq_time");
2482
+ # - "[redis_queued_locks.fail_fast_or_limits_reached_or_deadlock__dequeue]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2483
+ # - "[redis_queued_locks.expire_lock]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2484
+ # - "[redis_queued_locks.decrease_lock]" (logs "lock_key", "decreased_ttl", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2485
+ # - "[redis_queued_locks.start_lock_series_obtaining]" (logs "lock_keys", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2486
+ # - "[redis_queued_locks.lock_series_obtained]" (logs "lock_keys", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "acq_time");
2487
+ # - "[redis_queued_locks.expire_lock_series]" (logs "lock_keys", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2488
+ # - "rw_mode" is the requested lock mode ('write'/'read'), the held lock mode for "expire_lock" and "decrease_lock",
2489
+ # the mode of the series for lock series logs;
2197
2490
  # - by default uses VoidLogger that does nothing;
2198
2491
  config['logger'] = RedisQueuedLocks::Logging::VoidLogger
2199
2492
 
@@ -2201,19 +2494,25 @@ config['logger'] = RedisQueuedLocks::Logging::VoidLogger
2201
2494
  # - adds additional debug logs;
2202
2495
  # - enables additional logs for each internal try-retry lock acquiring (a lot of logs can be generated depending on your retry configurations);
2203
2496
  # - it adds following debug logs in addition to the existing:
2204
- # - "[redis_queued_locks.try_lock.start]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat");
2205
- # - "[redis_queued_locks.try_lock.rconn_fetched]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat");
2206
- # - "[redis_queued_locks.try_lock.same_process_conflict_detected]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat");
2207
- # - "[redis_queued_locks.try_lock.same_process_conflict_analyzed]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "spc_status");
2208
- # - "[redis_queued_locks.try_lock.reentrant_lock__extend_and_work_through]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "spc_status", "last_ext_ttl", "last_ext_ts");
2209
- # - "[redis_queued_locks.try_lock.reentrant_lock__work_through]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "spc_status", last_spc_ts);
2210
- # - "[redis_queued_locks.try_lock.acq_added_to_queue]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat")";
2211
- # - "[redis_queued_locks.try_lock.remove_expired_acqs]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat");
2212
- # - "[redis_queued_locks.try_lock.get_first_from_queue]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "first_acq_id_in_queue");
2213
- # - "[redis_queued_locks.try_lock.exit__queue_ttl_reached]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat");
2214
- # - "[redis_queued_locks.try_lock.exit__no_first]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "first_acq_id_in_queue", "<current_lock_data>");
2215
- # - "[redis_queued_locks.try_lock.exit__lock_still_obtained]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "first_acq_id_in_queue", "locked_by_acq_id", "<current_lock_data>");
2216
- # - "[redis_queued_locks.try_lock.obtain__free_to_acquire]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat");
2497
+ # - "[redis_queued_locks.try_lock.start]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2498
+ # - "[redis_queued_locks.try_lock.rconn_fetched]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2499
+ # - "[redis_queued_locks.try_lock.same_process_conflict_detected]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2500
+ # - "[redis_queued_locks.try_lock.same_process_conflict_analyzed]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "spc_status");
2501
+ # - "[redis_queued_locks.try_lock.reentrant_lock__extend_and_work_through]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode", "spc_status", "last_ext_ttl", "last_ext_ts");
2502
+ # - "[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");
2503
+ # - "[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");
2504
+ # - "[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");
2505
+ # - "[redis_queued_locks.try_lock.acq_added_to_queue]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2506
+ # - "[redis_queued_locks.try_lock.remove_expired_acqs]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2507
+ # - "[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");
2508
+ # - "[redis_queued_locks.try_lock.exit__queue_ttl_reached]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2509
+ # - "[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>");
2510
+ # - "[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>");
2511
+ # - "[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");
2512
+ # - "[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");
2513
+ # - "[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");
2514
+ # - "[redis_queued_locks.try_lock.obtain__free_to_acquire]" (logs "lock_key", "queue_ttl", "acq_id", "hst_id", "acs_strat", "rw_mode");
2515
+ # - "rw_mode" is the requested lock mode ('write'/'read');
2217
2516
  config['log_lock_try'] = false
2218
2517
 
2219
2518
  # (default: false)
@@ -2313,12 +2612,15 @@ config['instr_sampler'] = RedisQueuedLocks::Instrument::Sampler
2313
2612
 
2314
2613
  List of instrumentation events
2315
2614
 
2316
- - ["redis_queued_locks.lock_obtained"](#redis_queued_lockslock_hold_and_release)
2615
+ - ["redis_queued_locks.lock_obtained"](#redis_queued_lockslock_obtained)
2317
2616
  - ["redis_queued_locks.extendable_reentrant_lock_obtained"](#redis_queued_locksextendable_reentrant_lock_obtained)
2318
2617
  - ["redis_queued_locks.reentrant_lock_obtained"](#redis_queued_locksreentrant_lock_obtained)
2319
2618
  - ["redis_queued_locks.lock_hold_and_release"](#redis_queued_lockslock_hold_and_release)
2320
2619
  - ["redis_queued_locks.reentrant_lock_hold_completes"](#redis_queued_locksreentrant_lock_hold_completes)
2620
+ - ["redis_queued_locks.lock_series_obtained"](#redis_queued_lockslock_series_obtained)
2621
+ - ["redis_queued_locks.lock_series_hold_and_release"](#redis_queued_lockslock_series_hold_and_release)
2321
2622
  - ["redis_queued_locks.explicit_lock_release"](#redis_queued_locksexplicit_lock_release)
2623
+ - ["redis_queued_locks.explicit_read_lock_release"](#redis_queued_locksexplicit_read_lock_release)
2322
2624
  - ["redis_queued_locks.explicit_all_locks_release"](#redis_queued_locksexplicit_all_locks_release)
2323
2625
  - ["redis_queued_locks.release_locks_of"](#redis_queued_locksrelease_locks_of)
2324
2626
 
@@ -2333,6 +2635,7 @@ Detalized event semantics and payload structure:
2333
2635
  - `:acq_id` - `string` - lock acquirer identifier;
2334
2636
  - `:hst_id` - `string` - lock's host identifier;
2335
2637
  - `:lock_key` - `string` - lock name;
2638
+ - `:rw_mode` - `symbol` - requested lock mode: `:write` or `:read` (see [Read/Write Locks](#readwrite-locks));
2336
2639
  - `:ts` - `numeric`/`epoch` - the time when the lock was obtaiend;
2337
2640
  - `:acq_time` - `float`/`milliseconds` - time spent on lock acquiring;
2338
2641
  - `:instrument` - `nil`/`Any` - custom data passed to the `#lock`/`#lock!` method as `:instrument` attribute;
@@ -2343,6 +2646,8 @@ Detalized event semantics and payload structure:
2343
2646
  - raised from `#lock`/`#lock!` when the lock was obtained as reentrant lock;
2344
2647
  - payload:
2345
2648
  - `:lock_key` - `string` - lock name;
2649
+ - `:rw_mode` - `symbol` - requested lock mode: `:write` or `:read` (see [Read/Write Locks](#readwrite-locks));
2650
+ a read lock request under the write lock of the same acquirer works through the write lock (`:rw_mode` is `:read`);
2346
2651
  - `:ttl` - `integer`/`milliseconds` - last lock ttl by reentrant locking;
2347
2652
  - `:acq_id` - `string` - lock acquirer identifier;
2348
2653
  - `:hst_id` - `string` - lock's host identifier;
@@ -2356,6 +2661,8 @@ Detalized event semantics and payload structure:
2356
2661
  - raised from `#lock`/`#lock!` when the lock was obtained as reentrant lock;
2357
2662
  - payload:
2358
2663
  - `:lock_key` - `string` - lock name;
2664
+ - `:rw_mode` - `symbol` - requested lock mode: `:write` or `:read` (see [Read/Write Locks](#readwrite-locks));
2665
+ a read lock request under the write lock of the same acquirer works through the write lock (`:rw_mode` is `:read`);
2359
2666
  - `:ttl` - `integer`/`milliseconds` - last lock ttl by reentrant locking;
2360
2667
  - `:acq_id` - `string` - lock acquirer identifier;
2361
2668
  - `:hst_id` - `string` - lock's host identifier;
@@ -2373,6 +2680,7 @@ Detalized event semantics and payload structure:
2373
2680
  - `:acq_id` - `string` - lock acquirer identifier;
2374
2681
  - `:hst_id` - `string` - lock's host identifier;
2375
2682
  - `:lock_key` - `string` - lock name;
2683
+ - `:rw_mode` - `symbol` - requested lock mode: `:write` or `:read` (see [Read/Write Locks](#readwrite-locks));
2376
2684
  - `:ts` - `numeric`/`epoch` - the time when lock was obtained;
2377
2685
  - `:acq_time` - `float`/`milliseconds` - time spent on lock acquiring;
2378
2686
  - `:instrument` - `nil`/`Any` - custom data passed to the `#lock`/`#lock!` method as `:instrument` attribute;
@@ -2389,18 +2697,63 @@ Detalized event semantics and payload structure:
2389
2697
  - `:hst_id` - `string` - lock's host identifier;
2390
2698
  - `:ts` - `numeric`/`epoch` - the time when the lock was obtaiend as reentrant lock;
2391
2699
  - `:lock_key` - `string` - lock name;
2700
+ - `:rw_mode` - `symbol` - requested lock mode: `:write` or `:read` (see [Read/Write Locks](#readwrite-locks));
2701
+ a read lock request under the write lock of the same acquirer works through the write lock (`:rw_mode` is `:read`);
2392
2702
  - `:acq_time` - `float`/`milliseconds` - time spent on lock acquiring;
2393
2703
  - `:instrument` - `nil`/`Any` - custom data passed to the `#lock`/`#lock!` method as `:instrument` attribute;
2394
2704
 
2705
+ ##### `"redis_queued_locks.lock_series_obtained"`
2706
+ - <sup>\[[back to the list](#instrumentation-events)\]</sup>
2707
+ - (PoC) a moment when all locks of the series were obtained;
2708
+ - raised from `#lock_series`/`#lock_series!`;
2709
+ - payload:
2710
+ - `:lock_keys` - `array<string>` - lock names of the series;
2711
+ - `:rw_mode` - `symbol` - the mode of the series (`read_write_mode`): `:write` or `:read` (see [Read/Write Locks](#readwrite-locks));
2712
+ - `:ttl` - `integer`/`milliseconds` - the `ttl` option of the series (each lock of the series gets its own ttl,
2713
+ see [lock_series](#lock_series---poc-acquire-a-series-of-locks));
2714
+ - `:acq_id` - `string` - lock acquirer identifier;
2715
+ - `:hst_id` - `string` - lock's host identifier;
2716
+ - `:ts` - `numeric`/`epoch` - the time when the series was obtained;
2717
+ - `:acq_time` - `float`/`milliseconds` - time spent on the series acquiring;
2718
+ - `:instrument` - `nil`/`Any` - custom data passed to the `#lock_series`/`#lock_series!` method as `:instrument` attribute;
2719
+
2720
+ ##### `"redis_queued_locks.lock_series_hold_and_release"`
2721
+ - <sup>\[[back to the list](#instrumentation-events)\]</sup>
2722
+ - (PoC) an event signalizes about the "hold+and+release" process of the series is finished;
2723
+ - raised from `#lock_series`/`#lock_series!` when invoked with a block of code;
2724
+ - payload:
2725
+ - `:lock_keys` - `array<string>` - lock names of the series;
2726
+ - `:rw_mode` - `symbol` - the mode of the series (`read_write_mode`): `:write` or `:read` (see [Read/Write Locks](#readwrite-locks));
2727
+ - `:hold_time` - `float`/`milliseconds` - the series hold time;
2728
+ - `:ttl` - `integer`/`milliseconds` - the `ttl` option of the series;
2729
+ - `:acq_id` - `string` - lock acquirer identifier;
2730
+ - `:hst_id` - `string` - lock's host identifier;
2731
+ - `:ts` - `float`/`epoch` - the time when the series was released;
2732
+ - `:acq_time` - `float`/`milliseconds` - time spent on the series acquiring;
2733
+ - `:instrument` - `nil`/`Any` - custom data passed to the `#lock_series`/`#lock_series!` method as `:instrument` attribute;
2734
+
2395
2735
  ##### `"redis_queued_locks.explicit_lock_release"`
2396
2736
  - <sup>\[[back to the list](#instrumentation-events)\]</sup>
2397
2737
  - an event signalizes about the explicit lock release (invoked via `RedisQueuedLock#unlock`);
2398
2738
  - raised from `#unlock`;
2739
+ - the write lock, all read locks and both lock request queues (write and read) are released;
2399
2740
  - payload:
2400
2741
  - `:at` - `float`/`epoch` - the time when the lock was released;
2401
2742
  - `:rel_time` - `float`/`milliseconds` - time spent on lock releasing;
2402
2743
  - `:lock_key` - `string` - released lock (lock name);
2403
- - `:lock_key_queue` - `string` - released lock queue (lock queue name);
2744
+ - `:lock_key_queue` - `string` - released lock queue (the queue of write lock requests);
2745
+
2746
+ ##### `"redis_queued_locks.explicit_read_lock_release"`
2747
+ - <sup>\[[back to the list](#instrumentation-events)\]</sup>
2748
+ - an event signalizes about the explicit release of the read lock of the concrete acquirer (invoked via `RedisQueuedLock#unlock_read`);
2749
+ - raised from `#unlock_read` (aka `#release_read_lock`);
2750
+ - payload:
2751
+ - `:at` - `float`/`epoch` - the time when the read lock was released;
2752
+ - `:rel_time` - `float`/`milliseconds` - time spent on read lock releasing;
2753
+ - `:lock_key` - `string` - lock key (`rql:lock:<lock_name>`);
2754
+ - `:acq_id` - `string` - the read lock acquirer;
2755
+ - `:lock_res` - `symbol` - `:released` or `:nothing_to_release`;
2756
+ - `:instrument` - `nil`/`Any` - custom data passed to the `#unlock_read` method as `:instrument` attribute;
2404
2757
 
2405
2758
  ##### `"redis_queued_locks.explicit_all_locks_release"`
2406
2759
  - <sup>\[[back to the list](#instrumentation-events)\]</sup>
@@ -2409,7 +2762,8 @@ Detalized event semantics and payload structure:
2409
2762
  - payload:
2410
2763
  - `:rel_time` - `float`/`milliseconds` - time spent on "realese all locks" operation;
2411
2764
  - `:at` - `float`/`epoch` - the time when the operation has ended;
2412
- - `:rel_keys` - `integer` - released redis keys count (`released queue keys` + `released lock keys`);
2765
+ - `:rel_key_cnt` - `integer` - released redis keys count (write locks, readers registries, read lock data,
2766
+ write and read lock request queues);
2413
2767
 
2414
2768
  ##### `"redis_queued_locks.release_locks_of"`
2415
2769
  - <sup>\[[back to the list](#instrumentation-events)\]</sup>
@@ -2420,8 +2774,8 @@ Detalized event semantics and payload structure:
2420
2774
  - `:at` - `float`/`epoch` - the time when the opertaion has ended;
2421
2775
  - `:acq_id` - `string` - refused acquirer identifier;
2422
2776
  - `:hst_id` - `string` - refused host identifier;
2423
- - `:rel_key_cnt` - `integer` - released locks count;
2424
- - `:tch_queue_cnt` - `:integer` - the number of queues from which the concrete host/acquirer was removed;
2777
+ - `:rel_key_cnt` - `integer` - released locks count (write locks and read locks);
2778
+ - `:tch_queue_cnt` - `:integer` - the number of queues (write and read lock request queues) from which the concrete host/acquirer was removed;
2425
2779
 
2426
2780
  ---
2427
2781
 
@@ -2449,12 +2803,6 @@ Detalized event semantics and payload structure:
2449
2803
  - an ability to release all locks and all requests of the concrete acquirer id or host id (or both in validation-orianted combination);
2450
2804
  - 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);
2451
2805
  - a convenient way to mark any `lock` invocation as "non-instrumentable" / "non-loggable" (as an alternative to `VoidNotifier` and to `VoidLogger`);
2452
- - `Read`/`Write` semantics: you can mark your locks as `read` or `write` lock in order to simulate `read`/`write` lock behavior:
2453
- - `read` - watis - `write`;
2454
- - `read` - not waits - `read`;
2455
- - `write` - waits - `read`;
2456
- - `write` - waits - `write`;
2457
- - **write** mode is a default behavior for all RQL locks;
2458
2806
  - `README`-section `Issues with other Libs` about issues in other libraries that can broke RQL features:
2459
2807
  - `Sentry`'s' bugs with Ractors:
2460
2808
  - `Sentry`-OpenTelemetry problems with its global mutexes and non-sharable objects that is not accessible from the ractors (sentry tyies to