redis_queued_locks 1.20.0 → 1.22.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 +249 -36
- data/.claude/rules/acquirer.md +16 -14
- data/.claude/rules/arguments.md +2 -2
- data/.claude/rules/changelog.md +27 -0
- data/.claude/rules/git.md +1 -1
- data/.claude/rules/logic.md +34 -13
- data/.claude/rules/swarm.md +4 -5
- data/.claude/rules/tests.md +9 -6
- data/.claude/rules/type-checking.md +13 -14
- data/.claude/rules/visitors.md +13 -14
- data/.claude/skills/rql-code-review/SKILL.md +134 -43
- data/CHANGELOG.md +105 -125
- data/CLAUDE.md +11 -9
- data/README.md +501 -136
- data/Rakefile +11 -0
- 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 +68 -10
- data/lib/redis_queued_locks/acquirer/acquire_lock/log_visitor.rb +82 -15
- data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock/log_visitor.rb +92 -14
- data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rb +441 -63
- 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 +5 -5
- data/lib/redis_queued_locks/acquirer/acquire_lock.rb +176 -39
- data/lib/redis_queued_locks/acquirer/clear_dead_requests.rb +322 -49
- data/lib/redis_queued_locks/acquirer/extend_lock_ttl.rb +125 -13
- 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 +16 -3
- data/lib/redis_queued_locks/acquirer/lock_info.rb +31 -39
- data/lib/redis_queued_locks/acquirer/lock_series_poc/instr_visitor.rb +11 -5
- data/lib/redis_queued_locks/acquirer/lock_series_poc/log_visitor.rb +20 -10
- data/lib/redis_queued_locks/acquirer/lock_series_poc.rb +105 -36
- data/lib/redis_queued_locks/acquirer/locks.rb +32 -11
- data/lib/redis_queued_locks/acquirer/queue_info.rb +30 -8
- data/lib/redis_queued_locks/acquirer/queues.rb +26 -6
- data/lib/redis_queued_locks/acquirer/reindex_locks.rb +117 -4
- data/lib/redis_queued_locks/acquirer/release_all_locks.rb +37 -6
- data/lib/redis_queued_locks/acquirer/release_lock.rb +33 -7
- data/lib/redis_queued_locks/acquirer/release_locks_of.rb +45 -6
- data/lib/redis_queued_locks/acquirer/release_read_lock.rb +22 -3
- data/lib/redis_queued_locks/client.rb +140 -56
- data/lib/redis_queued_locks/config.rb +105 -14
- 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 +137 -12
- data/lib/redis_queued_locks/swarm/flush_zombies.rb +1 -0
- data/lib/redis_queued_locks/swarm/redis_client_builder.rb +8 -4
- data/lib/redis_queued_locks/swarm/swarm_element/threaded.rb +8 -6
- data/lib/redis_queued_locks/swarm/zombie_info.rb +6 -2
- data/lib/redis_queued_locks/swarm.rb +86 -5
- data/lib/redis_queued_locks/utilities.rb +1 -1
- data/lib/redis_queued_locks/version.rb +2 -2
- data/rbs_collection.lock.yaml +4 -0
- data/rbs_collection.yaml +2 -1
- data/sig/manifest.yml +1 -0
- data/sig/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue/log_visitor.rbs +2 -2
- data/sig/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue.rbs +2 -2
- data/sig/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rbs +25 -5
- data/sig/redis_queued_locks/acquirer/acquire_lock/log_visitor.rbs +32 -12
- data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock/log_visitor.rbs +61 -40
- data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rbs +59 -11
- data/sig/redis_queued_locks/acquirer/acquire_lock/{yield_with_expire → yield_expire}/log_visitor.rbs +4 -4
- data/sig/redis_queued_locks/acquirer/acquire_lock/yield_expire.rbs +4 -4
- data/sig/redis_queued_locks/acquirer/acquire_lock.rbs +6 -4
- data/sig/redis_queued_locks/acquirer/clear_dead_requests.rbs +23 -1
- data/sig/redis_queued_locks/acquirer/extend_lock_ttl.rbs +16 -3
- data/sig/redis_queued_locks/acquirer/queues.rbs +1 -1
- data/sig/redis_queued_locks/acquirer/reindex_locks.rbs +22 -1
- data/sig/redis_queued_locks/acquirer/release_lock.rbs +3 -2
- data/sig/redis_queued_locks/acquirer/release_read_lock.rbs +1 -1
- data/sig/redis_queued_locks/client.rbs +24 -10
- data/sig/redis_queued_locks/config.rbs +54 -1
- data/sig/redis_queued_locks/errors.rbs +5 -2
- data/sig/redis_queued_locks/instrument/sampler.rbs +3 -3
- data/sig/redis_queued_locks/instrument/void_notifier.rbs +3 -1
- data/sig/redis_queued_locks/logging/sampler.rbs +3 -3
- data/sig/redis_queued_locks/resource.rbs +23 -13
- data/sig/redis_queued_locks/swarm/swarm_element/threaded.rbs +1 -1
- data/sig/redis_queued_locks/utilities.rbs +2 -1
- data/sig/redis_queued_locks.rbs +7 -0
- data/sig/vendor/redis_client.rbs +1 -0
- metadata +4 -3
- /data/sig/redis_queued_locks/{acquier.rbs → acquirer.rbs} +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# @api private
|
|
4
4
|
# @since 1.13.0
|
|
5
|
-
# @version 1.
|
|
5
|
+
# @version 1.22.0
|
|
6
6
|
# rubocop:disable Metrics/ClassLength
|
|
7
7
|
class RedisQueuedLocks::Config
|
|
8
8
|
require_relative 'config/dsl'
|
|
@@ -32,6 +32,8 @@ class RedisQueuedLocks::Config
|
|
|
32
32
|
setting('is_timed_by_default', false)
|
|
33
33
|
setting('default_conflict_strategy', :wait_for_lock)
|
|
34
34
|
setting('default_access_strategy', :queued)
|
|
35
|
+
setting('lock_priorities', { lowest: -100, low: -50, normal: 0, high: 50, highest: 100 }.freeze)
|
|
36
|
+
setting('default_lock_priority', 0)
|
|
35
37
|
setting('log_sampling_enabled', false)
|
|
36
38
|
setting('log_sampling_percent', 15)
|
|
37
39
|
setting('log_sampler', RedisQueuedLocks::Logging::Sampler)
|
|
@@ -57,36 +59,36 @@ class RedisQueuedLocks::Config
|
|
|
57
59
|
setting('swarm.flush_zombies.redis_config.pool_config', {})
|
|
58
60
|
|
|
59
61
|
validate('swarm.auto_swarm') { |val| val == true || val == false }
|
|
60
|
-
validate('swarm.supervisor.liveness_probing_period') { |val| val.is_a?(Integer) }
|
|
62
|
+
validate('swarm.supervisor.liveness_probing_period') { |val| val.is_a?(Integer) && val > 0 }
|
|
61
63
|
|
|
62
64
|
validate('swarm.probe_hosts.enabled_for_swarm') { |val| val == true || val == false }
|
|
63
65
|
validate('swarm.probe_hosts.redis_config.sentinel') { |val| val == true || val == false }
|
|
64
66
|
validate('swarm.probe_hosts.redis_config.pooled') { |val| val == true || val == false }
|
|
65
67
|
validate('swarm.probe_hosts.redis_config.config') { |val| val.is_a?(Hash) }
|
|
66
68
|
validate('swarm.probe_hosts.redis_config.pool_config') { |val| val.is_a?(Hash) }
|
|
67
|
-
validate('swarm.probe_hosts.probe_period') { |val| val.is_a?(Integer) }
|
|
69
|
+
validate('swarm.probe_hosts.probe_period') { |val| val.is_a?(Integer) && val > 0 }
|
|
68
70
|
|
|
69
71
|
validate('swarm.flush_zombies.enabled_for_swarm') { |val| val == true || val == false }
|
|
70
72
|
validate('swarm.flush_zombies.redis_config.sentinel') { |val| val == true || val == false }
|
|
71
73
|
validate('swarm.flush_zombies.redis_config.pooled') { |val| val == true || val == false }
|
|
72
74
|
validate('swarm.flush_zombies.redis_config.config') { |val| val.is_a?(Hash) }
|
|
73
75
|
validate('swarm.flush_zombies.redis_config.pool_config') { |val| val.is_a?(Hash) }
|
|
74
|
-
validate('swarm.flush_zombies.zombie_ttl') { |val| val.is_a?(Integer) }
|
|
75
|
-
validate('swarm.flush_zombies.zombie_lock_scan_size') { |val| val.is_a?(Integer) }
|
|
76
|
-
validate('swarm.flush_zombies.zombie_queue_scan_size') { |val| val.is_a?(Integer) }
|
|
77
|
-
validate('swarm.flush_zombies.zombie_flush_period') { |val| val.is_a?(Integer) }
|
|
76
|
+
validate('swarm.flush_zombies.zombie_ttl') { |val| val.is_a?(Integer) && val > 0 }
|
|
77
|
+
validate('swarm.flush_zombies.zombie_lock_scan_size') { |val| val.is_a?(Integer) && val > 0 }
|
|
78
|
+
validate('swarm.flush_zombies.zombie_queue_scan_size') { |val| val.is_a?(Integer) && val > 0 }
|
|
79
|
+
validate('swarm.flush_zombies.zombie_flush_period') { |val| val.is_a?(Integer) && val > 0 }
|
|
78
80
|
|
|
79
81
|
validate('retry_count') { |val| val == nil || (val.is_a?(Integer) && val >= 0) }
|
|
80
82
|
validate('retry_delay') { |val| val.is_a?(Integer) && val >= 0 }
|
|
81
83
|
validate('retry_jitter') { |val| val.is_a?(Integer) && val >= 0 }
|
|
82
84
|
validate('try_to_lock_timeout') { |val| val == nil || (val.is_a?(Integer) && val >= 0) }
|
|
83
|
-
validate('default_lock_ttl') { |val| val.is_a?(Integer) }
|
|
84
|
-
validate('default_queue_ttl') { |val| val.is_a?(Integer) }
|
|
85
|
+
validate('default_lock_ttl') { |val| val.is_a?(Integer) && val > 0 }
|
|
86
|
+
validate('default_queue_ttl') { |val| val.is_a?(Integer) && val > 0 }
|
|
85
87
|
validate('detailed_acq_timeout_error') { |val| val == true || val == false }
|
|
86
|
-
validate('lock_release_batch_size') { |val| val.is_a?(Integer) }
|
|
87
|
-
validate('clear_locks_of__lock_scan_size') { |val| val.is_a?(Integer) }
|
|
88
|
-
validate('clear_locks_of__queue_scan_size') { |val| val.is_a?(Integer) }
|
|
89
|
-
validate('key_extraction_batch_size') { |val| val.is_a?(Integer) }
|
|
88
|
+
validate('lock_release_batch_size') { |val| val.is_a?(Integer) && val > 0 }
|
|
89
|
+
validate('clear_locks_of__lock_scan_size') { |val| val.is_a?(Integer) && val > 0 }
|
|
90
|
+
validate('clear_locks_of__queue_scan_size') { |val| val.is_a?(Integer) && val > 0 }
|
|
91
|
+
validate('key_extraction_batch_size') { |val| val.is_a?(Integer) && val > 0 }
|
|
90
92
|
validate('instrumenter') { |val| RedisQueuedLocks::Instrument.valid_interface?(val) }
|
|
91
93
|
validate('uniq_identifier') { |val| val.is_a?(Proc) }
|
|
92
94
|
validate('logger') { |val| RedisQueuedLocks::Logging.valid_interface?(val) }
|
|
@@ -115,6 +117,27 @@ class RedisQueuedLocks::Config
|
|
|
115
117
|
val == :random
|
|
116
118
|
# rubocop:enable Layout/MultilineOperationIndentation
|
|
117
119
|
end
|
|
120
|
+
validate('lock_priorities') do |val|
|
|
121
|
+
val.is_a?(Hash) && val.all? do |name, priority|
|
|
122
|
+
# rubocop:disable Layout/MultilineOperationIndentation
|
|
123
|
+
name.is_a?(Symbol) &&
|
|
124
|
+
priority.is_a?(Integer) &&
|
|
125
|
+
priority >= RedisQueuedLocks::Resource::MIN_LOCK_PRIORITY &&
|
|
126
|
+
priority <= RedisQueuedLocks::Resource::MAX_LOCK_PRIORITY
|
|
127
|
+
# rubocop:enable Layout/MultilineOperationIndentation
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
# NOTE: a Symbol is a priority name of `config['lock_priorities']`: a validator gets one value,
|
|
131
|
+
# so the name is checked by the configuration itself (`prevent_default_priority__unknown_name`:
|
|
132
|
+
# once after a configuration block, the keys can be assigned in any order; before a direct
|
|
133
|
+
# assignment is applied);
|
|
134
|
+
validate('default_lock_priority') do |val|
|
|
135
|
+
val.is_a?(Symbol) || (
|
|
136
|
+
val.is_a?(Integer) &&
|
|
137
|
+
val >= RedisQueuedLocks::Resource::MIN_LOCK_PRIORITY &&
|
|
138
|
+
val <= RedisQueuedLocks::Resource::MAX_LOCK_PRIORITY
|
|
139
|
+
)
|
|
140
|
+
end
|
|
118
141
|
|
|
119
142
|
# @return [Hash<Symbol,Any>]
|
|
120
143
|
#
|
|
@@ -124,10 +147,14 @@ class RedisQueuedLocks::Config
|
|
|
124
147
|
|
|
125
148
|
# @return [void]
|
|
126
149
|
#
|
|
150
|
+
# @raise [RedisQueuedLocks::ConfigValidationError]
|
|
151
|
+
#
|
|
127
152
|
# @api private
|
|
128
153
|
# @since 1.13.0
|
|
154
|
+
# @version 1.22.0
|
|
129
155
|
def initialize(&configuration)
|
|
130
156
|
@config_state = {}
|
|
157
|
+
@configuring = false
|
|
131
158
|
config_setters.each_value { |setter| setter.call(@config_state) }
|
|
132
159
|
configure(&configuration)
|
|
133
160
|
end
|
|
@@ -137,10 +164,40 @@ class RedisQueuedLocks::Config
|
|
|
137
164
|
# @yieldparam [?]
|
|
138
165
|
# @return [void]
|
|
139
166
|
#
|
|
167
|
+
# @raise [RedisQueuedLocks::ConfigValidationError]
|
|
168
|
+
#
|
|
140
169
|
# @api private
|
|
141
170
|
# @since 1.13.0
|
|
171
|
+
# @version 1.22.0
|
|
142
172
|
def configure(&configuration)
|
|
143
|
-
|
|
173
|
+
return unless block_given?
|
|
174
|
+
|
|
175
|
+
# NOTE: (priorities) the keys of the block can be assigned in any order: the priority name of
|
|
176
|
+
# `default_lock_priority` is checked against `lock_priorities` once, after the block (after
|
|
177
|
+
# the outer block for nested blocks); an inconsistent pair is never applied: both keys keep
|
|
178
|
+
# their values from before the block (also when the block fails: its error is raised);
|
|
179
|
+
lock_priorities = config_state['lock_priorities']
|
|
180
|
+
default_lock_priority = config_state['default_lock_priority']
|
|
181
|
+
nested = @configuring
|
|
182
|
+
completed = false
|
|
183
|
+
@configuring = true
|
|
184
|
+
begin
|
|
185
|
+
yield(self)
|
|
186
|
+
completed = true
|
|
187
|
+
ensure
|
|
188
|
+
@configuring = nested
|
|
189
|
+
unless nested
|
|
190
|
+
begin
|
|
191
|
+
prevent_default_priority__unknown_name(
|
|
192
|
+
config_state['lock_priorities'], config_state['default_lock_priority']
|
|
193
|
+
)
|
|
194
|
+
rescue RedisQueuedLocks::ConfigValidationError
|
|
195
|
+
config_state['lock_priorities'] = lock_priorities
|
|
196
|
+
config_state['default_lock_priority'] = default_lock_priority
|
|
197
|
+
raise if completed
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
144
201
|
end
|
|
145
202
|
|
|
146
203
|
# @param config_key [String]
|
|
@@ -164,9 +221,21 @@ class RedisQueuedLocks::Config
|
|
|
164
221
|
#
|
|
165
222
|
# @api public
|
|
166
223
|
# @since 1.13.0
|
|
224
|
+
# @version 1.22.0
|
|
167
225
|
def []=(config_key, config_value)
|
|
168
226
|
prevent_key__non_existent(config_key)
|
|
169
227
|
prevent_key__invalid_type(config_key, config_value)
|
|
228
|
+
# NOTE: (priorities) a direct assignment (out of a configuration block) keeps the priority name
|
|
229
|
+
# of `default_lock_priority` a key of `lock_priorities` (a configuration block checks it once
|
|
230
|
+
# after the block, see `configure`);
|
|
231
|
+
unless @configuring
|
|
232
|
+
case config_key
|
|
233
|
+
when 'lock_priorities'
|
|
234
|
+
prevent_default_priority__unknown_name(config_value, config_state['default_lock_priority'])
|
|
235
|
+
when 'default_lock_priority'
|
|
236
|
+
prevent_default_priority__unknown_name(config_state['lock_priorities'], config_value)
|
|
237
|
+
end
|
|
238
|
+
end
|
|
170
239
|
config_state[config_key] = config_value
|
|
171
240
|
end
|
|
172
241
|
|
|
@@ -237,5 +306,27 @@ class RedisQueuedLocks::Config
|
|
|
237
306
|
)
|
|
238
307
|
end
|
|
239
308
|
end
|
|
309
|
+
|
|
310
|
+
# A priority name of the default lock request priority should be a priority name of
|
|
311
|
+
# `lock_priorities` (an unknown name would fail each lock acquirement with the default priority).
|
|
312
|
+
#
|
|
313
|
+
# @param lock_priorities [Hash<Symbol,Integer>] The priority names.
|
|
314
|
+
# @param default_lock_priority [Integer,Symbol] The default lock request priority.
|
|
315
|
+
# @return [void]
|
|
316
|
+
#
|
|
317
|
+
# @raise [RedisQueuedLocks::ConfigValidationError]
|
|
318
|
+
#
|
|
319
|
+
# @api private
|
|
320
|
+
# @since 1.22.0
|
|
321
|
+
def prevent_default_priority__unknown_name(lock_priorities, default_lock_priority)
|
|
322
|
+
if default_lock_priority.is_a?(Symbol) && !lock_priorities.key?(default_lock_priority)
|
|
323
|
+
raise(
|
|
324
|
+
RedisQueuedLocks::ConfigValidationError,
|
|
325
|
+
"The priority name #{default_lock_priority.inspect} of the 'default_lock_priority' " \
|
|
326
|
+
"config key is not a key of the 'lock_priorities' config key " \
|
|
327
|
+
"(#{lock_priorities.keys.map(&:inspect).join(', ')})."
|
|
328
|
+
)
|
|
329
|
+
end
|
|
330
|
+
end
|
|
240
331
|
end
|
|
241
332
|
# rubocop:enable Metrics/ClassLength
|
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
# the one-line definition style (like "Error = Class.new(::StandardError)")
|
|
5
5
|
# is not used cuz that style can not be correctly defined via RBS types: we can not
|
|
6
6
|
# identify the superclass of our class for a constant;
|
|
7
|
+
# NOTE: (error hierarchy) each error inherits from the global base error of its branch:
|
|
8
|
+
# - `Error` (`::StandardError`) - the main hierarchy;
|
|
9
|
+
# - `ArgumentError` (`::ArgumentError`) - invalid arguments (the Ruby core branch that can not be
|
|
10
|
+
# a part of the main hierarchy);
|
|
11
|
+
# - `Exception` (`::Exception`) - super exceptional cases only: each error of this branch
|
|
12
|
+
# describes why it is not a `StandardError`;
|
|
7
13
|
module RedisQueuedLocks
|
|
8
14
|
# @api public
|
|
9
15
|
# @since 1.0.0
|
|
@@ -13,13 +19,29 @@ module RedisQueuedLocks
|
|
|
13
19
|
# @since 1.0.0
|
|
14
20
|
class ArgumentError < ::ArgumentError; end
|
|
15
21
|
|
|
22
|
+
# The base of the errors that should not be rescued by the code they interrupt
|
|
23
|
+
# (`rescue => e`, `rescue StandardError`, `rescue nil`): like `Timeout::ExitException`,
|
|
24
|
+
# they are rescued only by `rescue Exception` (and by the library itself).
|
|
25
|
+
# NOTE: inside `module RedisQueuedLocks` a bare `Exception` refers to this class;
|
|
26
|
+
#
|
|
27
|
+
# @api private
|
|
28
|
+
# @since 1.21.0
|
|
29
|
+
class Exception < ::Exception; end # rubocop:disable Lint/InheritException
|
|
30
|
+
|
|
16
31
|
# @api public
|
|
17
32
|
# @since 1.0.0
|
|
18
33
|
class LockAlreadyObtainedError < Error; end
|
|
19
34
|
|
|
35
|
+
# Interrupts the lock acquirement after the `timeout` (`Timeout.timeout`); rescued by
|
|
36
|
+
# `WithAcqTimeout` (=> `LockAcquirementTimeoutError` / `:timeout_reached`).
|
|
37
|
+
# NOTE: (why `Exception`) the acquirement calls the logger and the instrumenter: a `StandardError`
|
|
38
|
+
# that interrupts them is swallowed by `rescue nil` of the log/instrumentation events, and the
|
|
39
|
+
# acquirement continues without the timeout (with `retry_count: nil` - without any limit);
|
|
40
|
+
#
|
|
20
41
|
# @api private
|
|
21
42
|
# @since 1.11.0
|
|
22
|
-
|
|
43
|
+
# @version 1.21.0
|
|
44
|
+
class LockAcquirementIntermediateTimeoutError < Exception; end
|
|
23
45
|
|
|
24
46
|
# @api public
|
|
25
47
|
# @since 1.0.0
|
|
@@ -29,11 +51,19 @@ module RedisQueuedLocks
|
|
|
29
51
|
# @since 1.0.0
|
|
30
52
|
class LockAcquirementRetryLimitError < Error; end
|
|
31
53
|
|
|
54
|
+
# Interrupts the `timed: true` block after the lock ttl (`Timeout.timeout`); rescued by
|
|
55
|
+
# `YieldExpire` (=> `TimedLockTimeoutError`).
|
|
56
|
+
# NOTE: (why `Exception`) a `StandardError` is swallowed by `rescue => e` / `rescue nil` of
|
|
57
|
+
# the user block: the block continues after the lock ttl (without the mutual exclusion:
|
|
58
|
+
# another acquirer can obtain the expired lock, and the write lock release after the block
|
|
59
|
+
# deletes its lock);
|
|
60
|
+
#
|
|
32
61
|
# @api private
|
|
33
62
|
# @since 1.12.0
|
|
34
|
-
|
|
63
|
+
# @version 1.21.0
|
|
64
|
+
class TimedLockIntermediateTimeoutError < Exception; end
|
|
35
65
|
|
|
36
|
-
# @api
|
|
66
|
+
# @api public
|
|
37
67
|
# @since 1.0.0
|
|
38
68
|
class TimedLockTimeoutError < Error; end
|
|
39
69
|
|
|
@@ -57,7 +87,7 @@ module RedisQueuedLocks
|
|
|
57
87
|
# @since 1.13.0
|
|
58
88
|
class ConfigNotFoundError < ConfigError; end
|
|
59
89
|
|
|
60
|
-
# @api
|
|
90
|
+
# @api public
|
|
61
91
|
# @since 1.13.0
|
|
62
92
|
class ConfigValidationError < ConfigError; end
|
|
63
93
|
end
|
|
@@ -3,12 +3,6 @@
|
|
|
3
3
|
# @api public
|
|
4
4
|
# @since 1.6.0
|
|
5
5
|
module RedisQueuedLocks::Instrument::Sampler
|
|
6
|
-
# @return [Range]
|
|
7
|
-
#
|
|
8
|
-
# @api private
|
|
9
|
-
# @since 1.6.0
|
|
10
|
-
SAMPLING_PERCENT_RANGE = (0..100)
|
|
11
|
-
|
|
12
6
|
class << self
|
|
13
7
|
# Super simple probalistic function based on the `weight` of <true>/<false> values.
|
|
14
8
|
# Requires the <percent> parameter as the required percent of <true> values sampled.
|
|
@@ -20,8 +14,11 @@ module RedisQueuedLocks::Instrument::Sampler
|
|
|
20
14
|
#
|
|
21
15
|
# @api public
|
|
22
16
|
# @since 1.6.0
|
|
17
|
+
# @version 1.21.0
|
|
23
18
|
def sampling_happened?(sampling_percent)
|
|
24
|
-
|
|
19
|
+
# NOTE: one of 100 equally probable values (0..99): `sampling_percent` of them are lower
|
|
20
|
+
# (0% => never, 100% => always);
|
|
21
|
+
SecureRandom.rand(100) < sampling_percent
|
|
25
22
|
end
|
|
26
23
|
end
|
|
27
24
|
end
|
|
@@ -3,12 +3,6 @@
|
|
|
3
3
|
# @api public
|
|
4
4
|
# @since 1.5.0
|
|
5
5
|
module RedisQueuedLocks::Logging::Sampler
|
|
6
|
-
# @return [Range]
|
|
7
|
-
#
|
|
8
|
-
# @api private
|
|
9
|
-
# @since 1.6.0
|
|
10
|
-
SAMPLING_PERCENT_RANGE = (0..100)
|
|
11
|
-
|
|
12
6
|
class << self
|
|
13
7
|
# Super simple probalistic function based on the `weight` of <true>/<false> values.
|
|
14
8
|
# Requires the <percent> parameter as the required percent of <true> values sampled.
|
|
@@ -20,9 +14,11 @@ module RedisQueuedLocks::Logging::Sampler
|
|
|
20
14
|
#
|
|
21
15
|
# @api public
|
|
22
16
|
# @since 1.5.0
|
|
23
|
-
# @version 1.
|
|
17
|
+
# @version 1.21.0
|
|
24
18
|
def sampling_happened?(sampling_percent)
|
|
25
|
-
|
|
19
|
+
# NOTE: one of 100 equally probable values (0..99): `sampling_percent` of them are lower
|
|
20
|
+
# (0% => never, 100% => always);
|
|
21
|
+
SecureRandom.rand(100) < sampling_percent
|
|
26
22
|
end
|
|
27
23
|
end
|
|
28
24
|
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# @api private
|
|
4
4
|
# @since 1.0.0
|
|
5
|
+
# @version 1.22.0
|
|
5
6
|
# rubocop:disable Metrics/ModuleLength
|
|
6
7
|
module RedisQueuedLocks::Resource
|
|
7
8
|
# @return [String]
|
|
@@ -89,6 +90,44 @@ module RedisQueuedLocks::Resource
|
|
|
89
90
|
# @since 1.3.0
|
|
90
91
|
REDIS_TIMESHIFT_ERROR = 2
|
|
91
92
|
|
|
93
|
+
# The lowest lock request priority (the `priority` option of `#lock`).
|
|
94
|
+
#
|
|
95
|
+
# @return [Integer]
|
|
96
|
+
#
|
|
97
|
+
# @api private
|
|
98
|
+
# @since 1.22.0
|
|
99
|
+
MIN_LOCK_PRIORITY = -100
|
|
100
|
+
|
|
101
|
+
# The highest lock request priority (the `priority` option of `#lock`).
|
|
102
|
+
#
|
|
103
|
+
# @return [Integer]
|
|
104
|
+
#
|
|
105
|
+
# @api private
|
|
106
|
+
# @since 1.22.0
|
|
107
|
+
MAX_LOCK_PRIORITY = 100
|
|
108
|
+
|
|
109
|
+
# Lock request priority band: the position of the lock request in the lock request queue (the
|
|
110
|
+
# score) is the enqueue time (epoch, seconds) shifted by `priority * LOCK_PRIORITY_BAND` down:
|
|
111
|
+
# - each priority gets its own range of positions (the band): requests of the higher priority
|
|
112
|
+
# are always ahead of the requests of the lower priority, requests of the same priority are
|
|
113
|
+
# ordered by the enqueue time (FIFO);
|
|
114
|
+
# - 2**32 seconds (~136 years) is greater than any enqueue time (epoch) up to the year 2106,
|
|
115
|
+
# so the bands never overlap and the priority is decoded from the position unambiguously;
|
|
116
|
+
# - the default priority (`0`) keeps the position equal to the enqueue time (the positions of
|
|
117
|
+
# the RQL versions without priorities);
|
|
118
|
+
# - the band of the priority `p` is `(-p * LOCK_PRIORITY_BAND, (1 - p) * LOCK_PRIORITY_BAND)`;
|
|
119
|
+
# - the enqueue time is decoded from the position of any priority, so dead lock requests of any
|
|
120
|
+
# band are recognized by the position (the time barrier of the band, see
|
|
121
|
+
# `lock_priority_band_dead_score`) without the knowledge of the priority range;
|
|
122
|
+
# - the precision of the positions (the FIFO precision inside a priority) depends on the
|
|
123
|
+
# shift: ~1 µs for -1..1, ~61 µs for -100..100 (`MIN_LOCK_PRIORITY`..`MAX_LOCK_PRIORITY`):
|
|
124
|
+
# requests of the same priority made within this interval are ordered by acquirer ids;
|
|
125
|
+
# @return [Integer] In seconds.
|
|
126
|
+
#
|
|
127
|
+
# @api private
|
|
128
|
+
# @since 1.22.0
|
|
129
|
+
LOCK_PRIORITY_BAND = 4_294_967_296
|
|
130
|
+
|
|
92
131
|
# rubocop:disable Metrics/ClassLength
|
|
93
132
|
class << self
|
|
94
133
|
# Returns 16-byte unique identifier. It is used for uniquely
|
|
@@ -275,6 +314,22 @@ module RedisQueuedLocks::Resource
|
|
|
275
314
|
::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond)
|
|
276
315
|
end
|
|
277
316
|
|
|
317
|
+
# Is the lock name of the lock index expired (all the keys of the lock are expired)?
|
|
318
|
+
#
|
|
319
|
+
# NOTE: the lock index replies of ZSCAN have String scores: `+inf` (a key without TTL indexed
|
|
320
|
+
# by `ReindexLocks`) is `"inf"`, and `String#to_f` turns it into `0.0` (the name would be
|
|
321
|
+
# skipped as an expired one by every lock index lookup): it never expires;
|
|
322
|
+
#
|
|
323
|
+
# @param lock_index_score [String] The score of the lock name (a ZSCAN reply).
|
|
324
|
+
# @param lock_index_time [Integer] The current client time (see `lock_index_time`).
|
|
325
|
+
# @return [Boolean]
|
|
326
|
+
#
|
|
327
|
+
# @api private
|
|
328
|
+
# @since 1.22.0
|
|
329
|
+
def lock_index_score_expired?(lock_index_score, lock_index_time)
|
|
330
|
+
lock_index_score != 'inf' && lock_index_score.to_f < lock_index_time
|
|
331
|
+
end
|
|
332
|
+
|
|
278
333
|
# @param lock_readers_key [String]
|
|
279
334
|
# @return [String]
|
|
280
335
|
#
|
|
@@ -326,16 +381,23 @@ module RedisQueuedLocks::Resource
|
|
|
326
381
|
(Integer(redis_time[0]) * 1_000) + (Integer(redis_time[1]) / 1_000)
|
|
327
382
|
end
|
|
328
383
|
|
|
329
|
-
# @
|
|
384
|
+
# @param priority [Integer]
|
|
385
|
+
# The lock request priority (from `MIN_LOCK_PRIORITY` to `MAX_LOCK_PRIORITY`).
|
|
386
|
+
# @return [Float]
|
|
387
|
+
# Redis's <Set> score: the time (epoch, as a float) shifted by the priority band (see
|
|
388
|
+
# `LOCK_PRIORITY_BAND`; the default priority `0` keeps the time).
|
|
330
389
|
#
|
|
331
390
|
# @api private
|
|
332
391
|
# @since 1.0.0
|
|
333
|
-
|
|
334
|
-
|
|
392
|
+
# @version 1.22.0
|
|
393
|
+
def calc_initial_acquirer_position(priority)
|
|
394
|
+
Time.now.to_f - (priority * LOCK_PRIORITY_BAND)
|
|
335
395
|
end
|
|
336
396
|
|
|
337
397
|
# @param queue_ttl [Numeric] In seconds
|
|
338
|
-
# @return [Float]
|
|
398
|
+
# @return [Float]
|
|
399
|
+
# Redis's <Set> score barrier for acquirers that should be removed from queue (the time
|
|
400
|
+
# barrier: the barrier of the default priority band, see `lock_priority_band_dead_score`).
|
|
339
401
|
#
|
|
340
402
|
# @api private
|
|
341
403
|
# @since 1.0.0
|
|
@@ -343,6 +405,57 @@ module RedisQueuedLocks::Resource
|
|
|
343
405
|
Time.now.to_f - queue_ttl
|
|
344
406
|
end
|
|
345
407
|
|
|
408
|
+
# The lowest position (score) of the lock requests of the given priority band (see
|
|
409
|
+
# `LOCK_PRIORITY_BAND`): the lower bound of the dead lock request removal (`ZREMRANGEBYSCORE`).
|
|
410
|
+
#
|
|
411
|
+
# @param priority [Integer] Any priority (the bands of all priorities have the same layout).
|
|
412
|
+
# @return [Integer]
|
|
413
|
+
# The lower edge of the band (the positions of the band never reach its edges: an enqueue
|
|
414
|
+
# time is always greater than 0 and less than `LOCK_PRIORITY_BAND`).
|
|
415
|
+
#
|
|
416
|
+
# @api private
|
|
417
|
+
# @since 1.22.0
|
|
418
|
+
def lock_priority_band_floor(priority)
|
|
419
|
+
-priority * LOCK_PRIORITY_BAND
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
# Requests of the given priority band queued at this position or earlier are dead (the upper
|
|
423
|
+
# bound of the dead lock request removal, see `LOCK_PRIORITY_BAND`).
|
|
424
|
+
#
|
|
425
|
+
# @param dead_score [Float] The time barrier (see `acquirer_dead_score`).
|
|
426
|
+
# @param priority [Integer]
|
|
427
|
+
# @return [Float]
|
|
428
|
+
#
|
|
429
|
+
# @api private
|
|
430
|
+
# @since 1.22.0
|
|
431
|
+
def lock_priority_band_dead_score(dead_score, priority)
|
|
432
|
+
dead_score - (priority * LOCK_PRIORITY_BAND)
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
# Priority of the lock request by its position in the lock request queue (the score).
|
|
436
|
+
#
|
|
437
|
+
# NOTE: (infinite positions) RQL never writes infinite positions (manual writes of the lock
|
|
438
|
+
# request queues): they are out of all bands and are decoded to the bounds of the priority
|
|
439
|
+
# range (the floor of an infinite Float raises `FloatDomainError`):
|
|
440
|
+
# - `-inf`: `MAX_LOCK_PRIORITY` (ahead of all lock requests; such a request is dead: its
|
|
441
|
+
# enqueue time is unknown, it is dropped like before priorities);
|
|
442
|
+
# - `+inf`: `MIN_LOCK_PRIORITY` (behind all lock requests; never dead);
|
|
443
|
+
#
|
|
444
|
+
# @param acquirer_position [Numeric] The position (the score) of the lock request.
|
|
445
|
+
# @return [Integer] See `LOCK_PRIORITY_BAND`.
|
|
446
|
+
#
|
|
447
|
+
# @api private
|
|
448
|
+
# @since 1.22.0
|
|
449
|
+
def lock_request_priority(acquirer_position)
|
|
450
|
+
if acquirer_position == -Float::INFINITY
|
|
451
|
+
MAX_LOCK_PRIORITY
|
|
452
|
+
elsif acquirer_position == Float::INFINITY
|
|
453
|
+
MIN_LOCK_PRIORITY
|
|
454
|
+
else
|
|
455
|
+
-(acquirer_position / LOCK_PRIORITY_BAND).floor
|
|
456
|
+
end
|
|
457
|
+
end
|
|
458
|
+
|
|
346
459
|
# @param zombie_ttl [Float] In seconds with milliseconds.
|
|
347
460
|
# @return [Float]
|
|
348
461
|
#
|
|
@@ -353,8 +466,10 @@ module RedisQueuedLocks::Resource
|
|
|
353
466
|
end
|
|
354
467
|
|
|
355
468
|
# @param acquirer_position [Float]
|
|
356
|
-
# A time (epoch, seconds.milliseconds) that represents
|
|
357
|
-
# the acquirer position in lock request queue.
|
|
469
|
+
# A time (epoch, seconds.milliseconds) shifted by the priority band that represents
|
|
470
|
+
# the acquirer position in lock request queue (see `calc_initial_acquirer_position`).
|
|
471
|
+
# @param priority [Integer]
|
|
472
|
+
# The lock request priority (the band of the position).
|
|
358
473
|
# @param queue_ttl [Integer]
|
|
359
474
|
# In second.
|
|
360
475
|
# @return [Boolean]
|
|
@@ -362,18 +477,28 @@ module RedisQueuedLocks::Resource
|
|
|
362
477
|
#
|
|
363
478
|
# @api private
|
|
364
479
|
# @since 1.0.0
|
|
365
|
-
|
|
366
|
-
|
|
480
|
+
# @version 1.22.0
|
|
481
|
+
def dead_score_reached?(acquirer_position, priority, queue_ttl)
|
|
482
|
+
(acquirer_position + (priority * LOCK_PRIORITY_BAND) + queue_ttl) < Time.now.to_f
|
|
367
483
|
end
|
|
368
484
|
|
|
369
|
-
#
|
|
370
|
-
#
|
|
485
|
+
# NOTE: (debug) not used by the library: a manual runtime check (in a console) of the lock that
|
|
486
|
+
# a queue belongs to (e.g. `Resource.lock_key_from_queue(client.queues.first)`);
|
|
487
|
+
#
|
|
488
|
+
# @param lock_queue [String] `rql:lock_queue:<name>` or `rql:lock_read_queue:<name>`.
|
|
489
|
+
# @return [String,NilClass]
|
|
490
|
+
# - `rql:lock:<name>` for the write lock request queue and for the read lock request queue;
|
|
491
|
+
# - `nil` for any other key;
|
|
371
492
|
#
|
|
372
493
|
# @api private
|
|
373
494
|
# @since 1.0.0
|
|
495
|
+
# @version 1.21.0
|
|
374
496
|
def lock_key_from_queue(lock_queue)
|
|
375
|
-
|
|
376
|
-
|
|
497
|
+
key_family = lock_key_family(lock_queue)
|
|
498
|
+
return unless key_family == :lock_queue || key_family == :read_lock_queue
|
|
499
|
+
|
|
500
|
+
lock_name = lock_name_from_key(lock_queue, key_family) #: String
|
|
501
|
+
prepare_lock_key(lock_name)
|
|
377
502
|
end
|
|
378
503
|
|
|
379
504
|
# @return [Integer]
|
|
@@ -7,19 +7,23 @@ module RedisQueuedLocks::Swarm::RedisClientBuilder
|
|
|
7
7
|
# @option pooled [Boolean]
|
|
8
8
|
# @option sentinel [Boolean]
|
|
9
9
|
# @option config [Hash]
|
|
10
|
+
# @option pool_config [Hash]
|
|
10
11
|
# @return [RedisClient|RedisClient::Pooled]
|
|
11
12
|
#
|
|
12
13
|
# @api private
|
|
13
14
|
# @since 1.9.0
|
|
15
|
+
# @version 1.21.0
|
|
14
16
|
# rubocop:disable Style/RedundantAssignment
|
|
15
17
|
def build(pooled: false, sentinel: false, config: {}, pool_config: {})
|
|
16
|
-
config
|
|
17
|
-
|
|
18
|
+
# NOTE: the passed hashes are not mutated: they are the values of the client config (they
|
|
19
|
+
# can be frozen and are shared with the zombie flusher Ractor when they are shareable);
|
|
20
|
+
redis_options = config.transform_keys(&:to_sym)
|
|
21
|
+
pool_options = pool_config.transform_keys(&:to_sym)
|
|
18
22
|
|
|
19
23
|
redis_config =
|
|
20
|
-
sentinel ? sentinel_config(
|
|
24
|
+
sentinel ? sentinel_config(redis_options) : non_sentinel_config(redis_options)
|
|
21
25
|
redis_client =
|
|
22
|
-
pooled ? pooled_client(redis_config,
|
|
26
|
+
pooled ? pooled_client(redis_config, pool_options) : non_pooled_client(redis_config)
|
|
23
27
|
|
|
24
28
|
redis_client
|
|
25
29
|
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# @api private
|
|
4
4
|
# @since 1.9.0
|
|
5
|
+
# @version 1.21.1
|
|
5
6
|
# rubocop:disable Metrics/ClassLength
|
|
6
7
|
class RedisQueuedLocks::Swarm::SwarmElement::Threaded
|
|
7
8
|
# @since 1.9.0
|
|
@@ -69,7 +70,7 @@ class RedisQueuedLocks::Swarm::SwarmElement::Threaded
|
|
|
69
70
|
return unless enabled?
|
|
70
71
|
|
|
71
72
|
sync.synchronize do
|
|
72
|
-
|
|
73
|
+
swarm_element__terminate
|
|
73
74
|
swarm!
|
|
74
75
|
swarm_loop__start
|
|
75
76
|
end
|
|
@@ -78,7 +79,7 @@ class RedisQueuedLocks::Swarm::SwarmElement::Threaded
|
|
|
78
79
|
# @return [void]
|
|
79
80
|
#
|
|
80
81
|
# @api private
|
|
81
|
-
# @since
|
|
82
|
+
# @since 1.9.0
|
|
82
83
|
def reswarm_if_dead!
|
|
83
84
|
return unless enabled?
|
|
84
85
|
|
|
@@ -98,7 +99,7 @@ class RedisQueuedLocks::Swarm::SwarmElement::Threaded
|
|
|
98
99
|
# @since 1.9.0
|
|
99
100
|
def try_kill!
|
|
100
101
|
sync.synchronize do
|
|
101
|
-
|
|
102
|
+
swarm_element__terminate
|
|
102
103
|
end
|
|
103
104
|
end
|
|
104
105
|
|
|
@@ -154,9 +155,9 @@ class RedisQueuedLocks::Swarm::SwarmElement::Threaded
|
|
|
154
155
|
|
|
155
156
|
# Swarm element lifecycle have the following flow:
|
|
156
157
|
# => 1) init (swarm!): create a thread, main loop is not started;
|
|
157
|
-
# => 2) start (swarm_loop__start): run the main
|
|
158
|
+
# => 2) start (swarm_loop__start): run the main loop inside the created thread;
|
|
158
159
|
# => 3) stop (swarm_loop__stop): stop the main loop inside the created thread;
|
|
159
|
-
# => 4) terminate (
|
|
160
|
+
# => 4) terminate (swarm_element__terminate): kill the created thread;
|
|
160
161
|
# In addition you should implement `#spawn_main_loop!` method that will be the main logic
|
|
161
162
|
# of the your swarm element.
|
|
162
163
|
#
|
|
@@ -340,7 +341,8 @@ class RedisQueuedLocks::Swarm::SwarmElement::Threaded
|
|
|
340
341
|
#
|
|
341
342
|
# @api private
|
|
342
343
|
# @since 1.9.0
|
|
343
|
-
|
|
344
|
+
# @version 1.21.1
|
|
345
|
+
def swarm_element__terminate
|
|
344
346
|
return if idle? || swarmed__dead?
|
|
345
347
|
sync.synchronize do
|
|
346
348
|
main_loop&.kill
|
|
@@ -232,7 +232,8 @@ module RedisQueuedLocks::Swarm::ZombieInfo
|
|
|
232
232
|
# names, the full keyspace is never scanned): write locks and readers registries are processed
|
|
233
233
|
# in batches of `lock_scan_size` locks with one round trip per batch and per key family
|
|
234
234
|
# (batches are collected across ZSCAN pages). Expired lock names of the index are skipped
|
|
235
|
-
# (all their keys are expired
|
|
235
|
+
# (all their keys are expired; `+inf` names, keys without TTL, never expire: see
|
|
236
|
+
# `RedisQueuedLocks::Resource.lock_index_score_expired?`).
|
|
236
237
|
#
|
|
237
238
|
# @param rconn [RedisClient] redis connection obtained via `#with` from RedisClient instance;
|
|
238
239
|
# @param zombie_hosts [Set<String>]
|
|
@@ -244,6 +245,7 @@ module RedisQueuedLocks::Swarm::ZombieInfo
|
|
|
244
245
|
#
|
|
245
246
|
# @api private
|
|
246
247
|
# @since 1.20.0
|
|
248
|
+
# @version 1.22.0
|
|
247
249
|
def each_lock_index_zombie_lock(rconn, zombie_hosts, lock_scan_size, &block)
|
|
248
250
|
# @type var lock_keys: Array[String]
|
|
249
251
|
lock_keys = []
|
|
@@ -255,7 +257,9 @@ module RedisQueuedLocks::Swarm::ZombieInfo
|
|
|
255
257
|
RedisQueuedLocks::Resource::LOCK_INDEX_KEY, count: lock_scan_size
|
|
256
258
|
) do |lock_name, lock_index_score|
|
|
257
259
|
# NOTE: the expired lock name (not dropped from the index yet): its keys are expired
|
|
258
|
-
next if
|
|
260
|
+
next if RedisQueuedLocks::Resource.lock_index_score_expired?(
|
|
261
|
+
lock_index_score, lock_index_time
|
|
262
|
+
)
|
|
259
263
|
|
|
260
264
|
lock_keys << RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
|
|
261
265
|
lock_readers_keys << RedisQueuedLocks::Resource.prepare_lock_readers(lock_name)
|