redis_queued_locks 1.17.0 → 1.18.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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/project-overview.md +86 -10
  3. data/.claude/rules/acquirer.md +31 -11
  4. data/.claude/rules/arguments.md +2 -1
  5. data/.claude/rules/git.md +46 -0
  6. data/.claude/rules/logic.md +9 -3
  7. data/.claude/rules/swarm.md +4 -3
  8. data/.claude/rules/tests.md +9 -3
  9. data/.claude/rules/type-checking.md +10 -2
  10. data/.claude/rules/visitors.md +12 -7
  11. data/CHANGELOG.md +62 -0
  12. data/CLAUDE.md +7 -2
  13. data/README.md +348 -79
  14. data/lib/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue/log_visitor.rb +6 -3
  15. data/lib/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue.rb +9 -7
  16. data/lib/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rb +20 -10
  17. data/lib/redis_queued_locks/acquirer/acquire_lock/log_visitor.rb +34 -16
  18. data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock/log_visitor.rb +240 -28
  19. data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rb +398 -129
  20. data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire/log_visitor.rb +12 -6
  21. data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire.rb +32 -8
  22. data/lib/redis_queued_locks/acquirer/acquire_lock.rb +73 -20
  23. data/lib/redis_queued_locks/acquirer/clear_dead_requests.rb +9 -1
  24. data/lib/redis_queued_locks/acquirer/extend_lock_ttl.rb +182 -3
  25. data/lib/redis_queued_locks/acquirer/is_locked.rb +19 -2
  26. data/lib/redis_queued_locks/acquirer/is_queued.rb +6 -2
  27. data/lib/redis_queued_locks/acquirer/lock_info.rb +96 -6
  28. data/lib/redis_queued_locks/acquirer/lock_series_poc/instr_visitor.rb +8 -2
  29. data/lib/redis_queued_locks/acquirer/lock_series_poc/log_visitor.rb +18 -6
  30. data/lib/redis_queued_locks/acquirer/lock_series_poc.rb +126 -27
  31. data/lib/redis_queued_locks/acquirer/locks.rb +103 -6
  32. data/lib/redis_queued_locks/acquirer/queue_info.rb +38 -9
  33. data/lib/redis_queued_locks/acquirer/queues.rb +28 -4
  34. data/lib/redis_queued_locks/acquirer/release_all_locks.rb +35 -1
  35. data/lib/redis_queued_locks/acquirer/release_lock.rb +40 -9
  36. data/lib/redis_queued_locks/acquirer/release_locks_of.rb +43 -3
  37. data/lib/redis_queued_locks/acquirer/release_read_lock.rb +143 -0
  38. data/lib/redis_queued_locks/acquirer.rb +1 -0
  39. data/lib/redis_queued_locks/client.rb +129 -14
  40. data/lib/redis_queued_locks/resource.rb +117 -9
  41. data/lib/redis_queued_locks/swarm/flush_zombies.rb +38 -0
  42. data/lib/redis_queued_locks/swarm/zombie_info.rb +42 -0
  43. data/lib/redis_queued_locks/version.rb +1 -1
  44. data/rbs_collection.lock.yaml +10 -2
  45. data/rbs_collection.yaml +0 -2
  46. data/sig/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue/log_visitor.rbs +2 -1
  47. data/sig/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue.rbs +0 -1
  48. data/sig/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rbs +5 -0
  49. data/sig/redis_queued_locks/acquirer/acquire_lock/log_visitor.rbs +12 -6
  50. data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock/log_visitor.rbs +74 -7
  51. data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rbs +4 -2
  52. data/sig/redis_queued_locks/acquirer/acquire_lock/yield_expire.rbs +3 -0
  53. data/sig/redis_queued_locks/acquirer/acquire_lock/yield_with_expire/log_visitor.rbs +4 -2
  54. data/sig/redis_queued_locks/acquirer/extend_lock_ttl.rbs +19 -1
  55. data/sig/redis_queued_locks/acquirer/lock_info.rbs +9 -1
  56. data/sig/redis_queued_locks/acquirer/locks.rbs +10 -1
  57. data/sig/redis_queued_locks/acquirer/queue_info.rbs +2 -4
  58. data/sig/redis_queued_locks/acquirer/queues.rbs +1 -1
  59. data/sig/redis_queued_locks/acquirer/release_lock.rbs +8 -1
  60. data/sig/redis_queued_locks/acquirer/release_read_lock.rbs +44 -0
  61. data/sig/redis_queued_locks/client.rbs +22 -2
  62. data/sig/redis_queued_locks/resource.rbs +9 -2
  63. data/sig/redis_queued_locks/swarm/zombie_info.rbs +5 -0
  64. metadata +4 -1
@@ -11,11 +11,12 @@ module RedisQueuedLocks::Acquirer::AcquireLock::YieldExpire::LogVisitor
11
11
  # @param acquirer_id [String]
