redis_queued_locks 1.18.0 → 1.20.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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/project-overview.md +65 -13
  3. data/.claude/rules/acquirer.md +29 -11
  4. data/.claude/rules/arguments.md +3 -2
  5. data/.claude/rules/git.md +3 -2
  6. data/.claude/rules/logic.md +4 -3
  7. data/.claude/rules/swarm.md +12 -7
  8. data/.claude/rules/tests.md +6 -0
  9. data/.claude/rules/type-checking.md +2 -1
  10. data/.claude/rules/visitors.md +1 -1
  11. data/.claude/skills/rql-code-review/SKILL.md +372 -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 +74 -0
  15. data/CLAUDE.md +4 -2
  16. data/README.md +338 -24
  17. data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rb +684 -128
  18. data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire.rb +3 -1
  19. data/lib/redis_queued_locks/acquirer/acquire_lock.rb +24 -3
  20. data/lib/redis_queued_locks/acquirer/clear_dead_requests.rb +145 -18
  21. data/lib/redis_queued_locks/acquirer/extend_lock_ttl.rb +71 -10
  22. data/lib/redis_queued_locks/acquirer/keys.rb +142 -7
  23. data/lib/redis_queued_locks/acquirer/lock_info.rb +10 -4
  24. data/lib/redis_queued_locks/acquirer/lock_series_poc.rb +26 -7
  25. data/lib/redis_queued_locks/acquirer/locks.rb +195 -83
  26. data/lib/redis_queued_locks/acquirer/queue_info.rb +12 -18
  27. data/lib/redis_queued_locks/acquirer/queues.rb +114 -50
  28. data/lib/redis_queued_locks/acquirer/reindex_locks.rb +140 -0
  29. data/lib/redis_queued_locks/acquirer/release_all_locks.rb +147 -58
  30. data/lib/redis_queued_locks/acquirer/release_lock.rb +52 -19
  31. data/lib/redis_queued_locks/acquirer/release_locks_of.rb +235 -66
  32. data/lib/redis_queued_locks/acquirer.rb +1 -0
  33. data/lib/redis_queued_locks/client.rb +116 -16
  34. data/lib/redis_queued_locks/config.rb +5 -1
  35. data/lib/redis_queued_locks/resource.rb +99 -2
  36. data/lib/redis_queued_locks/swarm/flush_zombies.rb +332 -61
  37. data/lib/redis_queued_locks/swarm/zombie_info.rb +203 -53
  38. data/lib/redis_queued_locks/swarm.rb +15 -4
  39. data/lib/redis_queued_locks/version.rb +2 -2
  40. data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rbs +107 -1
  41. data/sig/redis_queued_locks/acquirer/acquire_lock/yield_expire.rbs +1 -1
  42. data/sig/redis_queued_locks/acquirer/acquire_lock.rbs +2 -0
  43. data/sig/redis_queued_locks/acquirer/clear_dead_requests.rbs +10 -1
  44. data/sig/redis_queued_locks/acquirer/extend_lock_ttl.rbs +5 -2
  45. data/sig/redis_queued_locks/acquirer/keys.rbs +11 -1
  46. data/sig/redis_queued_locks/acquirer/locks.rbs +15 -6
  47. data/sig/redis_queued_locks/acquirer/queues.rbs +9 -2
  48. data/sig/redis_queued_locks/acquirer/reindex_locks.rbs +31 -0
  49. data/sig/redis_queued_locks/acquirer/release_all_locks.rbs +8 -1
  50. data/sig/redis_queued_locks/acquirer/release_lock.rbs +3 -1
  51. data/sig/redis_queued_locks/acquirer/release_locks_of.rbs +20 -1
  52. data/sig/redis_queued_locks/client.rbs +9 -0
  53. data/sig/redis_queued_locks/resource.rbs +6 -0
  54. data/sig/redis_queued_locks/swarm/flush_zombies.rbs +51 -1
  55. data/sig/redis_queued_locks/swarm/zombie_info.rbs +58 -7
  56. data/sig/vendor/redis_client.rbs +1 -0
  57. metadata +6 -1
@@ -12,6 +12,7 @@ module RedisQueuedLocks::Acquirer::ReleaseLock
12
12
  # from the lock aquierment and should retry;
