redis_queued_locks 1.19.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 +62 -11
  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 +68 -0
  15. data/CLAUDE.md +4 -2
  16. data/README.md +257 -22
  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 +97 -11
  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 +8 -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,
@@ -923,7 +944,7 @@ class RedisQueuedLocks::Client
923
944
  #
924
945
  # @api public
925
946
  # @since 1.0.0
926
- # @version 1.18.0
947
+ # @version 1.20.0
927
948
  def extend_lock_ttl(
928
949
  lock_name,
929
950
  milliseconds,
@@ -949,6 +970,7 @@ class RedisQueuedLocks::Client
949
970
  read_write_mode,
950
971
  all_read_locks,
951
972
  current_acquirer_id(identity:),
973
+ config['lock_index_enabled'],
952
974
  logger,
953
975
  instrumenter,
954
976
  instrument,
@@ -967,6 +989,7 @@ class RedisQueuedLocks::Client
967
989
  # Returns:
968
990
  # - :rel_time - (milliseconds) - time spent to release all locks and queues;
969
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']`).
970
993
  #
971
994
  # @option batch_size [Integer]
972
995
  # @option logger [::Logger,#debug]
@@ -985,7 +1008,7 @@ class RedisQueuedLocks::Client
985
1008
  #
986
1009
  # @api public
987
1010
  # @since 1.0.0
988
- # @version 1.6.0
1011
+ # @version 1.20.0
989
1012
  def clear_locks(
990
1013
  batch_size: config['lock_release_batch_size'],
991
1014
  logger: config['logger'],
@@ -1003,6 +1026,7 @@ class RedisQueuedLocks::Client
1003
1026
  RedisQueuedLocks::Acquirer::ReleaseAllLocks.release_all_locks(
1004
1027
  redis_client,
1005
1028
  batch_size,
1029
+ config['lock_index_enabled'],
1006
1030
  logger,
1007
1031
  instrumenter,
1008
1032
  instrument,
@@ -1024,6 +1048,8 @@ class RedisQueuedLocks::Client
1024
1048
  # current puma request thread is killed by Rack::Timeout and you need to cleanup all zombie RQL
1025
1049
  # locks and lock reuqests obtained during the request processing).
1026
1050
  #
1051
+ # Locks are found via the keyspace SCAN or via the lock index (`config['lock_index_enabled']`).
1052
+ #
1027
1053
  # Identifiers can be extracted via:
1028
1054
  # - `#current_host_id`
1029
1055
  # - `#current_acquirer_id`
@@ -1071,6 +1097,7 @@ class RedisQueuedLocks::Client
1071
1097
  #
1072
1098
  # @api public
1073
1099
  # @since 1.14.0
1100
+ # @version 1.20.0
1074
1101
  def clear_locks_of(
1075
1102
  host_id:,
1076
1103
  acquirer_id:,
@@ -1094,6 +1121,7 @@ class RedisQueuedLocks::Client
1094
1121
  redis_client,
1095
1122
  lock_scan_size,
1096
1123
  queue_scan_size,
1124
+ config['lock_index_enabled'],
1097
1125
  logger,
1098
1126
  instrumenter,
1099
1127
  instrument,
@@ -1183,10 +1211,16 @@ class RedisQueuedLocks::Client
1183
1211
  # at the moment of Redis'es SCAN;
1184
1212
  # @return [Set<String>,Set<Hash<Symbol,Any>>]
1185
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
+ #
1186
1217
  # @api public
1187
1218
  # @since 1.0.0
1219
+ # @version 1.20.0
1188
1220
  def locks(scan_size: config['key_extraction_batch_size'], with_info: false)
1189
- 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
+ )
1190
1224
  end
1191
1225
 
1192
1226
  # Extracts lock keys with their info. See #locks(with_info: true) for details.
@@ -1214,10 +1248,16 @@ class RedisQueuedLocks::Client
1214
1248
  # at the moment of Redis'es SCAN;
1215
1249
  # @return [Set<String>,String<Hash<Symbol,Any>>]
1216
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
+ #
1217
1254
  # @api public
1218
1255
  # @since 1.0.0
1256
+ # @version 1.20.0
1219
1257
  def queues(scan_size: config['key_extraction_batch_size'], with_info: false)
1220
- 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
+ )
1221
1261
  end
1222
1262
 
1223
1263
  # Extracts lock queues with their info. See #queues(with_info: true) for details.
@@ -1233,11 +1273,17 @@ class RedisQueuedLocks::Client
1233
1273
 
1234
1274
  # @option scan_size [Integer]
1235
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);
1236
1279
  #
1237
1280
  # @api public
1238
1281
  # @since 1.0.0
1282
+ # @version 1.20.0
1239
1283
  def keys(scan_size: config['key_extraction_batch_size'])
1240
- 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
+ )
1241
1287
  end
1242
1288
 
1243
1289
  # @option dead_ttl [Integer]
@@ -1245,7 +1291,8 @@ class RedisQueuedLocks::Client
1245
1291
  # considered as dead;
1246
1292
  # - `config['dead_request_ttl']` is used by default;
1247
1293
  # @option scan_size [Integer]
1248
- # - 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']`);
1249
1296
  # - `config['lock_release_batch_size']` is used by default;
1250
1297
  # @option logger [::Logger,#debug]
1251
1298
  # @option instrumenter [#notify]
@@ -1263,7 +1310,7 @@ class RedisQueuedLocks::Client
1263
1310
  #
1264
1311
  # @api public
1265
1312
  # @since 1.0.0
1266
- # @version 1.6.0
1313
+ # @version 1.20.0
1267
1314
  def clear_dead_requests(
1268
1315
  dead_ttl: config['dead_request_ttl'],
1269
1316
  scan_size: config['lock_release_batch_size'],
@@ -1283,6 +1330,7 @@ class RedisQueuedLocks::Client
1283
1330
  redis_client,
1284
1331
  scan_size,
1285
1332
  dead_ttl,
1333
+ config['lock_index_enabled'],
1286
1334
  logger,
1287
1335
  instrumenter,
1288
1336
  instrument,
@@ -1296,5 +1344,43 @@ class RedisQueuedLocks::Client
1296
1344
  instr_sample_this
1297
1345
  )
1298
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
1299
1385
  end
1300
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