redis_queued_locks 1.20.0 → 1.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/project-overview.md +35 -13
  3. data/.claude/rules/acquirer.md +10 -9
  4. data/.claude/rules/logic.md +28 -5
  5. data/.claude/rules/swarm.md +2 -2
  6. data/.claude/rules/tests.md +6 -3
  7. data/.claude/rules/visitors.md +9 -10
  8. data/.claude/skills/rql-code-review/SKILL.md +24 -10
  9. data/CHANGELOG.md +126 -0
  10. data/CLAUDE.md +2 -1
  11. data/README.md +245 -70
  12. data/lib/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue/log_visitor.rb +1 -0
  13. data/lib/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rb +42 -0
  14. data/lib/redis_queued_locks/acquirer/acquire_lock/log_visitor.rb +45 -0
  15. data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock/log_visitor.rb +1 -0
  16. data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rb +12 -1
  17. data/lib/redis_queued_locks/acquirer/acquire_lock/with_acq_timeout.rb +2 -2
  18. data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire/log_visitor.rb +1 -0
  19. data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire.rb +5 -5
  20. data/lib/redis_queued_locks/acquirer/acquire_lock.rb +111 -24
  21. data/lib/redis_queued_locks/acquirer/clear_dead_requests.rb +86 -8
  22. data/lib/redis_queued_locks/acquirer/extend_lock_ttl.rb +122 -12
  23. data/lib/redis_queued_locks/acquirer/is_locked.rb +0 -1
  24. data/lib/redis_queued_locks/acquirer/is_queued.rb +0 -1
  25. data/lib/redis_queued_locks/acquirer/keys.rb +10 -1
  26. data/lib/redis_queued_locks/acquirer/lock_info.rb +31 -39
  27. data/lib/redis_queued_locks/acquirer/lock_series_poc/instr_visitor.rb +3 -2
  28. data/lib/redis_queued_locks/acquirer/lock_series_poc/log_visitor.rb +5 -4
  29. data/lib/redis_queued_locks/acquirer/lock_series_poc.rb +55 -15
  30. data/lib/redis_queued_locks/acquirer/locks.rb +26 -9
  31. data/lib/redis_queued_locks/acquirer/queue_info.rb +0 -1
  32. data/lib/redis_queued_locks/acquirer/queues.rb +10 -1
  33. data/lib/redis_queued_locks/acquirer/reindex_locks.rb +117 -4
  34. data/lib/redis_queued_locks/acquirer/release_all_locks.rb +31 -4
  35. data/lib/redis_queued_locks/acquirer/release_lock.rb +33 -7
  36. data/lib/redis_queued_locks/acquirer/release_locks_of.rb +39 -4
  37. data/lib/redis_queued_locks/acquirer/release_read_lock.rb +22 -3
  38. data/lib/redis_queued_locks/client.rb +82 -40
  39. data/lib/redis_queued_locks/config.rb +12 -13
  40. data/lib/redis_queued_locks/errors.rb +34 -4
  41. data/lib/redis_queued_locks/instrument/sampler.rb +4 -7
  42. data/lib/redis_queued_locks/logging/sampler.rb +4 -8
  43. data/lib/redis_queued_locks/resource.rb +14 -4
  44. data/lib/redis_queued_locks/swarm/flush_zombies.rb +1 -0
  45. data/lib/redis_queued_locks/swarm/redis_client_builder.rb +8 -4
  46. data/lib/redis_queued_locks/swarm/swarm_element/threaded.rb +3 -2
  47. data/lib/redis_queued_locks/swarm.rb +86 -5
  48. data/lib/redis_queued_locks/utilities.rb +1 -1
  49. data/lib/redis_queued_locks/version.rb +2 -2
  50. data/sig/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rbs +14 -0
  51. data/sig/redis_queued_locks/acquirer/acquire_lock/log_visitor.rbs +13 -0
  52. data/sig/redis_queued_locks/acquirer/acquire_lock/yield_expire.rbs +2 -2
  53. data/sig/redis_queued_locks/acquirer/acquire_lock.rbs +1 -1
  54. data/sig/redis_queued_locks/acquirer/clear_dead_requests.rbs +9 -0
  55. data/sig/redis_queued_locks/acquirer/extend_lock_ttl.rbs +15 -2
  56. data/sig/redis_queued_locks/acquirer/reindex_locks.rbs +22 -1
  57. data/sig/redis_queued_locks/acquirer/release_lock.rbs +3 -2
  58. data/sig/redis_queued_locks/acquirer/release_read_lock.rbs +1 -1
  59. data/sig/redis_queued_locks/client.rbs +14 -3
  60. data/sig/redis_queued_locks/errors.rbs +5 -2
  61. data/sig/redis_queued_locks/instrument/sampler.rbs +0 -2
  62. data/sig/redis_queued_locks/logging/sampler.rbs +0 -2
  63. data/sig/redis_queued_locks/resource.rbs +1 -1
  64. data/sig/vendor/redis_client.rbs +1 -0
  65. metadata +1 -1