13
13
  # - 2. delete the lock: drop lock key from Redis;
14
14
  # - 3. (RW) clear the read lock queue and drop all read locks (the readers registry);
15
+ # - 4. (lock index) drop the lock name from the lock index (the enabled lock index only);
15
16
  # It is safe because the lock obtain logic is transactional and
16
17
  # watches the original lock for changes.
17
18
  #
@@ -19,6 +20,8 @@ module RedisQueuedLocks::Acquirer::ReleaseLock
19
20
  # Redis connection client.
20
21
  # @param lock_name [String]
21
22
  # The lock name that should be released.
23
+ # @param lock_index_enabled [Boolean]
24
+ # Drop the lock name from the lock index (the enabled lock index).
22
25
  # @param instrumenter [#notify]
23
26
  # See RedisQueuedLocks::Instrument::ActiveSupport for example.
24
27
  # @param logger [::Logger,#debug]
@@ -72,11 +75,12 @@ module RedisQueuedLocks::Acquirer::ReleaseLock
72
75
  #
73
76
  # @api private
74
77
  # @since 1.0.0
75
- # @version 1.18.0
78
+ # @version 1.20.0
76
79
  # rubocop:disable Metrics/MethodLength
77
80
  def release_lock(
78
81
  redis,
79
82
  lock_name,
83
+ lock_index_enabled,
80
84
  instrumenter,
81
85
  logger,
82
86
  log_sampling_enabled,
@@ -96,7 +100,8 @@ module RedisQueuedLocks::Acquirer::ReleaseLock
96
100
  rel_start_time = clock_gettime
97
101
  # steep:ignore:start
98
102
  fully_release_lock(
99
- redis, lock_name, lock_key, lock_key_queue, read_lock_key_queue, lock_readers_key
103
+ redis, lock_name, lock_key, lock_key_queue, read_lock_key_queue, lock_readers_key,
104
+ lock_index_enabled
100
105
  ) => { ok:, result: }
101
106
  # steep:ignore:end
102
107
 
@@ -138,7 +143,8 @@ module RedisQueuedLocks::Acquirer::ReleaseLock
138
143
 
139
144
  private
140
145
 
141
- # Realease the lock: clear the lock queues and expire the lock (write lock and read locks).
146
+ # Realease the lock: clear the lock queues and expire the lock (write lock and read locks),
147
+ # drop the lock name from the lock index (the enabled lock index).
142
148
  #
143
149
  # @param redis [RedisClient]
144
150
  # @param lock_name [String]
@@ -146,6 +152,7 @@ module RedisQueuedLocks::Acquirer::ReleaseLock
146
152
  # @param lock_key_queue [String]
147
153
  # @param read_lock_key_queue [String]
148
154
  # @param lock_readers_key [String]
155
+ # @param lock_index_enabled [Boolean]
149
156
  # @return [Hash<Symbol,Boolean|Hash<Symbol,Symbol>>]
150
157
  # Format: {
151
158
  # ok: true/false,
@@ -157,43 +164,69 @@ module RedisQueuedLocks::Acquirer::ReleaseLock
157
164
  #
158
165
  # @api private
159
166
  # @since 1.0.0
160
- # @version 1.18.0
167
+ # @version 1.20.0
168
+ # rubocop:disable Metrics/MethodLength
161
169
  def fully_release_lock(
162
170
  redis,
163
171
  lock_name,
164
172
  lock_key,
165
173
  lock_key_queue,
166
174
  read_lock_key_queue,
167
- lock_readers_key
175
+ lock_readers_key,
176
+ lock_index_enabled
168
177
  )
169
- # @type var result: [Integer,Integer,Integer,Integer]
178
+ # @type var result: [Array[String],Integer,Integer,Integer,Integer]
170
179
  result = redis.with do |rconn|
171
- # NOTE: (RW) data of read locks (it is a data carrier only: read locks are released via
172
- # the readers registry, so the data of concurrently obtained read locks expires itself)
173
- # @type var read_lock_acquirers: Array[String]
174
- read_lock_acquirers = rconn.call('ZRANGE', lock_readers_key, '0', '-1')
175
-
176
- rconn.multi do |transact|
180
+ # Step 1: release the lock in one transaction (one round trip):
181
+ # - (RW) the readers registry is read in the same transaction: the snapshot of all
182
+ # released read locks (their data is dropped in Step 2);
183
+ # - (lock index) the enabled lock index: all the indexed keys of the lock are dropped,
184
+ # so the lock name is dropped from the lock index in the same transaction (a lock
185
+ # request queued or a lock obtained after this transaction indexes the lock name again);
186
+ # NOTE: (transaction layout) replies are read by their positions:
187
+ # [0] ZRANGE (released read locks), [1] write lock queue, [2] write lock,
188
+ # [3] read lock queue, [4] readers registry, [5] lock index (the enabled lock index
189
+ # only, the last command: not read);
190
+ released = rconn.multi do |transact|
191
+ transact.call('ZRANGE', lock_readers_key, '0', '-1')
177
192
  transact.call('ZREMRANGEBYSCORE', lock_key_queue, '-inf', '+inf')
178
193
  transact.call('EXPIRE', lock_key, '0')
179
194
  transact.call('ZREMRANGEBYSCORE', read_lock_key_queue, '-inf', '+inf')
180
195
  transact.call('EXPIRE', lock_readers_key, '0')
181
- read_lock_acquirers.each do |acquirer_id|
182
- transact.call(
183
- 'DEL',
184
- RedisQueuedLocks::Resource.prepare_read_lock_key(lock_name, acquirer_id)
185
- )
196
+ if lock_index_enabled
197
+ transact.call('ZREM', RedisQueuedLocks::Resource::LOCK_INDEX_KEY, lock_name)
186
198
  end
187
199
  end
200
+
201
+ # Step 2: (RW) drop the data of the released read locks (only when read locks existed)
202
+ # NOTE:
203
+ # - the read lock data is a data carrier only: read locks are released via the readers
204
+ # registry (Step 1);
205
+ # - (trade-off) a reader of the snapshot that obtains a new read lock between Step 1 and
206
+ # Step 2 loses the data of the new read lock (meta, timestamps); the read lock itself is
207
+ # not affected (the readers registry is the source of truth);
208
+ # @type var read_lock_acquirers: Array[String]
209
+ read_lock_acquirers = released[0]
210
+ unless read_lock_acquirers.empty?
211
+ rconn.call(
212
+ 'DEL',
213
+ *read_lock_acquirers.map do |acquirer_id|
214
+ RedisQueuedLocks::Resource.prepare_read_lock_key(lock_name, acquirer_id)
215
+ end
216
+ )
217
+ end
218
+
219
+ released
188
220
  end
189
221
 
190
222
  {
191
223
  ok: true,
192
224
  result: {
193
- queue: (result[0] != 0 || result[2] != 0) ? :released : :nothing_to_release,
194
- lock: (result[1] != 0 || result[3] != 0) ? :released : :nothing_to_release
225
+ queue: (result[1] != 0 || result[3] != 0) ? :released : :nothing_to_release,
226
+ lock: (result[2] != 0 || result[4] != 0) ? :released : :nothing_to_release
195
227
  }
196
228
  }
197
229
  end
230
+ # rubocop:enable Metrics/MethodLength
198
231
  end
199
232
  end
@@ -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;
@@ -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.20.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,7 @@ 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
+ )
111
117
  rel_start_time = clock_gettime
