redis_queued_locks 1.21.0 → 1.23.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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/project-overview.md +294 -51
  3. data/.claude/rules/acquirer.md +16 -11
  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 +14 -15
  8. data/.claude/rules/swarm.md +3 -4
  9. data/.claude/rules/tests.md +9 -9
  10. data/.claude/rules/type-checking.md +14 -16
  11. data/.claude/rules/visitors.md +11 -9
  12. data/.claude/skills/rql-code-review/SKILL.md +129 -47
  13. data/.ruby-version +1 -1
  14. data/CHANGELOG.md +122 -245
  15. data/CLAUDE.md +12 -10
  16. data/README.md +516 -175
  17. data/Rakefile +11 -0
  18. data/lib/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue.rb +4 -4
  19. data/lib/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rb +28 -12
  20. data/lib/redis_queued_locks/acquirer/acquire_lock/log_visitor.rb +38 -16
  21. data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock/log_visitor.rb +92 -15
  22. data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rb +945 -272
  23. data/lib/redis_queued_locks/acquirer/acquire_lock.rb +82 -32
  24. data/lib/redis_queued_locks/acquirer/clear_dead_requests.rb +240 -45
  25. data/lib/redis_queued_locks/acquirer/extend_lock_ttl.rb +15 -13
  26. data/lib/redis_queued_locks/acquirer/keys.rb +6 -2
  27. data/lib/redis_queued_locks/acquirer/lock_series/instr_visitor.rb +243 -0
  28. data/lib/redis_queued_locks/acquirer/lock_series/log_visitor.rb +472 -0
  29. data/lib/redis_queued_locks/acquirer/lock_series/release_lock_series.rb +181 -0
  30. data/lib/redis_queued_locks/acquirer/lock_series/try_to_lock_series.rb +1051 -0
  31. data/lib/redis_queued_locks/acquirer/lock_series.rb +948 -0
  32. data/lib/redis_queued_locks/acquirer/locks.rb +6 -2
  33. data/lib/redis_queued_locks/acquirer/queue_info.rb +30 -7
  34. data/lib/redis_queued_locks/acquirer/queues.rb +17 -6
  35. data/lib/redis_queued_locks/acquirer/release_all_locks.rb +6 -2
  36. data/lib/redis_queued_locks/acquirer/release_locks_of.rb +6 -2
  37. data/lib/redis_queued_locks/acquirer.rb +1 -1
  38. data/lib/redis_queued_locks/client.rb +116 -44
  39. data/lib/redis_queued_locks/config.rb +93 -1
  40. data/lib/redis_queued_locks/resource.rb +124 -9
  41. data/lib/redis_queued_locks/swarm/swarm_element/threaded.rb +6 -5
  42. data/lib/redis_queued_locks/swarm/zombie_info.rb +6 -2
  43. data/lib/redis_queued_locks/version.rb +2 -2
  44. data/rbs_collection.lock.yaml +4 -0
  45. data/rbs_collection.yaml +2 -1
  46. data/redis_queued_locks.gemspec +21 -14
  47. data/sig/manifest.yml +1 -0
  48. data/sig/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue/log_visitor.rbs +2 -2
  49. data/sig/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue.rbs +2 -2
  50. data/sig/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rbs +12 -6
  51. data/sig/redis_queued_locks/acquirer/acquire_lock/log_visitor.rbs +21 -14
  52. data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock/log_visitor.rbs +61 -40
  53. data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rbs +160 -11
  54. data/sig/redis_queued_locks/acquirer/acquire_lock/{yield_with_expire → yield_expire}/log_visitor.rbs +4 -4
  55. data/sig/redis_queued_locks/acquirer/acquire_lock/yield_expire.rbs +2 -2
  56. data/sig/redis_queued_locks/acquirer/acquire_lock.rbs +5 -3
  57. data/sig/redis_queued_locks/acquirer/clear_dead_requests.rbs +14 -1
  58. data/sig/redis_queued_locks/acquirer/extend_lock_ttl.rbs +2 -2
  59. data/sig/redis_queued_locks/acquirer/lock_series/instr_visitor.rbs +102 -0
  60. data/sig/redis_queued_locks/acquirer/lock_series/log_visitor.rbs +154 -0
  61. data/sig/redis_queued_locks/acquirer/lock_series/release_lock_series.rbs +37 -0
  62. data/sig/redis_queued_locks/acquirer/lock_series/try_to_lock_series.rbs +198 -0
  63. data/sig/redis_queued_locks/acquirer/lock_series.rbs +106 -0
  64. data/sig/redis_queued_locks/acquirer/queues.rbs +1 -1
  65. data/sig/redis_queued_locks/client.rbs +77 -7
  66. data/sig/redis_queued_locks/config.rbs +54 -1
  67. data/sig/redis_queued_locks/instrument/sampler.rbs +3 -1
  68. data/sig/redis_queued_locks/instrument/void_notifier.rbs +3 -1
  69. data/sig/redis_queued_locks/logging/sampler.rbs +3 -1
  70. data/sig/redis_queued_locks/resource.rbs +22 -12
  71. data/sig/redis_queued_locks/swarm/swarm_element/threaded.rbs +1 -1
  72. data/sig/redis_queued_locks/utilities.rbs +2 -1
  73. data/sig/redis_queued_locks.rbs +7 -0
  74. metadata +35 -13
  75. data/lib/redis_queued_locks/acquirer/lock_series_poc/instr_visitor.rb +0 -56
  76. data/lib/redis_queued_locks/acquirer/lock_series_poc/log_visitor.rb +0 -95
  77. data/lib/redis_queued_locks/acquirer/lock_series_poc.rb +0 -484
  78. /data/sig/redis_queued_locks/{acquier.rbs → acquirer.rbs} +0 -0
