redis_queued_locks 1.16.2 → 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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/project-overview.md +281 -0
  3. data/.claude/rules/acquirer.md +81 -0
  4. data/.claude/rules/arguments.md +60 -0
  5. data/.claude/rules/git.md +46 -0
  6. data/.claude/rules/logic.md +63 -0
  7. data/.claude/rules/swarm.md +120 -0
  8. data/.claude/rules/tests.md +48 -0
  9. data/.claude/rules/type-checking.md +50 -0
  10. data/.claude/rules/visitors.md +69 -0
  11. data/.rubocop.rbs.yml +31 -0
  12. data/.rubocop.yml +3 -3
  13. data/.ruby-version +1 -1
  14. data/CHANGELOG.md +80 -0
  15. data/CLAUDE.md +36 -0
  16. data/README.md +367 -82
  17. data/Rakefile +31 -2
  18. data/github_ci/.keep +0 -0
  19. data/lib/redis_queued_locks/acquirer/acquire_lock/delay_execution.rb +2 -2
  20. data/lib/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue/log_visitor.rb +6 -3
  21. data/lib/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue.rb +9 -7
  22. data/lib/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rb +20 -10
  23. data/lib/redis_queued_locks/acquirer/acquire_lock/log_visitor.rb +34 -16
  24. data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock/log_visitor.rb +240 -28
  25. data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rb +398 -132
  26. data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire/log_visitor.rb +12 -6
  27. data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire.rb +33 -9
  28. data/lib/redis_queued_locks/acquirer/acquire_lock.rb +73 -24
  29. data/lib/redis_queued_locks/acquirer/clear_dead_requests.rb +9 -1
  30. data/lib/redis_queued_locks/acquirer/extend_lock_ttl.rb +182 -3
  31. data/lib/redis_queued_locks/acquirer/is_locked.rb +19 -2
  32. data/lib/redis_queued_locks/acquirer/is_queued.rb +6 -2
  33. data/lib/redis_queued_locks/acquirer/lock_info.rb +96 -6
  34. data/lib/redis_queued_locks/acquirer/lock_series_poc/instr_visitor.rb +8 -4
  35. data/lib/redis_queued_locks/acquirer/lock_series_poc/log_visitor.rb +18 -7
  36. data/lib/redis_queued_locks/acquirer/lock_series_poc.rb +126 -29
  37. data/lib/redis_queued_locks/acquirer/locks.rb +103 -6
  38. data/lib/redis_queued_locks/acquirer/queue_info.rb +38 -9
  39. data/lib/redis_queued_locks/acquirer/queues.rb +28 -4
  40. data/lib/redis_queued_locks/acquirer/release_all_locks.rb +35 -1
  41. data/lib/redis_queued_locks/acquirer/release_lock.rb +40 -9
  42. data/lib/redis_queued_locks/acquirer/release_locks_of.rb +43 -3
  43. data/lib/redis_queued_locks/acquirer/release_read_lock.rb +143 -0
  44. data/lib/redis_queued_locks/acquirer.rb +1 -0
  45. data/lib/redis_queued_locks/client.rb +274 -158
  46. data/lib/redis_queued_locks/config/dsl.rb +4 -10
  47. data/lib/redis_queued_locks/resource.rb +117 -9
  48. data/lib/redis_queued_locks/swarm/flush_zombies.rb +46 -6
  49. data/lib/redis_queued_locks/swarm/supervisor.rb +1 -1
  50. data/lib/redis_queued_locks/swarm/swarm_element/isolated.rb +166 -44
  51. data/lib/redis_queued_locks/swarm/swarm_element/threaded.rb +45 -47
  52. data/lib/redis_queued_locks/swarm/zombie_info.rb +42 -0
  53. data/lib/redis_queued_locks/version.rb +2 -2
  54. data/rbs_collection.lock.yaml +10 -2
  55. data/rbs_collection.yaml +0 -2
  56. data/redis_queued_locks.gemspec +1 -1
  57. data/sig/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue/log_visitor.rbs +2 -1
  58. data/sig/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue.rbs +0 -1
  59. data/sig/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rbs +5 -0
  60. data/sig/redis_queued_locks/acquirer/acquire_lock/log_visitor.rbs +12 -6
  61. data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock/log_visitor.rbs +74 -7
  62. data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rbs +4 -2
  63. data/sig/redis_queued_locks/acquirer/acquire_lock/yield_expire.rbs +3 -0
  64. data/sig/redis_queued_locks/acquirer/acquire_lock/yield_with_expire/log_visitor.rbs +4 -2
  65. data/sig/redis_queued_locks/acquirer/extend_lock_ttl.rbs +19 -1
  66. data/sig/redis_queued_locks/acquirer/lock_info.rbs +9 -1
  67. data/sig/redis_queued_locks/acquirer/locks.rbs +10 -1
  68. data/sig/redis_queued_locks/acquirer/queue_info.rbs +2 -4
  69. data/sig/redis_queued_locks/acquirer/queues.rbs +1 -1
  70. data/sig/redis_queued_locks/acquirer/release_lock.rbs +8 -1
  71. data/sig/redis_queued_locks/acquirer/release_read_lock.rbs +44 -0
  72. data/sig/redis_queued_locks/client.rbs +22 -2
  73. data/sig/redis_queued_locks/resource.rbs +9 -2
  74. data/sig/redis_queued_locks/swarm/flush_zombies.rbs +1 -1
  75. data/sig/redis_queued_locks/swarm/swarm_element/isolated.rbs +13 -5
  76. data/sig/redis_queued_locks/swarm/swarm_element/threaded.rbs +6 -6
  77. data/sig/redis_queued_locks/swarm/zombie_info.rbs +5 -0
  78. metadata +17 -5
  79. data/github_ci/ruby3.3.gemfile +0 -17
  80. data/github_ci/ruby3.3.gemfile.lock +0 -216
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # NOTE: Lock Series PoC
4
- # steep:ignore
5
4
  # rubocop:disable all
