redis_queued_locks 1.20.0 → 1.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/project-overview.md +35 -13
  3. data/.claude/rules/acquirer.md +10 -9
  4. data/.claude/rules/logic.md +28 -5
  5. data/.claude/rules/swarm.md +2 -2
  6. data/.claude/rules/tests.md +6 -3
  7. data/.claude/rules/visitors.md +9 -10
  8. data/.claude/skills/rql-code-review/SKILL.md +24 -10
  9. data/CHANGELOG.md +126 -0
  10. data/CLAUDE.md +2 -1
  11. data/README.md +245 -70
  12. data/lib/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue/log_visitor.rb +1 -0
  13. data/lib/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rb +42 -0
  14. data/lib/redis_queued_locks/acquirer/acquire_lock/log_visitor.rb +45 -0
  15. data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock/log_visitor.rb +1 -0
  16. data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rb +12 -1
  17. data/lib/redis_queued_locks/acquirer/acquire_lock/with_acq_timeout.rb +2 -2
  18. data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire/log_visitor.rb +1 -0
  19. data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire.rb +5 -5
  20. data/lib/redis_queued_locks/acquirer/acquire_lock.rb +111 -24
  21. data/lib/redis_queued_locks/acquirer/clear_dead_requests.rb +86 -8
  22. data/lib/redis_queued_locks/acquirer/extend_lock_ttl.rb +122 -12
  23. data/lib/redis_queued_locks/acquirer/is_locked.rb +0 -1
  24. data/lib/redis_queued_locks/acquirer/is_queued.rb +0 -1
  25. data/lib/redis_queued_locks/acquirer/keys.rb +10 -1
  26. data/lib/redis_queued_locks/acquirer/lock_info.rb +31 -39
  27. data/lib/redis_queued_locks/acquirer/lock_series_poc/instr_visitor.rb +3 -2
  28. data/lib/redis_queued_locks/acquirer/lock_series_poc/log_visitor.rb +5 -4
  29. data/lib/redis_queued_locks/acquirer/lock_series_poc.rb +55 -15
  30. data/lib/redis_queued_locks/acquirer/locks.rb +26 -9
  31. data/lib/redis_queued_locks/acquirer/queue_info.rb +0 -1
  32. data/lib/redis_queued_locks/acquirer/queues.rb +10 -1
  33. data/lib/redis_queued_locks/acquirer/reindex_locks.rb +117 -4
  34. data/lib/redis_queued_locks/acquirer/release_all_locks.rb +31 -4
  35. data/lib/redis_queued_locks/acquirer/release_lock.rb +33 -7
  36. data/lib/redis_queued_locks/acquirer/release_locks_of.rb +39 -4
  37. data/lib/redis_queued_locks/acquirer/release_read_lock.rb +22 -3
  38. data/lib/redis_queued_locks/client.rb +82 -40
  39. data/lib/redis_queued_locks/config.rb +12 -13
  40. data/lib/redis_queued_locks/errors.rb +34 -4
  41. data/lib/redis_queued_locks/instrument/sampler.rb +4 -7
  42. data/lib/redis_queued_locks/logging/sampler.rb +4 -8
  43. data/lib/redis_queued_locks/resource.rb +14 -4
  44. data/lib/redis_queued_locks/swarm/flush_zombies.rb +1 -0
  45. data/lib/redis_queued_locks/swarm/redis_client_builder.rb +8 -4
  46. data/lib/redis_queued_locks/swarm/swarm_element/threaded.rb +3 -2
  47. data/lib/redis_queued_locks/swarm.rb +86 -5
  48. data/lib/redis_queued_locks/utilities.rb +1 -1
  49. data/lib/redis_queued_locks/version.rb +2 -2
  50. data/sig/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rbs +14 -0
  51. data/sig/redis_queued_locks/acquirer/acquire_lock/log_visitor.rbs +13 -0
  52. data/sig/redis_queued_locks/acquirer/acquire_lock/yield_expire.rbs +2 -2
  53. data/sig/redis_queued_locks/acquirer/acquire_lock.rbs +1 -1
  54. data/sig/redis_queued_locks/acquirer/clear_dead_requests.rbs +9 -0
  55. data/sig/redis_queued_locks/acquirer/extend_lock_ttl.rbs +15 -2
  56. data/sig/redis_queued_locks/acquirer/reindex_locks.rbs +22 -1
  57. data/sig/redis_queued_locks/acquirer/release_lock.rbs +3 -2
  58. data/sig/redis_queued_locks/acquirer/release_read_lock.rbs +1 -1
  59. data/sig/redis_queued_locks/client.rbs +14 -3
  60. data/sig/redis_queued_locks/errors.rbs +5 -2
  61. data/sig/redis_queued_locks/instrument/sampler.rbs +0 -2
  62. data/sig/redis_queued_locks/logging/sampler.rbs +0 -2
  63. data/sig/redis_queued_locks/resource.rbs +1 -1
  64. data/sig/vendor/redis_client.rbs +1 -0
  65. metadata +1 -1