@@ -2,6 +2,7 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.13.0
5
+ # @version 1.22.0
5
6
  # rubocop:disable Metrics/ClassLength
6
7
  class RedisQueuedLocks::Config
7
8
  require_relative 'config/dsl'
@@ -31,6 +32,8 @@ class RedisQueuedLocks::Config
31
32
  setting('is_timed_by_default', false)
32
33
  setting('default_conflict_strategy', :wait_for_lock)
33
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)
34
37
  setting('log_sampling_enabled', false)
35
38
  setting('log_sampling_percent', 15)
36
39
  setting('log_sampler', RedisQueuedLocks::Logging::Sampler)
@@ -114,6 +117,27 @@ class RedisQueuedLocks::Config
114
117
  val == :random
115
118
  # rubocop:enable Layout/MultilineOperationIndentation
116
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
117
141
 
118
142
  # @return [Hash<Symbol,Any>]
119
143
  #
@@ -123,10 +147,14 @@ class RedisQueuedLocks::Config
123
147
 
124
148
  # @return [void]
125
149
  #
150
+ # @raise [RedisQueuedLocks::ConfigValidationError]
151
+ #
126
152
  # @api private
127
153
  # @since 1.13.0
154
+ # @version 1.22.0
128
155
  def initialize(&configuration)
129
156
  @config_state = {}
157
+ @configuring = false
130
158
  config_setters.each_value { |setter| setter.call(@config_state) }
131
159
  configure(&configuration)
132
160
  end
@@ -136,10 +164,40 @@ class RedisQueuedLocks::Config
136
164
  # @yieldparam [?]
137
165
  # @return [void]
138
166
  #
167
+ # @raise [RedisQueuedLocks::ConfigValidationError]
168
+ #
139
169
  # @api private
140
170
  # @since 1.13.0
171
+ # @version 1.22.0
141
172
  def configure(&configuration)
142
- yield(self) if block_given?
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
143
201
  end
144
202
 
145
203
  # @param config_key [String]
@@ -163,9 +221,21 @@ class RedisQueuedLocks::Config
163
221
  #
164
222
  # @api public
165
223
  # @since 1.13.0
224
+ # @version 1.22.0
166
225
  def []=(config_key, config_value)
167
226
  prevent_key__non_existent(config_key)
168
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
169
239
  config_state[config_key] = config_value
170
240
  end
