redis_queued_locks 1.19.0 → 1.21.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 +96 -23
  3. data/.claude/rules/acquirer.md +39 -20
  4. data/.claude/rules/arguments.md +3 -2
  5. data/.claude/rules/git.md +3 -2
  6. data/.claude/rules/logic.md +32 -8
  7. data/.claude/rules/swarm.md +14 -9
  8. data/.claude/rules/tests.md +12 -3
  9. data/.claude/rules/type-checking.md +2 -1
  10. data/.claude/rules/visitors.md +10 -11
  11. data/.claude/skills/rql-code-review/SKILL.md +386 -0
  12. data/.claude/skills/rql-code-review/templates/report.en.md +31 -0
  13. data/.claude/skills/rql-code-review/templates/report.ru.md +31 -0
  14. data/CHANGELOG.md +194 -0
  15. data/CLAUDE.md +6 -3
  16. data/README.md +502 -92
  17. data/lib/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue/log_visitor.rb +1 -0
  18. data/lib/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rb +42 -0
  19. data/lib/redis_queued_locks/acquirer/acquire_lock/log_visitor.rb +45 -0
  20. data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock/log_visitor.rb +1 -0
  21. data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rb +695 -128
  22. data/lib/redis_queued_locks/acquirer/acquire_lock/with_acq_timeout.rb +2 -2
  23. data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire/log_visitor.rb +1 -0
  24. data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire.rb +8 -6
  25. data/lib/redis_queued_locks/acquirer/acquire_lock.rb +134 -26
  26. data/lib/redis_queued_locks/acquirer/clear_dead_requests.rb +229 -24
  27. data/lib/redis_queued_locks/acquirer/extend_lock_ttl.rb +189 -18
  28. data/lib/redis_queued_locks/acquirer/is_locked.rb +0 -1
  29. data/lib/redis_queued_locks/acquirer/is_queued.rb +0 -1
  30. data/lib/redis_queued_locks/acquirer/keys.rb +151 -7
  31. data/lib/redis_queued_locks/acquirer/lock_info.rb +39 -41
  32. data/lib/redis_queued_locks/acquirer/lock_series_poc/instr_visitor.rb +3 -2
  33. data/lib/redis_queued_locks/acquirer/lock_series_poc/log_visitor.rb +5 -4
  34. data/lib/redis_queued_locks/acquirer/lock_series_poc.rb +79 -20
  35. data/lib/redis_queued_locks/acquirer/locks.rb +212 -83
  36. data/lib/redis_queued_locks/acquirer/queue_info.rb +11 -18
  37. data/lib/redis_queued_locks/acquirer/queues.rb +123 -50
  38. data/lib/redis_queued_locks/acquirer/reindex_locks.rb +253 -0
  39. data/lib/redis_queued_locks/acquirer/release_all_locks.rb +177 -61
  40. data/lib/redis_queued_locks/acquirer/release_lock.rb +84 -25
  41. data/lib/redis_queued_locks/acquirer/release_locks_of.rb +274 -70
  42. data/lib/redis_queued_locks/acquirer/release_read_lock.rb +22 -3
  43. data/lib/redis_queued_locks/acquirer.rb +1 -0
  44. data/lib/redis_queued_locks/client.rb +164 -36
  45. data/lib/redis_queued_locks/config.rb +16 -13
  46. data/lib/redis_queued_locks/errors.rb +34 -4
  47. data/lib/redis_queued_locks/instrument/sampler.rb +4 -7
  48. data/lib/redis_queued_locks/logging/sampler.rb +4 -8
  49. data/lib/redis_queued_locks/resource.rb +113 -6
  50. data/lib/redis_queued_locks/swarm/flush_zombies.rb +333 -61
  51. data/lib/redis_queued_locks/swarm/redis_client_builder.rb +8 -4
  52. data/lib/redis_queued_locks/swarm/swarm_element/threaded.rb +3 -2
  53. data/lib/redis_queued_locks/swarm/zombie_info.rb +203 -53
  54. data/lib/redis_queued_locks/swarm.rb +97 -5
  55. data/lib/redis_queued_locks/utilities.rb +1 -1
  56. data/lib/redis_queued_locks/version.rb +2 -2
  57. data/sig/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rbs +14 -0
  58. data/sig/redis_queued_locks/acquirer/acquire_lock/log_visitor.rbs +13 -0
  59. data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rbs +107 -1
  60. data/sig/redis_queued_locks/acquirer/acquire_lock/yield_expire.rbs +3 -3
  61. data/sig/redis_queued_locks/acquirer/acquire_lock.rbs +3 -1
  62. data/sig/redis_queued_locks/acquirer/clear_dead_requests.rbs +19 -1
  63. data/sig/redis_queued_locks/acquirer/extend_lock_ttl.rbs +20 -4
  64. data/sig/redis_queued_locks/acquirer/keys.rbs +11 -1
  65. data/sig/redis_queued_locks/acquirer/locks.rbs +15 -6
  66. data/sig/redis_queued_locks/acquirer/queues.rbs +9 -2
  67. data/sig/redis_queued_locks/acquirer/reindex_locks.rbs +52 -0
  68. data/sig/redis_queued_locks/acquirer/release_all_locks.rbs +8 -1
  69. data/sig/redis_queued_locks/acquirer/release_lock.rbs +6 -3
  70. data/sig/redis_queued_locks/acquirer/release_locks_of.rbs +20 -1
  71. data/sig/redis_queued_locks/acquirer/release_read_lock.rbs +1 -1
  72. data/sig/redis_queued_locks/client.rbs +21 -2
  73. data/sig/redis_queued_locks/errors.rbs +5 -2
  74. data/sig/redis_queued_locks/instrument/sampler.rbs +0 -2
  75. data/sig/redis_queued_locks/logging/sampler.rbs +0 -2
  76. data/sig/redis_queued_locks/resource.rbs +7 -1
  77. data/sig/redis_queued_locks/swarm/flush_zombies.rbs +51 -1
  78. data/sig/redis_queued_locks/swarm/zombie_info.rbs +58 -7
  79. data/sig/vendor/redis_client.rbs +2 -0
  80. metadata +6 -1