@@ -2,6 +2,7 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.7.0
5
+ # @version 1.18.0
5
6
  module RedisQueuedLocks::Acquirer::AcquireLock::DequeueFromLockQueue::LogVisitor
6
7
  class << self
7
8
  # @param logger [::Logger,#debug]
@@ -2,7 +2,10 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.7.0
5
+ # @version 1.21.0
6
+ # rubocop:disable Metrics/ModuleLength
5
7
  module RedisQueuedLocks::Acquirer::AcquireLock::InstrVisitor
8
+ # rubocop:disable Metrics/ClassLength
6
9
  class << self
7
10
  # @param instrumenter [#notify]
8
11
  # @param instr_sampled [Boolean]
@@ -172,5 +175,44 @@ module RedisQueuedLocks::Acquirer::AcquireLock::InstrVisitor
172
175
  hold_time:, ttl:, acq_id:, hst_id:, ts:, lock_key:, rw_mode:, acq_time:, instrument:
173
176
  }) rescue nil
174
177
  end
178
+
179
+ # The lock is not obtained (the failure result of `#lock`/`#lock!`: the timeout, the retry
180
+ # limit, `fail_fast`, the dead lock or the lock upgrade conflict).
181
+ #
182
+ # @param instrumenter [#notify]
183
+ # @param instr_sampled [Boolean]
184
+ # @param lock_key [String]
185
+ # @param rw_mode [Symbol]
186
+ # @param ttl [Integer]
187
+ # @param acq_id [String]
188
+ # @param hst_id [String]
189
+ # @param ts [Numeric]
190
+ # @param acq_time [Numeric]
191
+ # @param result [Symbol] The failure result (`:timeout_reached`, `:retry_limit_reached`, ...).
192
+ # @param instrument [NilClass,Any]
193
+ # @return [void]
194
+ #
195
+ # @api private
196
+ # @since 1.21.0
197
+ def lock_obtaining_failed(
198
+ instrumenter,
199
+ instr_sampled,
200
+ lock_key,
201
+ rw_mode,
202
+ ttl,
203
+ acq_id,
204
+ hst_id,
205
+ ts,
206
+ acq_time,
207
+ result,
208
+ instrument
209
+ )
210
+ return unless instr_sampled
211
+ instrumenter.notify('redis_queued_locks.lock_obtaining_failed', {
212
+ lock_key:, rw_mode:, ttl:, acq_id:, hst_id:, ts:, acq_time:, result:, instrument:
213
+ }) rescue nil
214
+ end
175
215
  end
216
+ # rubocop:enable Metrics/ClassLength
176
217
  end
218
+ # rubocop:enable Metrics/ModuleLength
@@ -2,6 +2,7 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.7.0
5
+ # @version 1.21.0
5
6
  # rubocop:disable Metrics/ModuleLength
6
7
  module RedisQueuedLocks::Acquirer::AcquireLock::LogVisitor
7
8
  # rubocop:disable Metrics/ClassLength
@@ -230,6 +231,50 @@ module RedisQueuedLocks::Acquirer::AcquireLock::LogVisitor
230
231
  "acq_time => #{acq_time} (ms)"
231
232
  end rescue nil
232
233
  end
