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
@@ -19,8 +19,9 @@
19
19
  # - write lock request should be the first in the write lock queue (the classic lock queue)
20
20
  # and should wait for all earlier read lock requests;
21
21
  # - read lock request should wait for all earlier write lock requests;
22
- # - `:random` access strategy ignores both queues (no ordering between modes, so
23
- # a continuous flow of readers can starve writers);
22
+ # - `:random` access strategy ignores the order of both queues (no ordering between modes, so
23
+ # a continuous flow of readers can starve writers); its own lock request is queued like any
24
+ # lock request (`:queued` lock requests wait for it);
24
25
  #
25
26
  # Lock index (`RedisQueuedLocks::Resource::LOCK_INDEX_KEY`): only with the enabled lock index
26
27
  # (`lock_index_enabled`, `config['lock_index_enabled']`); with the disabled lock index (default)
@@ -37,9 +38,21 @@
37
38
  # - each queueing drops the expired lock names from the index (`ZREMRANGEBYSCORE`): the index
38
39
  # is maintained by the lock acquirement itself (no background jobs);
39
40
  #
41
+ # Lock request priorities (`priority`, see `RedisQueuedLocks::Resource::LOCK_PRIORITY_BAND`):
42
+ # - the position of the lock request (the score) is the enqueue time shifted by the priority
43
+ # band: requests of a higher priority are ahead of the requests of a lower priority in both
44
+ # queues (FIFO inside the same priority), so the queue heads and the order between modes
45
+ # (`find_opposite_request_ahead`) work with the positions as is;
46
+ # - dead lock requests are dropped per priority band (each band has its own time barrier):
47
+ # the band of the current request before the request is queued and the queue heads are read
48
+ # (both queues); a dead request of another band is dropped when it blocks the current request
49
+ # (the queue head or the earlier request of the opposite mode): its band and its enqueue time
50
+ # are decoded from its position, so the cost of an attempt does not depend on the number of
51
+ # priorities (see `drop_dead_requests_ahead`);
52
+ #
40
53
  # @api private
41
54
  # @since 1.0.0
42
- # @version 1.20.0
55
+ # @version 1.22.0
43
56
  # rubocop:disable Metrics/ModuleLength
44
57
  module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
45
58
  require_relative 'try_to_lock/log_visitor'
@@ -101,8 +114,8 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
101
114
  # @since 1.20.0
102
115
  LOCK_STATE_QUEUE_REPLIES_AT = 3
103
116
 
104
- # Position of the first request of the request queue (`ZRANGE`) in the replies of the lock
105
- # request queueing commands (`queue_lock_request`), relative to their first reply.
117
+ # Position of the first request of the request queue (`ZRANGE WITHSCORES`) in the replies of
118
+ # the lock request queueing commands (`queue_lock_request`), relative to their first reply.
106
119
  #
107
120
  # @return [Integer]
108
121
  #
@@ -120,6 +133,17 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
120
133
  # @since 1.20.0
121
134
  OPPOSITE_QUEUE_HEAD_REPLY = 4
122
135
 
136
+ # The maximum number of the round trips of one call of `drop_dead_requests_ahead` (the attempt or
137
+ # its pre-check; each round trip drops the dead blocking requests, the head of the write lock
138
+ # queue and the earlier request of the opposite mode, with the dead requests of their priority
139
+ # bands): the attempt fails when more dead lock requests block it (the next attempts drop them).
140
+ #
141
+ # @return [Integer]
142
+ #
143
+ # @api private
144
+ # @since 1.22.0
145
+ MAX_DEAD_REQUEST_REMOVALS = 5
146
+
123
147
  # @param redis [RedisClient]
124
148
  # @param logger [::Logger,#debug]
125
149
  # @param log_lock_try [Boolean]
@@ -135,6 +159,9 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
135
159
  # @param acquirer_id [String]
136
160
  # @param host_id [String]
137
161
  # @param acquirer_position [Numeric]
162
+ # The position of the lock request (the score: the enqueue time shifted by the priority band).
163
+ # @param priority [Integer]
164
+ # The lock request priority (the band of `acquirer_position`).
138
165
  # @param ttl [Integer]
139
166
  # @param queue_ttl [Integer]
140
167
  # @param lock_index_enabled [Boolean]
@@ -154,7 +181,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
154
181
  #
155
182
  # @api private
156
183
  # @since 1.0.0
157
- # @version 1.20.0
184
+ # @version 1.22.0
158
185
  # rubocop:disable Metrics/MethodLength