12
12
  # @param host_id [String]
13
13
  # @param access_strategy [Symbol]
14
+ # @param rw_mode [Symbol]
14
15
  # @return [void]
15
16
  #
16
17
  # @api private
17
18
  # @since 1.7.0
18
- # @version 1.9.0
19
+ # @version 1.18.0
19
20
  def expire_lock(
20
21
  logger,
21
22
  log_sampled,
@@ -23,7 +24,8 @@ module RedisQueuedLocks::Acquirer::AcquireLock::YieldExpire::LogVisitor
23
24
  queue_ttl,
24
25
  acquirer_id,
25
26
  host_id,
26
- access_strategy
27
+ access_strategy,
28
+ rw_mode
27
29
  )
28
30
  return unless log_sampled
29
31
 
@@ -33,7 +35,8 @@ module RedisQueuedLocks::Acquirer::AcquireLock::YieldExpire::LogVisitor
33
35
  "queue_ttl => #{queue_ttl} " \
34
36
  "acq_id => '#{acquirer_id}' " \
35
37
  "hst_id => '#{host_id}' " \
36
- "acs_strat => '#{access_strategy}'"
38
+ "acs_strat => '#{access_strategy}' " \
39
+ "rw_mode => '#{rw_mode}'"
37
40
  end rescue nil
38
41
  end
39
42
 
@@ -45,11 +48,12 @@ module RedisQueuedLocks::Acquirer::AcquireLock::YieldExpire::LogVisitor
45
48
  # @param acquirer_id [String]
46
49
  # @param host_id [String]
47
50
  # @param access_strategy [Symbol]
51
+ # @param rw_mode [Symbol]
48
52
  # @return [void]
49
53
  #
50
54
  # @api private
51
55
  # @since 1.7.0
52
- # @version 1.9.0
56
+ # @version 1.18.0
53
57
  def decrease_lock(
54
58
  logger,
55
59
  log_sampled,
@@ -58,7 +62,8 @@ module RedisQueuedLocks::Acquirer::AcquireLock::YieldExpire::LogVisitor
58
62
  queue_ttl,
59
63
  acquirer_id,
60
64
  host_id,
61
- access_strategy
65
+ access_strategy,
66
+ rw_mode
62
67
  )
63
68
  return unless log_sampled
64
69
 
@@ -69,7 +74,8 @@ module RedisQueuedLocks::Acquirer::AcquireLock::YieldExpire::LogVisitor
69
74
  "queue_ttl => #{queue_ttl} " \
70
75
  "acq_id => '#{acquirer_id}' " \
71
76
  "hst_id => '#{host_id}' " \
72
- "acs_strat => '#{access_strategy}'"
77
+ "acs_strat => '#{access_strategy}' " \
78
+ "rw_mode => '#{rw_mode}'"
73
79
  end rescue nil
74
80
  end
75
81
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.3.0
5
- # @version 1.7.0
5
+ # @version 1.18.0
6
6
  module RedisQueuedLocks::Acquirer::AcquireLock::YieldExpire
7
7
  require_relative 'yield_expire/log_visitor'
8
8
 
@@ -18,6 +18,11 @@ module RedisQueuedLocks::Acquirer::AcquireLock::YieldExpire
18
18
  # @param redis [RedisClient] Redis connection.
19
19
  # @param logger [::Logger,#debug] Logger object.
20
20
  # @param lock_key [String] Obtained lock key that should be expired.
21
+ # @param lock_readers_key [String] Registry of read lock holders.
22
+ # @param read_lock_key [String] Read lock data of the current acquirer.
23
+ # @param rw_mode [Symbol]
24
+ # - The mode of the obtained lock (`:read` or `:write`);
25
+ # - `:read` => only the read lock of the current acquirer is released/decreased;
21
26
  # @param acquirer_id [String] Acquirer identifier.
22
27
  # @param host_id [String] Host identifier.
23
28
  # @param access_strategy [Symbol] Lock obtaining strategy.
@@ -38,12 +43,15 @@ module RedisQueuedLocks::Acquirer::AcquireLock::YieldExpire
38
43
  #
39
44
  # @api private
40
45
  # @since 1.3.0
41
- # @version 1.9.0
46
+ # @version 1.18.0
42
47
  # rubocop:disable Metrics/MethodLength
