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
@@ -0,0 +1,472 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
4
+ # @since 1.23.0
5
+ # rubocop:disable Metrics/ModuleLength
6
+ module RedisQueuedLocks::Acquirer::LockSeries::LogVisitor
7
+ # rubocop:disable Metrics/ClassLength
8
+ class << self
9
+ # @param logger [::Logger,#debug]
10
+ # @param log_sampled [Boolean]
11
+ # @param lock_keys [Array<String>]
12
+ # @param queue_ttl [Integer]
13
+ # @param acquirer_id [String]
14
+ # @param host_id [String]
15
+ # @param access_strategy [Symbol]
16
+ # @param rw_mode [Symbol] The mode of the lock series (`:read` or `:write`).
17
+ # @param priority [Integer] The lock request priority of the series.
18
+ # @return [void]
19
+ #
20
+ # @api private
21
+ # @since 1.23.0
22
+ def start_lock_series_obtaining(
23
+ logger,
24
+ log_sampled,
25
+ lock_keys,
26
+ queue_ttl,
27
+ acquirer_id,
28
+ host_id,
29
+ access_strategy,
30
+ rw_mode,
31
+ priority
32
+ )
33
+ return unless log_sampled
34
+
35
+ logger.debug do
36
+ "[redis_queued_locks.start_lock_series_obtaining] " \
37
+ "lock_keys => '#{lock_keys.inspect}' " \
38
+ "queue_ttl => #{queue_ttl} " \
39
+ "acq_id => '#{acquirer_id}' " \
40
+ "hst_id => '#{host_id}' " \
41
+ "acs_strat => '#{access_strategy}' " \
42
+ "rw_mode => '#{rw_mode}' " \
43
+ "priority => #{priority}"
44
+ end rescue nil
45
+ end
46
+
47
+ # @param logger [::Logger,#debug]
48
+ # @param log_sampled [Boolean]
49
+ # @param lock_keys [Array<String>]
50
+ # @param queue_ttl [Integer]
51
+ # @param acquirer_id [String]
52
+ # @param host_id [String]
53
+ # @param access_strategy [Symbol]
54
+ # @param rw_mode [Symbol] The mode of the lock series (`:read` or `:write`).
55
+ # @param priority [Integer] The lock request priority of the series.
56
+ # @return [void]
57
+ #
58
+ # @api private
59
+ # @since 1.23.0
60
+ def start_try_to_lock_series_cycle(
61
+ logger,
62
+ log_sampled,
63
+ lock_keys,
64
+ queue_ttl,
65
+ acquirer_id,
66
+ host_id,
67
+ access_strategy,
68
+ rw_mode,
69
+ priority
70
+ )
71
+ return unless log_sampled
72
+
73
+ logger.debug do
74
+ "[redis_queued_locks.start_try_to_lock_series_cycle] " \
75
+ "lock_keys => '#{lock_keys.inspect}' " \
76
+ "queue_ttl => #{queue_ttl} " \
77
+ "acq_id => '#{acquirer_id}' " \
78
+ "hst_id => '#{host_id}' " \
79
+ "acs_strat => '#{access_strategy}' " \
80
+ "rw_mode => '#{rw_mode}' " \
81
+ "priority => #{priority}"
82
+ end rescue nil
83
+ end
84
+
85
+ # @param logger [::Logger,#debug]
86
+ # @param log_sampled [Boolean]
87
+ # @param lock_keys [Array<String>]
88
+ # @param queue_ttl [Integer]
89
+ # @param acquirer_id [String]
90
+ # @param host_id [String]
91
+ # @param access_strategy [Symbol]
92
+ # @param rw_mode [Symbol] The mode of the lock series (`:read` or `:write`).
93
+ # @param priority [Integer] The lock request priority of the series.
94
+ # @return [void]
95
+ #
96
+ # @api private
97
+ # @since 1.23.0
98
+ def dead_score_reached__reset_series_position(
99
+ logger,
100
+ log_sampled,
101
+ lock_keys,
102
+ queue_ttl,
103
+ acquirer_id,
104
+ host_id,
105
+ access_strategy,
106
+ rw_mode,
107
+ priority
108
+ )
109
+ return unless log_sampled
110
+
111
+ logger.debug do
112
+ "[redis_queued_locks.dead_score_reached__reset_series_position] " \
113
+ "lock_keys => '#{lock_keys.inspect}' " \
114
+ "queue_ttl => #{queue_ttl} " \
115
+ "acq_id => '#{acquirer_id}' " \
116
+ "hst_id => '#{host_id}' " \
117
+ "acs_strat => '#{access_strategy}' " \
118
+ "rw_mode => '#{rw_mode}' " \
119
+ "priority => #{priority}"
120
+ end rescue nil
121
+ end
122
+
123
+ # @param logger [::Logger,#debug]
124
+ # @param log_sampled [Boolean]
125
+ # @param lock_key [String] The lock of the series.
126
+ # @param lock_keys [Array<String>] All locks of the series.
127
+ # @param queue_ttl [Integer]
128
+ # @param acquirer_id [String]
129
+ # @param host_id [String]
130
+ # @param acq_time [Numeric]
131
+ # @param access_strategy [Symbol]
132
+ # @param rw_mode [Symbol] The mode of the lock series (`:read` or `:write`).
133
+ # @param priority [Integer] The lock request priority of the series.
134
+ # @return [void]
135
+ #
136
+ # @api private
137
+ # @since 1.23.0
138
+ def lock_series_intermediate_lock_obtained(
139
+ logger,
140
+ log_sampled,
141
+ lock_key,
142
+ lock_keys,
143
+ queue_ttl,
144
+ acquirer_id,
145
+ host_id,
146
+ acq_time,
147
+ access_strategy,
148
+ rw_mode,
149
+ priority
150
+ )
151
+ return unless log_sampled
152
+
153
+ logger.debug do
154
+ "[redis_queued_locks.lock_series_intermediate_lock_obtained] " \
155
+ "lock_key => '#{lock_key}' " \
156
+ "lock_keys => '#{lock_keys.inspect}' " \
157
+ "queue_ttl => #{queue_ttl} " \
158
+ "acq_id => '#{acquirer_id}' " \
159
+ "hst_id => '#{host_id}' " \
160
+ "acs_strat => '#{access_strategy}' " \
161
+ "rw_mode => '#{rw_mode}' " \
162
+ "priority => #{priority} " \
163
+ "acq_time => #{acq_time} (ms)"
164
+ end rescue nil
165
+ end
166
+
167
+ # @param logger [::Logger,#debug]
168
+ # @param log_sampled [Boolean]
169
+ # @param lock_key [String] The lock of the series.
170
+ # @param lock_keys [Array<String>] All locks of the series.
171
+ # @param queue_ttl [Integer]
172
+ # @param acquirer_id [String]
173
+ # @param host_id [String]
174
+ # @param acq_time [Numeric]
175
+ # @param access_strategy [Symbol]
176
+ # @param rw_mode [Symbol] The mode of the lock series (`:read` or `:write`).
177
+ # @param priority [Integer] The lock request priority of the series.
178
+ # @return [void]
179
+ #
180
+ # @api private
181
+ # @since 1.23.0
182
+ def lock_series_lock_obtained(
183
+ logger,
184
+ log_sampled,
185
+ lock_key,
186
+ lock_keys,
187
+ queue_ttl,
188
+ acquirer_id,
189
+ host_id,
190
+ acq_time,
191
+ access_strategy,
192
+ rw_mode,
193
+ priority
194
+ )
195
+ return unless log_sampled
196
+
197
+ logger.debug do
198
+ "[redis_queued_locks.lock_series_lock_obtained] " \
199
+ "lock_key => '#{lock_key}' " \
200
+ "lock_keys => '#{lock_keys.inspect}' " \
201
+ "queue_ttl => #{queue_ttl} " \
202
+ "acq_id => '#{acquirer_id}' " \
203
+ "hst_id => '#{host_id}' " \
204
+ "acs_strat => '#{access_strategy}' " \
205
+ "rw_mode => '#{rw_mode}' " \
206
+ "priority => #{priority} " \
207
+ "acq_time => #{acq_time} (ms)"
208
+ end rescue nil
209
+ end
210
+
211
+ # @param logger [::Logger,#debug]
212
+ # @param log_sampled [Boolean]
213
+ # @param lock_keys [Array<String>]
214
+ # @param queue_ttl [Integer]
215
+ # @param acquirer_id [String]
216
+ # @param host_id [String]
217
+ # @param acq_time [Numeric]
218
+ # @param access_strategy [Symbol]
219
+ # @param rw_mode [Symbol] The mode of the lock series (`:read` or `:write`).
220
+ # @param priority [Integer] The lock request priority of the series.
221
+ # @param reentrant_lock_keys [Array<String>]
222
+ # The locks of the series that are obtained by the outer logic of the current acquirer.
223
+ # @return [void]
224
+ #
225
+ # @api private
226
+ # @since 1.23.0
227
+ def lock_series_obtained(
228
+ logger,
229
+ log_sampled,
230
+ lock_keys,
231
+ queue_ttl,
232
+ acquirer_id,
233
+ host_id,
234
+ acq_time,
235
+ access_strategy,
236
+ rw_mode,
237
+ priority,
238
+ reentrant_lock_keys
239
+ )
240
+ return unless log_sampled
241
+
242
+ logger.debug do
243
+ "[redis_queued_locks.lock_series_obtained] " \
244
+ "lock_keys => '#{lock_keys.inspect}' " \
245
+ "queue_ttl => #{queue_ttl} " \
246
+ "acq_id => '#{acquirer_id}' " \
247
+ "hst_id => '#{host_id}' " \
248
+ "acs_strat => '#{access_strategy}' " \
249
+ "rw_mode => '#{rw_mode}' " \
250
+ "priority => #{priority} " \
251
+ "acq_time => #{acq_time} (ms) " \
252
+ "reentrant_lock_keys => '#{reentrant_lock_keys.inspect}'"
253
+ end rescue nil
254
+ end
255
+
256
+ # @param logger [::Logger,#debug]
257
+ # @param log_sampled [Boolean]
258
+ # @param lock_key [String] The lock that blocks the series.
259
+ # @param lock_keys [Array<String>] All locks of the series.
260
+ # @param queue_ttl [Integer]
261
+ # @param acquirer_id [String]
262
+ # @param host_id [String]
263
+ # @param acq_time [Numeric]
264
+ # @param access_strategy [Symbol]
265
+ # @param rw_mode [Symbol] The mode of the lock series (`:read` or `:write`).
266
+ # @param priority [Integer] The lock request priority of the series.
267
+ # @param result [Symbol] The failure result (`:timeout_reached`, `:retry_limit_reached`, ...).
268
+ # @return [void]
269
+ #
270
+ # @api private
271
+ # @since 1.23.0
272
+ def lock_series_lock_obtaining_failed(
273
+ logger,
274
+ log_sampled,
275
+ lock_key,
276
+ lock_keys,
277
+ queue_ttl,
278
+ acquirer_id,
279
+ host_id,
280
+ acq_time,
281
+ access_strategy,
282
+ rw_mode,
283
+ priority,
284
+ result
285
+ )
286
+ return unless log_sampled
287
+
288
+ logger.debug do
289
+ "[redis_queued_locks.lock_series_lock_obtaining_failed] " \
290
+ "lock_key => '#{lock_key}' " \
291
+ "lock_keys => '#{lock_keys.inspect}' " \
292
+ "queue_ttl => #{queue_ttl} " \
293
+ "acq_id => '#{acquirer_id}' " \
294
+ "hst_id => '#{host_id}' " \
295
+ "acs_strat => '#{access_strategy}' " \
296
+ "rw_mode => '#{rw_mode}' " \
297
+ "priority => #{priority} " \
298
+ "acq_time => #{acq_time} (ms) " \
299
+ "result => '#{result}'"
300
+ end rescue nil
301
+ end
302
+
303
+ # @param logger [::Logger,#debug]
304
+ # @param log_sampled [Boolean]
305
+ # @param lock_keys [Array<String>]
306
+ # @param queue_ttl [Integer]
307
+ # @param acquirer_id [String]
308
+ # @param host_id [String]
309
+ # @param acq_time [Numeric]
310
+ # @param access_strategy [Symbol]
311
+ # @param rw_mode [Symbol] The mode of the lock series (`:read` or `:write`).
312
+ # @param priority [Integer] The lock request priority of the series.
313
+ # @param result [Symbol] The failure result (`:timeout_reached`, `:retry_limit_reached`, ...).
314
+ # @param failed_lock_key [String,NilClass]
315
+ # The lock of the series that blocks the series (`nil`: unknown, an attempt was not finished
316
+ # or some lock state was changed during the attempt).
317
+ # @return [void]
318
+ #
319
+ # @api private
320
+ # @since 1.23.0
321
+ def lock_series_obtaining_failed(
322
+ logger,
323
+ log_sampled,
324
+ lock_keys,
325
+ queue_ttl,
326
+ acquirer_id,
327
+ host_id,
328
+ acq_time,
329
+ access_strategy,
330
+ rw_mode,
331
+ priority,
332
+ result,
333
+ failed_lock_key
334
+ )
335
+ return unless log_sampled
336
+
337
+ logger.debug do
338
+ "[redis_queued_locks.lock_series_obtaining_failed] " \
339
+ "lock_keys => '#{lock_keys.inspect}' " \
340
+ "queue_ttl => #{queue_ttl} " \
341
+ "acq_id => '#{acquirer_id}' " \
342
+ "hst_id => '#{host_id}' " \
343
+ "acs_strat => '#{access_strategy}' " \
344
+ "rw_mode => '#{rw_mode}' " \
345
+ "priority => #{priority} " \
346
+ "acq_time => #{acq_time} (ms) " \
347
+ "result => '#{result}' " \
348
+ "failed_lock_key => '#{failed_lock_key}'"
349
+ end rescue nil
350
+ end
351
+
352
+ # @param logger [::Logger,#debug]
353
+ # @param log_sampled [Boolean]
354
+ # @param lock_keys [Array<String>]
355
+ # @param queue_ttl [Integer]
356
+ # @param acquirer_id [String]
357
+ # @param host_id [String]
358
+ # @param access_strategy [Symbol]
359
+ # @param rw_mode [Symbol] The mode of the lock series (`:read` or `:write`).
360
+ # @param priority [Integer] The lock request priority of the series.
361
+ # @return [void]
362
+ #
363
+ # @api private
364
+ # @since 1.23.0
365
+ def dequeue_from_lock_series_queues(
366
+ logger,
367
+ log_sampled,
368
+ lock_keys,
369
+ queue_ttl,
370
+ acquirer_id,
371
+ host_id,
372
+ access_strategy,
373
+ rw_mode,
374
+ priority
375
+ )
376
+ return unless log_sampled
377
+
378
+ logger.debug do
379
+ "[redis_queued_locks.dequeue_from_lock_series_queues] " \
380
+ "lock_keys => '#{lock_keys.inspect}' " \
381
+ "queue_ttl => #{queue_ttl} " \
382
+ "acq_id => '#{acquirer_id}' " \
383
+ "hst_id => '#{host_id}' " \
384
+ "acs_strat => '#{access_strategy}' " \
385
+ "rw_mode => '#{rw_mode}' " \
386
+ "priority => #{priority}"
387
+ end rescue nil
388
+ end
389
+
390
+ # @param logger [::Logger,#debug]
391
+ # @param log_sampled [Boolean]
392
+ # @param lock_keys [Array<String>]
393
+ # @param queue_ttl [Integer]
394
+ # @param acquirer_id [String]
395
+ # @param host_id [String]
396
+ # @param access_strategy [Symbol]
397
+ # @param rw_mode [Symbol] The mode of the lock series (`:read` or `:write`).
398
+ # @param priority [Integer] The lock request priority of the series.
399
+ # @return [void]
400
+ #
401
+ # @api private
402
+ # @since 1.23.0
403
+ def expire_lock_series(
404
+ logger,
405
+ log_sampled,
406
+ lock_keys,
407
+ queue_ttl,
408
+ acquirer_id,
409
+ host_id,
410
+ access_strategy,
411
+ rw_mode,
412
+ priority
413
+ )
414
+ return unless log_sampled
415
+
416
+ logger.debug do
417
+ "[redis_queued_locks.expire_lock_series] " \
418
+ "lock_keys => '#{lock_keys.inspect}' " \
419
+ "queue_ttl => #{queue_ttl} " \
420
+ "acq_id => '#{acquirer_id}' " \
421
+ "hst_id => '#{host_id}' " \
422
+ "acs_strat => '#{access_strategy}' " \
423
+ "rw_mode => '#{rw_mode}' " \
424
+ "priority => #{priority}"
425
+ end rescue nil
426
+ end
427
+
428
+ # @param logger [::Logger,#debug]
429
+ # @param log_sampled [Boolean]
430
+ # @param lock_keys [Array<String>]
431
+ # The extended reentrant locks of the series (`:extendable_work_through`).
432
+ # @param decreased_ttl [Integer] In milliseconds.
433
+ # @param queue_ttl [Integer]
434
+ # @param acquirer_id [String]
435
+ # @param host_id [String]
436
+ # @param access_strategy [Symbol]
437
+ # @param rw_mode [Symbol] The mode of the lock series (`:read` or `:write`).
438
+ # @param priority [Integer] The lock request priority of the series.
439
+ # @return [void]
440
+ #
441
+ # @api private
442
+ # @since 1.23.0
443
+ def decrease_lock_series(
444
+ logger,
445
+ log_sampled,
446
+ lock_keys,
447
+ decreased_ttl,
448
+ queue_ttl,
449
+ acquirer_id,
450
+ host_id,
451
+ access_strategy,
452
+ rw_mode,
453
+ priority
454
+ )
455
+ return unless log_sampled
456
+
457
+ logger.debug do
458
+ "[redis_queued_locks.decrease_lock_series] " \
459
+ "lock_keys => '#{lock_keys.inspect}' " \
460
+ "decreased_ttl => #{decreased_ttl} " \
461
+ "queue_ttl => #{queue_ttl} " \
462
+ "acq_id => '#{acquirer_id}' " \
463
+ "hst_id => '#{host_id}' " \
464
+ "acs_strat => '#{access_strategy}' " \
465
+ "rw_mode => '#{rw_mode}' " \
466
+ "priority => #{priority}"
467
+ end rescue nil
468
+ end
469
+ end
470
+ # rubocop:enable Metrics/ClassLength
471
+ end
472
+ # rubocop:enable Metrics/ModuleLength
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Releases the lock series: only the locks that are obtained by the series itself and only if they
4
+ # are still owned by the current acquirer; the reentrant locks (obtained by the outer logic of
5
+ # the current acquirer) are kept and the extended ones get their ttl back.
6
+ #
7
+ # @api private
8
+ # @since 1.23.0
9
+ module RedisQueuedLocks::Acquirer::LockSeries::ReleaseLockSeries
10
+ # @param redis [RedisClient]
11
+ # @param lock_names [Array<String>]
12
+ # @param lock_processes [Array<Symbol,NilClass>]
13
+ # - The way each lock of the series is held (the order of `lock_names`):
14
+ # - `:lock_obtaining` - obtained by the series itself (released);
15
+ # - `:extendable_conflict_work_through` - extended reentrant lock (its ttl is decreased);
16
+ # - `:conflict_work_through` - reentrant lock (kept as is);
17
+ # @param held_rw_modes [Array<Symbol>]
18
+ # The mode of the lock that is really held by the acquirer (the order of `lock_names`).
19
+ # @param acquirer_id [String]
20
+ # @param decreased_ttl [Integer]
21
+ # The ttl to take back from the extended reentrant locks (in milliseconds): the extension
22
+ # without the time spent under the locks; a non-positive value keeps these locks as is.
23
+ # @return [void]
24
+ #
25
+ # @api private
26
+ # @since 1.23.0
27
+ def release_lock_series(
28
+ redis,
29
+ lock_names,
30
+ lock_processes,
31
+ held_rw_modes,
32
+ acquirer_id,
33
+ decreased_ttl
34
+ )
35
+ # @type var write_lock_names: Array[String]
36
+ write_lock_names = []
37
+ # @type var read_lock_names: Array[String]
38
+ read_lock_names = []
39
+ # @type var extended_write_lock_names: Array[String]
40
+ extended_write_lock_names = []
41
+ # @type var extended_read_lock_names: Array[String]
42
+ extended_read_lock_names = []
43
+
44
+ lock_names.each_with_index do |lock_name, index|
45
+ read_lock = held_rw_modes[index] == :read
46
+
47
+ case lock_processes[index]
48
+ when :lock_obtaining
49
+ (read_lock ? read_lock_names : write_lock_names) << lock_name
50
+ when :extendable_conflict_work_through
51
+ (read_lock ? extended_read_lock_names : extended_write_lock_names) << lock_name
52
+ end
53
+ end
54
+
55
+ # NOTE: a non-positive decreased ttl: the spent time exceeds the extension
56
+ if decreased_ttl <= 0
57
+ extended_read_lock_names.clear
58
+ extended_write_lock_names.clear
59
+ end
60
+
61
+ release_write_lock_series(
62
+ redis, write_lock_names, extended_write_lock_names, acquirer_id, decreased_ttl
63
+ )
64
+ release_read_lock_series(
65
+ redis, read_lock_names, extended_read_lock_names, acquirer_id, decreased_ttl
66
+ )
67
+ end
68
+
69
+ private
70
+
71
+ # Releases the write locks (and decreases the ttl of the extended ones) of the given acquirer
72
+ # (compare-and-delete: the lock can be expired and obtained by another acquirer when the series
73
+ # logic exceeds the lock ttl) in one transaction for the whole series (three round trips: WATCH,
74
+ # lock owners, MULTI/EXEC):
75
+ # - an aborted transaction (one of the watched locks is changed) is retried with the locks
76
+ # that were still owned by the acquirer at the last check: the locks of other acquirers
77
+ # are neither touched nor watched, so the retries converge;
78
+ #
79
+ # @param redis [RedisClient]
80
+ # @param lock_names [Array<String>] The locks to release.
81
+ # @param extended_lock_names [Array<String>] The extended reentrant locks.
82
+ # @param acquirer_id [String]
83
+ # @param decreased_ttl [Integer] In milliseconds.
84
+ # @return [void]
85
+ #
86
+ # @api private
87
+ # @since 1.23.0
88
+ # rubocop:disable Metrics/MethodLength
89
+ def release_write_lock_series(redis, lock_names, extended_lock_names, acquirer_id, decreased_ttl)
90
+ return if lock_names.empty? && extended_lock_names.empty?
91
+
92
+ own_lock_keys = lock_names.map do |lock_name|
93
+ RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
94
+ end
95
+ own_extended_lock_keys = extended_lock_names.map do |lock_name|
96
+ RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
97
+ end
98
+
99
+ redis.with do |conn|
100
+ until own_lock_keys.empty? && own_extended_lock_keys.empty?
101
+ released = conn.multi(watch: own_lock_keys + own_extended_lock_keys) do |transaction|
102
+ # NOTE: (pipeline layout) one reply (lock owner) per lock: the released locks first
103
+ # @type var lock_owners: Array[String?]
104
+ lock_owners = conn.pipelined do |pipeline|
105
+ own_lock_keys.each { |lock_key| pipeline.call('HGET', lock_key, 'acq_id') }
106
+ own_extended_lock_keys.each { |lock_key| pipeline.call('HGET', lock_key, 'acq_id') }
107
+ end
108
+ extended_locks_at = own_lock_keys.size
109
+ own_lock_keys = own_lock_keys.select.with_index do |_lock_key, index|
110
+ lock_owners[index] == acquirer_id
111
+ end
112
+ own_extended_lock_keys = own_extended_lock_keys.select.with_index do |_lock_key, index|
113
+ lock_owners[extended_locks_at + index] == acquirer_id
114
+ end
115
+ own_lock_keys.each { |lock_key| transaction.call('DEL', lock_key) }
116
+ # NOTE: EVAL signature -> <lua script>, (number of keys), *(keys), *(arguments)
117
+ own_extended_lock_keys.each do |lock_key|
118
+ transaction.call(
119
+ 'EVAL',
120
+ RedisQueuedLocks::Acquirer::AcquireLock::YieldExpire::DECREASE_LOCK_PTTL,
121
+ 1,
122
+ lock_key,
123
+ decreased_ttl
124
+ )
125
+ end
126
+ end
127
+ # NOTE: `nil` => the transaction is aborted (a watched lock is changed) => retry
128
+ break unless released == nil
129
+ end
130
+ rescue RedisQueuedLocks::LockAcquirementIntermediateTimeoutError,
131
+ RedisQueuedLocks::TimedLockIntermediateTimeoutError,
132
+ ::Timeout::ExitException
133
+ # NOTE: (timeouts) an interrupted WATCH block (the internal timeouts and the user's
134
+ # `Timeout.timeout` without an exception class): the closed connection drops the WATCH
135
+ # state (redis-client sends `UNWATCH` only on `StandardError`);
136
+ conn.close
137
+ raise
138
+ end
139
+ end
140
+ # rubocop:enable Metrics/MethodLength
141
+
142
+ # Releases the read locks (and decreases the extended ones) of the given acquirer in one
143
+ # transaction (other readers are not affected).
144
+ #
145
+ # @param redis [RedisClient]
146
+ # @param lock_names [Array<String>] The locks to release.
147
+ # @param extended_lock_names [Array<String>] The extended reentrant locks.
148
+ # @param acquirer_id [String]
149
+ # @param decreased_ttl [Integer] In milliseconds.
150
+ # @return [void]
151
+ #
152
+ # @api private
153
+ # @since 1.23.0
154
+ def release_read_lock_series(redis, lock_names, extended_lock_names, acquirer_id, decreased_ttl)
155
+ return if lock_names.empty? && extended_lock_names.empty?
156
+
157
+ redis.multi do |transaction|
158
+ lock_names.each do |lock_name|
159
+ transaction.call(
160
+ 'ZREM', RedisQueuedLocks::Resource.prepare_lock_readers(lock_name), acquirer_id
161
+ )
162
+ transaction.call(
163
+ 'DEL', RedisQueuedLocks::Resource.prepare_read_lock_key(lock_name, acquirer_id)
164
+ )
165
+ end
166
+ # NOTE:
167
+ # - XX => do nothing if the read lock is already expired and removed from the registry;
168
+ # - the read lock data TTL is not decreased (it is not used without the read lock);
169
+ extended_lock_names.each do |lock_name|
170
+ transaction.call(
171
+ 'ZADD',
172
+ RedisQueuedLocks::Resource.prepare_lock_readers(lock_name),
173
+ 'XX',
174
+ 'INCR',
175
+ -decreased_ttl,
176
+ acquirer_id
177
+ )
178
+ end
179
+ end
180
+ end
181
+ end