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,25 +2,41 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.0.0
5
- # @version 1.18.0
5
+ # @version 1.20.0
6
6
  module RedisQueuedLocks::Acquirer::Queues
7
7
  class << self
8
8
  # @param redis_client [RedisClient]
9
9
  # @option scan_size [Integer]
10
10
  # @option with_info [Boolean]
11
+ # @option lock_index_enabled [Boolean]
12
+ # Find lock queues via the lock index (`index_queues`) instead of the keyspace SCAN
13
+ # (`scan_queues`).
11
14
  # @return [Set<String>,Set<Hash<Symbol,Any>>]
12
15
  #
13
16
  # @api private
14
17
  # @since 1.0.0
15
- def queues(redis_client, scan_size:, with_info:)
18
+ # @version 1.21.0
19
+ def queues(redis_client, scan_size:, with_info:, lock_index_enabled:)
20
+ # NOTE: (arguments) the same range as the config key: zero/negative sizes fail SCAN/ZSCAN
21
+ # (`COUNT 0`) and the batching (`each_slice(0)`);
22
+ if !scan_size.is_a?(::Integer) || scan_size <= 0
23
+ raise(
24
+ RedisQueuedLocks::ArgumentError,
25
+ "`:scan_size` argument should be a positive Integer, got #{scan_size.inspect}."
26
+ )
27
+ end
28
+
16
29
  redis_client.with do |rconn|
17
- lock_queues = scan_queues(rconn, scan_size)
18
- with_info ? extract_queues_info(rconn, lock_queues) : lock_queues
30
+ lock_queues =
31
+ lock_index_enabled ? index_queues(rconn, scan_size) : scan_queues(rconn, scan_size)
32
+ with_info ? extract_queues_info(rconn, lock_queues, scan_size) : lock_queues
19
33
  end
20
34
  end
21
35
 
22
36
  private
23
37
 
38
+ # Write and read lock queues in one SCAN pass (the disabled lock index).
39
+ #
24
40
  # @param redis_client [RedisClient]
25
41
  # @param scan_size [Integer]
26
42
  # @return [Set<String>]
@@ -28,80 +44,137 @@ module RedisQueuedLocks::Acquirer::Queues
28
44
  #
29
45
  # @api private
30
46
  # @since 1.0.0
31
- # @version 1.18.0
47
+ # @version 1.20.0
32
48
  def scan_queues(redis_client, scan_size)
33
49
  Set.new.tap do |lock_queues|
34
50
  # @type var lock_queues: Set[String]
35
51
  redis_client.scan(
36
52
  'MATCH',
37
- RedisQueuedLocks::Resource::LOCK_QUEUE_PATTERN,
53
+ RedisQueuedLocks::Resource::ALL_LOCK_QUEUES_PATTERN,
38
54
  count: scan_size
39
- ) do |lock_queue|
40
- # TODO: reduce unnecessary iterations
41
- # @type var lock_queue: String
42
- lock_queues.add(lock_queue)
55
+ ) do |key|
56
+ # NOTE: the pattern matches other keys too (lock names with `queue:`): lock queues only
57
+ family = RedisQueuedLocks::Resource.lock_key_family(key)
58
+ lock_queues.add(key) if family == :lock_queue || family == :read_lock_queue
43
59
  end
60
+ end
61
+ end
44
62
 
