redis_queued_locks 1.18.0 → 1.20.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 +65 -13
- data/.claude/rules/acquirer.md +29 -11
- data/.claude/rules/arguments.md +3 -2
- data/.claude/rules/git.md +3 -2
- data/.claude/rules/logic.md +4 -3
- data/.claude/rules/swarm.md +12 -7
- data/.claude/rules/tests.md +6 -0
- data/.claude/rules/type-checking.md +2 -1
- data/.claude/rules/visitors.md +1 -1
- data/.claude/skills/rql-code-review/SKILL.md +372 -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 +74 -0
- data/CLAUDE.md +4 -2
- data/README.md +338 -24
- data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rb +684 -128
- data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire.rb +3 -1
- data/lib/redis_queued_locks/acquirer/acquire_lock.rb +24 -3
- data/lib/redis_queued_locks/acquirer/clear_dead_requests.rb +145 -18
- data/lib/redis_queued_locks/acquirer/extend_lock_ttl.rb +71 -10
- data/lib/redis_queued_locks/acquirer/keys.rb +142 -7
- data/lib/redis_queued_locks/acquirer/lock_info.rb +10 -4
- data/lib/redis_queued_locks/acquirer/lock_series_poc.rb +26 -7
- data/lib/redis_queued_locks/acquirer/locks.rb +195 -83
- data/lib/redis_queued_locks/acquirer/queue_info.rb +12 -18
- data/lib/redis_queued_locks/acquirer/queues.rb +114 -50
- data/lib/redis_queued_locks/acquirer/reindex_locks.rb +140 -0
- data/lib/redis_queued_locks/acquirer/release_all_locks.rb +147 -58
- data/lib/redis_queued_locks/acquirer/release_lock.rb +52 -19
- data/lib/redis_queued_locks/acquirer/release_locks_of.rb +235 -66
- data/lib/redis_queued_locks/acquirer.rb +1 -0
- data/lib/redis_queued_locks/client.rb +116 -16
- data/lib/redis_queued_locks/config.rb +5 -1
- data/lib/redis_queued_locks/resource.rb +99 -2
- data/lib/redis_queued_locks/swarm/flush_zombies.rb +332 -61
- data/lib/redis_queued_locks/swarm/zombie_info.rb +203 -53
- data/lib/redis_queued_locks/swarm.rb +15 -4
- data/lib/redis_queued_locks/version.rb +2 -2
- 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 +1 -1
- data/sig/redis_queued_locks/acquirer/acquire_lock.rbs +2 -0
- data/sig/redis_queued_locks/acquirer/clear_dead_requests.rbs +10 -1
- data/sig/redis_queued_locks/acquirer/extend_lock_ttl.rbs +5 -2
- 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 +31 -0
- data/sig/redis_queued_locks/acquirer/release_all_locks.rbs +8 -1
- data/sig/redis_queued_locks/acquirer/release_lock.rbs +3 -1
- data/sig/redis_queued_locks/acquirer/release_locks_of.rbs +20 -1
- data/sig/redis_queued_locks/client.rbs +9 -0
- data/sig/redis_queued_locks/resource.rbs +6 -0
- 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 +1 -0
- metadata +6 -1
|
@@ -14,7 +14,7 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
|
|
|
14
14
|
class << self
|
|
15
15
|
# @api private
|
|
16
16
|
# @since 1.16.0
|
|
17
|
-
# @version 1.
|
|
17
|
+
# @version 1.20.0
|
|
18
18
|
def lock_series_poc( # steep:ignore
|
|
19
19
|
redis,
|
|
20
20
|
lock_names,
|
|
@@ -25,6 +25,8 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
|
|
|
25
25
|
ractor_id:,
|
|
26
26
|
ttl:,
|
|
27
27
|
queue_ttl:,
|
|
28
|
+
lock_index_enabled:,
|
|
29
|
+
lock_index_grace_period:,
|
|
28
30
|
timeout:,
|
|
29
31
|
timed:,
|
|
30
32
|
retry_count:,
|
|
@@ -155,6 +157,8 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
|
|
|
155
157
|
ractor_id:,
|
|
156
158
|
ttl: lock_operation_options[:ttl],
|
|
157
159
|
queue_ttl:,
|
|
160
|
+
lock_index_enabled:,
|
|
161
|
+
lock_index_grace_period:,
|
|
158
162
|
timeout:,
|
|
159
163
|
timed:,
|
|
160
164
|
retry_count:,
|
|
@@ -357,7 +361,11 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
|
|
|
357
361
|
end
|
|
358
362
|
|
|
359
363
|
# 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)
|
|
364
|
+
# and obtained by another acquirer when the series logic exceeds the lock ttl) in one
|
|
365
|
+
# transaction for the whole series (three round trips: WATCH, lock owners, MULTI/EXEC):
|
|
366
|
+
# - an aborted transaction (one of the watched locks is changed) is retried with the locks
|
|
367
|
+
# that were still owned by the acquirer at the last check: the locks of other acquirers
|
|
368
|
+
# are neither deleted nor watched, so the retries converge;
|
|
361
369
|
#
|
|
362
370
|
# @param redis [RedisClient]
|
|
363
371
|
# @param lock_names [Array<String>]
|
|
@@ -366,17 +374,28 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
|
|
|
366
374
|
#
|
|
367
375
|
# @api private
|
|
368
376
|
# @since 1.18.0
|
|
377
|
+
# @version 1.20.0
|
|
369
378
|
def release_write_lock_series(redis, lock_names, acquirer_id) # steep:ignore
|
|
370
379
|
return if lock_names.empty?
|
|
371
380
|
|
|
381
|
+
own_lock_keys = lock_names.map do |lock_name|
|
|
382
|
+
RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
|
|
383
|
+
end
|
|
384
|
+
|
|
372
385
|
redis.with do |conn|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
386
|
+
until own_lock_keys.empty?
|
|
387
|
+
released = conn.multi(watch: own_lock_keys) do |transaction|
|
|
388
|
+
# NOTE: (pipeline layout) one reply (lock owner) per lock in the series order
|
|
389
|
+
lock_owners = conn.pipelined do |pipeline|
|
|
390
|
+
own_lock_keys.each { |lock_key| pipeline.call('HGET', lock_key, 'acq_id') }
|
|
391
|
+
end
|
|
392
|
+
own_lock_keys = own_lock_keys.select.with_index do |_lock_key, index|
|
|
393
|
+
lock_owners[index] == acquirer_id
|
|
378
394
|
end
|
|
395
|
+
own_lock_keys.each { |lock_key| transaction.call('DEL', lock_key) }
|
|
379
396
|
end
|
|
397
|
+
# NOTE: `nil` => the transaction is aborted (a watched lock is changed) => retry
|
|
398
|
+
break unless released.nil?
|
|
380
399
|
end
|
|
381
400
|
end
|
|
382
401
|
end
|
|
@@ -2,27 +2,42 @@
|
|
|
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.20.0
|
|
29
|
+
def locks(redis_client, scan_size:, with_info:, lock_index_enabled:)
|
|
18
30
|
redis_client.with do |rconn|
|
|
19
|
-
lock_keys =
|
|
20
|
-
|
|
31
|
+
lock_keys =
|
|
32
|
+
lock_index_enabled ? index_locks(rconn, scan_size) : scan_locks(rconn, scan_size)
|
|
33
|
+
with_info ? extract_locks_info(rconn, lock_keys, scan_size) : lock_keys
|
|
21
34
|
end
|
|
22
35
|
end
|
|
23
36
|
|
|
24
37
|
private
|
|
25
38
|
|
|
39
|
+
# Write locks and read locks in one SCAN pass (the disabled lock index).
|
|
40
|
+
#
|
|
26
41
|
# @param redis_client [RedisClient]
|
|
27
42
|
# @param scan_size [Integer]
|
|
28
43
|
# @return [Set<String>]
|
|
@@ -30,88 +45,192 @@ module RedisQueuedLocks::Acquirer::Locks
|
|
|
30
45
|
#
|
|
31
46
|
# @api private
|
|
32
47
|
# @since 1.0.0
|
|
33
|
-
# @version 1.
|
|
48
|
+
# @version 1.20.0
|
|
34
49
|
def scan_locks(redis_client, scan_size)
|
|
35
50
|
Set.new.tap do |lock_keys|
|
|
51
|
+
# @type var lock_keys: Set[String]
|
|
36
52
|
redis_client.scan(
|
|
37
53
|
'MATCH',
|
|
38
|
-
RedisQueuedLocks::Resource::
|
|
54
|
+
RedisQueuedLocks::Resource::KEY_PATTERN,
|
|
39
55
|
count: scan_size
|
|
40
|
-
) do |
|
|
41
|
-
|
|
42
|
-
|
|
56
|
+
) do |key|
|
|
57
|
+
case RedisQueuedLocks::Resource.lock_key_family(key)
|
|
58
|
+
when :lock
|
|
59
|
+
lock_keys.add(key)
|
|
60
|
+
when :lock_readers
|
|
61
|
+
# NOTE: (RW) read locks (readers registries can contain expired read locks only)
|
|
62
|
+
lock_keys.add(RedisQueuedLocks::Resource.lock_key_from_readers(key))
|
|
63
|
+
end
|
|
43
64
|
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
44
67
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
68
|
+
# Write locks and read locks of the lock index (the enabled lock index): one ZSCAN pass over
|
|
69
|
+
# the lock names (`RedisQueuedLocks::Resource::LOCK_INDEX_KEY`, the keyspace is never scanned)
|
|
70
|
+
# with one round trip per batch of `scan_size` lock names (`EXISTS` of the write lock and of
|
|
71
|
+
# the readers registry of each lock name).
|
|
72
|
+
#
|
|
73
|
+
# @param redis_client [RedisClient]
|
|
74
|
+
# @param scan_size [Integer] ZSCAN count and the number of lock names in one batch.
|
|
75
|
+
# @return [Set<String>]
|
|
76
|
+
# - (RW) read locks are represented by their lock keys (`rql:lock:<lock_name>`) too
|
|
77
|
+
# (readers registries can contain expired read locks only);
|
|
78
|
+
#
|
|
79
|
+
# @api private
|
|
80
|
+
# @since 1.20.0
|
|
81
|
+
def index_locks(redis_client, scan_size)
|
|
82
|
+
Set.new.tap do |lock_keys|
|
|
83
|
+
# @type var lock_keys: Set[String]
|
|
84
|
+
each_lock_name_batch(redis_client, scan_size) do |lock_names|
|
|
85
|
+
# NOTE: (pipeline layout) one reply per lock name in the batch order: the number of
|
|
86
|
+
# the existing keys of the lock (the write lock and the readers registry);
|
|
87
|
+
# @type var lock_keys_count: Array[Integer]
|
|
88
|
+
lock_keys_count = redis_client.pipelined do |pipeline|
|
|
89
|
+
lock_names.each do |lock_name|
|
|
90
|
+
pipeline.call(
|
|
91
|
+
'EXISTS',
|
|
92
|
+
RedisQueuedLocks::Resource.prepare_lock_key(lock_name),
|
|
93
|
+
RedisQueuedLocks::Resource.prepare_lock_readers(lock_name)
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
lock_names.each_with_index do |lock_name, index|
|
|
99
|
+
next if lock_keys_count[index] == 0
|
|
100
|
+
|
|
101
|
+
lock_keys.add(RedisQueuedLocks::Resource.prepare_lock_key(lock_name))
|
|
102
|
+
end
|
|
53
103
|
end
|
|
54
104
|
end
|
|
55
105
|
end
|
|
56
106
|
|
|
107
|
+
# Iterates over the lock names of the lock index (one ZSCAN pass) in batches of `scan_size`
|
|
108
|
+
# 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.
|
|
110
|
+
#
|
|
111
|
+
# NOTE: each operation module iterates over the lock index by itself (duplicated on purpose:
|
|
112
|
+
# operation modules are independent of each other, see `extract_locks_info`);
|
|
113
|
+
#
|
|
114
|
+
# @param redis_client [RedisClient]
|
|
115
|
+
# @param scan_size [Integer] ZSCAN count and the number of lock names in one batch.
|
|
116
|
+
# @yield [lock_names]
|
|
117
|
+
# @yieldparam lock_names [Array<String>] The batch of lock names (reused between batches).
|
|
118
|
+
# @return [void]
|
|
119
|
+
#
|
|
120
|
+
# @api private
|
|
121
|
+
# @since 1.20.0
|
|
122
|
+
def each_lock_name_batch(redis_client, scan_size)
|
|
123
|
+
lock_index_time = RedisQueuedLocks::Resource.lock_index_time
|
|
124
|
+
# @type var lock_names: Array[String]
|
|
125
|
+
lock_names = []
|
|
126
|
+
|
|
127
|
+
redis_client.zscan(
|
|
128
|
+
RedisQueuedLocks::Resource::LOCK_INDEX_KEY, count: scan_size
|
|
129
|
+
) do |lock_name, lock_index_score|
|
|
130
|
+
next if lock_index_score.to_f < lock_index_time
|
|
131
|
+
|
|
132
|
+
lock_names << lock_name
|
|
133
|
+
next if lock_names.size < scan_size
|
|
134
|
+
|
|
135
|
+
yield(lock_names)
|
|
136
|
+
lock_names.clear
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
yield(lock_names) unless lock_names.empty?
|
|
140
|
+
end
|
|
141
|
+
|
|
57
142
|
# NOTE:
|
|
58
143
|
# - the lock info is extracted by the module itself with the same approach as in `LockInfo`:
|
|
59
144
|
# public operation modules do not reuse each other (they share logic via `Resource`,
|
|
60
145
|
# `Utilities`), so the similar logic is duplicated on purpose;
|
|
146
|
+
# - locks are processed in batches of `scan_size` locks: two round trips per batch
|
|
147
|
+
# (the state of the locks, then the data of the live read locks);
|
|
61
148
|
#
|
|
62
149
|
# @param redis_client [RedisClient]
|
|
63
150
|
# @param lock_keys [Set<String>]
|
|
151
|
+
# @param scan_size [Integer] The number of locks in one batch.
|
|
64
152
|
# @return [Set<Hash<Symbol,Any>>]
|
|
65
153
|
#
|
|
66
154
|
# @api private
|
|
67
155
|
# @since 1.0.0
|
|
68
|
-
# @version 1.
|
|
156
|
+
# @version 1.20.0
|
|
69
157
|
# rubocop:disable Metrics/MethodLength
|
|
70
|
-
def extract_locks_info(redis_client, lock_keys)
|
|
158
|
+
def extract_locks_info(redis_client, lock_keys, scan_size)
|
|
71
159
|
Set.new.tap do |seeded_locks|
|
|
72
160
|
# rubocop:disable Layout/LineLength
|
|
73
161
|
# @type var seeded_locks: Set[{ lock: String, status: :released|:alive, info: Hash[String,untyped] }]
|
|
74
162
|
# rubocop:enable Layout/LineLength
|
|
75
163
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
#
|
|
164
|
+
lock_keys.each_slice(scan_size) do |lock_keys_batch|
|
|
165
|
+
# Step 1: extract the state of the batch of locks in one round trip
|
|
166
|
+
# NOTE: (pipeline layout) [0] TIME (read locks are checked in redis time), then
|
|
167
|
+
# `LOCK_STATE_REPLIES` replies per lock in the batch order (the replies of the lock
|
|
168
|
+
# with the index N start at `1 + N * LOCK_STATE_REPLIES`): HGETALL (write lock),
|
|
169
|
+
# PTTL (write lock), ZRANGE WITHSCORES (readers registry);
|
|
170
|
+
# @type var batch_state: Array[untyped]
|
|
171
|
+
batch_state = redis_client.pipelined do |pipeline|
|
|
172
|
+
pipeline.call('TIME')
|
|
173
|
+
lock_keys_batch.each do |lock_key|
|
|
174
|
+
lock_readers_key = RedisQueuedLocks::Resource.prepare_lock_readers(
|
|
175
|
+
lock_key.delete_prefix('rql:lock:')
|
|
176
|
+
)
|
|
177
|
+
pipeline.call('HGETALL', lock_key)
|
|
178
|
+
pipeline.call('PTTL', lock_key)
|
|
179
|
+
pipeline.call('ZRANGE', lock_readers_key, '0', '-1', 'WITHSCORES')
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
now = RedisQueuedLocks::Resource.redis_time_ms(batch_state[0])
|
|
79
183
|
|
|
80
|
-
#
|
|
81
|
-
|
|
82
|
-
|
|
184
|
+
# Step 2: (RW) live read locks of the locks without a write lock
|
|
185
|
+
# (`nil` for the locks with a write lock: their read locks are not reported);
|
|
186
|
+
# @type var live_read_locks: Array[Array[[String, Float]]?]
|
|
187
|
+
live_read_locks = lock_keys_batch.each_with_index.map do |_lock_key, index|
|
|
188
|
+
replies_at = 1 + (index * LOCK_STATE_REPLIES)
|
|
189
|
+
# NOTE: `nil` when the write lock is alive (the key exists)
|
|
190
|
+
next unless batch_state[replies_at].empty? || batch_state[replies_at + 1] == -2
|
|
83
191
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
192
|
+
# @type var read_locks: Array[[String, Float]]
|
|
193
|
+
read_locks = batch_state[replies_at + 2]
|
|
194
|
+
read_locks.select { |(_acquirer_id, expiration)| expiration > now }
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Step 3: (RW) read lock data of the live read locks in one round trip
|
|
198
|
+
# NOTE: (pipeline layout) one reply per live read lock in the lock and reader order;
|
|
199
|
+
# read lock data can be absent if it is dropped during the read lock life;
|
|
200
|
+
# @type var read_locks_data: Array[Hash[String,String]]
|
|
201
|
+
read_locks_data = redis_client.pipelined do |pipeline|
|
|
202
|
+
lock_keys_batch.each_with_index do |lock_key, index|
|
|
203
|
+
lock_name = lock_key.delete_prefix('rql:lock:')
|
|
204
|
+
live_read_locks[index]&.each do |(acquirer_id, _expiration)|
|
|
205
|
+
pipeline.call(
|
|
206
|
+
'HGETALL',
|
|
207
|
+
RedisQueuedLocks::Resource.prepare_read_lock_key(lock_name, acquirer_id)
|
|
208
|
+
)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Step 4: format the info of each lock and push it to the result store
|
|
214
|
+
read_locks_data_at = 0
|
|
215
|
+
lock_keys_batch.each_with_index do |lock_key, index|
|
|
216
|
+
lock_live_read_locks = live_read_locks[index]
|
|
217
|
+
|
|
218
|
+
# @type var lock_info: RedisQueuedLocks::Acquirer::Locks::lockInfo
|
|
219
|
+
lock_info =
|
|
220
|
+
if lock_live_read_locks
|
|
221
|
+
# Step 4.Y: write lock is released
|
|
109
222
|
# NOTE: (RW) read locks (or nothing if there are no live read locks)
|
|
110
223
|
read_lock_info(
|
|
111
|
-
|
|
224
|
+
lock_key, now, lock_live_read_locks, read_locks_data, read_locks_data_at
|
|
112
225
|
) || {} #: Hash[String,String|Float|Integer]
|
|
113
226
|
else
|
|
114
|
-
# Step
|
|
227
|
+
# Step 4.Z: lock is alive => format received info + add additional rem_ttl info
|
|
228
|
+
replies_at = 1 + (index * LOCK_STATE_REPLIES)
|
|
229
|
+
# @type var hget_cmd_res: Hash[String,String|Float|Integer]
|
|
230
|
+
hget_cmd_res = batch_state[replies_at] # NOTE: HGETALL result (hash)
|
|
231
|
+
# @type var pttl_cmd_res: Integer
|
|
232
|
+
pttl_cmd_res = batch_state[replies_at + 1] # NOTE: PTTL result (integer)
|
|
233
|
+
|
|
115
234
|
hget_cmd_res.tap do |lock_data|
|
|
116
235
|
lock_data['rw_mode'] = 'write'
|
|
117
236
|
lock_data['ts'] = Float(lock_data['ts'])
|
|
@@ -127,30 +246,30 @@ module RedisQueuedLocks::Acquirer::Locks
|
|
|
127
246
|
Float(lock_data['l_spc_ext_ts']) if lock_data['l_spc_ext_ts']
|
|
128
247
|
end
|
|
129
248
|
end
|
|
130
|
-
|
|
131
|
-
end
|
|
249
|
+
read_locks_data_at += lock_live_read_locks.size if lock_live_read_locks
|
|
132
250
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
251
|
+
seeded_locks << {
|
|
252
|
+
lock: lock_key,
|
|
253
|
+
status: (lock_info.empty? ? :released : :alive),
|
|
254
|
+
info: lock_info
|
|
255
|
+
}
|
|
256
|
+
end
|
|
139
257
|
end
|
|
140
258
|
end
|
|
141
259
|
end
|
|
142
260
|
# rubocop:enable Metrics/MethodLength
|
|
143
261
|
|
|
144
|
-
# Formats live read locks of the lock
|
|
262
|
+
# Formats live read locks of the lock.
|
|
145
263
|
#
|
|
146
264
|
# NOTE: the same approach as in `LockInfo` (duplicated on purpose: operation modules are
|
|
147
|
-
# independent of each other, see `extract_locks_info`);
|
|
265
|
+
# independent of each other, see `extract_locks_info`); the read lock data is extracted
|
|
266
|
+
# for the whole batch of locks in advance;
|
|
148
267
|
#
|
|
149
|
-
# @param redis_client [RedisClient]
|
|
150
|
-
# @param lock_name [String]
|
|
151
268
|
# @param lock_key [String]
|
|
152
|
-
# @param
|
|
153
|
-
# @param
|
|
269
|
+
# @param now [Integer] Redis time (in milliseconds).
|
|
270
|
+
# @param live_read_locks [Array<Array<String,Float>>] Live read locks (acquirer id, expiration).
|
|
271
|
+
# @param read_locks_data [Array<Hash<String,String>>] Read lock data of the batch of locks.
|
|
272
|
+
# @param read_locks_data_at [Integer] Position of the first read lock data of the lock.
|
|
154
273
|
# @return [Hash<String,String|Numeric|Array<Hash<String,String|Numeric>>>,NilClass]
|
|
155
274
|
# - `nil` is returned when there are no live read locks;
|
|
156
275
|
# - each reader has the same data as the write lock (acq_id, hst_id, ts, ini_ttl, rem_ttl,
|
|
@@ -158,32 +277,25 @@ module RedisQueuedLocks::Acquirer::Locks
|
|
|
158
277
|
#
|
|
159
278
|
# @api private
|
|
160
279
|
# @since 1.18.0
|
|
280
|
+
# @version 1.20.0
|
|
161
281
|
# 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 }
|
|
282
|
+
def read_lock_info(lock_key, now, live_read_locks, read_locks_data, read_locks_data_at)
|
|
165
283
|
return nil if live_read_locks.empty?
|
|
166
284
|
|
|
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
285
|
longest_rem_ttl = 0
|
|
179
286
|
readers = live_read_locks.each_with_index.map do |(acquirer_id, expiration), index|
|
|
180
287
|
rem_ttl = (expiration - now).ceil
|
|
181
288
|
longest_rem_ttl = rem_ttl if rem_ttl > longest_rem_ttl
|
|
182
289
|
|
|
290
|
+
# NOTE: acquirer and host ids are not stored in the read lock data (both are derived from
|
|
291
|
+
# the acquirer id): they are the first fields like in the write lock info (read lock data
|
|
292
|
+
# of older versions contains them with the same values);
|
|
183
293
|
# @type var reader: Hash[String,String|Float|Integer|nil]
|
|
184
|
-
reader =
|
|
185
|
-
|
|
186
|
-
|
|
294
|
+
reader = {
|
|
295
|
+
'acq_id' => acquirer_id,
|
|
296
|
+
'hst_id' => RedisQueuedLocks::Resource.host_identifier_from_acquirer(acquirer_id)
|
|
297
|
+
}
|
|
298
|
+
read_locks_data[read_locks_data_at + index]&.each { |field, value| reader[field] = value }
|
|
187
299
|
reader['ts'] = Float(reader['ts']) if reader['ts']
|
|
188
300
|
reader['ini_ttl'] = Integer(reader['ini_ttl']) if reader['ini_ttl']
|
|
189
301
|
reader['rem_ttl'] = rem_ttl
|
|
@@ -2,14 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
# @api private
|
|
4
4
|
# @since 1.0.0
|
|
5
|
-
# @version 1.
|
|
5
|
+
# @version 1.20.0
|
|
6
6
|
module RedisQueuedLocks::Acquirer::QueueInfo
|
|
7
7
|
class << self
|
|
8
8
|
# Returns an information about the required lock queue by the lock name. The result
|
|
9
9
|
# 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.
|
|
10
|
+
# lock acquirers and their position in queue. The write lock queue is empty when only
|
|
11
|
+
# the queue of read lock requests exists.
|
|
13
12
|
#
|
|
14
13
|
# @param redis_client [RedisClient]
|
|
15
14
|
# @param lock_name [String]
|
|
@@ -33,28 +32,24 @@ module RedisQueuedLocks::Acquirer::QueueInfo
|
|
|
33
32
|
#
|
|
34
33
|
# @api private
|
|
35
34
|
# @since 1.0.0
|
|
36
|
-
# @version 1.
|
|
37
|
-
# rubocop:disable Metrics/MethodLength
|
|
35
|
+
# @version 1.20.0
|
|
38
36
|
def queue_info(redis_client, lock_name)
|
|
39
37
|
lock_key_queue = RedisQueuedLocks::Resource.prepare_lock_queue(lock_name)
|
|
40
38
|
read_lock_key_queue = RedisQueuedLocks::Resource.prepare_read_lock_queue(lock_name)
|
|
41
39
|
|
|
40
|
+
# NOTE:
|
|
41
|
+
# redis deletes empty sorted sets, so an empty ZRANGE result means that the queue
|
|
42
|
+
# does not exist (no need for EXISTS checks);
|
|
42
43
|
result = redis_client.pipelined do |pipeline|
|
|
43
|
-
pipeline.call('EXISTS', lock_key_queue)
|
|
44
44
|
pipeline.call('ZRANGE', lock_key_queue, '0', '-1', 'WITHSCORES')
|
|
45
|
-
pipeline.call('EXISTS', read_lock_key_queue)
|
|
46
45
|
pipeline.call('ZRANGE', read_lock_key_queue, '0', '-1', 'WITHSCORES')
|
|
47
46
|
end
|
|
48
47
|
|
|
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]
|
|
48
|
+
# @type var result: [Array[[String,Integer|Float]],Array[[String,Integer|Float]]]
|
|
49
|
+
zrange_cmd_res = result[0]
|
|
50
|
+
read_zrange_cmd_res = result[1]
|
|
56
51
|
|
|
57
|
-
if
|
|
52
|
+
if !zrange_cmd_res.empty? || !read_zrange_cmd_res.empty?
|
|
58
53
|
# NOTE: queue existed during the piepline invocation
|
|
59
54
|
# @type var queue_info: RedisQueuedLocks::Acquirer::QueueInfo::queueInfo
|
|
60
55
|
queue_info = {
|
|
@@ -64,7 +59,7 @@ module RedisQueuedLocks::Acquirer::QueueInfo
|
|
|
64
59
|
end
|
|
65
60
|
}
|
|
66
61
|
# NOTE: (RW) the queue of read lock requests
|
|
67
|
-
|
|
62
|
+
unless read_zrange_cmd_res.empty?
|
|
68
63
|
queue_info['read_lock_queue'] = read_lock_key_queue
|
|
69
64
|
queue_info['read_queue'] = read_zrange_cmd_res.map do |val|
|
|
70
65
|
{ 'acq_id' => val[0], 'score' => val[1], 'rw_mode' => 'read' }
|
|
@@ -76,6 +71,5 @@ module RedisQueuedLocks::Acquirer::QueueInfo
|
|
|
76
71
|
nil
|
|
77
72
|
end
|
|
78
73
|
end
|
|
79
|
-
# rubocop:enable Metrics/MethodLength
|
|
80
74
|
end
|
|
81
75
|
end
|