@@ -2,7 +2,7 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.0.0
5
- # @version 1.18.0
5
+ # @version 1.21.0
6
6
  # rubocop:disable Metrics/ModuleLength
7
7
  module RedisQueuedLocks::Acquirer::ExtendLockTTL
8
8
  # @return [String]
@@ -14,11 +14,18 @@ module RedisQueuedLocks::Acquirer::ExtendLockTTL
14
14
  return redis.call("PEXPIRE", KEYS[1], new_lock_pttl);
15
15
  LUA_SCRIPT
16
16
 
17
+ # @since 1.21.0
18
+ extend RedisQueuedLocks::Utilities
19
+
17
20
  # rubocop:disable Metrics/ClassLength
18
21
  class << self
22
+ # Extends the lock TTL and reports the operation (one log and one instrumentation event
23
+ # `redis_queued_locks.extend_lock_ttl` with the time of the whole operation).
24
+ #
19
25
  # @param redis_client [RedisClient]
20
26
  # @param lock_name [String]
21
27
  # @param milliseconds [Integer]
28
+ # How many milliseconds should be added: a positive Integer.
22
29
  # @param read_write_mode [Symbol]
23
30
  # - `:write` - extends the write lock (of any acquirer);
24
31
  # - `:read` - extends the read lock of the given acquirer (other read locks are not affected)
@@ -28,6 +35,8 @@ module RedisQueuedLocks::Acquirer::ExtendLockTTL
28
35
  # (used for `:read` mode only, ignored for `:write` mode).
29
36
  # @param acquirer_id [String]
30
37
  # The read lock acquirer (used for `:read` mode without `all_read_locks` only).
38
+ # @param lock_index_enabled [Boolean]
39
+ # Increase the lock index score of the lock name by the extension (the enabled lock index).
31
40
  # @param logger [::Logger,#debug]
32
41
  # @param instrumenter [#notify]
33
42
  # @param instrument [NilClass,Any]
@@ -43,7 +52,7 @@ module RedisQueuedLocks::Acquirer::ExtendLockTTL
43
52
  #
44
53
  # @api private
45
54
  # @since 1.0.0
46
- # @version 1.18.0
55
+ # @version 1.21.0
47
56
  # rubocop:disable Metrics/MethodLength
48
57
  def extend_lock_ttl(
49
58
  redis_client,
@@ -52,6 +61,7 @@ module RedisQueuedLocks::Acquirer::ExtendLockTTL
52
61
  read_write_mode,
53
62
  all_read_locks,
54
63
  acquirer_id,
64
+ lock_index_enabled,
55
65
  logger,
56
66
  instrumenter,
57
67
  instrument,
@@ -72,31 +82,144 @@ module RedisQueuedLocks::Acquirer::ExtendLockTTL
72
82
  )
73
83
  end
74
84
 
85
+ # NOTE: a non-positive value would decrease the lock ttl or delete the lock;
86
+ if !milliseconds.is_a?(::Integer) || milliseconds <= 0
87
+ raise(
88
+ RedisQueuedLocks::ArgumentError,
89
+ "`milliseconds` argument should be a positive Integer, got #{milliseconds.inspect}."
90
+ )
91
+ end
92
+
93
+ if read_write_mode == :read && all_read_locks != true && all_read_locks != false
94
+ raise(
95
+ RedisQueuedLocks::ArgumentError,
96
+ "`:all_read_locks` argument should be a boolean, got #{all_read_locks.inspect}."
97
+ )
98
+ end
99
+
75
100
  lock_key = RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
