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,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# @api private
|
|
4
4
|
# @since 1.0.0
|
|
5
|
-
# @version 1.
|
|
5
|
+
# @version 1.21.0
|
|
6
6
|
# rubocop:disable Metrics/ModuleLength
|
|
7
7
|
module RedisQueuedLocks::Acquirer::ExtendLockTTL
|
|
8
8
|
# @return [String]
|
|
@@ -14,11 +14,18 @@ module RedisQueuedLocks::Acquirer::ExtendLockTTL
|
|
|
14
14
|
return redis.call("PEXPIRE", KEYS[1], new_lock_pttl);
|
|
15
15
|
LUA_SCRIPT
|
|
16
16
|
|
|
17
|
+
# @since 1.21.0
|
|
18
|
+
extend RedisQueuedLocks::Utilities
|
|
19
|
+
|
|
17
20
|
# rubocop:disable Metrics/ClassLength
|
|
18
21
|
class << self
|
|
22
|
+
# Extends the lock TTL and reports the operation (one log and one instrumentation event
|
|
23
|
+
# `redis_queued_locks.extend_lock_ttl` with the time of the whole operation).
|
|
24
|
+
#
|
|
19
25
|
# @param redis_client [RedisClient]
|
|
20
26
|
# @param lock_name [String]
|
|
21
27
|
# @param milliseconds [Integer]
|
|
28
|
+
# How many milliseconds should be added: a positive Integer.
|
|
22
29
|
# @param read_write_mode [Symbol]
|
|
23
30
|
# - `:write` - extends the write lock (of any acquirer);
|
|
24
31
|
# - `:read` - extends the read lock of the given acquirer (other read locks are not affected)
|
|
@@ -28,6 +35,8 @@ module RedisQueuedLocks::Acquirer::ExtendLockTTL
|
|
|
28
35
|
# (used for `:read` mode only, ignored for `:write` mode).
|
|
29
36
|
# @param acquirer_id [String]
|
|
30
37
|
# The read lock acquirer (used for `:read` mode without `all_read_locks` only).
|
|
38
|
+
# @param lock_index_enabled [Boolean]
|
|
39
|
+
# Increase the lock index score of the lock name by the extension (the enabled lock index).
|
|
31
40
|
# @param logger [::Logger,#debug]
|
|
32
41
|
# @param instrumenter [#notify]
|
|
33
42
|
# @param instrument [NilClass,Any]
|
|
@@ -43,7 +52,7 @@ module RedisQueuedLocks::Acquirer::ExtendLockTTL
|
|
|
43
52
|
#
|
|
44
53
|
# @api private
|
|
45
54
|
# @since 1.0.0
|
|
46
|
-
# @version 1.
|
|
55
|
+
# @version 1.21.0
|
|
47
56
|
# rubocop:disable Metrics/MethodLength
|
|
48
57
|
def extend_lock_ttl(
|
|
49
58
|
redis_client,
|
|
@@ -52,6 +61,7 @@ module RedisQueuedLocks::Acquirer::ExtendLockTTL
|
|
|
52
61
|
read_write_mode,
|
|
53
62
|
all_read_locks,
|
|
54
63
|
acquirer_id,
|
|
64
|
+
lock_index_enabled,
|
|
55
65
|
logger,
|
|
56
66
|
instrumenter,
|
|
57
67
|
instrument,
|
|
@@ -72,31 +82,144 @@ module RedisQueuedLocks::Acquirer::ExtendLockTTL
|
|
|
72
82
|
)
|
|
73
83
|
end
|
|
74
84
|
|
|
85
|
+
# NOTE: a non-positive value would decrease the lock ttl or delete the lock;
|
|
86
|
+
if !milliseconds.is_a?(::Integer) || milliseconds <= 0
|
|
87
|
+
raise(
|
|
88
|
+
RedisQueuedLocks::ArgumentError,
|
|
89
|
+
"`milliseconds` argument should be a positive Integer, got #{milliseconds.inspect}."
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
if read_write_mode == :read && all_read_locks != true && all_read_locks != false
|
|
94
|
+
raise(
|
|
95
|
+
RedisQueuedLocks::ArgumentError,
|
|
96
|
+
"`:all_read_locks` argument should be a boolean, got #{all_read_locks.inspect}."
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
|
|
75
100
|
lock_key = RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
|
|
76
101
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
102
|
+
ext_start_time = clock_gettime
|
|
103
|
+
result = fully_extend_lock_ttl(
|
|
104
|
+
redis_client, lock_name, lock_key, milliseconds, read_write_mode, all_read_locks,
|
|
105
|
+
acquirer_id, lock_index_enabled
|
|
106
|
+
)
|
|
107
|
+
time_at = Time.now.to_f
|
|
108
|
+
ext_end_time = clock_gettime
|
|
109
|
+
ext_time = ((ext_end_time - ext_start_time) / 1_000.0).ceil(2)
|
|
110
|
+
ext_result = result[:result]
|
|
111
|
+
extended_locks_count = ext_result.is_a?(::Hash) ? ext_result[:extended_locks_count] : 0
|
|
112
|
+
|
|
113
|
+
log_sampled = RedisQueuedLocks::Logging.should_log?(
|
|
114
|
+
log_sampling_enabled,
|
|
115
|
+
log_sample_this,
|
|
116
|
+
log_sampling_percent,
|
|
117
|
+
log_sampler
|
|
118
|
+
)
|
|
119
|
+
instr_sampled = RedisQueuedLocks::Instrument.should_instrument?(
|
|
120
|
+
instr_sampling_enabled,
|
|
121
|
+
instr_sample_this,
|
|
122
|
+
instr_sampling_percent,
|
|
123
|
+
instr_sampler
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
run_non_critical do
|
|
127
|
+
logger.debug do
|
|
128
|
+
"[redis_queued_locks.extend_lock_ttl] " \
|
|
129
|
+
"lock_key => '#{lock_key}' " \
|
|
130
|
+
"rw_mode => '#{read_write_mode}' " \
|
|
131
|
+
"all_read_locks => #{all_read_locks} " \
|
|
132
|
+
"ext_ttl => #{milliseconds} " \
|
|
133
|
+
"extended_locks_count => #{extended_locks_count} " \
|
|
134
|
+
"ext_time => #{ext_time} " \
|
|
135
|
+
"at => #{time_at}"
|
|
83
136
|
end
|
|
137
|
+
end if log_sampled
|
|
138
|
+
|
|
139
|
+
run_non_critical do
|
|
140
|
+
instrumenter.notify('redis_queued_locks.extend_lock_ttl', {
|
|
141
|
+
lock_key: lock_key,
|
|
142
|
+
rw_mode: read_write_mode,
|
|
143
|
+
all_read_locks: all_read_locks,
|
|
144
|
+
ext_ttl: milliseconds,
|
|
145
|
+
extended_locks_count: extended_locks_count,
|
|
146
|
+
ext_time: ext_time,
|
|
147
|
+
at: time_at,
|
|
148
|
+
instrument: instrument
|
|
149
|
+
})
|
|
150
|
+
end if instr_sampled
|
|
151
|
+
|
|
152
|
+
result
|
|
153
|
+
end
|
|
154
|
+
# rubocop:enable Metrics/MethodLength
|
|
155
|
+
|
|
156
|
+
private
|
|
84
157
|
|
|
158
|
+
# Extends the write lock, the read lock of the acquirer or all read locks of the lock.
|
|
159
|
+
#
|
|
160
|
+
# @param redis_client [RedisClient]
|
|
161
|
+
# @param lock_name [String]
|
|
162
|
+
# @param lock_key [String]
|
|
163
|
+
# @param milliseconds [Integer]
|
|
164
|
+
# @param read_write_mode [Symbol]
|
|
165
|
+
# @param all_read_locks [Boolean]
|
|
166
|
+
# @param acquirer_id [String]
|
|
167
|
+
# @param lock_index_enabled [Boolean]
|
|
168
|
+
# @return [Hash<Symbol,Boolean|Symbol|Hash<Symbol,Integer>>]
|
|
169
|
+
#
|
|
170
|
+
# @api private
|
|
171
|
+
# @since 1.21.0
|
|
172
|
+
# rubocop:disable Metrics/MethodLength
|
|
173
|
+
def fully_extend_lock_ttl(
|
|
174
|
+
redis_client,
|
|
175
|
+
lock_name,
|
|
176
|
+
lock_key,
|
|
177
|
+
milliseconds,
|
|
178
|
+
read_write_mode,
|
|
179
|
+
all_read_locks,
|
|
180
|
+
acquirer_id,
|
|
181
|
+
lock_index_enabled
|
|
182
|
+
)
|
|
183
|
+
if read_write_mode == :read
|
|
85
184
|
# NOTE: (RW) all live read locks of the lock
|
|
86
185
|
if all_read_locks
|
|
87
|
-
return extend_all_read_locks_ttl(
|
|
186
|
+
return extend_all_read_locks_ttl(
|
|
187
|
+
redis_client, lock_name, lock_key, milliseconds, lock_index_enabled
|
|
188
|
+
)
|
|
88
189
|
end
|
|
89
190
|
|
|
90
191
|
# NOTE: (RW) read lock of the concrete acquirer
|
|
91
|
-
return extend_read_lock_ttl(
|
|
192
|
+
return extend_read_lock_ttl(
|
|
193
|
+
redis_client, lock_name, lock_key, acquirer_id, milliseconds, lock_index_enabled
|
|
194
|
+
)
|
|
92
195
|
end
|
|
93
196
|
|
|
94
|
-
# NOTE:
|
|
95
|
-
|
|
197
|
+
# NOTE:
|
|
198
|
+
# - EVAL signature -> <lua script>, (number of keys), *(keys), *(arguments);
|
|
199
|
+
# - (lock index) the enabled lock index: the lock index score is increased by the extension
|
|
200
|
+
# in the same transaction (`ZADD XX INCR`: the upper bound of the lock lifetime stays
|
|
201
|
+
# the upper bound, locks without the index entry (of older RQL versions) are not indexed
|
|
202
|
+
# with a wrong score); a failed extension (no lock) increases the score of the lock name
|
|
203
|
+
# too: the name just stays in the index a bit longer;
|
|
204
|
+
# - the disabled lock index: one EVAL command (no transaction);
|
|
205
|
+
# @type var extended: Integer
|
|
206
|
+
extended =
|
|
207
|
+
if lock_index_enabled
|
|
208
|
+
# @type var result: [Integer, Float?]
|
|
209
|
+
result = redis_client.multi do |transact|
|
|
210
|
+
transact.call('EVAL', EXTEND_LOCK_PTTL, 1, lock_key, milliseconds)
|
|
211
|
+
transact.call(
|
|
212
|
+
'ZADD', RedisQueuedLocks::Resource::LOCK_INDEX_KEY,
|
|
213
|
+
'XX', 'INCR', milliseconds, lock_name
|
|
214
|
+
)
|
|
215
|
+
end
|
|
216
|
+
result[0]
|
|
217
|
+
else
|
|
218
|
+
redis_client.call('EVAL', EXTEND_LOCK_PTTL, 1, lock_key, milliseconds)
|
|
219
|
+
end
|
|
96
220
|
# TODO: upload scripts to the redis
|
|
97
221
|
|
|
98
|
-
|
|
99
|
-
if result == 1
|
|
222
|
+
if extended == 1
|
|
100
223
|
{ ok: true, result: { extended_locks_count: 1 } }
|
|
101
224
|
else
|
|
102
225
|
{ ok: false, result: :async_expire_or_no_lock }
|
|
@@ -104,8 +227,6 @@ module RedisQueuedLocks::Acquirer::ExtendLockTTL
|
|
|
104
227
|
end
|
|
105
228
|
# rubocop:enable Metrics/MethodLength
|
|
106
229
|
|
|
107
|
-
private
|
|
108
|
-
|
|
109
230
|
# Extends the live read lock of the acquirer:
|
|
110
231
|
# - the read lock expiration (readers registry score), the registry TTL and the read lock
|
|
111
232
|
# data TTL are extended;
|
|
@@ -117,12 +238,21 @@ module RedisQueuedLocks::Acquirer::ExtendLockTTL
|
|
|
117
238
|
# @param lock_key [String]
|
|
118
239
|
# @param acquirer_id [String]
|
|
119
240
|
# @param milliseconds [Integer]
|
|
241
|
+
# @param lock_index_enabled [Boolean]
|
|
120
242
|
# @return [Hash<Symbol,Boolean|Symbol|Hash<Symbol,Integer>>]
|
|
121
243
|
#
|
|
122
244
|
# @api private
|
|
123
245
|
# @since 1.18.0
|
|
246
|
+
# @version 1.21.0
|
|
124
247
|
# rubocop:disable Metrics/MethodLength
|
|
125
|
-
def extend_read_lock_ttl(
|
|
248
|
+
def extend_read_lock_ttl(
|
|
249
|
+
redis_client,
|
|
250
|
+
lock_name,
|
|
251
|
+
lock_key,
|
|
252
|
+
acquirer_id,
|
|
253
|
+
milliseconds,
|
|
254
|
+
lock_index_enabled
|
|
255
|
+
)
|
|
126
256
|
lock_readers_key = RedisQueuedLocks::Resource.prepare_lock_readers(lock_name)
|
|
127
257
|
read_lock_key = RedisQueuedLocks::Resource.prepare_read_lock_key(lock_name, acquirer_id)
|
|
128
258
|
|
|
@@ -145,8 +275,24 @@ module RedisQueuedLocks::Acquirer::ExtendLockTTL
|
|
|
145
275
|
transact.call('PEXPIRE', lock_readers_key, extended_ttl, 'GT')
|
|
146
276
|
transact.call('PEXPIRE', read_lock_key, extended_ttl, 'NX')
|
|
147
277
|
transact.call('PEXPIRE', read_lock_key, extended_ttl, 'GT')
|
|
278
|
+
# NOTE: (lock index) the lock index score is increased by the extension
|
|
279
|
+
# (the enabled lock index only; the last command: its reply is not read);
|
|
280
|
+
if lock_index_enabled
|
|
281
|
+
transact.call(
|
|
282
|
+
'ZADD', RedisQueuedLocks::Resource::LOCK_INDEX_KEY,
|
|
283
|
+
'XX', 'INCR', milliseconds, lock_name
|
|
284
|
+
)
|
|
285
|
+
end
|
|
148
286
|
end
|
|
149
287
|
end
|
|
288
|
+
rescue RedisQueuedLocks::LockAcquirementIntermediateTimeoutError,
|
|
289
|
+
RedisQueuedLocks::TimedLockIntermediateTimeoutError,
|
|
290
|
+
::Timeout::ExitException
|
|
291
|
+
# NOTE: (timeouts) an interrupted WATCH block (the internal timeouts and the user's
|
|
292
|
+
# `Timeout.timeout` without an exception class): the closed connection drops the WATCH
|
|
293
|
+
# state (redis-client sends `UNWATCH` only on `StandardError`);
|
|
294
|
+
rconn.close
|
|
295
|
+
raise
|
|
150
296
|
end
|
|
151
297
|
|
|
152
298
|
# NOTE:
|
|
@@ -174,12 +320,20 @@ module RedisQueuedLocks::Acquirer::ExtendLockTTL
|
|
|
174
320
|
# @param lock_name [String]
|
|
175
321
|
# @param lock_key [String]
|
|
176
322
|
# @param milliseconds [Integer]
|
|
323
|
+
# @param lock_index_enabled [Boolean]
|
|
177
324
|
# @return [Hash<Symbol,Boolean|Symbol|Hash<Symbol,Integer>>]
|
|
178
325
|
#
|
|
179
326
|
# @api private
|
|
180
327
|
# @since 1.18.0
|
|
328
|
+
# @version 1.21.0
|
|
181
329
|
# rubocop:disable Metrics/MethodLength
|
|
182
|
-
def extend_all_read_locks_ttl(
|
|
330
|
+
def extend_all_read_locks_ttl(
|
|
331
|
+
redis_client,
|
|
332
|
+
lock_name,
|
|
333
|
+
lock_key,
|
|
334
|
+
milliseconds,
|
|
335
|
+
lock_index_enabled
|
|
336
|
+
)
|
|
183
337
|
lock_readers_key = RedisQueuedLocks::Resource.prepare_lock_readers(lock_name)
|
|
184
338
|
extended_read_locks_count = 0
|
|
185
339
|
|
|
@@ -214,9 +368,26 @@ module RedisQueuedLocks::Acquirer::ExtendLockTTL
|
|
|
214
368
|
if extended_read_locks_count > 0
|
|
215
369
|
transact.call('PEXPIRE', lock_readers_key, registry_ttl, 'NX')
|
|
216
370
|
transact.call('PEXPIRE', lock_readers_key, registry_ttl, 'GT')
|
|
371
|
+
# NOTE: (lock index) the lock index score is increased by the extension
|
|
372
|
+
# (each read lock is extended by the same milliseconds; the enabled lock index
|
|
373
|
+
# only; the last command: its reply is not read);
|
|
374
|
+
if lock_index_enabled
|
|
375
|
+
transact.call(
|
|
376
|
+
'ZADD', RedisQueuedLocks::Resource::LOCK_INDEX_KEY,
|
|
377
|
+
'XX', 'INCR', milliseconds, lock_name
|
|
378
|
+
)
|
|
379
|
+
end
|
|
217
380
|
end
|
|
218
381
|
end
|
|
219
382
|
end
|
|
383
|
+
rescue RedisQueuedLocks::LockAcquirementIntermediateTimeoutError,
|
|
384
|
+
RedisQueuedLocks::TimedLockIntermediateTimeoutError,
|
|
385
|
+
::Timeout::ExitException
|
|
386
|
+
# NOTE: (timeouts) an interrupted WATCH block (the internal timeouts and the user's
|
|
387
|
+
# `Timeout.timeout` without an exception class): the closed connection drops the WATCH
|
|
388
|
+
# state (redis-client sends `UNWATCH` only on `StandardError`);
|
|
389
|
+
rconn.close
|
|
390
|
+
raise
|
|
220
391
|
end
|
|
221
392
|
|
|
222
393
|
# NOTE:
|
|
@@ -2,25 +2,169 @@
|
|
|
2
2
|
|
|
3
3
|
# @api private
|
|
4
4
|
# @since 1.0.0
|
|
5
|
+
# @version 1.20.0
|
|
5
6
|
module RedisQueuedLocks::Acquirer::Keys
|
|
7
|
+
# The number of replies of one lock name in the key state pipeline of a batch of lock names
|
|
8
|
+
# (`collect_lock_keys`): EXISTS (write lock), EXISTS (write lock queue), EXISTS (read lock
|
|
9
|
+
# queue), ZRANGE (readers registry).
|
|
10
|
+
#
|
|
11
|
+
# @return [Integer]
|
|
12
|
+
#
|
|
13
|
+
# @api private
|
|
14
|
+
# @since 1.20.0
|
|
15
|
+
KEY_STATE_REPLIES = 4
|
|
16
|
+
|
|
6
17
|
class << self
|
|
18
|
+
# RQL keys of the locks:
|
|
19
|
+
# - the disabled lock index: all the RQL keys (`RedisQueuedLocks::Resource::KEY_PATTERN`,
|
|
20
|
+
# `rql:lock*`) found in one SCAN pass over the keyspace;
|
|
21
|
+
# - the enabled lock index: all the existing keys of the locks of the lock index (write locks,
|
|
22
|
+
# lock queues, readers registries, read lock queues, read lock data): one ZSCAN pass over
|
|
23
|
+
# the lock names (`RedisQueuedLocks::Resource::LOCK_INDEX_KEY`, the keyspace is never
|
|
24
|
+
# scanned), lock keys are derived from the lock names;
|
|
25
|
+
#
|
|
7
26
|
# @param redis_client [RedisClient]
|
|
8
27
|
# @option scan_size [Integer]
|
|
28
|
+
# SCAN count (the disabled lock index) or ZSCAN count and the number of lock names in one
|
|
29
|
+
# batch (the enabled lock index).
|
|
30
|
+
# @option lock_index_enabled [Boolean]
|
|
9
31
|
# @return [Set<String>]
|
|
10
32
|
#
|
|
11
33
|
# @api private
|
|
12
34
|
# @since 1.0.0
|
|
13
|
-
|
|
35
|
+
# @version 1.21.0
|
|
36
|
+
def keys(redis_client, scan_size:, lock_index_enabled:)
|
|
37
|
+
# NOTE: (arguments) the same range as the config key: zero/negative sizes fail SCAN/ZSCAN
|
|
38
|
+
# (`COUNT 0`) and the batching (`each_slice(0)`);
|
|
39
|
+
if !scan_size.is_a?(::Integer) || scan_size <= 0
|
|
40
|
+
raise(
|
|
41
|
+
RedisQueuedLocks::ArgumentError,
|
|
42
|
+
"`:scan_size` argument should be a positive Integer, got #{scan_size.inspect}."
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
14
46
|
Set.new.tap do |keyset|
|
|
15
47
|
# @type var keyset: Set[String]
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
48
|
+
if lock_index_enabled
|
|
49
|
+
redis_client.with do |rconn|
|
|
50
|
+
each_lock_name_batch(rconn, scan_size) do |lock_names|
|
|
51
|
+
collect_lock_keys(rconn, lock_names, keyset)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
else
|
|
55
|
+
redis_client.scan(
|
|
56
|
+
'MATCH',
|
|
57
|
+
RedisQueuedLocks::Resource::KEY_PATTERN,
|
|
58
|
+
count: scan_size
|
|
59
|
+
) do |key|
|
|
60
|
+
keyset.add(key)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
# (lock index) Adds the existing keys of the given batch of lock names to the key set
|
|
69
|
+
# (one round trip for the lock keys, one more for the read lock data when read locks exist).
|
|
70
|
+
#
|
|
71
|
+
# @param redis_client [RedisClient]
|
|
72
|
+
# @param lock_names [Array<String>]
|
|
73
|
+
# @param keyset [Set<String>]
|
|
74
|
+
# @return [void]
|
|
75
|
+
#
|
|
76
|
+
# @api private
|
|
77
|
+
# @since 1.20.0
|
|
78
|
+
# rubocop:disable Metrics/MethodLength
|
|
79
|
+
def collect_lock_keys(redis_client, lock_names, keyset)
|
|
80
|
+
# Step 1: the state of the lock keys of the batch
|
|
81
|
+
# NOTE: (pipeline layout) `KEY_STATE_REPLIES` replies per lock name in the batch order
|
|
82
|
+
# (the replies of the lock name with the index N start at `N * KEY_STATE_REPLIES`):
|
|
83
|
+
# EXISTS (write lock), EXISTS (write lock queue), EXISTS (read lock queue),
|
|
84
|
+
# ZRANGE (readers registry: redis deletes empty sorted sets, so an empty result means
|
|
85
|
+
# the registry does not exist);
|
|
86
|
+
# @type var key_state: Array[untyped]
|
|
87
|
+
key_state = redis_client.pipelined do |pipeline|
|
|
88
|
+
lock_names.each do |lock_name|
|
|
89
|
+
pipeline.call('EXISTS', RedisQueuedLocks::Resource.prepare_lock_key(lock_name))
|
|
90
|
+
pipeline.call('EXISTS', RedisQueuedLocks::Resource.prepare_lock_queue(lock_name))
|
|
91
|
+
pipeline.call('EXISTS', RedisQueuedLocks::Resource.prepare_read_lock_queue(lock_name))
|
|
92
|
+
pipeline.call(
|
|
93
|
+
'ZRANGE', RedisQueuedLocks::Resource.prepare_lock_readers(lock_name), '0', '-1'
|
|
94
|
+
)
|
|
22
95
|
end
|
|
23
96
|
end
|
|
97
|
+
|
|
98
|
+
# @type var read_lock_keys: Array[String]
|
|
99
|
+
read_lock_keys = []
|
|
100
|
+
lock_names.each_with_index do |lock_name, index|
|
|
101
|
+
replies_at = index * KEY_STATE_REPLIES
|
|
102
|
+
if key_state[replies_at] == 1
|
|
103
|
+
keyset.add(RedisQueuedLocks::Resource.prepare_lock_key(lock_name))
|
|
104
|
+
end
|
|
105
|
+
if key_state[replies_at + 1] == 1
|
|
106
|
+
keyset.add(RedisQueuedLocks::Resource.prepare_lock_queue(lock_name))
|
|
107
|
+
end
|
|
108
|
+
if key_state[replies_at + 2] == 1
|
|
109
|
+
keyset.add(RedisQueuedLocks::Resource.prepare_read_lock_queue(lock_name))
|
|
110
|
+
end
|
|
111
|
+
# @type var read_lock_acquirers: Array[String]
|
|
112
|
+
read_lock_acquirers = key_state[replies_at + 3]
|
|
113
|
+
next if read_lock_acquirers.empty?
|
|
114
|
+
|
|
115
|
+
keyset.add(RedisQueuedLocks::Resource.prepare_lock_readers(lock_name))
|
|
116
|
+
read_lock_acquirers.each do |acquirer_id|
|
|
117
|
+
read_lock_keys << RedisQueuedLocks::Resource.prepare_read_lock_key(lock_name, acquirer_id)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
return if read_lock_keys.empty?
|
|
121
|
+
|
|
122
|
+
# Step 2: (RW) read lock data of the readers registries (it can be absent: the data of an
|
|
123
|
+
# expired read lock is expired before the registry cleanup)
|
|
124
|
+
# NOTE: (pipeline layout) one EXISTS reply per read lock data key
|
|
125
|
+
# @type var read_lock_keys_existence: Array[Integer]
|
|
126
|
+
read_lock_keys_existence = redis_client.pipelined do |pipeline|
|
|
127
|
+
read_lock_keys.each { |read_lock_key| pipeline.call('EXISTS', read_lock_key) }
|
|
128
|
+
end
|
|
129
|
+
read_lock_keys.each_with_index do |read_lock_key, index|
|
|
130
|
+
keyset.add(read_lock_key) if read_lock_keys_existence[index] == 1
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
# rubocop:enable Metrics/MethodLength
|
|
134
|
+
|
|
135
|
+
# Iterates over the lock names of the lock index (one ZSCAN pass) in batches of `scan_size`
|
|
136
|
+
# lock names (batches are collected across ZSCAN pages). Expired lock names (not dropped from
|
|
137
|
+
# the index yet) are skipped: all their keys are expired.
|
|
138
|
+
#
|
|
139
|
+
# NOTE: each operation module iterates over the lock index by itself (duplicated on purpose:
|
|
140
|
+
# operation modules are independent of each other, see `Locks.each_lock_name_batch`);
|
|
141
|
+
#
|
|
142
|
+
# @param redis_client [RedisClient]
|
|
143
|
+
# @param scan_size [Integer] ZSCAN count and the number of lock names in one batch.
|
|
144
|
+
# @yield [lock_names]
|
|
145
|
+
# @yieldparam lock_names [Array<String>] The batch of lock names (reused between batches).
|
|
146
|
+
# @return [void]
|
|
147
|
+
#
|
|
148
|
+
# @api private
|
|
149
|
+
# @since 1.20.0
|
|
150
|
+
def each_lock_name_batch(redis_client, scan_size)
|
|
151
|
+
lock_index_time = RedisQueuedLocks::Resource.lock_index_time
|
|
152
|
+
# @type var lock_names: Array[String]
|
|
153
|
+
lock_names = []
|
|
154
|
+
|
|
155
|
+
redis_client.zscan(
|
|
156
|
+
RedisQueuedLocks::Resource::LOCK_INDEX_KEY, count: scan_size
|
|
157
|
+
) do |lock_name, lock_index_score|
|
|
158
|
+
next if lock_index_score.to_f < lock_index_time
|
|
159
|
+
|
|
160
|
+
lock_names << lock_name
|
|
161
|
+
next if lock_names.size < scan_size
|
|
162
|
+
|
|
163
|
+
yield(lock_names)
|
|
164
|
+
lock_names.clear
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
yield(lock_names) unless lock_names.empty?
|
|
24
168
|
end
|
|
25
169
|
end
|
|
26
170
|
end
|
|
@@ -56,46 +56,38 @@ module RedisQueuedLocks::Acquirer::LockInfo
|
|
|
56
56
|
pipeline.call('ZRANGE', lock_readers_key, '0', '-1', 'WITHSCORES')
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
# NOTE: the result of MULTI-command is an array of results of each internal command
|
|
68
|
-
# - result[0] (HGETALL) (Hash<String,String>)
|
|
69
|
-
# => (will be mutated further with different value types)
|
|
70
|
-
# - result[1] (PTTL) (Integer)
|
|
71
|
-
# => (without any mutation, integer is atomic)
|
|
72
|
-
# - result[2] (TIME) (Array<String>) (redis time)
|
|
73
|
-
# - result[3] (ZRANGE) (Array<[String,Float]>) (read locks)
|
|
59
|
+
# NOTE: (pipeline layout) the replies of the pipeline commands (`pipelined` always returns
|
|
60
|
+
# an array of the replies):
|
|
61
|
+
# - result[0] (HGETALL) (Hash<String,String>)
|
|
62
|
+
# => (will be mutated further with different value types)
|
|
63
|
+
# - result[1] (PTTL) (Integer)
|
|
64
|
+
# => (without any mutation, integer is atomic)
|
|
65
|
+
# - result[2] (TIME) (Array<String>) (redis time)
|
|
66
|
+
# - result[3] (ZRANGE) (Array<[String,Float]>) (read locks)
|
|
74
67
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
68
|
+
# rubocop:disable Layout/LineLength
|
|
69
|
+
# @type var result: [Hash[String,String|Float|Integer], Integer, Array[String], Array[[String, Float]]]
|
|
70
|
+
# rubocop:enable Layout/LineLength
|
|
71
|
+
hget_cmd_res = result[0]
|
|
72
|
+
pttl_cmd_res = result[1]
|
|
80
73
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
end
|
|
74
|
+
if hget_cmd_res == {} || pttl_cmd_res == -2 # NOTE: key does not exist
|
|
75
|
+
# NOTE: (RW) there is no write lock => check read locks
|
|
76
|
+
read_lock_info(redis_client, lock_name, lock_key, result[2], result[3])
|
|
77
|
+
else
|
|
78
|
+
hget_cmd_res.tap do |lock_data|
|
|
79
|
+
lock_data['lock_key'] = lock_key
|
|
80
|
+
lock_data['rw_mode'] = 'write'
|
|
81
|
+
lock_data['ts'] = Float(lock_data['ts'])
|
|
82
|
+
lock_data['ini_ttl'] = Integer(lock_data['ini_ttl'])
|
|
83
|
+
lock_data['rem_ttl'] = ((pttl_cmd_res == -1) ? Float::INFINITY : pttl_cmd_res)
|
|
84
|
+
lock_data['spc_cnt'] = Integer(lock_data['spc_cnt']) if lock_data['spc_cnt']
|
|
85
|
+
lock_data['l_spc_ts'] = Float(lock_data['l_spc_ts']) if lock_data['l_spc_ts']
|
|
86
|
+
lock_data['spc_ext_ttl'] = Integer(lock_data['spc_ext_ttl']) if lock_data['spc_ext_ttl']
|
|
87
|
+
lock_data['l_spc_ext_ini_ttl'] =
|
|
88
|
+
Integer(lock_data['l_spc_ext_ini_ttl']) if lock_data.key?('l_spc_ext_ini_ttl')
|
|
89
|
+
lock_data['l_spc_ext_ts'] =
|
|
90
|
+
Float(lock_data['l_spc_ext_ts']) if lock_data['l_spc_ext_ts']
|
|
99
91
|
end
|
|
100
92
|
end
|
|
101
93
|
end
|
|
@@ -117,6 +109,7 @@ module RedisQueuedLocks::Acquirer::LockInfo
|
|
|
117
109
|
#
|
|
118
110
|
# @api private
|
|
119
111
|
# @since 1.18.0
|
|
112
|
+
# @version 1.20.0
|
|
120
113
|
# rubocop:disable Metrics/MethodLength
|
|
121
114
|
def read_lock_info(redis_client, lock_name, lock_key, redis_time, read_locks)
|
|
122
115
|
now = RedisQueuedLocks::Resource.redis_time_ms(redis_time)
|
|
@@ -139,10 +132,15 @@ module RedisQueuedLocks::Acquirer::LockInfo
|
|
|
139
132
|
rem_ttl = (expiration - now).ceil
|
|
140
133
|
longest_rem_ttl = rem_ttl if rem_ttl > longest_rem_ttl
|
|
141
134
|
|
|
135
|
+
# NOTE: acquirer and host ids are not stored in the read lock data (both are derived from
|
|
136
|
+
# the acquirer id): they are the first fields like in the write lock info (read lock data
|
|
137
|
+
# of older versions contains them with the same values);
|
|
142
138
|
# @type var reader: Hash[String,String|Float|Integer|nil]
|
|
143
|
-
reader =
|
|
144
|
-
|
|
145
|
-
|
|
139
|
+
reader = {
|
|
140
|
+
'acq_id' => acquirer_id,
|
|
141
|
+
'hst_id' => RedisQueuedLocks::Resource.host_identifier_from_acquirer(acquirer_id)
|
|
142
|
+
}
|
|
143
|
+
read_locks_data[index]&.each { |field, value| reader[field] = value }
|
|
146
144
|
reader['ts'] = Float(reader['ts']) if reader['ts']
|
|
147
145
|
reader['ini_ttl'] = Integer(reader['ini_ttl']) if reader['ini_ttl']
|
|
148
146
|
reader['rem_ttl'] = rem_ttl
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
# NOTE: Lock Series PoC
|
|
4
4
|
# @api private
|
|
5
5
|
# @since 1.16.1
|
|
6
|
+
# @version 1.18.0
|
|
6
7
|
module RedisQueuedLocks::Acquirer::LockSeriesPoC::InstrVisitor # steep:ignore
|
|
7
8
|
class << self
|
|
8
9
|
# NOTE: Lock Series PoC
|
|
@@ -32,7 +33,7 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC::InstrVisitor # steep:ignore
|
|
|
32
33
|
# @param rw_mode [Symbol] The mode of the lock series (`:read` or `:write`).
|
|
33
34
|
# @api private
|
|
34
35
|
# @since 1.16.1
|
|
35
|
-
# @version 1.
|
|
36
|
+
# @version 1.21.0
|
|
36
37
|
def lock_series_hold_and_release( # steep:ignore
|
|
37
38
|
instrumenter,
|
|
38
39
|
instr_sampled,
|
|
@@ -49,7 +50,7 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC::InstrVisitor # steep:ignore
|
|
|
49
50
|
return unless instr_sampled
|
|
50
51
|
instrumenter.notify('redis_queued_locks.lock_series_hold_and_release', {
|
|
51
52
|
lock_keys:, rw_mode:, hold_time:, ttl:, acq_id:, hst_id:, ts:, acq_time:, instrument:
|
|
52
|
-
})
|
|
53
|
+
}) rescue nil
|
|
53
54
|
end
|
|
54
55
|
end
|
|
55
56
|
end
|
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
# NOTE: Lock Series PoC
|
|
4
4
|
# @api private
|
|
5
5
|
# @since 1.16.1
|
|
6
|
+
# @version 1.18.0
|
|
6
7
|
module RedisQueuedLocks::Acquirer::LockSeriesPoC::LogVisitor # steep:ignore
|
|
7
8
|
class << self
|
|
8
9
|
# NOTE: Lock Series PoC
|
|
9
10
|
# @param rw_mode [Symbol] The mode of the lock series (`:read` or `:write`).
|
|
10
11
|
# @api private
|
|
11
12
|
# @since 1.16.0
|
|
12
|
-
# @version 1.
|
|
13
|
+
# @version 1.21.0
|
|
13
14
|
def start_lock_series_obtaining( # steep:ignore
|
|
14
15
|
logger,
|
|
15
16
|
log_sampled,
|
|
@@ -30,14 +31,14 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC::LogVisitor # steep:ignore
|
|
|
30
31
|
"hst_id => '#{host_id}' " \
|
|
31
32
|
"acs_strat => '#{access_strategy}' " \
|
|
32
33
|
"rw_mode => '#{rw_mode}'"
|
|
33
|
-
end
|
|
34
|
+
end rescue nil
|
|
34
35
|
end
|
|
35
36
|
|
|
36
37
|
# NOTE: Lock Series PoC
|
|
37
38
|
# @param rw_mode [Symbol] The mode of the lock series (`:read` or `:write`).
|
|
38
39
|
# @api private
|
|
39
40
|
# @since 1.16.0
|
|
40
|
-
# @version 1.
|
|
41
|
+
# @version 1.21.0
|
|
41
42
|
def lock_series_obtained( # steep:ignore
|
|
42
43
|
logger,
|
|
43
44
|
log_sampled,
|
|
@@ -60,7 +61,7 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC::LogVisitor # steep:ignore
|
|
|
60
61
|
"acs_strat => '#{access_strategy}' " \
|
|
61
62
|
"rw_mode => '#{rw_mode}' " \
|
|
62
63
|
"acq_time => #{acq_time} (ms)"
|
|
63
|
-
end
|
|
64
|
+
end rescue nil
|
|
64
65
|
end
|
|
65
66
|
|
|
66
67
|
# NOTE: Lock Series PoC
|