redis_queued_locks 1.19.0 → 1.20.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.
- checksums.yaml +4 -4
- data/.claude/project-overview.md +62 -11
- data/.claude/rules/acquirer.md +29 -11
- data/.claude/rules/arguments.md +3 -2
- data/.claude/rules/git.md +3 -2
- data/.claude/rules/logic.md +4 -3
- data/.claude/rules/swarm.md +12 -7
- data/.claude/rules/tests.md +6 -0
- data/.claude/rules/type-checking.md +2 -1
- data/.claude/rules/visitors.md +1 -1
- data/.claude/skills/rql-code-review/SKILL.md +372 -0
- data/.claude/skills/rql-code-review/templates/report.en.md +31 -0
- data/.claude/skills/rql-code-review/templates/report.ru.md +31 -0
- data/CHANGELOG.md +68 -0
- data/CLAUDE.md +4 -2
- data/README.md +257 -22
- data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rb +684 -128
- data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire.rb +3 -1
- data/lib/redis_queued_locks/acquirer/acquire_lock.rb +24 -3
- data/lib/redis_queued_locks/acquirer/clear_dead_requests.rb +145 -18
- data/lib/redis_queued_locks/acquirer/extend_lock_ttl.rb +71 -10
- data/lib/redis_queued_locks/acquirer/keys.rb +142 -7
- data/lib/redis_queued_locks/acquirer/lock_info.rb +10 -4
- data/lib/redis_queued_locks/acquirer/lock_series_poc.rb +26 -7
- data/lib/redis_queued_locks/acquirer/locks.rb +195 -83
- data/lib/redis_queued_locks/acquirer/queue_info.rb +12 -18
- data/lib/redis_queued_locks/acquirer/queues.rb +114 -50
- data/lib/redis_queued_locks/acquirer/reindex_locks.rb +140 -0
- data/lib/redis_queued_locks/acquirer/release_all_locks.rb +147 -58
- data/lib/redis_queued_locks/acquirer/release_lock.rb +52 -19
- data/lib/redis_queued_locks/acquirer/release_locks_of.rb +235 -66
- data/lib/redis_queued_locks/acquirer.rb +1 -0
- data/lib/redis_queued_locks/client.rb +97 -11
- data/lib/redis_queued_locks/config.rb +5 -1
- data/lib/redis_queued_locks/resource.rb +99 -2
- data/lib/redis_queued_locks/swarm/flush_zombies.rb +332 -61
- data/lib/redis_queued_locks/swarm/zombie_info.rb +203 -53
- data/lib/redis_queued_locks/swarm.rb +15 -4
- data/lib/redis_queued_locks/version.rb +2 -2
- data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rbs +107 -1
- data/sig/redis_queued_locks/acquirer/acquire_lock/yield_expire.rbs +1 -1
- data/sig/redis_queued_locks/acquirer/acquire_lock.rbs +2 -0
- data/sig/redis_queued_locks/acquirer/clear_dead_requests.rbs +10 -1
- data/sig/redis_queued_locks/acquirer/extend_lock_ttl.rbs +5 -2
- data/sig/redis_queued_locks/acquirer/keys.rbs +11 -1
- data/sig/redis_queued_locks/acquirer/locks.rbs +15 -6
- data/sig/redis_queued_locks/acquirer/queues.rbs +9 -2
- data/sig/redis_queued_locks/acquirer/reindex_locks.rbs +31 -0
- data/sig/redis_queued_locks/acquirer/release_all_locks.rbs +8 -1
- data/sig/redis_queued_locks/acquirer/release_lock.rbs +3 -1
- data/sig/redis_queued_locks/acquirer/release_locks_of.rbs +20 -1
- data/sig/redis_queued_locks/client.rbs +8 -0
- data/sig/redis_queued_locks/resource.rbs +6 -0
- data/sig/redis_queued_locks/swarm/flush_zombies.rbs +51 -1
- data/sig/redis_queued_locks/swarm/zombie_info.rbs +58 -7
- data/sig/vendor/redis_client.rbs +1 -0
- metadata +6 -1
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
# - `:read` lock is shared: it is obtained when there is no write lock;
|
|
6
6
|
# - read locks are stored in the readers registry: a sorted set where the member is an acquirer
|
|
7
7
|
# id and the score is the read lock expiration time (redis server time, in milliseconds);
|
|
8
|
-
# - each read lock has its own data hash (the same structure as the write lock:
|
|
9
|
-
#
|
|
10
|
-
#
|
|
8
|
+
# - each read lock has its own data hash (the same structure as the write lock: timestamp, ttl,
|
|
9
|
+
# meta, reentrant lock data; acquirer and host are derived from the acquirer id and are not
|
|
10
|
+
# stored) with the read lock ttl; the readers registry is the only source of truth for
|
|
11
|
+
# the read lock existence (the data hash is a data carrier only);
|
|
11
12
|
# - mutual exclusion relies on the optimistic transactions only (WATCH/MULTI):
|
|
12
13
|
# - readers WATCH the write lock key: a new write lock invalidates the reader's transaction;
|
|
13
14
|
# - writers WATCH the write lock key and the readers registry: a new read lock invalidates
|
|
@@ -21,9 +22,24 @@
|
|
|
21
22
|
# - `:random` access strategy ignores both queues (no ordering between modes, so
|
|
22
23
|
# a continuous flow of readers can starve writers);
|
|
23
24
|
#
|
|
25
|
+
# Lock index (`RedisQueuedLocks::Resource::LOCK_INDEX_KEY`): only with the enabled lock index
|
|
26
|
+
# (`lock_index_enabled`, `config['lock_index_enabled']`); with the disabled lock index (default)
|
|
27
|
+
# no lock index commands are sent and lock request queues have no TTL:
|
|
28
|
+
# - every command that creates a lock key is followed by the lock index update in the same
|
|
29
|
+
# pipeline/transaction: the lock name score is the upper bound of the key lifetime (`ZADD GT
|
|
30
|
+
# <client time + ttl + grace period>`): the request queue in the queueing pipeline, the lock
|
|
31
|
+
# (write lock / readers registry) in the obtaining transaction (atomically: the lock index
|
|
32
|
+
# entry can be dropped by `#unlock`/`#clear_locks` between the queueing and the obtaining);
|
|
33
|
+
# - lock extensions (reentrant `:extendable_work_through`) increase the score by the extension
|
|
34
|
+
# in the same transaction (`ZADD XX INCR`);
|
|
35
|
+
# - lock request queues live as long as the latest lock request (`PEXPIRE NX` + `GT` with
|
|
36
|
+
# the queue ttl on each queueing) so the lifetime of every lock key is bounded;
|
|
37
|
+
# - each queueing drops the expired lock names from the index (`ZREMRANGEBYSCORE`): the index
|
|
38
|
+
# is maintained by the lock acquirement itself (no background jobs);
|
|
39
|
+
#
|
|
24
40
|
# @api private
|
|
25
41
|
# @since 1.0.0
|
|
26
|
-
# @version 1.
|
|
42
|
+
# @version 1.20.0
|
|
27
43
|
# rubocop:disable Metrics/ModuleLength
|
|
28
44
|
module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
|
|
29
45
|
require_relative 'try_to_lock/log_visitor'
|
|
@@ -37,6 +53,73 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
|
|
|
37
53
|
return redis.call("PEXPIRE", KEYS[1], new_lock_pttl);
|
|
38
54
|
LUA_SCRIPT
|
|
39
55
|
|
|
56
|
+
# Lock data for the try-lock log events when the lock data is not extracted
|
|
57
|
+
# (logs are disabled): shared frozen object instead of a new hash for each failed attempt.
|
|
58
|
+
#
|
|
59
|
+
# NOTE: (steep) an empty hash literal with a method call can't be annotated inline
|
|
60
|
+
# (the constant type is declared in RBS);
|
|
61
|
+
#
|
|
62
|
+
# @return [Hash]
|
|
63
|
+
#
|
|
64
|
+
# @api private
|
|
65
|
+
# @since 1.20.0
|
|
66
|
+
NO_LOCK_DATA = {}.freeze # steep:ignore
|
|
67
|
+
|
|
68
|
+
# Position of the write lock obtainer (`HGET acq_id`) in the lock state pipeline
|
|
69
|
+
# (see "pipeline layout" in `pipeline_lock_state`).
|
|
70
|
+
#
|
|
71
|
+
# @return [Integer]
|
|
72
|
+
#
|
|
73
|
+
# @api private
|
|
74
|
+
# @since 1.20.0
|
|
75
|
+
LOCK_STATE_OBTAINER_REPLY = 0
|
|
76
|
+
|
|
77
|
+
# Position of the redis server time (`TIME`) in the lock state pipeline
|
|
78
|
+
# (see "pipeline layout" in `pipeline_lock_state`).
|
|
79
|
+
#
|
|
80
|
+
# @return [Integer]
|
|
81
|
+
#
|
|
82
|
+
# @api private
|
|
83
|
+
# @since 1.20.0
|
|
84
|
+
LOCK_STATE_REDIS_TIME_REPLY = 1
|
|
85
|
+
|
|
86
|
+
# Position of the read lock expiration of the current acquirer (`ZSCORE`) in the lock state
|
|
87
|
+
# pipeline (see "pipeline layout" in `pipeline_lock_state`).
|
|
88
|
+
#
|
|
89
|
+
# @return [Integer]
|
|
90
|
+
#
|
|
91
|
+
# @api private
|
|
92
|
+
# @since 1.20.0
|
|
93
|
+
LOCK_STATE_OWN_READ_LOCK_REPLY = 2
|
|
94
|
+
|
|
95
|
+
# Position of the first reply of the lock request queueing commands (`queue_lock_request`)
|
|
96
|
+
# in the lock state pipeline (see "pipeline layout" in `pipeline_lock_state`).
|
|
97
|
+
#
|
|
98
|
+
# @return [Integer]
|
|
99
|
+
#
|
|
100
|
+
# @api private
|
|
101
|
+
# @since 1.20.0
|
|
102
|
+
LOCK_STATE_QUEUE_REPLIES_AT = 3
|
|
103
|
+
|
|
104
|
+
# Position of the first request of the request queue (`ZRANGE`) in the replies of the lock
|
|
105
|
+
# request queueing commands (`queue_lock_request`), relative to their first reply.
|
|
106
|
+
#
|
|
107
|
+
# @return [Integer]
|
|
108
|
+
#
|
|
109
|
+
# @api private
|
|
110
|
+
# @since 1.20.0
|
|
111
|
+
QUEUE_HEAD_REPLY = 3
|
|
112
|
+
|
|
113
|
+
# Position of the first request of the opposite request queue (`ZRANGE WITHSCORES`) in the
|
|
114
|
+
# replies of the lock request queueing commands (`queue_lock_request`), relative to their
|
|
115
|
+
# first reply.
|
|
116
|
+
#
|
|
117
|
+
# @return [Integer]
|
|
118
|
+
#
|
|
119
|
+
# @api private
|
|
120
|
+
# @since 1.20.0
|
|
121
|
+
OPPOSITE_QUEUE_HEAD_REPLY = 4
|
|
122
|
+
|
|
40
123
|
# @param redis [RedisClient]
|
|
41
124
|
# @param logger [::Logger,#debug]
|
|
42
125
|
# @param log_lock_try [Boolean]
|
|
@@ -45,23 +128,33 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
|
|
|
45
128
|
# @param lock_key_queue [String] Queue of write lock requests.
|
|
46
129
|
# @param read_lock_key_queue [String] Queue of read lock requests.
|
|
47
130
|
# @param lock_readers_key [String] Registry of read lock holders.
|
|
48
|
-
# @param read_lock_key [String]
|
|
131
|
+
# @param read_lock_key [String,NilClass]
|
|
132
|
+
# Read lock data of the current acquirer (`nil` for write lock requests: they never obtain
|
|
133
|
+
# read locks).
|
|
134
|
+
# @param lock_name [String] The lock name (the member of the lock index).
|
|
49
135
|
# @param acquirer_id [String]
|
|
50
136
|
# @param host_id [String]
|
|
51
137
|
# @param acquirer_position [Numeric]
|
|
52
138
|
# @param ttl [Integer]
|
|
53
139
|
# @param queue_ttl [Integer]
|
|
140
|
+
# @param lock_index_enabled [Boolean]
|
|
141
|
+
# Maintain the lock index (the lock index commands and the lock request queue TTL).
|
|
142
|
+
# @param lock_index_grace_period [Integer]
|
|
143
|
+
# The lock index score margin (in milliseconds) over the key lifetime (client clock skew and
|
|
144
|
+
# network latency tolerance).
|
|
54
145
|
# @param fail_fast [Boolean]
|
|
55
146
|
# @param conflict_strategy [Symbol]
|
|
56
147
|
# @param access_strategy [Symbol]
|
|
57
148
|
# @param meta [NilClass,Hash<String|Symbol,Any>]
|
|
149
|
+
# @param retry_attempt [Boolean]
|
|
150
|
+
# The attempt is a retry (the previous attempt of the acquirement did not obtain the lock).
|
|
58
151
|
# @param log_sampled [Boolean]
|
|
59
152
|
# @param instr_sampled [Boolean]
|
|
60
153
|
# @return [Hash<Symbol,Any>] Format: { ok: true/false, result: Symbol|Hash<Symbol,Any> }
|
|
61
154
|
#
|
|
62
155
|
# @api private
|
|
63
156
|
# @since 1.0.0
|
|
64
|
-
# @version 1.
|
|
157
|
+
# @version 1.20.0
|
|
65
158
|
# rubocop:disable Metrics/MethodLength
|
|
66
159
|
def try_to_lock(
|
|
67
160
|
redis,
|
|
@@ -73,15 +166,19 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
|
|
|
73
166
|
read_lock_key_queue,
|
|
74
167
|
lock_readers_key,
|
|
75
168
|
read_lock_key,
|
|
169
|
+
lock_name,
|
|
76
170
|
acquirer_id,
|
|
77
171
|
host_id,
|
|
78
172
|
acquirer_position,
|
|
79
173
|
ttl,
|
|
80
174
|
queue_ttl,
|
|
175
|
+
lock_index_enabled,
|
|
176
|
+
lock_index_grace_period,
|
|
81
177
|
fail_fast,
|
|
82
178
|
conflict_strategy,
|
|
83
179
|
access_strategy,
|
|
84
180
|
meta,
|
|
181
|
+
retry_attempt,
|
|
85
182
|
log_sampled,
|
|
86
183
|
instr_sampled
|
|
87
184
|
)
|
|
@@ -98,6 +195,23 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
|
|
|
98
195
|
held_rw_mode = read_write_mode
|
|
99
196
|
# Step RW: read lock request flag
|
|
100
197
|
read_mode = (read_write_mode == :read)
|
|
198
|
+
# Step RW: request queue of the current mode and the queue of the opposite mode
|
|
199
|
+
request_queue = read_mode ? read_lock_key_queue : lock_key_queue
|
|
200
|
+
opposite_request_queue = read_mode ? lock_key_queue : read_lock_key_queue
|
|
201
|
+
# Step 2.1 (prepare): requests queued before this score are expired (in both queues)
|
|
202
|
+
acquirer_dead_score = RedisQueuedLocks::Resource.acquirer_dead_score(queue_ttl)
|
|
203
|
+
# Step X: eager queueing - the lock request is queued in the lock state round trip (Step 0.1)
|
|
204
|
+
# instead of a separate round trip in the classic logic (Steps 1-3). Only when the attempt
|
|
205
|
+
# always reaches the classic logic or dequeues the request after the failure:
|
|
206
|
+
# - `:wait_for_lock`: same-process conflicts are processed in the classic way;
|
|
207
|
+
# - `:dead_locking`: same-process conflicts fail the acquirement (the request is dequeued);
|
|
208
|
+
# - not for `fail_fast` (the failed "no try" attempt does not dequeue the request) and not for
|
|
209
|
+
# `:work_through`/`:extendable_work_through` (same-process conflicts exit without queueing);
|
|
210
|
+
eager_queueing =
|
|
211
|
+
!fail_fast && (conflict_strategy == :wait_for_lock || conflict_strategy == :dead_locking)
|
|
212
|
+
# Step X: pre-check - the lock state is checked without WATCH first (see Step PRE): `fail_fast`
|
|
213
|
+
# attempts (they fail on the obtained lock) and retries (the lock was not obtained yet);
|
|
214
|
+
precheck = fail_fast || retry_attempt
|
|
101
215
|
|
|
102
216
|
LogVisitor.start(
|
|
103
217
|
logger, log_sampled, log_lock_try, lock_key,
|
|
@@ -111,6 +225,21 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
|
|
|
111
225
|
queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode
|
|
112
226
|
)
|
|
113
227
|
|
|
228
|
+
# Step PRE: (pre-check) the lock state without WATCH: the attempt that can not obtain
|
|
229
|
+
# the lock fails in one round trip (no WATCH, lock state and MULTI/EXEC round trips);
|
|
230
|
+
# - the pre-check never obtains the lock: the attempt that can obtain the lock continues
|
|
231
|
+
# with the WATCH/MULTI logic below (the lock state is read again after WATCH);
|
|
232
|
+
# - see `precheck_lock` for details;
|
|
233
|
+
if precheck
|
|
234
|
+
inter_result = precheck_lock(
|
|
235
|
+
rconn, logger, log_sampled, log_lock_try, lock_key, read_write_mode, lock_readers_key,
|
|
236
|
+
request_queue, opposite_request_queue, lock_name, acquirer_id, host_id,
|
|
237
|
+
acquirer_position, acquirer_dead_score, queue_ttl, lock_index_enabled,
|
|
238
|
+
lock_index_grace_period, fail_fast, access_strategy
|
|
239
|
+
)
|
|
240
|
+
next if inter_result
|
|
241
|
+
end
|
|
242
|
+
|
|
114
243
|
# Step 0:
|
|
115
244
|
# watch the lock key changes (and discard acquirement if lock is already
|
|
116
245
|
# obtained by another acquirer during the current lock acquiremntt)
|
|
@@ -124,23 +253,33 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
|
|
|
124
253
|
# - current write lock obtainer;
|
|
125
254
|
# - redis server time (read lock expiration is calculated in redis time);
|
|
126
255
|
# - read lock expiration of the current acquirer;
|
|
127
|
-
# - the longest read lock (the last one in the readers registry)
|
|
128
|
-
#
|
|
256
|
+
# - the longest read lock (the last one in the readers registry): write lock requests
|
|
257
|
+
# only (read lock requests do not wait for other readers);
|
|
258
|
+
# - (eager queueing) the lock request queueing: Steps 1, 2.1, 3 (`queue_lock_request`);
|
|
259
|
+
# NOTE: replies are read by their positions: see "pipeline layout" in `pipeline_lock_state`
|
|
260
|
+
# @type var lock_state: Array[untyped]
|
|
129
261
|
lock_state = rconn.pipelined do |pipeline|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
262
|
+
pipeline_lock_state(
|
|
263
|
+
pipeline, lock_key, lock_readers_key, acquirer_id, read_mode, eager_queueing,
|
|
264
|
+
request_queue, opposite_request_queue, acquirer_position, acquirer_dead_score,
|
|
265
|
+
lock_name, queue_ttl, lock_index_enabled, lock_index_grace_period
|
|
266
|
+
)
|
|
134
267
|
end
|
|
135
|
-
current_lock_obtainer
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
268
|
+
# @type var current_lock_obtainer: String?
|
|
269
|
+
current_lock_obtainer = lock_state[LOCK_STATE_OBTAINER_REPLY]
|
|
270
|
+
redis_time = RedisQueuedLocks::Resource.redis_time_ms(lock_state[LOCK_STATE_REDIS_TIME_REPLY])
|
|
271
|
+
# @type var own_read_lock_expiration: Float?
|
|
272
|
+
own_read_lock_expiration = lock_state[LOCK_STATE_OWN_READ_LOCK_REPLY]
|
|
273
|
+
# NOTE: the write-only tail is the last reply of write lock requests only (read lock
|
|
274
|
+
# requests have no such reply: never read it without the condition of the pipeline)
|
|
275
|
+
# @type var longest_read_lock: [String, Float]?
|
|
276
|
+
longest_read_lock = read_mode ? nil : lock_state.last.first
|
|
139
277
|
# NOTE:
|
|
140
278
|
# - expired read locks are ignored (they can live in the registry till the cleanup);
|
|
141
279
|
# - nil expiration (no read lock) is converted to 0.0 (always expired);
|
|
280
|
+
# - `read_locked` is used by write lock requests only (always false for read requests);
|
|
142
281
|
read_locked_by_acquirer = own_read_lock_expiration.to_f > redis_time
|
|
143
|
-
read_locked = longest_read_lock
|
|
282
|
+
read_locked = longest_read_lock ? longest_read_lock[1] > redis_time : false
|
|
144
283
|
|
|
145
284
|
# SP-Conflict status PREPARING: the mode of the lock (already obtained by the current
|
|
146
285
|
# acquirer) that the current lock request conflicts with;
|
|
@@ -254,6 +393,18 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
|
|
|
254
393
|
transact.call('PEXPIRE', read_lock_key, extended_read_lock_ttl, 'NX')
|
|
255
394
|
transact.call('PEXPIRE', read_lock_key, extended_read_lock_ttl, 'GT')
|
|
256
395
|
end
|
|
396
|
+
# SPCF Step 5: (lock index) the lock (write or read) lives longer by the extension:
|
|
397
|
+
# - the lock index score is increased by the extension (the upper bound of the lock
|
|
398
|
+
# lifetime stays the upper bound);
|
|
399
|
+
# - XX => locks without the index entry (of older RQL versions) are not indexed with
|
|
400
|
+
# a wrong score (see `reindex_locks`);
|
|
401
|
+
# - [REDIS RESULT]: the new lock index score (nil for the lock without the index entry);
|
|
402
|
+
# - only with the enabled lock index (the last command of the transaction);
|
|
403
|
+
if lock_index_enabled
|
|
404
|
+
transact.call(
|
|
405
|
+
'ZADD', RedisQueuedLocks::Resource::LOCK_INDEX_KEY, 'XX', 'INCR', ttl, lock_name
|
|
406
|
+
)
|
|
407
|
+
end
|
|
257
408
|
inter_result = :extendable_conflict_work_through
|
|
258
409
|
|
|
259
410
|
# @type var spc_processed_timestamp: Float
|
|
@@ -342,131 +493,65 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
|
|
|
342
493
|
# Fast-Step X1: lock is already obtained. fail fast leads to "no try".
|
|
343
494
|
inter_result = :fail_fast_no_try
|
|
344
495
|
else
|
|
345
|
-
# Step RW: request queue of the current mode and the queue of the opposite mode
|
|
346
|
-
request_queue = read_mode ? read_lock_key_queue : lock_key_queue
|
|
347
|
-
opposite_request_queue = read_mode ? lock_key_queue : read_lock_key_queue
|
|
348
|
-
|
|
349
496
|
# Step 1: add an acquirer to the lock acquirement queue
|
|
350
|
-
#
|
|
351
|
-
#
|
|
352
|
-
#
|
|
353
|
-
#
|
|
354
|
-
#
|
|
355
|
-
|
|
356
|
-
|
|
497
|
+
# Step 2.1: drop expired acquirers from the lock queues
|
|
498
|
+
# Step 3: get the actual acquirer waiting in the queue
|
|
499
|
+
# - one round trip for all of them (see `queue_lock_request`);
|
|
500
|
+
# - eager queueing: already done in the lock state round trip (Step 0.1);
|
|
501
|
+
# @type var queue_replies: Array[untyped]
|
|
502
|
+
queue_replies =
|
|
503
|
+
if eager_queueing
|
|
504
|
+
lock_state
|
|
505
|
+
else
|
|
506
|
+
rconn.pipelined do |pipeline|
|
|
507
|
+
queue_lock_request(
|
|
508
|
+
pipeline, request_queue, opposite_request_queue,
|
|
509
|
+
acquirer_position, acquirer_id, acquirer_dead_score,
|
|
510
|
+
lock_name, queue_ttl, lock_index_enabled, lock_index_grace_period
|
|
511
|
+
)
|
|
512
|
+
end
|
|
513
|
+
end
|
|
514
|
+
# NOTE: the position of the first queueing reply (see "pipeline layout" in Step 0.1)
|
|
515
|
+
queue_replies_at = eager_queueing ? LOCK_STATE_QUEUE_REPLIES_AT : 0
|
|
516
|
+
# @type var waiting_acquirer: String?
|
|
517
|
+
waiting_acquirer = queue_replies[queue_replies_at + QUEUE_HEAD_REPLY].first
|
|
518
|
+
# @type var opposite_waiting_request: [String, Float]?
|
|
519
|
+
opposite_waiting_request =
|
|
520
|
+
queue_replies[queue_replies_at + OPPOSITE_QUEUE_HEAD_REPLY].first
|
|
357
521
|
|
|
358
522
|
LogVisitor.acq_added_to_queue(
|
|
359
523
|
logger, log_sampled, log_lock_try, lock_key,
|
|
360
524
|
queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode
|
|
361
525
|
)
|
|
362
|
-
|
|
363
|
-
# Step 2.1: drop expired acquirers from the lock queues
|
|
364
|
-
# - (RW) both queues are cleared: an expired request of the opposite mode should not
|
|
365
|
-
# block the current request forever;
|
|
366
|
-
acquirer_dead_score = RedisQueuedLocks::Resource.acquirer_dead_score(queue_ttl)
|
|
367
|
-
rconn.pipelined do |pipeline|
|
|
368
|
-
pipeline.call('ZREMRANGEBYSCORE', request_queue, '-inf', acquirer_dead_score)
|
|
369
|
-
pipeline.call('ZREMRANGEBYSCORE', opposite_request_queue, '-inf', acquirer_dead_score)
|
|
370
|
-
end
|
|
371
|
-
|
|
372
526
|
LogVisitor.remove_expired_acqs(
|
|
373
527
|
logger, log_sampled, log_lock_try, lock_key,
|
|
374
528
|
queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode
|
|
375
529
|
)
|
|
376
|
-
|
|
377
|
-
# Step 3: get the actual acquirer waiting in the queue
|
|
378
|
-
# - (RW) and the first request of the opposite mode;
|
|
379
|
-
# @type var queue_heads: [Array[String], Array[[String, Float]]]
|
|
380
|
-
queue_heads = rconn.pipelined do |pipeline|
|
|
381
|
-
pipeline.call('ZRANGE', request_queue, '0', '0')
|
|
382
|
-
pipeline.call('ZRANGE', opposite_request_queue, '0', '0', 'WITHSCORES')
|
|
383
|
-
end
|
|
384
|
-
waiting_acquirer = queue_heads[0].first
|
|
385
|
-
opposite_waiting_request = queue_heads[1].first
|
|
386
|
-
|
|
387
530
|
LogVisitor.get_first_from_queue(
|
|
388
531
|
logger, log_sampled, log_lock_try, lock_key,
|
|
389
532
|
queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode, waiting_acquirer
|
|
390
533
|
)
|
|
391
534
|
|
|
392
|
-
# Step 3.1 (RW)
|
|
393
|
-
#
|
|
394
|
-
#
|
|
535
|
+
# Step 3.1 (RW) + Steps PRE-4.x, ROLLBACK 1.1, 1.2, 2, 3: the obstacle to the lock
|
|
536
|
+
# obtaining (the same decision and logs as in the pre-check: `lock_obstacle`,
|
|
537
|
+
# `log_lock_obstacle`);
|
|
395
538
|
# @type var opposite_request_ahead: String?
|
|
396
|
-
opposite_request_ahead =
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
# Step PRE-4.x: check if the request time limit is reached
|
|
405
|
-
# (when the current try self-removes itself from queue (queue ttl has come))
|
|
406
|
-
if waiting_acquirer == nil
|
|
407
|
-
LogVisitor.exit__queue_ttl_reached(
|
|
408
|
-
logger, log_sampled, log_lock_try, lock_key,
|
|
409
|
-
queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode
|
|
410
|
-
)
|
|
411
|
-
|
|
412
|
-
inter_result = :dead_score_reached
|
|
413
|
-
# Step STRATEGY: check the stragegy and corresponding preventing factor
|
|
414
|
-
# Step STRATEGY (queued): check the actual acquirer: is it ours? are we aready to lock?
|
|
415
|
-
# - (RW) write lock requests are processed one by one;
|
|
416
|
-
# - (RW) read lock requests are processed in parallel;
|
|
417
|
-
elsif !read_mode && access_strategy == :queued && waiting_acquirer != acquirer_id
|
|
418
|
-
# Step ROLLBACK 1.1: our time hasn't come yet. retry!
|
|
539
|
+
opposite_request_ahead = find_opposite_request_ahead(
|
|
540
|
+
access_strategy, opposite_waiting_request, acquirer_position, acquirer_id
|
|
541
|
+
)
|
|
542
|
+
obstacle = lock_obstacle(
|
|
543
|
+
read_mode, access_strategy, acquirer_id, waiting_acquirer, opposite_request_ahead,
|
|
544
|
+
current_lock_obtainer, (read_mode ? read_locked_by_acquirer : read_locked)
|
|
545
|
+
)
|
|
419
546
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
inter_result = :acquirer_is_not_first_in_queue
|
|
427
|
-
elsif opposite_request_ahead
|
|
428
|
-
# Step ROLLBACK 1.2 (RW): an earlier request of the opposite mode goes first. retry!
|
|
429
|
-
if read_mode
|
|
430
|
-
LogVisitor.exit__write_request_ahead(
|
|
431
|
-
logger, log_sampled, log_lock_try, lock_key,
|
|
432
|
-
queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode,
|
|
433
|
-
opposite_request_ahead
|
|
434
|
-
)
|
|
435
|
-
inter_result = :write_request_is_ahead
|
|
436
|
-
else
|
|
437
|
-
LogVisitor.exit__read_request_ahead(
|
|
438
|
-
logger, log_sampled, log_lock_try, lock_key,
|
|
439
|
-
queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode,
|
|
440
|
-
opposite_request_ahead
|
|
441
|
-
)
|
|
442
|
-
inter_result = :read_request_is_ahead
|
|
443
|
-
end
|
|
444
|
-
# NOTE: our time has come! let's try to acquire the lock!
|
|
445
|
-
# Step 5: find the lock -> check if the our lock is already acquired
|
|
446
|
-
# - NOTE: the lock state is fetched after WATCH (step 0.1) so any lock state change
|
|
447
|
-
# invalidates the transaction;
|
|
448
|
-
elsif current_lock_obtainer
|
|
449
|
-
# Step ROLLBACK 2: required lock is stil acquired. retry!
|
|
450
|
-
|
|
451
|
-
LogVisitor.exit__lock_still_obtained(
|
|
452
|
-
logger, log_sampled, log_lock_try, lock_key,
|
|
453
|
-
queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode,
|
|
454
|
-
waiting_acquirer, current_lock_obtainer,
|
|
455
|
-
# NOTE: lock data is extracted for logs only
|
|
456
|
-
((log_sampled && log_lock_try) ? rconn.call('HGETALL', lock_key).to_h : {})
|
|
547
|
+
if obstacle
|
|
548
|
+
# Step ROLLBACK: the lock can not be obtained now. retry!
|
|
549
|
+
log_lock_obstacle(
|
|
550
|
+
rconn, obstacle, logger, log_sampled, log_lock_try, lock_key, queue_ttl,
|
|
551
|
+
acquirer_id, host_id, access_strategy, read_write_mode,
|
|
552
|
+
waiting_acquirer, opposite_request_ahead, current_lock_obtainer
|
|
457
553
|
)
|
|
458
|
-
inter_result =
|
|
459
|
-
elsif read_mode ? read_locked_by_acquirer : read_locked
|
|
460
|
-
# Step ROLLBACK 3 (RW): the lock is still obtained by readers. retry!
|
|
461
|
-
# - write lock request waits for all readers;
|
|
462
|
-
# - read lock request waits for its own read lock
|
|
463
|
-
# (`:wait_for_lock` conflict strategy);
|
|
464
|
-
|
|
465
|
-
LogVisitor.exit__read_lock_still_obtained(
|
|
466
|
-
logger, log_sampled, log_lock_try, lock_key,
|
|
467
|
-
queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode, waiting_acquirer
|
|
468
|
-
)
|
|
469
|
-
inter_result = :read_lock_is_still_acquired
|
|
554
|
+
inter_result = obstacle
|
|
470
555
|
elsif read_mode
|
|
471
556
|
# NOTE: required lock is free and ready to be acquired! acquire! (read lock)
|
|
472
557
|
|
|
@@ -485,21 +570,35 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
|
|
|
485
570
|
transact.call('PEXPIRE', lock_readers_key, ttl, 'NX')
|
|
486
571
|
transact.call('PEXPIRE', lock_readers_key, ttl, 'GT')
|
|
487
572
|
|
|
488
|
-
# Step 6.5 (RW): store the read lock data (the same structure as the write lock data
|
|
573
|
+
# Step 6.5 (RW): store the read lock data (the same structure as the write lock data
|
|
574
|
+
# without `acq_id`/`hst_id`: both are derived from the read lock acquirer id that is
|
|
575
|
+
# a part of the data key and the registry member, so the info adds them on formatting)
|
|
489
576
|
# - drop the data of the expired read lock of the current acquirer (if any);
|
|
490
577
|
# - the read lock data lives as long as the read lock;
|
|
491
578
|
transact.call('DEL', read_lock_key)
|
|
492
579
|
transact.call(
|
|
493
580
|
'HSET',
|
|
494
581
|
read_lock_key,
|
|
495
|
-
'acq_id', acquirer_id,
|
|
496
|
-
'hst_id', host_id,
|
|
497
582
|
'ts', timestamp = Time.now.to_f,
|
|
498
583
|
'ini_ttl', ttl,
|
|
499
584
|
*(meta.to_a if meta != nil) # steep:ignore
|
|
500
585
|
)
|
|
501
586
|
transact.call('PEXPIRE', read_lock_key, ttl)
|
|
502
587
|
|
|
588
|
+
# Step 6.6 (lock index): the lock name lives in the lock index as long as the read lock
|
|
589
|
+
# (the upper bound of the read lock lifetime: client time + ttl + grace period);
|
|
590
|
+
# - in the same transaction: an index entry dropped between the queueing and this
|
|
591
|
+
# transaction (`#unlock`, `#clear_locks`) is restored with the read lock;
|
|
592
|
+
# - GT => the score is never decreased (other keys of the lock can live longer);
|
|
593
|
+
# - only with the enabled lock index (the last command of the transaction);
|
|
594
|
+
if lock_index_enabled
|
|
595
|
+
transact.call(
|
|
596
|
+
'ZADD', RedisQueuedLocks::Resource::LOCK_INDEX_KEY, 'GT',
|
|
597
|
+
RedisQueuedLocks::Resource.lock_index_time + ttl.to_i + lock_index_grace_period,
|
|
598
|
+
lock_name
|
|
599
|
+
)
|
|
600
|
+
end
|
|
601
|
+
|
|
503
602
|
inter_result = :lock_obtaining
|
|
504
603
|
|
|
505
604
|
LogVisitor.obtain__free_to_acquire(
|
|
@@ -526,6 +625,20 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
|
|
|
526
625
|
# Step 6.3: set the lock expiration time in order to prevent "infinite locks"
|
|
527
626
|
transact.call('PEXPIRE', lock_key, ttl) # NOTE: in milliseconds
|
|
528
627
|
|
|
628
|
+
# Step 6.4 (lock index): the lock name lives in the lock index as long as the lock
|
|
629
|
+
# (the upper bound of the lock lifetime: client time + ttl + grace period);
|
|
630
|
+
# - in the same transaction: an index entry dropped between the queueing and this
|
|
631
|
+
# transaction (`#unlock`, `#clear_locks`) is restored with the lock;
|
|
632
|
+
# - GT => the score is never decreased (other keys of the lock can live longer);
|
|
633
|
+
# - only with the enabled lock index (the last command of the transaction);
|
|
634
|
+
if lock_index_enabled
|
|
635
|
+
transact.call(
|
|
636
|
+
'ZADD', RedisQueuedLocks::Resource::LOCK_INDEX_KEY, 'GT',
|
|
637
|
+
RedisQueuedLocks::Resource.lock_index_time + ttl.to_i + lock_index_grace_period,
|
|
638
|
+
lock_name
|
|
639
|
+
)
|
|
640
|
+
end
|
|
641
|
+
|
|
529
642
|
inter_result = :lock_obtaining
|
|
530
643
|
|
|
531
644
|
LogVisitor.obtain__free_to_acquire(
|
|
@@ -552,6 +665,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
|
|
|
552
665
|
# 1. ZADD XX INCR (new read lock expiration) (OK for != nil)
|
|
553
666
|
# 2. PEXPIRE NX/GT (registry TTL)
|
|
554
667
|
# 3. HINCRBY/HSET/PEXPIRE (read lock data)
|
|
668
|
+
# 4. ZADD XX INCR (the lock index score; the enabled lock index only)
|
|
555
669
|
if result[0] != nil
|
|
556
670
|
{
|
|
557
671
|
ok: true,
|
|
@@ -569,13 +683,16 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
|
|
|
569
683
|
# NOTE: the read lock is expired and removed from the registry during the extension
|
|
570
684
|
{ ok: false, result: :read_lock_is_expired_during_extension }
|
|
571
685
|
end
|
|
572
|
-
elsif result.is_a?(::Array) && result.size ==
|
|
686
|
+
elsif result.is_a?(::Array) && result.size == (lock_index_enabled ? 5 : 4)
|
|
687
|
+
# NOTE: four commands should be processed (five with the enabled lock index)
|
|
573
688
|
# TODO:
|
|
574
689
|
# => (!) analyze the command result and do actions with the depending on it
|
|
575
690
|
# 1. EVAL (extend lock pttl) (OK for != nil)
|
|
576
691
|
# 2. HINCRBY (ttl extension) (OK for != nil)
|
|
577
692
|
# 3. HINCRBY (increased spc count) (OK for != nil)
|
|
578
693
|
# 4. HSET (store the last spc time and ttl data) (OK for == 2 or != nil)
|
|
694
|
+
# 5. ZADD XX INCR (the lock index score; the enabled lock index only) (nil for the lock
|
|
695
|
+
# without the index entry: it does not affect the extension)
|
|
579
696
|
if result[0] != nil && result[1] != nil && result[2] != nil && result[3] != nil
|
|
580
697
|
{
|
|
581
698
|
ok: true,
|
|
@@ -698,5 +815,444 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
|
|
|
698
815
|
# rubocop:enable Lint/DuplicateBranch
|
|
699
816
|
end
|
|
700
817
|
# rubocop:enable Metrics/MethodLength
|
|
818
|
+
|
|
819
|
+
private
|
|
820
|
+
|
|
821
|
+
# Queues the lock request and reads the lock queue state (Steps 1, 2.1, 3 and 3.2-3.4 of
|
|
822
|
+
# the attempt): the commands are added to the given pipeline (one round trip with other
|
|
823
|
+
# pipelined commands).
|
|
824
|
+
# Replies (positions relative to the first one): [0] ZADD NX, [1] ZREMRANGEBYSCORE (request
|
|
825
|
+
# queue), [2] ZREMRANGEBYSCORE (opposite request queue), [QUEUE_HEAD_REPLY] ZRANGE (the first
|
|
826
|
+
# request of the request queue), [OPPOSITE_QUEUE_HEAD_REPLY] ZRANGE WITHSCORES (the first
|
|
827
|
+
# request of the opposite request queue); the enabled lock index only: [5] PEXPIRE NX, [6]
|
|
828
|
+
# PEXPIRE GT (the request queue TTL), [7] ZADD GT (the lock index), [8] ZREMRANGEBYSCORE
|
|
829
|
+
# (expired lock names of the lock index). Update these constants when the commands change
|
|
830
|
+
# (new commands go to the end: the replies are not read after `OPPOSITE_QUEUE_HEAD_REPLY`, so
|
|
831
|
+
# the conditional lock index commands do not shift the read replies).
|
|
832
|
+
#
|
|
833
|
+
# @param pipeline [RedisClient::Pipeline]
|
|
834
|
+
# @param request_queue [String] Queue of the requests of the requested lock mode.
|
|
835
|
+
# @param opposite_request_queue [String] Queue of the requests of the opposite lock mode.
|
|
836
|
+
# @param acquirer_position [Numeric]
|
|
837
|
+
# @param acquirer_id [String]
|
|
838
|
+
# @param acquirer_dead_score [Float] Requests queued before this score are expired.
|
|
839
|
+
# @param lock_name [String] The lock name (the member of the lock index).
|
|
840
|
+
# @param queue_ttl [Integer] Lifetime of the lock request (in seconds).
|
|
841
|
+
# @param lock_index_enabled [Boolean] Send the lock index commands (Steps 3.2-3.4).
|
|
842
|
+
# @param lock_index_grace_period [Integer] In milliseconds.
|
|
843
|
+
# @return [void]
|
|
844
|
+
#
|
|
845
|
+
# @api private
|
|
846
|
+
# @since 1.20.0
|
|
847
|
+
def queue_lock_request(
|
|
848
|
+
pipeline,
|
|
849
|
+
request_queue,
|
|
850
|
+
opposite_request_queue,
|
|
851
|
+
acquirer_position,
|
|
852
|
+
acquirer_id,
|
|
853
|
+
acquirer_dead_score,
|
|
854
|
+
lock_name,
|
|
855
|
+
queue_ttl,
|
|
856
|
+
lock_index_enabled,
|
|
857
|
+
lock_index_grace_period
|
|
858
|
+
)
|
|
859
|
+
# Step 1: add an acquirer to the lock acquirement queue
|
|
860
|
+
# NOTE:
|
|
861
|
+
# 'NX' means "Only add new elements. Don't update already existing elements."
|
|
862
|
+
# that works as:
|
|
863
|
+
# 1. (enqueue) <<if you are already in the queue - do nothing and wait your time>>
|
|
864
|
+
# 2. (requeue) or <<add to the right pre-calculated position if you are
|
|
865
|
+
# not in the queue now>>;
|
|
866
|
+
pipeline.call('ZADD', request_queue, 'NX', acquirer_position, acquirer_id)
|
|
867
|
+
|
|
868
|
+
# Step 2.1: drop expired acquirers from the lock queues
|
|
869
|
+
# - (RW) both queues are cleared: an expired request of the opposite mode should not
|
|
870
|
+
# block the current request forever;
|
|
871
|
+
pipeline.call('ZREMRANGEBYSCORE', request_queue, '-inf', acquirer_dead_score)
|
|
872
|
+
pipeline.call('ZREMRANGEBYSCORE', opposite_request_queue, '-inf', acquirer_dead_score)
|
|
873
|
+
|
|
874
|
+
# Step 3: get the actual acquirer waiting in the queue
|
|
875
|
+
# - (RW) and the first request of the opposite mode;
|
|
876
|
+
pipeline.call('ZRANGE', request_queue, '0', '0')
|
|
877
|
+
pipeline.call('ZRANGE', opposite_request_queue, '0', '0', 'WITHSCORES')
|
|
878
|
+
|
|
879
|
+
# NOTE: the disabled lock index: no lock index commands, lock request queues have no TTL
|
|
880
|
+
# (lock-iterating operations scan the keyspace);
|
|
881
|
+
return unless lock_index_enabled
|
|
882
|
+
|
|
883
|
+
# Step 3.2: (lock index) the request queue lives as long as the latest lock request
|
|
884
|
+
# - NX + GT => sets the queue TTL to max(current TTL, queue ttl): waiting acquirers refresh
|
|
885
|
+
# it on each attempt, the queue of dead acquirers expires (no "infinite queues");
|
|
886
|
+
# - queues order the lock requests only: a live acquirer whose request is dropped with
|
|
887
|
+
# the expired queue re-queues it on its next attempt (`ZADD NX` with its position);
|
|
888
|
+
# NOTE: `ceil` - queue ttl can be passed as a Float (in seconds), PEXPIRE requires Integer
|
|
889
|
+
queue_pttl = (queue_ttl * 1_000).ceil
|
|
890
|
+
pipeline.call('PEXPIRE', request_queue, queue_pttl, 'NX')
|
|
891
|
+
pipeline.call('PEXPIRE', request_queue, queue_pttl, 'GT')
|
|
892
|
+
|
|
893
|
+
# Step 3.3: (lock index) the lock name lives in the lock index as long as the request queue
|
|
894
|
+
# - the score is the upper bound of the queue lifetime: client time + queue ttl + grace;
|
|
895
|
+
# (the lock obtained by the attempt is indexed by its own transaction: Step 6.4/6.6);
|
|
896
|
+
# - GT => the score is never decreased (other keys of the lock can live longer);
|
|
897
|
+
# - after the queue command: an index entry dropping between the queue command and
|
|
898
|
+
# the index command (pruning, `#unlock`, `#clear_locks`) can not leave the queue without
|
|
899
|
+
# the index entry;
|
|
900
|
+
lock_index_time = RedisQueuedLocks::Resource.lock_index_time
|
|
901
|
+
pipeline.call(
|
|
902
|
+
'ZADD', RedisQueuedLocks::Resource::LOCK_INDEX_KEY, 'GT',
|
|
903
|
+
lock_index_time + queue_pttl + lock_index_grace_period, lock_name
|
|
904
|
+
)
|
|
905
|
+
|
|
906
|
+
# Step 3.4: (lock index) drop the expired lock names from the lock index: all the keys of
|
|
907
|
+
# these locks are expired (amortized cleanup: no background jobs are required);
|
|
908
|
+
pipeline.call(
|
|
909
|
+
'ZREMRANGEBYSCORE', RedisQueuedLocks::Resource::LOCK_INDEX_KEY, '-inf', lock_index_time - 1
|
|
910
|
+
)
|
|
911
|
+
end
|
|
912
|
+
|
|
913
|
+
# Adds the lock state commands to the given pipeline (the lock state of the attempt and of
|
|
914
|
+
# the pre-check: one round trip).
|
|
915
|
+
#
|
|
916
|
+
# NOTE: (pipeline layout) replies are read by their positions in the pipeline:
|
|
917
|
+
# - [LOCK_STATE_OBTAINER_REPLY] HGET, [LOCK_STATE_REDIS_TIME_REPLY] TIME,
|
|
918
|
+
# [LOCK_STATE_OWN_READ_LOCK_REPLY] ZSCORE - all requests (the head of the pipeline);
|
|
919
|
+
# - [LOCK_STATE_QUEUE_REPLIES_AT, +5] lock request queueing (`queue_lock_request`: the request
|
|
920
|
+
# queue commands; +9 with the lock index commands of the enabled lock index) - only with
|
|
921
|
+
# `queueing` (eager queueing, pre-check of retries);
|
|
922
|
+
# - the last reply: ZRANGE (the longest read lock) - write lock requests only (the "write-only
|
|
923
|
+
# tail" of the pipeline: it is read by `lock_state.last` under the same condition);
|
|
924
|
+
# - conditional commands go after the head (the queueing commands, then the write-only
|
|
925
|
+
# tail); the write-only tail must stay the last one: read lock requests do not send it,
|
|
926
|
+
# so a command added after it would get another position for read lock requests
|
|
927
|
+
# (and its reply would be read as the reply of another command);
|
|
928
|
+
# - new unconditional commands go into the head: update the position constants, the reply
|
|
929
|
+
# reading (`try_to_lock` Step 0.1 and `precheck_lock`) and the spec "lock state pipeline
|
|
930
|
+
# layout" (it checks the layout of both modes, with and without the queueing);
|
|
931
|
+
#
|
|
932
|
+
# @param pipeline [RedisClient::Pipeline]
|
|
933
|
+
# @param lock_key [String]
|
|
934
|
+
# @param lock_readers_key [String]
|
|
935
|
+
# @param acquirer_id [String]
|
|
936
|
+
# @param read_mode [Boolean]
|
|
937
|
+
# @param queueing [Boolean] Queue the lock request in the same round trip.
|
|
938
|
+
# @param request_queue [String]
|
|
939
|
+
# @param opposite_request_queue [String]
|
|
940
|
+
# @param acquirer_position [Numeric]
|
|
941
|
+
# @param acquirer_dead_score [Float]
|
|
942
|
+
# @param lock_name [String] The lock name (the member of the lock index).
|
|
943
|
+
# @param queue_ttl [Integer] In seconds.
|
|
944
|
+
# @param lock_index_enabled [Boolean]
|
|
945
|
+
# @param lock_index_grace_period [Integer] In milliseconds.
|
|
946
|
+
# @return [void]
|
|
947
|
+
#
|
|
948
|
+
# @api private
|
|
949
|
+
# @since 1.20.0
|
|
950
|
+
def pipeline_lock_state(
|
|
951
|
+
pipeline,
|
|
952
|
+
lock_key,
|
|
953
|
+
lock_readers_key,
|
|
954
|
+
acquirer_id,
|
|
955
|
+
read_mode,
|
|
956
|
+
queueing,
|
|
957
|
+
request_queue,
|
|
958
|
+
opposite_request_queue,
|
|
959
|
+
acquirer_position,
|
|
960
|
+
acquirer_dead_score,
|
|
961
|
+
lock_name,
|
|
962
|
+
queue_ttl,
|
|
963
|
+
lock_index_enabled,
|
|
964
|
+
lock_index_grace_period
|
|
965
|
+
)
|
|
966
|
+
pipeline.call('HGET', lock_key, 'acq_id')
|
|
967
|
+
pipeline.call('TIME')
|
|
968
|
+
pipeline.call('ZSCORE', lock_readers_key, acquirer_id)
|
|
969
|
+
# NOTE: the queueing commands (see "pipeline layout" above)
|
|
970
|
+
if queueing
|
|
971
|
+
queue_lock_request(
|
|
972
|
+
pipeline, request_queue, opposite_request_queue,
|
|
973
|
+
acquirer_position, acquirer_id, acquirer_dead_score,
|
|
974
|
+
lock_name, queue_ttl, lock_index_enabled, lock_index_grace_period
|
|
975
|
+
)
|
|
976
|
+
end
|
|
977
|
+
# NOTE: the write-only tail (keep it the last: see "pipeline layout" above)
|
|
978
|
+
pipeline.call('ZRANGE', lock_readers_key, '-1', '-1', 'WITHSCORES') unless read_mode
|
|
979
|
+
end
|
|
980
|
+
|
|
981
|
+
# (RW) The request of the opposite mode that should obtain the lock earlier
|
|
982
|
+
# - FIFO between modes (`:queued` access strategy only);
|
|
983
|
+
# - equal positions are ordered by acquirer ids;
|
|
984
|
+
#
|
|
985
|
+
# @param access_strategy [Symbol]
|
|
986
|
+
# @param opposite_waiting_request [Array<String,Float>,NilClass]
|
|
987
|
+
# The first request of the opposite request queue (acquirer id, position).
|
|
988
|
+
# @param acquirer_position [Numeric]
|
|
989
|
+
# @param acquirer_id [String]
|
|
990
|
+
# @return [String,NilClass] The acquirer of the earlier request of the opposite mode.
|
|
991
|
+
#
|
|
992
|
+
# @api private
|
|
993
|
+
# @since 1.20.0
|
|
994
|
+
def find_opposite_request_ahead(
|
|
995
|
+
access_strategy,
|
|
996
|
+
opposite_waiting_request,
|
|
997
|
+
acquirer_position,
|
|
998
|
+
acquirer_id
|
|
999
|
+
)
|
|
1000
|
+
return unless access_strategy == :queued && opposite_waiting_request
|
|
1001
|
+
|
|
1002
|
+
if opposite_waiting_request[1] < acquirer_position ||
|
|
1003
|
+
(opposite_waiting_request[1] == acquirer_position &&
|
|
1004
|
+
opposite_waiting_request[0] < acquirer_id)
|
|
1005
|
+
opposite_waiting_request[0]
|
|
1006
|
+
end
|
|
1007
|
+
end
|
|
1008
|
+
|
|
1009
|
+
# The obstacle to the lock obtaining for the classic (queued) logic of the attempt
|
|
1010
|
+
# (the same decision for the attempt and for its pre-check).
|
|
1011
|
+
#
|
|
1012
|
+
# @param read_mode [Boolean]
|
|
1013
|
+
# @param access_strategy [Symbol]
|
|
1014
|
+
# @param acquirer_id [String]
|
|
1015
|
+
# @param waiting_acquirer [String,NilClass] The first request of the request queue.
|
|
1016
|
+
# @param opposite_request_ahead [String,NilClass] See `find_opposite_request_ahead`.
|
|
1017
|
+
# @param current_lock_obtainer [String,NilClass] The write lock obtainer.
|
|
1018
|
+
# @param read_locked [Boolean]
|
|
1019
|
+
# Write lock requests: live read locks exist; read lock requests: the own read lock is live.
|
|
1020
|
+
# @return [Symbol,NilClass] The obstacle (the failure result of the attempt) or `nil`.
|
|
1021
|
+
#
|
|
1022
|
+
# @api private
|
|
1023
|
+
# @since 1.20.0
|
|
1024
|
+
def lock_obstacle(
|
|
1025
|
+
read_mode,
|
|
1026
|
+
access_strategy,
|
|
1027
|
+
acquirer_id,
|
|
1028
|
+
waiting_acquirer,
|
|
1029
|
+
opposite_request_ahead,
|
|
1030
|
+
current_lock_obtainer,
|
|
1031
|
+
read_locked
|
|
1032
|
+
)
|
|
1033
|
+
# Step PRE-4.x: check if the request time limit is reached
|
|
1034
|
+
# (when the current try self-removes itself from queue (queue ttl has come))
|
|
1035
|
+
if waiting_acquirer == nil
|
|
1036
|
+
:dead_score_reached
|
|
1037
|
+
# Step STRATEGY (queued): check the actual acquirer: is it ours? are we aready to lock?
|
|
1038
|
+
# - (RW) write lock requests are processed one by one;
|
|
1039
|
+
# - (RW) read lock requests are processed in parallel;
|
|
1040
|
+
elsif !read_mode && access_strategy == :queued && waiting_acquirer != acquirer_id
|
|
1041
|
+
# Step ROLLBACK 1.1: our time hasn't come yet. retry!
|
|
1042
|
+
:acquirer_is_not_first_in_queue
|
|
1043
|
+
elsif opposite_request_ahead
|
|
1044
|
+
# Step ROLLBACK 1.2 (RW): an earlier request of the opposite mode goes first. retry!
|
|
1045
|
+
read_mode ? :write_request_is_ahead : :read_request_is_ahead
|
|
1046
|
+
# Step 5: find the lock -> check if the our lock is already acquired
|
|
1047
|
+
# - NOTE: the attempt fetches the lock state after WATCH (step 0.1) so any lock state change
|
|
1048
|
+
# invalidates its transaction;
|
|
1049
|
+
elsif current_lock_obtainer
|
|
1050
|
+
# Step ROLLBACK 2: required lock is stil acquired. retry!
|
|
1051
|
+
:lock_is_still_acquired
|
|
1052
|
+
elsif read_locked
|
|
1053
|
+
# Step ROLLBACK 3 (RW): the lock is still obtained by readers. retry!
|
|
1054
|
+
# - write lock request waits for all readers;
|
|
1055
|
+
# - read lock request waits for its own read lock (`:wait_for_lock` conflict strategy);
|
|
1056
|
+
:read_lock_is_still_acquired
|
|
1057
|
+
end
|
|
1058
|
+
end
|
|
1059
|
+
|
|
1060
|
+
# Logs the obstacle to the lock obtaining (see `lock_obstacle`): the exit event of the attempt.
|
|
1061
|
+
#
|
|
1062
|
+
# @param rconn [RedisClient]
|
|
1063
|
+
# @param obstacle [Symbol]
|
|
1064
|
+
# @param logger [::Logger,#debug]
|
|
1065
|
+
# @param log_sampled [Boolean]
|
|
1066
|
+
# @param log_lock_try [Boolean]
|
|
1067
|
+
# @param lock_key [String]
|
|
1068
|
+
# @param queue_ttl [Integer]
|
|
1069
|
+
# @param acquirer_id [String]
|
|
1070
|
+
# @param host_id [String]
|
|
1071
|
+
# @param access_strategy [Symbol]
|
|
1072
|
+
# @param read_write_mode [Symbol]
|
|
1073
|
+
# @param waiting_acquirer [String,NilClass]
|
|
1074
|
+
# @param opposite_request_ahead [String,NilClass]
|
|
1075
|
+
# @param current_lock_obtainer [String,NilClass]
|
|
1076
|
+
# @return [void]
|
|
1077
|
+
#
|
|
1078
|
+
# @api private
|
|
1079
|
+
# @since 1.20.0
|
|
1080
|
+
# rubocop:disable Metrics/MethodLength
|
|
1081
|
+
def log_lock_obstacle(
|
|
1082
|
+
rconn,
|
|
1083
|
+
obstacle,
|
|
1084
|
+
logger,
|
|
1085
|
+
log_sampled,
|
|
1086
|
+
log_lock_try,
|
|
1087
|
+
lock_key,
|
|
1088
|
+
queue_ttl,
|
|
1089
|
+
acquirer_id,
|
|
1090
|
+
host_id,
|
|
1091
|
+
access_strategy,
|
|
1092
|
+
read_write_mode,
|
|
1093
|
+
waiting_acquirer,
|
|
1094
|
+
opposite_request_ahead,
|
|
1095
|
+
current_lock_obtainer
|
|
1096
|
+
)
|
|
1097
|
+
case obstacle
|
|
1098
|
+
when :dead_score_reached
|
|
1099
|
+
LogVisitor.exit__queue_ttl_reached(
|
|
1100
|
+
logger, log_sampled, log_lock_try, lock_key,
|
|
1101
|
+
queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode
|
|
1102
|
+
)
|
|
1103
|
+
when :acquirer_is_not_first_in_queue
|
|
1104
|
+
LogVisitor.exit__no_first(
|
|
1105
|
+
logger, log_sampled, log_lock_try, lock_key,
|
|
1106
|
+
queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode, waiting_acquirer,
|
|
1107
|
+
# NOTE: lock data is extracted for logs only
|
|
1108
|
+
((log_sampled && log_lock_try) ? rconn.call('HGETALL', lock_key).to_h : NO_LOCK_DATA)
|
|
1109
|
+
)
|
|
1110
|
+
when :write_request_is_ahead
|
|
1111
|
+
LogVisitor.exit__write_request_ahead(
|
|
1112
|
+
logger, log_sampled, log_lock_try, lock_key,
|
|
1113
|
+
queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode,
|
|
1114
|
+
opposite_request_ahead #: String
|
|
1115
|
+
)
|
|
1116
|
+
when :read_request_is_ahead
|
|
1117
|
+
LogVisitor.exit__read_request_ahead(
|
|
1118
|
+
logger, log_sampled, log_lock_try, lock_key,
|
|
1119
|
+
queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode,
|
|
1120
|
+
opposite_request_ahead #: String
|
|
1121
|
+
)
|
|
1122
|
+
when :lock_is_still_acquired
|
|
1123
|
+
LogVisitor.exit__lock_still_obtained(
|
|
1124
|
+
logger, log_sampled, log_lock_try, lock_key,
|
|
1125
|
+
queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode,
|
|
1126
|
+
waiting_acquirer, current_lock_obtainer, #: String
|
|
1127
|
+
# NOTE: lock data is extracted for logs only
|
|
1128
|
+
((log_sampled && log_lock_try) ? rconn.call('HGETALL', lock_key).to_h : NO_LOCK_DATA)
|
|
1129
|
+
)
|
|
1130
|
+
when :read_lock_is_still_acquired
|
|
1131
|
+
LogVisitor.exit__read_lock_still_obtained(
|
|
1132
|
+
logger, log_sampled, log_lock_try, lock_key,
|
|
1133
|
+
queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode, waiting_acquirer
|
|
1134
|
+
)
|
|
1135
|
+
end
|
|
1136
|
+
end
|
|
1137
|
+
# rubocop:enable Metrics/MethodLength
|
|
1138
|
+
|
|
1139
|
+
# Pre-check of the attempt (Step PRE): the lock state without WATCH (one round trip).
|
|
1140
|
+
# - `fail_fast` attempts: the lock state only (the request is not queued: the failed "no try"
|
|
1141
|
+
# attempt does not dequeue the request); fails with `:fail_fast_no_try` on the obtained lock;
|
|
1142
|
+
# - retries: the lock state and the lock request queueing (the request is already queued by
|
|
1143
|
+
# the previous attempts); fails with the obstacle of the classic logic (`lock_obstacle`)
|
|
1144
|
+
# and logs the same events as the failed attempt;
|
|
1145
|
+
# - same-process conflicts (the lock is obtained by the current acquirer) and locks that can be
|
|
1146
|
+
# obtained are not decided: the attempt continues with the WATCH/MULTI logic (`nil`);
|
|
1147
|
+
# - the pre-check never obtains the lock (mutual exclusion relies on WATCH/MULTI only);
|
|
1148
|
+
#
|
|
1149
|
+
# @param rconn [RedisClient]
|
|
1150
|
+
# @param logger [::Logger,#debug]
|
|
1151
|
+
# @param log_sampled [Boolean]
|
|
1152
|
+
# @param log_lock_try [Boolean]
|
|
1153
|
+
# @param lock_key [String]
|
|
1154
|
+
# @param read_write_mode [Symbol]
|
|
1155
|
+
# @param lock_readers_key [String]
|
|
1156
|
+
# @param request_queue [String]
|
|
1157
|
+
# @param opposite_request_queue [String]
|
|
1158
|
+
# @param lock_name [String] The lock name (the member of the lock index).
|
|
1159
|
+
# @param acquirer_id [String]
|
|
1160
|
+
# @param host_id [String]
|
|
1161
|
+
# @param acquirer_position [Numeric]
|
|
1162
|
+
# @param acquirer_dead_score [Float]
|
|
1163
|
+
# @param queue_ttl [Integer]
|
|
1164
|
+
# @param lock_index_enabled [Boolean]
|
|
1165
|
+
# @param lock_index_grace_period [Integer] In milliseconds.
|
|
1166
|
+
# @param fail_fast [Boolean]
|
|
1167
|
+
# @param access_strategy [Symbol]
|
|
1168
|
+
# @return [Symbol,NilClass] The failure result of the attempt or `nil` (continue the attempt).
|
|
1169
|
+
#
|
|
1170
|
+
# @api private
|
|
1171
|
+
# @since 1.20.0
|
|
1172
|
+
# rubocop:disable Metrics/MethodLength
|
|
1173
|
+
def precheck_lock(
|
|
1174
|
+
rconn,
|
|
1175
|
+
logger,
|
|
1176
|
+
log_sampled,
|
|
1177
|
+
log_lock_try,
|
|
1178
|
+
lock_key,
|
|
1179
|
+
read_write_mode,
|
|
1180
|
+
lock_readers_key,
|
|
1181
|
+
request_queue,
|
|
1182
|
+
opposite_request_queue,
|
|
1183
|
+
lock_name,
|
|
1184
|
+
acquirer_id,
|
|
1185
|
+
host_id,
|
|
1186
|
+
acquirer_position,
|
|
1187
|
+
acquirer_dead_score,
|
|
1188
|
+
queue_ttl,
|
|
1189
|
+
lock_index_enabled,
|
|
1190
|
+
lock_index_grace_period,
|
|
1191
|
+
fail_fast,
|
|
1192
|
+
access_strategy
|
|
1193
|
+
)
|
|
1194
|
+
read_mode = (read_write_mode == :read)
|
|
1195
|
+
# NOTE: the same pipeline layout and the same reply reading as in Step 0.1 of the attempt
|
|
1196
|
+
# @type var lock_state: Array[untyped]
|
|
1197
|
+
lock_state = rconn.pipelined do |pipeline|
|
|
1198
|
+
pipeline_lock_state(
|
|
1199
|
+
pipeline, lock_key, lock_readers_key, acquirer_id, read_mode, !fail_fast,
|
|
1200
|
+
request_queue, opposite_request_queue, acquirer_position, acquirer_dead_score,
|
|
1201
|
+
lock_name, queue_ttl, lock_index_enabled, lock_index_grace_period
|
|
1202
|
+
)
|
|
1203
|
+
end
|
|
1204
|
+
# @type var current_lock_obtainer: String?
|
|
1205
|
+
current_lock_obtainer = lock_state[LOCK_STATE_OBTAINER_REPLY]
|
|
1206
|
+
redis_time = RedisQueuedLocks::Resource.redis_time_ms(lock_state[LOCK_STATE_REDIS_TIME_REPLY])
|
|
1207
|
+
# @type var own_read_lock_expiration: Float?
|
|
1208
|
+
own_read_lock_expiration = lock_state[LOCK_STATE_OWN_READ_LOCK_REPLY]
|
|
1209
|
+
# @type var longest_read_lock: [String, Float]?
|
|
1210
|
+
longest_read_lock = read_mode ? nil : lock_state.last.first
|
|
1211
|
+
read_locked_by_acquirer = own_read_lock_expiration.to_f > redis_time
|
|
1212
|
+
read_locked = longest_read_lock ? longest_read_lock[1] > redis_time : false
|
|
1213
|
+
|
|
1214
|
+
# NOTE: same-process conflicts are processed by the attempt (conflict strategies)
|
|
1215
|
+
return if acquirer_id == current_lock_obtainer || read_locked_by_acquirer
|
|
1216
|
+
|
|
1217
|
+
# NOTE: the fail-fast check of the attempt (Fast-Step X0) without same-process conflicts
|
|
1218
|
+
return ((current_lock_obtainer || read_locked) ? :fail_fast_no_try : nil) if fail_fast
|
|
1219
|
+
|
|
1220
|
+
# @type var waiting_acquirer: String?
|
|
1221
|
+
waiting_acquirer =
|
|
1222
|
+
lock_state[LOCK_STATE_QUEUE_REPLIES_AT + QUEUE_HEAD_REPLY].first
|
|
1223
|
+
# @type var opposite_waiting_request: [String, Float]?
|
|
1224
|
+
opposite_waiting_request =
|
|
1225
|
+
lock_state[LOCK_STATE_QUEUE_REPLIES_AT + OPPOSITE_QUEUE_HEAD_REPLY].first
|
|
1226
|
+
# @type var opposite_request_ahead: String?
|
|
1227
|
+
opposite_request_ahead = find_opposite_request_ahead(
|
|
1228
|
+
access_strategy, opposite_waiting_request, acquirer_position, acquirer_id
|
|
1229
|
+
)
|
|
1230
|
+
obstacle = lock_obstacle(
|
|
1231
|
+
read_mode, access_strategy, acquirer_id, waiting_acquirer, opposite_request_ahead,
|
|
1232
|
+
current_lock_obtainer, read_locked
|
|
1233
|
+
)
|
|
1234
|
+
return unless obstacle
|
|
1235
|
+
|
|
1236
|
+
# NOTE: the same log events as the failed attempt (queueing steps + the exit event)
|
|
1237
|
+
LogVisitor.acq_added_to_queue(
|
|
1238
|
+
logger, log_sampled, log_lock_try, lock_key,
|
|
1239
|
+
queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode
|
|
1240
|
+
)
|
|
1241
|
+
LogVisitor.remove_expired_acqs(
|
|
1242
|
+
logger, log_sampled, log_lock_try, lock_key,
|
|
1243
|
+
queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode
|
|
1244
|
+
)
|
|
1245
|
+
LogVisitor.get_first_from_queue(
|
|
1246
|
+
logger, log_sampled, log_lock_try, lock_key,
|
|
1247
|
+
queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode, waiting_acquirer
|
|
1248
|
+
)
|
|
1249
|
+
log_lock_obstacle(
|
|
1250
|
+
rconn, obstacle, logger, log_sampled, log_lock_try, lock_key, queue_ttl,
|
|
1251
|
+
acquirer_id, host_id, access_strategy, read_write_mode,
|
|
1252
|
+
waiting_acquirer, opposite_request_ahead, current_lock_obtainer
|
|
1253
|
+
)
|
|
1254
|
+
obstacle
|
|
1255
|
+
end
|
|
1256
|
+
# rubocop:enable Metrics/MethodLength
|
|
701
1257
|
end
|
|
702
1258
|
# rubocop:enable Metrics/ModuleLength
|