43
48
  def yield_expire(
44
49
  redis,
45
50
  logger,
46
51
  lock_key,
52
+ lock_readers_key,
53
+ read_lock_key,
54
+ rw_mode,
47
55
  acquirer_id,
48
56
  host_id,
49
57
  access_strategy,
@@ -97,9 +105,17 @@ module RedisQueuedLocks::Acquirer::AcquireLock::YieldExpire
97
105
  if should_expire # TODO: comment all cases/examples when should_expire is true
98
106
  LogVisitor.expire_lock(
99
107
  logger, log_sampled, lock_key,
100
- queue_ttl, acquirer_id, host_id, access_strategy
108
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode
101
109
  )
102
- redis.call('EXPIRE', lock_key, '0')
110
+ if rw_mode == :read
111
+ # NOTE: release only the read lock of the current acquirer (other readers keep working)
112
+ redis.multi do |transact|
113
+ transact.call('ZREM', lock_readers_key, acquirer_id)
114
+ transact.call('DEL', read_lock_key)
115
+ end
116
+ else
117
+ redis.call('EXPIRE', lock_key, '0')
118
+ end
103
119
  elsif should_decrease # TODO: comment all cases/examples when should_expire is true
104
120
  finish_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, :millisecond)
105
121
  # @type var initial_time: Integer
@@ -110,11 +126,19 @@ module RedisQueuedLocks::Acquirer::AcquireLock::YieldExpire
110
126
  if decreased_ttl > 0
111
127
  LogVisitor.decrease_lock(
112
128
  logger, log_sampled, lock_key,
113
- decreased_ttl, queue_ttl, acquirer_id, host_id, access_strategy
129
+ decreased_ttl, queue_ttl, acquirer_id, host_id, access_strategy, rw_mode
114
130
  )
115
- # NOTE:# NOTE: EVAL signature -> <lua script>, (number of keys), *(keys), *(arguments)
116
- redis.call('EVAL', DECREASE_LOCK_PTTL, 1, lock_key, decreased_ttl)
117
- # TODO: upload scripts to the redis
131
+ if rw_mode == :read
132
+ # NOTE:
133
+ # - decrease the read lock expiration of the current acquirer;
134
+ # - XX => do nothing if the read lock is already expired and removed from the registry;
135
+ # - the read lock data TTL is not decreased (it is not used without the read lock);
136
+ redis.call('ZADD', lock_readers_key, 'XX', 'INCR', -decreased_ttl, acquirer_id)
137
+ else
138
+ # NOTE:# NOTE: EVAL signature -> <lua script>, (number of keys), *(keys), *(arguments)
139
+ redis.call('EVAL', DECREASE_LOCK_PTTL, 1, lock_key, decreased_ttl)
140
+ # TODO: upload scripts to the redis
141
+ end
118
142
  end
119
143
  end
120
144
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.0.0
5
- # @version 1.7.0
5
+ # @version 1.18.0
6
6
  # rubocop:disable Metrics/ModuleLength
7
7
  # rubocop:disable Metrics/MethodLength
8
8
  # rubocop:disable Metrics/ClassLength
@@ -96,7 +96,15 @@ module RedisQueuedLocks::Acquirer::AcquireLock
96
96
  # - `:wait_for_lock`;
97
97
  # - `:dead_locking`;
98
98
  # @option read_write_mode [Symbol]
99
- # - ?
99
+ # - `:write` (default) - exclusive lock: waits for the write lock and for all read locks;
100
+ # - `:read` - shared lock: waits for the write lock only (read locks do not block each other);
101
+ # - `:queued` access strategy orders read and write requests in FIFO between modes
102
+ # (read request waits for earlier write requests, write request waits for earlier
103
+ # read requests and for earlier write requests);
104
+ # - `:meta` of the read lock is stored in the read lock data of the acquirer;
105
+ # - same-process conflicts: read-to-write lock upgrade fails with
106
+ # `RedisQueuedLocks::ConflictLockObtainError` for any conflict strategy except
107
+ # `:wait_for_lock` (it waits for the read lock expiration);
100
108
  # @option access_strategy [Symbol]
101
109
  # - The way in which the lock will be obtained;
102
110
  # - By default it uses `:queued` strategy;
@@ -158,7 +166,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock
158
166
  #
159
167
  # @api private
160
168
  # @since 1.0.0
161
- # @version 1.14.0
169
+ # @version 1.18.0
162
170
  def acquire_lock(
163
171
  redis,
164
172
  lock_name,
@@ -206,6 +214,24 @@ module RedisQueuedLocks::Acquirer::AcquireLock
206
214
  )
207
215
  end
208
216
 
217
+ # Step 0.1 (RW): prevent :read_write_mode incompatabilities
218
+ if read_write_mode != :read && read_write_mode != :write
219
+ raise(
220
+ RedisQueuedLocks::ArgumentError,
221
+ "`:read_write_mode` argument should be `:read` or `:write`, " \
222
+ "got #{read_write_mode.inspect}."
223
+ )
224
+ end
225
+
226
+ # Step 0.1 (RW): prevent read lock incompatabilities
227
+ # - read lock expiration time is calculated from the lock ttl;
228
+ if read_write_mode == :read && (!ttl.is_a?(::Integer) || ttl <= 0)
229
+ raise(
230
+ RedisQueuedLocks::ArgumentError,
231
+ "`:ttl` argument should be a positive Integer for read locks, got #{ttl.inspect}."
232
+ )
233
+ end
234
+
209
235
  # Step 0.2: prevent :meta incompatabiltiies (structure)