76
101
 
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
- )
102
+ ext_start_time = clock_gettime
103
+ result = fully_extend_lock_ttl(
104
+ redis_client, lock_name, lock_key, milliseconds, read_write_mode, all_read_locks,
105
+ acquirer_id, lock_index_enabled
106
+ )
107
+ time_at = Time.now.to_f
108
+ ext_end_time = clock_gettime
109
+ ext_time = ((ext_end_time - ext_start_time) / 1_000.0).ceil(2)
110
+ ext_result = result[:result]
111
+ extended_locks_count = ext_result.is_a?(::Hash) ? ext_result[:extended_locks_count] : 0
112
+
113
+ log_sampled = RedisQueuedLocks::Logging.should_log?(
114
+ log_sampling_enabled,
115
+ log_sample_this,
116
+ log_sampling_percent,
117
+ log_sampler
118
+ )
119
+ instr_sampled = RedisQueuedLocks::Instrument.should_instrument?(
120
+ instr_sampling_enabled,
121
+ instr_sample_this,
122
+ instr_sampling_percent,
123
+ instr_sampler
124
+ )
125
+
126
+ run_non_critical do
127
+ logger.debug do
128
+ "[redis_queued_locks.extend_lock_ttl] " \
129
+ "lock_key => '#{lock_key}' " \
130
+ "rw_mode => '#{read_write_mode}' " \
131
+ "all_read_locks => #{all_read_locks} " \
132
+ "ext_ttl => #{milliseconds} " \
133
+ "extended_locks_count => #{extended_locks_count} " \
134
+ "ext_time => #{ext_time} " \
135
+ "at => #{time_at}"
83
136
  end
137
+ end if log_sampled
138
+
139
+ run_non_critical do
140
+ instrumenter.notify('redis_queued_locks.extend_lock_ttl', {
141
+ lock_key: lock_key,
142
+ rw_mode: read_write_mode,
143
+ all_read_locks: all_read_locks,
144
+ ext_ttl: milliseconds,
145
+ extended_locks_count: extended_locks_count,
146
+ ext_time: ext_time,
147
+ at: time_at,
148
+ instrument: instrument
149
+ })
150
+ end if instr_sampled
151
+
152
+ result
153
+ end
154
+ # rubocop:enable Metrics/MethodLength
155
+
156
+ private
84
157
 
158
+ # Extends the write lock, the read lock of the acquirer or all read locks of the lock.
159
+ #
160
+ # @param redis_client [RedisClient]
161
+ # @param lock_name [String]
162
+ # @param lock_key [String]
163
+ # @param milliseconds [Integer]
164
+ # @param read_write_mode [Symbol]
165
+ # @param all_read_locks [Boolean]
166
+ # @param acquirer_id [String]
167
+ # @param lock_index_enabled [Boolean]
168
+ # @return [Hash<Symbol,Boolean|Symbol|Hash<Symbol,Integer>>]
169
+ #
170
+ # @api private
171
+ # @since 1.21.0
172
+ # rubocop:disable Metrics/MethodLength
173
+ def fully_extend_lock_ttl(
174
+ redis_client,
175
+ lock_name,
176
+ lock_key,
177
+ milliseconds,
178
+ read_write_mode,
179
+ all_read_locks,
180
+ acquirer_id,
181
+ lock_index_enabled
182
+ )
183
+ if read_write_mode == :read
85
184
  # NOTE: (RW) all live read locks of the lock
86
185
  if all_read_locks
87
- return extend_all_read_locks_ttl(redis_client, lock_name, lock_key, milliseconds)
186
+ return extend_all_read_locks_ttl(
187
+ redis_client, lock_name, lock_key, milliseconds, lock_index_enabled
188
+ )
88
189
  end
89
190
 
90
191
  # NOTE: (RW) read lock of the concrete acquirer
91
- return extend_read_lock_ttl(redis_client, lock_name, lock_key, acquirer_id, milliseconds)
192
+ return extend_read_lock_ttl(
193
+ redis_client, lock_name, lock_key, acquirer_id, milliseconds, lock_index_enabled
194
+ )
92
195
  end
93
196
 
94
- # NOTE: EVAL signature -> <lua script>, (number of keys), *(keys), *(arguments)
95
- result = redis_client.call('EVAL', EXTEND_LOCK_PTTL, 1, lock_key, milliseconds)
197
+ # NOTE:
198
+ # - EVAL signature -> <lua script>, (number of keys), *(keys), *(arguments);
199
+ # - (lock index) the enabled lock index: the lock index score is increased by the extension
200
+ # in the same transaction (`ZADD XX INCR`: the upper bound of the lock lifetime stays
201
+ # the upper bound, locks without the index entry (of older RQL versions) are not indexed
202
+ # with a wrong score); a failed extension (no lock) increases the score of the lock name
203
+ # too: the name just stays in the index a bit longer;
204
+ # - the disabled lock index: one EVAL command (no transaction);
205
+ # @type var extended: Integer
206
+ extended =
207
+ if lock_index_enabled
208
+ # @type var result: [Integer, Float?]
209
+ result = redis_client.multi do |transact|
210
+ transact.call('EVAL', EXTEND_LOCK_PTTL, 1, lock_key, milliseconds)
211
+ transact.call(
212
+ 'ZADD', RedisQueuedLocks::Resource::LOCK_INDEX_KEY,
213
+ 'XX', 'INCR', milliseconds, lock_name
214
+ )
215
+ end
216
+ result[0]
217
+ else
218
+ redis_client.call('EVAL', EXTEND_LOCK_PTTL, 1, lock_key, milliseconds)
219
+ end
96
220
  # TODO: upload scripts to the redis
