redis_queued_locks 1.22.0 → 1.23.1
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.
- checksums.yaml +4 -4
- data/.claude/project-overview.md +90 -38
- data/.claude/rules/acquirer.md +12 -7
- data/.claude/rules/arguments.md +2 -2
- data/.claude/rules/logic.md +5 -4
- data/.claude/rules/tests.md +4 -4
- data/.claude/rules/type-checking.md +2 -3
- data/.claude/rules/visitors.md +8 -6
- data/.claude/skills/rql-code-review/SKILL.md +25 -20
- data/.ruby-version +1 -1
- data/CHANGELOG.md +30 -1
- data/CLAUDE.md +5 -4
- data/README.md +259 -103
- data/lib/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue.rb +4 -4
- data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rb +582 -276
- data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire.rb +29 -12
- data/lib/redis_queued_locks/acquirer/acquire_lock.rb +36 -27
- data/lib/redis_queued_locks/acquirer/extend_lock_ttl.rb +13 -13
- data/lib/redis_queued_locks/acquirer/lock_series/instr_visitor.rb +243 -0
- data/lib/redis_queued_locks/acquirer/lock_series/log_visitor.rb +472 -0
- data/lib/redis_queued_locks/acquirer/lock_series/release_lock_series.rb +181 -0
- data/lib/redis_queued_locks/acquirer/lock_series/try_to_lock_series.rb +1051 -0
- data/lib/redis_queued_locks/acquirer/lock_series.rb +957 -0
- data/lib/redis_queued_locks/acquirer.rb +1 -1
- data/lib/redis_queued_locks/client.rb +55 -25
- data/lib/redis_queued_locks/errors.rb +8 -0
- data/lib/redis_queued_locks/version.rb +2 -2
- data/redis_queued_locks.gemspec +21 -14
- data/sig/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue.rbs +1 -1
- data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rbs +106 -5
- data/sig/redis_queued_locks/acquirer/acquire_lock.rbs +1 -1
- data/sig/redis_queued_locks/acquirer/extend_lock_ttl.rbs +2 -2
- data/sig/redis_queued_locks/acquirer/lock_series/instr_visitor.rbs +102 -0
- data/sig/redis_queued_locks/acquirer/lock_series/log_visitor.rbs +154 -0
- data/sig/redis_queued_locks/acquirer/lock_series/release_lock_series.rbs +37 -0
- data/sig/redis_queued_locks/acquirer/lock_series/try_to_lock_series.rbs +198 -0
- data/sig/redis_queued_locks/acquirer/lock_series.rbs +106 -0
- data/sig/redis_queued_locks/client.rbs +70 -3
- data/sig/redis_queued_locks/errors.rbs +6 -0
- metadata +32 -11
- data/lib/redis_queued_locks/acquirer/lock_series_poc/instr_visitor.rb +0 -61
- data/lib/redis_queued_locks/acquirer/lock_series_poc/log_visitor.rb +0 -104
- 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
|