redis_queued_locks 1.19.0 → 1.21.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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/project-overview.md +96 -23
  3. data/.claude/rules/acquirer.md +39 -20
  4. data/.claude/rules/arguments.md +3 -2
  5. data/.claude/rules/git.md +3 -2
  6. data/.claude/rules/logic.md +32 -8
  7. data/.claude/rules/swarm.md +14 -9
  8. data/.claude/rules/tests.md +12 -3
  9. data/.claude/rules/type-checking.md +2 -1
  10. data/.claude/rules/visitors.md +10 -11
  11. data/.claude/skills/rql-code-review/SKILL.md +386 -0
  12. data/.claude/skills/rql-code-review/templates/report.en.md +31 -0
  13. data/.claude/skills/rql-code-review/templates/report.ru.md +31 -0
  14. data/CHANGELOG.md +194 -0
  15. data/CLAUDE.md +6 -3
  16. data/README.md +502 -92
  17. data/lib/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue/log_visitor.rb +1 -0
  18. data/lib/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rb +42 -0
  19. data/lib/redis_queued_locks/acquirer/acquire_lock/log_visitor.rb +45 -0
  20. data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock/log_visitor.rb +1 -0
  21. data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rb +695 -128
  22. data/lib/redis_queued_locks/acquirer/acquire_lock/with_acq_timeout.rb +2 -2
  23. data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire/log_visitor.rb +1 -0
  24. data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire.rb +8 -6
  25. data/lib/redis_queued_locks/acquirer/acquire_lock.rb +134 -26
  26. data/lib/redis_queued_locks/acquirer/clear_dead_requests.rb +229 -24
  27. data/lib/redis_queued_locks/acquirer/extend_lock_ttl.rb +189 -18
  28. data/lib/redis_queued_locks/acquirer/is_locked.rb +0 -1
  29. data/lib/redis_queued_locks/acquirer/is_queued.rb +0 -1
  30. data/lib/redis_queued_locks/acquirer/keys.rb +151 -7
  31. data/lib/redis_queued_locks/acquirer/lock_info.rb +39 -41
  32. data/lib/redis_queued_locks/acquirer/lock_series_poc/instr_visitor.rb +3 -2
  33. data/lib/redis_queued_locks/acquirer/lock_series_poc/log_visitor.rb +5 -4
  34. data/lib/redis_queued_locks/acquirer/lock_series_poc.rb +79 -20
  35. data/lib/redis_queued_locks/acquirer/locks.rb +212 -83
  36. data/lib/redis_queued_locks/acquirer/queue_info.rb +11 -18
  37. data/lib/redis_queued_locks/acquirer/queues.rb +123 -50
  38. data/lib/redis_queued_locks/acquirer/reindex_locks.rb +253 -0
  39. data/lib/redis_queued_locks/acquirer/release_all_locks.rb +177 -61
  40. data/lib/redis_queued_locks/acquirer/release_lock.rb +84 -25
  41. data/lib/redis_queued_locks/acquirer/release_locks_of.rb +274 -70
  42. data/lib/redis_queued_locks/acquirer/release_read_lock.rb +22 -3
  43. data/lib/redis_queued_locks/acquirer.rb +1 -0
  44. data/lib/redis_queued_locks/client.rb +164 -36
  45. data/lib/redis_queued_locks/config.rb +16 -13
  46. data/lib/redis_queued_locks/errors.rb +34 -4
  47. data/lib/redis_queued_locks/instrument/sampler.rb +4 -7
  48. data/lib/redis_queued_locks/logging/sampler.rb +4 -8
  49. data/lib/redis_queued_locks/resource.rb +113 -6
  50. data/lib/redis_queued_locks/swarm/flush_zombies.rb +333 -61
  51. data/lib/redis_queued_locks/swarm/redis_client_builder.rb +8 -4
  52. data/lib/redis_queued_locks/swarm/swarm_element/threaded.rb +3 -2
  53. data/lib/redis_queued_locks/swarm/zombie_info.rb +203 -53
  54. data/lib/redis_queued_locks/swarm.rb +97 -5
  55. data/lib/redis_queued_locks/utilities.rb +1 -1
  56. data/lib/redis_queued_locks/version.rb +2 -2
  57. data/sig/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rbs +14 -0
  58. data/sig/redis_queued_locks/acquirer/acquire_lock/log_visitor.rbs +13 -0
  59. data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rbs +107 -1
  60. data/sig/redis_queued_locks/acquirer/acquire_lock/yield_expire.rbs +3 -3
  61. data/sig/redis_queued_locks/acquirer/acquire_lock.rbs +3 -1
  62. data/sig/redis_queued_locks/acquirer/clear_dead_requests.rbs +19 -1
  63. data/sig/redis_queued_locks/acquirer/extend_lock_ttl.rbs +20 -4
  64. data/sig/redis_queued_locks/acquirer/keys.rbs +11 -1
  65. data/sig/redis_queued_locks/acquirer/locks.rbs +15 -6
  66. data/sig/redis_queued_locks/acquirer/queues.rbs +9 -2
  67. data/sig/redis_queued_locks/acquirer/reindex_locks.rbs +52 -0
  68. data/sig/redis_queued_locks/acquirer/release_all_locks.rbs +8 -1
  69. data/sig/redis_queued_locks/acquirer/release_lock.rbs +6 -3
  70. data/sig/redis_queued_locks/acquirer/release_locks_of.rbs +20 -1
  71. data/sig/redis_queued_locks/acquirer/release_read_lock.rbs +1 -1
  72. data/sig/redis_queued_locks/client.rbs +21 -2
  73. data/sig/redis_queued_locks/errors.rbs +5 -2
  74. data/sig/redis_queued_locks/instrument/sampler.rbs +0 -2
  75. data/sig/redis_queued_locks/logging/sampler.rbs +0 -2
  76. data/sig/redis_queued_locks/resource.rbs +7 -1
  77. data/sig/redis_queued_locks/swarm/flush_zombies.rbs +51 -1
  78. data/sig/redis_queued_locks/swarm/zombie_info.rbs +58 -7
  79. data/sig/vendor/redis_client.rbs +2 -0
  80. metadata +6 -1
