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.
- checksums.yaml +4 -4
- data/.claude/project-overview.md +62 -11
- data/.claude/rules/acquirer.md +29 -11
- data/.claude/rules/arguments.md +3 -2
- data/.claude/rules/git.md +3 -2
- data/.claude/rules/logic.md +4 -3
- data/.claude/rules/swarm.md +12 -7
- data/.claude/rules/tests.md +6 -0
- data/.claude/rules/type-checking.md +2 -1
- data/.claude/rules/visitors.md +1 -1
- data/.claude/skills/rql-code-review/SKILL.md +372 -0
- data/.claude/skills/rql-code-review/templates/report.en.md +31 -0
- data/.claude/skills/rql-code-review/templates/report.ru.md +31 -0
- data/CHANGELOG.md +68 -0
- data/CLAUDE.md +4 -2
- data/README.md +257 -22
- data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rb +684 -128
- data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire.rb +3 -1
- data/lib/redis_queued_locks/acquirer/acquire_lock.rb +24 -3
- data/lib/redis_queued_locks/acquirer/clear_dead_requests.rb +145 -18
- data/lib/redis_queued_locks/acquirer/extend_lock_ttl.rb +71 -10
- data/lib/redis_queued_locks/acquirer/keys.rb +142 -7
- data/lib/redis_queued_locks/acquirer/lock_info.rb +10 -4
- data/lib/redis_queued_locks/acquirer/lock_series_poc.rb +26 -7
- data/lib/redis_queued_locks/acquirer/locks.rb +195 -83
- data/lib/redis_queued_locks/acquirer/queue_info.rb +12 -18
- data/lib/redis_queued_locks/acquirer/queues.rb +114 -50
- data/lib/redis_queued_locks/acquirer/reindex_locks.rb +140 -0
- data/lib/redis_queued_locks/acquirer/release_all_locks.rb +147 -58
- data/lib/redis_queued_locks/acquirer/release_lock.rb +52 -19
- data/lib/redis_queued_locks/acquirer/release_locks_of.rb +235 -66
- data/lib/redis_queued_locks/acquirer.rb +1 -0
- data/lib/redis_queued_locks/client.rb +97 -11
- data/lib/redis_queued_locks/config.rb +5 -1
- data/lib/redis_queued_locks/resource.rb +99 -2
- data/lib/redis_queued_locks/swarm/flush_zombies.rb +332 -61
- data/lib/redis_queued_locks/swarm/zombie_info.rb +203 -53
- data/lib/redis_queued_locks/swarm.rb +15 -4
- data/lib/redis_queued_locks/version.rb +2 -2
- data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rbs +107 -1
- data/sig/redis_queued_locks/acquirer/acquire_lock/yield_expire.rbs +1 -1
- data/sig/redis_queued_locks/acquirer/acquire_lock.rbs +2 -0
- data/sig/redis_queued_locks/acquirer/clear_dead_requests.rbs +10 -1
- data/sig/redis_queued_locks/acquirer/extend_lock_ttl.rbs +5 -2
- data/sig/redis_queued_locks/acquirer/keys.rbs +11 -1
- data/sig/redis_queued_locks/acquirer/locks.rbs +15 -6
- data/sig/redis_queued_locks/acquirer/queues.rbs +9 -2
- data/sig/redis_queued_locks/acquirer/reindex_locks.rbs +31 -0
- data/sig/redis_queued_locks/acquirer/release_all_locks.rbs +8 -1
- data/sig/redis_queued_locks/acquirer/release_lock.rbs +3 -1
- data/sig/redis_queued_locks/acquirer/release_locks_of.rbs +20 -1
- data/sig/redis_queued_locks/client.rbs +8 -0
- data/sig/redis_queued_locks/resource.rbs +6 -0
- data/sig/redis_queued_locks/swarm/flush_zombies.rbs +51 -1
- data/sig/redis_queued_locks/swarm/zombie_info.rbs +58 -7
- data/sig/vendor/redis_client.rbs +1 -0
- metadata +6 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# @api public
|
|
4
4
|
# @since 1.0.0
|
|
5
|
-
# @version 1.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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(
|
|
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(
|
|
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(
|
|
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.
|
|
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.
|
|
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
|
-
# (
|
|
147
|
-
# the readers registry is the source of
|
|
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
|