@@ -2,7 +2,7 @@
2
2
 
3
3
  # @api public
4
4
  # @since 1.0.0
5
- # @version 1.20.0
5
+ # @version 1.21.0
6
6
  # rubocop:disable Metrics/ClassLength
7
7
  class RedisQueuedLocks::Client
8
8
  # @return [RedisClient]
@@ -79,6 +79,7 @@ class RedisQueuedLocks::Client
79
79
  #
80
80
  # @api public
81
81
  # @since 1.9.0
82
+ # @version 1.21.0
82
83
  def swarm_info(zombie_ttl: config['swarm.flush_zombies.zombie_ttl'])
83
84
  swarm.swarm_info(zombie_ttl:)
84
85
  end
@@ -107,6 +108,7 @@ class RedisQueuedLocks::Client
107
108
  #
108
109
  # @api public
109
110
  # @since 1.9.0
111
+ # @version 1.21.0
110
112
  def flush_zombies(
111
113
  zombie_ttl: config['swarm.flush_zombies.zombie_ttl'],
112
114
  lock_scan_size: config['swarm.flush_zombies.zombie_lock_scan_size'],
@@ -121,6 +123,7 @@ class RedisQueuedLocks::Client
121
123
  #
122
124
  # @api public
123
125
  # @since 1.9.0
126
+ # @version 1.21.0
124
127
  def zombie_locks(
125
128
  zombie_ttl: config['swarm.flush_zombies.zombie_ttl'],
126
129
  lock_scan_size: config['swarm.flush_zombies.zombie_lock_scan_size']
@@ -132,8 +135,9 @@ class RedisQueuedLocks::Client
132
135
  # @option lock_scan_size [Integer]
133
136
  # @return [Set<String>]
134
137
  #
135
- # @api ppublic
138
+ # @api public
136
139
  # @since 1.9.0
140
+ # @version 1.21.0
137
141
  def zombie_acquirers(
138
142
  zombie_ttl: config['swarm.flush_zombies.zombie_ttl'],
139
143
  lock_scan_size: config['swarm.flush_zombies.zombie_lock_scan_size']
@@ -146,6 +150,7 @@ class RedisQueuedLocks::Client
146
150
  #
147
151
  # @api public
148
152
  # @since 1.9.0
153
+ # @version 1.21.0
149
154
  def zombie_hosts(zombie_ttl: config['swarm.flush_zombies.zombie_ttl'])
150
155
  swarm.zombie_hosts(zombie_ttl:)
151
156
  end
@@ -159,7 +164,7 @@ class RedisQueuedLocks::Client
159
164
  #
160
165
  # @api public
161
166
  # @since 1.9.0
162
- # @version 1.17.0
167
+ # @version 1.21.0
163
168
  def zombies_info(
164
169
  zombie_ttl: config['swarm.flush_zombies.zombie_ttl'],
165
170
  lock_scan_size: config['swarm.flush_zombies.zombie_lock_scan_size']
@@ -171,7 +176,9 @@ class RedisQueuedLocks::Client
171
176
  # @param lock_name [String]
172
177
  # Lock name to be obtained.
173
178
  # @option ttl [Integer]
174
- # Lock's time to live (in milliseconds).
179
+ # - Lock's time to live (in milliseconds);
180
+ # - a positive Integer;
181
+ # - pre-configured in `config['default_lock_ttl']`;
175
182
  # @option queue_ttl [Integer]
176
183
  # Lifetime of the acuier's lock request. In seconds.
177
184
  # @option lock_index_grace_period [Integer]
@@ -183,7 +190,8 @@ class RedisQueuedLocks::Client
183
190
  # mode: the lock index is maintained by all lock acquirements of the client);
184
191
  # - pre-configured in `config['lock_index_grace_period']`;
185
192
  # @option timeout [Integer,NilClass]
186
- # Time period whe should try to acquire the lock (in seconds). Nil means "without timeout".
193
+ # Time period whe should try to acquire the lock (in seconds). Nil means "without timeout"
194
+ # (`0` is the same as `nil`).
187
195
  # @option timed [Boolean]
188
196
  # Limit the invocation time period of the passed block of code by the lock's TTL.
189
197
  # @option retry_count [Integer,NilClass]
@@ -294,7 +302,7 @@ class RedisQueuedLocks::Client
294
302
  # - works in tandem with <config['log_sampling_percent'] and <config['log_sampler']>;
295
303
  # @option log_sampling_percent [Integer]
296
304
  # - the percent of cases that should be logged;
297
- # - take an effect when <config['log_sampling_enalbed']> is true;
305
+ # - take an effect when <config['log_sampling_enabled']> is true;
298
306
  # - works in tandem with <config['log_sampling_enabled']> and <config['log_sampler']> configs;
299
307
  # @option log_sampler [#sampling_happened?,Module<RedisQueuedLocks::Logging::Sampler>]
300
308
  # - percent-based log sampler that decides should be RQL case logged or not;
@@ -315,7 +323,7 @@ class RedisQueuedLocks::Client
315
323
  # - works in tandem with <config['instr_sampling_percent']> and <config['instr_sampler']>;
316
324
  # @option instr_sampling_percent [Integer]
317
325
  # - the percent of cases that should be instrumented;
318
- # - take an effect when <config['instr_sampling_enalbed']> is true;
326
+ # - take an effect when <config['instr_sampling_enabled']> is true;
319
327
  # - works in tandem with <config['instr_sampling_enabled']>
320
328
  # and <config['instr_sampler']> configs;
321
329
  # @option instr_sampler [#sampling_happened?,Module<RedisQueuedLocks::Instrument::Sampler>]
@@ -339,7 +347,7 @@ class RedisQueuedLocks::Client
339
347
  #
340
348
  # @api public
341
349
  # @since 1.0.0
342
- # @version 1.20.0
350
+ # @version 1.21.0
343
351
  # rubocop:disable Metrics/MethodLength
344
352
  def lock(
345
353
  lock_name,
@@ -418,7 +426,7 @@ class RedisQueuedLocks::Client
418
426
  # rubocop:disable all
419
427
  # @api public
420
428
  # @since 1.16.0
421
- # @version 1.20.0
429
+ # @version 1.21.0
422
430
  def lock_series( # steep:ignore
423
431
  *lock_names,
424
432
  detailed_result: false,
@@ -498,7 +506,7 @@ class RedisQueuedLocks::Client
498
506
  # rubocop:disable all
499
507
  # @api public
500
508
  # @since 1.16.0
501
- # @version 1.20.0
509
+ # @version 1.21.0
502
510
  def lock_series!( # steep:ignore
503
511
  *lock_names,
504
512
  detailed_result: false,
@@ -570,7 +578,7 @@ class RedisQueuedLocks::Client
570
578
  #
571
579
  # @api public
572
580
  # @since 1.0.0
573
- # @version 1.20.0
581
+ # @version 1.21.0
574
582
  def lock!(
575
583
  lock_name,
576
584
  ttl: config['default_lock_ttl'],
@@ -665,7 +673,7 @@ class RedisQueuedLocks::Client
665
673
  #
666
674
  # @api public
667
675
  # @since 1.0.0
668
- # @version 1.20.0
676
+ # @version 1.21.0
669
677
  def unlock(
670
678
  lock_name,
671
679
  logger: config['logger'],
@@ -684,8 +692,9 @@ class RedisQueuedLocks::Client
684
692
  redis_client,
685
693
  lock_name,
686
694
  config['lock_index_enabled'],
687
- instrumenter,
688
695
  logger,
696
+ instrumenter,
697
+ instrument,
689
698
  log_sampling_enabled,
690
699
  log_sampling_percent,
691
700
  log_sampler,
@@ -742,7 +751,7 @@ class RedisQueuedLocks::Client
742
751
  #
743
752
  # @api public
744
753
  # @since 1.18.0
745
- # @version 1.19.0
754
+ # @version 1.21.0
746
755
  def unlock_read(
747
756
  lock_name,
748
757
  acquirer_id: nil,
@@ -763,8 +772,8 @@ class RedisQueuedLocks::Client
763
772
  redis_client,
764
773
  lock_name,
765
774
  acquirer_id || current_acquirer_id(identity:),
766
- instrumenter,
767
775
  logger,
776
+ instrumenter,
768
777
  instrument,
769
778
  log_sampling_enabled,
770
779
  log_sampling_percent,
@@ -902,19 +911,14 @@ class RedisQueuedLocks::Client
902
911
  RedisQueuedLocks::Resource.possible_host_identifiers(identity)
903
912
  end
904
913
 
905
- # This method is non-atomic cuz redis does not provide an atomic function for TTL/PTTL extension.
906
- # So the methid is spliited into the two commands:
907
- # (1) read current pttl
908
- # (2) set new ttl that is calculated as "current pttl + additional milliseconds"
909
- # What can happen during these steps
910
- # - lock is expired between commands or before the first command;
911
- # - lock is expired before the second command;
912
- # - lock is expired AND newly acquired by another process (so you will extend the
913
- # totally new lock with fresh PTTL);
914
+ # The write lock TTL is extended atomically by one Lua script (`PTTL` + `PEXPIRE` with
915
+ # "current pttl + additional milliseconds") without the lock owner check: when your lock is
916
+ # expired and newly obtained by another acquirer, the lock of that acquirer is extended.
914
917
  # Use it at your own risk and consider async nature when calling this method.
915
918
  #
916
919
  # @param lock_name [String]
917
- # @param milliseconds [Integer] How many milliseconds should be added.
920
+ # @param milliseconds [Integer]
921
+ # How many milliseconds should be added: a positive Integer.
918
922
  # @option read_write_mode [Symbol]
919
923
  # - `:write` (default) - extends the write lock (the method description above);
920
924
  # - `:read` - extends the read lock of the current acquirer only (an expired read lock or
@@ -944,7 +948,7 @@ class RedisQueuedLocks::Client
944
948
  #
945
949
  # @api public
946
950
  # @since 1.0.0
947
- # @version 1.20.0
951
+ # @version 1.21.0
948
952
  def extend_lock_ttl(
949
953
  lock_name,
950
954
  milliseconds,
@@ -1008,7 +1012,7 @@ class RedisQueuedLocks::Client
1008
1012
  #
1009
1013
  # @api public
1010
1014
  # @since 1.0.0
1011
- # @version 1.20.0
1015
+ # @version 1.21.0
1012
1016
  def clear_locks(
1013
1017
  batch_size: config['lock_release_batch_size'],
1014
1018
  logger: config['logger'],
@@ -1097,12 +1101,12 @@ class RedisQueuedLocks::Client
1097
1101
  #
1098
1102
  # @api public
1099
1103
  # @since 1.14.0
1100
- # @version 1.20.0
1104
+ # @version 1.21.0
1101
1105
  def clear_locks_of(
1102
1106
  host_id:,
1103
1107
  acquirer_id:,
1104
1108
  lock_scan_size: config['clear_locks_of__lock_scan_size'],
1105
- queue_scan_size: config['clear_locks_of__queue_scan_size'], # steep:ingore
1109
+ queue_scan_size: config['clear_locks_of__queue_scan_size'],
1106
1110
  logger: config['logger'],
1107
1111
  instrumenter: config['instrumenter'],
1108
1112
  instrument: nil,
@@ -1140,7 +1144,8 @@ class RedisQueuedLocks::Client
1140
1144
  # Release all locks of the current acquirer/host and
1141
1145
  # remove the current acquirer/host from all queues;
1142
1146
  #
1143
- # @option batch_size [Integer]
1147
+ # @option lock_scan_size [Integer]
1148
+ # @option queue_scan_size [Integer]
1144
1149
  # @option logger [::Logger,#debug]
1145
1150
  # @option instrumenter [#notify] See `config['instrumenter']` docs for details.
1146
1151
  # @option instrument [NilClass,Any]
@@ -1159,9 +1164,10 @@ class RedisQueuedLocks::Client
1159
1164
  #
1160
1165
  # @api public
1161
1166
  # @since 1.14.0
1167
+ # @version 1.21.0
1162
1168
  def clear_current_locks(
1163
1169
  lock_scan_size: config['clear_locks_of__lock_scan_size'],
1164
- queue_scan_size: config['clear_locks_of__queue_scan_size'], # steep:ingore
1170
+ queue_scan_size: config['clear_locks_of__queue_scan_size'],
1165
1171
  logger: config['logger'],
1166
1172
  instrumenter: config['instrumenter'],
1167
1173
  instrument: nil,
@@ -1216,7 +1222,7 @@ class RedisQueuedLocks::Client
1216
1222
  #
1217
1223
  # @api public
1218
1224
  # @since 1.0.0
1219
- # @version 1.20.0
1225
+ # @version 1.21.0
1220
1226
  def locks(scan_size: config['key_extraction_batch_size'], with_info: false)
1221
1227
  RedisQueuedLocks::Acquirer::Locks.locks(
1222
1228
  redis_client, scan_size:, with_info:, lock_index_enabled: config['lock_index_enabled']
@@ -1230,8 +1236,9 @@ class RedisQueuedLocks::Client
1230
1236
  #
1231
1237
  # @api public
1232
1238
  # @since 1.0.0
1233
- def locks_info(scan_size: config['key_extraction_batch_size']) # steep:ignore
1234
- locks(scan_size:, with_info: true)
1239
+ # @version 1.21.0
1240
+ def locks_info(scan_size: config['key_extraction_batch_size'])
1241
+ locks(scan_size:, with_info: true) #: RedisQueuedLocks::Acquirer::Locks::locksInfo
1235
1242
  end
1236
1243
 
1237
1244
  # @option scan_size [Integer]
@@ -1253,7 +1260,7 @@ class RedisQueuedLocks::Client
1253
1260
  #
1254
1261
  # @api public
1255
1262
  # @since 1.0.0
1256
- # @version 1.20.0
1263
+ # @version 1.21.0
1257
1264
  def queues(scan_size: config['key_extraction_batch_size'], with_info: false)
1258
1265
  RedisQueuedLocks::Acquirer::Queues.queues(
1259
1266
  redis_client, scan_size:, with_info:, lock_index_enabled: config['lock_index_enabled']
@@ -1267,8 +1274,9 @@ class RedisQueuedLocks::Client
1267
1274
  #
1268
1275
  # @api public
1269
1276
  # @since 1.0.0
1277
+ # @version 1.21.0
1270
1278
  def queues_info(scan_size: config['key_extraction_batch_size'])
1271
- queues(scan_size:, with_info: true)
1279
+ queues(scan_size:, with_info: true) #: RedisQueuedLocks::Acquirer::Queues::queuesInfo
1272
1280
  end
1273
1281
 
1274
1282
  # @option scan_size [Integer]
@@ -1279,7 +1287,7 @@ class RedisQueuedLocks::Client
1279
1287
  #
1280
1288
  # @api public
1281
1289
  # @since 1.0.0
1282
- # @version 1.20.0
1290
+ # @version 1.21.0
1283
1291
  def keys(scan_size: config['key_extraction_batch_size'])
1284
1292
  RedisQueuedLocks::Acquirer::Keys.keys(
1285
1293
  redis_client, scan_size:, lock_index_enabled: config['lock_index_enabled']
@@ -1310,7 +1318,7 @@ class RedisQueuedLocks::Client
1310
1318
  #
1311
1319
  # @api public
1312
1320
  # @since 1.0.0
1313
- # @version 1.20.0
1321
+ # @version 1.21.0
1314
1322
  def clear_dead_requests(
1315
1323
  dead_ttl: config['dead_request_ttl'],
1316
1324
  scan_size: config['lock_release_batch_size'],
@@ -1365,21 +1373,55 @@ class RedisQueuedLocks::Client
1365
1373
  # @option lock_index_grace_period [Integer]
1366
1374
  # - the lock index score margin over the key lifetime (in milliseconds);
1367
1375
  # - `config['lock_index_grace_period']` is used by default;
1376
+ # @option logger [::Logger,#debug]
1377
+ # @option instrumenter [#notify]
1378
+ # @option instrument [NilClass,Any]
1379
+ # @option log_sampling_enabled [Boolean]
1380
+ # @option log_sampling_percent [Integer]
1381
+ # @option log_sampler [#sampling_happened?,Module<RedisQueuedLocks::Logging::Sampler>]
1382
+ # @option log_sample_this [Boolean]
1383
+ # @option instr_sampling_enabled [Boolean]
1384
+ # @option instr_sampling_percent [Integer]
1385
+ # @option instr_sampler [#sampling_happened?,Module<RedisQueuedLocks::Instrument::Sampler>]
1386
+ # @option instr_sample_this [Boolean]
1368
1387
  # @return [Hash<Symbol,Boolean|Hash<Symbol,Integer>>]
1369
1388
  # Format: { ok: true, result: { idx_key_cnt: Integer } } (the number of indexed keys)
1370
1389
  #
1371
1390
  # @api public
1372
1391
  # @since 1.20.0
1392
+ # @version 1.21.0
1373
1393
  def reindex_locks(
1374
1394
  scan_size: config['lock_release_batch_size'],
1375
1395
  queue_ttl: config['default_queue_ttl'],
1376
- lock_index_grace_period: config['lock_index_grace_period']
1396
+ lock_index_grace_period: config['lock_index_grace_period'],
1397
+ logger: config['logger'],
1398
+ instrumenter: config['instrumenter'],
1399
+ instrument: nil,
1400
+ log_sampling_enabled: config['log_sampling_enabled'],
1401
+ log_sampling_percent: config['log_sampling_percent'],
1402
+ log_sampler: config['log_sampler'],
1403
+ log_sample_this: false,
1404
+ instr_sampling_enabled: config['instr_sampling_enabled'],
1405
+ instr_sampling_percent: config['instr_sampling_percent'],
1406
+ instr_sampler: config['instr_sampler'],
1407
+ instr_sample_this: false
1377
1408
  )
1378
1409
  RedisQueuedLocks::Acquirer::ReindexLocks.reindex_locks(
1379
1410
  redis_client,
1380
1411
  scan_size,
1381
1412
  queue_ttl,
1382
- lock_index_grace_period
1413
+ lock_index_grace_period,
1414
+ logger,
1415
+ instrumenter,
1416
+ instrument,
1417
+ log_sampling_enabled,
1418
+ log_sampling_percent,
1419
+ log_sampler,
1420
+ log_sample_this,
1421
+ instr_sampling_enabled,
1422
+ instr_sampling_percent,
1423
+ instr_sampler,
1424
+ instr_sample_this
1383
1425
  )
1384
1426
  end
1385
1427
  end
@@ -2,7 +2,6 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.13.0
5
- # @version 1.20.0
6
5
  # rubocop:disable Metrics/ClassLength
7
6
  class RedisQueuedLocks::Config
8
7
  require_relative 'config/dsl'
@@ -57,36 +56,36 @@ class RedisQueuedLocks::Config
57
56
  setting('swarm.flush_zombies.redis_config.pool_config', {})
58
57
 
59
58
  validate('swarm.auto_swarm') { |val| val == true || val == false }
60
- validate('swarm.supervisor.liveness_probing_period') { |val| val.is_a?(Integer) }
59
+ validate('swarm.supervisor.liveness_probing_period') { |val| val.is_a?(Integer) && val > 0 }
61
60
 
62
61
  validate('swarm.probe_hosts.enabled_for_swarm') { |val| val == true || val == false }
63
62
  validate('swarm.probe_hosts.redis_config.sentinel') { |val| val == true || val == false }
64
63
  validate('swarm.probe_hosts.redis_config.pooled') { |val| val == true || val == false }
65
64
  validate('swarm.probe_hosts.redis_config.config') { |val| val.is_a?(Hash) }
66
65
  validate('swarm.probe_hosts.redis_config.pool_config') { |val| val.is_a?(Hash) }
67
- validate('swarm.probe_hosts.probe_period') { |val| val.is_a?(Integer) }
66
+ validate('swarm.probe_hosts.probe_period') { |val| val.is_a?(Integer) && val > 0 }
68
67
 
69
68
  validate('swarm.flush_zombies.enabled_for_swarm') { |val| val == true || val == false }
70
69
  validate('swarm.flush_zombies.redis_config.sentinel') { |val| val == true || val == false }
71
70
  validate('swarm.flush_zombies.redis_config.pooled') { |val| val == true || val == false }
72
71
  validate('swarm.flush_zombies.redis_config.config') { |val| val.is_a?(Hash) }
73
72
  validate('swarm.flush_zombies.redis_config.pool_config') { |val| val.is_a?(Hash) }
74
- validate('swarm.flush_zombies.zombie_ttl') { |val| val.is_a?(Integer) }
75
- validate('swarm.flush_zombies.zombie_lock_scan_size') { |val| val.is_a?(Integer) }
76
- validate('swarm.flush_zombies.zombie_queue_scan_size') { |val| val.is_a?(Integer) }
77
- validate('swarm.flush_zombies.zombie_flush_period') { |val| val.is_a?(Integer) }
73
+ validate('swarm.flush_zombies.zombie_ttl') { |val| val.is_a?(Integer) && val > 0 }
74
+ validate('swarm.flush_zombies.zombie_lock_scan_size') { |val| val.is_a?(Integer) && val > 0 }
75
+ validate('swarm.flush_zombies.zombie_queue_scan_size') { |val| val.is_a?(Integer) && val > 0 }
76
+ validate('swarm.flush_zombies.zombie_flush_period') { |val| val.is_a?(Integer) && val > 0 }
78
77
 
79
78
  validate('retry_count') { |val| val == nil || (val.is_a?(Integer) && val >= 0) }
80
79
  validate('retry_delay') { |val| val.is_a?(Integer) && val >= 0 }
81
80
  validate('retry_jitter') { |val| val.is_a?(Integer) && val >= 0 }
82
81
  validate('try_to_lock_timeout') { |val| val == nil || (val.is_a?(Integer) && val >= 0) }
83
- validate('default_lock_ttl') { |val| val.is_a?(Integer) }
84
- validate('default_queue_ttl') { |val| val.is_a?(Integer) }
82
+ validate('default_lock_ttl') { |val| val.is_a?(Integer) && val > 0 }
83
+ validate('default_queue_ttl') { |val| val.is_a?(Integer) && val > 0 }
85
84
  validate('detailed_acq_timeout_error') { |val| val == true || val == false }
86
- validate('lock_release_batch_size') { |val| val.is_a?(Integer) }
87
- validate('clear_locks_of__lock_scan_size') { |val| val.is_a?(Integer) }
88
- validate('clear_locks_of__queue_scan_size') { |val| val.is_a?(Integer) }
89
- validate('key_extraction_batch_size') { |val| val.is_a?(Integer) }
85
+ validate('lock_release_batch_size') { |val| val.is_a?(Integer) && val > 0 }
86
+ validate('clear_locks_of__lock_scan_size') { |val| val.is_a?(Integer) && val > 0 }
87
+ validate('clear_locks_of__queue_scan_size') { |val| val.is_a?(Integer) && val > 0 }
88
+ validate('key_extraction_batch_size') { |val| val.is_a?(Integer) && val > 0 }
90
89
  validate('instrumenter') { |val| RedisQueuedLocks::Instrument.valid_interface?(val) }
91
90
  validate('uniq_identifier') { |val| val.is_a?(Proc) }
92
91
  validate('logger') { |val| RedisQueuedLocks::Logging.valid_interface?(val) }
@@ -4,6 +4,12 @@
4
4
  # the one-line definition style (like "Error = Class.new(::StandardError)")
5
5
  # is not used cuz that style can not be correctly defined via RBS types: we can not
6
6
  # identify the superclass of our class for a constant;
7
+ # NOTE: (error hierarchy) each error inherits from the global base error of its branch:
8
+ # - `Error` (`::StandardError`) - the main hierarchy;
9
+ # - `ArgumentError` (`::ArgumentError`) - invalid arguments (the Ruby core branch that can not be
10
+ # a part of the main hierarchy);
11
+ # - `Exception` (`::Exception`) - super exceptional cases only: each error of this branch
12
+ # describes why it is not a `StandardError`;
7
13
  module RedisQueuedLocks
8
14
  # @api public
9
15
  # @since 1.0.0
@@ -13,13 +19,29 @@ module RedisQueuedLocks
13
19
  # @since 1.0.0
14
20
  class ArgumentError < ::ArgumentError; end
15
21
 
22
+ # The base of the errors that should not be rescued by the code they interrupt
23
+ # (`rescue => e`, `rescue StandardError`, `rescue nil`): like `Timeout::ExitException`,
24
+ # they are rescued only by `rescue Exception` (and by the library itself).
25
+ # NOTE: inside `module RedisQueuedLocks` a bare `Exception` refers to this class;
26
+ #
27
+ # @api private
28
+ # @since 1.21.0
29
+ class Exception < ::Exception; end # rubocop:disable Lint/InheritException
30
+
16
31
  # @api public
17
32
  # @since 1.0.0
18
33
  class LockAlreadyObtainedError < Error; end
19
34
 
35
+ # Interrupts the lock acquirement after the `timeout` (`Timeout.timeout`); rescued by
36
+ # `WithAcqTimeout` (=> `LockAcquirementTimeoutError` / `:timeout_reached`).
37
+ # NOTE: (why `Exception`) the acquirement calls the logger and the instrumenter: a `StandardError`
38
+ # that interrupts them is swallowed by `rescue nil` of the log/instrumentation events, and the
39
+ # acquirement continues without the timeout (with `retry_count: nil` - without any limit);
40
+ #
20
41
  # @api private
21
42
  # @since 1.11.0
22
- class LockAcquirementIntermediateTimeoutError < ::Timeout::Error; end
43
+ # @version 1.21.0
44
+ class LockAcquirementIntermediateTimeoutError < Exception; end
23
45
 
24
46
  # @api public
25
47
  # @since 1.0.0
@@ -29,11 +51,19 @@ module RedisQueuedLocks
29
51
  # @since 1.0.0
30
52
  class LockAcquirementRetryLimitError < Error; end
31
53
 
54
+ # Interrupts the `timed: true` block after the lock ttl (`Timeout.timeout`); rescued by
55
+ # `YieldExpire` (=> `TimedLockTimeoutError`).
56
+ # NOTE: (why `Exception`) a `StandardError` is swallowed by `rescue => e` / `rescue nil` of
57
+ # the user block: the block continues after the lock ttl (without the mutual exclusion:
58
+ # another acquirer can obtain the expired lock, and the write lock release after the block
59
+ # deletes its lock);
60
+ #
32
61
  # @api private
33
62
  # @since 1.12.0
34
- class TimedLockIntermediateTimeoutError < ::Timeout::Error; end
63
+ # @version 1.21.0
64
+ class TimedLockIntermediateTimeoutError < Exception; end
35
65
 
36
- # @api pulic
66
+ # @api public
37
67
  # @since 1.0.0
38
68
  class TimedLockTimeoutError < Error; end
39
69
 
@@ -57,7 +87,7 @@ module RedisQueuedLocks
57
87
  # @since 1.13.0
58
88
  class ConfigNotFoundError < ConfigError; end
59
89
 
60
- # @api pub;ic
90
+ # @api public
61
91
  # @since 1.13.0
62
92
  class ConfigValidationError < ConfigError; end
63
93
  end
@@ -3,12 +3,6 @@
3
3
  # @api public
4
4
  # @since 1.6.0
5
5
  module RedisQueuedLocks::Instrument::Sampler
6
- # @return [Range]
7
- #
8
- # @api private
9
- # @since 1.6.0
10
- SAMPLING_PERCENT_RANGE = (0..100)
11
-
12
6
  class << self
13
7
  # Super simple probalistic function based on the `weight` of <true>/<false> values.
14
8
  # Requires the <percent> parameter as the required percent of <true> values sampled.
@@ -20,8 +14,11 @@ module RedisQueuedLocks::Instrument::Sampler
20
14
  #
21
15
  # @api public
22
16
  # @since 1.6.0
17
+ # @version 1.21.0
23
18
  def sampling_happened?(sampling_percent)
24
- sampling_percent >= SecureRandom.rand(SAMPLING_PERCENT_RANGE)
19
+ # NOTE: one of 100 equally probable values (0..99): `sampling_percent` of them are lower
20
+ # (0% => never, 100% => always);
21
+ SecureRandom.rand(100) < sampling_percent
25
22
  end
26
23
  end
27
24
  end
@@ -3,12 +3,6 @@
3
3
  # @api public
4
4
  # @since 1.5.0
5
5
  module RedisQueuedLocks::Logging::Sampler
6
- # @return [Range]
7
- #
8
- # @api private
9
- # @since 1.6.0
10
- SAMPLING_PERCENT_RANGE = (0..100)
11
-
12
6
  class << self
13
7
  # Super simple probalistic function based on the `weight` of <true>/<false> values.
14
8
  # Requires the <percent> parameter as the required percent of <true> values sampled.
@@ -20,9 +14,11 @@ module RedisQueuedLocks::Logging::Sampler
20
14
  #
21
15
  # @api public
22
16
  # @since 1.5.0
23
- # @version 1.6.0
17
+ # @version 1.21.0
24
18
  def sampling_happened?(sampling_percent)
25
- sampling_percent >= SecureRandom.rand(SAMPLING_PERCENT_RANGE)
19
+ # NOTE: one of 100 equally probable values (0..99): `sampling_percent` of them are lower
20
+ # (0% => never, 100% => always);
21
+ SecureRandom.rand(100) < sampling_percent
26
22
  end
27
23
  end
28
24
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.0.0
5
+ # @version 1.20.0
5
6
  # rubocop:disable Metrics/ModuleLength
6
7
  module RedisQueuedLocks::Resource
7
8
  # @return [String]
@@ -366,14 +367,23 @@ module RedisQueuedLocks::Resource
366
367
  (acquirer_position + queue_ttl) < Time.now.to_f
367
368
  end
368
369
 
369
- # @param lock_queue [String]
370
- # @return [String]
370
+ # NOTE: (debug) not used by the library: a manual runtime check (in a console) of the lock that
371
+ # a queue belongs to (e.g. `Resource.lock_key_from_queue(client.queues.first)`);
372
+ #
373
+ # @param lock_queue [String] `rql:lock_queue:<name>` or `rql:lock_read_queue:<name>`.
374
+ # @return [String,NilClass]
375
+ # - `rql:lock:<name>` for the write lock request queue and for the read lock request queue;
376
+ # - `nil` for any other key;
371
377
  #
372
378
  # @api private
373
379
  # @since 1.0.0
380
+ # @version 1.21.0
374
381
  def lock_key_from_queue(lock_queue)
375
- # NOTE: 15 is a start position of the lock name
376
- "rql:lock:#{lock_queue[15..]}"
382
+ key_family = lock_key_family(lock_queue)
383
+ return unless key_family == :lock_queue || key_family == :read_lock_queue
384
+
385
+ lock_name = lock_name_from_key(lock_queue, key_family) #: String
386
+ prepare_lock_key(lock_name)
377
387
  end
378
388
 
379
389
  # @return [Integer]
@@ -2,6 +2,7 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.9.0
5
+ # @version 1.20.0
5
6
  # rubocop:disable Metrics/ClassLength
6
7
  class RedisQueuedLocks::Swarm::FlushZombies < RedisQueuedLocks::Swarm::SwarmElement::Isolated
7
8
  class << self
@@ -7,19 +7,23 @@ module RedisQueuedLocks::Swarm::RedisClientBuilder
7
7
  # @option pooled [Boolean]
8
8
  # @option sentinel [Boolean]
9
9
  # @option config [Hash]
10
+ # @option pool_config [Hash]
10
11
  # @return [RedisClient|RedisClient::Pooled]
11
12
  #
12
13
  # @api private
13
14
  # @since 1.9.0
15
+ # @version 1.21.0
14
16
  # rubocop:disable Style/RedundantAssignment
15
17
  def build(pooled: false, sentinel: false, config: {}, pool_config: {})
16
- config.transform_keys!(&:to_sym)
17
- pool_config.transform_keys!(&:to_sym)
18
+ # NOTE: the passed hashes are not mutated: they are the values of the client config (they
19
+ # can be frozen and are shared with the zombie flusher Ractor when they are shareable);
20
+ redis_options = config.transform_keys(&:to_sym)
21
+ pool_options = pool_config.transform_keys(&:to_sym)
18
22
 
19
23
  redis_config =
20
- sentinel ? sentinel_config(config) : non_sentinel_config(config)
24
+ sentinel ? sentinel_config(redis_options) : non_sentinel_config(redis_options)
21
25
  redis_client =
22
- pooled ? pooled_client(redis_config, pool_config) : non_pooled_client(redis_config)
26
+ pooled ? pooled_client(redis_config, pool_options) : non_pooled_client(redis_config)
23
27
 
24
28
  redis_client
25
29
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.9.0
5
+ # @version 1.17.0
5
6
  # rubocop:disable Metrics/ClassLength
6
7
  class RedisQueuedLocks::Swarm::SwarmElement::Threaded
7
8
  # @since 1.9.0
@@ -78,7 +79,7 @@ class RedisQueuedLocks::Swarm::SwarmElement::Threaded
78
79
  # @return [void]
79
80
  #
80
81
  # @api private
81
- # @since 19.0.0
82
+ # @since 1.9.0
82
83
  def reswarm_if_dead!
83
84
  return unless enabled?
84
85
 
@@ -154,7 +155,7 @@ class RedisQueuedLocks::Swarm::SwarmElement::Threaded
154
155
 
155
156
  # Swarm element lifecycle have the following flow:
156
157
  # => 1) init (swarm!): create a thread, main loop is not started;
157
- # => 2) start (swarm_loop__start): run the main lopp inside the created thread;
158
+ # => 2) start (swarm_loop__start): run the main loop inside the created thread;
158
159
  # => 3) stop (swarm_loop__stop): stop the main loop inside the created thread;
159
160
  # => 4) terminate (swarm_element__termiante): kill the created thread;
160
161
  # In addition you should implement `#spawn_main_loop!` method that will be the main logic