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.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/project-overview.md +249 -36
  3. data/.claude/rules/acquirer.md +16 -14
  4. data/.claude/rules/arguments.md +2 -2
  5. data/.claude/rules/changelog.md +27 -0
  6. data/.claude/rules/git.md +1 -1
  7. data/.claude/rules/logic.md +34 -13
  8. data/.claude/rules/swarm.md +4 -5
  9. data/.claude/rules/tests.md +9 -6
  10. data/.claude/rules/type-checking.md +13 -14
  11. data/.claude/rules/visitors.md +13 -14
  12. data/.claude/skills/rql-code-review/SKILL.md +134 -43
  13. data/CHANGELOG.md +105 -125
  14. data/CLAUDE.md +11 -9
  15. data/README.md +501 -136
  16. data/Rakefile +11 -0
  17. data/lib/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue/log_visitor.rb +1 -0
  18. data/lib/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rb +68 -10
  19. data/lib/redis_queued_locks/acquirer/acquire_lock/log_visitor.rb +82 -15
  20. data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock/log_visitor.rb +92 -14
  21. data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rb +441 -63
  22. data/lib/redis_queued_locks/acquirer/acquire_lock/with_acq_timeout.rb +2 -2
  23. data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire/log_visitor.rb +1 -0
  24. data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire.rb +5 -5
  25. data/lib/redis_queued_locks/acquirer/acquire_lock.rb +176 -39
  26. data/lib/redis_queued_locks/acquirer/clear_dead_requests.rb +322 -49
  27. data/lib/redis_queued_locks/acquirer/extend_lock_ttl.rb +125 -13
  28. data/lib/redis_queued_locks/acquirer/is_locked.rb +0 -1
  29. data/lib/redis_queued_locks/acquirer/is_queued.rb +0 -1
  30. data/lib/redis_queued_locks/acquirer/keys.rb +16 -3
  31. data/lib/redis_queued_locks/acquirer/lock_info.rb +31 -39
  32. data/lib/redis_queued_locks/acquirer/lock_series_poc/instr_visitor.rb +11 -5
  33. data/lib/redis_queued_locks/acquirer/lock_series_poc/log_visitor.rb +20 -10
  34. data/lib/redis_queued_locks/acquirer/lock_series_poc.rb +105 -36
  35. data/lib/redis_queued_locks/acquirer/locks.rb +32 -11
  36. data/lib/redis_queued_locks/acquirer/queue_info.rb +30 -8
  37. data/lib/redis_queued_locks/acquirer/queues.rb +26 -6
  38. data/lib/redis_queued_locks/acquirer/reindex_locks.rb +117 -4
  39. data/lib/redis_queued_locks/acquirer/release_all_locks.rb +37 -6
  40. data/lib/redis_queued_locks/acquirer/release_lock.rb +33 -7
  41. data/lib/redis_queued_locks/acquirer/release_locks_of.rb +45 -6
  42. data/lib/redis_queued_locks/acquirer/release_read_lock.rb +22 -3
  43. data/lib/redis_queued_locks/client.rb +140 -56
  44. data/lib/redis_queued_locks/config.rb +105 -14
  45. data/lib/redis_queued_locks/errors.rb +34 -4
  46. data/lib/redis_queued_locks/instrument/sampler.rb +4 -7
  47. data/lib/redis_queued_locks/logging/sampler.rb +4 -8
  48. data/lib/redis_queued_locks/resource.rb +137 -12
  49. data/lib/redis_queued_locks/swarm/flush_zombies.rb +1 -0
  50. data/lib/redis_queued_locks/swarm/redis_client_builder.rb +8 -4
  51. data/lib/redis_queued_locks/swarm/swarm_element/threaded.rb +8 -6
  52. data/lib/redis_queued_locks/swarm/zombie_info.rb +6 -2
  53. data/lib/redis_queued_locks/swarm.rb +86 -5
  54. data/lib/redis_queued_locks/utilities.rb +1 -1
  55. data/lib/redis_queued_locks/version.rb +2 -2
  56. data/rbs_collection.lock.yaml +4 -0
  57. data/rbs_collection.yaml +2 -1
  58. data/sig/manifest.yml +1 -0
  59. data/sig/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue/log_visitor.rbs +2 -2
  60. data/sig/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue.rbs +2 -2
  61. data/sig/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rbs +25 -5
  62. data/sig/redis_queued_locks/acquirer/acquire_lock/log_visitor.rbs +32 -12
  63. data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock/log_visitor.rbs +61 -40
  64. data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rbs +59 -11
  65. data/sig/redis_queued_locks/acquirer/acquire_lock/{yield_with_expire → yield_expire}/log_visitor.rbs +4 -4
  66. data/sig/redis_queued_locks/acquirer/acquire_lock/yield_expire.rbs +4 -4
  67. data/sig/redis_queued_locks/acquirer/acquire_lock.rbs +6 -4
  68. data/sig/redis_queued_locks/acquirer/clear_dead_requests.rbs +23 -1
  69. data/sig/redis_queued_locks/acquirer/extend_lock_ttl.rbs +16 -3
  70. data/sig/redis_queued_locks/acquirer/queues.rbs +1 -1
  71. data/sig/redis_queued_locks/acquirer/reindex_locks.rbs +22 -1
  72. data/sig/redis_queued_locks/acquirer/release_lock.rbs +3 -2
  73. data/sig/redis_queued_locks/acquirer/release_read_lock.rbs +1 -1
  74. data/sig/redis_queued_locks/client.rbs +24 -10
  75. data/sig/redis_queued_locks/config.rbs +54 -1
  76. data/sig/redis_queued_locks/errors.rbs +5 -2
  77. data/sig/redis_queued_locks/instrument/sampler.rbs +3 -3
  78. data/sig/redis_queued_locks/instrument/void_notifier.rbs +3 -1
  79. data/sig/redis_queued_locks/logging/sampler.rbs +3 -3
  80. data/sig/redis_queued_locks/resource.rbs +23 -13
  81. data/sig/redis_queued_locks/swarm/swarm_element/threaded.rbs +1 -1
  82. data/sig/redis_queued_locks/utilities.rbs +2 -1
  83. data/sig/redis_queued_locks.rbs +7 -0
  84. data/sig/vendor/redis_client.rbs +1 -0
  85. metadata +4 -3
  86. /data/sig/redis_queued_locks/{acquier.rbs → acquirer.rbs} +0 -0