97
221
 
98
- # @type var result: Integer
99
- if result == 1
222
+ if extended == 1
100
223
  { ok: true, result: { extended_locks_count: 1 } }
101
224
  else
102
225
  { ok: false, result: :async_expire_or_no_lock }
@@ -104,8 +227,6 @@ module RedisQueuedLocks::Acquirer::ExtendLockTTL
104
227
  end
105
228
  # rubocop:enable Metrics/MethodLength
106
229
 
107
- private
108
-
109
230
  # Extends the live read lock of the acquirer:
110
231
  # - the read lock expiration (readers registry score), the registry TTL and the read lock
111
232
  # data TTL are extended;
@@ -117,12 +238,21 @@ module RedisQueuedLocks::Acquirer::ExtendLockTTL
117
238
  # @param lock_key [String]
118
239
  # @param acquirer_id [String]
119
240
  # @param milliseconds [Integer]
241
+ # @param lock_index_enabled [Boolean]
120
242
  # @return [Hash<Symbol,Boolean|Symbol|Hash<Symbol,Integer>>]
121
243
  #
122
244
  # @api private
123
245
  # @since 1.18.0
246
+ # @version 1.21.0
124
247
  # rubocop:disable Metrics/MethodLength
125
- def extend_read_lock_ttl(redis_client, lock_name, lock_key, acquirer_id, milliseconds)
248
+ def extend_read_lock_ttl(
249
+ redis_client,
250
+ lock_name,
251
+ lock_key,
252
+ acquirer_id,
253
+ milliseconds,
254
+ lock_index_enabled
255
+ )
126
256
  lock_readers_key = RedisQueuedLocks::Resource.prepare_lock_readers(lock_name)
127
257
  read_lock_key = RedisQueuedLocks::Resource.prepare_read_lock_key(lock_name, acquirer_id)
128
258
 
@@ -145,8 +275,24 @@ module RedisQueuedLocks::Acquirer::ExtendLockTTL
145
275
  transact.call('PEXPIRE', lock_readers_key, extended_ttl, 'GT')
146
276
  transact.call('PEXPIRE', read_lock_key, extended_ttl, 'NX')
147
277
  transact.call('PEXPIRE', read_lock_key, extended_ttl, 'GT')
278
+ # NOTE: (lock index) the lock index score is increased by the extension
279
+ # (the enabled lock index only; the last command: its reply is not read);
280
+ if lock_index_enabled
281
+ transact.call(
282
+ 'ZADD', RedisQueuedLocks::Resource::LOCK_INDEX_KEY,
283
+ 'XX', 'INCR', milliseconds, lock_name
284
+ )
285
+ end
148
286
  end
149
287
  end
288
+ rescue RedisQueuedLocks::LockAcquirementIntermediateTimeoutError,
289
+ RedisQueuedLocks::TimedLockIntermediateTimeoutError,
290
+ ::Timeout::ExitException
291
+ # NOTE: (timeouts) an interrupted WATCH block (the internal timeouts and the user's
292
+ # `Timeout.timeout` without an exception class): the closed connection drops the WATCH
293
+ # state (redis-client sends `UNWATCH` only on `StandardError`);
294
+ rconn.close
295
+ raise
150
296
  end
151
297
 
152
298
  # NOTE:
@@ -174,12 +320,20 @@ module RedisQueuedLocks::Acquirer::ExtendLockTTL
174
320
  # @param lock_name [String]
175
321
  # @param lock_key [String]
176
322
  # @param milliseconds [Integer]
323
+ # @param lock_index_enabled [Boolean]
177
324
  # @return [Hash<Symbol,Boolean|Symbol|Hash<Symbol,Integer>>]
178
325
  #
179
326
  # @api private
180
327
  # @since 1.18.0
328
+ # @version 1.21.0
181
329
  # rubocop:disable Metrics/MethodLength
182
- def extend_all_read_locks_ttl(redis_client, lock_name, lock_key, milliseconds)
330
+ def extend_all_read_locks_ttl(
331
+ redis_client,
332
+ lock_name,
333
+ lock_key,
334
+ milliseconds,
335
+ lock_index_enabled
336
+ )
183
337
  lock_readers_key = RedisQueuedLocks::Resource.prepare_lock_readers(lock_name)
184
338
  extended_read_locks_count = 0
185
339
 
@@ -214,9 +368,26 @@ module RedisQueuedLocks::Acquirer::ExtendLockTTL
214
368
  if extended_read_locks_count > 0