6
5
  # @api private
7
6
  # @since 1.16.0
@@ -15,7 +14,7 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
15
14
  class << self
16
15
  # @api private
17
16
  # @since 1.16.0
18
- # @version 1.16.2
17
+ # @version 1.18.0
19
18
  def lock_series_poc( # steep:ignore
20
19
  redis,
21
20
  lock_names,
@@ -53,6 +52,9 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
53
52
  instr_sample_this:,
54
53
  &block
55
54
  )
55
+ # NOTE: (RW) the lock mode is the same for all locks of the series
56
+ # (see RedisQueuedLocks::Acquirer::AcquireLock for read_write_mode validations);
57
+
56
58
  case meta
57
59
  when Hash, NilClass then nil
58
60
  else
@@ -73,13 +75,14 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
73
75
  key == 'spc_cnt' ||
74
76
  key == 'l_spc_ext_ini_ttl' ||
75
77
  key == 'l_spc_ext_ts' ||
76
- key == 'l_spc_ts'
78
+ key == 'l_spc_ts' ||
79
+ key == 'rw_mode'
77
80
  end)
78
81
  raise(
79
82
  RedisQueuedLocks::ArgumentError,
80
83
  '`:meta` keys can not overlap reserved lock data keys ' \
81
84
  '"acq_id", "hst_id", "ts", "ini_ttl", "lock_key", "rem_ttl", "spc_cnt", ' \
82
- '"spc_ext_ttl", "l_spc_ext_ini_ttl", "l_spc_ext_ts", "l_spc_ts"'
85
+ '"spc_ext_ttl", "l_spc_ext_ini_ttl", "l_spc_ext_ts", "l_spc_ts", "rw_mode"'
83
86
  )
84
87
  end
85
88
 
@@ -129,13 +132,16 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
129
132
 
130
133
  RedisQueuedLocks::Acquirer::LockSeriesPoC::LogVisitor.start_lock_series_obtaining( # steep:ignore
131
134
  logger, log_sampled, lock_keys_for_instrumentation,
132
- queue_ttl, acquirer_id_for_instrumentation, host_id_for_instrumentation, access_strategy
135
+ queue_ttl, acquirer_id_for_instrumentation, host_id_for_instrumentation, access_strategy,
136
+ read_write_mode
133
137
  )
134
138
 
135
139
  acq_start_time = RedisQueuedLocks::Utilities.clock_gettime
136
140
  successfully_acquired_locks = [] # steep:ignore
137
141
  failed_locks_and_errors = {} # steep:ignore
138
142
  failed_on_lock = nil
143
+ # NOTE: locks of the failed series are released once (exceptional or not)
144
+ is_failed_series_released = false
139
145
 
140
146
  lock_acquirement_operations.map do |lock_operation_options|
141
147
  result =
@@ -180,17 +186,11 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
180
186
  RedisQueuedLocks::LockAcquirementTimeoutError,
181
187
  RedisQueuedLocks::LockAcquirementRetryLimitError,
182
188
  RedisQueuedLocks::ConflictLockObtainError => error
183
- if successfully_acquired_locks.any?
184
- # NOTE: release all previously acquired locks if any next lock is already locked
185
- successfully_acquired_locks.each do |operation_result|
186
- lock_key = RedisQueuedLocks::Resource.prepare_lock_key(operation_result[:lock_name])
187
- redis.with do |conn|
188
- conn.multi(watch: [lock_key]) do |transact|
189
- transact.call('DEL', lock_key)
190
- end
191
- end
192
- end
193
- end
189
+ # NOTE: release all previously acquired locks if any next lock is already locked
190
+ release_lock_series( # steep:ignore
191
+ redis, read_write_mode, successfully_acquired_locks, acquirer_id_for_instrumentation
192
+ )
193
+ is_failed_series_released = true
194
194
 
195
195
  raise(error) if raise_errors
196
196
  end