@@ -1,20 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # NOTE: Lock Series PoC
4
- # rubocop:disable all
4
+ # rubocop:disable Metrics/ModuleLength, Metrics/ClassLength, Metrics/MethodLength
5
5
  # @api private
6
6
  # @since 1.16.0
7
+ # @version 1.22.0
7
8
  module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
8
9
  require_relative 'lock_series_poc/log_visitor'
9
10
  require_relative 'lock_series_poc/instr_visitor'
10
11
 
11
- # @sine 1.16.0
12
+ # @since 1.16.0
12
13
  extend RedisQueuedLocks::Acquirer::AcquireLock::YieldExpire
13
14
 
14
15
  class << self
15
16
  # @api private
16
17
  # @since 1.16.0
17
- # @version 1.20.0
18
+ # @version 1.22.0
18
19
  def lock_series_poc( # steep:ignore
19
20
  redis,
20
21
  lock_names,
@@ -44,6 +45,8 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
44
45
  conflict_strategy:,
45
46
  read_write_mode:,
46
47
  access_strategy:,
48
+ priority:,
49
+ lock_priorities:,
47
50
  log_sampling_enabled:,
48
51
  log_sampling_percent:,
49
52
  log_sampler:,
@@ -56,6 +59,8 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
56
59
  )
57
60
  # NOTE: (RW) the lock mode is the same for all locks of the series
58
61
  # (see RedisQueuedLocks::Acquirer::AcquireLock for read_write_mode validations);
62
+ # NOTE: the lock request priority is the same for all locks of the series too (validated
63
+ # below: the series logs and instrumentation report it);
59
64
 
60
65
  case meta
61
66
  when Hash, NilClass then nil
@@ -66,19 +71,22 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
66
71
  )
67
72
  end
68
73
 
