redis_queued_locks 1.16.2 → 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 +281 -0
- data/.claude/rules/acquirer.md +81 -0
- data/.claude/rules/arguments.md +60 -0
- data/.claude/rules/git.md +46 -0
- data/.claude/rules/logic.md +63 -0
- data/.claude/rules/swarm.md +120 -0
- data/.claude/rules/tests.md +48 -0
- data/.claude/rules/type-checking.md +50 -0
- data/.claude/rules/visitors.md +69 -0
- data/.rubocop.rbs.yml +31 -0
- data/.rubocop.yml +3 -3
- data/.ruby-version +1 -1
- data/CHANGELOG.md +80 -0
- data/CLAUDE.md +36 -0
- data/README.md +367 -82
- data/Rakefile +31 -2
- data/github_ci/.keep +0 -0
- data/lib/redis_queued_locks/acquirer/acquire_lock/delay_execution.rb +2 -2
- 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 -132
- 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 +33 -9
- data/lib/redis_queued_locks/acquirer/acquire_lock.rb +73 -24
- 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 -4
- data/lib/redis_queued_locks/acquirer/lock_series_poc/log_visitor.rb +18 -7
- data/lib/redis_queued_locks/acquirer/lock_series_poc.rb +126 -29
- 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 +274 -158
- data/lib/redis_queued_locks/config/dsl.rb +4 -10
- data/lib/redis_queued_locks/resource.rb +117 -9
- data/lib/redis_queued_locks/swarm/flush_zombies.rb +46 -6
- data/lib/redis_queued_locks/swarm/supervisor.rb +1 -1
- data/lib/redis_queued_locks/swarm/swarm_element/isolated.rb +166 -44
- data/lib/redis_queued_locks/swarm/swarm_element/threaded.rb +45 -47
- 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/redis_queued_locks.gemspec +1 -1
- 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/flush_zombies.rbs +1 -1
- data/sig/redis_queued_locks/swarm/swarm_element/isolated.rbs +13 -5
- data/sig/redis_queued_locks/swarm/swarm_element/threaded.rbs +6 -6
- data/sig/redis_queued_locks/swarm/zombie_info.rbs +5 -0
- metadata +17 -5
- data/github_ci/ruby3.3.gemfile +0 -17
- data/github_ci/ruby3.3.gemfile.lock +0 -216
|
@@ -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
|
|
@@ -2,17 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
# @api private
|
|
4
4
|
# @since 1.0.0
|
|
5
|
+
# @version 1.18.0
|
|
5
6
|
module RedisQueuedLocks::Acquirer::IsLocked
|
|
6
7
|
class << self
|
|
7
8
|
# @param redis_client [RedisClient]
|
|
8
9
|
# @param lock_name [String]
|
|
9
|
-
# @return [Boolean]
|
|
10
|
+
# @return [Boolean] Is the lock obtained by a writer or by any reader (live read lock).
|
|
10
11
|
#
|
|
11
12
|
# @api private
|
|
12
13
|
# @since 1.0.0
|
|
14
|
+
# @version 1.18.0
|
|
13
15
|
def locked?(redis_client, lock_name)
|
|
14
16
|
lock_key = RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
|
|
15
|
-
|
|
17
|
+
lock_readers_key = RedisQueuedLocks::Resource.prepare_lock_readers(lock_name)
|
|
18
|
+
|
|
19
|
+
# @type var result: [Integer, Array[String], Array[[String, Float]]]
|
|
20
|
+
result = redis_client.pipelined do |pipeline|
|
|
21
|
+
pipeline.call('EXISTS', lock_key)
|
|
22
|
+
pipeline.call('TIME')
|
|
23
|
+
pipeline.call('ZRANGE', lock_readers_key, '-1', '-1', 'WITHSCORES')
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# NOTE: write lock
|
|
27
|
+
return true if result[0] == 1
|
|
28
|
+
|
|
29
|
+
# NOTE: (RW) read lock (expired read locks can live in the readers registry till the cleanup)
|
|
30
|
+
longest_read_lock = result[2].first
|
|
31
|
+
longest_read_lock != nil &&
|
|
32
|
+
longest_read_lock[1] > RedisQueuedLocks::Resource.redis_time_ms(result[1])
|
|
16
33
|
end
|
|
17
34
|
end
|
|
18
35
|
end
|
|
@@ -2,17 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
# @api private
|
|
4
4
|
# @since 1.0.0
|
|
5
|
+
# @version 1.18.0
|
|
5
6
|
module RedisQueuedLocks::Acquirer::IsQueued
|
|
6
7
|
class << self
|
|
7
8
|
# @param redis_client [RedisClient]
|
|
8
9
|
# @param lock_name [String]
|
|
9
|
-
# @return [Boolean]
|
|
10
|
+
# @return [Boolean] Are there any write or read lock requests.
|
|
10
11
|
#
|
|
11
12
|
# @api private
|
|
12
13
|
# @since 1.0.0
|
|
14
|
+
# @version 1.18.0
|
|
13
15
|
def queued?(redis_client, lock_name)
|
|
14
16
|
lock_key_queue = RedisQueuedLocks::Resource.prepare_lock_queue(lock_name)
|
|
15
|
-
|
|
17
|
+
read_lock_key_queue = RedisQueuedLocks::Resource.prepare_read_lock_queue(lock_name)
|
|
18
|
+
# NOTE: EXISTS returns the number of existing keys
|
|
19
|
+
redis_client.call('EXISTS', lock_key_queue, read_lock_key_queue) > 0
|
|
16
20
|
end
|
|
17
21
|
end
|
|
18
22
|
end
|
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
# @api private
|
|
4
4
|
# @since 1.0.0
|
|
5
|
+
# @version 1.18.0
|
|
5
6
|
module RedisQueuedLocks::Acquirer::LockInfo
|
|
6
7
|
class << self
|
|
7
8
|
# @param redis_client [RedisClient]
|
|
8
9
|
# @param lock_name [String]
|
|
9
|
-
# @return [Hash<String,String|Numeric
|
|
10
|
-
# - `nil` is returned when lock key does not exist or expired;
|
|
11
|
-
# - result format: {
|
|
10
|
+
# @return [Hash<String,String|Numeric|Array<Hash<String,String|Numeric>>>,NilClass]
|
|
11
|
+
# - `nil` is returned when lock key does not exist or expired (and there are no read locks);
|
|
12
|
+
# - result format (write lock): {
|
|
12
13
|
# 'lock_key' => "rql:lock:your_lockname", # acquired lock key
|
|
14
|
+
# 'rw_mode' => 'write', # lock mode
|
|
13
15
|
# 'acq_id' => "rql:acq:123/456/789/987/uniqstring", # lock acquirer identifier
|
|
14
16
|
# 'hst_id' => "rql:hst:123/456/987/uniqstring", # lock host identifier
|
|
15
17
|
# 'ts' => 123456789.2649841, # <locked at> time stamp (epoch, seconds.microseconds)
|
|
@@ -22,17 +24,36 @@ module RedisQueuedLocks::Acquirer::LockInfo
|
|
|
22
24
|
# 'l_spc_ext_ini_ttl' => 5000, # (milliseconds) the last ttl of reentrant lock
|
|
23
25
|
# 'l_spc_ext_ts' => 123456.789 # (epoch) <extendable reentrant lock obtained at> timestamp
|
|
24
26
|
# }
|
|
27
|
+
# - result format (read locks, when there is no write lock): {
|
|
28
|
+
# 'lock_key' => "rql:lock:your_lockname", # lock key
|
|
29
|
+
# 'rw_mode' => 'read', # lock mode
|
|
30
|
+
# 'rem_ttl' => 123456789, # remaining ttl of the longest read lock (milliseconds)
|
|
31
|
+
# 'readers' => [ # live read locks
|
|
32
|
+
# {
|
|
33
|
+
# 'acq_id' => "rql:acq:123/456/789/987/uniqstring", # read lock acquirer identifier
|
|
34
|
+
# 'hst_id' => "rql:hst:123/456/987/uniqstring", # read lock host identifier
|
|
35
|
+
# 'ts' => 123456789.2649841, # <locked at> time stamp (epoch, seconds.microseconds)
|
|
36
|
+
# 'ini_ttl' => 123456789, # initial read lock ttl (milliseconds)
|
|
37
|
+
# 'rem_ttl' => 123456789, # remaining read lock ttl (milliseconds)
|
|
38
|
+
# <custom metadata (`meta`) and additional keys for reentrant locks (see above)>
|
|
39
|
+
# },
|
|
40
|
+
# ...
|
|
41
|
+
# ]
|
|
42
|
+
# }
|
|
25
43
|
#
|
|
26
44
|
# @api private
|
|
27
45
|
# @since 1.0.0
|
|
28
|
-
# @version 1.
|
|
46
|
+
# @version 1.18.0
|
|
29
47
|
# rubocop:disable Metrics/MethodLength
|
|
30
48
|
def lock_info(redis_client, lock_name)
|
|
31
49
|
lock_key = RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
|
|
50
|
+
lock_readers_key = RedisQueuedLocks::Resource.prepare_lock_readers(lock_name)
|
|
32
51
|
|
|
33
52
|
result = redis_client.pipelined do |pipeline|
|
|
34
53
|
pipeline.call('HGETALL', lock_key)
|
|
35
54
|
pipeline.call('PTTL', lock_key)
|
|
55
|
+
pipeline.call('TIME')
|
|
56
|
+
pipeline.call('ZRANGE', lock_readers_key, '0', '-1', 'WITHSCORES')
|
|
36
57
|
end
|
|
37
58
|
|
|
38
59
|
if result == nil
|
|
@@ -48,16 +69,22 @@ module RedisQueuedLocks::Acquirer::LockInfo
|
|
|
48
69
|
# => (will be mutated further with different value types)
|
|
49
70
|
# - result[1] (PTTL) (Integer)
|
|
50
71
|
# => (without any mutation, integer is atomic)
|
|
72
|
+
# - result[2] (TIME) (Array<String>) (redis time)
|
|
73
|
+
# - result[3] (ZRANGE) (Array<[String,Float]>) (read locks)
|
|
51
74
|
|
|
52
|
-
#
|
|
75
|
+
# rubocop:disable Layout/LineLength
|
|
76
|
+
# @type var result: [Hash[String,String|Float|Integer], Integer, Array[String], Array[[String, Float]]]
|
|
77
|
+
# rubocop:enable Layout/LineLength
|
|
53
78
|
hget_cmd_res = result[0]
|
|
54
79
|
pttl_cmd_res = result[1]
|
|
55
80
|
|
|
56
81
|
if hget_cmd_res == {} || pttl_cmd_res == -2 # NOTE: key does not exist
|
|
57
|
-
|
|
82
|
+
# NOTE: (RW) there is no write lock => check read locks
|
|
83
|
+
read_lock_info(redis_client, lock_name, lock_key, result[2], result[3])
|
|
58
84
|
else
|
|
59
85
|
hget_cmd_res.tap do |lock_data|
|
|
60
86
|
lock_data['lock_key'] = lock_key
|
|
87
|
+
lock_data['rw_mode'] = 'write'
|
|
61
88
|
lock_data['ts'] = Float(lock_data['ts'])
|
|
62
89
|
lock_data['ini_ttl'] = Integer(lock_data['ini_ttl'])
|
|
63
90
|
lock_data['rem_ttl'] = ((pttl_cmd_res == -1) ? Float::INFINITY : pttl_cmd_res)
|
|
@@ -73,5 +100,68 @@ module RedisQueuedLocks::Acquirer::LockInfo
|
|
|
73
100
|
end
|
|
74
101
|
end
|
|
75
102
|
# rubocop:enable Metrics/MethodLength
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
# Formats live read locks of the lock (expired read locks are ignored).
|
|
107
|
+
#
|
|
108
|
+
# @param redis_client [RedisClient]
|
|
109
|
+
# @param lock_name [String]
|
|
110
|
+
# @param lock_key [String]
|
|
111
|
+
# @param redis_time [Array<String>] Result of the redis `TIME` command.
|
|
112
|
+
# @param read_locks [Array<Array<String,Float>>] Readers registry (acquirer id, expiration).
|
|
113
|
+
# @return [Hash<String,String|Numeric|Array<Hash<String,String|Numeric>>>,NilClass]
|
|
114
|
+
# - `nil` is returned when there are no live read locks;
|
|
115
|
+
# - each reader has the same data as the write lock (acq_id, hst_id, ts, ini_ttl, rem_ttl,
|
|
116
|
+
# meta, reentrant lock data);
|
|
117
|
+
#
|
|
118
|
+
# @api private
|
|
119
|
+
# @since 1.18.0
|
|
120
|
+
# rubocop:disable Metrics/MethodLength
|
|
121
|
+
def read_lock_info(redis_client, lock_name, lock_key, redis_time, read_locks)
|
|
122
|
+
now = RedisQueuedLocks::Resource.redis_time_ms(redis_time)
|
|
123
|
+
live_read_locks = read_locks.select { |(_acquirer_id, expiration)| expiration > now }
|
|
124
|
+
return nil if live_read_locks.empty?
|
|
125
|
+
|
|
126
|
+
# NOTE: read lock data (it can be absent if it is dropped during the read lock life)
|
|
127
|
+
# @type var read_locks_data: Array[Hash[String,String]]
|
|
128
|
+
read_locks_data = redis_client.pipelined do |pipeline|
|
|
129
|
+
live_read_locks.each do |(acquirer_id, _expiration)|
|
|
130
|
+
pipeline.call(
|
|
131
|
+
'HGETALL',
|
|
132
|
+
RedisQueuedLocks::Resource.prepare_read_lock_key(lock_name, acquirer_id)
|
|
133
|
+
)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
longest_rem_ttl = 0
|
|
138
|
+
readers = live_read_locks.each_with_index.map do |(acquirer_id, expiration), index|
|
|
139
|
+
rem_ttl = (expiration - now).ceil
|
|
140
|
+
longest_rem_ttl = rem_ttl if rem_ttl > longest_rem_ttl
|
|
141
|
+
|
|
142
|
+
# @type var reader: Hash[String,String|Float|Integer|nil]
|
|
143
|
+
reader = read_locks_data[index] || {}
|
|
144
|
+
reader['acq_id'] = acquirer_id
|
|
145
|
+
reader['hst_id'] ||= RedisQueuedLocks::Resource.host_identifier_from_acquirer(acquirer_id)
|
|
146
|
+
reader['ts'] = Float(reader['ts']) if reader['ts']
|
|
147
|
+
reader['ini_ttl'] = Integer(reader['ini_ttl']) if reader['ini_ttl']
|
|
148
|
+
reader['rem_ttl'] = rem_ttl
|
|
149
|
+
reader['spc_cnt'] = Integer(reader['spc_cnt']) if reader['spc_cnt']
|
|
150
|
+
reader['l_spc_ts'] = Float(reader['l_spc_ts']) if reader['l_spc_ts']
|
|
151
|
+
reader['spc_ext_ttl'] = Integer(reader['spc_ext_ttl']) if reader['spc_ext_ttl']
|
|
152
|
+
reader['l_spc_ext_ini_ttl'] =
|
|
153
|
+
Integer(reader['l_spc_ext_ini_ttl']) if reader['l_spc_ext_ini_ttl']
|
|
154
|
+
reader['l_spc_ext_ts'] = Float(reader['l_spc_ext_ts']) if reader['l_spc_ext_ts']
|
|
155
|
+
reader
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
{
|
|
159
|
+
'lock_key' => lock_key,
|
|
160
|
+
'rw_mode' => 'read',
|
|
161
|
+
'rem_ttl' => longest_rem_ttl,
|
|
162
|
+
'readers' => readers
|
|
163
|
+
}
|
|
164
|
+
end
|
|
165
|
+
# rubocop:enable Metrics/MethodLength
|
|
76
166
|
end
|
|
77
167
|
end
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# NOTE: Lock Series PoC
|
|
4
|
-
# steep:ignore
|
|
5
4
|
# @api private
|
|
6
5
|
# @since 1.16.1
|
|
7
6
|
module RedisQueuedLocks::Acquirer::LockSeriesPoC::InstrVisitor # steep:ignore
|
|
8
7
|
class << self
|
|
9
8
|
# NOTE: Lock Series PoC
|
|
9
|
+
# @param rw_mode [Symbol] The mode of the lock series (`:read` or `:write`).
|
|
10
10
|
# @api private
|
|
11
11
|
# @since 1.16.1
|
|
12
|
+
# @version 1.18.0
|
|
12
13
|
def lock_series_obtained( # steep:ignore
|
|
13
14
|
instrumenter,
|
|
14
15
|
instr_sampled,
|
|
15
16
|
lock_keys,
|
|
17
|
+
rw_mode,
|
|
16
18
|
ttl,
|
|
17
19
|
acq_id,
|
|
18
20
|
hst_id,
|
|
@@ -22,17 +24,20 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC::InstrVisitor # steep:ignore
|
|
|
22
24
|
)
|
|
23
25
|
return unless instr_sampled
|
|
24
26
|
instrumenter.notify('redis_queued_locks.lock_series_obtained', {
|
|
25
|
-
lock_keys:, ttl:, acq_id:, hst_id:, ts:, acq_time:, instrument:
|
|
27
|
+
lock_keys:, rw_mode:, ttl:, acq_id:, hst_id:, ts:, acq_time:, instrument:
|
|
26
28
|
}) rescue nil
|
|
27
29
|
end
|
|
28
30
|
|
|
29
31
|
# NOTE: Lock Series PoC
|
|
32
|
+
# @param rw_mode [Symbol] The mode of the lock series (`:read` or `:write`).
|
|
30
33
|
# @api private
|
|
31
34
|
# @since 1.16.1
|
|
35
|
+
# @version 1.18.0
|
|
32
36
|
def lock_series_hold_and_release( # steep:ignore
|
|
33
37
|
instrumenter,
|
|
34
38
|
instr_sampled,
|
|
35
39
|
lock_keys,
|
|
40
|
+
rw_mode,
|
|
36
41
|
ttl,
|
|
37
42
|
acq_id,
|
|
38
43
|
hst_id,
|
|
@@ -43,9 +48,8 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC::InstrVisitor # steep:ignore
|
|
|
43
48
|
)
|
|
44
49
|
return unless instr_sampled
|
|
45
50
|
instrumenter.notify('redis_queued_locks.lock_series_hold_and_release', {
|
|
46
|
-
lock_keys:, hold_time:, ttl:, acq_id:, hst_id:, ts:, acq_time:, instrument:
|
|
51
|
+
lock_keys:, rw_mode:, hold_time:, ttl:, acq_id:, hst_id:, ts:, acq_time:, instrument:
|
|
47
52
|
}) # rescue nil
|
|
48
53
|
end
|
|
49
54
|
end
|
|
50
55
|
end
|
|
51
|
-
# steep:ignore
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# NOTE: Lock Series PoC
|
|
4
|
-
# steep:ignore
|
|
5
4
|
# @api private
|
|
6
5
|
# @since 1.16.1
|
|
7
6
|
module RedisQueuedLocks::Acquirer::LockSeriesPoC::LogVisitor # steep:ignore
|
|
8
7
|
class << self
|
|
9
8
|
# NOTE: Lock Series PoC
|
|
9
|
+
# @param rw_mode [Symbol] The mode of the lock series (`:read` or `:write`).
|
|
10
10
|
# @api private
|
|
11
11
|
# @since 1.16.0
|
|
12
|
+
# @version 1.18.0
|
|
12
13
|
def start_lock_series_obtaining( # steep:ignore
|
|
13
14
|
logger,
|
|
14
15
|
log_sampled,
|
|
@@ -16,23 +17,27 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC::LogVisitor # steep:ignore
|
|
|
16
17
|
queue_ttl,
|
|
17
18
|
acquirer_id,
|
|
18
19
|
host_id,
|
|
19
|
-
access_strategy
|
|
20
|
+
access_strategy,
|
|
21
|
+
rw_mode
|
|
20
22
|
)
|
|
21
23
|
return unless log_sampled
|
|
22
24
|
|
|
23
25
|
logger.debug do
|
|
24
26
|
"[redis_queued_locks.start_lock_series_obtaining] " \
|
|
25
|
-
"lock_keys => '#{lock_keys.inspect}'" \
|
|
27
|
+
"lock_keys => '#{lock_keys.inspect}' " \
|
|
26
28
|
"queue_ttl => #{queue_ttl} " \
|
|
27
29
|
"acq_id => '#{acquirer_id}' " \
|
|
28
30
|
"hst_id => '#{host_id}' " \
|
|
29
|
-
"acs_strat => '#{access_strategy}'"
|
|
31
|
+
"acs_strat => '#{access_strategy}' " \
|
|
32
|
+
"rw_mode => '#{rw_mode}'"
|
|
30
33
|
end # rescue nil
|
|
31
34
|
end
|
|
32
35
|
|
|
33
36
|
# NOTE: Lock Series PoC
|
|
37
|
+
# @param rw_mode [Symbol] The mode of the lock series (`:read` or `:write`).
|
|
34
38
|
# @api private
|
|
35
39
|
# @since 1.16.0
|
|
40
|
+
# @version 1.18.0
|
|
36
41
|
def lock_series_obtained( # steep:ignore
|
|
37
42
|
logger,
|
|
38
43
|
log_sampled,
|
|
@@ -41,7 +46,8 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC::LogVisitor # steep:ignore
|
|
|
41
46
|
acquirer_id,
|
|
42
47
|
host_id,
|
|
43
48
|
acq_time,
|
|
44
|
-
access_strategy
|
|
49
|
+
access_strategy,
|
|
50
|
+
rw_mode
|
|
45
51
|
)
|
|
46
52
|
return unless log_sampled
|
|
47
53
|
|
|
@@ -52,13 +58,16 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC::LogVisitor # steep:ignore
|
|
|
52
58
|
"acq_id => '#{acquirer_id}' " \
|
|
53
59
|
"hst_id => '#{host_id}' " \
|
|
54
60
|
"acs_strat => '#{access_strategy}' " \
|
|
61
|
+
"rw_mode => '#{rw_mode}' " \
|
|
55
62
|
"acq_time => #{acq_time} (ms)"
|
|
56
63
|
end # rescue nil
|
|
57
64
|
end
|
|
58
65
|
|
|
59
66
|
# NOTE: Lock Series PoC
|
|
67
|
+
# @param rw_mode [Symbol] The mode of the lock series (`:read` or `:write`).
|
|
60
68
|
# @api private
|
|
61
69
|
# @since 1.16.0
|
|
70
|
+
# @version 1.18.0
|
|
62
71
|
def expire_lock_series( # steep:ignore
|
|
63
72
|
logger,
|
|
64
73
|
log_sampled,
|
|
@@ -66,7 +75,8 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC::LogVisitor # steep:ignore
|
|
|
66
75
|
queue_ttl,
|
|
67
76
|
acquirer_id,
|
|
68
77
|
host_id,
|
|
69
|
-
access_strategy
|
|
78
|
+
access_strategy,
|
|
79
|
+
rw_mode
|
|
70
80
|
)
|
|
71
81
|
return unless log_sampled
|
|
72
82
|
|
|
@@ -76,7 +86,8 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC::LogVisitor # steep:ignore
|
|
|
76
86
|
"queue_ttl => #{queue_ttl} " \
|
|
77
87
|
"acq_id => '#{acquirer_id}' " \
|
|
78
88
|
"hst_id => '#{host_id}' " \
|
|
79
|
-
"acs_strat => '#{access_strategy}'"
|
|
89
|
+
"acs_strat => '#{access_strategy}' " \
|
|
90
|
+
"rw_mode => '#{rw_mode}'"
|
|
80
91
|
end rescue nil
|
|
81
92
|
end
|
|
82
93
|
end
|