redis_queued_locks 1.16.2 → 1.18.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 +281 -0
- data/.claude/rules/acquirer.md +81 -0
- data/.claude/rules/arguments.md +60 -0
- data/.claude/rules/git.md +46 -0
- data/.claude/rules/logic.md +63 -0
- data/.claude/rules/swarm.md +120 -0
- data/.claude/rules/tests.md +48 -0
- data/.claude/rules/type-checking.md +50 -0
- data/.claude/rules/visitors.md +69 -0
- data/.rubocop.rbs.yml +31 -0
- data/.rubocop.yml +3 -3
- data/.ruby-version +1 -1
- data/CHANGELOG.md +80 -0
- data/CLAUDE.md +36 -0
- data/README.md +367 -82
- data/Rakefile +31 -2
- data/github_ci/.keep +0 -0
- data/lib/redis_queued_locks/acquirer/acquire_lock/delay_execution.rb +2 -2
- data/lib/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue/log_visitor.rb +6 -3
- data/lib/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue.rb +9 -7
- data/lib/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rb +20 -10
- data/lib/redis_queued_locks/acquirer/acquire_lock/log_visitor.rb +34 -16
- data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock/log_visitor.rb +240 -28
- data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rb +398 -132
- data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire/log_visitor.rb +12 -6
- data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire.rb +33 -9
- data/lib/redis_queued_locks/acquirer/acquire_lock.rb +73 -24
- data/lib/redis_queued_locks/acquirer/clear_dead_requests.rb +9 -1
- data/lib/redis_queued_locks/acquirer/extend_lock_ttl.rb +182 -3
- data/lib/redis_queued_locks/acquirer/is_locked.rb +19 -2
- data/lib/redis_queued_locks/acquirer/is_queued.rb +6 -2
- data/lib/redis_queued_locks/acquirer/lock_info.rb +96 -6
- data/lib/redis_queued_locks/acquirer/lock_series_poc/instr_visitor.rb +8 -4
- data/lib/redis_queued_locks/acquirer/lock_series_poc/log_visitor.rb +18 -7
- data/lib/redis_queued_locks/acquirer/lock_series_poc.rb +126 -29
- data/lib/redis_queued_locks/acquirer/locks.rb +103 -6
- data/lib/redis_queued_locks/acquirer/queue_info.rb +38 -9
- data/lib/redis_queued_locks/acquirer/queues.rb +28 -4
- data/lib/redis_queued_locks/acquirer/release_all_locks.rb +35 -1
- data/lib/redis_queued_locks/acquirer/release_lock.rb +40 -9
- data/lib/redis_queued_locks/acquirer/release_locks_of.rb +43 -3
- data/lib/redis_queued_locks/acquirer/release_read_lock.rb +143 -0
- data/lib/redis_queued_locks/acquirer.rb +1 -0
- data/lib/redis_queued_locks/client.rb +274 -158
- data/lib/redis_queued_locks/config/dsl.rb +4 -10
- data/lib/redis_queued_locks/resource.rb +117 -9
- data/lib/redis_queued_locks/swarm/flush_zombies.rb +46 -6
- data/lib/redis_queued_locks/swarm/supervisor.rb +1 -1
- data/lib/redis_queued_locks/swarm/swarm_element/isolated.rb +166 -44
- data/lib/redis_queued_locks/swarm/swarm_element/threaded.rb +45 -47
- data/lib/redis_queued_locks/swarm/zombie_info.rb +42 -0
- data/lib/redis_queued_locks/version.rb +2 -2
- data/rbs_collection.lock.yaml +10 -2
- data/rbs_collection.yaml +0 -2
- data/redis_queued_locks.gemspec +1 -1
- data/sig/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue/log_visitor.rbs +2 -1
- data/sig/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue.rbs +0 -1
- data/sig/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rbs +5 -0
- data/sig/redis_queued_locks/acquirer/acquire_lock/log_visitor.rbs +12 -6
- data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock/log_visitor.rbs +74 -7
- data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rbs +4 -2
- data/sig/redis_queued_locks/acquirer/acquire_lock/yield_expire.rbs +3 -0
- data/sig/redis_queued_locks/acquirer/acquire_lock/yield_with_expire/log_visitor.rbs +4 -2
- data/sig/redis_queued_locks/acquirer/extend_lock_ttl.rbs +19 -1
- data/sig/redis_queued_locks/acquirer/lock_info.rbs +9 -1
- data/sig/redis_queued_locks/acquirer/locks.rbs +10 -1
- data/sig/redis_queued_locks/acquirer/queue_info.rbs +2 -4
- data/sig/redis_queued_locks/acquirer/queues.rbs +1 -1
- data/sig/redis_queued_locks/acquirer/release_lock.rbs +8 -1
- data/sig/redis_queued_locks/acquirer/release_read_lock.rbs +44 -0
- data/sig/redis_queued_locks/client.rbs +22 -2
- data/sig/redis_queued_locks/resource.rbs +9 -2
- data/sig/redis_queued_locks/swarm/flush_zombies.rbs +1 -1
- data/sig/redis_queued_locks/swarm/swarm_element/isolated.rbs +13 -5
- data/sig/redis_queued_locks/swarm/swarm_element/threaded.rbs +6 -6
- data/sig/redis_queued_locks/swarm/zombie_info.rbs +5 -0
- metadata +17 -5
- data/github_ci/ruby3.3.gemfile +0 -17
- data/github_ci/ruby3.3.gemfile.lock +0 -216
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# NOTE: Lock Series PoC
|
|
4
|
-
# steep:ignore
|
|
5
4
|
# rubocop:disable all
|
|
6
5
|
# @api private
|
|
7
6
|
# @since 1.16.0
|
|
@@ -15,7 +14,7 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
|
|
|
15
14
|
class << self
|
|
16
15
|
# @api private
|
|
17
16
|
# @since 1.16.0
|
|
18
|
-
# @version 1.
|
|
17
|
+
# @version 1.18.0
|
|
19
18
|
def lock_series_poc( # steep:ignore
|
|
20
19
|
redis,
|
|
21
20
|
lock_names,
|
|
@@ -53,6 +52,9 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
|
|
|
53
52
|
instr_sample_this:,
|
|
54
53
|
&block
|
|
55
54
|
)
|
|
55
|
+
# NOTE: (RW) the lock mode is the same for all locks of the series
|
|
56
|
+
# (see RedisQueuedLocks::Acquirer::AcquireLock for read_write_mode validations);
|
|
57
|
+
|
|
56
58
|
case meta
|
|
57
59
|
when Hash, NilClass then nil
|
|
58
60
|
else
|
|
@@ -73,13 +75,14 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
|
|
|
73
75
|
key == 'spc_cnt' ||
|
|
74
76
|
key == 'l_spc_ext_ini_ttl' ||
|
|
75
77
|
key == 'l_spc_ext_ts' ||
|
|
76
|
-
key == 'l_spc_ts'
|
|
78
|
+
key == 'l_spc_ts' ||
|
|
79
|
+
key == 'rw_mode'
|
|
77
80
|
end)
|
|
78
81
|
raise(
|
|
79
82
|
RedisQueuedLocks::ArgumentError,
|
|
80
83
|
'`:meta` keys can not overlap reserved lock data keys ' \
|
|
81
84
|
'"acq_id", "hst_id", "ts", "ini_ttl", "lock_key", "rem_ttl", "spc_cnt", ' \
|
|
82
|
-
'"spc_ext_ttl", "l_spc_ext_ini_ttl", "l_spc_ext_ts", "l_spc_ts"'
|
|
85
|
+
'"spc_ext_ttl", "l_spc_ext_ini_ttl", "l_spc_ext_ts", "l_spc_ts", "rw_mode"'
|
|
83
86
|
)
|
|
84
87
|
end
|
|
85
88
|
|
|
@@ -129,13 +132,16 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
|
|
|
129
132
|
|
|
130
133
|
RedisQueuedLocks::Acquirer::LockSeriesPoC::LogVisitor.start_lock_series_obtaining( # steep:ignore
|
|
131
134
|
logger, log_sampled, lock_keys_for_instrumentation,
|
|
132
|
-
queue_ttl, acquirer_id_for_instrumentation, host_id_for_instrumentation, access_strategy
|
|
135
|
+
queue_ttl, acquirer_id_for_instrumentation, host_id_for_instrumentation, access_strategy,
|
|
136
|
+
read_write_mode
|
|
133
137
|
)
|
|
134
138
|
|
|
135
139
|
acq_start_time = RedisQueuedLocks::Utilities.clock_gettime
|
|
136
140
|
successfully_acquired_locks = [] # steep:ignore
|
|
137
141
|
failed_locks_and_errors = {} # steep:ignore
|
|
138
142
|
failed_on_lock = nil
|
|
143
|
+
# NOTE: locks of the failed series are released once (exceptional or not)
|
|
144
|
+
is_failed_series_released = false
|
|
139
145
|
|
|
140
146
|
lock_acquirement_operations.map do |lock_operation_options|
|
|
141
147
|
result =
|
|
@@ -180,17 +186,11 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
|
|
|
180
186
|
RedisQueuedLocks::LockAcquirementTimeoutError,
|
|
181
187
|
RedisQueuedLocks::LockAcquirementRetryLimitError,
|
|
182
188
|
RedisQueuedLocks::ConflictLockObtainError => error
|
|
183
|
-
if
|
|
184
|
-
|
|
185
|
-
successfully_acquired_locks
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
conn.multi(watch: [lock_key]) do |transact|
|
|
189
|
-
transact.call('DEL', lock_key)
|
|
190
|
-
end
|
|
191
|
-
end
|
|
192
|
-
end
|
|
193
|
-
end
|
|
189
|
+
# NOTE: release all previously acquired locks if any next lock is already locked
|
|
190
|
+
release_lock_series( # steep:ignore
|
|
191
|
+
redis, read_write_mode, successfully_acquired_locks, acquirer_id_for_instrumentation
|
|
192
|
+
)
|
|
193
|
+
is_failed_series_released = true
|
|
194
194
|
|
|
195
195
|
raise(error) if raise_errors
|
|
196
196
|
end
|
|
@@ -204,6 +204,13 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
|
|
|
204
204
|
else
|
|
205
205
|
failed_on_lock = lock_operation_options[:lock_name]
|
|
206
206
|
failed_locks_and_errors[lock_operation_options[:lock_name]] = result
|
|
207
|
+
# NOTE: release all previously acquired locks (non-exceptional failure: raise_errors: false)
|
|
208
|
+
unless is_failed_series_released
|
|
209
|
+
release_lock_series( # steep:ignore
|
|
210
|
+
redis, read_write_mode, successfully_acquired_locks, acquirer_id_for_instrumentation
|
|
211
|
+
)
|
|
212
|
+
is_failed_series_released = true
|
|
213
|
+
end
|
|
207
214
|
break
|
|
208
215
|
end
|
|
209
216
|
end
|
|
@@ -211,16 +218,16 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
|
|
|
211
218
|
if (successfully_acquired_locks.size == lock_names.size && (successfully_acquired_locks.all? { |res| res[:ok] }))
|
|
212
219
|
acq_end_time = RedisQueuedLocks::Utilities.clock_gettime
|
|
213
220
|
acq_time = ((acq_end_time - acq_start_time) / 1_000.0).ceil(2)
|
|
214
|
-
ts = Time.now.
|
|
221
|
+
ts = Time.now.to_f
|
|
215
222
|
|
|
216
223
|
RedisQueuedLocks::Acquirer::LockSeriesPoC::LogVisitor.lock_series_obtained( # steep:ignore
|
|
217
224
|
logger, log_sampled, lock_keys_for_instrumentation,
|
|
218
225
|
queue_ttl, acquirer_id_for_instrumentation, host_id_for_instrumentation,
|
|
219
|
-
acq_time, access_strategy
|
|
226
|
+
acq_time, access_strategy, read_write_mode
|
|
220
227
|
)
|
|
221
228
|
|
|
222
229
|
RedisQueuedLocks::Acquirer::LockSeriesPoC::InstrVisitor.lock_series_obtained( # steep:ignore
|
|
223
|
-
instrumenter, instr_sampled, lock_keys_for_instrumentation,
|
|
230
|
+
instrumenter, instr_sampled, lock_keys_for_instrumentation, read_write_mode,
|
|
224
231
|
ttl, acquirer_id_for_instrumentation, host_id_for_instrumentation, ts, acq_time, instrument
|
|
225
232
|
)
|
|
226
233
|
|
|
@@ -239,6 +246,11 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
|
|
|
239
246
|
redis,
|
|
240
247
|
logger,
|
|
241
248
|
lock_keys_for_instrumentation.last,
|
|
249
|
+
RedisQueuedLocks::Resource.prepare_lock_readers(lock_names.last),
|
|
250
|
+
RedisQueuedLocks::Resource.prepare_read_lock_key(
|
|
251
|
+
lock_names.last, acquirer_id_for_instrumentation
|
|
252
|
+
),
|
|
253
|
+
read_write_mode,
|
|
242
254
|
acquirer_id_for_instrumentation,
|
|
243
255
|
host_id_for_instrumentation,
|
|
244
256
|
access_strategy,
|
|
@@ -258,14 +270,9 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
|
|
|
258
270
|
|
|
259
271
|
# expire locks manually
|
|
260
272
|
if block_given?
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
lock_keys_for_instrumentation.each do |lock_key|
|
|
265
|
-
transaction.call('EXPIRE', lock_key, '0')
|
|
266
|
-
end
|
|
267
|
-
end
|
|
268
|
-
end
|
|
273
|
+
release_lock_series( # steep:ignore
|
|
274
|
+
redis, read_write_mode, successfully_acquired_locks, acquirer_id_for_instrumentation
|
|
275
|
+
)
|
|
269
276
|
is_lock_manually_released = true
|
|
270
277
|
end
|
|
271
278
|
|
|
@@ -275,13 +282,15 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
|
|
|
275
282
|
|
|
276
283
|
RedisQueuedLocks::Acquirer::LockSeriesPoC::LogVisitor.expire_lock_series( # steep:ignore
|
|
277
284
|
logger, log_sampled, lock_keys_for_instrumentation,
|
|
278
|
-
queue_ttl, acquirer_id_for_instrumentation, host_id_for_instrumentation, access_strategy
|
|
285
|
+
queue_ttl, acquirer_id_for_instrumentation, host_id_for_instrumentation, access_strategy,
|
|
286
|
+
read_write_mode
|
|
279
287
|
) if is_lock_manually_released
|
|
280
288
|
|
|
281
289
|
RedisQueuedLocks::Acquirer::LockSeriesPoC::InstrVisitor.lock_series_hold_and_release( # steep:ignore
|
|
282
290
|
instrumenter,
|
|
283
291
|
instr_sampled,
|
|
284
292
|
lock_keys_for_instrumentation,
|
|
293
|
+
read_write_mode,
|
|
285
294
|
ttl,
|
|
286
295
|
acquirer_id_for_instrumentation,
|
|
287
296
|
host_id_for_instrumentation,
|
|
@@ -322,7 +331,95 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
|
|
|
322
331
|
}
|
|
323
332
|
end
|
|
324
333
|
end
|
|
334
|
+
|
|
335
|
+
private
|
|
336
|
+
|
|
337
|
+
# Releases locks of the series that are obtained by the series itself:
|
|
338
|
+
# - reentrant locks (already obtained by the outer logic of the current acquirer) are kept;
|
|
339
|
+
# - only locks of the current acquirer are released (owner check);
|
|
340
|
+
#
|
|
341
|
+
# @param redis [RedisClient]
|
|
342
|
+
# @param read_write_mode [Symbol]
|
|
343
|
+
# @param successfully_acquired_locks [Array<Hash<Symbol,Any>>]
|
|
344
|
+
# @param acquirer_id [String]
|
|
345
|
+
# @return [void]
|
|
346
|
+
#
|
|
347
|
+
# @api private
|
|
348
|
+
# @since 1.18.0
|
|
349
|
+
def release_lock_series(redis, read_write_mode, successfully_acquired_locks, acquirer_id) # steep:ignore
|
|
350
|
+
lock_names = obtained_lock_names(successfully_acquired_locks) # steep:ignore
|
|
351
|
+
|
|
352
|
+
if read_write_mode == :read
|
|
353
|
+
release_read_lock_series(redis, lock_names, acquirer_id) # steep:ignore
|
|
354
|
+
else
|
|
355
|
+
release_write_lock_series(redis, lock_names, acquirer_id) # steep:ignore
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# 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).
|
|
361
|
+
#
|
|
362
|
+
# @param redis [RedisClient]
|
|
363
|
+
# @param lock_names [Array<String>]
|
|
364
|
+
# @param acquirer_id [String]
|
|
365
|
+
# @return [void]
|
|
366
|
+
#
|
|
367
|
+
# @api private
|
|
368
|
+
# @since 1.18.0
|
|
369
|
+
def release_write_lock_series(redis, lock_names, acquirer_id) # steep:ignore
|
|
370
|
+
return if lock_names.empty?
|
|
371
|
+
|
|
372
|
+
redis.with do |conn|
|
|
373
|
+
lock_names.each do |lock_name|
|
|
374
|
+
lock_key = RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
|
|
375
|
+
conn.multi(watch: [lock_key]) do |transaction|
|
|
376
|
+
if conn.call('HGET', lock_key, 'acq_id') == acquirer_id
|
|
377
|
+
transaction.call('DEL', lock_key)
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
# Locks of the series that are obtained by the series itself (reentrant locks are obtained
|
|
385
|
+
# by the outer logic of the current acquirer and should be released by it).
|
|
386
|
+
#
|
|
387
|
+
# @param successfully_acquired_locks [Array<Hash<Symbol,Any>>]
|
|
388
|
+
# @return [Array<String>]
|
|
389
|
+
#
|
|
390
|
+
# @api private
|
|
391
|
+
# @since 1.18.0
|
|
392
|
+
def obtained_lock_names(successfully_acquired_locks) # steep:ignore
|
|
393
|
+
successfully_acquired_locks.filter_map do |operation_result|
|
|
394
|
+
operation_result[:lock_name] if operation_result[:result][:process] == :lock_obtaining
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
# Releases read locks of the given acquirer (other readers are not affected).
|
|
399
|
+
#
|
|
400
|
+
# @param redis [RedisClient]
|
|
401
|
+
# @param lock_names [Array<String>]
|
|
402
|
+
# @param acquirer_id [String]
|
|
403
|
+
# @return [void]
|
|
404
|
+
#
|
|
405
|
+
# @api private
|
|
406
|
+
# @since 1.18.0
|
|
407
|
+
def release_read_lock_series(redis, lock_names, acquirer_id) # steep:ignore
|
|
408
|
+
return if lock_names.empty?
|
|
409
|
+
|
|
410
|
+
redis.with do |conn|
|
|
411
|
+
conn.multi do |transaction|
|
|
412
|
+
lock_names.each do |lock_name|
|
|
413
|
+
transaction.call(
|
|
414
|
+
'ZREM', RedisQueuedLocks::Resource.prepare_lock_readers(lock_name), acquirer_id
|
|
415
|
+
)
|
|
416
|
+
transaction.call(
|
|
417
|
+
'DEL', RedisQueuedLocks::Resource.prepare_read_lock_key(lock_name, acquirer_id)
|
|
418
|
+
)
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
end
|
|
325
423
|
end
|
|
326
424
|
end
|
|
327
|
-
# steep:ignore
|
|
328
425
|
# rubocop:enable all
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
# @api private
|
|
4
4
|
# @since 1.0.0
|
|
5
|
+
# @version 1.18.0
|
|
6
|
+
# rubocop:disable Metrics/ModuleLength
|
|
5
7
|
module RedisQueuedLocks::Acquirer::Locks
|
|
8
|
+
# rubocop:disable Metrics/ClassLength
|
|
6
9
|
class << self
|
|
7
10
|
# @param redis_client [RedisClient]
|
|
8
11
|
# @option scan_size [Integer]
|
|
@@ -23,9 +26,11 @@ module RedisQueuedLocks::Acquirer::Locks
|
|
|
23
26
|
# @param redis_client [RedisClient]
|
|
24
27
|
# @param scan_size [Integer]
|
|
25
28
|
# @return [Set<String>]
|
|
29
|
+
# - (RW) read locks are represented by their lock keys (`rql:lock:<lock_name>`) too;
|
|
26
30
|
#
|
|
27
31
|
# @api private
|
|
28
32
|
# @since 1.0.0
|
|
33
|
+
# @version 1.18.0
|
|
29
34
|
def scan_locks(redis_client, scan_size)
|
|
30
35
|
Set.new.tap do |lock_keys|
|
|
31
36
|
redis_client.scan(
|
|
@@ -36,19 +41,33 @@ module RedisQueuedLocks::Acquirer::Locks
|
|
|
36
41
|
# TODO: reduce unnecessary iterations
|
|
37
42
|
lock_keys.add(lock_key)
|
|
38
43
|
end
|
|
44
|
+
|
|
45
|
+
# NOTE: (RW) read locks (readers registries can contain expired read locks only)
|
|
46
|
+
redis_client.scan(
|
|
47
|
+
'MATCH',
|
|
48
|
+
RedisQueuedLocks::Resource::LOCK_READERS_PATTERN,
|
|
49
|
+
count: scan_size
|
|
50
|
+
) do |lock_readers_key|
|
|
51
|
+
# TODO: reduce unnecessary iterations
|
|
52
|
+
lock_keys.add(RedisQueuedLocks::Resource.lock_key_from_readers(lock_readers_key))
|
|
53
|
+
end
|
|
39
54
|
end
|
|
40
55
|
end
|
|
41
56
|
|
|
57
|
+
# NOTE:
|
|
58
|
+
# - the lock info is extracted by the module itself with the same approach as in `LockInfo`:
|
|
59
|
+
# public operation modules do not reuse each other (they share logic via `Resource`,
|
|
60
|
+
# `Utilities`), so the similar logic is duplicated on purpose;
|
|
61
|
+
#
|
|
42
62
|
# @param redis_client [RedisClient]
|
|
43
63
|
# @param lock_keys [Set<String>]
|
|
44
64
|
# @return [Set<Hash<Symbol,Any>>]
|
|
45
65
|
#
|
|
46
66
|
# @api private
|
|
47
67
|
# @since 1.0.0
|
|
48
|
-
# @version 1.
|
|
68
|
+
# @version 1.18.0
|
|
49
69
|
# rubocop:disable Metrics/MethodLength
|
|
50
70
|
def extract_locks_info(redis_client, lock_keys)
|
|
51
|
-
# TODO: refactor with RedisQueuedLocks::Acquier::LockInfo
|
|
52
71
|
Set.new.tap do |seeded_locks|
|
|
53
72
|
# rubocop:disable Layout/LineLength
|
|
54
73
|
# @type var seeded_locks: Set[{ lock: String, status: :released|:alive, info: Hash[String,untyped] }]
|
|
@@ -58,10 +77,16 @@ module RedisQueuedLocks::Acquirer::Locks
|
|
|
58
77
|
lock_keys.each do |lock_key|
|
|
59
78
|
# Step 1: extract lock info from redis
|
|
60
79
|
|
|
61
|
-
#
|
|
80
|
+
# NOTE: (RW) readers registry of the lock
|
|
81
|
+
lock_name = lock_key.delete_prefix('rql:lock:')
|
|
82
|
+
lock_readers_key = RedisQueuedLocks::Resource.prepare_lock_readers(lock_name)
|
|
83
|
+
|
|
84
|
+
# @type var lock_info: RedisQueuedLocks::Acquirer::Locks::lockInfo
|
|
62
85
|
lock_info = redis_client.pipelined do |pipeline|
|
|
63
86
|
pipeline.call('HGETALL', lock_key)
|
|
64
87
|
pipeline.call('PTTL', lock_key)
|
|
88
|
+
pipeline.call('TIME')
|
|
89
|
+
pipeline.call('ZRANGE', lock_readers_key, '0', '-1', 'WITHSCORES')
|
|
65
90
|
end.yield_self do |result| # Step 2: format the result
|
|
66
91
|
# Step 2.X: lock is released
|
|
67
92
|
if result == nil
|
|
@@ -73,16 +98,22 @@ module RedisQueuedLocks::Acquirer::Locks
|
|
|
73
98
|
# - result[1] (PTTL) (Integer)
|
|
74
99
|
# => (without any mutation, integer is atomic)
|
|
75
100
|
|
|
76
|
-
#
|
|
101
|
+
# rubocop:disable Layout/LineLength
|
|
102
|
+
# @type var result: [Hash[String,String|Float|Integer],Integer,Array[String],Array[[String, Float]]]
|
|
103
|
+
# rubocop:enable Layout/LineLength
|
|
77
104
|
hget_cmd_res = result[0] # NOTE: HGETALL result (hash)
|
|
78
105
|
pttl_cmd_res = result[1] # NOTE: PTTL result (integer)
|
|
79
106
|
|
|
80
|
-
# Step 2.Y: lock is released
|
|
107
|
+
# Step 2.Y: write lock is released
|
|
81
108
|
if hget_cmd_res == {} || pttl_cmd_res == -2 # NOTE: key does not exist
|
|
82
|
-
|
|
109
|
+
# NOTE: (RW) read locks (or nothing if there are no live read locks)
|
|
110
|
+
read_lock_info(
|
|
111
|
+
redis_client, lock_name, lock_key, result[2], result[3]
|
|
112
|
+
) || {} #: Hash[String,String|Float|Integer]
|
|
83
113
|
else
|
|
84
114
|
# Step 2.Z: lock is alive => format received info + add additional rem_ttl info
|
|
85
115
|
hget_cmd_res.tap do |lock_data|
|
|
116
|
+
lock_data['rw_mode'] = 'write'
|
|
86
117
|
lock_data['ts'] = Float(lock_data['ts'])
|
|
87
118
|
lock_data['ini_ttl'] = Integer(lock_data['ini_ttl'])
|
|
88
119
|
lock_data['rem_ttl'] = ((pttl_cmd_res == -1) ? Float::INFINITY : pttl_cmd_res)
|
|
@@ -109,5 +140,71 @@ module RedisQueuedLocks::Acquirer::Locks
|
|
|
109
140
|
end
|
|
110
141
|
end
|
|
111
142
|
# rubocop:enable Metrics/MethodLength
|
|
143
|
+
|
|
144
|
+
# Formats live read locks of the lock (expired read locks are ignored).
|
|
145
|
+
#
|
|
146
|
+
# NOTE: the same approach as in `LockInfo` (duplicated on purpose: operation modules are
|
|
147
|
+
# independent of each other, see `extract_locks_info`);
|
|
148
|
+
#
|
|
149
|
+
# @param redis_client [RedisClient]
|
|
150
|
+
# @param lock_name [String]
|
|
151
|
+
# @param lock_key [String]
|
|
152
|
+
# @param redis_time [Array<String>] Result of the redis `TIME` command.
|
|
153
|
+
# @param read_locks [Array<Array<String,Float>>] Readers registry (acquirer id, expiration).
|
|
154
|
+
# @return [Hash<String,String|Numeric|Array<Hash<String,String|Numeric>>>,NilClass]
|
|
155
|
+
# - `nil` is returned when there are no live read locks;
|
|
156
|
+
# - each reader has the same data as the write lock (acq_id, hst_id, ts, ini_ttl, rem_ttl,
|
|
157
|
+
# meta, reentrant lock data);
|
|
158
|
+
#
|
|
159
|
+
# @api private
|
|
160
|
+
# @since 1.18.0
|
|
161
|
+
# rubocop:disable Metrics/MethodLength
|
|
162
|
+
def read_lock_info(redis_client, lock_name, lock_key, redis_time, read_locks)
|
|
163
|
+
now = RedisQueuedLocks::Resource.redis_time_ms(redis_time)
|
|
164
|
+
live_read_locks = read_locks.select { |(_acquirer_id, expiration)| expiration > now }
|
|
165
|
+
return nil if live_read_locks.empty?
|
|
166
|
+
|
|
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
|
+
longest_rem_ttl = 0
|
|
179
|
+
readers = live_read_locks.each_with_index.map do |(acquirer_id, expiration), index|
|
|
180
|
+
rem_ttl = (expiration - now).ceil
|
|
181
|
+
longest_rem_ttl = rem_ttl if rem_ttl > longest_rem_ttl
|
|
182
|
+
|
|
183
|
+
# @type var reader: Hash[String,String|Float|Integer|nil]
|
|
184
|
+
reader = read_locks_data[index] || {}
|
|
185
|
+
reader['acq_id'] = acquirer_id
|
|
186
|
+
reader['hst_id'] ||= RedisQueuedLocks::Resource.host_identifier_from_acquirer(acquirer_id)
|
|
187
|
+
reader['ts'] = Float(reader['ts']) if reader['ts']
|
|
188
|
+
reader['ini_ttl'] = Integer(reader['ini_ttl']) if reader['ini_ttl']
|
|
189
|
+
reader['rem_ttl'] = rem_ttl
|
|
190
|
+
reader['spc_cnt'] = Integer(reader['spc_cnt']) if reader['spc_cnt']
|
|
191
|
+
reader['l_spc_ts'] = Float(reader['l_spc_ts']) if reader['l_spc_ts']
|
|
192
|
+
reader['spc_ext_ttl'] = Integer(reader['spc_ext_ttl']) if reader['spc_ext_ttl']
|
|
193
|
+
reader['l_spc_ext_ini_ttl'] =
|
|
194
|
+
Integer(reader['l_spc_ext_ini_ttl']) if reader['l_spc_ext_ini_ttl']
|
|
195
|
+
reader['l_spc_ext_ts'] = Float(reader['l_spc_ext_ts']) if reader['l_spc_ext_ts']
|
|
196
|
+
reader
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
{
|
|
200
|
+
'lock_key' => lock_key,
|
|
201
|
+
'rw_mode' => 'read',
|
|
202
|
+
'rem_ttl' => longest_rem_ttl,
|
|
203
|
+
'readers' => readers
|
|
204
|
+
}
|
|
205
|
+
end
|
|
206
|
+
# rubocop:enable Metrics/MethodLength
|
|
112
207
|
end
|
|
208
|
+
# rubocop:enable Metrics/ClassLength
|
|
113
209
|
end
|
|
210
|
+
# rubocop:enable Metrics/ModuleLength
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# @api private
|
|
4
4
|
# @since 1.0.0
|
|
5
|
+
# @version 1.18.0
|
|
5
6
|
module RedisQueuedLocks::Acquirer::QueueInfo
|
|
6
7
|
class << self
|
|
7
8
|
# Returns an information about the required lock queue by the lock name. The result
|
|
@@ -12,41 +13,69 @@ module RedisQueuedLocks::Acquirer::QueueInfo
|
|
|
12
13
|
#
|
|
13
14
|
# @param redis_client [RedisClient]
|
|
14
15
|
# @param lock_name [String]
|
|
15
|
-
# @return [Hash<String,Array<Hash<String,String|Numeric>>,NilClass]
|
|
16
|
-
# - `nil` is returned when lock
|
|
16
|
+
# @return [Hash<String,String|Array<Hash<String,String|Numeric>>,NilClass]
|
|
17
|
+
# - `nil` is returned when lock queues (write and read) do not exist;
|
|
17
18
|
# - result format: {
|
|
18
19
|
# "lock_queue" => "rql:lock_queue:your_lock_name", # lock queue key in redis,
|
|
19
20
|
# "queue" => [
|
|
20
|
-
# { "acq_id" => "rql:acq:123/456/789/987/
|
|
21
|
-
# { "acq_id" => "rql:acq:123/686/789/987/
|
|
21
|
+
# { "acq_id" => "rql:acq:123/456/789/987/id", "score" => 123.456, "rw_mode" => "write" },
|
|
22
|
+
# { "acq_id" => "rql:acq:123/686/789/987/id", "score" => 456.789, "rw_mode" => "write" },
|
|
22
23
|
# ...
|
|
23
|
-
# ] # ordered set (by score) with information about an acquirer
|
|
24
|
+
# ] # ordered set (by score) with information about an acquirer, their position in queue
|
|
25
|
+
# # and the requested lock mode
|
|
26
|
+
# <additional keys when the queue of read lock requests exists>:
|
|
27
|
+
# "read_lock_queue" => "rql:lock_read_queue:your_lock_name", # read lock queue key in redis
|
|
28
|
+
# "read_queue" => [
|
|
29
|
+
# { "acq_id" => "rql:acq:123/456/789/987/id", "score" => 123.456, "rw_mode" => "read" },
|
|
30
|
+
# ...
|
|
31
|
+
# ] # read lock requests (positions are comparable with the write lock request positions)
|
|
24
32
|
# }
|
|
25
33
|
#
|
|
26
34
|
# @api private
|
|
27
35
|
# @since 1.0.0
|
|
36
|
+
# @version 1.18.0
|
|
37
|
+
# rubocop:disable Metrics/MethodLength
|
|
28
38
|
def queue_info(redis_client, lock_name)
|
|
29
39
|
lock_key_queue = RedisQueuedLocks::Resource.prepare_lock_queue(lock_name)
|
|
40
|
+
read_lock_key_queue = RedisQueuedLocks::Resource.prepare_read_lock_queue(lock_name)
|
|
30
41
|
|
|
31
42
|
result = redis_client.pipelined do |pipeline|
|
|
32
43
|
pipeline.call('EXISTS', lock_key_queue)
|
|
33
44
|
pipeline.call('ZRANGE', lock_key_queue, '0', '-1', 'WITHSCORES')
|
|
45
|
+
pipeline.call('EXISTS', read_lock_key_queue)
|
|
46
|
+
pipeline.call('ZRANGE', read_lock_key_queue, '0', '-1', 'WITHSCORES')
|
|
34
47
|
end
|
|
35
48
|
|
|
36
|
-
#
|
|
49
|
+
# rubocop:disable Layout/LineLength
|
|
50
|
+
# @type var result: [Integer,Array[[String,Integer|Float]],Integer,Array[[String,Integer|Float]]]
|
|
51
|
+
# rubocop:enable Layout/LineLength
|
|
37
52
|
exists_cmd_res = result[0]
|
|
38
53
|
zrange_cmd_res = result[1]
|
|
54
|
+
read_exists_cmd_res = result[2]
|
|
55
|
+
read_zrange_cmd_res = result[3]
|
|
39
56
|
|
|
40
|
-
if exists_cmd_res == 1
|
|
57
|
+
if exists_cmd_res == 1 || read_exists_cmd_res == 1
|
|
41
58
|
# NOTE: queue existed during the piepline invocation
|
|
42
|
-
|
|
59
|
+
# @type var queue_info: RedisQueuedLocks::Acquirer::QueueInfo::queueInfo
|
|
60
|
+
queue_info = {
|
|
43
61
|
'lock_queue' => lock_key_queue,
|
|
44
|
-
'queue' => zrange_cmd_res.map
|
|
62
|
+
'queue' => zrange_cmd_res.map do |val|
|
|
63
|
+
{ 'acq_id' => val[0], 'score' => val[1], 'rw_mode' => 'write' }
|
|
64
|
+
end
|
|
45
65
|
}
|
|
66
|
+
# NOTE: (RW) the queue of read lock requests
|
|
67
|
+
if read_exists_cmd_res == 1
|
|
68
|
+
queue_info['read_lock_queue'] = read_lock_key_queue
|
|
69
|
+
queue_info['read_queue'] = read_zrange_cmd_res.map do |val|
|
|
70
|
+
{ 'acq_id' => val[0], 'score' => val[1], 'rw_mode' => 'read' }
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
queue_info
|
|
46
74
|
else
|
|
47
75
|
# NOTE: queue did not exist during the pipeline invocation
|
|
48
76
|
nil
|
|
49
77
|
end
|
|
50
78
|
end
|
|
79
|
+
# rubocop:enable Metrics/MethodLength
|
|
51
80
|
end
|
|
52
81
|
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# @api private
|
|
4
4
|
# @since 1.0.0
|
|
5
|
+
# @version 1.18.0
|
|
5
6
|
module RedisQueuedLocks::Acquirer::Queues
|
|
6
7
|
class << self
|
|
7
8
|
# @param redis_client [RedisClient]
|
|
@@ -23,9 +24,11 @@ module RedisQueuedLocks::Acquirer::Queues
|
|
|
23
24
|
# @param redis_client [RedisClient]
|
|
24
25
|
# @param scan_size [Integer]
|
|
25
26
|
# @return [Set<String>]
|
|
27
|
+
# - (RW) queues of read lock requests are included;
|
|
26
28
|
#
|
|
27
29
|
# @api private
|
|
28
30
|
# @since 1.0.0
|
|
31
|
+
# @version 1.18.0
|
|
29
32
|
def scan_queues(redis_client, scan_size)
|
|
30
33
|
Set.new.tap do |lock_queues|
|
|
31
34
|
# @type var lock_queues: Set[String]
|
|
@@ -38,21 +41,40 @@ module RedisQueuedLocks::Acquirer::Queues
|
|
|
38
41
|
# @type var lock_queue: String
|
|
39
42
|
lock_queues.add(lock_queue)
|
|
40
43
|
end
|
|
44
|
+
|
|
45
|
+
# NOTE: (RW) queues of read lock requests
|
|
46
|
+
redis_client.scan(
|
|
47
|
+
'MATCH',
|
|
48
|
+
RedisQueuedLocks::Resource::READ_LOCK_QUEUE_PATTERN,
|
|
49
|
+
count: scan_size
|
|
50
|
+
) do |read_lock_queue|
|
|
51
|
+
# TODO: reduce unnecessary iterations
|
|
52
|
+
# @type var read_lock_queue: String
|
|
53
|
+
lock_queues.add(read_lock_queue)
|
|
54
|
+
end
|
|
41
55
|
end
|
|
42
56
|
end
|
|
43
57
|
|
|
44
58
|
# @param redis_client [RedisClient]
|
|
45
59
|
# @param lock_queues [Set<String>]
|
|
46
60
|
# @return [Set<Hash<Symbol,Any>>]
|
|
61
|
+
# - each request: `{ 'acq_id' => String, 'score' => Float, 'rw_mode' => 'read'/'write' }`;
|
|
47
62
|
#
|
|
48
63
|
# @api private
|
|
49
64
|
# @since 1.0.0
|
|
65
|
+
# @version 1.18.0
|
|
50
66
|
def extract_queues_info(redis_client, lock_queues)
|
|
51
|
-
#
|
|
67
|
+
# NOTE: the same approach as in `QueueInfo` (duplicated on purpose: public operation modules
|
|
68
|
+
# do not reuse each other, they share logic via `Resource`, `Utilities`);
|
|
52
69
|
Set.new.tap do |seeded_queues|
|
|
53
70
|
# Step X: iterate over each lock queue and extract their info
|
|
54
71
|
# @type var seeded_queues: Set[Hash[Symbol,untyped]]
|
|
55
72
|
lock_queues.each do |lock_queue|
|
|
73
|
+
# NOTE:
|
|
74
|
+
# - (RW) the mode of the requests of the queue (`'read'` or `'write'`);
|
|
75
|
+
# - lock queues are scanned by the lock queue patterns, so the mode is never `nil`;
|
|
76
|
+
rw_mode = RedisQueuedLocks::Resource.lock_queue_rw_mode(lock_queue) #: String
|
|
77
|
+
|
|
56
78
|
# Step 1: extract lock queue info from reids
|
|
57
79
|
queue_info = redis_client.pipelined do |pipeline|
|
|
58
80
|
pipeline.call('EXISTS', lock_queue)
|
|
@@ -64,14 +86,16 @@ module RedisQueuedLocks::Acquirer::Queues
|
|
|
64
86
|
zrange_cmd_res = result[1]
|
|
65
87
|
|
|
66
88
|
if exists_cmd_res == 1 # Step 2.X: lock queue existed during the piepline invocation
|
|
67
|
-
zrange_cmd_res.map
|
|
89
|
+
zrange_cmd_res.map do |val|
|
|
90
|
+
{ 'acq_id' => val[0], 'score' => val[1], 'rw_mode' => rw_mode }
|
|
91
|
+
end
|
|
68
92
|
else
|
|
69
93
|
# Step 2.Y: lock queue did not exist during the pipeline invocation
|
|
70
|
-
[] #: Array[Hash[String,Float]]
|
|
94
|
+
[] #: Array[Hash[String,String|Float]]
|
|
71
95
|
end
|
|
72
96
|
end
|
|
73
97
|
|
|
74
|
-
# @type var queue_info: Array[Hash[String,Float]]
|
|
98
|
+
# @type var queue_info: Array[Hash[String,String|Float]]
|
|
75
99
|
|
|
76
100
|
# Step 3: push the lock queue info to the result store
|
|
77
101
|
seeded_queues << {
|
|
@@ -10,6 +10,7 @@ module RedisQueuedLocks::Acquirer::ReleaseAllLocks
|
|
|
10
10
|
# Release all locks:
|
|
11
11
|
# - 1. clear all lock queus: drop them all from Redis database by the lock queue pattern;
|
|
12
12
|
# - 2. delete all locks: drop lock keys from Redis by the lock key pattern;
|
|
13
|
+
# - 3. (RW) clear all read lock queues and drop all read locks (readers registries and data);
|
|
13
14
|
#
|
|
14
15
|
# @param redis [RedisClient]
|
|
15
16
|
# Redis connection client.
|
|
@@ -71,7 +72,7 @@ module RedisQueuedLocks::Acquirer::ReleaseAllLocks
|
|
|
71
72
|
#
|
|
72
73
|
# @api private
|
|
73
74
|
# @since 1.0.0
|
|
74
|
-
# @version 1.
|
|
75
|
+
# @version 1.18.0
|
|
75
76
|
def release_all_locks(
|
|
76
77
|
redis,
|
|
77
78
|
batch_size,
|
|
@@ -129,6 +130,8 @@ module RedisQueuedLocks::Acquirer::ReleaseAllLocks
|
|
|
129
130
|
#
|
|
130
131
|
# @api private
|
|
131
132
|
# @since 1.0.0
|
|
133
|
+
# @version 1.18.0
|
|
134
|
+
# rubocop:disable Metrics/MethodLength
|
|
132
135
|
def fully_release_all_locks(redis, batch_size)
|
|
133
136
|
result = redis.with do |rconn|
|
|
134
137
|
rconn.pipelined do |pipeline|
|
|
@@ -151,10 +154,41 @@ module RedisQueuedLocks::Acquirer::ReleaseAllLocks
|
|
|
151
154
|
# TODO: reduce unnecessary iterations
|
|
152
155
|
pipeline.call('EXPIRE', lock_key, '0')
|
|
153
156
|
end
|
|
157
|
+
|
|
158
|
+
# Step C (RW): release all read lock queues
|
|
159
|
+
rconn.scan(
|
|
160
|
+
'MATCH',
|
|
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')
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Step D (RW): release all read locks
|
|
169
|
+
rconn.scan(
|
|
170
|
+
'MATCH',
|
|
171
|
+
RedisQueuedLocks::Resource::LOCK_READERS_PATTERN,
|
|
172
|
+
count: batch_size
|
|
173
|
+
) do |lock_readers|
|
|
174
|
+
# TODO: reduce unnecessary iterations
|
|
175
|
+
pipeline.call('EXPIRE', lock_readers, '0')
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Step E (RW): release all read lock data
|
|
179
|
+
rconn.scan(
|
|
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')
|
|
186
|
+
end
|
|
154
187
|
end
|
|
155
188
|
end
|
|
156
189
|
|
|
157
190
|
{ ok: true, result: { rel_key_cnt: result.sum } }
|
|
158
191
|
end
|
|
192
|
+
# rubocop:enable Metrics/MethodLength
|
|
159
193
|
end
|
|
160
194
|
end
|