redis_queued_locks 1.16.2 → 1.18.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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/project-overview.md +281 -0
  3. data/.claude/rules/acquirer.md +81 -0
  4. data/.claude/rules/arguments.md +60 -0
  5. data/.claude/rules/git.md +46 -0
  6. data/.claude/rules/logic.md +63 -0
  7. data/.claude/rules/swarm.md +120 -0
  8. data/.claude/rules/tests.md +48 -0
  9. data/.claude/rules/type-checking.md +50 -0
  10. data/.claude/rules/visitors.md +69 -0
  11. data/.rubocop.rbs.yml +31 -0
  12. data/.rubocop.yml +3 -3
  13. data/.ruby-version +1 -1
  14. data/CHANGELOG.md +80 -0
  15. data/CLAUDE.md +36 -0
  16. data/README.md +367 -82
  17. data/Rakefile +31 -2
  18. data/github_ci/.keep +0 -0
  19. data/lib/redis_queued_locks/acquirer/acquire_lock/delay_execution.rb +2 -2
  20. data/lib/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue/log_visitor.rb +6 -3
  21. data/lib/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue.rb +9 -7
  22. data/lib/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rb +20 -10
  23. data/lib/redis_queued_locks/acquirer/acquire_lock/log_visitor.rb +34 -16
  24. data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock/log_visitor.rb +240 -28
  25. data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rb +398 -132
  26. data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire/log_visitor.rb +12 -6
  27. data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire.rb +33 -9
  28. data/lib/redis_queued_locks/acquirer/acquire_lock.rb +73 -24
  29. data/lib/redis_queued_locks/acquirer/clear_dead_requests.rb +9 -1
  30. data/lib/redis_queued_locks/acquirer/extend_lock_ttl.rb +182 -3
  31. data/lib/redis_queued_locks/acquirer/is_locked.rb +19 -2
  32. data/lib/redis_queued_locks/acquirer/is_queued.rb +6 -2
  33. data/lib/redis_queued_locks/acquirer/lock_info.rb +96 -6
  34. data/lib/redis_queued_locks/acquirer/lock_series_poc/instr_visitor.rb +8 -4
  35. data/lib/redis_queued_locks/acquirer/lock_series_poc/log_visitor.rb +18 -7
  36. data/lib/redis_queued_locks/acquirer/lock_series_poc.rb +126 -29
  37. data/lib/redis_queued_locks/acquirer/locks.rb +103 -6
  38. data/lib/redis_queued_locks/acquirer/queue_info.rb +38 -9
  39. data/lib/redis_queued_locks/acquirer/queues.rb +28 -4
  40. data/lib/redis_queued_locks/acquirer/release_all_locks.rb +35 -1
  41. data/lib/redis_queued_locks/acquirer/release_lock.rb +40 -9
  42. data/lib/redis_queued_locks/acquirer/release_locks_of.rb +43 -3
  43. data/lib/redis_queued_locks/acquirer/release_read_lock.rb +143 -0
  44. data/lib/redis_queued_locks/acquirer.rb +1 -0
  45. data/lib/redis_queued_locks/client.rb +274 -158
  46. data/lib/redis_queued_locks/config/dsl.rb +4 -10
  47. data/lib/redis_queued_locks/resource.rb +117 -9
  48. data/lib/redis_queued_locks/swarm/flush_zombies.rb +46 -6
  49. data/lib/redis_queued_locks/swarm/supervisor.rb +1 -1
  50. data/lib/redis_queued_locks/swarm/swarm_element/isolated.rb +166 -44
  51. data/lib/redis_queued_locks/swarm/swarm_element/threaded.rb +45 -47
  52. data/lib/redis_queued_locks/swarm/zombie_info.rb +42 -0
  53. data/lib/redis_queued_locks/version.rb +2 -2
  54. data/rbs_collection.lock.yaml +10 -2
  55. data/rbs_collection.yaml +0 -2
  56. data/redis_queued_locks.gemspec +1 -1
  57. data/sig/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue/log_visitor.rbs +2 -1
  58. data/sig/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue.rbs +0 -1
  59. data/sig/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rbs +5 -0
  60. data/sig/redis_queued_locks/acquirer/acquire_lock/log_visitor.rbs +12 -6
  61. data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock/log_visitor.rbs +74 -7
  62. data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rbs +4 -2
  63. data/sig/redis_queued_locks/acquirer/acquire_lock/yield_expire.rbs +3 -0
  64. data/sig/redis_queued_locks/acquirer/acquire_lock/yield_with_expire/log_visitor.rbs +4 -2
  65. data/sig/redis_queued_locks/acquirer/extend_lock_ttl.rbs +19 -1
  66. data/sig/redis_queued_locks/acquirer/lock_info.rbs +9 -1
  67. data/sig/redis_queued_locks/acquirer/locks.rbs +10 -1
  68. data/sig/redis_queued_locks/acquirer/queue_info.rbs +2 -4
  69. data/sig/redis_queued_locks/acquirer/queues.rbs +1 -1
  70. data/sig/redis_queued_locks/acquirer/release_lock.rbs +8 -1
  71. data/sig/redis_queued_locks/acquirer/release_read_lock.rbs +44 -0
  72. data/sig/redis_queued_locks/client.rbs +22 -2
  73. data/sig/redis_queued_locks/resource.rbs +9 -2
  74. data/sig/redis_queued_locks/swarm/flush_zombies.rbs +1 -1
  75. data/sig/redis_queued_locks/swarm/swarm_element/isolated.rbs +13 -5
  76. data/sig/redis_queued_locks/swarm/swarm_element/threaded.rbs +6 -6
  77. data/sig/redis_queued_locks/swarm/zombie_info.rbs +5 -0
  78. metadata +17 -5
  79. data/github_ci/ruby3.3.gemfile +0 -17
  80. data/github_ci/ruby3.3.gemfile.lock +0 -216
