redis_queued_locks 1.22.0 → 1.23.1

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/project-overview.md +90 -38
  3. data/.claude/rules/acquirer.md +12 -7
  4. data/.claude/rules/arguments.md +2 -2
  5. data/.claude/rules/logic.md +5 -4
  6. data/.claude/rules/tests.md +4 -4
  7. data/.claude/rules/type-checking.md +2 -3
  8. data/.claude/rules/visitors.md +8 -6
  9. data/.claude/skills/rql-code-review/SKILL.md +25 -20
  10. data/.ruby-version +1 -1
  11. data/CHANGELOG.md +30 -1
  12. data/CLAUDE.md +5 -4
  13. data/README.md +259 -103
  14. data/lib/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue.rb +4 -4
  15. data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rb +582 -276
  16. data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire.rb +29 -12
  17. data/lib/redis_queued_locks/acquirer/acquire_lock.rb +36 -27
  18. data/lib/redis_queued_locks/acquirer/extend_lock_ttl.rb +13 -13
  19. data/lib/redis_queued_locks/acquirer/lock_series/instr_visitor.rb +243 -0
  20. data/lib/redis_queued_locks/acquirer/lock_series/log_visitor.rb +472 -0
  21. data/lib/redis_queued_locks/acquirer/lock_series/release_lock_series.rb +181 -0
  22. data/lib/redis_queued_locks/acquirer/lock_series/try_to_lock_series.rb +1051 -0
  23. data/lib/redis_queued_locks/acquirer/lock_series.rb +957 -0
  24. data/lib/redis_queued_locks/acquirer.rb +1 -1
  25. data/lib/redis_queued_locks/client.rb +55 -25
  26. data/lib/redis_queued_locks/errors.rb +8 -0
  27. data/lib/redis_queued_locks/version.rb +2 -2
  28. data/redis_queued_locks.gemspec +21 -14
  29. data/sig/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue.rbs +1 -1
  30. data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rbs +106 -5
  31. data/sig/redis_queued_locks/acquirer/acquire_lock.rbs +1 -1
  32. data/sig/redis_queued_locks/acquirer/extend_lock_ttl.rbs +2 -2
  33. data/sig/redis_queued_locks/acquirer/lock_series/instr_visitor.rbs +102 -0
  34. data/sig/redis_queued_locks/acquirer/lock_series/log_visitor.rbs +154 -0
  35. data/sig/redis_queued_locks/acquirer/lock_series/release_lock_series.rbs +37 -0
  36. data/sig/redis_queued_locks/acquirer/lock_series/try_to_lock_series.rbs +198 -0
  37. data/sig/redis_queued_locks/acquirer/lock_series.rbs +106 -0
  38. data/sig/redis_queued_locks/client.rbs +70 -3
  39. data/sig/redis_queued_locks/errors.rbs +6 -0
  40. metadata +32 -11
  41. data/lib/redis_queued_locks/acquirer/lock_series_poc/instr_visitor.rb +0 -61
  42. data/lib/redis_queued_locks/acquirer/lock_series_poc/log_visitor.rb +0 -104
  43. data/lib/redis_queued_locks/acquirer/lock_series_poc.rb +0 -513
@@ -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
@@ -52,7 +52,7 @@
52
52
  #
53
53
  # @api private
54
54
  # @since 1.0.0
55
- # @version 1.22.0
55
+ # @version 1.23.0
56
56
  # rubocop:disable Metrics/ModuleLength
57
57
  module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
58
58
  require_relative 'try_to_lock/log_visitor'
@@ -133,6 +133,33 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
133
133
  # @since 1.20.0
134
134
  OPPOSITE_QUEUE_HEAD_REPLY = 4
135
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
+
136
163
  # The maximum number of the round trips of one call of `drop_dead_requests_ahead` (the attempt or
137
164
  # its pre-check; each round trip drops the dead blocking requests, the head of the write lock
138
165
  # queue and the earlier request of the opposite mode, with the dead requests of their priority
@@ -148,7 +175,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
148
175
  # @param logger [::Logger,#debug]
149
176
  # @param log_lock_try [Boolean]
150
177
  # @param lock_key [String]
151
- # @param read_write_mode [Symbol] `:read` or `:write`
178
+ # @param rw_mode [Symbol] `:read` or `:write`
152
179
  # @param lock_key_queue [String] Queue of write lock requests.
