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
|
@@ -2,20 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
# @api private
|
|
4
4
|
# @since 1.0.0
|
|
5
|
+
# @version 1.20.0
|
|
6
|
+
# rubocop:disable Metrics/ModuleLength
|
|
5
7
|
module RedisQueuedLocks::Acquirer::ReleaseAllLocks
|
|
6
8
|
# @since 1.0.0
|
|
7
9
|
extend RedisQueuedLocks::Utilities
|
|
8
10
|
|
|
11
|
+
# rubocop:disable Metrics/ClassLength
|
|
9
12
|
class << self
|
|
10
|
-
# Release all locks:
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
13
|
+
# Release all locks: delete the keys of all lock families (write locks, lock queues,
|
|
14
|
+
# readers registries, read lock queues and read lock data):
|
|
15
|
+
# - the disabled lock index: the keys found in one SCAN pass, with one `DEL` per batch of
|
|
16
|
+
# keys;
|
|
17
|
+
# - the enabled lock index: the keys of the locks of the lock index (one ZSCAN pass), their
|
|
18
|
+
# lock names are dropped from the lock index, with one transaction (`DEL` + `ZREM`) per
|
|
19
|
+
# batch of lock names;
|
|
14
20
|
#
|
|
15
21
|
# @param redis [RedisClient]
|
|
16
22
|
# Redis connection client.
|
|
17
23
|
# @param batch_size [Integer]
|
|
18
|
-
# The number of lock keys
|
|
24
|
+
# The number of lock keys (the disabled lock index) or locks (lock names, the enabled lock
|
|
25
|
+
# index) that should be released in a time.
|
|
26
|
+
# @param lock_index_enabled [Boolean]
|
|
27
|
+
# Find locks via the lock index instead of the keyspace SCAN.
|
|
19
28
|
# @param logger [::Logger,#debug]
|
|
20
29
|
# - Logger object used from `configuration` layer (see config['logger']);
|
|
21
30
|
# - See RedisQueuedLocks::Logging::VoidLogger for example;
|
|
@@ -30,7 +39,7 @@ module RedisQueuedLocks::Acquirer::ReleaseAllLocks
|
|
|
30
39
|
# - works in tandem with <config['log_sampling_percent']> and <config['log_sampler']>;
|
|
31
40
|
# @param log_sampling_percent [Integer]
|
|
32
41
|
# - the percent of cases that should be logged;
|
|
33
|
-
# - take an effect when <config['
|
|
42
|
+
# - take an effect when <config['log_sampling_enabled']> is true;
|
|
34
43
|
# - works in tandem with <config['log_sampling_enabled']> and <config['log_sampler']> configs;
|
|
35
44
|
# @param log_sampler [#sampling_happened?,Module<RedisQueuedLocks::Logging::Sampler>]
|
|
36
45
|
# - percent-based log sampler that decides should be RQL case logged or not;
|
|
@@ -51,7 +60,7 @@ module RedisQueuedLocks::Acquirer::ReleaseAllLocks
|
|
|
51
60
|
# - works in tandem with <config['instr_sampling_percent']> and <config['instr_sampler']>;
|
|
52
61
|
# @param instr_sampling_percent [Integer]
|
|
53
62
|
# - the percent of cases that should be instrumented;
|
|
54
|
-
# - take an effect when <config['
|
|
63
|
+
# - take an effect when <config['instr_sampling_enabled']> is true;
|
|
55
64
|
# - works in tandem with <config['instr_sampling_enabled']>
|
|
56
65
|
# and <config['instr_sampler']> configs;
|
|
57
66
|
# @param instr_sampler [#sampling_happened?,Module<RedisQueuedLocks::Instrument::Sampler>]
|
|
@@ -72,10 +81,12 @@ module RedisQueuedLocks::Acquirer::ReleaseAllLocks
|
|
|
72
81
|
#
|
|
73
82
|
# @api private
|
|
74
83
|
# @since 1.0.0
|
|
75
|
-
# @version 1.
|
|
84
|
+
# @version 1.21.0
|
|
85
|
+
# rubocop:disable Metrics/MethodLength
|
|
76
86
|
def release_all_locks(
|
|
77
87
|
redis,
|
|
78
88
|
batch_size,
|
|
89
|
+
lock_index_enabled,
|
|
79
90
|
logger,
|
|
80
91
|
instrumenter,
|
|
81
92
|
instrument,
|
|
@@ -88,8 +99,19 @@ module RedisQueuedLocks::Acquirer::ReleaseAllLocks
|
|
|
88
99
|
instr_sampler,
|
|
89
100
|
instr_sample_this
|
|
90
101
|
)
|
|
102
|
+
# NOTE: (arguments) the same ranges as the config keys: zero/negative sizes fail SCAN/ZSCAN
|
|
103
|
+
# (`COUNT 0`) and the batching (`each_slice(0)`);
|
|
104
|
+
if !batch_size.is_a?(::Integer) || batch_size <= 0
|
|
105
|
+
raise(
|
|
106
|
+
RedisQueuedLocks::ArgumentError,
|
|
107
|
+
"`:batch_size` argument should be a positive Integer, got #{batch_size.inspect}."
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
|
|
91
111
|
rel_start_time = clock_gettime
|
|
92
|
-
|
|
112
|
+
# steep:ignore:start
|
|
113
|
+
fully_release_all_locks(redis, batch_size, lock_index_enabled) => { ok:, result: }
|
|
114
|
+
# steep:ignore:end
|
|
93
115
|
|
|
94
116
|
# @type var ok: bool
|
|
95
117
|
# @type var result: Hash[Symbol,Integer]
|
|
@@ -98,6 +120,12 @@ module RedisQueuedLocks::Acquirer::ReleaseAllLocks
|
|
|
98
120
|
rel_end_time = clock_gettime
|
|
99
121
|
rel_time = ((rel_end_time - rel_start_time) / 1_000.0).ceil(2)
|
|
100
122
|
|
|
123
|
+
log_sampled = RedisQueuedLocks::Logging.should_log?(
|
|
124
|
+
log_sampling_enabled,
|
|
125
|
+
log_sample_this,
|
|
126
|
+
log_sampling_percent,
|
|
127
|
+
log_sampler
|
|
128
|
+
)
|
|
101
129
|
instr_sampled = RedisQueuedLocks::Instrument.should_instrument?(
|
|
102
130
|
instr_sampling_enabled,
|
|
103
131
|
instr_sample_this,
|
|
@@ -105,11 +133,21 @@ module RedisQueuedLocks::Acquirer::ReleaseAllLocks
|
|
|
105
133
|
instr_sampler
|
|
106
134
|
)
|
|
107
135
|
|
|
136
|
+
run_non_critical do
|
|
137
|
+
logger.debug do
|
|
138
|
+
"[redis_queued_locks.explicit_all_locks_release] " \
|
|
139
|
+
"at => #{time_at} " \
|
|
140
|
+
"rel_time => #{rel_time} " \
|
|
141
|
+
"rel_key_cnt => #{result[:rel_key_cnt]}"
|
|
142
|
+
end
|
|
143
|
+
end if log_sampled
|
|
144
|
+
|
|
108
145
|
run_non_critical do
|
|
109
146
|
instrumenter.notify('redis_queued_locks.explicit_all_locks_release', {
|
|
110
147
|
at: time_at,
|
|
111
148
|
rel_time: rel_time,
|
|
112
|
-
rel_key_cnt: result[:rel_key_cnt]
|
|
149
|
+
rel_key_cnt: result[:rel_key_cnt],
|
|
150
|
+
instrument: instrument
|
|
113
151
|
})
|
|
114
152
|
end if instr_sampled
|
|
115
153
|
|
|
@@ -118,77 +156,155 @@ module RedisQueuedLocks::Acquirer::ReleaseAllLocks
|
|
|
118
156
|
result: { rel_key_cnt: result[:rel_key_cnt], rel_time: rel_time }
|
|
119
157
|
}
|
|
120
158
|
end
|
|
159
|
+
# rubocop:enable Metrics/MethodLength
|
|
121
160
|
|
|
122
161
|
private
|
|
123
162
|
|
|
124
|
-
# Release all locks:
|
|
163
|
+
# Release all locks:
|
|
164
|
+
# - the disabled lock index: one SCAN pass over the RQL keys (`KEY_PATTERN`); the keys of all
|
|
165
|
+
# lock families are deleted with one `DEL` per batch of `batch_size` keys (the keys are
|
|
166
|
+
# deleted during the scan: no list of all keys is kept in memory);
|
|
167
|
+
# - the enabled lock index: one ZSCAN pass over the lock index (`LOCK_INDEX_KEY`: lock names,
|
|
168
|
+
# the keyspace is never scanned); the keys of all lock families of each batch of
|
|
169
|
+
# `batch_size` lock names are deleted with one `DEL` in one transaction with the `ZREM` of
|
|
170
|
+
# the lock names from the lock index (two round trips per batch: the read lock acquirers,
|
|
171
|
+
# then the transaction):
|
|
172
|
+
# - expired lock names are dropped from the lock index first (all their keys are expired);
|
|
173
|
+
# - lock names of the lock requests queued (or the locks obtained) after the transaction of
|
|
174
|
+
# their batch are indexed again by the lock acquirement;
|
|
125
175
|
#
|
|
126
176
|
# @param redis [RedisClient]
|
|
127
177
|
# @param batch_size [Integer]
|
|
178
|
+
# SCAN count and the number of keys in one `DEL` (the disabled lock index) or ZSCAN count and
|
|
179
|
+
# the number of lock names in one transaction (the enabled lock index).
|
|
180
|
+
# @param lock_index_enabled [Boolean]
|
|
128
181
|
# @return [Hash<Symbol,Boolean|Hash<Symbol,Integer>>]
|
|
129
182
|
# - Exmaple: { ok: true, result: { rel_key_cnt: 12345 } }
|
|
130
183
|
#
|
|
131
184
|
# @api private
|
|
132
185
|
# @since 1.0.0
|
|
133
|
-
# @version 1.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
result = redis.with do |rconn|
|
|
137
|
-
rconn.pipelined do |pipeline|
|
|
138
|
-
# Step A: release all queus and their related locks
|
|
139
|
-
rconn.scan(
|
|
140
|
-
'MATCH',
|
|
141
|
-
RedisQueuedLocks::Resource::LOCK_QUEUE_PATTERN,
|
|
142
|
-
count: batch_size
|
|
143
|
-
) do |lock_queue|
|
|
144
|
-
# TODO: reduce unnecessary iterations
|
|
145
|
-
pipeline.call('EXPIRE', lock_queue, '0')
|
|
146
|
-
end
|
|
186
|
+
# @version 1.20.0
|
|
187
|
+
def fully_release_all_locks(redis, batch_size, lock_index_enabled)
|
|
188
|
+
rel_key_cnt = 0
|
|
147
189
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
end
|
|
190
|
+
redis.with do |rconn|
|
|
191
|
+
if lock_index_enabled
|
|
192
|
+
# Step 1 (lock index): drop the expired lock names from the lock index
|
|
193
|
+
# (all their keys are expired);
|
|
194
|
+
rconn.call(
|
|
195
|
+
'ZREMRANGEBYSCORE', RedisQueuedLocks::Resource::LOCK_INDEX_KEY,
|
|
196
|
+
'-inf', RedisQueuedLocks::Resource.lock_index_time - 1
|
|
197
|
+
)
|
|
157
198
|
|
|
158
|
-
# Step
|
|
159
|
-
rconn
|
|
160
|
-
|
|
161
|
-
RedisQueuedLocks::Resource::READ_LOCK_QUEUE_PATTERN,
|
|
162
|
-
count: batch_size
|
|
163
|
-
) do |read_lock_queue|
|
|
164
|
-
# TODO: reduce unnecessary iterations
|
|
165
|
-
pipeline.call('EXPIRE', read_lock_queue, '0')
|
|
199
|
+
# Step 2 (lock index): release the locks of the lock index in batches of lock names
|
|
200
|
+
each_lock_name_batch(rconn, batch_size) do |lock_names|
|
|
201
|
+
rel_key_cnt += release_locks(rconn, lock_names)
|
|
166
202
|
end
|
|
203
|
+
else
|
|
204
|
+
# Step 1 (keyspace): delete the RQL keys of the lock families during one SCAN pass
|
|
205
|
+
# @type var lock_keys: Array[String]
|
|
206
|
+
lock_keys = []
|
|
167
207
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
# TODO: reduce unnecessary iterations
|
|
175
|
-
pipeline.call('EXPIRE', lock_readers, '0')
|
|
176
|
-
end
|
|
208
|
+
rconn.scan('MATCH', RedisQueuedLocks::Resource::KEY_PATTERN, count: batch_size) do |key|
|
|
209
|
+
# NOTE: lock families only (KEY_PATTERN matches any `rql:lock*` key)
|
|
210
|
+
next if RedisQueuedLocks::Resource.lock_key_family(key) == nil
|
|
211
|
+
|
|
212
|
+
lock_keys << key
|
|
213
|
+
next if lock_keys.size < batch_size
|
|
177
214
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
'MATCH',
|
|
181
|
-
RedisQueuedLocks::Resource::READ_LOCK_PATTERN,
|
|
182
|
-
count: batch_size
|
|
183
|
-
) do |read_lock|
|
|
184
|
-
# TODO: reduce unnecessary iterations
|
|
185
|
-
pipeline.call('EXPIRE', read_lock, '0')
|
|
215
|
+
rel_key_cnt += rconn.call('DEL', *lock_keys)
|
|
216
|
+
lock_keys.clear
|
|
186
217
|
end
|
|
218
|
+
|
|
219
|
+
rel_key_cnt += rconn.call('DEL', *lock_keys) unless lock_keys.empty?
|
|
187
220
|
end
|
|
188
221
|
end
|
|
189
222
|
|
|
190
|
-
{ ok: true, result: { rel_key_cnt:
|
|
223
|
+
{ ok: true, result: { rel_key_cnt: } }
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# (lock index) Releases the locks of the given batch of lock names: deletes all their keys and
|
|
227
|
+
# drops the lock names from the lock index in one transaction.
|
|
228
|
+
# - (RW) read lock data keys are found via the readers registries (one more round trip
|
|
229
|
+
# before the transaction); the data of a read lock obtained between the registry read and
|
|
230
|
+
# the transaction stays till its ttl (the read lock itself is released with the registry);
|
|
231
|
+
#
|
|
232
|
+
# @param redis_client [RedisClient]
|
|
233
|
+
# @param lock_names [Array<String>]
|
|
234
|
+
# @return [Integer] The number of the deleted keys.
|
|
235
|
+
#
|
|
236
|
+
# @api private
|
|
237
|
+
# @since 1.20.0
|
|
238
|
+
def release_locks(redis_client, lock_names)
|
|
239
|
+
# Step A: (RW) read lock acquirers of the readers registries (their read lock data keys)
|
|
240
|
+
# NOTE: (pipeline layout) one reply per lock name in the batch order
|
|
241
|
+
# @type var read_lock_acquirers: Array[Array[String]]
|
|
242
|
+
read_lock_acquirers = redis_client.pipelined do |pipeline|
|
|
243
|
+
lock_names.each do |lock_name|
|
|
244
|
+
pipeline.call(
|
|
245
|
+
'ZRANGE', RedisQueuedLocks::Resource.prepare_lock_readers(lock_name), '0', '-1'
|
|
246
|
+
)
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# @type var lock_keys: Array[String]
|
|
251
|
+
lock_keys = []
|
|
252
|
+
lock_names.each_with_index do |lock_name, index|
|
|
253
|
+
lock_keys << RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
|
|
254
|
+
lock_keys << RedisQueuedLocks::Resource.prepare_lock_queue(lock_name)
|
|
255
|
+
lock_keys << RedisQueuedLocks::Resource.prepare_lock_readers(lock_name)
|
|
256
|
+
lock_keys << RedisQueuedLocks::Resource.prepare_read_lock_queue(lock_name)
|
|
257
|
+
read_lock_acquirers[index].each do |acquirer_id|
|
|
258
|
+
lock_keys << RedisQueuedLocks::Resource.prepare_read_lock_key(lock_name, acquirer_id)
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Step B: delete the keys and drop the lock names from the lock index in one transaction
|
|
263
|
+
# (a lock obtained after the transaction indexes its lock name again);
|
|
264
|
+
# NOTE: (transaction layout) [0] DEL (the number of the deleted keys), [1] ZREM (not read)
|
|
265
|
+
# @type var released: [Integer, Integer]
|
|
266
|
+
released = redis_client.multi do |transact|
|
|
267
|
+
transact.call('DEL', *lock_keys)
|
|
268
|
+
transact.call('ZREM', RedisQueuedLocks::Resource::LOCK_INDEX_KEY, *lock_names)
|
|
269
|
+
end
|
|
270
|
+
released[0]
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Iterates over the lock names of the lock index (one ZSCAN pass) in batches of `scan_size`
|
|
274
|
+
# lock names (batches are collected across ZSCAN pages). Expired lock names (not dropped from
|
|
275
|
+
# the index yet) are skipped: all their keys are expired.
|
|
276
|
+
#
|
|
277
|
+
# NOTE: each operation module iterates over the lock index by itself (duplicated on purpose:
|
|
278
|
+
# operation modules are independent of each other, see `Locks.each_lock_name_batch`);
|
|
279
|
+
#
|
|
280
|
+
# @param redis_client [RedisClient]
|
|
281
|
+
# @param scan_size [Integer] ZSCAN count and the number of lock names in one batch.
|
|
282
|
+
# @yield [lock_names]
|
|
283
|
+
# @yieldparam lock_names [Array<String>] The batch of lock names (reused between batches).
|
|
284
|
+
# @return [void]
|
|
285
|
+
#
|
|
286
|
+
# @api private
|
|
287
|
+
# @since 1.20.0
|
|
288
|
+
def each_lock_name_batch(redis_client, scan_size)
|
|
289
|
+
lock_index_time = RedisQueuedLocks::Resource.lock_index_time
|
|
290
|
+
# @type var lock_names: Array[String]
|
|
291
|
+
lock_names = []
|
|
292
|
+
|
|
293
|
+
redis_client.zscan(
|
|
294
|
+
RedisQueuedLocks::Resource::LOCK_INDEX_KEY, count: scan_size
|
|
295
|
+
) do |lock_name, lock_index_score|
|
|
296
|
+
next if lock_index_score.to_f < lock_index_time
|
|
297
|
+
|
|
298
|
+
lock_names << lock_name
|
|
299
|
+
next if lock_names.size < scan_size
|
|
300
|
+
|
|
301
|
+
yield(lock_names)
|
|
302
|
+
lock_names.clear
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
yield(lock_names) unless lock_names.empty?
|
|
191
306
|
end
|
|
192
|
-
# rubocop:enable Metrics/MethodLength
|
|
193
307
|
end
|
|
308
|
+
# rubocop:enable Metrics/ClassLength
|
|
194
309
|
end
|
|
310
|
+
# rubocop:enable Metrics/ModuleLength
|
|
@@ -2,16 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
# @api private
|
|
4
4
|
# @since 1.0.0
|
|
5
|
+
# @version 1.21.0
|
|
6
|
+
# rubocop:disable Metrics/ModuleLength
|
|
5
7
|
module RedisQueuedLocks::Acquirer::ReleaseLock
|
|
6
8
|
# @since 1.0.0
|
|
7
9
|
extend RedisQueuedLocks::Utilities
|
|
8
10
|
|
|
11
|
+
# rubocop:disable Metrics/ClassLength
|
|
9
12
|
class << self
|
|
10
13
|
# Release the concrete lock:
|
|
11
14
|
# - 1. clear lock queue: al; related processes released
|
|
12
15
|
# from the lock aquierment and should retry;
|
|
13
16
|
# - 2. delete the lock: drop lock key from Redis;
|
|
14
17
|
# - 3. (RW) clear the read lock queue and drop all read locks (the readers registry);
|
|
18
|
+
# - 4. (lock index) drop the lock name from the lock index (the enabled lock index only);
|
|
15
19
|
# It is safe because the lock obtain logic is transactional and
|
|
16
20
|
# watches the original lock for changes.
|
|
17
21
|
#
|
|
@@ -19,18 +23,23 @@ module RedisQueuedLocks::Acquirer::ReleaseLock
|
|
|
19
23
|
# Redis connection client.
|
|
20
24
|
# @param lock_name [String]
|
|
21
25
|
# The lock name that should be released.
|
|
22
|
-
# @param
|
|
23
|
-
#
|
|
26
|
+
# @param lock_index_enabled [Boolean]
|
|
27
|
+
# Drop the lock name from the lock index (the enabled lock index).
|
|
24
28
|
# @param logger [::Logger,#debug]
|
|
25
29
|
# - Logger object used from `configuration` layer (see config['logger']);
|
|
26
30
|
# - See RedisQueuedLocks::Logging::VoidLogger for example;
|
|
31
|
+
# @param instrumenter [#notify]
|
|
32
|
+
# See RedisQueuedLocks::Instrument::ActiveSupport for example.
|
|
33
|
+
# @param instrument [NilClass,Any]
|
|
34
|
+
# - Custom instrumentation data wich will be passed to the instrumenter's payload
|
|
35
|
+
# with :instrument key;
|
|
27
36
|
# @param log_sampling_enabled [Boolean]
|
|
28
37
|
# - enables <log sampling>: only the configured percent of RQL cases will be logged;
|
|
29
38
|
# - disabled by default;
|
|
30
39
|
# - works in tandem with <config['log_sampling_percent']> and <config['log_sampler']>;
|
|
31
40
|
# @param log_sampling_percent [Integer]
|
|
32
41
|
# - the percent of cases that should be logged;
|
|
33
|
-
# - take an effect when <config['
|
|
42
|
+
# - take an effect when <config['log_sampling_enabled']> is true;
|
|
34
43
|
# - works in tandem with <config['log_sampling_enabled']> and <config['log_sampler']> configs;
|
|
35
44
|
# @param log_sampler [#sampling_happened?,Module<RedisQueuedLocks::Logging::Sampler>]
|
|
36
45
|
# - percent-based log sampler that decides should be RQL case logged or not;
|
|
@@ -51,7 +60,7 @@ module RedisQueuedLocks::Acquirer::ReleaseLock
|
|
|
51
60
|
# - works in tandem with <config['instr_sampling_percent']> and <config['instr_sampler']>;
|
|
52
61
|
# @param instr_sampling_percent [Integer]
|
|
53
62
|
# - the percent of cases that should be instrumented;
|
|
54
|
-
# - take an effect when <config['
|
|
63
|
+
# - take an effect when <config['instr_sampling_enabled']> is true;
|
|
55
64
|
# - works in tandem with <config['instr_sampling_enabled']>
|
|
56
65
|
# and <config['instr_sampler']> configs;
|
|
57
66
|
# @param instr_sampler [#sampling_happened?,Module<RedisQueuedLocks::Instrument::Sampler>]
|
|
@@ -72,13 +81,15 @@ module RedisQueuedLocks::Acquirer::ReleaseLock
|
|
|
72
81
|
#
|
|
73
82
|
# @api private
|
|
74
83
|
# @since 1.0.0
|
|
75
|
-
# @version 1.
|
|
84
|
+
# @version 1.21.0
|
|
76
85
|
# rubocop:disable Metrics/MethodLength
|
|
77
86
|
def release_lock(
|
|
78
87
|
redis,
|
|
79
88
|
lock_name,
|
|
80
|
-
|
|
89
|
+
lock_index_enabled,
|
|
81
90
|
logger,
|
|
91
|
+
instrumenter,
|
|
92
|
+
instrument,
|
|
82
93
|
log_sampling_enabled,
|
|
83
94
|
log_sampling_percent,
|
|
84
95
|
log_sampler,
|
|
@@ -96,7 +107,8 @@ module RedisQueuedLocks::Acquirer::ReleaseLock
|
|
|
96
107
|
rel_start_time = clock_gettime
|
|
97
108
|
# steep:ignore:start
|
|
98
109
|
fully_release_lock(
|
|
99
|
-
redis, lock_name, lock_key, lock_key_queue, read_lock_key_queue, lock_readers_key
|
|
110
|
+
redis, lock_name, lock_key, lock_key_queue, read_lock_key_queue, lock_readers_key,
|
|
111
|
+
lock_index_enabled
|
|
100
112
|
) => { ok:, result: }
|
|
101
113
|
# steep:ignore:end
|
|
102
114
|
|
|
@@ -107,6 +119,12 @@ module RedisQueuedLocks::Acquirer::ReleaseLock
|
|
|
107
119
|
rel_end_time = clock_gettime
|
|
108
120
|
rel_time = ((rel_end_time - rel_start_time) / 1_000.0).ceil(2)
|
|
109
121
|
|
|
122
|
+
log_sampled = RedisQueuedLocks::Logging.should_log?(
|
|
123
|
+
log_sampling_enabled,
|
|
124
|
+
log_sample_this,
|
|
125
|
+
log_sampling_percent,
|
|
126
|
+
log_sampler
|
|
127
|
+
)
|
|
110
128
|
instr_sampled = RedisQueuedLocks::Instrument.should_instrument?(
|
|
111
129
|
instr_sampling_enabled,
|
|
112
130
|
instr_sample_this,
|
|
@@ -114,12 +132,23 @@ module RedisQueuedLocks::Acquirer::ReleaseLock
|
|
|
114
132
|
instr_sampler
|
|
115
133
|
)
|
|
116
134
|
|
|
135
|
+
run_non_critical do
|
|
136
|
+
logger.debug do
|
|
137
|
+
"[redis_queued_locks.explicit_lock_release] " \
|
|
138
|
+
"lock_key => '#{lock_key}' " \
|
|
139
|
+
"lock_key_queue => '#{lock_key_queue}' " \
|
|
140
|
+
"rel_time => #{rel_time} " \
|
|
141
|
+
"at => #{time_at}"
|
|
142
|
+
end
|
|
143
|
+
end if log_sampled
|
|
144
|
+
|
|
117
145
|
run_non_critical do
|
|
118
146
|
instrumenter.notify('redis_queued_locks.explicit_lock_release', {
|
|
119
147
|
lock_key: lock_key,
|
|
120
148
|
lock_key_queue: lock_key_queue,
|
|
121
149
|
rel_time: rel_time,
|
|
122
|
-
at: time_at
|
|
150
|
+
at: time_at,
|
|
151
|
+
instrument: instrument
|
|
123
152
|
})
|
|
124
153
|
end if instr_sampled
|
|
125
154
|
|
|
@@ -138,7 +167,8 @@ module RedisQueuedLocks::Acquirer::ReleaseLock
|
|
|
138
167
|
|
|
139
168
|
private
|
|
140
169
|
|
|
141
|
-
# Realease the lock: clear the lock queues and expire the lock (write lock and read locks)
|
|
170
|
+
# Realease the lock: clear the lock queues and expire the lock (write lock and read locks),
|
|
171
|
+
# drop the lock name from the lock index (the enabled lock index).
|
|
142
172
|
#
|
|
143
173
|
# @param redis [RedisClient]
|
|
144
174
|
# @param lock_name [String]
|
|
@@ -146,6 +176,7 @@ module RedisQueuedLocks::Acquirer::ReleaseLock
|
|
|
146
176
|
# @param lock_key_queue [String]
|
|
147
177
|
# @param read_lock_key_queue [String]
|
|
148
178
|
# @param lock_readers_key [String]
|
|
179
|
+
# @param lock_index_enabled [Boolean]
|
|
149
180
|
# @return [Hash<Symbol,Boolean|Hash<Symbol,Symbol>>]
|
|
150
181
|
# Format: {
|
|
151
182
|
# ok: true/false,
|
|
@@ -157,43 +188,71 @@ module RedisQueuedLocks::Acquirer::ReleaseLock
|
|
|
157
188
|
#
|
|
158
189
|
# @api private
|
|
159
190
|
# @since 1.0.0
|
|
160
|
-
# @version 1.
|
|
191
|
+
# @version 1.20.0
|
|
192
|
+
# rubocop:disable Metrics/MethodLength
|
|
161
193
|
def fully_release_lock(
|
|
162
194
|
redis,
|
|
163
195
|
lock_name,
|
|
164
196
|
lock_key,
|
|
165
197
|
lock_key_queue,
|
|
166
198
|
read_lock_key_queue,
|
|
167
|
-
lock_readers_key
|
|
199
|
+
lock_readers_key,
|
|
200
|
+
lock_index_enabled
|
|
168
201
|
)
|
|
169
|
-
# @type var result: [Integer,Integer,Integer,Integer]
|
|
202
|
+
# @type var result: [Array[String],Integer,Integer,Integer,Integer]
|
|
170
203
|
result = redis.with do |rconn|
|
|
171
|
-
#
|
|
172
|
-
# the readers registry
|
|
173
|
-
#
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
204
|
+
# Step 1: release the lock in one transaction (one round trip):
|
|
205
|
+
# - (RW) the readers registry is read in the same transaction: the snapshot of all
|
|
206
|
+
# released read locks (their data is dropped in Step 2);
|
|
207
|
+
# - (lock index) the enabled lock index: all the indexed keys of the lock are dropped,
|
|
208
|
+
# so the lock name is dropped from the lock index in the same transaction (a lock
|
|
209
|
+
# request queued or a lock obtained after this transaction indexes the lock name again);
|
|
210
|
+
# NOTE: (transaction layout) replies are read by their positions:
|
|
211
|
+
# [0] ZRANGE (released read locks), [1] write lock queue, [2] write lock,
|
|
212
|
+
# [3] read lock queue, [4] readers registry, [5] lock index (the enabled lock index
|
|
213
|
+
# only, the last command: not read);
|
|
214
|
+
released = rconn.multi do |transact|
|
|
215
|
+
transact.call('ZRANGE', lock_readers_key, '0', '-1')
|
|
177
216
|
transact.call('ZREMRANGEBYSCORE', lock_key_queue, '-inf', '+inf')
|
|
178
217
|
transact.call('EXPIRE', lock_key, '0')
|
|
179
218
|
transact.call('ZREMRANGEBYSCORE', read_lock_key_queue, '-inf', '+inf')
|
|
180
219
|
transact.call('EXPIRE', lock_readers_key, '0')
|
|
181
|
-
|
|
182
|
-
transact.call(
|
|
183
|
-
'DEL',
|
|
184
|
-
RedisQueuedLocks::Resource.prepare_read_lock_key(lock_name, acquirer_id)
|
|
185
|
-
)
|
|
220
|
+
if lock_index_enabled
|
|
221
|
+
transact.call('ZREM', RedisQueuedLocks::Resource::LOCK_INDEX_KEY, lock_name)
|
|
186
222
|
end
|
|
187
223
|
end
|
|
224
|
+
|
|
225
|
+
# Step 2: (RW) drop the data of the released read locks (only when read locks existed)
|
|
226
|
+
# NOTE:
|
|
227
|
+
# - the read lock data is a data carrier only: read locks are released via the readers
|
|
228
|
+
# registry (Step 1);
|
|
229
|
+
# - (trade-off) a reader of the snapshot that obtains a new read lock between Step 1 and
|
|
230
|
+
# Step 2 loses the data of the new read lock (meta, timestamps); the read lock itself is
|
|
231
|
+
# not affected (the readers registry is the source of truth);
|
|
232
|
+
# @type var read_lock_acquirers: Array[String]
|
|
233
|
+
read_lock_acquirers = released[0]
|
|
234
|
+
unless read_lock_acquirers.empty?
|
|
235
|
+
rconn.call(
|
|
236
|
+
'DEL',
|
|
237
|
+
*read_lock_acquirers.map do |acquirer_id|
|
|
238
|
+
RedisQueuedLocks::Resource.prepare_read_lock_key(lock_name, acquirer_id)
|
|
239
|
+
end
|
|
240
|
+
)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
released
|
|
188
244
|
end
|
|
189
245
|
|
|
190
246
|
{
|
|
191
247
|
ok: true,
|
|
192
248
|
result: {
|
|
193
|
-
queue: (result[
|
|
194
|
-
lock: (result[
|
|
249
|
+
queue: (result[1] != 0 || result[3] != 0) ? :released : :nothing_to_release,
|
|
250
|
+
lock: (result[2] != 0 || result[4] != 0) ? :released : :nothing_to_release
|
|
195
251
|
}
|
|
196
252
|
}
|
|
197
253
|
end
|
|
254
|
+
# rubocop:enable Metrics/MethodLength
|
|
198
255
|
end
|
|
256
|
+
# rubocop:enable Metrics/ClassLength
|
|
199
257
|
end
|
|
258
|
+
# rubocop:enable Metrics/ModuleLength
|