215
369
  transact.call('PEXPIRE', lock_readers_key, registry_ttl, 'NX')
216
370
  transact.call('PEXPIRE', lock_readers_key, registry_ttl, 'GT')
371
+ # NOTE: (lock index) the lock index score is increased by the extension
372
+ # (each read lock is extended by the same milliseconds; the enabled lock index
373
+ # only; the last command: its reply is not read);
374
+ if lock_index_enabled
375
+ transact.call(
376
+ 'ZADD', RedisQueuedLocks::Resource::LOCK_INDEX_KEY,
377
+ 'XX', 'INCR', milliseconds, lock_name
378
+ )
379
+ end
217
380
  end
218
381
  end
219
382
  end
383
+ rescue RedisQueuedLocks::LockAcquirementIntermediateTimeoutError,
384
+ RedisQueuedLocks::TimedLockIntermediateTimeoutError,
385
+ ::Timeout::ExitException
386
+ # NOTE: (timeouts) an interrupted WATCH block (the internal timeouts and the user's
387
+ # `Timeout.timeout` without an exception class): the closed connection drops the WATCH
388
+ # state (redis-client sends `UNWATCH` only on `StandardError`);
389
+ rconn.close
390
+ raise
220
391
  end
221
392
 
222
393
  # NOTE:
@@ -2,7 +2,6 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.0.0
5
- # @version 1.18.0
6
5
  module RedisQueuedLocks::Acquirer::IsLocked
7
6
  class << self
8
7
  # @param redis_client [RedisClient]
@@ -2,7 +2,6 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.0.0
5
- # @version 1.18.0
6
5
  module RedisQueuedLocks::Acquirer::IsQueued
7
6
  class << self
8
7
  # @param redis_client [RedisClient]
@@ -2,25 +2,169 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.0.0
5
+ # @version 1.20.0
5
6
  module RedisQueuedLocks::Acquirer::Keys
7
+ # The number of replies of one lock name in the key state pipeline of a batch of lock names
8
+ # (`collect_lock_keys`): EXISTS (write lock), EXISTS (write lock queue), EXISTS (read lock
9
+ # queue), ZRANGE (readers registry).
10
+ #
11
+ # @return [Integer]
12
+ #
13
+ # @api private
14
+ # @since 1.20.0
15
+ KEY_STATE_REPLIES = 4
16
+
6
17
  class << self
18
+ # RQL keys of the locks:
19
+ # - the disabled lock index: all the RQL keys (`RedisQueuedLocks::Resource::KEY_PATTERN`,
20
+ # `rql:lock*`) found in one SCAN pass over the keyspace;
21
+ # - the enabled lock index: all the existing keys of the locks of the lock index (write locks,
22
+ # lock queues, readers registries, read lock queues, read lock data): one ZSCAN pass over
23
+ # the lock names (`RedisQueuedLocks::Resource::LOCK_INDEX_KEY`, the keyspace is never
24
+ # scanned), lock keys are derived from the lock names;
25
+ #
7
26
  # @param redis_client [RedisClient]
8
27
  # @option scan_size [Integer]
28
+ # SCAN count (the disabled lock index) or ZSCAN count and the number of lock names in one
29
+ # batch (the enabled lock index).
30
+ # @option lock_index_enabled [Boolean]
9
31
  # @return [Set<String>]
10
32
  #
11
33
  # @api private
12
34
  # @since 1.0.0
13
- def keys(redis_client, scan_size:)
35
+ # @version 1.21.0
36
+ def keys(redis_client, scan_size:, lock_index_enabled:)
37
+ # NOTE: (arguments) the same range as the config key: zero/negative sizes fail SCAN/ZSCAN
38
+ # (`COUNT 0`) and the batching (`each_slice(0)`);
39
+ if !scan_size.is_a?(::Integer) || scan_size <= 0
40
+ raise(
41
+ RedisQueuedLocks::ArgumentError,
42
+ "`:scan_size` argument should be a positive Integer, got #{scan_size.inspect}."
43
+ )
44
+ end
45
+
14
46
  Set.new.tap do |keyset|
15
47
  # @type var keyset: Set[String]