171
241
 
@@ -236,5 +306,27 @@ class RedisQueuedLocks::Config
236
306
  )
237
307
  end
238
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
239
331
  end
240
332
  # rubocop:enable Metrics/ClassLength
@@ -2,7 +2,7 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.0.0
5
- # @version 1.20.0
5
+ # @version 1.22.0
6
6
  # rubocop:disable Metrics/ModuleLength
7
7
  module RedisQueuedLocks::Resource
8
8
  # @return [String]
@@ -90,6 +90,44 @@ module RedisQueuedLocks::Resource
90
90
  # @since 1.3.0
91
91
  REDIS_TIMESHIFT_ERROR = 2
92
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
+
93
131
  # rubocop:disable Metrics/ClassLength
94
132
  class << self
95
133
  # Returns 16-byte unique identifier. It is used for uniquely
@@ -276,6 +314,22 @@ module RedisQueuedLocks::Resource
276
314
  ::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond)
277
315
  end
278
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
+
279
333
  # @param lock_readers_key [String]
280
334
  # @return [String]
281
335
  #
@@ -327,16 +381,23 @@ module RedisQueuedLocks::Resource
327
381
  (Integer(redis_time[0]) * 1_000) + (Integer(redis_time[1]) / 1_000)
328
382
  end
329
383
 
330
- # @return [Float] Redis's <Set> score that is calculated from the time (epoch) as a float.
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).
331
389
  #
332
390
  # @api private
333
391
  # @since 1.0.0
334
- def calc_initial_acquirer_position
335
- Time.now.to_f
392
+ # @version 1.22.0
393
+ def calc_initial_acquirer_position(priority)
394
+ Time.now.to_f - (priority * LOCK_PRIORITY_BAND)
336
395
  end
337
396
 
338
397
  # @param queue_ttl [Numeric] In seconds
339
- # @return [Float] Redis's <Set> score barrier for acquirers that should be removed from queue.
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`).
340
401
  #
341
402
  # @api private
342
403
  # @since 1.0.0
@@ -344,6 +405,57 @@ module RedisQueuedLocks::Resource
344
405
  Time.now.to_f - queue_ttl
345
406
  end
346
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
+
347
459
  # @param zombie_ttl [Float] In seconds with milliseconds.
348
460
  # @return [Float]
349
461
  #
@@ -354,8 +466,10 @@ module RedisQueuedLocks::Resource
354
466
  end
355
467
 
356
468
  # @param acquirer_position [Float]
357
- # A time (epoch, seconds.milliseconds) that represents
358
- # 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).
359
473
  # @param queue_ttl [Integer]
360
474
  # In second.
361
475
  # @return [Boolean]
@@ -363,8 +477,9 @@ module RedisQueuedLocks::Resource
363
477
  #
364
478
  # @api private
365
479
  # @since 1.0.0
366
- def dead_score_reached?(acquirer_position, queue_ttl)
367
- (acquirer_position + queue_ttl) < Time.now.to_f
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
368
483
  end
369
484
 
370
485
  # NOTE: (debug) not used by the library: a manual runtime check (in a console) of the lock that
@@ -2,7 +2,7 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.9.0
5
- # @version 1.17.0
5
+ # @version 1.21.1
6
6
  # rubocop:disable Metrics/ClassLength
7
7
  class RedisQueuedLocks::Swarm::SwarmElement::Threaded
8
8
  # @since 1.9.0
@@ -70,7 +70,7 @@ class RedisQueuedLocks::Swarm::SwarmElement::Threaded
70
70
  return unless enabled?
71
71
 
72
72
  sync.synchronize do
73
- swarm_element__termiante
73
+ swarm_element__terminate
74
74
  swarm!
75
75
  swarm_loop__start
76
76
  end
@@ -99,7 +99,7 @@ class RedisQueuedLocks::Swarm::SwarmElement::Threaded
99
99
  # @since 1.9.0
100
100
  def try_kill!
101
101
  sync.synchronize do
102
- swarm_element__termiante
102
+ swarm_element__terminate
103
103
  end
104
104
  end
105
105
 
@@ -157,7 +157,7 @@ class RedisQueuedLocks::Swarm::SwarmElement::Threaded
157
157
  # => 1) init (swarm!): create a thread, main loop is not started;
158
158
  # => 2) start (swarm_loop__start): run the main loop inside the created thread;
159
159
  # => 3) stop (swarm_loop__stop): stop the main loop inside the created thread;
160
- # => 4) terminate (swarm_element__termiante): kill the created thread;
160
+ # => 4) terminate (swarm_element__terminate): kill the created thread;
161
161
  # In addition you should implement `#spawn_main_loop!` method that will be the main logic
