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,6 +2,7 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.9.0
5
+ # @version 1.20.0
5
6
  # rubocop:disable Metrics/ClassLength
6
7
  class RedisQueuedLocks::Swarm::FlushZombies < RedisQueuedLocks::Swarm::SwarmElement::Isolated
7
8
  class << self
@@ -9,6 +10,8 @@ class RedisQueuedLocks::Swarm::FlushZombies < RedisQueuedLocks::Swarm::SwarmElem
9
10
  # @param zombie_ttl [Integer]
10
11
  # @param lock_scan_size [Integer]
11
12
  # @param queue_scan_size [Integer]
13
+ # @param lock_index_enabled [Boolean]
14
+ # Find zombie locks and requests via the lock index instead of the keyspace SCAN.
12
15
  # @return [Hash<Symbol,Boolean|Set<String>]] Format:
13
16
  # {
14
17
  # ok: <Boolean>,
@@ -19,13 +22,14 @@ class RedisQueuedLocks::Swarm::FlushZombies < RedisQueuedLocks::Swarm::SwarmElem
19
22
  #
20
23
  # @api private
21
24
  # @since 1.9.0
22
- # @version 1.18.0
25
+ # @version 1.20.0
23
26
  # rubocop:disable Metrics/MethodLength
24
27
  def flush_zombies(
25
28
  redis_client,
26
29
  zombie_ttl,
27
30
  lock_scan_size,
28
- queue_scan_size
31
+ queue_scan_size,
32
+ lock_index_enabled
29
33
  )
30
34
  redis_client.with do |rconn|
31
35
  # Step 1:
@@ -47,43 +51,306 @@ class RedisQueuedLocks::Swarm::FlushZombies < RedisQueuedLocks::Swarm::SwarmElem
47
51
  deleted_zombie_locks: Set.new
48
52
  } if zombie_hosts.empty?
49
53
 
50
- # Step 3: find zombie locks held by zombies and delete them
51
- # TODO: indexing (in order to prevent full database scan);
52
- # NOTE: original redis does not support indexing so we need to use
53
- # internal data structers to simulate data indexing (such as sorted sets or lists);
54
+ # Step 3: find zombie locks and zombie requests held by zombies and delete them in one pass
55
+ # (the keyspace SCAN or the lock index ZSCAN): write locks and readers registries are
56
+ # processed in batches of `lock_scan_size` locks, lock queues - in batches of
57
+ # `queue_scan_size` queues (two round trips per batch);
54
58
  zombie_locks = Set.new #: Set[String]
55
59
  zombie_acquirers = Set.new #: Set[String]
56
60
 