@@ -2,6 +2,8 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.0.0
5
+ # @version 1.18.0
6
+ # rubocop:disable Metrics/ModuleLength
5
7
  module RedisQueuedLocks::Acquirer::ExtendLockTTL
6
8
  # @return [String]
7
9
  #
@@ -12,10 +14,20 @@ module RedisQueuedLocks::Acquirer::ExtendLockTTL
12
14
  return redis.call("PEXPIRE", KEYS[1], new_lock_pttl);
13
15
  LUA_SCRIPT
14
16
 
17
+ # rubocop:disable Metrics/ClassLength
15
18
  class << self
16
19
  # @param redis_client [RedisClient]
17
20
  # @param lock_name [String]
18
21
  # @param milliseconds [Integer]
22
+ # @param read_write_mode [Symbol]
23
+ # - `:write` - extends the write lock (of any acquirer);
24
+ # - `:read` - extends the read lock of the given acquirer (other read locks are not affected)
25
+ # or all live read locks of the lock (see `all_read_locks`);
26
+ # @param all_read_locks [Boolean]
27
+ # Extend all live read locks of the lock instead of the read lock of the given acquirer
28
+ # (used for `:read` mode only, ignored for `:write` mode).
29
+ # @param acquirer_id [String]
30
+ # The read lock acquirer (used for `:read` mode without `all_read_locks` only).
19
31
  # @param logger [::Logger,#debug]
20
32
  # @param instrumenter [#notify]
21
33
  # @param instrument [NilClass,Any]
@@ -27,15 +39,19 @@ module RedisQueuedLocks::Acquirer::ExtendLockTTL
27
39
  # @param instr_sampling_percent [Integer]
28
40
  # @param instr_sampler [#sampling_happened?,Module<RedisQueuedLocks::Instrument::Sampler>]
29
41
  # @param instr_sample_this [Boolean]
30
- # @return [Hash<Symbol,Boolean|Symbol>]
42
+ # @return [Hash<Symbol,Boolean|Symbol|Hash<Symbol,Integer>>]
31
43
  #
32
44
  # @api private
33
45
  # @since 1.0.0
34
- # @version 1.6.0
46
+ # @version 1.18.0
47
+ # rubocop:disable Metrics/MethodLength
35
48
  def extend_lock_ttl(
36
49
  redis_client,
37
50
  lock_name,
38
51
  milliseconds,
52
+ read_write_mode,
53
+ all_read_locks,
54
+ acquirer_id,
39
55
  logger,
40
56
  instrumenter,
41
57
  instrument,
@@ -48,18 +64,181 @@ module RedisQueuedLocks::Acquirer::ExtendLockTTL
48
64
  instr_sampler,
49
65
  instr_sample_this
50
66
  )
67
+ if read_write_mode != :read && read_write_mode != :write
68
+ raise(
69
+ RedisQueuedLocks::ArgumentError,
70
+ "`:read_write_mode` argument should be `:read` or `:write`, " \
71
+ "got #{read_write_mode.inspect}."
72
+ )
73
+ end
74
+
51
75
  lock_key = RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
