redis_queued_locks 1.22.0 → 1.23.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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/project-overview.md +88 -36
  3. data/.claude/rules/acquirer.md +11 -7
  4. data/.claude/rules/arguments.md +2 -2
  5. data/.claude/rules/logic.md +5 -4
  6. data/.claude/rules/tests.md +4 -4
  7. data/.claude/rules/type-checking.md +2 -3
  8. data/.claude/rules/visitors.md +8 -6
  9. data/.claude/skills/rql-code-review/SKILL.md +25 -20
  10. data/.ruby-version +1 -1
  11. data/CHANGELOG.md +24 -1
  12. data/CLAUDE.md +5 -4
  13. data/README.md +247 -96
  14. data/lib/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue.rb +4 -4
  15. data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rb +582 -276
  16. data/lib/redis_queued_locks/acquirer/acquire_lock.rb +26 -26
  17. data/lib/redis_queued_locks/acquirer/extend_lock_ttl.rb +13 -13
  18. data/lib/redis_queued_locks/acquirer/lock_series/instr_visitor.rb +243 -0
  19. data/lib/redis_queued_locks/acquirer/lock_series/log_visitor.rb +472 -0
  20. data/lib/redis_queued_locks/acquirer/lock_series/release_lock_series.rb +181 -0
  21. data/lib/redis_queued_locks/acquirer/lock_series/try_to_lock_series.rb +1051 -0
  22. data/lib/redis_queued_locks/acquirer/lock_series.rb +948 -0
  23. data/lib/redis_queued_locks/acquirer.rb +1 -1
  24. data/lib/redis_queued_locks/client.rb +55 -25
  25. data/lib/redis_queued_locks/version.rb +2 -2
  26. data/redis_queued_locks.gemspec +21 -14
  27. data/sig/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue.rbs +1 -1
  28. data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rbs +106 -5
  29. data/sig/redis_queued_locks/acquirer/acquire_lock.rbs +1 -1
  30. data/sig/redis_queued_locks/acquirer/extend_lock_ttl.rbs +2 -2
  31. data/sig/redis_queued_locks/acquirer/lock_series/instr_visitor.rbs +102 -0
  32. data/sig/redis_queued_locks/acquirer/lock_series/log_visitor.rbs +154 -0
  33. data/sig/redis_queued_locks/acquirer/lock_series/release_lock_series.rbs +37 -0
  34. data/sig/redis_queued_locks/acquirer/lock_series/try_to_lock_series.rbs +198 -0
  35. data/sig/redis_queued_locks/acquirer/lock_series.rbs +106 -0
  36. data/sig/redis_queued_locks/client.rbs +70 -3
  37. metadata +32 -11
  38. data/lib/redis_queued_locks/acquirer/lock_series_poc/instr_visitor.rb +0 -61
  39. data/lib/redis_queued_locks/acquirer/lock_series_poc/log_visitor.rb +0 -104
  40. data/lib/redis_queued_locks/acquirer/lock_series_poc.rb +0 -513