112
118
 
113
119
  # steep:ignore:start
@@ -116,7 +122,8 @@ module RedisQueuedLocks::Acquirer::ReleaseLocksOf
116
122
  refused_acquirer_id,
117
123
  redis,
118
124
  lock_scan_size,
119
- queue_scan_size
125
+ queue_scan_size,
126
+ lock_index_enabled
120
127
  ) => { ok:, result: }
121
128
  # steep:ignore:end
122
129
 
@@ -158,104 +165,266 @@ module RedisQueuedLocks::Acquirer::ReleaseLocksOf
158
165
 
159
166
  private
160
167
 
168
+ # Releases the locks and the lock requests of the passed host/acquirer found in one pass:
169
+ # - the disabled lock index: one SCAN pass over the RQL keys (`KEY_PATTERN`): the found keys
170
+ # are processed in batches with one round trip per batch (batches are collected across
171
+ # SCAN pages);
172
+ # - the enabled lock index: one ZSCAN pass over the lock index (`LOCK_INDEX_KEY`: lock names,
173
+ # the keyspace is never scanned): the keys of each batch of `lock_scan_size` lock names are
174
+ # derived from the lock names and processed with one round trip per key family and batch;
175
+ # the lock names stay in the lock index (other keys of the locks can exist);
176
+ # Batches:
177
+ # - write locks of the passed acquirer (`lock_scan_size` locks per batch);
178
+ # - (RW) read locks of the passed acquirer (`lock_scan_size` readers registries per batch);
179
+ # - requests of the passed acquirer in the lock queues (`queue_scan_size` queues per batch);
180
+ #
161
181
  # @param refused_host_id [String]