45
- # NOTE: (RW) queues of read lock requests
46
- redis_client.scan(
47
- 'MATCH',
48
- RedisQueuedLocks::Resource::READ_LOCK_QUEUE_PATTERN,
49
- count: scan_size
50
- ) do |read_lock_queue|
51
- # TODO: reduce unnecessary iterations
52
- # @type var read_lock_queue: String
53
- lock_queues.add(read_lock_queue)
63
+ # Write and read lock queues of the lock index (the enabled lock index): one ZSCAN pass over
64
+ # the lock names (`RedisQueuedLocks::Resource::LOCK_INDEX_KEY`, the keyspace is never scanned)
65
+ # with one round trip per batch of `scan_size` lock names (`EXISTS` of both lock queues of
66
+ # each lock name).
67
+ #
68
+ # @param redis_client [RedisClient]
69
+ # @param scan_size [Integer] ZSCAN count and the number of lock names in one batch.
70
+ # @return [Set<String>]
71
+ # - (RW) queues of read lock requests are included;
72
+ #
73
+ # @api private
74
+ # @since 1.20.0
75
+ def index_queues(redis_client, scan_size)
76
+ Set.new.tap do |lock_queues|
77
+ # @type var lock_queues: Set[String]
78
+ each_lock_name_batch(redis_client, scan_size) do |lock_names|
79
+ # NOTE: (pipeline layout) two replies per lock name in the batch order:
80
+ # EXISTS (write lock queue), EXISTS (read lock queue);
81
+ # @type var queues_existence: Array[Integer]
82
+ queues_existence = redis_client.pipelined do |pipeline|
83
+ lock_names.each do |lock_name|
84
+ pipeline.call('EXISTS', RedisQueuedLocks::Resource.prepare_lock_queue(lock_name))
85
+ pipeline.call(
86
+ 'EXISTS', RedisQueuedLocks::Resource.prepare_read_lock_queue(lock_name)
87
+ )
88
+ end
89
+ end
90
+
91
+ lock_names.each_with_index do |lock_name, index|
92
+ if queues_existence[index * 2] == 1
93
+ lock_queues.add(RedisQueuedLocks::Resource.prepare_lock_queue(lock_name))
94
+ end
95
+ if queues_existence[(index * 2) + 1] == 1
96
+ lock_queues.add(RedisQueuedLocks::Resource.prepare_read_lock_queue(lock_name))
97
+ end
98
+ end
54
99
  end
55
100
  end
56
101
  end
57
102
 
103
+ # Iterates over the lock names of the lock index (one ZSCAN pass) in batches of `scan_size`
104
+ # lock names (batches are collected across ZSCAN pages). Expired lock names (not dropped from
105
+ # the index yet) are skipped: all their keys are expired.
106
+ #
107
+ # NOTE: each operation module iterates over the lock index by itself (duplicated on purpose:
108
+ # operation modules are independent of each other, see `Locks.each_lock_name_batch`);
109
+ #
110
+ # @param redis_client [RedisClient]
111
+ # @param scan_size [Integer] ZSCAN count and the number of lock names in one batch.
112
+ # @yield [lock_names]
113
+ # @yieldparam lock_names [Array<String>] The batch of lock names (reused between batches).
114
+ # @return [void]
115
+ #
116
+ # @api private
117
+ # @since 1.20.0
118
+ def each_lock_name_batch(redis_client, scan_size)
119
+ lock_index_time = RedisQueuedLocks::Resource.lock_index_time
120
+ # @type var lock_names: Array[String]
121
+ lock_names = []
122
+
123
+ redis_client.zscan(
124
+ RedisQueuedLocks::Resource::LOCK_INDEX_KEY, count: scan_size
125
+ ) do |lock_name, lock_index_score|
126
+ next if lock_index_score.to_f < lock_index_time
127
+
128
+ lock_names << lock_name
129
+ next if lock_names.size < scan_size
130
+
131
+ yield(lock_names)
132
+ lock_names.clear
133
+ end
134
+
135
+ yield(lock_names) unless lock_names.empty?
136
+ end
137
+
58
138
  # @param redis_client [RedisClient]
59
139
  # @param lock_queues [Set<String>]
140
+ # @param scan_size [Integer] The number of queues whose requests are read in one round trip.
60
141
  # @return [Set<Hash<Symbol,Any>>]
61
142
  # - each request: `{ 'acq_id' => String, 'score' => Float, 'rw_mode' => 'read'/'write' }`;
62
143
  #
63
144
  # @api private
64
145
  # @since 1.0.0