210
236
  if meta.is_a?(::Hash) && (meta.any? do |key, _value|
211
237
  key == 'acq_id' ||
@@ -218,13 +244,14 @@ module RedisQueuedLocks::Acquirer::AcquireLock
218
244
  key == 'spc_cnt' ||
219
245
  key == 'l_spc_ext_ini_ttl' ||
220
246
  key == 'l_spc_ext_ts' ||
221
- key == 'l_spc_ts'
247
+ key == 'l_spc_ts' ||
248
+ key == 'rw_mode'
222
249
  end)
223
250
  raise(
224
251
  RedisQueuedLocks::ArgumentError,
225
252
  '`:meta` keys can not overlap reserved lock data keys ' \
226
253
  '"acq_id", "hst_id", "ts", "ini_ttl", "lock_key", "rem_ttl", "spc_cnt", ' \
227
- '"spc_ext_ttl", "l_spc_ext_ini_ttl", "l_spc_ext_ts", "l_spc_ts"'
254
+ '"spc_ext_ttl", "l_spc_ext_ini_ttl", "l_spc_ext_ts", "l_spc_ts", "rw_mode"'
228
255
  )
229
256
  end
230
257
 
@@ -246,8 +273,10 @@ module RedisQueuedLocks::Acquirer::AcquireLock
246
273
  lock_key = RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
247
274
  lock_key_queue = RedisQueuedLocks::Resource.prepare_lock_queue(lock_name)
248
275
 
276
+ # NOTE: (RW) write lock requests use the classic lock queue (lock_key_queue)
249
277
  read_lock_key_queue = RedisQueuedLocks::Resource.prepare_read_lock_queue(lock_name)
250
- write_lock_key_queue = RedisQueuedLocks::Resource.prepare_write_lock_queue(lock_name)
278
+ lock_readers_key = RedisQueuedLocks::Resource.prepare_lock_readers(lock_name)
279
+ read_lock_key = RedisQueuedLocks::Resource.prepare_read_lock_key(lock_name, acquirer_id)
251
280
 
252
281
  acquirer_position = RedisQueuedLocks::Resource.calc_initial_acquirer_position
253
282
 
@@ -285,7 +314,6 @@ module RedisQueuedLocks::Acquirer::AcquireLock
285
314
  read_write_mode,
286
315
  lock_key_queue,
287
316
  read_lock_key_queue,
288
- write_lock_key_queue,
289
317
  queue_ttl,
290
318
  acquirer_id,
291
319
  host_id,
@@ -297,7 +325,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock
297
325
 
298
326
  LogVisitor.start_lock_obtaining(
299
327
  logger, log_sampled, lock_key,
300
- queue_ttl, acquirer_id, host_id, access_strategy
328
+ queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode
301
329
  )
302
330
 
303
331
  # Step 2: try to lock with timeout
@@ -317,7 +345,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock
317
345
 
318
346
  LogVisitor.start_try_to_lock_cycle(
319
347
  logger, log_sampled, lock_key,
320
- queue_ttl, acquirer_id, host_id, access_strategy
348
+ queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode
321
349
  )
322
350
 
323
351
  # Step 2.X: check the actual score: is it in queue ttl limit or not?
@@ -327,7 +355,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock
327
355
 
328
356
  LogVisitor.dead_score_reached__reset_acquirer_position(
329
357
  logger, log_sampled, lock_key,
330
- queue_ttl, acquirer_id, host_id, access_strategy
358
+ queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode
331
359
  )
332
360
  end
333
361
 
@@ -341,7 +369,8 @@ module RedisQueuedLocks::Acquirer::AcquireLock
341
369
  read_write_mode,
342
370
  lock_key_queue,
343
371
  read_lock_key_queue,
344
- write_lock_key_queue,
372
+ lock_readers_key,
373
+ read_lock_key,
345
374
  acquirer_id,
346
375
  host_id,
347
376
  acquirer_position,
@@ -375,10 +404,10 @@ module RedisQueuedLocks::Acquirer::AcquireLock
375
404
  # instrumetnation: (reentrant lock with ttl extension)
376
405
  LogVisitor.extendable_reentrant_lock_obtained(
377
406
  logger, log_sampled, result[:lock_key],
378
- queue_ttl, acquirer_id, host_id, acq_time, access_strategy
407
+ queue_ttl, acquirer_id, host_id, acq_time, access_strategy, read_write_mode
379
408
  )