162
162
  # of the your swarm element.
163
163
  #
@@ -341,7 +341,8 @@ class RedisQueuedLocks::Swarm::SwarmElement::Threaded
341
341
  #
342
342
  # @api private
343
343
  # @since 1.9.0
344
- def swarm_element__termiante
344
+ # @version 1.21.1
345
+ def swarm_element__terminate
345
346
  return if idle? || swarmed__dead?
346
347
  sync.synchronize do
347
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 lock_index_score.to_f < lock_index_time
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)
@@ -5,6 +5,6 @@ module RedisQueuedLocks
5
5
  #
6
6
  # @api public
7
7
  # @since 0.0.1
8
- # @version 1.21.0
9
- VERSION = '1.21.0'
8
+ # @version 1.23.0
9
+ VERSION = '1.23.0'
10
10
  end
@@ -9,6 +9,10 @@ gems:
9
9
  revision: c69c75ba79d7c19870f8d0e6fa7fe2c1d356e5ae
10
10
  remote: https://github.com/ruby/gem_rbs_collection.git
11
11
  repo_dir: gems
12
+ - name: json
13
+ version: '0'
14
+ source:
15
+ type: stdlib
12
16
  - name: logger
13
17
  version: '1.7'
14
18
  source:
data/rbs_collection.yaml CHANGED
@@ -2,7 +2,7 @@ sources:
2
2
  - type: git
3
3
  name: ruby/gem_rbs_collection
4
4
  remote: https://github.com/ruby/gem_rbs_collection.git
5
- revision: main
5
+ revision: c69c75ba79d7c19870f8d0e6fa7fe2c1d356e5ae # NOTE: pinned for reproducible checks; bump deliberately (`bundle exec rbs collection update`)
6
6
  repo_dir: gems
7
7
 
8
8
  path: .gem_rbs_collection
@@ -13,3 +13,4 @@ gems:
13
13
  - name: timeout
14
14
  - name: logger
15
15
  - name: monitor
16
+ - name: json # NOTE: used by the runtime type checking only, not by the library code
@@ -14,20 +14,27 @@ Gem::Specification.new do |spec|
14
14
  'Distributed locks with "prioritized lock acquisition queue" ' \
15
15
  'capabilities based on the Redis Database.'
16
16
 
17
- spec.description =
18
- "|> Distributed locks with \"prioritized lock acquisition queue\" capabilities " \
19
- "based on the Redis Database.\n" \
20
- "|> Each lock request is put into the request queue " \
21
- "(each lock is hosted by its own queue separately from other queues) and processed " \
22
- "in order of their priority (FIFO).\n" \
23
- "|> Each lock request lives some period of time (RTTL) " \
24
- "(with requeue capabilities) which guarantees the request queue will never be stacked.\n" \
25
- "|> In addition to the classic `queued` (FIFO) strategy RQL supports " \
26
- "`random` (RANDOM) lock obtaining strategy when any acquirer from the lock queue " \
27
- "can obtain the lock regardless the position in the queue.\n" \
28
- "|> Provides flexible invocation flow, parametrized limits " \
29
- "(lock request ttl, lock ttl, queue ttl, lock attempts limit, fast failing, etc), " \
30
- "logging and instrumentation."
17
+ spec.description = <<~GEMSPEC_DESCRIPTION
18
+ RedisQueuedLocks (aka RQL, aka RQL-lock) - distributed locks with
19
+ "prioritized lock acquisition queue" capabilities based on the Redis Database.
20
+
21
+ Each lock request is put into the request queue
22
+ (each lock is hosted by its own queue separately from other queues)
23
+ and processed in order of their priority: requests of a higher priority go first,
24
+ requests of the same priority are processed in FIFO order.
25
+ Each lock request lives some period of time (RTTL) (with requeue capabilities)
26
+ which guarantees the request queue will never be stacked.
27
+
28
+ In addition to the classic queued (FIFO) strategy RQL supports random (RANDOM)
29
+ lock obtaining strategy when any acquirer from the lock queue
30
+ can obtain the lock regardless the position in the queue.
31
+
32
+ Supports lock request prioritization, Read/Write locks and Reentrant locks.
33
+
34
+ Provides flexible invocation flow, parametrized limits (lock request ttl, lock ttl,
35
+ queue ttl, lock attempts limit, fast failing, etc), zombie locks elimination,
36
+ lock indexing, logging and instrumentation (and much more).
37
+ GEMSPEC_DESCRIPTION
31
38
 