@@ -204,6 +204,13 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
204
204
  else
205
205
  failed_on_lock = lock_operation_options[:lock_name]
206
206
  failed_locks_and_errors[lock_operation_options[:lock_name]] = result
207
+ # NOTE: release all previously acquired locks (non-exceptional failure: raise_errors: false)
208
+ unless is_failed_series_released
209
+ release_lock_series( # steep:ignore
210
+ redis, read_write_mode, successfully_acquired_locks, acquirer_id_for_instrumentation
211
+ )
212
+ is_failed_series_released = true
213
+ end
207
214
  break
208
215
  end
209
216
  end
@@ -211,16 +218,16 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
211
218
  if (successfully_acquired_locks.size == lock_names.size && (successfully_acquired_locks.all? { |res| res[:ok] }))
212
219
  acq_end_time = RedisQueuedLocks::Utilities.clock_gettime
213
220
  acq_time = ((acq_end_time - acq_start_time) / 1_000.0).ceil(2)
214
- ts = Time.now.to_s
221
+ ts = Time.now.to_f
215
222
 
216
223
  RedisQueuedLocks::Acquirer::LockSeriesPoC::LogVisitor.lock_series_obtained( # steep:ignore
217
224
  logger, log_sampled, lock_keys_for_instrumentation,
218
225
  queue_ttl, acquirer_id_for_instrumentation, host_id_for_instrumentation,
219
- acq_time, access_strategy
226
+ acq_time, access_strategy, read_write_mode
220
227
  )
221
228
 
222
229
  RedisQueuedLocks::Acquirer::LockSeriesPoC::InstrVisitor.lock_series_obtained( # steep:ignore
223
- instrumenter, instr_sampled, lock_keys_for_instrumentation,
230
+ instrumenter, instr_sampled, lock_keys_for_instrumentation, read_write_mode,
224
231
  ttl, acquirer_id_for_instrumentation, host_id_for_instrumentation, ts, acq_time, instrument
225
232
  )
226
233
 
@@ -239,6 +246,11 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
239
246
  redis,
240
247
  logger,
241
248
  lock_keys_for_instrumentation.last,
249
+ RedisQueuedLocks::Resource.prepare_lock_readers(lock_names.last),
250
+ RedisQueuedLocks::Resource.prepare_read_lock_key(
251
+ lock_names.last, acquirer_id_for_instrumentation
252
+ ),
253
+ read_write_mode,
242
254
  acquirer_id_for_instrumentation,
243
255
  host_id_for_instrumentation,
244
256
  access_strategy,
@@ -258,14 +270,9 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
258
270
 
259
271
  # expire locks manually
260
272
  if block_given?
261
- redis.with do |conn|
262
- # use transaction in order to exclude any cross-locking during the group expiration
263
- conn.multi(watch: lock_keys_for_instrumentation) do |transaction|
264
- lock_keys_for_instrumentation.each do |lock_key|
265
- transaction.call('EXPIRE', lock_key, '0')
266
- end
267
- end
268
- end
273
+ release_lock_series( # steep:ignore
274
+ redis, read_write_mode, successfully_acquired_locks, acquirer_id_for_instrumentation
275
+ )
269
276
  is_lock_manually_released = true
270
277
  end
271
278
 
@@ -275,13 +282,15 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
275
282
 
276
283
  RedisQueuedLocks::Acquirer::LockSeriesPoC::LogVisitor.expire_lock_series( # steep:ignore
277
284
  logger, log_sampled, lock_keys_for_instrumentation,
278
- queue_ttl, acquirer_id_for_instrumentation, host_id_for_instrumentation, access_strategy
285
+ queue_ttl, acquirer_id_for_instrumentation, host_id_for_instrumentation, access_strategy,
286
+ read_write_mode
279
287
  ) if is_lock_manually_released
280
288
 
281
289
  RedisQueuedLocks::Acquirer::LockSeriesPoC::InstrVisitor.lock_series_hold_and_release( # steep:ignore
282
290
  instrumenter,
283
291
  instr_sampled,
284
292
  lock_keys_for_instrumentation,
293
+ read_write_mode,
285
294
  ttl,
286
295
  acquirer_id_for_instrumentation,
287
296
  host_id_for_instrumentation,
@@ -322,7 +331,95 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
322
331
  }
323
332
  end
324
333
  end