380
409
  InstrVisitor.extendable_reentrant_lock_obtained(
381
- instrumenter, instr_sampled, result[:lock_key],
410
+ instrumenter, instr_sampled, result[:lock_key], read_write_mode,
382
411
  result[:ttl], result[:acq_id], result[:hst_id], result[:ts], acq_time,
383
412
  instrument
384
413
  )
@@ -386,10 +415,10 @@ module RedisQueuedLocks::Acquirer::AcquireLock
386
415
  # instrumetnation: (reentrant lock without ttl extension)
387
416
  LogVisitor.reentrant_lock_obtained(
388
417
  logger, log_sampled, result[:lock_key],
389
- queue_ttl, acquirer_id, host_id, acq_time, access_strategy
418
+ queue_ttl, acquirer_id, host_id, acq_time, access_strategy, read_write_mode
390
419
  )
391
420
  InstrVisitor.reentrant_lock_obtained(
392
- instrumenter, instr_sampled, result[:lock_key],
421
+ instrumenter, instr_sampled, result[:lock_key], read_write_mode,
393
422
  result[:ttl], result[:acq_id], result[:hst_id], result[:ts], acq_time,
394
423
  instrument
395
424
  )
@@ -398,10 +427,10 @@ module RedisQueuedLocks::Acquirer::AcquireLock
398
427
  # NOTE: classic is: acq_process[:result][:process] == :lock_obtaining
399
428
  LogVisitor.lock_obtained(
400
429
  logger, log_sampled, result[:lock_key],
401
- queue_ttl, acquirer_id, host_id, acq_time, access_strategy
430
+ queue_ttl, acquirer_id, host_id, acq_time, access_strategy, read_write_mode
402
431
  )
403
432
  InstrVisitor.lock_obtained(
404
- instrumenter, instr_sampled, result[:lock_key],
433
+ instrumenter, instr_sampled, result[:lock_key], read_write_mode,
405
434
  result[:ttl], result[:acq_id], result[:hst_id], result[:ts], acq_time,
406
435
  instrument
407
436
  )
@@ -416,6 +445,8 @@ module RedisQueuedLocks::Acquirer::AcquireLock
416
445
  ttl: result[:ttl],
417
446
  process: result[:process]
418
447
  }
448
+ # NOTE: (RW) the mode of the really held lock (internal, used for the lock release)
449
+ acq_process[:held_rw_mode] = result[:rw_mode]
419
450
  acq_process[:acquired] = true
420
451
  acq_process[:should_try] = false
421
452
  acq_process[:acq_time] = acq_time
@@ -424,7 +455,8 @@ module RedisQueuedLocks::Acquirer::AcquireLock
424
455
  # @type var result: Symbol
425
456
 
426
457
  # Step 2.2: failed to acquire. anylize each case and act in accordance
427
- if acq_process[:result] == :fail_fast_no_try # Step 2.2.a: fail without try
458
+ case acq_process[:result]
459
+ when :fail_fast_no_try # Step 2.2.a: fail without try
428
460
  acq_process[:should_try] = false
429
461
 
430
462
  if raise_errors
@@ -433,7 +465,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock
433
465
  "Lock \"#{lock_key}\" is already obtained."
434
466
  )
435
467
  end
436
- elsif acq_process[:result] == :conflict_dead_lock # Step 2.2.b: fail after dead lock
468
+ when :conflict_dead_lock # Step 2.2.b: fail after dead lock
437
469
  acq_process[:tries] += 1
438
470
  acq_process[:should_try] = false
439
471
  acq_process[:result] = :conflict_dead_lock
@@ -446,6 +478,21 @@ module RedisQueuedLocks::Acquirer::AcquireLock
446
478
  "that is already acquired by the current acquirer (acq_id: \"#{acquirer_id}\")."
447
479
  )
448
480
  end
481
+ when :conflict_lock_upgrade # Step 2.2.c (RW): fail after lock upgrade attempt
482
+ acq_process[:tries] += 1
483
+ acq_process[:should_try] = false
484
+ acq_process[:result] = :conflict_lock_upgrade
485
+ acq_dequeue.call
486
+
487
+ if raise_errors
488
+ raise(
489
+ RedisQueuedLocks::ConflictLockObtainError,
490
+ "Lock Conflict: trying to acquire the write lock \"#{lock_key}\" " \
491
+ "while the current acquirer holds its read lock (acq_id: \"#{acquirer_id}\"): " \
492
+ "read-to-write lock upgrade is not supported " \
493
+ "(conflict strategy: :#{conflict_strategy})."
494
+ )
495
+ end
449
496
  else
450
497
  acq_process[:tries] += 1 # Step RETRY: possible retry case
451
498
 
@@ -510,6 +557,9 @@ module RedisQueuedLocks::Acquirer::AcquireLock
510
557
  redis,
511
558
  logger,
512
559
  lock_key,
560
+ lock_readers_key,
561
+ read_lock_key,
562
+ acq_process[:held_rw_mode],
513
563
  acquirer_id,