74
+ # NOTE: Symbol keys are written to the lock data as Strings (the same check as in
75
+ # RedisQueuedLocks::Acquirer::AcquireLock);
69
76
  if meta.is_a?(::Hash) && (meta.any? do |key, _value|
70
- key == 'acq_id' ||
71
- key == 'hst_id' ||
72
- key == 'ts' ||
73
- key == 'ini_ttl' ||
74
- key == 'lock_key' ||
75
- key == 'rem_ttl' ||
76
- key == 'spc_ext_ttl' ||
77
- key == 'spc_cnt' ||
78
- key == 'l_spc_ext_ini_ttl' ||
79
- key == 'l_spc_ext_ts' ||
80
- key == 'l_spc_ts' ||
81
- key == 'rw_mode'
77
+ meta_key = key.is_a?(::Symbol) ? key.name : key
78
+ meta_key == 'acq_id' ||
79
+ meta_key == 'hst_id' ||
80
+ meta_key == 'ts' ||
81
+ meta_key == 'ini_ttl' ||
82
+ meta_key == 'lock_key' ||
83
+ meta_key == 'rem_ttl' ||
84
+ meta_key == 'spc_ext_ttl' ||
85
+ meta_key == 'spc_cnt' ||
86
+ meta_key == 'l_spc_ext_ini_ttl' ||
87
+ meta_key == 'l_spc_ext_ts' ||
88
+ meta_key == 'l_spc_ts' ||
89
+ meta_key == 'rw_mode'
82
90
  end)
83
91
  raise(
84
92
  RedisQueuedLocks::ArgumentError,
@@ -88,6 +96,51 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
88
96
  )
89
97
  end
90
98
 
99
+ # NOTE: the series ttl is validated before the lock ttl calculation: each lock ttl of
100
+ # the series includes the retry delay and jitter (a non-positive series ttl can produce
101
+ # a positive lock ttl), the series ttl limits the `timed` block (the same check as in
102
+ # RedisQueuedLocks::Acquirer::AcquireLock);
103
+ if !ttl.is_a?(::Integer) || ttl <= 0
104
+ raise(
105
+ RedisQueuedLocks::ArgumentError,
106
+ "`:ttl` argument should be a positive Integer, got #{ttl.inspect}."
107
+ )
108
+ end
109
+
110
+ # NOTE: the retry delay and jitter are parts of the lock ttls of the series (validated before
111
+ # the lock ttl calculation; other options are validated by
112
+ # RedisQueuedLocks::Acquirer::AcquireLock on the first lock of the series, before any lock
113
+ # is obtained);
114
+ if !retry_delay.is_a?(::Integer) || retry_delay < 0
115
+ raise(
116
+ RedisQueuedLocks::ArgumentError,
117
+ "`:retry_delay` argument should be a non-negative Integer, got #{retry_delay.inspect}."
118
+ )
119
+ end
120
+ if !retry_jitter.is_a?(::Integer) || retry_jitter < 0
121
+ raise(
122
+ RedisQueuedLocks::ArgumentError,
123
+ "`:retry_jitter` argument should be a non-negative Integer, got #{retry_jitter.inspect}."
124
+ )
125
+ end
126
+
127
+ # NOTE: the priority of the series is validated before the series logs and instrumentation:
128
+ # a priority name is resolved to its Integer priority (the series events report the Integer
129
+ # priority; the same check as in RedisQueuedLocks::Acquirer::AcquireLock);
130
+ priority = lock_priorities.fetch(priority, priority) if priority.is_a?(::Symbol)
131
+ if !priority.is_a?(::Integer) ||
132
+ priority < RedisQueuedLocks::Resource::MIN_LOCK_PRIORITY ||
133
+ priority > RedisQueuedLocks::Resource::MAX_LOCK_PRIORITY
134
+ raise(
135
+ RedisQueuedLocks::ArgumentError,
136
+ "`:priority` argument should be an Integer from " \
137
+ "#{RedisQueuedLocks::Resource::MIN_LOCK_PRIORITY} to " \
138
+ "#{RedisQueuedLocks::Resource::MAX_LOCK_PRIORITY} or a priority name of " \
139
+ "`config['lock_priorities']` (#{lock_priorities.keys.map(&:inspect).join(', ')}), " \
140
+ "got #{priority.inspect}."
141
+ )
142
+ end
143
+
91
144
  locks_count = lock_names.size
92
145
 
93
146
  lock_acquirement_operations = lock_names.each_with_index.map do |lock_name, position|
@@ -132,10 +185,10 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
132
185
  identity
133
186
  )
134
187
 