162
182
  # @param refused_acquirer_id [String]
163
183
  # @param redis [RedisClient]
164
184
  # @param lock_scan_size [Integer]
165
- # @param queue_scan_size [Integer]
185
+ # SCAN count and the batch size of locks (the disabled lock index) or ZSCAN count and
186
+ # the batch size of locks (lock names, the enabled lock index).
187
+ # @param queue_scan_size [Integer] The batch size of lock queues.
188
+ # @param lock_index_enabled [Boolean]
166
189
  # @return [Hash<Symbol,Boolean|Hash<Symbol,Integer>>]
167
190
  # - Example: { ok: true, result: { rel_key_cnt: 12345, tch_queue_cnt: 321 } }
168
191
  #
169
192
  # @api private
170
193
  # @since 1.14.0
171
- # @version 1.18.0
194
+ # @version 1.20.0
172
195
  # rubocop:disable Metrics/MethodLength
173
196
  def fully_release_locks_of(
174
197
  refused_host_id,
175
198
  refused_acquirer_id,
176
199
  redis,
177
200
  lock_scan_size,
178
- queue_scan_size
201
+ queue_scan_size,
202
+ lock_index_enabled
179
203
  )
180
- # TODO: some indexing approach isntead of <scan>
181
204
  rel_key_cnt = 0
182
205
  tch_queue_cnt = 0
206
+ # NOTE: (RW) read locks store acquirer ids only (the host is a part of the acquirer id),
207
+ # so they are released only when the passed acquirer belongs to the passed host;
208
+ with_read_locks =
209
+ RedisQueuedLocks::Resource.host_identifier_from_acquirer(refused_acquirer_id) ==
210
+ refused_host_id
183
211
 
184
212
  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
213
+ # @type var lock_keys: Array[String]
214
+ lock_keys = []
215
+ # @type var lock_readers_keys: Array[String]
216
+ lock_readers_keys = []
217
+ # @type var lock_queues: Array[String]
218
+ lock_queues = []
196
219
 
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
220
+ if lock_index_enabled
221
+ # NOTE: (lock index) the keys of each batch of lock names are derived from the lock names
222
+ each_lock_name_batch(rconn, lock_scan_size) do |lock_names|
223
+ lock_names.each do |lock_name|
224
+ lock_keys << RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
225
+ if with_read_locks
226
+ lock_readers_keys << RedisQueuedLocks::Resource.prepare_lock_readers(lock_name)
227
+ end
228
+ lock_queues << RedisQueuedLocks::Resource.prepare_lock_queue(lock_name)
229
+ lock_queues << RedisQueuedLocks::Resource.prepare_read_lock_queue(lock_name)
230
+ end
204
231
 
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
232
+ # Step A: drop locks of the passed host/acquirer
233
+ rel_key_cnt += release_refused_locks(
234
+ rconn, lock_keys, refused_host_id, refused_acquirer_id
235
+ )
236
+ # Step A (RW): drop read locks of the passed host/acquirer
237
+ unless lock_readers_keys.empty?
238
+ rel_key_cnt += release_refused_read_locks(
239
+ rconn, lock_readers_keys, refused_acquirer_id
240
+ )
241
+ end
242
+ # Step B: drop passed host/acquirer from lock queues (write and read)
243
+ lock_queues.each_slice(queue_scan_size) do |lock_queues_batch|
244
+ tch_queue_cnt += dequeue_refused_acquirer(
245
+ rconn, lock_queues_batch, refused_acquirer_id
246
+ )
247
+ end
210
248
 
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
249
+ lock_keys.clear
250
+ lock_readers_keys.clear
251
+ lock_queues.clear
252
+ end
253
+ else
254
+ # NOTE: (keyspace) the found keys are collected into batches by their key families
215
255
  rconn.scan(
216
256
  'MATCH',
217
- RedisQueuedLocks::Resource::LOCK_READERS_PATTERN,
257
+ RedisQueuedLocks::Resource::KEY_PATTERN,
218
258
  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
259
+ ) do |key|
260
+ case RedisQueuedLocks::Resource.lock_key_family(key)
261
+ when :lock
262
+ # Step A: drop locks of the passed host/acquirer
263
+ lock_keys << key
264
+ next if lock_keys.size < lock_scan_size
265
+
266
+ rel_key_cnt += release_refused_locks(
267
+ rconn, lock_keys, refused_host_id, refused_acquirer_id
268
+ )
269
+ lock_keys.clear
270
+ when :lock_readers
271
+ # Step A (RW): drop read locks of the passed host/acquirer
272
+ next unless with_read_locks
273
+
274
+ lock_readers_keys << key
275
+ next if lock_readers_keys.size < lock_scan_size
222
276
 
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
277
+ rel_key_cnt += release_refused_read_locks(
278
+ rconn, lock_readers_keys, refused_acquirer_id
230
279
  )
