redis_queued_locks 1.17.0 → 1.19.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 +87 -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 +68 -0
- data/CLAUDE.md +7 -2
- data/README.md +427 -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 +143 -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 +2 -2
- 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 +23 -2
- data/sig/redis_queued_locks/resource.rbs +9 -2
- data/sig/redis_queued_locks/swarm/zombie_info.rbs +5 -0
- metadata +4 -1
|
@@ -2,7 +2,10 @@
|
|
|
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::Locks
|
|
8
|
+
# rubocop:disable Metrics/ClassLength
|
|
6
9
|
class << self
|
|
7
10
|
# @param redis_client [RedisClient]
|
|
8
11
|
# @option scan_size [Integer]
|
|
@@ -23,9 +26,11 @@ module RedisQueuedLocks::Acquirer::Locks
|
|
|
23
26
|
# @param redis_client [RedisClient]
|
|
24
27
|
# @param scan_size [Integer]
|
|
25
28
|
# @return [Set<String>]
|
|
29
|
+
# - (RW) read locks are represented by their lock keys (`rql:lock:<lock_name>`) too;
|
|
26
30
|
#
|
|
27
31
|
# @api private
|
|
28
32
|
# @since 1.0.0
|
|
33
|
+
# @version 1.18.0
|
|
29
34
|
def scan_locks(redis_client, scan_size)
|
|
30
35
|
Set.new.tap do |lock_keys|
|
|
31
36
|
redis_client.scan(
|
|
@@ -36,19 +41,33 @@ module RedisQueuedLocks::Acquirer::Locks
|
|
|
36
41
|
# TODO: reduce unnecessary iterations
|
|
37
42
|
lock_keys.add(lock_key)
|
|
38
43
|
end
|
|
44
|
+
|
|
45
|
+
# NOTE: (RW) read locks (readers registries can contain expired read locks only)
|
|
46
|
+
redis_client.scan(
|
|
47
|
+
'MATCH',
|
|
48
|
+
RedisQueuedLocks::Resource::LOCK_READERS_PATTERN,
|
|
49
|
+
count: scan_size
|
|
50
|
+
) do |lock_readers_key|
|
|
51
|
+
# TODO: reduce unnecessary iterations
|
|
52
|
+
lock_keys.add(RedisQueuedLocks::Resource.lock_key_from_readers(lock_readers_key))
|
|
53
|
+
end
|
|
39
54
|
end
|
|
40
55
|
end
|
|
41
56
|
|
|
57
|
+
# NOTE:
|
|
58
|
+
# - the lock info is extracted by the module itself with the same approach as in `LockInfo`:
|
|
59
|
+
# public operation modules do not reuse each other (they share logic via `Resource`,
|
|
60
|
+
# `Utilities`), so the similar logic is duplicated on purpose;
|
|
61
|
+
#
|
|
42
62
|
# @param redis_client [RedisClient]
|
|
43
63
|
# @param lock_keys [Set<String>]
|
|
44
64
|
# @return [Set<Hash<Symbol,Any>>]
|
|
45
65
|
#
|
|
46
66
|
# @api private
|
|
47
67
|
# @since 1.0.0
|
|
48
|
-
# @version 1.
|
|
68
|
+
# @version 1.18.0
|
|
49
69
|
# rubocop:disable Metrics/MethodLength
|
|
50
70
|
def extract_locks_info(redis_client, lock_keys)
|
|
51
|
-
# TODO: refactor with RedisQueuedLocks::Acquier::LockInfo
|
|
52
71
|
Set.new.tap do |seeded_locks|
|
|
53
72
|
# rubocop:disable Layout/LineLength
|
|
54
73
|
# @type var seeded_locks: Set[{ lock: String, status: :released|:alive, info: Hash[String,untyped] }]
|
|
@@ -58,10 +77,16 @@ module RedisQueuedLocks::Acquirer::Locks
|
|
|
58
77
|
lock_keys.each do |lock_key|
|
|
59
78
|
# Step 1: extract lock info from redis
|
|
60
79
|
|
|
61
|
-
#
|
|
80
|
+
# NOTE: (RW) readers registry of the lock
|
|
81
|
+
lock_name = lock_key.delete_prefix('rql:lock:')
|
|
82
|
+
lock_readers_key = RedisQueuedLocks::Resource.prepare_lock_readers(lock_name)
|
|
83
|
+
|
|
84
|
+
# @type var lock_info: RedisQueuedLocks::Acquirer::Locks::lockInfo
|
|
62
85
|
lock_info = redis_client.pipelined do |pipeline|
|
|
63
86
|
pipeline.call('HGETALL', lock_key)
|
|
64
87
|
pipeline.call('PTTL', lock_key)
|
|
88
|
+
pipeline.call('TIME')
|
|
89
|
+
pipeline.call('ZRANGE', lock_readers_key, '0', '-1', 'WITHSCORES')
|
|
65
90
|
end.yield_self do |result| # Step 2: format the result
|
|
66
91
|
# Step 2.X: lock is released
|
|
67
92
|
if result == nil
|
|
@@ -73,16 +98,22 @@ module RedisQueuedLocks::Acquirer::Locks
|
|
|
73
98
|
# - result[1] (PTTL) (Integer)
|
|
74
99
|
# => (without any mutation, integer is atomic)
|
|
75
100
|
|
|
76
|
-
#
|
|
101
|
+
# rubocop:disable Layout/LineLength
|
|
102
|
+
# @type var result: [Hash[String,String|Float|Integer],Integer,Array[String],Array[[String, Float]]]
|
|
103
|
+
# rubocop:enable Layout/LineLength
|
|
77
104
|
hget_cmd_res = result[0] # NOTE: HGETALL result (hash)
|
|
78
105
|
pttl_cmd_res = result[1] # NOTE: PTTL result (integer)
|
|
79
106
|
|
|
80
|
-
# Step 2.Y: lock is released
|
|
107
|
+
# Step 2.Y: write lock is released
|
|
81
108
|
if hget_cmd_res == {} || pttl_cmd_res == -2 # NOTE: key does not exist
|
|
82
|
-
|
|
109
|
+
# NOTE: (RW) read locks (or nothing if there are no live read locks)
|
|
110
|
+
read_lock_info(
|
|
111
|
+
redis_client, lock_name, lock_key, result[2], result[3]
|
|
112
|
+
) || {} #: Hash[String,String|Float|Integer]
|
|
83
113
|
else
|
|
84
114
|
# Step 2.Z: lock is alive => format received info + add additional rem_ttl info
|
|
85
115
|
hget_cmd_res.tap do |lock_data|
|
|
116
|
+
lock_data['rw_mode'] = 'write'
|
|
86
117
|
lock_data['ts'] = Float(lock_data['ts'])
|
|
87
118
|
lock_data['ini_ttl'] = Integer(lock_data['ini_ttl'])
|
|
88
119
|
lock_data['rem_ttl'] = ((pttl_cmd_res == -1) ? Float::INFINITY : pttl_cmd_res)
|
|
@@ -109,5 +140,71 @@ module RedisQueuedLocks::Acquirer::Locks
|
|
|
109
140
|
end
|
|
110
141
|
end
|
|
111
142
|
# rubocop:enable Metrics/MethodLength
|
|
143
|
+
|
|
144
|
+
# Formats live read locks of the lock (expired read locks are ignored).
|
|
145
|
+
#
|
|
146
|
+
# NOTE: the same approach as in `LockInfo` (duplicated on purpose: operation modules are
|
|
147
|
+
# independent of each other, see `extract_locks_info`);
|
|
148
|
+
#
|
|
149
|
+
# @param redis_client [RedisClient]
|
|
150
|
+
# @param lock_name [String]
|
|
151
|
+
# @param lock_key [String]
|
|
152
|
+
# @param redis_time [Array<String>] Result of the redis `TIME` command.
|
|
153
|
+
# @param read_locks [Array<Array<String,Float>>] Readers registry (acquirer id, expiration).
|
|
154
|
+
# @return [Hash<String,String|Numeric|Array<Hash<String,String|Numeric>>>,NilClass]
|
|
155
|
+
# - `nil` is returned when there are no live read locks;
|
|
156
|
+
# - each reader has the same data as the write lock (acq_id, hst_id, ts, ini_ttl, rem_ttl,
|
|
157
|
+
# meta, reentrant lock data);
|
|
158
|
+
#
|
|
159
|
+
# @api private
|
|
160
|
+
# @since 1.18.0
|
|
161
|
+
# rubocop:disable Metrics/MethodLength
|
|
162
|
+
def read_lock_info(redis_client, lock_name, lock_key, redis_time, read_locks)
|
|
163
|
+
now = RedisQueuedLocks::Resource.redis_time_ms(redis_time)
|
|
164
|
+
live_read_locks = read_locks.select { |(_acquirer_id, expiration)| expiration > now }
|
|
165
|
+
return nil if live_read_locks.empty?
|
|
166
|
+
|
|
167
|
+
# NOTE: read lock data (it can be absent if it is dropped during the read lock life)
|
|
168
|
+
# @type var read_locks_data: Array[Hash[String,String]]
|
|
169
|
+
read_locks_data = redis_client.pipelined do |pipeline|
|
|
170
|
+
live_read_locks.each do |(acquirer_id, _expiration)|
|
|
171
|
+
pipeline.call(
|
|
172
|
+
'HGETALL',
|
|
173
|
+
RedisQueuedLocks::Resource.prepare_read_lock_key(lock_name, acquirer_id)
|
|
174
|
+
)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
longest_rem_ttl = 0
|
|
179
|
+
readers = live_read_locks.each_with_index.map do |(acquirer_id, expiration), index|
|
|
180
|
+
rem_ttl = (expiration - now).ceil
|
|
181
|
+
longest_rem_ttl = rem_ttl if rem_ttl > longest_rem_ttl
|
|
182
|
+
|
|
183
|
+
# @type var reader: Hash[String,String|Float|Integer|nil]
|
|
184
|
+
reader = read_locks_data[index] || {}
|
|
185
|
+
reader['acq_id'] = acquirer_id
|
|
186
|
+
reader['hst_id'] ||= RedisQueuedLocks::Resource.host_identifier_from_acquirer(acquirer_id)
|
|
187
|
+
reader['ts'] = Float(reader['ts']) if reader['ts']
|
|
188
|
+
reader['ini_ttl'] = Integer(reader['ini_ttl']) if reader['ini_ttl']
|
|
189
|
+
reader['rem_ttl'] = rem_ttl
|
|
190
|
+
reader['spc_cnt'] = Integer(reader['spc_cnt']) if reader['spc_cnt']
|
|
191
|
+
reader['l_spc_ts'] = Float(reader['l_spc_ts']) if reader['l_spc_ts']
|
|
192
|
+
reader['spc_ext_ttl'] = Integer(reader['spc_ext_ttl']) if reader['spc_ext_ttl']
|
|
193
|
+
reader['l_spc_ext_ini_ttl'] =
|
|
194
|
+
Integer(reader['l_spc_ext_ini_ttl']) if reader['l_spc_ext_ini_ttl']
|
|
195
|
+
reader['l_spc_ext_ts'] = Float(reader['l_spc_ext_ts']) if reader['l_spc_ext_ts']
|
|
196
|
+
reader
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
{
|
|
200
|
+
'lock_key' => lock_key,
|
|
201
|
+
'rw_mode' => 'read',
|
|
202
|
+
'rem_ttl' => longest_rem_ttl,
|
|
203
|
+
'readers' => readers
|
|
204
|
+
}
|
|
205
|
+
end
|
|
206
|
+
# rubocop:enable Metrics/MethodLength
|
|
112
207
|
end
|
|
208
|
+
# rubocop:enable Metrics/ClassLength
|
|
113
209
|
end
|
|
210
|
+
# rubocop:enable Metrics/ModuleLength
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# @api private
|
|
4
4
|
# @since 1.0.0
|
|
5
|
+
# @version 1.18.0
|
|
5
6
|
module RedisQueuedLocks::Acquirer::QueueInfo
|
|
6
7
|
class << self
|
|
7
8
|
# Returns an information about the required lock queue by the lock name. The result
|
|
@@ -12,41 +13,69 @@ module RedisQueuedLocks::Acquirer::QueueInfo
|
|
|
12
13
|
#
|
|
13
14
|
# @param redis_client [RedisClient]
|
|
14
15
|
# @param lock_name [String]
|
|
15
|
-
# @return [Hash<String,Array<Hash<String,String|Numeric>>,NilClass]
|
|
16
|
-
# - `nil` is returned when lock
|
|
16
|
+
# @return [Hash<String,String|Array<Hash<String,String|Numeric>>,NilClass]
|
|
17
|
+
# - `nil` is returned when lock queues (write and read) do not exist;
|
|
17
18
|
# - result format: {
|
|
18
19
|
# "lock_queue" => "rql:lock_queue:your_lock_name", # lock queue key in redis,
|
|
19
20
|
# "queue" => [
|
|
20
|
-
# { "acq_id" => "rql:acq:123/456/789/987/
|
|
21
|
-
# { "acq_id" => "rql:acq:123/686/789/987/
|
|
21
|
+
# { "acq_id" => "rql:acq:123/456/789/987/id", "score" => 123.456, "rw_mode" => "write" },
|
|
22
|
+
# { "acq_id" => "rql:acq:123/686/789/987/id", "score" => 456.789, "rw_mode" => "write" },
|
|
22
23
|
# ...
|
|
23
|
-
# ] # ordered set (by score) with information about an acquirer
|
|
24
|
+
# ] # ordered set (by score) with information about an acquirer, their position in queue
|
|
25
|
+
# # and the requested lock mode
|
|
26
|
+
# <additional keys when the queue of read lock requests exists>:
|
|
27
|
+
# "read_lock_queue" => "rql:lock_read_queue:your_lock_name", # read lock queue key in redis
|
|
28
|
+
# "read_queue" => [
|
|
29
|
+
# { "acq_id" => "rql:acq:123/456/789/987/id", "score" => 123.456, "rw_mode" => "read" },
|
|
30
|
+
# ...
|
|
31
|
+
# ] # read lock requests (positions are comparable with the write lock request positions)
|
|
24
32
|
# }
|
|
25
33
|
#
|
|
26
34
|
# @api private
|
|
27
35
|
# @since 1.0.0
|
|
36
|
+
# @version 1.18.0
|
|
37
|
+
# rubocop:disable Metrics/MethodLength
|
|
28
38
|
def queue_info(redis_client, lock_name)
|
|
29
39
|
lock_key_queue = RedisQueuedLocks::Resource.prepare_lock_queue(lock_name)
|
|
40
|
+
read_lock_key_queue = RedisQueuedLocks::Resource.prepare_read_lock_queue(lock_name)
|
|
30
41
|
|
|
31
42
|
result = redis_client.pipelined do |pipeline|
|
|
32
43
|
pipeline.call('EXISTS', lock_key_queue)
|
|
33
44
|
pipeline.call('ZRANGE', lock_key_queue, '0', '-1', 'WITHSCORES')
|
|
45
|
+
pipeline.call('EXISTS', read_lock_key_queue)
|
|
46
|
+
pipeline.call('ZRANGE', read_lock_key_queue, '0', '-1', 'WITHSCORES')
|
|
34
47
|
end
|
|
35
48
|
|
|
36
|
-
#
|
|
49
|
+
# rubocop:disable Layout/LineLength
|
|
50
|
+
# @type var result: [Integer,Array[[String,Integer|Float]],Integer,Array[[String,Integer|Float]]]
|
|
51
|
+
# rubocop:enable Layout/LineLength
|
|
37
52
|
exists_cmd_res = result[0]
|
|
38
53
|
zrange_cmd_res = result[1]
|
|
54
|
+
read_exists_cmd_res = result[2]
|
|
55
|
+
read_zrange_cmd_res = result[3]
|
|
39
56
|
|
|
40
|
-
if exists_cmd_res == 1
|
|
57
|
+
if exists_cmd_res == 1 || read_exists_cmd_res == 1
|
|
41
58
|
# NOTE: queue existed during the piepline invocation
|
|
42
|
-
|
|
59
|
+
# @type var queue_info: RedisQueuedLocks::Acquirer::QueueInfo::queueInfo
|
|
60
|
+
queue_info = {
|
|
43
61
|
'lock_queue' => lock_key_queue,
|
|
44
|
-
'queue' => zrange_cmd_res.map
|
|
62
|
+
'queue' => zrange_cmd_res.map do |val|
|
|
63
|
+
{ 'acq_id' => val[0], 'score' => val[1], 'rw_mode' => 'write' }
|
|
64
|
+
end
|
|
45
65
|
}
|
|
66
|
+
# NOTE: (RW) the queue of read lock requests
|
|
67
|
+
if read_exists_cmd_res == 1
|
|
68
|
+
queue_info['read_lock_queue'] = read_lock_key_queue
|
|
69
|
+
queue_info['read_queue'] = read_zrange_cmd_res.map do |val|
|
|
70
|
+
{ 'acq_id' => val[0], 'score' => val[1], 'rw_mode' => 'read' }
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
queue_info
|
|
46
74
|
else
|
|
47
75
|
# NOTE: queue did not exist during the pipeline invocation
|
|
48
76
|
nil
|
|
49
77
|
end
|
|
50
78
|
end
|
|
79
|
+
# rubocop:enable Metrics/MethodLength
|
|
51
80
|
end
|
|
52
81
|
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# @api private
|
|
4
4
|
# @since 1.0.0
|
|
5
|
+
# @version 1.18.0
|
|
5
6
|
module RedisQueuedLocks::Acquirer::Queues
|
|
6
7
|
class << self
|
|
7
8
|
# @param redis_client [RedisClient]
|
|
@@ -23,9 +24,11 @@ module RedisQueuedLocks::Acquirer::Queues
|
|
|
23
24
|
# @param redis_client [RedisClient]
|
|
24
25
|
# @param scan_size [Integer]
|
|
25
26
|
# @return [Set<String>]
|
|
27
|
+
# - (RW) queues of read lock requests are included;
|
|
26
28
|
#
|
|
27
29
|
# @api private
|
|
28
30
|
# @since 1.0.0
|
|
31
|
+
# @version 1.18.0
|
|
29
32
|
def scan_queues(redis_client, scan_size)
|
|
30
33
|
Set.new.tap do |lock_queues|
|
|
31
34
|
# @type var lock_queues: Set[String]
|
|
@@ -38,21 +41,40 @@ module RedisQueuedLocks::Acquirer::Queues
|
|
|
38
41
|
# @type var lock_queue: String
|
|
39
42
|
lock_queues.add(lock_queue)
|
|
40
43
|
end
|
|
44
|
+
|
|
45
|
+
# NOTE: (RW) queues of read lock requests
|
|
46
|
+
redis_client.scan(
|
|
47
|
+
'MATCH',
|
|
48
|
+
RedisQueuedLocks::Resource::READ_LOCK_QUEUE_PATTERN,
|
|
49
|
+
count: scan_size
|
|
50
|
+
) do |read_lock_queue|
|
|
51
|
+
# TODO: reduce unnecessary iterations
|
|
52
|
+
# @type var read_lock_queue: String
|
|
53
|
+
lock_queues.add(read_lock_queue)
|
|
54
|
+
end
|
|
41
55
|
end
|
|
42
56
|
end
|
|
43
57
|
|
|
44
58
|
# @param redis_client [RedisClient]
|
|
45
59
|
# @param lock_queues [Set<String>]
|
|
46
60
|
# @return [Set<Hash<Symbol,Any>>]
|
|
61
|
+
# - each request: `{ 'acq_id' => String, 'score' => Float, 'rw_mode' => 'read'/'write' }`;
|
|
47
62
|
#
|
|
48
63
|
# @api private
|
|
49
64
|
# @since 1.0.0
|
|
65
|
+
# @version 1.18.0
|
|
50
66
|
def extract_queues_info(redis_client, lock_queues)
|
|
51
|
-
#
|
|
67
|
+
# NOTE: the same approach as in `QueueInfo` (duplicated on purpose: public operation modules
|
|
68
|
+
# do not reuse each other, they share logic via `Resource`, `Utilities`);
|
|
52
69
|
Set.new.tap do |seeded_queues|
|
|
53
70
|
# Step X: iterate over each lock queue and extract their info
|
|
54
71
|
# @type var seeded_queues: Set[Hash[Symbol,untyped]]
|
|
55
72
|
lock_queues.each do |lock_queue|
|
|
73
|
+
# NOTE:
|
|
74
|
+
# - (RW) the mode of the requests of the queue (`'read'` or `'write'`);
|
|
75
|
+
# - lock queues are scanned by the lock queue patterns, so the mode is never `nil`;
|
|
76
|
+
rw_mode = RedisQueuedLocks::Resource.lock_queue_rw_mode(lock_queue) #: String
|
|
77
|
+
|
|
56
78
|
# Step 1: extract lock queue info from reids
|
|
57
79
|
queue_info = redis_client.pipelined do |pipeline|
|
|
58
80
|
pipeline.call('EXISTS', lock_queue)
|
|
@@ -64,14 +86,16 @@ module RedisQueuedLocks::Acquirer::Queues
|
|
|
64
86
|
zrange_cmd_res = result[1]
|
|
65
87
|
|
|
66
88
|
if exists_cmd_res == 1 # Step 2.X: lock queue existed during the piepline invocation
|
|
67
|
-
zrange_cmd_res.map
|
|
89
|
+
zrange_cmd_res.map do |val|
|
|
90
|
+
{ 'acq_id' => val[0], 'score' => val[1], 'rw_mode' => rw_mode }
|
|
91
|
+
end
|
|
68
92
|
else
|
|
69
93
|
# Step 2.Y: lock queue did not exist during the pipeline invocation
|
|
70
|
-
[] #: Array[Hash[String,Float]]
|
|
94
|
+
[] #: Array[Hash[String,String|Float]]
|
|
71
95
|
end
|
|
72
96
|
end
|
|
73
97
|
|
|
74
|
-
# @type var queue_info: Array[Hash[String,Float]]
|
|
98
|
+
# @type var queue_info: Array[Hash[String,String|Float]]
|
|
75
99
|
|
|
76
100
|
# Step 3: push the lock queue info to the result store
|
|
77
101
|
seeded_queues << {
|
|
@@ -10,6 +10,7 @@ module RedisQueuedLocks::Acquirer::ReleaseAllLocks
|
|
|
10
10
|
# Release all locks:
|
|
11
11
|
# - 1. clear all lock queus: drop them all from Redis database by the lock queue pattern;
|
|
12
12
|
# - 2. delete all locks: drop lock keys from Redis by the lock key pattern;
|
|
13
|
+
# - 3. (RW) clear all read lock queues and drop all read locks (readers registries and data);
|
|
13
14
|
#
|
|
14
15
|
# @param redis [RedisClient]
|
|
15
16
|
# Redis connection client.
|
|
@@ -71,7 +72,7 @@ module RedisQueuedLocks::Acquirer::ReleaseAllLocks
|
|
|
71
72
|
#
|
|
72
73
|
# @api private
|
|
73
74
|
# @since 1.0.0
|
|
74
|
-
# @version 1.
|
|
75
|
+
# @version 1.18.0
|
|
75
76
|
def release_all_locks(
|
|
76
77
|
redis,
|
|
77
78
|
batch_size,
|
|
@@ -129,6 +130,8 @@ module RedisQueuedLocks::Acquirer::ReleaseAllLocks
|
|
|
129
130
|
#
|
|
130
131
|
# @api private
|
|
131
132
|
# @since 1.0.0
|
|
133
|
+
# @version 1.18.0
|
|
134
|
+
# rubocop:disable Metrics/MethodLength
|
|
132
135
|
def fully_release_all_locks(redis, batch_size)
|
|
133
136
|
result = redis.with do |rconn|
|
|
134
137
|
rconn.pipelined do |pipeline|
|
|
@@ -151,10 +154,41 @@ module RedisQueuedLocks::Acquirer::ReleaseAllLocks
|
|
|
151
154
|
# TODO: reduce unnecessary iterations
|
|
152
155
|
pipeline.call('EXPIRE', lock_key, '0')
|
|
153
156
|
end
|
|
157
|
+
|
|
158
|
+
# Step C (RW): release all read lock queues
|
|
159
|
+
rconn.scan(
|
|
160
|
+
'MATCH',
|
|
161
|
+
RedisQueuedLocks::Resource::READ_LOCK_QUEUE_PATTERN,
|
|
162
|
+
count: batch_size
|
|
163
|
+
) do |read_lock_queue|
|
|
164
|
+
# TODO: reduce unnecessary iterations
|
|
165
|
+
pipeline.call('EXPIRE', read_lock_queue, '0')
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Step D (RW): release all read locks
|
|
169
|
+
rconn.scan(
|
|
170
|
+
'MATCH',
|
|
171
|
+
RedisQueuedLocks::Resource::LOCK_READERS_PATTERN,
|
|
172
|
+
count: batch_size
|
|
173
|
+
) do |lock_readers|
|
|
174
|
+
# TODO: reduce unnecessary iterations
|
|
175
|
+
pipeline.call('EXPIRE', lock_readers, '0')
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Step E (RW): release all read lock data
|
|
179
|
+
rconn.scan(
|
|
180
|
+
'MATCH',
|
|
181
|
+
RedisQueuedLocks::Resource::READ_LOCK_PATTERN,
|
|
182
|
+
count: batch_size
|
|
183
|
+
) do |read_lock|
|
|
184
|
+
# TODO: reduce unnecessary iterations
|
|
185
|
+
pipeline.call('EXPIRE', read_lock, '0')
|
|
186
|
+
end
|
|
154
187
|
end
|
|
155
188
|
end
|
|
156
189
|
|
|
157
190
|
{ ok: true, result: { rel_key_cnt: result.sum } }
|
|
158
191
|
end
|
|
192
|
+
# rubocop:enable Metrics/MethodLength
|
|
159
193
|
end
|
|
160
194
|
end
|
|
@@ -11,6 +11,7 @@ module RedisQueuedLocks::Acquirer::ReleaseLock
|
|
|
11
11
|
# - 1. clear lock queue: al; related processes released
|
|
12
12
|
# from the lock aquierment and should retry;
|
|
13
13
|
# - 2. delete the lock: drop lock key from Redis;
|
|
14
|
+
# - 3. (RW) clear the read lock queue and drop all read locks (the readers registry);
|
|
14
15
|
# It is safe because the lock obtain logic is transactional and
|
|
15
16
|
# watches the original lock for changes.
|
|
16
17
|
#
|
|
@@ -71,7 +72,7 @@ module RedisQueuedLocks::Acquirer::ReleaseLock
|
|
|
71
72
|
#
|
|
72
73
|
# @api private
|
|
73
74
|
# @since 1.0.0
|
|
74
|
-
# @version 1.
|
|
75
|
+
# @version 1.18.0
|
|
75
76
|
# rubocop:disable Metrics/MethodLength
|
|
76
77
|
def release_lock(
|
|
77
78
|
redis,
|
|
@@ -89,9 +90,15 @@ module RedisQueuedLocks::Acquirer::ReleaseLock
|
|
|
89
90
|
)
|
|
90
91
|
lock_key = RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
|
|
91
92
|
lock_key_queue = RedisQueuedLocks::Resource.prepare_lock_queue(lock_name)
|
|
93
|
+
read_lock_key_queue = RedisQueuedLocks::Resource.prepare_read_lock_queue(lock_name)
|
|
94
|
+
lock_readers_key = RedisQueuedLocks::Resource.prepare_lock_readers(lock_name)
|
|
92
95
|
|
|
93
96
|
rel_start_time = clock_gettime
|
|
94
|
-
|
|
97
|
+
# steep:ignore:start
|
|
98
|
+
fully_release_lock(
|
|
99
|
+
redis, lock_name, lock_key, lock_key_queue, read_lock_key_queue, lock_readers_key
|
|
100
|
+
) => { ok:, result: }
|
|
101
|
+
# steep:ignore:end
|
|
95
102
|
|
|
96
103
|
# @type var ok: bool
|
|
97
104
|
# @type var result: Hash[Symbol,Symbol]
|
|
@@ -131,36 +138,60 @@ module RedisQueuedLocks::Acquirer::ReleaseLock
|
|
|
131
138
|
|
|
132
139
|
private
|
|
133
140
|
|
|
134
|
-
# Realease the lock: clear the lock
|
|
141
|
+
# Realease the lock: clear the lock queues and expire the lock (write lock and read locks).
|
|
135
142
|
#
|
|
136
143
|
# @param redis [RedisClient]
|
|
144
|
+
# @param lock_name [String]
|
|
137
145
|
# @param lock_key [String]
|
|
138
146
|
# @param lock_key_queue [String]
|
|
147
|
+
# @param read_lock_key_queue [String]
|
|
148
|
+
# @param lock_readers_key [String]
|
|
139
149
|
# @return [Hash<Symbol,Boolean|Hash<Symbol,Symbol>>]
|
|
140
150
|
# Format: {
|
|
141
151
|
# ok: true/false,
|
|
142
152
|
# result: {
|
|
143
|
-
# queue: :released/:nothing_to_release,
|
|
144
|
-
# lock: :released/:nothing_to_release
|
|
153
|
+
# queue: :released/:nothing_to_release, # (any of the lock queues)
|
|
154
|
+
# lock: :released/:nothing_to_release # (write lock or read locks)
|
|
145
155
|
# }
|
|
146
156
|
# }
|
|
147
157
|
#
|
|
148
158
|
# @api private
|
|
149
159
|
# @since 1.0.0
|
|
150
|
-
|
|
151
|
-
|
|
160
|
+
# @version 1.18.0
|
|
161
|
+
def fully_release_lock(
|
|
162
|
+
redis,
|
|
163
|
+
lock_name,
|
|
164
|
+
lock_key,
|
|
165
|
+
lock_key_queue,
|
|
166
|
+
read_lock_key_queue,
|
|
167
|
+
lock_readers_key
|
|
168
|
+
)
|
|
169
|
+
# @type var result: [Integer,Integer,Integer,Integer]
|
|
152
170
|
result = redis.with do |rconn|
|
|
171
|
+
# NOTE: (RW) data of read locks (it is a data carrier only: read locks are released via
|
|
172
|
+
# the readers registry, so the data of concurrently obtained read locks expires itself)
|
|
173
|
+
# @type var read_lock_acquirers: Array[String]
|
|
174
|
+
read_lock_acquirers = rconn.call('ZRANGE', lock_readers_key, '0', '-1')
|
|
175
|
+
|
|
153
176
|
rconn.multi do |transact|
|
|
154
177
|
transact.call('ZREMRANGEBYSCORE', lock_key_queue, '-inf', '+inf')
|
|
155
178
|
transact.call('EXPIRE', lock_key, '0')
|
|
179
|
+
transact.call('ZREMRANGEBYSCORE', read_lock_key_queue, '-inf', '+inf')
|
|
180
|
+
transact.call('EXPIRE', lock_readers_key, '0')
|
|
181
|
+
read_lock_acquirers.each do |acquirer_id|
|
|
182
|
+
transact.call(
|
|
183
|
+
'DEL',
|
|
184
|
+
RedisQueuedLocks::Resource.prepare_read_lock_key(lock_name, acquirer_id)
|
|
185
|
+
)
|
|
186
|
+
end
|
|
156
187
|
end
|
|
157
188
|
end
|
|
158
189
|
|
|
159
190
|
{
|
|
160
191
|
ok: true,
|
|
161
192
|
result: {
|
|
162
|
-
queue: (result[0] != 0) ? :released : :nothing_to_release,
|
|
163
|
-
lock: (result[1] != 0) ? :released : :nothing_to_release
|
|
193
|
+
queue: (result[0] != 0 || result[2] != 0) ? :released : :nothing_to_release,
|
|
194
|
+
lock: (result[1] != 0 || result[3] != 0) ? :released : :nothing_to_release
|
|
164
195
|
}
|
|
165
196
|
}
|
|
166
197
|
end
|
|
@@ -2,12 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
# @api private
|
|
4
4
|
# @since 1.14.0
|
|
5
|
+
# @version 1.18.0
|
|
6
|
+
# rubocop:disable Metrics/ModuleLength
|
|
5
7
|
module RedisQueuedLocks::Acquirer::ReleaseLocksOf
|
|
6
8
|
# @since 1.14.0
|
|
7
9
|
extend RedisQueuedLocks::Utilities
|
|
8
10
|
|
|
11
|
+
# rubocop:disable Metrics/ClassLength
|
|
9
12
|
class << self
|
|
10
|
-
# Release all queues and locks that belong to the given host
|
|
13
|
+
# Release all queues and locks (write locks and read locks) that belong to the given host
|
|
14
|
+
# and its associated acquirer.
|
|
11
15
|
#
|
|
12
16
|
# @param refused_host_id [String]
|
|
13
17
|
# A host whose locks and queues should be released.
|
|
@@ -84,7 +88,7 @@ module RedisQueuedLocks::Acquirer::ReleaseLocksOf
|
|
|
84
88
|
#
|
|
85
89
|
# @api private
|
|
86
90
|
# @since 1.14.0
|
|
87
|
-
# @version 1.
|
|
91
|
+
# @version 1.18.0
|
|
88
92
|
# rubocop:disable Metrics/MethodLength
|
|
89
93
|
def release_locks_of(
|
|
90
94
|
refused_host_id,
|
|
@@ -164,6 +168,7 @@ module RedisQueuedLocks::Acquirer::ReleaseLocksOf
|
|
|
164
168
|
#
|
|
165
169
|
# @api private
|
|
166
170
|
# @since 1.14.0
|
|
171
|
+
# @version 1.18.0
|
|
167
172
|
# rubocop:disable Metrics/MethodLength
|
|
168
173
|
def fully_release_locks_of(
|
|
169
174
|
refused_host_id,
|
|
@@ -203,6 +208,30 @@ module RedisQueuedLocks::Acquirer::ReleaseLocksOf
|
|
|
203
208
|
rel_key_cnt += refused_locks.size
|
|
204
209
|
end
|
|
205
210
|
|
|
211
|
+
# Step A (RW): drop read locks of the passed host/acquirer
|
|
212
|
+
# - read locks store acquirer ids only (the host is a part of the acquirer id);
|
|
213
|
+
if RedisQueuedLocks::Resource.host_identifier_from_acquirer(refused_acquirer_id) ==
|
|
214
|
+
refused_host_id
|
|
215
|
+
rconn.scan(
|
|
216
|
+
'MATCH',
|
|
217
|
+
RedisQueuedLocks::Resource::LOCK_READERS_PATTERN,
|
|
218
|
+
count: lock_scan_size
|
|
219
|
+
) do |lock_readers_key|
|
|
220
|
+
res = rconn.call('ZREM', lock_readers_key, refused_acquirer_id)
|
|
221
|
+
next if res == 0
|
|
222
|
+
|
|
223
|
+
rel_key_cnt += 1
|
|
224
|
+
# NOTE: drop the read lock data
|
|
225
|
+
rconn.call(
|
|
226
|
+
'DEL',
|
|
227
|
+
RedisQueuedLocks::Resource.prepare_read_lock_key(
|
|
228
|
+
RedisQueuedLocks::Resource.lock_name_from_readers(lock_readers_key),
|
|
229
|
+
refused_acquirer_id
|
|
230
|
+
)
|
|
231
|
+
)
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
206
235
|
# Step B: drop passed host/acquirer from lock queues
|
|
207
236
|
rconn.scan(
|
|
208
237
|
'MATCH',
|
|
@@ -212,10 +241,21 @@ module RedisQueuedLocks::Acquirer::ReleaseLocksOf
|
|
|
212
241
|
res = rconn.call('ZREM', lock_queue, refused_acquirer_id)
|
|
213
242
|
tch_queue_cnt += 1 if res != 0
|
|
214
243
|
end
|
|
244
|
+
|
|
245
|
+
# Step B (RW): drop passed host/acquirer from read lock queues
|
|
246
|
+
rconn.scan(
|
|
247
|
+
'MATCH',
|
|
248
|
+
RedisQueuedLocks::Resource::READ_LOCK_QUEUE_PATTERN,
|
|
249
|
+
count: queue_scan_size
|
|
250
|
+
) do |read_lock_queue|
|
|
251
|
+
res = rconn.call('ZREM', read_lock_queue, refused_acquirer_id)
|
|
252
|
+
tch_queue_cnt += 1 if res != 0
|
|
253
|
+
end
|
|
215
254
|
end
|
|
216
255
|
|
|
217
256
|
{ ok: true, result: { rel_key_cnt:, tch_queue_cnt: } }
|
|
218
257
|
end
|
|
219
258
|
end
|
|
220
|
-
# rubocop:enable Metrics/MethodLength
|
|
259
|
+
# rubocop:enable Metrics/MethodLength, Metrics/ClassLength
|
|
221
260
|
end
|
|
261
|
+
# rubocop:enable Metrics/ModuleLength
|