159
186
  def try_to_lock(
160
187
  redis,
@@ -170,6 +197,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
170
197
  acquirer_id,
171
198
  host_id,
172
199
  acquirer_position,
200
+ priority,
173
201
  ttl,
174
202
  queue_ttl,
175
203
  lock_index_enabled,
@@ -234,7 +262,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
234
262
  inter_result = precheck_lock(
235
263
  rconn, logger, log_sampled, log_lock_try, lock_key, read_write_mode, lock_readers_key,
236
264
  request_queue, opposite_request_queue, lock_name, acquirer_id, host_id,
237
- acquirer_position, acquirer_dead_score, queue_ttl, lock_index_enabled,
265
+ acquirer_position, priority, acquirer_dead_score, queue_ttl, lock_index_enabled,
238
266
  lock_index_grace_period, fail_fast, access_strategy
239
267
  )
240
268
  next if inter_result
@@ -255,14 +283,14 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
255
283
  # - read lock expiration of the current acquirer;
256
284
  # - the longest read lock (the last one in the readers registry): write lock requests
257
285
  # only (read lock requests do not wait for other readers);
258
- # - (eager queueing) the lock request queueing: Steps 1, 2.1, 3 (`queue_lock_request`);
286
+ # - (eager queueing) the lock request queueing: Steps 2.1, 1, 3 (`queue_lock_request`);
259
287
  # NOTE: replies are read by their positions: see "pipeline layout" in `pipeline_lock_state`
260
288
  # @type var lock_state: Array[untyped]
261
289
  lock_state = rconn.pipelined do |pipeline|
262
290
  pipeline_lock_state(
263
291
  pipeline, lock_key, lock_readers_key, acquirer_id, read_mode, eager_queueing,
264
- request_queue, opposite_request_queue, acquirer_position, acquirer_dead_score,
265
- lock_name, queue_ttl, lock_index_enabled, lock_index_grace_period
292
+ request_queue, opposite_request_queue, acquirer_position, priority,
293
+ acquirer_dead_score, lock_name, queue_ttl, lock_index_enabled, lock_index_grace_period
266
294
  )
267
295
  end
268
296
  # @type var current_lock_obtainer: String?
@@ -493,11 +521,12 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
493
521
  # Fast-Step X1: lock is already obtained. fail fast leads to "no try".
494
522
  inter_result = :fail_fast_no_try
495
523
  else
496
- # Step 1: add an acquirer to the lock acquirement queue
497
524
  # Step 2.1: drop expired acquirers from the lock queues
525
+ # Step 1: add an acquirer to the lock acquirement queue
498
526
  # Step 3: get the actual acquirer waiting in the queue
499
527
  # - one round trip for all of them (see `queue_lock_request`);
500
528
  # - eager queueing: already done in the lock state round trip (Step 0.1);
529
+ # - Step 2.2 (priorities) runs after them (see below);
501
530
  # @type var queue_replies: Array[untyped]
502
531
  queue_replies =
503
532
  if eager_queueing
@@ -506,30 +535,51 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
506
535
  rconn.pipelined do |pipeline|
507
536
  queue_lock_request(
508
537
  pipeline, request_queue, opposite_request_queue,
509
- acquirer_position, acquirer_id, acquirer_dead_score,
538
+ acquirer_position, priority, acquirer_id, acquirer_dead_score,
510
539
  lock_name, queue_ttl, lock_index_enabled, lock_index_grace_period
511
540
  )
512
541
  end
513
542
  end
514
543
  # NOTE: the position of the first queueing reply (see "pipeline layout" in Step 0.1)
515
544
  queue_replies_at = eager_queueing ? LOCK_STATE_QUEUE_REPLIES_AT : 0
516
- # @type var waiting_acquirer: String?
517
- waiting_acquirer = queue_replies[queue_replies_at + QUEUE_HEAD_REPLY].first
545
+ # @type var waiting_request: [String, Float]?
546
+ waiting_request = queue_replies[queue_replies_at + QUEUE_HEAD_REPLY].first
518
547
  # @type var opposite_waiting_request: [String, Float]?
519
548
  opposite_waiting_request =
520
549
  queue_replies[queue_replies_at + OPPOSITE_QUEUE_HEAD_REPLY].first
521
550
 
522
551
  LogVisitor.acq_added_to_queue(
523
552
  logger, log_sampled, log_lock_try, lock_key,
524
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode
553
+ queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode, priority
525
554
  )
526
555
  LogVisitor.remove_expired_acqs(
527
556
  logger, log_sampled, log_lock_try, lock_key,
528
557
  queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode
529
558
  )
559
+
560
+ # Step 2.2: (priorities) drop the dead lock requests of other priority bands that block
561
+ # the current one (the queue head or the earlier request of the opposite mode) and get
562
+ # the actual queue heads (no round trips while no dead request blocks the current one);
563
+ actual_queue_heads = drop_dead_requests_ahead(
564
+ rconn, read_write_mode, request_queue, opposite_request_queue, acquirer_id,
565
+ acquirer_position, acquirer_dead_score, access_strategy, waiting_request,
566
+ opposite_waiting_request
567
+ )
568
+ if actual_queue_heads
569
+ waiting_request, opposite_waiting_request, dropped_requests = actual_queue_heads
570
+ log_dropped_requests(
571
+ logger, log_sampled, log_lock_try, lock_key, queue_ttl, acquirer_id, host_id,
572
+ access_strategy, read_write_mode, dropped_requests
573
+ )
574
+ end
575
+ waiting_acquirer = waiting_request ? waiting_request[0] : nil
576
+ # NOTE: the priority of the first request is decoded for logs only
577
+ waiting_priority = logged_request_priority(log_sampled, log_lock_try, waiting_request)
578
+
530
579
  LogVisitor.get_first_from_queue(
531
580
  logger, log_sampled, log_lock_try, lock_key,
532
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode, waiting_acquirer
581
+ queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode, waiting_acquirer,
582
+ waiting_priority
533
583
  )
534
584
 
535
585
  # Step 3.1 (RW) + Steps PRE-4.x, ROLLBACK 1.1, 1.2, 2, 3: the obstacle to the lock
@@ -549,7 +599,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
549
599
  log_lock_obstacle(
550
600
  rconn, obstacle, logger, log_sampled, log_lock_try, lock_key, queue_ttl,
551
601
  acquirer_id, host_id, access_strategy, read_write_mode,
552
- waiting_acquirer, opposite_request_ahead, current_lock_obtainer
602
+ waiting_acquirer, waiting_priority, opposite_waiting_request, current_lock_obtainer
553
603
  )
554
604
  inter_result = obstacle
555
605
  elsif read_mode
@@ -648,6 +698,17 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
648
698
  end
649
699
  end
650
700
  end
701
+ rescue RedisQueuedLocks::LockAcquirementIntermediateTimeoutError,
702
+ RedisQueuedLocks::TimedLockIntermediateTimeoutError,
703
+ ::Timeout::ExitException
704
+ # NOTE: (timeouts) the timeout can interrupt the attempt inside the WATCH block:
705
+ # redis-client sends `UNWATCH` only on `StandardError` (the internal timeouts and the
706
+ # `Timeout.timeout` of the user without an exception class are `::Exception`s), and the
707
+ # connection would return to the pool with the active WATCH (an EXEC of the next
708
+ # transaction on it is aborted when a watched key is changed);
709
+ # - the closed connection drops the WATCH state and is reconnected on the next use;
710
+ rconn.close
711
+ raise
651
712
  end
652
713
 
653
714
  # Step 7: Analyze the aquirement attempt:
@@ -818,24 +879,27 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
818
879
 
819
880
  private
820
881
 
821
- # Queues the lock request and reads the lock queue state (Steps 1, 2.1, 3 and 3.2-3.4 of
882
+ # Queues the lock request and reads the lock queue state (Steps 2.1, 1, 3 and 3.2-3.4 of
822
883
  # the attempt): the commands are added to the given pipeline (one round trip with other
823
884
  # pipelined commands).
824
- # Replies (positions relative to the first one): [0] ZADD NX, [1] ZREMRANGEBYSCORE (request
825
- # queue), [2] ZREMRANGEBYSCORE (opposite request queue), [QUEUE_HEAD_REPLY] ZRANGE (the first
826
- # request of the request queue), [OPPOSITE_QUEUE_HEAD_REPLY] ZRANGE WITHSCORES (the first
827
- # request of the opposite request queue); the enabled lock index only: [5] PEXPIRE NX, [6]
828
- # PEXPIRE GT (the request queue TTL), [7] ZADD GT (the lock index), [8] ZREMRANGEBYSCORE
829
- # (expired lock names of the lock index). Update these constants when the commands change
830
- # (new commands go to the end: the replies are not read after `OPPOSITE_QUEUE_HEAD_REPLY`, so
831
- # the conditional lock index commands do not shift the read replies).
885
+ # Replies (positions relative to the first one): [0] ZREMRANGEBYSCORE (request queue, the
886
+ # priority band of the request), [1] ZREMRANGEBYSCORE (opposite request queue, the priority band
887
+ # of the request), [2] ZADD NX, [QUEUE_HEAD_REPLY] ZRANGE WITHSCORES (the first request of
888
+ # the request queue), [OPPOSITE_QUEUE_HEAD_REPLY] ZRANGE WITHSCORES (the first request of
889
+ # the opposite request queue); the enabled lock index only: [5] PEXPIRE NX, [6] PEXPIRE GT (the
890
+ # request queue TTL), [7] ZADD GT (the lock index), [8] ZREMRANGEBYSCORE (expired lock names of
891
+ # the lock index). Update these constants when the commands change (new commands go to the end:
892
+ # the replies are not read after `OPPOSITE_QUEUE_HEAD_REPLY`, so the conditional lock index
893
+ # commands do not shift the read replies).
832
894
  #
833
895
  # @param pipeline [RedisClient::Pipeline]
834
896
  # @param request_queue [String] Queue of the requests of the requested lock mode.
835
897
  # @param opposite_request_queue [String] Queue of the requests of the opposite lock mode.
836
- # @param acquirer_position [Numeric]
898
+ # @param acquirer_position [Numeric] The enqueue time shifted by the priority band.
899
+ # @param priority [Integer] The priority band of `acquirer_position`.
837
900
  # @param acquirer_id [String]
838
- # @param acquirer_dead_score [Float] Requests queued before this score are expired.
901
+ # @param acquirer_dead_score [Float]
902
+ # Requests queued before this time are expired (the barrier of the default priority band).
839
903
  # @param lock_name [String] The lock name (the member of the lock index).
840
904
  # @param queue_ttl [Integer] Lifetime of the lock request (in seconds).
841
905
  # @param lock_index_enabled [Boolean] Send the lock index commands (Steps 3.2-3.4).
@@ -844,11 +908,13 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
844
908
  #
845
909
  # @api private
846
910
  # @since 1.20.0
911
+ # @version 1.22.0
847
912
  def queue_lock_request(
848
913
  pipeline,
849
914
  request_queue,
850
915
  opposite_request_queue,
851
916
  acquirer_position,
917
+ priority,
852
918
  acquirer_id,
853
919
  acquirer_dead_score,
854
920
  lock_name,
@@ -856,6 +922,22 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
856
922
  lock_index_enabled,
857
923
  lock_index_grace_period
858
924
  )
925
+ # Step 2.1: drop expired acquirers from the lock queues
926
+ # - (RW) both queues are cleared: an expired request of the opposite mode should not
927
+ # block the current request forever;
928
+ # - (priorities) the requests of the priority band of the current request (the band has its
929
+ # own time barrier, see `RedisQueuedLocks::Resource::LOCK_PRIORITY_BAND`); a dead request of
930
+ # another band is dropped when it blocks the current request (`drop_dead_requests_ahead`),
931
+ # requests of lower priorities are never ahead of the current request;
932
+ # - before the queueing (Step 1): the old (dead) position of the requeued request (its
933
+ # position is reset after `queue_ttl`, see `dead_score_reached?`) is dropped first, so
934
+ # `ZADD NX` adds its new position in the same round trip (after `ZADD NX` the old position
935
+ # would be kept by `NX` and dropped by the removal: the request would be missing in the
936
+ # queue in this attempt);
937
+ pipeline_dead_requests_removal(
938
+ pipeline, request_queue, opposite_request_queue, acquirer_dead_score, priority
939
+ )
940
+
859
941
  # Step 1: add an acquirer to the lock acquirement queue
860
942
  # NOTE:
861
943
  # 'NX' means "Only add new elements. Don't update already existing elements."
@@ -865,15 +947,10 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
865
947
  # not in the queue now>>;
866
948
  pipeline.call('ZADD', request_queue, 'NX', acquirer_position, acquirer_id)
867
949
 
868
- # Step 2.1: drop expired acquirers from the lock queues
869
- # - (RW) both queues are cleared: an expired request of the opposite mode should not
870
- # block the current request forever;
871
- pipeline.call('ZREMRANGEBYSCORE', request_queue, '-inf', acquirer_dead_score)
872
- pipeline.call('ZREMRANGEBYSCORE', opposite_request_queue, '-inf', acquirer_dead_score)
873
-
874
- # Step 3: get the actual acquirer waiting in the queue
950
+ # Step 3: get the actual acquirer waiting in the queue (with its position: the band and
951
+ # the liveness of the queue head are decoded from it, see `drop_dead_requests_ahead`)
875
952
  # - (RW) and the first request of the opposite mode;
876
- pipeline.call('ZRANGE', request_queue, '0', '0')
953
+ pipeline.call('ZRANGE', request_queue, '0', '0', 'WITHSCORES')
877
954
  pipeline.call('ZRANGE', opposite_request_queue, '0', '0', 'WITHSCORES')
878
955
 
879
956
  # NOTE: the disabled lock index: no lock index commands, lock request queues have no TTL
@@ -910,6 +987,34 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
910
987
  )