16
- redis_client.scan(
17
- 'MATCH',
18
- RedisQueuedLocks::Resource::KEY_PATTERN,
19
- count: scan_size
20
- ) do |key|
21
- keyset.add(key)
48
+ if lock_index_enabled
49
+ redis_client.with do |rconn|
50
+ each_lock_name_batch(rconn, scan_size) do |lock_names|
51
+ collect_lock_keys(rconn, lock_names, keyset)
52
+ end
53
+ end
54
+ else
55
+ redis_client.scan(
56
+ 'MATCH',
57
+ RedisQueuedLocks::Resource::KEY_PATTERN,
58
+ count: scan_size
59
+ ) do |key|
60
+ keyset.add(key)
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ # (lock index) Adds the existing keys of the given batch of lock names to the key set
69
+ # (one round trip for the lock keys, one more for the read lock data when read locks exist).
70
+ #
71
+ # @param redis_client [RedisClient]
72
+ # @param lock_names [Array<String>]
73
+ # @param keyset [Set<String>]
74
+ # @return [void]
75
+ #
76
+ # @api private
77
+ # @since 1.20.0
78
+ # rubocop:disable Metrics/MethodLength
79
+ def collect_lock_keys(redis_client, lock_names, keyset)
80
+ # Step 1: the state of the lock keys of the batch
81
+ # NOTE: (pipeline layout) `KEY_STATE_REPLIES` replies per lock name in the batch order
82
+ # (the replies of the lock name with the index N start at `N * KEY_STATE_REPLIES`):
83
+ # EXISTS (write lock), EXISTS (write lock queue), EXISTS (read lock queue),
84
+ # ZRANGE (readers registry: redis deletes empty sorted sets, so an empty result means
85
+ # the registry does not exist);
86
+ # @type var key_state: Array[untyped]
87
+ key_state = redis_client.pipelined do |pipeline|
88
+ lock_names.each do |lock_name|
89
+ pipeline.call('EXISTS', RedisQueuedLocks::Resource.prepare_lock_key(lock_name))
90
+ pipeline.call('EXISTS', RedisQueuedLocks::Resource.prepare_lock_queue(lock_name))
91
+ pipeline.call('EXISTS', RedisQueuedLocks::Resource.prepare_read_lock_queue(lock_name))
92
+ pipeline.call(
93
+ 'ZRANGE', RedisQueuedLocks::Resource.prepare_lock_readers(lock_name), '0', '-1'
94
+ )
22
95
  end
23
96
  end
97
+
98
+ # @type var read_lock_keys: Array[String]
99
+ read_lock_keys = []
100
+ lock_names.each_with_index do |lock_name, index|
101
+ replies_at = index * KEY_STATE_REPLIES
102
+ if key_state[replies_at] == 1
103
+ keyset.add(RedisQueuedLocks::Resource.prepare_lock_key(lock_name))
104
+ end
105
+ if key_state[replies_at + 1] == 1
106
+ keyset.add(RedisQueuedLocks::Resource.prepare_lock_queue(lock_name))
107
+ end
108
+ if key_state[replies_at + 2] == 1
109
+ keyset.add(RedisQueuedLocks::Resource.prepare_read_lock_queue(lock_name))
110
+ end
111
+ # @type var read_lock_acquirers: Array[String]
112
+ read_lock_acquirers = key_state[replies_at + 3]
113
+ next if read_lock_acquirers.empty?
114
+
115
+ keyset.add(RedisQueuedLocks::Resource.prepare_lock_readers(lock_name))
116
+ read_lock_acquirers.each do |acquirer_id|
117
+ read_lock_keys << RedisQueuedLocks::Resource.prepare_read_lock_key(lock_name, acquirer_id)
118
+ end
119
+ end
120
+ return if read_lock_keys.empty?
121
+
122
+ # Step 2: (RW) read lock data of the readers registries (it can be absent: the data of an
123
+ # expired read lock is expired before the registry cleanup)
124
+ # NOTE: (pipeline layout) one EXISTS reply per read lock data key
125
+ # @type var read_lock_keys_existence: Array[Integer]
126
+ read_lock_keys_existence = redis_client.pipelined do |pipeline|
127
+ read_lock_keys.each { |read_lock_key| pipeline.call('EXISTS', read_lock_key) }
128
+ end
129
+ read_lock_keys.each_with_index do |read_lock_key, index|
130
+ keyset.add(read_lock_key) if read_lock_keys_existence[index] == 1
131
+ end
132
+ end
133
+ # rubocop:enable Metrics/MethodLength
134
+
135
+ # Iterates over the lock names of the lock index (one ZSCAN pass) in batches of `scan_size`
136
+ # lock names (batches are collected across ZSCAN pages). Expired lock names (not dropped from
137
+ # the index yet) are skipped: all their keys are expired.
138
+ #
139
+ # NOTE: each operation module iterates over the lock index by itself (duplicated on purpose:
140
+ # operation modules are independent of each other, see `Locks.each_lock_name_batch`);
141
+ #
142
+ # @param redis_client [RedisClient]
143
+ # @param scan_size [Integer] ZSCAN count and the number of lock names in one batch.
144
+ # @yield [lock_names]
145
+ # @yieldparam lock_names [Array<String>] The batch of lock names (reused between batches).
146
+ # @return [void]
147
+ #
148
+ # @api private
149
+ # @since 1.20.0
150
+ def each_lock_name_batch(redis_client, scan_size)
151
+ lock_index_time = RedisQueuedLocks::Resource.lock_index_time
152
+ # @type var lock_names: Array[String]
153
+ lock_names = []
154
+
155
+ redis_client.zscan(
156
+ RedisQueuedLocks::Resource::LOCK_INDEX_KEY, count: scan_size
157
+ ) do |lock_name, lock_index_score|
158
+ next if lock_index_score.to_f < lock_index_time
159
+
160
+ lock_names << lock_name
161
+ next if lock_names.size < scan_size
162
+
163
+ yield(lock_names)
164
+ lock_names.clear
165
+ end
166
+
167
+ yield(lock_names) unless lock_names.empty?
24
168
  end
