redis_queued_locks 1.18.0 → 1.20.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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/project-overview.md +65 -13
  3. data/.claude/rules/acquirer.md +29 -11
  4. data/.claude/rules/arguments.md +3 -2
  5. data/.claude/rules/git.md +3 -2
  6. data/.claude/rules/logic.md +4 -3
  7. data/.claude/rules/swarm.md +12 -7
  8. data/.claude/rules/tests.md +6 -0
  9. data/.claude/rules/type-checking.md +2 -1
  10. data/.claude/rules/visitors.md +1 -1
  11. data/.claude/skills/rql-code-review/SKILL.md +372 -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 +74 -0
  15. data/CLAUDE.md +4 -2
  16. data/README.md +338 -24
  17. data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rb +684 -128
  18. data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire.rb +3 -1
  19. data/lib/redis_queued_locks/acquirer/acquire_lock.rb +24 -3
  20. data/lib/redis_queued_locks/acquirer/clear_dead_requests.rb +145 -18
  21. data/lib/redis_queued_locks/acquirer/extend_lock_ttl.rb +71 -10
  22. data/lib/redis_queued_locks/acquirer/keys.rb +142 -7
  23. data/lib/redis_queued_locks/acquirer/lock_info.rb +10 -4
  24. data/lib/redis_queued_locks/acquirer/lock_series_poc.rb +26 -7
  25. data/lib/redis_queued_locks/acquirer/locks.rb +195 -83
  26. data/lib/redis_queued_locks/acquirer/queue_info.rb +12 -18
  27. data/lib/redis_queued_locks/acquirer/queues.rb +114 -50
  28. data/lib/redis_queued_locks/acquirer/reindex_locks.rb +140 -0
  29. data/lib/redis_queued_locks/acquirer/release_all_locks.rb +147 -58
  30. data/lib/redis_queued_locks/acquirer/release_lock.rb +52 -19
  31. data/lib/redis_queued_locks/acquirer/release_locks_of.rb +235 -66
  32. data/lib/redis_queued_locks/acquirer.rb +1 -0
  33. data/lib/redis_queued_locks/client.rb +116 -16
  34. data/lib/redis_queued_locks/config.rb +5 -1
  35. data/lib/redis_queued_locks/resource.rb +99 -2
  36. data/lib/redis_queued_locks/swarm/flush_zombies.rb +332 -61
  37. data/lib/redis_queued_locks/swarm/zombie_info.rb +203 -53
  38. data/lib/redis_queued_locks/swarm.rb +15 -4
  39. data/lib/redis_queued_locks/version.rb +2 -2
  40. data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rbs +107 -1
  41. data/sig/redis_queued_locks/acquirer/acquire_lock/yield_expire.rbs +1 -1
  42. data/sig/redis_queued_locks/acquirer/acquire_lock.rbs +2 -0
  43. data/sig/redis_queued_locks/acquirer/clear_dead_requests.rbs +10 -1
  44. data/sig/redis_queued_locks/acquirer/extend_lock_ttl.rbs +5 -2
  45. data/sig/redis_queued_locks/acquirer/keys.rbs +11 -1
  46. data/sig/redis_queued_locks/acquirer/locks.rbs +15 -6
  47. data/sig/redis_queued_locks/acquirer/queues.rbs +9 -2
  48. data/sig/redis_queued_locks/acquirer/reindex_locks.rbs +31 -0
  49. data/sig/redis_queued_locks/acquirer/release_all_locks.rbs +8 -1
  50. data/sig/redis_queued_locks/acquirer/release_lock.rbs +3 -1
  51. data/sig/redis_queued_locks/acquirer/release_locks_of.rbs +20 -1
  52. data/sig/redis_queued_locks/client.rbs +9 -0
  53. data/sig/redis_queued_locks/resource.rbs +6 -0
  54. data/sig/redis_queued_locks/swarm/flush_zombies.rbs +51 -1
  55. data/sig/redis_queued_locks/swarm/zombie_info.rbs +58 -7
  56. data/sig/vendor/redis_client.rbs +1 -0
  57. 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.20.0