911
988
  end
912
989
 
990
+ # Adds the removal of the dead lock requests of the given priority band (both lock queues) to
991
+ # the given pipeline (see `queue_lock_request` and `drop_dead_requests_ahead`).
992
+ #
993
+ # @param pipeline [RedisClient::Pipeline]
994
+ # @param request_queue [String]
995
+ # @param opposite_request_queue [String]
996
+ # @param acquirer_dead_score [Float]
997
+ # Requests queued before this time are expired (the barrier of the default priority band).
998
+ # @param priority [Integer]
999
+ # The priority band (see `RedisQueuedLocks::Resource::LOCK_PRIORITY_BAND`).
1000
+ # @return [void]
1001
+ #
1002
+ # @api private
1003
+ # @since 1.22.0
1004
+ def pipeline_dead_requests_removal(
1005
+ pipeline,
1006
+ request_queue,
1007
+ opposite_request_queue,
1008
+ acquirer_dead_score,
1009
+ priority
1010
+ )
1011
+ band_floor = RedisQueuedLocks::Resource.lock_priority_band_floor(priority)
1012
+ band_dead_score =
1013
+ RedisQueuedLocks::Resource.lock_priority_band_dead_score(acquirer_dead_score, priority)
1014
+ pipeline.call('ZREMRANGEBYSCORE', request_queue, band_floor, band_dead_score)
1015
+ pipeline.call('ZREMRANGEBYSCORE', opposite_request_queue, band_floor, band_dead_score)
1016
+ end
1017
+
913
1018
  # Adds the lock state commands to the given pipeline (the lock state of the attempt and of
