redis_queued_locks 1.20.0 → 1.22.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 +249 -36
- data/.claude/rules/acquirer.md +16 -14
- data/.claude/rules/arguments.md +2 -2
- data/.claude/rules/changelog.md +27 -0
- data/.claude/rules/git.md +1 -1
- data/.claude/rules/logic.md +34 -13
- data/.claude/rules/swarm.md +4 -5
- data/.claude/rules/tests.md +9 -6
- data/.claude/rules/type-checking.md +13 -14
- data/.claude/rules/visitors.md +13 -14
- data/.claude/skills/rql-code-review/SKILL.md +134 -43
- data/CHANGELOG.md +105 -125
- data/CLAUDE.md +11 -9
- data/README.md +501 -136
- data/Rakefile +11 -0
- 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 +68 -10
- data/lib/redis_queued_locks/acquirer/acquire_lock/log_visitor.rb +82 -15
- data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock/log_visitor.rb +92 -14
- data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rb +441 -63
- 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 +5 -5
- data/lib/redis_queued_locks/acquirer/acquire_lock.rb +176 -39
- data/lib/redis_queued_locks/acquirer/clear_dead_requests.rb +322 -49
- data/lib/redis_queued_locks/acquirer/extend_lock_ttl.rb +125 -13
- 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 +16 -3
- data/lib/redis_queued_locks/acquirer/lock_info.rb +31 -39
- data/lib/redis_queued_locks/acquirer/lock_series_poc/instr_visitor.rb +11 -5
- data/lib/redis_queued_locks/acquirer/lock_series_poc/log_visitor.rb +20 -10
- data/lib/redis_queued_locks/acquirer/lock_series_poc.rb +105 -36
- data/lib/redis_queued_locks/acquirer/locks.rb +32 -11
- data/lib/redis_queued_locks/acquirer/queue_info.rb +30 -8
- data/lib/redis_queued_locks/acquirer/queues.rb +26 -6
- data/lib/redis_queued_locks/acquirer/reindex_locks.rb +117 -4
- data/lib/redis_queued_locks/acquirer/release_all_locks.rb +37 -6
- data/lib/redis_queued_locks/acquirer/release_lock.rb +33 -7
- data/lib/redis_queued_locks/acquirer/release_locks_of.rb +45 -6
- data/lib/redis_queued_locks/acquirer/release_read_lock.rb +22 -3
- data/lib/redis_queued_locks/client.rb +140 -56
- data/lib/redis_queued_locks/config.rb +105 -14
- 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 +137 -12
- data/lib/redis_queued_locks/swarm/flush_zombies.rb +1 -0
- data/lib/redis_queued_locks/swarm/redis_client_builder.rb +8 -4
- data/lib/redis_queued_locks/swarm/swarm_element/threaded.rb +8 -6
- data/lib/redis_queued_locks/swarm/zombie_info.rb +6 -2
- data/lib/redis_queued_locks/swarm.rb +86 -5
- data/lib/redis_queued_locks/utilities.rb +1 -1
- data/lib/redis_queued_locks/version.rb +2 -2
- data/rbs_collection.lock.yaml +4 -0
- data/rbs_collection.yaml +2 -1
- data/sig/manifest.yml +1 -0
- data/sig/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue/log_visitor.rbs +2 -2
- data/sig/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue.rbs +2 -2
- data/sig/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rbs +25 -5
- data/sig/redis_queued_locks/acquirer/acquire_lock/log_visitor.rbs +32 -12
- data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock/log_visitor.rbs +61 -40
- data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rbs +59 -11
- data/sig/redis_queued_locks/acquirer/acquire_lock/{yield_with_expire → yield_expire}/log_visitor.rbs +4 -4
- data/sig/redis_queued_locks/acquirer/acquire_lock/yield_expire.rbs +4 -4
- data/sig/redis_queued_locks/acquirer/acquire_lock.rbs +6 -4
- data/sig/redis_queued_locks/acquirer/clear_dead_requests.rbs +23 -1
- data/sig/redis_queued_locks/acquirer/extend_lock_ttl.rbs +16 -3
- data/sig/redis_queued_locks/acquirer/queues.rbs +1 -1
- data/sig/redis_queued_locks/acquirer/reindex_locks.rbs +22 -1
- data/sig/redis_queued_locks/acquirer/release_lock.rbs +3 -2
- data/sig/redis_queued_locks/acquirer/release_read_lock.rbs +1 -1
- data/sig/redis_queued_locks/client.rbs +24 -10
- data/sig/redis_queued_locks/config.rbs +54 -1
- data/sig/redis_queued_locks/errors.rbs +5 -2
- data/sig/redis_queued_locks/instrument/sampler.rbs +3 -3
- data/sig/redis_queued_locks/instrument/void_notifier.rbs +3 -1
- data/sig/redis_queued_locks/logging/sampler.rbs +3 -3
- data/sig/redis_queued_locks/resource.rbs +23 -13
- data/sig/redis_queued_locks/swarm/swarm_element/threaded.rbs +1 -1
- data/sig/redis_queued_locks/utilities.rbs +2 -1
- data/sig/redis_queued_locks.rbs +7 -0
- data/sig/vendor/redis_client.rbs +1 -0
- metadata +4 -3
- /data/sig/redis_queued_locks/{acquier.rbs → acquirer.rbs} +0 -0
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# NOTE: Lock Series PoC
|
|
4
|
-
# rubocop:disable
|
|
4
|
+
# rubocop:disable Metrics/ModuleLength, Metrics/ClassLength, Metrics/MethodLength
|
|
5
5
|
# @api private
|
|
6
6
|
# @since 1.16.0
|
|
7
|
+
# @version 1.22.0
|
|
7
8
|
module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
|
|
8
9
|
require_relative 'lock_series_poc/log_visitor'
|
|
9
10
|
require_relative 'lock_series_poc/instr_visitor'
|
|
10
11
|
|
|
11
|
-
# @
|
|
12
|
+
# @since 1.16.0
|
|
12
13
|
extend RedisQueuedLocks::Acquirer::AcquireLock::YieldExpire
|
|
13
14
|
|
|
14
15
|
class << self
|
|
15
16
|
# @api private
|
|
16
17
|
# @since 1.16.0
|
|
17
|
-
# @version 1.
|
|
18
|
+
# @version 1.22.0
|
|
18
19
|
def lock_series_poc( # steep:ignore
|
|
19
20
|
redis,
|
|
20
21
|
lock_names,
|
|
@@ -44,6 +45,8 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
|
|
|
44
45
|
conflict_strategy:,
|
|
45
46
|
read_write_mode:,
|
|
46
47
|
access_strategy:,
|
|
48
|
+
priority:,
|
|
49
|
+
lock_priorities:,
|
|
47
50
|
log_sampling_enabled:,
|
|
48
51
|
log_sampling_percent:,
|
|
49
52
|
log_sampler:,
|
|
@@ -56,6 +59,8 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
|
|
|
56
59
|
)
|
|
57
60
|
# NOTE: (RW) the lock mode is the same for all locks of the series
|
|
58
61
|
# (see RedisQueuedLocks::Acquirer::AcquireLock for read_write_mode validations);
|
|
62
|
+
# NOTE: the lock request priority is the same for all locks of the series too (validated
|
|
63
|
+
# below: the series logs and instrumentation report it);
|
|
59
64
|
|
|
60
65
|
case meta
|
|
61
66
|
when Hash, NilClass then nil
|
|
@@ -66,19 +71,22 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
|
|
|
66
71
|
)
|
|
67
72
|
end
|
|
68
73
|
|
|
74
|
+
# NOTE: Symbol keys are written to the lock data as Strings (the same check as in
|
|
75
|
+
# RedisQueuedLocks::Acquirer::AcquireLock);
|
|
69
76
|
if meta.is_a?(::Hash) && (meta.any? do |key, _value|
|
|
70
|
-
key
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
meta_key = key.is_a?(::Symbol) ? key.name : key
|
|
78
|
+
meta_key == 'acq_id' ||
|
|
79
|
+
meta_key == 'hst_id' ||
|
|
80
|
+
meta_key == 'ts' ||
|
|
81
|
+
meta_key == 'ini_ttl' ||
|
|
82
|
+
meta_key == 'lock_key' ||
|
|
83
|
+
meta_key == 'rem_ttl' ||
|
|
84
|
+
meta_key == 'spc_ext_ttl' ||
|
|
85
|
+
meta_key == 'spc_cnt' ||
|
|
86
|
+
meta_key == 'l_spc_ext_ini_ttl' ||
|
|
87
|
+
meta_key == 'l_spc_ext_ts' ||
|
|
88
|
+
meta_key == 'l_spc_ts' ||
|
|
89
|
+
meta_key == 'rw_mode'
|
|
82
90
|
end)
|
|
83
91
|
raise(
|
|
84
92
|
RedisQueuedLocks::ArgumentError,
|
|
@@ -88,6 +96,51 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
|
|
|
88
96
|
)
|
|
89
97
|
end
|
|
90
98
|
|
|
99
|
+
# NOTE: the series ttl is validated before the lock ttl calculation: each lock ttl of
|
|
100
|
+
# the series includes the retry delay and jitter (a non-positive series ttl can produce
|
|
101
|
+
# a positive lock ttl), the series ttl limits the `timed` block (the same check as in
|
|
102
|
+
# RedisQueuedLocks::Acquirer::AcquireLock);
|
|
103
|
+
if !ttl.is_a?(::Integer) || ttl <= 0
|
|
104
|
+
raise(
|
|
105
|
+
RedisQueuedLocks::ArgumentError,
|
|
106
|
+
"`:ttl` argument should be a positive Integer, got #{ttl.inspect}."
|
|
107
|
+
)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# NOTE: the retry delay and jitter are parts of the lock ttls of the series (validated before
|
|
111
|
+
# the lock ttl calculation; other options are validated by
|
|
112
|
+
# RedisQueuedLocks::Acquirer::AcquireLock on the first lock of the series, before any lock
|
|
113
|
+
# is obtained);
|
|
114
|
+
if !retry_delay.is_a?(::Integer) || retry_delay < 0
|
|
115
|
+
raise(
|
|
116
|
+
RedisQueuedLocks::ArgumentError,
|
|
117
|
+
"`:retry_delay` argument should be a non-negative Integer, got #{retry_delay.inspect}."
|
|
118
|
+
)
|
|
119
|
+
end
|
|
120
|
+
if !retry_jitter.is_a?(::Integer) || retry_jitter < 0
|
|
121
|
+
raise(
|
|
122
|
+
RedisQueuedLocks::ArgumentError,
|
|
123
|
+
"`:retry_jitter` argument should be a non-negative Integer, got #{retry_jitter.inspect}."
|
|
124
|
+
)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# NOTE: the priority of the series is validated before the series logs and instrumentation:
|
|
128
|
+
# a priority name is resolved to its Integer priority (the series events report the Integer
|
|
129
|
+
# priority; the same check as in RedisQueuedLocks::Acquirer::AcquireLock);
|
|
130
|
+
priority = lock_priorities.fetch(priority, priority) if priority.is_a?(::Symbol)
|
|
131
|
+
if !priority.is_a?(::Integer) ||
|
|
132
|
+
priority < RedisQueuedLocks::Resource::MIN_LOCK_PRIORITY ||
|
|
133
|
+
priority > RedisQueuedLocks::Resource::MAX_LOCK_PRIORITY
|
|
134
|
+
raise(
|
|
135
|
+
RedisQueuedLocks::ArgumentError,
|
|
136
|
+
"`:priority` argument should be an Integer from " \
|
|
137
|
+
"#{RedisQueuedLocks::Resource::MIN_LOCK_PRIORITY} to " \
|
|
138
|
+
"#{RedisQueuedLocks::Resource::MAX_LOCK_PRIORITY} or a priority name of " \
|
|
139
|
+
"`config['lock_priorities']` (#{lock_priorities.keys.map(&:inspect).join(', ')}), " \
|
|
140
|
+
"got #{priority.inspect}."
|
|
141
|
+
)
|
|
142
|
+
end
|
|
143
|
+
|
|
91
144
|
locks_count = lock_names.size
|
|
92
145
|
|
|
93
146
|
lock_acquirement_operations = lock_names.each_with_index.map do |lock_name, position|
|
|
@@ -132,10 +185,10 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
|
|
|
132
185
|
identity
|
|
133
186
|
)
|
|
134
187
|
|
|
135
|
-
|
|
188
|
+
LogVisitor.start_lock_series_obtaining( # steep:ignore
|
|
136
189
|
logger, log_sampled, lock_keys_for_instrumentation,
|
|
137
|
-
queue_ttl, acquirer_id_for_instrumentation, host_id_for_instrumentation,
|
|
138
|
-
read_write_mode
|
|
190
|
+
queue_ttl, acquirer_id_for_instrumentation, host_id_for_instrumentation,
|
|
191
|
+
access_strategy, read_write_mode, priority
|
|
139
192
|
)
|
|
140
193
|
|
|
141
194
|
acq_start_time = RedisQueuedLocks::Utilities.clock_gettime
|
|
@@ -176,6 +229,8 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
|
|
|
176
229
|
conflict_strategy:,
|
|
177
230
|
read_write_mode:,
|
|
178
231
|
access_strategy:,
|
|
232
|
+
priority:,
|
|
233
|
+
lock_priorities:,
|
|
179
234
|
log_sampling_enabled:,
|
|
180
235
|
log_sampling_percent:,
|
|
181
236
|
log_sampler:,
|
|
@@ -208,7 +263,8 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
|
|
|
208
263
|
else
|
|
209
264
|
failed_on_lock = lock_operation_options[:lock_name]
|
|
210
265
|
failed_locks_and_errors[lock_operation_options[:lock_name]] = result
|
|
211
|
-
# NOTE: release all previously acquired locks
|
|
266
|
+
# NOTE: release all previously acquired locks
|
|
267
|
+
# (non-exceptional failure: raise_errors: false)
|
|
212
268
|
unless is_failed_series_released
|
|
213
269
|
release_lock_series( # steep:ignore
|
|
214
270
|
redis, read_write_mode, successfully_acquired_locks, acquirer_id_for_instrumentation
|
|
@@ -219,20 +275,22 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
|
|
|
219
275
|
end
|
|
220
276
|
end
|
|
221
277
|
|
|
222
|
-
if
|
|
278
|
+
if successfully_acquired_locks.size == lock_names.size &&
|
|
279
|
+
successfully_acquired_locks.all? { |res| res[:ok] }
|
|
223
280
|
acq_end_time = RedisQueuedLocks::Utilities.clock_gettime
|
|
224
281
|
acq_time = ((acq_end_time - acq_start_time) / 1_000.0).ceil(2)
|
|
225
282
|
ts = Time.now.to_f
|
|
226
283
|
|
|
227
|
-
|
|
284
|
+
LogVisitor.lock_series_obtained( # steep:ignore
|
|
228
285
|
logger, log_sampled, lock_keys_for_instrumentation,
|
|
229
286
|
queue_ttl, acquirer_id_for_instrumentation, host_id_for_instrumentation,
|
|
230
|
-
acq_time, access_strategy, read_write_mode
|
|
287
|
+
acq_time, access_strategy, read_write_mode, priority
|
|
231
288
|
)
|
|
232
289
|
|
|
233
|
-
|
|
234
|
-
instrumenter, instr_sampled, lock_keys_for_instrumentation, read_write_mode,
|
|
235
|
-
ttl, acquirer_id_for_instrumentation, host_id_for_instrumentation, ts, acq_time,
|
|
290
|
+
InstrVisitor.lock_series_obtained( # steep:ignore
|
|
291
|
+
instrumenter, instr_sampled, lock_keys_for_instrumentation, read_write_mode, priority,
|
|
292
|
+
ttl, acquirer_id_for_instrumentation, host_id_for_instrumentation, ts, acq_time,
|
|
293
|
+
instrument
|
|
236
294
|
)
|
|
237
295
|
|
|
238
296
|
yield_time = RedisQueuedLocks::Utilities.clock_gettime
|
|
@@ -242,8 +300,8 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
|
|
|
242
300
|
).ceil(2)
|
|
243
301
|
|
|
244
302
|
yield_result = nil
|
|
245
|
-
is_lock_manually_released
|
|
246
|
-
hold_time
|
|
303
|
+
# @type var is_lock_manually_released: bool
|
|
304
|
+
# @type var hold_time: Float | Integer
|
|
247
305
|
|
|
248
306
|
begin
|
|
249
307
|
yield_result = yield_expire( # steep:ignore
|
|
@@ -284,17 +342,18 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
|
|
|
284
342
|
hold_time = ((rel_time - acq_end_time) / 1_000.0).ceil(2)
|
|
285
343
|
ts = Time.now.to_f
|
|
286
344
|
|
|
287
|
-
|
|
345
|
+
LogVisitor.expire_lock_series( # steep:ignore
|
|
288
346
|
logger, log_sampled, lock_keys_for_instrumentation,
|
|
289
|
-
queue_ttl, acquirer_id_for_instrumentation, host_id_for_instrumentation,
|
|
290
|
-
read_write_mode
|
|
347
|
+
queue_ttl, acquirer_id_for_instrumentation, host_id_for_instrumentation,
|
|
348
|
+
access_strategy, read_write_mode, priority
|
|
291
349
|
) if is_lock_manually_released
|
|
292
350
|
|
|
293
|
-
|
|
351
|
+
InstrVisitor.lock_series_hold_and_release( # steep:ignore
|
|
294
352
|
instrumenter,
|
|
295
353
|
instr_sampled,
|
|
296
354
|
lock_keys_for_instrumentation,
|
|
297
355
|
read_write_mode,
|
|
356
|
+
priority,
|
|
298
357
|
ttl,
|
|
299
358
|
acquirer_id_for_instrumentation,
|
|
300
359
|
host_id_for_instrumentation,
|
|
@@ -330,7 +389,7 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
|
|
|
330
389
|
lock_series: lock_names,
|
|
331
390
|
acquired_locks:,
|
|
332
391
|
missing_locks:,
|
|
333
|
-
failed_on_lock
|
|
392
|
+
failed_on_lock:
|
|
334
393
|
}
|
|
335
394
|
}
|
|
336
395
|
end
|
|
@@ -350,7 +409,9 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
|
|
|
350
409
|
#
|
|
351
410
|
# @api private
|
|
352
411
|
# @since 1.18.0
|
|
353
|
-
def release_lock_series(
|
|
412
|
+
def release_lock_series( # steep:ignore
|
|
413
|
+
redis, read_write_mode, successfully_acquired_locks, acquirer_id
|
|
414
|
+
)
|
|
354
415
|
lock_names = obtained_lock_names(successfully_acquired_locks) # steep:ignore
|
|
355
416
|
|
|
356
417
|
if read_write_mode == :read
|
|
@@ -374,7 +435,7 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
|
|
|
374
435
|
#
|
|
375
436
|
# @api private
|
|
376
437
|
# @since 1.18.0
|
|
377
|
-
# @version 1.
|
|
438
|
+
# @version 1.21.0
|
|
378
439
|
def release_write_lock_series(redis, lock_names, acquirer_id) # steep:ignore
|
|
379
440
|
return if lock_names.empty?
|
|
380
441
|
|
|
@@ -395,8 +456,16 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
|
|
|
395
456
|
own_lock_keys.each { |lock_key| transaction.call('DEL', lock_key) }
|
|
396
457
|
end
|
|
397
458
|
# NOTE: `nil` => the transaction is aborted (a watched lock is changed) => retry
|
|
398
|
-
break unless released
|
|
459
|
+
break unless released == nil
|
|
399
460
|
end
|
|
461
|
+
rescue RedisQueuedLocks::LockAcquirementIntermediateTimeoutError,
|
|
462
|
+
RedisQueuedLocks::TimedLockIntermediateTimeoutError,
|
|
463
|
+
::Timeout::ExitException
|
|
464
|
+
# NOTE: (timeouts) an interrupted WATCH block (the internal timeouts and the user's
|
|
465
|
+
# `Timeout.timeout` without an exception class): the closed connection drops the WATCH
|
|
466
|
+
# state (redis-client sends `UNWATCH` only on `StandardError`);
|
|
467
|
+
conn.close
|
|
468
|
+
raise
|
|
400
469
|
end
|
|
401
470
|
end
|
|
402
471
|
|
|
@@ -441,4 +510,4 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
|
|
|
441
510
|
end
|
|
442
511
|
end
|
|
443
512
|
end
|
|
444
|
-
# rubocop:enable
|
|
513
|
+
# rubocop:enable Metrics/ModuleLength, Metrics/ClassLength, Metrics/MethodLength
|
|
@@ -25,8 +25,17 @@ module RedisQueuedLocks::Acquirer::Locks
|
|
|
25
25
|
#
|
|
26
26
|
# @api private
|
|
27
27
|
# @since 1.0.0
|
|
28
|
-
# @version 1.
|
|
28
|
+
# @version 1.21.0
|
|
29
29
|
def locks(redis_client, scan_size:, with_info:, lock_index_enabled:)
|
|
30
|
+
# NOTE: (arguments) the same range as the config key: zero/negative sizes fail SCAN/ZSCAN
|
|
31
|
+
# (`COUNT 0`) and the batching (`each_slice(0)`);
|
|
32
|
+
if !scan_size.is_a?(::Integer) || scan_size <= 0
|
|
33
|
+
raise(
|
|
34
|
+
RedisQueuedLocks::ArgumentError,
|
|
35
|
+
"`:scan_size` argument should be a positive Integer, got #{scan_size.inspect}."
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
30
39
|
redis_client.with do |rconn|
|
|
31
40
|
lock_keys =
|
|
32
41
|
lock_index_enabled ? index_locks(rconn, scan_size) : scan_locks(rconn, scan_size)
|
|
@@ -106,7 +115,8 @@ module RedisQueuedLocks::Acquirer::Locks
|
|
|
106
115
|
|
|
107
116
|
# Iterates over the lock names of the lock index (one ZSCAN pass) in batches of `scan_size`
|
|
108
117
|
# lock names (batches are collected across ZSCAN pages). Expired lock names (not dropped from
|
|
109
|
-
# the index yet) are skipped: all their keys are expired
|
|
118
|
+
# the index yet) are skipped: all their keys are expired (`+inf` names, keys without TTL, never
|
|
119
|
+
# expire: see `RedisQueuedLocks::Resource.lock_index_score_expired?`).
|
|
110
120
|
#
|
|
111
121
|
# NOTE: each operation module iterates over the lock index by itself (duplicated on purpose:
|
|
112
122
|
# operation modules are independent of each other, see `extract_locks_info`);
|
|
@@ -119,6 +129,7 @@ module RedisQueuedLocks::Acquirer::Locks
|
|
|
119
129
|
#
|
|
120
130
|
# @api private
|
|
121
131
|
# @since 1.20.0
|
|
132
|
+
# @version 1.22.0
|
|
122
133
|
def each_lock_name_batch(redis_client, scan_size)
|
|
123
134
|
lock_index_time = RedisQueuedLocks::Resource.lock_index_time
|
|
124
135
|
# @type var lock_names: Array[String]
|
|
@@ -127,7 +138,9 @@ module RedisQueuedLocks::Acquirer::Locks
|
|
|
127
138
|
redis_client.zscan(
|
|
128
139
|
RedisQueuedLocks::Resource::LOCK_INDEX_KEY, count: scan_size
|
|
129
140
|
) do |lock_name, lock_index_score|
|
|
130
|
-
next if
|
|
141
|
+
next if RedisQueuedLocks::Resource.lock_index_score_expired?(
|
|
142
|
+
lock_index_score, lock_index_time
|
|
143
|
+
)
|
|
131
144
|
|
|
132
145
|
lock_names << lock_name
|
|
133
146
|
next if lock_names.size < scan_size
|
|
@@ -153,7 +166,7 @@ module RedisQueuedLocks::Acquirer::Locks
|
|
|
153
166
|
#
|
|
154
167
|
# @api private
|
|
155
168
|
# @since 1.0.0
|
|
156
|
-
# @version 1.
|
|
169
|
+
# @version 1.21.0
|
|
157
170
|
# rubocop:disable Metrics/MethodLength
|
|
158
171
|
def extract_locks_info(redis_client, lock_keys, scan_size)
|
|
159
172
|
Set.new.tap do |seeded_locks|
|
|
@@ -162,6 +175,13 @@ module RedisQueuedLocks::Acquirer::Locks
|
|
|
162
175
|
# rubocop:enable Layout/LineLength
|
|
163
176
|
|
|
164
177
|
lock_keys.each_slice(scan_size) do |lock_keys_batch|
|
|
178
|
+
# Step 0: lock names of the batch (once per lock: the readers registry keys and the read
|
|
179
|
+
# lock data keys are built from them)
|
|
180
|
+
# @type var lock_names_batch: Array[String]
|
|
181
|
+
lock_names_batch = lock_keys_batch.map do |lock_key|
|
|
182
|
+
RedisQueuedLocks::Resource.lock_name_from_key(lock_key, :lock) #: String
|
|
183
|
+
end
|
|
184
|
+
|
|
165
185
|
# Step 1: extract the state of the batch of locks in one round trip
|
|
166
186
|
# NOTE: (pipeline layout) [0] TIME (read locks are checked in redis time), then
|
|
167
187
|
# `LOCK_STATE_REPLIES` replies per lock in the batch order (the replies of the lock
|
|
@@ -170,10 +190,9 @@ module RedisQueuedLocks::Acquirer::Locks
|
|
|
170
190
|
# @type var batch_state: Array[untyped]
|
|
171
191
|
batch_state = redis_client.pipelined do |pipeline|
|
|
172
192
|
pipeline.call('TIME')
|
|
173
|
-
lock_keys_batch.
|
|
174
|
-
lock_readers_key =
|
|
175
|
-
|
|
176
|
-
)
|
|
193
|
+
lock_keys_batch.each_with_index do |lock_key, index|
|
|
194
|
+
lock_readers_key =
|
|
195
|
+
RedisQueuedLocks::Resource.prepare_lock_readers(lock_names_batch[index])
|
|
177
196
|
pipeline.call('HGETALL', lock_key)
|
|
178
197
|
pipeline.call('PTTL', lock_key)
|
|
179
198
|
pipeline.call('ZRANGE', lock_readers_key, '0', '-1', 'WITHSCORES')
|
|
@@ -199,12 +218,13 @@ module RedisQueuedLocks::Acquirer::Locks
|
|
|
199
218
|
# read lock data can be absent if it is dropped during the read lock life;
|
|
200
219
|
# @type var read_locks_data: Array[Hash[String,String]]
|
|
201
220
|
read_locks_data = redis_client.pipelined do |pipeline|
|
|
202
|
-
lock_keys_batch.
|
|
203
|
-
lock_name = lock_key.delete_prefix('rql:lock:')
|
|
221
|
+
lock_keys_batch.each_index do |index|
|
|
204
222
|
live_read_locks[index]&.each do |(acquirer_id, _expiration)|
|
|
205
223
|
pipeline.call(
|
|
206
224
|
'HGETALL',
|
|
207
|
-
RedisQueuedLocks::Resource.prepare_read_lock_key(
|
|
225
|
+
RedisQueuedLocks::Resource.prepare_read_lock_key(
|
|
226
|
+
lock_names_batch[index], acquirer_id
|
|
227
|
+
)
|
|
208
228
|
)
|
|
209
229
|
end
|
|
210
230
|
end
|
|
@@ -232,6 +252,7 @@ module RedisQueuedLocks::Acquirer::Locks
|
|
|
232
252
|
pttl_cmd_res = batch_state[replies_at + 1] # NOTE: PTTL result (integer)
|
|
233
253
|
|
|
234
254
|
hget_cmd_res.tap do |lock_data|
|
|
255
|
+
lock_data['lock_key'] = lock_key
|
|
235
256
|
lock_data['rw_mode'] = 'write'
|
|
236
257
|
lock_data['ts'] = Float(lock_data['ts'])
|
|
237
258
|
lock_data['ini_ttl'] = Integer(lock_data['ini_ttl'])
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
# @api private
|
|
4
4
|
# @since 1.0.0
|
|
5
|
-
# @version 1.20.0
|
|
6
5
|
module RedisQueuedLocks::Acquirer::QueueInfo
|
|
7
6
|
class << self
|
|
8
7
|
# Returns an information about the required lock queue by the lock name. The result
|
|
@@ -17,22 +16,34 @@ module RedisQueuedLocks::Acquirer::QueueInfo
|
|
|
17
16
|
# - result format: {
|
|
18
17
|
# "lock_queue" => "rql:lock_queue:your_lock_name", # lock queue key in redis,
|
|
19
18
|
# "queue" => [
|
|
20
|
-
# {
|
|
21
|
-
#
|
|
19
|
+
# {
|
|
20
|
+
# "acq_id" => "rql:acq:123/456/789/987/id",
|
|
21
|
+
# "score" => 123.456,
|
|
22
|
+
# "rw_mode" => "write",
|
|
23
|
+
# "priority" => 0
|
|
24
|
+
# },
|
|
22
25
|
# ...
|
|
23
26
|
# ] # ordered set (by score) with information about an acquirer, their position in queue
|
|
24
|
-
# #
|
|
27
|
+
# # (the enqueue time shifted by the priority band: see
|
|
28
|
+
# # `RedisQueuedLocks::Resource::LOCK_PRIORITY_BAND`), the requested lock mode and
|
|
29
|
+
# # the lock request priority (decoded from the position)
|
|
25
30
|
# <additional keys when the queue of read lock requests exists>:
|
|
26
31
|
# "read_lock_queue" => "rql:lock_read_queue:your_lock_name", # read lock queue key in redis
|
|
27
32
|
# "read_queue" => [
|
|
28
|
-
# {
|
|
33
|
+
# {
|
|
34
|
+
# "acq_id" => "rql:acq:123/456/789/987/id",
|
|
35
|
+
# "score" => 123.456,
|
|
36
|
+
# "rw_mode" => "read",
|
|
37
|
+
# "priority" => 0
|
|
38
|
+
# },
|
|
29
39
|
# ...
|
|
30
40
|
# ] # read lock requests (positions are comparable with the write lock request positions)
|
|
31
41
|
# }
|
|
32
42
|
#
|
|
33
43
|
# @api private
|
|
34
44
|
# @since 1.0.0
|
|
35
|
-
# @version 1.
|
|
45
|
+
# @version 1.22.0
|
|
46
|
+
# rubocop:disable Metrics/MethodLength
|
|
36
47
|
def queue_info(redis_client, lock_name)
|
|
37
48
|
lock_key_queue = RedisQueuedLocks::Resource.prepare_lock_queue(lock_name)
|
|
38
49
|
read_lock_key_queue = RedisQueuedLocks::Resource.prepare_read_lock_queue(lock_name)
|
|
@@ -55,14 +66,24 @@ module RedisQueuedLocks::Acquirer::QueueInfo
|
|
|
55
66
|
queue_info = {
|
|
56
67
|
'lock_queue' => lock_key_queue,
|
|
57
68
|
'queue' => zrange_cmd_res.map do |val|
|
|
58
|
-
{
|
|
69
|
+
{
|
|
70
|
+
'acq_id' => val[0],
|
|
71
|
+
'score' => val[1],
|
|
72
|
+
'rw_mode' => 'write',
|
|
73
|
+
'priority' => RedisQueuedLocks::Resource.lock_request_priority(val[1])
|
|
74
|
+
}
|
|
59
75
|
end
|
|
60
76
|
}
|
|
61
77
|
# NOTE: (RW) the queue of read lock requests
|
|
62
78
|
unless read_zrange_cmd_res.empty?
|
|
63
79
|
queue_info['read_lock_queue'] = read_lock_key_queue
|
|
64
80
|
queue_info['read_queue'] = read_zrange_cmd_res.map do |val|
|
|
65
|
-
{
|
|
81
|
+
{
|
|
82
|
+
'acq_id' => val[0],
|
|
83
|
+
'score' => val[1],
|
|
84
|
+
'rw_mode' => 'read',
|
|
85
|
+
'priority' => RedisQueuedLocks::Resource.lock_request_priority(val[1])
|
|
86
|
+
}
|
|
66
87
|
end
|
|
67
88
|
end
|
|
68
89
|
queue_info
|
|
@@ -71,5 +92,6 @@ module RedisQueuedLocks::Acquirer::QueueInfo
|
|
|
71
92
|
nil
|
|
72
93
|
end
|
|
73
94
|
end
|
|
95
|
+
# rubocop:enable Metrics/MethodLength
|
|
74
96
|
end
|
|
75
97
|
end
|
|
@@ -15,8 +15,17 @@ module RedisQueuedLocks::Acquirer::Queues
|
|
|
15
15
|
#
|
|
16
16
|
# @api private
|
|
17
17
|
# @since 1.0.0
|
|
18
|
-
# @version 1.
|
|
18
|
+
# @version 1.22.0
|
|
19
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
|
+
|
|
20
29
|
redis_client.with do |rconn|
|
|
21
30
|
lock_queues =
|
|
22
31
|
lock_index_enabled ? index_queues(rconn, scan_size) : scan_queues(rconn, scan_size)
|
|
@@ -93,7 +102,8 @@ module RedisQueuedLocks::Acquirer::Queues
|
|
|
93
102
|
|
|
94
103
|
# Iterates over the lock names of the lock index (one ZSCAN pass) in batches of `scan_size`
|
|
95
104
|
# lock names (batches are collected across ZSCAN pages). Expired lock names (not dropped from
|
|
96
|
-
# the index yet) are skipped: all their keys are expired
|
|
105
|
+
# the index yet) are skipped: all their keys are expired (`+inf` names, keys without TTL, never
|
|
106
|
+
# expire: see `RedisQueuedLocks::Resource.lock_index_score_expired?`).
|
|
97
107
|
#
|
|
98
108
|
# NOTE: each operation module iterates over the lock index by itself (duplicated on purpose:
|
|
99
109
|
# operation modules are independent of each other, see `Locks.each_lock_name_batch`);
|
|
@@ -106,6 +116,7 @@ module RedisQueuedLocks::Acquirer::Queues
|
|
|
106
116
|
#
|
|
107
117
|
# @api private
|
|
108
118
|
# @since 1.20.0
|
|
119
|
+
# @version 1.22.0
|
|
109
120
|
def each_lock_name_batch(redis_client, scan_size)
|
|
110
121
|
lock_index_time = RedisQueuedLocks::Resource.lock_index_time
|
|
111
122
|
# @type var lock_names: Array[String]
|
|
@@ -114,7 +125,9 @@ module RedisQueuedLocks::Acquirer::Queues
|
|
|
114
125
|
redis_client.zscan(
|
|
115
126
|
RedisQueuedLocks::Resource::LOCK_INDEX_KEY, count: scan_size
|
|
116
127
|
) do |lock_name, lock_index_score|
|
|
117
|
-
next if
|
|
128
|
+
next if RedisQueuedLocks::Resource.lock_index_score_expired?(
|
|
129
|
+
lock_index_score, lock_index_time
|
|
130
|
+
)
|
|
118
131
|
|
|
119
132
|
lock_names << lock_name
|
|
120
133
|
next if lock_names.size < scan_size
|
|
@@ -130,11 +143,13 @@ module RedisQueuedLocks::Acquirer::Queues
|
|
|
130
143
|
# @param lock_queues [Set<String>]
|
|
131
144
|
# @param scan_size [Integer] The number of queues whose requests are read in one round trip.
|
|
132
145
|
# @return [Set<Hash<Symbol,Any>>]
|
|
133
|
-
# - each request: `{ 'acq_id' => String, 'score' => Float, 'rw_mode' => 'read'/'write'
|
|
146
|
+
# - each request: `{ 'acq_id' => String, 'score' => Float, 'rw_mode' => 'read'/'write',
|
|
147
|
+
# 'priority' => Integer }` (the priority is decoded from the position: see
|
|
148
|
+
# `RedisQueuedLocks::Resource::LOCK_PRIORITY_BAND`);
|
|
134
149
|
#
|
|
135
150
|
# @api private
|
|
136
151
|
# @since 1.0.0
|
|
137
|
-
# @version 1.
|
|
152
|
+
# @version 1.22.0
|
|
138
153
|
def extract_queues_info(redis_client, lock_queues, scan_size)
|
|
139
154
|
# NOTE: the same approach as in `QueueInfo` (duplicated on purpose: public operation modules
|
|
140
155
|
# do not reuse each other, they share logic via `Resource`, `Utilities`);
|
|
@@ -162,7 +177,12 @@ module RedisQueuedLocks::Acquirer::Queues
|
|
|
162
177
|
seeded_queues << {
|
|
163
178
|
queue: lock_queue,
|
|
164
179
|
requests: queues_requests[index].map do |val|
|
|
165
|
-
{
|
|
180
|
+
{
|
|
181
|
+
'acq_id' => val[0],
|
|
182
|
+
'score' => val[1],
|
|
183
|
+
'rw_mode' => rw_mode,
|
|
184
|
+
'priority' => RedisQueuedLocks::Resource.lock_request_priority(val[1])
|
|
185
|
+
}
|
|
166
186
|
end
|
|
167
187
|
}
|
|
168
188
|
end
|