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
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Read/Write locks (`read_write_mode`):
3
+ # Read/Write locks (`rw_mode`):
4
4
  # - `:write` lock is exclusive: it is obtained when there is no write lock and no live read lock;
5
5
  # - `:read` lock is shared: it is obtained when there is no write lock;
6
6
  # - read locks are stored in the readers registry: a sorted set where the member is an acquirer
@@ -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.23.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,11 +133,49 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
120
133
  # @since 1.20.0
121
134
  OPPOSITE_QUEUE_HEAD_REPLY = 4
122
135
 
136
+ # The number of the replies of the lock request queueing commands (`queue_lock_request`) with
137
+ # the disabled lock index.
138
+ #
139
+ # @return [Integer]
140
+ #
141
+ # @api private
142
+ # @since 1.23.0
143
+ QUEUE_REPLIES = 5
144
+
145
+ # The number of the replies of the lock index commands of the lock request queueing
146
+ # (`queue_lock_request`, the enabled lock index only).
147
+ #
148
+ # @return [Integer]
149
+ #
150
+ # @api private
151
+ # @since 1.23.0
152
+ QUEUE_LOCK_INDEX_REPLIES = 4
153
+
154
+ # The number of the commands of the read lock extension (`transact_read_lock_extension`) without
155
+ # the lock index command (the enabled lock index only: the last command).
156
+ #
157
+ # @return [Integer]
158
+ #
159
+ # @api private
160
+ # @since 1.23.0
161
+ READ_LOCK_EXTENSION_COMMANDS = 8
162
+
163
+ # The maximum number of the round trips of one call of `drop_dead_requests_ahead` (the attempt or
164
+ # its pre-check; each round trip drops the dead blocking requests, the head of the write lock
165
+ # queue and the earlier request of the opposite mode, with the dead requests of their priority
166
+ # bands): the attempt fails when more dead lock requests block it (the next attempts drop them).
167
+ #
168
+ # @return [Integer]
169
+ #
170
+ # @api private
171
+ # @since 1.22.0
172
+ MAX_DEAD_REQUEST_REMOVALS = 5
173
+
123
174
  # @param redis [RedisClient]
124
175
  # @param logger [::Logger,#debug]
125
176
  # @param log_lock_try [Boolean]
126
177
  # @param lock_key [String]
127
- # @param read_write_mode [Symbol] `:read` or `:write`
178
+ # @param rw_mode [Symbol] `:read` or `:write`
128
179
  # @param lock_key_queue [String] Queue of write lock requests.
129
180
  # @param read_lock_key_queue [String] Queue of read lock requests.
130
181
  # @param lock_readers_key [String] Registry of read lock holders.
@@ -135,6 +186,9 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
135
186
  # @param acquirer_id [String]
136
187
  # @param host_id [String]
137
188
  # @param acquirer_position [Numeric]
189
+ # The position of the lock request (the score: the enqueue time shifted by the priority band).
190
+ # @param priority [Integer]
191
+ # The lock request priority (the band of `acquirer_position`).
138
192
  # @param ttl [Integer]
139
193
  # @param queue_ttl [Integer]
140
194
  # @param lock_index_enabled [Boolean]
@@ -154,14 +208,14 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
154
208
  #
155
209
  # @api private
156
210
  # @since 1.0.0
157
- # @version 1.21.0
211
+ # @version 1.22.0
158
212
  # rubocop:disable Metrics/MethodLength