52
76
 
77
+ if read_write_mode == :read
78
+ if all_read_locks != true && all_read_locks != false
79
+ raise(
80
+ RedisQueuedLocks::ArgumentError,
81
+ "`:all_read_locks` argument should be a boolean, got #{all_read_locks.inspect}."
82
+ )
83
+ end
84
+
85
+ # NOTE: (RW) all live read locks of the lock
86
+ if all_read_locks
87
+ return extend_all_read_locks_ttl(redis_client, lock_name, lock_key, milliseconds)
88
+ end
89
+
90
+ # NOTE: (RW) read lock of the concrete acquirer
91
+ return extend_read_lock_ttl(redis_client, lock_name, lock_key, acquirer_id, milliseconds)
92
+ end
93
+
53
94
  # NOTE: EVAL signature -> <lua script>, (number of keys), *(keys), *(arguments)
54
95
  result = redis_client.call('EVAL', EXTEND_LOCK_PTTL, 1, lock_key, milliseconds)
55
96
  # TODO: upload scripts to the redis
56
97
 
57
98
  # @type var result: Integer
58
99
  if result == 1
59
- { ok: true, result: :ttl_extended }
100
+ { ok: true, result: { extended_locks_count: 1 } }
101
+ else
102
+ { ok: false, result: :async_expire_or_no_lock }
103
+ end
104
+ end
105
+ # rubocop:enable Metrics/MethodLength
106
+
107
+ private
108
+
109
+ # Extends the live read lock of the acquirer:
110
+ # - the read lock expiration (readers registry score), the registry TTL and the read lock
111
+ # data TTL are extended;
112
+ # - the write lock key is watched: an expired read lock can be already replaced by a writer,
113
+ # so the expired (or replaced) read lock is never "revived";
114
+ #
115
+ # @param redis_client [RedisClient]
116
+ # @param lock_name [String]
117
+ # @param lock_key [String]
118
+ # @param acquirer_id [String]
119
+ # @param milliseconds [Integer]
120
+ # @return [Hash<Symbol,Boolean|Symbol|Hash<Symbol,Integer>>]
121
+ #
122
+ # @api private
123
+ # @since 1.18.0
124
+ # rubocop:disable Metrics/MethodLength
125
+ def extend_read_lock_ttl(redis_client, lock_name, lock_key, acquirer_id, milliseconds)
126
+ lock_readers_key = RedisQueuedLocks::Resource.prepare_lock_readers(lock_name)
127
+ read_lock_key = RedisQueuedLocks::Resource.prepare_read_lock_key(lock_name, acquirer_id)
128
+
129
+ result = redis_client.with do |rconn|
130
+ rconn.multi(watch: [lock_key]) do |transact|
131
+ # @type var lock_state: [Integer, Array[String], Float?]
132
+ lock_state = rconn.pipelined do |pipeline|
133
+ pipeline.call('EXISTS', lock_key)
134
+ pipeline.call('TIME')
135
+ pipeline.call('ZSCORE', lock_readers_key, acquirer_id)
136
+ end
137
+ redis_time = RedisQueuedLocks::Resource.redis_time_ms(lock_state[1])
138
+ read_lock_expiration = lock_state[2].to_f
139
+
140
+ # NOTE: no write lock and the read lock of the acquirer is alive
141
+ if lock_state[0] == 0 && read_lock_expiration > redis_time
142
+ extended_ttl = (read_lock_expiration + milliseconds - redis_time).ceil
143
+ transact.call('ZADD', lock_readers_key, 'XX', 'INCR', milliseconds, acquirer_id)
144
+ transact.call('PEXPIRE', lock_readers_key, extended_ttl, 'NX')
145
+ transact.call('PEXPIRE', lock_readers_key, extended_ttl, 'GT')
146
+ transact.call('PEXPIRE', read_lock_key, extended_ttl, 'NX')
147
+ transact.call('PEXPIRE', read_lock_key, extended_ttl, 'GT')
148
+ end
149
+ end
150
+ end
151
+
152
+ # NOTE:
153
+ # - [] => there is no live read lock of the acquirer (nothing to extend);
154
+ # - nil => the write lock is changed during the extension (the read lock can be expired);
155
+ # - result[0] => the new read lock expiration (nil if the read lock is removed);
156
+ if result.is_a?(::Array) && !result.empty? && result[0] != nil
157
+ { ok: true, result: { extended_locks_count: 1 } }
158
+ else
159
+ { ok: false, result: :async_expire_or_no_lock }
160
+ end
161
+ end
162
+ # rubocop:enable Metrics/MethodLength
163
+
164
+ # Extends all live read locks of the lock (the same rules as for `extend_read_lock_ttl`):
165
+ # - each live read lock expiration (readers registry score) and its read lock data TTL are
166
+ # extended by the given milliseconds; the registry TTL is extended to the longest read lock;
167
+ # - expired read locks are not extended ("revived");
168
+ # - the write lock key is watched (a writer can replace the expired read locks), the registry
169
+ # is not watched: concurrent read lock acquirements/releases do not abort the extension
170
+ # (read locks obtained after the registry snapshot are not extended, read locks released
171
+ # after it are not revived cuz of `ZADD XX`);
172
+ #
173
+ # @param redis_client [RedisClient]
174
+ # @param lock_name [String]
175
+ # @param lock_key [String]
176
+ # @param milliseconds [Integer]
177
+ # @return [Hash<Symbol,Boolean|Symbol|Hash<Symbol,Integer>>]
178
+ #
179
+ # @api private
180
+ # @since 1.18.0
181
+ # rubocop:disable Metrics/MethodLength
182
+ def extend_all_read_locks_ttl(redis_client, lock_name, lock_key, milliseconds)
183
+ lock_readers_key = RedisQueuedLocks::Resource.prepare_lock_readers(lock_name)
184
+ extended_read_locks_count = 0
185
+
186
+ result = redis_client.with do |rconn|
187
+ rconn.multi(watch: [lock_key]) do |transact|
188
+ # @type var lock_state: [Integer, Array[String], Array[[String, Float]]]
189
+ lock_state = rconn.pipelined do |pipeline|
190
+ pipeline.call('EXISTS', lock_key)
191
+ pipeline.call('TIME')
192
+ pipeline.call('ZRANGE', lock_readers_key, '0', '-1', 'WITHSCORES')
193
+ end
194
+ redis_time = RedisQueuedLocks::Resource.redis_time_ms(lock_state[1])
195
+
196
+ # NOTE: no write lock => extend all live read locks
197
+ if lock_state[0] == 0
198
+ registry_ttl = 0
199
+
200
+ lock_state[2].each do |(acquirer_id, read_lock_expiration)|
201
+ next unless read_lock_expiration > redis_time
202
+
203
+ read_lock_key =
204
+ RedisQueuedLocks::Resource.prepare_read_lock_key(lock_name, acquirer_id)
205
+ extended_ttl = (read_lock_expiration + milliseconds - redis_time).ceil
206
+ registry_ttl = extended_ttl if extended_ttl > registry_ttl
207
+ extended_read_locks_count += 1
208
+
209
+ transact.call('ZADD', lock_readers_key, 'XX', 'INCR', milliseconds, acquirer_id)
210
+ transact.call('PEXPIRE', read_lock_key, extended_ttl, 'NX')
211
+ transact.call('PEXPIRE', read_lock_key, extended_ttl, 'GT')
212
+ end
213
+
214
+ if extended_read_locks_count > 0
215
+ transact.call('PEXPIRE', lock_readers_key, registry_ttl, 'NX')
216
+ transact.call('PEXPIRE', lock_readers_key, registry_ttl, 'GT')
217
+ end
218
+ end
219
+ end
220
+ end
221
+
222
+ # NOTE:
223
+ # - [] => there are no live read locks (nothing to extend);
224
+ # - nil => the write lock is changed during the extension (read locks can be expired);
225
+ # - result[index * 3] => the new read lock expiration of each extended read lock
226
+ # (nil if the read lock is released during the extension => it is not counted);
227
+ extended_locks_count =
228
+ if result.is_a?(::Array)
229
+ extended_read_locks_count.times.count { |index| result[index * 3] != nil }
230
+ else
231
+ 0
232
+ end
233
+
234
+ if extended_locks_count > 0
235
+ { ok: true, result: { extended_locks_count: } }
60
236
  else