65
- # @version 1.18.0
66
- def extract_queues_info(redis_client, lock_queues)
146
+ # @version 1.20.0
147
+ def extract_queues_info(redis_client, lock_queues, scan_size)
67
148
  # NOTE: the same approach as in `QueueInfo` (duplicated on purpose: public operation modules
68
149
  # do not reuse each other, they share logic via `Resource`, `Utilities`);
69
150
  Set.new.tap do |seeded_queues|
70
- # Step X: iterate over each lock queue and extract their info
71
151
  # @type var seeded_queues: Set[Hash[Symbol,untyped]]
72
- lock_queues.each do |lock_queue|
73
- # NOTE:
74
- # - (RW) the mode of the requests of the queue (`'read'` or `'write'`);
75
- # - lock queues are scanned by the lock queue patterns, so the mode is never `nil`;
76
- rw_mode = RedisQueuedLocks::Resource.lock_queue_rw_mode(lock_queue) #: String
77
-
78
- # Step 1: extract lock queue info from reids
79
- queue_info = redis_client.pipelined do |pipeline|
80
- pipeline.call('EXISTS', lock_queue)
81
- pipeline.call('ZRANGE', lock_queue, '0', '-1', 'WITHSCORES')
82
- end.yield_self do |result| # Step 2: format the result
83
- # @type var result: [Integer, Array[[String,Float]]]
152
+ lock_queues.each_slice(scan_size) do |lock_queues_batch|
153
+ # Step 1: extract the requests of the batch of lock queues in one round trip
154
+ # - redis deletes empty sorted sets, so a queue that does not exist anymore gives
155
+ # an empty ZRANGE result (no need for EXISTS checks);
156
+ # - (pipeline layout) one reply per queue in the batch order;
157
+ # @type var queues_requests: Array[Array[[String,Float]]]
158
+ queues_requests = redis_client.pipelined do |pipeline|
159
+ lock_queues_batch.each do |lock_queue|
160
+ pipeline.call('ZRANGE', lock_queue, '0', '-1', 'WITHSCORES')
161
+ end
162
+ end
84
163
 
85
- exists_cmd_res = result[0]
86
- zrange_cmd_res = result[1]
164
+ lock_queues_batch.each_with_index do |lock_queue, index|
165
+ # NOTE:
166
+ # - (RW) the mode of the requests of the queue (`'read'` or `'write'`);
167
+ # - lock queues are scanned by the lock queue patterns, so the mode is never `nil`;
168
+ rw_mode = RedisQueuedLocks::Resource.lock_queue_rw_mode(lock_queue) #: String
87
169
 
88
- if exists_cmd_res == 1 # Step 2.X: lock queue existed during the piepline invocation
89
- zrange_cmd_res.map do |val|
170
+ # Step 2: format the result and push it to the result store
171
+ seeded_queues << {
172
+ queue: lock_queue,
173
+ requests: queues_requests[index].map do |val|
90
174
  { 'acq_id' => val[0], 'score' => val[1], 'rw_mode' => rw_mode }
91
175
  end
92
- else
93
- # Step 2.Y: lock queue did not exist during the pipeline invocation
94
- [] #: Array[Hash[String,String|Float]]
95
- end
176
+ }
96
177
  end
97
-
98
- # @type var queue_info: Array[Hash[String,String|Float]]
99
-
100
- # Step 3: push the lock queue info to the result store
101
- seeded_queues << {
102
- queue: lock_queue,
103
- requests: queue_info
104
- }
105
178
  end
106
179
  end
107
180
  end
