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 private
4
4
  # @since 1.0.0
5
- # @version 1.20.0
5
+ # @version 1.21.0
6
6
  # rubocop:disable Metrics/ModuleLength
7
7
  module RedisQueuedLocks::Acquirer::ExtendLockTTL
8
8
  # @return [String]
@@ -14,11 +14,18 @@ module RedisQueuedLocks::Acquirer::ExtendLockTTL
14
14
  return redis.call("PEXPIRE", KEYS[1], new_lock_pttl);
15
15
  LUA_SCRIPT
16
16
 
17
+ # @since 1.21.0
18
+ extend RedisQueuedLocks::Utilities
19
+
17
20
  # rubocop:disable Metrics/ClassLength
18
21
  class << self
22
+ # Extends the lock TTL and reports the operation (one log and one instrumentation event
23
+ # `redis_queued_locks.extend_lock_ttl` with the time of the whole operation).
24
+ #
19
25
  # @param redis_client [RedisClient]
20
26
  # @param lock_name [String]
21
27
  # @param milliseconds [Integer]
28
+ # How many milliseconds should be added: a positive Integer.
22
29
  # @param read_write_mode [Symbol]
23
30
  # - `:write` - extends the write lock (of any acquirer);
24
31
  # - `:read` - extends the read lock of the given acquirer (other read locks are not affected)
@@ -45,7 +52,7 @@ module RedisQueuedLocks::Acquirer::ExtendLockTTL
45
52
  #
46
53
  # @api private
47
54
  # @since 1.0.0
48
- # @version 1.20.0
55
+ # @version 1.21.0
49
56
  # rubocop:disable Metrics/MethodLength
50
57
  def extend_lock_ttl(
51
58
  redis_client,
@@ -75,16 +82,105 @@ module RedisQueuedLocks::Acquirer::ExtendLockTTL
75
82
  )
76
83
  end
77
84
 
85
+ # NOTE: a non-positive value would decrease the lock ttl or delete the lock;
86
+ if !milliseconds.is_a?(::Integer) || milliseconds <= 0
87
+ raise(
88
+ RedisQueuedLocks::ArgumentError,
89
+ "`milliseconds` argument should be a positive Integer, got #{milliseconds.inspect}."
90
+ )
91
+ end
92
+
93
+ if read_write_mode == :read && all_read_locks != true && all_read_locks != false
94
+ raise(
95
+ RedisQueuedLocks::ArgumentError,
96
+ "`:all_read_locks` argument should be a boolean, got #{all_read_locks.inspect}."
97
+ )
98
+ end
99
+
78
100
  lock_key = RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
79
101
 
80
- if read_write_mode == :read
81
- if all_read_locks != true && all_read_locks != false
82
- raise(
83
- RedisQueuedLocks::ArgumentError,
84
- "`:all_read_locks` argument should be a boolean, got #{all_read_locks.inspect}."
85
- )
102
+ ext_start_time = clock_gettime
103
+ result = fully_extend_lock_ttl(
104
+ redis_client, lock_name, lock_key, milliseconds, read_write_mode, all_read_locks,
105
+ acquirer_id, lock_index_enabled
106
+ )
107
+ time_at = Time.now.to_f
108
+ ext_end_time = clock_gettime
109
+ ext_time = ((ext_end_time - ext_start_time) / 1_000.0).ceil(2)
110
+ ext_result = result[:result]
111
+ extended_locks_count = ext_result.is_a?(::Hash) ? ext_result[:extended_locks_count] : 0
112
+
113
+ log_sampled = RedisQueuedLocks::Logging.should_log?(
114
+ log_sampling_enabled,
115
+ log_sample_this,
116
+ log_sampling_percent,
117
+ log_sampler
118
+ )
119
+ instr_sampled = RedisQueuedLocks::Instrument.should_instrument?(
120
+ instr_sampling_enabled,
121
+ instr_sample_this,
122
+ instr_sampling_percent,
123
+ instr_sampler
124
+ )
125
+
126
+ run_non_critical do
127
+ logger.debug do
128
+ "[redis_queued_locks.extend_lock_ttl] " \
129
+ "lock_key => '#{lock_key}' " \
130
+ "rw_mode => '#{read_write_mode}' " \
131
+ "all_read_locks => #{all_read_locks} " \
132
+ "ext_ttl => #{milliseconds} " \
133
+ "extended_locks_count => #{extended_locks_count} " \
134
+ "ext_time => #{ext_time} " \
135
+ "at => #{time_at}"
86
136
  end