234
+
235
+ # The lock is not obtained (the failure result of `#lock`/`#lock!`: the timeout, the retry
236
+ # limit, `fail_fast`, the dead lock or the lock upgrade conflict).
237
+ #
238
+ # @param logger [::Logger,#debug]
239
+ # @param log_sampled [Boolean]
240
+ # @param lock_key [String]
241
+ # @param queue_ttl [Integer]
242
+ # @param acquirer_id [String]
243
+ # @param host_id [String]
244
+ # @param acq_time [Numeric]
245
+ # @param access_strategy [Symbol]
246
+ # @param rw_mode [Symbol]
247
+ # @param result [Symbol] The failure result (`:timeout_reached`, `:retry_limit_reached`, ...).
248
+ # @return [void]
249
+ #
250
+ # @api private
251
+ # @since 1.21.0
252
+ def lock_obtaining_failed(
253
+ logger,
254
+ log_sampled,
255
+ lock_key,
256
+ queue_ttl,
257
+ acquirer_id,
258
+ host_id,
259
+ acq_time,
260
+ access_strategy,
261
+ rw_mode,
262
+ result
263
+ )
264
+ return unless log_sampled
265
+
266
+ logger.debug do
267
+ "[redis_queued_locks.lock_obtaining_failed] " \
268
+ "lock_key => '#{lock_key}' " \
269
+ "queue_ttl => #{queue_ttl} " \
270
+ "acq_id => '#{acquirer_id}' " \
271
+ "hst_id => '#{host_id}' " \
272
+ "acs_strat => '#{access_strategy}' " \
273
+ "rw_mode => '#{rw_mode}' " \
274
+ "acq_time => #{acq_time} (ms) " \
275
+ "result => '#{result}'"
276
+ end rescue nil
277
+ end
233
278
  end
234
279
  # rubocop:enable Metrics/ClassLength
235
280
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.7.0
5
+ # @version 1.18.0
5
6
  # rubocop:disable Metrics/ModuleLength
6
7
  module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock::LogVisitor
7
8
  # rubocop:disable Metrics/ClassLength
@@ -154,7 +154,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
154
154
  #
155
155
  # @api private
156
156
  # @since 1.0.0
157
- # @version 1.20.0
157
+ # @version 1.21.0
158
158
  # rubocop:disable Metrics/MethodLength