153
180
  # @param read_lock_key_queue [String] Queue of read lock requests.
154
181
  # @param lock_readers_key [String] Registry of read lock holders.
@@ -188,7 +215,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
188
215
  logger,
189
216
  log_lock_try,
190
217
  lock_key,
191
- read_write_mode,
218
+ rw_mode,
192
219
  lock_key_queue,
193
220
  read_lock_key_queue,
194
221
  lock_readers_key,
@@ -220,9 +247,9 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
220
247
  # Step RW: the mode of the lock that is really held by the acquirer after the attempt
221
248
  # (the requested mode or the mode of the already obtained lock for same-process conflicts);
222
249
  # @type var held_rw_mode: Symbol
223
- held_rw_mode = read_write_mode
250
+ held_rw_mode = rw_mode
224
251
  # Step RW: read lock request flag
225
- read_mode = (read_write_mode == :read)
252
+ read_mode = (rw_mode == :read)
226
253
  # Step RW: request queue of the current mode and the queue of the opposite mode
227
254
  request_queue = read_mode ? read_lock_key_queue : lock_key_queue
228
255
  opposite_request_queue = read_mode ? lock_key_queue : read_lock_key_queue
@@ -243,14 +270,14 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
243
270
 
244
271
  LogVisitor.start(
245
272
  logger, log_sampled, log_lock_try, lock_key,
246
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode
273
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode
247
274
  )
248
275
 
249
276
  # Step X: start to work with lock acquiring
250
277
  result = redis.with do |rconn|
251
278
  LogVisitor.rconn_fetched(
252
279
  logger, log_sampled, log_lock_try, lock_key,
253
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode
280
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode
254
281
  )
255
282
 
256
283
  # Step PRE: (pre-check) the lock state without WATCH: the attempt that can not obtain
@@ -260,7 +287,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
260
287
  # - see `precheck_lock` for details;
261
288
  if precheck