159
213
  def try_to_lock(
160
214
  redis,
161
215
  logger,
162
216
  log_lock_try,
163
217
  lock_key,
164
- read_write_mode,
218
+ rw_mode,
165
219
  lock_key_queue,
166
220
  read_lock_key_queue,
167
221
  lock_readers_key,
@@ -170,6 +224,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
170
224
  acquirer_id,
171
225
  host_id,
172
226
  acquirer_position,
227
+ priority,
173
228
  ttl,
174
229
  queue_ttl,
175
230
  lock_index_enabled,
@@ -192,9 +247,9 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
192
247
  # Step RW: the mode of the lock that is really held by the acquirer after the attempt
193
248
  # (the requested mode or the mode of the already obtained lock for same-process conflicts);
194
249
  # @type var held_rw_mode: Symbol
195
- held_rw_mode = read_write_mode
250
+ held_rw_mode = rw_mode
196
251
  # Step RW: read lock request flag
197
- read_mode = (read_write_mode == :read)
252
+ read_mode = (rw_mode == :read)
198
253
  # Step RW: request queue of the current mode and the queue of the opposite mode
199
254
  request_queue = read_mode ? read_lock_key_queue : lock_key_queue
200
255
  opposite_request_queue = read_mode ? lock_key_queue : read_lock_key_queue
@@ -215,14 +270,14 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
215
270
 
216
271
  LogVisitor.start(
217
272
  logger, log_sampled, log_lock_try, lock_key,
218
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode
273
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode
219
274
  )
220
275
 
221
276
  # Step X: start to work with lock acquiring
222
277
  result = redis.with do |rconn|
223
278
  LogVisitor.rconn_fetched(
224
279
  logger, log_sampled, log_lock_try, lock_key,
225
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode
280
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode
226
281
  )
227
282
 
228
283
  # Step PRE: (pre-check) the lock state without WATCH: the attempt that can not obtain
@@ -232,9 +287,9 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
232
287
  # - see `precheck_lock` for details;
233
288
  if precheck
234
289
  inter_result = precheck_lock(
235
- rconn, logger, log_sampled, log_lock_try, lock_key, read_write_mode, lock_readers_key,
290
+ rconn, logger, log_sampled, log_lock_try, lock_key, rw_mode, lock_readers_key,
236
291
  request_queue, opposite_request_queue, lock_name, acquirer_id, host_id,
237
- acquirer_position, acquirer_dead_score, queue_ttl, lock_index_enabled,
292
+ acquirer_position, priority, acquirer_dead_score, queue_ttl, lock_index_enabled,
238
293
  lock_index_grace_period, fail_fast, access_strategy
239
294
  )
240
295
  next if inter_result
@@ -255,14 +310,14 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
255
310
  # - read lock expiration of the current acquirer;
256
311
  # - the longest read lock (the last one in the readers registry): write lock requests
257
312
  # 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`);
313
+ # - (eager queueing) the lock request queueing: Steps 2.1, 1, 3 (`queue_lock_request`);
259
314
  # NOTE: replies are read by their positions: see "pipeline layout" in `pipeline_lock_state`
260
315
  # @type var lock_state: Array[untyped]
261
316
  lock_state = rconn.pipelined do |pipeline|
262
317
  pipeline_lock_state(
263
318
  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
319
+ request_queue, opposite_request_queue, acquirer_position, priority,
320
+ acquirer_dead_score, lock_name, queue_ttl, lock_index_enabled, lock_index_grace_period
266
321
  )
267
322
  end
268
323
  # @type var current_lock_obtainer: String?
@@ -296,7 +351,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
296
351
  if sp_conflict_rw_mode != nil
297
352
  LogVisitor.same_process_conflict_detected(
298
353
  logger, log_sampled, log_lock_try, lock_key,
299
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode
354
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode
300
355
  )
301
356
 
302
357
  # NOTE:
@@ -306,30 +361,10 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
306
361
 
307
362
  # SP-Conflict Step X2: self-process dead lock moment started.
308
363
  # SP-Conflict CHECK (Step CHECK): check chosen strategy and flag the current status
309
- case conflict_strategy
310
- when :work_through
311
- # <SP-Conflict Moment>: work through => exit
312
- sp_conflict_status = lock_upgrade ? :conflict_lock_upgrade : :conflict_work_through
313
- when :extendable_work_through
314
- # <SP-Conflict Moment>: extendable_work_through => extend the lock pttl and exit
315
- sp_conflict_status =
316
- lock_upgrade ? :conflict_lock_upgrade : :extendable_conflict_work_through
317
- when :wait_for_lock
318
- # <SP-Conflict Moment>: wait_for_lock => obtain a lock in classic way
319
- sp_conflict_status = :conflict_wait_for_lock
320
- when :dead_locking
321
- # <SP-Conflict Moment>: dead_locking => exit and fail
322
- sp_conflict_status = :conflict_dead_lock
323
- else
324
- # <SP-Conflict Moment>:
325
- # - unknown status => work in classic way <wait_for_lock>
326
- # - it is a case when the new status is added to the code base in the past
327
- # but is forgotten to be added here;
328
- sp_conflict_status = :conflict_wait_for_lock
329
- end
364
+ sp_conflict_status = same_process_conflict_status(conflict_strategy, lock_upgrade)
330
365
  LogVisitor.same_process_conflict_analyzed(
331
366
  logger, log_sampled, log_lock_try, lock_key,
332
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode, sp_conflict_status
367
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode, sp_conflict_status
333
368
  )
334
369
  end
335
370
 
@@ -338,71 +373,17 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
338
373
  # SP-Conflict-Step FINAL (SPCF): extend the lock and work through
339
374
  # - extend the lock ttl;
340
375
  # - store extensions in lock metadata;
376
+ spc_processed_timestamp = Time.now.to_f
341
377
  if sp_conflict_rw_mode == :write
342
378
  held_rw_mode = :write
343
-
344
- # SPCF Step 1: extend the lock pttl
345
- # - [REDIS RESULT]: in normal cases should return the last script command value
346
- # - for the current script should return:
347
- # <1> => timeout was set;
348
- # <0> => timeount was not set;
349
- transact.call('EVAL', EXTEND_LOCK_PTTL, 1, lock_key, ttl)
350
- # SPCF Step <Meta>: store conflict-state additionals in lock metadata:
351
- # SPCF Step 2: (lock meta-data)
352
- # - add the added ttl to reflect the real lock TTL in info;
353
- # - [REDIS RESULT]: in normal cases should return the value of <ttl> key
354
- # - for non-existent key value starts from <0> (zero)
355
- transact.call('HINCRBY', lock_key, 'spc_ext_ttl', ttl)
356
- # SPCF Step 3: (lock meta-data)
357
- # - increment the conflcit counter in order to remember
358
- # how many times dead lock happened;
359
- # - [REDIS RESULT]: in normal cases should return the value of <spc_cnt> key
360
- # - for non-existent key starts from 0
361
- transact.call('HINCRBY', lock_key, 'spc_cnt', 1)
362
- # SPCF Step 4: (lock meta-data)
363
- # - remember the last ext-timestamp and the last ext-initial ttl;
364
- # - [REDIS RESULT]: for normal cases should return the number of fields
365
- # were added/changed;
366
- transact.call(
367
- 'HSET',
368
- lock_key,
369
- 'l_spc_ext_ts', spc_processed_timestamp = Time.now.to_f,
370
- 'l_spc_ext_ini_ttl', ttl
379
+ transact_write_lock_extension(
380
+ transact, lock_key, lock_name, ttl, lock_index_enabled, spc_processed_timestamp
371
381
  )
372
382
  else
373
383
  held_rw_mode = :read
374
- # SPCF Step 1 (read lock): extend the read lock of the current acquirer
375
- # - [REDIS RESULT]: the new read lock expiration time
376
- # (or nil if the read lock has been removed from the registry);
377
- transact.call('ZADD', lock_readers_key, 'XX', 'INCR', ttl, acquirer_id)
378
- # SPCF Step 2 (read lock): the registry should live as long as the longest read lock
379
- # - NX + GT => sets the registry TTL to max(current TTL, extended read lock TTL);
380
- extended_read_lock_ttl = (own_read_lock_expiration.to_f + ttl.to_i - redis_time).ceil
381
- transact.call('PEXPIRE', lock_readers_key, extended_read_lock_ttl, 'NX')
382
- transact.call('PEXPIRE', lock_readers_key, extended_read_lock_ttl, 'GT')
383
- # SPCF Step 3 (read lock data): the same reentrant lock data as for the write lock
384
- transact.call('HINCRBY', read_lock_key, 'spc_ext_ttl', ttl)
385
- transact.call('HINCRBY', read_lock_key, 'spc_cnt', 1)
386
- transact.call(
387
- 'HSET',
388
- read_lock_key,
389
- 'l_spc_ext_ts', spc_processed_timestamp = Time.now.to_f,
390
- 'l_spc_ext_ini_ttl', ttl
391
- )
392
- # SPCF Step 4 (read lock data): the read lock data lives as long as the read lock
393
- transact.call('PEXPIRE', read_lock_key, extended_read_lock_ttl, 'NX')
394
- transact.call('PEXPIRE', read_lock_key, extended_read_lock_ttl, 'GT')
395
- end
396
- # SPCF Step 5: (lock index) the lock (write or read) lives longer by the extension:
397
- # - the lock index score is increased by the extension (the upper bound of the lock
398
- # lifetime stays the upper bound);
399
- # - XX => locks without the index entry (of older RQL versions) are not indexed with
400
- # a wrong score (see `reindex_locks`);
401
- # - [REDIS RESULT]: the new lock index score (nil for the lock without the index entry);
402
- # - only with the enabled lock index (the last command of the transaction);
403
- if lock_index_enabled
404
- transact.call(
405
- 'ZADD', RedisQueuedLocks::Resource::LOCK_INDEX_KEY, 'XX', 'INCR', ttl, lock_name
384
+ transact_read_lock_extension(
385
+ transact, lock_readers_key, read_lock_key, lock_name, acquirer_id, ttl,
386
+ lock_index_enabled, own_read_lock_expiration, redis_time, spc_processed_timestamp
406
387
  )
407
388
  end
408
389
  inter_result = :extendable_conflict_work_through
@@ -410,53 +391,29 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
410
391
  # @type var spc_processed_timestamp: Float
411
392
  LogVisitor.reentrant_lock__extend_and_work_through(
412
393
  logger, log_sampled, log_lock_try, lock_key,
413
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode,
394
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode,
414
395
  sp_conflict_status, ttl, spc_processed_timestamp
415
396
  )
416
397
  # SP-Conflict-Step X2: switch to dead lock logic or not
417
398
  elsif sp_conflict_status == :conflict_work_through
418
399
  inter_result = :conflict_work_through
419
400
 
401
+ spc_processed_timestamp = Time.now.to_f
402
+
420
403
  if sp_conflict_rw_mode == :write
421
404
  held_rw_mode = :write
422
-
423
- # SPCF Step X: (lock meta-data)
424
- # - increment the conflcit counter in order to remember
425
- # how many times dead lock happened;
426
- # - [REDIS RESULT]: in normal cases should return the value of <spc_cnt> key
427
- # - for non-existent key starts from 0
428
- transact.call('HINCRBY', lock_key, 'spc_cnt', 1)
429
- # SPCF Step 4: (lock meta-data)
430
- # - remember the last ext-timestamp and the last ext-initial ttl;
431
- # - [REDIS RESULT]: for normal cases should return the number of fields
432
- # were added/changed;
433
- transact.call(
434
- 'HSET',
435
- lock_key,
436
- 'l_spc_ts', spc_processed_timestamp = Time.now.to_f
437
- )
405
+ transact_write_lock_work_through(transact, lock_key, spc_processed_timestamp)
438
406
  else
439
407
  held_rw_mode = :read
440
-
441
- # SPCF Step X (read lock data): the same reentrant lock data as for the write lock
442
- transact.call('HINCRBY', read_lock_key, 'spc_cnt', 1)
443
- transact.call(
444
- 'HSET', read_lock_key, 'l_spc_ts', spc_processed_timestamp = Time.now.to_f
445
- )
446
- # SPCF Step X (read lock data): (NX) the read lock data should not outlive the read lock
447
- # (it is the case when the read lock data is dropped during the read lock life);
448
- transact.call(
449
- 'PEXPIRE',
450
- read_lock_key,
451
- (own_read_lock_expiration.to_f - redis_time).ceil,
452
- 'NX'
408
+ transact_read_lock_work_through(
409
+ transact, read_lock_key, own_read_lock_expiration, redis_time, spc_processed_timestamp
453
410
  )
454
411
  end
455
412
 
456
413
  # @type var spc_processed_timestamp: Float
457
414
  LogVisitor.reentrant_lock__work_through(
458
415
  logger, log_sampled, log_lock_try, lock_key,
459
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode,
416
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode,
460
417
  sp_conflict_status, spc_processed_timestamp
461
418
  )
462
419
  # SP-Conflict-Step X2: switch to dead lock logic or not
@@ -467,7 +424,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
467
424
  # @type var spc_processed_timestamp: Float
468
425
  LogVisitor.single_process_lock_conflict__dead_lock(
469
426
  logger, log_sampled, log_lock_try, lock_key,
470
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode,
427
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode,
471
428
  sp_conflict_status, spc_processed_timestamp
472
429
  )
473
430
  # SP-Conflict-Step X2 (RW): read-to-write lock upgrade is not supported => exit and fail
@@ -478,7 +435,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
478
435
  # @type var spc_processed_timestamp: Float
479
436
  LogVisitor.single_process_lock_conflict__lock_upgrade(
480
437
  logger, log_sampled, log_lock_try, lock_key,
481
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode,
438
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode,
482
439
  sp_conflict_status, spc_processed_timestamp
483
440
  )
484
441
  # Reached the SP-Non-Conflict Mode (NOTE):
@@ -493,11 +450,12 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
493
450
  # Fast-Step X1: lock is already obtained. fail fast leads to "no try".
494
451
  inter_result = :fail_fast_no_try
495
452
  else
496
- # Step 1: add an acquirer to the lock acquirement queue
497
453
  # Step 2.1: drop expired acquirers from the lock queues
454
+ # Step 1: add an acquirer to the lock acquirement queue
498
455
  # Step 3: get the actual acquirer waiting in the queue
499
456
  # - one round trip for all of them (see `queue_lock_request`);
500
457
  # - eager queueing: already done in the lock state round trip (Step 0.1);
458
+ # - Step 2.2 (priorities) runs after them (see below);
501
459
  # @type var queue_replies: Array[untyped]
502
460
  queue_replies =
503
461
  if eager_queueing
@@ -506,30 +464,51 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
506
464
  rconn.pipelined do |pipeline|
507
465
  queue_lock_request(
508
466
  pipeline, request_queue, opposite_request_queue,
509
- acquirer_position, acquirer_id, acquirer_dead_score,
467
+ acquirer_position, priority, acquirer_id, acquirer_dead_score,
510
468
  lock_name, queue_ttl, lock_index_enabled, lock_index_grace_period
511
469
  )
512
470
  end
513
471
  end
514
472
  # NOTE: the position of the first queueing reply (see "pipeline layout" in Step 0.1)
515
473
  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
474
+ # @type var waiting_request: [String, Float]?
475
+ waiting_request = queue_replies[queue_replies_at + QUEUE_HEAD_REPLY].first
518
476
  # @type var opposite_waiting_request: [String, Float]?
519
477
  opposite_waiting_request =
520
478
  queue_replies[queue_replies_at + OPPOSITE_QUEUE_HEAD_REPLY].first
521
479
 
522
480
  LogVisitor.acq_added_to_queue(
523
481
  logger, log_sampled, log_lock_try, lock_key,
524
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode
482
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode, priority
525
483
  )
526
484
  LogVisitor.remove_expired_acqs(
527
485
  logger, log_sampled, log_lock_try, lock_key,
528
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode
486
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode
487
+ )
488
+
489
+ # Step 2.2: (priorities) drop the dead lock requests of other priority bands that block
490
+ # the current one (the queue head or the earlier request of the opposite mode) and get
491
+ # the actual queue heads (no round trips while no dead request blocks the current one);
492
+ actual_queue_heads = drop_dead_requests_ahead(
493
+ rconn, rw_mode, request_queue, opposite_request_queue, acquirer_id,
494
+ acquirer_position, acquirer_dead_score, access_strategy, waiting_request,
495
+ opposite_waiting_request
529
496
  )
497
+ if actual_queue_heads
498
+ waiting_request, opposite_waiting_request, dropped_requests = actual_queue_heads
499
+ log_dropped_requests(
500
+ logger, log_sampled, log_lock_try, lock_key, queue_ttl, acquirer_id, host_id,
501
+ access_strategy, rw_mode, dropped_requests
502
+ )
503
+ end
504
+ waiting_acquirer = waiting_request ? waiting_request[0] : nil
505
+ # NOTE: the priority of the first request is decoded for logs only
506
+ waiting_priority = logged_request_priority(log_sampled, log_lock_try, waiting_request)
507
+
530
508
  LogVisitor.get_first_from_queue(
531
509
  logger, log_sampled, log_lock_try, lock_key,
532
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode, waiting_acquirer
510
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode, waiting_acquirer,
511
+ waiting_priority
533
512
  )
534
513
 
535
514
  # Step 3.1 (RW) + Steps PRE-4.x, ROLLBACK 1.1, 1.2, 2, 3: the obstacle to the lock
@@ -548,102 +527,43 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
548
527
  # Step ROLLBACK: the lock can not be obtained now. retry!
549
528
  log_lock_obstacle(
550
529
  rconn, obstacle, logger, log_sampled, log_lock_try, lock_key, queue_ttl,
551
- acquirer_id, host_id, access_strategy, read_write_mode,
552
- waiting_acquirer, opposite_request_ahead, current_lock_obtainer
530
+ acquirer_id, host_id, access_strategy, rw_mode,
531
+ waiting_acquirer, waiting_priority, opposite_waiting_request, current_lock_obtainer
553
532
  )
554
533
  inter_result = obstacle
555
534
  elsif read_mode
556
535
  # NOTE: required lock is free and ready to be acquired! acquire! (read lock)
557
536
 
558
- # Step 6.1: remove our acquirer from waiting queue
559
- transact.call('ZREM', request_queue, acquirer_id)
560
-
561
- # Step 6.2 (RW): drop expired read locks from the readers registry
562
- transact.call('ZREMRANGEBYSCORE', lock_readers_key, '-inf', "(#{redis_time}")
563
-
564
- # Step 6.3 (RW): register the read lock (expiration time in redis time)
565
- transact.call('ZADD', lock_readers_key, redis_time + ttl.to_i, acquirer_id)
566
-
567
- # Step 6.4 (RW): the registry should live as long as the longest read lock
568
- # - NX + GT => sets the registry TTL to max(current TTL, read lock TTL);
569
- # - in order to prevent "infinite registries" after process crashes;
570
- transact.call('PEXPIRE', lock_readers_key, ttl, 'NX')
571
- transact.call('PEXPIRE', lock_readers_key, ttl, 'GT')
572
-
573
- # Step 6.5 (RW): store the read lock data (the same structure as the write lock data
574
- # without `acq_id`/`hst_id`: both are derived from the read lock acquirer id that is
575
- # a part of the data key and the registry member, so the info adds them on formatting)
576
- # - drop the data of the expired read lock of the current acquirer (if any);
577
- # - the read lock data lives as long as the read lock;
578
- transact.call('DEL', read_lock_key)
579
- transact.call(
580
- 'HSET',
581
- read_lock_key,
582
- 'ts', timestamp = Time.now.to_f,
583
- 'ini_ttl', ttl,
584
- *(meta.to_a if meta != nil) # steep:ignore
537
+ # Steps 6.1-6.6 (RW): see `transact_read_lock_obtaining`
538
+ timestamp = Time.now.to_f
539
+ # @type var timestamp: Float
540
+ transact_read_lock_obtaining(
541
+ transact, request_queue, lock_readers_key, read_lock_key, lock_name, acquirer_id, ttl,
542
+ lock_index_enabled, lock_index_grace_period, meta, redis_time, timestamp
585
543
  )
586
- transact.call('PEXPIRE', read_lock_key, ttl)
587
-
588
- # Step 6.6 (lock index): the lock name lives in the lock index as long as the read lock
589
- # (the upper bound of the read lock lifetime: client time + ttl + grace period);
590
- # - in the same transaction: an index entry dropped between the queueing and this
591
- # transaction (`#unlock`, `#clear_locks`) is restored with the read lock;
592
- # - GT => the score is never decreased (other keys of the lock can live longer);
593
- # - only with the enabled lock index (the last command of the transaction);
594
- if lock_index_enabled
595
- transact.call(
596
- 'ZADD', RedisQueuedLocks::Resource::LOCK_INDEX_KEY, 'GT',
597
- RedisQueuedLocks::Resource.lock_index_time + ttl.to_i + lock_index_grace_period,
598
- lock_name
599
- )
600
- end
601
544
 
602
545
  inter_result = :lock_obtaining
603
546
 
604
547
  LogVisitor.obtain__free_to_acquire(
605
548
  logger, log_sampled, log_lock_try, lock_key,
606
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode
549
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode
607
550
  )
608
551
  else
609
552
  # NOTE: required lock is free and ready to be acquired! acquire! (write lock)
610
553
 
611
- # Step 6.1: remove our acquirer from waiting queue
612
- transact.call('ZREM', request_queue, acquirer_id)
613
-
614
- # Step 6.2: acquire a lock and store an info about the acquirer and host
615
- transact.call(
616
- 'HSET',
617
- lock_key,
618
- 'acq_id', acquirer_id,
619
- 'hst_id', host_id,
620
- 'ts', timestamp = Time.now.to_f,
621
- 'ini_ttl', ttl,
622
- *(meta.to_a if meta != nil) # steep:ignore
554
+ # Steps 6.1-6.4: see `transact_write_lock_obtaining`
555
+ timestamp = Time.now.to_f
556
+ # @type var timestamp: Float
557
+ transact_write_lock_obtaining(
558
+ transact, lock_key, request_queue, lock_name, acquirer_id, host_id, ttl,
559
+ lock_index_enabled, lock_index_grace_period, meta, timestamp
623
560
  )
624
561
 
625
- # Step 6.3: set the lock expiration time in order to prevent "infinite locks"
626
- transact.call('PEXPIRE', lock_key, ttl) # NOTE: in milliseconds
627
-
628
- # Step 6.4 (lock index): the lock name lives in the lock index as long as the lock
629
- # (the upper bound of the lock lifetime: client time + ttl + grace period);
630
- # - in the same transaction: an index entry dropped between the queueing and this
631
- # transaction (`#unlock`, `#clear_locks`) is restored with the lock;
632
- # - GT => the score is never decreased (other keys of the lock can live longer);
633
- # - only with the enabled lock index (the last command of the transaction);
634
- if lock_index_enabled
635
- transact.call(
636
- 'ZADD', RedisQueuedLocks::Resource::LOCK_INDEX_KEY, 'GT',
637
- RedisQueuedLocks::Resource.lock_index_time + ttl.to_i + lock_index_grace_period,
638
- lock_name
639
- )
640
- end
641
-
642
562
  inter_result = :lock_obtaining
643
563
 
644
564
  LogVisitor.obtain__free_to_acquire(
645
565
  logger, log_sampled, log_lock_try, lock_key,
646
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode
566
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode
647
567
  )
648
568
  end
649
569
  end
@@ -829,24 +749,395 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
829
749
 
830
750
  private
831
751
 
832
- # Queues the lock request and reads the lock queue state (Steps 1, 2.1, 3 and 3.2-3.4 of
752
+ # (SP-Conflict) The status of the conflict of the lock request with the lock that is already
753
+ # obtained by the current acquirer (Step CHECK of the attempt).
754
+ #
755
+ # @param conflict_strategy [Symbol]
756
+ # @param lock_upgrade [Boolean]
757
+ # The conflict is a read-to-write lock upgrade: it can not "work through" (the read lock is not
758
+ # exclusive, so the "work through" logic will work without an exclusive lock).
759
+ # @return [Symbol]
760
+ #
761
+ # @api private
762
+ # @since 1.23.0
763
+ def same_process_conflict_status(conflict_strategy, lock_upgrade)
764
+ case conflict_strategy
765
+ when :work_through
766
+ # <SP-Conflict Moment>: work through => exit
767
+ lock_upgrade ? :conflict_lock_upgrade : :conflict_work_through
768
+ when :extendable_work_through
769
+ # <SP-Conflict Moment>: extendable_work_through => extend the lock pttl and exit
770
+ lock_upgrade ? :conflict_lock_upgrade : :extendable_conflict_work_through
771
+ when :dead_locking
772
+ # <SP-Conflict Moment>: dead_locking => exit and fail
773
+ :conflict_dead_lock
774
+ else
775
+ # <SP-Conflict Moment>: wait_for_lock => obtain a lock in classic way
776
+ # - unknown status => work in classic way <wait_for_lock>
777
+ # - it is a case when the new status is added to the code base in the past
778
+ # but is forgotten to be added here;
779
+ :conflict_wait_for_lock
780
+ end
781
+ end
782
+
783
+ # (SP-Conflict) Adds the commands of the write lock extension (reentrant
784
+ # `:extendable_work_through`) to the given transaction.
785
+ #
786
+ # NOTE: (transaction layout) 4 commands (5 with the enabled lock index): EVAL, HINCRBY, HINCRBY,
787
+ # HSET (+ ZADD XX INCR); the replies are read by the attempt (`try_to_lock` Step 7);
788
+ #
789
+ # @param transact [RedisClient::Multi]
790
+ # @param lock_key [String]
791
+ # @param lock_name [String] The lock name (the member of the lock index).
792
+ # @param ttl [Integer] The extension (in milliseconds).
793
+ # @param lock_index_enabled [Boolean]
794
+ # @param spc_processed_timestamp [Float]
795
+ # @return [void]
796
+ #
797
+ # @api private
798
+ # @since 1.23.0
799
+ def transact_write_lock_extension(
800
+ transact,
801
+ lock_key,
802
+ lock_name,
803
+ ttl,
804
+ lock_index_enabled,
805
+ spc_processed_timestamp
806
+ )
807
+ # SPCF Step 1: extend the lock pttl
808
+ # - [REDIS RESULT]: in normal cases should return the last script command value
809
+ # - for the current script should return:
810
+ # <1> => timeout was set;
811
+ # <0> => timeount was not set;
812
+ transact.call('EVAL', EXTEND_LOCK_PTTL, 1, lock_key, ttl)
813
+ # SPCF Step <Meta>: store conflict-state additionals in lock metadata:
814
+ # SPCF Step 2: (lock meta-data)
815
+ # - add the added ttl to reflect the real lock TTL in info;
816
+ # - [REDIS RESULT]: in normal cases should return the value of <ttl> key
817
+ # - for non-existent key value starts from <0> (zero)
818
+ transact.call('HINCRBY', lock_key, 'spc_ext_ttl', ttl)
819
+ # SPCF Step 3: (lock meta-data)
820
+ # - increment the conflcit counter in order to remember
821
+ # how many times dead lock happened;
822
+ # - [REDIS RESULT]: in normal cases should return the value of <spc_cnt> key
823
+ # - for non-existent key starts from 0
824
+ transact.call('HINCRBY', lock_key, 'spc_cnt', 1)
825
+ # SPCF Step 4: (lock meta-data)
826
+ # - remember the last ext-timestamp and the last ext-initial ttl;
827
+ # - [REDIS RESULT]: for normal cases should return the number of fields
828
+ # were added/changed;
829
+ transact.call(
830
+ 'HSET',
831
+ lock_key,
832
+ 'l_spc_ext_ts', spc_processed_timestamp,
833
+ 'l_spc_ext_ini_ttl', ttl
834
+ )
835
+ # SPCF Step 5: (lock index), see `transact_lock_index_extension`
836
+ transact_lock_index_extension(transact, lock_name, ttl, lock_index_enabled)
837
+ end
838
+
839
+ # (SP-Conflict) Adds the commands of the read lock extension (reentrant
840
+ # `:extendable_work_through`) to the given transaction.
841
+ #
842
+ # NOTE: (transaction layout) `READ_LOCK_EXTENSION_COMMANDS` commands (+ one command with
843
+ # the enabled lock index): ZADD XX INCR is the first one (its `nil` reply = the read lock is
844
+ # expired and removed from the registry during the extension);
845
+ #
846
+ # @param transact [RedisClient::Multi]
847
+ # @param lock_readers_key [String]
848
+ # @param read_lock_key [String,NilClass]
849
+ # @param lock_name [String] The lock name (the member of the lock index).
850
+ # @param acquirer_id [String]
851
+ # @param ttl [Integer,NilClass] The extension (in milliseconds).
852
+ # @param lock_index_enabled [Boolean]
853
+ # @param own_read_lock_expiration [Float,NilClass]
854
+ # @param redis_time [Integer] Redis server time (in milliseconds).
855
+ # @param spc_processed_timestamp [Float]
856
+ # @return [void]
857
+ #
858
+ # @api private
859
+ # @since 1.23.0
860
+ def transact_read_lock_extension(
861
+ transact,
862
+ lock_readers_key,
863
+ read_lock_key,
864
+ lock_name,
865
+ acquirer_id,
866
+ ttl,
867
+ lock_index_enabled,
868
+ own_read_lock_expiration,
869
+ redis_time,
870
+ spc_processed_timestamp
871
+ )
872
+ # SPCF Step 1 (read lock): extend the read lock of the current acquirer
873
+ # - [REDIS RESULT]: the new read lock expiration time
874
+ # (or nil if the read lock has been removed from the registry);
875
+ transact.call('ZADD', lock_readers_key, 'XX', 'INCR', ttl, acquirer_id)
876
+ # SPCF Step 2 (read lock): the registry should live as long as the longest read lock
877
+ # - NX + GT => sets the registry TTL to max(current TTL, extended read lock TTL);
878
+ extended_read_lock_ttl = (own_read_lock_expiration.to_f + ttl.to_i - redis_time).ceil
879
+ transact.call('PEXPIRE', lock_readers_key, extended_read_lock_ttl, 'NX')
880
+ transact.call('PEXPIRE', lock_readers_key, extended_read_lock_ttl, 'GT')
881
+ # SPCF Step 3 (read lock data): the same reentrant lock data as for the write lock
882
+ transact.call('HINCRBY', read_lock_key, 'spc_ext_ttl', ttl)
883
+ transact.call('HINCRBY', read_lock_key, 'spc_cnt', 1)
884
+ transact.call(
885
+ 'HSET',
886
+ read_lock_key,
887
+ 'l_spc_ext_ts', spc_processed_timestamp,
888
+ 'l_spc_ext_ini_ttl', ttl
889
+ )
890
+ # SPCF Step 4 (read lock data): the read lock data lives as long as the read lock
891
+ transact.call('PEXPIRE', read_lock_key, extended_read_lock_ttl, 'NX')
892
+ transact.call('PEXPIRE', read_lock_key, extended_read_lock_ttl, 'GT')
893
+ # SPCF Step 5: (lock index), see `transact_lock_index_extension`
894
+ transact_lock_index_extension(transact, lock_name, ttl, lock_index_enabled)
895
+ end
896
+
897
+ # (SP-Conflict) (lock index) The lock (write or read) lives longer by the extension:
898
+ # - the lock index score is increased by the extension (the upper bound of the lock
899
+ # lifetime stays the upper bound);
900
+ # - XX => locks without the index entry (of older RQL versions) are not indexed with
901
+ # a wrong score (see `reindex_locks`);
902
+ # - [REDIS RESULT]: the new lock index score (nil for the lock without the index entry);
903
+ # - only with the enabled lock index (the last command of the transaction);
904
+ #
905
+ # @param transact [RedisClient::Multi]
906
+ # @param lock_name [String]
907
+ # @param ttl [Integer,NilClass] The extension (in milliseconds).
908
+ # @param lock_index_enabled [Boolean]
909
+ # @return [void]
910
+ #
911
+ # @api private
912
+ # @since 1.23.0
913
+ def transact_lock_index_extension(transact, lock_name, ttl, lock_index_enabled)
914
+ return unless lock_index_enabled
915
+
916
+ transact.call(
917
+ 'ZADD', RedisQueuedLocks::Resource::LOCK_INDEX_KEY, 'XX', 'INCR', ttl, lock_name
918
+ )
919
+ end
920
+
921
+ # (SP-Conflict) Adds the commands of the write lock "work through" (reentrant `:work_through`)
922
+ # to the given transaction: the reentrant lock data only.
923
+ #
924
+ # @param transact [RedisClient::Multi]
925
+ # @param lock_key [String]
926
+ # @param spc_processed_timestamp [Float]
927
+ # @return [void]
928
+ #
929
+ # @api private
930
+ # @since 1.23.0
931
+ def transact_write_lock_work_through(transact, lock_key, spc_processed_timestamp)
932
+ # SPCF Step X: (lock meta-data)
933
+ # - increment the conflcit counter in order to remember
934
+ # how many times dead lock happened;
935
+ # - [REDIS RESULT]: in normal cases should return the value of <spc_cnt> key
936
+ # - for non-existent key starts from 0
937
+ transact.call('HINCRBY', lock_key, 'spc_cnt', 1)
938
+ # SPCF Step 4: (lock meta-data)
939
+ # - remember the last ext-timestamp and the last ext-initial ttl;
940
+ # - [REDIS RESULT]: for normal cases should return the number of fields
941
+ # were added/changed;
942
+ transact.call(
943
+ 'HSET',
944
+ lock_key,
945
+ 'l_spc_ts', spc_processed_timestamp
946
+ )
947
+ end
948
+
949
+ # (SP-Conflict) Adds the commands of the read lock "work through" (reentrant `:work_through`)
950
+ # to the given transaction: the reentrant lock data only.
951
+ #
952
+ # @param transact [RedisClient::Multi]
953
+ # @param read_lock_key [String,NilClass]
954
+ # @param own_read_lock_expiration [Float,NilClass]
955
+ # @param redis_time [Integer] Redis server time (in milliseconds).
956
+ # @param spc_processed_timestamp [Float]
957
+ # @return [void]
958
+ #
959
+ # @api private
960
+ # @since 1.23.0
961
+ def transact_read_lock_work_through(
962
+ transact,
963
+ read_lock_key,
964
+ own_read_lock_expiration,
965
+ redis_time,
966
+ spc_processed_timestamp
967
+ )
968
+ # SPCF Step X (read lock data): the same reentrant lock data as for the write lock
969
+ transact.call('HINCRBY', read_lock_key, 'spc_cnt', 1)
970
+ transact.call('HSET', read_lock_key, 'l_spc_ts', spc_processed_timestamp)
971
+ # SPCF Step X (read lock data): (NX) the read lock data should not outlive the read lock
972
+ # (it is the case when the read lock data is dropped during the read lock life);
973
+ transact.call(
974
+ 'PEXPIRE',
975
+ read_lock_key,
976
+ (own_read_lock_expiration.to_f - redis_time).ceil,
977
+ 'NX'
978
+ )
979
+ end
980
+
981
+ # Adds the commands of the read lock obtaining to the given transaction.
982
+ #
983
+ # @param transact [RedisClient::Multi]
984
+ # @param request_queue [String]
985
+ # @param lock_readers_key [String]
986
+ # @param read_lock_key [String,NilClass]
987
+ # @param lock_name [String] The lock name (the member of the lock index).
988
+ # @param acquirer_id [String]
989
+ # @param ttl [Integer,NilClass]
990
+ # @param lock_index_enabled [Boolean]
991
+ # @param lock_index_grace_period [Integer] In milliseconds.
992
+ # @param meta [NilClass,Hash<String|Symbol,Any>]
993
+ # @param redis_time [Integer] Redis server time (in milliseconds).
994
+ # @param timestamp [Float]
995
+ # @return [void]
996
+ #
997
+ # @api private
998
+ # @since 1.23.0
999
+ def transact_read_lock_obtaining(
1000
+ transact,
1001
+ request_queue,
1002
+ lock_readers_key,
1003
+ read_lock_key,
1004
+ lock_name,
1005
+ acquirer_id,
1006
+ ttl,
1007
+ lock_index_enabled,
1008
+ lock_index_grace_period,
1009
+ meta,
1010
+ redis_time,
1011
+ timestamp
1012
+ )
1013
+ # Step 6.1: remove our acquirer from waiting queue
1014
+ transact.call('ZREM', request_queue, acquirer_id)
1015
+
1016
+ # Step 6.2 (RW): drop expired read locks from the readers registry
1017
+ transact.call('ZREMRANGEBYSCORE', lock_readers_key, '-inf', "(#{redis_time}")
1018
+
1019
+ # Step 6.3 (RW): register the read lock (expiration time in redis time)
1020
+ transact.call('ZADD', lock_readers_key, redis_time + ttl.to_i, acquirer_id)
1021
+
1022
+ # Step 6.4 (RW): the registry should live as long as the longest read lock
1023
+ # - NX + GT => sets the registry TTL to max(current TTL, read lock TTL);
1024
+ # - in order to prevent "infinite registries" after process crashes;
1025
+ transact.call('PEXPIRE', lock_readers_key, ttl, 'NX')
1026
+ transact.call('PEXPIRE', lock_readers_key, ttl, 'GT')
1027
+
1028
+ # Step 6.5 (RW): store the read lock data (the same structure as the write lock data
1029
+ # without `acq_id`/`hst_id`: both are derived from the read lock acquirer id that is
1030
+ # a part of the data key and the registry member, so the info adds them on formatting)
1031
+ # - drop the data of the expired read lock of the current acquirer (if any);
1032
+ # - the read lock data lives as long as the read lock;
1033
+ transact.call('DEL', read_lock_key)
1034
+ transact.call(
1035
+ 'HSET',
1036
+ read_lock_key,
1037
+ 'ts', timestamp,
1038
+ 'ini_ttl', ttl,
1039
+ *(meta.to_a if meta != nil) # steep:ignore
1040
+ )
1041
+ transact.call('PEXPIRE', read_lock_key, ttl)
1042
+
1043
+ # Step 6.6 (lock index): the lock name lives in the lock index as long as the read lock
1044
+ # (the upper bound of the read lock lifetime: client time + ttl + grace period);
1045
+ # - in the same transaction: an index entry dropped between the queueing and this
1046
+ # transaction (`#unlock`, `#clear_locks`) is restored with the read lock;
1047
+ # - GT => the score is never decreased (other keys of the lock can live longer);
1048
+ # - only with the enabled lock index (the last command of the transaction);
1049
+ if lock_index_enabled
1050
+ transact.call(
1051
+ 'ZADD', RedisQueuedLocks::Resource::LOCK_INDEX_KEY, 'GT',
1052
+ RedisQueuedLocks::Resource.lock_index_time + ttl.to_i + lock_index_grace_period,
1053
+ lock_name
1054
+ )
1055
+ end
1056
+ end
1057
+
1058
+ # Adds the commands of the write lock obtaining to the given transaction.
1059
+ #
1060
+ # @param transact [RedisClient::Multi]
1061
+ # @param lock_key [String]
1062
+ # @param request_queue [String]
1063
+ # @param lock_name [String] The lock name (the member of the lock index).
1064
+ # @param acquirer_id [String]
1065
+ # @param host_id [String]
1066
+ # @param ttl [Integer,NilClass]
1067
+ # @param lock_index_enabled [Boolean]
1068
+ # @param lock_index_grace_period [Integer] In milliseconds.
1069
+ # @param meta [NilClass,Hash<String|Symbol,Any>]
1070
+ # @param timestamp [Float]
1071
+ # @return [void]
1072
+ #
1073
+ # @api private
1074
+ # @since 1.23.0
1075
+ def transact_write_lock_obtaining(
1076
+ transact,
1077
+ lock_key,
1078
+ request_queue,
1079
+ lock_name,
1080
+ acquirer_id,
1081
+ host_id,
1082
+ ttl,
1083
+ lock_index_enabled,
1084
+ lock_index_grace_period,
1085
+ meta,
1086
+ timestamp
1087
+ )
1088
+ # Step 6.1: remove our acquirer from waiting queue
1089
+ transact.call('ZREM', request_queue, acquirer_id)
1090
+
1091
+ # Step 6.2: acquire a lock and store an info about the acquirer and host
1092
+ transact.call(
1093
+ 'HSET',
1094
+ lock_key,
1095
+ 'acq_id', acquirer_id,
1096
+ 'hst_id', host_id,
1097
+ 'ts', timestamp,
1098
+ 'ini_ttl', ttl,
1099
+ *(meta.to_a if meta != nil) # steep:ignore
1100
+ )
1101
+
1102
+ # Step 6.3: set the lock expiration time in order to prevent "infinite locks"
1103
+ transact.call('PEXPIRE', lock_key, ttl) # NOTE: in milliseconds
1104
+
1105
+ # Step 6.4 (lock index): the lock name lives in the lock index as long as the lock
1106
+ # (the upper bound of the lock lifetime: client time + ttl + grace period);
1107
+ # - in the same transaction: an index entry dropped between the queueing and this
1108
+ # transaction (`#unlock`, `#clear_locks`) is restored with the lock;
1109
+ # - GT => the score is never decreased (other keys of the lock can live longer);
1110
+ # - only with the enabled lock index (the last command of the transaction);
1111
+ if lock_index_enabled
1112
+ transact.call(
1113
+ 'ZADD', RedisQueuedLocks::Resource::LOCK_INDEX_KEY, 'GT',
1114
+ RedisQueuedLocks::Resource.lock_index_time + ttl.to_i + lock_index_grace_period,
1115
+ lock_name
1116
+ )
1117
+ end
1118
+ end
1119
+
1120
+ # Queues the lock request and reads the lock queue state (Steps 2.1, 1, 3 and 3.2-3.4 of
833
1121
  # the attempt): the commands are added to the given pipeline (one round trip with other
834
1122
  # pipelined commands).
835
- # Replies (positions relative to the first one): [0] ZADD NX, [1] ZREMRANGEBYSCORE (request
836
- # queue), [2] ZREMRANGEBYSCORE (opposite request queue), [QUEUE_HEAD_REPLY] ZRANGE (the first
837
- # request of the request queue), [OPPOSITE_QUEUE_HEAD_REPLY] ZRANGE WITHSCORES (the first
838
- # request of the opposite request queue); the enabled lock index only: [5] PEXPIRE NX, [6]
839
- # PEXPIRE GT (the request queue TTL), [7] ZADD GT (the lock index), [8] ZREMRANGEBYSCORE
840
- # (expired lock names of the lock index). Update these constants when the commands change
841
- # (new commands go to the end: the replies are not read after `OPPOSITE_QUEUE_HEAD_REPLY`, so
842
- # the conditional lock index commands do not shift the read replies).
1123
+ # Replies (positions relative to the first one): [0] ZREMRANGEBYSCORE (request queue, the
1124
+ # priority band of the request), [1] ZREMRANGEBYSCORE (opposite request queue, the priority band
1125
+ # of the request), [2] ZADD NX, [QUEUE_HEAD_REPLY] ZRANGE WITHSCORES (the first request of
1126
+ # the request queue), [OPPOSITE_QUEUE_HEAD_REPLY] ZRANGE WITHSCORES (the first request of
1127
+ # the opposite request queue); the enabled lock index only: [5] PEXPIRE NX, [6] PEXPIRE GT (the
1128
+ # request queue TTL), [7] ZADD GT (the lock index), [8] ZREMRANGEBYSCORE (expired lock names of
1129
+ # the lock index). Update these constants when the commands change (new commands go to the end:
1130
+ # the replies are not read after `OPPOSITE_QUEUE_HEAD_REPLY`, so the conditional lock index
1131
+ # commands do not shift the read replies).
843
1132
  #
844
1133
  # @param pipeline [RedisClient::Pipeline]
845
1134
  # @param request_queue [String] Queue of the requests of the requested lock mode.
846
1135
  # @param opposite_request_queue [String] Queue of the requests of the opposite lock mode.
847
- # @param acquirer_position [Numeric]
1136
+ # @param acquirer_position [Numeric] The enqueue time shifted by the priority band.
1137
+ # @param priority [Integer] The priority band of `acquirer_position`.
848
1138
  # @param acquirer_id [String]
849
- # @param acquirer_dead_score [Float] Requests queued before this score are expired.
1139
+ # @param acquirer_dead_score [Float]
1140
+ # Requests queued before this time are expired (the barrier of the default priority band).
850
1141
  # @param lock_name [String] The lock name (the member of the lock index).
851
1142
  # @param queue_ttl [Integer] Lifetime of the lock request (in seconds).
852
1143
  # @param lock_index_enabled [Boolean] Send the lock index commands (Steps 3.2-3.4).
@@ -855,11 +1146,13 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
855
1146
  #
856
1147
  # @api private
857
1148
  # @since 1.20.0
1149
+ # @version 1.22.0
858
1150
  def queue_lock_request(
859
1151
  pipeline,
860
1152
  request_queue,
861
1153
  opposite_request_queue,
862
1154
  acquirer_position,
1155
+ priority,
863
1156
  acquirer_id,
864
1157
  acquirer_dead_score,
865
1158
  lock_name,
@@ -867,6 +1160,22 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
867
1160
  lock_index_enabled,
868
1161
  lock_index_grace_period
869
1162
  )
1163
+ # Step 2.1: drop expired acquirers from the lock queues
1164
+ # - (RW) both queues are cleared: an expired request of the opposite mode should not
1165
+ # block the current request forever;
1166
+ # - (priorities) the requests of the priority band of the current request (the band has its
1167
+ # own time barrier, see `RedisQueuedLocks::Resource::LOCK_PRIORITY_BAND`); a dead request of
1168
+ # another band is dropped when it blocks the current request (`drop_dead_requests_ahead`),
1169
+ # requests of lower priorities are never ahead of the current request;
1170
+ # - before the queueing (Step 1): the old (dead) position of the requeued request (its
1171
+ # position is reset after `queue_ttl`, see `dead_score_reached?`) is dropped first, so
1172
+ # `ZADD NX` adds its new position in the same round trip (after `ZADD NX` the old position
1173
+ # would be kept by `NX` and dropped by the removal: the request would be missing in the
1174
+ # queue in this attempt);
1175
+ pipeline_dead_requests_removal(
1176
+ pipeline, request_queue, opposite_request_queue, acquirer_dead_score, priority
1177
+ )
1178
+
870
1179
  # Step 1: add an acquirer to the lock acquirement queue
871
1180
  # NOTE:
872
1181
  # 'NX' means "Only add new elements. Don't update already existing elements."
@@ -876,15 +1185,10 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
876
1185
  # not in the queue now>>;
877
1186
  pipeline.call('ZADD', request_queue, 'NX', acquirer_position, acquirer_id)
878
1187
 
879
- # Step 2.1: drop expired acquirers from the lock queues
880
- # - (RW) both queues are cleared: an expired request of the opposite mode should not
881
- # block the current request forever;
882
- pipeline.call('ZREMRANGEBYSCORE', request_queue, '-inf', acquirer_dead_score)
883
- pipeline.call('ZREMRANGEBYSCORE', opposite_request_queue, '-inf', acquirer_dead_score)
884
-
885
- # Step 3: get the actual acquirer waiting in the queue
1188
+ # Step 3: get the actual acquirer waiting in the queue (with its position: the band and
1189
+ # the liveness of the queue head are decoded from it, see `drop_dead_requests_ahead`)
886
1190
  # - (RW) and the first request of the opposite mode;
887
- pipeline.call('ZRANGE', request_queue, '0', '0')
1191
+ pipeline.call('ZRANGE', request_queue, '0', '0', 'WITHSCORES')
888
1192
  pipeline.call('ZRANGE', opposite_request_queue, '0', '0', 'WITHSCORES')
889
1193
 
890
1194
  # NOTE: the disabled lock index: no lock index commands, lock request queues have no TTL
@@ -921,6 +1225,34 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
921
1225
  )
922
1226
  end
923
1227
 
1228
+ # Adds the removal of the dead lock requests of the given priority band (both lock queues) to
1229
+ # the given pipeline (see `queue_lock_request` and `drop_dead_requests_ahead`).
1230
+ #
1231
+ # @param pipeline [RedisClient::Pipeline]
1232
+ # @param request_queue [String]
1233
+ # @param opposite_request_queue [String]
1234
+ # @param acquirer_dead_score [Float]
1235
+ # Requests queued before this time are expired (the barrier of the default priority band).
1236
+ # @param priority [Integer]
1237
+ # The priority band (see `RedisQueuedLocks::Resource::LOCK_PRIORITY_BAND`).
1238
+ # @return [void]
1239
+ #
1240
+ # @api private
1241
+ # @since 1.22.0
1242
+ def pipeline_dead_requests_removal(
1243
+ pipeline,
1244
+ request_queue,
1245
+ opposite_request_queue,
1246
+ acquirer_dead_score,
1247
+ priority
1248
+ )
1249
+ band_floor = RedisQueuedLocks::Resource.lock_priority_band_floor(priority)
1250
+ band_dead_score =
1251
+ RedisQueuedLocks::Resource.lock_priority_band_dead_score(acquirer_dead_score, priority)
1252
+ pipeline.call('ZREMRANGEBYSCORE', request_queue, band_floor, band_dead_score)
1253
+ pipeline.call('ZREMRANGEBYSCORE', opposite_request_queue, band_floor, band_dead_score)
1254
+ end
1255
+
924
1256
  # Adds the lock state commands to the given pipeline (the lock state of the attempt and of
925
1257
  # the pre-check: one round trip).
926
1258
  #
@@ -949,6 +1281,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
949
1281
  # @param request_queue [String]
950
1282
  # @param opposite_request_queue [String]
951
1283
  # @param acquirer_position [Numeric]
1284
+ # @param priority [Integer]
952
1285
  # @param acquirer_dead_score [Float]
953
1286
  # @param lock_name [String] The lock name (the member of the lock index).
954
1287
  # @param queue_ttl [Integer] In seconds.
@@ -958,6 +1291,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
958
1291
  #
959
1292
  # @api private
960
1293
  # @since 1.20.0
1294
+ # @version 1.22.0
961
1295
  def pipeline_lock_state(
962
1296
  pipeline,
963
1297
  lock_key,
@@ -968,6 +1302,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
968
1302
  request_queue,
969
1303
  opposite_request_queue,
970
1304
  acquirer_position,
1305
+ priority,
971
1306
  acquirer_dead_score,
972
1307
  lock_name,
973
1308
  queue_ttl,
@@ -981,7 +1316,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
981
1316
  if queueing
982
1317
  queue_lock_request(
983
1318
  pipeline, request_queue, opposite_request_queue,
984
- acquirer_position, acquirer_id, acquirer_dead_score,
1319
+ acquirer_position, priority, acquirer_id, acquirer_dead_score,
985
1320
  lock_name, queue_ttl, lock_index_enabled, lock_index_grace_period
986
1321
  )
987
1322
  end
@@ -991,6 +1326,8 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
991
1326
 
992
1327
  # (RW) The request of the opposite mode that should obtain the lock earlier
993
1328
  # - FIFO between modes (`:queued` access strategy only);
1329
+ # - (priorities) positions are shifted by the priority bands: a request of a higher priority
1330
+ # is ahead of any request of a lower priority, requests of the same priority are FIFO;
994
1331
  # - equal positions are ordered by acquirer ids;
995
1332
  #
996
1333
  # @param access_strategy [Symbol]
@@ -1080,14 +1417,19 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
1080
1417
  # @param acquirer_id [String]
1081
1418
  # @param host_id [String]
1082
1419
  # @param access_strategy [Symbol]
1083
- # @param read_write_mode [Symbol]
1420
+ # @param rw_mode [Symbol]
1084
1421
  # @param waiting_acquirer [String,NilClass]
1085
- # @param opposite_request_ahead [String,NilClass]
1422
+ # @param waiting_priority [Integer,NilClass]
1423
+ # The priority of the first request (decoded for logs only: `nil` without logs).
1424
+ # @param opposite_waiting_request [Array<String,Float>,NilClass]
1425
+ # The first request of the opposite queue (the request ahead of the `*_request_is_ahead`
1426
+ # obstacles, see `find_opposite_request_ahead`).
1086
1427
  # @param current_lock_obtainer [String,NilClass]
1087
1428
  # @return [void]
1088
1429
  #
1089
1430
  # @api private
1090
1431
  # @since 1.20.0
1432
+ # @version 1.22.0
1091
1433
  # rubocop:disable Metrics/MethodLength
1092
1434
  def log_lock_obstacle(
1093
1435
  rconn,
@@ -1100,59 +1442,354 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
1100
1442
  acquirer_id,
1101
1443
  host_id,
1102
1444
  access_strategy,
1103
- read_write_mode,
1445
+ rw_mode,
1104
1446
  waiting_acquirer,
1105
- opposite_request_ahead,
1447
+ waiting_priority,
1448
+ opposite_waiting_request,
1106
1449
  current_lock_obtainer
1107
1450
  )
1108
1451
  case obstacle
1109
1452
  when :dead_score_reached
1110
1453
  LogVisitor.exit__queue_ttl_reached(
1111
1454
  logger, log_sampled, log_lock_try, lock_key,
1112
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode
1455
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode
1113
1456
  )
1114
1457
  when :acquirer_is_not_first_in_queue
1115
1458
  LogVisitor.exit__no_first(
1116
1459
  logger, log_sampled, log_lock_try, lock_key,
1117
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode, waiting_acquirer,
1460
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode, waiting_acquirer,
1461
+ waiting_priority,
1118
1462
  # NOTE: lock data is extracted for logs only
1119
1463
  ((log_sampled && log_lock_try) ? rconn.call('HGETALL', lock_key).to_h : NO_LOCK_DATA)
1120
1464
  )
1121
- when :write_request_is_ahead
1122
- LogVisitor.exit__write_request_ahead(
1123
- logger, log_sampled, log_lock_try, lock_key,
1124
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode,
1125
- opposite_request_ahead #: String
1126
- )
1127
- when :read_request_is_ahead
1128
- LogVisitor.exit__read_request_ahead(
1129
- logger, log_sampled, log_lock_try, lock_key,
1130
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode,
1131
- opposite_request_ahead #: String
1132
- )
1465
+ when :write_request_is_ahead, :read_request_is_ahead
1466
+ ahead_request = opposite_waiting_request #: [String, Float]
1467
+ # NOTE: the priority of the request ahead is decoded for logs only
1468
+ ahead_priority = logged_request_priority(log_sampled, log_lock_try, ahead_request)
1469
+ if obstacle == :write_request_is_ahead
1470
+ LogVisitor.exit__write_request_ahead(
1471
+ logger, log_sampled, log_lock_try, lock_key,
1472
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode,
1473
+ ahead_request[0], ahead_priority
1474
+ )
1475
+ else
1476
+ LogVisitor.exit__read_request_ahead(
1477
+ logger, log_sampled, log_lock_try, lock_key,
1478
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode,
1479
+ ahead_request[0], ahead_priority
1480
+ )
1481
+ end
1133
1482
  when :lock_is_still_acquired
1134
1483
  LogVisitor.exit__lock_still_obtained(
1135
1484
  logger, log_sampled, log_lock_try, lock_key,
1136
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode,
1137
- waiting_acquirer, current_lock_obtainer, #: String
1485
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode,
1486
+ waiting_acquirer, waiting_priority,
1487
+ current_lock_obtainer, #: String
1138
1488
  # NOTE: lock data is extracted for logs only
1139
1489
  ((log_sampled && log_lock_try) ? rconn.call('HGETALL', lock_key).to_h : NO_LOCK_DATA)
1140
1490
  )
1141
1491
  when :read_lock_is_still_acquired
1142
1492
  LogVisitor.exit__read_lock_still_obtained(
1143
1493
  logger, log_sampled, log_lock_try, lock_key,
1144
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode, waiting_acquirer
1494
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode, waiting_acquirer,
1495
+ waiting_priority
1145
1496
  )
1146
1497
  end
1147
1498
  end
1148
1499
  # rubocop:enable Metrics/MethodLength
1149
1500
 
1501
+ # (priorities) The priority of the lock request for the try-lock logs (the first request of
1502
+ # the queue, the request ahead): decoded only when the try-lock logs are written (log-only values
1503
+ # are never computed without the logs: `log_sampled && log_lock_try`).
1504
+ #
1505
+ # @param log_sampled [Boolean]
1506
+ # @param log_lock_try [Boolean]
1507
+ # @param lock_request [Array<String,Float>,NilClass]
1508
+ # The acquirer and the position of the lock request.
1509
+ # @return [Integer,NilClass] `nil` without the try-lock logs or without the lock request.
1510
+ #
1511
+ # @api private
1512
+ # @since 1.22.0
1513
+ def logged_request_priority(log_sampled, log_lock_try, lock_request)
1514
+ return unless log_sampled && log_lock_try && lock_request
1515
+
1516
+ RedisQueuedLocks::Resource.lock_request_priority(lock_request[1])
1517
+ end
1518
+
1519
+ # (priorities) Drops the dead lock requests of other priority bands that block the current lock
1520
+ # request (`:queued` access strategy: the head of the write lock queue for write lock requests,
1521
+ # the earlier lock request of the opposite mode for both modes) and reads the actual queue heads
1522
+ # (Step 2.2 of the attempt and of its pre-check):
1523
+ # - the dead lock requests of the band of the current request are dropped by the queueing
1524
+ # (Step 2.1, before the queue heads are read) and requests of lower priorities are never
1525
+ # ahead of it: only a request of a higher priority can be a dead blocking request;
1526
+ # - the band and the enqueue time of the blocking request are decoded from its position (see
1527
+ # `RedisQueuedLocks::Resource::LOCK_PRIORITY_BAND`): no Redis commands to check the liveness,
1528
+ # any band is recognized (the priority range of the current RQL version is not used), the
1529
+ # time barrier is the barrier of the current request (`acquirer_dead_score`: its `queue_ttl`);
1530
+ # - the dead blocking requests (the head of the write lock queue and the earlier request of the
1531
+ # opposite mode) are dropped with all dead requests of their bands (both queues) in one round
1532
+ # trip with the reading of the actual queue heads; no round trips while no dead request
1533
+ # blocks the current one (the lock state does not matter: the attempt drops the dead blocking
1534
+ # requests even when the lock is obtained by another acquirer);
1535
+ # - up to `MAX_DEAD_REQUEST_REMOVALS` round trips (one or two bands each) per call;
1536
+ # - (infinite positions) RQL never writes them (manual writes of the queues, see
1537
+ # `RedisQueuedLocks::Resource.lock_request_priority`): a blocking request at `-inf` is always
1538
+ # dead (the highest band, no enqueue time) and is dropped with all requests at `-inf` of both
1539
+ # queues in the same round trip; a request at `+inf` is never ahead of the current request;
1540
+ # - the dropped blocking requests are returned, not logged: the attempt and its pre-check log
1541
+ # them at the same step (after the queueing log events, see `log_dropped_requests`);
1542
+ #
1543
+ # @param rconn [RedisClient]
1544
+ # @param rw_mode [Symbol]
1545
+ # @param request_queue [String]
1546
+ # @param opposite_request_queue [String]
1547
+ # @param acquirer_id [String]
1548
+ # @param acquirer_position [Numeric]
1549
+ # @param acquirer_dead_score [Float]
1550
+ # Requests queued before this time are expired (the barrier of the default priority band).
1551
+ # @param access_strategy [Symbol]
1552
+ # @param waiting_request [Array<String,Float>,NilClass] The head of the request queue.
1553
+ # @param opposite_waiting_request [Array<String,Float>,NilClass]
1554
+ # The head of the opposite request queue.
1555
+ # @return [Array<Array<String,Float>,NilClass,Array<Array<String,Integer>>>,NilClass]
1556
+ # - the actual queue heads and the dropped blocking requests (the acquirer and the priority of
1557
+ # each one, in the order of the removal): `[waiting_request, opposite_waiting_request,
1558
+ # dropped_requests]` when at least one dead lock request is dropped;
1559
+ # - `nil` otherwise;
1560
+ #
1561
+ # @api private
1562
+ # @since 1.22.0
1563
+ # rubocop:disable Metrics/MethodLength
1564
+ def drop_dead_requests_ahead(
1565
+ rconn,
1566
+ rw_mode,
1567
+ request_queue,
1568
+ opposite_request_queue,
1569
+ acquirer_id,
1570
+ acquirer_position,
1571
+ acquirer_dead_score,
1572
+ access_strategy,
1573
+ waiting_request,
1574
+ opposite_waiting_request
1575
+ )
1576
+ return unless access_strategy == :queued
1577
+
1578
+ read_mode = (rw_mode == :read)
1579
+ round_trips = 0
1580
+ # NOTE: allocated by the first removal only (no allocations while nothing dead blocks)
1581
+ # @type var dropped_requests: Array[[String, Integer]]?
1582
+ dropped_requests = nil
1583
+
1584
+ while round_trips < MAX_DEAD_REQUEST_REMOVALS
1585
+ dead_requests = find_dead_requests_ahead(
1586
+ read_mode, access_strategy, acquirer_id, acquirer_position, acquirer_dead_score,
1587
+ waiting_request, opposite_waiting_request
1588
+ )
1589
+ break unless dead_requests
1590
+
1591
+ dead_head, dead_head_priority, dead_ahead, dead_ahead_priority = dead_requests
1592
+
1593
+ # NOTE: (pipeline layout) see `pipeline_dead_requests_ahead_removal`, then [-2] ZRANGE
1594
+ # WITHSCORES (the head of the request queue) and [-1] ZRANGE WITHSCORES (the head of
1595
+ # the opposite request queue);
1596
+ # @type var queue_state: Array[untyped]
1597
+ queue_state = rconn.pipelined do |pipeline|
1598
+ pipeline_dead_requests_ahead_removal(
1599
+ pipeline, request_queue, opposite_request_queue, acquirer_dead_score,
1600
+ dead_head, dead_head_priority, dead_ahead, dead_ahead_priority
1601
+ )
1602
+ pipeline.call('ZRANGE', request_queue, '0', '0', 'WITHSCORES')
1603
+ pipeline.call('ZRANGE', opposite_request_queue, '0', '0', 'WITHSCORES')
1604
+ end
1605
+ # @type var removed_requests: Array[[String, Integer]]
1606
+ removed_requests = dropped_requests || []
1607
+ removed_requests << [dead_head[0], dead_head_priority] if dead_head && dead_head_priority
1608
+ removed_requests << [dead_ahead[0], dead_ahead_priority] if dead_ahead && dead_ahead_priority
1609
+ dropped_requests = removed_requests
1610
+ waiting_request = queue_state[-2].first
1611
+ opposite_waiting_request = queue_state[-1].first
1612
+ round_trips += 1
1613
+ end
1614
+
1615
+ [waiting_request, opposite_waiting_request, dropped_requests] if dropped_requests
1616
+ end
1617
+ # rubocop:enable Metrics/MethodLength
1618
+
1619
+ # (priorities) The dead lock requests that block the current lock request (see
1620
+ # `drop_dead_requests_ahead`, the same order as in `lock_obstacle`): the head of the write lock
1621
+ # queue (write lock requests only) and the earlier lock request of the opposite mode; a live head
1622
+ # of the write lock queue stops the removal (the attempt waits for it).
1623
+ #
1624
+ # @param read_mode [Boolean]
1625
+ # @param access_strategy [Symbol]
1626
+ # @param acquirer_id [String]
1627
+ # @param acquirer_position [Numeric]
1628
+ # @param acquirer_dead_score [Float]
1629
+ # Requests queued before this time are expired (the barrier of the default priority band).
1630
+ # @param waiting_request [Array<String,Float>,NilClass] The head of the request queue.
1631
+ # @param opposite_waiting_request [Array<String,Float>,NilClass]
1632
+ # The head of the opposite request queue.
1633
+ # @return [Array<Array<String,Float>,Integer,NilClass>,NilClass]
1634
+ # `[dead_head, dead_head_priority, dead_ahead, dead_ahead_priority]` (one of the pairs can be
1635
+ # `nil`) or `nil` when no dead lock request blocks the current one.
1636
+ #
1637
+ # @api private
1638
+ # @since 1.23.0
1639
+ def find_dead_requests_ahead(
1640
+ read_mode,
1641
+ access_strategy,
1642
+ acquirer_id,
1643
+ acquirer_position,
1644
+ acquirer_dead_score,
1645
+ waiting_request,
1646
+ opposite_waiting_request
1647
+ )
1648
+ # @type var dead_head: [String, Float]?
1649
+ dead_head = nil
1650
+ # @type var dead_head_priority: Integer?
1651
+ dead_head_priority = nil
1652
+ if !read_mode && waiting_request && waiting_request[0] != acquirer_id
1653
+ dead_head_priority = dead_request_priority(waiting_request, acquirer_dead_score)
1654
+ return unless dead_head_priority
1655
+
1656
+ dead_head = waiting_request
1657
+ end
1658
+ # @type var dead_ahead: [String, Float]?
1659
+ dead_ahead = nil
1660
+ # @type var dead_ahead_priority: Integer?
1661
+ dead_ahead_priority = nil
1662
+ if find_opposite_request_ahead(
1663
+ access_strategy, opposite_waiting_request, acquirer_position, acquirer_id
1664
+ )
1665
+ ahead_request = opposite_waiting_request #: [String, Float]
1666
+ dead_ahead_priority = dead_request_priority(ahead_request, acquirer_dead_score)
1667
+ dead_ahead = ahead_request if dead_ahead_priority
1668
+ end
1669
+ return unless dead_head_priority || dead_ahead_priority
1670
+
1671
+ [dead_head, dead_head_priority, dead_ahead, dead_ahead_priority]
1672
+ end
1673
+
1674
+ # (priorities) Adds the removal of the dead blocking lock requests (see
1675
+ # `find_dead_requests_ahead`) to the given pipeline: ZREMRANGEBYSCORE of both queues for each
1676
+ # dropped band (one or two bands) and for the requests at `-inf` (a dead blocking request at
1677
+ # `-inf` only).
1678
+ #
1679
+ # @param pipeline [RedisClient::Pipeline]
1680
+ # @param request_queue [String]
1681
+ # @param opposite_request_queue [String]
1682
+ # @param acquirer_dead_score [Float]
1683
+ # Requests queued before this time are expired (the barrier of the default priority band).
1684
+ # @param dead_head [Array<String,Float>,NilClass]
1685
+ # @param dead_head_priority [Integer,NilClass]
1686
+ # @param dead_ahead [Array<String,Float>,NilClass]
1687
+ # @param dead_ahead_priority [Integer,NilClass]
1688
+ # @return [void]
1689
+ #
1690
+ # @api private
1691
+ # @since 1.23.0
1692
+ def pipeline_dead_requests_ahead_removal(
1693
+ pipeline,
1694
+ request_queue,
1695
+ opposite_request_queue,
1696
+ acquirer_dead_score,
1697
+ dead_head,
1698
+ dead_head_priority,
1699
+ dead_ahead,
1700
+ dead_ahead_priority
1701
+ )
1702
+ if dead_head_priority
1703
+ pipeline_dead_requests_removal(
1704
+ pipeline, request_queue, opposite_request_queue, acquirer_dead_score, dead_head_priority
1705
+ )
1706
+ end
1707
+ if dead_ahead_priority && dead_ahead_priority != dead_head_priority
1708
+ pipeline_dead_requests_removal(
1709
+ pipeline, request_queue, opposite_request_queue, acquirer_dead_score, dead_ahead_priority
1710
+ )
1711
+ end
1712
+ # NOTE: (infinite positions) a dead blocking request at `-inf` (a manual write: out of all
1713
+ # bands, see `RedisQueuedLocks::Resource.lock_request_priority`) is not covered by
1714
+ # the band removal of its decoded priority (it drops the dead requests of that band
1715
+ # only): the requests at `-inf` are dropped from both queues by their position;
1716
+ if (dead_head && dead_head[1] == -Float::INFINITY) ||
1717
+ (dead_ahead && dead_ahead[1] == -Float::INFINITY)
1718
+ pipeline.call('ZREMRANGEBYSCORE', request_queue, '-inf', '-inf')
1719
+ pipeline.call('ZREMRANGEBYSCORE', opposite_request_queue, '-inf', '-inf')
1720
+ end
1721
+ end
1722
+
1723
+ # (priorities) Logs the dead lock requests dropped by `drop_dead_requests_ahead` (the attempt
1724
+ # and its pre-check log them at the same step: after the queueing log events).
1725
+ #
1726
+ # @param logger [::Logger,#debug]
1727
+ # @param log_sampled [Boolean]
1728
+ # @param log_lock_try [Boolean]
1729
+ # @param lock_key [String]
1730
+ # @param queue_ttl [Integer]
1731
+ # @param acquirer_id [String]
1732
+ # @param host_id [String]
1733
+ # @param access_strategy [Symbol]
1734
+ # @param rw_mode [Symbol]
1735
+ # @param dropped_requests [Array<Array<String,Integer>>]
1736
+ # The acquirer and the priority of each dropped lock request.
1737
+ # @return [void]
1738
+ #
1739
+ # @api private
1740
+ # @since 1.22.0
1741
+ def log_dropped_requests(
1742
+ logger,
1743
+ log_sampled,
1744
+ log_lock_try,
1745
+ lock_key,
1746
+ queue_ttl,
1747
+ acquirer_id,
1748
+ host_id,
1749
+ access_strategy,
1750
+ rw_mode,
1751
+ dropped_requests
1752
+ )
1753
+ return unless log_sampled && log_lock_try
1754
+
1755
+ dropped_requests.each do |dead_acquirer_id, dead_priority|
1756
+ LogVisitor.dead_request_removed(
1757
+ logger, log_sampled, log_lock_try, lock_key, queue_ttl, acquirer_id, host_id,
1758
+ access_strategy, rw_mode, dead_acquirer_id, dead_priority
1759
+ )
1760
+ end
1761
+ end
1762
+
1763
+ # (priorities) The priority band of the dead lock request (see `drop_dead_requests_ahead`): the
1764
+ # band and the enqueue time are decoded from the position of the request (infinite positions:
1765
+ # `-inf` is decoded to the highest band and is always dead, `+inf` to the lowest band and is never
1766
+ # dead, see `RedisQueuedLocks::Resource.lock_request_priority`).
1767
+ #
1768
+ # @param lock_request [Array<String,Float>] The acquirer and the position of the lock request.
1769
+ # @param acquirer_dead_score [Float]
1770
+ # Requests queued before this time are expired (the barrier of the default priority band).
1771
+ # @return [Integer,NilClass]
1772
+ # The priority of the dead lock request, `nil` for a live one (its position is after the time
1773
+ # barrier of its band: the same barrier as the dropping of the dead requests of the band).
1774
+ #
1775
+ # @api private
1776
+ # @since 1.22.0
1777
+ def dead_request_priority(lock_request, acquirer_dead_score)
1778
+ priority = RedisQueuedLocks::Resource.lock_request_priority(lock_request[1])
1779
+ band_dead_score =
1780
+ RedisQueuedLocks::Resource.lock_priority_band_dead_score(acquirer_dead_score, priority)
1781
+ priority if lock_request[1] <= band_dead_score
1782
+ end
1783
+
1150
1784
  # Pre-check of the attempt (Step PRE): the lock state without WATCH (one round trip).
1151
1785
  # - `fail_fast` attempts: the lock state only (the request is not queued: the failed "no try"
1152
1786
  # attempt does not dequeue the request); fails with `:fail_fast_no_try` on the obtained lock;
1153
1787
  # - retries: the lock state and the lock request queueing (the request is already queued by
1154
1788
  # the previous attempts); fails with the obstacle of the classic logic (`lock_obstacle`)
1155
- # and logs the same events as the failed attempt;
1789
+ # and logs the same events as the failed attempt in the same order;
1790
+ # - (priorities) retries drop the dead lock requests of other priority bands that block the
1791
+ # request before the decision (Step 2.2 of the attempt: one more round trip per dropped
1792
+ # band, see `drop_dead_requests_ahead`);
1156
1793
  # - same-process conflicts (the lock is obtained by the current acquirer) and locks that can be
1157
1794
  # obtained are not decided: the attempt continues with the WATCH/MULTI logic (`nil`);
1158
1795
  # - the pre-check never obtains the lock (mutual exclusion relies on WATCH/MULTI only);
@@ -1162,7 +1799,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
1162
1799
  # @param log_sampled [Boolean]
1163
1800
  # @param log_lock_try [Boolean]
1164
1801
  # @param lock_key [String]
1165
- # @param read_write_mode [Symbol]
1802
+ # @param rw_mode [Symbol]
1166
1803
  # @param lock_readers_key [String]
1167
1804
  # @param request_queue [String]
1168
1805
  # @param opposite_request_queue [String]
@@ -1170,6 +1807,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
1170
1807
  # @param acquirer_id [String]
1171
1808
  # @param host_id [String]
1172
1809
  # @param acquirer_position [Numeric]
1810
+ # @param priority [Integer]
1173
1811
  # @param acquirer_dead_score [Float]
1174
1812
  # @param queue_ttl [Integer]
1175
1813
  # @param lock_index_enabled [Boolean]
@@ -1180,6 +1818,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
1180
1818
  #
1181
1819
  # @api private
1182
1820
  # @since 1.20.0
1821
+ # @version 1.22.0
1183
1822
  # rubocop:disable Metrics/MethodLength
1184
1823
  def precheck_lock(
1185
1824
  rconn,
@@ -1187,7 +1826,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
1187
1826
  log_sampled,
1188
1827
  log_lock_try,
1189
1828
  lock_key,
1190
- read_write_mode,
1829
+ rw_mode,
1191
1830
  lock_readers_key,
1192
1831
  request_queue,
1193
1832
  opposite_request_queue,
@@ -1195,6 +1834,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
1195
1834
  acquirer_id,
1196
1835
  host_id,
1197
1836
  acquirer_position,
1837
+ priority,
1198
1838
  acquirer_dead_score,
1199
1839
  queue_ttl,
1200
1840
  lock_index_enabled,
@@ -1202,13 +1842,13 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
1202
1842
  fail_fast,
1203
1843
  access_strategy
1204
1844
  )
1205
- read_mode = (read_write_mode == :read)
1845
+ read_mode = (rw_mode == :read)
1206
1846
  # NOTE: the same pipeline layout and the same reply reading as in Step 0.1 of the attempt
1207
1847
  # @type var lock_state: Array[untyped]
1208
1848
  lock_state = rconn.pipelined do |pipeline|
1209
1849
  pipeline_lock_state(
1210
1850
  pipeline, lock_key, lock_readers_key, acquirer_id, read_mode, !fail_fast,
1211
- request_queue, opposite_request_queue, acquirer_position, acquirer_dead_score,
1851
+ request_queue, opposite_request_queue, acquirer_position, priority, acquirer_dead_score,
1212
1852
  lock_name, queue_ttl, lock_index_enabled, lock_index_grace_period
1213
1853
  )
1214
1854
  end
@@ -1228,12 +1868,26 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
1228
1868
  # NOTE: the fail-fast check of the attempt (Fast-Step X0) without same-process conflicts
1229
1869
  return ((current_lock_obtainer || read_locked) ? :fail_fast_no_try : nil) if fail_fast
1230
1870
 
1231
- # @type var waiting_acquirer: String?
1232
- waiting_acquirer =
1233
- lock_state[LOCK_STATE_QUEUE_REPLIES_AT + QUEUE_HEAD_REPLY].first
1871
+ # @type var waiting_request: [String, Float]?
1872
+ waiting_request = lock_state[LOCK_STATE_QUEUE_REPLIES_AT + QUEUE_HEAD_REPLY].first
1234
1873
  # @type var opposite_waiting_request: [String, Float]?
1235
1874
  opposite_waiting_request =
1236
1875
  lock_state[LOCK_STATE_QUEUE_REPLIES_AT + OPPOSITE_QUEUE_HEAD_REPLY].first
1876
+ # NOTE: (priorities) Step 2.2 of the attempt: the dead lock requests of other priority bands
1877
+ # that block the current one are dropped before the obstacle decision; they are logged at
1878
+ # the step of the attempt (after the queueing log events of the failed pre-check, they
1879
+ # follow the decision) or before the attempt that continues (it logs its own queueing);
1880
+ actual_queue_heads = drop_dead_requests_ahead(
1881
+ rconn, rw_mode, request_queue, opposite_request_queue, acquirer_id,
1882
+ acquirer_position, acquirer_dead_score, access_strategy, waiting_request,
1883
+ opposite_waiting_request
1884
+ )
1885
+ # @type var dropped_requests: Array[[String, Integer]]?
1886
+ dropped_requests = nil
1887
+ if actual_queue_heads
1888
+ waiting_request, opposite_waiting_request, dropped_requests = actual_queue_heads
1889
+ end
1890
+ waiting_acquirer = waiting_request ? waiting_request[0] : nil
1237
1891
  # @type var opposite_request_ahead: String?
1238
1892
  opposite_request_ahead = find_opposite_request_ahead(
1239
1893
  access_strategy, opposite_waiting_request, acquirer_position, acquirer_id
@@ -1242,25 +1896,44 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
1242
1896
  read_mode, access_strategy, acquirer_id, waiting_acquirer, opposite_request_ahead,
1243
1897
  current_lock_obtainer, read_locked
1244
1898
  )
1245
- return unless obstacle
1899
+ unless obstacle
1900
+ if dropped_requests
1901
+ log_dropped_requests(
1902
+ logger, log_sampled, log_lock_try, lock_key, queue_ttl, acquirer_id, host_id,
1903
+ access_strategy, rw_mode, dropped_requests
1904
+ )
1905
+ end
1906
+ return
1907
+ end
1908
+
1909
+ # NOTE: the priority of the first request is decoded for logs only
1910
+ waiting_priority = logged_request_priority(log_sampled, log_lock_try, waiting_request)
1246
1911
 
1247
- # NOTE: the same log events as the failed attempt (queueing steps + the exit event)
1912
+ # NOTE: the same log events as the failed attempt in the same order (queueing steps, the dropped
1913
+ # dead requests, the first request, the exit event)
1248
1914
  LogVisitor.acq_added_to_queue(
1249
1915
  logger, log_sampled, log_lock_try, lock_key,
1250
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode
1916
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode, priority
1251
1917
  )
1252
1918
  LogVisitor.remove_expired_acqs(
1253
1919
  logger, log_sampled, log_lock_try, lock_key,
1254
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode
1920
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode
1255
1921
  )
1922
+ if dropped_requests
1923
+ log_dropped_requests(
1924
+ logger, log_sampled, log_lock_try, lock_key, queue_ttl, acquirer_id, host_id,
1925
+ access_strategy, rw_mode, dropped_requests
1926
+ )
1927
+ end
1256
1928
  LogVisitor.get_first_from_queue(
1257
1929
  logger, log_sampled, log_lock_try, lock_key,
1258
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode, waiting_acquirer
1930
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode, waiting_acquirer,
1931
+ waiting_priority
1259
1932
  )
1260
1933
  log_lock_obstacle(
1261
1934
  rconn, obstacle, logger, log_sampled, log_lock_try, lock_key, queue_ttl,
1262
- acquirer_id, host_id, access_strategy, read_write_mode,
1263
- waiting_acquirer, opposite_request_ahead, current_lock_obtainer
1935
+ acquirer_id, host_id, access_strategy, rw_mode,
1936
+ waiting_acquirer, waiting_priority, opposite_waiting_request, current_lock_obtainer
1264
1937
  )
1265
1938
  obstacle
1266
1939
  end