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.14.0
5
- # @version 1.18.0
5
+ # @version 1.20.0
6
6
  # rubocop:disable Metrics/ModuleLength
7
7
  module RedisQueuedLocks::Acquirer::ReleaseLocksOf
8
8
  # @since 1.14.0
@@ -20,11 +20,16 @@ module RedisQueuedLocks::Acquirer::ReleaseLocksOf
20
20
  # @param redis [RedisClient]
21
21
  # Redis connection client.
22
22
  # @param lock_scan_size [Integer]
23
- # The number of lock keys that should be released in a time.
23
+ # The number of lock keys (the disabled lock index: the SCAN count of the single SCAN pass
24
+ # over the lock keys and lock queues) or locks (lock names, the enabled lock index: the ZSCAN
25
+ # count of the single ZSCAN pass over the lock index) that should be processed in a time.
24
26
  # Affects the RubyVM memmory (cuz each found lock will be temporary stored in memory for
25
27
  # subsequent removal from redis in one query at a time).
26
28
  # @param queue_scan_size [Integer]
27
- # The number of lock queues that should be scanned removing an acquirer from them (at a time).
29
+ # The number of lock queues that should be processed removing an acquirer from them
30
+ # (in one round trip).
31
+ # @param lock_index_enabled [Boolean]
32
+ # Find locks via the lock index instead of the keyspace SCAN.
28
33
  # @param logger [::Logger,#debug]
29
34
  # - Logger object used from `configuration` layer (see config['logger']);
30
35
  # - See RedisQueuedLocks::Logging::VoidLogger for example;
@@ -39,7 +44,7 @@ module RedisQueuedLocks::Acquirer::ReleaseLocksOf
39
44
  # - works in tandem with <config['log_sampling_percent']> and <config['log_sampler']>;
40
45
  # @param log_sampling_percent [Integer]
41
46
  # - the percent of cases that should be logged;
42
- # - take an effect when <config['log_sampling_enalbed']> is true;
47
+ # - take an effect when <config['log_sampling_enabled']> is true;
43
48
  # - works in tandem with <config['log_sampling_enabled']> and <config['log_sampler']> configs;
44
49
  # @param log_sampler [#sampling_happened?,Module<RedisQueuedLocks::Logging::Sampler>]
45
50
  # - percent-based log sampler that decides should be RQL case logged or not;
@@ -60,7 +65,7 @@ module RedisQueuedLocks::Acquirer::ReleaseLocksOf
60
65
  # - works in tandem with <config['instr_sampling_percent']> and <config['instr_sampler']>;
61
66
  # @param instr_sampling_percent [Integer]
62
67
  # - the percent of cases that should be instrumented;
63
- # - take an effect when <config['instr_sampling_enalbed']> is true;
68
+ # - take an effect when <config['instr_sampling_enabled']> is true;
64
69
  # - works in tandem with <config['instr_sampling_enabled']>
65
70
  # and <config['instr_sampler']> configs;
66
71
  # @param instr_sampler [#sampling_happened?,Module<RedisQueuedLocks::Instrument::Sampler>]
@@ -88,7 +93,7 @@ module RedisQueuedLocks::Acquirer::ReleaseLocksOf
88
93
  #
89
94
  # @api private
90
95
  # @since 1.14.0
91
- # @version 1.18.0
96
+ # @version 1.21.0
92
97
  # rubocop:disable Metrics/MethodLength
93
98
  def release_locks_of(
94
99
  refused_host_id,
@@ -96,6 +101,7 @@ module RedisQueuedLocks::Acquirer::ReleaseLocksOf
96
101
  redis,
97
102
  lock_scan_size,
98
103
  queue_scan_size,
104
+ lock_index_enabled,
99
105
  logger,
100
106
  instrumenter,
101
107
  instrument,
@@ -107,7 +113,23 @@ module RedisQueuedLocks::Acquirer::ReleaseLocksOf
107
113
  instr_sampling_percent,
108
114
  instr_sampler,
109
115
  instr_sample_this
110
- ) # TODO: move from SCAN to indexes :thinking:
116
+ )
117
+ # NOTE: (arguments) the same ranges as the config keys: zero/negative sizes fail SCAN/ZSCAN
118
+ # (`COUNT 0`) and the batching (`each_slice(0)`);
119
+ if !lock_scan_size.is_a?(::Integer) || lock_scan_size <= 0
120
+ raise(
121
+ RedisQueuedLocks::ArgumentError,
122
+ "`:lock_scan_size` argument should be a positive Integer, got #{lock_scan_size.inspect}."
123
+ )
124
+ end
125
+ if !queue_scan_size.is_a?(::Integer) || queue_scan_size <= 0
126
+ raise(
127
+ RedisQueuedLocks::ArgumentError,
128
+ "`:queue_scan_size` argument should be a positive Integer, " \
129
+ "got #{queue_scan_size.inspect}."
130
+ )
131
+ end
132
+
111
133
  rel_start_time = clock_gettime