135
- RedisQueuedLocks::Acquirer::LockSeriesPoC::LogVisitor.start_lock_series_obtaining( # steep:ignore
188
+ LogVisitor.start_lock_series_obtaining( # steep:ignore
136
189
  logger, log_sampled, lock_keys_for_instrumentation,
137
- queue_ttl, acquirer_id_for_instrumentation, host_id_for_instrumentation, access_strategy,
138
- read_write_mode
190
+ queue_ttl, acquirer_id_for_instrumentation, host_id_for_instrumentation,
191
+ access_strategy, read_write_mode, priority
139
192
  )
140
193
 
141
194
  acq_start_time = RedisQueuedLocks::Utilities.clock_gettime
@@ -176,6 +229,8 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
176
229
  conflict_strategy:,
177
230
  read_write_mode:,
178
231
  access_strategy:,
232
+ priority:,
233
+ lock_priorities:,
179
234
  log_sampling_enabled:,
180
235
  log_sampling_percent:,
181
236
  log_sampler:,
@@ -208,7 +263,8 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
208
263
  else
209
264
  failed_on_lock = lock_operation_options[:lock_name]
210
265
  failed_locks_and_errors[lock_operation_options[:lock_name]] = result
211
- # NOTE: release all previously acquired locks (non-exceptional failure: raise_errors: false)
266
+ # NOTE: release all previously acquired locks
267
+ # (non-exceptional failure: raise_errors: false)
212
268
  unless is_failed_series_released
213
269
  release_lock_series( # steep:ignore
214
270
  redis, read_write_mode, successfully_acquired_locks, acquirer_id_for_instrumentation
@@ -219,20 +275,22 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
219
275
  end
220
276
  end
221
277
 
222
- if (successfully_acquired_locks.size == lock_names.size && (successfully_acquired_locks.all? { |res| res[:ok] }))
278
+ if successfully_acquired_locks.size == lock_names.size &&
279
+ successfully_acquired_locks.all? { |res| res[:ok] }
223
280
  acq_end_time = RedisQueuedLocks::Utilities.clock_gettime
224
281
  acq_time = ((acq_end_time - acq_start_time) / 1_000.0).ceil(2)
225
282
  ts = Time.now.to_f
226
283
 
227
- RedisQueuedLocks::Acquirer::LockSeriesPoC::LogVisitor.lock_series_obtained( # steep:ignore
284
+ LogVisitor.lock_series_obtained( # steep:ignore
228
285
  logger, log_sampled, lock_keys_for_instrumentation,
229
286
  queue_ttl, acquirer_id_for_instrumentation, host_id_for_instrumentation,
230
- acq_time, access_strategy, read_write_mode
287
+ acq_time, access_strategy, read_write_mode, priority
231
288
  )
232
289
 
233
- RedisQueuedLocks::Acquirer::LockSeriesPoC::InstrVisitor.lock_series_obtained( # steep:ignore
234
- instrumenter, instr_sampled, lock_keys_for_instrumentation, read_write_mode,
235
- ttl, acquirer_id_for_instrumentation, host_id_for_instrumentation, ts, acq_time, instrument
290
+ InstrVisitor.lock_series_obtained( # steep:ignore
291
+ instrumenter, instr_sampled, lock_keys_for_instrumentation, read_write_mode, priority,
292
+ ttl, acquirer_id_for_instrumentation, host_id_for_instrumentation, ts, acq_time,
293
+ instrument
236
294
  )
237
295
 
238
296
  yield_time = RedisQueuedLocks::Utilities.clock_gettime
@@ -242,8 +300,8 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
242
300
  ).ceil(2)
243
301
 
244
302
  yield_result = nil
245
- is_lock_manually_released = nil
246
- hold_time = nil
303
+ # @type var is_lock_manually_released: bool
304
+ # @type var hold_time: Float | Integer
247
305
 
248
306
  begin
249
307
  yield_result = yield_expire( # steep:ignore
@@ -284,17 +342,18 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
284
342
  hold_time = ((rel_time - acq_end_time) / 1_000.0).ceil(2)
285
343
  ts = Time.now.to_f
286
344
 
287
- RedisQueuedLocks::Acquirer::LockSeriesPoC::LogVisitor.expire_lock_series( # steep:ignore
345
+ LogVisitor.expire_lock_series( # steep:ignore
288
346
  logger, log_sampled, lock_keys_for_instrumentation,
289
- queue_ttl, acquirer_id_for_instrumentation, host_id_for_instrumentation, access_strategy,
290
- read_write_mode
347
+ queue_ttl, acquirer_id_for_instrumentation, host_id_for_instrumentation,
348
+ access_strategy, read_write_mode, priority
291
349
  ) if is_lock_manually_released
292
350
 
293
- RedisQueuedLocks::Acquirer::LockSeriesPoC::InstrVisitor.lock_series_hold_and_release( # steep:ignore
351
+ InstrVisitor.lock_series_hold_and_release( # steep:ignore
294
352
  instrumenter,
295
353
  instr_sampled,
296
354
  lock_keys_for_instrumentation,
297
355
  read_write_mode,
356
+ priority,
298
357
  ttl,
299
358
  acquirer_id_for_instrumentation,
300
359
  host_id_for_instrumentation,
@@ -330,7 +389,7 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
330
389
  lock_series: lock_names,
331
390
  acquired_locks:,
332
391
  missing_locks:,
333
- failed_on_lock:,
392
+ failed_on_lock:
334
393
  }
335
394
  }
336
395
  end
@@ -350,7 +409,9 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
350
409
  #
351
410
  # @api private
352
411
  # @since 1.18.0
353
- def release_lock_series(redis, read_write_mode, successfully_acquired_locks, acquirer_id) # steep:ignore
412
+ def release_lock_series( # steep:ignore
413
+ redis, read_write_mode, successfully_acquired_locks, acquirer_id
414
+ )
354
415
  lock_names = obtained_lock_names(successfully_acquired_locks) # steep:ignore
355
416
 
356
417
  if read_write_mode == :read
@@ -374,7 +435,7 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
374
435
  #
375
436
  # @api private
376
437
  # @since 1.18.0
377
- # @version 1.20.0
438
+ # @version 1.21.0
378
439
  def release_write_lock_series(redis, lock_names, acquirer_id) # steep:ignore
379
440
  return if lock_names.empty?
380
441
 
@@ -395,8 +456,16 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
395
456
  own_lock_keys.each { |lock_key| transaction.call('DEL', lock_key) }
396
457
  end
397
458
  # NOTE: `nil` => the transaction is aborted (a watched lock is changed) => retry
398
- break unless released.nil?
459
+ break unless released == nil
399
460
  end
461
+ rescue RedisQueuedLocks::LockAcquirementIntermediateTimeoutError,
462
+ RedisQueuedLocks::TimedLockIntermediateTimeoutError,
463
+ ::Timeout::ExitException
464
+ # NOTE: (timeouts) an interrupted WATCH block (the internal timeouts and the user's
465
+ # `Timeout.timeout` without an exception class): the closed connection drops the WATCH
466
+ # state (redis-client sends `UNWATCH` only on `StandardError`);
467
+ conn.close
468
+ raise
400
469
  end
401
470
  end
402
471
 
@@ -441,4 +510,4 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
441
510
  end
442
511
  end
443
512
  end
444
- # rubocop:enable all
513
+ # rubocop:enable Metrics/ModuleLength, Metrics/ClassLength, Metrics/MethodLength
@@ -25,8 +25,17 @@ module RedisQueuedLocks::Acquirer::Locks
25
25
  #
26
26
  # @api private
27
27
  # @since 1.0.0
28
- # @version 1.20.0
28
+ # @version 1.21.0
29
29
  def locks(redis_client, scan_size:, with_info:, lock_index_enabled:)
30
+ # NOTE: (arguments) the same range as the config key: zero/negative sizes fail SCAN/ZSCAN
31
+ # (`COUNT 0`) and the batching (`each_slice(0)`);
32
+ if !scan_size.is_a?(::Integer) || scan_size <= 0
33
+ raise(
34
+ RedisQueuedLocks::ArgumentError,
35
+ "`:scan_size` argument should be a positive Integer, got #{scan_size.inspect}."
36
+ )
37
+ end
38
+
30
39
  redis_client.with do |rconn|
31
40
  lock_keys =
32
41
  lock_index_enabled ? index_locks(rconn, scan_size) : scan_locks(rconn, scan_size)
@@ -106,7 +115,8 @@ module RedisQueuedLocks::Acquirer::Locks
106
115
 
107
116
  # Iterates over the lock names of the lock index (one ZSCAN pass) in batches of `scan_size`
108
117
  # lock names (batches are collected across ZSCAN pages). Expired lock names (not dropped from
109
- # the index yet) are skipped: all their keys are expired.
118
+ # the index yet) are skipped: all their keys are expired (`+inf` names, keys without TTL, never
119
+ # expire: see `RedisQueuedLocks::Resource.lock_index_score_expired?`).
110
120
  #
111
121
  # NOTE: each operation module iterates over the lock index by itself (duplicated on purpose:
112
122
  # operation modules are independent of each other, see `extract_locks_info`);
@@ -119,6 +129,7 @@ module RedisQueuedLocks::Acquirer::Locks
119
129
  #
120
130
  # @api private
121
131
  # @since 1.20.0
132
+ # @version 1.22.0
122
133
  def each_lock_name_batch(redis_client, scan_size)
123
134
  lock_index_time = RedisQueuedLocks::Resource.lock_index_time
124
135
  # @type var lock_names: Array[String]
@@ -127,7 +138,9 @@ module RedisQueuedLocks::Acquirer::Locks
127
138
  redis_client.zscan(
128
139
  RedisQueuedLocks::Resource::LOCK_INDEX_KEY, count: scan_size
129
140
  ) do |lock_name, lock_index_score|
130
- next if lock_index_score.to_f < lock_index_time
141
+ next if RedisQueuedLocks::Resource.lock_index_score_expired?(
142
+ lock_index_score, lock_index_time
143
+ )
131
144
 
132
145
  lock_names << lock_name
133
146
  next if lock_names.size < scan_size
@@ -153,7 +166,7 @@ module RedisQueuedLocks::Acquirer::Locks
153
166
  #
154
167
  # @api private
155
168
  # @since 1.0.0
156
- # @version 1.20.0
169
+ # @version 1.21.0
157
170
  # rubocop:disable Metrics/MethodLength
158
171
  def extract_locks_info(redis_client, lock_keys, scan_size)
159
172
  Set.new.tap do |seeded_locks|
@@ -162,6 +175,13 @@ module RedisQueuedLocks::Acquirer::Locks
162
175
  # rubocop:enable Layout/LineLength
163
176
 
164
177
  lock_keys.each_slice(scan_size) do |lock_keys_batch|
178
+ # Step 0: lock names of the batch (once per lock: the readers registry keys and the read
179
+ # lock data keys are built from them)
180
+ # @type var lock_names_batch: Array[String]
181
+ lock_names_batch = lock_keys_batch.map do |lock_key|
182
+ RedisQueuedLocks::Resource.lock_name_from_key(lock_key, :lock) #: String
183
+ end
184
+
165
185
  # Step 1: extract the state of the batch of locks in one round trip
166
186
  # NOTE: (pipeline layout) [0] TIME (read locks are checked in redis time), then
167
187
  # `LOCK_STATE_REPLIES` replies per lock in the batch order (the replies of the lock
@@ -170,10 +190,9 @@ module RedisQueuedLocks::Acquirer::Locks
170
190
  # @type var batch_state: Array[untyped]
171
191
  batch_state = redis_client.pipelined do |pipeline|
172
192
  pipeline.call('TIME')
173
- lock_keys_batch.each do |lock_key|
174
- lock_readers_key = RedisQueuedLocks::Resource.prepare_lock_readers(
175
- lock_key.delete_prefix('rql:lock:')
176
- )
193
+ lock_keys_batch.each_with_index do |lock_key, index|
194
+ lock_readers_key =
195
+ RedisQueuedLocks::Resource.prepare_lock_readers(lock_names_batch[index])
177
196
  pipeline.call('HGETALL', lock_key)
178
197
  pipeline.call('PTTL', lock_key)
179
198
  pipeline.call('ZRANGE', lock_readers_key, '0', '-1', 'WITHSCORES')
@@ -199,12 +218,13 @@ module RedisQueuedLocks::Acquirer::Locks
199
218
  # read lock data can be absent if it is dropped during the read lock life;
200
219
  # @type var read_locks_data: Array[Hash[String,String]]
201
220
  read_locks_data = redis_client.pipelined do |pipeline|
202
- lock_keys_batch.each_with_index do |lock_key, index|
203
- lock_name = lock_key.delete_prefix('rql:lock:')
221
+ lock_keys_batch.each_index do |index|
204
222
  live_read_locks[index]&.each do |(acquirer_id, _expiration)|
205
223
  pipeline.call(
206
224
  'HGETALL',
207
- RedisQueuedLocks::Resource.prepare_read_lock_key(lock_name, acquirer_id)
225
+ RedisQueuedLocks::Resource.prepare_read_lock_key(
226
+ lock_names_batch[index], acquirer_id
227
+ )
208
228
  )
209
229
  end
210
230
  end
@@ -232,6 +252,7 @@ module RedisQueuedLocks::Acquirer::Locks
232
252
  pttl_cmd_res = batch_state[replies_at + 1] # NOTE: PTTL result (integer)
233
253
 
234
254
  hget_cmd_res.tap do |lock_data|
255
+ lock_data['lock_key'] = lock_key
235
256
  lock_data['rw_mode'] = 'write'
236
257
  lock_data['ts'] = Float(lock_data['ts'])
237
258
  lock_data['ini_ttl'] = Integer(lock_data['ini_ttl'])
@@ -2,7 +2,6 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.0.0
5
- # @version 1.20.0
6
5
  module RedisQueuedLocks::Acquirer::QueueInfo
7
6
  class << self
8
7
  # Returns an information about the required lock queue by the lock name. The result
@@ -17,22 +16,34 @@ module RedisQueuedLocks::Acquirer::QueueInfo
17
16
  # - result format: {
18
17
  # "lock_queue" => "rql:lock_queue:your_lock_name", # lock queue key in redis,
19
18
  # "queue" => [
20
- # { "acq_id" => "rql:acq:123/456/789/987/id", "score" => 123.456, "rw_mode" => "write" },
21
- # { "acq_id" => "rql:acq:123/686/789/987/id", "score" => 456.789, "rw_mode" => "write" },
19
+ # {
20
+ # "acq_id" => "rql:acq:123/456/789/987/id",
21
+ # "score" => 123.456,
22
+ # "rw_mode" => "write",
23
+ # "priority" => 0
24
+ # },
22
25
  # ...
23
26
  # ] # ordered set (by score) with information about an acquirer, their position in queue
24
- # # and the requested lock mode
27
+ # # (the enqueue time shifted by the priority band: see
28
+ # # `RedisQueuedLocks::Resource::LOCK_PRIORITY_BAND`), the requested lock mode and
29
+ # # the lock request priority (decoded from the position)
25
30
  # <additional keys when the queue of read lock requests exists>:
26
31
  # "read_lock_queue" => "rql:lock_read_queue:your_lock_name", # read lock queue key in redis
27
32
  # "read_queue" => [
28
- # { "acq_id" => "rql:acq:123/456/789/987/id", "score" => 123.456, "rw_mode" => "read" },
33
+ # {
34
+ # "acq_id" => "rql:acq:123/456/789/987/id",
35
+ # "score" => 123.456,
36
+ # "rw_mode" => "read",
37
+ # "priority" => 0
38
+ # },
29
39
  # ...
30
40
  # ] # read lock requests (positions are comparable with the write lock request positions)
31
41
  # }
32
42
  #
33
43
  # @api private
34
44
  # @since 1.0.0
35
- # @version 1.20.0
45
+ # @version 1.22.0
46
+ # rubocop:disable Metrics/MethodLength
36
47
  def queue_info(redis_client, lock_name)
37
48
  lock_key_queue = RedisQueuedLocks::Resource.prepare_lock_queue(lock_name)
38
49
  read_lock_key_queue = RedisQueuedLocks::Resource.prepare_read_lock_queue(lock_name)
@@ -55,14 +66,24 @@ module RedisQueuedLocks::Acquirer::QueueInfo
55
66
  queue_info = {
56
67
  'lock_queue' => lock_key_queue,
57
68
  'queue' => zrange_cmd_res.map do |val|
58
- { 'acq_id' => val[0], 'score' => val[1], 'rw_mode' => 'write' }
69
+ {
70
+ 'acq_id' => val[0],
71
+ 'score' => val[1],
72
+ 'rw_mode' => 'write',
73
+ 'priority' => RedisQueuedLocks::Resource.lock_request_priority(val[1])
74
+ }
59
75
  end
60
76
  }
61
77
  # NOTE: (RW) the queue of read lock requests
62
78
  unless read_zrange_cmd_res.empty?
63
79
  queue_info['read_lock_queue'] = read_lock_key_queue
64
80
  queue_info['read_queue'] = read_zrange_cmd_res.map do |val|
65
- { 'acq_id' => val[0], 'score' => val[1], 'rw_mode' => 'read' }
81
+ {
82
+ 'acq_id' => val[0],
83
+ 'score' => val[1],
84
+ 'rw_mode' => 'read',
85
+ 'priority' => RedisQueuedLocks::Resource.lock_request_priority(val[1])
86
+ }
66
87
  end
67
88
  end
68
89
  queue_info
@@ -71,5 +92,6 @@ module RedisQueuedLocks::Acquirer::QueueInfo
71
92
  nil
72
93
  end
73
94
  end
95
+ # rubocop:enable Metrics/MethodLength
74
96
  end
75
97
  end
@@ -15,8 +15,17 @@ module RedisQueuedLocks::Acquirer::Queues
15
15
  #
16
16
  # @api private
17
17
  # @since 1.0.0
18
- # @version 1.20.0
18
+ # @version 1.22.0
19
19
  def queues(redis_client, scan_size:, with_info:, lock_index_enabled:)
20
+ # NOTE: (arguments) the same range as the config key: zero/negative sizes fail SCAN/ZSCAN
21
+ # (`COUNT 0`) and the batching (`each_slice(0)`);
22
+ if !scan_size.is_a?(::Integer) || scan_size <= 0
23
+ raise(
24
+ RedisQueuedLocks::ArgumentError,
25
+ "`:scan_size` argument should be a positive Integer, got #{scan_size.inspect}."
26
+ )
27
+ end
28
+
20
29
  redis_client.with do |rconn|
21
30
  lock_queues =
22
31
  lock_index_enabled ? index_queues(rconn, scan_size) : scan_queues(rconn, scan_size)
@@ -93,7 +102,8 @@ module RedisQueuedLocks::Acquirer::Queues
93
102
 
94
103
  # Iterates over the lock names of the lock index (one ZSCAN pass) in batches of `scan_size`
95
104
  # lock names (batches are collected across ZSCAN pages). Expired lock names (not dropped from
96
- # the index yet) are skipped: all their keys are expired.
105
+ # the index yet) are skipped: all their keys are expired (`+inf` names, keys without TTL, never
106
+ # expire: see `RedisQueuedLocks::Resource.lock_index_score_expired?`).
97
107
  #
98
108
  # NOTE: each operation module iterates over the lock index by itself (duplicated on purpose:
99
109
  # operation modules are independent of each other, see `Locks.each_lock_name_batch`);
@@ -106,6 +116,7 @@ module RedisQueuedLocks::Acquirer::Queues
106
116
  #
107
117
  # @api private
108
118
  # @since 1.20.0
119
+ # @version 1.22.0
109
120
  def each_lock_name_batch(redis_client, scan_size)
110
121
  lock_index_time = RedisQueuedLocks::Resource.lock_index_time
111
122
  # @type var lock_names: Array[String]
@@ -114,7 +125,9 @@ module RedisQueuedLocks::Acquirer::Queues
114
125
  redis_client.zscan(
115
126
  RedisQueuedLocks::Resource::LOCK_INDEX_KEY, count: scan_size
116
127
  ) do |lock_name, lock_index_score|
117
- next if lock_index_score.to_f < lock_index_time
128
+ next if RedisQueuedLocks::Resource.lock_index_score_expired?(
129
+ lock_index_score, lock_index_time
130
+ )
118
131
 
119
132
  lock_names << lock_name
120
133
  next if lock_names.size < scan_size
@@ -130,11 +143,13 @@ module RedisQueuedLocks::Acquirer::Queues
130
143
  # @param lock_queues [Set<String>]
131
144
  # @param scan_size [Integer] The number of queues whose requests are read in one round trip.
132
145
  # @return [Set<Hash<Symbol,Any>>]
133
- # - each request: `{ 'acq_id' => String, 'score' => Float, 'rw_mode' => 'read'/'write' }`;
146
+ # - each request: `{ 'acq_id' => String, 'score' => Float, 'rw_mode' => 'read'/'write',
147
+ # 'priority' => Integer }` (the priority is decoded from the position: see
148
+ # `RedisQueuedLocks::Resource::LOCK_PRIORITY_BAND`);
134
149
  #
135
150
  # @api private
136
151
  # @since 1.0.0
137
- # @version 1.20.0
152
+ # @version 1.22.0
138
153
  def extract_queues_info(redis_client, lock_queues, scan_size)
139
154
  # NOTE: the same approach as in `QueueInfo` (duplicated on purpose: public operation modules
140
155
  # do not reuse each other, they share logic via `Resource`, `Utilities`);
@@ -162,7 +177,12 @@ module RedisQueuedLocks::Acquirer::Queues
162
177
  seeded_queues << {
163
178
  queue: lock_queue,
164
179
  requests: queues_requests[index].map do |val|
165
- { 'acq_id' => val[0], 'score' => val[1], 'rw_mode' => rw_mode }
180
+ {
181
+ 'acq_id' => val[0],
182
+ 'score' => val[1],
183
+ 'rw_mode' => rw_mode,
184
+ 'priority' => RedisQueuedLocks::Resource.lock_request_priority(val[1])
185
+ }
166
186
  end
167
187
  }
168
188
  end