280
+ lock_readers_keys.clear
281
+ when :lock_queue, :read_lock_queue
282
+ # Step B: drop passed host/acquirer from lock queues (write and read)
283
+ lock_queues << key
284
+ next if lock_queues.size < queue_scan_size
285
+
286
+ tch_queue_cnt += dequeue_refused_acquirer(rconn, lock_queues, refused_acquirer_id)
287
+ lock_queues.clear
288
+ end
289
+ end
290
+
291
+ unless lock_keys.empty?
292
+ rel_key_cnt += release_refused_locks(
293
+ rconn, lock_keys, refused_host_id, refused_acquirer_id
294
+ )
295
+ end
296
+ unless lock_readers_keys.empty?
297
+ rel_key_cnt += release_refused_read_locks(
298
+ rconn, lock_readers_keys, refused_acquirer_id
231
299
  )
232
300
  end
301
+ unless lock_queues.empty?
302
+ tch_queue_cnt += dequeue_refused_acquirer(rconn, lock_queues, refused_acquirer_id)
303
+ end
233
304
  end
305
+ end
306
+
307
+ { ok: true, result: { rel_key_cnt:, tch_queue_cnt: } }
308
+ end
309
+ # rubocop:enable Metrics/MethodLength
310
+
311
+ # Drops the locks of the passed host/acquirer from the given batch of write locks
312
+ # (two round trips: lock owners, then one `DEL` of the refused locks).
313
+ #
314
+ # @param redis_client [RedisClient]
315
+ # @param lock_keys [Array<String>]
316
+ # @param refused_host_id [String]
317
+ # @param refused_acquirer_id [String]
318
+ # @return [Integer] The number of the refused locks.
319
+ #
320
+ # @api private
321
+ # @since 1.20.0
322
+ def release_refused_locks(redis_client, lock_keys, refused_host_id, refused_acquirer_id)
323
+ # NOTE: (pipeline layout) one reply (acquirer and host) per lock in the batch order
324
+ # @type var lock_owners: Array[[String?, String?]]
325
+ lock_owners = redis_client.pipelined do |pipeline|
326
+ lock_keys.each { |lock_key| pipeline.call('HMGET', lock_key, 'acq_id', 'hst_id') }
327
+ end
234
328
 
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
329
+ # @type var refused_locks: Array[String]
330
+ refused_locks = []
331
+ lock_keys.each_with_index do |lock_key, index|
332
+ acquirer_id, host_id = lock_owners[index]
333
+ if refused_host_id == host_id && refused_acquirer_id == acquirer_id
334
+ refused_locks << lock_key
243
335
  end