25
169
  end
26
170
  end
@@ -56,46 +56,38 @@ module RedisQueuedLocks::Acquirer::LockInfo
56
56
  pipeline.call('ZRANGE', lock_readers_key, '0', '-1', 'WITHSCORES')
57
57
  end
58
58
 
59
- if result == nil
60
- # NOTE:
61
- # - nil result means that during transaction invocation the lock is changed (CAS):
62
- # - lock is expired;
63
- # - lock is released;
64
- # - lock is expired + re-obtained;
65
- nil
66
- else
67
- # NOTE: the result of MULTI-command is an array of results of each internal command
68
- # - result[0] (HGETALL) (Hash<String,String>)
69
- # => (will be mutated further with different value types)
70
- # - result[1] (PTTL) (Integer)
71
- # => (without any mutation, integer is atomic)
72
- # - result[2] (TIME) (Array<String>) (redis time)
73
- # - result[3] (ZRANGE) (Array<[String,Float]>) (read locks)
59
+ # NOTE: (pipeline layout) the replies of the pipeline commands (`pipelined` always returns
60
+ # an array of the replies):
61
+ # - result[0] (HGETALL) (Hash<String,String>)
62
+ # => (will be mutated further with different value types)
63
+ # - result[1] (PTTL) (Integer)
64
+ # => (without any mutation, integer is atomic)
65
+ # - result[2] (TIME) (Array<String>) (redis time)
66
+ # - result[3] (ZRANGE) (Array<[String,Float]>) (read locks)
74
67
 
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
78
- hget_cmd_res = result[0]
79
- pttl_cmd_res = result[1]
68
+ # rubocop:disable Layout/LineLength
69
+ # @type var result: [Hash[String,String|Float|Integer], Integer, Array[String], Array[[String, Float]]]
70
+ # rubocop:enable Layout/LineLength
71
+ hget_cmd_res = result[0]
72
+ pttl_cmd_res = result[1]
80
73
 
81
- if hget_cmd_res == {} || pttl_cmd_res == -2 # NOTE: key does not exist
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])
84
- else
85
- hget_cmd_res.tap do |lock_data|
86
- lock_data['lock_key'] = lock_key
87
- lock_data['rw_mode'] = 'write'
88
- lock_data['ts'] = Float(lock_data['ts'])
89
- lock_data['ini_ttl'] = Integer(lock_data['ini_ttl'])
90
- lock_data['rem_ttl'] = ((pttl_cmd_res == -1) ? Float::INFINITY : pttl_cmd_res)
91
- lock_data['spc_cnt'] = Integer(lock_data['spc_cnt']) if lock_data['spc_cnt']
92
- lock_data['l_spc_ts'] = Float(lock_data['l_spc_ts']) if lock_data['l_spc_ts']
93
- lock_data['spc_ext_ttl'] = Integer(lock_data['spc_ext_ttl']) if lock_data['spc_ext_ttl']
94
- lock_data['l_spc_ext_ini_ttl'] =
95
- Integer(lock_data['l_spc_ext_ini_ttl']) if lock_data.key?('l_spc_ext_ini_ttl')
96
- lock_data['l_spc_ext_ts'] =
97
- Float(lock_data['l_spc_ext_ts']) if lock_data['l_spc_ext_ts']
98
- end
74
+ if hget_cmd_res == {} || pttl_cmd_res == -2 # NOTE: key does not exist
75
+ # NOTE: (RW) there is no write lock => check read locks
76
+ read_lock_info(redis_client, lock_name, lock_key, result[2], result[3])
77
+ else
78
+ hget_cmd_res.tap do |lock_data|
79
+ lock_data['lock_key'] = lock_key
80
+ lock_data['rw_mode'] = 'write'
81
+ lock_data['ts'] = Float(lock_data['ts'])
82
+ lock_data['ini_ttl'] = Integer(lock_data['ini_ttl'])
83
+ lock_data['rem_ttl'] = ((pttl_cmd_res == -1) ? Float::INFINITY : pttl_cmd_res)
84
+ lock_data['spc_cnt'] = Integer(lock_data['spc_cnt']) if lock_data['spc_cnt']
85
+ lock_data['l_spc_ts'] = Float(lock_data['l_spc_ts']) if lock_data['l_spc_ts']
86
+ lock_data['spc_ext_ttl'] = Integer(lock_data['spc_ext_ttl']) if lock_data['spc_ext_ttl']
87
+ lock_data['l_spc_ext_ini_ttl'] =
88
+ Integer(lock_data['l_spc_ext_ini_ttl']) if lock_data.key?('l_spc_ext_ini_ttl')
89
+ lock_data['l_spc_ext_ts'] =
90
+ Float(lock_data['l_spc_ext_ts']) if lock_data['l_spc_ext_ts']
99
91
  end