334
+
335
+ private
336
+
337
+ # Releases locks of the series that are obtained by the series itself:
338
+ # - reentrant locks (already obtained by the outer logic of the current acquirer) are kept;
339
+ # - only locks of the current acquirer are released (owner check);
340
+ #
341
+ # @param redis [RedisClient]
342
+ # @param read_write_mode [Symbol]
343
+ # @param successfully_acquired_locks [Array<Hash<Symbol,Any>>]
344
+ # @param acquirer_id [String]
345
+ # @return [void]
346
+ #
347
+ # @api private
348
+ # @since 1.18.0
349
+ def release_lock_series(redis, read_write_mode, successfully_acquired_locks, acquirer_id) # steep:ignore
350
+ lock_names = obtained_lock_names(successfully_acquired_locks) # steep:ignore
351
+
352
+ if read_write_mode == :read
353
+ release_read_lock_series(redis, lock_names, acquirer_id) # steep:ignore
354
+ else
355
+ release_write_lock_series(redis, lock_names, acquirer_id) # steep:ignore
356
+ end
357
+ end
358
+
359
+ # Releases write locks of the given acquirer (compare-and-delete: the lock can be expired
360
+ # and obtained by another acquirer when the series logic exceeds the lock ttl).
361
+ #
362
+ # @param redis [RedisClient]
363
+ # @param lock_names [Array<String>]
364
+ # @param acquirer_id [String]
365
+ # @return [void]
366
+ #
367
+ # @api private
368
+ # @since 1.18.0
369
+ def release_write_lock_series(redis, lock_names, acquirer_id) # steep:ignore
370
+ return if lock_names.empty?
371
+
372
+ redis.with do |conn|
373
+ lock_names.each do |lock_name|
374
+ lock_key = RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
375
+ conn.multi(watch: [lock_key]) do |transaction|
376
+ if conn.call('HGET', lock_key, 'acq_id') == acquirer_id
377
+ transaction.call('DEL', lock_key)
378
+ end
379
+ end
380
+ end
381
+ end
382
+ end
383
+
384
+ # Locks of the series that are obtained by the series itself (reentrant locks are obtained
385
+ # by the outer logic of the current acquirer and should be released by it).
386
+ #
387
+ # @param successfully_acquired_locks [Array<Hash<Symbol,Any>>]
388
+ # @return [Array<String>]
389
+ #
390
+ # @api private
391
+ # @since 1.18.0
392
+ def obtained_lock_names(successfully_acquired_locks) # steep:ignore
393
+ successfully_acquired_locks.filter_map do |operation_result|
394
+ operation_result[:lock_name] if operation_result[:result][:process] == :lock_obtaining
395
+ end
396
+ end
397
+
398
+ # Releases read locks of the given acquirer (other readers are not affected).
399
+ #
400
+ # @param redis [RedisClient]
401
+ # @param lock_names [Array<String>]
402
+ # @param acquirer_id [String]
403
+ # @return [void]
404
+ #
405
+ # @api private
406
+ # @since 1.18.0
407
+ def release_read_lock_series(redis, lock_names, acquirer_id) # steep:ignore
408
+ return if lock_names.empty?
409
+
410
+ redis.with do |conn|
411
+ conn.multi do |transaction|
412
+ lock_names.each do |lock_name|
413
+ transaction.call(
414
+ 'ZREM', RedisQueuedLocks::Resource.prepare_lock_readers(lock_name), acquirer_id
415
+ )
416
+ transaction.call(
417
+ 'DEL', RedisQueuedLocks::Resource.prepare_read_lock_key(lock_name, acquirer_id)
418
+ )
419
+ end
420
+ end
421
+ end
422
+ end
325
423
  end
326
424
  end
327
- # steep:ignore
328
425
  # rubocop:enable all
@@ -2,7 +2,10 @@
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::Locks
8
+ # rubocop:disable Metrics/ClassLength
6
9
  class << self
7
10
  # @param redis_client [RedisClient]
8
11
  # @option scan_size [Integer]
@@ -23,9 +26,11 @@ module RedisQueuedLocks::Acquirer::Locks
23
26
  # @param redis_client [RedisClient]
24
27
  # @param scan_size [Integer]
25
28
  # @return [Set<String>]
29
+ # - (RW) read locks are represented by their lock keys (`rql:lock:<lock_name>`) too;
26
30
  #
27
31
  # @api private
28
32
  # @since 1.0.0
33
+ # @version 1.18.0
29
34
  def scan_locks(redis_client, scan_size)
30
35
  Set.new.tap do |lock_keys|