336
+ end
337
+ return 0 if refused_locks.empty?
244
338
 
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
339
+ redis_client.call('DEL', *refused_locks)
340
+ refused_locks.size
341
+ end
342
+
343
+ # (RW) Drops the read locks of the passed acquirer from the given batch of readers registries
344
+ # in one round trip: `ZREM` of the acquirer and `DEL` of its read lock data for each registry
345
+ # (the read lock data is a data carrier only: `DEL` of an absent data is a no-op).
346
+ #
347
+ # @param redis_client [RedisClient]
348
+ # @param lock_readers_keys [Array<String>]
349
+ # @param refused_acquirer_id [String]
350
+ # @return [Integer] The number of the dropped read locks.
351
+ #
352
+ # @api private
353
+ # @since 1.20.0
354
+ def release_refused_read_locks(redis_client, lock_readers_keys, refused_acquirer_id)
355
+ # NOTE: (pipeline layout) two replies per registry in the batch order: ZREM, DEL
356
+ # @type var replies: Array[Integer]
357
+ replies = redis_client.pipelined do |pipeline|
358
+ lock_readers_keys.each do |lock_readers_key|
359
+ pipeline.call('ZREM', lock_readers_key, refused_acquirer_id)
360
+ pipeline.call(
361
+ 'DEL',
362
+ RedisQueuedLocks::Resource.prepare_read_lock_key(
363
+ RedisQueuedLocks::Resource.lock_name_from_readers(lock_readers_key),
364
+ refused_acquirer_id
365
+ )
366
+ )
253
367
  end
254
368
  end
255
369
 
256
- { ok: true, result: { rel_key_cnt:, tch_queue_cnt: } }
370
+ released = 0
371
+ replies.each_slice(2) { |(zrem_cmd_res, _del_cmd_res)| released += 1 if zrem_cmd_res != 0 }
372
+ released
373
+ end
374
+
375
+ # Drops the requests of the passed acquirer from the given batch of lock queues
376
+ # (write and read) in one round trip.
377
+ #
378
+ # @param redis_client [RedisClient]
379
+ # @param lock_queues [Array<String>]
380
+ # @param refused_acquirer_id [String]
381
+ # @return [Integer] The number of the touched lock queues.
382
+ #
383
+ # @api private
384
+ # @since 1.20.0
385
+ def dequeue_refused_acquirer(redis_client, lock_queues, refused_acquirer_id)
386
+ # @type var replies: Array[Integer]
387
+ replies = redis_client.pipelined do |pipeline|
388
+ lock_queues.each { |lock_queue| pipeline.call('ZREM', lock_queue, refused_acquirer_id) }
389
+ end
390
+ replies.count { |zrem_cmd_res| zrem_cmd_res != 0 }
391
+ end
392
+
393
+ # Iterates over the lock names of the lock index (one ZSCAN pass) in batches of `scan_size`
394
+ # lock names (batches are collected across ZSCAN pages). Expired lock names (not dropped from
395
+ # the index yet) are skipped: all their keys are expired.
396
+ #
397
+ # NOTE: each operation module iterates over the lock index by itself (duplicated on purpose:
398
+ # operation modules are independent of each other, see `Locks.each_lock_name_batch`);
399
+ #
400
+ # @param redis_client [RedisClient]
401
+ # @param scan_size [Integer] ZSCAN count and the number of lock names in one batch.
402
+ # @yield [lock_names]
403
+ # @yieldparam lock_names [Array<String>] The batch of lock names (reused between batches).
404
+ # @return [void]
405
+ #
406
+ # @api private
407
+ # @since 1.20.0
408
+ def each_lock_name_batch(redis_client, scan_size)
409
+ lock_index_time = RedisQueuedLocks::Resource.lock_index_time
410
+ # @type var lock_names: Array[String]
411
+ lock_names = []
412
+
413
+ redis_client.zscan(
414
+ RedisQueuedLocks::Resource::LOCK_INDEX_KEY, count: scan_size
415
+ ) do |lock_name, lock_index_score|
416
+ next if lock_index_score.to_f < lock_index_time
417
+
418
+ lock_names << lock_name
419
+ next if lock_names.size < scan_size
420
+
421
+ yield(lock_names)
422
+ lock_names.clear
423
+ end
424
+
425
+ yield(lock_names) unless lock_names.empty?
257
426
  end
258
427
  end
259
- # rubocop:enable Metrics/MethodLength, Metrics/ClassLength
428
+ # rubocop:enable Metrics/ClassLength
260
429
  end
261
430
  # rubocop:enable Metrics/ModuleLength
@@ -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