137
+ end if log_sampled
138
+
139
+ run_non_critical do
140
+ instrumenter.notify('redis_queued_locks.extend_lock_ttl', {
141
+ lock_key: lock_key,
142
+ rw_mode: read_write_mode,
143
+ all_read_locks: all_read_locks,
144
+ ext_ttl: milliseconds,
145
+ extended_locks_count: extended_locks_count,
146
+ ext_time: ext_time,
147
+ at: time_at,
148
+ instrument: instrument
149
+ })
150
+ end if instr_sampled
151
+
152
+ result
153
+ end
154
+ # rubocop:enable Metrics/MethodLength
155
+
156
+ private
87
157
 
158
+ # Extends the write lock, the read lock of the acquirer or all read locks of the lock.
159
+ #
160
+ # @param redis_client [RedisClient]
161
+ # @param lock_name [String]
162
+ # @param lock_key [String]
163
+ # @param milliseconds [Integer]
164
+ # @param read_write_mode [Symbol]
165
+ # @param all_read_locks [Boolean]
166
+ # @param acquirer_id [String]
167
+ # @param lock_index_enabled [Boolean]
168
+ # @return [Hash<Symbol,Boolean|Symbol|Hash<Symbol,Integer>>]
169
+ #
170
+ # @api private
171
+ # @since 1.21.0
172
+ # rubocop:disable Metrics/MethodLength
173
+ def fully_extend_lock_ttl(
174
+ redis_client,
175
+ lock_name,
176
+ lock_key,
177
+ milliseconds,
178
+ read_write_mode,
179
+ all_read_locks,
180
+ acquirer_id,
181
+ lock_index_enabled
182
+ )
183
+ if read_write_mode == :read
88
184
  # NOTE: (RW) all live read locks of the lock
89
185
  if all_read_locks