61
237
  { ok: false, result: :async_expire_or_no_lock }
62
238
  end
63
239
  end
240
+ # rubocop:enable Metrics/MethodLength
64
241
  end
242
+ # rubocop:enable Metrics/ClassLength
65
243
  end
244
+ # rubocop:enable Metrics/ModuleLength
@@ -2,17 +2,34 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.0.0
5
+ # @version 1.18.0
5
6
  module RedisQueuedLocks::Acquirer::IsLocked
6
7
  class << self
7
8
  # @param redis_client [RedisClient]
8
9
  # @param lock_name [String]
9
- # @return [Boolean]
10
+ # @return [Boolean] Is the lock obtained by a writer or by any reader (live read lock).
10
11
  #
11
12
  # @api private
12
13
  # @since 1.0.0
14
+ # @version 1.18.0
13
15
  def locked?(redis_client, lock_name)
14
16
  lock_key = RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
15
- redis_client.call('EXISTS', lock_key) == 1
17
+ lock_readers_key = RedisQueuedLocks::Resource.prepare_lock_readers(lock_name)
18
+
19
+ # @type var result: [Integer, Array[String], Array[[String, Float]]]
20
+ result = redis_client.pipelined do |pipeline|
21
+ pipeline.call('EXISTS', lock_key)
22
+ pipeline.call('TIME')
23
+ pipeline.call('ZRANGE', lock_readers_key, '-1', '-1', 'WITHSCORES')
24
+ end
25
+
26
+ # NOTE: write lock
27
+ return true if result[0] == 1
28
+
29
+ # NOTE: (RW) read lock (expired read locks can live in the readers registry till the cleanup)
30
+ longest_read_lock = result[2].first
31
+ longest_read_lock != nil &&
32
+ longest_read_lock[1] > RedisQueuedLocks::Resource.redis_time_ms(result[1])
16
33
  end
