redis_queued_locks 1.21.0 → 1.22.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 +216 -25
- data/.claude/rules/acquirer.md +6 -5
- data/.claude/rules/arguments.md +2 -2
- data/.claude/rules/changelog.md +27 -0
- data/.claude/rules/git.md +1 -1
- data/.claude/rules/logic.md +10 -12
- data/.claude/rules/swarm.md +3 -4
- data/.claude/rules/tests.md +6 -6
- data/.claude/rules/type-checking.md +13 -14
- data/.claude/rules/visitors.md +5 -5
- data/.claude/skills/rql-code-review/SKILL.md +112 -35
- data/CHANGELOG.md +98 -244
- data/CLAUDE.md +9 -8
- data/README.md +275 -85
- data/Rakefile +11 -0
- data/lib/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rb +28 -12
- data/lib/redis_queued_locks/acquirer/acquire_lock/log_visitor.rb +38 -16
- data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock/log_visitor.rb +92 -15
- data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rb +430 -63
- data/lib/redis_queued_locks/acquirer/acquire_lock.rb +72 -22
- data/lib/redis_queued_locks/acquirer/clear_dead_requests.rb +240 -45
- data/lib/redis_queued_locks/acquirer/extend_lock_ttl.rb +3 -1
- data/lib/redis_queued_locks/acquirer/keys.rb +6 -2
- data/lib/redis_queued_locks/acquirer/lock_series_poc/instr_visitor.rb +10 -5
- data/lib/redis_queued_locks/acquirer/lock_series_poc/log_visitor.rb +18 -9
- data/lib/redis_queued_locks/acquirer/lock_series_poc.rb +52 -23
- data/lib/redis_queued_locks/acquirer/locks.rb +6 -2
- data/lib/redis_queued_locks/acquirer/queue_info.rb +30 -7
- data/lib/redis_queued_locks/acquirer/queues.rb +17 -6
- data/lib/redis_queued_locks/acquirer/release_all_locks.rb +6 -2
- data/lib/redis_queued_locks/acquirer/release_locks_of.rb +6 -2
- data/lib/redis_queued_locks/client.rb +66 -24
- data/lib/redis_queued_locks/config.rb +93 -1
- data/lib/redis_queued_locks/resource.rb +124 -9
- data/lib/redis_queued_locks/swarm/swarm_element/threaded.rb +6 -5
- data/lib/redis_queued_locks/swarm/zombie_info.rb +6 -2
- data/lib/redis_queued_locks/version.rb +2 -2
- data/rbs_collection.lock.yaml +4 -0
- data/rbs_collection.yaml +2 -1
- data/sig/manifest.yml +1 -0
- data/sig/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue/log_visitor.rbs +2 -2
- data/sig/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue.rbs +2 -2
- data/sig/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rbs +12 -6
- data/sig/redis_queued_locks/acquirer/acquire_lock/log_visitor.rbs +21 -14
- data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock/log_visitor.rbs +61 -40
- data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rbs +59 -11
- data/sig/redis_queued_locks/acquirer/acquire_lock/{yield_with_expire → yield_expire}/log_visitor.rbs +4 -4
- data/sig/redis_queued_locks/acquirer/acquire_lock/yield_expire.rbs +2 -2
- data/sig/redis_queued_locks/acquirer/acquire_lock.rbs +5 -3
- data/sig/redis_queued_locks/acquirer/clear_dead_requests.rbs +14 -1
- data/sig/redis_queued_locks/acquirer/extend_lock_ttl.rbs +2 -2
- data/sig/redis_queued_locks/acquirer/queues.rbs +1 -1
- data/sig/redis_queued_locks/client.rbs +10 -7
- data/sig/redis_queued_locks/config.rbs +54 -1
- data/sig/redis_queued_locks/instrument/sampler.rbs +3 -1
- data/sig/redis_queued_locks/instrument/void_notifier.rbs +3 -1
- data/sig/redis_queued_locks/logging/sampler.rbs +3 -1
- data/sig/redis_queued_locks/resource.rbs +22 -12
- data/sig/redis_queued_locks/swarm/swarm_element/threaded.rbs +1 -1
- data/sig/redis_queued_locks/utilities.rbs +2 -1
- data/sig/redis_queued_locks.rbs +7 -0
- metadata +4 -3
- /data/sig/redis_queued_locks/{acquier.rbs → acquirer.rbs} +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# @api public
|
|
4
4
|
# @since 1.0.0
|
|
5
|
-
# @version 1.
|
|
5
|
+
# @version 1.22.0
|
|
6
6
|
# rubocop:disable Metrics/ClassLength
|
|
7
7
|
class RedisQueuedLocks::Client
|
|
8
8
|
# @return [RedisClient]
|
|
@@ -43,7 +43,7 @@ class RedisQueuedLocks::Client
|
|
|
43
43
|
# @version 1.13.0
|
|
44
44
|
def initialize(redis_client, &configuration)
|
|
45
45
|
@config = RedisQueuedLocks::Config.new(&configuration)
|
|
46
|
-
@uniq_identity = config['uniq_identifier'].call
|
|
46
|
+
@uniq_identity = config['uniq_identifier'].call
|
|
47
47
|
@redis_client = redis_client
|
|
48
48
|
@swarm = RedisQueuedLocks::Swarm.new(self)
|
|
49
49
|
@swarm.swarm! if config['swarm.auto_swarm']
|
|
@@ -232,8 +232,9 @@ class RedisQueuedLocks::Client
|
|
|
232
232
|
# - `:queued` access strategy orders read and write lock requests in FIFO between modes
|
|
233
233
|
# (read lock request waits for earlier write lock requests, write lock request waits for
|
|
234
234
|
# earlier read and write lock requests) so writers are not starved by readers;
|
|
235
|
-
# - `:random` access strategy
|
|
236
|
-
# writers)
|
|
235
|
+
# - `:random` access strategy: the lock request does not wait for earlier requests (no order
|
|
236
|
+
# between modes: a continuous flow of readers can starve writers), but it is queued like any
|
|
237
|
+
# lock request (`:queued` lock requests wait for it);
|
|
237
238
|
# - read lock specifics:
|
|
238
239
|
# - read lock is released (with a block of code) or expired (by `ttl`) independently
|
|
239
240
|
# of other read locks; `#unlock` releases all read locks and the write lock;
|
|
@@ -255,6 +256,24 @@ class RedisQueuedLocks::Client
|
|
|
255
256
|
# (but with checking the limist, retries, timeouts and so on). if lock is
|
|
256
257
|
# free to obtain - it will be obtained;
|
|
257
258
|
# - pre-configured in `config['default_access_strategy']`;
|
|
259
|
+
# @option priority [Integer,Symbol]
|
|
260
|
+
# - The lock request priority: an Integer from `-100` (the lowest) to `100` (the highest),
|
|
261
|
+
# `0` by default (`RedisQueuedLocks::Resource::MIN_LOCK_PRIORITY`..`MAX_LOCK_PRIORITY`),
|
|
262
|
+
# or a priority name of `config['lock_priorities']` (`:lowest` (-100), `:low` (-50),
|
|
263
|
+
# `:normal` (0), `:high` (50), `:highest` (100) by default; the application can define its
|
|
264
|
+
# own names, `merge` keeps the default ones:
|
|
265
|
+
# `config['lock_priorities'] = config['lock_priorities'].merge(payment: 90, email: -10)`);
|
|
266
|
+
# - `:queued` access strategy: the lock request is placed in the lock request queue after all
|
|
267
|
+
# requests of the same or higher priority and before all requests of the lower priority
|
|
268
|
+
# (strict priority, FIFO inside the same priority; read and write lock requests too);
|
|
269
|
+
# - `:random` access strategy: the lock request does not wait for its turn (it ignores the order
|
|
270
|
+
# of the lock request queues and the priorities of other requests), but it is queued at the
|
|
271
|
+
# position of its priority like a `:queued` one: `:queued` lock requests wait for it;
|
|
272
|
+
# - the obtained lock is never taken away: the priority orders the waiting lock requests;
|
|
273
|
+
# - positive priorities need all processes that share the lock on RQL versions with priorities
|
|
274
|
+
# (older versions drop such lock requests as dead ones: the order is broken, the mutual
|
|
275
|
+
# exclusion is not affected); `0` and negative priorities work with older versions too;
|
|
276
|
+
# - pre-configured in `config['default_lock_priority']`;
|
|
258
277
|
# @option meta [NilClass,Hash<String|Symbol,Any>]
|
|
259
278
|
# - A custom metadata wich will be passed to the lock data in addition to the existing data;
|
|
260
279
|
# - Metadata can not contain reserved lock data keys;
|
|
@@ -291,8 +310,8 @@ class RedisQueuedLocks::Client
|
|
|
291
310
|
# - Custom instrumenter that will be invoked via #notify method with `event` and `payload` data;
|
|
292
311
|
# - See RedisQueuedLocks::Instrument::ActiveSupport for examples and implementation details;
|
|
293
312
|
# - See [Instrumentation](#instrumentation) section of docs;
|
|
294
|
-
# - pre-configured in `config['
|
|
295
|
-
# (`RedisQueuedLocks::
|
|
313
|
+
# - pre-configured in `config['instrumenter']` with void notifier
|
|
314
|
+
# (`RedisQueuedLocks::Instrument::VoidNotifier`);
|
|
296
315
|
# @option instrument [NilClass,Any]
|
|
297
316
|
# - Custom instrumentation data wich will be passed to the instrumenter's payload
|
|
298
317
|
# with :instrument key;
|
|
@@ -347,7 +366,7 @@ class RedisQueuedLocks::Client
|
|
|
347
366
|
#
|
|
348
367
|
# @api public
|
|
349
368
|
# @since 1.0.0
|
|
350
|
-
# @version 1.
|
|
369
|
+
# @version 1.22.0
|
|
351
370
|
# rubocop:disable Metrics/MethodLength
|
|
352
371
|
def lock(
|
|
353
372
|
lock_name,
|
|
@@ -364,6 +383,7 @@ class RedisQueuedLocks::Client
|
|
|
364
383
|
conflict_strategy: config['default_conflict_strategy'],
|
|
365
384
|
read_write_mode: :write,
|
|
366
385
|
access_strategy: config['default_access_strategy'],
|
|
386
|
+
priority: config['default_lock_priority'],
|
|
367
387
|
identity: uniq_identity,
|
|
368
388
|
meta: nil,
|
|
369
389
|
detailed_acq_timeout_error: config['detailed_acq_timeout_error'],
|
|
@@ -404,6 +424,8 @@ class RedisQueuedLocks::Client
|
|
|
404
424
|
conflict_strategy:,
|
|
405
425
|
read_write_mode:,
|
|
406
426
|
access_strategy:,
|
|
427
|
+
priority:,
|
|
428
|
+
lock_priorities: config['lock_priorities'],
|
|
407
429
|
meta:,
|
|
408
430
|
detailed_acq_timeout_error:,
|
|
409
431
|
logger:,
|
|
@@ -423,10 +445,10 @@ class RedisQueuedLocks::Client
|
|
|
423
445
|
# rubocop:enable Metrics/MethodLength
|
|
424
446
|
|
|
425
447
|
# NOTE: Lock Series PoC
|
|
426
|
-
# rubocop:disable
|
|
448
|
+
# rubocop:disable Metrics/MethodLength
|
|
427
449
|
# @api public
|
|
428
450
|
# @since 1.16.0
|
|
429
|
-
# @version 1.
|
|
451
|
+
# @version 1.22.0
|
|
430
452
|
def lock_series( # steep:ignore
|
|
431
453
|
*lock_names,
|
|
432
454
|
detailed_result: false,
|
|
@@ -443,6 +465,7 @@ class RedisQueuedLocks::Client
|
|
|
443
465
|
conflict_strategy: config['default_conflict_strategy'],
|
|
444
466
|
read_write_mode: :write,
|
|
445
467
|
access_strategy: config['default_access_strategy'],
|
|
468
|
+
priority: config['default_lock_priority'],
|
|
446
469
|
identity: uniq_identity,
|
|
447
470
|
meta: nil,
|
|
448
471
|
detailed_acq_timeout_error: config['detailed_acq_timeout_error'],
|
|
@@ -484,6 +507,8 @@ class RedisQueuedLocks::Client
|
|
|
484
507
|
conflict_strategy:,
|
|
485
508
|
read_write_mode:,
|
|
486
509
|
access_strategy:,
|
|
510
|
+
priority:,
|
|
511
|
+
lock_priorities: config['lock_priorities'],
|
|
487
512
|
meta:,
|
|
488
513
|
detailed_acq_timeout_error:,
|
|
489
514
|
logger:,
|
|
@@ -500,13 +525,13 @@ class RedisQueuedLocks::Client
|
|
|
500
525
|
&block
|
|
501
526
|
)
|
|
502
527
|
end
|
|
503
|
-
# rubocop:enable
|
|
528
|
+
# rubocop:enable Metrics/MethodLength
|
|
504
529
|
|
|
505
530
|
# NOTE: Lock Series PoC
|
|
506
|
-
# rubocop:disable
|
|
531
|
+
# rubocop:disable Metrics/MethodLength
|
|
507
532
|
# @api public
|
|
508
533
|
# @since 1.16.0
|
|
509
|
-
# @version 1.
|
|
534
|
+
# @version 1.22.0
|
|
510
535
|
def lock_series!( # steep:ignore
|
|
511
536
|
*lock_names,
|
|
512
537
|
detailed_result: false,
|
|
@@ -522,6 +547,7 @@ class RedisQueuedLocks::Client
|
|
|
522
547
|
conflict_strategy: config['default_conflict_strategy'],
|
|
523
548
|
read_write_mode: :write,
|
|
524
549
|
access_strategy: config['default_access_strategy'],
|
|
550
|
+
priority: config['default_lock_priority'],
|
|
525
551
|
identity: uniq_identity,
|
|
526
552
|
meta: nil,
|
|
527
553
|
detailed_acq_timeout_error: config['detailed_acq_timeout_error'],
|
|
@@ -555,6 +581,7 @@ class RedisQueuedLocks::Client
|
|
|
555
581
|
conflict_strategy:,
|
|
556
582
|
read_write_mode:,
|
|
557
583
|
access_strategy:,
|
|
584
|
+
priority:,
|
|
558
585
|
identity:,
|
|
559
586
|
meta:,
|
|
560
587
|
detailed_acq_timeout_error:,
|
|
@@ -573,12 +600,14 @@ class RedisQueuedLocks::Client
|
|
|
573
600
|
&block
|
|
574
601
|
)
|
|
575
602
|
end
|
|
603
|
+
# rubocop:enable Metrics/MethodLength
|
|
576
604
|
|
|
577
605
|
# @note See #lock method signature.
|
|
578
606
|
#
|
|
607
|
+
# rubocop:disable Metrics/MethodLength
|
|
579
608
|
# @api public
|
|
580
609
|
# @since 1.0.0
|
|
581
|
-
# @version 1.
|
|
610
|
+
# @version 1.22.0
|
|
582
611
|
def lock!(
|
|
583
612
|
lock_name,
|
|
584
613
|
ttl: config['default_lock_ttl'],
|
|
@@ -593,6 +622,7 @@ class RedisQueuedLocks::Client
|
|
|
593
622
|
conflict_strategy: config['default_conflict_strategy'],
|
|
594
623
|
read_write_mode: :write,
|
|
595
624
|
access_strategy: config['default_access_strategy'],
|
|
625
|
+
priority: config['default_lock_priority'],
|
|
596
626
|
identity: uniq_identity,
|
|
597
627
|
instrumenter: config['instrumenter'],
|
|
598
628
|
meta: nil,
|
|
@@ -632,6 +662,7 @@ class RedisQueuedLocks::Client
|
|
|
632
662
|
conflict_strategy:,
|
|
633
663
|
read_write_mode:,
|
|
634
664
|
access_strategy:,
|
|
665
|
+
priority:,
|
|
635
666
|
log_sampling_enabled:,
|
|
636
667
|
log_sampling_percent:,
|
|
637
668
|
log_sampler:,
|
|
@@ -714,13 +745,15 @@ class RedisQueuedLocks::Client
|
|
|
714
745
|
#
|
|
715
746
|
# @param lock_name [String] The lock name whose read lock should be released.
|
|
716
747
|
# @option acquirer_id [String,NilClass]
|
|
717
|
-
# -
|
|
718
|
-
# - `nil` => the current acquirer is used (calculated from `identity:`,
|
|
719
|
-
#
|
|
748
|
+
# - the acquirer whose read lock should be released (`nil` by default);
|
|
749
|
+
# - `nil` => the current acquirer is used (calculated from `identity:`,
|
|
750
|
+
# see `#current_acquirer_id`);
|
|
751
|
+
# - can be extracted from the lock data (`#lock_info`, `#locks_info`, `#queue_info`,
|
|
752
|
+
# `#queues_info`);
|
|
720
753
|
# @option identity [String]
|
|
721
754
|
# Unique acquirer identity (the same as for `#lock`) that is used to calculate the current
|
|
722
|
-
# acquirer id (see `#current_acquirer_id`). Ignored when `acquirer_id:` is passed: the
|
|
723
|
-
# already contains its own identity.
|
|
755
|
+
# acquirer id (see `#current_acquirer_id`). Ignored when `acquirer_id:` is passed: the
|
|
756
|
+
# acquirer id already contains its own identity.
|
|
724
757
|
# @option logger [::Logger,#debug]
|
|
725
758
|
# @option instrumenter [#notify]
|
|
726
759
|
# @option instrument [NilClass,Any]
|
|
@@ -826,7 +859,7 @@ class RedisQueuedLocks::Client
|
|
|
826
859
|
#
|
|
827
860
|
# @api public
|
|
828
861
|
# @since 1.0.0
|
|
829
|
-
# @version 1.
|
|
862
|
+
# @version 1.22.0
|
|
830
863
|
def queue_info(lock_name)
|
|
831
864
|
RedisQueuedLocks::Acquirer::QueueInfo.queue_info(redis_client, lock_name)
|
|
832
865
|
end
|
|
@@ -1260,7 +1293,7 @@ class RedisQueuedLocks::Client
|
|
|
1260
1293
|
#
|
|
1261
1294
|
# @api public
|
|
1262
1295
|
# @since 1.0.0
|
|
1263
|
-
# @version 1.
|
|
1296
|
+
# @version 1.22.0
|
|
1264
1297
|
def queues(scan_size: config['key_extraction_batch_size'], with_info: false)
|
|
1265
1298
|
RedisQueuedLocks::Acquirer::Queues.queues(
|
|
1266
1299
|
redis_client, scan_size:, with_info:, lock_index_enabled: config['lock_index_enabled']
|
|
@@ -1274,7 +1307,7 @@ class RedisQueuedLocks::Client
|
|
|
1274
1307
|
#
|
|
1275
1308
|
# @api public
|
|
1276
1309
|
# @since 1.0.0
|
|
1277
|
-
# @version 1.
|
|
1310
|
+
# @version 1.22.0
|
|
1278
1311
|
def queues_info(scan_size: config['key_extraction_batch_size'])
|
|
1279
1312
|
queues(scan_size:, with_info: true) #: RedisQueuedLocks::Acquirer::Queues::queuesInfo
|
|
1280
1313
|
end
|
|
@@ -1302,6 +1335,13 @@ class RedisQueuedLocks::Client
|
|
|
1302
1335
|
# - the batch of scanned keys for Redis'es SCAN command (or the number of locks in one batch of
|
|
1303
1336
|
# the lock index ZSCAN pass: the enabled `config['lock_index_enabled']`);
|
|
1304
1337
|
# - `config['lock_release_batch_size']` is used by default;
|
|
1338
|
+
# @option by_priority [Boolean]
|
|
1339
|
+
# - read only the first lock request of each priority in the lock queues instead of all lock
|
|
1340
|
+
# requests (the dead requests of a priority are found by its first lock request): the
|
|
1341
|
+
# transfer does not depend on the length of the lock queues, a batch of lock queues takes
|
|
1342
|
+
# one round trip per priority of its lock queue with the most priorities + 1 (instead of two
|
|
1343
|
+
# round trips): for long lock queues with few priorities;
|
|
1344
|
+
# - `false` by default (all lock requests are read);
|
|
1305
1345
|
# @option logger [::Logger,#debug]
|
|
1306
1346
|
# @option instrumenter [#notify]
|
|
1307
1347
|
# @option instrument [NilClass,Any]
|
|
@@ -1318,10 +1358,11 @@ class RedisQueuedLocks::Client
|
|
|
1318
1358
|
#
|
|
1319
1359
|
# @api public
|
|
1320
1360
|
# @since 1.0.0
|
|
1321
|
-
# @version 1.
|
|
1361
|
+
# @version 1.22.0
|
|
1322
1362
|
def clear_dead_requests(
|
|
1323
1363
|
dead_ttl: config['dead_request_ttl'],
|
|
1324
1364
|
scan_size: config['lock_release_batch_size'],
|
|
1365
|
+
by_priority: false,
|
|
1325
1366
|
logger: config['logger'],
|
|
1326
1367
|
instrumenter: config['instrumenter'],
|
|
1327
1368
|
instrument: nil,
|
|
@@ -1338,6 +1379,7 @@ class RedisQueuedLocks::Client
|
|
|
1338
1379
|
redis_client,
|
|
1339
1380
|
scan_size,
|
|
1340
1381
|
dead_ttl,
|
|
1382
|
+
by_priority,
|
|
1341
1383
|
config['lock_index_enabled'],
|
|
1342
1384
|
logger,
|
|
1343
1385
|
instrumenter,
|
|
@@ -1354,8 +1396,8 @@ class RedisQueuedLocks::Client
|
|
|
1354
1396
|
end
|
|
1355
1397
|
|
|
1356
1398
|
# 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
|
|
1358
|
-
# index is enabled (`config['lock_index_enabled']`) on all your processes: lock-iterating
|
|
1399
|
+
# the lock index was disabled) to the lock index (`rql:index:locks`). Invoke it once after the
|
|
1400
|
+
# lock index is enabled (`config['lock_index_enabled']`) on all your processes: lock-iterating
|
|
1359
1401
|
# operations (the swarm, `#locks`, `#queues`, `#keys`, `#clear_locks`, ...) find locks via
|
|
1360
1402
|
# the lock index only.
|
|
1361
1403
|
# - works regardless of `config['lock_index_enabled']` (the lock index is not used and not
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# @api private
|
|
4
4
|
# @since 1.13.0
|
|
5
|
+
# @version 1.22.0
|
|
5
6
|
# rubocop:disable Metrics/ClassLength
|
|
6
7
|
class RedisQueuedLocks::Config
|
|
7
8
|
require_relative 'config/dsl'
|
|
@@ -31,6 +32,8 @@ class RedisQueuedLocks::Config
|
|
|
31
32
|
setting('is_timed_by_default', false)
|
|
32
33
|
setting('default_conflict_strategy', :wait_for_lock)
|
|
33
34
|
setting('default_access_strategy', :queued)
|
|
35
|
+
setting('lock_priorities', { lowest: -100, low: -50, normal: 0, high: 50, highest: 100 }.freeze)
|
|
36
|
+
setting('default_lock_priority', 0)
|
|
34
37
|
setting('log_sampling_enabled', false)
|
|
35
38
|
setting('log_sampling_percent', 15)
|
|
36
39
|
setting('log_sampler', RedisQueuedLocks::Logging::Sampler)
|
|
@@ -114,6 +117,27 @@ class RedisQueuedLocks::Config
|
|
|
114
117
|
val == :random
|
|
115
118
|
# rubocop:enable Layout/MultilineOperationIndentation
|
|
116
119
|
end
|
|
120
|
+
validate('lock_priorities') do |val|
|
|
121
|
+
val.is_a?(Hash) && val.all? do |name, priority|
|
|
122
|
+
# rubocop:disable Layout/MultilineOperationIndentation
|
|
123
|
+
name.is_a?(Symbol) &&
|
|
124
|
+
priority.is_a?(Integer) &&
|
|
125
|
+
priority >= RedisQueuedLocks::Resource::MIN_LOCK_PRIORITY &&
|
|
126
|
+
priority <= RedisQueuedLocks::Resource::MAX_LOCK_PRIORITY
|
|
127
|
+
# rubocop:enable Layout/MultilineOperationIndentation
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
# NOTE: a Symbol is a priority name of `config['lock_priorities']`: a validator gets one value,
|
|
131
|
+
# so the name is checked by the configuration itself (`prevent_default_priority__unknown_name`:
|
|
132
|
+
# once after a configuration block, the keys can be assigned in any order; before a direct
|
|
133
|
+
# assignment is applied);
|
|
134
|
+
validate('default_lock_priority') do |val|
|
|
135
|
+
val.is_a?(Symbol) || (
|
|
136
|
+
val.is_a?(Integer) &&
|
|
137
|
+
val >= RedisQueuedLocks::Resource::MIN_LOCK_PRIORITY &&
|
|
138
|
+
val <= RedisQueuedLocks::Resource::MAX_LOCK_PRIORITY
|
|
139
|
+
)
|
|
140
|
+
end
|
|
117
141
|
|
|
118
142
|
# @return [Hash<Symbol,Any>]
|
|
119
143
|
#
|
|
@@ -123,10 +147,14 @@ class RedisQueuedLocks::Config
|
|
|
123
147
|
|
|
124
148
|
# @return [void]
|
|
125
149
|
#
|
|
150
|
+
# @raise [RedisQueuedLocks::ConfigValidationError]
|
|
151
|
+
#
|
|
126
152
|
# @api private
|
|
127
153
|
# @since 1.13.0
|
|
154
|
+
# @version 1.22.0
|
|
128
155
|
def initialize(&configuration)
|
|
129
156
|
@config_state = {}
|
|
157
|
+
@configuring = false
|
|
130
158
|
config_setters.each_value { |setter| setter.call(@config_state) }
|
|
131
159
|
configure(&configuration)
|
|
132
160
|
end
|
|
@@ -136,10 +164,40 @@ class RedisQueuedLocks::Config
|
|
|
136
164
|
# @yieldparam [?]
|
|
137
165
|
# @return [void]
|
|
138
166
|
#
|
|
167
|
+
# @raise [RedisQueuedLocks::ConfigValidationError]
|
|
168
|
+
#
|
|
139
169
|
# @api private
|
|
140
170
|
# @since 1.13.0
|
|
171
|
+
# @version 1.22.0
|
|
141
172
|
def configure(&configuration)
|
|
142
|
-
|
|
173
|
+
return unless block_given?
|
|
174
|
+
|
|
175
|
+
# NOTE: (priorities) the keys of the block can be assigned in any order: the priority name of
|
|
176
|
+
# `default_lock_priority` is checked against `lock_priorities` once, after the block (after
|
|
177
|
+
# the outer block for nested blocks); an inconsistent pair is never applied: both keys keep
|
|
178
|
+
# their values from before the block (also when the block fails: its error is raised);
|
|
179
|
+
lock_priorities = config_state['lock_priorities']
|
|
180
|
+
default_lock_priority = config_state['default_lock_priority']
|
|
181
|
+
nested = @configuring
|
|
182
|
+
completed = false
|
|
183
|
+
@configuring = true
|
|
184
|
+
begin
|
|
185
|
+
yield(self)
|
|
186
|
+
completed = true
|
|
187
|
+
ensure
|
|
188
|
+
@configuring = nested
|
|
189
|
+
unless nested
|
|
190
|
+
begin
|
|
191
|
+
prevent_default_priority__unknown_name(
|
|
192
|
+
config_state['lock_priorities'], config_state['default_lock_priority']
|
|
193
|
+
)
|
|
194
|
+
rescue RedisQueuedLocks::ConfigValidationError
|
|
195
|
+
config_state['lock_priorities'] = lock_priorities
|
|
196
|
+
config_state['default_lock_priority'] = default_lock_priority
|
|
197
|
+
raise if completed
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
143
201
|
end
|
|
144
202
|
|
|
145
203
|
# @param config_key [String]
|
|
@@ -163,9 +221,21 @@ class RedisQueuedLocks::Config
|
|
|
163
221
|
#
|
|
164
222
|
# @api public
|
|
165
223
|
# @since 1.13.0
|
|
224
|
+
# @version 1.22.0
|
|
166
225
|
def []=(config_key, config_value)
|
|
167
226
|
prevent_key__non_existent(config_key)
|
|
168
227
|
prevent_key__invalid_type(config_key, config_value)
|
|
228
|
+
# NOTE: (priorities) a direct assignment (out of a configuration block) keeps the priority name
|
|
229
|
+
# of `default_lock_priority` a key of `lock_priorities` (a configuration block checks it once
|
|
230
|
+
# after the block, see `configure`);
|
|
231
|
+
unless @configuring
|
|
232
|
+
case config_key
|
|
233
|
+
when 'lock_priorities'
|
|
234
|
+
prevent_default_priority__unknown_name(config_value, config_state['default_lock_priority'])
|
|
235
|
+
when 'default_lock_priority'
|
|
236
|
+
prevent_default_priority__unknown_name(config_state['lock_priorities'], config_value)
|
|
237
|
+
end
|
|
238
|
+
end
|
|
169
239
|
config_state[config_key] = config_value
|
|
170
240
|
end
|
|
171
241
|
|
|
@@ -236,5 +306,27 @@ class RedisQueuedLocks::Config
|
|
|
236
306
|
)
|
|
237
307
|
end
|
|
238
308
|
end
|
|
309
|
+
|
|
310
|
+
# A priority name of the default lock request priority should be a priority name of
|
|
311
|
+
# `lock_priorities` (an unknown name would fail each lock acquirement with the default priority).
|
|
312
|
+
#
|
|
313
|
+
# @param lock_priorities [Hash<Symbol,Integer>] The priority names.
|
|
314
|
+
# @param default_lock_priority [Integer,Symbol] The default lock request priority.
|
|
315
|
+
# @return [void]
|
|
316
|
+
#
|
|
317
|
+
# @raise [RedisQueuedLocks::ConfigValidationError]
|
|
318
|
+
#
|
|
319
|
+
# @api private
|
|
320
|
+
# @since 1.22.0
|
|
321
|
+
def prevent_default_priority__unknown_name(lock_priorities, default_lock_priority)
|
|
322
|
+
if default_lock_priority.is_a?(Symbol) && !lock_priorities.key?(default_lock_priority)
|
|
323
|
+
raise(
|
|
324
|
+
RedisQueuedLocks::ConfigValidationError,
|
|
325
|
+
"The priority name #{default_lock_priority.inspect} of the 'default_lock_priority' " \
|
|
326
|
+
"config key is not a key of the 'lock_priorities' config key " \
|
|
327
|
+
"(#{lock_priorities.keys.map(&:inspect).join(', ')})."
|
|
328
|
+
)
|
|
329
|
+
end
|
|
330
|
+
end
|
|
239
331
|
end
|
|
240
332
|
# rubocop:enable Metrics/ClassLength
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# @api private
|
|
4
4
|
# @since 1.0.0
|
|
5
|
-
# @version 1.
|
|
5
|
+
# @version 1.22.0
|
|
6
6
|
# rubocop:disable Metrics/ModuleLength
|
|
7
7
|
module RedisQueuedLocks::Resource
|
|
8
8
|
# @return [String]
|
|
@@ -90,6 +90,44 @@ module RedisQueuedLocks::Resource
|
|
|
90
90
|
# @since 1.3.0
|
|
91
91
|
REDIS_TIMESHIFT_ERROR = 2
|
|
92
92
|
|
|
93
|
+
# The lowest lock request priority (the `priority` option of `#lock`).
|
|
94
|
+
#
|
|
95
|
+
# @return [Integer]
|
|
96
|
+
#
|
|
97
|
+
# @api private
|
|
98
|
+
# @since 1.22.0
|
|
99
|
+
MIN_LOCK_PRIORITY = -100
|
|
100
|
+
|
|
101
|
+
# The highest lock request priority (the `priority` option of `#lock`).
|
|
102
|
+
#
|
|
103
|
+
# @return [Integer]
|
|
104
|
+
#
|
|
105
|
+
# @api private
|
|
106
|
+
# @since 1.22.0
|
|
107
|
+
MAX_LOCK_PRIORITY = 100
|
|
108
|
+
|
|
109
|
+
# Lock request priority band: the position of the lock request in the lock request queue (the
|
|
110
|
+
# score) is the enqueue time (epoch, seconds) shifted by `priority * LOCK_PRIORITY_BAND` down:
|
|
111
|
+
# - each priority gets its own range of positions (the band): requests of the higher priority
|
|
112
|
+
# are always ahead of the requests of the lower priority, requests of the same priority are
|
|
113
|
+
# ordered by the enqueue time (FIFO);
|
|
114
|
+
# - 2**32 seconds (~136 years) is greater than any enqueue time (epoch) up to the year 2106,
|
|
115
|
+
# so the bands never overlap and the priority is decoded from the position unambiguously;
|
|
116
|
+
# - the default priority (`0`) keeps the position equal to the enqueue time (the positions of
|
|
117
|
+
# the RQL versions without priorities);
|
|
118
|
+
# - the band of the priority `p` is `(-p * LOCK_PRIORITY_BAND, (1 - p) * LOCK_PRIORITY_BAND)`;
|
|
119
|
+
# - the enqueue time is decoded from the position of any priority, so dead lock requests of any
|
|
120
|
+
# band are recognized by the position (the time barrier of the band, see
|
|
121
|
+
# `lock_priority_band_dead_score`) without the knowledge of the priority range;
|
|
122
|
+
# - the precision of the positions (the FIFO precision inside a priority) depends on the
|
|
123
|
+
# shift: ~1 µs for -1..1, ~61 µs for -100..100 (`MIN_LOCK_PRIORITY`..`MAX_LOCK_PRIORITY`):
|
|
124
|
+
# requests of the same priority made within this interval are ordered by acquirer ids;
|
|
125
|
+
# @return [Integer] In seconds.
|
|
126
|
+
#
|
|
127
|
+
# @api private
|
|
128
|
+
# @since 1.22.0
|
|
129
|
+
LOCK_PRIORITY_BAND = 4_294_967_296
|
|
130
|
+
|
|
93
131
|
# rubocop:disable Metrics/ClassLength
|
|
94
132
|
class << self
|
|
95
133
|
# Returns 16-byte unique identifier. It is used for uniquely
|
|
@@ -276,6 +314,22 @@ module RedisQueuedLocks::Resource
|
|
|
276
314
|
::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond)
|
|
277
315
|
end
|
|
278
316
|
|
|
317
|
+
# Is the lock name of the lock index expired (all the keys of the lock are expired)?
|
|
318
|
+
#
|
|
319
|
+
# NOTE: the lock index replies of ZSCAN have String scores: `+inf` (a key without TTL indexed
|
|
320
|
+
# by `ReindexLocks`) is `"inf"`, and `String#to_f` turns it into `0.0` (the name would be
|
|
321
|
+
# skipped as an expired one by every lock index lookup): it never expires;
|
|
322
|
+
#
|
|
323
|
+
# @param lock_index_score [String] The score of the lock name (a ZSCAN reply).
|
|
324
|
+
# @param lock_index_time [Integer] The current client time (see `lock_index_time`).
|
|
325
|
+
# @return [Boolean]
|
|
326
|
+
#
|
|
327
|
+
# @api private
|
|
328
|
+
# @since 1.22.0
|
|
329
|
+
def lock_index_score_expired?(lock_index_score, lock_index_time)
|
|
330
|
+
lock_index_score != 'inf' && lock_index_score.to_f < lock_index_time
|
|
331
|
+
end
|
|
332
|
+
|
|
279
333
|
# @param lock_readers_key [String]
|
|
280
334
|
# @return [String]
|
|
281
335
|
#
|
|
@@ -327,16 +381,23 @@ module RedisQueuedLocks::Resource
|
|
|
327
381
|
(Integer(redis_time[0]) * 1_000) + (Integer(redis_time[1]) / 1_000)
|
|
328
382
|
end
|
|
329
383
|
|
|
330
|
-
# @
|
|
384
|
+
# @param priority [Integer]
|
|
385
|
+
# The lock request priority (from `MIN_LOCK_PRIORITY` to `MAX_LOCK_PRIORITY`).
|
|
386
|
+
# @return [Float]
|
|
387
|
+
# Redis's <Set> score: the time (epoch, as a float) shifted by the priority band (see
|
|
388
|
+
# `LOCK_PRIORITY_BAND`; the default priority `0` keeps the time).
|
|
331
389
|
#
|
|
332
390
|
# @api private
|
|
333
391
|
# @since 1.0.0
|
|
334
|
-
|
|
335
|
-
|
|
392
|
+
# @version 1.22.0
|
|
393
|
+
def calc_initial_acquirer_position(priority)
|
|
394
|
+
Time.now.to_f - (priority * LOCK_PRIORITY_BAND)
|
|
336
395
|
end
|
|
337
396
|
|
|
338
397
|
# @param queue_ttl [Numeric] In seconds
|
|
339
|
-
# @return [Float]
|
|
398
|
+
# @return [Float]
|
|
399
|
+
# Redis's <Set> score barrier for acquirers that should be removed from queue (the time
|
|
400
|
+
# barrier: the barrier of the default priority band, see `lock_priority_band_dead_score`).
|
|
340
401
|
#
|
|
341
402
|
# @api private
|
|
342
403
|
# @since 1.0.0
|
|
@@ -344,6 +405,57 @@ module RedisQueuedLocks::Resource
|
|
|
344
405
|
Time.now.to_f - queue_ttl
|
|
345
406
|
end
|
|
346
407
|
|
|
408
|
+
# The lowest position (score) of the lock requests of the given priority band (see
|
|
409
|
+
# `LOCK_PRIORITY_BAND`): the lower bound of the dead lock request removal (`ZREMRANGEBYSCORE`).
|
|
410
|
+
#
|
|
411
|
+
# @param priority [Integer] Any priority (the bands of all priorities have the same layout).
|
|
412
|
+
# @return [Integer]
|
|
413
|
+
# The lower edge of the band (the positions of the band never reach its edges: an enqueue
|
|
414
|
+
# time is always greater than 0 and less than `LOCK_PRIORITY_BAND`).
|
|
415
|
+
#
|
|
416
|
+
# @api private
|
|
417
|
+
# @since 1.22.0
|
|
418
|
+
def lock_priority_band_floor(priority)
|
|
419
|
+
-priority * LOCK_PRIORITY_BAND
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
# Requests of the given priority band queued at this position or earlier are dead (the upper
|
|
423
|
+
# bound of the dead lock request removal, see `LOCK_PRIORITY_BAND`).
|
|
424
|
+
#
|
|
425
|
+
# @param dead_score [Float] The time barrier (see `acquirer_dead_score`).
|
|
426
|
+
# @param priority [Integer]
|
|
427
|
+
# @return [Float]
|
|
428
|
+
#
|
|
429
|
+
# @api private
|
|
430
|
+
# @since 1.22.0
|
|
431
|
+
def lock_priority_band_dead_score(dead_score, priority)
|
|
432
|
+
dead_score - (priority * LOCK_PRIORITY_BAND)
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
# Priority of the lock request by its position in the lock request queue (the score).
|
|
436
|
+
#
|
|
437
|
+
# NOTE: (infinite positions) RQL never writes infinite positions (manual writes of the lock
|
|
438
|
+
# request queues): they are out of all bands and are decoded to the bounds of the priority
|
|
439
|
+
# range (the floor of an infinite Float raises `FloatDomainError`):
|
|
440
|
+
# - `-inf`: `MAX_LOCK_PRIORITY` (ahead of all lock requests; such a request is dead: its
|
|
441
|
+
# enqueue time is unknown, it is dropped like before priorities);
|
|
442
|
+
# - `+inf`: `MIN_LOCK_PRIORITY` (behind all lock requests; never dead);
|
|
443
|
+
#
|
|
444
|
+
# @param acquirer_position [Numeric] The position (the score) of the lock request.
|
|
445
|
+
# @return [Integer] See `LOCK_PRIORITY_BAND`.
|
|
446
|
+
#
|
|
447
|
+
# @api private
|
|
448
|
+
# @since 1.22.0
|
|
449
|
+
def lock_request_priority(acquirer_position)
|
|
450
|
+
if acquirer_position == -Float::INFINITY
|
|
451
|
+
MAX_LOCK_PRIORITY
|
|
452
|
+
elsif acquirer_position == Float::INFINITY
|
|
453
|
+
MIN_LOCK_PRIORITY
|
|
454
|
+
else
|
|
455
|
+
-(acquirer_position / LOCK_PRIORITY_BAND).floor
|
|
456
|
+
end
|
|
457
|
+
end
|
|
458
|
+
|
|
347
459
|
# @param zombie_ttl [Float] In seconds with milliseconds.
|
|
348
460
|
# @return [Float]
|
|
349
461
|
#
|
|
@@ -354,8 +466,10 @@ module RedisQueuedLocks::Resource
|
|
|
354
466
|
end
|
|
355
467
|
|
|
356
468
|
# @param acquirer_position [Float]
|
|
357
|
-
# A time (epoch, seconds.milliseconds) that represents
|
|
358
|
-
# the acquirer position in lock request queue.
|
|
469
|
+
# A time (epoch, seconds.milliseconds) shifted by the priority band that represents
|
|
470
|
+
# the acquirer position in lock request queue (see `calc_initial_acquirer_position`).
|
|
471
|
+
# @param priority [Integer]
|
|
472
|
+
# The lock request priority (the band of the position).
|
|
359
473
|
# @param queue_ttl [Integer]
|
|
360
474
|
# In second.
|
|
361
475
|
# @return [Boolean]
|
|
@@ -363,8 +477,9 @@ module RedisQueuedLocks::Resource
|
|
|
363
477
|
#
|
|
364
478
|
# @api private
|
|
365
479
|
# @since 1.0.0
|
|
366
|
-
|
|
367
|
-
|
|
480
|
+
# @version 1.22.0
|
|
481
|
+
def dead_score_reached?(acquirer_position, priority, queue_ttl)
|
|
482
|
+
(acquirer_position + (priority * LOCK_PRIORITY_BAND) + queue_ttl) < Time.now.to_f
|
|
368
483
|
end
|
|
369
484
|
|
|
370
485
|
# NOTE: (debug) not used by the library: a manual runtime check (in a console) of the lock that
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# @api private
|
|
4
4
|
# @since 1.9.0
|
|
5
|
-
# @version 1.
|
|
5
|
+
# @version 1.21.1
|
|
6
6
|
# rubocop:disable Metrics/ClassLength
|
|
7
7
|
class RedisQueuedLocks::Swarm::SwarmElement::Threaded
|
|
8
8
|
# @since 1.9.0
|
|
@@ -70,7 +70,7 @@ class RedisQueuedLocks::Swarm::SwarmElement::Threaded
|
|
|
70
70
|
return unless enabled?
|
|
71
71
|
|
|
72
72
|
sync.synchronize do
|
|
73
|
-
|
|
73
|
+
swarm_element__terminate
|
|
74
74
|
swarm!
|
|
75
75
|
swarm_loop__start
|
|
76
76
|
end
|
|
@@ -99,7 +99,7 @@ class RedisQueuedLocks::Swarm::SwarmElement::Threaded
|
|
|
99
99
|
# @since 1.9.0
|
|
100
100
|
def try_kill!
|
|
101
101
|
sync.synchronize do
|
|
102
|
-
|
|
102
|
+
swarm_element__terminate
|
|
103
103
|
end
|
|
104
104
|
end
|
|
105
105
|
|
|
@@ -157,7 +157,7 @@ class RedisQueuedLocks::Swarm::SwarmElement::Threaded
|
|
|
157
157
|
# => 1) init (swarm!): create a thread, main loop is not started;
|
|
158
158
|
# => 2) start (swarm_loop__start): run the main loop inside the created thread;
|
|
159
159
|
# => 3) stop (swarm_loop__stop): stop the main loop inside the created thread;
|
|
160
|
-
# => 4) terminate (
|
|
160
|
+
# => 4) terminate (swarm_element__terminate): kill the created thread;
|
|
161
161
|
# In addition you should implement `#spawn_main_loop!` method that will be the main logic
|
|
162
162
|
# of the your swarm element.
|
|
163
163
|
#
|
|
@@ -341,7 +341,8 @@ class RedisQueuedLocks::Swarm::SwarmElement::Threaded
|
|
|
341
341
|
#
|
|
342
342
|
# @api private
|
|
343
343
|
# @since 1.9.0
|
|
344
|
-
|
|
344
|
+
# @version 1.21.1
|
|
345
|
+
def swarm_element__terminate
|
|
345
346
|
return if idle? || swarmed__dead?
|
|
346
347
|
sync.synchronize do
|
|
347
348
|
main_loop&.kill
|
|
@@ -232,7 +232,8 @@ module RedisQueuedLocks::Swarm::ZombieInfo
|
|
|
232
232
|
# names, the full keyspace is never scanned): write locks and readers registries are processed
|
|
233
233
|
# in batches of `lock_scan_size` locks with one round trip per batch and per key family
|
|
234
234
|
# (batches are collected across ZSCAN pages). Expired lock names of the index are skipped
|
|
235
|
-
# (all their keys are expired
|
|
235
|
+
# (all their keys are expired; `+inf` names, keys without TTL, never expire: see
|
|
236
|
+
# `RedisQueuedLocks::Resource.lock_index_score_expired?`).
|
|
236
237
|
#
|
|
237
238
|
# @param rconn [RedisClient] redis connection obtained via `#with` from RedisClient instance;
|
|
238
239
|
# @param zombie_hosts [Set<String>]
|
|
@@ -244,6 +245,7 @@ module RedisQueuedLocks::Swarm::ZombieInfo
|
|
|
244
245
|
#
|
|
245
246
|
# @api private
|
|
246
247
|
# @since 1.20.0
|
|
248
|
+
# @version 1.22.0
|
|
247
249
|
def each_lock_index_zombie_lock(rconn, zombie_hosts, lock_scan_size, &block)
|
|
248
250
|
# @type var lock_keys: Array[String]
|
|
249
251
|
lock_keys = []
|
|
@@ -255,7 +257,9 @@ module RedisQueuedLocks::Swarm::ZombieInfo
|
|
|
255
257
|
RedisQueuedLocks::Resource::LOCK_INDEX_KEY, count: lock_scan_size
|
|
256
258
|
) do |lock_name, lock_index_score|
|
|
257
259
|
# NOTE: the expired lock name (not dropped from the index yet): its keys are expired
|
|
258
|
-
next if
|
|
260
|
+
next if RedisQueuedLocks::Resource.lock_index_score_expired?(
|
|
261
|
+
lock_index_score, lock_index_time
|
|
262
|
+
)
|
|
259
263
|
|
|
260
264
|
lock_keys << RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
|
|
261
265
|
lock_readers_keys << RedisQueuedLocks::Resource.prepare_lock_readers(lock_name)
|