100
92
  end
101
93
  end
@@ -117,6 +109,7 @@ module RedisQueuedLocks::Acquirer::LockInfo
117
109
  #
118
110
  # @api private
119
111
  # @since 1.18.0
112
+ # @version 1.20.0
120
113
  # rubocop:disable Metrics/MethodLength
121
114
  def read_lock_info(redis_client, lock_name, lock_key, redis_time, read_locks)
122
115
  now = RedisQueuedLocks::Resource.redis_time_ms(redis_time)
@@ -139,10 +132,15 @@ module RedisQueuedLocks::Acquirer::LockInfo
139
132
  rem_ttl = (expiration - now).ceil
140
133
  longest_rem_ttl = rem_ttl if rem_ttl > longest_rem_ttl
141
134
 
135
+ # NOTE: acquirer and host ids are not stored in the read lock data (both are derived from
136
+ # the acquirer id): they are the first fields like in the write lock info (read lock data
137
+ # of older versions contains them with the same values);
142
138
  # @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)
139
+ reader = {
140
+ 'acq_id' => acquirer_id,
141
+ 'hst_id' => RedisQueuedLocks::Resource.host_identifier_from_acquirer(acquirer_id)
142
+ }
143
+ read_locks_data[index]&.each { |field, value| reader[field] = value }
146
144
  reader['ts'] = Float(reader['ts']) if reader['ts']
147
145
  reader['ini_ttl'] = Integer(reader['ini_ttl']) if reader['ini_ttl']
148
146
  reader['rem_ttl'] = rem_ttl
@@ -3,6 +3,7 @@
3
3
  # NOTE: Lock Series PoC
4
4
  # @api private
5
5
  # @since 1.16.1
6
+ # @version 1.18.0
6
7
  module RedisQueuedLocks::Acquirer::LockSeriesPoC::InstrVisitor # steep:ignore
7
8
  class << self
8
9
  # NOTE: Lock Series PoC
@@ -32,7 +33,7 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC::InstrVisitor # steep:ignore
32
33
  # @param rw_mode [Symbol] The mode of the lock series (`:read` or `:write`).
33
34
  # @api private
34
35
  # @since 1.16.1
35
- # @version 1.18.0
36
+ # @version 1.21.0
36
37
  def lock_series_hold_and_release( # steep:ignore
37
38
  instrumenter,
38
39
  instr_sampled,
@@ -49,7 +50,7 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC::InstrVisitor # steep:ignore
49
50
  return unless instr_sampled
50
51
  instrumenter.notify('redis_queued_locks.lock_series_hold_and_release', {
51
52
  lock_keys:, rw_mode:, hold_time:, ttl:, acq_id:, hst_id:, ts:, acq_time:, instrument:
52
- }) # rescue nil
53
+ }) rescue nil
53
54
  end
54
55
  end
55
56
  end
@@ -3,13 +3,14 @@
3
3
  # NOTE: Lock Series PoC
4
4
  # @api private
5
5
  # @since 1.16.1
6
+ # @version 1.18.0
6
7
  module RedisQueuedLocks::Acquirer::LockSeriesPoC::LogVisitor # steep:ignore
7
8
  class << self
8
9
  # NOTE: Lock Series PoC
9
10
  # @param rw_mode [Symbol] The mode of the lock series (`:read` or `:write`).
10
11
  # @api private
11
12
  # @since 1.16.0
12
- # @version 1.18.0
13
+ # @version 1.21.0
13
14
  def start_lock_series_obtaining( # steep:ignore
14
15
  logger,
15
16
  log_sampled,
@@ -30,14 +31,14 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC::LogVisitor # steep:ignore
30
31
  "hst_id => '#{host_id}' " \
31
32
  "acs_strat => '#{access_strategy}' " \
32
33
  "rw_mode => '#{rw_mode}'"
33
- end # rescue nil
34
+ end rescue nil
34
35
  end
35
36
 
36
37
  # NOTE: Lock Series PoC
37
38
  # @param rw_mode [Symbol] The mode of the lock series (`:read` or `:write`).
38
39
  # @api private
39
40
  # @since 1.16.0
40
- # @version 1.18.0
41
+ # @version 1.21.0
41
42
  def lock_series_obtained( # steep:ignore
42
43
  logger,
43
44
  log_sampled,
@@ -60,7 +61,7 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC::LogVisitor # steep:ignore
60
61
  "acs_strat => '#{access_strategy}' " \
61
62
  "rw_mode => '#{rw_mode}' " \
62
63
  "acq_time => #{acq_time} (ms)"
63
- end # rescue nil
64
+ end rescue nil
64
65
  end
65
66
 
66
67
  # NOTE: Lock Series PoC