17
34
  end
18
35
  end
@@ -2,17 +2,21 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.0.0
5
+ # @version 1.18.0
5
6
  module RedisQueuedLocks::Acquirer::IsQueued
6
7
  class << self
7
8
  # @param redis_client [RedisClient]
8
9
  # @param lock_name [String]
9
- # @return [Boolean]
10
+ # @return [Boolean] Are there any write or read lock requests.
10
11
  #
11
12
  # @api private
12
13
  # @since 1.0.0
14
+ # @version 1.18.0
13
15
  def queued?(redis_client, lock_name)
14
16
  lock_key_queue = RedisQueuedLocks::Resource.prepare_lock_queue(lock_name)
15
- redis_client.call('EXISTS', lock_key_queue) == 1
17
+ read_lock_key_queue = RedisQueuedLocks::Resource.prepare_read_lock_queue(lock_name)
18
+ # NOTE: EXISTS returns the number of existing keys
19
+ redis_client.call('EXISTS', lock_key_queue, read_lock_key_queue) > 0
16
20
  end
17
21
  end
18
22
  end
@@ -2,14 +2,16 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.0.0
5
+ # @version 1.18.0
5
6
  module RedisQueuedLocks::Acquirer::LockInfo
6
7
  class << self
7
8
  # @param redis_client [RedisClient]
8
9
  # @param lock_name [String]
9
- # @return [Hash<String,String|Numeric>,NilClass]
10
- # - `nil` is returned when lock key does not exist or expired;
11
- # - result format: {
10
+ # @return [Hash<String,String|Numeric|Array<Hash<String,String|Numeric>>>,NilClass]
11
+ # - `nil` is returned when lock key does not exist or expired (and there are no read locks);
12
+ # - result format (write lock): {
12
13
  # 'lock_key' => "rql:lock:your_lockname", # acquired lock key
14
+ # 'rw_mode' => 'write', # lock mode
13
15
  # 'acq_id' => "rql:acq:123/456/789/987/uniqstring", # lock acquirer identifier
14
16
  # 'hst_id' => "rql:hst:123/456/987/uniqstring", # lock host identifier
15
17
  # 'ts' => 123456789.2649841, # <locked at> time stamp (epoch, seconds.microseconds)
@@ -22,17 +24,36 @@ module RedisQueuedLocks::Acquirer::LockInfo
22
24
  # 'l_spc_ext_ini_ttl' => 5000, # (milliseconds) the last ttl of reentrant lock
23
25
  # 'l_spc_ext_ts' => 123456.789 # (epoch) <extendable reentrant lock obtained at> timestamp
24
26
  # }
