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
|
@@ -6,7 +6,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::WithAcqTimeout
|
|
|
6
6
|
# @param redis [RedisClient]
|
|
7
7
|
# Redis connection manager required for additional data extraction for error message.
|
|
8
8
|
# @param timeout [NilClass,Integer]
|
|
9
|
-
# Time period after which the logic will fail with timeout error.
|
|
9
|
+
# Time period after which the logic will fail with timeout error (`nil` and `0`: no timeout).
|
|
10
10
|
# @param lock_key [String]
|
|
11
11
|
# Lock name in RQL notation (rql:lock:some-lock-name).
|
|
12
12
|
# @param lock_name [String]
|
|
@@ -26,7 +26,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::WithAcqTimeout
|
|
|
26
26
|
#
|
|
27
27
|
# @api private
|
|
28
28
|
# @since 1.0.0
|
|
29
|
-
# @version 1.
|
|
29
|
+
# @version 1.21.0
|
|
30
30
|
def with_acq_timeout(
|
|
31
31
|
redis,
|
|
32
32
|
timeout,
|
|
@@ -19,7 +19,9 @@ module RedisQueuedLocks::Acquirer::AcquireLock::YieldExpire
|
|
|
19
19
|
# @param logger [::Logger,#debug] Logger object.
|
|
20
20
|
# @param lock_key [String] Obtained lock key that should be expired.
|
|
21
21
|
# @param lock_readers_key [String] Registry of read lock holders.
|
|
22
|
-
# @param read_lock_key [String]
|
|
22
|
+
# @param read_lock_key [String,NilClass]
|
|
23
|
+
# Read lock data of the current acquirer (`nil` for write lock requests: they never obtain
|
|
24
|
+
# read locks).
|
|
23
25
|
# @param rw_mode [Symbol]
|
|
24
26
|
# - The mode of the obtained lock (`:read` or `:write`);
|
|
25
27
|
# - `:read` => only the read lock of the current acquirer is released/decreased;
|
|
@@ -28,7 +30,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::YieldExpire
|
|
|
28
30
|
# @param access_strategy [Symbol] Lock obtaining strategy.
|
|
29
31
|
# @param timed [Boolean] Should the lock be wrapped by Timeout with with lock's ttl
|
|
30
32
|
# @param ttl_shift [Numeric] Lock's TTL shifting. Should affect block's ttl. In millisecodns.
|
|
31
|
-
# @param ttl [Integer
|
|
33
|
+
# @param ttl [Integer] Lock's time to live (in ms). A positive Integer.
|
|
32
34
|
# @param queue_ttl [Integer] Lock request lifetime.
|
|
33
35
|
# @param meta [NilClass,Hash<String|Symbol,Any>] Custom metadata wich is passed to the lock data;
|
|
34
36
|
# @param log_sampled [Boolean] Should the logic be logged or not.
|
|
@@ -43,7 +45,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::YieldExpire
|
|
|
43
45
|
#
|
|
44
46
|
# @api private
|
|
45
47
|
# @since 1.3.0
|
|
46
|
-
# @version 1.
|
|
48
|
+
# @version 1.21.0
|
|
47
49
|
# rubocop:disable Metrics/MethodLength
|
|
48
50
|
def yield_expire(
|
|
49
51
|
redis,
|
|
@@ -69,7 +71,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::YieldExpire
|
|
|
69
71
|
initial_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, :millisecond)
|
|
70
72
|
|
|
71
73
|
if block_given?
|
|
72
|
-
if timed
|
|
74
|
+
if timed
|
|
73
75
|
# NOTE:
|
|
74
76
|
# - steep is ignored cuz steep can not recognize `::Integer - ::Integer|::Float`
|
|
75
77
|
# operation here for some mystical reason (it tryes to find overloadd `-` method for
|
|
@@ -148,7 +150,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::YieldExpire
|
|
|
148
150
|
|
|
149
151
|
# @param timeout [Float]
|
|
150
152
|
# @param lock_key [String]
|
|
151
|
-
# @param lock_ttl [Integer
|
|
153
|
+
# @param lock_ttl [Integer]
|
|
152
154
|
# @param acquirer_id [String]
|
|
153
155
|
# @param host_id [String]
|
|
154
156
|
# @param meta [NilClass,Hash<Symbol|String,Any>]
|
|
@@ -159,7 +161,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::YieldExpire
|
|
|
159
161
|
#
|
|
160
162
|
# @api private
|
|
161
163
|
# @since 1.3.0
|
|
162
|
-
# @version 1.
|
|
164
|
+
# @version 1.21.0
|
|
163
165
|
def yield_with_timeout(timeout, lock_key, lock_ttl, acquirer_id, host_id, meta, &block)
|
|
164
166
|
::Timeout.timeout(timeout, RedisQueuedLocks::TimedLockIntermediateTimeoutError, &block)
|
|
165
167
|
rescue RedisQueuedLocks::TimedLockIntermediateTimeoutError
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# @api private
|
|
4
4
|
# @since 1.0.0
|
|
5
|
-
# @version 1.
|
|
5
|
+
# @version 1.20.0
|
|
6
6
|
# rubocop:disable Metrics/ModuleLength
|
|
7
7
|
# rubocop:disable Metrics/MethodLength
|
|
8
8
|
# rubocop:disable Metrics/ClassLength
|
|
@@ -42,12 +42,23 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
42
42
|
# A current fiber that want to acquire a lock.
|
|
43
43
|
# @option ractor_id [Integer,String]
|
|
44
44
|
# The current ractor that want to acquire a lock.
|
|
45
|
-
# @option ttl [Integer
|
|
46
|
-
# Lock's time to live (in milliseconds).
|
|
45
|
+
# @option ttl [Integer]
|
|
46
|
+
# Lock's time to live (in milliseconds). A positive Integer (both modes).
|
|
47
47
|
# @option queue_ttl [Integer]
|
|
48
48
|
# Lifetime of the acuier's lock request. In seconds.
|
|
49
|
+
# @option lock_index_enabled [Boolean]
|
|
50
|
+
# - Maintain the lock index (`rql:index:locks`): the lock request and the obtained lock are
|
|
51
|
+
# indexed, the lock request queue gets TTL, expired lock names are dropped from the index;
|
|
52
|
+
# - `config['lock_index_enabled']` (the client-wide mode, not a per-call option);
|
|
53
|
+
# @option lock_index_grace_period [Integer]
|
|
54
|
+
# - The lock index score margin over the lock key lifetime (in milliseconds): the lock name
|
|
55
|
+
# stays in the lock index for this period after the expiration of the lock keys;
|
|
56
|
+
# - tolerance of the client clock skew (between hosts) and of the network latency;
|
|
57
|
+
# - pre-configured in `config['lock_index_grace_period']`;
|
|
58
|
+
# - used only with the enabled lock index (`lock_index_enabled`);
|
|
49
59
|
# @option timeout [Integer,NilClass]
|
|
50
|
-
# Time period whe should try to acquire the lock (in seconds).
|
|
60
|
+
# Time period whe should try to acquire the lock (in seconds). Nil means "without timeout"
|
|
61
|
+
# (`0` is the same as `nil`: `Timeout.timeout(0)` sets no timeout).
|
|
51
62
|
# @option timed [Boolean]
|
|
52
63
|
# Limit the invocation time period of the passed block of code by the lock's TTL.
|
|
53
64
|
# @option retry_count [Integer,NilClass]
|
|
@@ -121,7 +132,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
121
132
|
# - works in tandem with <config['log_sampling_percent'] and <config['log_sampler']>;
|
|
122
133
|
# @option log_sampling_percent [Integer]
|
|
123
134
|
# - the percent of cases that should be logged;
|
|
124
|
-
# - take an effect when <config['
|
|
135
|
+
# - take an effect when <config['log_sampling_enabled']> is true;
|
|
125
136
|
# - works in tandem with <config['log_sampling_enabled']> and <config['log_sampler']> configs;
|
|
126
137
|
# @option log_sampler [#sampling_happened?,Module<RedisQueuedLocks::Logging::Sampler>]
|
|
127
138
|
# - percent-based log sampler that decides should be RQL case logged or not;
|
|
@@ -142,7 +153,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
142
153
|
# - works in tandem with <config['instr_sampling_percent']> and <config['instr_sampler']>;
|
|
143
154
|
# @option instr_sampling_percent [Integer]
|
|
144
155
|
# - the percent of cases that should be instrumented;
|
|
145
|
-
# - take an effect when <config['
|
|
156
|
+
# - take an effect when <config['instr_sampling_enabled']> is true;
|
|
146
157
|
# - works in tandem with <config['instr_sampling_enabled']>
|
|
147
158
|
# and <config['instr_sampler']> configs;
|
|
148
159
|
# @option instr_sampler [#sampling_happened?,Module<RedisQueuedLocks::Instrument::Sampler>]
|
|
@@ -166,7 +177,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
166
177
|
#
|
|
167
178
|
# @api private
|
|
168
179
|
# @since 1.0.0
|
|
169
|
-
# @version 1.
|
|
180
|
+
# @version 1.21.0
|
|
170
181
|
def acquire_lock(
|
|
171
182
|
redis,
|
|
172
183
|
lock_name,
|
|
@@ -176,6 +187,8 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
176
187
|
ractor_id:,
|
|
177
188
|
ttl:,
|
|
178
189
|
queue_ttl:,
|
|
190
|
+
lock_index_enabled:,
|
|
191
|
+
lock_index_grace_period:,
|
|
179
192
|
timeout:,
|
|
180
193
|
timed:,
|
|
181
194
|
retry_count:,
|
|
@@ -223,29 +236,79 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
223
236
|
)
|
|
224
237
|
end
|
|
225
238
|
|
|
226
|
-
# Step 0.1
|
|
227
|
-
# -
|
|
228
|
-
|
|
239
|
+
# Step 0.1: prevent :ttl incompatibilities (both modes)
|
|
240
|
+
# - write lock: `PEXPIRE` with a non-positive ttl deletes the just obtained lock key in the
|
|
241
|
+
# obtaining transaction (the lock is "obtained" without the mutual exclusion);
|
|
242
|
+
# - read lock: the read lock expiration time is calculated from the lock ttl;
|
|
243
|
+
if !ttl.is_a?(::Integer) || ttl <= 0
|
|
229
244
|
raise(
|
|
230
245
|
RedisQueuedLocks::ArgumentError,
|
|
231
|
-
"`:ttl` argument should be a positive Integer
|
|
246
|
+
"`:ttl` argument should be a positive Integer, got #{ttl.inspect}."
|
|
247
|
+
)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Step 0.1: prevent :queue_ttl, :timeout, :retry_* and :lock_index_grace_period
|
|
251
|
+
# incompatibilities (the same ranges as their config keys):
|
|
252
|
+
# - a non-positive queue ttl drops the own lock request in the queueing pipeline (the lock
|
|
253
|
+
# request is always dead);
|
|
254
|
+
# - a negative timeout/retry delay/retry jitter fails `Timeout.timeout`/`sleep` in the
|
|
255
|
+
# middle of the acquirement (`0` timeout is the same as `nil`: no timeout);
|
|
256
|
+
if !queue_ttl.is_a?(::Integer) || queue_ttl <= 0
|
|
257
|
+
raise(
|
|
258
|
+
RedisQueuedLocks::ArgumentError,
|
|
259
|
+
"`:queue_ttl` argument should be a positive Integer, got #{queue_ttl.inspect}."
|
|
260
|
+
)
|
|
261
|
+
end
|
|
262
|
+
if timeout != nil && (!timeout.is_a?(::Integer) || timeout < 0)
|
|
263
|
+
raise(
|
|
264
|
+
RedisQueuedLocks::ArgumentError,
|
|
265
|
+
"`:timeout` argument should be nil or a non-negative Integer, got #{timeout.inspect}."
|
|
266
|
+
)
|
|
267
|
+
end
|
|
268
|
+
if retry_count != nil && (!retry_count.is_a?(::Integer) || retry_count < 0)
|
|
269
|
+
raise(
|
|
270
|
+
RedisQueuedLocks::ArgumentError,
|
|
271
|
+
"`:retry_count` argument should be nil or a non-negative Integer, " \
|
|
272
|
+
"got #{retry_count.inspect}."
|
|
273
|
+
)
|
|
274
|
+
end
|
|
275
|
+
if !retry_delay.is_a?(::Integer) || retry_delay < 0
|
|
276
|
+
raise(
|
|
277
|
+
RedisQueuedLocks::ArgumentError,
|
|
278
|
+
"`:retry_delay` argument should be a non-negative Integer, got #{retry_delay.inspect}."
|
|
279
|
+
)
|
|
280
|
+
end
|
|
281
|
+
if !retry_jitter.is_a?(::Integer) || retry_jitter < 0
|
|
282
|
+
raise(
|
|
283
|
+
RedisQueuedLocks::ArgumentError,
|
|
284
|
+
"`:retry_jitter` argument should be a non-negative Integer, got #{retry_jitter.inspect}."
|
|
285
|
+
)
|
|
286
|
+
end
|
|
287
|
+
if !lock_index_grace_period.is_a?(::Integer) || lock_index_grace_period < 0
|
|
288
|
+
raise(
|
|
289
|
+
RedisQueuedLocks::ArgumentError,
|
|
290
|
+
"`:lock_index_grace_period` argument should be a non-negative Integer, " \
|
|
291
|
+
"got #{lock_index_grace_period.inspect}."
|
|
232
292
|
)
|
|
233
293
|
end
|
|
234
294
|
|
|
235
295
|
# Step 0.2: prevent :meta incompatabiltiies (structure)
|
|
296
|
+
# - Symbol keys are written to the lock data as Strings (`Symbol#name`, as redis-client
|
|
297
|
+
# does it) and would overwrite the reserved lock data fields too;
|
|
236
298
|
if meta.is_a?(::Hash) && (meta.any? do |key, _value|
|
|
237
|
-
key
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
299
|
+
meta_key = key.is_a?(::Symbol) ? key.name : key
|
|
300
|
+
meta_key == 'acq_id' ||
|
|
301
|
+
meta_key == 'hst_id' ||
|
|
302
|
+
meta_key == 'ts' ||
|
|
303
|
+
meta_key == 'ini_ttl' ||
|
|
304
|
+
meta_key == 'lock_key' ||
|
|
305
|
+
meta_key == 'rem_ttl' ||
|
|
306
|
+
meta_key == 'spc_ext_ttl' ||
|
|
307
|
+
meta_key == 'spc_cnt' ||
|
|
308
|
+
meta_key == 'l_spc_ext_ini_ttl' ||
|
|
309
|
+
meta_key == 'l_spc_ext_ts' ||
|
|
310
|
+
meta_key == 'l_spc_ts' ||
|
|
311
|
+
meta_key == 'rw_mode'
|
|
249
312
|
end)
|
|
250
313
|
raise(
|
|
251
314
|
RedisQueuedLocks::ArgumentError,
|
|
@@ -276,7 +339,12 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
276
339
|
# NOTE: (RW) write lock requests use the classic lock queue (lock_key_queue)
|
|
277
340
|
read_lock_key_queue = RedisQueuedLocks::Resource.prepare_read_lock_queue(lock_name)
|
|
278
341
|
lock_readers_key = RedisQueuedLocks::Resource.prepare_lock_readers(lock_name)
|
|
279
|
-
|
|
342
|
+
# NOTE: (RW) read lock data key is required by read lock requests only (write lock requests
|
|
343
|
+
# never obtain read locks: read-to-write lock upgrade is not supported);
|
|
344
|
+
read_lock_key =
|
|
345
|
+
if read_write_mode == :read
|
|
346
|
+
RedisQueuedLocks::Resource.prepare_read_lock_key(lock_name, acquirer_id)
|
|
347
|
+
end
|
|
280
348
|
|
|
281
349
|
acquirer_position = RedisQueuedLocks::Resource.calc_initial_acquirer_position
|
|
282
350
|
|
|
@@ -329,6 +397,9 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
329
397
|
)
|
|
330
398
|
|
|
331
399
|
# Step 2: try to lock with timeout
|
|
400
|
+
# NOTE: the acquirement start time is defined out of the timeout block: the failed
|
|
401
|
+
# acquirement reports the acquirement time too (Steps 3.b and 3.c);
|
|
402
|
+
acq_start_time = clock_gettime
|
|
332
403
|
with_acq_timeout(
|
|
333
404
|
redis,
|
|
334
405
|
timeout,
|
|
@@ -338,8 +409,6 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
338
409
|
detailed_acq_timeout_error,
|
|
339
410
|
on_timeout: acq_dequeue
|
|
340
411
|
) do
|
|
341
|
-
acq_start_time = clock_gettime
|
|
342
|
-
|
|
343
412
|
# Step 2.1: cyclically try to obtain the lock
|
|
344
413
|
while acq_process[:should_try]
|
|
345
414
|
|
|
@@ -371,15 +440,19 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
371
440
|
read_lock_key_queue,
|
|
372
441
|
lock_readers_key,
|
|
373
442
|
read_lock_key,
|
|
443
|
+
lock_name,
|
|
374
444
|
acquirer_id,
|
|
375
445
|
host_id,
|
|
376
446
|
acquirer_position,
|
|
377
447
|
lock_ttl,
|
|
378
448
|
queue_ttl,
|
|
449
|
+
lock_index_enabled,
|
|
450
|
+
lock_index_grace_period,
|
|
379
451
|
fail_fast,
|
|
380
452
|
conflict_strategy,
|
|
381
453
|
access_strategy,
|
|
382
454
|
meta,
|
|
455
|
+
acq_process[:tries] > 0, # NOTE: retry attempt (see "pre-check" in `try_to_lock`)
|
|
383
456
|
log_sampled,
|
|
384
457
|
instr_sampled
|
|
385
458
|
) => { ok:, result: }
|
|
@@ -629,8 +702,43 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
629
702
|
acq_process[:result] = :timeout_reached
|
|
630
703
|
end
|
|
631
704
|
# Step 3.b: lock is not acquired (acquirer is dequeued by timeout callback)
|
|
705
|
+
acq_time = ((clock_gettime - acq_start_time) / 1_000.0).ceil(2)
|
|
706
|
+
LogVisitor.lock_obtaining_failed(
|
|
707
|
+
logger, log_sampled, lock_key, queue_ttl, acquirer_id, host_id,
|
|
708
|
+
acq_time, access_strategy, read_write_mode, acq_process[:result]
|
|
709
|
+
)
|
|
710
|
+
InstrVisitor.lock_obtaining_failed(
|
|
711
|
+
instrumenter, instr_sampled, lock_key, read_write_mode, lock_ttl, acquirer_id, host_id,
|
|
712
|
+
Time.now.to_f, acq_time, acq_process[:result], instrument
|
|
713
|
+
)
|
|
632
714
|
{ ok: false, result: acq_process[:result] } #: { ok: bool, result: Symbol }
|
|
633
715
|
end
|
|
716
|
+
rescue RedisQueuedLocks::LockAlreadyObtainedError,
|
|
717
|
+
RedisQueuedLocks::ConflictLockObtainError,
|
|
718
|
+
RedisQueuedLocks::LockAcquirementRetryLimitError,
|
|
719
|
+
RedisQueuedLocks::LockAcquirementTimeoutError => error
|
|
720
|
+
# Step 3.c: (raise_errors) the failed acquirement is reported before the error is raised
|
|
721
|
+
# (like Step 3.b); errors of the block of code under the obtained lock are not reported;
|
|
722
|
+
# NOTE: (steep) the locals of the method body are nilable in the method-level `rescue` for
|
|
723
|
+
# Steep, but these errors are raised by the acquirement only (after Step 1: the locals
|
|
724
|
+
# are assigned);
|
|
725
|
+
# steep:ignore:start
|
|
726
|
+
unless acq_process[:acquired]
|
|
727
|
+
if error.is_a?(RedisQueuedLocks::LockAcquirementTimeoutError)
|
|
728
|
+
acq_process[:result] = :timeout_reached
|
|
729
|
+
end
|
|
730
|
+
acq_time = ((clock_gettime - acq_start_time) / 1_000.0).ceil(2)
|
|
731
|
+
LogVisitor.lock_obtaining_failed(
|
|
732
|
+
logger, log_sampled, lock_key, queue_ttl, acquirer_id, host_id,
|
|
733
|
+
acq_time, access_strategy, read_write_mode, acq_process[:result]
|
|
734
|
+
)
|
|
735
|
+
InstrVisitor.lock_obtaining_failed(
|
|
736
|
+
instrumenter, instr_sampled, lock_key, read_write_mode, lock_ttl, acquirer_id, host_id,
|
|
737
|
+
Time.now.to_f, acq_time, acq_process[:result], instrument
|
|
738
|
+
)
|
|
739
|
+
end
|
|
740
|
+
# steep:ignore:end
|
|
741
|
+
raise
|
|
634
742
|
end
|
|
635
743
|
end
|
|
636
744
|
end
|
|
@@ -2,11 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
# @api private
|
|
4
4
|
# @since 1.0.0
|
|
5
|
+
# @version 1.21.0
|
|
6
|
+
# rubocop:disable Metrics/ModuleLength
|
|
5
7
|
module RedisQueuedLocks::Acquirer::ClearDeadRequests
|
|
8
|
+
# @since 1.21.0
|
|
9
|
+
extend RedisQueuedLocks::Utilities
|
|
10
|
+
|
|
11
|
+
# rubocop:disable Metrics/ClassLength
|
|
6
12
|
class << self
|
|
13
|
+
# Drops dead lock requests (requests older than `dead_ttl`) from the lock queues (write and
|
|
14
|
+
# read):
|
|
15
|
+
# - the disabled lock index: the lock queues found in one SCAN pass, one round trip per batch
|
|
16
|
+
# of `scan_size` lock queues (batches are collected across SCAN pages);
|
|
17
|
+
# - the enabled lock index: the lock queues of the locks of the lock index: one ZSCAN pass
|
|
18
|
+
# over the lock names (the keyspace is never scanned), one round trip per batch of
|
|
19
|
+
# `scan_size` lock names (both lock queues of each lock name); expired lock names are
|
|
20
|
+
# dropped from the lock index first;
|
|
21
|
+
# - the operation is reported with one log and one instrumentation event
|
|
22
|
+
# (`redis_queued_locks.clear_dead_requests`) with the time of the whole operation;
|
|
23
|
+
#
|
|
7
24
|
# @param redis_client [RedisClient]
|
|
8
25
|
# @param scan_size [Integer]
|
|
26
|
+
# SCAN count and the number of lock queues in one batch (the disabled lock index) or ZSCAN
|
|
27
|
+
# count and the number of lock names in one batch (the enabled lock index).
|
|
9
28
|
# @param dead_ttl [Integer] In milliseconds
|
|
29
|
+
# @param lock_index_enabled [Boolean]
|
|
30
|
+
# Find lock queues via the lock index instead of the keyspace SCAN.
|
|
10
31
|
# @param logger [::Logger,#debug]
|
|
11
32
|
# @param instrumenter [#notify]
|
|
12
33
|
# @param instrument [NilClass,Any]
|
|
@@ -22,11 +43,13 @@ module RedisQueuedLocks::Acquirer::ClearDeadRequests
|
|
|
22
43
|
#
|
|
23
44
|
# @api private
|
|
24
45
|
# @since 1.0.0
|
|
25
|
-
# @version 1.
|
|
46
|
+
# @version 1.21.0
|
|
47
|
+
# rubocop:disable Metrics/MethodLength
|
|
26
48
|
def clear_dead_requests(
|
|
27
49
|
redis_client,
|
|
28
50
|
scan_size,
|
|
29
51
|
dead_ttl,
|
|
52
|
+
lock_index_enabled,
|
|
30
53
|
logger,
|
|
31
54
|
instrumenter,
|
|
32
55
|
instrument,
|
|
@@ -39,48 +62,230 @@ module RedisQueuedLocks::Acquirer::ClearDeadRequests
|
|
|
39
62
|
instr_sampler,
|
|
40
63
|
instr_sample_this
|
|
41
64
|
)
|
|
65
|
+
# NOTE: (arguments) the same ranges as the config keys: zero/negative sizes fail SCAN/ZSCAN
|
|
66
|
+
# (`COUNT 0`) and the batching, a non-positive dead ttl drops all lock requests;
|
|
67
|
+
if !scan_size.is_a?(::Integer) || scan_size <= 0
|
|
68
|
+
raise(
|
|
69
|
+
RedisQueuedLocks::ArgumentError,
|
|
70
|
+
"`:scan_size` argument should be a positive Integer, got #{scan_size.inspect}."
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
if !dead_ttl.is_a?(::Integer) || dead_ttl <= 0
|
|
74
|
+
raise(
|
|
75
|
+
RedisQueuedLocks::ArgumentError,
|
|
76
|
+
"`:dead_ttl` argument should be a positive Integer, got #{dead_ttl.inspect}."
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
rel_start_time = clock_gettime
|
|
81
|
+
processed_queues = fully_clear_dead_requests(
|
|
82
|
+
redis_client, scan_size, dead_ttl, lock_index_enabled
|
|
83
|
+
)
|
|
84
|
+
time_at = Time.now.to_f
|
|
85
|
+
rel_end_time = clock_gettime
|
|
86
|
+
rel_time = ((rel_end_time - rel_start_time) / 1_000.0).ceil(2)
|
|
87
|
+
|
|
88
|
+
log_sampled = RedisQueuedLocks::Logging.should_log?(
|
|
89
|
+
log_sampling_enabled,
|
|
90
|
+
log_sample_this,
|
|
91
|
+
log_sampling_percent,
|
|
92
|
+
log_sampler
|
|
93
|
+
)
|
|
94
|
+
instr_sampled = RedisQueuedLocks::Instrument.should_instrument?(
|
|
95
|
+
instr_sampling_enabled,
|
|
96
|
+
instr_sample_this,
|
|
97
|
+
instr_sampling_percent,
|
|
98
|
+
instr_sampler
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
run_non_critical do
|
|
102
|
+
logger.debug do
|
|
103
|
+
"[redis_queued_locks.clear_dead_requests] " \
|
|
104
|
+
"dead_ttl => #{dead_ttl} " \
|
|
105
|
+
"prc_queue_cnt => #{processed_queues.size} " \
|
|
106
|
+
"rel_time => #{rel_time} " \
|
|
107
|
+
"at => #{time_at}"
|
|
108
|
+
end
|
|
109
|
+
end if log_sampled
|
|
110
|
+
|
|
111
|
+
run_non_critical do
|
|
112
|
+
instrumenter.notify('redis_queued_locks.clear_dead_requests', {
|
|
113
|
+
dead_ttl: dead_ttl,
|
|
114
|
+
prc_queue_cnt: processed_queues.size,
|
|
115
|
+
rel_time: rel_time,
|
|
116
|
+
at: time_at,
|
|
117
|
+
instrument: instrument
|
|
118
|
+
})
|
|
119
|
+
end if instr_sampled
|
|
120
|
+
|
|
121
|
+
{ ok: true, result: { processed_queues: } }
|
|
122
|
+
end
|
|
123
|
+
# rubocop:enable Metrics/MethodLength
|
|
124
|
+
|
|
125
|
+
private
|
|
126
|
+
|
|
127
|
+
# Drops dead lock requests from the lock queues (see `clear_dead_requests`).
|
|
128
|
+
#
|
|
129
|
+
# @param redis_client [RedisClient]
|
|
130
|
+
# @param scan_size [Integer]
|
|
131
|
+
# @param dead_ttl [Integer] In milliseconds
|
|
132
|
+
# @param lock_index_enabled [Boolean]
|
|
133
|
+
# @return [Set<String>] The processed lock queues (write and read lock request queues).
|
|
134
|
+
#
|
|
135
|
+
# @api private
|
|
136
|
+
# @since 1.21.0
|
|
137
|
+
def fully_clear_dead_requests(redis_client, scan_size, dead_ttl, lock_index_enabled)
|
|
42
138
|
dead_score = RedisQueuedLocks::Resource.acquirer_dead_score(dead_ttl / 1_000.0)
|
|
43
139
|
|
|
44
|
-
|
|
45
|
-
result = Set.new.tap do |processed_queues|
|
|
140
|
+
Set.new.tap do |processed_queues|
|
|
46
141
|
# @type var processed_queues: Set[String]
|
|
47
142
|
redis_client.with do |rconn|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
143
|
+
if lock_index_enabled
|
|
144
|
+
# Step 1 (lock index): drop the expired lock names from the lock index
|
|
145
|
+
# (all their keys are expired);
|
|
146
|
+
rconn.call(
|
|
147
|
+
'ZREMRANGEBYSCORE', RedisQueuedLocks::Resource::LOCK_INDEX_KEY,
|
|
148
|
+
'-inf', RedisQueuedLocks::Resource.lock_index_time - 1
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Step 2 (lock index): drop dead requests from the lock queues of the lock index
|
|
152
|
+
each_lock_name_batch(rconn, scan_size) do |lock_names|
|
|
153
|
+
drop_dead_requests_of_locks(rconn, lock_names, dead_score, processed_queues)
|
|
154
|
+
end
|
|
155
|
+
else
|
|
156
|
+
# Step 1 (keyspace): drop dead requests from the lock queues found in one SCAN pass
|
|
157
|
+
# NOTE: dead requests are dropped with one pipeline per batch of `scan_size` queues
|
|
158
|
+
# (batches are collected across SCAN pages: lock queues can be rare among other keys);
|
|
159
|
+
# @type var lock_queues: Array[String]
|
|
160
|
+
lock_queues = []
|
|
161
|
+
|
|
162
|
+
each_lock_queue(rconn, scan_size) do |lock_queue|
|
|
163
|
+
processed_queues << lock_queue
|
|
164
|
+
lock_queues << lock_queue
|
|
165
|
+
next if lock_queues.size < scan_size
|
|
166
|
+
|
|
167
|
+
drop_dead_requests(rconn, lock_queues, dead_score)
|
|
168
|
+
lock_queues.clear
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
drop_dead_requests(rconn, lock_queues, dead_score) unless lock_queues.empty?
|
|
51
172
|
end
|
|
52
173
|
end
|
|
53
174
|
end
|
|
54
|
-
|
|
55
|
-
{ ok: true, result: { processed_queues: result } }
|
|
56
175
|
end
|
|
57
176
|
|
|
58
|
-
|
|
59
|
-
|
|
177
|
+
# Write and read lock queues in one SCAN pass (the disabled lock index).
|
|
178
|
+
#
|
|
60
179
|
# @param redis_client [RedisClient]
|
|
61
180
|
# @param scan_size [Integer]
|
|
62
181
|
# @yield [lock_queue]
|
|
63
182
|
# @yieldparam lock_queue [String]
|
|
64
183
|
# @yieldreturn [void]
|
|
65
|
-
# @return [
|
|
184
|
+
# @return [void]
|
|
66
185
|
#
|
|
67
186
|
# @api private
|
|
68
187
|
# @since 1.0.0
|
|
69
|
-
# @version 1.
|
|
70
|
-
def each_lock_queue(redis_client, scan_size
|
|
71
|
-
redis_client.scan(
|
|
72
|
-
'MATCH',
|
|
73
|
-
RedisQueuedLocks::Resource::LOCK_QUEUE_PATTERN,
|
|
74
|
-
count: scan_size,
|
|
75
|
-
&block
|
|
76
|
-
)
|
|
77
|
-
# NOTE: (RW) read lock queues
|
|
188
|
+
# @version 1.20.0
|
|
189
|
+
def each_lock_queue(redis_client, scan_size)
|
|
78
190
|
redis_client.scan(
|
|
79
191
|
'MATCH',
|
|
80
|
-
RedisQueuedLocks::Resource::
|
|
81
|
-
count: scan_size
|
|
82
|
-
|
|
83
|
-
|
|
192
|
+
RedisQueuedLocks::Resource::ALL_LOCK_QUEUES_PATTERN,
|
|
193
|
+
count: scan_size
|
|
194
|
+
) do |key|
|
|
195
|
+
# NOTE: the pattern matches other keys too (lock names with `queue:`): lock queues only
|
|
196
|
+
family = RedisQueuedLocks::Resource.lock_key_family(key)
|
|
197
|
+
yield(key) if family == :lock_queue || family == :read_lock_queue
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Drops dead requests of the given lock queues in one round trip (the disabled lock index).
|
|
202
|
+
#
|
|
203
|
+
# @param redis_client [RedisClient]
|
|
204
|
+
# @param lock_queues [Array<String>]
|
|
205
|
+
# @param dead_score [Float]
|
|
206
|
+
# @return [void]
|
|
207
|
+
#
|
|
208
|
+
# @api private
|
|
209
|
+
# @since 1.20.0
|
|
210
|
+
def drop_dead_requests(redis_client, lock_queues, dead_score)
|
|
211
|
+
redis_client.pipelined do |pipeline|
|
|
212
|
+
lock_queues.each do |lock_queue|
|
|
213
|
+
pipeline.call('ZREMRANGEBYSCORE', lock_queue, '-inf', dead_score)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Drops dead requests of both lock queues of the given lock names in one round trip
|
|
219
|
+
# (the enabled lock index).
|
|
220
|
+
#
|
|
221
|
+
# @param redis_client [RedisClient]
|
|
222
|
+
# @param lock_names [Array<String>]
|
|
223
|
+
# @param dead_score [Float]
|
|
224
|
+
# @param processed_queues [Set<String>] Collects the existing (processed) lock queues.
|
|
225
|
+
# @return [void]
|
|
226
|
+
#
|
|
227
|
+
# @api private
|
|
228
|
+
# @since 1.20.0
|
|
229
|
+
def drop_dead_requests_of_locks(redis_client, lock_names, dead_score, processed_queues)
|
|
230
|
+
# NOTE: (pipeline layout) two replies per lock queue (two lock queues per lock name) in
|
|
231
|
+
# the batch order: EXISTS (the queue is processed when it exists), ZREMRANGEBYSCORE;
|
|
232
|
+
# @type var replies: Array[Integer]
|
|
233
|
+
replies = redis_client.pipelined do |pipeline|
|
|
234
|
+
lock_names.each do |lock_name|
|
|
235
|
+
lock_queue = RedisQueuedLocks::Resource.prepare_lock_queue(lock_name)
|
|
236
|
+
read_lock_queue = RedisQueuedLocks::Resource.prepare_read_lock_queue(lock_name)
|
|
237
|
+
pipeline.call('EXISTS', lock_queue)
|
|
238
|
+
pipeline.call('ZREMRANGEBYSCORE', lock_queue, '-inf', dead_score)
|
|
239
|
+
pipeline.call('EXISTS', read_lock_queue)
|
|
240
|
+
pipeline.call('ZREMRANGEBYSCORE', read_lock_queue, '-inf', dead_score)
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
lock_names.each_with_index do |lock_name, index|
|
|
245
|
+
if replies[index * 4] == 1
|
|
246
|
+
processed_queues << RedisQueuedLocks::Resource.prepare_lock_queue(lock_name)
|
|
247
|
+
end
|
|
248
|
+
if replies[(index * 4) + 2] == 1
|
|
249
|
+
processed_queues << RedisQueuedLocks::Resource.prepare_read_lock_queue(lock_name)
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Iterates over the lock names of the lock index (one ZSCAN pass) in batches of `scan_size`
|
|
255
|
+
# lock names (batches are collected across ZSCAN pages). Expired lock names (not dropped from
|
|
256
|
+
# the index yet) are skipped: all their keys are expired.
|
|
257
|
+
#
|
|
258
|
+
# NOTE: each operation module iterates over the lock index by itself (duplicated on purpose:
|
|
259
|
+
# operation modules are independent of each other, see `Locks.each_lock_name_batch`);
|
|
260
|
+
#
|
|
261
|
+
# @param redis_client [RedisClient]
|
|
262
|
+
# @param scan_size [Integer] ZSCAN count and the number of lock names in one batch.
|
|
263
|
+
# @yield [lock_names]
|
|
264
|
+
# @yieldparam lock_names [Array<String>] The batch of lock names (reused between batches).
|
|
265
|
+
# @return [void]
|
|
266
|
+
#
|
|
267
|
+
# @api private
|
|
268
|
+
# @since 1.20.0
|
|
269
|
+
def each_lock_name_batch(redis_client, scan_size)
|
|
270
|
+
lock_index_time = RedisQueuedLocks::Resource.lock_index_time
|
|
271
|
+
# @type var lock_names: Array[String]
|
|
272
|
+
lock_names = []
|
|
273
|
+
|
|
274
|
+
redis_client.zscan(
|
|
275
|
+
RedisQueuedLocks::Resource::LOCK_INDEX_KEY, count: scan_size
|
|
276
|
+
) do |lock_name, lock_index_score|
|
|
277
|
+
next if lock_index_score.to_f < lock_index_time
|
|
278
|
+
|
|
279
|
+
lock_names << lock_name
|
|
280
|
+
next if lock_names.size < scan_size
|
|
281
|
+
|
|
282
|
+
yield(lock_names)
|
|
283
|
+
lock_names.clear
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
yield(lock_names) unless lock_names.empty?
|
|
84
287
|
end
|
|
85
288
|
end
|
|
289
|
+
# rubocop:enable Metrics/ClassLength
|
|
86
290
|
end
|
|
291
|
+
# rubocop:enable Metrics/ModuleLength
|