159
159
  def try_to_lock(
160
160
  redis,
@@ -648,6 +648,17 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
648
648
  end
649
649
  end
650
650
  end
651
+ rescue RedisQueuedLocks::LockAcquirementIntermediateTimeoutError,
652
+ RedisQueuedLocks::TimedLockIntermediateTimeoutError,
653
+ ::Timeout::ExitException
654
+ # NOTE: (timeouts) the timeout can interrupt the attempt inside the WATCH block:
655
+ # redis-client sends `UNWATCH` only on `StandardError` (the internal timeouts and the
656
+ # `Timeout.timeout` of the user without an exception class are `::Exception`s), and the
657
+ # connection would return to the pool with the active WATCH (an EXEC of the next
658
+ # transaction on it is aborted when a watched key is changed);
659
+ # - the closed connection drops the WATCH state and is reconnected on the next use;
660
+ rconn.close
661
+ raise
651
662
  end
652
663
 
653
664
  # Step 7: Analyze the aquirement attempt:
@@ -6,7 +6,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::WithAcqTimeout
6
6
  # @param redis [RedisClient]
7
7
  # Redis connection manager required for additional data extraction for error message.
8
8
  # @param timeout [NilClass,Integer]
9
- # Time period after which the logic will fail with timeout error.
9
+ # Time period after which the logic will fail with timeout error (`nil` and `0`: no timeout).
10
10
  # @param lock_key [String]
11
11
  # Lock name in RQL notation (rql:lock:some-lock-name).
12
12
  # @param lock_name [String]
@@ -26,7 +26,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::WithAcqTimeout
26
26
  #
27
27
  # @api private
28
28
  # @since 1.0.0
29
- # @version 1.11.0
29
+ # @version 1.21.0
30
30
  def with_acq_timeout(
31
31
  redis,
32
32
  timeout,
@@ -2,6 +2,7 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.16.0
5
+ # @version 1.18.0
5
6
  module RedisQueuedLocks::Acquirer::AcquireLock::YieldExpire::LogVisitor
6
7
  class << self
7
8
  # @param logger [::Logger,#debug]
@@ -30,7 +30,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::YieldExpire
30
30
  # @param access_strategy [Symbol] Lock obtaining strategy.
31
31
  # @param timed [Boolean] Should the lock be wrapped by Timeout with with lock's ttl
32
32
  # @param ttl_shift [Numeric] Lock's TTL shifting. Should affect block's ttl. In millisecodns.
33
- # @param ttl [Integer,NilClass] Lock's time to live (in ms). Nil means "without timeout".
33
+ # @param ttl [Integer] Lock's time to live (in ms). A positive Integer.
34
34
  # @param queue_ttl [Integer] Lock request lifetime.
35
35
  # @param meta [NilClass,Hash<String|Symbol,Any>] Custom metadata wich is passed to the lock data;
36
36
  # @param log_sampled [Boolean] Should the logic be logged or not.
@@ -45,7 +45,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::YieldExpire
45
45
  #
46
46
  # @api private
47
47
  # @since 1.3.0
48
- # @version 1.18.0
48
+ # @version 1.21.0
49
49
  # rubocop:disable Metrics/MethodLength
50
50
  def yield_expire(
51
51
  redis,
@@ -71,7 +71,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::YieldExpire
71
71
  initial_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, :millisecond)
72
72
 
73
73
  if block_given?
74
- if timed && ttl != nil
74
+ if timed
75
75
  # NOTE:
76
76
  # - steep is ignored cuz steep can not recognize `::Integer - ::Integer|::Float`
77
77
  # operation here for some mystical reason (it tryes to find overloadd `-` method for
@@ -150,7 +150,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::YieldExpire
150
150
 
151
151
  # @param timeout [Float]
152
152
  # @param lock_key [String]
153
- # @param lock_ttl [Integer,NilClass]
153
+ # @param lock_ttl [Integer]
154
154
  # @param acquirer_id [String]
155
155
  # @param host_id [String]
156
156
  # @param meta [NilClass,Hash<Symbol|String,Any>]
@@ -161,7 +161,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::YieldExpire
161
161
  #
162
162
  # @api private
163
163
  # @since 1.3.0
164
- # @version 1.12.0
164
+ # @version 1.21.0
165
165
  def yield_with_timeout(timeout, lock_key, lock_ttl, acquirer_id, host_id, meta, &block)
166
166
  ::Timeout.timeout(timeout, RedisQueuedLocks::TimedLockIntermediateTimeoutError, &block)
167
167
  rescue RedisQueuedLocks::TimedLockIntermediateTimeoutError
@@ -42,8 +42,8 @@ module RedisQueuedLocks::Acquirer::AcquireLock
42
42
  # A current fiber that want to acquire a lock.
43
43
  # @option ractor_id [Integer,String]
44
44
  # The current ractor that want to acquire a lock.
45
- # @option ttl [Integer,NilClass]
46
- # Lock's time to live (in milliseconds). Nil means "without timeout".
45
+ # @option ttl [Integer]
46
+ # Lock's time to live (in milliseconds). A positive Integer (both modes).
47
47
  # @option queue_ttl [Integer]
48
48
  # Lifetime of the acuier's lock request. In seconds.
49
49
  # @option lock_index_enabled [Boolean]
@@ -57,7 +57,8 @@ module RedisQueuedLocks::Acquirer::AcquireLock
57
57
  # - pre-configured in `config['lock_index_grace_period']`;
58
58
  # - used only with the enabled lock index (`lock_index_enabled`);
59
59
  # @option timeout [Integer,NilClass]
60
- # Time period whe should try to acquire the lock (in seconds).
60
+ # Time period whe should try to acquire the lock (in seconds). Nil means "without timeout"
61
+ # (`0` is the same as `nil`: `Timeout.timeout(0)` sets no timeout).
61
62
  # @option timed [Boolean]
62
63
  # Limit the invocation time period of the passed block of code by the lock's TTL.
63
64
  # @option retry_count [Integer,NilClass]
@@ -131,7 +132,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock
131
132
  # - works in tandem with <config['log_sampling_percent'] and <config['log_sampler']>;
132
133
  # @option log_sampling_percent [Integer]
133
134
  # - the percent of cases that should be logged;
134
- # - take an effect when <config['log_sampling_enalbed']> is true;
135
+ # - take an effect when <config['log_sampling_enabled']> is true;
135
136
  # - works in tandem with <config['log_sampling_enabled']> and <config['log_sampler']> configs;
136
137
  # @option log_sampler [#sampling_happened?,Module<RedisQueuedLocks::Logging::Sampler>]
137
138
  # - percent-based log sampler that decides should be RQL case logged or not;
@@ -152,7 +153,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock
152
153
  # - works in tandem with <config['instr_sampling_percent']> and <config['instr_sampler']>;
153
154
  # @option instr_sampling_percent [Integer]
154
155
  # - the percent of cases that should be instrumented;
155
- # - take an effect when <config['instr_sampling_enalbed']> is true;
156
+ # - take an effect when <config['instr_sampling_enabled']> is true;
156
157
  # - works in tandem with <config['instr_sampling_enabled']>
157
158
  # and <config['instr_sampler']> configs;
158
159
  # @option instr_sampler [#sampling_happened?,Module<RedisQueuedLocks::Instrument::Sampler>]
@@ -176,7 +177,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock
176
177
  #
177
178
  # @api private
178
179
  # @since 1.0.0
179
- # @version 1.20.0
180
+ # @version 1.21.0
180
181
  def acquire_lock(
181
182
  redis,
182
183
  lock_name,
@@ -235,29 +236,79 @@ module RedisQueuedLocks::Acquirer::AcquireLock
235
236
  )
236
237
  end
237
238
 
238
- # Step 0.1 (RW): prevent read lock incompatabilities
239
- # - read lock expiration time is calculated from the lock ttl;
240
- if read_write_mode == :read && (!ttl.is_a?(::Integer) || ttl <= 0)
239
+ # Step 0.1: prevent :ttl incompatibilities (both modes)
240
+ # - write lock: `PEXPIRE` with a non-positive ttl deletes the just obtained lock key in the
241
+ # obtaining transaction (the lock is "obtained" without the mutual exclusion);
242
+ # - read lock: the read lock expiration time is calculated from the lock ttl;
243
+ if !ttl.is_a?(::Integer) || ttl <= 0
241
244
  raise(
242
245
  RedisQueuedLocks::ArgumentError,
243
- "`:ttl` argument should be a positive Integer for read locks, got #{ttl.inspect}."
246
+ "`:ttl` argument should be a positive Integer, got #{ttl.inspect}."
247
+ )
248
+ end
249
+
250
+ # Step 0.1: prevent :queue_ttl, :timeout, :retry_* and :lock_index_grace_period
251
+ # incompatibilities (the same ranges as their config keys):
252
+ # - a non-positive queue ttl drops the own lock request in the queueing pipeline (the lock
253
+ # request is always dead);
254
+ # - a negative timeout/retry delay/retry jitter fails `Timeout.timeout`/`sleep` in the
255
+ # middle of the acquirement (`0` timeout is the same as `nil`: no timeout);
256
+ if !queue_ttl.is_a?(::Integer) || queue_ttl <= 0
257
+ raise(
258
+ RedisQueuedLocks::ArgumentError,
259
+ "`:queue_ttl` argument should be a positive Integer, got #{queue_ttl.inspect}."
260
+ )
261
+ end
262
+ if timeout != nil && (!timeout.is_a?(::Integer) || timeout < 0)
263
+ raise(
264
+ RedisQueuedLocks::ArgumentError,
265
+ "`:timeout` argument should be nil or a non-negative Integer, got #{timeout.inspect}."
266
+ )
267
+ end
268
+ if retry_count != nil && (!retry_count.is_a?(::Integer) || retry_count < 0)
269
+ raise(
270
+ RedisQueuedLocks::ArgumentError,
271
+ "`:retry_count` argument should be nil or a non-negative Integer, " \
272
+ "got #{retry_count.inspect}."
273
+ )
274
+ end
275
+ if !retry_delay.is_a?(::Integer) || retry_delay < 0
276
+ raise(
277
+ RedisQueuedLocks::ArgumentError,
278
+ "`:retry_delay` argument should be a non-negative Integer, got #{retry_delay.inspect}."
279
+ )
280
+ end
281
+ if !retry_jitter.is_a?(::Integer) || retry_jitter < 0
282
+ raise(
283
+ RedisQueuedLocks::ArgumentError,
284
+ "`:retry_jitter` argument should be a non-negative Integer, got #{retry_jitter.inspect}."
285
+ )
286
+ end
287
+ if !lock_index_grace_period.is_a?(::Integer) || lock_index_grace_period < 0
288
+ raise(
289
+ RedisQueuedLocks::ArgumentError,
290
+ "`:lock_index_grace_period` argument should be a non-negative Integer, " \
291
+ "got #{lock_index_grace_period.inspect}."
244
292
  )
245
293
  end
246
294
 
247
295
  # Step 0.2: prevent :meta incompatabiltiies (structure)
296
+ # - Symbol keys are written to the lock data as Strings (`Symbol#name`, as redis-client
297
+ # does it) and would overwrite the reserved lock data fields too;
248
298
  if meta.is_a?(::Hash) && (meta.any? do |key, _value|
249
- key == 'acq_id' ||
250
- key == 'hst_id' ||
251
- key == 'ts' ||
252
- key == 'ini_ttl' ||
253
- key == 'lock_key' ||
254
- key == 'rem_ttl' ||
255
- key == 'spc_ext_ttl' ||
256
- key == 'spc_cnt' ||
257
- key == 'l_spc_ext_ini_ttl' ||
258
- key == 'l_spc_ext_ts' ||
259
- key == 'l_spc_ts' ||
260
- key == 'rw_mode'
299
+ meta_key = key.is_a?(::Symbol) ? key.name : key
300
+ meta_key == 'acq_id' ||
301
+ meta_key == 'hst_id' ||
302
+ meta_key == 'ts' ||
303
+ meta_key == 'ini_ttl' ||
304
+ meta_key == 'lock_key' ||
305
+ meta_key == 'rem_ttl' ||
306
+ meta_key == 'spc_ext_ttl' ||
307
+ meta_key == 'spc_cnt' ||
308
+ meta_key == 'l_spc_ext_ini_ttl' ||
309
+ meta_key == 'l_spc_ext_ts' ||
310
+ meta_key == 'l_spc_ts' ||
311
+ meta_key == 'rw_mode'
261
312
  end)
262
313
  raise(
263
314
  RedisQueuedLocks::ArgumentError,
@@ -346,6 +397,9 @@ module RedisQueuedLocks::Acquirer::AcquireLock
346
397
  )
347
398
 
348
399
  # Step 2: try to lock with timeout
400
+ # NOTE: the acquirement start time is defined out of the timeout block: the failed
401
+ # acquirement reports the acquirement time too (Steps 3.b and 3.c);
402
+ acq_start_time = clock_gettime
349
403
  with_acq_timeout(
350
404
  redis,
351
405
  timeout,
@@ -355,8 +409,6 @@ module RedisQueuedLocks::Acquirer::AcquireLock
355
409
  detailed_acq_timeout_error,
356
410
  on_timeout: acq_dequeue
357
411
  ) do
358
- acq_start_time = clock_gettime
359
-
360
412
  # Step 2.1: cyclically try to obtain the lock
361
413
  while acq_process[:should_try]
362
414
 
@@ -650,8 +702,43 @@ module RedisQueuedLocks::Acquirer::AcquireLock
650
702
  acq_process[:result] = :timeout_reached
651
703
  end
652
704
  # Step 3.b: lock is not acquired (acquirer is dequeued by timeout callback)
705
+ acq_time = ((clock_gettime - acq_start_time) / 1_000.0).ceil(2)
706
+ LogVisitor.lock_obtaining_failed(
707
+ logger, log_sampled, lock_key, queue_ttl, acquirer_id, host_id,
708
+ acq_time, access_strategy, read_write_mode, acq_process[:result]
709
+ )
710
+ InstrVisitor.lock_obtaining_failed(
711
+ instrumenter, instr_sampled, lock_key, read_write_mode, lock_ttl, acquirer_id, host_id,
712
+ Time.now.to_f, acq_time, acq_process[:result], instrument
713
+ )
653
714
  { ok: false, result: acq_process[:result] } #: { ok: bool, result: Symbol }
654
715
  end
716
+ rescue RedisQueuedLocks::LockAlreadyObtainedError,
717
+ RedisQueuedLocks::ConflictLockObtainError,
718
+ RedisQueuedLocks::LockAcquirementRetryLimitError,
719
+ RedisQueuedLocks::LockAcquirementTimeoutError => error
720
+ # Step 3.c: (raise_errors) the failed acquirement is reported before the error is raised
721
+ # (like Step 3.b); errors of the block of code under the obtained lock are not reported;
722
+ # NOTE: (steep) the locals of the method body are nilable in the method-level `rescue` for
723
+ # Steep, but these errors are raised by the acquirement only (after Step 1: the locals
724
+ # are assigned);
725
+ # steep:ignore:start
726
+ unless acq_process[:acquired]
727
+ if error.is_a?(RedisQueuedLocks::LockAcquirementTimeoutError)
728
+ acq_process[:result] = :timeout_reached
729
+ end
730
+ acq_time = ((clock_gettime - acq_start_time) / 1_000.0).ceil(2)
731
+ LogVisitor.lock_obtaining_failed(
732
+ logger, log_sampled, lock_key, queue_ttl, acquirer_id, host_id,
733
+ acq_time, access_strategy, read_write_mode, acq_process[:result]
734
+ )
735
+ InstrVisitor.lock_obtaining_failed(
736
+ instrumenter, instr_sampled, lock_key, read_write_mode, lock_ttl, acquirer_id, host_id,
737
+ Time.now.to_f, acq_time, acq_process[:result], instrument
738
+ )
739
+ end
740
+ # steep:ignore:end
741
+ raise
655
742
  end
656
743
  end
657
744
  end
@@ -2,8 +2,13 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.0.0
5
- # @version 1.20.0
5
+ # @version 1.21.0
6
+ # rubocop:disable Metrics/ModuleLength
6
7
  module RedisQueuedLocks::Acquirer::ClearDeadRequests
8
+ # @since 1.21.0
9
+ extend RedisQueuedLocks::Utilities
10
+
11
+ # rubocop:disable Metrics/ClassLength
7
12
  class << self
8
13
  # Drops dead lock requests (requests older than `dead_ttl`) from the lock queues (write and
9
14
  # read):
@@ -13,6 +18,8 @@ module RedisQueuedLocks::Acquirer::ClearDeadRequests
13
18
  # over the lock names (the keyspace is never scanned), one round trip per batch of
14
19
  # `scan_size` lock names (both lock queues of each lock name); expired lock names are
15
20
  # dropped from the lock index first;
21
+ # - the operation is reported with one log and one instrumentation event
22
+ # (`redis_queued_locks.clear_dead_requests`) with the time of the whole operation;
16
23
  #
17
24
  # @param redis_client [RedisClient]
18
25
  # @param scan_size [Integer]
@@ -36,7 +43,8 @@ module RedisQueuedLocks::Acquirer::ClearDeadRequests
36
43
  #
37
44
  # @api private
38
45
  # @since 1.0.0
39
- # @version 1.20.0
46
+ # @version 1.21.0
47
+ # rubocop:disable Metrics/MethodLength
40
48
  def clear_dead_requests(
41
49
  redis_client,
42
50
  scan_size,
@@ -54,10 +62,82 @@ module RedisQueuedLocks::Acquirer::ClearDeadRequests
54
62
  instr_sampler,
55
63
  instr_sample_this
56
64
  )
65
+ # NOTE: (arguments) the same ranges as the config keys: zero/negative sizes fail SCAN/ZSCAN
66
+ # (`COUNT 0`) and the batching, a non-positive dead ttl drops all lock requests;
67
+ if !scan_size.is_a?(::Integer) || scan_size <= 0
68
+ raise(
69
+ RedisQueuedLocks::ArgumentError,
70
+ "`:scan_size` argument should be a positive Integer, got #{scan_size.inspect}."
71
+ )
72
+ end
73
+ if !dead_ttl.is_a?(::Integer) || dead_ttl <= 0
74
+ raise(
75
+ RedisQueuedLocks::ArgumentError,
76
+ "`:dead_ttl` argument should be a positive Integer, got #{dead_ttl.inspect}."
77
+ )
78
+ end
79
+
80
+ rel_start_time = clock_gettime
81
+ processed_queues = fully_clear_dead_requests(
82
+ redis_client, scan_size, dead_ttl, lock_index_enabled
83
+ )
84
+ time_at = Time.now.to_f
85
+ rel_end_time = clock_gettime
86
+ rel_time = ((rel_end_time - rel_start_time) / 1_000.0).ceil(2)
87
+
88
+ log_sampled = RedisQueuedLocks::Logging.should_log?(
89
+ log_sampling_enabled,
90
+ log_sample_this,
91
+ log_sampling_percent,
92
+ log_sampler
93
+ )
94
+ instr_sampled = RedisQueuedLocks::Instrument.should_instrument?(
95
+ instr_sampling_enabled,
96
+ instr_sample_this,
97
+ instr_sampling_percent,
98
+ instr_sampler
99
+ )
100
+
101
+ run_non_critical do
102
+ logger.debug do
103
+ "[redis_queued_locks.clear_dead_requests] " \
104
+ "dead_ttl => #{dead_ttl} " \
105
+ "prc_queue_cnt => #{processed_queues.size} " \
106
+ "rel_time => #{rel_time} " \
107
+ "at => #{time_at}"
108
+ end
109
+ end if log_sampled
110
+
111
+ run_non_critical do
112
+ instrumenter.notify('redis_queued_locks.clear_dead_requests', {
113
+ dead_ttl: dead_ttl,
114
+ prc_queue_cnt: processed_queues.size,
115
+ rel_time: rel_time,
116
+ at: time_at,
117
+ instrument: instrument
118
+ })
119
+ end if instr_sampled
120
+
121
+ { ok: true, result: { processed_queues: } }
122
+ end
123
+ # rubocop:enable Metrics/MethodLength
124
+
125
+ private
126
+
127
+ # Drops dead lock requests from the lock queues (see `clear_dead_requests`).
128
+ #
129
+ # @param redis_client [RedisClient]
130
+ # @param scan_size [Integer]
131
+ # @param dead_ttl [Integer] In milliseconds
132
+ # @param lock_index_enabled [Boolean]
133
+ # @return [Set<String>] The processed lock queues (write and read lock request queues).
134
+ #
135
+ # @api private
136
+ # @since 1.21.0
137
+ def fully_clear_dead_requests(redis_client, scan_size, dead_ttl, lock_index_enabled)
57
138
  dead_score = RedisQueuedLocks::Resource.acquirer_dead_score(dead_ttl / 1_000.0)
58
139
 
59
- # @type var result: Set[String]
60
- result = Set.new.tap do |processed_queues|
140
+ Set.new.tap do |processed_queues|
61
141
  # @type var processed_queues: Set[String]
62
142
  redis_client.with do |rconn|
63
143
  if lock_index_enabled
@@ -92,12 +172,8 @@ module RedisQueuedLocks::Acquirer::ClearDeadRequests
92
172
  end
93
173
  end
94
174
  end
95
-
96
- { ok: true, result: { processed_queues: result } }
97
175
  end
98
176
 
99
- private
100
-
101
177
  # Write and read lock queues in one SCAN pass (the disabled lock index).
102
178
  #
103
179
  # @param redis_client [RedisClient]
@@ -210,4 +286,6 @@ module RedisQueuedLocks::Acquirer::ClearDeadRequests
210
286
  yield(lock_names) unless lock_names.empty?
211
287
  end
212
288
  end
289
+ # rubocop:enable Metrics/ClassLength
213
290
  end
291
+ # rubocop:enable Metrics/ModuleLength