514
564
  host_id,
515
565
  access_strategy,
@@ -537,6 +587,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock
537
587
  instrumenter,
538
588
  instr_sampled,
539
589
  acq_process[:lock_info][:lock_key],
590
+ read_write_mode,
540
591
  acq_process[:lock_info][:ttl],
541
592
  acq_process[:lock_info][:acq_id],
542
593
  acq_process[:lock_info][:hst_id],
@@ -551,6 +602,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock
551
602
  instrumenter,
552
603
  instr_sampled,
553
604
  acq_process[:lock_info][:lock_key],
605
+ read_write_mode,
554
606
  acq_process[:lock_info][:ttl],
555
607
  acq_process[:lock_info][:acq_id],
556
608
  acq_process[:lock_info][:hst_id],
@@ -568,7 +620,8 @@ module RedisQueuedLocks::Acquirer::AcquireLock
568
620
  if acq_process[:result] != :retry_limit_reached &&
569
621
  acq_process[:result] != :fail_fast_no_try &&
570
622
  acq_process[:result] != :fail_fast_after_try &&
571
- acq_process[:result] != :conflict_dead_lock
623
+ acq_process[:result] != :conflict_dead_lock &&
624
+ acq_process[:result] != :conflict_lock_upgrade
572
625
  # NOTE: we have only two situations if lock is not acquired without explicit failures:
573
626
  # - time limit is reached;
574
627
  # - retry count limit is reached;
@@ -22,7 +22,7 @@ module RedisQueuedLocks::Acquirer::ClearDeadRequests
22
22
  #
23
23
  # @api private
24
24
  # @since 1.0.0