57
- rconn.scan(
58
- 'MATCH', RedisQueuedLocks::Resource::LOCK_PATTERN, count: lock_scan_size
59
- ) do |lock_key|
60
- acquirer_id, host_id = rconn.call('HMGET', lock_key, 'acq_id', 'hst_id')
61
- if zombie_hosts.include?(host_id)
62
- zombie_locks << lock_key
63
- zombie_acquirers << acquirer_id
64
- end
61
+ if lock_index_enabled
62
+ flush_lock_index_zombies(
63
+ rconn, zombie_hosts, lock_scan_size, queue_scan_size, zombie_locks, zombie_acquirers
64
+ )
65
+ else
66
+ flush_keyspace_zombies(
67
+ rconn, zombie_hosts, lock_scan_size, queue_scan_size, zombie_locks, zombie_acquirers
68
+ )
69
+ end
70
+
71
+ # Step 4: drop zombies from the swarm
72
+ rconn.call('HDEL', RedisQueuedLocks::Resource::SWARM_KEY, *zombie_hosts)
73
+
74
+ # Step 5: inform about deleted zombies
75
+ {
76
+ ok: true,
77
+ deleted_zombie_hosts: zombie_hosts,
78
+ deleted_zombie_acquirers: zombie_acquirers,
79
+ deleted_zombie_locks: zombie_locks
80
+ }
81
+ end
82
+ end
83
+ # rubocop:enable Metrics/MethodLength
84
+
85
+ private
86
+
87
+ # (keyspace) Finds zombie locks (write and read) and zombie requests in one SCAN pass over
88
+ # the RQL keys (`KEY_PATTERN`) and deletes them (the disabled lock index):
89
+ # - write locks and readers registries are processed in batches of `lock_scan_size` keys
90
+ # (two round trips per batch, batches are collected across SCAN pages);
91
+ # - lock queues are collected during the pass and processed after it in batches of
92
+ # `queue_scan_size` queues (two round trips per batch);
93
+ #
94
+ # @param rconn [RedisClient] redis connection obtained via `#with` from RedisClient instance;
95
+ # @param zombie_hosts [Set<String>]
96
+ # @param lock_scan_size [Integer] SCAN count and the batch size of locks.
97
+ # @param queue_scan_size [Integer] The batch size of lock queues.
98
+ # @param zombie_locks [Set<String>] Collects dropped zombie locks.
99
+ # @param zombie_acquirers [Set<String>] Collects acquirers of the dropped zombie locks/requests.
100
+ # @return [void]
101
+ #
102
+ # @api private
103
+ # @since 1.20.0
104
+ # rubocop:disable Metrics/MethodLength
105
+ def flush_keyspace_zombies(
106
+ rconn,
107
+ zombie_hosts,
108
+ lock_scan_size,
109
+ queue_scan_size,
110
+ zombie_locks,
111
+ zombie_acquirers
112
+ )
113
+ # @type var lock_keys: Array[String]
114
+ lock_keys = []
115
+ # @type var lock_readers_keys: Array[String]
116
+ lock_readers_keys = []
117
+ # @type var lock_queues: Array[String]
118
+ lock_queues = []
119
+
120
+ rconn.scan(
121
+ 'MATCH', RedisQueuedLocks::Resource::KEY_PATTERN, count: lock_scan_size
122
+ ) do |key|
123
+ case RedisQueuedLocks::Resource.lock_key_family(key)
124
+ when :lock
125
+ lock_keys << key
126
+ next if lock_keys.size < lock_scan_size
127
+
128
+ flush_zombie_locks(rconn, lock_keys, zombie_hosts, zombie_locks, zombie_acquirers)
129
+ lock_keys.clear
130
+ when :lock_readers
131
+ # NOTE: (RW) zombie read locks (read locks of zombie hosts)
132
+ lock_readers_keys << key
133
+ next if lock_readers_keys.size < lock_scan_size
134
+
135
+ flush_zombie_read_locks(
136
+ rconn, lock_readers_keys, zombie_hosts, zombie_locks, zombie_acquirers
137
+ )
138
+ lock_readers_keys.clear
139
+ when :lock_queue, :read_lock_queue
140
+ lock_queues << key
141
+ end
142
+ end
143
+
144
+ unless lock_keys.empty?
145
+ flush_zombie_locks(rconn, lock_keys, zombie_hosts, zombie_locks, zombie_acquirers)
146
+ end
147
+ unless lock_readers_keys.empty?
148
+ flush_zombie_read_locks(
149
+ rconn, lock_readers_keys, zombie_hosts, zombie_locks, zombie_acquirers
150
+ )
151
+ end
152
+
153
+ # NOTE: drop zombie requests from the lock queues (write and read)
154
+ # - requests of zombie hosts: the host is derived from the acquirer id of the request,
155
+ # so the requests of zombie acquirers that hold no locks are dropped too (and their
156
+ # acquirers are reported as zombie acquirers);
157
+ # - two round trips per batch of `queue_scan_size` queues (requests, then ZREM);
158
+ lock_queues.each_slice(queue_scan_size) do |lock_queues_batch|
159
+ flush_zombie_requests(rconn, lock_queues_batch, zombie_hosts, zombie_acquirers)
160
+ end
161
+ end
162
+ # rubocop:enable Metrics/MethodLength
163
+
164
+ # (lock index) Finds zombie locks (write and read) and zombie requests in one ZSCAN pass over
165
+ # the lock index (`LOCK_INDEX_KEY`: lock names) and deletes them (the enabled lock index):
166
+ # - expired lock names are dropped from the lock index first: all the keys of these locks are
167
+ # expired (they are not visited by the index pass);
168
+ # - lock keys are derived from the lock names instead of the full keyspace SCAN (keys of other
169
+ # applications are never visited);
170
+ # - write locks and readers registries are processed in batches of `lock_scan_size` locks
171
+ # (two round trips per batch, batches are collected across ZSCAN pages);
172
+ # - lock queues (write and read) are processed in batches of `queue_scan_size` queues during
173
+ # the same pass (two round trips per batch): zombie requests are found by the host of
174
+ # the request, so they do not depend on the zombie locks;
175
+ #
176
+ # @param rconn [RedisClient] redis connection obtained via `#with` from RedisClient instance;
177
+ # @param zombie_hosts [Set<String>]
178
+ # @param lock_scan_size [Integer] ZSCAN count and the batch size of locks (lock names).
179
+ # @param queue_scan_size [Integer] The batch size of lock queues.
180
+ # @param zombie_locks [Set<String>] Collects dropped zombie locks.
181
+ # @param zombie_acquirers [Set<String>] Collects acquirers of the dropped zombie locks/requests.
182
+ # @return [void]
183
+ #
184
+ # @api private
185
+ # @since 1.20.0
186
+ # rubocop:disable Metrics/MethodLength
187
+ def flush_lock_index_zombies(
188
+ rconn,
189
+ zombie_hosts,
190
+ lock_scan_size,
191
+ queue_scan_size,
192
+ zombie_locks,
193
+ zombie_acquirers
194
+ )
195
+ # NOTE: drop the expired lock names from the lock index
196
+ rconn.call(
197
+ 'ZREMRANGEBYSCORE', RedisQueuedLocks::Resource::LOCK_INDEX_KEY,
198
+ '-inf', RedisQueuedLocks::Resource.lock_index_time - 1
199
+ )
200
+
201
+ # @type var lock_keys: Array[String]
202
+ lock_keys = []
203
+ # @type var lock_readers_keys: Array[String]
204
+ lock_readers_keys = []
205
+ # @type var lock_queues: Array[String]
206
+ lock_queues = []
207
+
208
+ rconn.zscan(
209
+ RedisQueuedLocks::Resource::LOCK_INDEX_KEY, count: lock_scan_size
210
+ ) do |lock_name|
211
+ lock_keys << RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
212
+ # NOTE: (RW) zombie read locks (read locks of zombie hosts)
213
+ lock_readers_keys << RedisQueuedLocks::Resource.prepare_lock_readers(lock_name)
214
+ if lock_keys.size >= lock_scan_size
215
+ flush_zombie_locks(rconn, lock_keys, zombie_hosts, zombie_locks, zombie_acquirers)
216
+ flush_zombie_read_locks(
217
+ rconn, lock_readers_keys, zombie_hosts, zombie_locks, zombie_acquirers
218
+ )
219
+ lock_keys.clear
220
+ lock_readers_keys.clear
221
+ end
222
+
223
+ # NOTE: drop zombie requests from the lock queues (write and read)
224
+ # - requests of zombie hosts: the host is derived from the acquirer id of the request,
225
+ # so the requests of zombie acquirers that hold no locks are dropped too (and their
226
+ # acquirers are reported as zombie acquirers);
227
+ batch_zombie_requests(
228
+ rconn, lock_queues, RedisQueuedLocks::Resource.prepare_lock_queue(lock_name),
229
+ queue_scan_size, zombie_hosts, zombie_acquirers
230
+ )
231
+ batch_zombie_requests(
232
+ rconn, lock_queues, RedisQueuedLocks::Resource.prepare_read_lock_queue(lock_name),
233
+ queue_scan_size, zombie_hosts, zombie_acquirers
234
+ )
235
+ end
236
+
237
+ unless lock_keys.empty?
238
+ flush_zombie_locks(rconn, lock_keys, zombie_hosts, zombie_locks, zombie_acquirers)
239
+ flush_zombie_read_locks(
240
+ rconn, lock_readers_keys, zombie_hosts, zombie_locks, zombie_acquirers
241
+ )
242
+ end
243
+ return if lock_queues.empty?
244
+
245
+ flush_zombie_requests(rconn, lock_queues, zombie_hosts, zombie_acquirers)
246
+ end
247
+ # rubocop:enable Metrics/MethodLength
248
+
249
+ # (lock index) Adds the lock queue to the batch of lock queues and drops the zombie requests of
250
+ # the batch when it is full (`queue_scan_size` queues; see `flush_zombie_requests`).
251
+ #
252
+ # @param redis_client [RedisClient]
253
+ # @param lock_queues [Array<String>] The batch of lock queues (cleared after the flush).
254
+ # @param lock_queue [String]
255
+ # @param queue_scan_size [Integer]
256
+ # @param zombie_hosts [Set<String>]
257
+ # @param zombie_acquirers [Set<String>] Collects acquirers of the dropped zombie requests.
258
+ # @return [void]
259
+ #
260
+ # @api private
261
+ # @since 1.20.0
262
+ def batch_zombie_requests(
263
+ redis_client,
264
+ lock_queues,
265
+ lock_queue,
266
+ queue_scan_size,
267
+ zombie_hosts,
268
+ zombie_acquirers
269
+ )
270
+ lock_queues << lock_queue
271
+ return if lock_queues.size < queue_scan_size
272
+
273
+ flush_zombie_requests(redis_client, lock_queues, zombie_hosts, zombie_acquirers)
274
+ lock_queues.clear
275
+ end
276
+
277
+ # Drops zombie locks (write locks of zombie hosts) of the given batch of write locks
278
+ # (two round trips: lock owners, then one `DEL` of the zombie locks).
279
+ #
280
+ # @param redis_client [RedisClient]
281
+ # @param lock_keys [Array<String>]
282
+ # @param zombie_hosts [Set<String>]
283
+ # @param zombie_locks [Set<String>] Collects dropped zombie locks.
284
+ # @param zombie_acquirers [Set<String>] Collects acquirers of the dropped zombie locks.
285
+ # @return [void]
286
+ #
287
+ # @api private
288
+ # @since 1.20.0
289
+ def flush_zombie_locks(redis_client, lock_keys, zombie_hosts, zombie_locks, zombie_acquirers)
290
+ # NOTE: (pipeline layout) one reply (acquirer and host) per lock in the batch order
291
+ # @type var lock_owners: Array[[String?, String?]]
292
+ lock_owners = redis_client.pipelined do |pipeline|
293
+ lock_keys.each { |lock_key| pipeline.call('HMGET', lock_key, 'acq_id', 'hst_id') }
294
+ end
295
+
296
+ # @type var batch_zombie_locks: Array[String]
297
+ batch_zombie_locks = []
298
+ lock_keys.each_with_index do |lock_key, index|
299
+ acquirer_id, host_id = lock_owners[index]
300
+ next unless acquirer_id && host_id && zombie_hosts.include?(host_id)
301
+
302
+ batch_zombie_locks << lock_key
303
+ zombie_acquirers << acquirer_id
304
+ end
305
+ return if batch_zombie_locks.empty?
306
+
307
+ redis_client.call('DEL', *batch_zombie_locks)
308
+ zombie_locks.merge(batch_zombie_locks)
309
+ end
310
+
311
+ # (RW) Drops zombie read locks (read locks of zombie hosts) of the given batch of readers
312
+ # registries (two round trips: registry members, then `ZREM` of zombie readers + `DEL` of
313
+ # their read lock data for each registry with zombie readers).
314
+ # - read locks store acquirer ids only (the host is a part of the acquirer id);
315
+ # - readers registry is reported as a zombie lock if any of its read locks is dropped;
316
+ #
317
+ # @param redis_client [RedisClient]
318
+ # @param lock_readers_keys [Array<String>]
319
+ # @param zombie_hosts [Set<String>]
320
+ # @param zombie_locks [Set<String>] Collects registries with dropped zombie read locks.
321
+ # @param zombie_acquirers [Set<String>] Collects acquirers of the dropped zombie read locks.
322
+ # @return [void]
323
+ #
324
+ # @api private
325
+ # @since 1.20.0
326
+ def flush_zombie_read_locks(
327
+ redis_client,
328
+ lock_readers_keys,
329
+ zombie_hosts,
330
+ zombie_locks,
331
+ zombie_acquirers
332
+ )
333
+ # NOTE: (pipeline layout) one reply (read lock acquirers) per registry in the batch order
334
+ # @type var read_lock_acquirers: Array[Array[String]]
335
+ read_lock_acquirers = redis_client.pipelined do |pipeline|
336
+ lock_readers_keys.each do |lock_readers_key|
337
+ pipeline.call('ZRANGE', lock_readers_key, '0', '-1')
65
338
  end