27
+ # - result format (read locks, when there is no write lock): {
28
+ # 'lock_key' => "rql:lock:your_lockname", # lock key
29
+ # 'rw_mode' => 'read', # lock mode
30
+ # 'rem_ttl' => 123456789, # remaining ttl of the longest read lock (milliseconds)
31
+ # 'readers' => [ # live read locks
32
+ # {
33
+ # 'acq_id' => "rql:acq:123/456/789/987/uniqstring", # read lock acquirer identifier
34
+ # 'hst_id' => "rql:hst:123/456/987/uniqstring", # read lock host identifier
35
+ # 'ts' => 123456789.2649841, # <locked at> time stamp (epoch, seconds.microseconds)
36
+ # 'ini_ttl' => 123456789, # initial read lock ttl (milliseconds)
37
+ # 'rem_ttl' => 123456789, # remaining read lock ttl (milliseconds)
38
+ # <custom metadata (`meta`) and additional keys for reentrant locks (see above)>
39
+ # },
40
+ # ...
41
+ # ]
42
+ # }
25
43
  #
26
44
  # @api private
27
45
  # @since 1.0.0
28
- # @version 1.9.0
46
+ # @version 1.18.0
29
47
  # rubocop:disable Metrics/MethodLength
30
48
  def lock_info(redis_client, lock_name)
31
49
  lock_key = RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
50
+ lock_readers_key = RedisQueuedLocks::Resource.prepare_lock_readers(lock_name)
32
51
 
33
52
  result = redis_client.pipelined do |pipeline|
34
53
  pipeline.call('HGETALL', lock_key)
35
54
  pipeline.call('PTTL', lock_key)
55
+ pipeline.call('TIME')
56
+ pipeline.call('ZRANGE', lock_readers_key, '0', '-1', 'WITHSCORES')
36
57
  end
37
58
 
38
59
  if result == nil
@@ -48,16 +69,22 @@ module RedisQueuedLocks::Acquirer::LockInfo
48
69
  # => (will be mutated further with different value types)
49
70
  # - result[1] (PTTL) (Integer)
50
71
  # => (without any mutation, integer is atomic)
72
+ # - result[2] (TIME) (Array<String>) (redis time)
73
+ # - result[3] (ZRANGE) (Array<[String,Float]>) (read locks)
51
74
 
52
- # @type var result: [Hash[String,String|Float|Integer],Integer]
75
+ # rubocop:disable Layout/LineLength
76
+ # @type var result: [Hash[String,String|Float|Integer], Integer, Array[String], Array[[String, Float]]]
77
+ # rubocop:enable Layout/LineLength
53
78
  hget_cmd_res = result[0]
54
79
  pttl_cmd_res = result[1]
55
80
 
56
81
  if hget_cmd_res == {} || pttl_cmd_res == -2 # NOTE: key does not exist
57
- nil
82
+ # NOTE: (RW) there is no write lock => check read locks
83
+ read_lock_info(redis_client, lock_name, lock_key, result[2], result[3])
58
84
  else
59
85
  hget_cmd_res.tap do |lock_data|
60
86
  lock_data['lock_key'] = lock_key
87
+ lock_data['rw_mode'] = 'write'
61
88
  lock_data['ts'] = Float(lock_data['ts'])
62
89
  lock_data['ini_ttl'] = Integer(lock_data['ini_ttl'])
63
90
  lock_data['rem_ttl'] = ((pttl_cmd_res == -1) ? Float::INFINITY : pttl_cmd_res)
@@ -73,5 +100,68 @@ module RedisQueuedLocks::Acquirer::LockInfo
73
100
  end
74
101
  end
75
102
  # rubocop:enable Metrics/MethodLength