112
134
 
113
135
  # steep:ignore:start
@@ -116,7 +138,8 @@ module RedisQueuedLocks::Acquirer::ReleaseLocksOf
116
138
  refused_acquirer_id,
117
139
  redis,
118
140
  lock_scan_size,
119
- queue_scan_size
141
+ queue_scan_size,
142
+ lock_index_enabled
120
143
  ) => { ok:, result: }
121
144
  # steep:ignore:end
122
145
 
@@ -127,6 +150,12 @@ module RedisQueuedLocks::Acquirer::ReleaseLocksOf
127
150
  rel_end_time = clock_gettime
128
151
  rel_time = ((rel_end_time - rel_start_time) / 1_000.0).ceil(2)
129
152
 
153
+ log_sampled = RedisQueuedLocks::Logging.should_log?(
154
+ log_sampling_enabled,
155
+ log_sample_this,
156
+ log_sampling_percent,
157
+ log_sampler
158
+ )
130
159
  instr_sampled = RedisQueuedLocks::Instrument.should_instrument?(
131
160
  instr_sampling_enabled,
132
161
  instr_sample_this,
@@ -134,6 +163,18 @@ module RedisQueuedLocks::Acquirer::ReleaseLocksOf
134
163
  instr_sampler
135
164
  )
136
165
 
166
+ run_non_critical do
167
+ logger.debug do
168
+ "[redis_queued_locks.release_locks_of] " \
169
+ "at => #{time_at} " \
170
+ "hst_id => '#{refused_host_id}' " \
171
+ "acq_id => '#{refused_acquirer_id}' " \
172
+ "rel_time => #{rel_time} " \
173
+ "rel_key_cnt => #{result[:rel_key_cnt]} " \
174
+ "tch_queue_cnt => #{result[:tch_queue_cnt]}"
175
+ end
176
+ end if log_sampled
177
+
137
178
  run_non_critical do
138
179
  instrumenter.notify('redis_queued_locks.release_locks_of', {
139
180
  at: time_at,
@@ -141,7 +182,8 @@ module RedisQueuedLocks::Acquirer::ReleaseLocksOf
141
182
  acq_id: refused_acquirer_id,
142
183
  rel_time: rel_time,
143
184
  rel_key_cnt: result[:rel_key_cnt],
144
- tch_queue_cnt: result[:tch_queue_cnt]
185
+ tch_queue_cnt: result[:tch_queue_cnt],
186
+ instrument: instrument
145
187
  })
146
188
  end if instr_sampled
147
189
 
@@ -158,104 +200,266 @@ module RedisQueuedLocks::Acquirer::ReleaseLocksOf
158
200
 
159
201
  private
160
202
 
203
+ # Releases the locks and the lock requests of the passed host/acquirer found in one pass:
204
+ # - the disabled lock index: one SCAN pass over the RQL keys (`KEY_PATTERN`): the found keys
205
+ # are processed in batches with one round trip per batch (batches are collected across
206
+ # SCAN pages);
207
+ # - the enabled lock index: one ZSCAN pass over the lock index (`LOCK_INDEX_KEY`: lock names,
208
+ # the keyspace is never scanned): the keys of each batch of `lock_scan_size` lock names are
209
+ # derived from the lock names and processed with one round trip per key family and batch;
210
+ # the lock names stay in the lock index (other keys of the locks can exist);
211
+ # Batches:
212
+ # - write locks of the passed acquirer (`lock_scan_size` locks per batch);
213
+ # - (RW) read locks of the passed acquirer (`lock_scan_size` readers registries per batch);
214
+ # - requests of the passed acquirer in the lock queues (`queue_scan_size` queues per batch);
215
+ #
161
216
  # @param refused_host_id [String]
162
217
  # @param refused_acquirer_id [String]
163
218
  # @param redis [RedisClient]
164
219
  # @param lock_scan_size [Integer]