339
+ end
66
340
 
67
- # NOTE: (steep) steep can't use <Set>s for splats
68
- rconn.call('DEL', *zombie_locks) if zombie_locks.any? # steep:ignore
69
-
70
- # Step 3 (RW): find zombie read locks (read locks of zombie hosts) and drop them
71
- # - read locks store acquirer ids only (the host is a part of the acquirer id);
72
- # - readers registry is reported as a zombie lock if any of its read locks is dropped;
73
- rconn.scan(
74
- 'MATCH', RedisQueuedLocks::Resource::LOCK_READERS_PATTERN, count: lock_scan_size
75
- ) do |lock_readers_key|
76
- # @type var read_lock_acquirers: Array[String]
77
- read_lock_acquirers = rconn.call('ZRANGE', lock_readers_key, '0', '-1')
78
- zombie_readers = read_lock_acquirers.select do |acquirer_id|
79
- zombie_hosts.include?(RedisQueuedLocks::Resource.host_identifier_from_acquirer(acquirer_id))
341
+ redis_client.pipelined do |pipeline|
342
+ lock_readers_keys.each_with_index do |lock_readers_key, index|
343
+ zombie_readers = read_lock_acquirers[index].select do |acquirer_id|
344
+ zombie_hosts.include?(
345
+ RedisQueuedLocks::Resource.host_identifier_from_acquirer(acquirer_id)
346
+ )
80
347
  end