103
+
104
+ private
105
+
106
+ # Formats live read locks of the lock (expired read locks are ignored).
107
+ #
108
+ # @param redis_client [RedisClient]
109
+ # @param lock_name [String]
110
+ # @param lock_key [String]
111
+ # @param redis_time [Array<String>] Result of the redis `TIME` command.
112
+ # @param read_locks [Array<Array<String,Float>>] Readers registry (acquirer id, expiration).
113
+ # @return [Hash<String,String|Numeric|Array<Hash<String,String|Numeric>>>,NilClass]
114
+ # - `nil` is returned when there are no live read locks;
115
+ # - each reader has the same data as the write lock (acq_id, hst_id, ts, ini_ttl, rem_ttl,
116
+ # meta, reentrant lock data);
117
+ #
118
+ # @api private
119
+ # @since 1.18.0
120
+ # rubocop:disable Metrics/MethodLength
121
+ def read_lock_info(redis_client, lock_name, lock_key, redis_time, read_locks)
122
+ now = RedisQueuedLocks::Resource.redis_time_ms(redis_time)
123
+ live_read_locks = read_locks.select { |(_acquirer_id, expiration)| expiration > now }
124
+ return nil if live_read_locks.empty?
125
+
126
+ # NOTE: read lock data (it can be absent if it is dropped during the read lock life)
127
+ # @type var read_locks_data: Array[Hash[String,String]]
128
+ read_locks_data = redis_client.pipelined do |pipeline|
129
+ live_read_locks.each do |(acquirer_id, _expiration)|
130
+ pipeline.call(
131
+ 'HGETALL',
132
+ RedisQueuedLocks::Resource.prepare_read_lock_key(lock_name, acquirer_id)
133
+ )
134
+ end
135
+ end
136
+
137
+ longest_rem_ttl = 0
138
+ readers = live_read_locks.each_with_index.map do |(acquirer_id, expiration), index|
139
+ rem_ttl = (expiration - now).ceil
140
+ longest_rem_ttl = rem_ttl if rem_ttl > longest_rem_ttl
141
+
142
+ # @type var reader: Hash[String,String|Float|Integer|nil]
143
+ reader = read_locks_data[index] || {}
144
+ reader['acq_id'] = acquirer_id
145
+ reader['hst_id'] ||= RedisQueuedLocks::Resource.host_identifier_from_acquirer(acquirer_id)
146
+ reader['ts'] = Float(reader['ts']) if reader['ts']
147
+ reader['ini_ttl'] = Integer(reader['ini_ttl']) if reader['ini_ttl']
148
+ reader['rem_ttl'] = rem_ttl
149
+ reader['spc_cnt'] = Integer(reader['spc_cnt']) if reader['spc_cnt']
150
+ reader['l_spc_ts'] = Float(reader['l_spc_ts']) if reader['l_spc_ts']
151
+ reader['spc_ext_ttl'] = Integer(reader['spc_ext_ttl']) if reader['spc_ext_ttl']
152
+ reader['l_spc_ext_ini_ttl'] =
153
+ Integer(reader['l_spc_ext_ini_ttl']) if reader['l_spc_ext_ini_ttl']
154
+ reader['l_spc_ext_ts'] = Float(reader['l_spc_ext_ts']) if reader['l_spc_ext_ts']
155
+ reader
156
+ end
157
+
158
+ {
159
+ 'lock_key' => lock_key,
160
+ 'rw_mode' => 'read',
161
+ 'rem_ttl' => longest_rem_ttl,
162
+ 'readers' => readers
163
+ }
164
+ end
165
+ # rubocop:enable Metrics/MethodLength
76
166
  end
77
167
  end
@@ -1,18 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # NOTE: Lock Series PoC
4
- # steep:ignore
5
4
  # @api private
6
5
  # @since 1.16.1
7
6
  module RedisQueuedLocks::Acquirer::LockSeriesPoC::InstrVisitor # steep:ignore
8
7
  class << self
9
8
  # NOTE: Lock Series PoC
9
+ # @param rw_mode [Symbol] The mode of the lock series (`:read` or `:write`).
10
10
  # @api private
11
11
  # @since 1.16.1
12
+ # @version 1.18.0
12
13
  def lock_series_obtained( # steep:ignore
13
14
  instrumenter,
14
15
  instr_sampled,
15
16
  lock_keys,
17
+ rw_mode,
16
18
  ttl,
17
19
  acq_id,
18
20
  hst_id,
@@ -22,17 +24,20 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC::InstrVisitor # steep:ignore
22
24
  )
23
25
  return unless instr_sampled
24
26
  instrumenter.notify('redis_queued_locks.lock_series_obtained', {
25
- lock_keys:, ttl:, acq_id:, hst_id:, ts:, acq_time:, instrument:
27
+ lock_keys:, rw_mode:, ttl:, acq_id:, hst_id:, ts:, acq_time:, instrument:
26
28
  }) rescue nil
27
29
  end
28
30
 
29
31
  # NOTE: Lock Series PoC
32
+ # @param rw_mode [Symbol] The mode of the lock series (`:read` or `:write`).
30
33
  # @api private
31
34
  # @since 1.16.1
35
+ # @version 1.18.0
32
36
  def lock_series_hold_and_release( # steep:ignore
33
37
  instrumenter,
34
38
  instr_sampled,
35
39
  lock_keys,
40
+ rw_mode,
36
41
  ttl,
37
42
  acq_id,
38
43
  hst_id,
@@ -43,9 +48,8 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC::InstrVisitor # steep:ignore
43
48
  )