165
- # @param queue_scan_size [Integer]
220
+ # SCAN count and the batch size of locks (the disabled lock index) or ZSCAN count and
221
+ # the batch size of locks (lock names, the enabled lock index).
222
+ # @param queue_scan_size [Integer] The batch size of lock queues.
223
+ # @param lock_index_enabled [Boolean]
166
224
  # @return [Hash<Symbol,Boolean|Hash<Symbol,Integer>>]
167
225
  # - Example: { ok: true, result: { rel_key_cnt: 12345, tch_queue_cnt: 321 } }
168
226
  #
169
227
  # @api private
170
228
  # @since 1.14.0
171
- # @version 1.18.0
229
+ # @version 1.20.0
172
230
  # rubocop:disable Metrics/MethodLength
173
231
  def fully_release_locks_of(
174
232
  refused_host_id,
175
233
  refused_acquirer_id,
176
234
  redis,
177
235
  lock_scan_size,
178
- queue_scan_size
236
+ queue_scan_size,
237
+ lock_index_enabled
179
238
  )
180
- # TODO: some indexing approach isntead of <scan>
181
239
  rel_key_cnt = 0
182
240
  tch_queue_cnt = 0
241
+ # NOTE: (RW) read locks store acquirer ids only (the host is a part of the acquirer id),
242
+ # so they are released only when the passed acquirer belongs to the passed host;
243
+ with_read_locks =
244
+ RedisQueuedLocks::Resource.host_identifier_from_acquirer(refused_acquirer_id) ==
245
+ refused_host_id
183
246
 
184
247
  redis.with do |rconn|
185
- # Step A: drop locks of the passed host/acquirer
186
- refused_locks = Set.new #: Set[String]
187
- rconn.scan(
188
- 'MATCH',
189
- RedisQueuedLocks::Resource::LOCK_PATTERN,
190
- count: lock_scan_size
191
- ) do |lock_key|
192
- acquirer_id, host_id = rconn.call('HMGET', lock_key, 'acq_id', 'hst_id')
193
- if refused_host_id == host_id && refused_acquirer_id == acquirer_id
194
- refused_locks << lock_key
195
- end
248
+ # @type var lock_keys: Array[String]
249
+ lock_keys = []
250
+ # @type var lock_readers_keys: Array[String]
251
+ lock_readers_keys = []
252
+ # @type var lock_queues: Array[String]
253
+ lock_queues = []
196
254
 
197
- if refused_locks.size >= lock_scan_size
198
- # NOTE: steep can not recognize the `*`-splat operator on Set objects
199
- rconn.call('DEL', *refused_locks) # steep:ignore
200
- rel_key_cnt += refused_locks.size
201
- refused_locks.clear
202
- end
203
- end
255
+ if lock_index_enabled
256
+ # NOTE: (lock index) the keys of each batch of lock names are derived from the lock names
257
+ each_lock_name_batch(rconn, lock_scan_size) do |lock_names|
258
+ lock_names.each do |lock_name|
259
+ lock_keys << RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
260
+ if with_read_locks
261
+ lock_readers_keys << RedisQueuedLocks::Resource.prepare_lock_readers(lock_name)
262
+ end
263
+ lock_queues << RedisQueuedLocks::Resource.prepare_lock_queue(lock_name)
264
+ lock_queues << RedisQueuedLocks::Resource.prepare_read_lock_queue(lock_name)
265
+ end
204
266
 
205
- if refused_locks.any?
206
- # NOTE: steep can not recognize the `*`-splat operator on Set objects
207
- rconn.call('DEL', *refused_locks) # steep:ignore
208
- rel_key_cnt += refused_locks.size
209
- end
267
+ # Step A: drop locks of the passed host/acquirer
268
+ rel_key_cnt += release_refused_locks(
269
+ rconn, lock_keys, refused_host_id, refused_acquirer_id
270
+ )
271
+ # Step A (RW): drop read locks of the passed host/acquirer
272
+ unless lock_readers_keys.empty?
273
+ rel_key_cnt += release_refused_read_locks(
274
+ rconn, lock_readers_keys, refused_acquirer_id
275
+ )
276
+ end
277
+ # Step B: drop passed host/acquirer from lock queues (write and read)
278
+ lock_queues.each_slice(queue_scan_size) do |lock_queues_batch|
279
+ tch_queue_cnt += dequeue_refused_acquirer(
280
+ rconn, lock_queues_batch, refused_acquirer_id
281
+ )
282
+ end
210
283
 
