redis_queued_locks 1.19.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 +62 -11
- 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 +68 -0
- data/CLAUDE.md +4 -2
- data/README.md +257 -22
- 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 +97 -11
- 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 +8 -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
|
@@ -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;
|
|
@@ -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
|
|
@@ -46,6 +46,16 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
46
46
|
# Lock's time to live (in milliseconds). Nil means "without timeout".
|
|
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
60
|
# Time period whe should try to acquire the lock (in seconds).
|
|
51
61
|
# @option timed [Boolean]
|
|
@@ -166,7 +176,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
166
176
|
#
|
|
167
177
|
# @api private
|
|
168
178
|
# @since 1.0.0
|
|
169
|
-
# @version 1.
|
|
179
|
+
# @version 1.20.0
|
|
170
180
|
def acquire_lock(
|
|
171
181
|
redis,
|
|
172
182
|
lock_name,
|
|
@@ -176,6 +186,8 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
176
186
|
ractor_id:,
|
|
177
187
|
ttl:,
|
|
178
188
|
queue_ttl:,
|
|
189
|
+
lock_index_enabled:,
|
|
190
|
+
lock_index_grace_period:,
|
|
179
191
|
timeout:,
|
|
180
192
|
timed:,
|
|
181
193
|
retry_count:,
|
|
@@ -276,7 +288,12 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
276
288
|
# NOTE: (RW) write lock requests use the classic lock queue (lock_key_queue)
|
|
277
289
|
read_lock_key_queue = RedisQueuedLocks::Resource.prepare_read_lock_queue(lock_name)
|
|
278
290
|
lock_readers_key = RedisQueuedLocks::Resource.prepare_lock_readers(lock_name)
|
|
279
|
-
|
|
291
|
+
# NOTE: (RW) read lock data key is required by read lock requests only (write lock requests
|
|
292
|
+
# never obtain read locks: read-to-write lock upgrade is not supported);
|
|
293
|
+
read_lock_key =
|
|
294
|
+
if read_write_mode == :read
|
|
295
|
+
RedisQueuedLocks::Resource.prepare_read_lock_key(lock_name, acquirer_id)
|
|
296
|
+
end
|
|
280
297
|
|
|
281
298
|
acquirer_position = RedisQueuedLocks::Resource.calc_initial_acquirer_position
|
|
282
299
|
|
|
@@ -371,15 +388,19 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
371
388
|
read_lock_key_queue,
|
|
372
389
|
lock_readers_key,
|
|
373
390
|
read_lock_key,
|
|
391
|
+
lock_name,
|
|
374
392
|
acquirer_id,
|
|
375
393
|
host_id,
|
|
376
394
|
acquirer_position,
|
|
377
395
|
lock_ttl,
|
|
378
396
|
queue_ttl,
|
|
397
|
+
lock_index_enabled,
|
|
398
|
+
lock_index_grace_period,
|
|
379
399
|
fail_fast,
|
|
380
400
|
conflict_strategy,
|
|
381
401
|
access_strategy,
|
|
382
402
|
meta,
|
|
403
|
+
acq_process[:tries] > 0, # NOTE: retry attempt (see "pre-check" in `try_to_lock`)
|
|
383
404
|
log_sampled,
|
|
384
405
|
instr_sampled
|
|
385
406
|
) => { ok:, result: }
|
|
@@ -2,11 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
# @api private
|
|
4
4
|
# @since 1.0.0
|
|
5
|
+
# @version 1.20.0
|
|
5
6
|
module RedisQueuedLocks::Acquirer::ClearDeadRequests
|
|
6
7
|
class << self
|
|
8
|
+
# Drops dead lock requests (requests older than `dead_ttl`) from the lock queues (write and
|
|
9
|
+
# read):
|
|
10
|
+
# - the disabled lock index: the lock queues found in one SCAN pass, one round trip per batch
|
|
11
|
+
# of `scan_size` lock queues (batches are collected across SCAN pages);
|
|
12
|
+
# - the enabled lock index: the lock queues of the locks of the lock index: one ZSCAN pass
|
|
13
|
+
# over the lock names (the keyspace is never scanned), one round trip per batch of
|
|
14
|
+
# `scan_size` lock names (both lock queues of each lock name); expired lock names are
|
|
15
|
+
# dropped from the lock index first;
|
|
16
|
+
#
|
|
7
17
|
# @param redis_client [RedisClient]
|
|
8
18
|
# @param scan_size [Integer]
|
|
19
|
+
# SCAN count and the number of lock queues in one batch (the disabled lock index) or ZSCAN
|
|
20
|
+
# count and the number of lock names in one batch (the enabled lock index).
|
|
9
21
|
# @param dead_ttl [Integer] In milliseconds
|
|
22
|
+
# @param lock_index_enabled [Boolean]
|
|
23
|
+
# Find lock queues via the lock index instead of the keyspace SCAN.
|
|
10
24
|
# @param logger [::Logger,#debug]
|
|
11
25
|
# @param instrumenter [#notify]
|
|
12
26
|
# @param instrument [NilClass,Any]
|
|
@@ -22,11 +36,12 @@ module RedisQueuedLocks::Acquirer::ClearDeadRequests
|
|
|
22
36
|
#
|
|
23
37
|
# @api private
|
|
24
38
|
# @since 1.0.0
|
|
25
|
-
# @version 1.
|
|
39
|
+
# @version 1.20.0
|
|
26
40
|
def clear_dead_requests(
|
|
27
41
|
redis_client,
|
|
28
42
|
scan_size,
|
|
29
43
|
dead_ttl,
|
|
44
|
+
lock_index_enabled,
|
|
30
45
|
logger,
|
|
31
46
|
instrumenter,
|
|
32
47
|
instrument,
|
|
@@ -45,9 +60,35 @@ module RedisQueuedLocks::Acquirer::ClearDeadRequests
|
|
|
45
60
|
result = Set.new.tap do |processed_queues|
|
|
46
61
|
# @type var processed_queues: Set[String]
|
|
47
62
|
redis_client.with do |rconn|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
63
|
+
if lock_index_enabled
|
|
64
|
+
# Step 1 (lock index): drop the expired lock names from the lock index
|
|
65
|
+
# (all their keys are expired);
|
|
66
|
+
rconn.call(
|
|
67
|
+
'ZREMRANGEBYSCORE', RedisQueuedLocks::Resource::LOCK_INDEX_KEY,
|
|
68
|
+
'-inf', RedisQueuedLocks::Resource.lock_index_time - 1
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Step 2 (lock index): drop dead requests from the lock queues of the lock index
|
|
72
|
+
each_lock_name_batch(rconn, scan_size) do |lock_names|
|
|
73
|
+
drop_dead_requests_of_locks(rconn, lock_names, dead_score, processed_queues)
|
|
74
|
+
end
|
|
75
|
+
else
|
|
76
|
+
# Step 1 (keyspace): drop dead requests from the lock queues found in one SCAN pass
|
|
77
|
+
# NOTE: dead requests are dropped with one pipeline per batch of `scan_size` queues
|
|
78
|
+
# (batches are collected across SCAN pages: lock queues can be rare among other keys);
|
|
79
|
+
# @type var lock_queues: Array[String]
|
|
80
|
+
lock_queues = []
|
|
81
|
+
|
|
82
|
+
each_lock_queue(rconn, scan_size) do |lock_queue|
|
|
83
|
+
processed_queues << lock_queue
|
|
84
|
+
lock_queues << lock_queue
|
|
85
|
+
next if lock_queues.size < scan_size
|
|
86
|
+
|
|
87
|
+
drop_dead_requests(rconn, lock_queues, dead_score)
|
|
88
|
+
lock_queues.clear
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
drop_dead_requests(rconn, lock_queues, dead_score) unless lock_queues.empty?
|
|
51
92
|
end
|
|
52
93
|
end
|
|
53
94
|
end
|
|
@@ -57,30 +98,116 @@ module RedisQueuedLocks::Acquirer::ClearDeadRequests
|
|
|
57
98
|
|
|
58
99
|
private
|
|
59
100
|
|
|
101
|
+
# Write and read lock queues in one SCAN pass (the disabled lock index).
|
|
102
|
+
#
|
|
60
103
|
# @param redis_client [RedisClient]
|
|
61
104
|
# @param scan_size [Integer]
|
|
62
105
|
# @yield [lock_queue]
|
|
63
106
|
# @yieldparam lock_queue [String]
|
|
64
107
|
# @yieldreturn [void]
|
|
65
|
-
# @return [
|
|
108
|
+
# @return [void]
|
|
66
109
|
#
|
|
67
110
|
# @api private
|
|
68
111
|
# @since 1.0.0
|
|
69
|
-
# @version 1.
|
|
70
|
-
def each_lock_queue(redis_client, scan_size
|
|
112
|
+
# @version 1.20.0
|
|
113
|
+
def each_lock_queue(redis_client, scan_size)
|
|
71
114
|
redis_client.scan(
|
|
72
115
|
'MATCH',
|
|
73
|
-
RedisQueuedLocks::Resource::
|
|
74
|
-
count: scan_size
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
116
|
+
RedisQueuedLocks::Resource::ALL_LOCK_QUEUES_PATTERN,
|
|
117
|
+
count: scan_size
|
|
118
|
+
) do |key|
|
|
119
|
+
# NOTE: the pattern matches other keys too (lock names with `queue:`): lock queues only
|
|
120
|
+
family = RedisQueuedLocks::Resource.lock_key_family(key)
|
|
121
|
+
yield(key) if family == :lock_queue || family == :read_lock_queue
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Drops dead requests of the given lock queues in one round trip (the disabled lock index).
|
|
126
|
+
#
|
|
127
|
+
# @param redis_client [RedisClient]
|
|
128
|
+
# @param lock_queues [Array<String>]
|
|
129
|
+
# @param dead_score [Float]
|
|
130
|
+
# @return [void]
|
|
131
|
+
#
|
|
132
|
+
# @api private
|
|
133
|
+
# @since 1.20.0
|
|
134
|
+
def drop_dead_requests(redis_client, lock_queues, dead_score)
|
|
135
|
+
redis_client.pipelined do |pipeline|
|
|
136
|
+
lock_queues.each do |lock_queue|
|
|
137
|
+
pipeline.call('ZREMRANGEBYSCORE', lock_queue, '-inf', dead_score)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Drops dead requests of both lock queues of the given lock names in one round trip
|
|
143
|
+
# (the enabled lock index).
|
|
144
|
+
#
|
|
145
|
+
# @param redis_client [RedisClient]
|
|
146
|
+
# @param lock_names [Array<String>]
|
|
147
|
+
# @param dead_score [Float]
|
|
148
|
+
# @param processed_queues [Set<String>] Collects the existing (processed) lock queues.
|
|
149
|
+
# @return [void]
|
|
150
|
+
#
|
|
151
|
+
# @api private
|
|
152
|
+
# @since 1.20.0
|
|
153
|
+
def drop_dead_requests_of_locks(redis_client, lock_names, dead_score, processed_queues)
|
|
154
|
+
# NOTE: (pipeline layout) two replies per lock queue (two lock queues per lock name) in
|
|
155
|
+
# the batch order: EXISTS (the queue is processed when it exists), ZREMRANGEBYSCORE;
|
|
156
|
+
# @type var replies: Array[Integer]
|
|
157
|
+
replies = redis_client.pipelined do |pipeline|
|
|
158
|
+
lock_names.each do |lock_name|
|
|
159
|
+
lock_queue = RedisQueuedLocks::Resource.prepare_lock_queue(lock_name)
|
|
160
|
+
read_lock_queue = RedisQueuedLocks::Resource.prepare_read_lock_queue(lock_name)
|
|
161
|
+
pipeline.call('EXISTS', lock_queue)
|
|
162
|
+
pipeline.call('ZREMRANGEBYSCORE', lock_queue, '-inf', dead_score)
|
|
163
|
+
pipeline.call('EXISTS', read_lock_queue)
|
|
164
|
+
pipeline.call('ZREMRANGEBYSCORE', read_lock_queue, '-inf', dead_score)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
lock_names.each_with_index do |lock_name, index|
|
|
169
|
+
if replies[index * 4] == 1
|
|
170
|
+
processed_queues << RedisQueuedLocks::Resource.prepare_lock_queue(lock_name)
|
|
171
|
+
end
|
|
172
|
+
if replies[(index * 4) + 2] == 1
|
|
173
|
+
processed_queues << RedisQueuedLocks::Resource.prepare_read_lock_queue(lock_name)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Iterates over the lock names of the lock index (one ZSCAN pass) in batches of `scan_size`
|
|
179
|
+
# lock names (batches are collected across ZSCAN pages). Expired lock names (not dropped from
|
|
180
|
+
# the index yet) are skipped: all their keys are expired.
|
|
181
|
+
#
|
|
182
|
+
# NOTE: each operation module iterates over the lock index by itself (duplicated on purpose:
|
|
183
|
+
# operation modules are independent of each other, see `Locks.each_lock_name_batch`);
|
|
184
|
+
#
|
|
185
|
+
# @param redis_client [RedisClient]
|
|
186
|
+
# @param scan_size [Integer] ZSCAN count and the number of lock names in one batch.
|
|
187
|
+
# @yield [lock_names]
|
|
188
|
+
# @yieldparam lock_names [Array<String>] The batch of lock names (reused between batches).
|
|
189
|
+
# @return [void]
|
|
190
|
+
#
|
|
191
|
+
# @api private
|
|
192
|
+
# @since 1.20.0
|
|
193
|
+
def each_lock_name_batch(redis_client, scan_size)
|
|
194
|
+
lock_index_time = RedisQueuedLocks::Resource.lock_index_time
|
|
195
|
+
# @type var lock_names: Array[String]
|
|
196
|
+
lock_names = []
|
|
197
|
+
|
|
198
|
+
redis_client.zscan(
|
|
199
|
+
RedisQueuedLocks::Resource::LOCK_INDEX_KEY, count: scan_size
|
|
200
|
+
) do |lock_name, lock_index_score|
|
|
201
|
+
next if lock_index_score.to_f < lock_index_time
|
|
202
|
+
|
|
203
|
+
lock_names << lock_name
|
|
204
|
+
next if lock_names.size < scan_size
|
|
205
|
+
|
|
206
|
+
yield(lock_names)
|
|
207
|
+
lock_names.clear
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
yield(lock_names) unless lock_names.empty?
|
|
84
211
|
end
|
|
85
212
|
end
|
|
86
213
|
end
|
|
@@ -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
|
module RedisQueuedLocks::Acquirer::ExtendLockTTL
|
|
8
8
|
# @return [String]
|
|
@@ -28,6 +28,8 @@ module RedisQueuedLocks::Acquirer::ExtendLockTTL
|
|
|
28
28
|
# (used for `:read` mode only, ignored for `:write` mode).
|
|
29
29
|
# @param acquirer_id [String]
|
|
30
30
|
# The read lock acquirer (used for `:read` mode without `all_read_locks` only).
|
|
31
|
+
# @param lock_index_enabled [Boolean]
|
|
32
|
+
# Increase the lock index score of the lock name by the extension (the enabled lock index).
|
|
31
33
|
# @param logger [::Logger,#debug]
|
|
32
34
|
# @param instrumenter [#notify]
|
|
33
35
|
# @param instrument [NilClass,Any]
|
|
@@ -43,7 +45,7 @@ module RedisQueuedLocks::Acquirer::ExtendLockTTL
|
|
|
43
45
|
#
|
|
44
46
|
# @api private
|
|
45
47
|
# @since 1.0.0
|
|
46
|
-
# @version 1.
|
|
48
|
+
# @version 1.20.0
|
|
47
49
|
# rubocop:disable Metrics/MethodLength
|
|
48
50
|
def extend_lock_ttl(
|
|
49
51
|
redis_client,
|
|
@@ -52,6 +54,7 @@ module RedisQueuedLocks::Acquirer::ExtendLockTTL
|
|
|
52
54
|
read_write_mode,
|
|
53
55
|
all_read_locks,
|
|
54
56
|
acquirer_id,
|
|
57
|
+
lock_index_enabled,
|
|
55
58
|
logger,
|
|
56
59
|
instrumenter,
|
|
57
60
|
instrument,
|
|
@@ -84,19 +87,43 @@ module RedisQueuedLocks::Acquirer::ExtendLockTTL
|
|
|
84
87
|
|
|
85
88
|
# NOTE: (RW) all live read locks of the lock
|
|
86
89
|
if all_read_locks
|
|
87
|
-
return extend_all_read_locks_ttl(
|
|
90
|
+
return extend_all_read_locks_ttl(
|
|
91
|
+
redis_client, lock_name, lock_key, milliseconds, lock_index_enabled
|
|
92
|
+
)
|
|
88
93
|
end
|
|
89
94
|
|
|
90
95
|
# NOTE: (RW) read lock of the concrete acquirer
|
|
91
|
-
return extend_read_lock_ttl(
|
|
96
|
+
return extend_read_lock_ttl(
|
|
97
|
+
redis_client, lock_name, lock_key, acquirer_id, milliseconds, lock_index_enabled
|
|
98
|
+
)
|
|
92
99
|
end
|
|
93
100
|
|
|
94
|
-
# NOTE:
|
|
95
|
-
|
|
101
|
+
# NOTE:
|
|
102
|
+
# - EVAL signature -> <lua script>, (number of keys), *(keys), *(arguments);
|
|
103
|
+
# - (lock index) the enabled lock index: the lock index score is increased by the extension
|
|
104
|
+
# in the same transaction (`ZADD XX INCR`: the upper bound of the lock lifetime stays
|
|
105
|
+
# the upper bound, locks without the index entry (of older RQL versions) are not indexed
|
|
106
|
+
# with a wrong score); a failed extension (no lock) increases the score of the lock name
|
|
107
|
+
# too: the name just stays in the index a bit longer;
|
|
108
|
+
# - the disabled lock index: one EVAL command (no transaction);
|
|
109
|
+
# @type var extended: Integer
|
|
110
|
+
extended =
|
|
111
|
+
if lock_index_enabled
|
|
112
|
+
# @type var result: [Integer, Float?]
|
|
113
|
+
result = redis_client.multi do |transact|
|
|
114
|
+
transact.call('EVAL', EXTEND_LOCK_PTTL, 1, lock_key, milliseconds)
|
|
115
|
+
transact.call(
|
|
116
|
+
'ZADD', RedisQueuedLocks::Resource::LOCK_INDEX_KEY,
|
|
117
|
+
'XX', 'INCR', milliseconds, lock_name
|
|
118
|
+
)
|
|
119
|
+
end
|
|
120
|
+
result[0]
|
|
121
|
+
else
|
|
122
|
+
redis_client.call('EVAL', EXTEND_LOCK_PTTL, 1, lock_key, milliseconds)
|
|
123
|
+
end
|
|
96
124
|
# TODO: upload scripts to the redis
|
|
97
125
|
|
|
98
|
-
|
|
99
|
-
if result == 1
|
|
126
|
+
if extended == 1
|
|
100
127
|
{ ok: true, result: { extended_locks_count: 1 } }
|
|
101
128
|
else
|
|
102
129
|
{ ok: false, result: :async_expire_or_no_lock }
|
|
@@ -117,12 +144,21 @@ module RedisQueuedLocks::Acquirer::ExtendLockTTL
|
|
|
117
144
|
# @param lock_key [String]
|
|
118
145
|
# @param acquirer_id [String]
|
|
119
146
|
# @param milliseconds [Integer]
|
|
147
|
+
# @param lock_index_enabled [Boolean]
|
|
120
148
|
# @return [Hash<Symbol,Boolean|Symbol|Hash<Symbol,Integer>>]
|
|
121
149
|
#
|
|
122
150
|
# @api private
|
|
123
151
|
# @since 1.18.0
|
|
152
|
+
# @version 1.20.0
|
|
124
153
|
# rubocop:disable Metrics/MethodLength
|
|
125
|
-
def extend_read_lock_ttl(
|
|
154
|
+
def extend_read_lock_ttl(
|
|
155
|
+
redis_client,
|
|
156
|
+
lock_name,
|
|
157
|
+
lock_key,
|
|
158
|
+
acquirer_id,
|
|
159
|
+
milliseconds,
|
|
160
|
+
lock_index_enabled
|
|
161
|
+
)
|
|
126
162
|
lock_readers_key = RedisQueuedLocks::Resource.prepare_lock_readers(lock_name)
|
|
127
163
|
read_lock_key = RedisQueuedLocks::Resource.prepare_read_lock_key(lock_name, acquirer_id)
|
|
128
164
|
|
|
@@ -145,6 +181,14 @@ module RedisQueuedLocks::Acquirer::ExtendLockTTL
|
|
|
145
181
|
transact.call('PEXPIRE', lock_readers_key, extended_ttl, 'GT')
|
|
146
182
|
transact.call('PEXPIRE', read_lock_key, extended_ttl, 'NX')
|
|
147
183
|
transact.call('PEXPIRE', read_lock_key, extended_ttl, 'GT')
|
|
184
|
+
# NOTE: (lock index) the lock index score is increased by the extension
|
|
185
|
+
# (the enabled lock index only; the last command: its reply is not read);
|
|
186
|
+
if lock_index_enabled
|
|
187
|
+
transact.call(
|
|
188
|
+
'ZADD', RedisQueuedLocks::Resource::LOCK_INDEX_KEY,
|
|
189
|
+
'XX', 'INCR', milliseconds, lock_name
|
|
190
|
+
)
|
|
191
|
+
end
|
|
148
192
|
end
|
|
149
193
|
end
|
|
150
194
|
end
|
|
@@ -174,12 +218,20 @@ module RedisQueuedLocks::Acquirer::ExtendLockTTL
|
|
|
174
218
|
# @param lock_name [String]
|
|
175
219
|
# @param lock_key [String]
|
|
176
220
|
# @param milliseconds [Integer]
|
|
221
|
+
# @param lock_index_enabled [Boolean]
|
|
177
222
|
# @return [Hash<Symbol,Boolean|Symbol|Hash<Symbol,Integer>>]
|
|
178
223
|
#
|
|
179
224
|
# @api private
|
|
180
225
|
# @since 1.18.0
|
|
226
|
+
# @version 1.20.0
|
|
181
227
|
# rubocop:disable Metrics/MethodLength
|
|
182
|
-
def extend_all_read_locks_ttl(
|
|
228
|
+
def extend_all_read_locks_ttl(
|
|
229
|
+
redis_client,
|
|
230
|
+
lock_name,
|
|
231
|
+
lock_key,
|
|
232
|
+
milliseconds,
|
|
233
|
+
lock_index_enabled
|
|
234
|
+
)
|
|
183
235
|
lock_readers_key = RedisQueuedLocks::Resource.prepare_lock_readers(lock_name)
|
|
184
236
|
extended_read_locks_count = 0
|
|
185
237
|
|
|
@@ -214,6 +266,15 @@ module RedisQueuedLocks::Acquirer::ExtendLockTTL
|
|
|
214
266
|
if extended_read_locks_count > 0
|
|
215
267
|
transact.call('PEXPIRE', lock_readers_key, registry_ttl, 'NX')
|
|
216
268
|
transact.call('PEXPIRE', lock_readers_key, registry_ttl, 'GT')
|
|
269
|
+
# NOTE: (lock index) the lock index score is increased by the extension
|
|
270
|
+
# (each read lock is extended by the same milliseconds; the enabled lock index
|
|
271
|
+
# only; the last command: its reply is not read);
|
|
272
|
+
if lock_index_enabled
|
|
273
|
+
transact.call(
|
|
274
|
+
'ZADD', RedisQueuedLocks::Resource::LOCK_INDEX_KEY,
|
|
275
|
+
'XX', 'INCR', milliseconds, lock_name
|
|
276
|
+
)
|
|
277
|
+
end
|
|
217
278
|
end
|
|
218
279
|
end
|
|
219
280
|
end
|
|
@@ -2,25 +2,160 @@
|
|
|
2
2
|
|
|
3
3
|
# @api private
|
|
4
4
|
# @since 1.0.0
|
|
5
|
+
# @version 1.20.0
|
|
5
6
|
module RedisQueuedLocks::Acquirer::Keys
|
|
7
|
+
# The number of replies of one lock name in the key state pipeline of a batch of lock names
|
|
8
|
+
# (`collect_lock_keys`): EXISTS (write lock), EXISTS (write lock queue), EXISTS (read lock
|
|
9
|
+
# queue), ZRANGE (readers registry).
|
|
10
|
+
#
|
|
11
|
+
# @return [Integer]
|
|
12
|
+
#
|
|
13
|
+
# @api private
|
|
14
|
+
# @since 1.20.0
|
|
15
|
+
KEY_STATE_REPLIES = 4
|
|
16
|
+
|
|
6
17
|
class << self
|
|
18
|
+
# RQL keys of the locks:
|
|
19
|
+
# - the disabled lock index: all the RQL keys (`RedisQueuedLocks::Resource::KEY_PATTERN`,
|
|
20
|
+
# `rql:lock*`) found in one SCAN pass over the keyspace;
|
|
21
|
+
# - the enabled lock index: all the existing keys of the locks of the lock index (write locks,
|
|
22
|
+
# lock queues, readers registries, read lock queues, read lock data): one ZSCAN pass over
|
|
23
|
+
# the lock names (`RedisQueuedLocks::Resource::LOCK_INDEX_KEY`, the keyspace is never
|
|
24
|
+
# scanned), lock keys are derived from the lock names;
|
|
25
|
+
#
|
|
7
26
|
# @param redis_client [RedisClient]
|
|
8
27
|
# @option scan_size [Integer]
|
|
28
|
+
# SCAN count (the disabled lock index) or ZSCAN count and the number of lock names in one
|
|
29
|
+
# batch (the enabled lock index).
|
|
30
|
+
# @option lock_index_enabled [Boolean]
|
|
9
31
|
# @return [Set<String>]
|
|
10
32
|
#
|
|
11
33
|
# @api private
|
|
12
34
|
# @since 1.0.0
|
|
13
|
-
|
|
35
|
+
# @version 1.20.0
|
|
36
|
+
def keys(redis_client, scan_size:, lock_index_enabled:)
|
|
14
37
|
Set.new.tap do |keyset|
|
|
15
38
|
# @type var keyset: Set[String]
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
39
|
+
if lock_index_enabled
|
|
40
|
+
redis_client.with do |rconn|
|
|
41
|
+
each_lock_name_batch(rconn, scan_size) do |lock_names|
|
|
42
|
+
collect_lock_keys(rconn, lock_names, keyset)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
else
|
|
46
|
+
redis_client.scan(
|
|
47
|
+
'MATCH',
|
|
48
|
+
RedisQueuedLocks::Resource::KEY_PATTERN,
|
|
49
|
+
count: scan_size
|
|
50
|
+
) do |key|
|
|
51
|
+
keyset.add(key)
|
|
52
|
+
end
|
|
22
53
|
end
|
|
23
54
|
end
|
|
24
55
|
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
# (lock index) Adds the existing keys of the given batch of lock names to the key set
|
|
60
|
+
# (one round trip for the lock keys, one more for the read lock data when read locks exist).
|
|
61
|
+
#
|
|
62
|
+
# @param redis_client [RedisClient]
|
|
63
|
+
# @param lock_names [Array<String>]
|
|
64
|
+
# @param keyset [Set<String>]
|
|
65
|
+
# @return [void]
|
|
66
|
+
#
|
|
67
|
+
# @api private
|
|
68
|
+
# @since 1.20.0
|
|
69
|
+
# rubocop:disable Metrics/MethodLength
|
|
70
|
+
def collect_lock_keys(redis_client, lock_names, keyset)
|
|
71
|
+
# Step 1: the state of the lock keys of the batch
|
|
72
|
+
# NOTE: (pipeline layout) `KEY_STATE_REPLIES` replies per lock name in the batch order
|
|
73
|
+
# (the replies of the lock name with the index N start at `N * KEY_STATE_REPLIES`):
|
|
74
|
+
# EXISTS (write lock), EXISTS (write lock queue), EXISTS (read lock queue),
|
|
75
|
+
# ZRANGE (readers registry: redis deletes empty sorted sets, so an empty result means
|
|
76
|
+
# the registry does not exist);
|
|
77
|
+
# @type var key_state: Array[untyped]
|
|
78
|
+
key_state = redis_client.pipelined do |pipeline|
|
|
79
|
+
lock_names.each do |lock_name|
|
|
80
|
+
pipeline.call('EXISTS', RedisQueuedLocks::Resource.prepare_lock_key(lock_name))
|
|
81
|
+
pipeline.call('EXISTS', RedisQueuedLocks::Resource.prepare_lock_queue(lock_name))
|
|
82
|
+
pipeline.call('EXISTS', RedisQueuedLocks::Resource.prepare_read_lock_queue(lock_name))
|
|
83
|
+
pipeline.call(
|
|
84
|
+
'ZRANGE', RedisQueuedLocks::Resource.prepare_lock_readers(lock_name), '0', '-1'
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# @type var read_lock_keys: Array[String]
|
|
90
|
+
read_lock_keys = []
|
|
91
|
+
lock_names.each_with_index do |lock_name, index|
|
|
92
|
+
replies_at = index * KEY_STATE_REPLIES
|
|
93
|
+
if key_state[replies_at] == 1
|
|
94
|
+
keyset.add(RedisQueuedLocks::Resource.prepare_lock_key(lock_name))
|
|
95
|
+
end
|
|
96
|
+
if key_state[replies_at + 1] == 1
|
|
97
|
+
keyset.add(RedisQueuedLocks::Resource.prepare_lock_queue(lock_name))
|
|
98
|
+
end
|
|
99
|
+
if key_state[replies_at + 2] == 1
|
|
100
|
+
keyset.add(RedisQueuedLocks::Resource.prepare_read_lock_queue(lock_name))
|
|
101
|
+
end
|
|
102
|
+
# @type var read_lock_acquirers: Array[String]
|
|
103
|
+
read_lock_acquirers = key_state[replies_at + 3]
|
|
104
|
+
next if read_lock_acquirers.empty?
|
|
105
|
+
|
|
106
|
+
keyset.add(RedisQueuedLocks::Resource.prepare_lock_readers(lock_name))
|
|
107
|
+
read_lock_acquirers.each do |acquirer_id|
|
|
108
|
+
read_lock_keys << RedisQueuedLocks::Resource.prepare_read_lock_key(lock_name, acquirer_id)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
return if read_lock_keys.empty?
|
|
112
|
+
|
|
113
|
+
# Step 2: (RW) read lock data of the readers registries (it can be absent: the data of an
|
|
114
|
+
# expired read lock is expired before the registry cleanup)
|
|
115
|
+
# NOTE: (pipeline layout) one EXISTS reply per read lock data key
|
|
116
|
+
# @type var read_lock_keys_existence: Array[Integer]
|
|
117
|
+
read_lock_keys_existence = redis_client.pipelined do |pipeline|
|
|
118
|
+
read_lock_keys.each { |read_lock_key| pipeline.call('EXISTS', read_lock_key) }
|
|
119
|
+
end
|
|
120
|
+
read_lock_keys.each_with_index do |read_lock_key, index|
|
|
121
|
+
keyset.add(read_lock_key) if read_lock_keys_existence[index] == 1
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
# rubocop:enable Metrics/MethodLength
|
|
125
|
+
|
|
126
|
+
# Iterates over the lock names of the lock index (one ZSCAN pass) in batches of `scan_size`
|
|
127
|
+
# lock names (batches are collected across ZSCAN pages). Expired lock names (not dropped from
|
|
128
|
+
# the index yet) are skipped: all their keys are expired.
|
|
129
|
+
#
|
|
130
|
+
# NOTE: each operation module iterates over the lock index by itself (duplicated on purpose:
|
|
131
|
+
# operation modules are independent of each other, see `Locks.each_lock_name_batch`);
|
|
132
|
+
#
|
|
133
|
+
# @param redis_client [RedisClient]
|
|
134
|
+
# @param scan_size [Integer] ZSCAN count and the number of lock names in one batch.
|
|
135
|
+
# @yield [lock_names]
|
|
136
|
+
# @yieldparam lock_names [Array<String>] The batch of lock names (reused between batches).
|
|
137
|
+
# @return [void]
|
|
138
|
+
#
|
|
139
|
+
# @api private
|
|
140
|
+
# @since 1.20.0
|
|
141
|
+
def each_lock_name_batch(redis_client, scan_size)
|
|
142
|
+
lock_index_time = RedisQueuedLocks::Resource.lock_index_time
|
|
143
|
+
# @type var lock_names: Array[String]
|
|
144
|
+
lock_names = []
|
|
145
|
+
|
|
146
|
+
redis_client.zscan(
|
|
147
|
+
RedisQueuedLocks::Resource::LOCK_INDEX_KEY, count: scan_size
|
|
148
|
+
) do |lock_name, lock_index_score|
|
|
149
|
+
next if lock_index_score.to_f < lock_index_time
|
|
150
|
+
|
|
151
|
+
lock_names << lock_name
|
|
152
|
+
next if lock_names.size < scan_size
|
|
153
|
+
|
|
154
|
+
yield(lock_names)
|
|
155
|
+
lock_names.clear
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
yield(lock_names) unless lock_names.empty?
|
|
159
|
+
end
|
|
25
160
|
end
|
|
26
161
|
end
|
|
@@ -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
|
module RedisQueuedLocks::Acquirer::LockInfo
|
|
7
7
|
class << self
|
|
8
8
|
# @param redis_client [RedisClient]
|
|
@@ -117,6 +117,7 @@ module RedisQueuedLocks::Acquirer::LockInfo
|
|
|
117
117
|
#
|
|
118
118
|
# @api private
|
|
119
119
|
# @since 1.18.0
|
|
120
|
+
# @version 1.20.0
|
|
120
121
|
# rubocop:disable Metrics/MethodLength
|
|
121
122
|
def read_lock_info(redis_client, lock_name, lock_key, redis_time, read_locks)
|
|
122
123
|
now = RedisQueuedLocks::Resource.redis_time_ms(redis_time)
|
|
@@ -139,10 +140,15 @@ module RedisQueuedLocks::Acquirer::LockInfo
|
|
|
139
140
|
rem_ttl = (expiration - now).ceil
|
|
140
141
|
longest_rem_ttl = rem_ttl if rem_ttl > longest_rem_ttl
|
|
141
142
|
|
|
143
|
+
# NOTE: acquirer and host ids are not stored in the read lock data (both are derived from
|
|
144
|
+
# the acquirer id): they are the first fields like in the write lock info (read lock data
|
|
145
|
+
# of older versions contains them with the same values);
|
|
142
146
|
# @type var reader: Hash[String,String|Float|Integer|nil]
|
|
143
|
-
reader =
|
|
144
|
-
|
|
145
|
-
|
|
147
|
+
reader = {
|
|
148
|
+
'acq_id' => acquirer_id,
|
|
149
|
+
'hst_id' => RedisQueuedLocks::Resource.host_identifier_from_acquirer(acquirer_id)
|
|
150
|
+
}
|
|
151
|
+
read_locks_data[index]&.each { |field, value| reader[field] = value }
|
|
146
152
|
reader['ts'] = Float(reader['ts']) if reader['ts']
|
|
147
153
|
reader['ini_ttl'] = Integer(reader['ini_ttl']) if reader['ini_ttl']
|
|
148
154
|
reader['rem_ttl'] = rem_ttl
|