@@ -1,61 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # NOTE: Lock Series PoC
4
- # @api private
5
- # @since 1.16.1
6
- # @version 1.22.0
7
- module RedisQueuedLocks::Acquirer::LockSeriesPoC::InstrVisitor # steep:ignore
8
- class << self
9
- # NOTE: Lock Series PoC
10
- # @param rw_mode [Symbol] The mode of the lock series (`:read` or `:write`).
11
- # @param priority [Integer] The lock request priority of the series.
12
- # @api private
13
- # @since 1.16.1
14
- # @version 1.22.0
15
- def lock_series_obtained( # steep:ignore
16
- instrumenter,
17
- instr_sampled,
18
- lock_keys,
19
- rw_mode,
20
- priority,
21
- ttl,
22
- acq_id,
23
- hst_id,
24
- ts,
25
- acq_time,
26
- instrument
27
- )
28
- return unless instr_sampled
29
- instrumenter.notify('redis_queued_locks.lock_series_obtained', {
30
- lock_keys:, rw_mode:, priority:, ttl:, acq_id:, hst_id:, ts:, acq_time:, instrument:
31
- }) rescue nil
32
- end
33
-
34
- # NOTE: Lock Series PoC
35
- # @param rw_mode [Symbol] The mode of the lock series (`:read` or `:write`).
36
- # @param priority [Integer] The lock request priority of the series.
37
- # @api private
38
- # @since 1.16.1
39
- # @version 1.22.0
40
- def lock_series_hold_and_release( # steep:ignore
41
- instrumenter,
42
- instr_sampled,
43
- lock_keys,
44
- rw_mode,
45
- priority,
46
- ttl,
47
- acq_id,
48
- hst_id,
49
- ts,
50
- acq_time,
51
- hold_time,
52
- instrument
53
- )
54
- return unless instr_sampled
55
- instrumenter.notify('redis_queued_locks.lock_series_hold_and_release', {
56
- lock_keys:, rw_mode:, priority:, hold_time:, ttl:, acq_id:, hst_id:, ts:, acq_time:,
57
- instrument:
58
- }) rescue nil
59
- end
60
- end
61
- end
@@ -1,104 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # NOTE: Lock Series PoC
4
- # @api private
5
- # @since 1.16.1
6
- # @version 1.22.0
7
- module RedisQueuedLocks::Acquirer::LockSeriesPoC::LogVisitor # steep:ignore
8
- class << self
9
- # NOTE: Lock Series PoC
10
- # @param rw_mode [Symbol] The mode of the lock series (`:read` or `:write`).
11
- # @param priority [Integer] The lock request priority of the series.
12
- # @api private
13
- # @since 1.16.0
14
- # @version 1.22.0
15
- def start_lock_series_obtaining( # steep:ignore
16
- logger,
17
- log_sampled,
18
- lock_keys,
19
- queue_ttl,
20
- acquirer_id,
21
- host_id,
22
- access_strategy,
23
- rw_mode,
24
- priority
25
- )
26
- return unless log_sampled
27
-
28
- logger.debug do
29
- "[redis_queued_locks.start_lock_series_obtaining] " \
30
- "lock_keys => '#{lock_keys.inspect}' " \
31
- "queue_ttl => #{queue_ttl} " \
32
- "acq_id => '#{acquirer_id}' " \
33
- "hst_id => '#{host_id}' " \
34
- "acs_strat => '#{access_strategy}' " \
35
- "rw_mode => '#{rw_mode}' " \
36
- "priority => #{priority}"
37
- end rescue nil
38
- end
39
-
40
- # NOTE: Lock Series PoC
41
- # @param rw_mode [Symbol] The mode of the lock series (`:read` or `:write`).
42
- # @param priority [Integer] The lock request priority of the series.
43
- # @api private
44
- # @since 1.16.0
45
- # @version 1.22.0
46
- def lock_series_obtained( # steep:ignore
47
- logger,
48
- log_sampled,
49
- lock_keys,
50
- queue_ttl,
51
- acquirer_id,
52
- host_id,
53
- acq_time,
54
- access_strategy,
55
- rw_mode,
56
- priority
57
- )
58
- return unless log_sampled
59
-
60
- logger.debug do
61
- "[redis_queued_locks.lock_series_obtained] " \
62
- "lock_keys => '#{lock_keys.inspect}' " \
63
- "queue_ttl => #{queue_ttl} " \
64
- "acq_id => '#{acquirer_id}' " \
65
- "hst_id => '#{host_id}' " \
66
- "acs_strat => '#{access_strategy}' " \
67
- "rw_mode => '#{rw_mode}' " \
68
- "priority => #{priority} " \
69
- "acq_time => #{acq_time} (ms)"
70
- end rescue nil
71
- end
72
-
73
- # NOTE: Lock Series PoC
74
- # @param rw_mode [Symbol] The mode of the lock series (`:read` or `:write`).
75
- # @param priority [Integer] The lock request priority of the series.
76
- # @api private
77
- # @since 1.16.0
78
- # @version 1.22.0
79
- def expire_lock_series( # steep:ignore
80
- logger,
81
- log_sampled,
82
- lock_series,
83
- queue_ttl,
84
- acquirer_id,
85
- host_id,
86
- access_strategy,
87
- rw_mode,
88
- priority
89
- )
90
- return unless log_sampled
91
-
92
- logger.debug do
93
- "[redis_queued_locks.expire_lock_series] " \
94
- "lock_keys => '#{lock_series.inspect}' " \
95
- "queue_ttl => #{queue_ttl} " \
96
- "acq_id => '#{acquirer_id}' " \
97
- "hst_id => '#{host_id}' " \
98
- "acs_strat => '#{access_strategy}' " \
99
- "rw_mode => '#{rw_mode}' " \
100
- "priority => #{priority}"
101
- end rescue nil
102
- end
103
- end
104
- end
@@ -1,513 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # NOTE: Lock Series PoC
4
- # rubocop:disable Metrics/ModuleLength, Metrics/ClassLength, Metrics/MethodLength
5
- # @api private
6
- # @since 1.16.0
7
- # @version 1.22.0
8
- module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
9
- require_relative 'lock_series_poc/log_visitor'
10
- require_relative 'lock_series_poc/instr_visitor'
11
-
12
- # @since 1.16.0
13
- extend RedisQueuedLocks::Acquirer::AcquireLock::YieldExpire
14
-
15
- class << self
16
- # @api private
17
- # @since 1.16.0
18
- # @version 1.22.0
19
- def lock_series_poc( # steep:ignore
20
- redis,
21
- lock_names,
22
- detailed_result:,
23
- process_id:,
24
- thread_id:,
25
- fiber_id:,
26
- ractor_id:,
27
- ttl:,
28
- queue_ttl:,
29
- lock_index_enabled:,
30
- lock_index_grace_period:,
31
- timeout:,
32
- timed:,
33
- retry_count:,
34
- retry_delay:,
35
- retry_jitter:,
36
- raise_errors:,
37
- instrumenter:,
38
- identity:,
39
- fail_fast:,
40
- meta:,
41
- detailed_acq_timeout_error:,
42
- instrument:,
43
- logger:,
44
- log_lock_try:,
45
- conflict_strategy:,
46
- read_write_mode:,
47
- access_strategy:,
48
- priority:,
49
- lock_priorities:,
50
- log_sampling_enabled:,
51
- log_sampling_percent:,
52
- log_sampler:,
53
- log_sample_this:,
54
- instr_sampling_enabled:,
55
- instr_sampling_percent:,
56
- instr_sampler:,
57
- instr_sample_this:,
58
- &block
59
- )
60
- # NOTE: (RW) the lock mode is the same for all locks of the series
61
- # (see RedisQueuedLocks::Acquirer::AcquireLock for read_write_mode validations);
62
- # NOTE: the lock request priority is the same for all locks of the series too (validated
63
- # below: the series logs and instrumentation report it);
64
-
65
- case meta
66
- when Hash, NilClass then nil
67
- else
68
- raise(
69
- RedisQueuedLocks::ArgumentError,
70
- "`:meta` argument should be a type of NilClass or Hash, got #{meta.class}."
71
- )
72
- end
73
-
74
- # NOTE: Symbol keys are written to the lock data as Strings (the same check as in
75
- # RedisQueuedLocks::Acquirer::AcquireLock);
76
- if meta.is_a?(::Hash) && (meta.any? do |key, _value|
77
- meta_key = key.is_a?(::Symbol) ? key.name : key
78
- meta_key == 'acq_id' ||
79
- meta_key == 'hst_id' ||
80
- meta_key == 'ts' ||
81
- meta_key == 'ini_ttl' ||
82
- meta_key == 'lock_key' ||
83
- meta_key == 'rem_ttl' ||
84
- meta_key == 'spc_ext_ttl' ||
85
- meta_key == 'spc_cnt' ||
86
- meta_key == 'l_spc_ext_ini_ttl' ||
87
- meta_key == 'l_spc_ext_ts' ||
88
- meta_key == 'l_spc_ts' ||
89
- meta_key == 'rw_mode'
90
- end)
91
- raise(
92
- RedisQueuedLocks::ArgumentError,
93
- '`:meta` keys can not overlap reserved lock data keys ' \
94
- '"acq_id", "hst_id", "ts", "ini_ttl", "lock_key", "rem_ttl", "spc_cnt", ' \
95
- '"spc_ext_ttl", "l_spc_ext_ini_ttl", "l_spc_ext_ts", "l_spc_ts", "rw_mode"'
96
- )
97
- end
98
-
99
- # NOTE: the series ttl is validated before the lock ttl calculation: each lock ttl of
100
- # the series includes the retry delay and jitter (a non-positive series ttl can produce
101
- # a positive lock ttl), the series ttl limits the `timed` block (the same check as in
102
- # RedisQueuedLocks::Acquirer::AcquireLock);
103
- if !ttl.is_a?(::Integer) || ttl <= 0
104
- raise(
105
- RedisQueuedLocks::ArgumentError,
106
- "`:ttl` argument should be a positive Integer, got #{ttl.inspect}."
107
- )
108
- end
109
-
110
- # NOTE: the retry delay and jitter are parts of the lock ttls of the series (validated before
111
- # the lock ttl calculation; other options are validated by
112
- # RedisQueuedLocks::Acquirer::AcquireLock on the first lock of the series, before any lock
113
- # is obtained);
114
- if !retry_delay.is_a?(::Integer) || retry_delay < 0
115
- raise(
116
- RedisQueuedLocks::ArgumentError,
117
- "`:retry_delay` argument should be a non-negative Integer, got #{retry_delay.inspect}."
118
- )
119
- end
120
- if !retry_jitter.is_a?(::Integer) || retry_jitter < 0
121
- raise(
122
- RedisQueuedLocks::ArgumentError,
123
- "`:retry_jitter` argument should be a non-negative Integer, got #{retry_jitter.inspect}."
124
- )
125
- end
126
-
127
- # NOTE: the priority of the series is validated before the series logs and instrumentation:
128
- # a priority name is resolved to its Integer priority (the series events report the Integer
129
- # priority; the same check as in RedisQueuedLocks::Acquirer::AcquireLock);
130
- priority = lock_priorities.fetch(priority, priority) if priority.is_a?(::Symbol)
131
- if !priority.is_a?(::Integer) ||
132
- priority < RedisQueuedLocks::Resource::MIN_LOCK_PRIORITY ||
133
- priority > RedisQueuedLocks::Resource::MAX_LOCK_PRIORITY
134
- raise(
135
- RedisQueuedLocks::ArgumentError,
136
- "`:priority` argument should be an Integer from " \
137
- "#{RedisQueuedLocks::Resource::MIN_LOCK_PRIORITY} to " \
138
- "#{RedisQueuedLocks::Resource::MAX_LOCK_PRIORITY} or a priority name of " \
139
- "`config['lock_priorities']` (#{lock_priorities.keys.map(&:inspect).join(', ')}), " \
140
- "got #{priority.inspect}."
141
- )
142
- end
143
-
144
- locks_count = lock_names.size
145
-
146
- lock_acquirement_operations = lock_names.each_with_index.map do |lock_name, position|
147
- lock_ttl =
148
- ttl * (locks_count - position) +
149
- retry_delay * (locks_count - position) +
150
- retry_jitter * (locks_count - position)
151
-
152
- { lock_name: lock_name, ttl: lock_ttl }
153
- end
154
-
155
- log_sampled = RedisQueuedLocks::Logging.should_log?(
156
- log_sampling_enabled,
157
- log_sample_this,
158
- log_sampling_percent,
159
- log_sampler
160
- )
161
-
162
- instr_sampled = RedisQueuedLocks::Instrument.should_instrument?(
163
- instr_sampling_enabled,
164
- instr_sample_this,
165
- instr_sampling_percent,
166
- instr_sampler
167
- )
168
-
169
- lock_keys_for_instrumentation = lock_names.map do |lock_name|
170
- RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
171
- end
172
-
173
- acquirer_id_for_instrumentation = RedisQueuedLocks::Resource.acquirer_identifier(
174
- process_id,
175
- thread_id,
176
- fiber_id,
177
- ractor_id,
178
- identity
179
- )
180
-
181
- host_id_for_instrumentation = RedisQueuedLocks::Resource.host_identifier(
182
- process_id,
183
- thread_id,
184
- ractor_id,
185
- identity
186
- )
187
-
188
- LogVisitor.start_lock_series_obtaining( # steep:ignore
189
- logger, log_sampled, lock_keys_for_instrumentation,
190
- queue_ttl, acquirer_id_for_instrumentation, host_id_for_instrumentation,
191
- access_strategy, read_write_mode, priority
192
- )
193
-
194
- acq_start_time = RedisQueuedLocks::Utilities.clock_gettime
195
- successfully_acquired_locks = [] # steep:ignore
196
- failed_locks_and_errors = {} # steep:ignore
197
- failed_on_lock = nil
198
- # NOTE: locks of the failed series are released once (exceptional or not)
199
- is_failed_series_released = false
200
-
201
- lock_acquirement_operations.map do |lock_operation_options|
202
- result =
203
- begin
204
- RedisQueuedLocks::Acquirer::AcquireLock.acquire_lock(
205
- redis,
206
- lock_operation_options[:lock_name],
207
- process_id:,
208
- thread_id:,
209
- fiber_id:,
210
- ractor_id:,
211
- ttl: lock_operation_options[:ttl],
212
- queue_ttl:,
213
- lock_index_enabled:,
214
- lock_index_grace_period:,
215
- timeout:,
216
- timed:,
217
- retry_count:,
218
- retry_delay:,
219
- retry_jitter:,
220
- raise_errors:,
221
- instrumenter:,
222
- identity:,
223
- fail_fast:,
224
- meta:,
225
- detailed_acq_timeout_error:,
226
- instrument:,
227
- logger:,
228
- log_lock_try:,
229
- conflict_strategy:,
230
- read_write_mode:,
231
- access_strategy:,
232
- priority:,
233
- lock_priorities:,
234
- log_sampling_enabled:,
235
- log_sampling_percent:,
236
- log_sampler:,
237
- log_sample_this:,
238
- instr_sampling_enabled:,
239
- instr_sampling_percent:,
240
- instr_sampler:,
241
- instr_sample_this:
242
- )
243
- rescue RedisQueuedLocks::LockAlreadyObtainedError,
244
- RedisQueuedLocks::LockAcquirementIntermediateTimeoutError,
245
- RedisQueuedLocks::LockAcquirementTimeoutError,
246
- RedisQueuedLocks::LockAcquirementRetryLimitError,
247
- RedisQueuedLocks::ConflictLockObtainError => error
248
- # NOTE: release all previously acquired locks if any next lock is already locked
249
- release_lock_series( # steep:ignore
250
- redis, read_write_mode, successfully_acquired_locks, acquirer_id_for_instrumentation
251
- )
252
- is_failed_series_released = true
253
-
254
- raise(error) if raise_errors
255
- end
256
-
257
- if result[:ok]
258
- successfully_acquired_locks << {
259
- lock_name: lock_operation_options[:lock_name],
260
- ok: result[:ok],
261
- result: result[:result]
262
- }
263
- else
264
- failed_on_lock = lock_operation_options[:lock_name]
265
- failed_locks_and_errors[lock_operation_options[:lock_name]] = result
266
- # NOTE: release all previously acquired locks
267
- # (non-exceptional failure: raise_errors: false)
268
- unless is_failed_series_released
269
- release_lock_series( # steep:ignore
270
- redis, read_write_mode, successfully_acquired_locks, acquirer_id_for_instrumentation
271
- )
272
- is_failed_series_released = true
273
- end
274
- break
275
- end
276
- end
277
-
278
- if successfully_acquired_locks.size == lock_names.size &&
279
- successfully_acquired_locks.all? { |res| res[:ok] }
280
- acq_end_time = RedisQueuedLocks::Utilities.clock_gettime
281
- acq_time = ((acq_end_time - acq_start_time) / 1_000.0).ceil(2)
282
- ts = Time.now.to_f
283
-
284
- LogVisitor.lock_series_obtained( # steep:ignore
285
- logger, log_sampled, lock_keys_for_instrumentation,
286
- queue_ttl, acquirer_id_for_instrumentation, host_id_for_instrumentation,
287
- acq_time, access_strategy, read_write_mode, priority
288
- )
289
-
290
- InstrVisitor.lock_series_obtained( # steep:ignore
291
- instrumenter, instr_sampled, lock_keys_for_instrumentation, read_write_mode, priority,
292
- ttl, acquirer_id_for_instrumentation, host_id_for_instrumentation, ts, acq_time,
293
- instrument
294
- )
295
-
296
- yield_time = RedisQueuedLocks::Utilities.clock_gettime
297
- ttl_shift = (
298
- (yield_time - acq_end_time) / 1_000.0 -
299
- RedisQueuedLocks::Resource::REDIS_TIMESHIFT_ERROR
300
- ).ceil(2)
301
-
302
- yield_result = nil
303
- # @type var is_lock_manually_released: bool
304
- # @type var hold_time: Float | Integer
305
-
306
- begin
307
- yield_result = yield_expire( # steep:ignore
308
- redis,
309
- logger,
310
- lock_keys_for_instrumentation.last,
311
- RedisQueuedLocks::Resource.prepare_lock_readers(lock_names.last),
312
- RedisQueuedLocks::Resource.prepare_read_lock_key(
313
- lock_names.last, acquirer_id_for_instrumentation
314
- ),
315
- read_write_mode,
316
- acquirer_id_for_instrumentation,
317
- host_id_for_instrumentation,
318
- access_strategy,
319
- timed,
320
- ttl_shift,
321
- ttl,
322
- queue_ttl,
323
- meta,
324
- log_sampled,
325
- instr_sampled,
326
- false, # should_expire (expire manually)
327
- false, # should_decrease (expire manually)
328
- &block
329
- )
330
- ensure
331
- is_lock_manually_released = false
332
-
333
- # expire locks manually
334
- if block_given?
335
- release_lock_series( # steep:ignore
336
- redis, read_write_mode, successfully_acquired_locks, acquirer_id_for_instrumentation
337
- )
338
- is_lock_manually_released = true
339
- end
340
-
341
- rel_time = RedisQueuedLocks::Utilities.clock_gettime
342
- hold_time = ((rel_time - acq_end_time) / 1_000.0).ceil(2)
343
- ts = Time.now.to_f
344
-
345
- LogVisitor.expire_lock_series( # steep:ignore
346
- logger, log_sampled, lock_keys_for_instrumentation,
347
- queue_ttl, acquirer_id_for_instrumentation, host_id_for_instrumentation,
348
- access_strategy, read_write_mode, priority
349
- ) if is_lock_manually_released
350
-
351
- InstrVisitor.lock_series_hold_and_release( # steep:ignore
352
- instrumenter,
353
- instr_sampled,
354
- lock_keys_for_instrumentation,
355
- read_write_mode,
356
- priority,
357
- ttl,
358
- acquirer_id_for_instrumentation,
359
- host_id_for_instrumentation,
360
- ts,
361
- acq_time,
362
- hold_time,
363
- instrument
364
- ) if is_lock_manually_released
365
- end
366
-
367
- if detailed_result
368
- {
369
- yield_result: yield_result,
370
- locks_release_strategy: block_given? ? :immediate_release_after_yield : :redis_key_ttl,
371
- locks_released_at: block_given? ? ts : nil,
372
- locks_acq_time: acq_time,
373
- locks_hold_time: is_lock_manually_released ? hold_time : nil,
374
- lock_series: lock_names,
375
- rql_lock_series: lock_keys_for_instrumentation
376
- }
377
- else
378
- yield_result
379
- end
380
- else
381
- acquired_locks = successfully_acquired_locks.map { |state| state[:lock_name] }
382
- missing_locks = lock_names - acquired_locks
383
-
384
- {
385
- ok: false,
386
- result: {
387
- error: :failed_to_acquire_lock_series,
388
- detailed_errors: failed_locks_and_errors,
389
- lock_series: lock_names,
390
- acquired_locks:,
391
- missing_locks:,
392
- failed_on_lock:
393
- }
394
- }
395
- end
396
- end
397
-
398
- private
399
-
400
- # Releases locks of the series that are obtained by the series itself:
401
- # - reentrant locks (already obtained by the outer logic of the current acquirer) are kept;
402
- # - only locks of the current acquirer are released (owner check);
403
- #
404
- # @param redis [RedisClient]
405
- # @param read_write_mode [Symbol]
406
- # @param successfully_acquired_locks [Array<Hash<Symbol,Any>>]
407
- # @param acquirer_id [String]
408
- # @return [void]
409
- #
410
- # @api private
411
- # @since 1.18.0
412
- def release_lock_series( # steep:ignore
413
- redis, read_write_mode, successfully_acquired_locks, acquirer_id
414
- )
415
- lock_names = obtained_lock_names(successfully_acquired_locks) # steep:ignore
416
-
417
- if read_write_mode == :read
418
- release_read_lock_series(redis, lock_names, acquirer_id) # steep:ignore
419
- else
420
- release_write_lock_series(redis, lock_names, acquirer_id) # steep:ignore
421
- end
422
- end
423
-
424
- # Releases write locks of the given acquirer (compare-and-delete: the lock can be expired
425
- # and obtained by another acquirer when the series logic exceeds the lock ttl) in one
426
- # transaction for the whole series (three round trips: WATCH, lock owners, MULTI/EXEC):
427
- # - an aborted transaction (one of the watched locks is changed) is retried with the locks
428
- # that were still owned by the acquirer at the last check: the locks of other acquirers
429
- # are neither deleted nor watched, so the retries converge;
430
- #
431
- # @param redis [RedisClient]
432
- # @param lock_names [Array<String>]
433
- # @param acquirer_id [String]
434
- # @return [void]
435
- #
436
- # @api private
437
- # @since 1.18.0
438
- # @version 1.21.0
439
- def release_write_lock_series(redis, lock_names, acquirer_id) # steep:ignore
440
- return if lock_names.empty?
441
-
442
- own_lock_keys = lock_names.map do |lock_name|
443
- RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
444
- end
445
-
446
- redis.with do |conn|
447
- until own_lock_keys.empty?
448
- released = conn.multi(watch: own_lock_keys) do |transaction|
449
- # NOTE: (pipeline layout) one reply (lock owner) per lock in the series order
450
- lock_owners = conn.pipelined do |pipeline|
451
- own_lock_keys.each { |lock_key| pipeline.call('HGET', lock_key, 'acq_id') }
452
- end
453
- own_lock_keys = own_lock_keys.select.with_index do |_lock_key, index|
454
- lock_owners[index] == acquirer_id
455
- end
456
- own_lock_keys.each { |lock_key| transaction.call('DEL', lock_key) }
457
- end
458
- # NOTE: `nil` => the transaction is aborted (a watched lock is changed) => retry
459
- break unless released == nil
460
- end
461
- rescue RedisQueuedLocks::LockAcquirementIntermediateTimeoutError,
462
- RedisQueuedLocks::TimedLockIntermediateTimeoutError,
463
- ::Timeout::ExitException
464
- # NOTE: (timeouts) an interrupted WATCH block (the internal timeouts and the user's
465
- # `Timeout.timeout` without an exception class): the closed connection drops the WATCH
466
- # state (redis-client sends `UNWATCH` only on `StandardError`);
467
- conn.close
468
- raise
469
- end
470
- end
471
-
472
- # Locks of the series that are obtained by the series itself (reentrant locks are obtained
473
- # by the outer logic of the current acquirer and should be released by it).
474
- #
475
- # @param successfully_acquired_locks [Array<Hash<Symbol,Any>>]
476
- # @return [Array<String>]
477
- #
478
- # @api private
479
- # @since 1.18.0
480
- def obtained_lock_names(successfully_acquired_locks) # steep:ignore
481
- successfully_acquired_locks.filter_map do |operation_result|
482
- operation_result[:lock_name] if operation_result[:result][:process] == :lock_obtaining
483
- end
484
- end
485
-
486
- # Releases read locks of the given acquirer (other readers are not affected).
487
- #
488
- # @param redis [RedisClient]
489
- # @param lock_names [Array<String>]
490
- # @param acquirer_id [String]
491
- # @return [void]
492
- #
493
- # @api private
494
- # @since 1.18.0
495
- def release_read_lock_series(redis, lock_names, acquirer_id) # steep:ignore
496
- return if lock_names.empty?
497
-
498
- redis.with do |conn|
499
- conn.multi do |transaction|
500
- lock_names.each do |lock_name|
501
- transaction.call(
502
- 'ZREM', RedisQueuedLocks::Resource.prepare_lock_readers(lock_name), acquirer_id
503
- )
504
- transaction.call(
505
- 'DEL', RedisQueuedLocks::Resource.prepare_read_lock_key(lock_name, acquirer_id)
506
- )
507
- end
508
- end
509
- end
510
- end
511
- end
512
- end
513
- # rubocop:enable Metrics/ModuleLength, Metrics/ClassLength, Metrics/MethodLength