@@ -2,7 +2,7 @@
2
2
 
3
3
  # @api public
4
4
  # @since 1.0.0
5
- # @version 1.18.0
5
+ # @version 1.21.0
6
6
  # rubocop:disable Metrics/ClassLength
7
7
  class RedisQueuedLocks::Client
8
8
  # @return [RedisClient]
@@ -79,6 +79,7 @@ class RedisQueuedLocks::Client
79
79
  #
80
80
  # @api public
81
81
  # @since 1.9.0
82
+ # @version 1.21.0
82
83
  def swarm_info(zombie_ttl: config['swarm.flush_zombies.zombie_ttl'])
83
84
  swarm.swarm_info(zombie_ttl:)
84
85
  end
@@ -107,6 +108,7 @@ class RedisQueuedLocks::Client
107
108
  #
108
109
  # @api public
109
110
  # @since 1.9.0
111
+ # @version 1.21.0
110
112
  def flush_zombies(
111
113
  zombie_ttl: config['swarm.flush_zombies.zombie_ttl'],
112
114
  lock_scan_size: config['swarm.flush_zombies.zombie_lock_scan_size'],
@@ -121,6 +123,7 @@ class RedisQueuedLocks::Client
121
123
  #
122
124
  # @api public
123
125
  # @since 1.9.0
126
+ # @version 1.21.0
124
127
  def zombie_locks(
125
128
  zombie_ttl: config['swarm.flush_zombies.zombie_ttl'],
126
129
  lock_scan_size: config['swarm.flush_zombies.zombie_lock_scan_size']
@@ -132,8 +135,9 @@ class RedisQueuedLocks::Client
132
135
  # @option lock_scan_size [Integer]
133
136
  # @return [Set<String>]
134
137
  #
135
- # @api ppublic
138
+ # @api public
136
139
  # @since 1.9.0
140
+ # @version 1.21.0
137
141
  def zombie_acquirers(
138
142
  zombie_ttl: config['swarm.flush_zombies.zombie_ttl'],
139
143
  lock_scan_size: config['swarm.flush_zombies.zombie_lock_scan_size']
@@ -146,6 +150,7 @@ class RedisQueuedLocks::Client
146
150
  #
147
151
  # @api public
148
152
  # @since 1.9.0
153
+ # @version 1.21.0
149
154
  def zombie_hosts(zombie_ttl: config['swarm.flush_zombies.zombie_ttl'])
150
155
  swarm.zombie_hosts(zombie_ttl:)
151
156
  end
@@ -159,7 +164,7 @@ class RedisQueuedLocks::Client
159
164
  #
160
165
  # @api public
161
166
  # @since 1.9.0
162
- # @version 1.17.0
167
+ # @version 1.21.0
163
168
  def zombies_info(
164
169
  zombie_ttl: config['swarm.flush_zombies.zombie_ttl'],
165
170
  lock_scan_size: config['swarm.flush_zombies.zombie_lock_scan_size']
@@ -171,11 +176,22 @@ class RedisQueuedLocks::Client
171
176
  # @param lock_name [String]
172
177
  # Lock name to be obtained.
173
178
  # @option ttl [Integer]
174
- # Lock's time to live (in milliseconds).
179
+ # - Lock's time to live (in milliseconds);
180
+ # - a positive Integer;
181
+ # - pre-configured in `config['default_lock_ttl']`;
175
182
  # @option queue_ttl [Integer]
176
183
  # Lifetime of the acuier's lock request. In seconds.
184
+ # @option lock_index_grace_period [Integer]
185
+ # - The lock index score margin over the lock key lifetime (in milliseconds): the lock name
186
+ # stays in the lock index (`rql:index:locks`) for this period after the expiration of
187
+ # the lock keys;
188
+ # - should be greater than the client clock skew between your hosts plus the network latency;
189
+ # - used only with the enabled lock index (`config['lock_index_enabled']`, the client-wide
190
+ # mode: the lock index is maintained by all lock acquirements of the client);
191
+ # - pre-configured in `config['lock_index_grace_period']`;
177
192
  # @option timeout [Integer,NilClass]
178
- # Time period whe should try to acquire the lock (in seconds). Nil means "without timeout".
193
+ # Time period whe should try to acquire the lock (in seconds). Nil means "without timeout"
194
+ # (`0` is the same as `nil`).
179
195
  # @option timed [Boolean]
180
196
  # Limit the invocation time period of the passed block of code by the lock's TTL.
181
197
  # @option retry_count [Integer,NilClass]
@@ -286,7 +302,7 @@ class RedisQueuedLocks::Client
286
302
  # - works in tandem with <config['log_sampling_percent'] and <config['log_sampler']>;
287
303
  # @option log_sampling_percent [Integer]
288
304
  # - the percent of cases that should be logged;
289
- # - take an effect when <config['log_sampling_enalbed']> is true;
305
+ # - take an effect when <config['log_sampling_enabled']> is true;
290
306
  # - works in tandem with <config['log_sampling_enabled']> and <config['log_sampler']> configs;
291
307
  # @option log_sampler [#sampling_happened?,Module<RedisQueuedLocks::Logging::Sampler>]
292
308
  # - percent-based log sampler that decides should be RQL case logged or not;
@@ -307,7 +323,7 @@ class RedisQueuedLocks::Client
307
323
  # - works in tandem with <config['instr_sampling_percent']> and <config['instr_sampler']>;
308
324
  # @option instr_sampling_percent [Integer]
309
325
  # - the percent of cases that should be instrumented;
310
- # - take an effect when <config['instr_sampling_enalbed']> is true;
326
+ # - take an effect when <config['instr_sampling_enabled']> is true;
311
327
  # - works in tandem with <config['instr_sampling_enabled']>
312
328
  # and <config['instr_sampler']> configs;
313
329
  # @option instr_sampler [#sampling_happened?,Module<RedisQueuedLocks::Instrument::Sampler>]
@@ -331,12 +347,13 @@ class RedisQueuedLocks::Client
331
347
  #
332
348
  # @api public
333
349
  # @since 1.0.0
334
- # @version 1.18.0
350
+ # @version 1.21.0
335
351
  # rubocop:disable Metrics/MethodLength
336
352
  def lock(
337
353
  lock_name,
338
354
  ttl: config['default_lock_ttl'],
339
355
  queue_ttl: config['default_queue_ttl'],
356
+ lock_index_grace_period: config['lock_index_grace_period'],
340
357
  timeout: config['try_to_lock_timeout'],
341
358
  timed: config['is_timed_by_default'],
342
359
  retry_count: config['retry_count'],
@@ -373,6 +390,8 @@ class RedisQueuedLocks::Client
373
390
  ractor_id: RedisQueuedLocks::Resource.get_ractor_id,
374
391
  ttl:,
375
392
  queue_ttl:,
393
+ lock_index_enabled: config['lock_index_enabled'],
394
+ lock_index_grace_period:,
376
395
  timeout:,
377
396
  timed:,
378
397
  retry_count:,
@@ -407,11 +426,13 @@ class RedisQueuedLocks::Client
407
426
  # rubocop:disable all
408
427
  # @api public
409
428
  # @since 1.16.0
429
+ # @version 1.21.0
410
430
  def lock_series( # steep:ignore
411
431
  *lock_names,
412
432
  detailed_result: false,
413
433
  ttl: config['default_lock_ttl'],
414
434
  queue_ttl: config['default_queue_ttl'],
435
+ lock_index_grace_period: config['lock_index_grace_period'],
415
436
  timeout: config['try_to_lock_timeout'],
416
437
  timed: config['is_timed_by_default'],
417
438
  retry_count: config['retry_count'],
@@ -449,6 +470,8 @@ class RedisQueuedLocks::Client
449
470
  ractor_id: RedisQueuedLocks::Resource.get_ractor_id,
450
471
  ttl:,
451
472
  queue_ttl:,
473
+ lock_index_enabled: config['lock_index_enabled'],
474
+ lock_index_grace_period:,
452
475
  timeout:,
453
476
  timed:,
454
477
  retry_count:,
@@ -483,11 +506,13 @@ class RedisQueuedLocks::Client
483
506
  # rubocop:disable all
484
507
  # @api public
485
508
  # @since 1.16.0
509
+ # @version 1.21.0
486
510
  def lock_series!( # steep:ignore
487
511
  *lock_names,
488
512
  detailed_result: false,
489
513
  ttl: config['default_lock_ttl'],
490
514
  queue_ttl: config['default_queue_ttl'],
515
+ lock_index_grace_period: config['lock_index_grace_period'],
491
516
  timeout: config['try_to_lock_timeout'],
492
517
  timed: config['is_timed_by_default'],
493
518
  retry_count: config['retry_count'],
@@ -519,6 +544,7 @@ class RedisQueuedLocks::Client
519
544
  detailed_result:,
520
545
  ttl:,
521
546
  queue_ttl:,
547
+ lock_index_grace_period:,
522
548
  timeout:,
523
549
  timed:,
524
550
  retry_count:,
@@ -552,11 +578,12 @@ class RedisQueuedLocks::Client
552
578
  #
553
579
  # @api public
554
580
  # @since 1.0.0
555
- # @version 1.18.0
581
+ # @version 1.21.0
556
582
  def lock!(
557
583
  lock_name,
558
584
  ttl: config['default_lock_ttl'],
559
585
  queue_ttl: config['default_queue_ttl'],
586
+ lock_index_grace_period: config['lock_index_grace_period'],
560
587
  timeout: config['try_to_lock_timeout'],
561
588
  timed: config['is_timed_by_default'],
562
589
  retry_count: config['retry_count'],
@@ -587,6 +614,7 @@ class RedisQueuedLocks::Client
587
614
  lock_name,
588
615
  ttl:,
589
616
  queue_ttl:,
617
+ lock_index_grace_period:,
590
618
  timeout:,
591
619
  timed:,
592
620
  retry_count:,
@@ -645,7 +673,7 @@ class RedisQueuedLocks::Client
645
673
  #
646
674
  # @api public
647
675
  # @since 1.0.0
648
- # @version 1.18.0
676
+ # @version 1.21.0
649
677
  def unlock(
650
678
  lock_name,
651
679
  logger: config['logger'],
@@ -663,8 +691,10 @@ class RedisQueuedLocks::Client
663
691
  RedisQueuedLocks::Acquirer::ReleaseLock.release_lock(
664
692
  redis_client,
665
693
  lock_name,
666
- instrumenter,
694
+ config['lock_index_enabled'],
667
695
  logger,
696
+ instrumenter,
697
+ instrument,
668
698
  log_sampling_enabled,
669
699
  log_sampling_percent,
670
700
  log_sampler,
@@ -721,7 +751,7 @@ class RedisQueuedLocks::Client
721
751
  #
722
752
  # @api public
723
753
  # @since 1.18.0
724
- # @version 1.19.0
754
+ # @version 1.21.0
725
755
  def unlock_read(
726
756
  lock_name,
727
757
  acquirer_id: nil,
@@ -742,8 +772,8 @@ class RedisQueuedLocks::Client
742
772
  redis_client,
743
773
  lock_name,
744
774
  acquirer_id || current_acquirer_id(identity:),
745
- instrumenter,
746
775
  logger,
776
+ instrumenter,
747
777
  instrument,
748
778
  log_sampling_enabled,
749
779
  log_sampling_percent,
@@ -881,19 +911,14 @@ class RedisQueuedLocks::Client
881
911
  RedisQueuedLocks::Resource.possible_host_identifiers(identity)
882
912
  end
883
913
 
884
- # This method is non-atomic cuz redis does not provide an atomic function for TTL/PTTL extension.
885
- # So the methid is spliited into the two commands:
886
- # (1) read current pttl
887
- # (2) set new ttl that is calculated as "current pttl + additional milliseconds"
888
- # What can happen during these steps
889
- # - lock is expired between commands or before the first command;
890
- # - lock is expired before the second command;
891
- # - lock is expired AND newly acquired by another process (so you will extend the
892
- # totally new lock with fresh PTTL);
914
+ # The write lock TTL is extended atomically by one Lua script (`PTTL` + `PEXPIRE` with
915
+ # "current pttl + additional milliseconds") without the lock owner check: when your lock is
916
+ # expired and newly obtained by another acquirer, the lock of that acquirer is extended.
893
917
  # Use it at your own risk and consider async nature when calling this method.
894
918
  #
895
919
  # @param lock_name [String]
896
- # @param milliseconds [Integer] How many milliseconds should be added.
920
+ # @param milliseconds [Integer]
921
+ # How many milliseconds should be added: a positive Integer.
897
922
  # @option read_write_mode [Symbol]
898
923
  # - `:write` (default) - extends the write lock (the method description above);
899
924
  # - `:read` - extends the read lock of the current acquirer only (an expired read lock or
@@ -923,7 +948,7 @@ class RedisQueuedLocks::Client
923
948
  #
924
949
  # @api public
925
950
  # @since 1.0.0
926
- # @version 1.18.0
951
+ # @version 1.21.0
927
952
  def extend_lock_ttl(
928
953
  lock_name,
929
954
  milliseconds,
@@ -949,6 +974,7 @@ class RedisQueuedLocks::Client
949
974
  read_write_mode,
950
975
  all_read_locks,
951
976
  current_acquirer_id(identity:),
977
+ config['lock_index_enabled'],
952
978
  logger,
953
979
  instrumenter,
954
980
  instrument,
@@ -967,6 +993,7 @@ class RedisQueuedLocks::Client
967
993
  # Returns:
968
994
  # - :rel_time - (milliseconds) - time spent to release all locks and queues;
969
995
  # - :rel_key_cnt - (integer) - the number of released redis keys (queus+locks);
996
+ # Locks are found via the keyspace SCAN or via the lock index (`config['lock_index_enabled']`).
970
997
  #
971
998
  # @option batch_size [Integer]
972
999
  # @option logger [::Logger,#debug]
@@ -985,7 +1012,7 @@ class RedisQueuedLocks::Client
985
1012
  #
986
1013
  # @api public
987
1014
  # @since 1.0.0
988
- # @version 1.6.0
1015
+ # @version 1.21.0
989
1016
  def clear_locks(
990
1017
  batch_size: config['lock_release_batch_size'],
991
1018
  logger: config['logger'],
@@ -1003,6 +1030,7 @@ class RedisQueuedLocks::Client
1003
1030
  RedisQueuedLocks::Acquirer::ReleaseAllLocks.release_all_locks(
1004
1031
  redis_client,
1005
1032
  batch_size,
1033
+ config['lock_index_enabled'],
1006
1034
  logger,
1007
1035
  instrumenter,
1008
1036
  instrument,
@@ -1024,6 +1052,8 @@ class RedisQueuedLocks::Client
1024
1052
  # current puma request thread is killed by Rack::Timeout and you need to cleanup all zombie RQL
1025
1053
  # locks and lock reuqests obtained during the request processing).
1026
1054
  #
1055
+ # Locks are found via the keyspace SCAN or via the lock index (`config['lock_index_enabled']`).
1056
+ #
1027
1057
  # Identifiers can be extracted via:
1028
1058
  # - `#current_host_id`
1029
1059
  # - `#current_acquirer_id`
@@ -1071,11 +1101,12 @@ class RedisQueuedLocks::Client
1071
1101
  #
1072
1102
  # @api public
1073
1103
  # @since 1.14.0
1104
+ # @version 1.21.0
1074
1105
  def clear_locks_of(
1075
1106
  host_id:,
1076
1107
  acquirer_id:,
1077
1108
  lock_scan_size: config['clear_locks_of__lock_scan_size'],
1078
- queue_scan_size: config['clear_locks_of__queue_scan_size'], # steep:ingore
1109
+ queue_scan_size: config['clear_locks_of__queue_scan_size'],
1079
1110
  logger: config['logger'],
1080
1111
  instrumenter: config['instrumenter'],
1081
1112
  instrument: nil,
@@ -1094,6 +1125,7 @@ class RedisQueuedLocks::Client
1094
1125
  redis_client,
1095
1126
  lock_scan_size,
1096
1127
  queue_scan_size,
1128
+ config['lock_index_enabled'],
1097
1129
  logger,
1098
1130
  instrumenter,
1099
1131
  instrument,
@@ -1112,7 +1144,8 @@ class RedisQueuedLocks::Client
1112
1144
  # Release all locks of the current acquirer/host and
1113
1145
  # remove the current acquirer/host from all queues;
1114
1146
  #
1115
- # @option batch_size [Integer]
1147
+ # @option lock_scan_size [Integer]
1148
+ # @option queue_scan_size [Integer]
1116
1149
  # @option logger [::Logger,#debug]
1117
1150
  # @option instrumenter [#notify] See `config['instrumenter']` docs for details.
1118
1151
  # @option instrument [NilClass,Any]
@@ -1131,9 +1164,10 @@ class RedisQueuedLocks::Client
1131
1164
  #
1132
1165
  # @api public
1133
1166
  # @since 1.14.0
1167
+ # @version 1.21.0
1134
1168
  def clear_current_locks(
1135
1169
  lock_scan_size: config['clear_locks_of__lock_scan_size'],
1136
- queue_scan_size: config['clear_locks_of__queue_scan_size'], # steep:ingore
1170
+ queue_scan_size: config['clear_locks_of__queue_scan_size'],
1137
1171
  logger: config['logger'],
1138
1172
  instrumenter: config['instrumenter'],
1139
1173
  instrument: nil,
@@ -1183,10 +1217,16 @@ class RedisQueuedLocks::Client
1183
1217
  # at the moment of Redis'es SCAN;
1184
1218
  # @return [Set<String>,Set<Hash<Symbol,Any>>]
1185
1219
  #
1220
+ # NOTE: locks are found via the keyspace SCAN or via the lock index (ZSCAN, the enabled
1221
+ # `config['lock_index_enabled']`: `scan_size` is the number of locks in one batch);
1222
+ #
1186
1223
  # @api public
1187
1224
  # @since 1.0.0
1225
+ # @version 1.21.0
1188
1226
  def locks(scan_size: config['key_extraction_batch_size'], with_info: false)
1189
- RedisQueuedLocks::Acquirer::Locks.locks(redis_client, scan_size:, with_info:)
1227
+ RedisQueuedLocks::Acquirer::Locks.locks(
1228
+ redis_client, scan_size:, with_info:, lock_index_enabled: config['lock_index_enabled']
1229
+ )
1190
1230
  end
1191
1231
 
1192
1232
  # Extracts lock keys with their info. See #locks(with_info: true) for details.
@@ -1196,8 +1236,9 @@ class RedisQueuedLocks::Client
1196
1236
  #
1197
1237
  # @api public
1198
1238
  # @since 1.0.0
1199
- def locks_info(scan_size: config['key_extraction_batch_size']) # steep:ignore
1200
- locks(scan_size:, with_info: true)
1239
+ # @version 1.21.0
1240
+ def locks_info(scan_size: config['key_extraction_batch_size'])
1241
+ locks(scan_size:, with_info: true) #: RedisQueuedLocks::Acquirer::Locks::locksInfo
1201
1242
  end
1202
1243
 
1203
1244
  # @option scan_size [Integer]
@@ -1214,10 +1255,16 @@ class RedisQueuedLocks::Client
1214
1255
  # at the moment of Redis'es SCAN;
1215
1256
  # @return [Set<String>,String<Hash<Symbol,Any>>]
1216
1257
  #
1258
+ # NOTE: lock queues are found via the keyspace SCAN or via the lock index (ZSCAN, the enabled
1259
+ # `config['lock_index_enabled']`: `scan_size` is the number of locks in one batch);
1260
+ #
1217
1261
  # @api public
1218
1262
  # @since 1.0.0
1263
+ # @version 1.21.0
1219
1264
  def queues(scan_size: config['key_extraction_batch_size'], with_info: false)
1220
- RedisQueuedLocks::Acquirer::Queues.queues(redis_client, scan_size:, with_info:)
1265
+ RedisQueuedLocks::Acquirer::Queues.queues(
1266
+ redis_client, scan_size:, with_info:, lock_index_enabled: config['lock_index_enabled']
1267
+ )
1221
1268
  end
1222
1269
 
1223
1270
  # Extracts lock queues with their info. See #queues(with_info: true) for details.
@@ -1227,17 +1274,24 @@ class RedisQueuedLocks::Client
1227
1274
  #
1228
1275
  # @api public
1229
1276
  # @since 1.0.0
1277
+ # @version 1.21.0
1230
1278
  def queues_info(scan_size: config['key_extraction_batch_size'])
1231
- queues(scan_size:, with_info: true)
1279
+ queues(scan_size:, with_info: true) #: RedisQueuedLocks::Acquirer::Queues::queuesInfo
1232
1280
  end
1233
1281
 
1234
1282
  # @option scan_size [Integer]
1235
1283
  # @return [Set<String>]
1284
+ # - the disabled lock index: all the RQL keys (`rql:lock*`) found via the keyspace SCAN;
1285
+ # - the enabled lock index (`config['lock_index_enabled']`): the existing keys of the locks of
1286
+ # the lock index (ZSCAN, `scan_size` is the number of locks in one batch);
1236
1287
  #
1237
1288
  # @api public
1238
1289
  # @since 1.0.0
1290
+ # @version 1.21.0
1239
1291
  def keys(scan_size: config['key_extraction_batch_size'])
1240
- RedisQueuedLocks::Acquirer::Keys.keys(redis_client, scan_size:)
1292
+ RedisQueuedLocks::Acquirer::Keys.keys(
1293
+ redis_client, scan_size:, lock_index_enabled: config['lock_index_enabled']
1294
+ )
1241
1295
  end
1242
1296
 
1243
1297
  # @option dead_ttl [Integer]
@@ -1245,7 +1299,8 @@ class RedisQueuedLocks::Client
1245
1299
  # considered as dead;
1246
1300
  # - `config['dead_request_ttl']` is used by default;
1247
1301
  # @option scan_size [Integer]
1248
- # - the batch of scanned keys for Redis'es SCAN command;
1302
+ # - the batch of scanned keys for Redis'es SCAN command (or the number of locks in one batch of
1303
+ # the lock index ZSCAN pass: the enabled `config['lock_index_enabled']`);
1249
1304
  # - `config['lock_release_batch_size']` is used by default;
1250
1305
  # @option logger [::Logger,#debug]
1251
1306
  # @option instrumenter [#notify]
@@ -1263,7 +1318,7 @@ class RedisQueuedLocks::Client
1263
1318
  #
1264
1319
  # @api public
1265
1320
  # @since 1.0.0
1266
- # @version 1.6.0
1321
+ # @version 1.21.0
1267
1322
  def clear_dead_requests(
1268
1323
  dead_ttl: config['dead_request_ttl'],
1269
1324
  scan_size: config['lock_release_batch_size'],
@@ -1283,6 +1338,79 @@ class RedisQueuedLocks::Client
1283
1338
  redis_client,
1284
1339
  scan_size,
1285
1340
  dead_ttl,
1341
+ config['lock_index_enabled'],
1342
+ logger,
1343
+ instrumenter,
1344
+ instrument,
1345
+ log_sampling_enabled,
1346
+ log_sampling_percent,
1347
+ log_sampler,
1348
+ log_sample_this,
1349
+ instr_sampling_enabled,
1350
+ instr_sampling_percent,
1351
+ instr_sampler,
1352
+ instr_sample_this
1353
+ )
1354
+ end
1355
+
1356
+ # Adds the lock keys without the lock index entry (keys of older RQL versions, keys created while
1357
+ # the lock index was disabled) to the lock index (`rql:index:locks`). Invoke it once after the lock
1358
+ # index is enabled (`config['lock_index_enabled']`) on all your processes: lock-iterating
1359
+ # operations (the swarm, `#locks`, `#queues`, `#keys`, `#clear_locks`, ...) find locks via
1360
+ # the lock index only.
1361
+ # - works regardless of `config['lock_index_enabled']` (the lock index is not used and not
1362
+ # maintained while it is disabled);
1363
+ # - one SCAN pass over the RQL keys;
1364
+ # - lock queues without TTL (older RQL versions) get `queue_ttl` as their TTL;
1365
+ # - idempotent: the lock index scores are never decreased, already indexed keys stay as is;
1366
+ #
1367
+ # @option scan_size [Integer]
1368
+ # - the batch of scanned keys for Redis'es SCAN command;
1369
+ # - `config['lock_release_batch_size']` is used by default;
1370
+ # @option queue_ttl [Integer]
1371
+ # - TTL of the lock queues without TTL (in seconds);
1372
+ # - `config['default_queue_ttl']` is used by default;
1373
+ # @option lock_index_grace_period [Integer]
1374
+ # - the lock index score margin over the key lifetime (in milliseconds);
1375
+ # - `config['lock_index_grace_period']` is used by default;
1376
+ # @option logger [::Logger,#debug]
1377
+ # @option instrumenter [#notify]
1378
+ # @option instrument [NilClass,Any]
1379
+ # @option log_sampling_enabled [Boolean]
1380
+ # @option log_sampling_percent [Integer]
1381
+ # @option log_sampler [#sampling_happened?,Module<RedisQueuedLocks::Logging::Sampler>]
1382
+ # @option log_sample_this [Boolean]
1383
+ # @option instr_sampling_enabled [Boolean]
1384
+ # @option instr_sampling_percent [Integer]
1385
+ # @option instr_sampler [#sampling_happened?,Module<RedisQueuedLocks::Instrument::Sampler>]
1386
+ # @option instr_sample_this [Boolean]
1387
+ # @return [Hash<Symbol,Boolean|Hash<Symbol,Integer>>]
1388
+ # Format: { ok: true, result: { idx_key_cnt: Integer } } (the number of indexed keys)
1389
+ #
1390
+ # @api public
1391
+ # @since 1.20.0
1392
+ # @version 1.21.0
1393
+ def reindex_locks(
1394
+ scan_size: config['lock_release_batch_size'],
1395
+ queue_ttl: config['default_queue_ttl'],
1396
+ lock_index_grace_period: config['lock_index_grace_period'],
1397
+ logger: config['logger'],
1398
+ instrumenter: config['instrumenter'],
1399
+ instrument: nil,
1400
+ log_sampling_enabled: config['log_sampling_enabled'],
1401
+ log_sampling_percent: config['log_sampling_percent'],
1402
+ log_sampler: config['log_sampler'],
1403
+ log_sample_this: false,
1404
+ instr_sampling_enabled: config['instr_sampling_enabled'],
1405
+ instr_sampling_percent: config['instr_sampling_percent'],
1406
+ instr_sampler: config['instr_sampler'],
1407
+ instr_sample_this: false
1408
+ )
1409
+ RedisQueuedLocks::Acquirer::ReindexLocks.reindex_locks(
1410
+ redis_client,
1411
+ scan_size,
1412
+ queue_ttl,
1413
+ lock_index_grace_period,
1286
1414
  logger,
1287
1415
  instrumenter,
1288
1416
  instrument,
@@ -2,7 +2,6 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.13.0
5
- # @version 1.15.1
6
5
  # rubocop:disable Metrics/ClassLength
7
6
  class RedisQueuedLocks::Config
8
7
  require_relative 'config/dsl'
@@ -27,6 +26,8 @@ class RedisQueuedLocks::Config
27
26
  setting('logger', RedisQueuedLocks::Logging::VoidLogger)
28
27
  setting('log_lock_try', false)
29
28
  setting('dead_request_ttl', 1 * 24 * 60 * 60 * 1000) # NOTE: 1 day in milliseconds)
29
+ setting('lock_index_enabled', false)
30
+ setting('lock_index_grace_period', 60_000) # NOTE: in milliseconds)
30
31
  setting('is_timed_by_default', false)
31
32
  setting('default_conflict_strategy', :wait_for_lock)
32
33
  setting('default_access_strategy', :queued)
@@ -55,41 +56,43 @@ class RedisQueuedLocks::Config
55
56
  setting('swarm.flush_zombies.redis_config.pool_config', {})
56
57
 
57
58
  validate('swarm.auto_swarm') { |val| val == true || val == false }
58
- validate('swarm.supervisor.liveness_probing_period') { |val| val.is_a?(Integer) }
59
+ validate('swarm.supervisor.liveness_probing_period') { |val| val.is_a?(Integer) && val > 0 }
59
60
 
60
61
  validate('swarm.probe_hosts.enabled_for_swarm') { |val| val == true || val == false }
61
62
  validate('swarm.probe_hosts.redis_config.sentinel') { |val| val == true || val == false }
62
63
  validate('swarm.probe_hosts.redis_config.pooled') { |val| val == true || val == false }
63
64
  validate('swarm.probe_hosts.redis_config.config') { |val| val.is_a?(Hash) }
64
65
  validate('swarm.probe_hosts.redis_config.pool_config') { |val| val.is_a?(Hash) }
65
- validate('swarm.probe_hosts.probe_period') { |val| val.is_a?(Integer) }
66
+ validate('swarm.probe_hosts.probe_period') { |val| val.is_a?(Integer) && val > 0 }
66
67
 
67
68
  validate('swarm.flush_zombies.enabled_for_swarm') { |val| val == true || val == false }
68
69
  validate('swarm.flush_zombies.redis_config.sentinel') { |val| val == true || val == false }
69
70
  validate('swarm.flush_zombies.redis_config.pooled') { |val| val == true || val == false }
70
71
  validate('swarm.flush_zombies.redis_config.config') { |val| val.is_a?(Hash) }
71
72
  validate('swarm.flush_zombies.redis_config.pool_config') { |val| val.is_a?(Hash) }
72
- validate('swarm.flush_zombies.zombie_ttl') { |val| val.is_a?(Integer) }
73
- validate('swarm.flush_zombies.zombie_lock_scan_size') { |val| val.is_a?(Integer) }
74
- validate('swarm.flush_zombies.zombie_queue_scan_size') { |val| val.is_a?(Integer) }
75
- validate('swarm.flush_zombies.zombie_flush_period') { |val| val.is_a?(Integer) }
73
+ validate('swarm.flush_zombies.zombie_ttl') { |val| val.is_a?(Integer) && val > 0 }
74
+ validate('swarm.flush_zombies.zombie_lock_scan_size') { |val| val.is_a?(Integer) && val > 0 }
75
+ validate('swarm.flush_zombies.zombie_queue_scan_size') { |val| val.is_a?(Integer) && val > 0 }
76
+ validate('swarm.flush_zombies.zombie_flush_period') { |val| val.is_a?(Integer) && val > 0 }
76
77
 
77
78
  validate('retry_count') { |val| val == nil || (val.is_a?(Integer) && val >= 0) }
78
79
  validate('retry_delay') { |val| val.is_a?(Integer) && val >= 0 }
79
80
  validate('retry_jitter') { |val| val.is_a?(Integer) && val >= 0 }
80
81
  validate('try_to_lock_timeout') { |val| val == nil || (val.is_a?(Integer) && val >= 0) }
81
- validate('default_lock_ttl') { |val| val.is_a?(Integer) }
82
- validate('default_queue_ttl') { |val| val.is_a?(Integer) }
82
+ validate('default_lock_ttl') { |val| val.is_a?(Integer) && val > 0 }
83
+ validate('default_queue_ttl') { |val| val.is_a?(Integer) && val > 0 }
83
84
  validate('detailed_acq_timeout_error') { |val| val == true || val == false }
84
- validate('lock_release_batch_size') { |val| val.is_a?(Integer) }
85
- validate('clear_locks_of__lock_scan_size') { |val| val.is_a?(Integer) }
86
- validate('clear_locks_of__queue_scan_size') { |val| val.is_a?(Integer) }
87
- validate('key_extraction_batch_size') { |val| val.is_a?(Integer) }
85
+ validate('lock_release_batch_size') { |val| val.is_a?(Integer) && val > 0 }
86
+ validate('clear_locks_of__lock_scan_size') { |val| val.is_a?(Integer) && val > 0 }
87
+ validate('clear_locks_of__queue_scan_size') { |val| val.is_a?(Integer) && val > 0 }
88
+ validate('key_extraction_batch_size') { |val| val.is_a?(Integer) && val > 0 }
88
89
  validate('instrumenter') { |val| RedisQueuedLocks::Instrument.valid_interface?(val) }
89
90
  validate('uniq_identifier') { |val| val.is_a?(Proc) }
90
91
  validate('logger') { |val| RedisQueuedLocks::Logging.valid_interface?(val) }
91
92
  validate('log_lock_try') { |val| val == true || val == false }
92
93
  validate('dead_request_ttl') { |val| val.is_a?(Integer) && val > 0 }
94
+ validate('lock_index_enabled') { |val| val == true || val == false }
95
+ validate('lock_index_grace_period') { |val| val.is_a?(Integer) && val >= 0 }
93
96
  validate('is_timed_by_default') { |val| val == true || val == false }
94
97
  validate('log_sampler') { |val| RedisQueuedLocks::Logging.valid_sampler?(val) }
95
98
  validate('log_sampling_enabled') { |val| val == true || val == false }
@@ -4,6 +4,12 @@
4
4
  # the one-line definition style (like "Error = Class.new(::StandardError)")
5
5
  # is not used cuz that style can not be correctly defined via RBS types: we can not
6
6
  # identify the superclass of our class for a constant;
7
+ # NOTE: (error hierarchy) each error inherits from the global base error of its branch:
8
+ # - `Error` (`::StandardError`) - the main hierarchy;
9
+ # - `ArgumentError` (`::ArgumentError`) - invalid arguments (the Ruby core branch that can not be
10
+ # a part of the main hierarchy);
11
+ # - `Exception` (`::Exception`) - super exceptional cases only: each error of this branch
12
+ # describes why it is not a `StandardError`;
7
13
  module RedisQueuedLocks
8
14
  # @api public
9
15
  # @since 1.0.0
@@ -13,13 +19,29 @@ module RedisQueuedLocks
13
19
  # @since 1.0.0
14
20
  class ArgumentError < ::ArgumentError; end
15
21
 
22
+ # The base of the errors that should not be rescued by the code they interrupt
23
+ # (`rescue => e`, `rescue StandardError`, `rescue nil`): like `Timeout::ExitException`,
24
+ # they are rescued only by `rescue Exception` (and by the library itself).
25
+ # NOTE: inside `module RedisQueuedLocks` a bare `Exception` refers to this class;
26
+ #
27
+ # @api private
28
+ # @since 1.21.0
29
+ class Exception < ::Exception; end # rubocop:disable Lint/InheritException
30
+
16
31
  # @api public
17
32
  # @since 1.0.0
18
33
  class LockAlreadyObtainedError < Error; end
19
34
 
35
+ # Interrupts the lock acquirement after the `timeout` (`Timeout.timeout`); rescued by
36
+ # `WithAcqTimeout` (=> `LockAcquirementTimeoutError` / `:timeout_reached`).
37
+ # NOTE: (why `Exception`) the acquirement calls the logger and the instrumenter: a `StandardError`
38
+ # that interrupts them is swallowed by `rescue nil` of the log/instrumentation events, and the
39
+ # acquirement continues without the timeout (with `retry_count: nil` - without any limit);
40
+ #
20
41
  # @api private
21
42
  # @since 1.11.0
22
- class LockAcquirementIntermediateTimeoutError < ::Timeout::Error; end
43
+ # @version 1.21.0
44
+ class LockAcquirementIntermediateTimeoutError < Exception; end
23
45
 
24
46
  # @api public
25
47
  # @since 1.0.0
@@ -29,11 +51,19 @@ module RedisQueuedLocks
29
51
  # @since 1.0.0
30
52
  class LockAcquirementRetryLimitError < Error; end
31
53
 
54
+ # Interrupts the `timed: true` block after the lock ttl (`Timeout.timeout`); rescued by
55
+ # `YieldExpire` (=> `TimedLockTimeoutError`).
56
+ # NOTE: (why `Exception`) a `StandardError` is swallowed by `rescue => e` / `rescue nil` of
57
+ # the user block: the block continues after the lock ttl (without the mutual exclusion:
58
+ # another acquirer can obtain the expired lock, and the write lock release after the block
59
+ # deletes its lock);
60
+ #
32
61
  # @api private
33
62
  # @since 1.12.0
34
- class TimedLockIntermediateTimeoutError < ::Timeout::Error; end
63
+ # @version 1.21.0
64
+ class TimedLockIntermediateTimeoutError < Exception; end
35
65
 
36
- # @api pulic
66
+ # @api public
37
67
  # @since 1.0.0
38
68
  class TimedLockTimeoutError < Error; end
39
69
 
@@ -57,7 +87,7 @@ module RedisQueuedLocks
57
87
  # @since 1.13.0
58
88
  class ConfigNotFoundError < ConfigError; end
59
89
 
60
- # @api pub;ic
90
+ # @api public
61
91
  # @since 1.13.0
62
92
  class ConfigValidationError < ConfigError; end
63
93
  end