25
- # @version 1.6.0
25
+ # @version 1.18.0
26
26
  def clear_dead_requests(
27
27
  redis_client,
28
28
  scan_size,
@@ -66,6 +66,7 @@ module RedisQueuedLocks::Acquirer::ClearDeadRequests
66
66
  #
67
67
  # @api private
68
68
  # @since 1.0.0
69
+ # @version 1.18.0
69
70
  def each_lock_queue(redis_client, scan_size, &block)
70
71
  redis_client.scan(
71
72
  'MATCH',
@@ -73,6 +74,13 @@ module RedisQueuedLocks::Acquirer::ClearDeadRequests
73
74
  count: scan_size,
74
75
  &block
75
76
  )
77
+ # NOTE: (RW) read lock queues
78
+ redis_client.scan(
79
+ 'MATCH',
80
+ RedisQueuedLocks::Resource::READ_LOCK_QUEUE_PATTERN,
81
+ count: scan_size,
82
+ &block
83
+ )
76
84
  end
77
85
  end
78
86
  end
@@ -2,6 +2,8 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.0.0
5
+ # @version 1.18.0
6
+ # rubocop:disable Metrics/ModuleLength
5
7
  module RedisQueuedLocks::Acquirer::ExtendLockTTL
6
8
  # @return [String]
7
9
  #
@@ -12,10 +14,20 @@ module RedisQueuedLocks::Acquirer::ExtendLockTTL
12
14
  return redis.call("PEXPIRE", KEYS[1], new_lock_pttl);
13
15
  LUA_SCRIPT
14
16
 
17
+ # rubocop:disable Metrics/ClassLength
15
18
  class << self
16
19
  # @param redis_client [RedisClient]
17
20
  # @param lock_name [String]
18
21
  # @param milliseconds [Integer]
22
+ # @param read_write_mode [Symbol]
23
+ # - `:write` - extends the write lock (of any acquirer);
24
+ # - `:read` - extends the read lock of the given acquirer (other read locks are not affected)
25
+ # or all live read locks of the lock (see `all_read_locks`);
26
+ # @param all_read_locks [Boolean]
27
+ # Extend all live read locks of the lock instead of the read lock of the given acquirer
28
+ # (used for `:read` mode only, ignored for `:write` mode).
29
+ # @param acquirer_id [String]
30
+ # The read lock acquirer (used for `:read` mode without `all_read_locks` only).
19
31
  # @param logger [::Logger,#debug]
20
32
  # @param instrumenter [#notify]
21
33
  # @param instrument [NilClass,Any]
@@ -27,15 +39,19 @@ module RedisQueuedLocks::Acquirer::ExtendLockTTL
27
39
  # @param instr_sampling_percent [Integer]
28
40
  # @param instr_sampler [#sampling_happened?,Module<RedisQueuedLocks::Instrument::Sampler>]
29
41
  # @param instr_sample_this [Boolean]
30
- # @return [Hash<Symbol,Boolean|Symbol>]
42
+ # @return [Hash<Symbol,Boolean|Symbol|Hash<Symbol,Integer>>]
31
43
  #
32
44
  # @api private
33
45
  # @since 1.0.0
34
- # @version 1.6.0
46
+ # @version 1.18.0
47
+ # rubocop:disable Metrics/MethodLength
35
48
  def extend_lock_ttl(
36
49
  redis_client,
37
50
  lock_name,
38
51
  milliseconds,
52
+ read_write_mode,
53
+ all_read_locks,
54
+ acquirer_id,
39
55
  logger,
40
56
  instrumenter,
41
57
  instrument,
@@ -48,18 +64,181 @@ module RedisQueuedLocks::Acquirer::ExtendLockTTL
48
64
  instr_sampler,
49
65
  instr_sample_this
50
66
  )
67
+ if read_write_mode != :read && read_write_mode != :write
68
+ raise(
69
+ RedisQueuedLocks::ArgumentError,
70
+ "`:read_write_mode` argument should be `:read` or `:write`, " \
71
+ "got #{read_write_mode.inspect}."
72
+ )
73
+ end
74
+
51
75
  lock_key = RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
52
76
 
77
+ if read_write_mode == :read
78
+ if all_read_locks != true && all_read_locks != false
79
+ raise(
80
+ RedisQueuedLocks::ArgumentError,
81
+ "`:all_read_locks` argument should be a boolean, got #{all_read_locks.inspect}."
82
+ )
83
+ end
84
+
85
+ # NOTE: (RW) all live read locks of the lock
86
+ if all_read_locks
87
+ return extend_all_read_locks_ttl(redis_client, lock_name, lock_key, milliseconds)
88
+ end
89
+
90
+ # NOTE: (RW) read lock of the concrete acquirer
91
+ return extend_read_lock_ttl(redis_client, lock_name, lock_key, acquirer_id, milliseconds)
92
+ end
93
+
53
94
  # NOTE: EVAL signature -> <lua script>, (number of keys), *(keys), *(arguments)
54
95
  result = redis_client.call('EVAL', EXTEND_LOCK_PTTL, 1, lock_key, milliseconds)
55
96
  # TODO: upload scripts to the redis
56
97
 
57
98
  # @type var result: Integer
58
99
  if result == 1
59
- { ok: true, result: :ttl_extended }
100
+ { ok: true, result: { extended_locks_count: 1 } }
101
+ else
102
+ { ok: false, result: :async_expire_or_no_lock }
103
+ end
104
+ end
105
+ # rubocop:enable Metrics/MethodLength
106
+
107
+ private
108
+
109
+ # Extends the live read lock of the acquirer:
110
+ # - the read lock expiration (readers registry score), the registry TTL and the read lock
111
+ # data TTL are extended;
112
+ # - the write lock key is watched: an expired read lock can be already replaced by a writer,
113
+ # so the expired (or replaced) read lock is never "revived";
114
+ #
115
+ # @param redis_client [RedisClient]
116
+ # @param lock_name [String]
117
+ # @param lock_key [String]
118
+ # @param acquirer_id [String]
119
+ # @param milliseconds [Integer]
120
+ # @return [Hash<Symbol,Boolean|Symbol|Hash<Symbol,Integer>>]
121
+ #
122
+ # @api private
123
+ # @since 1.18.0
124
+ # rubocop:disable Metrics/MethodLength
125
+ def extend_read_lock_ttl(redis_client, lock_name, lock_key, acquirer_id, milliseconds)
126
+ lock_readers_key = RedisQueuedLocks::Resource.prepare_lock_readers(lock_name)
127
+ read_lock_key = RedisQueuedLocks::Resource.prepare_read_lock_key(lock_name, acquirer_id)
128
+
129
+ result = redis_client.with do |rconn|
130
+ rconn.multi(watch: [lock_key]) do |transact|
131
+ # @type var lock_state: [Integer, Array[String], Float?]
132
+ lock_state = rconn.pipelined do |pipeline|
133
+ pipeline.call('EXISTS', lock_key)
134
+ pipeline.call('TIME')
135
+ pipeline.call('ZSCORE', lock_readers_key, acquirer_id)
136
+ end
137
+ redis_time = RedisQueuedLocks::Resource.redis_time_ms(lock_state[1])
138
+ read_lock_expiration = lock_state[2].to_f
139
+
140
+ # NOTE: no write lock and the read lock of the acquirer is alive
141
+ if lock_state[0] == 0 && read_lock_expiration > redis_time
142
+ extended_ttl = (read_lock_expiration + milliseconds - redis_time).ceil
143
+ transact.call('ZADD', lock_readers_key, 'XX', 'INCR', milliseconds, acquirer_id)
144
+ transact.call('PEXPIRE', lock_readers_key, extended_ttl, 'NX')
145
+ transact.call('PEXPIRE', lock_readers_key, extended_ttl, 'GT')
146
+ transact.call('PEXPIRE', read_lock_key, extended_ttl, 'NX')
147
+ transact.call('PEXPIRE', read_lock_key, extended_ttl, 'GT')
148
+ end
149
+ end
150
+ end
151
+
152
+ # NOTE:
153
+ # - [] => there is no live read lock of the acquirer (nothing to extend);
154
+ # - nil => the write lock is changed during the extension (the read lock can be expired);
155
+ # - result[0] => the new read lock expiration (nil if the read lock is removed);
156
+ if result.is_a?(::Array) && !result.empty? && result[0] != nil
157
+ { ok: true, result: { extended_locks_count: 1 } }
158
+ else
159
+ { ok: false, result: :async_expire_or_no_lock }
160
+ end
161
+ end
162
+ # rubocop:enable Metrics/MethodLength
163
+
164
+ # Extends all live read locks of the lock (the same rules as for `extend_read_lock_ttl`):
165
+ # - each live read lock expiration (readers registry score) and its read lock data TTL are
166
+ # extended by the given milliseconds; the registry TTL is extended to the longest read lock;
167
+ # - expired read locks are not extended ("revived");
168
+ # - the write lock key is watched (a writer can replace the expired read locks), the registry
169
+ # is not watched: concurrent read lock acquirements/releases do not abort the extension
170
+ # (read locks obtained after the registry snapshot are not extended, read locks released
171
+ # after it are not revived cuz of `ZADD XX`);
172
+ #
173
+ # @param redis_client [RedisClient]
174
+ # @param lock_name [String]
175
+ # @param lock_key [String]
176
+ # @param milliseconds [Integer]
177
+ # @return [Hash<Symbol,Boolean|Symbol|Hash<Symbol,Integer>>]
178
+ #
179
+ # @api private
180
+ # @since 1.18.0
181
+ # rubocop:disable Metrics/MethodLength
182
+ def extend_all_read_locks_ttl(redis_client, lock_name, lock_key, milliseconds)
183
+ lock_readers_key = RedisQueuedLocks::Resource.prepare_lock_readers(lock_name)
184
+ extended_read_locks_count = 0
185
+
186
+ result = redis_client.with do |rconn|
187
+ rconn.multi(watch: [lock_key]) do |transact|
188
+ # @type var lock_state: [Integer, Array[String], Array[[String, Float]]]
189
+ lock_state = rconn.pipelined do |pipeline|
190
+ pipeline.call('EXISTS', lock_key)
191
+ pipeline.call('TIME')
192
+ pipeline.call('ZRANGE', lock_readers_key, '0', '-1', 'WITHSCORES')
193
+ end
194
+ redis_time = RedisQueuedLocks::Resource.redis_time_ms(lock_state[1])
195
+
196
+ # NOTE: no write lock => extend all live read locks
197
+ if lock_state[0] == 0
198
+ registry_ttl = 0
199
+
200
+ lock_state[2].each do |(acquirer_id, read_lock_expiration)|
201
+ next unless read_lock_expiration > redis_time
202
+
203
+ read_lock_key =
204
+ RedisQueuedLocks::Resource.prepare_read_lock_key(lock_name, acquirer_id)
205
+ extended_ttl = (read_lock_expiration + milliseconds - redis_time).ceil
206
+ registry_ttl = extended_ttl if extended_ttl > registry_ttl
207
+ extended_read_locks_count += 1
208
+
209
+ transact.call('ZADD', lock_readers_key, 'XX', 'INCR', milliseconds, acquirer_id)
210
+ transact.call('PEXPIRE', read_lock_key, extended_ttl, 'NX')
211
+ transact.call('PEXPIRE', read_lock_key, extended_ttl, 'GT')
212
+ end
213
+
214
+ if extended_read_locks_count > 0
215
+ transact.call('PEXPIRE', lock_readers_key, registry_ttl, 'NX')
216
+ transact.call('PEXPIRE', lock_readers_key, registry_ttl, 'GT')
217
+ end
218
+ end
219
+ end
220
+ end
221
+
222
+ # NOTE:
223
+ # - [] => there are no live read locks (nothing to extend);
224
+ # - nil => the write lock is changed during the extension (read locks can be expired);
225
+ # - result[index * 3] => the new read lock expiration of each extended read lock
226
+ # (nil if the read lock is released during the extension => it is not counted);
227
+ extended_locks_count =
228
+ if result.is_a?(::Array)
229
+ extended_read_locks_count.times.count { |index| result[index * 3] != nil }
230
+ else
231
+ 0
232
+ end
233
+
234
+ if extended_locks_count > 0
235
+ { ok: true, result: { extended_locks_count: } }
60
236
  else
61
237
  { ok: false, result: :async_expire_or_no_lock }
62
238
  end
63
239
  end
240
+ # rubocop:enable Metrics/MethodLength
64
241
  end
242
+ # rubocop:enable Metrics/ClassLength
65
243
  end
244
+ # rubocop:enable Metrics/ModuleLength