redis_queued_locks 1.17.0 → 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 +86 -10
- data/.claude/rules/acquirer.md +31 -11
- data/.claude/rules/arguments.md +2 -1
- data/.claude/rules/git.md +46 -0
- data/.claude/rules/logic.md +9 -3
- data/.claude/rules/swarm.md +4 -3
- data/.claude/rules/tests.md +9 -3
- data/.claude/rules/type-checking.md +10 -2
- data/.claude/rules/visitors.md +12 -7
- data/CHANGELOG.md +62 -0
- data/CLAUDE.md +7 -2
- data/README.md +348 -79
- 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 -129
- 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 +32 -8
- data/lib/redis_queued_locks/acquirer/acquire_lock.rb +73 -20
- 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 -2
- data/lib/redis_queued_locks/acquirer/lock_series_poc/log_visitor.rb +18 -6
- data/lib/redis_queued_locks/acquirer/lock_series_poc.rb +126 -27
- 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 +129 -14
- data/lib/redis_queued_locks/resource.rb +117 -9
- data/lib/redis_queued_locks/swarm/flush_zombies.rb +38 -0
- data/lib/redis_queued_locks/swarm/zombie_info.rb +42 -0
- data/lib/redis_queued_locks/version.rb +1 -1
- data/rbs_collection.lock.yaml +10 -2
- data/rbs_collection.yaml +0 -2
- 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/zombie_info.rbs +5 -0
- metadata +4 -1
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# @api private
|
|
4
|
+
# @since 1.18.0
|
|
5
|
+
module RedisQueuedLocks::Acquirer::ReleaseReadLock
|
|
6
|
+
# @since 1.18.0
|
|
7
|
+
extend RedisQueuedLocks::Utilities
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
# Release the read lock of the concrete acquirer (other read locks, the write lock and
|
|
11
|
+
# lock requests are not affected): drop the acquirer from the readers registry and drop
|
|
12
|
+
# its read lock data.
|
|
13
|
+
#
|
|
14
|
+
# @param redis [RedisClient]
|
|
15
|
+
# Redis connection client.
|
|
16
|
+
# @param lock_name [String]
|
|
17
|
+
# The lock name whose read lock should be released.
|
|
18
|
+
# @param acquirer_id [String]
|
|
19
|
+
# The read lock acquirer.
|
|
20
|
+
# @param instrumenter [#notify]
|
|
21
|
+
# See RedisQueuedLocks::Instrument::ActiveSupport for example.
|
|
22
|
+
# @param logger [::Logger,#debug]
|
|
23
|
+
# - Logger object used from `configuration` layer (see config['logger']);
|
|
24
|
+
# - See RedisQueuedLocks::Logging::VoidLogger for example;
|
|
25
|
+
# @param instrument [NilClass,Any]
|
|
26
|
+
# - Custom instrumentation data wich will be passed to the instrumenter's payload
|
|
27
|
+
# with :instrument key;
|
|
28
|
+
# @param log_sampling_enabled [Boolean]
|
|
29
|
+
# @param log_sampling_percent [Integer]
|
|
30
|
+
# @param log_sampler [#sampling_happened?,Module<RedisQueuedLocks::Logging::Sampler>]
|
|
31
|
+
# @param log_sample_this [Boolean]
|
|
32
|
+
# @param instr_sampling_enabled [Boolean]
|
|
33
|
+
# - enables <instrumentaion sampling>: only the configured percent
|
|
34
|
+
# of RQL cases will be instrumented;
|
|
35
|
+
# @param instr_sampling_percent [Integer]
|
|
36
|
+
# - the percent of cases that should be instrumented;
|
|
37
|
+
# @param instr_sampler [#sampling_happened?,Module<RedisQueuedLocks::Instrument::Sampler>]
|
|
38
|
+
# - percent-based sampler that decides should be RQL case instrumented or not;
|
|
39
|
+
# @param instr_sample_this [Boolean]
|
|
40
|
+
# - marks the method that everything should be instrumneted
|
|
41
|
+
# despite the enabled instrumentation sampling;
|
|
42
|
+
# @return [Hash<Symbol,Boolean<Hash<Symbol,Numeric|String|Symbol>>]
|
|
43
|
+
# Format: {
|
|
44
|
+
# ok: true,
|
|
45
|
+
# result: {
|
|
46
|
+
# rel_time: Numeric, # <milliseconds>
|
|
47
|
+
# rel_key: String, # lock key
|
|
48
|
+
# rel_acq_id: String, # read lock acquirer
|
|
49
|
+
# lock_res: Symbol # :released or :nothing_to_release
|
|
50
|
+
# }
|
|
51
|
+
# }
|
|
52
|
+
#
|
|
53
|
+
# @api private
|
|
54
|
+
# @since 1.18.0
|
|
55
|
+
# rubocop:disable Metrics/MethodLength
|
|
56
|
+
def release_read_lock(
|
|
57
|
+
redis,
|
|
58
|
+
lock_name,
|
|
59
|
+
acquirer_id,
|
|
60
|
+
instrumenter,
|
|
61
|
+
logger,
|
|
62
|
+
instrument,
|
|
63
|
+
log_sampling_enabled,
|
|
64
|
+
log_sampling_percent,
|
|
65
|
+
log_sampler,
|
|
66
|
+
log_sample_this,
|
|
67
|
+
instr_sampling_enabled,
|
|
68
|
+
instr_sampling_percent,
|
|
69
|
+
instr_sampler,
|
|
70
|
+
instr_sample_this
|
|
71
|
+
)
|
|
72
|
+
lock_key = RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
|
|
73
|
+
lock_readers_key = RedisQueuedLocks::Resource.prepare_lock_readers(lock_name)
|
|
74
|
+
read_lock_key = RedisQueuedLocks::Resource.prepare_read_lock_key(lock_name, acquirer_id)
|
|
75
|
+
|
|
76
|
+
rel_start_time = clock_gettime
|
|
77
|
+
# steep:ignore:start
|
|
78
|
+
fully_release_read_lock(
|
|
79
|
+
redis, lock_readers_key, read_lock_key, acquirer_id
|
|
80
|
+
) => { ok:, result: }
|
|
81
|
+
# steep:ignore:end
|
|
82
|
+
|
|
83
|
+
# @type var ok: bool
|
|
84
|
+
# @type var result: Hash[Symbol,Symbol]
|
|
85
|
+
|
|
86
|
+
time_at = Time.now.to_f
|
|
87
|
+
rel_end_time = clock_gettime
|
|
88
|
+
rel_time = ((rel_end_time - rel_start_time) / 1_000.0).ceil(2)
|
|
89
|
+
|
|
90
|
+
instr_sampled = RedisQueuedLocks::Instrument.should_instrument?(
|
|
91
|
+
instr_sampling_enabled,
|
|
92
|
+
instr_sample_this,
|
|
93
|
+
instr_sampling_percent,
|
|
94
|
+
instr_sampler
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
run_non_critical do
|
|
98
|
+
instrumenter.notify('redis_queued_locks.explicit_read_lock_release', {
|
|
99
|
+
lock_key: lock_key,
|
|
100
|
+
acq_id: acquirer_id,
|
|
101
|
+
lock_res: result[:lock],
|
|
102
|
+
rel_time: rel_time,
|
|
103
|
+
at: time_at,
|
|
104
|
+
instrument: instrument
|
|
105
|
+
})
|
|
106
|
+
end if instr_sampled
|
|
107
|
+
|
|
108
|
+
{
|
|
109
|
+
ok: true,
|
|
110
|
+
result: {
|
|
111
|
+
rel_time: rel_time,
|
|
112
|
+
rel_key: lock_key,
|
|
113
|
+
rel_acq_id: acquirer_id,
|
|
114
|
+
lock_res: result[:lock]
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
end
|
|
118
|
+
# rubocop:enable Metrics/MethodLength
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
# @param redis [RedisClient]
|
|
123
|
+
# @param lock_readers_key [String]
|
|
124
|
+
# @param read_lock_key [String]
|
|
125
|
+
# @param acquirer_id [String]
|
|
126
|
+
# @return [Hash<Symbol,Boolean|Hash<Symbol,Symbol>>]
|
|
127
|
+
# Format: { ok: true, result: { lock: :released/:nothing_to_release } }
|
|
128
|
+
#
|
|
129
|
+
# @api private
|
|
130
|
+
# @since 1.18.0
|
|
131
|
+
def fully_release_read_lock(redis, lock_readers_key, read_lock_key, acquirer_id)
|
|
132
|
+
# @type var result: [Integer,Integer]
|
|
133
|
+
result = redis.with do |rconn|
|
|
134
|
+
rconn.multi do |transact|
|
|
135
|
+
transact.call('ZREM', lock_readers_key, acquirer_id)
|
|
136
|
+
transact.call('DEL', read_lock_key)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
{ ok: true, result: { lock: (result[0] != 0) ? :released : :nothing_to_release } }
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -6,6 +6,7 @@ module RedisQueuedLocks::Acquirer
|
|
|
6
6
|
require_relative 'acquirer/acquire_lock'
|
|
7
7
|
require_relative 'acquirer/lock_series_poc'
|
|
8
8
|
require_relative 'acquirer/release_lock'
|
|
9
|
+
require_relative 'acquirer/release_read_lock'
|
|
9
10
|
require_relative 'acquirer/release_all_locks'
|
|
10
11
|
require_relative 'acquirer/release_locks_of'
|
|
11
12
|
require_relative 'acquirer/is_locked'
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# @api public
|
|
4
4
|
# @since 1.0.0
|
|
5
|
-
# @version 1.
|
|
5
|
+
# @version 1.18.0
|
|
6
6
|
# rubocop:disable Metrics/ClassLength
|
|
7
7
|
class RedisQueuedLocks::Client
|
|
8
8
|
# @return [RedisClient]
|
|
@@ -207,7 +207,28 @@ class RedisQueuedLocks::Client
|
|
|
207
207
|
# (with timeouts, retry delays, retry limits, etc - in classic way :));
|
|
208
208
|
# - `:dead_locking` - fail with deadlock exception;
|
|
209
209
|
# @option read_write_mode [Symbol]
|
|
210
|
-
# -
|
|
210
|
+
# - The lock mode (read/write lock semantics);
|
|
211
|
+
# - `:write` mode is used by default;
|
|
212
|
+
# - Supports:
|
|
213
|
+
# - `:write` - exclusive lock: waits for the write lock and for all read locks;
|
|
214
|
+
# - `:read` - shared lock: waits for the write lock only (read locks of different
|
|
215
|
+
# acquirers do not block each other);
|
|
216
|
+
# - `:queued` access strategy orders read and write lock requests in FIFO between modes
|
|
217
|
+
# (read lock request waits for earlier write lock requests, write lock request waits for
|
|
218
|
+
# earlier read and write lock requests) so writers are not starved by readers;
|
|
219
|
+
# - `:random` access strategy ignores lock queues (a continuous flow of readers can starve
|
|
220
|
+
# writers);
|
|
221
|
+
# - read lock specifics:
|
|
222
|
+
# - read lock is released (with a block of code) or expired (by `ttl`) independently
|
|
223
|
+
# of other read locks; `#unlock` releases all read locks and the write lock;
|
|
224
|
+
# - each read lock has its own data (acquirer, host, timestamp, ttl, `:meta`, reentrant
|
|
225
|
+
# lock data) that is returned by `#lock_info` for each reader;
|
|
226
|
+
# - same-process conflicts (`conflict_strategy`) work for both modes:
|
|
227
|
+
# - read lock request under the obtained write lock (or read lock) works as a reentrant
|
|
228
|
+
# lock of the obtained lock;
|
|
229
|
+
# - write lock request under the obtained read lock (read-to-write lock upgrade) fails with
|
|
230
|
+
# `RedisQueuedLocks::ConflictLockObtainError` for any conflict strategy except
|
|
231
|
+
# `:wait_for_lock` (it waits for the read lock expiration);
|
|
211
232
|
# @option access_strategy [Symbol]
|
|
212
233
|
# - The way in which the lock will be obtained;
|
|
213
234
|
# - By default it uses `:queued` strategy;
|
|
@@ -310,7 +331,7 @@ class RedisQueuedLocks::Client
|
|
|
310
331
|
#
|
|
311
332
|
# @api public
|
|
312
333
|
# @since 1.0.0
|
|
313
|
-
# @version 1.
|
|
334
|
+
# @version 1.18.0
|
|
314
335
|
# rubocop:disable Metrics/MethodLength
|
|
315
336
|
def lock(
|
|
316
337
|
lock_name,
|
|
@@ -531,7 +552,7 @@ class RedisQueuedLocks::Client
|
|
|
531
552
|
#
|
|
532
553
|
# @api public
|
|
533
554
|
# @since 1.0.0
|
|
534
|
-
# @version 1.
|
|
555
|
+
# @version 1.18.0
|
|
535
556
|
def lock!(
|
|
536
557
|
lock_name,
|
|
537
558
|
ttl: config['default_lock_ttl'],
|
|
@@ -596,6 +617,8 @@ class RedisQueuedLocks::Client
|
|
|
596
617
|
end
|
|
597
618
|
# rubocop:enable Metrics/MethodLength
|
|
598
619
|
|
|
620
|
+
# Releases the lock: the write lock, all read locks and all lock requests (write and read).
|
|
621
|
+
#
|
|
599
622
|
# @param lock_name [String] The lock name that should be released.
|
|
600
623
|
# @option logger [::Logger,#debug]
|
|
601
624
|
# @option instrumenter [#notify]
|
|
@@ -615,14 +638,14 @@ class RedisQueuedLocks::Client
|
|
|
615
638
|
# rel_time: Integer, # <millisecnds>
|
|
616
639
|
# rel_key: String, # lock key
|
|
617
640
|
# rel_queue: String, # lock queue
|
|
618
|
-
# queue_res: Symbol, # :released or :nothing_to_release
|
|
619
|
-
# lock_res: Symbol # :released or :nothing_to_release
|
|
641
|
+
# queue_res: Symbol, # :released or :nothing_to_release (write or read lock requests)
|
|
642
|
+
# lock_res: Symbol # :released or :nothing_to_release (write lock or read locks)
|
|
620
643
|
# }
|
|
621
644
|
# }
|
|
622
645
|
#
|
|
623
646
|
# @api public
|
|
624
647
|
# @since 1.0.0
|
|
625
|
-
# @version 1.
|
|
648
|
+
# @version 1.18.0
|
|
626
649
|
def unlock(
|
|
627
650
|
lock_name,
|
|
628
651
|
logger: config['logger'],
|
|
@@ -654,38 +677,112 @@ class RedisQueuedLocks::Client
|
|
|
654
677
|
end
|
|
655
678
|
alias_method :release_lock, :unlock
|
|
656
679
|
|
|
680
|
+
# Releases the read lock of the current acquirer only: other read locks, the write lock and
|
|
681
|
+
# lock requests are not affected (`#unlock` releases the lock entirely). It is suitable for
|
|
682
|
+
# read locks obtained without a block of code.
|
|
683
|
+
#
|
|
684
|
+
# @param lock_name [String] The lock name whose read lock should be released.
|
|
685
|
+
# @option identity [String]
|
|
686
|
+
# Unique acquirer identity (the same as for `#lock`) that is used to calculate the current
|
|
687
|
+
# acquirer id (see `#current_acquirer_id`).
|
|
688
|
+
# @option logger [::Logger,#debug]
|
|
689
|
+
# @option instrumenter [#notify]
|
|
690
|
+
# @option instrument [NilClass,Any]
|
|
691
|
+
# @option log_sampling_enabled [Boolean]
|
|
692
|
+
# @option log_sampling_percent [Integer]
|
|
693
|
+
# @option log_sampler [#sampling_happened?,Module<RedisQueuedLocks::Logging::Sampler>]
|
|
694
|
+
# @option log_sample_this [Boolean]
|
|
695
|
+
# @option instr_sampling_enabled [Boolean]
|
|
696
|
+
# @option instr_sampling_percent [Integer]
|
|
697
|
+
# @option instr_sampler [#sampling_happened?,Module<RedisQueuedLocks::Instrument::Sampler>]
|
|
698
|
+
# @option instr_sample_this [Boolean]
|
|
699
|
+
# @return [Hash<Symbol,Any>]
|
|
700
|
+
# Format: {
|
|
701
|
+
# ok: true,
|
|
702
|
+
# result: {
|
|
703
|
+
# rel_time: Numeric, # <milliseconds>
|
|
704
|
+
# rel_key: String, # lock key
|
|
705
|
+
# rel_acq_id: String, # read lock acquirer
|
|
706
|
+
# lock_res: Symbol # :released or :nothing_to_release
|
|
707
|
+
# }
|
|
708
|
+
# }
|
|
709
|
+
#
|
|
710
|
+
# @api public
|
|
711
|
+
# @since 1.18.0
|
|
712
|
+
def unlock_read(
|
|
713
|
+
lock_name,
|
|
714
|
+
identity: uniq_identity,
|
|
715
|
+
logger: config['logger'],
|
|
716
|
+
instrumenter: config['instrumenter'],
|
|
717
|
+
instrument: nil,
|
|
718
|
+
log_sampling_enabled: config['log_sampling_enabled'],
|
|
719
|
+
log_sampling_percent: config['log_sampling_percent'],
|
|
720
|
+
log_sampler: config['log_sampler'],
|
|
721
|
+
log_sample_this: false,
|
|
722
|
+
instr_sampling_enabled: config['instr_sampling_enabled'],
|
|
723
|
+
instr_sampling_percent: config['instr_sampling_percent'],
|
|
724
|
+
instr_sampler: config['instr_sampler'],
|
|
725
|
+
instr_sample_this: false
|
|
726
|
+
)
|
|
727
|
+
RedisQueuedLocks::Acquirer::ReleaseReadLock.release_read_lock(
|
|
728
|
+
redis_client,
|
|
729
|
+
lock_name,
|
|
730
|
+
current_acquirer_id(identity:),
|
|
731
|
+
instrumenter,
|
|
732
|
+
logger,
|
|
733
|
+
instrument,
|
|
734
|
+
log_sampling_enabled,
|
|
735
|
+
log_sampling_percent,
|
|
736
|
+
log_sampler,
|
|
737
|
+
log_sample_this,
|
|
738
|
+
instr_sampling_enabled,
|
|
739
|
+
instr_sampling_percent,
|
|
740
|
+
instr_sampler,
|
|
741
|
+
instr_sample_this
|
|
742
|
+
)
|
|
743
|
+
end
|
|
744
|
+
alias_method :release_read_lock, :unlock_read
|
|
745
|
+
|
|
657
746
|
# @param lock_name [String]
|
|
658
|
-
# @return [Boolean]
|
|
747
|
+
# @return [Boolean] Is the lock obtained by a writer or by any reader.
|
|
659
748
|
#
|
|
660
749
|
# @api public
|
|
661
750
|
# @since 1.0.0
|
|
751
|
+
# @version 1.18.0
|
|
662
752
|
def locked?(lock_name)
|
|
663
753
|
RedisQueuedLocks::Acquirer::IsLocked.locked?(redis_client, lock_name)
|
|
664
754
|
end
|
|
665
755
|
|
|
666
756
|
# @param lock_name [String]
|
|
667
|
-
# @return [Boolean]
|
|
757
|
+
# @return [Boolean] Are there any write or read lock requests.
|
|
668
758
|
#
|
|
669
759
|
# @api public
|
|
670
760
|
# @since 1.0.0
|
|
761
|
+
# @version 1.18.0
|
|
671
762
|
def queued?(lock_name)
|
|
672
763
|
RedisQueuedLocks::Acquirer::IsQueued.queued?(redis_client, lock_name)
|
|
673
764
|
end
|
|
674
765
|
|
|
675
766
|
# @param lock_name [String]
|
|
676
|
-
# @return [Hash<String,String|Numeric
|
|
767
|
+
# @return [Hash<String,String|Numeric|Array<Hash<String,String|Numeric>>>,NilClass]
|
|
768
|
+
# - write lock info or read locks info (when there is no write lock);
|
|
769
|
+
# - see `RedisQueuedLocks::Acquirer::LockInfo.lock_info` for details;
|
|
677
770
|
#
|
|
678
771
|
# @api public
|
|
679
772
|
# @since 1.0.0
|
|
773
|
+
# @version 1.18.0
|
|
680
774
|
def lock_info(lock_name)
|
|
681
775
|
RedisQueuedLocks::Acquirer::LockInfo.lock_info(redis_client, lock_name)
|
|
682
776
|
end
|
|
683
777
|
|
|
684
778
|
# @param lock_name [String]
|
|
685
779
|
# @return [Hash<String|Array<Hash<String,String|Numeric>>,NilClass]
|
|
780
|
+
# - write lock requests and read lock requests (when they exist);
|
|
781
|
+
# - see `RedisQueuedLocks::Acquirer::QueueInfo.queue_info` for details;
|
|
686
782
|
#
|
|
687
783
|
# @api public
|
|
688
784
|
# @since 1.0.0
|
|
785
|
+
# @version 1.18.0
|
|
689
786
|
def queue_info(lock_name)
|
|
690
787
|
RedisQueuedLocks::Acquirer::QueueInfo.queue_info(redis_client, lock_name)
|
|
691
788
|
end
|
|
@@ -783,6 +880,16 @@ class RedisQueuedLocks::Client
|
|
|
783
880
|
#
|
|
784
881
|
# @param lock_name [String]
|
|
785
882
|
# @param milliseconds [Integer] How many milliseconds should be added.
|
|
883
|
+
# @option read_write_mode [Symbol]
|
|
884
|
+
# - `:write` (default) - extends the write lock (the method description above);
|
|
885
|
+
# - `:read` - extends the read lock of the current acquirer only (an expired read lock or
|
|
886
|
+
# a read lock replaced by a writer is not extended);
|
|
887
|
+
# @option all_read_locks [Boolean]
|
|
888
|
+
# `:read` mode only (ignored for `:write` mode): extends all live read locks of the lock
|
|
889
|
+
# instead of the read lock of the current acquirer (`false` by default).
|
|
890
|
+
# @option identity [String]
|
|
891
|
+
# Unique acquirer identity (the same as for `#lock`) that is used to calculate the current
|
|
892
|
+
# acquirer id for `:read` mode (see `#current_acquirer_id`).
|
|
786
893
|
# @option logger [::Logger,#debug]
|
|
787
894
|
# @option instrumenter [#notify] See `config['instrumenter']` docs for details.
|
|
788
895
|
# @option instrument [NilClass,Any]
|
|
@@ -794,16 +901,21 @@ class RedisQueuedLocks::Client
|
|
|
794
901
|
# @option instr_sampling_percent [Integer]
|
|
795
902
|
# @option instr_sampler [#sampling_happened?,Module<RedisQueuedLocks::Instrument::Sampler>]
|
|
796
903
|
# @option instr_sample_this [Boolean]
|
|
797
|
-
# @return [Hash<Symbol,Boolean|Symbol
|
|
798
|
-
# - { ok: true, result: :
|
|
904
|
+
# @return [Hash<Symbol,Boolean|Symbol|Hash<Symbol,Integer>>]
|
|
905
|
+
# - { ok: true, result: { extended_locks_count: Integer } } - the number of extended locks:
|
|
906
|
+
# `1` for the write lock and for the read lock of the current acquirer,
|
|
907
|
+
# the number of extended read locks for `all_read_locks: true`;
|
|
799
908
|
# - { ok: false, result: :async_expire_or_no_lock }
|
|
800
909
|
#
|
|
801
910
|
# @api public
|
|
802
911
|
# @since 1.0.0
|
|
803
|
-
# @version 1.
|
|
912
|
+
# @version 1.18.0
|
|
804
913
|
def extend_lock_ttl(
|
|
805
914
|
lock_name,
|
|
806
915
|
milliseconds,
|
|
916
|
+
read_write_mode: :write,
|
|
917
|
+
all_read_locks: false,
|
|
918
|
+
identity: uniq_identity,
|
|
807
919
|
logger: config['logger'],
|
|
808
920
|
instrumenter: config['instrumenter'],
|
|
809
921
|
instrument: nil,
|
|
@@ -820,6 +932,9 @@ class RedisQueuedLocks::Client
|
|
|
820
932
|
redis_client,
|
|
821
933
|
lock_name,
|
|
822
934
|
milliseconds,
|
|
935
|
+
read_write_mode,
|
|
936
|
+
all_read_locks,
|
|
937
|
+
current_acquirer_id(identity:),
|
|
823
938
|
logger,
|
|
824
939
|
instrumenter,
|
|
825
940
|
instrument,
|
|
@@ -928,7 +1043,7 @@ class RedisQueuedLocks::Client
|
|
|
928
1043
|
# @example Release locks of a different host/acquirer:
|
|
929
1044
|
# client.clear_locks_of(
|
|
930
1045
|
# host_id: "rql:hst:62681/2016/2032/b30ec5e4bea10512",
|
|
931
|
-
# acquirer_id: "
|
|
1046
|
+
# acquirer_id: "rql:acq:62681/2016/2024/2032/b30ec5e4bea10512"
|
|
932
1047
|
# )
|
|
933
1048
|
#
|
|
934
1049
|
# @see #clear_current_locks
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# @api private
|
|
4
4
|
# @since 1.0.0
|
|
5
|
+
# rubocop:disable Metrics/ModuleLength
|
|
5
6
|
module RedisQueuedLocks::Resource
|
|
6
7
|
# @return [String]
|
|
7
8
|
#
|
|
@@ -21,17 +22,30 @@ module RedisQueuedLocks::Resource
|
|
|
21
22
|
# @since 1.0.0
|
|
22
23
|
LOCK_QUEUE_PATTERN = 'rql:lock_queue:*'
|
|
23
24
|
|
|
25
|
+
# NOTE:
|
|
26
|
+
# - read/write keys use their own prefixes (not suffixes of the lock name) cuz the lock name
|
|
27
|
+
# is an arbitrary string: `rql:lock_queue:<name>:read` collides with the queue of a lock
|
|
28
|
+
# named "<name>:read";
|
|
29
|
+
# - new prefixes are not matched by LOCK_PATTERN/LOCK_QUEUE_PATTERN
|
|
30
|
+
# (`rql:lock_` vs `rql:lock:`) but are matched by KEY_PATTERN;
|
|
31
|
+
# - write lock requests use the classic lock queue (LOCK_QUEUE_PATTERN);
|
|
24
32
|
# @return [String]
|
|
25
33
|
#
|
|
26
34
|
# @api private
|
|
27
|
-
# @since
|
|
28
|
-
READ_LOCK_QUEUE_PATTERN = 'rql:
|
|
35
|
+
# @since 1.18.0
|
|
36
|
+
READ_LOCK_QUEUE_PATTERN = 'rql:lock_read_queue:*'
|
|
29
37
|
|
|
30
38
|
# @return [String]
|
|
31
39
|
#
|
|
32
40
|
# @api private
|
|
33
|
-
# @since
|
|
34
|
-
|
|
41
|
+
# @since 1.18.0
|
|
42
|
+
LOCK_READERS_PATTERN = 'rql:lock_readers:*'
|
|
43
|
+
|
|
44
|
+
# @return [String]
|
|
45
|
+
#
|
|
46
|
+
# @api private
|
|
47
|
+
# @since 1.18.0
|
|
48
|
+
READ_LOCK_PATTERN = 'rql:lock_reader:*'
|
|
35
49
|
|
|
36
50
|
# @return [String]
|
|
37
51
|
#
|
|
@@ -105,22 +119,115 @@ module RedisQueuedLocks::Resource
|
|
|
105
119
|
"rql:lock_queue:#{lock_name}"
|
|
106
120
|
end
|
|
107
121
|
|
|
122
|
+
# Queue of read lock requests (write lock requests use the classic lock queue).
|
|
123
|
+
#
|
|
108
124
|
# @param lock_name [String]
|
|
109
125
|
# @return [String]
|
|
110
126
|
#
|
|
111
127
|
# @api private
|
|
112
|
-
# @
|
|
128
|
+
# @since 1.18.0
|
|
113
129
|
def prepare_read_lock_queue(lock_name)
|
|
114
|
-
"rql:
|
|
130
|
+
"rql:lock_read_queue:#{lock_name}"
|
|
115
131
|
end
|
|
116
132
|
|
|
133
|
+
# Registry of read lock holders: a sorted set where the member is an acquirer id and
|
|
134
|
+
# the score is the read lock expiration time (redis server time, epoch milliseconds).
|
|
135
|
+
#
|
|
117
136
|
# @param lock_name [String]
|
|
118
137
|
# @return [String]
|
|
119
138
|
#
|
|
120
139
|
# @api private
|
|
121
|
-
# @
|
|
122
|
-
def
|
|
123
|
-
"rql:
|
|
140
|
+
# @since 1.18.0
|
|
141
|
+
def prepare_lock_readers(lock_name)
|
|
142
|
+
"rql:lock_readers:#{lock_name}"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Read lock data of the concrete acquirer: a hash with the same structure as the write lock
|
|
146
|
+
# (acquirer, host, timestamp, ttl, meta, reentrant lock data). It is a data carrier only:
|
|
147
|
+
# the readers registry is the source of truth for the read lock existence.
|
|
148
|
+
#
|
|
149
|
+
# @param lock_name [String]
|
|
150
|
+
# @param acquirer_id [String]
|
|
151
|
+
# @return [String]
|
|
152
|
+
#
|
|
153
|
+
# @api private
|
|
154
|
+
# @since 1.18.0
|
|
155
|
+
def prepare_read_lock_key(lock_name, acquirer_id)
|
|
156
|
+
"rql:lock_reader:#{lock_name}:#{acquirer_id}"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Mode of the lock requests stored in the given lock queue.
|
|
160
|
+
#
|
|
161
|
+
# @param lock_queue [String]
|
|
162
|
+
# Write lock queue (`rql:lock_queue:*`) or read lock queue (`rql:lock_read_queue:*`).
|
|
163
|
+
# @return [String,NilClass]
|
|
164
|
+
# - `'write'` for the write lock queue, `'read'` for the read lock queue;
|
|
165
|
+
# - `nil` for any other key (unreachable: lock queues are always passed consistently);
|
|
166
|
+
#
|
|
167
|
+
# @api private
|
|
168
|
+
# @since 1.18.0
|
|
169
|
+
def lock_queue_rw_mode(lock_queue)
|
|
170
|
+
# NOTE:
|
|
171
|
+
# - `start_with?` instead of regexp `when`-matchers: no MatchData allocations;
|
|
172
|
+
# - write lock queues are checked first: they are met more often than read lock queues;
|
|
173
|
+
case
|
|
174
|
+
when lock_queue.start_with?('rql:lock_queue:') then 'write'
|
|
175
|
+
when lock_queue.start_with?('rql:lock_read_queue:') then 'read'
|
|
176
|
+
else
|
|
177
|
+
# NOTE: unreachable: the logic always passes a consistent lock queue (read or write)
|
|
178
|
+
nil
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# @param lock_readers_key [String]
|
|
183
|
+
# @return [String]
|
|
184
|
+
#
|
|
185
|
+
# @api private
|
|
186
|
+
# @since 1.18.0
|
|
187
|
+
def lock_name_from_readers(lock_readers_key)
|
|
188
|
+
lock_readers_key.delete_prefix('rql:lock_readers:')
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# @param lock_readers_key [String]
|
|
192
|
+
# @return [String]
|
|
193
|
+
#
|
|
194
|
+
# @api private
|
|
195
|
+
# @since 1.18.0
|
|
196
|
+
def lock_key_from_readers(lock_readers_key)
|
|
197
|
+
"rql:lock:#{lock_readers_key.delete_prefix('rql:lock_readers:')}"
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Host identifier of the given acquirer
|
|
201
|
+
# (acquirer and host identifiers are built from the same process/thread/ractor/identity data):
|
|
202
|
+
# - acquirer id: `rql:acq:<pid>/<thread>/<fiber>/<ractor>/<identity>`;
|
|
203
|
+
# - host id: `rql:hst:<pid>/<thread>/<ractor>/<identity>` (the same as `host_identifier`);
|
|
204
|
+
#
|
|
205
|
+
# @param acquirer_id [String]
|
|
206
|
+
# @return [String]
|
|
207
|
+
#
|
|
208
|
+
# @api private
|
|
209
|
+
# @since 1.18.0
|
|
210
|
+
def host_identifier_from_acquirer(acquirer_id)
|
|
211
|
+
# NOTE:
|
|
212
|
+
# - the host id is the acquirer id with another prefix and without the fiber part, so it is
|
|
213
|
+
# built in place from the acquirer id copy (one allocation: no intermediate strings and
|
|
214
|
+
# arrays of `delete_prefix`/`split`, no substrings);
|
|
215
|
+
# - 8 is the size of the `rql:acq:` prefix;
|
|
216
|
+
# - byte positions are used cuz identity can contain any (multibyte) symbols;
|
|
217
|
+
thread_end = acquirer_id.byteindex('/', acquirer_id.byteindex('/', 8).to_i + 1).to_i
|
|
218
|
+
fiber_end = acquirer_id.byteindex('/', thread_end + 1).to_i
|
|
219
|
+
host_id = acquirer_id.dup
|
|
220
|
+
host_id.bytesplice(thread_end, fiber_end - thread_end, '') # NOTE: drop "/<fiber>"
|
|
221
|
+
host_id.bytesplice(0, 8, 'rql:hst:')
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# @param redis_time [Array<String>] Result of the redis `TIME` command (seconds, microseconds).
|
|
225
|
+
# @return [Integer] Redis server time (epoch, in milliseconds).
|
|
226
|
+
#
|
|
227
|
+
# @api private
|
|
228
|
+
# @since 1.18.0
|
|
229
|
+
def redis_time_ms(redis_time)
|
|
230
|
+
(Integer(redis_time[0]) * 1_000) + (Integer(redis_time[1]) / 1_000)
|
|
124
231
|
end
|
|
125
232
|
|
|
126
233
|
# @return [Float] Redis's <Set> score that is calculated from the time (epoch) as a float.
|
|
@@ -241,3 +348,4 @@ module RedisQueuedLocks::Resource
|
|
|
241
348
|
end
|
|
242
349
|
end
|
|
243
350
|
end
|
|
351
|
+
# rubocop:enable Metrics/ModuleLength
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# @api private
|
|
4
4
|
# @since 1.9.0
|
|
5
|
+
# rubocop:disable Metrics/ClassLength
|
|
5
6
|
class RedisQueuedLocks::Swarm::FlushZombies < RedisQueuedLocks::Swarm::SwarmElement::Isolated
|
|
6
7
|
class << self
|
|
7
8
|
# @param redis_client [RedisClient]
|
|
@@ -18,6 +19,7 @@ class RedisQueuedLocks::Swarm::FlushZombies < RedisQueuedLocks::Swarm::SwarmElem
|
|
|
18
19
|
#
|
|
19
20
|
# @api private
|
|
20
21
|
# @since 1.9.0
|
|
22
|
+
# @version 1.18.0
|
|
21
23
|
# rubocop:disable Metrics/MethodLength
|
|
22
24
|
def flush_zombies(
|
|
23
25
|
redis_client,
|
|
@@ -65,6 +67,32 @@ class RedisQueuedLocks::Swarm::FlushZombies < RedisQueuedLocks::Swarm::SwarmElem
|
|
|
65
67
|
# NOTE: (steep) steep can't use <Set>s for splats
|
|
66
68
|
rconn.call('DEL', *zombie_locks) if zombie_locks.any? # steep:ignore
|
|
67
69
|
|
|
70
|
+
# Step 3 (RW): find zombie read locks (read locks of zombie hosts) and drop them
|
|
71
|
+
# - read locks store acquirer ids only (the host is a part of the acquirer id);
|
|
72
|
+
# - readers registry is reported as a zombie lock if any of its read locks is dropped;
|
|
73
|
+
rconn.scan(
|
|
74
|
+
'MATCH', RedisQueuedLocks::Resource::LOCK_READERS_PATTERN, count: lock_scan_size
|
|
75
|
+
) do |lock_readers_key|
|
|
76
|
+
# @type var read_lock_acquirers: Array[String]
|
|
77
|
+
read_lock_acquirers = rconn.call('ZRANGE', lock_readers_key, '0', '-1')
|
|
78
|
+
zombie_readers = read_lock_acquirers.select do |acquirer_id|
|
|
79
|
+
zombie_hosts.include?(RedisQueuedLocks::Resource.host_identifier_from_acquirer(acquirer_id))
|
|
80
|
+
end
|
|
81
|
+
next if zombie_readers.empty?
|
|
82
|
+
|
|
83
|
+
rconn.call('ZREM', lock_readers_key, *zombie_readers)
|
|
84
|
+
# NOTE: drop the zombie read lock data
|
|
85
|
+
lock_name = RedisQueuedLocks::Resource.lock_name_from_readers(lock_readers_key)
|
|
86
|
+
rconn.call(
|
|
87
|
+
'DEL',
|
|
88
|
+
*zombie_readers.map do |acquirer_id|
|
|
89
|
+
RedisQueuedLocks::Resource.prepare_read_lock_key(lock_name, acquirer_id)
|
|
90
|
+
end
|
|
91
|
+
)
|
|
92
|
+
zombie_locks << lock_readers_key
|
|
93
|
+
zombie_acquirers.merge(zombie_readers)
|
|
94
|
+
end
|
|
95
|
+
|
|
68
96
|
# Step 4: find zombie requests => and drop them
|
|
69
97
|
# TODO: indexing (in order to prevent full database scan);
|
|
70
98
|
# NOTE: original redis does not support indexing so we need to use
|
|
@@ -77,6 +105,15 @@ class RedisQueuedLocks::Swarm::FlushZombies < RedisQueuedLocks::Swarm::SwarmElem
|
|
|
77
105
|
end
|
|
78
106
|
end
|
|
79
107
|
|
|
108
|
+
# Step 4 (RW): drop zombie requests from the queues of read lock requests
|
|
109
|
+
rconn.scan(
|
|
110
|
+
'MATCH', RedisQueuedLocks::Resource::READ_LOCK_QUEUE_PATTERN, count: queue_scan_size
|
|
111
|
+
) do |read_lock_queue|
|
|
112
|
+
zombie_acquirers.each do |zombie_acquirer|
|
|
113
|
+
rconn.call('ZREM', read_lock_queue, zombie_acquirer)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
80
117
|
# Step 5: drop zombies from the swarm
|
|
81
118
|
rconn.call('HDEL', RedisQueuedLocks::Resource::SWARM_KEY, *zombie_hosts)
|
|
82
119
|
|
|
@@ -134,3 +171,4 @@ class RedisQueuedLocks::Swarm::FlushZombies < RedisQueuedLocks::Swarm::SwarmElem
|
|
|
134
171
|
end
|
|
135
172
|
end
|
|
136
173
|
end
|
|
174
|
+
# rubocop:enable Metrics/ClassLength
|