redis_queued_locks 0.0.1 → 0.0.3
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/CHANGELOG.md +17 -0
- data/README.md +25 -6
- data/lib/redis_queued_locks/acquier/delay.rb +2 -0
- data/lib/redis_queued_locks/acquier/expire.rb +4 -4
- data/lib/redis_queued_locks/acquier/release.rb +20 -14
- data/lib/redis_queued_locks/acquier.rb +75 -18
- data/lib/redis_queued_locks/client.rb +20 -11
- data/lib/redis_queued_locks/version.rb +2 -2
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5dbd11cefb9d18a816eb4c63fe1178f87325c30d75efc2bc806d29f41994fc2c
|
4
|
+
data.tar.gz: 9697f4bf452aaaa7cade45320c50d16b510c657074ba97513c5f8de9c51f6c49
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b28ef92375c7eef4d6c43fccb5e44105d653fb00e052edcf67316f8da13578ad7b39086b3799782592b38bf046760f8faa1238b92de293b83eb1e4a7c441413e
|
7
|
+
data.tar.gz: dfd3891ae8d0303b88ac95268056addc3b4e03f862a5200166de9553b412eee78b77f42193f133f9dd340793b67928806e2e05451517585c2282fb7e4c871d1f
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,22 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.0.3] - 2024-02-26
|
4
|
+
### Changed
|
5
|
+
- Instrumentation events:
|
6
|
+
- `"redis_queued_locks.explicit_all_locks_release"`
|
7
|
+
- re-factored with fully pipelined invocation;
|
8
|
+
- removed `rel_queue_cnt` and `rel_lock_cnt` because of the pipelined invocation
|
9
|
+
misses the concrete results and now we can receive only "released redis keys count";
|
10
|
+
- adde `rel_keys` payload data (released redis keys);
|
11
|
+
|
12
|
+
## [0.0.2] - 2024-02-26
|
13
|
+
### Added
|
14
|
+
- Instrumentation events:
|
15
|
+
- `"redis_queued_locks.lock_obtained"`;
|
16
|
+
- `"redis_queued_locks.lock_hold_and_release"`;
|
17
|
+
- `"redis_queued_locks.explicit_lock_release"`;
|
18
|
+
- `"redis_queued_locks.explicit_all_locks_release"`;
|
19
|
+
|
3
20
|
## [0.0.1] - 2024-02-26
|
4
21
|
|
5
22
|
- Still the initial release version;
|
data/README.md
CHANGED
@@ -5,14 +5,33 @@ Distributed lock implementation with "lock acquisition queue" capabilities based
|
|
5
5
|
## Instrumentation events
|
6
6
|
|
7
7
|
- `"redis_queued_locks.lock_obtained"`
|
8
|
-
-
|
8
|
+
- a moment when the lock was obtained;
|
9
9
|
- payload:
|
10
10
|
- `ttl` - `integer`/`milliseconds` - lock ttl;
|
11
11
|
- `acq_id` - `string` - lock acquier identifier;
|
12
|
-
- `lock_key` - `string` - lock name
|
12
|
+
- `lock_key` - `string` - lock name;
|
13
13
|
- `ts` - `integer`/`epoch` - the time when the lock was obtaiend;
|
14
14
|
- `acq_time` - `float`/`milliseconds` - time spent on lock acquiring;
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
-
|
15
|
+
- `"redis_queued_locks.lock_hold_and_release"`
|
16
|
+
- an event signalizes about the "hold+and+release" process
|
17
|
+
when the lock obtained and hold by the block of logic;
|
18
|
+
- payload:
|
19
|
+
- `hold_time` - `float`/`milliseconds` - lock hold time;
|
20
|
+
- `ttl` - `integer`/`milliseconds` - lock ttl;
|
21
|
+
- `acq_id` - `string` - lock acquier identifier;
|
22
|
+
- `lock_key` - `string` - lock name;
|
23
|
+
- `ts` - `integer`/`epoch` - the time when lock was obtained;
|
24
|
+
- `acq_time` - `float`/`milliseconds` - time spent on lock acquiring;
|
25
|
+
- `"redis_queued_locks.explicit_lock_release"`
|
26
|
+
- an event signalizes about the explicit lock release (invoked via `RedisQueuedLock#unlock`);
|
27
|
+
- payload:
|
28
|
+
- `at` - `integer`/`epoch` - the time when the lock was released;
|
29
|
+
- `rel_time` - `float`/`milliseconds` - time spent on lock releasing;
|
30
|
+
- `lock_key` - `string` - released lock (lock name);
|
31
|
+
- `lock_key_queue` - `string` - released lock queue (lock queue name);
|
32
|
+
- `"redis_queued_locks.explicit_all_locks_release"`
|
33
|
+
- an event signalizes about the explicit all locks release (invoked via `RedisQueuedLock#clear_locks`);
|
34
|
+
- payload:
|
35
|
+
- `rel_time` - `float`/`milliseconds` - time spent on the lock release;
|
36
|
+
- `at` - `integer`/`epoch` - the time when the operation has ended;
|
37
|
+
- `rel_keys` - `integer` - released redis keys count (`released queu keys` + `released lock keys`);
|
@@ -3,6 +3,8 @@
|
|
3
3
|
# @api private
|
4
4
|
# @since 0.1.0
|
5
5
|
module RedisQueuedLocks::Acquier::Delay
|
6
|
+
# Sleep with random time-shifting (it is necessary for empty-time-slot lock acquirement).
|
7
|
+
#
|
6
8
|
# @param retry_delay [Integer] In milliseconds
|
7
9
|
# @param retry_jitter [Integer] In milliseconds
|
8
10
|
# @return [void]
|
@@ -3,10 +3,10 @@
|
|
3
3
|
# @api private
|
4
4
|
# @since 0.1.0
|
5
5
|
module RedisQueuedLocks::Acquier::Expire
|
6
|
-
# @param redis [RedisClient]
|
7
|
-
# @param lock_key [String]
|
8
|
-
# @param block [Block]
|
9
|
-
# @return [
|
6
|
+
# @param redis [RedisClient] Redis connection manager.
|
7
|
+
# @param lock_key [String] Lock key to be expired.
|
8
|
+
# @param block [Block] Custom logic that should be invoked unter the obtained lock.
|
9
|
+
# @return [Any,NilClass] nil is returned no block parametr is provided.
|
10
10
|
#
|
11
11
|
# @api private
|
12
12
|
# @since 0.1.0
|
@@ -8,47 +8,53 @@ module RedisQueuedLocks::Acquier::Release
|
|
8
8
|
# @param redis [RedisClient]
|
9
9
|
# @param lock_key [String]
|
10
10
|
# @param lock_key_queue [String]
|
11
|
-
# @return [
|
11
|
+
# @return [Hash<Symbol,Any>] Format: { ok: true/false, result: Any }
|
12
12
|
#
|
13
13
|
# @api private
|
14
14
|
# @since 0.1.0
|
15
15
|
def fully_release_lock(redis, lock_key, lock_key_queue)
|
16
|
-
redis.multi do |transact|
|
16
|
+
result = redis.multi do |transact|
|
17
17
|
transact.call('ZREMRANGEBYSCORE', lock_key_queue, '-inf', '+inf')
|
18
18
|
transact.call('EXPIRE', lock_key, '0')
|
19
19
|
end
|
20
|
+
|
21
|
+
{ ok: true, result: }
|
20
22
|
end
|
21
23
|
|
22
24
|
# Release all locks: clear all lock queus and expire all locks.
|
23
25
|
#
|
24
26
|
# @param redis [RedisClient]
|
25
27
|
# @param batch_size [Integer]
|
26
|
-
# @return [
|
28
|
+
# @return [Hash<Symbol,Any>] Format: { ok: true/false, result: Any }
|
27
29
|
#
|
28
30
|
# @api private
|
29
31
|
# @since 0.1.0
|
30
32
|
def fully_release_all_locks(redis, batch_size)
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
33
|
+
result = redis.pipelined do |pipeline|
|
34
|
+
# Step A: release all queus and their related locks
|
35
|
+
redis.scan(
|
36
|
+
'MATCH',
|
37
|
+
RedisQueuedLocks::Resource::LOCK_QUEUE_PATTERN,
|
38
|
+
count: batch_size
|
39
|
+
) do |lock_queue|
|
40
|
+
puts "RELEASE (lock_queue): #{lock_queue}"
|
38
41
|
pipeline.call('ZREMRANGEBYSCORE', lock_queue, '-inf', '+inf')
|
39
|
-
pipeline.call('EXPIRE', RedisQueuedLocks::Resource.lock_key_from_queue(lock_queue))
|
42
|
+
pipeline.call('EXPIRE', RedisQueuedLocks::Resource.lock_key_from_queue(lock_queue), "0")
|
40
43
|
end
|
41
|
-
end
|
42
44
|
|
43
|
-
|
44
|
-
redis.pipelined do |pipeline|
|
45
|
+
# Step B: release all locks
|
45
46
|
redis.scan(
|
46
47
|
'MATCH',
|
47
48
|
RedisQueuedLocks::Resource::LOCK_PATTERN,
|
48
49
|
count: batch_size
|
49
50
|
) do |lock_key|
|
51
|
+
puts "RELEASE (lock_key): #{lock_key}"
|
50
52
|
pipeline.call('EXPIRE', lock_key, '0')
|
51
53
|
end
|
52
54
|
end
|
55
|
+
|
56
|
+
rel_keys = result.count { |red_res| red_res == 0 }
|
57
|
+
|
58
|
+
{ ok: true, result: { rel_keys: rel_keys } }
|
53
59
|
end
|
54
60
|
end
|
@@ -37,14 +37,14 @@ module RedisQueuedLocks::Acquier
|
|
37
37
|
# The process that want to acquire the lock.
|
38
38
|
# @option thread_id [Integer,String]
|
39
39
|
# The process's thread that want to acquire the lock.
|
40
|
-
# @option ttl [Integer]
|
41
|
-
# Lock's time to live (in milliseconds).
|
40
|
+
# @option ttl [Integer,NilClass]
|
41
|
+
# Lock's time to live (in milliseconds). Nil means "without timeout".
|
42
42
|
# @option queue_ttl [Integer]
|
43
|
-
#
|
43
|
+
# Lifetime of the acuier's lock request. In seconds.
|
44
44
|
# @option timeout [Integer]
|
45
45
|
# Time period whe should try to acquire the lock (in seconds).
|
46
|
-
# @option retry_count [Integer]
|
47
|
-
# How many times we should try to acquire a lock.
|
46
|
+
# @option retry_count [Integer,NilClass]
|
47
|
+
# How many times we should try to acquire a lock. Nil means "infinite retries".
|
48
48
|
# @option retry_delay [Integer]
|
49
49
|
# A time-interval between the each retry (in milliseconds).
|
50
50
|
# @option retry_jitter [Integer]
|
@@ -90,7 +90,9 @@ module RedisQueuedLocks::Acquier
|
|
90
90
|
tries: 0,
|
91
91
|
acquired: false,
|
92
92
|
result: nil,
|
93
|
-
acq_time: nil # NOTE: in milliseconds
|
93
|
+
acq_time: nil, # NOTE: in milliseconds
|
94
|
+
hold_time: nil, # NOTE: in milliseconds
|
95
|
+
rel_time: nil # NOTE: in milliseconds
|
94
96
|
}
|
95
97
|
acq_dequeue = -> { dequeue_from_lock_queue(redis, lock_key_queue, acquier_id) }
|
96
98
|
|
@@ -111,14 +113,14 @@ module RedisQueuedLocks::Acquier
|
|
111
113
|
) => { ok:, result: }
|
112
114
|
|
113
115
|
acq_end_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
114
|
-
acq_time = ((acq_end_time - acq_start_time) * 1_000).ceil
|
116
|
+
acq_time = ((acq_end_time - acq_start_time) * 1_000).ceil(2)
|
115
117
|
|
116
118
|
# Step X: save the intermediate results to the result observer
|
117
119
|
acq_process[:result] = result
|
118
120
|
|
119
121
|
# Step 2.1: analyze an acquirement attempt
|
120
122
|
if ok
|
121
|
-
#
|
123
|
+
# Step X (instrumentation): lock obtained
|
122
124
|
instrumenter.notify('redis_queued_locks.lock_obtained', {
|
123
125
|
lock_key: result[:lock_key],
|
124
126
|
ttl: result[:ttl],
|
@@ -137,20 +139,20 @@ module RedisQueuedLocks::Acquier
|
|
137
139
|
acq_process[:acquired] = true
|
138
140
|
acq_process[:should_try] = false
|
139
141
|
acq_process[:acq_time] = acq_time
|
142
|
+
acq_process[:acq_end_time] = acq_end_time
|
140
143
|
else
|
141
144
|
# Step 2.1.b: failed acquirement => retry
|
142
145
|
acq_process[:tries] += 1
|
143
146
|
|
144
|
-
if acq_process[:tries] >= retry_count
|
147
|
+
if retry_count != nil && acq_process[:tries] >= retry_count
|
145
148
|
# NOTE: reached the retry limit => quit from the loop
|
146
149
|
acq_process[:should_try] = false
|
147
150
|
# NOTE: reached the retry limit => dequeue from the lock queue
|
148
151
|
acq_dequeue.call
|
149
|
-
|
152
|
+
elsif delay_execution(retry_delay, retry_jitter)
|
150
153
|
# NOTE:
|
151
154
|
# delay the exceution in order to prevent chaotic attempts
|
152
155
|
# and to allow other processes and threads to obtain the lock too.
|
153
|
-
delay_execution(retry_delay, retry_jitter)
|
154
156
|
end
|
155
157
|
end
|
156
158
|
end
|
@@ -160,7 +162,26 @@ module RedisQueuedLocks::Acquier
|
|
160
162
|
if acq_process[:acquired]
|
161
163
|
# Step 3.a: acquired successfully => run logic or return the result of acquirement
|
162
164
|
if block_given?
|
163
|
-
|
165
|
+
begin
|
166
|
+
yield_with_expire(redis, lock_key, &block)
|
167
|
+
ensure
|
168
|
+
acq_process[:rel_time] = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
169
|
+
acq_process[:hold_time] = (
|
170
|
+
(acq_process[:rel_time] - acq_process[:acq_end_time]) * 1000
|
171
|
+
).ceil(2)
|
172
|
+
|
173
|
+
# Step X (instrumentation): lock_hold_and_release
|
174
|
+
run_non_critical do
|
175
|
+
instrumenter.notify('redis_queued_locks.lock_hold_and_release', {
|
176
|
+
hold_time: acq_process[:hold_time],
|
177
|
+
ttl: acq_process[:lock_info][:ttl],
|
178
|
+
acq_id: acq_process[:lock_info][:acq_id],
|
179
|
+
ts: acq_process[:lock_info][:ts],
|
180
|
+
lock_key: acq_process[:lock_info][:lock_key],
|
181
|
+
acq_time: acq_process[:acq_time]
|
182
|
+
})
|
183
|
+
end
|
184
|
+
end
|
164
185
|
else
|
165
186
|
{ ok: true, result: acq_process[:lock_info] }
|
166
187
|
end
|
@@ -181,16 +202,30 @@ module RedisQueuedLocks::Acquier
|
|
181
202
|
#
|
182
203
|
# @param redis [RedisClient] Redis connection client.
|
183
204
|
# @param lock_name [String] The lock name that should be released.
|
205
|
+
# @param isntrumenter [#notify] See RedisQueuedLocks::Instrument::ActiveSupport for example.
|
184
206
|
# @return [Hash<Symbol,Any>] Format: { ok: true/false, result: Any }
|
185
207
|
#
|
186
208
|
# @api private
|
187
209
|
# @since 0.1.0
|
188
|
-
def release_lock!(redis, lock_name)
|
210
|
+
def release_lock!(redis, lock_name, instrumenter)
|
189
211
|
lock_key = RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
|
190
212
|
lock_key_queue = RedisQueuedLocks::Resource.prepare_lock_queue(lock_name)
|
191
213
|
|
192
|
-
|
193
|
-
|
214
|
+
rel_start_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
215
|
+
fully_release_lock(redis, lock_key, lock_key_queue) => { ok:, result: }
|
216
|
+
time_at = Time.now.to_i
|
217
|
+
rel_end_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
218
|
+
rel_time = ((rel_end_time - rel_start_time) * 1_000).ceil(2)
|
219
|
+
|
220
|
+
run_non_critical do
|
221
|
+
instrumenter.notify('redis_queued_locks.explicit_lock_release', {
|
222
|
+
lock_key: lock_key,
|
223
|
+
lock_key_queue: lock_key_queue,
|
224
|
+
rel_time: rel_time,
|
225
|
+
at: time_at
|
226
|
+
})
|
227
|
+
end
|
228
|
+
|
194
229
|
{ ok: true, result: result }
|
195
230
|
end
|
196
231
|
|
@@ -200,13 +235,26 @@ module RedisQueuedLocks::Acquier
|
|
200
235
|
#
|
201
236
|
# @param redis [RedisClient] Redis connection client.
|
202
237
|
# @param batch_size [Integer] The number of lock keys that should be released in a time.
|
238
|
+
# @param isntrumenter [#notify] See RedisQueuedLocks::Instrument::ActiveSupport for example.
|
203
239
|
# @return [Hash<Symbol,Any>] Format: { ok: true/false, result: Any }
|
204
240
|
#
|
205
241
|
# @api private
|
206
242
|
# @since 0.1.0
|
207
|
-
def release_all_locks!(redis, batch_size)
|
208
|
-
|
209
|
-
|
243
|
+
def release_all_locks!(redis, batch_size, instrumenter)
|
244
|
+
rel_start_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
245
|
+
fully_release_all_locks(redis, batch_size) => { ok:, result: }
|
246
|
+
time_at = Time.now.to_i
|
247
|
+
rel_end_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
248
|
+
rel_time = ((rel_end_time - rel_start_time) * 1_000).ceil(2)
|
249
|
+
|
250
|
+
run_non_critical do
|
251
|
+
instrumenter.notify('redis_queued_locks.explicit_all_locks_release', {
|
252
|
+
at: time_at,
|
253
|
+
rel_time: rel_time,
|
254
|
+
rel_keys: result[:rel_keys]
|
255
|
+
})
|
256
|
+
end
|
257
|
+
|
210
258
|
{ ok: true, result: result }
|
211
259
|
end
|
212
260
|
|
@@ -235,6 +283,15 @@ module RedisQueuedLocks::Acquier
|
|
235
283
|
ERROR_MESSAGE
|
236
284
|
end
|
237
285
|
end
|
286
|
+
|
287
|
+
# @param block [Block]
|
288
|
+
# @return [Any]
|
289
|
+
#
|
290
|
+
# @api private
|
291
|
+
# @since 0.1.0
|
292
|
+
def run_non_critical(&block)
|
293
|
+
yield rescue nil
|
294
|
+
end
|
238
295
|
end
|
239
296
|
# rubocop:enable Metrics/ClassLength
|
240
297
|
end
|
@@ -12,15 +12,15 @@ class RedisQueuedLocks::Client
|
|
12
12
|
setting :retry_jitter, 50 # NOTE: milliseconds
|
13
13
|
setting :default_timeout, 10 # NOTE: seconds
|
14
14
|
setting :exp_precision, 1 # NOTE: milliseconds
|
15
|
-
setting :default_lock_ttl,
|
16
|
-
setting :default_queue_ttl,
|
15
|
+
setting :default_lock_ttl, 5_000 # NOTE: milliseconds
|
16
|
+
setting :default_queue_ttl, 30 # NOTE: seconds
|
17
17
|
setting :lock_release_batch_size, 100
|
18
18
|
setting :instrumenter, RedisQueuedLocks::Instrument::VoidNotifier
|
19
19
|
|
20
20
|
# TODO: setting :logger, Logger.new(IO::NULL)
|
21
21
|
# TODO: setting :debug, true/false
|
22
22
|
|
23
|
-
validate('retry_count'
|
23
|
+
validate('retry_count') { |val| val == nil || (val.is_a?(::Integer) && val >= 0) }
|
24
24
|
validate('retry_delay', :integer)
|
25
25
|
validate('retry_jitter', :integer)
|
26
26
|
validate('default_timeout', :integer)
|
@@ -28,7 +28,7 @@ class RedisQueuedLocks::Client
|
|
28
28
|
validate('default_lock_tt', :integer)
|
29
29
|
validate('default_queue_ttl', :integer)
|
30
30
|
validate('lock_release_batch_size', :integer)
|
31
|
-
validate('instrumenter') { |
|
31
|
+
validate('instrumenter') { |val| RedisQueuedLocks::Instrument.valid_interface?(val) }
|
32
32
|
end
|
33
33
|
|
34
34
|
# @return [RedisClient]
|
@@ -60,11 +60,11 @@ class RedisQueuedLocks::Client
|
|
60
60
|
# @option ttl [Integer]
|
61
61
|
# Lock's time to live (in milliseconds).
|
62
62
|
# @option queue_ttl [Integer]
|
63
|
-
#
|
64
|
-
# @option timeout [Integer]
|
65
|
-
# Time period whe should try to acquire the lock (in seconds).
|
66
|
-
# @option retry_count [Integer]
|
67
|
-
# How many times we should try to acquire a lock.
|
63
|
+
# Lifetime of the acuier's lock request. In seconds.
|
64
|
+
# @option timeout [Integer,NilClass]
|
65
|
+
# Time period whe should try to acquire the lock (in seconds). Nil means "without timeout".
|
66
|
+
# @option retry_count [Integer,NilClass]
|
67
|
+
# How many times we should try to acquire a lock. Nil means "infinite retries".
|
68
68
|
# @option retry_delay [Integer]
|
69
69
|
# A time-interval between the each retry (in milliseconds).
|
70
70
|
# @option retry_jitter [Integer]
|
@@ -147,14 +147,23 @@ class RedisQueuedLocks::Client
|
|
147
147
|
# @api public
|
148
148
|
# @since 0.1.0
|
149
149
|
def unlock(lock_name)
|
150
|
-
RedisQueuedLocks::Acquier.release_lock!(
|
150
|
+
RedisQueuedLocks::Acquier.release_lock!(
|
151
|
+
redis_client,
|
152
|
+
lock_name,
|
153
|
+
config[:instrumenter]
|
154
|
+
)
|
151
155
|
end
|
152
156
|
|
157
|
+
# @option batch_size [Integer]
|
153
158
|
# @return [Hash<Symbol,Any>] Format: { ok: true/false, result: Symbol/Hash }.
|
154
159
|
#
|
155
160
|
# @api public
|
156
161
|
# @since 0.1.0
|
157
162
|
def clear_locks(batch_size: config[:lock_release_batch_size])
|
158
|
-
RedisQueuedLocks::Acquier.release_all_locks!(
|
163
|
+
RedisQueuedLocks::Acquier.release_all_locks!(
|
164
|
+
redis_client,
|
165
|
+
batch_size,
|
166
|
+
config[:instrumenter]
|
167
|
+
)
|
159
168
|
end
|
160
169
|
end
|