31
36
  redis_client.scan(
@@ -36,19 +41,33 @@ module RedisQueuedLocks::Acquirer::Locks
36
41
  # TODO: reduce unnecessary iterations
37
42
  lock_keys.add(lock_key)
38
43
  end
44
+
45
+ # NOTE: (RW) read locks (readers registries can contain expired read locks only)
46
+ redis_client.scan(
47
+ 'MATCH',
48
+ RedisQueuedLocks::Resource::LOCK_READERS_PATTERN,
49
+ count: scan_size
50
+ ) do |lock_readers_key|
51
+ # TODO: reduce unnecessary iterations
52
+ lock_keys.add(RedisQueuedLocks::Resource.lock_key_from_readers(lock_readers_key))
53
+ end
39
54
  end
40
55
  end
41
56
 
57
+ # NOTE:
58
+ # - the lock info is extracted by the module itself with the same approach as in `LockInfo`:
59
+ # public operation modules do not reuse each other (they share logic via `Resource`,
60
+ # `Utilities`), so the similar logic is duplicated on purpose;
61
+ #
42
62
  # @param redis_client [RedisClient]
43
63
  # @param lock_keys [Set<String>]
44
64
  # @return [Set<Hash<Symbol,Any>>]
45
65
  #
46
66
  # @api private
47
67
  # @since 1.0.0
48
- # @version 1.9.0
68
+ # @version 1.18.0
49
69
  # rubocop:disable Metrics/MethodLength
50
70
  def extract_locks_info(redis_client, lock_keys)
51
- # TODO: refactor with RedisQueuedLocks::Acquier::LockInfo
52
71
  Set.new.tap do |seeded_locks|
53
72
  # rubocop:disable Layout/LineLength
54
73
  # @type var seeded_locks: Set[{ lock: String, status: :released|:alive, info: Hash[String,untyped] }]
@@ -58,10 +77,16 @@ module RedisQueuedLocks::Acquirer::Locks
58
77
  lock_keys.each do |lock_key|
59
78
  # Step 1: extract lock info from redis
60
79
 
61
- # @type var lock_info: Hash[String,String|Float|Integer]
80
+ # NOTE: (RW) readers registry of the lock
81
+ lock_name = lock_key.delete_prefix('rql:lock:')
82
+ lock_readers_key = RedisQueuedLocks::Resource.prepare_lock_readers(lock_name)
83
+
84
+ # @type var lock_info: RedisQueuedLocks::Acquirer::Locks::lockInfo
62
85
  lock_info = redis_client.pipelined do |pipeline|
63
86
  pipeline.call('HGETALL', lock_key)
64
87
  pipeline.call('PTTL', lock_key)
88
+ pipeline.call('TIME')
89
+ pipeline.call('ZRANGE', lock_readers_key, '0', '-1', 'WITHSCORES')
65
90
  end.yield_self do |result| # Step 2: format the result
66
91
  # Step 2.X: lock is released
67
92
  if result == nil
@@ -73,16 +98,22 @@ module RedisQueuedLocks::Acquirer::Locks
73
98
  # - result[1] (PTTL) (Integer)
74
99
  # => (without any mutation, integer is atomic)
75
100
 
76
- # @type var result: [Hash[String,String|Float|Integer],Integer]
101
+ # rubocop:disable Layout/LineLength
102
+ # @type var result: [Hash[String,String|Float|Integer],Integer,Array[String],Array[[String, Float]]]
103
+ # rubocop:enable Layout/LineLength
77
104
  hget_cmd_res = result[0] # NOTE: HGETALL result (hash)
78
105
  pttl_cmd_res = result[1] # NOTE: PTTL result (integer)
79
106
 
80
- # Step 2.Y: lock is released
107
+ # Step 2.Y: write lock is released
81
108
  if hget_cmd_res == {} || pttl_cmd_res == -2 # NOTE: key does not exist
82
- {} #: Hash[String,String|Float|Integer]
109
+ # NOTE: (RW) read locks (or nothing if there are no live read locks)
110
+ read_lock_info(
111
+ redis_client, lock_name, lock_key, result[2], result[3]
112
+ ) || {} #: Hash[String,String|Float|Integer]
83
113
  else
84
114
  # Step 2.Z: lock is alive => format received info + add additional rem_ttl info
85
115
  hget_cmd_res.tap do |lock_data|
116
+ lock_data['rw_mode'] = 'write'
86
117
  lock_data['ts'] = Float(lock_data['ts'])
87
118
  lock_data['ini_ttl'] = Integer(lock_data['ini_ttl'])
88
119
  lock_data['rem_ttl'] = ((pttl_cmd_res == -1) ? Float::INFINITY : pttl_cmd_res)
@@ -109,5 +140,71 @@ module RedisQueuedLocks::Acquirer::Locks
109
140
  end
110
141
  end
111
142
  # rubocop:enable Metrics/MethodLength
143
+
144
+ # Formats live read locks of the lock (expired read locks are ignored).
145
+ #
146
+ # NOTE: the same approach as in `LockInfo` (duplicated on purpose: operation modules are
147
+ # independent of each other, see `extract_locks_info`);
148
+ #
149
+ # @param redis_client [RedisClient]
150
+ # @param lock_name [String]
151
+ # @param lock_key [String]
152
+ # @param redis_time [Array<String>] Result of the redis `TIME` command.
153
+ # @param read_locks [Array<Array<String,Float>>] Readers registry (acquirer id, expiration).
154
+ # @return [Hash<String,String|Numeric|Array<Hash<String,String|Numeric>>>,NilClass]
155
+ # - `nil` is returned when there are no live read locks;
156
+ # - each reader has the same data as the write lock (acq_id, hst_id, ts, ini_ttl, rem_ttl,
157
+ # meta, reentrant lock data);
158
+ #
159
+ # @api private
160
+ # @since 1.18.0
161
+ # rubocop:disable Metrics/MethodLength
162
+ def read_lock_info(redis_client, lock_name, lock_key, redis_time, read_locks)
163
+ now = RedisQueuedLocks::Resource.redis_time_ms(redis_time)
164
+ live_read_locks = read_locks.select { |(_acquirer_id, expiration)| expiration > now }
165
+ return nil if live_read_locks.empty?
166
+
167
+ # NOTE: read lock data (it can be absent if it is dropped during the read lock life)
168
+ # @type var read_locks_data: Array[Hash[String,String]]
169
+ read_locks_data = redis_client.pipelined do |pipeline|
170
+ live_read_locks.each do |(acquirer_id, _expiration)|
171
+ pipeline.call(
172
+ 'HGETALL',
173
+ RedisQueuedLocks::Resource.prepare_read_lock_key(lock_name, acquirer_id)
174
+ )
175
+ end
176
+ end
177
+
178
+ longest_rem_ttl = 0
179
+ readers = live_read_locks.each_with_index.map do |(acquirer_id, expiration), index|
180
+ rem_ttl = (expiration - now).ceil
181
+ longest_rem_ttl = rem_ttl if rem_ttl > longest_rem_ttl
182
+
183
+ # @type var reader: Hash[String,String|Float|Integer|nil]
184
+ reader = read_locks_data[index] || {}
185
+ reader['acq_id'] = acquirer_id
186
+ reader['hst_id'] ||= RedisQueuedLocks::Resource.host_identifier_from_acquirer(acquirer_id)
187
+ reader['ts'] = Float(reader['ts']) if reader['ts']
188
+ reader['ini_ttl'] = Integer(reader['ini_ttl']) if reader['ini_ttl']
189
+ reader['rem_ttl'] = rem_ttl
190
+ reader['spc_cnt'] = Integer(reader['spc_cnt']) if reader['spc_cnt']
191
+ reader['l_spc_ts'] = Float(reader['l_spc_ts']) if reader['l_spc_ts']
192
+ reader['spc_ext_ttl'] = Integer(reader['spc_ext_ttl']) if reader['spc_ext_ttl']
193
+ reader['l_spc_ext_ini_ttl'] =
194
+ Integer(reader['l_spc_ext_ini_ttl']) if reader['l_spc_ext_ini_ttl']
195
+ reader['l_spc_ext_ts'] = Float(reader['l_spc_ext_ts']) if reader['l_spc_ext_ts']
196
+ reader
197
+ end
198
+
199
+ {
200
+ 'lock_key' => lock_key,
201
+ 'rw_mode' => 'read',
202
+ 'rem_ttl' => longest_rem_ttl,
203
+ 'readers' => readers
204
+ }
205
+ end
206
+ # rubocop:enable Metrics/MethodLength
112
207
  end
208
+ # rubocop:enable Metrics/ClassLength
113
209
  end
210
+ # rubocop:enable Metrics/ModuleLength
@@ -2,6 +2,7 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.0.0
5
+ # @version 1.18.0
5
6
  module RedisQueuedLocks::Acquirer::QueueInfo
6
7
  class << self
7
8
  # Returns an information about the required lock queue by the lock name. The result
@@ -12,41 +13,69 @@ module RedisQueuedLocks::Acquirer::QueueInfo
12
13
  #
13
14
  # @param redis_client [RedisClient]
14
15
  # @param lock_name [String]
15
- # @return [Hash<String,Array<Hash<String,String|Numeric>>,NilClass]
16
- # - `nil` is returned when lock queue does not exist;
16
+ # @return [Hash<String,String|Array<Hash<String,String|Numeric>>,NilClass]
17
+ # - `nil` is returned when lock queues (write and read) do not exist;
17
18
  # - result format: {
18
19
  # "lock_queue" => "rql:lock_queue:your_lock_name", # lock queue key in redis,
19
20
  # "queue" => [
20
- # { "acq_id" => "rql:acq:123/456/789/987/identity", "score" => 123.456 },
21
- # { "acq_id" => "rql:acq:123/686/789/987/identity", "score" => 456.789 },
21
+ # { "acq_id" => "rql:acq:123/456/789/987/id", "score" => 123.456, "rw_mode" => "write" },
22
+ # { "acq_id" => "rql:acq:123/686/789/987/id", "score" => 456.789, "rw_mode" => "write" },
22
23
  # ...
23
- # ] # ordered set (by score) with information about an acquirer and their position in queue
24
+ # ] # ordered set (by score) with information about an acquirer, their position in queue
25
+ # # and the requested lock mode
26
+ # <additional keys when the queue of read lock requests exists>:
27
+ # "read_lock_queue" => "rql:lock_read_queue:your_lock_name", # read lock queue key in redis
28
+ # "read_queue" => [
29
+ # { "acq_id" => "rql:acq:123/456/789/987/id", "score" => 123.456, "rw_mode" => "read" },
30
+ # ...
31
+ # ] # read lock requests (positions are comparable with the write lock request positions)
24
32
  # }
25
33
  #
26
34
  # @api private
27
35
  # @since 1.0.0
36
+ # @version 1.18.0
37
+ # rubocop:disable Metrics/MethodLength
28
38
  def queue_info(redis_client, lock_name)
29
39
  lock_key_queue = RedisQueuedLocks::Resource.prepare_lock_queue(lock_name)
40
+ read_lock_key_queue = RedisQueuedLocks::Resource.prepare_read_lock_queue(lock_name)
30
41
 
31
42
  result = redis_client.pipelined do |pipeline|
32
43
  pipeline.call('EXISTS', lock_key_queue)
33
44
  pipeline.call('ZRANGE', lock_key_queue, '0', '-1', 'WITHSCORES')
45
+ pipeline.call('EXISTS', read_lock_key_queue)
46
+ pipeline.call('ZRANGE', read_lock_key_queue, '0', '-1', 'WITHSCORES')
34
47
  end
35
48
 
36
- # @type var result: [Integer,Array[[String,Integer|Float]]]
49
+ # rubocop:disable Layout/LineLength
50
+ # @type var result: [Integer,Array[[String,Integer|Float]],Integer,Array[[String,Integer|Float]]]
51
+ # rubocop:enable Layout/LineLength
37
52
  exists_cmd_res = result[0]
38
53
  zrange_cmd_res = result[1]
54
+ read_exists_cmd_res = result[2]
55
+ read_zrange_cmd_res = result[3]
39
56
 
40
- if exists_cmd_res == 1
57
+ if exists_cmd_res == 1 || read_exists_cmd_res == 1
41
58
  # NOTE: queue existed during the piepline invocation
42
- {
59
+ # @type var queue_info: RedisQueuedLocks::Acquirer::QueueInfo::queueInfo
60
+ queue_info = {
43
61
  'lock_queue' => lock_key_queue,
44
- 'queue' => zrange_cmd_res.map { |val| { 'acq_id' => val[0], 'score' => val[1] } }
62
+ 'queue' => zrange_cmd_res.map do |val|
63
+ { 'acq_id' => val[0], 'score' => val[1], 'rw_mode' => 'write' }
64
+ end
45
65
  }
66
+ # NOTE: (RW) the queue of read lock requests
67
+ if read_exists_cmd_res == 1
68
+ queue_info['read_lock_queue'] = read_lock_key_queue
69
+ queue_info['read_queue'] = read_zrange_cmd_res.map do |val|
70
+ { 'acq_id' => val[0], 'score' => val[1], 'rw_mode' => 'read' }
71
+ end
72
+ end
73
+ queue_info
46
74
  else
47
75
  # NOTE: queue did not exist during the pipeline invocation
48
76
  nil
49
77
  end
50
78
  end
79
+ # rubocop:enable Metrics/MethodLength
51
80
  end
52
81
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.0.0
5
+ # @version 1.18.0
5
6
  module RedisQueuedLocks::Acquirer::Queues
6
7
  class << self
7
8
  # @param redis_client [RedisClient]
@@ -23,9 +24,11 @@ module RedisQueuedLocks::Acquirer::Queues
23
24
  # @param redis_client [RedisClient]
24
25
  # @param scan_size [Integer]
25
26
  # @return [Set<String>]
27
+ # - (RW) queues of read lock requests are included;
26
28
  #
27
29
  # @api private
28
30
  # @since 1.0.0
31
+ # @version 1.18.0
29
32
  def scan_queues(redis_client, scan_size)
30
33
  Set.new.tap do |lock_queues|
31
34
  # @type var lock_queues: Set[String]
@@ -38,21 +41,40 @@ module RedisQueuedLocks::Acquirer::Queues
38
41
  # @type var lock_queue: String
39
42
  lock_queues.add(lock_queue)
40
43
  end
44
+
45
+ # NOTE: (RW) queues of read lock requests
46
+ redis_client.scan(
47
+ 'MATCH',
48
+ RedisQueuedLocks::Resource::READ_LOCK_QUEUE_PATTERN,
49
+ count: scan_size
50
+ ) do |read_lock_queue|
51
+ # TODO: reduce unnecessary iterations
52
+ # @type var read_lock_queue: String
53
+ lock_queues.add(read_lock_queue)
54
+ end
41
55
  end
42
56
  end
43
57
 
44
58
  # @param redis_client [RedisClient]
45
59
  # @param lock_queues [Set<String>]
46
60
  # @return [Set<Hash<Symbol,Any>>]
61
+ # - each request: `{ 'acq_id' => String, 'score' => Float, 'rw_mode' => 'read'/'write' }`;
47
62
  #
48
63
  # @api private
49
64
  # @since 1.0.0
65
+ # @version 1.18.0
50
66
  def extract_queues_info(redis_client, lock_queues)
51
- # TODO: refactor with RedisQueuedLocks::Acquier::QueueInfo
67
+ # NOTE: the same approach as in `QueueInfo` (duplicated on purpose: public operation modules
68
+ # do not reuse each other, they share logic via `Resource`, `Utilities`);
52
69
  Set.new.tap do |seeded_queues|
53
70
  # Step X: iterate over each lock queue and extract their info
54
71
  # @type var seeded_queues: Set[Hash[Symbol,untyped]]
55
72
  lock_queues.each do |lock_queue|
73
+ # NOTE:
74
+ # - (RW) the mode of the requests of the queue (`'read'` or `'write'`);
75
+ # - lock queues are scanned by the lock queue patterns, so the mode is never `nil`;
76
+ rw_mode = RedisQueuedLocks::Resource.lock_queue_rw_mode(lock_queue) #: String
77
+
56
78
  # Step 1: extract lock queue info from reids
57
79
  queue_info = redis_client.pipelined do |pipeline|
58
80
  pipeline.call('EXISTS', lock_queue)
@@ -64,14 +86,16 @@ module RedisQueuedLocks::Acquirer::Queues
64
86
  zrange_cmd_res = result[1]
65
87
 
66
88
  if exists_cmd_res == 1 # Step 2.X: lock queue existed during the piepline invocation
67
- zrange_cmd_res.map { |val| { 'acq_id' => val[0], 'score' => val[1] } }
89
+ zrange_cmd_res.map do |val|
90
+ { 'acq_id' => val[0], 'score' => val[1], 'rw_mode' => rw_mode }
91
+ end
68
92
  else
69
93
  # Step 2.Y: lock queue did not exist during the pipeline invocation
70
- [] #: Array[Hash[String,Float]]
94
+ [] #: Array[Hash[String,String|Float]]
71
95
  end
72
96
  end
73
97
 
74
- # @type var queue_info: Array[Hash[String,Float]]
98
+ # @type var queue_info: Array[Hash[String,String|Float]]
75
99
 
76
100
  # Step 3: push the lock queue info to the result store
77
101
  seeded_queues << {
@@ -10,6 +10,7 @@ module RedisQueuedLocks::Acquirer::ReleaseAllLocks
10
10
  # Release all locks:
11
11
  # - 1. clear all lock queus: drop them all from Redis database by the lock queue pattern;
12
12
  # - 2. delete all locks: drop lock keys from Redis by the lock key pattern;
13
+ # - 3. (RW) clear all read lock queues and drop all read locks (readers registries and data);
13
14
  #
14
15
  # @param redis [RedisClient]
15
16
  # Redis connection client.
@@ -71,7 +72,7 @@ module RedisQueuedLocks::Acquirer::ReleaseAllLocks
71
72
  #
72
73
  # @api private
73
74
  # @since 1.0.0
74
- # @version 1.14.0
75
+ # @version 1.18.0
75
76
  def release_all_locks(
76
77
  redis,
77
78
  batch_size,
@@ -129,6 +130,8 @@ module RedisQueuedLocks::Acquirer::ReleaseAllLocks
129
130
  #
130
131
  # @api private
131
132
  # @since 1.0.0
133
+ # @version 1.18.0
134
+ # rubocop:disable Metrics/MethodLength
132
135
  def fully_release_all_locks(redis, batch_size)
133
136
  result = redis.with do |rconn|
134
137
  rconn.pipelined do |pipeline|
@@ -151,10 +154,41 @@ module RedisQueuedLocks::Acquirer::ReleaseAllLocks
151
154
  # TODO: reduce unnecessary iterations
152
155
  pipeline.call('EXPIRE', lock_key, '0')
153
156
  end
157
+
158
+ # Step C (RW): release all read lock queues
159
+ rconn.scan(
160
+ 'MATCH',
161
+ RedisQueuedLocks::Resource::READ_LOCK_QUEUE_PATTERN,
162
+ count: batch_size
163
+ ) do |read_lock_queue|
164
+ # TODO: reduce unnecessary iterations
165
+ pipeline.call('EXPIRE', read_lock_queue, '0')
166
+ end
167
+
168
+ # Step D (RW): release all read locks
169
+ rconn.scan(
170
+ 'MATCH',
171
+ RedisQueuedLocks::Resource::LOCK_READERS_PATTERN,
172
+ count: batch_size
173
+ ) do |lock_readers|
174
+ # TODO: reduce unnecessary iterations
175
+ pipeline.call('EXPIRE', lock_readers, '0')
176
+ end
177
+
178
+ # Step E (RW): release all read lock data
179
+ rconn.scan(
180
+ 'MATCH',
181
+ RedisQueuedLocks::Resource::READ_LOCK_PATTERN,
182
+ count: batch_size
183
+ ) do |read_lock|
184
+ # TODO: reduce unnecessary iterations
185
+ pipeline.call('EXPIRE', read_lock, '0')
186
+ end
154
187
  end
155
188
  end
156
189
 
157
190
  { ok: true, result: { rel_key_cnt: result.sum } }
158
191
  end
192
+ # rubocop:enable Metrics/MethodLength
159
193
  end
160
194
  end