262
289
  inter_result = precheck_lock(
263
- 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,
264
291
  request_queue, opposite_request_queue, lock_name, acquirer_id, host_id,
265
292
  acquirer_position, priority, acquirer_dead_score, queue_ttl, lock_index_enabled,
266
293
  lock_index_grace_period, fail_fast, access_strategy
@@ -324,7 +351,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
324
351
  if sp_conflict_rw_mode != nil
325
352
  LogVisitor.same_process_conflict_detected(
326
353
  logger, log_sampled, log_lock_try, lock_key,
327
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode
354
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode
328
355
  )
329
356
 
330
357
  # NOTE:
@@ -334,30 +361,10 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
334
361
 
335
362
  # SP-Conflict Step X2: self-process dead lock moment started.
336
363
  # SP-Conflict CHECK (Step CHECK): check chosen strategy and flag the current status
337
- case conflict_strategy
338
- when :work_through
339
- # <SP-Conflict Moment>: work through => exit
340
- sp_conflict_status = lock_upgrade ? :conflict_lock_upgrade : :conflict_work_through
341
- when :extendable_work_through
342
- # <SP-Conflict Moment>: extendable_work_through => extend the lock pttl and exit
343
- sp_conflict_status =
344
- lock_upgrade ? :conflict_lock_upgrade : :extendable_conflict_work_through
345
- when :wait_for_lock
346
- # <SP-Conflict Moment>: wait_for_lock => obtain a lock in classic way
347
- sp_conflict_status = :conflict_wait_for_lock
348
- when :dead_locking
349
- # <SP-Conflict Moment>: dead_locking => exit and fail
350
- sp_conflict_status = :conflict_dead_lock
351
- else
352
- # <SP-Conflict Moment>:
353
- # - unknown status => work in classic way <wait_for_lock>
354
- # - it is a case when the new status is added to the code base in the past
355
- # but is forgotten to be added here;
356
- sp_conflict_status = :conflict_wait_for_lock
357
- end
364
+ sp_conflict_status = same_process_conflict_status(conflict_strategy, lock_upgrade)
358
365
  LogVisitor.same_process_conflict_analyzed(
359
366
  logger, log_sampled, log_lock_try, lock_key,
360
- 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
361
368
  )
362
369
  end
363
370
 
@@ -366,71 +373,17 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
366
373
  # SP-Conflict-Step FINAL (SPCF): extend the lock and work through
367
374
  # - extend the lock ttl;
368
375
  # - store extensions in lock metadata;
376
+ spc_processed_timestamp = Time.now.to_f
369
377
  if sp_conflict_rw_mode == :write
370
378
  held_rw_mode = :write
371
-
372
- # SPCF Step 1: extend the lock pttl
373
- # - [REDIS RESULT]: in normal cases should return the last script command value
374
- # - for the current script should return:
375
- # <1> => timeout was set;
376
- # <0> => timeount was not set;
377
- transact.call('EVAL', EXTEND_LOCK_PTTL, 1, lock_key, ttl)
378
- # SPCF Step <Meta>: store conflict-state additionals in lock metadata:
379
- # SPCF Step 2: (lock meta-data)
380
- # - add the added ttl to reflect the real lock TTL in info;
381
- # - [REDIS RESULT]: in normal cases should return the value of <ttl> key
382
- # - for non-existent key value starts from <0> (zero)
383
- transact.call('HINCRBY', lock_key, 'spc_ext_ttl', ttl)
384
- # SPCF Step 3: (lock meta-data)
385
- # - increment the conflcit counter in order to remember
386
- # how many times dead lock happened;
387
- # - [REDIS RESULT]: in normal cases should return the value of <spc_cnt> key
388
- # - for non-existent key starts from 0
389
- transact.call('HINCRBY', lock_key, 'spc_cnt', 1)
390
- # SPCF Step 4: (lock meta-data)
391
- # - remember the last ext-timestamp and the last ext-initial ttl;
392
- # - [REDIS RESULT]: for normal cases should return the number of fields
393
- # were added/changed;
394
- transact.call(
395
- 'HSET',
396
- lock_key,
397
- 'l_spc_ext_ts', spc_processed_timestamp = Time.now.to_f,
398
- 'l_spc_ext_ini_ttl', ttl
379
+ transact_write_lock_extension(
380
+ transact, lock_key, lock_name, ttl, lock_index_enabled, spc_processed_timestamp
399
381
  )
400
382
  else
401
383
  held_rw_mode = :read
402
- # SPCF Step 1 (read lock): extend the read lock of the current acquirer
403
- # - [REDIS RESULT]: the new read lock expiration time
404
- # (or nil if the read lock has been removed from the registry);
405
- transact.call('ZADD', lock_readers_key, 'XX', 'INCR', ttl, acquirer_id)
406
- # SPCF Step 2 (read lock): the registry should live as long as the longest read lock
407
- # - NX + GT => sets the registry TTL to max(current TTL, extended read lock TTL);
408
- extended_read_lock_ttl = (own_read_lock_expiration.to_f + ttl.to_i - redis_time).ceil
409
- transact.call('PEXPIRE', lock_readers_key, extended_read_lock_ttl, 'NX')
410
- transact.call('PEXPIRE', lock_readers_key, extended_read_lock_ttl, 'GT')
411
- # SPCF Step 3 (read lock data): the same reentrant lock data as for the write lock
412
- transact.call('HINCRBY', read_lock_key, 'spc_ext_ttl', ttl)
413
- transact.call('HINCRBY', read_lock_key, 'spc_cnt', 1)
414
- transact.call(
415
- 'HSET',
416
- read_lock_key,
417
- 'l_spc_ext_ts', spc_processed_timestamp = Time.now.to_f,
418
- 'l_spc_ext_ini_ttl', ttl
419
- )
420
- # SPCF Step 4 (read lock data): the read lock data lives as long as the read lock
421
- transact.call('PEXPIRE', read_lock_key, extended_read_lock_ttl, 'NX')
422
- transact.call('PEXPIRE', read_lock_key, extended_read_lock_ttl, 'GT')
423
- end
424
- # SPCF Step 5: (lock index) the lock (write or read) lives longer by the extension:
425
- # - the lock index score is increased by the extension (the upper bound of the lock
426
- # lifetime stays the upper bound);
427
- # - XX => locks without the index entry (of older RQL versions) are not indexed with
428
- # a wrong score (see `reindex_locks`);
429
- # - [REDIS RESULT]: the new lock index score (nil for the lock without the index entry);
430
- # - only with the enabled lock index (the last command of the transaction);
431
- if lock_index_enabled
432
- transact.call(
433
- '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
434
387
  )
435
388
  end
436
389
  inter_result = :extendable_conflict_work_through
@@ -438,53 +391,29 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
438
391
  # @type var spc_processed_timestamp: Float
439
392
  LogVisitor.reentrant_lock__extend_and_work_through(
440
393
  logger, log_sampled, log_lock_try, lock_key,
441
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode,
394
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode,
442
395
  sp_conflict_status, ttl, spc_processed_timestamp
443
396
  )
444
397
  # SP-Conflict-Step X2: switch to dead lock logic or not
445
398
  elsif sp_conflict_status == :conflict_work_through
446
399
  inter_result = :conflict_work_through
447
400
 
401
+ spc_processed_timestamp = Time.now.to_f
402
+
448
403
  if sp_conflict_rw_mode == :write
449
404
  held_rw_mode = :write
450
-
451
- # SPCF Step X: (lock meta-data)
452
- # - increment the conflcit counter in order to remember
453
- # how many times dead lock happened;
454
- # - [REDIS RESULT]: in normal cases should return the value of <spc_cnt> key
455
- # - for non-existent key starts from 0
456
- transact.call('HINCRBY', lock_key, 'spc_cnt', 1)
457
- # SPCF Step 4: (lock meta-data)
458
- # - remember the last ext-timestamp and the last ext-initial ttl;
459
- # - [REDIS RESULT]: for normal cases should return the number of fields
460
- # were added/changed;
461
- transact.call(
462
- 'HSET',
463
- lock_key,
464
- 'l_spc_ts', spc_processed_timestamp = Time.now.to_f
465
- )
405
+ transact_write_lock_work_through(transact, lock_key, spc_processed_timestamp)
466
406
  else
467
407
  held_rw_mode = :read
468
-
469
- # SPCF Step X (read lock data): the same reentrant lock data as for the write lock
470
- transact.call('HINCRBY', read_lock_key, 'spc_cnt', 1)
471
- transact.call(
472
- 'HSET', read_lock_key, 'l_spc_ts', spc_processed_timestamp = Time.now.to_f
473
- )
474
- # SPCF Step X (read lock data): (NX) the read lock data should not outlive the read lock
475
- # (it is the case when the read lock data is dropped during the read lock life);
476
- transact.call(
477
- 'PEXPIRE',
478
- read_lock_key,
479
- (own_read_lock_expiration.to_f - redis_time).ceil,
480
- 'NX'
408
+ transact_read_lock_work_through(
409
+ transact, read_lock_key, own_read_lock_expiration, redis_time, spc_processed_timestamp
481
410
  )
482
411
  end
483
412
 
484
413
  # @type var spc_processed_timestamp: Float
485
414
  LogVisitor.reentrant_lock__work_through(
486
415
  logger, log_sampled, log_lock_try, lock_key,
487
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode,
416
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode,
488
417
  sp_conflict_status, spc_processed_timestamp
489
418
  )
490
419
  # SP-Conflict-Step X2: switch to dead lock logic or not
@@ -495,7 +424,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
495
424
  # @type var spc_processed_timestamp: Float
496
425
  LogVisitor.single_process_lock_conflict__dead_lock(
497
426
  logger, log_sampled, log_lock_try, lock_key,
498
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode,
427
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode,
499
428
  sp_conflict_status, spc_processed_timestamp
500
429
  )
501
430
  # SP-Conflict-Step X2 (RW): read-to-write lock upgrade is not supported => exit and fail
@@ -506,7 +435,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
506
435
  # @type var spc_processed_timestamp: Float
507
436
  LogVisitor.single_process_lock_conflict__lock_upgrade(
508
437
  logger, log_sampled, log_lock_try, lock_key,
509
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode,
438
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode,
510
439
  sp_conflict_status, spc_processed_timestamp
511
440
  )
512
441
  # Reached the SP-Non-Conflict Mode (NOTE):
@@ -550,18 +479,18 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
550
479
 
551
480
  LogVisitor.acq_added_to_queue(
552
481
  logger, log_sampled, log_lock_try, lock_key,
553
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode, priority
482
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode, priority
554
483
  )
555
484
  LogVisitor.remove_expired_acqs(
556
485
  logger, log_sampled, log_lock_try, lock_key,
557
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode
486
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode
558
487
  )
559
488
 
560
489
  # Step 2.2: (priorities) drop the dead lock requests of other priority bands that block
561
490
  # the current one (the queue head or the earlier request of the opposite mode) and get
562
491
  # the actual queue heads (no round trips while no dead request blocks the current one);
563
492
  actual_queue_heads = drop_dead_requests_ahead(
564
- rconn, read_write_mode, request_queue, opposite_request_queue, acquirer_id,
493
+ rconn, rw_mode, request_queue, opposite_request_queue, acquirer_id,
565
494
  acquirer_position, acquirer_dead_score, access_strategy, waiting_request,
566
495
  opposite_waiting_request
567
496
  )
@@ -569,7 +498,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
569
498
  waiting_request, opposite_waiting_request, dropped_requests = actual_queue_heads
570
499
  log_dropped_requests(
571
500
  logger, log_sampled, log_lock_try, lock_key, queue_ttl, acquirer_id, host_id,
572
- access_strategy, read_write_mode, dropped_requests
501
+ access_strategy, rw_mode, dropped_requests
573
502
  )
574
503
  end
575
504
  waiting_acquirer = waiting_request ? waiting_request[0] : nil
@@ -578,7 +507,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
578
507
 
579
508
  LogVisitor.get_first_from_queue(
580
509
  logger, log_sampled, log_lock_try, lock_key,
581
- 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,
582
511
  waiting_priority
583
512
  )
584
513
 
@@ -598,102 +527,43 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
598
527
  # Step ROLLBACK: the lock can not be obtained now. retry!
599
528
  log_lock_obstacle(
600
529
  rconn, obstacle, logger, log_sampled, log_lock_try, lock_key, queue_ttl,
601
- acquirer_id, host_id, access_strategy, read_write_mode,
530
+ acquirer_id, host_id, access_strategy, rw_mode,
602
531
  waiting_acquirer, waiting_priority, opposite_waiting_request, current_lock_obtainer
603
532
  )
604
533
  inter_result = obstacle
605
534
  elsif read_mode
606
535
  # NOTE: required lock is free and ready to be acquired! acquire! (read lock)
607
536
 
608
- # Step 6.1: remove our acquirer from waiting queue
609
- transact.call('ZREM', request_queue, acquirer_id)
610
-
611
- # Step 6.2 (RW): drop expired read locks from the readers registry
612
- transact.call('ZREMRANGEBYSCORE', lock_readers_key, '-inf', "(#{redis_time}")
613
-
614
- # Step 6.3 (RW): register the read lock (expiration time in redis time)
615
- transact.call('ZADD', lock_readers_key, redis_time + ttl.to_i, acquirer_id)
616
-
617
- # Step 6.4 (RW): the registry should live as long as the longest read lock
618
- # - NX + GT => sets the registry TTL to max(current TTL, read lock TTL);
619
- # - in order to prevent "infinite registries" after process crashes;
620
- transact.call('PEXPIRE', lock_readers_key, ttl, 'NX')
621
- transact.call('PEXPIRE', lock_readers_key, ttl, 'GT')
622
-
623
- # Step 6.5 (RW): store the read lock data (the same structure as the write lock data
624
- # without `acq_id`/`hst_id`: both are derived from the read lock acquirer id that is
625
- # a part of the data key and the registry member, so the info adds them on formatting)
626
- # - drop the data of the expired read lock of the current acquirer (if any);
627
- # - the read lock data lives as long as the read lock;
628
- transact.call('DEL', read_lock_key)
629
- transact.call(
630
- 'HSET',
631
- read_lock_key,
632
- 'ts', timestamp = Time.now.to_f,
633
- 'ini_ttl', ttl,
634
- *(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
635
543
  )
636
- transact.call('PEXPIRE', read_lock_key, ttl)
637
-
638
- # Step 6.6 (lock index): the lock name lives in the lock index as long as the read lock
639
- # (the upper bound of the read lock lifetime: client time + ttl + grace period);
640
- # - in the same transaction: an index entry dropped between the queueing and this
641
- # transaction (`#unlock`, `#clear_locks`) is restored with the read lock;
642
- # - GT => the score is never decreased (other keys of the lock can live longer);
643
- # - only with the enabled lock index (the last command of the transaction);
644
- if lock_index_enabled
645
- transact.call(
646
- 'ZADD', RedisQueuedLocks::Resource::LOCK_INDEX_KEY, 'GT',
647
- RedisQueuedLocks::Resource.lock_index_time + ttl.to_i + lock_index_grace_period,
648
- lock_name
649
- )
650
- end
651
544
 
652
545
  inter_result = :lock_obtaining
653
546
 
654
547
  LogVisitor.obtain__free_to_acquire(
655
548
  logger, log_sampled, log_lock_try, lock_key,
656
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode
549
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode
657
550
  )
658
551
  else
659
552
  # NOTE: required lock is free and ready to be acquired! acquire! (write lock)
660
553
 
661
- # Step 6.1: remove our acquirer from waiting queue
662
- transact.call('ZREM', request_queue, acquirer_id)
663
-
664
- # Step 6.2: acquire a lock and store an info about the acquirer and host
665
- transact.call(
666
- 'HSET',
667
- lock_key,
668
- 'acq_id', acquirer_id,
669
- 'hst_id', host_id,
670
- 'ts', timestamp = Time.now.to_f,
671
- 'ini_ttl', ttl,
672
- *(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
673
560
  )
674
561
 
675
- # Step 6.3: set the lock expiration time in order to prevent "infinite locks"
676
- transact.call('PEXPIRE', lock_key, ttl) # NOTE: in milliseconds
677
-
678
- # Step 6.4 (lock index): the lock name lives in the lock index as long as the lock
679
- # (the upper bound of the lock lifetime: client time + ttl + grace period);
680
- # - in the same transaction: an index entry dropped between the queueing and this
681
- # transaction (`#unlock`, `#clear_locks`) is restored with the lock;
682
- # - GT => the score is never decreased (other keys of the lock can live longer);
683
- # - only with the enabled lock index (the last command of the transaction);
684
- if lock_index_enabled
685
- transact.call(
686
- 'ZADD', RedisQueuedLocks::Resource::LOCK_INDEX_KEY, 'GT',
687
- RedisQueuedLocks::Resource.lock_index_time + ttl.to_i + lock_index_grace_period,
688
- lock_name
689
- )
690
- end
691
-
692
562
  inter_result = :lock_obtaining
693
563
 
694
564
  LogVisitor.obtain__free_to_acquire(
695
565
  logger, log_sampled, log_lock_try, lock_key,
696
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode
566
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode
697
567
  )
698
568
  end
699
569
  end
@@ -879,6 +749,374 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
879
749
 
880
750
  private
881
751
 
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
+
882
1120
  # Queues the lock request and reads the lock queue state (Steps 2.1, 1, 3 and 3.2-3.4 of
883
1121
  # the attempt): the commands are added to the given pipeline (one round trip with other
884
1122
  # pipelined commands).
@@ -1179,7 +1417,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
1179
1417
  # @param acquirer_id [String]
1180
1418
  # @param host_id [String]
1181
1419
  # @param access_strategy [Symbol]
1182
- # @param read_write_mode [Symbol]
1420
+ # @param rw_mode [Symbol]
1183
1421
  # @param waiting_acquirer [String,NilClass]
1184
1422
  # @param waiting_priority [Integer,NilClass]
1185
1423
  # The priority of the first request (decoded for logs only: `nil` without logs).
@@ -1204,7 +1442,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
1204
1442
  acquirer_id,
1205
1443
  host_id,
1206
1444
  access_strategy,
1207
- read_write_mode,
1445
+ rw_mode,
1208
1446
  waiting_acquirer,
1209
1447
  waiting_priority,
1210
1448
  opposite_waiting_request,
@@ -1214,12 +1452,12 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
1214
1452
  when :dead_score_reached
1215
1453
  LogVisitor.exit__queue_ttl_reached(
1216
1454
  logger, log_sampled, log_lock_try, lock_key,
1217
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode
1455
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode
1218
1456
  )
1219
1457
  when :acquirer_is_not_first_in_queue
1220
1458
  LogVisitor.exit__no_first(
1221
1459
  logger, log_sampled, log_lock_try, lock_key,
1222
- 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,
1223
1461
  waiting_priority,
1224
1462
  # NOTE: lock data is extracted for logs only
1225
1463
  ((log_sampled && log_lock_try) ? rconn.call('HGETALL', lock_key).to_h : NO_LOCK_DATA)
@@ -1231,20 +1469,20 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
1231
1469
  if obstacle == :write_request_is_ahead
1232
1470
  LogVisitor.exit__write_request_ahead(
1233
1471
  logger, log_sampled, log_lock_try, lock_key,
1234
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode,
1472
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode,
1235
1473
  ahead_request[0], ahead_priority
1236
1474
  )
1237
1475
  else
1238
1476
  LogVisitor.exit__read_request_ahead(
1239
1477
  logger, log_sampled, log_lock_try, lock_key,
1240
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode,
1478
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode,
1241
1479
  ahead_request[0], ahead_priority
1242
1480
  )
1243
1481
  end
1244
1482
  when :lock_is_still_acquired
1245
1483
  LogVisitor.exit__lock_still_obtained(
1246
1484
  logger, log_sampled, log_lock_try, lock_key,
1247
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode,
1485
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode,
1248
1486
  waiting_acquirer, waiting_priority,
1249
1487
  current_lock_obtainer, #: String
1250
1488
  # NOTE: lock data is extracted for logs only
@@ -1253,7 +1491,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
1253
1491
  when :read_lock_is_still_acquired
1254
1492
  LogVisitor.exit__read_lock_still_obtained(
1255
1493
  logger, log_sampled, log_lock_try, lock_key,
1256
- 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,
1257
1495
  waiting_priority
1258
1496
  )
1259
1497
  end
@@ -1303,7 +1541,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
1303
1541
  # them at the same step (after the queueing log events, see `log_dropped_requests`);
1304
1542
  #
1305
1543
  # @param rconn [RedisClient]
1306
- # @param read_write_mode [Symbol]
1544
+ # @param rw_mode [Symbol]
1307
1545
  # @param request_queue [String]
1308
1546
  # @param opposite_request_queue [String]
1309
1547
  # @param acquirer_id [String]
@@ -1325,7 +1563,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
1325
1563
  # rubocop:disable Metrics/MethodLength
1326
1564
  def drop_dead_requests_ahead(
1327
1565
  rconn,
1328
- read_write_mode,
1566
+ rw_mode,
1329
1567
  request_queue,
1330
1568
  opposite_request_queue,
1331
1569
  acquirer_id,
@@ -1337,66 +1575,30 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
1337
1575
  )
1338
1576
  return unless access_strategy == :queued
1339
1577
 
1340
- read_mode = (read_write_mode == :read)
1578
+ read_mode = (rw_mode == :read)
1341
1579
  round_trips = 0
1342
1580
  # NOTE: allocated by the first removal only (no allocations while nothing dead blocks)
1343
1581
  # @type var dropped_requests: Array[[String, Integer]]?
1344
1582
  dropped_requests = nil
1345
1583
 
1346
1584
  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
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
1367
1588
  )
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
1589
+ break unless dead_requests
1590
+
1591
+ dead_head, dead_head_priority, dead_ahead, dead_ahead_priority = dead_requests
1373
1592
 
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
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
1377
1595
  # the opposite request queue);
1378
1596
  # @type var queue_state: Array[untyped]
1379
1597
  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
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
+ )
1400
1602
  pipeline.call('ZRANGE', request_queue, '0', '0', 'WITHSCORES')
1401
1603
  pipeline.call('ZRANGE', opposite_request_queue, '0', '0', 'WITHSCORES')
1402
1604
  end
@@ -1414,6 +1616,110 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
1414
1616
  end
1415
1617
  # rubocop:enable Metrics/MethodLength
1416
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
+
1417
1723
  # (priorities) Logs the dead lock requests dropped by `drop_dead_requests_ahead` (the attempt
1418
1724
  # and its pre-check log them at the same step: after the queueing log events).
1419
1725
  #
@@ -1425,7 +1731,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
1425
1731
  # @param acquirer_id [String]
1426
1732
  # @param host_id [String]
1427
1733
  # @param access_strategy [Symbol]
1428
- # @param read_write_mode [Symbol]
1734
+ # @param rw_mode [Symbol]
1429
1735
  # @param dropped_requests [Array<Array<String,Integer>>]
1430
1736
  # The acquirer and the priority of each dropped lock request.
1431
1737
  # @return [void]
@@ -1441,7 +1747,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
1441
1747
  acquirer_id,
1442
1748
  host_id,
1443
1749
  access_strategy,
1444
- read_write_mode,
1750
+ rw_mode,
1445
1751
  dropped_requests
1446
1752
  )
1447
1753
  return unless log_sampled && log_lock_try
@@ -1449,7 +1755,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
1449
1755
  dropped_requests.each do |dead_acquirer_id, dead_priority|
1450
1756
  LogVisitor.dead_request_removed(
1451
1757
  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
1758
+ access_strategy, rw_mode, dead_acquirer_id, dead_priority
1453
1759
  )
1454
1760
  end
1455
1761
  end
@@ -1493,7 +1799,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
1493
1799
  # @param log_sampled [Boolean]
1494
1800
  # @param log_lock_try [Boolean]
1495
1801
  # @param lock_key [String]
1496
- # @param read_write_mode [Symbol]
1802
+ # @param rw_mode [Symbol]
1497
1803
  # @param lock_readers_key [String]
1498
1804
  # @param request_queue [String]
1499
1805
  # @param opposite_request_queue [String]
@@ -1520,7 +1826,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
1520
1826
  log_sampled,
1521
1827
  log_lock_try,
1522
1828
  lock_key,
1523
- read_write_mode,
1829
+ rw_mode,
1524
1830
  lock_readers_key,
1525
1831
  request_queue,
1526
1832
  opposite_request_queue,
@@ -1536,7 +1842,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
1536
1842
  fail_fast,
1537
1843
  access_strategy
1538
1844
  )
1539
- read_mode = (read_write_mode == :read)
1845
+ read_mode = (rw_mode == :read)
1540
1846
  # NOTE: the same pipeline layout and the same reply reading as in Step 0.1 of the attempt
1541
1847
  # @type var lock_state: Array[untyped]
1542
1848
  lock_state = rconn.pipelined do |pipeline|
@@ -1572,7 +1878,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
1572
1878
  # the step of the attempt (after the queueing log events of the failed pre-check, they
1573
1879
  # follow the decision) or before the attempt that continues (it logs its own queueing);
1574
1880
  actual_queue_heads = drop_dead_requests_ahead(
1575
- rconn, read_write_mode, request_queue, opposite_request_queue, acquirer_id,
1881
+ rconn, rw_mode, request_queue, opposite_request_queue, acquirer_id,
1576
1882
  acquirer_position, acquirer_dead_score, access_strategy, waiting_request,
1577
1883
  opposite_waiting_request
1578
1884
  )
@@ -1594,7 +1900,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
1594
1900
  if dropped_requests
1595
1901
  log_dropped_requests(
1596
1902
  logger, log_sampled, log_lock_try, lock_key, queue_ttl, acquirer_id, host_id,
1597
- access_strategy, read_write_mode, dropped_requests
1903
+ access_strategy, rw_mode, dropped_requests
1598
1904
  )
1599
1905
  end
1600
1906
  return
@@ -1607,26 +1913,26 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
1607
1913
  # dead requests, the first request, the exit event)
1608
1914
  LogVisitor.acq_added_to_queue(
1609
1915
  logger, log_sampled, log_lock_try, lock_key,
1610
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode, priority
1916
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode, priority
1611
1917
  )
1612
1918
  LogVisitor.remove_expired_acqs(
1613
1919
  logger, log_sampled, log_lock_try, lock_key,
1614
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode
1920
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode
1615
1921
  )
1616
1922
  if dropped_requests
1617
1923
  log_dropped_requests(
1618
1924
  logger, log_sampled, log_lock_try, lock_key, queue_ttl, acquirer_id, host_id,
1619
- access_strategy, read_write_mode, dropped_requests
1925
+ access_strategy, rw_mode, dropped_requests
1620
1926
  )
1621
1927
  end
1622
1928
  LogVisitor.get_first_from_queue(
1623
1929
  logger, log_sampled, log_lock_try, lock_key,
1624
- 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,
1625
1931
  waiting_priority
1626
1932
  )
1627
1933
  log_lock_obstacle(
1628
1934
  rconn, obstacle, logger, log_sampled, log_lock_try, lock_key, queue_ttl,
1629
- acquirer_id, host_id, access_strategy, read_write_mode,
1935
+ acquirer_id, host_id, access_strategy, rw_mode,
1630
1936
  waiting_acquirer, waiting_priority, opposite_waiting_request, current_lock_obtainer
1631
1937
  )
1632
1938
  obstacle