32
39
  spec.homepage = 'https://github.com/0exp/redis_queued_locks'
33
40
  spec.license = 'MIT'
data/sig/manifest.yml CHANGED
@@ -4,3 +4,4 @@ dependencies:
4
4
  - securerandom
5
5
  - logger
6
6
  - monitor
7
+ - json # NOTE: used by the runtime type checking only, not by the library code
@@ -12,8 +12,8 @@ module RedisQueuedLocks
12
12
  Integer queue_ttl,
13
13
  String acquirer_id,
14
14
  String host_id,
15
- Symbol access_strategy,
16
- Symbol rw_mode
15
+ RQL::accessStrategy access_strategy,
16
+ RQL::readWriteMode rw_mode
17
17
  ) -> void
18
18
  end
19
19
  end
@@ -9,13 +9,13 @@ module RedisQueuedLocks
9
9
  RC::client redis,
10
10
  RQL::loggerObj logger,
11
11
  String lock_key,
12
- Symbol read_write_mode,
12
+ RQL::readWriteMode rw_mode,
13
13
  String lock_key_queue,
14
14
  String read_lock_key_queue,
15
15
  Integer queue_ttl,
16
16
  String acquirer_id,
17
17
  String host_id,
18
- Symbol access_strategy,
18
+ RQL::accessStrategy access_strategy,
19
19
  bool log_sampled,
20
20
  bool instr_sampled
21
21
  ) -> { ok: bool, result: Integer }
@@ -8,7 +8,8 @@ module RedisQueuedLocks
8
8
  RQL::instrObj instrumenter,
9
9
  bool instr_sampled,
10
10
  String lock_key,
11
- Symbol rw_mode,
11
+ RQL::readWriteMode rw_mode,
12
+ Integer priority,
12
13
  Integer? ttl,
13
14
  String acq_id,
14
15
  String hst_id,
@@ -21,7 +22,8 @@ module RedisQueuedLocks
21
22
  RQL::instrObj instrumenter,
22
23
  bool instr_sampled,
23
24
  String lock_key,
24
- Symbol rw_mode,
25
+ RQL::readWriteMode rw_mode,
26
+ Integer priority,
25
27
  Integer? ttl,
26
28
  String acq_id,
27
29
  String hst_id,
@@ -34,7 +36,8 @@ module RedisQueuedLocks
34
36
  RQL::instrObj instrumenter,
35
37
  bool instr_sampled,
36
38
  String lock_key,
37
- Symbol rw_mode,
39
+ RQL::readWriteMode rw_mode,
40
+ Integer priority,
38
41
  Integer? ttl,
39
42
  String acq_id,
40
43
  String hst_id,
@@ -47,7 +50,8 @@ module RedisQueuedLocks
47
50
  RQL::instrObj instrumenter,
48
51
  bool instr_sampled,
49
52
  String lock_key,
50
- Symbol rw_mode,
53
+ RQL::readWriteMode rw_mode,
54
+ Integer priority,
51
55
  Integer? ttl,
52
56
  String acq_id,
53
57
  String hst_id,
