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.
- checksums.yaml +4 -4
- data/.claude/project-overview.md +96 -23
- data/.claude/rules/acquirer.md +39 -20
- data/.claude/rules/arguments.md +3 -2
- data/.claude/rules/git.md +3 -2
- data/.claude/rules/logic.md +32 -8
- data/.claude/rules/swarm.md +14 -9
- data/.claude/rules/tests.md +12 -3
- data/.claude/rules/type-checking.md +2 -1
- data/.claude/rules/visitors.md +10 -11
- data/.claude/skills/rql-code-review/SKILL.md +386 -0
- data/.claude/skills/rql-code-review/templates/report.en.md +31 -0
- data/.claude/skills/rql-code-review/templates/report.ru.md +31 -0
- data/CHANGELOG.md +194 -0
- data/CLAUDE.md +6 -3
- data/README.md +502 -92
- data/lib/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue/log_visitor.rb +1 -0
- data/lib/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rb +42 -0
- data/lib/redis_queued_locks/acquirer/acquire_lock/log_visitor.rb +45 -0
- data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock/log_visitor.rb +1 -0
- data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rb +695 -128
- data/lib/redis_queued_locks/acquirer/acquire_lock/with_acq_timeout.rb +2 -2
- data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire/log_visitor.rb +1 -0
- data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire.rb +8 -6
- data/lib/redis_queued_locks/acquirer/acquire_lock.rb +134 -26
- data/lib/redis_queued_locks/acquirer/clear_dead_requests.rb +229 -24
- data/lib/redis_queued_locks/acquirer/extend_lock_ttl.rb +189 -18
- data/lib/redis_queued_locks/acquirer/is_locked.rb +0 -1
- data/lib/redis_queued_locks/acquirer/is_queued.rb +0 -1
- data/lib/redis_queued_locks/acquirer/keys.rb +151 -7
- data/lib/redis_queued_locks/acquirer/lock_info.rb +39 -41
- data/lib/redis_queued_locks/acquirer/lock_series_poc/instr_visitor.rb +3 -2
- data/lib/redis_queued_locks/acquirer/lock_series_poc/log_visitor.rb +5 -4
- data/lib/redis_queued_locks/acquirer/lock_series_poc.rb +79 -20
- data/lib/redis_queued_locks/acquirer/locks.rb +212 -83
- data/lib/redis_queued_locks/acquirer/queue_info.rb +11 -18
- data/lib/redis_queued_locks/acquirer/queues.rb +123 -50
- data/lib/redis_queued_locks/acquirer/reindex_locks.rb +253 -0
- data/lib/redis_queued_locks/acquirer/release_all_locks.rb +177 -61
- data/lib/redis_queued_locks/acquirer/release_lock.rb +84 -25
- data/lib/redis_queued_locks/acquirer/release_locks_of.rb +274 -70
- data/lib/redis_queued_locks/acquirer/release_read_lock.rb +22 -3
- data/lib/redis_queued_locks/acquirer.rb +1 -0
- data/lib/redis_queued_locks/client.rb +164 -36
- data/lib/redis_queued_locks/config.rb +16 -13
- data/lib/redis_queued_locks/errors.rb +34 -4
- data/lib/redis_queued_locks/instrument/sampler.rb +4 -7
- data/lib/redis_queued_locks/logging/sampler.rb +4 -8
- data/lib/redis_queued_locks/resource.rb +113 -6
- data/lib/redis_queued_locks/swarm/flush_zombies.rb +333 -61
- data/lib/redis_queued_locks/swarm/redis_client_builder.rb +8 -4
- data/lib/redis_queued_locks/swarm/swarm_element/threaded.rb +3 -2
- data/lib/redis_queued_locks/swarm/zombie_info.rb +203 -53
- data/lib/redis_queued_locks/swarm.rb +97 -5
- data/lib/redis_queued_locks/utilities.rb +1 -1
- data/lib/redis_queued_locks/version.rb +2 -2
- data/sig/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rbs +14 -0
- data/sig/redis_queued_locks/acquirer/acquire_lock/log_visitor.rbs +13 -0
- data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rbs +107 -1
- data/sig/redis_queued_locks/acquirer/acquire_lock/yield_expire.rbs +3 -3
- data/sig/redis_queued_locks/acquirer/acquire_lock.rbs +3 -1
- data/sig/redis_queued_locks/acquirer/clear_dead_requests.rbs +19 -1
- data/sig/redis_queued_locks/acquirer/extend_lock_ttl.rbs +20 -4
- data/sig/redis_queued_locks/acquirer/keys.rbs +11 -1
- data/sig/redis_queued_locks/acquirer/locks.rbs +15 -6
- data/sig/redis_queued_locks/acquirer/queues.rbs +9 -2
- data/sig/redis_queued_locks/acquirer/reindex_locks.rbs +52 -0
- data/sig/redis_queued_locks/acquirer/release_all_locks.rbs +8 -1
- data/sig/redis_queued_locks/acquirer/release_lock.rbs +6 -3
- data/sig/redis_queued_locks/acquirer/release_locks_of.rbs +20 -1
- data/sig/redis_queued_locks/acquirer/release_read_lock.rbs +1 -1
- data/sig/redis_queued_locks/client.rbs +21 -2
- data/sig/redis_queued_locks/errors.rbs +5 -2
- data/sig/redis_queued_locks/instrument/sampler.rbs +0 -2
- data/sig/redis_queued_locks/logging/sampler.rbs +0 -2
- data/sig/redis_queued_locks/resource.rbs +7 -1
- data/sig/redis_queued_locks/swarm/flush_zombies.rbs +51 -1
- data/sig/redis_queued_locks/swarm/zombie_info.rbs +58 -7
- data/sig/vendor/redis_client.rbs +2 -0
- metadata +6 -1
|
@@ -2,25 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
# @api private
|
|
4
4
|
# @since 1.0.0
|
|
5
|
-
# @version 1.
|
|
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
|
-
|
|
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 =
|
|
18
|
-
|
|
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.
|
|
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::
|
|
53
|
+
RedisQueuedLocks::Resource::ALL_LOCK_QUEUES_PATTERN,
|
|
38
54
|
count: scan_size
|
|
39
|
-
) do |
|
|
40
|
-
#
|
|
41
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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.
|
|
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.
|
|
73
|
-
#
|
|
74
|
-
# -
|
|
75
|
-
#
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
end
|
|
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
|
-
|
|
86
|
-
|
|
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
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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
|