81
348
  next if zombie_readers.empty?
82
349
 
83
- rconn.call('ZREM', lock_readers_key, *zombie_readers)
350
+ pipeline.call('ZREM', lock_readers_key, *zombie_readers)
84
351
  # NOTE: drop the zombie read lock data
85
352
  lock_name = RedisQueuedLocks::Resource.lock_name_from_readers(lock_readers_key)
86
- rconn.call(
353
+ pipeline.call(
87
354
  'DEL',
88
355
  *zombie_readers.map do |acquirer_id|
89
356
  RedisQueuedLocks::Resource.prepare_read_lock_key(lock_name, acquirer_id)
@@ -92,41 +359,42 @@ class RedisQueuedLocks::Swarm::FlushZombies < RedisQueuedLocks::Swarm::SwarmElem
92
359
  zombie_locks << lock_readers_key
93
360
  zombie_acquirers.merge(zombie_readers)
94
361
  end
362
+ end
363
+ end
95
364
 
96
- # Step 4: find zombie requests => and drop them
97
- # TODO: indexing (in order to prevent full database scan);
98
- # NOTE: original redis does not support indexing so we need to use
99
- # internal data structers to simulate data indexing (such as sorted sets or lists);
100
- rconn.scan(
101
- 'MATCH', RedisQueuedLocks::Resource::LOCK_QUEUE_PATTERN, count: queue_scan_size
102
- ) do |lock_queue|
103
- zombie_acquirers.each do |zombie_acquirer|
104
- rconn.call('ZREM', lock_queue, zombie_acquirer)
105
- end
106
- end
365
+ # Drops the requests of zombie hosts from the given batch of lock queues (write and read)
366
+ # in two round trips: queued acquirers, then one `ZREM` of the zombie requests per queue
367
+ # (the host of a request is derived from its acquirer id).
368
+ #
369
+ # @param redis_client [RedisClient]
370
+ # @param lock_queues [Array<String>]
371
+ # @param zombie_hosts [Set<String>]
372
+ # @param zombie_acquirers [Set<String>] Collects acquirers of the dropped zombie requests.
373
+ # @return [void]
374
+ #
375
+ # @api private
376
+ # @since 1.20.0
377
+ def flush_zombie_requests(redis_client, lock_queues, zombie_hosts, zombie_acquirers)
378
+ # NOTE: (pipeline layout) one reply (queued acquirers) per queue in the batch order
379
+ # @type var queued_acquirers: Array[Array[String]]
380
+ queued_acquirers = redis_client.pipelined do |pipeline|
381
+ lock_queues.each { |lock_queue| pipeline.call('ZRANGE', lock_queue, '0', '-1') }
382
+ end
107
383
 
108
- # Step 4 (RW): drop zombie requests from the queues of read lock requests
109
- rconn.scan(
110
- 'MATCH', RedisQueuedLocks::Resource::READ_LOCK_QUEUE_PATTERN, count: queue_scan_size
111
- ) do |read_lock_queue|
112
- zombie_acquirers.each do |zombie_acquirer|
113
- rconn.call('ZREM', read_lock_queue, zombie_acquirer)
384
+ redis_client.pipelined do |pipeline|
385
+ lock_queues.each_with_index do |lock_queue, index|
386
+ zombie_requests = queued_acquirers[index].select do |acquirer_id|
387
+ zombie_hosts.include?(
388
+ RedisQueuedLocks::Resource.host_identifier_from_acquirer(acquirer_id)
389
+ )
114
390
  end
115
- end
391
+ next if zombie_requests.empty?
116
392
 
117
- # Step 5: drop zombies from the swarm
118
- rconn.call('HDEL', RedisQueuedLocks::Resource::SWARM_KEY, *zombie_hosts)
119
-
120
- # Step 6: inform about deleted zombies
121
- {
122
- ok: true,
123
- deleted_zombie_hosts: zombie_hosts,
124
- deleted_zombie_acquirers: zombie_acquirers,
125
- deleted_zombie_locks: zombie_locks
126
- }
393
+ pipeline.call('ZREM', lock_queue, *zombie_requests)
394
+ zombie_acquirers.merge(zombie_requests)
395
+ end
127
396
  end
128
397
  end
129
- # rubocop:enable Metrics/MethodLength
130
398
  end
131
399
 
132
400
  # @return [Boolean]
@@ -142,6 +410,8 @@ class RedisQueuedLocks::Swarm::FlushZombies < RedisQueuedLocks::Swarm::SwarmElem
142
410
  #
143
411
  # @api private
144
412
  # @since 1.17.0
413
+ # @version 1.20.0
414
+ # rubocop:disable Metrics/MethodLength
145
415
  def spawn_swarm_element!(swarm_element_results_port)
146
416
  Ractor.new(
147
417
  swarm_element_results_port,
@@ -149,8 +419,9 @@ class RedisQueuedLocks::Swarm::FlushZombies < RedisQueuedLocks::Swarm::SwarmElem
149
419
  rql_client.config['swarm.flush_zombies.zombie_ttl'],
150
420
  rql_client.config['swarm.flush_zombies.zombie_lock_scan_size'],
151
421
  rql_client.config['swarm.flush_zombies.zombie_queue_scan_size'],
152
- rql_client.config['swarm.flush_zombies.zombie_flush_period']
153
- ) do |r_res_p, rc, z_ttl, z_lss, z_qss, z_fl_prd|
422
+ rql_client.config['swarm.flush_zombies.zombie_flush_period'],
423
+ rql_client.config['lock_index_enabled']
424
+ ) do |r_res_p, rc, z_ttl, z_lss, z_qss, z_fl_prd, l_idx_en|
154
425
  RedisQueuedLocks::Swarm::FlushZombies.swarm_loop(r_res_p) do
155
426
  Thread.new do
156
427
  redis_client = RedisQueuedLocks::Swarm::RedisClientBuilder.build(
@@ -162,7 +433,7 @@ class RedisQueuedLocks::Swarm::FlushZombies < RedisQueuedLocks::Swarm::SwarmElem
162
433
 
163
434
  loop do
164
435
  RedisQueuedLocks::Swarm::FlushZombies.flush_zombies(
165
- redis_client, z_ttl, z_lss, z_qss
436
+ redis_client, z_ttl, z_lss, z_qss, l_idx_en
166
437
  )
167
438
  sleep(z_fl_prd)
168
439
  end
@@ -170,5 +441,6 @@ class RedisQueuedLocks::Swarm::FlushZombies < RedisQueuedLocks::Swarm::SwarmElem
170
441
  end
171
442
  end
172
443
  end
444
+ # rubocop:enable Metrics/MethodLength
173
445
  end
174
446
  # rubocop:enable Metrics/ClassLength
@@ -7,19 +7,23 @@ module RedisQueuedLocks::Swarm::RedisClientBuilder
7
7
  # @option pooled [Boolean]
8
8
  # @option sentinel [Boolean]
9
9
  # @option config [Hash]
10
+ # @option pool_config [Hash]
10
11
  # @return [RedisClient|RedisClient::Pooled]
11
12
  #
12
13
  # @api private
13
14
  # @since 1.9.0
15
+ # @version 1.21.0
14
16
  # rubocop:disable Style/RedundantAssignment
15
17
  def build(pooled: false, sentinel: false, config: {}, pool_config: {})
16
- config.transform_keys!(&:to_sym)
17
- pool_config.transform_keys!(&:to_sym)
18
+ # NOTE: the passed hashes are not mutated: they are the values of the client config (they
19
+ # can be frozen and are shared with the zombie flusher Ractor when they are shareable);
20
+ redis_options = config.transform_keys(&:to_sym)
21
+ pool_options = pool_config.transform_keys(&:to_sym)
18
22
 
19
23
  redis_config =
20
- sentinel ? sentinel_config(config) : non_sentinel_config(config)
24
+ sentinel ? sentinel_config(redis_options) : non_sentinel_config(redis_options)
21
25
  redis_client =
22
- pooled ? pooled_client(redis_config, pool_config) : non_pooled_client(redis_config)
26
+ pooled ? pooled_client(redis_config, pool_options) : non_pooled_client(redis_config)
23
27
 
24
28
  redis_client
25
29
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.9.0
5
+ # @version 1.17.0
5
6
  # rubocop:disable Metrics/ClassLength
6
7
  class RedisQueuedLocks::Swarm::SwarmElement::Threaded
7
8
  # @since 1.9.0
@@ -78,7 +79,7 @@ class RedisQueuedLocks::Swarm::SwarmElement::Threaded
78
79
  # @return [void]
79
80
  #
80
81
  # @api private
81
- # @since 19.0.0
82
+ # @since 1.9.0
82
83
  def reswarm_if_dead!
83
84
  return unless enabled?
84
85
 
@@ -154,7 +155,7 @@ class RedisQueuedLocks::Swarm::SwarmElement::Threaded
154
155
 
155
156
  # Swarm element lifecycle have the following flow:
156
157
  # => 1) init (swarm!): create a thread, main loop is not started;
157
- # => 2) start (swarm_loop__start): run the main lopp inside the created thread;
158
+ # => 2) start (swarm_loop__start): run the main loop inside the created thread;
158
159
  # => 3) stop (swarm_loop__stop): stop the main loop inside the created thread;
159
160
  # => 4) terminate (swarm_element__termiante): kill the created thread;
160
161
  # In addition you should implement `#spawn_main_loop!` method that will be the main logic