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,1051 @@
1
+ # frozen_string_literal: true
2
+
3
+ # One attempt to obtain all locks of the lock series (all or nothing, see
4
+ # `RedisQueuedLocks::Acquirer::LockSeries`):
5
+ # - mutual exclusion relies on the optimistic transaction only (WATCH/MULTI): the attempt watches
6
+ # the lock state of all locks of the series (write lock requests: the write lock keys and the
7
+ # readers registries, read lock requests: the write lock keys), reads the state of all locks
8
+ # in one round trip and obtains all locks with one transaction;
9
+ # - nothing is held while the series waits: the attempt that can not obtain some lock of
10
+ # the series obtains nothing, so hold-and-wait is impossible;
11
+ # - the lock request of the series has one position in all lock request queues of the series
12
+ # (and in both modes): the requests are ordered by `(score, acq_id)` in all queues in the same
13
+ # way, so a request waits for earlier requests only and the requests can not wait for each
14
+ # other in a cycle (lock series are deadlock-free: for any order of the lock names);
15
+ # - the state and the queueing commands of each lock are the commands of the single lock attempt
16
+ # (`pipeline_lock_state`, `queue_lock_request`), the replies are read by the same position
17
+ # constants per lock; the decisions (`same_process_conflict_status`, `lock_obstacle`,
18
+ # `find_opposite_request_ahead`, `find_dead_requests_ahead`) and the transaction commands
19
+ # (`transact_*`) are shared with the single lock attempt too;
20
+ # - the try-lock logs are the logs of the single lock attempt: one event per lock of the series
21
+ # (`RedisQueuedLocks::Acquirer::AcquireLock::TryToLock::LogVisitor`, it is the `LogVisitor`
22
+ # constant here: `TryToLock` is included);
23
+ #
24
+ # @api private
25
+ # @since 1.23.0
26
+ # rubocop:disable Metrics/ModuleLength
27
+ module RedisQueuedLocks::Acquirer::LockSeries::TryToLockSeries
28
+ # @since 1.23.0
29
+ include RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
30
+ # @since 1.23.0
31
+ include RedisQueuedLocks::Acquirer::LockSeries::ReleaseLockSeries
32
+
33
+ # @param redis [RedisClient]
34
+ # @param logger [::Logger,#debug]
35
+ # @param log_lock_try [Boolean]
36
+ # @param lock_keys [Array<String>]
37
+ # @param rw_mode [Symbol] `:read` or `:write` (one mode for the whole series).
38
+ # @param lock_key_queues [Array<String>] Queues of write lock requests.
39
+ # @param read_lock_key_queues [Array<String>] Queues of read lock requests.
40
+ # @param lock_readers_keys [Array<String>] Registries of read lock holders.
41
+ # @param read_lock_keys [Array<String,NilClass>]
42
+ # Read lock data of the current acquirer (`nil` for write lock requests).
43
+ # @param lock_names [Array<String>] The lock names (the members of the lock index).
44
+ # @param watched_keys [Array<String>]
45
+ # The lock state keys (write lock requests: `lock_keys` and `lock_readers_keys`, read lock
46
+ # requests: `lock_keys`).
47
+ # @param acquirer_id [String]
48
+ # @param host_id [String]
49
+ # @param acquirer_position [Numeric]
50
+ # The position of the lock request of the series in all its lock request queues.
51
+ # @param priority [Integer] The lock request priority (the band of `acquirer_position`).
52
+ # @param ttl [Integer]
53
+ # @param queue_ttl [Integer]
54
+ # @param lock_index_enabled [Boolean]
55
+ # @param lock_index_grace_period [Integer]
56
+ # @param fail_fast [Boolean]
57
+ # @param conflict_strategy [Symbol]
58
+ # @param access_strategy [Symbol]
59
+ # @param meta [NilClass,Hash<String|Symbol,Any>]
60
+ # @param retry_attempt [Boolean]
61
+ # The attempt is a retry (the previous attempt of the acquirement did not obtain the series).
62
+ # @param acq_process [Hash<Symbol,Any>]
63
+ # - The observer of the acquirement: the attempt puts `:ts`, `:lock_processes` and
64
+ # `:held_rw_modes` here before the obtaining transaction is sent: an interrupted acquirement
65
+ # (the transaction can be applied by Redis without the reply) releases these locks;
66
+ # - `:lock_processes` and `:held_rw_modes` are dropped when the locks are not obtained;
67
+ # @param log_sampled [Boolean]
68
+ # @return [Hash<Symbol,Any>]
69
+ # - Format: { ok: true/false, result: Symbol, lock_name: String/nil, ready_locks: Array };
70
+ # - `lock_name`: the first lock of the series that blocks the attempt (`nil` without it);
71
+ # - `ready_locks`: the indexes of the locks that passed all checks of the attempt before
72
+ # the lock that blocks it (the order of the lock names, reentrant locks are not checked);
73
+ #
74
+ # @api private
75
+ # @since 1.23.0
76
+ # rubocop:disable Metrics/MethodLength
77
+ def try_to_lock_series(
78
+ redis,
79
+ logger,
80
+ log_lock_try,
81
+ lock_keys,
82
+ rw_mode,
83
+ lock_key_queues,
84
+ read_lock_key_queues,
85
+ lock_readers_keys,
86
+ read_lock_keys,
87
+ lock_names,
88
+ watched_keys,
89
+ acquirer_id,
90
+ host_id,
91
+ acquirer_position,
92
+ priority,
93
+ ttl,
94
+ queue_ttl,
95
+ lock_index_enabled,
96
+ lock_index_grace_period,
97
+ fail_fast,
98
+ conflict_strategy,
99
+ access_strategy,
100
+ meta,
101
+ retry_attempt,
102
+ acq_process,
103
+ log_sampled
104
+ )
105
+ # Step X: the failure of the attempt and the lock of the series that causes it
106
+ # @type var inter_result: Symbol?
107
+ inter_result = nil
108
+ # @type var failed_lock_index: Integer?
109
+ failed_lock_index = nil
110
+ # Step X: the read locks extended by the attempt (their replies are checked, see Step 6)
111
+ # @type var extended_read_locks: Array[Integer]
112
+ extended_read_locks = []
113
+ # Step X: the locks that passed all checks of the attempt (see `find_series_lock_obstacle`)
114
+ # @type var ready_locks: Array[Integer]
115
+ ready_locks = []
116
+ # @type var lock_processes: Array[Symbol?]
117
+ lock_processes = Array.new(lock_names.size)
118
+ # Step RW: the mode of the lock that is really held by the acquirer after the attempt (see
119
+ # `TryToLock#try_to_lock`): the requested mode or the mode of the already obtained lock;
120
+ # @type var held_rw_modes: Array[RedisQueuedLocks::readWriteMode]
121
+ held_rw_modes = Array.new(lock_names.size, rw_mode)
122
+ # Step RW: read lock request flag
123
+ read_mode = (rw_mode == :read)
124
+ # Step RW: request queues of the current mode and the queues of the opposite mode
125
+ request_queues = read_mode ? read_lock_key_queues : lock_key_queues
126
+ opposite_request_queues = read_mode ? lock_key_queues : read_lock_key_queues
127
+ # Step 2.1 (prepare): requests queued before this score are expired (in both queues)
128
+ acquirer_dead_score = RedisQueuedLocks::Resource.acquirer_dead_score(queue_ttl)
129
+ # Step X: eager queueing (see `TryToLock#try_to_lock`)
130
+ eager_queueing =
131
+ !fail_fast && (conflict_strategy == :wait_for_lock || conflict_strategy == :dead_locking)
132
+ # Step X: pre-check (see `TryToLock#try_to_lock`)
133
+ precheck = fail_fast || retry_attempt
134
+
135
+ if log_sampled && log_lock_try
136
+ lock_keys.each do |lock_key|
137
+ LogVisitor.start(
138
+ logger, log_sampled, log_lock_try, lock_key,
139
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode
140
+ )
141
+ end
142
+ end
143
+
144
+ # Step X: start to work with lock acquiring
145
+ result = redis.with do |rconn|
146
+ if log_sampled && log_lock_try
147
+ lock_keys.each do |lock_key|
148
+ LogVisitor.rconn_fetched(
149
+ logger, log_sampled, log_lock_try, lock_key,
150
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode
151
+ )
152
+ end
153
+ end
154
+
155
+ # Step PRE: (pre-check) the lock state of the series without WATCH: the attempt that can not
156
+ # obtain the series fails in one round trip (see `precheck_lock_series`);
157
+ if precheck
158
+ precheck_failure = precheck_lock_series(
159
+ rconn, logger, log_sampled, log_lock_try, lock_keys, rw_mode, lock_readers_keys,
160
+ request_queues, opposite_request_queues, lock_names, acquirer_id, host_id,
161
+ acquirer_position, priority, acquirer_dead_score, queue_ttl, lock_index_enabled,
162
+ lock_index_grace_period, fail_fast, access_strategy, ready_locks
163
+ )
164
+ if precheck_failure
165
+ inter_result, failed_lock_index = precheck_failure
166
+ next
167
+ end
168
+ end
169
+
170
+ # Step 0: watch the lock state of all locks of the series (and discard the acquirement if
171
+ # any lock state is changed during the attempt): see `watched_keys`;
172
+ rconn.multi(watch: watched_keys) do |transact|
173
+ # Step 0.1: fetch the current state of all locks in one round trip (and the lock request
174
+ # queueing: eager queueing);
175
+ # @type var state_replies: Array[untyped]
176
+ state_replies = rconn.pipelined do |pipeline|
177
+ pipeline_series_lock_state(
178
+ pipeline, lock_keys, lock_readers_keys, acquirer_id, read_mode, eager_queueing,
179
+ request_queues, opposite_request_queues, acquirer_position, priority,
180
+ acquirer_dead_score, lock_names, queue_ttl, lock_index_enabled, lock_index_grace_period
181
+ )
182
+ end
183
+ lock_states = series_lock_states(
184
+ state_replies, lock_names.size, acquirer_id, read_mode, eager_queueing, lock_index_enabled
185
+ )
186
+
187
+ # SP-Conflict Step X1: same-process conflicts (the same logic as in the single lock attempt)
188
+ # - `:conflict_dead_lock`/`:conflict_lock_upgrade` fail the whole series;
189
+ # - "work through" locks are held by the current acquirer already (they are not queued
190
+ # and not obtained by the series: only their reentrant lock data is changed);
191
+ lock_states.each_with_index do |lock_state, index|
192
+ sp_conflict_rw_mode = lock_state[:conflict_rw_mode]
193
+ next unless sp_conflict_rw_mode
194
+
195
+ lock_key = lock_keys[index]
196
+ LogVisitor.same_process_conflict_detected(
197
+ logger, log_sampled, log_lock_try, lock_key,
198
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode
199
+ )
200
+ sp_conflict_status = same_process_conflict_status(
201
+ conflict_strategy, !read_mode && sp_conflict_rw_mode == :read
202
+ )
203
+ LogVisitor.same_process_conflict_analyzed(
204
+ logger, log_sampled, log_lock_try, lock_key,
205
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode, sp_conflict_status
206
+ )
207
+
208
+ case sp_conflict_status
209
+ when :conflict_work_through, :extendable_conflict_work_through
210
+ lock_processes[index] = sp_conflict_status
211
+ held_rw_modes[index] = sp_conflict_rw_mode
212
+ when :conflict_dead_lock
213
+ LogVisitor.single_process_lock_conflict__dead_lock(
214
+ logger, log_sampled, log_lock_try, lock_key,
215
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode,
216
+ sp_conflict_status, Time.now.to_f
217
+ )
218
+ inter_result = sp_conflict_status
219
+ failed_lock_index = index
220
+ break
221
+ when :conflict_lock_upgrade
222
+ LogVisitor.single_process_lock_conflict__lock_upgrade(
223
+ logger, log_sampled, log_lock_try, lock_key,
224
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode,
225
+ sp_conflict_status, Time.now.to_f
226
+ )
227
+ inter_result = sp_conflict_status
228
+ failed_lock_index = index
229
+ break
230
+ end
231
+ end
232
+ next if inter_result
233
+
234
+ # Fast-Step X0: fail-fast check (all locks that are not reentrant ones)
235
+ # - write lock request: the lock is obtained by a writer or by readers;
236
+ # - read lock request: the lock is obtained by a writer (or by the current acquirer);
237
+ if fail_fast
238
+ failed_lock_index = lock_states.each_index.find do |index|
239
+ lock_state = lock_states[index]
240
+ lock_processes[index] == nil &&
241
+ (lock_state[:obtainer] != nil || lock_state[:read_locked])
242
+ end
243
+ if failed_lock_index
244
+ inter_result = :fail_fast_no_try
245
+ next
246
+ end
247
+ end
248
+
249
+ # Steps 2.1, 1, 3: the lock request queueing in one round trip (the same commands as in
250
+ # the single lock attempt, see `queue_lock_request`), reentrant locks are not queued;
251
+ # - eager queueing: already done in the lock state round trip (Step 0.1);
252
+ unless eager_queueing
253
+ queue_series_lock_requests(
254
+ rconn, lock_states, lock_processes, request_queues, opposite_request_queues,
255
+ acquirer_position, priority, acquirer_id, acquirer_dead_score, lock_names, queue_ttl,
256
+ lock_index_enabled, lock_index_grace_period
257
+ )
258
+ end
259
+
260
+ # Step 2.2: (priorities) drop the dead lock requests of other priority bands that block
261
+ # the series and get the actual queue heads (see `drop_series_dead_requests_ahead`);
262
+ drop_series_dead_requests_ahead(
263
+ rconn, read_mode, request_queues, opposite_request_queues, lock_states, acquirer_id,
264
+ acquirer_position, acquirer_dead_score, access_strategy
265
+ )
266
+ log_series_queueing(
267
+ logger, log_sampled, log_lock_try, lock_keys, lock_states, queue_ttl, acquirer_id,
268
+ host_id, access_strategy, rw_mode, priority
269
+ )
270
+
271
+ # Step 3.1 (RW) + Steps PRE-4.x, ROLLBACK 1.1, 1.2, 2, 3: the first lock of the series that
272
+ # can not be obtained now: nothing is obtained, retry!
273
+ series_obstacle = find_series_lock_obstacle(
274
+ lock_states, read_mode, access_strategy, acquirer_id, acquirer_position, ready_locks
275
+ )
276
+ if series_obstacle
277
+ log_series_lock_obstacle(
278
+ rconn, series_obstacle[0], series_obstacle[1], lock_states, logger, log_sampled,
279
+ log_lock_try, lock_keys, queue_ttl, acquirer_id, host_id, access_strategy,
280
+ rw_mode
281
+ )
282
+ inter_result, failed_lock_index = series_obstacle
283
+ next
284
+ end
285
+
286
+ # NOTE: all locks are free and ready to be acquired! acquire!
287
+ timestamp = Time.now.to_f
288
+
289
+ # Step 6: (transaction layout) the extensions of the read locks are the first commands of
290
+ # the transaction: their replies are read by the positions (see Step 7), the replies of
291
+ # other commands are not read;
292
+ lock_states.each_with_index do |lock_state, index|
293
+ next unless lock_processes[index] == :extendable_conflict_work_through
294
+ next unless held_rw_modes[index] == :read
295
+
296
+ transact_read_lock_extension(
297
+ transact, lock_readers_keys[index], read_lock_keys[index], lock_names[index],
298
+ acquirer_id, ttl, lock_index_enabled, lock_state[:own_read_lock_expiration],
299
+ lock_state[:redis_time], timestamp
300
+ )
301
+ extended_read_locks << index
302
+ end
303
+
304
+ # Step 6.X: other commands of the transaction: all locks of the series at once;
305
+ # - a reentrant lock has no lock request except the request queued by the pre-check of
306
+ # a retry (the lock state is not known before the queueing): it is dropped here;
307
+ lock_states.each_with_index do |lock_state, index|
308
+ lock_key = lock_keys[index]
309
+
310
+ case lock_processes[index]
311
+ when :extendable_conflict_work_through
312
+ if held_rw_modes[index] == :write
313
+ transact_write_lock_extension(
314
+ transact, lock_key, lock_names[index], ttl, lock_index_enabled, timestamp
315
+ )
316
+ end
317
+ transact.call('ZREM', request_queues[index], acquirer_id)
318
+ LogVisitor.reentrant_lock__extend_and_work_through(
319
+ logger, log_sampled, log_lock_try, lock_key,
320
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode,
321
+ :extendable_conflict_work_through, ttl, timestamp
322
+ )
323
+ when :conflict_work_through
324
+ if held_rw_modes[index] == :write
325
+ transact_write_lock_work_through(transact, lock_key, timestamp)
326
+ else
327
+ transact_read_lock_work_through(
328
+ transact, read_lock_keys[index], lock_state[:own_read_lock_expiration],
329
+ lock_state[:redis_time], timestamp
330
+ )
331
+ end
332
+ transact.call('ZREM', request_queues[index], acquirer_id)
333
+ LogVisitor.reentrant_lock__work_through(
334
+ logger, log_sampled, log_lock_try, lock_key,
335
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode,
336
+ :conflict_work_through, timestamp
337
+ )
338
+ else
339
+ lock_processes[index] = :lock_obtaining
340
+
341
+ if read_mode
342
+ transact_read_lock_obtaining(
343
+ transact, request_queues[index], lock_readers_keys[index], read_lock_keys[index],
344
+ lock_names[index], acquirer_id, ttl, lock_index_enabled, lock_index_grace_period,
345
+ meta, lock_state[:redis_time], timestamp
346
+ )
347
+ else
348
+ transact_write_lock_obtaining(
349
+ transact, lock_key, request_queues[index], lock_names[index], acquirer_id, host_id,
350
+ ttl, lock_index_enabled, lock_index_grace_period, meta, timestamp
351
+ )
352
+ end
353
+ LogVisitor.obtain__free_to_acquire(
354
+ logger, log_sampled, log_lock_try, lock_key,
355
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode
356
+ )
357
+ end
358
+ end
359
+
360
+ # NOTE: an interruption after this point can leave the applied transaction without its
361
+ # reply: the acquirement releases these locks (see `@param acq_process`);
362
+ acq_process[:ts] = timestamp
363
+ acq_process[:lock_processes] = lock_processes
364
+ acq_process[:held_rw_modes] = held_rw_modes
365
+ end
366
+ rescue RedisQueuedLocks::LockAcquirementIntermediateTimeoutError,
367
+ RedisQueuedLocks::TimedLockIntermediateTimeoutError,
368
+ ::Timeout::ExitException
369
+ # NOTE: (timeouts) the same as in the single lock attempt: the closed connection drops
370
+ # the WATCH state and is reconnected on the next use (see `TryToLock#try_to_lock`);
371
+ rconn.close
372
+ raise
373
+ end
374
+
375
+ # Step 7: Analyze the aquirement attempt:
376
+ if inter_result
377
+ # Step 7.a: nothing is obtained (the transaction has no commands)
378
+ {
379
+ ok: false,
380
+ result: inter_result,
381
+ lock_name: failed_lock_index ? lock_names[failed_lock_index] : nil,
382
+ ready_locks:
383
+ }
384
+ elsif result == nil || (result.is_a?(::Array) && result.empty?)
385
+ # Step 7.b: some lock state is changed during the acquire race => nothing is obtained
386
+ acq_process[:lock_processes] = nil
387
+ acq_process[:held_rw_modes] = nil
388
+ { ok: false, result: :lock_is_acquired_during_acquire_race, lock_name: nil, ready_locks: }
389
+ else
390
+ # Step 7.c (RW): the read lock can expire during the extension (the registry is not watched):
391
+ # a transaction can not be rolled back, so the series releases its locks and retries;
392
+ # - (transaction layout) see Step 6: each extended read lock takes the same number of
393
+ # commands, ZADD XX INCR is the first one (`nil`: the read lock is expired);
394
+ extension_commands =
395
+ READ_LOCK_EXTENSION_COMMANDS + (lock_index_enabled ? 1 : 0)
396
+ expired_read_lock = extended_read_locks.each_with_index.find do |_index, extension_position|
397
+ result[extension_position * extension_commands] == nil
398
+ end
399
+
400
+ if expired_read_lock
401
+ release_lock_series(
402
+ redis, lock_names, lock_processes, held_rw_modes, acquirer_id,
403
+ ttl - RedisQueuedLocks::Resource::REDIS_TIMESHIFT_ERROR
404
+ )
405
+ RedisQueuedLocks::Acquirer::LockSeries::LogVisitor.expire_lock_series(
406
+ logger, log_sampled, lock_keys,
407
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode, priority
408
+ )
409
+ acq_process[:lock_processes] = nil
410
+ acq_process[:held_rw_modes] = nil
411
+ {
412
+ ok: false,
413
+ result: :read_lock_is_expired_during_extension,
414
+ lock_name: lock_names[expired_read_lock[0]],
415
+ ready_locks:
416
+ }
417
+ else
418
+ # Step 7.d: locked! :) let's go! => all locks of the series are successfully acquired
419
+ { ok: true, result: :lock_series_obtaining, lock_name: nil, ready_locks: }
420
+ end
421
+ end
422
+ end
423
+ # rubocop:enable Metrics/MethodLength
424
+
425
+ private
426
+
427
+ # Adds the lock state commands of all locks of the series to the given pipeline.
428
+ #
429
+ # NOTE: (pipeline layout) the commands of each lock are the commands of the single lock attempt
430
+ # (`pipeline_lock_state`), so each lock has the same number of replies (see
431
+ # `series_lock_state_replies`) and its replies are read by the position constants of
432
+ # `pipeline_lock_state` (see `series_lock_states`);
433
+ #
434
+ # @param pipeline [RedisClient::Pipeline]
435
+ # @param lock_keys [Array<String>]
436
+ # @param lock_readers_keys [Array<String>]
437
+ # @param acquirer_id [String]
438
+ # @param read_mode [Boolean]
439
+ # @param queueing [Boolean] Queue the lock requests in the same round trip.
440
+ # @param request_queues [Array<String>]
441
+ # @param opposite_request_queues [Array<String>]
442
+ # @param acquirer_position [Numeric]
443
+ # @param priority [Integer]
444
+ # @param acquirer_dead_score [Float]
445
+ # @param lock_names [Array<String>]
446
+ # @param queue_ttl [Integer] In seconds.
447
+ # @param lock_index_enabled [Boolean]
448
+ # @param lock_index_grace_period [Integer] In milliseconds.
449
+ # @return [void]
450
+ #
451
+ # @api private
452
+ # @since 1.23.0
453
+ def pipeline_series_lock_state(
454
+ pipeline,
455
+ lock_keys,
456
+ lock_readers_keys,
457
+ acquirer_id,
458
+ read_mode,
459
+ queueing,
460
+ request_queues,
461
+ opposite_request_queues,
462
+ acquirer_position,
463
+ priority,
464
+ acquirer_dead_score,
465
+ lock_names,
466
+ queue_ttl,
467
+ lock_index_enabled,
468
+ lock_index_grace_period
469
+ )
470
+ lock_keys.each_with_index do |lock_key, index|
471
+ pipeline_lock_state(
472
+ pipeline, lock_key, lock_readers_keys[index], acquirer_id, read_mode, queueing,
473
+ request_queues[index], opposite_request_queues[index], acquirer_position, priority,
474
+ acquirer_dead_score, lock_names[index], queue_ttl, lock_index_enabled,
475
+ lock_index_grace_period
476
+ )
477
+ end
478
+ end
479
+
480
+ # The number of the replies of one lock in the lock state pipeline of the series
481
+ # (see `pipeline_series_lock_state`).
482
+ #
483
+ # @param read_mode [Boolean]
484
+ # @param queueing [Boolean] The lock requests are queued in the same round trip.
485
+ # @param lock_index_enabled [Boolean]
486
+ # @return [Integer]
487
+ #
488
+ # @api private
489
+ # @since 1.23.0
490
+ def series_lock_state_replies(read_mode, queueing, lock_index_enabled)
491
+ LOCK_STATE_QUEUE_REPLIES_AT +
492
+ (queueing ? series_queue_replies(lock_index_enabled) : 0) +
493
+ (read_mode ? 0 : 1) # NOTE: the write-only tail of the lock state pipeline
494
+ end
495
+
496
+ # The number of the replies of one lock request queueing (see `queue_lock_request`).
497
+ #
498
+ # @param lock_index_enabled [Boolean]
499
+ # @return [Integer]
500
+ #
501
+ # @api private
502
+ # @since 1.23.0
503
+ def series_queue_replies(lock_index_enabled)
504
+ QUEUE_REPLIES + (lock_index_enabled ? QUEUE_LOCK_INDEX_REPLIES : 0)
505
+ end
506
+
507
+ # The state of each lock of the series: the replies of the lock state pipeline of the series
508
+ # (the same reply reading as in the single lock attempt, per lock).
509
+ #
510
+ # @param state_replies [Array<Any>] See `pipeline_series_lock_state`.
511
+ # @param locks_count [Integer]
512
+ # @param acquirer_id [String]
513
+ # @param read_mode [Boolean]
514
+ # @param queueing [Boolean] The lock requests are queued in the same round trip.
515
+ # @param lock_index_enabled [Boolean]
516
+ # @return [Array<Hash<Symbol,Any>>]
517
+ # - `:obtainer` - the write lock obtainer;
518
+ # - `:redis_time` - redis server time (in milliseconds);
519
+ # - `:own_read_lock_expiration` - the read lock expiration of the current acquirer;
520
+ # - `:read_locked` - write lock requests: live read locks exist; read lock requests: the own
521
+ # read lock is live;
522
+ # - `:conflict_rw_mode` - the mode of the lock (already obtained by the current acquirer) that
523
+ # the lock request conflicts with;
524
+ # - `:queued`, `:waiting_request`, `:opposite_waiting_request` - the lock request is queued and
525
+ # the queue heads;
526
+ # - `:dropped_requests` - the dead lock requests dropped by the attempt (for logs);
527
+ #
528
+ # @api private
529
+ # @since 1.23.0
530
+ # rubocop:disable Metrics/MethodLength
531
+ def series_lock_states(
532
+ state_replies,
533
+ locks_count,
534
+ acquirer_id,
535
+ read_mode,
536
+ queueing,
537
+ lock_index_enabled
538
+ )
539
+ lock_replies = series_lock_state_replies(read_mode, queueing, lock_index_enabled)
540
+
541
+ Array.new(locks_count) do |index|
542
+ replies_at = index * lock_replies
543
+ # @type var current_lock_obtainer: String?
544
+ current_lock_obtainer = state_replies[replies_at + LOCK_STATE_OBTAINER_REPLY]
545
+ redis_time = RedisQueuedLocks::Resource.redis_time_ms(
546
+ state_replies[replies_at + LOCK_STATE_REDIS_TIME_REPLY]
547
+ )
548
+ # @type var own_read_lock_expiration: Float?
549
+ own_read_lock_expiration = state_replies[replies_at + LOCK_STATE_OWN_READ_LOCK_REPLY]
550
+ # NOTE: the write-only tail is the last reply of the lock (write lock requests only)
551
+ # @type var longest_read_lock: [String, Float]?
552
+ longest_read_lock = read_mode ? nil : state_replies[replies_at + lock_replies - 1].first
553
+ read_locked_by_acquirer = own_read_lock_expiration.to_f > redis_time
554
+ read_locked = longest_read_lock ? longest_read_lock[1] > redis_time : false
555
+ queue_replies_at = replies_at + LOCK_STATE_QUEUE_REPLIES_AT
556
+
557
+ {
558
+ obtainer: current_lock_obtainer,
559
+ redis_time:,
560
+ own_read_lock_expiration:,
561
+ read_locked: read_mode ? read_locked_by_acquirer : read_locked,
562
+ conflict_rw_mode:
563
+ if current_lock_obtainer != nil && acquirer_id == current_lock_obtainer
564
+ :write
565
+ elsif read_locked_by_acquirer
566
+ :read
567
+ end,
568
+ queued: queueing,
569
+ waiting_request:
570
+ queueing ? state_replies[queue_replies_at + QUEUE_HEAD_REPLY].first : nil,
571
+ opposite_waiting_request:
572
+ queueing ? state_replies[queue_replies_at + OPPOSITE_QUEUE_HEAD_REPLY].first : nil,
573
+ dropped_requests: nil
574
+ }
575
+ end
576
+ end
577
+ # rubocop:enable Metrics/MethodLength
578
+
579
+ # Queues the lock requests of the locks that are not reentrant ones in one round trip (the same
580
+ # commands as in the single lock attempt) and puts the queue heads to the lock states.
581
+ #
582
+ # @param rconn [RedisClient]
583
+ # @param lock_states [Array<Hash<Symbol,Any>>] See `series_lock_states`.
584
+ # @param lock_processes [Array<Symbol,NilClass>] Reentrant locks have their process here.
585
+ # @param request_queues [Array<String>]
586
+ # @param opposite_request_queues [Array<String>]
587
+ # @param acquirer_position [Numeric]
588
+ # @param priority [Integer]
589
+ # @param acquirer_id [String]
590
+ # @param acquirer_dead_score [Float]
591
+ # @param lock_names [Array<String>]
592
+ # @param queue_ttl [Integer]
593
+ # @param lock_index_enabled [Boolean]
594
+ # @param lock_index_grace_period [Integer]
595
+ # @return [void]
596
+ #
597
+ # @api private
598
+ # @since 1.23.0
599
+ def queue_series_lock_requests(
600
+ rconn,
601
+ lock_states,
602
+ lock_processes,
603
+ request_queues,
604
+ opposite_request_queues,
605
+ acquirer_position,
606
+ priority,
607
+ acquirer_id,
608
+ acquirer_dead_score,
609
+ lock_names,
610
+ queue_ttl,
611
+ lock_index_enabled,
612
+ lock_index_grace_period
613
+ )
614
+ queued_locks = lock_states.each_index.select { |index| lock_processes[index] == nil }
615
+ return if queued_locks.empty?
616
+
617
+ # NOTE: (pipeline layout) the replies of each lock are the replies of `queue_lock_request`
618
+ # @type var queue_replies: Array[untyped]
619
+ queue_replies = rconn.pipelined do |pipeline|
620
+ queued_locks.each do |index|
621
+ queue_lock_request(
622
+ pipeline, request_queues[index], opposite_request_queues[index], acquirer_position,
623
+ priority, acquirer_id, acquirer_dead_score, lock_names[index], queue_ttl,
624
+ lock_index_enabled, lock_index_grace_period
625
+ )
626
+ end
627
+ end
628
+ lock_replies = series_queue_replies(lock_index_enabled)
629
+
630
+ queued_locks.each_with_index do |index, queue_position|
631
+ lock_state = lock_states[index]
632
+ replies_at = queue_position * lock_replies
633
+ lock_state[:queued] = true
634
+ lock_state[:waiting_request] = queue_replies[replies_at + QUEUE_HEAD_REPLY].first
635
+ lock_state[:opposite_waiting_request] =
636
+ queue_replies[replies_at + OPPOSITE_QUEUE_HEAD_REPLY].first
637
+ end
638
+ end
639
+
640
+ # (priorities) Drops the dead lock requests of other priority bands that block the series and
641
+ # puts the actual queue heads to the lock states: Step 2.2 of the attempt and of its pre-check
642
+ # for all queued locks of the series (see `TryToLock#drop_dead_requests_ahead`):
643
+ # - one round trip drops the dead blocking requests of all locks and reads the actual queue
644
+ # heads of these locks, up to `MAX_DEAD_REQUEST_REMOVALS` round trips per call: the cost does
645
+ # not depend on the number of locks of the series;
646
+ # - no round trips while no dead request blocks the series;
647
+ #
648
+ # @param rconn [RedisClient]
649
+ # @param read_mode [Boolean]
650
+ # @param request_queues [Array<String>]
651
+ # @param opposite_request_queues [Array<String>]
652
+ # @param lock_states [Array<Hash<Symbol,Any>>] See `series_lock_states`.
653
+ # @param acquirer_id [String]
654
+ # @param acquirer_position [Numeric]
655
+ # @param acquirer_dead_score [Float]
656
+ # @param access_strategy [Symbol]
657
+ # @return [void]
658
+ #
659
+ # @api private
660
+ # @since 1.23.0
661
+ # rubocop:disable Metrics/MethodLength
662
+ def drop_series_dead_requests_ahead(
663
+ rconn,
664
+ read_mode,
665
+ request_queues,
666
+ opposite_request_queues,
667
+ lock_states,
668
+ acquirer_id,
669
+ acquirer_position,
670
+ acquirer_dead_score,
671
+ access_strategy
672
+ )
673
+ return unless access_strategy == :queued
674
+
675
+ round_trips = 0
676
+
677
+ while round_trips < MAX_DEAD_REQUEST_REMOVALS
678
+ # @type var dead_requests: Array[deadRequests]
679
+ dead_requests = []
680
+ lock_states.each_with_index do |lock_state, index|
681
+ next unless lock_state[:queued]
682
+
683
+ dead_lock_requests = find_dead_requests_ahead(
684
+ read_mode, access_strategy, acquirer_id, acquirer_position, acquirer_dead_score,
685
+ lock_state[:waiting_request], lock_state[:opposite_waiting_request]
686
+ )
687
+ dead_requests << [index, dead_lock_requests] if dead_lock_requests
688
+ end
689
+ break if dead_requests.empty?
690
+
691
+ # NOTE: (pipeline layout) the removal commands of all locks (see
692
+ # `pipeline_dead_requests_ahead_removal`), then two ZRANGE WITHSCORES per lock (the heads
693
+ # of the request queue and of the opposite request queue, the tail of the pipeline);
694
+ # @type var queue_state: Array[untyped]
695
+ queue_state = rconn.pipelined do |pipeline|
696
+ dead_requests.each do |index, lock_requests|
697
+ dead_head, dead_head_priority, dead_ahead, dead_ahead_priority = lock_requests
698
+ pipeline_dead_requests_ahead_removal(
699
+ pipeline, request_queues[index], opposite_request_queues[index], acquirer_dead_score,
700
+ dead_head, dead_head_priority, dead_ahead, dead_ahead_priority
701
+ )
702
+ end
703
+ dead_requests.each do |lock_dead_requests|
704
+ index = lock_dead_requests[0]
705
+ pipeline.call('ZRANGE', request_queues[index], '0', '0', 'WITHSCORES')
706
+ pipeline.call('ZRANGE', opposite_request_queues[index], '0', '0', 'WITHSCORES')
707
+ end
708
+ end
709
+ heads_at = queue_state.size - (dead_requests.size * 2)
710
+
711
+ dead_requests.each_with_index do |(index, lock_requests), position|
712
+ lock_state = lock_states[index]
713
+ dead_head, dead_head_priority, dead_ahead, dead_ahead_priority = lock_requests
714
+ # @type var removed_requests: Array[[String, Integer]]
715
+ removed_requests = lock_state[:dropped_requests] || []
716
+ removed_requests << [dead_head[0], dead_head_priority] if dead_head && dead_head_priority
717
+ if dead_ahead && dead_ahead_priority
718
+ removed_requests << [dead_ahead[0], dead_ahead_priority]
719
+ end
720
+ lock_state[:dropped_requests] = removed_requests
721
+ lock_state[:waiting_request] = queue_state[heads_at + (position * 2)].first
722
+ lock_state[:opposite_waiting_request] = queue_state[heads_at + (position * 2) + 1].first
723
+ end
724
+ round_trips += 1
725
+ end
726
+ end
727
+ # rubocop:enable Metrics/MethodLength
728
+
729
+ # The first lock of the series that can not be obtained now (the same decision as in the single
730
+ # lock attempt, per lock: `find_opposite_request_ahead`, `lock_obstacle`).
731
+ #
732
+ # @param lock_states [Array<Hash<Symbol,Any>>] See `series_lock_states`.
733
+ # @param read_mode [Boolean]
734
+ # @param access_strategy [Symbol]
735
+ # @param acquirer_id [String]
736
+ # @param acquirer_position [Numeric]
737
+ # @param ready_locks [Array<Integer>]
738
+ # The indexes of the checked locks without an obstacle are added here (in the order of the lock
739
+ # names: the locks after the obstacle are not checked).
740
+ # @return [Array<Symbol,Integer>,NilClass]
741
+ # The obstacle and the index of the lock, `nil` when all locks can be obtained.
742
+ #
743
+ # @api private
744
+ # @since 1.23.0
745
+ def find_series_lock_obstacle(
746
+ lock_states,
747
+ read_mode,
748
+ access_strategy,
749
+ acquirer_id,
750
+ acquirer_position,
751
+ ready_locks
752
+ )
753
+ lock_states.each_with_index do |lock_state, index|
754
+ next unless lock_state[:queued]
755
+
756
+ # @type var waiting_request: [String, Float]?
757
+ waiting_request = lock_state[:waiting_request]
758
+ opposite_request_ahead = find_opposite_request_ahead(
759
+ access_strategy, lock_state[:opposite_waiting_request], acquirer_position, acquirer_id
760
+ )
761
+ obstacle = lock_obstacle(
762
+ read_mode, access_strategy, acquirer_id, waiting_request ? waiting_request[0] : nil,
763
+ opposite_request_ahead, lock_state[:obtainer], lock_state[:read_locked]
764
+ )
765
+ return [obstacle, index] if obstacle
766
+
767
+ ready_locks << index
768
+ end
769
+
770
+ nil
771
+ end
772
+
773
+ # Logs the queueing steps of the queued locks of the series (the same events as in the single
774
+ # lock attempt, per lock).
775
+ #
776
+ # @param logger [::Logger,#debug]
777
+ # @param log_sampled [Boolean]
778
+ # @param log_lock_try [Boolean]
779
+ # @param lock_keys [Array<String>]
780
+ # @param lock_states [Array<Hash<Symbol,Any>>] See `series_lock_states`.
781
+ # @param queue_ttl [Integer]
782
+ # @param acquirer_id [String]
783
+ # @param host_id [String]
784
+ # @param access_strategy [Symbol]
785
+ # @param rw_mode [Symbol]
786
+ # @param priority [Integer]
787
+ # @return [void]
788
+ #
789
+ # @api private
790
+ # @since 1.23.0
791
+ # rubocop:disable Metrics/MethodLength
792
+ def log_series_queueing(
793
+ logger,
794
+ log_sampled,
795
+ log_lock_try,
796
+ lock_keys,
797
+ lock_states,
798
+ queue_ttl,
799
+ acquirer_id,
800
+ host_id,
801
+ access_strategy,
802
+ rw_mode,
803
+ priority
804
+ )
805
+ return unless log_sampled && log_lock_try
806
+
807
+ lock_states.each_with_index do |lock_state, index|
808
+ next unless lock_state[:queued]
809
+
810
+ lock_key = lock_keys[index]
811
+ # @type var waiting_request: [String, Float]?
812
+ waiting_request = lock_state[:waiting_request]
813
+
814
+ LogVisitor.acq_added_to_queue(
815
+ logger, log_sampled, log_lock_try, lock_key,
816
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode, priority
817
+ )
818
+ LogVisitor.remove_expired_acqs(
819
+ logger, log_sampled, log_lock_try, lock_key,
820
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode
821
+ )
822
+ # @type var dropped_requests: Array[[String, Integer]]?
823
+ dropped_requests = lock_state[:dropped_requests]
824
+ if dropped_requests
825
+ log_dropped_requests(
826
+ logger, log_sampled, log_lock_try, lock_key, queue_ttl, acquirer_id, host_id,
827
+ access_strategy, rw_mode, dropped_requests
828
+ )
829
+ end
830
+ LogVisitor.get_first_from_queue(
831
+ logger, log_sampled, log_lock_try, lock_key,
832
+ queue_ttl, acquirer_id, host_id, access_strategy, rw_mode,
833
+ waiting_request ? waiting_request[0] : nil,
834
+ logged_request_priority(log_sampled, log_lock_try, waiting_request)
835
+ )
836
+ end
837
+ end
838
+ # rubocop:enable Metrics/MethodLength
839
+
840
+ # (priorities) Logs the dead lock requests dropped by a pre-check that does not fail
841
+ # (see `TryToLock#log_dropped_requests`).
842
+ #
843
+ # @param logger [::Logger,#debug]
844
+ # @param log_sampled [Boolean]
845
+ # @param log_lock_try [Boolean]
846
+ # @param lock_keys [Array<String>]
847
+ # @param lock_states [Array<Hash<Symbol,Any>>] See `series_lock_states`.
848
+ # @param queue_ttl [Integer]
849
+ # @param acquirer_id [String]
850
+ # @param host_id [String]
851
+ # @param access_strategy [Symbol]
852
+ # @param rw_mode [Symbol]
853
+ # @return [void]
854
+ #
855
+ # @api private
856
+ # @since 1.23.0
857
+ def log_series_dropped_requests(
858
+ logger,
859
+ log_sampled,
860
+ log_lock_try,
861
+ lock_keys,
862
+ lock_states,
863
+ queue_ttl,
864
+ acquirer_id,
865
+ host_id,
866
+ access_strategy,
867
+ rw_mode
868
+ )
869
+ return unless log_sampled && log_lock_try
870
+
871
+ lock_states.each_with_index do |lock_state, index|
872
+ # @type var dropped_requests: Array[[String, Integer]]?
873
+ dropped_requests = lock_state[:dropped_requests]
874
+ next unless dropped_requests
875
+
876
+ log_dropped_requests(
877
+ logger, log_sampled, log_lock_try, lock_keys[index], queue_ttl, acquirer_id, host_id,
878
+ access_strategy, rw_mode, dropped_requests
879
+ )
880
+ end
881
+ end
882
+
883
+ # Logs the obstacle to the series obtaining: the exit event of the single lock attempt for
884
+ # the lock that blocks the series (see `log_lock_obstacle`).
885
+ #
886
+ # @param rconn [RedisClient]
887
+ # @param obstacle [Symbol]
888
+ # @param lock_index [Integer] The index of the lock that blocks the series.
889
+ # @param lock_states [Array<Hash<Symbol,Any>>] See `series_lock_states`.
890
+ # @param logger [::Logger,#debug]
891
+ # @param log_sampled [Boolean]
892
+ # @param log_lock_try [Boolean]
893
+ # @param lock_keys [Array<String>]
894
+ # @param queue_ttl [Integer]
895
+ # @param acquirer_id [String]
896
+ # @param host_id [String]
897
+ # @param access_strategy [Symbol]
898
+ # @param rw_mode [Symbol]
899
+ # @return [void]
900
+ #
901
+ # @api private
902
+ # @since 1.23.0
903
+ def log_series_lock_obstacle(
904
+ rconn,
905
+ obstacle,
906
+ lock_index,
907
+ lock_states,
908
+ logger,
909
+ log_sampled,
910
+ log_lock_try,
911
+ lock_keys,
912
+ queue_ttl,
913
+ acquirer_id,
914
+ host_id,
915
+ access_strategy,
916
+ rw_mode
917
+ )
918
+ return unless log_sampled && log_lock_try
919
+
920
+ lock_state = lock_states[lock_index]
921
+ # @type var waiting_request: [String, Float]?
922
+ waiting_request = lock_state[:waiting_request]
923
+
924
+ log_lock_obstacle(
925
+ rconn, obstacle, logger, log_sampled, log_lock_try, lock_keys[lock_index], queue_ttl,
926
+ acquirer_id, host_id, access_strategy, rw_mode,
927
+ waiting_request ? waiting_request[0] : nil,
928
+ logged_request_priority(log_sampled, log_lock_try, waiting_request),
929
+ lock_state[:opposite_waiting_request], lock_state[:obtainer]
930
+ )
931
+ end
932
+
933
+ # Pre-check of the attempt (Step PRE): the lock state of the series without WATCH (one round
934
+ # trip, see `TryToLock#precheck_lock`):
935
+ # - `fail_fast` attempts: the lock state only (the lock requests are not queued); fails with
936
+ # `:fail_fast_no_try` on the first obtained lock;
937
+ # - retries: the lock state and the lock request queueing; fails on the first obstacle (the same
938
+ # decision and logs as in the attempt);
939
+ # - a same-process conflict of any lock is not decided: the attempt continues with
940
+ # the WATCH/MULTI logic (a retry of the series with reentrant locks has no one round trip
941
+ # failures);
942
+ # - the pre-check never obtains locks (mutual exclusion relies on WATCH/MULTI only);
943
+ #
944
+ # @param rconn [RedisClient]
945
+ # @param logger [::Logger,#debug]
946
+ # @param log_sampled [Boolean]
947
+ # @param log_lock_try [Boolean]
948
+ # @param lock_keys [Array<String>]
949
+ # @param rw_mode [Symbol]
950
+ # @param lock_readers_keys [Array<String>]
951
+ # @param request_queues [Array<String>]
952
+ # @param opposite_request_queues [Array<String>]
953
+ # @param lock_names [Array<String>]
954
+ # @param acquirer_id [String]
955
+ # @param host_id [String]
956
+ # @param acquirer_position [Numeric]
957
+ # @param priority [Integer]
958
+ # @param acquirer_dead_score [Float]
959
+ # @param queue_ttl [Integer]
960
+ # @param lock_index_enabled [Boolean]
961
+ # @param lock_index_grace_period [Integer]
962
+ # @param fail_fast [Boolean]
963
+ # @param access_strategy [Symbol]
964
+ # @param ready_locks [Array<Integer>] See `find_series_lock_obstacle`.
965
+ # @return [Array<Symbol,Integer>,NilClass]
966
+ # The failure result of the attempt and the index of the lock or `nil` (continue the attempt).
967
+ #
968
+ # @api private
969
+ # @since 1.23.0
970
+ # rubocop:disable Metrics/MethodLength
971
+ def precheck_lock_series(
972
+ rconn,
973
+ logger,
974
+ log_sampled,
975
+ log_lock_try,
976
+ lock_keys,
977
+ rw_mode,
978
+ lock_readers_keys,
979
+ request_queues,
980
+ opposite_request_queues,
981
+ lock_names,
982
+ acquirer_id,
983
+ host_id,
984
+ acquirer_position,
985
+ priority,
986
+ acquirer_dead_score,
987
+ queue_ttl,
988
+ lock_index_enabled,
989
+ lock_index_grace_period,
990
+ fail_fast,
991
+ access_strategy,
992
+ ready_locks
993
+ )
994
+ read_mode = (rw_mode == :read)
995
+ # NOTE: the same pipeline layout and the same reply reading as in Step 0.1 of the attempt
996
+ # @type var state_replies: Array[untyped]
997
+ state_replies = rconn.pipelined do |pipeline|
998
+ pipeline_series_lock_state(
999
+ pipeline, lock_keys, lock_readers_keys, acquirer_id, read_mode, !fail_fast,
1000
+ request_queues, opposite_request_queues, acquirer_position, priority, acquirer_dead_score,
1001
+ lock_names, queue_ttl, lock_index_enabled, lock_index_grace_period
1002
+ )
1003
+ end
1004
+ lock_states = series_lock_states(
1005
+ state_replies, lock_names.size, acquirer_id, read_mode, !fail_fast, lock_index_enabled
1006
+ )
1007
+
1008
+ # NOTE: same-process conflicts are processed by the attempt (conflict strategies)
1009
+ return if lock_states.any? { |lock_state| lock_state[:conflict_rw_mode] != nil }
1010
+
1011
+ if fail_fast
1012
+ # NOTE: the fail-fast check of the attempt (Fast-Step X0) without same-process conflicts
1013
+ failed_lock_index = lock_states.find_index do |lock_state|
1014
+ lock_state[:obtainer] != nil || lock_state[:read_locked]
1015
+ end
1016
+ return failed_lock_index ? [:fail_fast_no_try, failed_lock_index] : nil
1017
+ end
1018
+
1019
+ # NOTE: (priorities) Step 2.2 of the attempt: before the obstacle decision
1020
+ drop_series_dead_requests_ahead(
1021
+ rconn, read_mode, request_queues, opposite_request_queues, lock_states, acquirer_id,
1022
+ acquirer_position, acquirer_dead_score, access_strategy
1023
+ )
1024
+ series_obstacle = find_series_lock_obstacle(
1025
+ lock_states, read_mode, access_strategy, acquirer_id, acquirer_position, ready_locks
1026
+ )
1027
+
1028
+ if series_obstacle
1029
+ # NOTE: the same log events as the failed attempt in the same order
1030
+ log_series_queueing(
1031
+ logger, log_sampled, log_lock_try, lock_keys, lock_states, queue_ttl, acquirer_id,
1032
+ host_id, access_strategy, rw_mode, priority
1033
+ )
1034
+ log_series_lock_obstacle(
1035
+ rconn, series_obstacle[0], series_obstacle[1], lock_states, logger, log_sampled,
1036
+ log_lock_try, lock_keys, queue_ttl, acquirer_id, host_id, access_strategy, rw_mode
1037
+ )
1038
+ else
1039
+ # NOTE: the attempt continues: it logs its own queueing, the dropped dead lock requests are
1040
+ # logged here (the attempt does not see them, see `TryToLock#precheck_lock`);
1041
+ log_series_dropped_requests(
1042
+ logger, log_sampled, log_lock_try, lock_keys, lock_states, queue_ttl, acquirer_id,
1043
+ host_id, access_strategy, rw_mode
1044
+ )
1045
+ end
1046
+
1047
+ series_obstacle
1048
+ end
1049
+ # rubocop:enable Metrics/MethodLength
1050
+ end
1051
+ # rubocop:enable Metrics/ModuleLength