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
|
@@ -4,17 +4,18 @@
|
|
|
4
4
|
# rubocop:disable all
|
|
5
5
|
# @api private
|
|
6
6
|
# @since 1.16.0
|
|
7
|
+
# @version 1.20.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.21.0
|
|
18
19
|
def lock_series_poc( # steep:ignore
|
|
19
20
|
redis,
|
|
20
21
|
lock_names,
|
|
@@ -25,6 +26,8 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
|
|
|
25
26
|
ractor_id:,
|
|
26
27
|
ttl:,
|
|
27
28
|
queue_ttl:,
|
|
29
|
+
lock_index_enabled:,
|
|
30
|
+
lock_index_grace_period:,
|
|
28
31
|
timeout:,
|
|
29
32
|
timed:,
|
|
30
33
|
retry_count:,
|
|
@@ -64,19 +67,22 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
|
|
|
64
67
|
)
|
|
65
68
|
end
|
|
66
69
|
|
|
70
|
+
# NOTE: Symbol keys are written to the lock data as Strings (the same check as in
|
|
71
|
+
# RedisQueuedLocks::Acquirer::AcquireLock);
|
|
67
72
|
if meta.is_a?(::Hash) && (meta.any? do |key, _value|
|
|
68
|
-
key
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
73
|
+
meta_key = key.is_a?(::Symbol) ? key.name : key
|
|
74
|
+
meta_key == 'acq_id' ||
|
|
75
|
+
meta_key == 'hst_id' ||
|
|
76
|
+
meta_key == 'ts' ||
|
|
77
|
+
meta_key == 'ini_ttl' ||
|
|
78
|
+
meta_key == 'lock_key' ||
|
|
79
|
+
meta_key == 'rem_ttl' ||
|
|
80
|
+
meta_key == 'spc_ext_ttl' ||
|
|
81
|
+
meta_key == 'spc_cnt' ||
|
|
82
|
+
meta_key == 'l_spc_ext_ini_ttl' ||
|
|
83
|
+
meta_key == 'l_spc_ext_ts' ||
|
|
84
|
+
meta_key == 'l_spc_ts' ||
|
|
85
|
+
meta_key == 'rw_mode'
|
|
80
86
|
end)
|
|
81
87
|
raise(
|
|
82
88
|
RedisQueuedLocks::ArgumentError,
|
|
@@ -86,6 +92,34 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
|
|
|
86
92
|
)
|
|
87
93
|
end
|
|
88
94
|
|
|
95
|
+
# NOTE: the series ttl is validated before the lock ttl calculation: each lock ttl of
|
|
96
|
+
# the series includes the retry delay and jitter (a non-positive series ttl can produce
|
|
97
|
+
# a positive lock ttl), the series ttl limits the `timed` block (the same check as in
|
|
98
|
+
# RedisQueuedLocks::Acquirer::AcquireLock);
|
|
99
|
+
if !ttl.is_a?(::Integer) || ttl <= 0
|
|
100
|
+
raise(
|
|
101
|
+
RedisQueuedLocks::ArgumentError,
|
|
102
|
+
"`:ttl` argument should be a positive Integer, got #{ttl.inspect}."
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# NOTE: the retry delay and jitter are parts of the lock ttls of the series (validated before
|
|
107
|
+
# the lock ttl calculation; other options are validated by
|
|
108
|
+
# RedisQueuedLocks::Acquirer::AcquireLock on the first lock of the series, before any lock
|
|
109
|
+
# is obtained);
|
|
110
|
+
if !retry_delay.is_a?(::Integer) || retry_delay < 0
|
|
111
|
+
raise(
|
|
112
|
+
RedisQueuedLocks::ArgumentError,
|
|
113
|
+
"`:retry_delay` argument should be a non-negative Integer, got #{retry_delay.inspect}."
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
if !retry_jitter.is_a?(::Integer) || retry_jitter < 0
|
|
117
|
+
raise(
|
|
118
|
+
RedisQueuedLocks::ArgumentError,
|
|
119
|
+
"`:retry_jitter` argument should be a non-negative Integer, got #{retry_jitter.inspect}."
|
|
120
|
+
)
|
|
121
|
+
end
|
|
122
|
+
|
|
89
123
|
locks_count = lock_names.size
|
|
90
124
|
|
|
91
125
|
lock_acquirement_operations = lock_names.each_with_index.map do |lock_name, position|
|
|
@@ -155,6 +189,8 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
|
|
|
155
189
|
ractor_id:,
|
|
156
190
|
ttl: lock_operation_options[:ttl],
|
|
157
191
|
queue_ttl:,
|
|
192
|
+
lock_index_enabled:,
|
|
193
|
+
lock_index_grace_period:,
|
|
158
194
|
timeout:,
|
|
159
195
|
timed:,
|
|
160
196
|
retry_count:,
|
|
@@ -357,7 +393,11 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
|
|
|
357
393
|
end
|
|
358
394
|
|
|
359
395
|
# Releases write locks of the given acquirer (compare-and-delete: the lock can be expired
|
|
360
|
-
# and obtained by another acquirer when the series logic exceeds the lock ttl)
|
|
396
|
+
# and obtained by another acquirer when the series logic exceeds the lock ttl) in one
|
|
397
|
+
# transaction for the whole series (three round trips: WATCH, lock owners, MULTI/EXEC):
|
|
398
|
+
# - an aborted transaction (one of the watched locks is changed) is retried with the locks
|
|
399
|
+
# that were still owned by the acquirer at the last check: the locks of other acquirers
|
|
400
|
+
# are neither deleted nor watched, so the retries converge;
|
|
361
401
|
#
|
|
362
402
|
# @param redis [RedisClient]
|
|
363
403
|
# @param lock_names [Array<String>]
|
|
@@ -366,18 +406,37 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
|
|
|
366
406
|
#
|
|
367
407
|
# @api private
|
|
368
408
|
# @since 1.18.0
|
|
409
|
+
# @version 1.21.0
|
|
369
410
|
def release_write_lock_series(redis, lock_names, acquirer_id) # steep:ignore
|
|
370
411
|
return if lock_names.empty?
|
|
371
412
|
|
|
413
|
+
own_lock_keys = lock_names.map do |lock_name|
|
|
414
|
+
RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
|
|
415
|
+
end
|
|
416
|
+
|
|
372
417
|
redis.with do |conn|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
418
|
+
until own_lock_keys.empty?
|
|
419
|
+
released = conn.multi(watch: own_lock_keys) do |transaction|
|
|
420
|
+
# NOTE: (pipeline layout) one reply (lock owner) per lock in the series order
|
|
421
|
+
lock_owners = conn.pipelined do |pipeline|
|
|
422
|
+
own_lock_keys.each { |lock_key| pipeline.call('HGET', lock_key, 'acq_id') }
|
|
423
|
+
end
|
|
424
|
+
own_lock_keys = own_lock_keys.select.with_index do |_lock_key, index|
|
|
425
|
+
lock_owners[index] == acquirer_id
|
|
378
426
|
end
|
|
427
|
+
own_lock_keys.each { |lock_key| transaction.call('DEL', lock_key) }
|
|
379
428
|
end
|
|
429
|
+
# NOTE: `nil` => the transaction is aborted (a watched lock is changed) => retry
|
|
430
|
+
break unless released.nil?
|
|
380
431
|
end
|
|
432
|
+
rescue RedisQueuedLocks::LockAcquirementIntermediateTimeoutError,
|
|
433
|
+
RedisQueuedLocks::TimedLockIntermediateTimeoutError,
|
|
434
|
+
::Timeout::ExitException
|
|
435
|
+
# NOTE: (timeouts) an interrupted WATCH block (the internal timeouts and the user's
|
|
436
|
+
# `Timeout.timeout` without an exception class): the closed connection drops the WATCH
|
|
437
|
+
# state (redis-client sends `UNWATCH` only on `StandardError`);
|
|
438
|
+
conn.close
|
|
439
|
+
raise
|
|
381
440
|
end
|
|
382
441
|
end
|
|
383
442
|
|
|
@@ -2,27 +2,51 @@
|
|
|
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
|
module RedisQueuedLocks::Acquirer::Locks
|
|
8
|
+
# The number of replies of one lock in the lock state pipeline of a batch of locks
|
|
9
|
+
# (`extract_locks_info`): HGETALL (write lock), PTTL (write lock), ZRANGE (readers registry).
|
|
10
|
+
#
|
|
11
|
+
# @return [Integer]
|
|
12
|
+
#
|
|
13
|
+
# @api private
|
|
14
|
+
# @since 1.20.0
|
|
15
|
+
LOCK_STATE_REPLIES = 3
|
|
16
|
+
|
|
8
17
|
# rubocop:disable Metrics/ClassLength
|
|
9
18
|
class << self
|
|
10
19
|
# @param redis_client [RedisClient]
|
|
11
20
|
# @option scan_size [Integer]
|
|
12
21
|
# @option with_info [Boolean]
|
|
22
|
+
# @option lock_index_enabled [Boolean]
|
|
23
|
+
# Find locks via the lock index (`index_locks`) instead of the keyspace SCAN (`scan_locks`).
|
|
13
24
|
# @return [Set<String>,Set<Hash<Symbol,Any>>]
|
|
14
25
|
#
|
|
15
26
|
# @api private
|
|
16
27
|
# @since 1.0.0
|
|
17
|
-
|
|
28
|
+
# @version 1.21.0
|
|
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
|
+
|
|
18
39
|
redis_client.with do |rconn|
|
|
19
|
-
lock_keys =
|
|
20
|
-
|
|
40
|
+
lock_keys =
|
|
41
|
+
lock_index_enabled ? index_locks(rconn, scan_size) : scan_locks(rconn, scan_size)
|
|
42
|
+
with_info ? extract_locks_info(rconn, lock_keys, scan_size) : lock_keys
|
|
21
43
|
end
|
|
22
44
|
end
|
|
23
45
|
|
|
24
46
|
private
|
|
25
47
|
|
|
48
|
+
# Write locks and read locks in one SCAN pass (the disabled lock index).
|
|
49
|
+
#
|
|
26
50
|
# @param redis_client [RedisClient]
|
|
27
51
|
# @param scan_size [Integer]
|
|
28
52
|
# @return [Set<String>]
|
|
@@ -30,89 +54,201 @@ module RedisQueuedLocks::Acquirer::Locks
|
|
|
30
54
|
#
|
|
31
55
|
# @api private
|
|
32
56
|
# @since 1.0.0
|
|
33
|
-
# @version 1.
|
|
57
|
+
# @version 1.20.0
|
|
34
58
|
def scan_locks(redis_client, scan_size)
|
|
35
59
|
Set.new.tap do |lock_keys|
|
|
60
|
+
# @type var lock_keys: Set[String]
|
|
36
61
|
redis_client.scan(
|
|
37
62
|
'MATCH',
|
|
38
|
-
RedisQueuedLocks::Resource::
|
|
63
|
+
RedisQueuedLocks::Resource::KEY_PATTERN,
|
|
39
64
|
count: scan_size
|
|
40
|
-
) do |
|
|
41
|
-
|
|
42
|
-
|
|
65
|
+
) do |key|
|
|
66
|
+
case RedisQueuedLocks::Resource.lock_key_family(key)
|
|
67
|
+
when :lock
|
|
68
|
+
lock_keys.add(key)
|
|
69
|
+
when :lock_readers
|
|
70
|
+
# NOTE: (RW) read locks (readers registries can contain expired read locks only)
|
|
71
|
+
lock_keys.add(RedisQueuedLocks::Resource.lock_key_from_readers(key))
|
|
72
|
+
end
|
|
43
73
|
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
44
76
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
77
|
+
# Write locks and read locks of the lock index (the enabled lock index): one ZSCAN pass over
|
|
78
|
+
# the lock names (`RedisQueuedLocks::Resource::LOCK_INDEX_KEY`, the keyspace is never scanned)
|
|
79
|
+
# with one round trip per batch of `scan_size` lock names (`EXISTS` of the write lock and of
|
|
80
|
+
# the readers registry of each lock name).
|
|
81
|
+
#
|
|
82
|
+
# @param redis_client [RedisClient]
|
|
83
|
+
# @param scan_size [Integer] ZSCAN count and the number of lock names in one batch.
|
|
84
|
+
# @return [Set<String>]
|
|
85
|
+
# - (RW) read locks are represented by their lock keys (`rql:lock:<lock_name>`) too
|
|
86
|
+
# (readers registries can contain expired read locks only);
|
|
87
|
+
#
|
|
88
|
+
# @api private
|
|
89
|
+
# @since 1.20.0
|
|
90
|
+
def index_locks(redis_client, scan_size)
|
|
91
|
+
Set.new.tap do |lock_keys|
|
|
92
|
+
# @type var lock_keys: Set[String]
|
|
93
|
+
each_lock_name_batch(redis_client, scan_size) do |lock_names|
|
|
94
|
+
# NOTE: (pipeline layout) one reply per lock name in the batch order: the number of
|
|
95
|
+
# the existing keys of the lock (the write lock and the readers registry);
|
|
96
|
+
# @type var lock_keys_count: Array[Integer]
|
|
97
|
+
lock_keys_count = redis_client.pipelined do |pipeline|
|
|
98
|
+
lock_names.each do |lock_name|
|
|
99
|
+
pipeline.call(
|
|
100
|
+
'EXISTS',
|
|
101
|
+
RedisQueuedLocks::Resource.prepare_lock_key(lock_name),
|
|
102
|
+
RedisQueuedLocks::Resource.prepare_lock_readers(lock_name)
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
lock_names.each_with_index do |lock_name, index|
|
|
108
|
+
next if lock_keys_count[index] == 0
|
|
109
|
+
|
|
110
|
+
lock_keys.add(RedisQueuedLocks::Resource.prepare_lock_key(lock_name))
|
|
111
|
+
end
|
|
53
112
|
end
|
|
54
113
|
end
|
|
55
114
|
end
|
|
56
115
|
|
|
116
|
+
# Iterates over the lock names of the lock index (one ZSCAN pass) in batches of `scan_size`
|
|
117
|
+
# lock names (batches are collected across ZSCAN pages). Expired lock names (not dropped from
|
|
118
|
+
# the index yet) are skipped: all their keys are expired.
|
|
119
|
+
#
|
|
120
|
+
# NOTE: each operation module iterates over the lock index by itself (duplicated on purpose:
|
|
121
|
+
# operation modules are independent of each other, see `extract_locks_info`);
|
|
122
|
+
#
|
|
123
|
+
# @param redis_client [RedisClient]
|
|
124
|
+
# @param scan_size [Integer] ZSCAN count and the number of lock names in one batch.
|
|
125
|
+
# @yield [lock_names]
|
|
126
|
+
# @yieldparam lock_names [Array<String>] The batch of lock names (reused between batches).
|
|
127
|
+
# @return [void]
|
|
128
|
+
#
|
|
129
|
+
# @api private
|
|
130
|
+
# @since 1.20.0
|
|
131
|
+
def each_lock_name_batch(redis_client, scan_size)
|
|
132
|
+
lock_index_time = RedisQueuedLocks::Resource.lock_index_time
|
|
133
|
+
# @type var lock_names: Array[String]
|
|
134
|
+
lock_names = []
|
|
135
|
+
|
|
136
|
+
redis_client.zscan(
|
|
137
|
+
RedisQueuedLocks::Resource::LOCK_INDEX_KEY, count: scan_size
|
|
138
|
+
) do |lock_name, lock_index_score|
|
|
139
|
+
next if lock_index_score.to_f < lock_index_time
|
|
140
|
+
|
|
141
|
+
lock_names << lock_name
|
|
142
|
+
next if lock_names.size < scan_size
|
|
143
|
+
|
|
144
|
+
yield(lock_names)
|
|
145
|
+
lock_names.clear
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
yield(lock_names) unless lock_names.empty?
|
|
149
|
+
end
|
|
150
|
+
|
|
57
151
|
# NOTE:
|
|
58
152
|
# - the lock info is extracted by the module itself with the same approach as in `LockInfo`:
|
|
59
153
|
# public operation modules do not reuse each other (they share logic via `Resource`,
|
|
60
154
|
# `Utilities`), so the similar logic is duplicated on purpose;
|
|
155
|
+
# - locks are processed in batches of `scan_size` locks: two round trips per batch
|
|
156
|
+
# (the state of the locks, then the data of the live read locks);
|
|
61
157
|
#
|
|
62
158
|
# @param redis_client [RedisClient]
|
|
63
159
|
# @param lock_keys [Set<String>]
|
|
160
|
+
# @param scan_size [Integer] The number of locks in one batch.
|
|
64
161
|
# @return [Set<Hash<Symbol,Any>>]
|
|
65
162
|
#
|
|
66
163
|
# @api private
|
|
67
164
|
# @since 1.0.0
|
|
68
|
-
# @version 1.
|
|
165
|
+
# @version 1.21.0
|
|
69
166
|
# rubocop:disable Metrics/MethodLength
|
|
70
|
-
def extract_locks_info(redis_client, lock_keys)
|
|
167
|
+
def extract_locks_info(redis_client, lock_keys, scan_size)
|
|
71
168
|
Set.new.tap do |seeded_locks|
|
|
72
169
|
# rubocop:disable Layout/LineLength
|
|
73
170
|
# @type var seeded_locks: Set[{ lock: String, status: :released|:alive, info: Hash[String,untyped] }]
|
|
74
171
|
# rubocop:enable Layout/LineLength
|
|
75
172
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
#
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
173
|
+
lock_keys.each_slice(scan_size) do |lock_keys_batch|
|
|
174
|
+
# Step 0: lock names of the batch (once per lock: the readers registry keys and the read
|
|
175
|
+
# lock data keys are built from them)
|
|
176
|
+
# @type var lock_names_batch: Array[String]
|
|
177
|
+
lock_names_batch = lock_keys_batch.map do |lock_key|
|
|
178
|
+
RedisQueuedLocks::Resource.lock_name_from_key(lock_key, :lock) #: String
|
|
179
|
+
end
|
|
83
180
|
|
|
84
|
-
#
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
181
|
+
# Step 1: extract the state of the batch of locks in one round trip
|
|
182
|
+
# NOTE: (pipeline layout) [0] TIME (read locks are checked in redis time), then
|
|
183
|
+
# `LOCK_STATE_REPLIES` replies per lock in the batch order (the replies of the lock
|
|
184
|
+
# with the index N start at `1 + N * LOCK_STATE_REPLIES`): HGETALL (write lock),
|
|
185
|
+
# PTTL (write lock), ZRANGE WITHSCORES (readers registry);
|
|
186
|
+
# @type var batch_state: Array[untyped]
|
|
187
|
+
batch_state = redis_client.pipelined do |pipeline|
|
|
88
188
|
pipeline.call('TIME')
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
189
|
+
lock_keys_batch.each_with_index do |lock_key, index|
|
|
190
|
+
lock_readers_key =
|
|
191
|
+
RedisQueuedLocks::Resource.prepare_lock_readers(lock_names_batch[index])
|
|
192
|
+
pipeline.call('HGETALL', lock_key)
|
|
193
|
+
pipeline.call('PTTL', lock_key)
|
|
194
|
+
pipeline.call('ZRANGE', lock_readers_key, '0', '-1', 'WITHSCORES')
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
now = RedisQueuedLocks::Resource.redis_time_ms(batch_state[0])
|
|
198
|
+
|
|
199
|
+
# Step 2: (RW) live read locks of the locks without a write lock
|
|
200
|
+
# (`nil` for the locks with a write lock: their read locks are not reported);
|
|
201
|
+
# @type var live_read_locks: Array[Array[[String, Float]]?]
|
|
202
|
+
live_read_locks = lock_keys_batch.each_with_index.map do |_lock_key, index|
|
|
203
|
+
replies_at = 1 + (index * LOCK_STATE_REPLIES)
|
|
204
|
+
# NOTE: `nil` when the write lock is alive (the key exists)
|
|
205
|
+
next unless batch_state[replies_at].empty? || batch_state[replies_at + 1] == -2
|
|
206
|
+
|
|
207
|
+
# @type var read_locks: Array[[String, Float]]
|
|
208
|
+
read_locks = batch_state[replies_at + 2]
|
|
209
|
+
read_locks.select { |(_acquirer_id, expiration)| expiration > now }
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Step 3: (RW) read lock data of the live read locks in one round trip
|
|
213
|
+
# NOTE: (pipeline layout) one reply per live read lock in the lock and reader order;
|
|
214
|
+
# read lock data can be absent if it is dropped during the read lock life;
|
|
215
|
+
# @type var read_locks_data: Array[Hash[String,String]]
|
|
216
|
+
read_locks_data = redis_client.pipelined do |pipeline|
|
|
217
|
+
lock_keys_batch.each_index do |index|
|
|
218
|
+
live_read_locks[index]&.each do |(acquirer_id, _expiration)|
|
|
219
|
+
pipeline.call(
|
|
220
|
+
'HGETALL',
|
|
221
|
+
RedisQueuedLocks::Resource.prepare_read_lock_key(
|
|
222
|
+
lock_names_batch[index], acquirer_id
|
|
223
|
+
)
|
|
224
|
+
)
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Step 4: format the info of each lock and push it to the result store
|
|
230
|
+
read_locks_data_at = 0
|
|
231
|
+
lock_keys_batch.each_with_index do |lock_key, index|
|
|
232
|
+
lock_live_read_locks = live_read_locks[index]
|
|
233
|
+
|
|
234
|
+
# @type var lock_info: RedisQueuedLocks::Acquirer::Locks::lockInfo
|
|
235
|
+
lock_info =
|
|
236
|
+
if lock_live_read_locks
|
|
237
|
+
# Step 4.Y: write lock is released
|
|
109
238
|
# NOTE: (RW) read locks (or nothing if there are no live read locks)
|
|
110
239
|
read_lock_info(
|
|
111
|
-
|
|
240
|
+
lock_key, now, lock_live_read_locks, read_locks_data, read_locks_data_at
|
|
112
241
|
) || {} #: Hash[String,String|Float|Integer]
|
|
113
242
|
else
|
|
114
|
-
# Step
|
|
243
|
+
# Step 4.Z: lock is alive => format received info + add additional rem_ttl info
|
|
244
|
+
replies_at = 1 + (index * LOCK_STATE_REPLIES)
|
|
245
|
+
# @type var hget_cmd_res: Hash[String,String|Float|Integer]
|
|
246
|
+
hget_cmd_res = batch_state[replies_at] # NOTE: HGETALL result (hash)
|
|
247
|
+
# @type var pttl_cmd_res: Integer
|
|
248
|
+
pttl_cmd_res = batch_state[replies_at + 1] # NOTE: PTTL result (integer)
|
|
249
|
+
|
|
115
250
|
hget_cmd_res.tap do |lock_data|
|
|
251
|
+
lock_data['lock_key'] = lock_key
|
|
116
252
|
lock_data['rw_mode'] = 'write'
|
|
117
253
|
lock_data['ts'] = Float(lock_data['ts'])
|
|
118
254
|
lock_data['ini_ttl'] = Integer(lock_data['ini_ttl'])
|
|
@@ -127,30 +263,30 @@ module RedisQueuedLocks::Acquirer::Locks
|
|
|
127
263
|
Float(lock_data['l_spc_ext_ts']) if lock_data['l_spc_ext_ts']
|
|
128
264
|
end
|
|
129
265
|
end
|
|
130
|
-
|
|
131
|
-
end
|
|
266
|
+
read_locks_data_at += lock_live_read_locks.size if lock_live_read_locks
|
|
132
267
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
268
|
+
seeded_locks << {
|
|
269
|
+
lock: lock_key,
|
|
270
|
+
status: (lock_info.empty? ? :released : :alive),
|
|
271
|
+
info: lock_info
|
|
272
|
+
}
|
|
273
|
+
end
|
|
139
274
|
end
|
|
140
275
|
end
|
|
141
276
|
end
|
|
142
277
|
# rubocop:enable Metrics/MethodLength
|
|
143
278
|
|
|
144
|
-
# Formats live read locks of the lock
|
|
279
|
+
# Formats live read locks of the lock.
|
|
145
280
|
#
|
|
146
281
|
# NOTE: the same approach as in `LockInfo` (duplicated on purpose: operation modules are
|
|
147
|
-
# independent of each other, see `extract_locks_info`);
|
|
282
|
+
# independent of each other, see `extract_locks_info`); the read lock data is extracted
|
|
283
|
+
# for the whole batch of locks in advance;
|
|
148
284
|
#
|
|
149
|
-
# @param redis_client [RedisClient]
|
|
150
|
-
# @param lock_name [String]
|
|
151
285
|
# @param lock_key [String]
|
|
152
|
-
# @param
|
|
153
|
-
# @param
|
|
286
|
+
# @param now [Integer] Redis time (in milliseconds).
|
|
287
|
+
# @param live_read_locks [Array<Array<String,Float>>] Live read locks (acquirer id, expiration).
|
|
288
|
+
# @param read_locks_data [Array<Hash<String,String>>] Read lock data of the batch of locks.
|
|
289
|
+
# @param read_locks_data_at [Integer] Position of the first read lock data of the lock.
|
|
154
290
|
# @return [Hash<String,String|Numeric|Array<Hash<String,String|Numeric>>>,NilClass]
|
|
155
291
|
# - `nil` is returned when there are no live read locks;
|
|
156
292
|
# - each reader has the same data as the write lock (acq_id, hst_id, ts, ini_ttl, rem_ttl,
|
|
@@ -158,32 +294,25 @@ module RedisQueuedLocks::Acquirer::Locks
|
|
|
158
294
|
#
|
|
159
295
|
# @api private
|
|
160
296
|
# @since 1.18.0
|
|
297
|
+
# @version 1.20.0
|
|
161
298
|
# rubocop:disable Metrics/MethodLength
|
|
162
|
-
def read_lock_info(
|
|
163
|
-
now = RedisQueuedLocks::Resource.redis_time_ms(redis_time)
|
|
164
|
-
live_read_locks = read_locks.select { |(_acquirer_id, expiration)| expiration > now }
|
|
299
|
+
def read_lock_info(lock_key, now, live_read_locks, read_locks_data, read_locks_data_at)
|
|
165
300
|
return nil if live_read_locks.empty?
|
|
166
301
|
|
|
167
|
-
# NOTE: read lock data (it can be absent if it is dropped during the read lock life)
|
|
168
|
-
# @type var read_locks_data: Array[Hash[String,String]]
|
|
169
|
-
read_locks_data = redis_client.pipelined do |pipeline|
|
|
170
|
-
live_read_locks.each do |(acquirer_id, _expiration)|
|
|
171
|
-
pipeline.call(
|
|
172
|
-
'HGETALL',
|
|
173
|
-
RedisQueuedLocks::Resource.prepare_read_lock_key(lock_name, acquirer_id)
|
|
174
|
-
)
|
|
175
|
-
end
|
|
176
|
-
end
|
|
177
|
-
|
|
178
302
|
longest_rem_ttl = 0
|
|
179
303
|
readers = live_read_locks.each_with_index.map do |(acquirer_id, expiration), index|
|
|
180
304
|
rem_ttl = (expiration - now).ceil
|
|
181
305
|
longest_rem_ttl = rem_ttl if rem_ttl > longest_rem_ttl
|
|
182
306
|
|
|
307
|
+
# NOTE: acquirer and host ids are not stored in the read lock data (both are derived from
|
|
308
|
+
# the acquirer id): they are the first fields like in the write lock info (read lock data
|
|
309
|
+
# of older versions contains them with the same values);
|
|
183
310
|
# @type var reader: Hash[String,String|Float|Integer|nil]
|
|
184
|
-
reader =
|
|
185
|
-
|
|
186
|
-
|
|
311
|
+
reader = {
|
|
312
|
+
'acq_id' => acquirer_id,
|
|
313
|
+
'hst_id' => RedisQueuedLocks::Resource.host_identifier_from_acquirer(acquirer_id)
|
|
314
|
+
}
|
|
315
|
+
read_locks_data[read_locks_data_at + index]&.each { |field, value| reader[field] = value }
|
|
187
316
|
reader['ts'] = Float(reader['ts']) if reader['ts']
|
|
188
317
|
reader['ini_ttl'] = Integer(reader['ini_ttl']) if reader['ini_ttl']
|
|
189
318
|
reader['rem_ttl'] = rem_ttl
|
|
@@ -2,14 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
# @api private
|
|
4
4
|
# @since 1.0.0
|
|
5
|
-
# @version 1.18.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
|
|
9
8
|
# represnts the ordered lock request queue that is ordered by score (Redis sets) and shows
|
|
10
|
-
# lock acquirers and their position in queue.
|
|
11
|
-
# the
|
|
12
|
-
# you can receive the lock queue info with empty queue.
|
|
9
|
+
# lock acquirers and their position in queue. The write lock queue is empty when only
|
|
10
|
+
# the queue of read lock requests exists.
|
|
13
11
|
#
|
|
14
12
|
# @param redis_client [RedisClient]
|
|
15
13
|
# @param lock_name [String]
|
|
@@ -33,28 +31,24 @@ module RedisQueuedLocks::Acquirer::QueueInfo
|
|
|
33
31
|
#
|
|
34
32
|
# @api private
|
|
35
33
|
# @since 1.0.0
|
|
36
|
-
# @version 1.
|
|
37
|
-
# rubocop:disable Metrics/MethodLength
|
|
34
|
+
# @version 1.20.0
|
|
38
35
|
def queue_info(redis_client, lock_name)
|
|
39
36
|
lock_key_queue = RedisQueuedLocks::Resource.prepare_lock_queue(lock_name)
|
|
40
37
|
read_lock_key_queue = RedisQueuedLocks::Resource.prepare_read_lock_queue(lock_name)
|
|
41
38
|
|
|
39
|
+
# NOTE:
|
|
40
|
+
# redis deletes empty sorted sets, so an empty ZRANGE result means that the queue
|
|
41
|
+
# does not exist (no need for EXISTS checks);
|
|
42
42
|
result = redis_client.pipelined do |pipeline|
|
|
43
|
-
pipeline.call('EXISTS', lock_key_queue)
|
|
44
43
|
pipeline.call('ZRANGE', lock_key_queue, '0', '-1', 'WITHSCORES')
|
|
45
|
-
pipeline.call('EXISTS', read_lock_key_queue)
|
|
46
44
|
pipeline.call('ZRANGE', read_lock_key_queue, '0', '-1', 'WITHSCORES')
|
|
47
45
|
end
|
|
48
46
|
|
|
49
|
-
#
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
exists_cmd_res = result[0]
|
|
53
|
-
zrange_cmd_res = result[1]
|
|
54
|
-
read_exists_cmd_res = result[2]
|
|
55
|
-
read_zrange_cmd_res = result[3]
|
|
47
|
+
# @type var result: [Array[[String,Integer|Float]],Array[[String,Integer|Float]]]
|
|
48
|
+
zrange_cmd_res = result[0]
|
|
49
|
+
read_zrange_cmd_res = result[1]
|
|
56
50
|
|
|
57
|
-
if
|
|
51
|
+
if !zrange_cmd_res.empty? || !read_zrange_cmd_res.empty?
|
|
58
52
|
# NOTE: queue existed during the piepline invocation
|
|
59
53
|
# @type var queue_info: RedisQueuedLocks::Acquirer::QueueInfo::queueInfo
|
|
60
54
|
queue_info = {
|
|
@@ -64,7 +58,7 @@ module RedisQueuedLocks::Acquirer::QueueInfo
|
|
|
64
58
|
end
|
|
65
59
|
}
|
|
66
60
|
# NOTE: (RW) the queue of read lock requests
|
|
67
|
-
|
|
61
|
+
unless read_zrange_cmd_res.empty?
|
|
68
62
|
queue_info['read_lock_queue'] = read_lock_key_queue
|
|
69
63
|
queue_info['read_queue'] = read_zrange_cmd_res.map do |val|
|
|
70
64
|
{ 'acq_id' => val[0], 'score' => val[1], 'rw_mode' => 'read' }
|
|
@@ -76,6 +70,5 @@ module RedisQueuedLocks::Acquirer::QueueInfo
|
|
|
76
70
|
nil
|
|
77
71
|
end
|
|
78
72
|
end
|
|
79
|
-
# rubocop:enable Metrics/MethodLength
|
|
80
73
|
end
|
|
81
74
|
end
|