@@ -61,7 +65,8 @@ module RedisQueuedLocks
61
65
  RQL::instrObj instrumenter,
62
66
  bool instr_sampled,
63
67
  String lock_key,
64
- Symbol rw_mode,
68
+ RQL::readWriteMode rw_mode,
69
+ Integer priority,
65
70
  Integer? ttl,
66
71
  String acq_id,
67
72
  String hst_id,
@@ -75,7 +80,8 @@ module RedisQueuedLocks
75
80
  RQL::instrObj instrumenter,
76
81
  bool instr_sampled,
77
82
  String lock_key,
78
- Symbol rw_mode,
83
+ RQL::readWriteMode rw_mode,
84
+ Integer priority,
79
85
  Integer ttl,
80
86
  String acq_id,
81
87
  String hst_id,
@@ -11,8 +11,9 @@ module RedisQueuedLocks
11
11
  Integer queue_ttl,
12
12
  String acquirer_id,
13
13
  String host_id,
14
- Symbol access_strategy,
15
- Symbol rw_mode
14
+ RQL::accessStrategy access_strategy,
15
+ RQL::readWriteMode rw_mode,
16
+ Integer priority
16
17
  ) -> void
17
18
 
18
19
  def self.start_try_to_lock_cycle: (
@@ -22,8 +23,9 @@ module RedisQueuedLocks
22
23
  Integer queue_ttl,
23
24
  String acquirer_id,
24
25
  String host_id,
25
- Symbol access_strategy,
26
- Symbol rw_mode
26
+ RQL::accessStrategy access_strategy,
27
+ RQL::readWriteMode rw_mode,
28
+ Integer priority
27
29
  ) -> void
28
30
 
29
31
  def self.dead_score_reached__reset_acquirer_position: (
@@ -33,8 +35,9 @@ module RedisQueuedLocks
33
35
  Integer queue_ttl,
34
36
  String acquirer_id,
35
37
  String host_id,
36
- Symbol access_strategy,
37
- Symbol rw_mode
38
+ RQL::accessStrategy access_strategy,
39
+ RQL::readWriteMode rw_mode,
40
+ Integer priority
38
41
  ) -> void
39
42
 
40
43
  def self.extendable_reentrant_lock_obtained: (
@@ -45,8 +48,9 @@ module RedisQueuedLocks
45
48
  String acquirer_id,
46
49
  String host_id,
47
50
  Integer|Float acq_time,
48
- Symbol access_strategy,
49
- Symbol rw_mode
51
+ RQL::accessStrategy access_strategy,
52
+ RQL::readWriteMode rw_mode,
53
+ Integer priority
50
54
  ) -> void
51
55
 
52
56
  def self.reentrant_lock_obtained: (
@@ -57,8 +61,9 @@ module RedisQueuedLocks
57
61
  String acquirer_id,
58
62
  String host_id,
59
63
  Integer|Float acq_time,
60
- Symbol access_strategy,
61
- Symbol rw_mode
64
+ RQL::accessStrategy access_strategy,
65
+ RQL::readWriteMode rw_mode,
66
+ Integer priority
62
67
  ) -> void
63
68
 
64
69
  def self.lock_obtained: (
@@ -69,8 +74,9 @@ module RedisQueuedLocks
69
74
  String acquirer_id,
70
75
  String host_id,
71
76
  Integer|Float acq_time,
72
- Symbol access_strategy,
73
- Symbol rw_mode
77
+ RQL::accessStrategy access_strategy,
78
+ RQL::readWriteMode rw_mode,
79
+ Integer priority
74
80
  ) -> void
75
81
 
76
82
  def self.lock_obtaining_failed: (
@@ -81,8 +87,9 @@ module RedisQueuedLocks
81
87
  String acquirer_id,
82
88
  String host_id,
83
89
  Integer|Float acq_time,
84
- Symbol access_strategy,
85
- Symbol rw_mode,
90
+ RQL::accessStrategy access_strategy,
91
+ RQL::readWriteMode rw_mode,
92
+ Integer priority,
86
93
  Symbol result
87
94
  ) -> void
88
95
  end