90
186
  return extend_all_read_locks_ttl(
@@ -131,8 +227,6 @@ module RedisQueuedLocks::Acquirer::ExtendLockTTL
131
227
  end
132
228
  # rubocop:enable Metrics/MethodLength
133
229
 
134
- private
135
-
136
230
  # Extends the live read lock of the acquirer:
137
231
  # - the read lock expiration (readers registry score), the registry TTL and the read lock
138
232
  # data TTL are extended;
@@ -149,7 +243,7 @@ module RedisQueuedLocks::Acquirer::ExtendLockTTL
149
243
  #
150
244
  # @api private
151
245
  # @since 1.18.0
152
- # @version 1.20.0
246
+ # @version 1.21.0
153
247
  # rubocop:disable Metrics/MethodLength
154
248
  def extend_read_lock_ttl(
155
249
  redis_client,
@@ -191,6 +285,14 @@ module RedisQueuedLocks::Acquirer::ExtendLockTTL
191
285
  end
192
286
  end
193
287
  end
288
+ rescue RedisQueuedLocks::LockAcquirementIntermediateTimeoutError,
289
+ RedisQueuedLocks::TimedLockIntermediateTimeoutError,
290
+ ::Timeout::ExitException
291
+ # NOTE: (timeouts) an interrupted WATCH block (the internal timeouts and the user's
292
+ # `Timeout.timeout` without an exception class): the closed connection drops the WATCH
293
+ # state (redis-client sends `UNWATCH` only on `StandardError`);
294
+ rconn.close
295
+ raise
194
296
  end
195
297
 
196
298
  # NOTE:
@@ -223,7 +325,7 @@ module RedisQueuedLocks::Acquirer::ExtendLockTTL
223
325
  #
224
326
  # @api private
225
327
  # @since 1.18.0
226
- # @version 1.20.0
328
+ # @version 1.21.0
227
329
  # rubocop:disable Metrics/MethodLength
228
330
  def extend_all_read_locks_ttl(
229
331
  redis_client,
@@ -278,6 +380,14 @@ module RedisQueuedLocks::Acquirer::ExtendLockTTL
278
380
  end
279
381
  end
280
382
  end
383
+ rescue RedisQueuedLocks::LockAcquirementIntermediateTimeoutError,
384
+ RedisQueuedLocks::TimedLockIntermediateTimeoutError,
385
+ ::Timeout::ExitException
386
+ # NOTE: (timeouts) an interrupted WATCH block (the internal timeouts and the user's
387
+ # `Timeout.timeout` without an exception class): the closed connection drops the WATCH
388
+ # state (redis-client sends `UNWATCH` only on `StandardError`);
389
+ rconn.close
390
+ raise
281
391
  end
282
392
 
283
393
  # NOTE:
@@ -2,7 +2,6 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.0.0
5
- # @version 1.18.0
6
5
  module RedisQueuedLocks::Acquirer::IsLocked
7
6
  class << self
8
7
  # @param redis_client [RedisClient]
@@ -2,7 +2,6 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.0.0
5
- # @version 1.18.0
6
5
  module RedisQueuedLocks::Acquirer::IsQueued
7
6
  class << self
8
7
  # @param redis_client [RedisClient]
@@ -32,8 +32,17 @@ module RedisQueuedLocks::Acquirer::Keys
32
32
  #
33
33
  # @api private
34
34
  # @since 1.0.0
35
- # @version 1.20.0
35
+ # @version 1.21.0
36
36
  def keys(redis_client, scan_size:, lock_index_enabled:)
37
+ # NOTE: (arguments) the same range as the config key: zero/negative sizes fail SCAN/ZSCAN
38
+ # (`COUNT 0`) and the batching (`each_slice(0)`);
39
+ if !scan_size.is_a?(::Integer) || scan_size <= 0
40
+ raise(
41
+ RedisQueuedLocks::ArgumentError,
42
+ "`:scan_size` argument should be a positive Integer, got #{scan_size.inspect}."
43
+ )
44
+ end
45
+
37
46
  Set.new.tap do |keyset|
38
47
  # @type var keyset: Set[String]
39
48
  if lock_index_enabled
@@ -2,7 +2,7 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.0.0
5
- # @version 1.20.0
5
+ # @version 1.18.0
6
6
  module RedisQueuedLocks::Acquirer::LockInfo
7
7
  class << self
8
8
  # @param redis_client [RedisClient]
@@ -56,46 +56,38 @@ module RedisQueuedLocks::Acquirer::LockInfo
56
56
  pipeline.call('ZRANGE', lock_readers_key, '0', '-1', 'WITHSCORES')
57
57
  end
58
58
 
59
- if result == nil
60
- # NOTE:
61
- # - nil result means that during transaction invocation the lock is changed (CAS):
62
- # - lock is expired;
63
- # - lock is released;
64
- # - lock is expired + re-obtained;
65
- nil
66
- else
67
- # NOTE: the result of MULTI-command is an array of results of each internal command
68
- # - result[0] (HGETALL) (Hash<String,String>)
69
- # => (will be mutated further with different value types)
70
- # - result[1] (PTTL) (Integer)
71
- # => (without any mutation, integer is atomic)
72
- # - result[2] (TIME) (Array<String>) (redis time)
73
- # - result[3] (ZRANGE) (Array<[String,Float]>) (read locks)
59
+ # NOTE: (pipeline layout) the replies of the pipeline commands (`pipelined` always returns
60
+ # an array of the replies):
61
+ # - result[0] (HGETALL) (Hash<String,String>)
62
+ # => (will be mutated further with different value types)
63
+ # - result[1] (PTTL) (Integer)
64
+ # => (without any mutation, integer is atomic)
65
+ # - result[2] (TIME) (Array<String>) (redis time)
66
+ # - result[3] (ZRANGE) (Array<[String,Float]>) (read locks)
74
67
 
75
- # rubocop:disable Layout/LineLength
76
- # @type var result: [Hash[String,String|Float|Integer], Integer, Array[String], Array[[String, Float]]]
77
- # rubocop:enable Layout/LineLength
78
- hget_cmd_res = result[0]
79
- pttl_cmd_res = result[1]
68
+ # rubocop:disable Layout/LineLength
69
+ # @type var result: [Hash[String,String|Float|Integer], Integer, Array[String], Array[[String, Float]]]
70
+ # rubocop:enable Layout/LineLength
71
+ hget_cmd_res = result[0]
72
+ pttl_cmd_res = result[1]
80
73
 
81
- if hget_cmd_res == {} || pttl_cmd_res == -2 # NOTE: key does not exist
82
- # NOTE: (RW) there is no write lock => check read locks
83
- read_lock_info(redis_client, lock_name, lock_key, result[2], result[3])
84
- else
85
- hget_cmd_res.tap do |lock_data|
86
- lock_data['lock_key'] = lock_key
87
- lock_data['rw_mode'] = 'write'
88
- lock_data['ts'] = Float(lock_data['ts'])
89
- lock_data['ini_ttl'] = Integer(lock_data['ini_ttl'])
90
- lock_data['rem_ttl'] = ((pttl_cmd_res == -1) ? Float::INFINITY : pttl_cmd_res)
91
- lock_data['spc_cnt'] = Integer(lock_data['spc_cnt']) if lock_data['spc_cnt']
92
- lock_data['l_spc_ts'] = Float(lock_data['l_spc_ts']) if lock_data['l_spc_ts']
93
- lock_data['spc_ext_ttl'] = Integer(lock_data['spc_ext_ttl']) if lock_data['spc_ext_ttl']
94
- lock_data['l_spc_ext_ini_ttl'] =
95
- Integer(lock_data['l_spc_ext_ini_ttl']) if lock_data.key?('l_spc_ext_ini_ttl')
96
- lock_data['l_spc_ext_ts'] =
97
- Float(lock_data['l_spc_ext_ts']) if lock_data['l_spc_ext_ts']
98
- end
74
+ if hget_cmd_res == {} || pttl_cmd_res == -2 # NOTE: key does not exist
75
+ # NOTE: (RW) there is no write lock => check read locks
76
+ read_lock_info(redis_client, lock_name, lock_key, result[2], result[3])
77
+ else
78
+ hget_cmd_res.tap do |lock_data|
79
+ lock_data['lock_key'] = lock_key
80
+ lock_data['rw_mode'] = 'write'
81
+ lock_data['ts'] = Float(lock_data['ts'])
82
+ lock_data['ini_ttl'] = Integer(lock_data['ini_ttl'])
83
+ lock_data['rem_ttl'] = ((pttl_cmd_res == -1) ? Float::INFINITY : pttl_cmd_res)
84
+ lock_data['spc_cnt'] = Integer(lock_data['spc_cnt']) if lock_data['spc_cnt']
85
+ lock_data['l_spc_ts'] = Float(lock_data['l_spc_ts']) if lock_data['l_spc_ts']
86
+ lock_data['spc_ext_ttl'] = Integer(lock_data['spc_ext_ttl']) if lock_data['spc_ext_ttl']
87
+ lock_data['l_spc_ext_ini_ttl'] =
88
+ Integer(lock_data['l_spc_ext_ini_ttl']) if lock_data.key?('l_spc_ext_ini_ttl')
89
+ lock_data['l_spc_ext_ts'] =
90
+ Float(lock_data['l_spc_ext_ts']) if lock_data['l_spc_ext_ts']
99
91
  end
100
92
  end
101
93
  end
@@ -3,6 +3,7 @@
3
3
  # NOTE: Lock Series PoC
4
4
  # @api private
5
5
  # @since 1.16.1
6
+ # @version 1.18.0
6
7
  module RedisQueuedLocks::Acquirer::LockSeriesPoC::InstrVisitor # steep:ignore
7
8
  class << self
8
9
  # NOTE: Lock Series PoC
@@ -32,7 +33,7 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC::InstrVisitor # steep:ignore
32
33
  # @param rw_mode [Symbol] The mode of the lock series (`:read` or `:write`).
33
34
  # @api private
34
35
  # @since 1.16.1
35
- # @version 1.18.0
36
+ # @version 1.21.0
36
37
  def lock_series_hold_and_release( # steep:ignore
37
38
  instrumenter,
38
39
  instr_sampled,
@@ -49,7 +50,7 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC::InstrVisitor # steep:ignore
49
50
  return unless instr_sampled
50
51
  instrumenter.notify('redis_queued_locks.lock_series_hold_and_release', {
51
52
  lock_keys:, rw_mode:, hold_time:, ttl:, acq_id:, hst_id:, ts:, acq_time:, instrument:
52
- }) # rescue nil
53
+ }) rescue nil
53
54
  end
54
55
  end
55
56
  end
@@ -3,13 +3,14 @@
3
3
  # NOTE: Lock Series PoC
4
4
  # @api private
5
5
  # @since 1.16.1
6
+ # @version 1.18.0
6
7
  module RedisQueuedLocks::Acquirer::LockSeriesPoC::LogVisitor # steep:ignore
7
8
  class << self
8
9
  # NOTE: Lock Series PoC
9
10
  # @param rw_mode [Symbol] The mode of the lock series (`:read` or `:write`).
10
11
  # @api private
11
12
  # @since 1.16.0
12
- # @version 1.18.0
13
+ # @version 1.21.0
13
14
  def start_lock_series_obtaining( # steep:ignore
14
15
  logger,
15
16
  log_sampled,
@@ -30,14 +31,14 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC::LogVisitor # steep:ignore
30
31
  "hst_id => '#{host_id}' " \
31
32
  "acs_strat => '#{access_strategy}' " \
32
33
  "rw_mode => '#{rw_mode}'"
33
- end # rescue nil
34
+ end rescue nil
34
35
  end
35
36
 
36
37
  # NOTE: Lock Series PoC
37
38
  # @param rw_mode [Symbol] The mode of the lock series (`:read` or `:write`).
38
39
  # @api private
39
40
  # @since 1.16.0
40
- # @version 1.18.0
41
+ # @version 1.21.0
41
42
  def lock_series_obtained( # steep:ignore
42
43
  logger,
43
44
  log_sampled,
@@ -60,7 +61,7 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC::LogVisitor # steep:ignore
60
61
  "acs_strat => '#{access_strategy}' " \
61
62
  "rw_mode => '#{rw_mode}' " \
62
63
  "acq_time => #{acq_time} (ms)"
63
- end # rescue nil
64
+ end rescue nil
64
65
  end
65
66
 
66
67
  # NOTE: Lock Series PoC
@@ -4,17 +4,18 @@
4
4
  # rubocop:disable all
5
5
  # @api private
6
6
  # @since 1.16.0
7
+ # @version 1.20.0
7
8
  module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
8
9
  require_relative 'lock_series_poc/log_visitor'
9
10
  require_relative 'lock_series_poc/instr_visitor'
10
11
 
11
- # @sine 1.16.0
12
+ # @since 1.16.0
12
13
  extend RedisQueuedLocks::Acquirer::AcquireLock::YieldExpire
13
14
 
14
15
  class << self
15
16
  # @api private
16
17
  # @since 1.16.0
17
- # @version 1.20.0
18
+ # @version 1.21.0
18
19
  def lock_series_poc( # steep:ignore
19
20
  redis,
20
21
  lock_names,
@@ -66,19 +67,22 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
66
67
  )
67
68
  end
68
69
 
70
+ # NOTE: Symbol keys are written to the lock data as Strings (the same check as in
71
+ # RedisQueuedLocks::Acquirer::AcquireLock);
69
72
  if meta.is_a?(::Hash) && (meta.any? do |key, _value|
70
- key == 'acq_id' ||
71
- key == 'hst_id' ||
72
- key == 'ts' ||
73
- key == 'ini_ttl' ||
74
- key == 'lock_key' ||
75
- key == 'rem_ttl' ||
76
- key == 'spc_ext_ttl' ||
77
- key == 'spc_cnt' ||
78
- key == 'l_spc_ext_ini_ttl' ||
79
- key == 'l_spc_ext_ts' ||
80
- key == 'l_spc_ts' ||
81
- key == 'rw_mode'
73
+ meta_key = key.is_a?(::Symbol) ? key.name : key
74
+ meta_key == 'acq_id' ||
75
+ meta_key == 'hst_id' ||
76
+ meta_key == 'ts' ||
77
+ meta_key == 'ini_ttl' ||
78
+ meta_key == 'lock_key' ||
79
+ meta_key == 'rem_ttl' ||
80
+ meta_key == 'spc_ext_ttl' ||
81
+ meta_key == 'spc_cnt' ||
82
+ meta_key == 'l_spc_ext_ini_ttl' ||
83
+ meta_key == 'l_spc_ext_ts' ||
84
+ meta_key == 'l_spc_ts' ||
85
+ meta_key == 'rw_mode'
82
86
  end)
83
87
  raise(
84
88
  RedisQueuedLocks::ArgumentError,
@@ -88,6 +92,34 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
88
92
  )
89
93
  end
90
94
 
95
+ # NOTE: the series ttl is validated before the lock ttl calculation: each lock ttl of
96
+ # the series includes the retry delay and jitter (a non-positive series ttl can produce
97
+ # a positive lock ttl), the series ttl limits the `timed` block (the same check as in
98
+ # RedisQueuedLocks::Acquirer::AcquireLock);
99
+ if !ttl.is_a?(::Integer) || ttl <= 0
100
+ raise(
101
+ RedisQueuedLocks::ArgumentError,
102
+ "`:ttl` argument should be a positive Integer, got #{ttl.inspect}."
103
+ )
104
+ end
105
+
106
+ # NOTE: the retry delay and jitter are parts of the lock ttls of the series (validated before
107
+ # the lock ttl calculation; other options are validated by
108
+ # RedisQueuedLocks::Acquirer::AcquireLock on the first lock of the series, before any lock
109
+ # is obtained);
110
+ if !retry_delay.is_a?(::Integer) || retry_delay < 0
111
+ raise(
112
+ RedisQueuedLocks::ArgumentError,
113
+ "`:retry_delay` argument should be a non-negative Integer, got #{retry_delay.inspect}."
114
+ )
115
+ end
116
+ if !retry_jitter.is_a?(::Integer) || retry_jitter < 0
117
+ raise(
118
+ RedisQueuedLocks::ArgumentError,
119
+ "`:retry_jitter` argument should be a non-negative Integer, got #{retry_jitter.inspect}."
120
+ )
121
+ end
122
+
91
123
  locks_count = lock_names.size
92
124
 
93
125
  lock_acquirement_operations = lock_names.each_with_index.map do |lock_name, position|
@@ -374,7 +406,7 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
374
406
  #
375
407
  # @api private
376
408
  # @since 1.18.0
377
- # @version 1.20.0
409
+ # @version 1.21.0
378
410
  def release_write_lock_series(redis, lock_names, acquirer_id) # steep:ignore
379
411
  return if lock_names.empty?
380
412
 
@@ -397,6 +429,14 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
397
429
  # NOTE: `nil` => the transaction is aborted (a watched lock is changed) => retry
398
430
  break unless released.nil?
399
431
  end
432
+ rescue RedisQueuedLocks::LockAcquirementIntermediateTimeoutError,
433
+ RedisQueuedLocks::TimedLockIntermediateTimeoutError,
434
+ ::Timeout::ExitException
435
+ # NOTE: (timeouts) an interrupted WATCH block (the internal timeouts and the user's
436
+ # `Timeout.timeout` without an exception class): the closed connection drops the WATCH
437
+ # state (redis-client sends `UNWATCH` only on `StandardError`);
438
+ conn.close
439
+ raise
400
440
  end
401
441
  end
402
442
 
@@ -25,8 +25,17 @@ module RedisQueuedLocks::Acquirer::Locks
25
25
  #
26
26
  # @api private
27
27
  # @since 1.0.0
28
- # @version 1.20.0
28
+ # @version 1.21.0
29
29
  def locks(redis_client, scan_size:, with_info:, lock_index_enabled:)
30
+ # NOTE: (arguments) the same range as the config key: zero/negative sizes fail SCAN/ZSCAN
31
+ # (`COUNT 0`) and the batching (`each_slice(0)`);
32
+ if !scan_size.is_a?(::Integer) || scan_size <= 0
33
+ raise(
34
+ RedisQueuedLocks::ArgumentError,
35
+ "`:scan_size` argument should be a positive Integer, got #{scan_size.inspect}."
36
+ )
37
+ end
38
+
30
39
  redis_client.with do |rconn|
31
40
  lock_keys =
32
41
  lock_index_enabled ? index_locks(rconn, scan_size) : scan_locks(rconn, scan_size)
@@ -153,7 +162,7 @@ module RedisQueuedLocks::Acquirer::Locks
153
162
  #
154
163
  # @api private
155
164
  # @since 1.0.0
156
- # @version 1.20.0
165
+ # @version 1.21.0
157
166
  # rubocop:disable Metrics/MethodLength
158
167
  def extract_locks_info(redis_client, lock_keys, scan_size)
159
168
  Set.new.tap do |seeded_locks|
@@ -162,6 +171,13 @@ module RedisQueuedLocks::Acquirer::Locks
162
171
  # rubocop:enable Layout/LineLength
163
172
 
164
173
  lock_keys.each_slice(scan_size) do |lock_keys_batch|
174
+ # Step 0: lock names of the batch (once per lock: the readers registry keys and the read
175
+ # lock data keys are built from them)
176
+ # @type var lock_names_batch: Array[String]
177
+ lock_names_batch = lock_keys_batch.map do |lock_key|
178
+ RedisQueuedLocks::Resource.lock_name_from_key(lock_key, :lock) #: String
179
+ end
180
+
165
181
  # Step 1: extract the state of the batch of locks in one round trip
166
182
  # NOTE: (pipeline layout) [0] TIME (read locks are checked in redis time), then
167
183
  # `LOCK_STATE_REPLIES` replies per lock in the batch order (the replies of the lock
@@ -170,10 +186,9 @@ module RedisQueuedLocks::Acquirer::Locks
170
186
  # @type var batch_state: Array[untyped]
171
187
  batch_state = redis_client.pipelined do |pipeline|
172
188
  pipeline.call('TIME')
173
- lock_keys_batch.each do |lock_key|
174
- lock_readers_key = RedisQueuedLocks::Resource.prepare_lock_readers(
175
- lock_key.delete_prefix('rql:lock:')
176
- )
189
+ lock_keys_batch.each_with_index do |lock_key, index|
190
+ lock_readers_key =
191
+ RedisQueuedLocks::Resource.prepare_lock_readers(lock_names_batch[index])
177
192
  pipeline.call('HGETALL', lock_key)
178
193
  pipeline.call('PTTL', lock_key)
179
194
  pipeline.call('ZRANGE', lock_readers_key, '0', '-1', 'WITHSCORES')
@@ -199,12 +214,13 @@ module RedisQueuedLocks::Acquirer::Locks
199
214
  # read lock data can be absent if it is dropped during the read lock life;
200
215
  # @type var read_locks_data: Array[Hash[String,String]]
201
216
  read_locks_data = redis_client.pipelined do |pipeline|
202
- lock_keys_batch.each_with_index do |lock_key, index|
203
- lock_name = lock_key.delete_prefix('rql:lock:')
217
+ lock_keys_batch.each_index do |index|
204
218
  live_read_locks[index]&.each do |(acquirer_id, _expiration)|
205
219
  pipeline.call(
206
220
  'HGETALL',
207
- RedisQueuedLocks::Resource.prepare_read_lock_key(lock_name, acquirer_id)
221
+ RedisQueuedLocks::Resource.prepare_read_lock_key(
222
+ lock_names_batch[index], acquirer_id
223
+ )
208
224
  )
209
225
  end
210
226
  end
@@ -232,6 +248,7 @@ module RedisQueuedLocks::Acquirer::Locks
232
248
  pttl_cmd_res = batch_state[replies_at + 1] # NOTE: PTTL result (integer)
233
249
 
234
250
  hget_cmd_res.tap do |lock_data|
251
+ lock_data['lock_key'] = lock_key
235
252
  lock_data['rw_mode'] = 'write'
236
253
  lock_data['ts'] = Float(lock_data['ts'])
237
254
  lock_data['ini_ttl'] = Integer(lock_data['ini_ttl'])
@@ -2,7 +2,6 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.0.0
5
- # @version 1.20.0
6
5
  module RedisQueuedLocks::Acquirer::QueueInfo
7
6
  class << self
8
7
  # Returns an information about the required lock queue by the lock name. The result
@@ -15,8 +15,17 @@ module RedisQueuedLocks::Acquirer::Queues
15
15
  #
16
16
  # @api private
17
17
  # @since 1.0.0
18
- # @version 1.20.0
18
+ # @version 1.21.0
19
19
  def queues(redis_client, scan_size:, with_info:, lock_index_enabled:)
20
+ # NOTE: (arguments) the same range as the config key: zero/negative sizes fail SCAN/ZSCAN
21
+ # (`COUNT 0`) and the batching (`each_slice(0)`);
22
+ if !scan_size.is_a?(::Integer) || scan_size <= 0
23
+ raise(
24
+ RedisQueuedLocks::ArgumentError,
25
+ "`:scan_size` argument should be a positive Integer, got #{scan_size.inspect}."
26
+ )
27
+ end
28
+
20
29
  redis_client.with do |rconn|
21
30
  lock_queues =
22
31
  lock_index_enabled ? index_queues(rconn, scan_size) : scan_queues(rconn, scan_size)