914
1019
  # the pre-check: one round trip).
915
1020
  #
@@ -938,6 +1043,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
938
1043
  # @param request_queue [String]
939
1044
  # @param opposite_request_queue [String]
940
1045
  # @param acquirer_position [Numeric]
1046
+ # @param priority [Integer]
941
1047
  # @param acquirer_dead_score [Float]
942
1048
  # @param lock_name [String] The lock name (the member of the lock index).
943
1049
  # @param queue_ttl [Integer] In seconds.
@@ -947,6 +1053,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
947
1053
  #
948
1054
  # @api private
949
1055
  # @since 1.20.0
1056
+ # @version 1.22.0
950
1057
  def pipeline_lock_state(
951
1058
  pipeline,
952
1059
  lock_key,
@@ -957,6 +1064,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
957
1064
  request_queue,
958
1065
  opposite_request_queue,
959
1066
  acquirer_position,
1067
+ priority,
960
1068
  acquirer_dead_score,
961
1069
  lock_name,
962
1070
  queue_ttl,
@@ -970,7 +1078,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
970
1078
  if queueing
971
1079
  queue_lock_request(
972
1080
  pipeline, request_queue, opposite_request_queue,
973
- acquirer_position, acquirer_id, acquirer_dead_score,
1081
+ acquirer_position, priority, acquirer_id, acquirer_dead_score,
974
1082
  lock_name, queue_ttl, lock_index_enabled, lock_index_grace_period
975
1083
  )
976
1084
  end
@@ -980,6 +1088,8 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
980
1088
 
981
1089
  # (RW) The request of the opposite mode that should obtain the lock earlier
982
1090
  # - FIFO between modes (`:queued` access strategy only);
1091
+ # - (priorities) positions are shifted by the priority bands: a request of a higher priority
1092
+ # is ahead of any request of a lower priority, requests of the same priority are FIFO;
983
1093
  # - equal positions are ordered by acquirer ids;
984
1094
  #
985
1095
  # @param access_strategy [Symbol]
@@ -1071,12 +1181,17 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
1071
1181
  # @param access_strategy [Symbol]
1072
1182
  # @param read_write_mode [Symbol]
1073
1183
  # @param waiting_acquirer [String,NilClass]
1074
- # @param opposite_request_ahead [String,NilClass]
1184
+ # @param waiting_priority [Integer,NilClass]
1185
+ # The priority of the first request (decoded for logs only: `nil` without logs).
1186
+ # @param opposite_waiting_request [Array<String,Float>,NilClass]
1187
+ # The first request of the opposite queue (the request ahead of the `*_request_is_ahead`
1188
+ # obstacles, see `find_opposite_request_ahead`).
1075
1189
  # @param current_lock_obtainer [String,NilClass]
1076
1190
  # @return [void]
1077
1191
  #
1078
1192
  # @api private
1079
1193
  # @since 1.20.0
1194
+ # @version 1.22.0
1080
1195
  # rubocop:disable Metrics/MethodLength
1081
1196
  def log_lock_obstacle(
1082
1197
  rconn,
@@ -1091,7 +1206,8 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
1091
1206
  access_strategy,
1092
1207
  read_write_mode,
1093
1208
  waiting_acquirer,
1094
- opposite_request_ahead,
1209
+ waiting_priority,
1210
+ opposite_waiting_request,
1095
1211
  current_lock_obtainer
1096
1212
  )
1097
1213
  case obstacle
@@ -1104,44 +1220,270 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
1104
1220
  LogVisitor.exit__no_first(
1105
1221
  logger, log_sampled, log_lock_try, lock_key,
1106
1222
  queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode, waiting_acquirer,
1223
+ waiting_priority,
1107
1224
  # NOTE: lock data is extracted for logs only
1108
1225
  ((log_sampled && log_lock_try) ? rconn.call('HGETALL', lock_key).to_h : NO_LOCK_DATA)
1109
1226
  )
1110
- when :write_request_is_ahead
1111
- LogVisitor.exit__write_request_ahead(
1112
- logger, log_sampled, log_lock_try, lock_key,
1113
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode,
1114
- opposite_request_ahead #: String
1115
- )
1116
- when :read_request_is_ahead
1117
- LogVisitor.exit__read_request_ahead(
1118
- logger, log_sampled, log_lock_try, lock_key,
1119
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode,
1120
- opposite_request_ahead #: String
1121
- )
1227
+ when :write_request_is_ahead, :read_request_is_ahead
1228
+ ahead_request = opposite_waiting_request #: [String, Float]
1229
+ # NOTE: the priority of the request ahead is decoded for logs only
1230
+ ahead_priority = logged_request_priority(log_sampled, log_lock_try, ahead_request)
1231
+ if obstacle == :write_request_is_ahead
1232
+ LogVisitor.exit__write_request_ahead(
1233
+ logger, log_sampled, log_lock_try, lock_key,
1234
+ queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode,
1235
+ ahead_request[0], ahead_priority
1236
+ )
1237
+ else
1238
+ LogVisitor.exit__read_request_ahead(
1239
+ logger, log_sampled, log_lock_try, lock_key,
1240
+ queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode,
1241
+ ahead_request[0], ahead_priority
1242
+ )
1243
+ end
1122
1244
  when :lock_is_still_acquired
1123
1245
  LogVisitor.exit__lock_still_obtained(
1124
1246
  logger, log_sampled, log_lock_try, lock_key,
1125
1247
  queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode,
1126
- waiting_acquirer, current_lock_obtainer, #: String
1248
+ waiting_acquirer, waiting_priority,
1249
+ current_lock_obtainer, #: String
1127
1250
  # NOTE: lock data is extracted for logs only
1128
1251
  ((log_sampled && log_lock_try) ? rconn.call('HGETALL', lock_key).to_h : NO_LOCK_DATA)
1129
1252
  )
1130
1253
  when :read_lock_is_still_acquired
1131
1254
  LogVisitor.exit__read_lock_still_obtained(
1132
1255
  logger, log_sampled, log_lock_try, lock_key,
1133
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode, waiting_acquirer
1256
+ queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode, waiting_acquirer,
1257
+ waiting_priority
1134
1258
  )
1135
1259
  end
1136
1260
  end
1137
1261
  # rubocop:enable Metrics/MethodLength
1138
1262
 
1263
+ # (priorities) The priority of the lock request for the try-lock logs (the first request of
1264
+ # the queue, the request ahead): decoded only when the try-lock logs are written (log-only values
1265
+ # are never computed without the logs: `log_sampled && log_lock_try`).
1266
+ #
1267
+ # @param log_sampled [Boolean]
1268
+ # @param log_lock_try [Boolean]
1269
+ # @param lock_request [Array<String,Float>,NilClass]
1270
+ # The acquirer and the position of the lock request.
1271
+ # @return [Integer,NilClass] `nil` without the try-lock logs or without the lock request.
1272
+ #
1273
+ # @api private
1274
+ # @since 1.22.0
1275
+ def logged_request_priority(log_sampled, log_lock_try, lock_request)
1276
+ return unless log_sampled && log_lock_try && lock_request
1277
+
1278
+ RedisQueuedLocks::Resource.lock_request_priority(lock_request[1])
1279
+ end
1280
+
1281
+ # (priorities) Drops the dead lock requests of other priority bands that block the current lock
1282
+ # request (`:queued` access strategy: the head of the write lock queue for write lock requests,
1283
+ # the earlier lock request of the opposite mode for both modes) and reads the actual queue heads
1284
+ # (Step 2.2 of the attempt and of its pre-check):
1285
+ # - the dead lock requests of the band of the current request are dropped by the queueing
1286
+ # (Step 2.1, before the queue heads are read) and requests of lower priorities are never
1287
+ # ahead of it: only a request of a higher priority can be a dead blocking request;
1288
+ # - the band and the enqueue time of the blocking request are decoded from its position (see
1289
+ # `RedisQueuedLocks::Resource::LOCK_PRIORITY_BAND`): no Redis commands to check the liveness,
1290
+ # any band is recognized (the priority range of the current RQL version is not used), the
1291
+ # time barrier is the barrier of the current request (`acquirer_dead_score`: its `queue_ttl`);
1292
+ # - the dead blocking requests (the head of the write lock queue and the earlier request of the
1293
+ # opposite mode) are dropped with all dead requests of their bands (both queues) in one round
1294
+ # trip with the reading of the actual queue heads; no round trips while no dead request
1295
+ # blocks the current one (the lock state does not matter: the attempt drops the dead blocking
1296
+ # requests even when the lock is obtained by another acquirer);
1297
+ # - up to `MAX_DEAD_REQUEST_REMOVALS` round trips (one or two bands each) per call;
1298
+ # - (infinite positions) RQL never writes them (manual writes of the queues, see
1299
+ # `RedisQueuedLocks::Resource.lock_request_priority`): a blocking request at `-inf` is always
1300
+ # dead (the highest band, no enqueue time) and is dropped with all requests at `-inf` of both
1301
+ # queues in the same round trip; a request at `+inf` is never ahead of the current request;
1302
+ # - the dropped blocking requests are returned, not logged: the attempt and its pre-check log
1303
+ # them at the same step (after the queueing log events, see `log_dropped_requests`);
1304
+ #
1305
+ # @param rconn [RedisClient]
1306
+ # @param read_write_mode [Symbol]
1307
+ # @param request_queue [String]
1308
+ # @param opposite_request_queue [String]
1309
+ # @param acquirer_id [String]
1310
+ # @param acquirer_position [Numeric]
1311
+ # @param acquirer_dead_score [Float]
1312
+ # Requests queued before this time are expired (the barrier of the default priority band).
1313
+ # @param access_strategy [Symbol]
1314
+ # @param waiting_request [Array<String,Float>,NilClass] The head of the request queue.
1315
+ # @param opposite_waiting_request [Array<String,Float>,NilClass]
1316
+ # The head of the opposite request queue.
1317
+ # @return [Array<Array<String,Float>,NilClass,Array<Array<String,Integer>>>,NilClass]
1318
+ # - the actual queue heads and the dropped blocking requests (the acquirer and the priority of
1319
+ # each one, in the order of the removal): `[waiting_request, opposite_waiting_request,
1320
+ # dropped_requests]` when at least one dead lock request is dropped;
1321
+ # - `nil` otherwise;
1322
+ #
1323
+ # @api private
1324
+ # @since 1.22.0
1325
+ # rubocop:disable Metrics/MethodLength
1326
+ def drop_dead_requests_ahead(
1327
+ rconn,
1328
+ read_write_mode,
1329
+ request_queue,
1330
+ opposite_request_queue,
1331
+ acquirer_id,
1332
+ acquirer_position,
1333
+ acquirer_dead_score,
1334
+ access_strategy,
1335
+ waiting_request,
1336
+ opposite_waiting_request
1337
+ )
1338
+ return unless access_strategy == :queued
1339
+
1340
+ read_mode = (read_write_mode == :read)
1341
+ round_trips = 0
1342
+ # NOTE: allocated by the first removal only (no allocations while nothing dead blocks)
1343
+ # @type var dropped_requests: Array[[String, Integer]]?
1344
+ dropped_requests = nil
1345
+
1346
+ while round_trips < MAX_DEAD_REQUEST_REMOVALS
1347
+ # NOTE: the lock requests that block the current one (the same order as in `lock_obstacle`):
1348
+ # the head of the write lock queue (write lock requests only) and the earlier lock request
1349
+ # of the opposite mode; a live head of the write lock queue stops the removal (the attempt
1350
+ # waits for it), the dead blocking requests are dropped in one round trip (both bands);
1351
+ # @type var dead_head: [String, Float]?
1352
+ dead_head = nil
1353
+ # @type var dead_head_priority: Integer?
1354
+ dead_head_priority = nil
1355
+ if !read_mode && waiting_request && waiting_request[0] != acquirer_id
1356
+ dead_head_priority = dead_request_priority(waiting_request, acquirer_dead_score)
1357
+ break unless dead_head_priority
1358
+
1359
+ dead_head = waiting_request
1360
+ end
1361
+ # @type var dead_ahead: [String, Float]?
1362
+ dead_ahead = nil
1363
+ # @type var dead_ahead_priority: Integer?
1364
+ dead_ahead_priority = nil
1365
+ if find_opposite_request_ahead(
1366
+ access_strategy, opposite_waiting_request, acquirer_position, acquirer_id
1367
+ )
1368
+ ahead_request = opposite_waiting_request #: [String, Float]
1369
+ dead_ahead_priority = dead_request_priority(ahead_request, acquirer_dead_score)
1370
+ dead_ahead = ahead_request if dead_ahead_priority
1371
+ end
1372
+ break unless dead_head_priority || dead_ahead_priority
1373
+
1374
+ # NOTE: (pipeline layout) ZREMRANGEBYSCORE of both queues for each dropped band (one or two
1375
+ # bands) and for the requests at `-inf` (a dead blocking request at `-inf` only), then [-2]
1376
+ # ZRANGE WITHSCORES (the head of the request queue) and [-1] ZRANGE WITHSCORES (the head of
1377
+ # the opposite request queue);
1378
+ # @type var queue_state: Array[untyped]
1379
+ queue_state = rconn.pipelined do |pipeline|
1380
+ if dead_head_priority
1381
+ pipeline_dead_requests_removal(
1382
+ pipeline, request_queue, opposite_request_queue, acquirer_dead_score, dead_head_priority
1383
+ )
1384
+ end
1385
+ if dead_ahead_priority && dead_ahead_priority != dead_head_priority
1386
+ pipeline_dead_requests_removal(
1387
+ pipeline, request_queue, opposite_request_queue, acquirer_dead_score,
1388
+ dead_ahead_priority
1389
+ )
1390
+ end
1391
+ # NOTE: (infinite positions) a dead blocking request at `-inf` (a manual write: out of all
1392
+ # bands, see `RedisQueuedLocks::Resource.lock_request_priority`) is not covered by
1393
+ # the band removal of its decoded priority (it drops the dead requests of that band
1394
+ # only): the requests at `-inf` are dropped from both queues by their position;
1395
+ if (dead_head && dead_head[1] == -Float::INFINITY) ||
1396
+ (dead_ahead && dead_ahead[1] == -Float::INFINITY)
1397
+ pipeline.call('ZREMRANGEBYSCORE', request_queue, '-inf', '-inf')
1398
+ pipeline.call('ZREMRANGEBYSCORE', opposite_request_queue, '-inf', '-inf')
1399
+ end
1400
+ pipeline.call('ZRANGE', request_queue, '0', '0', 'WITHSCORES')
1401
+ pipeline.call('ZRANGE', opposite_request_queue, '0', '0', 'WITHSCORES')
1402
+ end
1403
+ # @type var removed_requests: Array[[String, Integer]]
1404
+ removed_requests = dropped_requests || []
1405
+ removed_requests << [dead_head[0], dead_head_priority] if dead_head && dead_head_priority
1406
+ removed_requests << [dead_ahead[0], dead_ahead_priority] if dead_ahead && dead_ahead_priority
1407
+ dropped_requests = removed_requests
1408
+ waiting_request = queue_state[-2].first
1409
+ opposite_waiting_request = queue_state[-1].first
1410
+ round_trips += 1
1411
+ end
1412
+
1413
+ [waiting_request, opposite_waiting_request, dropped_requests] if dropped_requests
1414
+ end
1415
+ # rubocop:enable Metrics/MethodLength
1416
+
1417
+ # (priorities) Logs the dead lock requests dropped by `drop_dead_requests_ahead` (the attempt
1418
+ # and its pre-check log them at the same step: after the queueing log events).
1419
+ #
1420
+ # @param logger [::Logger,#debug]
1421
+ # @param log_sampled [Boolean]
1422
+ # @param log_lock_try [Boolean]
1423
+ # @param lock_key [String]
1424
+ # @param queue_ttl [Integer]
1425
+ # @param acquirer_id [String]
1426
+ # @param host_id [String]
1427
+ # @param access_strategy [Symbol]
1428
+ # @param read_write_mode [Symbol]
1429
+ # @param dropped_requests [Array<Array<String,Integer>>]
1430
+ # The acquirer and the priority of each dropped lock request.
1431
+ # @return [void]
1432
+ #
1433
+ # @api private
1434
+ # @since 1.22.0
1435
+ def log_dropped_requests(
1436
+ logger,
1437
+ log_sampled,
1438
+ log_lock_try,
1439
+ lock_key,
1440
+ queue_ttl,
1441
+ acquirer_id,
1442
+ host_id,
1443
+ access_strategy,
1444
+ read_write_mode,
1445
+ dropped_requests
1446
+ )
1447
+ return unless log_sampled && log_lock_try
1448
+
1449
+ dropped_requests.each do |dead_acquirer_id, dead_priority|
1450
+ LogVisitor.dead_request_removed(
1451
+ logger, log_sampled, log_lock_try, lock_key, queue_ttl, acquirer_id, host_id,
1452
+ access_strategy, read_write_mode, dead_acquirer_id, dead_priority
1453
+ )
1454
+ end
1455
+ end
1456
+
1457
+ # (priorities) The priority band of the dead lock request (see `drop_dead_requests_ahead`): the
1458
+ # band and the enqueue time are decoded from the position of the request (infinite positions:
1459
+ # `-inf` is decoded to the highest band and is always dead, `+inf` to the lowest band and is never
1460
+ # dead, see `RedisQueuedLocks::Resource.lock_request_priority`).
1461
+ #
1462
+ # @param lock_request [Array<String,Float>] The acquirer and the position of the lock request.
1463
+ # @param acquirer_dead_score [Float]
1464
+ # Requests queued before this time are expired (the barrier of the default priority band).
1465
+ # @return [Integer,NilClass]
1466
+ # The priority of the dead lock request, `nil` for a live one (its position is after the time
1467
+ # barrier of its band: the same barrier as the dropping of the dead requests of the band).
1468
+ #
1469
+ # @api private
1470
+ # @since 1.22.0
1471
+ def dead_request_priority(lock_request, acquirer_dead_score)
1472
+ priority = RedisQueuedLocks::Resource.lock_request_priority(lock_request[1])
1473
+ band_dead_score =
1474
+ RedisQueuedLocks::Resource.lock_priority_band_dead_score(acquirer_dead_score, priority)
1475
+ priority if lock_request[1] <= band_dead_score
1476
+ end
1477
+
1139
1478
  # Pre-check of the attempt (Step PRE): the lock state without WATCH (one round trip).
1140
1479
  # - `fail_fast` attempts: the lock state only (the request is not queued: the failed "no try"
1141
1480
  # attempt does not dequeue the request); fails with `:fail_fast_no_try` on the obtained lock;
1142
1481
  # - retries: the lock state and the lock request queueing (the request is already queued by
1143
1482
  # the previous attempts); fails with the obstacle of the classic logic (`lock_obstacle`)
1144
- # and logs the same events as the failed attempt;
1483
+ # and logs the same events as the failed attempt in the same order;
1484
+ # - (priorities) retries drop the dead lock requests of other priority bands that block the
1485
+ # request before the decision (Step 2.2 of the attempt: one more round trip per dropped
1486
+ # band, see `drop_dead_requests_ahead`);
1145
1487
  # - same-process conflicts (the lock is obtained by the current acquirer) and locks that can be
1146
1488
  # obtained are not decided: the attempt continues with the WATCH/MULTI logic (`nil`);
1147
1489
  # - the pre-check never obtains the lock (mutual exclusion relies on WATCH/MULTI only);
@@ -1159,6 +1501,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
1159
1501
  # @param acquirer_id [String]
1160
1502
  # @param host_id [String]
1161
1503
  # @param acquirer_position [Numeric]
1504
+ # @param priority [Integer]
1162
1505
  # @param acquirer_dead_score [Float]
1163
1506
  # @param queue_ttl [Integer]
1164
1507
  # @param lock_index_enabled [Boolean]
@@ -1169,6 +1512,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
1169
1512
  #
1170
1513
  # @api private
1171
1514
  # @since 1.20.0
1515
+ # @version 1.22.0
1172
1516
  # rubocop:disable Metrics/MethodLength
1173
1517
  def precheck_lock(
1174
1518
  rconn,
@@ -1184,6 +1528,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
1184
1528
  acquirer_id,
1185
1529
  host_id,
1186
1530
  acquirer_position,
1531
+ priority,
1187
1532
  acquirer_dead_score,
1188
1533
  queue_ttl,
1189
1534
  lock_index_enabled,
@@ -1197,7 +1542,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
1197
1542
  lock_state = rconn.pipelined do |pipeline|
1198
1543
  pipeline_lock_state(
1199
1544
  pipeline, lock_key, lock_readers_key, acquirer_id, read_mode, !fail_fast,
1200
- request_queue, opposite_request_queue, acquirer_position, acquirer_dead_score,
1545
+ request_queue, opposite_request_queue, acquirer_position, priority, acquirer_dead_score,
1201
1546
  lock_name, queue_ttl, lock_index_enabled, lock_index_grace_period
1202
1547
  )
1203
1548
  end
@@ -1217,12 +1562,26 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
1217
1562
  # NOTE: the fail-fast check of the attempt (Fast-Step X0) without same-process conflicts
1218
1563
  return ((current_lock_obtainer || read_locked) ? :fail_fast_no_try : nil) if fail_fast
1219
1564
 
1220
- # @type var waiting_acquirer: String?
1221
- waiting_acquirer =
1222
- lock_state[LOCK_STATE_QUEUE_REPLIES_AT + QUEUE_HEAD_REPLY].first
1565
+ # @type var waiting_request: [String, Float]?
1566
+ waiting_request = lock_state[LOCK_STATE_QUEUE_REPLIES_AT + QUEUE_HEAD_REPLY].first
1223
1567
  # @type var opposite_waiting_request: [String, Float]?
1224
1568
  opposite_waiting_request =
1225
1569
  lock_state[LOCK_STATE_QUEUE_REPLIES_AT + OPPOSITE_QUEUE_HEAD_REPLY].first
1570
+ # NOTE: (priorities) Step 2.2 of the attempt: the dead lock requests of other priority bands
1571
+ # that block the current one are dropped before the obstacle decision; they are logged at
1572
+ # the step of the attempt (after the queueing log events of the failed pre-check, they
1573
+ # follow the decision) or before the attempt that continues (it logs its own queueing);
1574
+ actual_queue_heads = drop_dead_requests_ahead(
1575
+ rconn, read_write_mode, request_queue, opposite_request_queue, acquirer_id,
1576
+ acquirer_position, acquirer_dead_score, access_strategy, waiting_request,
1577
+ opposite_waiting_request
1578
+ )
1579
+ # @type var dropped_requests: Array[[String, Integer]]?
1580
+ dropped_requests = nil
1581
+ if actual_queue_heads
1582
+ waiting_request, opposite_waiting_request, dropped_requests = actual_queue_heads
1583
+ end
1584
+ waiting_acquirer = waiting_request ? waiting_request[0] : nil
1226
1585
  # @type var opposite_request_ahead: String?
1227
1586
  opposite_request_ahead = find_opposite_request_ahead(
1228
1587
  access_strategy, opposite_waiting_request, acquirer_position, acquirer_id
@@ -1231,25 +1590,44 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
1231
1590
  read_mode, access_strategy, acquirer_id, waiting_acquirer, opposite_request_ahead,
1232
1591
  current_lock_obtainer, read_locked
1233
1592
  )
1234
- return unless obstacle
1593
+ unless obstacle
1594
+ if dropped_requests
1595
+ log_dropped_requests(
1596
+ logger, log_sampled, log_lock_try, lock_key, queue_ttl, acquirer_id, host_id,
1597
+ access_strategy, read_write_mode, dropped_requests
1598
+ )
1599
+ end
1600
+ return
1601
+ end
1235
1602
 
1236
- # NOTE: the same log events as the failed attempt (queueing steps + the exit event)
1603
+ # NOTE: the priority of the first request is decoded for logs only
1604
+ waiting_priority = logged_request_priority(log_sampled, log_lock_try, waiting_request)
1605
+
1606
+ # NOTE: the same log events as the failed attempt in the same order (queueing steps, the dropped
1607
+ # dead requests, the first request, the exit event)
1237
1608
  LogVisitor.acq_added_to_queue(
1238
1609
  logger, log_sampled, log_lock_try, lock_key,
1239
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode
1610
+ queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode, priority
1240
1611
  )
1241
1612
  LogVisitor.remove_expired_acqs(
1242
1613
  logger, log_sampled, log_lock_try, lock_key,
1243
1614
  queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode
1244
1615
  )
1616
+ if dropped_requests
1617
+ log_dropped_requests(
1618
+ logger, log_sampled, log_lock_try, lock_key, queue_ttl, acquirer_id, host_id,
1619
+ access_strategy, read_write_mode, dropped_requests
1620
+ )
1621
+ end
1245
1622
  LogVisitor.get_first_from_queue(
1246
1623
  logger, log_sampled, log_lock_try, lock_key,
1247
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode, waiting_acquirer
1624
+ queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode, waiting_acquirer,
1625
+ waiting_priority
1248
1626
  )
1249
1627
  log_lock_obstacle(
1250
1628
  rconn, obstacle, logger, log_sampled, log_lock_try, lock_key, queue_ttl,
1251
1629
  acquirer_id, host_id, access_strategy, read_write_mode,
1252
- waiting_acquirer, opposite_request_ahead, current_lock_obtainer
1630
+ waiting_acquirer, waiting_priority, opposite_waiting_request, current_lock_obtainer
1253
1631
  )
1254
1632
  obstacle
1255
1633
  end