6
6
  # rubocop:disable Metrics/ClassLength
7
7
  class RedisQueuedLocks::Client
8
8
  # @return [RedisClient]
@@ -174,6 +174,14 @@ class RedisQueuedLocks::Client
174
174
  # Lock's time to live (in milliseconds).
175
175
  # @option queue_ttl [Integer]
176
176
  # Lifetime of the acuier's lock request. In seconds.
177
+ # @option lock_index_grace_period [Integer]
178
+ # - The lock index score margin over the lock key lifetime (in milliseconds): the lock name
179
+ # stays in the lock index (`rql:index:locks`) for this period after the expiration of
180
+ # the lock keys;
181
+ # - should be greater than the client clock skew between your hosts plus the network latency;
182
+ # - used only with the enabled lock index (`config['lock_index_enabled']`, the client-wide
183
+ # mode: the lock index is maintained by all lock acquirements of the client);
184
+ # - pre-configured in `config['lock_index_grace_period']`;
177
185
  # @option timeout [Integer,NilClass]
178
186
  # Time period whe should try to acquire the lock (in seconds). Nil means "without timeout".
179
187
  # @option timed [Boolean]
@@ -331,12 +339,13 @@ class RedisQueuedLocks::Client
331
339
  #
332
340
  # @api public
333
341
  # @since 1.0.0
334
- # @version 1.18.0
342
+ # @version 1.20.0
335
343
  # rubocop:disable Metrics/MethodLength