44
49
  return unless instr_sampled
45
50
  instrumenter.notify('redis_queued_locks.lock_series_hold_and_release', {
46
- lock_keys:, hold_time:, ttl:, acq_id:, hst_id:, ts:, acq_time:, instrument:
51
+ lock_keys:, rw_mode:, hold_time:, ttl:, acq_id:, hst_id:, ts:, acq_time:, instrument:
47
52
  }) # rescue nil
48
53
  end
49
54
  end
50
55
  end
51
- # steep:ignore
@@ -1,14 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # NOTE: Lock Series PoC
4
- # steep:ignore
5
4
  # @api private
6
5
  # @since 1.16.1
7
6
  module RedisQueuedLocks::Acquirer::LockSeriesPoC::LogVisitor # steep:ignore
8
7
  class << self
9
8
  # NOTE: Lock Series PoC
9
+ # @param rw_mode [Symbol] The mode of the lock series (`:read` or `:write`).
10
10
  # @api private
11
11
  # @since 1.16.0
12
+ # @version 1.18.0
12
13
  def start_lock_series_obtaining( # steep:ignore
13
14
  logger,
14
15
  log_sampled,
@@ -16,23 +17,27 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC::LogVisitor # steep:ignore
16
17
  queue_ttl,
17
18
  acquirer_id,
18
19
  host_id,
19
- access_strategy
20
+ access_strategy,
21
+ rw_mode
20
22
  )
21
23
  return unless log_sampled
22
24
 
23
25
  logger.debug do
24
26
  "[redis_queued_locks.start_lock_series_obtaining] " \
25
- "lock_keys => '#{lock_keys.inspect}'" \
27
+ "lock_keys => '#{lock_keys.inspect}' " \
26
28
  "queue_ttl => #{queue_ttl} " \
27
29
  "acq_id => '#{acquirer_id}' " \
28
30
  "hst_id => '#{host_id}' " \
29
- "acs_strat => '#{access_strategy}'"
31
+ "acs_strat => '#{access_strategy}' " \
32
+ "rw_mode => '#{rw_mode}'"
30
33
  end # rescue nil
31
34
  end
32
35
 
33
36
  # NOTE: Lock Series PoC
37
+ # @param rw_mode [Symbol] The mode of the lock series (`:read` or `:write`).
34
38
  # @api private
35
39
  # @since 1.16.0
40
+ # @version 1.18.0
36
41
  def lock_series_obtained( # steep:ignore
37
42
  logger,
38
43
  log_sampled,
@@ -41,7 +46,8 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC::LogVisitor # steep:ignore
41
46
  acquirer_id,
42
47
  host_id,
43
48
  acq_time,
44
- access_strategy
49
+ access_strategy,
50
+ rw_mode
45
51
  )
46
52
  return unless log_sampled
47
53
 
@@ -52,13 +58,16 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC::LogVisitor # steep:ignore
52
58
  "acq_id => '#{acquirer_id}' " \
53
59
  "hst_id => '#{host_id}' " \
54
60
  "acs_strat => '#{access_strategy}' " \
61
+ "rw_mode => '#{rw_mode}' " \
55
62
  "acq_time => #{acq_time} (ms)"
56
63
  end # rescue nil
57
64
  end
58
65
 
59
66
  # NOTE: Lock Series PoC
67
+ # @param rw_mode [Symbol] The mode of the lock series (`:read` or `:write`).
60
68
  # @api private
61
69
  # @since 1.16.0
70
+ # @version 1.18.0
62
71
  def expire_lock_series( # steep:ignore
63
72
  logger,
64
73
  log_sampled,
@@ -66,7 +75,8 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC::LogVisitor # steep:ignore
66
75
  queue_ttl,
67
76
  acquirer_id,
68
77
  host_id,
69
- access_strategy
78
+ access_strategy,
79
+ rw_mode
70
80
  )
71
81
  return unless log_sampled
72
82
 
@@ -76,7 +86,8 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC::LogVisitor # steep:ignore
76
86
  "queue_ttl => #{queue_ttl} " \
77
87
  "acq_id => '#{acquirer_id}' " \
78
88
  "hst_id => '#{host_id}' " \
79
- "acs_strat => '#{access_strategy}'"
89
+ "acs_strat => '#{access_strategy}' " \
90
+ "rw_mode => '#{rw_mode}'"
80
91
  end rescue nil
81
92
  end
82
93
  end