211
- # Step A (RW): drop read locks of the passed host/acquirer
212
- # - read locks store acquirer ids only (the host is a part of the acquirer id);
213
- if RedisQueuedLocks::Resource.host_identifier_from_acquirer(refused_acquirer_id) ==
214
- refused_host_id
284
+ lock_keys.clear
285
+ lock_readers_keys.clear
286
+ lock_queues.clear
287
+ end
288
+ else
289
+ # NOTE: (keyspace) the found keys are collected into batches by their key families
215
290
  rconn.scan(
216
291
  'MATCH',
217
- RedisQueuedLocks::Resource::LOCK_READERS_PATTERN,
292
+ RedisQueuedLocks::Resource::KEY_PATTERN,
218
293
  count: lock_scan_size
219
- ) do |lock_readers_key|
220
- res = rconn.call('ZREM', lock_readers_key, refused_acquirer_id)
221
- next if res == 0
222
-
223
- rel_key_cnt += 1
224
- # NOTE: drop the read lock data
225
- rconn.call(
226
- 'DEL',
227
- RedisQueuedLocks::Resource.prepare_read_lock_key(
228
- RedisQueuedLocks::Resource.lock_name_from_readers(lock_readers_key),
229
- refused_acquirer_id
294
+ ) do |key|
295
+ case RedisQueuedLocks::Resource.lock_key_family(key)
296
+ when :lock
297
+ # Step A: drop locks of the passed host/acquirer
298
+ lock_keys << key
299
+ next if lock_keys.size < lock_scan_size
300
+
301
+ rel_key_cnt += release_refused_locks(
302
+ rconn, lock_keys, refused_host_id, refused_acquirer_id
230
303
  )
304
+ lock_keys.clear
305
+ when :lock_readers
306
+ # Step A (RW): drop read locks of the passed host/acquirer
307
+ next unless with_read_locks
308
+
309
+ lock_readers_keys << key
310
+ next if lock_readers_keys.size < lock_scan_size
311
+
312
+ rel_key_cnt += release_refused_read_locks(
313
+ rconn, lock_readers_keys, refused_acquirer_id
314
+ )
315
+ lock_readers_keys.clear
316
+ when :lock_queue, :read_lock_queue
317
+ # Step B: drop passed host/acquirer from lock queues (write and read)
318
+ lock_queues << key
319
+ next if lock_queues.size < queue_scan_size
320
+
321
+ tch_queue_cnt += dequeue_refused_acquirer(rconn, lock_queues, refused_acquirer_id)
322
+ lock_queues.clear
323
+ end
324
+ end
325
+
326
+ unless lock_keys.empty?
327
+ rel_key_cnt += release_refused_locks(
328
+ rconn, lock_keys, refused_host_id, refused_acquirer_id
231
329
  )
232
330
  end
331
+ unless lock_readers_keys.empty?
332
+ rel_key_cnt += release_refused_read_locks(
333
+ rconn, lock_readers_keys, refused_acquirer_id
334
+ )
335
+ end
336
+ unless lock_queues.empty?
337
+ tch_queue_cnt += dequeue_refused_acquirer(rconn, lock_queues, refused_acquirer_id)
338
+ end
233
339
  end
340
+ end
341
+
342
+ { ok: true, result: { rel_key_cnt:, tch_queue_cnt: } }
343
+ end
344
+ # rubocop:enable Metrics/MethodLength
234
345
 
235
- # Step B: drop passed host/acquirer from lock queues
236
- rconn.scan(
237
- 'MATCH',
238
- RedisQueuedLocks::Resource::LOCK_QUEUE_PATTERN,
239
- count: queue_scan_size
240
- ) do |lock_queue|
241
- res = rconn.call('ZREM', lock_queue, refused_acquirer_id)
242
- tch_queue_cnt += 1 if res != 0
346
+ # Drops the locks of the passed host/acquirer from the given batch of write locks
347
+ # (two round trips: lock owners, then one `DEL` of the refused locks).
348
+ #
349
+ # @param redis_client [RedisClient]
350
+ # @param lock_keys [Array<String>]
351
+ # @param refused_host_id [String]
352
+ # @param refused_acquirer_id [String]
353
+ # @return [Integer] The number of the refused locks.
354
+ #
355
+ # @api private
356
+ # @since 1.20.0
357
+ def release_refused_locks(redis_client, lock_keys, refused_host_id, refused_acquirer_id)
358
+ # NOTE: (pipeline layout) one reply (acquirer and host) per lock in the batch order
359
+ # @type var lock_owners: Array[[String?, String?]]
360
+ lock_owners = redis_client.pipelined do |pipeline|
361
+ lock_keys.each { |lock_key| pipeline.call('HMGET', lock_key, 'acq_id', 'hst_id') }
362
+ end
363
+
364
+ # @type var refused_locks: Array[String]
365
+ refused_locks = []
366
+ lock_keys.each_with_index do |lock_key, index|
367
+ acquirer_id, host_id = lock_owners[index]
368
+ if refused_host_id == host_id && refused_acquirer_id == acquirer_id
369
+ refused_locks << lock_key
243
370
  end
371
+ end
372
+ return 0 if refused_locks.empty?
244
373
 
245
- # Step B (RW): drop passed host/acquirer from read lock queues
246
- rconn.scan(
247
- 'MATCH',
248
- RedisQueuedLocks::Resource::READ_LOCK_QUEUE_PATTERN,
249
- count: queue_scan_size
250
- ) do |read_lock_queue|
251
- res = rconn.call('ZREM', read_lock_queue, refused_acquirer_id)
252
- tch_queue_cnt += 1 if res != 0
374
+ redis_client.call('DEL', *refused_locks)
375
+ refused_locks.size
376
+ end
377
+
378
+ # (RW) Drops the read locks of the passed acquirer from the given batch of readers registries
379
+ # in one round trip: `ZREM` of the acquirer and `DEL` of its read lock data for each registry
380
+ # (the read lock data is a data carrier only: `DEL` of an absent data is a no-op).
381
+ #
382
+ # @param redis_client [RedisClient]
383
+ # @param lock_readers_keys [Array<String>]
384
+ # @param refused_acquirer_id [String]
385
+ # @return [Integer] The number of the dropped read locks.
386
+ #
387
+ # @api private
388
+ # @since 1.20.0
389
+ def release_refused_read_locks(redis_client, lock_readers_keys, refused_acquirer_id)
390
+ # NOTE: (pipeline layout) two replies per registry in the batch order: ZREM, DEL
391
+ # @type var replies: Array[Integer]
392
+ replies = redis_client.pipelined do |pipeline|
393
+ lock_readers_keys.each do |lock_readers_key|
394
+ pipeline.call('ZREM', lock_readers_key, refused_acquirer_id)
395
+ pipeline.call(
396
+ 'DEL',
397
+ RedisQueuedLocks::Resource.prepare_read_lock_key(
398
+ RedisQueuedLocks::Resource.lock_name_from_readers(lock_readers_key),
399
+ refused_acquirer_id
400
+ )
401
+ )
253
402
  end
254
403
  end
255
404
 
256
- { ok: true, result: { rel_key_cnt:, tch_queue_cnt: } }
405
+ released = 0
406
+ replies.each_slice(2) { |(zrem_cmd_res, _del_cmd_res)| released += 1 if zrem_cmd_res != 0 }
407
+ released
408
+ end
409
+
410
+ # Drops the requests of the passed acquirer from the given batch of lock queues
411
+ # (write and read) in one round trip.
412
+ #
413
+ # @param redis_client [RedisClient]
414
+ # @param lock_queues [Array<String>]
415
+ # @param refused_acquirer_id [String]
416
+ # @return [Integer] The number of the touched lock queues.
417
+ #
418
+ # @api private
419
+ # @since 1.20.0
420
+ def dequeue_refused_acquirer(redis_client, lock_queues, refused_acquirer_id)
421
+ # @type var replies: Array[Integer]
422
+ replies = redis_client.pipelined do |pipeline|
423
+ lock_queues.each { |lock_queue| pipeline.call('ZREM', lock_queue, refused_acquirer_id) }
424
+ end
425
+ replies.count { |zrem_cmd_res| zrem_cmd_res != 0 }
426
+ end
427
+
428
+ # Iterates over the lock names of the lock index (one ZSCAN pass) in batches of `scan_size`
429
+ # lock names (batches are collected across ZSCAN pages). Expired lock names (not dropped from
430
+ # the index yet) are skipped: all their keys are expired.
431
+ #
432
+ # NOTE: each operation module iterates over the lock index by itself (duplicated on purpose:
433
+ # operation modules are independent of each other, see `Locks.each_lock_name_batch`);
434
+ #
435
+ # @param redis_client [RedisClient]
436
+ # @param scan_size [Integer] ZSCAN count and the number of lock names in one batch.
437
+ # @yield [lock_names]
438
+ # @yieldparam lock_names [Array<String>] The batch of lock names (reused between batches).
439
+ # @return [void]
440
+ #
441
+ # @api private
442
+ # @since 1.20.0
443
+ def each_lock_name_batch(redis_client, scan_size)
444
+ lock_index_time = RedisQueuedLocks::Resource.lock_index_time
445
+ # @type var lock_names: Array[String]
446
+ lock_names = []
447
+
448
+ redis_client.zscan(
449
+ RedisQueuedLocks::Resource::LOCK_INDEX_KEY, count: scan_size
450
+ ) do |lock_name, lock_index_score|
451
+ next if lock_index_score.to_f < lock_index_time
452
+
453
+ lock_names << lock_name
454
+ next if lock_names.size < scan_size
455
+
456
+ yield(lock_names)
457
+ lock_names.clear
458
+ end
459
+
460
+ yield(lock_names) unless lock_names.empty?
257
461
  end
258
462
  end
259
- # rubocop:enable Metrics/MethodLength, Metrics/ClassLength
463
+ # rubocop:enable Metrics/ClassLength
260
464
  end
261
465
  # rubocop:enable Metrics/ModuleLength
@@ -2,6 +2,7 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.18.0
5
+ # @version 1.21.0
5
6
  module RedisQueuedLocks::Acquirer::ReleaseReadLock
6
7
  # @since 1.18.0
7
8
  extend RedisQueuedLocks::Utilities
@@ -17,11 +18,11 @@ module RedisQueuedLocks::Acquirer::ReleaseReadLock
17
18
  # The lock name whose read lock should be released.
18
19
  # @param acquirer_id [String]
19
20
  # The read lock acquirer.
20
- # @param instrumenter [#notify]
21
- # See RedisQueuedLocks::Instrument::ActiveSupport for example.
22
21
  # @param logger [::Logger,#debug]
23
22
  # - Logger object used from `configuration` layer (see config['logger']);
24
23
  # - See RedisQueuedLocks::Logging::VoidLogger for example;
24
+ # @param instrumenter [#notify]
25
+ # See RedisQueuedLocks::Instrument::ActiveSupport for example.
25
26
  # @param instrument [NilClass,Any]
26
27
  # - Custom instrumentation data wich will be passed to the instrumenter's payload
27
28
  # with :instrument key;
@@ -52,13 +53,14 @@ module RedisQueuedLocks::Acquirer::ReleaseReadLock
52
53
  #
53
54
  # @api private
54
55
  # @since 1.18.0
56
+ # @version 1.21.0
55
57
  # rubocop:disable Metrics/MethodLength
56
58
  def release_read_lock(
57
59
  redis,
58
60
  lock_name,
59
61
  acquirer_id,
60
- instrumenter,
61
62
  logger,
63
+ instrumenter,
62
64
  instrument,
63
65
  log_sampling_enabled,
64
66
  log_sampling_percent,
@@ -87,6 +89,12 @@ module RedisQueuedLocks::Acquirer::ReleaseReadLock
87
89
  rel_end_time = clock_gettime
88
90
  rel_time = ((rel_end_time - rel_start_time) / 1_000.0).ceil(2)
89
91
 
92
+ log_sampled = RedisQueuedLocks::Logging.should_log?(
93
+ log_sampling_enabled,
94
+ log_sample_this,
95
+ log_sampling_percent,
96
+ log_sampler
97
+ )
90
98
  instr_sampled = RedisQueuedLocks::Instrument.should_instrument?(
91
99
  instr_sampling_enabled,
92
100
  instr_sample_this,
@@ -94,6 +102,17 @@ module RedisQueuedLocks::Acquirer::ReleaseReadLock
94
102
  instr_sampler
95
103
  )
96
104
 
105
+ run_non_critical do
106
+ logger.debug do
107
+ "[redis_queued_locks.explicit_read_lock_release] " \
108
+ "lock_key => '#{lock_key}' " \
109
+ "acq_id => '#{acquirer_id}' " \
110
+ "lock_res => '#{result[:lock]}' " \
111
+ "rel_time => #{rel_time} " \
112
+ "at => #{time_at}"
113
+ end
114
+ end if log_sampled
115
+
97
116
  run_non_critical do
98
117
  instrumenter.notify('redis_queued_locks.explicit_read_lock_release', {
99
118
  lock_key: lock_key,
@@ -18,4 +18,5 @@ module RedisQueuedLocks::Acquirer
18
18
  require_relative 'acquirer/keys'
19
19
  require_relative 'acquirer/extend_lock_ttl'
20
20
  require_relative 'acquirer/clear_dead_requests'
21
+ require_relative 'acquirer/reindex_locks'
21
22
  end