redis_queued_locks 1.17.0 → 1.18.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 +86 -10
- data/.claude/rules/acquirer.md +31 -11
- data/.claude/rules/arguments.md +2 -1
- data/.claude/rules/git.md +46 -0
- data/.claude/rules/logic.md +9 -3
- data/.claude/rules/swarm.md +4 -3
- data/.claude/rules/tests.md +9 -3
- data/.claude/rules/type-checking.md +10 -2
- data/.claude/rules/visitors.md +12 -7
- data/CHANGELOG.md +62 -0
- data/CLAUDE.md +7 -2
- data/README.md +348 -79
- data/lib/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue/log_visitor.rb +6 -3
- data/lib/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue.rb +9 -7
- data/lib/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rb +20 -10
- data/lib/redis_queued_locks/acquirer/acquire_lock/log_visitor.rb +34 -16
- data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock/log_visitor.rb +240 -28
- data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rb +398 -129
- data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire/log_visitor.rb +12 -6
- data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire.rb +32 -8
- data/lib/redis_queued_locks/acquirer/acquire_lock.rb +73 -20
- data/lib/redis_queued_locks/acquirer/clear_dead_requests.rb +9 -1
- data/lib/redis_queued_locks/acquirer/extend_lock_ttl.rb +182 -3
- data/lib/redis_queued_locks/acquirer/is_locked.rb +19 -2
- data/lib/redis_queued_locks/acquirer/is_queued.rb +6 -2
- data/lib/redis_queued_locks/acquirer/lock_info.rb +96 -6
- data/lib/redis_queued_locks/acquirer/lock_series_poc/instr_visitor.rb +8 -2
- data/lib/redis_queued_locks/acquirer/lock_series_poc/log_visitor.rb +18 -6
- data/lib/redis_queued_locks/acquirer/lock_series_poc.rb +126 -27
- data/lib/redis_queued_locks/acquirer/locks.rb +103 -6
- data/lib/redis_queued_locks/acquirer/queue_info.rb +38 -9
- data/lib/redis_queued_locks/acquirer/queues.rb +28 -4
- data/lib/redis_queued_locks/acquirer/release_all_locks.rb +35 -1
- data/lib/redis_queued_locks/acquirer/release_lock.rb +40 -9
- data/lib/redis_queued_locks/acquirer/release_locks_of.rb +43 -3
- data/lib/redis_queued_locks/acquirer/release_read_lock.rb +143 -0
- data/lib/redis_queued_locks/acquirer.rb +1 -0
- data/lib/redis_queued_locks/client.rb +129 -14
- data/lib/redis_queued_locks/resource.rb +117 -9
- data/lib/redis_queued_locks/swarm/flush_zombies.rb +38 -0
- data/lib/redis_queued_locks/swarm/zombie_info.rb +42 -0
- data/lib/redis_queued_locks/version.rb +1 -1
- data/rbs_collection.lock.yaml +10 -2
- data/rbs_collection.yaml +0 -2
- data/sig/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue/log_visitor.rbs +2 -1
- data/sig/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue.rbs +0 -1
- data/sig/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rbs +5 -0
- data/sig/redis_queued_locks/acquirer/acquire_lock/log_visitor.rbs +12 -6
- data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock/log_visitor.rbs +74 -7
- data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rbs +4 -2
- data/sig/redis_queued_locks/acquirer/acquire_lock/yield_expire.rbs +3 -0
- data/sig/redis_queued_locks/acquirer/acquire_lock/yield_with_expire/log_visitor.rbs +4 -2
- data/sig/redis_queued_locks/acquirer/extend_lock_ttl.rbs +19 -1
- data/sig/redis_queued_locks/acquirer/lock_info.rbs +9 -1
- data/sig/redis_queued_locks/acquirer/locks.rbs +10 -1
- data/sig/redis_queued_locks/acquirer/queue_info.rbs +2 -4
- data/sig/redis_queued_locks/acquirer/queues.rbs +1 -1
- data/sig/redis_queued_locks/acquirer/release_lock.rbs +8 -1
- data/sig/redis_queued_locks/acquirer/release_read_lock.rbs +44 -0
- data/sig/redis_queued_locks/client.rbs +22 -2
- data/sig/redis_queued_locks/resource.rbs +9 -2
- data/sig/redis_queued_locks/swarm/zombie_info.rbs +5 -0
- metadata +4 -1
|
@@ -11,11 +11,12 @@ module RedisQueuedLocks::Acquirer::AcquireLock::YieldExpire::LogVisitor
|
|
|
11
11
|
# @param acquirer_id [String]
|
|
12
12
|
# @param host_id [String]
|
|
13
13
|
# @param access_strategy [Symbol]
|
|
14
|
+
# @param rw_mode [Symbol]
|
|
14
15
|
# @return [void]
|
|
15
16
|
#
|
|
16
17
|
# @api private
|
|
17
18
|
# @since 1.7.0
|
|
18
|
-
# @version 1.
|
|
19
|
+
# @version 1.18.0
|
|
19
20
|
def expire_lock(
|
|
20
21
|
logger,
|
|
21
22
|
log_sampled,
|
|
@@ -23,7 +24,8 @@ module RedisQueuedLocks::Acquirer::AcquireLock::YieldExpire::LogVisitor
|
|
|
23
24
|
queue_ttl,
|
|
24
25
|
acquirer_id,
|
|
25
26
|
host_id,
|
|
26
|
-
access_strategy
|
|
27
|
+
access_strategy,
|
|
28
|
+
rw_mode
|
|
27
29
|
)
|
|
28
30
|
return unless log_sampled
|
|
29
31
|
|
|
@@ -33,7 +35,8 @@ module RedisQueuedLocks::Acquirer::AcquireLock::YieldExpire::LogVisitor
|
|
|
33
35
|
"queue_ttl => #{queue_ttl} " \
|
|
34
36
|
"acq_id => '#{acquirer_id}' " \
|
|
35
37
|
"hst_id => '#{host_id}' " \
|
|
36
|
-
"acs_strat => '#{access_strategy}'"
|
|
38
|
+
"acs_strat => '#{access_strategy}' " \
|
|
39
|
+
"rw_mode => '#{rw_mode}'"
|
|
37
40
|
end rescue nil
|
|
38
41
|
end
|
|
39
42
|
|
|
@@ -45,11 +48,12 @@ module RedisQueuedLocks::Acquirer::AcquireLock::YieldExpire::LogVisitor
|
|
|
45
48
|
# @param acquirer_id [String]
|
|
46
49
|
# @param host_id [String]
|
|
47
50
|
# @param access_strategy [Symbol]
|
|
51
|
+
# @param rw_mode [Symbol]
|
|
48
52
|
# @return [void]
|
|
49
53
|
#
|
|
50
54
|
# @api private
|
|
51
55
|
# @since 1.7.0
|
|
52
|
-
# @version 1.
|
|
56
|
+
# @version 1.18.0
|
|
53
57
|
def decrease_lock(
|
|
54
58
|
logger,
|
|
55
59
|
log_sampled,
|
|
@@ -58,7 +62,8 @@ module RedisQueuedLocks::Acquirer::AcquireLock::YieldExpire::LogVisitor
|
|
|
58
62
|
queue_ttl,
|
|
59
63
|
acquirer_id,
|
|
60
64
|
host_id,
|
|
61
|
-
access_strategy
|
|
65
|
+
access_strategy,
|
|
66
|
+
rw_mode
|
|
62
67
|
)
|
|
63
68
|
return unless log_sampled
|
|
64
69
|
|
|
@@ -69,7 +74,8 @@ module RedisQueuedLocks::Acquirer::AcquireLock::YieldExpire::LogVisitor
|
|
|
69
74
|
"queue_ttl => #{queue_ttl} " \
|
|
70
75
|
"acq_id => '#{acquirer_id}' " \
|
|
71
76
|
"hst_id => '#{host_id}' " \
|
|
72
|
-
"acs_strat => '#{access_strategy}'"
|
|
77
|
+
"acs_strat => '#{access_strategy}' " \
|
|
78
|
+
"rw_mode => '#{rw_mode}'"
|
|
73
79
|
end rescue nil
|
|
74
80
|
end
|
|
75
81
|
end
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# @api private
|
|
4
4
|
# @since 1.3.0
|
|
5
|
-
# @version 1.
|
|
5
|
+
# @version 1.18.0
|
|
6
6
|
module RedisQueuedLocks::Acquirer::AcquireLock::YieldExpire
|
|
7
7
|
require_relative 'yield_expire/log_visitor'
|
|
8
8
|
|
|
@@ -18,6 +18,11 @@ module RedisQueuedLocks::Acquirer::AcquireLock::YieldExpire
|
|
|
18
18
|
# @param redis [RedisClient] Redis connection.
|
|
19
19
|
# @param logger [::Logger,#debug] Logger object.
|
|
20
20
|
# @param lock_key [String] Obtained lock key that should be expired.
|
|
21
|
+
# @param lock_readers_key [String] Registry of read lock holders.
|
|
22
|
+
# @param read_lock_key [String] Read lock data of the current acquirer.
|
|
23
|
+
# @param rw_mode [Symbol]
|
|
24
|
+
# - The mode of the obtained lock (`:read` or `:write`);
|
|
25
|
+
# - `:read` => only the read lock of the current acquirer is released/decreased;
|
|
21
26
|
# @param acquirer_id [String] Acquirer identifier.
|
|
22
27
|
# @param host_id [String] Host identifier.
|
|
23
28
|
# @param access_strategy [Symbol] Lock obtaining strategy.
|
|
@@ -38,12 +43,15 @@ module RedisQueuedLocks::Acquirer::AcquireLock::YieldExpire
|
|
|
38
43
|
#
|
|
39
44
|
# @api private
|
|
40
45
|
# @since 1.3.0
|
|
41
|
-
# @version 1.
|
|
46
|
+
# @version 1.18.0
|
|
42
47
|
# rubocop:disable Metrics/MethodLength
|
|
43
48
|
def yield_expire(
|
|
44
49
|
redis,
|
|
45
50
|
logger,
|
|
46
51
|
lock_key,
|
|
52
|
+
lock_readers_key,
|
|
53
|
+
read_lock_key,
|
|
54
|
+
rw_mode,
|
|
47
55
|
acquirer_id,
|
|
48
56
|
host_id,
|
|
49
57
|
access_strategy,
|
|
@@ -97,9 +105,17 @@ module RedisQueuedLocks::Acquirer::AcquireLock::YieldExpire
|
|
|
97
105
|
if should_expire # TODO: comment all cases/examples when should_expire is true
|
|
98
106
|
LogVisitor.expire_lock(
|
|
99
107
|
logger, log_sampled, lock_key,
|
|
100
|
-
queue_ttl, acquirer_id, host_id, access_strategy
|
|
108
|
+
queue_ttl, acquirer_id, host_id, access_strategy, rw_mode
|
|
101
109
|
)
|
|
102
|
-
|
|
110
|
+
if rw_mode == :read
|
|
111
|
+
# NOTE: release only the read lock of the current acquirer (other readers keep working)
|
|
112
|
+
redis.multi do |transact|
|
|
113
|
+
transact.call('ZREM', lock_readers_key, acquirer_id)
|
|
114
|
+
transact.call('DEL', read_lock_key)
|
|
115
|
+
end
|
|
116
|
+
else
|
|
117
|
+
redis.call('EXPIRE', lock_key, '0')
|
|
118
|
+
end
|
|
103
119
|
elsif should_decrease # TODO: comment all cases/examples when should_expire is true
|
|
104
120
|
finish_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, :millisecond)
|
|
105
121
|
# @type var initial_time: Integer
|
|
@@ -110,11 +126,19 @@ module RedisQueuedLocks::Acquirer::AcquireLock::YieldExpire
|
|
|
110
126
|
if decreased_ttl > 0
|
|
111
127
|
LogVisitor.decrease_lock(
|
|
112
128
|
logger, log_sampled, lock_key,
|
|
113
|
-
decreased_ttl, queue_ttl, acquirer_id, host_id, access_strategy
|
|
129
|
+
decreased_ttl, queue_ttl, acquirer_id, host_id, access_strategy, rw_mode
|
|
114
130
|
)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
131
|
+
if rw_mode == :read
|
|
132
|
+
# NOTE:
|
|
133
|
+
# - decrease the read lock expiration of the current acquirer;
|
|
134
|
+
# - XX => do nothing if the read lock is already expired and removed from the registry;
|
|
135
|
+
# - the read lock data TTL is not decreased (it is not used without the read lock);
|
|
136
|
+
redis.call('ZADD', lock_readers_key, 'XX', 'INCR', -decreased_ttl, acquirer_id)
|
|
137
|
+
else
|
|
138
|
+
# NOTE:# NOTE: EVAL signature -> <lua script>, (number of keys), *(keys), *(arguments)
|
|
139
|
+
redis.call('EVAL', DECREASE_LOCK_PTTL, 1, lock_key, decreased_ttl)
|
|
140
|
+
# TODO: upload scripts to the redis
|
|
141
|
+
end
|
|
118
142
|
end
|
|
119
143
|
end
|
|
120
144
|
end
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# @api private
|
|
4
4
|
# @since 1.0.0
|
|
5
|
-
# @version 1.
|
|
5
|
+
# @version 1.18.0
|
|
6
6
|
# rubocop:disable Metrics/ModuleLength
|
|
7
7
|
# rubocop:disable Metrics/MethodLength
|
|
8
8
|
# rubocop:disable Metrics/ClassLength
|
|
@@ -96,7 +96,15 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
96
96
|
# - `:wait_for_lock`;
|
|
97
97
|
# - `:dead_locking`;
|
|
98
98
|
# @option read_write_mode [Symbol]
|
|
99
|
-
# -
|
|
99
|
+
# - `:write` (default) - exclusive lock: waits for the write lock and for all read locks;
|
|
100
|
+
# - `:read` - shared lock: waits for the write lock only (read locks do not block each other);
|
|
101
|
+
# - `:queued` access strategy orders read and write requests in FIFO between modes
|
|
102
|
+
# (read request waits for earlier write requests, write request waits for earlier
|
|
103
|
+
# read requests and for earlier write requests);
|
|
104
|
+
# - `:meta` of the read lock is stored in the read lock data of the acquirer;
|
|
105
|
+
# - same-process conflicts: read-to-write lock upgrade fails with
|
|
106
|
+
# `RedisQueuedLocks::ConflictLockObtainError` for any conflict strategy except
|
|
107
|
+
# `:wait_for_lock` (it waits for the read lock expiration);
|
|
100
108
|
# @option access_strategy [Symbol]
|
|
101
109
|
# - The way in which the lock will be obtained;
|
|
102
110
|
# - By default it uses `:queued` strategy;
|
|
@@ -158,7 +166,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
158
166
|
#
|
|
159
167
|
# @api private
|
|
160
168
|
# @since 1.0.0
|
|
161
|
-
# @version 1.
|
|
169
|
+
# @version 1.18.0
|
|
162
170
|
def acquire_lock(
|
|
163
171
|
redis,
|
|
164
172
|
lock_name,
|
|
@@ -206,6 +214,24 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
206
214
|
)
|
|
207
215
|
end
|
|
208
216
|
|
|
217
|
+
# Step 0.1 (RW): prevent :read_write_mode incompatabilities
|
|
218
|
+
if read_write_mode != :read && read_write_mode != :write
|
|
219
|
+
raise(
|
|
220
|
+
RedisQueuedLocks::ArgumentError,
|
|
221
|
+
"`:read_write_mode` argument should be `:read` or `:write`, " \
|
|
222
|
+
"got #{read_write_mode.inspect}."
|
|
223
|
+
)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Step 0.1 (RW): prevent read lock incompatabilities
|
|
227
|
+
# - read lock expiration time is calculated from the lock ttl;
|
|
228
|
+
if read_write_mode == :read && (!ttl.is_a?(::Integer) || ttl <= 0)
|
|
229
|
+
raise(
|
|
230
|
+
RedisQueuedLocks::ArgumentError,
|
|
231
|
+
"`:ttl` argument should be a positive Integer for read locks, got #{ttl.inspect}."
|
|
232
|
+
)
|
|
233
|
+
end
|
|
234
|
+
|
|
209
235
|
# Step 0.2: prevent :meta incompatabiltiies (structure)
|
|
210
236
|
if meta.is_a?(::Hash) && (meta.any? do |key, _value|
|
|
211
237
|
key == 'acq_id' ||
|
|
@@ -218,13 +244,14 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
218
244
|
key == 'spc_cnt' ||
|
|
219
245
|
key == 'l_spc_ext_ini_ttl' ||
|
|
220
246
|
key == 'l_spc_ext_ts' ||
|
|
221
|
-
key == 'l_spc_ts'
|
|
247
|
+
key == 'l_spc_ts' ||
|
|
248
|
+
key == 'rw_mode'
|
|
222
249
|
end)
|
|
223
250
|
raise(
|
|
224
251
|
RedisQueuedLocks::ArgumentError,
|
|
225
252
|
'`:meta` keys can not overlap reserved lock data keys ' \
|
|
226
253
|
'"acq_id", "hst_id", "ts", "ini_ttl", "lock_key", "rem_ttl", "spc_cnt", ' \
|
|
227
|
-
'"spc_ext_ttl", "l_spc_ext_ini_ttl", "l_spc_ext_ts", "l_spc_ts"'
|
|
254
|
+
'"spc_ext_ttl", "l_spc_ext_ini_ttl", "l_spc_ext_ts", "l_spc_ts", "rw_mode"'
|
|
228
255
|
)
|
|
229
256
|
end
|
|
230
257
|
|
|
@@ -246,8 +273,10 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
246
273
|
lock_key = RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
|
|
247
274
|
lock_key_queue = RedisQueuedLocks::Resource.prepare_lock_queue(lock_name)
|
|
248
275
|
|
|
276
|
+
# NOTE: (RW) write lock requests use the classic lock queue (lock_key_queue)
|
|
249
277
|
read_lock_key_queue = RedisQueuedLocks::Resource.prepare_read_lock_queue(lock_name)
|
|
250
|
-
|
|
278
|
+
lock_readers_key = RedisQueuedLocks::Resource.prepare_lock_readers(lock_name)
|
|
279
|
+
read_lock_key = RedisQueuedLocks::Resource.prepare_read_lock_key(lock_name, acquirer_id)
|
|
251
280
|
|
|
252
281
|
acquirer_position = RedisQueuedLocks::Resource.calc_initial_acquirer_position
|
|
253
282
|
|
|
@@ -285,7 +314,6 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
285
314
|
read_write_mode,
|
|
286
315
|
lock_key_queue,
|
|
287
316
|
read_lock_key_queue,
|
|
288
|
-
write_lock_key_queue,
|
|
289
317
|
queue_ttl,
|
|
290
318
|
acquirer_id,
|
|
291
319
|
host_id,
|
|
@@ -297,7 +325,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
297
325
|
|
|
298
326
|
LogVisitor.start_lock_obtaining(
|
|
299
327
|
logger, log_sampled, lock_key,
|
|
300
|
-
queue_ttl, acquirer_id, host_id, access_strategy
|
|
328
|
+
queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode
|
|
301
329
|
)
|
|
302
330
|
|
|
303
331
|
# Step 2: try to lock with timeout
|
|
@@ -317,7 +345,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
317
345
|
|
|
318
346
|
LogVisitor.start_try_to_lock_cycle(
|
|
319
347
|
logger, log_sampled, lock_key,
|
|
320
|
-
queue_ttl, acquirer_id, host_id, access_strategy
|
|
348
|
+
queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode
|
|
321
349
|
)
|
|
322
350
|
|
|
323
351
|
# Step 2.X: check the actual score: is it in queue ttl limit or not?
|
|
@@ -327,7 +355,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
327
355
|
|
|
328
356
|
LogVisitor.dead_score_reached__reset_acquirer_position(
|
|
329
357
|
logger, log_sampled, lock_key,
|
|
330
|
-
queue_ttl, acquirer_id, host_id, access_strategy
|
|
358
|
+
queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode
|
|
331
359
|
)
|
|
332
360
|
end
|
|
333
361
|
|
|
@@ -341,7 +369,8 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
341
369
|
read_write_mode,
|
|
342
370
|
lock_key_queue,
|
|
343
371
|
read_lock_key_queue,
|
|
344
|
-
|
|
372
|
+
lock_readers_key,
|
|
373
|
+
read_lock_key,
|
|
345
374
|
acquirer_id,
|
|
346
375
|
host_id,
|
|
347
376
|
acquirer_position,
|
|
@@ -375,10 +404,10 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
375
404
|
# instrumetnation: (reentrant lock with ttl extension)
|
|
376
405
|
LogVisitor.extendable_reentrant_lock_obtained(
|
|
377
406
|
logger, log_sampled, result[:lock_key],
|
|
378
|
-
queue_ttl, acquirer_id, host_id, acq_time, access_strategy
|
|
407
|
+
queue_ttl, acquirer_id, host_id, acq_time, access_strategy, read_write_mode
|
|
379
408
|
)
|
|
380
409
|
InstrVisitor.extendable_reentrant_lock_obtained(
|
|
381
|
-
instrumenter, instr_sampled, result[:lock_key],
|
|
410
|
+
instrumenter, instr_sampled, result[:lock_key], read_write_mode,
|
|
382
411
|
result[:ttl], result[:acq_id], result[:hst_id], result[:ts], acq_time,
|
|
383
412
|
instrument
|
|
384
413
|
)
|
|
@@ -386,10 +415,10 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
386
415
|
# instrumetnation: (reentrant lock without ttl extension)
|
|
387
416
|
LogVisitor.reentrant_lock_obtained(
|
|
388
417
|
logger, log_sampled, result[:lock_key],
|
|
389
|
-
queue_ttl, acquirer_id, host_id, acq_time, access_strategy
|
|
418
|
+
queue_ttl, acquirer_id, host_id, acq_time, access_strategy, read_write_mode
|
|
390
419
|
)
|
|
391
420
|
InstrVisitor.reentrant_lock_obtained(
|
|
392
|
-
instrumenter, instr_sampled, result[:lock_key],
|
|
421
|
+
instrumenter, instr_sampled, result[:lock_key], read_write_mode,
|
|
393
422
|
result[:ttl], result[:acq_id], result[:hst_id], result[:ts], acq_time,
|
|
394
423
|
instrument
|
|
395
424
|
)
|
|
@@ -398,10 +427,10 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
398
427
|
# NOTE: classic is: acq_process[:result][:process] == :lock_obtaining
|
|
399
428
|
LogVisitor.lock_obtained(
|
|
400
429
|
logger, log_sampled, result[:lock_key],
|
|
401
|
-
queue_ttl, acquirer_id, host_id, acq_time, access_strategy
|
|
430
|
+
queue_ttl, acquirer_id, host_id, acq_time, access_strategy, read_write_mode
|
|
402
431
|
)
|
|
403
432
|
InstrVisitor.lock_obtained(
|
|
404
|
-
instrumenter, instr_sampled, result[:lock_key],
|
|
433
|
+
instrumenter, instr_sampled, result[:lock_key], read_write_mode,
|
|
405
434
|
result[:ttl], result[:acq_id], result[:hst_id], result[:ts], acq_time,
|
|
406
435
|
instrument
|
|
407
436
|
)
|
|
@@ -416,6 +445,8 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
416
445
|
ttl: result[:ttl],
|
|
417
446
|
process: result[:process]
|
|
418
447
|
}
|
|
448
|
+
# NOTE: (RW) the mode of the really held lock (internal, used for the lock release)
|
|
449
|
+
acq_process[:held_rw_mode] = result[:rw_mode]
|
|
419
450
|
acq_process[:acquired] = true
|
|
420
451
|
acq_process[:should_try] = false
|
|
421
452
|
acq_process[:acq_time] = acq_time
|
|
@@ -424,7 +455,8 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
424
455
|
# @type var result: Symbol
|
|
425
456
|
|
|
426
457
|
# Step 2.2: failed to acquire. anylize each case and act in accordance
|
|
427
|
-
|
|
458
|
+
case acq_process[:result]
|
|
459
|
+
when :fail_fast_no_try # Step 2.2.a: fail without try
|
|
428
460
|
acq_process[:should_try] = false
|
|
429
461
|
|
|
430
462
|
if raise_errors
|
|
@@ -433,7 +465,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
433
465
|
"Lock \"#{lock_key}\" is already obtained."
|
|
434
466
|
)
|
|
435
467
|
end
|
|
436
|
-
|
|
468
|
+
when :conflict_dead_lock # Step 2.2.b: fail after dead lock
|
|
437
469
|
acq_process[:tries] += 1
|
|
438
470
|
acq_process[:should_try] = false
|
|
439
471
|
acq_process[:result] = :conflict_dead_lock
|
|
@@ -446,6 +478,21 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
446
478
|
"that is already acquired by the current acquirer (acq_id: \"#{acquirer_id}\")."
|
|
447
479
|
)
|
|
448
480
|
end
|
|
481
|
+
when :conflict_lock_upgrade # Step 2.2.c (RW): fail after lock upgrade attempt
|
|
482
|
+
acq_process[:tries] += 1
|
|
483
|
+
acq_process[:should_try] = false
|
|
484
|
+
acq_process[:result] = :conflict_lock_upgrade
|
|
485
|
+
acq_dequeue.call
|
|
486
|
+
|
|
487
|
+
if raise_errors
|
|
488
|
+
raise(
|
|
489
|
+
RedisQueuedLocks::ConflictLockObtainError,
|
|
490
|
+
"Lock Conflict: trying to acquire the write lock \"#{lock_key}\" " \
|
|
491
|
+
"while the current acquirer holds its read lock (acq_id: \"#{acquirer_id}\"): " \
|
|
492
|
+
"read-to-write lock upgrade is not supported " \
|
|
493
|
+
"(conflict strategy: :#{conflict_strategy})."
|
|
494
|
+
)
|
|
495
|
+
end
|
|
449
496
|
else
|
|
450
497
|
acq_process[:tries] += 1 # Step RETRY: possible retry case
|
|
451
498
|
|
|
@@ -510,6 +557,9 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
510
557
|
redis,
|
|
511
558
|
logger,
|
|
512
559
|
lock_key,
|
|
560
|
+
lock_readers_key,
|
|
561
|
+
read_lock_key,
|
|
562
|
+
acq_process[:held_rw_mode],
|
|
513
563
|
acquirer_id,
|
|
514
564
|
host_id,
|
|
515
565
|
access_strategy,
|
|
@@ -537,6 +587,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
537
587
|
instrumenter,
|
|
538
588
|
instr_sampled,
|
|
539
589
|
acq_process[:lock_info][:lock_key],
|
|
590
|
+
read_write_mode,
|
|
540
591
|
acq_process[:lock_info][:ttl],
|
|
541
592
|
acq_process[:lock_info][:acq_id],
|
|
542
593
|
acq_process[:lock_info][:hst_id],
|
|
@@ -551,6 +602,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
551
602
|
instrumenter,
|
|
552
603
|
instr_sampled,
|
|
553
604
|
acq_process[:lock_info][:lock_key],
|
|
605
|
+
read_write_mode,
|
|
554
606
|
acq_process[:lock_info][:ttl],
|
|
555
607
|
acq_process[:lock_info][:acq_id],
|
|
556
608
|
acq_process[:lock_info][:hst_id],
|
|
@@ -568,7 +620,8 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
568
620
|
if acq_process[:result] != :retry_limit_reached &&
|
|
569
621
|
acq_process[:result] != :fail_fast_no_try &&
|
|
570
622
|
acq_process[:result] != :fail_fast_after_try &&
|
|
571
|
-
acq_process[:result] != :conflict_dead_lock
|
|
623
|
+
acq_process[:result] != :conflict_dead_lock &&
|
|
624
|
+
acq_process[:result] != :conflict_lock_upgrade
|
|
572
625
|
# NOTE: we have only two situations if lock is not acquired without explicit failures:
|
|
573
626
|
# - time limit is reached;
|
|
574
627
|
# - retry count limit is reached;
|
|
@@ -22,7 +22,7 @@ module RedisQueuedLocks::Acquirer::ClearDeadRequests
|
|
|
22
22
|
#
|
|
23
23
|
# @api private
|
|
24
24
|
# @since 1.0.0
|
|
25
|
-
# @version 1.
|
|
25
|
+
# @version 1.18.0
|
|
26
26
|
def clear_dead_requests(
|
|
27
27
|
redis_client,
|
|
28
28
|
scan_size,
|
|
@@ -66,6 +66,7 @@ module RedisQueuedLocks::Acquirer::ClearDeadRequests
|
|
|
66
66
|
#
|
|
67
67
|
# @api private
|
|
68
68
|
# @since 1.0.0
|
|
69
|
+
# @version 1.18.0
|
|
69
70
|
def each_lock_queue(redis_client, scan_size, &block)
|
|
70
71
|
redis_client.scan(
|
|
71
72
|
'MATCH',
|
|
@@ -73,6 +74,13 @@ module RedisQueuedLocks::Acquirer::ClearDeadRequests
|
|
|
73
74
|
count: scan_size,
|
|
74
75
|
&block
|
|
75
76
|
)
|
|
77
|
+
# NOTE: (RW) read lock queues
|
|
78
|
+
redis_client.scan(
|
|
79
|
+
'MATCH',
|
|
80
|
+
RedisQueuedLocks::Resource::READ_LOCK_QUEUE_PATTERN,
|
|
81
|
+
count: scan_size,
|
|
82
|
+
&block
|
|
83
|
+
)
|
|
76
84
|
end
|
|
77
85
|
end
|
|
78
86
|
end
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
# @api private
|
|
4
4
|
# @since 1.0.0
|
|
5
|
+
# @version 1.18.0
|
|
6
|
+
# rubocop:disable Metrics/ModuleLength
|
|
5
7
|
module RedisQueuedLocks::Acquirer::ExtendLockTTL
|
|
6
8
|
# @return [String]
|
|
7
9
|
#
|
|
@@ -12,10 +14,20 @@ module RedisQueuedLocks::Acquirer::ExtendLockTTL
|
|
|
12
14
|
return redis.call("PEXPIRE", KEYS[1], new_lock_pttl);
|
|
13
15
|
LUA_SCRIPT
|
|
14
16
|
|
|
17
|
+
# rubocop:disable Metrics/ClassLength
|
|
15
18
|
class << self
|
|
16
19
|
# @param redis_client [RedisClient]
|
|
17
20
|
# @param lock_name [String]
|
|
18
21
|
# @param milliseconds [Integer]
|
|
22
|
+
# @param read_write_mode [Symbol]
|
|
23
|
+
# - `:write` - extends the write lock (of any acquirer);
|
|
24
|
+
# - `:read` - extends the read lock of the given acquirer (other read locks are not affected)
|
|
25
|
+
# or all live read locks of the lock (see `all_read_locks`);
|
|
26
|
+
# @param all_read_locks [Boolean]
|
|
27
|
+
# Extend all live read locks of the lock instead of the read lock of the given acquirer
|
|
28
|
+
# (used for `:read` mode only, ignored for `:write` mode).
|
|
29
|
+
# @param acquirer_id [String]
|
|
30
|
+
# The read lock acquirer (used for `:read` mode without `all_read_locks` only).
|
|
19
31
|
# @param logger [::Logger,#debug]
|
|
20
32
|
# @param instrumenter [#notify]
|
|
21
33
|
# @param instrument [NilClass,Any]
|
|
@@ -27,15 +39,19 @@ module RedisQueuedLocks::Acquirer::ExtendLockTTL
|
|
|
27
39
|
# @param instr_sampling_percent [Integer]
|
|
28
40
|
# @param instr_sampler [#sampling_happened?,Module<RedisQueuedLocks::Instrument::Sampler>]
|
|
29
41
|
# @param instr_sample_this [Boolean]
|
|
30
|
-
# @return [Hash<Symbol,Boolean|Symbol
|
|
42
|
+
# @return [Hash<Symbol,Boolean|Symbol|Hash<Symbol,Integer>>]
|
|
31
43
|
#
|
|
32
44
|
# @api private
|
|
33
45
|
# @since 1.0.0
|
|
34
|
-
# @version 1.
|
|
46
|
+
# @version 1.18.0
|
|
47
|
+
# rubocop:disable Metrics/MethodLength
|
|
35
48
|
def extend_lock_ttl(
|
|
36
49
|
redis_client,
|
|
37
50
|
lock_name,
|
|
38
51
|
milliseconds,
|
|
52
|
+
read_write_mode,
|
|
53
|
+
all_read_locks,
|
|
54
|
+
acquirer_id,
|
|
39
55
|
logger,
|
|
40
56
|
instrumenter,
|
|
41
57
|
instrument,
|
|
@@ -48,18 +64,181 @@ module RedisQueuedLocks::Acquirer::ExtendLockTTL
|
|
|
48
64
|
instr_sampler,
|
|
49
65
|
instr_sample_this
|
|
50
66
|
)
|
|
67
|
+
if read_write_mode != :read && read_write_mode != :write
|
|
68
|
+
raise(
|
|
69
|
+
RedisQueuedLocks::ArgumentError,
|
|
70
|
+
"`:read_write_mode` argument should be `:read` or `:write`, " \
|
|
71
|
+
"got #{read_write_mode.inspect}."
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
|
|
51
75
|
lock_key = RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
|
|
52
76
|
|
|
77
|
+
if read_write_mode == :read
|
|
78
|
+
if all_read_locks != true && all_read_locks != false
|
|
79
|
+
raise(
|
|
80
|
+
RedisQueuedLocks::ArgumentError,
|
|
81
|
+
"`:all_read_locks` argument should be a boolean, got #{all_read_locks.inspect}."
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# NOTE: (RW) all live read locks of the lock
|
|
86
|
+
if all_read_locks
|
|
87
|
+
return extend_all_read_locks_ttl(redis_client, lock_name, lock_key, milliseconds)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# NOTE: (RW) read lock of the concrete acquirer
|
|
91
|
+
return extend_read_lock_ttl(redis_client, lock_name, lock_key, acquirer_id, milliseconds)
|
|
92
|
+
end
|
|
93
|
+
|
|
53
94
|
# NOTE: EVAL signature -> <lua script>, (number of keys), *(keys), *(arguments)
|
|
54
95
|
result = redis_client.call('EVAL', EXTEND_LOCK_PTTL, 1, lock_key, milliseconds)
|
|
55
96
|
# TODO: upload scripts to the redis
|
|
56
97
|
|
|
57
98
|
# @type var result: Integer
|
|
58
99
|
if result == 1
|
|
59
|
-
{ ok: true, result: :
|
|
100
|
+
{ ok: true, result: { extended_locks_count: 1 } }
|
|
101
|
+
else
|
|
102
|
+
{ ok: false, result: :async_expire_or_no_lock }
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
# rubocop:enable Metrics/MethodLength
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
# Extends the live read lock of the acquirer:
|
|
110
|
+
# - the read lock expiration (readers registry score), the registry TTL and the read lock
|
|
111
|
+
# data TTL are extended;
|
|
112
|
+
# - the write lock key is watched: an expired read lock can be already replaced by a writer,
|
|
113
|
+
# so the expired (or replaced) read lock is never "revived";
|
|
114
|
+
#
|
|
115
|
+
# @param redis_client [RedisClient]
|
|
116
|
+
# @param lock_name [String]
|
|
117
|
+
# @param lock_key [String]
|
|
118
|
+
# @param acquirer_id [String]
|
|
119
|
+
# @param milliseconds [Integer]
|
|
120
|
+
# @return [Hash<Symbol,Boolean|Symbol|Hash<Symbol,Integer>>]
|
|
121
|
+
#
|
|
122
|
+
# @api private
|
|
123
|
+
# @since 1.18.0
|
|
124
|
+
# rubocop:disable Metrics/MethodLength
|
|
125
|
+
def extend_read_lock_ttl(redis_client, lock_name, lock_key, acquirer_id, milliseconds)
|
|
126
|
+
lock_readers_key = RedisQueuedLocks::Resource.prepare_lock_readers(lock_name)
|
|
127
|
+
read_lock_key = RedisQueuedLocks::Resource.prepare_read_lock_key(lock_name, acquirer_id)
|
|
128
|
+
|
|
129
|
+
result = redis_client.with do |rconn|
|
|
130
|
+
rconn.multi(watch: [lock_key]) do |transact|
|
|
131
|
+
# @type var lock_state: [Integer, Array[String], Float?]
|
|
132
|
+
lock_state = rconn.pipelined do |pipeline|
|
|
133
|
+
pipeline.call('EXISTS', lock_key)
|
|
134
|
+
pipeline.call('TIME')
|
|
135
|
+
pipeline.call('ZSCORE', lock_readers_key, acquirer_id)
|
|
136
|
+
end
|
|
137
|
+
redis_time = RedisQueuedLocks::Resource.redis_time_ms(lock_state[1])
|
|
138
|
+
read_lock_expiration = lock_state[2].to_f
|
|
139
|
+
|
|
140
|
+
# NOTE: no write lock and the read lock of the acquirer is alive
|
|
141
|
+
if lock_state[0] == 0 && read_lock_expiration > redis_time
|
|
142
|
+
extended_ttl = (read_lock_expiration + milliseconds - redis_time).ceil
|
|
143
|
+
transact.call('ZADD', lock_readers_key, 'XX', 'INCR', milliseconds, acquirer_id)
|
|
144
|
+
transact.call('PEXPIRE', lock_readers_key, extended_ttl, 'NX')
|
|
145
|
+
transact.call('PEXPIRE', lock_readers_key, extended_ttl, 'GT')
|
|
146
|
+
transact.call('PEXPIRE', read_lock_key, extended_ttl, 'NX')
|
|
147
|
+
transact.call('PEXPIRE', read_lock_key, extended_ttl, 'GT')
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# NOTE:
|
|
153
|
+
# - [] => there is no live read lock of the acquirer (nothing to extend);
|
|
154
|
+
# - nil => the write lock is changed during the extension (the read lock can be expired);
|
|
155
|
+
# - result[0] => the new read lock expiration (nil if the read lock is removed);
|
|
156
|
+
if result.is_a?(::Array) && !result.empty? && result[0] != nil
|
|
157
|
+
{ ok: true, result: { extended_locks_count: 1 } }
|
|
158
|
+
else
|
|
159
|
+
{ ok: false, result: :async_expire_or_no_lock }
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
# rubocop:enable Metrics/MethodLength
|
|
163
|
+
|
|
164
|
+
# Extends all live read locks of the lock (the same rules as for `extend_read_lock_ttl`):
|
|
165
|
+
# - each live read lock expiration (readers registry score) and its read lock data TTL are
|
|
166
|
+
# extended by the given milliseconds; the registry TTL is extended to the longest read lock;
|
|
167
|
+
# - expired read locks are not extended ("revived");
|
|
168
|
+
# - the write lock key is watched (a writer can replace the expired read locks), the registry
|
|
169
|
+
# is not watched: concurrent read lock acquirements/releases do not abort the extension
|
|
170
|
+
# (read locks obtained after the registry snapshot are not extended, read locks released
|
|
171
|
+
# after it are not revived cuz of `ZADD XX`);
|
|
172
|
+
#
|
|
173
|
+
# @param redis_client [RedisClient]
|
|
174
|
+
# @param lock_name [String]
|
|
175
|
+
# @param lock_key [String]
|
|
176
|
+
# @param milliseconds [Integer]
|
|
177
|
+
# @return [Hash<Symbol,Boolean|Symbol|Hash<Symbol,Integer>>]
|
|
178
|
+
#
|
|
179
|
+
# @api private
|
|
180
|
+
# @since 1.18.0
|
|
181
|
+
# rubocop:disable Metrics/MethodLength
|
|
182
|
+
def extend_all_read_locks_ttl(redis_client, lock_name, lock_key, milliseconds)
|
|
183
|
+
lock_readers_key = RedisQueuedLocks::Resource.prepare_lock_readers(lock_name)
|
|
184
|
+
extended_read_locks_count = 0
|
|
185
|
+
|
|
186
|
+
result = redis_client.with do |rconn|
|
|
187
|
+
rconn.multi(watch: [lock_key]) do |transact|
|
|
188
|
+
# @type var lock_state: [Integer, Array[String], Array[[String, Float]]]
|
|
189
|
+
lock_state = rconn.pipelined do |pipeline|
|
|
190
|
+
pipeline.call('EXISTS', lock_key)
|
|
191
|
+
pipeline.call('TIME')
|
|
192
|
+
pipeline.call('ZRANGE', lock_readers_key, '0', '-1', 'WITHSCORES')
|
|
193
|
+
end
|
|
194
|
+
redis_time = RedisQueuedLocks::Resource.redis_time_ms(lock_state[1])
|
|
195
|
+
|
|
196
|
+
# NOTE: no write lock => extend all live read locks
|
|
197
|
+
if lock_state[0] == 0
|
|
198
|
+
registry_ttl = 0
|
|
199
|
+
|
|
200
|
+
lock_state[2].each do |(acquirer_id, read_lock_expiration)|
|
|
201
|
+
next unless read_lock_expiration > redis_time
|
|
202
|
+
|
|
203
|
+
read_lock_key =
|
|
204
|
+
RedisQueuedLocks::Resource.prepare_read_lock_key(lock_name, acquirer_id)
|
|
205
|
+
extended_ttl = (read_lock_expiration + milliseconds - redis_time).ceil
|
|
206
|
+
registry_ttl = extended_ttl if extended_ttl > registry_ttl
|
|
207
|
+
extended_read_locks_count += 1
|
|
208
|
+
|
|
209
|
+
transact.call('ZADD', lock_readers_key, 'XX', 'INCR', milliseconds, acquirer_id)
|
|
210
|
+
transact.call('PEXPIRE', read_lock_key, extended_ttl, 'NX')
|
|
211
|
+
transact.call('PEXPIRE', read_lock_key, extended_ttl, 'GT')
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
if extended_read_locks_count > 0
|
|
215
|
+
transact.call('PEXPIRE', lock_readers_key, registry_ttl, 'NX')
|
|
216
|
+
transact.call('PEXPIRE', lock_readers_key, registry_ttl, 'GT')
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# NOTE:
|
|
223
|
+
# - [] => there are no live read locks (nothing to extend);
|
|
224
|
+
# - nil => the write lock is changed during the extension (read locks can be expired);
|
|
225
|
+
# - result[index * 3] => the new read lock expiration of each extended read lock
|
|
226
|
+
# (nil if the read lock is released during the extension => it is not counted);
|
|
227
|
+
extended_locks_count =
|
|
228
|
+
if result.is_a?(::Array)
|
|
229
|
+
extended_read_locks_count.times.count { |index| result[index * 3] != nil }
|
|
230
|
+
else
|
|
231
|
+
0
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
if extended_locks_count > 0
|
|
235
|
+
{ ok: true, result: { extended_locks_count: } }
|
|
60
236
|
else
|
|
61
237
|
{ ok: false, result: :async_expire_or_no_lock }
|
|
62
238
|
end
|
|
63
239
|
end
|
|
240
|
+
# rubocop:enable Metrics/MethodLength
|
|
64
241
|
end
|
|
242
|
+
# rubocop:enable Metrics/ClassLength
|
|
65
243
|
end
|
|
244
|
+
# rubocop:enable Metrics/ModuleLength
|