@@ -0,0 +1,253 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
4
+ # @since 1.20.0
5
+ # @version 1.21.0
6
+ # rubocop:disable Metrics/ModuleLength
7
+ module RedisQueuedLocks::Acquirer::ReindexLocks
8
+ # @since 1.21.0
9
+ extend RedisQueuedLocks::Utilities
10
+
11
+ # rubocop:disable Metrics/ClassLength
12
+ class << self
13
+ # Adds the lock keys without the lock index entry (keys of older RQL versions created before
14
+ # the lock index) to the lock index (`RedisQueuedLocks::Resource::LOCK_INDEX_KEY`):
15
+ # - one SCAN pass over the RQL keys (`KEY_PATTERN`) with pipelined batches of `scan_size`
16
+ # keys collected across SCAN pages (two round trips per batch: key TTLs, then the index);
17
+ # - write locks and readers registries are indexed with their remaining TTL (`PTTL`):
18
+ # the score is the client time + the remaining TTL + the grace period;
19
+ # - lock queues without TTL (older RQL versions) get the queue TTL first (`PEXPIRE NX`):
20
+ # the lifetime of every indexed key is bounded;
21
+ # - read lock data is not indexed: it is found via its readers registry;
22
+ # - keys without TTL (unreachable for RQL keys) are indexed forever (`+inf` score);
23
+ # - the index score is never decreased (`ZADD GT`): the operation is idempotent and can be
24
+ # invoked at any time (keys of the current RQL version are indexed already);
25
+ # - the operation is reported with one log and one instrumentation event
26
+ # (`redis_queued_locks.reindex_locks`) with the time of the whole operation;
27
+ #
28
+ # @param redis_client [RedisClient]
29
+ # @param scan_size [Integer]
30
+ # @param queue_ttl [Integer] TTL of the lock queues without TTL (in seconds).
31
+ # @param lock_index_grace_period [Integer]
32
+ # The lock index score margin over the key lifetime (in milliseconds).
33
+ # @param logger [::Logger,#debug]
34
+ # @param instrumenter [#notify]
35
+ # @param instrument [NilClass,Any]
36
+ # @param log_sampling_enabled [Boolean]
37
+ # @param log_sampling_percent [Integer]
38
+ # @param log_sampler [#sampling_happened?,Module<RedisQueuedLocks::Logging::Sampler>]
39
+ # @param log_sample_this [Boolean]
40
+ # @param instr_sampling_enabled [Boolean]
41
+ # @param instr_sampling_percent [Integer]
42
+ # @param instr_sampler [#sampling_happened?,Module<RedisQueuedLocks::Instrument::Sampler>]
43
+ # @param instr_sample_this [Boolean]
44
+ # @return [Hash<Symbol,Boolean|Hash<Symbol,Integer>>]
45
+ # Format: { ok: true, result: { idx_key_cnt: Integer } } (the number of indexed keys)
46
+ #
47
+ # @api private
48
+ # @since 1.20.0
49
+ # @version 1.21.0
50
+ # rubocop:disable Metrics/MethodLength
51
+ def reindex_locks(
52
+ redis_client,
53
+ scan_size,
54
+ queue_ttl,
55
+ lock_index_grace_period,
56
+ logger,
57
+ instrumenter,
58
+ instrument,
59
+ log_sampling_enabled,
60
+ log_sampling_percent,
61
+ log_sampler,
62
+ log_sample_this,
63
+ instr_sampling_enabled,
64
+ instr_sampling_percent,
65
+ instr_sampler,
66
+ instr_sample_this
67
+ )
68
+ # NOTE: (arguments) the same ranges as the config keys: zero/negative sizes fail SCAN
69
+ # (`COUNT 0`), a non-positive queue ttl expires the lock queues without TTL at once;
70
+ if !scan_size.is_a?(::Integer) || scan_size <= 0
71
+ raise(
72
+ RedisQueuedLocks::ArgumentError,
73
+ "`:scan_size` argument should be a positive Integer, got #{scan_size.inspect}."
74
+ )
75
+ end
76
+ if !queue_ttl.is_a?(::Integer) || queue_ttl <= 0
77
+ raise(
78
+ RedisQueuedLocks::ArgumentError,
79
+ "`:queue_ttl` argument should be a positive Integer, got #{queue_ttl.inspect}."
80
+ )
81
+ end
82
+ if !lock_index_grace_period.is_a?(::Integer) || lock_index_grace_period < 0
83
+ raise(
84
+ RedisQueuedLocks::ArgumentError,
85
+ "`:lock_index_grace_period` argument should be a non-negative Integer, " \
86
+ "got #{lock_index_grace_period.inspect}."
87
+ )
88
+ end
89
+
90
+ idx_start_time = clock_gettime
91
+ idx_key_cnt = fully_reindex_locks(redis_client, scan_size, queue_ttl, lock_index_grace_period)
92
+ time_at = Time.now.to_f
93
+ idx_end_time = clock_gettime
94
+ idx_time = ((idx_end_time - idx_start_time) / 1_000.0).ceil(2)
95
+
96
+ log_sampled = RedisQueuedLocks::Logging.should_log?(
97
+ log_sampling_enabled,
98
+ log_sample_this,
99
+ log_sampling_percent,
100
+ log_sampler
101
+ )
102
+ instr_sampled = RedisQueuedLocks::Instrument.should_instrument?(
103
+ instr_sampling_enabled,
104
+ instr_sample_this,
105
+ instr_sampling_percent,
106
+ instr_sampler
107
+ )
108
+
109
+ run_non_critical do
110
+ logger.debug do
111
+ "[redis_queued_locks.reindex_locks] " \
112
+ "idx_key_cnt => #{idx_key_cnt} " \
113
+ "idx_time => #{idx_time} " \
114
+ "at => #{time_at}"
115
+ end
116
+ end if log_sampled
117
+
118
+ run_non_critical do
119
+ instrumenter.notify('redis_queued_locks.reindex_locks', {
120
+ idx_key_cnt: idx_key_cnt,
121
+ idx_time: idx_time,
122
+ at: time_at,
123
+ instrument: instrument
124
+ })
125
+ end if instr_sampled
126
+
127
+ { ok: true, result: { idx_key_cnt: } }
128
+ end
129
+ # rubocop:enable Metrics/MethodLength
130
+
131
+ private
132
+
133
+ # Adds the lock keys without the lock index entry to the lock index (see `reindex_locks`).
134
+ #
135
+ # @param redis_client [RedisClient]
136
+ # @param scan_size [Integer]
137
+ # @param queue_ttl [Integer] TTL of the lock queues without TTL (in seconds).
138
+ # @param lock_index_grace_period [Integer]
139
+ # @return [Integer] The number of indexed keys.
140
+ #
141
+ # @api private
142
+ # @since 1.21.0
143
+ def fully_reindex_locks(redis_client, scan_size, queue_ttl, lock_index_grace_period)
144
+ # NOTE: `ceil` - queue ttl can be passed as a Float (in seconds), PEXPIRE requires Integer
145
+ queue_pttl = (queue_ttl * 1_000).ceil
146
+ idx_key_cnt = 0
147
+
148
+ redis_client.with do |rconn|
149
+ # NOTE: write locks and readers registries of the batch
150
+ # @type var lock_keys: Array[String]
151
+ lock_keys = []
152
+ # @type var lock_queues: Array[String]
153
+ lock_queues = []
154
+
155
+ rconn.scan('MATCH', RedisQueuedLocks::Resource::KEY_PATTERN, count: scan_size) do |key|
156
+ case RedisQueuedLocks::Resource.lock_key_family(key)
157
+ when :lock, :lock_readers then lock_keys << key
158
+ when :lock_queue, :read_lock_queue then lock_queues << key
159
+ else next # NOTE: read lock data and keys out of the lock key families
160
+ end
161
+ next if (lock_keys.size + lock_queues.size) < scan_size
162
+
163
+ idx_key_cnt += index_lock_keys(
164
+ rconn, lock_keys, lock_queues, queue_pttl, lock_index_grace_period
165
+ )
166
+ lock_keys.clear
167
+ lock_queues.clear
168
+ end
169
+
170
+ unless lock_keys.empty? && lock_queues.empty?
171
+ idx_key_cnt += index_lock_keys(
172
+ rconn, lock_keys, lock_queues, queue_pttl, lock_index_grace_period
173
+ )
174
+ end
175
+ end
176
+
177
+ idx_key_cnt
178
+ end
179
+
180
+ # Adds the given batch of lock keys to the lock index (two round trips: key TTLs, then
181
+ # the index entries).
182
+ #
183
+ # @param redis_client [RedisClient]
184
+ # @param lock_keys [Array<String>] Write locks and readers registries.
185
+ # @param lock_queues [Array<String>] Lock queues (write and read).
186
+ # @param queue_pttl [Integer] TTL of the lock queues without TTL (in milliseconds).
187
+ # @param lock_index_grace_period [Integer] In milliseconds.
188
+ # @return [Integer] The number of indexed keys (keys deleted after the SCAN are not indexed).
189
+ #
190
+ # @api private
191
+ # @since 1.20.0
192
+ def index_lock_keys(redis_client, lock_keys, lock_queues, queue_pttl, lock_index_grace_period)
193
+ # NOTE: (pipeline layout) `PEXPIRE NX` + `PTTL` for each lock queue (two replies per queue),
194
+ # then `PTTL` for each lock key, in the batch order;
195
+ # @type var key_ttls: Array[Integer]
196
+ key_ttls = redis_client.pipelined do |pipeline|
197
+ lock_queues.each do |lock_queue|
198
+ pipeline.call('PEXPIRE', lock_queue, queue_pttl, 'NX')
199
+ pipeline.call('PTTL', lock_queue)
200
+ end
201
+ lock_keys.each { |lock_key| pipeline.call('PTTL', lock_key) }
202
+ end
203
+ # NOTE: the client time after the TTLs are received: the score is not less than the key
204
+ # expiration time (+ the grace period for the client clock skew);
205
+ lock_index_time = RedisQueuedLocks::Resource.lock_index_time
206
+ indexed_keys_count = 0
207
+
208
+ redis_client.pipelined do |pipeline|
209
+ lock_queues.each_with_index do |lock_queue, index|
210
+ indexed_keys_count += index_lock_key(
211
+ pipeline, lock_queue, key_ttls[(index * 2) + 1], lock_index_time,
212
+ lock_index_grace_period
213
+ )
214
+ end
215
+ lock_keys.each_with_index do |lock_key, index|
216
+ indexed_keys_count += index_lock_key(
217
+ pipeline, lock_key, key_ttls[(lock_queues.size * 2) + index], lock_index_time,
218
+ lock_index_grace_period
219
+ )
220
+ end
221
+ end
222
+
223
+ indexed_keys_count
224
+ end
225
+
226
+ # Adds the lock name of the given key to the lock index (`ZADD GT`).
227
+ #
228
+ # @param pipeline [RedisClient::Pipeline]
229
+ # @param key [String] Write lock, readers registry or lock queue.
230
+ # @param key_ttl [Integer] The remaining key TTL (`PTTL`: `-2` - no key, `-1` - no TTL).
231
+ # @param lock_index_time [Integer] Epoch, in milliseconds.
232
+ # @param lock_index_grace_period [Integer] In milliseconds.
233
+ # @return [Integer] `1` - the key is indexed, `0` - the key does not exist anymore.
234
+ #
235
+ # @api private
236
+ # @since 1.20.0
237
+ def index_lock_key(pipeline, key, key_ttl, lock_index_time, lock_index_grace_period)
238
+ return 0 if key_ttl == -2
239
+
240
+ lock_name = RedisQueuedLocks::Resource.lock_name_from_key(
241
+ key, RedisQueuedLocks::Resource.lock_key_family(key)
242
+ )
243
+ lock_index_score =
244
+ (key_ttl == -1) ? '+inf' : (lock_index_time + key_ttl + lock_index_grace_period)
245
+ pipeline.call(
246
+ 'ZADD', RedisQueuedLocks::Resource::LOCK_INDEX_KEY, 'GT', lock_index_score, lock_name
247
+ )
248
+ 1
249
+ end
250
+ end
251
+ # rubocop:enable Metrics/ClassLength
252
+ end
253
+ # rubocop:enable Metrics/ModuleLength