336
344
  def lock(
337
345
  lock_name,
338
346
  ttl: config['default_lock_ttl'],
339
347
  queue_ttl: config['default_queue_ttl'],
348
+ lock_index_grace_period: config['lock_index_grace_period'],
340
349
  timeout: config['try_to_lock_timeout'],
341
350
  timed: config['is_timed_by_default'],
342
351
  retry_count: config['retry_count'],
@@ -373,6 +382,8 @@ class RedisQueuedLocks::Client
373
382
  ractor_id: RedisQueuedLocks::Resource.get_ractor_id,
374
383
  ttl:,
375
384
  queue_ttl:,
385
+ lock_index_enabled: config['lock_index_enabled'],
386
+ lock_index_grace_period:,
376
387
  timeout:,
377
388
  timed:,
378
389
  retry_count:,
@@ -407,11 +418,13 @@ class RedisQueuedLocks::Client
407
418
  # rubocop:disable all
408
419
  # @api public
409
420
  # @since 1.16.0
421
+ # @version 1.20.0
410
422
  def lock_series( # steep:ignore
411
423
  *lock_names,
412
424
  detailed_result: false,
413
425
  ttl: config['default_lock_ttl'],
414
426
  queue_ttl: config['default_queue_ttl'],
427
+ lock_index_grace_period: config['lock_index_grace_period'],
415
428
  timeout: config['try_to_lock_timeout'],
416
429
  timed: config['is_timed_by_default'],
417
430
  retry_count: config['retry_count'],
@@ -449,6 +462,8 @@ class RedisQueuedLocks::Client
449
462
  ractor_id: RedisQueuedLocks::Resource.get_ractor_id,
450
463
  ttl:,
451
464
  queue_ttl:,
465
+ lock_index_enabled: config['lock_index_enabled'],
466
+ lock_index_grace_period:,
452
467
  timeout:,
453
468
  timed:,
454
469
  retry_count:,
@@ -483,11 +498,13 @@ class RedisQueuedLocks::Client
483
498
  # rubocop:disable all
484
499
  # @api public
485
500
  # @since 1.16.0
501
+ # @version 1.20.0
486
502
  def lock_series!( # steep:ignore
487
503
  *lock_names,
488
504
  detailed_result: false,
489
505
  ttl: config['default_lock_ttl'],
490
506
  queue_ttl: config['default_queue_ttl'],
507
+ lock_index_grace_period: config['lock_index_grace_period'],
491
508
  timeout: config['try_to_lock_timeout'],
492
509
  timed: config['is_timed_by_default'],
493
510
  retry_count: config['retry_count'],
@@ -519,6 +536,7 @@ class RedisQueuedLocks::Client
519
536
  detailed_result:,
520
537
  ttl:,
521
538
  queue_ttl:,
539
+ lock_index_grace_period:,
522
540
  timeout:,
523
541
  timed:,
524
542
  retry_count:,
@@ -552,11 +570,12 @@ class RedisQueuedLocks::Client
552
570
  #
553
571
  # @api public
554
572
  # @since 1.0.0
555
- # @version 1.18.0
573
+ # @version 1.20.0
556
574
  def lock!(
557
575
  lock_name,
558
576
  ttl: config['default_lock_ttl'],
559
577
  queue_ttl: config['default_queue_ttl'],
578
+ lock_index_grace_period: config['lock_index_grace_period'],
560
579
  timeout: config['try_to_lock_timeout'],
561
580
  timed: config['is_timed_by_default'],
562
581
  retry_count: config['retry_count'],
@@ -587,6 +606,7 @@ class RedisQueuedLocks::Client
587
606
  lock_name,
588
607
  ttl:,
589
608
  queue_ttl:,
609
+ lock_index_grace_period:,
590
610
  timeout:,
591
611
  timed:,
592
612
  retry_count:,
@@ -645,7 +665,7 @@ class RedisQueuedLocks::Client
645
665
  #
646
666
  # @api public
647
667
  # @since 1.0.0
648
- # @version 1.18.0
668
+ # @version 1.20.0
649
669
  def unlock(
650
670
  lock_name,
651
671
  logger: config['logger'],
@@ -663,6 +683,7 @@ class RedisQueuedLocks::Client
663
683
  RedisQueuedLocks::Acquirer::ReleaseLock.release_lock(
664
684
  redis_client,
665
685
  lock_name,
686
+ config['lock_index_enabled'],
666
687
  instrumenter,
667
688
  logger,
668
689
  log_sampling_enabled,
@@ -677,14 +698,20 @@ class RedisQueuedLocks::Client
677
698
  end
678
699
  alias_method :release_lock, :unlock
679
700
 
680
- # Releases the read lock of the current acquirer only: other read locks, the write lock and
681
- # lock requests are not affected (`#unlock` releases the lock entirely). It is suitable for
682
- # read locks obtained without a block of code.
701
+ # Releases the read lock of the current acquirer (or of the custom acquirer passed via
702
+ # `acquirer_id:`) only: other read locks, the write lock and lock requests are not affected
703
+ # (`#unlock` releases the lock entirely). It is suitable for read locks obtained without
704
+ # a block of code.
683
705
  #
684
706
  # @param lock_name [String] The lock name whose read lock should be released.
707
+ # @option acquirer_id [String,NilClass]
708
+ # - The acquirer whose read lock should be released (`nil` by default);
709
+ # - `nil` => the current acquirer is used (calculated from `identity:`, see `#current_acquirer_id`);
710
+ # - can be extracted from the lock data (`#lock_info`, `#locks_info`, `#queue_info`, `#queues_info`);
685
711
  # @option identity [String]
686
712
  # Unique acquirer identity (the same as for `#lock`) that is used to calculate the current
687
- # acquirer id (see `#current_acquirer_id`).
713
+ # acquirer id (see `#current_acquirer_id`). Ignored when `acquirer_id:` is passed: the acquirer id
714
+ # already contains its own identity.
688
715
  # @option logger [::Logger,#debug]
689
716
  # @option instrumenter [#notify]
690
717
  # @option instrument [NilClass,Any]
@@ -707,10 +734,18 @@ class RedisQueuedLocks::Client
707
734
  # }
708
735
  # }
709
736
  #
737
+ # @example Release the read lock of the current acquirer:
738
+ # client.unlock_read('report')
739
+ #
740
+ # @example Release the read lock of a different acquirer:
741
+ # client.unlock_read('report', acquirer_id: "rql:acq:62681/2016/2024/2032/b30ec5e4bea10512")
742
+ #
710
743
  # @api public
711
744
  # @since 1.18.0
745
+ # @version 1.19.0
712
746
  def unlock_read(
713
747
  lock_name,
748
+ acquirer_id: nil,
714
749
  identity: uniq_identity,
715
750
  logger: config['logger'],
716
751
  instrumenter: config['instrumenter'],
@@ -727,7 +762,7 @@ class RedisQueuedLocks::Client
727
762
  RedisQueuedLocks::Acquirer::ReleaseReadLock.release_read_lock(
728
763
  redis_client,
729
764
  lock_name,
730
- current_acquirer_id(identity:),
765
+ acquirer_id || current_acquirer_id(identity:),
731
766
  instrumenter,
732
767
  logger,
733
768
  instrument,
@@ -909,7 +944,7 @@ class RedisQueuedLocks::Client
909
944
  #
910
945
  # @api public
911
946
  # @since 1.0.0
912
- # @version 1.18.0
947
+ # @version 1.20.0
913
948
  def extend_lock_ttl(
914
949
  lock_name,
915
950
  milliseconds,
@@ -935,6 +970,7 @@ class RedisQueuedLocks::Client
935
970
  read_write_mode,
936
971
  all_read_locks,
937
972
  current_acquirer_id(identity:),
973
+ config['lock_index_enabled'],
938
974
  logger,
939
975
  instrumenter,
940
976
  instrument,
@@ -953,6 +989,7 @@ class RedisQueuedLocks::Client
953
989
  # Returns:
954
990
  # - :rel_time - (milliseconds) - time spent to release all locks and queues;
955
991
  # - :rel_key_cnt - (integer) - the number of released redis keys (queus+locks);
992
+ # Locks are found via the keyspace SCAN or via the lock index (`config['lock_index_enabled']`).
956
993
  #
957
994
  # @option batch_size [Integer]
958
995
  # @option logger [::Logger,#debug]
@@ -971,7 +1008,7 @@ class RedisQueuedLocks::Client
971
1008
  #
972
1009
  # @api public
973
1010
  # @since 1.0.0
974
- # @version 1.6.0
1011
+ # @version 1.20.0
975
1012
  def clear_locks(
976
1013
  batch_size: config['lock_release_batch_size'],
977
1014
  logger: config['logger'],
@@ -989,6 +1026,7 @@ class RedisQueuedLocks::Client
989
1026
  RedisQueuedLocks::Acquirer::ReleaseAllLocks.release_all_locks(
990
1027
  redis_client,
991
1028
  batch_size,
1029
+ config['lock_index_enabled'],
992
1030
  logger,
993
1031
  instrumenter,
994
1032
  instrument,
@@ -1010,6 +1048,8 @@ class RedisQueuedLocks::Client
1010
1048
  # current puma request thread is killed by Rack::Timeout and you need to cleanup all zombie RQL
1011
1049
  # locks and lock reuqests obtained during the request processing).
1012
1050
  #
1051
+ # Locks are found via the keyspace SCAN or via the lock index (`config['lock_index_enabled']`).
1052
+ #
1013
1053
  # Identifiers can be extracted via:
1014
1054
  # - `#current_host_id`
1015
1055
  # - `#current_acquirer_id`
@@ -1057,6 +1097,7 @@ class RedisQueuedLocks::Client
1057
1097
  #
1058
1098
  # @api public
1059
1099
  # @since 1.14.0
1100
+ # @version 1.20.0
1060
1101
  def clear_locks_of(
1061
1102
  host_id:,
1062
1103
  acquirer_id:,
@@ -1080,6 +1121,7 @@ class RedisQueuedLocks::Client
1080
1121
  redis_client,
1081
1122
  lock_scan_size,
1082
1123
  queue_scan_size,
1124
+ config['lock_index_enabled'],
1083
1125
  logger,
1084
1126
  instrumenter,
1085
1127
  instrument,
@@ -1169,10 +1211,16 @@ class RedisQueuedLocks::Client
1169
1211
  # at the moment of Redis'es SCAN;
1170
1212
  # @return [Set<String>,Set<Hash<Symbol,Any>>]
1171
1213
  #
1214
+ # NOTE: locks are found via the keyspace SCAN or via the lock index (ZSCAN, the enabled
1215
+ # `config['lock_index_enabled']`: `scan_size` is the number of locks in one batch);
1216
+ #
1172
1217
  # @api public
1173
1218
  # @since 1.0.0
1219
+ # @version 1.20.0
1174
1220
  def locks(scan_size: config['key_extraction_batch_size'], with_info: false)
1175
- RedisQueuedLocks::Acquirer::Locks.locks(redis_client, scan_size:, with_info:)
1221
+ RedisQueuedLocks::Acquirer::Locks.locks(
1222
+ redis_client, scan_size:, with_info:, lock_index_enabled: config['lock_index_enabled']
1223
+ )
1176
1224
  end
1177
1225
 
1178
1226
  # Extracts lock keys with their info. See #locks(with_info: true) for details.
@@ -1200,10 +1248,16 @@ class RedisQueuedLocks::Client
1200
1248
  # at the moment of Redis'es SCAN;
1201
1249
  # @return [Set<String>,String<Hash<Symbol,Any>>]
1202
1250
  #
1251
+ # NOTE: lock queues are found via the keyspace SCAN or via the lock index (ZSCAN, the enabled
1252
+ # `config['lock_index_enabled']`: `scan_size` is the number of locks in one batch);
1253
+ #
1203
1254
  # @api public
1204
1255
  # @since 1.0.0
1256
+ # @version 1.20.0
1205
1257
  def queues(scan_size: config['key_extraction_batch_size'], with_info: false)
1206
- RedisQueuedLocks::Acquirer::Queues.queues(redis_client, scan_size:, with_info:)
1258
+ RedisQueuedLocks::Acquirer::Queues.queues(
1259
+ redis_client, scan_size:, with_info:, lock_index_enabled: config['lock_index_enabled']
1260
+ )
1207
1261
  end
1208
1262
 
1209
1263
  # Extracts lock queues with their info. See #queues(with_info: true) for details.
@@ -1219,11 +1273,17 @@ class RedisQueuedLocks::Client
1219
1273
 
1220
1274
  # @option scan_size [Integer]
1221
1275
  # @return [Set<String>]
1276
+ # - the disabled lock index: all the RQL keys (`rql:lock*`) found via the keyspace SCAN;
1277
+ # - the enabled lock index (`config['lock_index_enabled']`): the existing keys of the locks of
1278
+ # the lock index (ZSCAN, `scan_size` is the number of locks in one batch);
1222
1279
  #
1223
1280
  # @api public
1224
1281
  # @since 1.0.0
1282
+ # @version 1.20.0
1225
1283
  def keys(scan_size: config['key_extraction_batch_size'])
1226
- RedisQueuedLocks::Acquirer::Keys.keys(redis_client, scan_size:)
1284
+ RedisQueuedLocks::Acquirer::Keys.keys(
1285
+ redis_client, scan_size:, lock_index_enabled: config['lock_index_enabled']
1286
+ )
1227
1287
  end
1228
1288
 
1229
1289
  # @option dead_ttl [Integer]
@@ -1231,7 +1291,8 @@ class RedisQueuedLocks::Client
1231
1291
  # considered as dead;
1232
1292
  # - `config['dead_request_ttl']` is used by default;
1233
1293
  # @option scan_size [Integer]
1234
- # - the batch of scanned keys for Redis'es SCAN command;
1294
+ # - the batch of scanned keys for Redis'es SCAN command (or the number of locks in one batch of
1295
+ # the lock index ZSCAN pass: the enabled `config['lock_index_enabled']`);
1235
1296
  # - `config['lock_release_batch_size']` is used by default;
1236
1297
  # @option logger [::Logger,#debug]
1237
1298
  # @option instrumenter [#notify]
@@ -1249,7 +1310,7 @@ class RedisQueuedLocks::Client
1249
1310
  #
1250
1311
  # @api public
1251
1312
  # @since 1.0.0
1252
- # @version 1.6.0
1313
+ # @version 1.20.0
1253
1314
  def clear_dead_requests(
1254
1315
  dead_ttl: config['dead_request_ttl'],
1255
1316
  scan_size: config['lock_release_batch_size'],
@@ -1269,6 +1330,7 @@ class RedisQueuedLocks::Client
1269
1330
  redis_client,
1270
1331
  scan_size,
1271
1332
  dead_ttl,
1333
+ config['lock_index_enabled'],
1272
1334
  logger,
1273
1335
  instrumenter,
1274
1336
  instrument,
@@ -1282,5 +1344,43 @@ class RedisQueuedLocks::Client
1282
1344
  instr_sample_this
1283
1345
  )
1284
1346
  end
1347
+
1348
+ # Adds the lock keys without the lock index entry (keys of older RQL versions, keys created while
1349
+ # the lock index was disabled) to the lock index (`rql:index:locks`). Invoke it once after the lock
1350
+ # index is enabled (`config['lock_index_enabled']`) on all your processes: lock-iterating
1351
+ # operations (the swarm, `#locks`, `#queues`, `#keys`, `#clear_locks`, ...) find locks via
1352
+ # the lock index only.
1353
+ # - works regardless of `config['lock_index_enabled']` (the lock index is not used and not
1354
+ # maintained while it is disabled);
1355
+ # - one SCAN pass over the RQL keys;
1356
+ # - lock queues without TTL (older RQL versions) get `queue_ttl` as their TTL;
1357
+ # - idempotent: the lock index scores are never decreased, already indexed keys stay as is;
1358
+ #
1359
+ # @option scan_size [Integer]
1360
+ # - the batch of scanned keys for Redis'es SCAN command;
1361
+ # - `config['lock_release_batch_size']` is used by default;
1362
+ # @option queue_ttl [Integer]
1363
+ # - TTL of the lock queues without TTL (in seconds);
1364
+ # - `config['default_queue_ttl']` is used by default;
1365
+ # @option lock_index_grace_period [Integer]
1366
+ # - the lock index score margin over the key lifetime (in milliseconds);
1367
+ # - `config['lock_index_grace_period']` is used by default;
1368
+ # @return [Hash<Symbol,Boolean|Hash<Symbol,Integer>>]
1369
+ # Format: { ok: true, result: { idx_key_cnt: Integer } } (the number of indexed keys)
1370
+ #
1371
+ # @api public
1372
+ # @since 1.20.0
1373
+ def reindex_locks(
1374
+ scan_size: config['lock_release_batch_size'],
1375
+ queue_ttl: config['default_queue_ttl'],
1376
+ lock_index_grace_period: config['lock_index_grace_period']
1377
+ )
1378
+ RedisQueuedLocks::Acquirer::ReindexLocks.reindex_locks(
1379
+ redis_client,
1380
+ scan_size,
1381
+ queue_ttl,
1382
+ lock_index_grace_period
1383
+ )
1384
+ end
1285
1385
  end
1286
1386
  # rubocop:enable Metrics/ClassLength
@@ -2,7 +2,7 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.13.0
5
- # @version 1.15.1
5
+ # @version 1.20.0
6
6
  # rubocop:disable Metrics/ClassLength
7
7
  class RedisQueuedLocks::Config
8
8
  require_relative 'config/dsl'
@@ -27,6 +27,8 @@ class RedisQueuedLocks::Config
27
27
  setting('logger', RedisQueuedLocks::Logging::VoidLogger)
28
28
  setting('log_lock_try', false)
29
29
  setting('dead_request_ttl', 1 * 24 * 60 * 60 * 1000) # NOTE: 1 day in milliseconds)
30
+ setting('lock_index_enabled', false)
31
+ setting('lock_index_grace_period', 60_000) # NOTE: in milliseconds)
30
32
  setting('is_timed_by_default', false)
31
33
  setting('default_conflict_strategy', :wait_for_lock)
32
34
  setting('default_access_strategy', :queued)
@@ -90,6 +92,8 @@ class RedisQueuedLocks::Config
90
92
  validate('logger') { |val| RedisQueuedLocks::Logging.valid_interface?(val) }
91
93
  validate('log_lock_try') { |val| val == true || val == false }
92
94
  validate('dead_request_ttl') { |val| val.is_a?(Integer) && val > 0 }
95
+ validate('lock_index_enabled') { |val| val == true || val == false }
96
+ validate('lock_index_grace_period') { |val| val.is_a?(Integer) && val >= 0 }
93
97
  validate('is_timed_by_default') { |val| val == true || val == false }
94
98
  validate('log_sampler') { |val| RedisQueuedLocks::Logging.valid_sampler?(val) }
95
99
  validate('log_sampling_enabled') { |val| val == true || val == false }
@@ -47,18 +47,49 @@ module RedisQueuedLocks::Resource
47
47
  # @since 1.18.0
48
48
  READ_LOCK_PATTERN = 'rql:lock_reader:*'
49
49
 
50
+ # Write and read lock queues in one SCAN pass (the keyspace lookup: the disabled lock index).
51
+ # NOTE:
52
+ # - the pattern matches other keys too when the lock name contains `queue:` (for example,
53
+ # `rql:lock_readers:my_queue:a`), so callers check the key family (`lock_key_family`);
54
+ # @return [String]
55
+ #
56
+ # @api private
57
+ # @since 1.20.0
58
+ ALL_LOCK_QUEUES_PATTERN = 'rql:lock_*queue:*'
59
+
50
60
  # @return [String]
51
61
  #
52
62
  # @api private
53
63
  # @since 1.9.0
54
64
  SWARM_KEY = 'rql:swarm:hsts'
55
65
 
66
+ # Lock index: a sorted set of lock names (members) where the score is the upper bound of the
67
+ # lifetime of all keys of the lock (client time, epoch milliseconds + the index grace period).
68
+ # It replaces the full keyspace SCAN for the operations that iterate over locks (lock names are
69
+ # taken from the index, lock keys are derived from the lock names).
70
+ # NOTE:
71
+ # - it is used and maintained only with the enabled lock index (`config['lock_index_enabled']`,
72
+ # disabled by default: lock-iterating operations scan the keyspace (`KEY_PATTERN`) then);
73
+ # - the key is not matched by `KEY_PATTERN` (`rql:index:` vs `rql:lock`) and can not collide
74
+ # with lock keys (`rql:lock:<name>`, `rql:lock_<kind>:<name>`);
75
+ # - the index is a superset of the existing locks: a lock name is added (or its score is
76
+ # increased) by every command that creates or extends a lock key (in the same pipeline or
77
+ # transaction, after the key command) and it is removed only when its score is reached
78
+ # (`ZREMRANGEBYSCORE`: all the keys of the lock are expired by that time) or in the same
79
+ # transaction with all the keys of the lock (`#unlock`, `#clear_locks`);
80
+ # @return [String]
81
+ #
82
+ # @api private
83
+ # @since 1.20.0
84
+ LOCK_INDEX_KEY = 'rql:index:locks'
85
+
56
86
  # @return [Integer] Redis time error (in milliseconds).
57
87
  #
58
88
  # @api private
59
89
  # @since 1.3.0
60
90
  REDIS_TIMESHIFT_ERROR = 2
61
91
 
92
+ # rubocop:disable Metrics/ClassLength
62
93
  class << self
63
94
  # Returns 16-byte unique identifier. It is used for uniquely
64
95
  # identify current process between different nodes/pods of your application
@@ -143,8 +174,9 @@ module RedisQueuedLocks::Resource
143
174
  end
144
175
 
145
176
  # Read lock data of the concrete acquirer: a hash with the same structure as the write lock
146
- # (acquirer, host, timestamp, ttl, meta, reentrant lock data). It is a data carrier only:
147
- # the readers registry is the source of truth for the read lock existence.
177
+ # (timestamp, ttl, meta, reentrant lock data; acquirer and host are derived from the acquirer
178
+ # id and are not stored). It is a data carrier only: the readers registry is the source of
179
+ # truth for the read lock existence.
148
180
  #
149
181
  # @param lock_name [String]
150
182
  # @param acquirer_id [String]
@@ -179,6 +211,70 @@ module RedisQueuedLocks::Resource
179
211
  end
180
212
  end
181
213
 
214
+ # Family of the given RQL key: one SCAN pass over `KEY_PATTERN` (or `ALL_LOCK_QUEUES_PATTERN`)
215
+ # returns keys of several families, so callers keep the keys of the families they need.
216
+ #
217
+ # @param key [String]
218
+ # @return [Symbol,NilClass]
219
+ # - `:lock` - write lock (`rql:lock:*`);
220
+ # - `:lock_queue` - write lock requests (`rql:lock_queue:*`);
221
+ # - `:lock_readers` - readers registry (`rql:lock_readers:*`);
222
+ # - `:read_lock_queue` - read lock requests (`rql:lock_read_queue:*`);
223
+ # - `:read_lock` - read lock data (`rql:lock_reader:*`);
224
+ # - `nil` - any other key (for example, `rql:lockX` matched by `KEY_PATTERN`);
225
+ #
226
+ # @api private
227
+ # @since 1.20.0
228
+ def lock_key_family(key)
229
+ # NOTE:
230
+ # - `start_with?` instead of regexp `when`-matchers: no MatchData allocations;
231
+ # - prefixes are unambiguous: `rql:lock_reader:` is not a prefix of `rql:lock_readers:`;
232
+ case
233
+ when key.start_with?('rql:lock:') then :lock
234
+ when key.start_with?('rql:lock_queue:') then :lock_queue
235
+ when key.start_with?('rql:lock_readers:') then :lock_readers
236
+ when key.start_with?('rql:lock_read_queue:') then :read_lock_queue
237
+ when key.start_with?('rql:lock_reader:') then :read_lock
238
+ end
239
+ end
240
+
241
+ # Lock name of the given RQL key (the member of the lock index).
242
+ #
243
+ # @param key [String]
244
+ # @param key_family [Symbol,NilClass] Family of the key (see `lock_key_family`).
245
+ # @return [String,NilClass]
246
+ # - the lock name for the write lock, the readers registry and the lock queues (both modes);
247
+ # - `nil` for the read lock data (`:read_lock`: the lock name can not be separated from
248
+ # the acquirer id unambiguously) and for any other key;
249
+ #
250
+ # @api private
251
+ # @since 1.20.0
252
+ def lock_name_from_key(key, key_family)
253
+ case key_family
254
+ when :lock then key.delete_prefix('rql:lock:')
255
+ when :lock_queue then key.delete_prefix('rql:lock_queue:')
256
+ when :lock_readers then key.delete_prefix('rql:lock_readers:')
257
+ when :read_lock_queue then key.delete_prefix('rql:lock_read_queue:')
258
+ end
259
+ end
260
+
261
+ # Client time for the lock index scores (see `LOCK_INDEX_KEY`).
262
+ #
263
+ # @return [Integer] Epoch, in milliseconds.
264
+ #
265
+ # @api private
266
+ # @since 1.20.0
267
+ def lock_index_time
268
+ # NOTE:
269
+ # - an Integer of milliseconds without Time/Float objects;
270
+ # - CLOCK_REALTIME (epoch), not CLOCK_MONOTONIC: the lock index scores are absolute times
271
+ # stored in the shared Redis and compared by other hosts (pruning, expired names),
272
+ # while monotonic clocks have a per-host origin (boot time) and are not comparable
273
+ # between hosts; the wall clock skew between hosts is covered by
274
+ # `lock_index_grace_period`;
275
+ ::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond)
276
+ end
277
+
182
278
  # @param lock_readers_key [String]
183
279
  # @return [String]
184
280
  #
@@ -347,5 +443,6 @@ module RedisQueuedLocks::Resource
347
443
  # steep:ignore:end
348
444
  end
349
445
  end
446
+ # rubocop:enable Metrics/ClassLength
350
447
  end
351
448
  # rubocop:enable Metrics/ModuleLength