redis_queued_locks 0.0.0 → 0.0.2
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/.rubocop.yml +4 -1
- data/.ruby-version +1 -1
- data/CHANGELOG.md +13 -0
- data/README.md +40 -0
- data/lib/redis_queued_locks/acquier/release.rb +25 -5
- data/lib/redis_queued_locks/acquier/try.rb +3 -3
- data/lib/redis_queued_locks/acquier.rb +107 -16
- data/lib/redis_queued_locks/client.rb +66 -17
- data/lib/redis_queued_locks/debugger.rb +1 -1
- data/lib/redis_queued_locks/instrument/active_support.rb +17 -0
- data/lib/redis_queued_locks/instrument/void_notifier.rb +15 -0
- data/lib/redis_queued_locks/instrument.rb +48 -0
- data/lib/redis_queued_locks/version.rb +2 -2
- data/lib/redis_queued_locks.rb +1 -1
- data/redis_queued_locks.gemspec +1 -1
- metadata +7 -5
- data/lib/redis_queued_locks/instrumentation.rb +0 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e984603ea8f509f8e5f7f685c356d1f838b16bc950e1de83ff8f66e6afa83fcf
|
4
|
+
data.tar.gz: 1b2eee0f32c911caecf08214b2b404d620ffb10501ae72d7721235b64fc02362
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b7e2d574eeeff4a9a235f39ee25bde152880dcd12e7066196a8304e54098480ef05672edd89a6931ece225711b96602f23ce76304c555dbd623d30099e35ca0a
|
7
|
+
data.tar.gz: d7f5075bbf136e672ebb1632da5c6a78b2b72886bc1d5e8b0d9425cf38b14f5eef5a097fdc835c78f5f2d26885512279926a33bdbdb559151b875453d5312c9e
|
data/.rubocop.yml
CHANGED
@@ -5,7 +5,7 @@ inherit_gem:
|
|
5
5
|
- lib/rubocop.rspec.yml
|
6
6
|
|
7
7
|
AllCops:
|
8
|
-
TargetRubyVersion: 3.
|
8
|
+
TargetRubyVersion: 3.1
|
9
9
|
NewCops: enable
|
10
10
|
Include:
|
11
11
|
- lib/**/*.rb
|
@@ -33,3 +33,6 @@ Metrics/AbcSize:
|
|
33
33
|
|
34
34
|
Metrics/CyclomaticComplexity:
|
35
35
|
Enabled: false
|
36
|
+
|
37
|
+
Metrics/PerceivedComplexity:
|
38
|
+
Enabled: false
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
3.
|
1
|
+
3.1.2
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,18 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.0.2] - 2024-02-26
|
4
|
+
### Added
|
5
|
+
- Instrumentation events:
|
6
|
+
- `"redis_queued_locks.lock_obtained"`;
|
7
|
+
- `"redis_queued_locks.lock_hold_and_release"`;
|
8
|
+
- `"redis_queued_locks.explicit_lock_release"`;
|
9
|
+
- `"redis_queued_locks.explicit_all_locks_release"`;
|
10
|
+
|
11
|
+
## [0.0.1] - 2024-02-26
|
12
|
+
|
13
|
+
- Still the initial release version;
|
14
|
+
- Downgrade the minimal Ruby version requirement from 3.2 to 3.1;
|
15
|
+
|
3
16
|
## [0.0.0] - 2024-02-25
|
4
17
|
|
5
18
|
- Initial release
|
data/README.md
CHANGED
@@ -1,3 +1,43 @@
|
|
1
1
|
# RedisQueuedLocks
|
2
2
|
|
3
3
|
Distributed lock implementation with "lock acquisition queue" capabilities based on the Redis Database.
|
4
|
+
|
5
|
+
## Instrumentation events
|
6
|
+
|
7
|
+
- `"redis_queued_locks.lock_obtained"`
|
8
|
+
- a moment when the lock was obtained;
|
9
|
+
- payload:
|
10
|
+
- `ttl` - `integer`/`milliseconds` - lock ttl;
|
11
|
+
- `acq_id` - `string` - lock acquier identifier;
|
12
|
+
- `lock_key` - `string` - lock name;
|
13
|
+
- `ts` - `integer`/`epoch` - the time when the lock was obtaiend;
|
14
|
+
- `acq_time` - `float`/`milliseconds` - time spent on lock acquiring;
|
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
|
+
- `rel_time` - `float`/`milliseconds` - time spent on lock "holding + releasing";
|
26
|
+
- `"redis_queued_locks.explicit_lock_release"`
|
27
|
+
- an event signalizes about the explicit lock release (invoked via `RedisQueuedLock#unlock`);
|
28
|
+
- payload:
|
29
|
+
- `at` - `integer`/`epoch` - the time when the lock was released;
|
30
|
+
- `rel_time` - `float`/`milliseconds` - time spent on lock releasing;
|
31
|
+
- `lock_key` - `string` - released lock (lock name);
|
32
|
+
- `lock_key_queue` - `string` - released lock queue (lock queue name);
|
33
|
+
- `"redis_queued_locks.explicit_all_locks_release"`
|
34
|
+
- an event signalizes about the explicit all locks release (invoked viaa `RedisQueuedLock#clear_locks`);
|
35
|
+
- payload:
|
36
|
+
- `rel_time` - `float`/`milliseconds` - time when the locks was released
|
37
|
+
- `at` - `integer`/`epoch` - the time when the operation has ended;
|
38
|
+
- `rel_lock_cnt` - `integer` - released lock count;
|
39
|
+
- `rel_queue_cnt` - `integer` - released queue count;
|
40
|
+
|
41
|
+
## Todo
|
42
|
+
|
43
|
+
- CI (github actions);
|
@@ -8,28 +8,40 @@ 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)
|
33
|
+
rel_queue_cnt = 0
|
34
|
+
rel_lock_cnt = 0
|
35
|
+
|
31
36
|
# Step A: release all queus and their related locks
|
32
|
-
redis.scan(
|
37
|
+
redis.scan(
|
38
|
+
'MATCH',
|
39
|
+
RedisQueuedLocks::Resource::LOCK_QUEUE_PATTERN,
|
40
|
+
count: batch_size
|
41
|
+
) do |lock_queue|
|
42
|
+
rel_queue_cnt += 1
|
43
|
+
rel_lock_cnt += 1
|
44
|
+
|
33
45
|
redis.pipelined do |pipeline|
|
34
46
|
pipeline.call('ZREMRANGEBYSCORE', lock_queue, '-inf', '+inf')
|
35
47
|
pipeline.call('EXPIRE', RedisQueuedLocks::Resource.lock_key_from_queue(lock_queue))
|
@@ -38,9 +50,17 @@ module RedisQueuedLocks::Acquier::Release
|
|
38
50
|
|
39
51
|
# Step B: release all locks
|
40
52
|
redis.pipelined do |pipeline|
|
41
|
-
|
53
|
+
rel_lock_cnt += 1
|
54
|
+
|
55
|
+
redis.scan(
|
56
|
+
'MATCH',
|
57
|
+
RedisQueuedLocks::Resource::LOCK_PATTERN,
|
58
|
+
count: batch_size
|
59
|
+
) do |lock_key|
|
42
60
|
pipeline.call('EXPIRE', lock_key, '0')
|
43
61
|
end
|
44
62
|
end
|
63
|
+
|
64
|
+
{ ok: true, result: { rel_queue_cnt:, rel_lock_cnt: } }
|
45
65
|
end
|
46
66
|
end
|
@@ -14,7 +14,7 @@ module RedisQueuedLocks::Acquier::Try
|
|
14
14
|
#
|
15
15
|
# @api private
|
16
16
|
# @since 0.1.0
|
17
|
-
# rubocop:disable Metrics/MethodLength
|
17
|
+
# rubocop:disable Metrics/MethodLength
|
18
18
|
def try_to_lock(redis, lock_key, lock_key_queue, acquier_id, acquier_position, ttl, queue_ttl)
|
19
19
|
# Step X: intermediate invocation results
|
20
20
|
inter_result = nil
|
@@ -111,13 +111,13 @@ module RedisQueuedLocks::Acquier::Try
|
|
111
111
|
when result == nil || (result.is_a?(::Array) && result.empty?)
|
112
112
|
# Step 7.b: lock is already acquired durign the acquire race => failed to acquire
|
113
113
|
{ ok: false, result: :lock_is_acquired_during_acquire_race }
|
114
|
-
when result.is_a?(::Array) && result.size == 3 # NOTE: 3 is a count of lock commands
|
114
|
+
when result.is_a?(::Array) && result.size == 3 # NOTE: 3 is a count of redis lock commands
|
115
115
|
# TODO:
|
116
116
|
# => (!) analyze the command result and do actions with the depending on it;
|
117
117
|
# => (*) at this moment we accept that all comamnds are completed successfully;
|
118
118
|
# => (!) need to analyze:
|
119
119
|
# 1. zpopmin should return our process (array with <acq_id> and <score>)
|
120
|
-
# 2. hset should return 2 (lock key is added to the redis
|
120
|
+
# 2. hset should return 2 (lock key is added to the redis as a hashmap with 2 fields)
|
121
121
|
# 3. pexpire should return 1 (expiration time is successfully applied)
|
122
122
|
|
123
123
|
# Step 7.c: locked! :) let's go! => successfully acquired
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
# @api private
|
4
4
|
# @since 0.1.0
|
5
|
+
# rubocop:disable Metrics/ModuleLength
|
5
6
|
module RedisQueuedLocks::Acquier
|
6
7
|
require_relative 'acquier/try'
|
7
8
|
require_relative 'acquier/delay'
|
@@ -26,6 +27,7 @@ module RedisQueuedLocks::Acquier
|
|
26
27
|
# @since 0.1.0
|
27
28
|
REDIS_EXPIRATION_DEVIATION = 2 # NOTE: milliseconds
|
28
29
|
|
30
|
+
# rubocop:disable Metrics/ClassLength
|
29
31
|
class << self
|
30
32
|
# @param redis [RedisClient]
|
31
33
|
# Redis connection client.
|
@@ -35,10 +37,10 @@ module RedisQueuedLocks::Acquier
|
|
35
37
|
# The process that want to acquire the lock.
|
36
38
|
# @option thread_id [Integer,String]
|
37
39
|
# The process's thread that want to acquire the lock.
|
38
|
-
# @option ttl [Integer]
|
39
|
-
# Lock's time to live (in milliseconds).
|
40
|
+
# @option ttl [Integer,NilClass]
|
41
|
+
# Lock's time to live (in milliseconds). Nil means "without timeout".
|
40
42
|
# @option queue_ttl [Integer]
|
41
|
-
#
|
43
|
+
# Lifetime of the acuier's lock request. In seconds.
|
42
44
|
# @option timeout [Integer]
|
43
45
|
# Time period whe should try to acquire the lock (in seconds).
|
44
46
|
# @option retry_count [Integer]
|
@@ -47,6 +49,10 @@ module RedisQueuedLocks::Acquier
|
|
47
49
|
# A time-interval between the each retry (in milliseconds).
|
48
50
|
# @option retry_jitter [Integer]
|
49
51
|
# Time-shift range for retry-delay (in milliseconds).
|
52
|
+
# @option raise_errors [Boolean]
|
53
|
+
# Raise errors on exceptional cases.
|
54
|
+
# @option instrumenter [#notify]
|
55
|
+
# See RedisQueuedLocks::Instrument::ActiveSupport for example.
|
50
56
|
# @param [Block]
|
51
57
|
# A block of code that should be executed after the successfully acquired lock.
|
52
58
|
# @return [Hash<Symbol,Any>]
|
@@ -66,6 +72,8 @@ module RedisQueuedLocks::Acquier
|
|
66
72
|
retry_count:,
|
67
73
|
retry_delay:,
|
68
74
|
retry_jitter:,
|
75
|
+
raise_errors:,
|
76
|
+
instrumenter:,
|
69
77
|
&block
|
70
78
|
)
|
71
79
|
# Step 1: prepare lock requirements (generate lock name, calc lock ttl, etc).
|
@@ -76,11 +84,22 @@ module RedisQueuedLocks::Acquier
|
|
76
84
|
acquier_position = RedisQueuedLocks::Resource.calc_initial_acquier_position
|
77
85
|
|
78
86
|
# Step X: intermediate result observer
|
79
|
-
acq_process = {
|
87
|
+
acq_process = {
|
88
|
+
lock_info: {},
|
89
|
+
should_try: true,
|
90
|
+
tries: 0,
|
91
|
+
acquired: false,
|
92
|
+
result: nil,
|
93
|
+
acq_time: nil, # NOTE: in milliseconds
|
94
|
+
hold_time: nil, # NOTE: in milliseconds
|
95
|
+
rel_time: nil # NOTE: in milliseconds
|
96
|
+
}
|
80
97
|
acq_dequeue = -> { dequeue_from_lock_queue(redis, lock_key_queue, acquier_id) }
|
81
98
|
|
82
99
|
# Step 2: try to lock with timeout
|
83
|
-
with_timeout(timeout, lock_key, on_timeout: acq_dequeue) do
|
100
|
+
with_timeout(timeout, lock_key, raise_errors, on_timeout: acq_dequeue) do
|
101
|
+
acq_start_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
102
|
+
|
84
103
|
# Step 2.1: caclically try to obtain the lock
|
85
104
|
while acq_process[:should_try]
|
86
105
|
try_to_lock(
|
@@ -93,15 +112,33 @@ module RedisQueuedLocks::Acquier
|
|
93
112
|
queue_ttl
|
94
113
|
) => { ok:, result: }
|
95
114
|
|
115
|
+
acq_end_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
116
|
+
acq_time = ((acq_end_time - acq_start_time) * 1_000).ceil
|
117
|
+
|
96
118
|
# Step X: save the intermediate results to the result observer
|
97
119
|
acq_process[:result] = result
|
98
120
|
|
99
121
|
# Step 2.1: analyze an acquirement attempt
|
100
122
|
if ok
|
123
|
+
# Step X (instrumentation): lock obtained
|
124
|
+
instrumenter.notify('redis_queued_locks.lock_obtained', {
|
125
|
+
lock_key: result[:lock_key],
|
126
|
+
ttl: result[:ttl],
|
127
|
+
acq_id: result[:acq_id],
|
128
|
+
ts: result[:ts],
|
129
|
+
acq_time: acq_time
|
130
|
+
})
|
131
|
+
|
101
132
|
# Step 2.1.a: successfully acquired => build the result
|
102
|
-
acq_process[:lock_info] =
|
133
|
+
acq_process[:lock_info] = {
|
134
|
+
lock_key: result[:lock_key],
|
135
|
+
acq_id: result[:acq_id],
|
136
|
+
ts: result[:ts],
|
137
|
+
ttl: result[:ttl]
|
138
|
+
}
|
103
139
|
acq_process[:acquired] = true
|
104
140
|
acq_process[:should_try] = false
|
141
|
+
acq_process[:acq_time] = acq_time
|
105
142
|
else
|
106
143
|
# Step 2.1.b: failed acquirement => retry
|
107
144
|
acq_process[:tries] += 1
|
@@ -125,7 +162,25 @@ module RedisQueuedLocks::Acquier
|
|
125
162
|
if acq_process[:acquired]
|
126
163
|
# Step 3.a: acquired successfully => run logic or return the result of acquirement
|
127
164
|
if block_given?
|
128
|
-
|
165
|
+
begin
|
166
|
+
yield_with_expire(redis, lock_key, instrumenter, &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_time]
|
171
|
+
) * 1000).ceil
|
172
|
+
|
173
|
+
# Step X (instrumentation): lock_hold_and_release
|
174
|
+
instrumenter.notify('redis_queued_locks.lock_hold_and_release', {
|
175
|
+
hold_time: acq_process[:hold_time],
|
176
|
+
rel_time: acq_process[:rel_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
|
129
184
|
else
|
130
185
|
{ ok: true, result: acq_process[:lock_info] }
|
131
186
|
end
|
@@ -146,15 +201,28 @@ module RedisQueuedLocks::Acquier
|
|
146
201
|
#
|
147
202
|
# @param redis [RedisClient] Redis connection client.
|
148
203
|
# @param lock_name [String] The lock name that should be released.
|
204
|
+
# @param isntrumenter [#notify] See RedisQueuedLocks::Instrument::ActiveSupport for example.
|
149
205
|
# @return [Hash<Symbol,Any>] Format: { ok: true/false, result: Any }
|
150
206
|
#
|
151
207
|
# @api private
|
152
208
|
# @since 0.1.0
|
153
|
-
def release_lock!(redis, lock_name)
|
209
|
+
def release_lock!(redis, lock_name, instrumenter)
|
154
210
|
lock_key = RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
|
155
211
|
lock_key_queue = RedisQueuedLocks::Resource.prepare_lock_queue(lock_name)
|
156
212
|
|
213
|
+
rel_start_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
157
214
|
result = fully_release_lock(redis, lock_key, lock_key_queue)
|
215
|
+
time_at = Time.now.to_i
|
216
|
+
rel_end_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
217
|
+
rel_time = ((rel_end_time - rel_start_time) * 1_000).ceil
|
218
|
+
|
219
|
+
instrumenter.notify('redis_queued_locks.explicit_lock_release', {
|
220
|
+
lock_key: lock_key,
|
221
|
+
lock_key_queue: lock_key_queue,
|
222
|
+
rel_time: rel_time,
|
223
|
+
at: time_at
|
224
|
+
})
|
225
|
+
|
158
226
|
{ ok: true, result: result }
|
159
227
|
end
|
160
228
|
|
@@ -164,31 +232,54 @@ module RedisQueuedLocks::Acquier
|
|
164
232
|
#
|
165
233
|
# @param redis [RedisClient] Redis connection client.
|
166
234
|
# @param batch_size [Integer] The number of lock keys that should be released in a time.
|
235
|
+
# @param isntrumenter [#notify] See RedisQueuedLocks::Instrument::ActiveSupport for example.
|
167
236
|
# @return [Hash<Symbol,Any>] Format: { ok: true/false, result: Any }
|
168
237
|
#
|
169
238
|
# @api private
|
170
239
|
# @since 0.1.0
|
171
|
-
def release_all_locks!(redis, batch_size)
|
240
|
+
def release_all_locks!(redis, batch_size, instrumenter)
|
241
|
+
rel_start_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
172
242
|
result = fully_release_all_locks(redis, batch_size)
|
243
|
+
time_at = Time.now.to_i
|
244
|
+
rel_end_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
245
|
+
rel_time = ((rel_end_time - rel_start_time) * 1_000).ceil
|
246
|
+
|
247
|
+
instrumenter.notify('redis_queued_locks.explicit_all_locks_release', {
|
248
|
+
at: time_at,
|
249
|
+
rel_time: rel_time,
|
250
|
+
rel_lock_cnt: result[:rel_lock_cnt],
|
251
|
+
rel_queue_cnt: result[:rel_queue_cnt]
|
252
|
+
})
|
253
|
+
|
173
254
|
{ ok: true, result: result }
|
174
255
|
end
|
175
256
|
|
176
257
|
private
|
177
258
|
|
178
|
-
# @param timeout [Integer]
|
179
|
-
#
|
180
|
-
# @
|
259
|
+
# @param timeout [NilClass,Integer]
|
260
|
+
# Time period after which the logic will fail with timeout error.
|
261
|
+
# @param lock_key [String]
|
262
|
+
# Lock name.
|
263
|
+
# @param raise_errors [Boolean]
|
264
|
+
# Raise erros on exceptional cases.
|
265
|
+
# @option on_timeout [Proc,NilClass]
|
266
|
+
# Callback invoked on Timeout::Error.
|
181
267
|
# @return [Any]
|
182
268
|
#
|
183
269
|
# @api private
|
184
270
|
# @since 0.1.0
|
185
|
-
def with_timeout(timeout, lock_key, on_timeout: nil, &block)
|
271
|
+
def with_timeout(timeout, lock_key, raise_errors, on_timeout: nil, &block)
|
186
272
|
::Timeout.timeout(timeout, &block)
|
187
273
|
rescue ::Timeout::Error
|
188
274
|
on_timeout.call unless on_timeout == nil
|
189
|
-
|
190
|
-
|
191
|
-
|
275
|
+
|
276
|
+
if raise_errors
|
277
|
+
raise(RedisQueuedLocks::LockAcquiermentTimeoutError, <<~ERROR_MESSAGE.strip)
|
278
|
+
Failed to acquire the lock "#{lock_key}" for the given timeout (#{timeout} seconds).
|
279
|
+
ERROR_MESSAGE
|
280
|
+
end
|
192
281
|
end
|
193
282
|
end
|
283
|
+
# rubocop:enable Metrics/ClassLength
|
194
284
|
end
|
285
|
+
# rubocop:enable Metrics/ModuleLength
|
@@ -10,23 +10,25 @@ class RedisQueuedLocks::Client
|
|
10
10
|
setting :retry_count, 3
|
11
11
|
setting :retry_delay, 200 # NOTE: milliseconds
|
12
12
|
setting :retry_jitter, 50 # NOTE: milliseconds
|
13
|
-
setting :
|
13
|
+
setting :default_timeout, 10 # NOTE: seconds
|
14
14
|
setting :exp_precision, 1 # NOTE: milliseconds
|
15
15
|
setting :default_lock_ttl, 10_000 # NOTE: milliseconds
|
16
16
|
setting :default_queue_ttl, 5 # NOTE: seconds
|
17
17
|
setting :lock_release_batch_size, 100
|
18
|
+
setting :instrumenter, RedisQueuedLocks::Instrument::VoidNotifier
|
18
19
|
|
19
20
|
# TODO: setting :logger, Logger.new(IO::NULL)
|
20
21
|
# TODO: setting :debug, true/false
|
21
22
|
|
22
|
-
validate
|
23
|
-
validate
|
24
|
-
validate
|
25
|
-
validate
|
26
|
-
validate
|
27
|
-
validate
|
28
|
-
validate
|
29
|
-
validate
|
23
|
+
validate('retry_count', :integer)
|
24
|
+
validate('retry_delay', :integer)
|
25
|
+
validate('retry_jitter', :integer)
|
26
|
+
validate('default_timeout', :integer)
|
27
|
+
validate('exp_precision', :integer)
|
28
|
+
validate('default_lock_tt', :integer)
|
29
|
+
validate('default_queue_ttl', :integer)
|
30
|
+
validate('lock_release_batch_size', :integer)
|
31
|
+
validate('instrumenter') { |instr| RedisQueuedLocks::Instrument.valid_interface?(instr) }
|
30
32
|
end
|
31
33
|
|
32
34
|
# @return [RedisClient]
|
@@ -58,15 +60,19 @@ class RedisQueuedLocks::Client
|
|
58
60
|
# @option ttl [Integer]
|
59
61
|
# Lock's time to live (in milliseconds).
|
60
62
|
# @option queue_ttl [Integer]
|
61
|
-
#
|
62
|
-
# @option timeout [Integer]
|
63
|
-
# Time period whe should try to acquire the lock (in seconds).
|
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".
|
64
66
|
# @option retry_count [Integer]
|
65
67
|
# How many times we should try to acquire a lock.
|
66
68
|
# @option retry_delay [Integer]
|
67
69
|
# A time-interval between the each retry (in milliseconds).
|
68
70
|
# @option retry_jitter [Integer]
|
69
71
|
# Time-shift range for retry-delay (in milliseconds).
|
72
|
+
# @option instrumenter [#notify]
|
73
|
+
# See RedisQueuedLocks::Instrument::ActiveSupport for example.
|
74
|
+
# @option raise_errors [Boolean]
|
75
|
+
# Raise errors on library-related limits such as timeout or failed lock obtain.
|
70
76
|
# @param [Block]
|
71
77
|
# A block of code that should be executed after the successfully acquired lock.
|
72
78
|
# @return [Hash<Symbol,Any>]
|
@@ -80,10 +86,11 @@ class RedisQueuedLocks::Client
|
|
80
86
|
thread_id: RedisQueuedLocks::Resource.get_thread_id,
|
81
87
|
ttl: config[:default_lock_ttl],
|
82
88
|
queue_ttl: config[:default_queue_ttl],
|
83
|
-
timeout: config[:
|
89
|
+
timeout: config[:default_timeout],
|
84
90
|
retry_count: config[:retry_count],
|
85
91
|
retry_delay: config[:retry_delay],
|
86
92
|
retry_jitter: config[:retry_jitter],
|
93
|
+
raise_errors: false,
|
87
94
|
&block
|
88
95
|
)
|
89
96
|
RedisQueuedLocks::Acquier.acquire_lock!(
|
@@ -97,24 +104,66 @@ class RedisQueuedLocks::Client
|
|
97
104
|
retry_count:,
|
98
105
|
retry_delay:,
|
99
106
|
retry_jitter:,
|
107
|
+
raise_errors:,
|
108
|
+
instrumenter: config[:instrumenter],
|
109
|
+
&block
|
110
|
+
)
|
111
|
+
end
|
112
|
+
|
113
|
+
# @note See #lock method signature.
|
114
|
+
#
|
115
|
+
# @api public
|
116
|
+
# @since 0.1.0
|
117
|
+
def lock!(
|
118
|
+
lock_name,
|
119
|
+
process_id: RedisQueuedLocks::Resource.get_process_id,
|
120
|
+
thread_id: RedisQueuedLocks::Resource.get_thread_id,
|
121
|
+
ttl: config[:default_lock_ttl],
|
122
|
+
queue_ttl: config[:default_queue_ttl],
|
123
|
+
timeout: config[:default_timeout],
|
124
|
+
retry_count: config[:retry_count],
|
125
|
+
retry_delay: config[:retry_delay],
|
126
|
+
retry_jitter: config[:retry_jitter],
|
127
|
+
&block
|
128
|
+
)
|
129
|
+
lock(
|
130
|
+
lock_name,
|
131
|
+
process_id:,
|
132
|
+
thread_id:,
|
133
|
+
ttl:,
|
134
|
+
queue_ttl:,
|
135
|
+
timeout:,
|
136
|
+
retry_count:,
|
137
|
+
retry_delay:,
|
138
|
+
retry_jitter:,
|
139
|
+
raise_errors: true,
|
100
140
|
&block
|
101
141
|
)
|
102
142
|
end
|
103
143
|
|
104
144
|
# @param lock_name [String] The lock name that should be released.
|
105
|
-
# @return [
|
145
|
+
# @return [Hash<Symbol,Any>] Format: { ok: true/false, result: Symbol/Hash }.
|
106
146
|
#
|
107
147
|
# @api public
|
108
148
|
# @since 0.1.0
|
109
149
|
def unlock(lock_name)
|
110
|
-
RedisQueuedLocks::Acquier.release_lock!(
|
150
|
+
RedisQueuedLocks::Acquier.release_lock!(
|
151
|
+
redis_client,
|
152
|
+
lock_name,
|
153
|
+
config[:instrumenter]
|
154
|
+
)
|
111
155
|
end
|
112
156
|
|
113
|
-
# @
|
157
|
+
# @option batch_size [Integer]
|
158
|
+
# @return [Hash<Symbol,Any>] Format: { ok: true/false, result: Symbol/Hash }.
|
114
159
|
#
|
115
160
|
# @api public
|
116
161
|
# @since 0.1.0
|
117
162
|
def clear_locks(batch_size: config[:lock_release_batch_size])
|
118
|
-
RedisQueuedLocks::Acquier.release_all_locks!(
|
163
|
+
RedisQueuedLocks::Acquier.release_all_locks!(
|
164
|
+
redis_client,
|
165
|
+
batch_size,
|
166
|
+
config[:instrumenter]
|
167
|
+
)
|
119
168
|
end
|
120
169
|
end
|
@@ -10,7 +10,7 @@ module RedisQueuedLocks::Debugger
|
|
10
10
|
# @api private
|
11
11
|
# @since 0.1.0
|
12
12
|
DEBUG_ENABLED_METHOD = <<~METHOD_DECLARATION.strip.freeze
|
13
|
-
def debug(message) = STDOUT.
|
13
|
+
def debug(message) = STDOUT.write("#\{message}\n")
|
14
14
|
METHOD_DECLARATION
|
15
15
|
|
16
16
|
# @return [String]
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# @api public
|
4
|
+
# @since 0.1.0
|
5
|
+
module RedisQueuedLocks::Instrument::ActiveSupport
|
6
|
+
class << self
|
7
|
+
# @param event [String]
|
8
|
+
# @param payload [Hash<String|Symbol,Any>]
|
9
|
+
# @return [void]
|
10
|
+
#
|
11
|
+
# @api private
|
12
|
+
# @since 0.1.0
|
13
|
+
def notify(event, payload = {})
|
14
|
+
::ActiveSupport::Notifications.instrument(event, payload)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# @api public
|
4
|
+
# @since 0.1.0
|
5
|
+
module RedisQueuedLocks::Instrument::VoidNotifier
|
6
|
+
class << self
|
7
|
+
# @param event [String]
|
8
|
+
# @param payload [Hash<String|Symbol,Any>]
|
9
|
+
# @return [void]
|
10
|
+
#
|
11
|
+
# @api private
|
12
|
+
# @since 0.1.0
|
13
|
+
def notify(event, payload = {}); end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# @api public
|
4
|
+
# @since 0.1.0
|
5
|
+
module RedisQueuedLocks::Instrument
|
6
|
+
require_relative 'instrument/void_notifier'
|
7
|
+
require_relative 'instrument/active_support'
|
8
|
+
|
9
|
+
class << self
|
10
|
+
# @param instrumenter [Class,Module,Object]
|
11
|
+
# @return [Boolean]
|
12
|
+
#
|
13
|
+
# @api public
|
14
|
+
# @since 0.1.0
|
15
|
+
def valid_interface?(instrumenter)
|
16
|
+
if instrumenter == RedisQueuedLocks::Instrument::ActiveSupport
|
17
|
+
# NOTE: active_support should be required in your app
|
18
|
+
defined?(::ActiveSupport::Notifications)
|
19
|
+
elsif instrumenter.respond_to?(:notify)
|
20
|
+
# NOTE: the method signature should be (event, payload). Supported variants:
|
21
|
+
# => [[:req, :event], [:req, :payload]]
|
22
|
+
# => [[:req, :event], [:opt, :payload]]
|
23
|
+
# => [[:opt, :event], [:opt, :payload]]
|
24
|
+
|
25
|
+
m_obj = instrumenter.method(:notify)
|
26
|
+
m_sig = m_obj.parameters
|
27
|
+
|
28
|
+
f_prm = m_sig[0][0]
|
29
|
+
s_prm = m_sig[1][0]
|
30
|
+
|
31
|
+
if m_sig.size == 2
|
32
|
+
# rubocop:disable Layout/MultilineOperationIndentation
|
33
|
+
# NOTE: check the signature vairants
|
34
|
+
f_prm == :req && s_prm == :req ||
|
35
|
+
f_prm == :req && s_prm == :opt ||
|
36
|
+
f_prm == :opt && s_prm == :opt
|
37
|
+
# rubocop:enable Layout/MultilineOperationIndentation
|
38
|
+
else
|
39
|
+
# NOTE: incompatible signature
|
40
|
+
false
|
41
|
+
end
|
42
|
+
else
|
43
|
+
# NOTE: no required method :notify
|
44
|
+
false
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/lib/redis_queued_locks.rb
CHANGED
@@ -12,7 +12,7 @@ module RedisQueuedLocks
|
|
12
12
|
require_relative 'redis_queued_locks/debugger'
|
13
13
|
require_relative 'redis_queued_locks/resource'
|
14
14
|
require_relative 'redis_queued_locks/acquier'
|
15
|
-
require_relative 'redis_queued_locks/
|
15
|
+
require_relative 'redis_queued_locks/instrument'
|
16
16
|
require_relative 'redis_queued_locks/client'
|
17
17
|
|
18
18
|
# @since 0.1.0
|
data/redis_queued_locks.gemspec
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: redis_queued_locks
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rustam Ibragimov
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-02-
|
11
|
+
date: 2024-02-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis-client
|
@@ -64,7 +64,9 @@ files:
|
|
64
64
|
- lib/redis_queued_locks/debugger.rb
|
65
65
|
- lib/redis_queued_locks/debugger/interface.rb
|
66
66
|
- lib/redis_queued_locks/errors.rb
|
67
|
-
- lib/redis_queued_locks/
|
67
|
+
- lib/redis_queued_locks/instrument.rb
|
68
|
+
- lib/redis_queued_locks/instrument/active_support.rb
|
69
|
+
- lib/redis_queued_locks/instrument/void_notifier.rb
|
68
70
|
- lib/redis_queued_locks/resource.rb
|
69
71
|
- lib/redis_queued_locks/version.rb
|
70
72
|
- redis_queued_locks.gemspec
|
@@ -83,14 +85,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
83
85
|
requirements:
|
84
86
|
- - ">="
|
85
87
|
- !ruby/object:Gem::Version
|
86
|
-
version: 3.
|
88
|
+
version: '3.1'
|
87
89
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
88
90
|
requirements:
|
89
91
|
- - ">="
|
90
92
|
- !ruby/object:Gem::Version
|
91
93
|
version: '0'
|
92
94
|
requirements: []
|
93
|
-
rubygems_version: 3.5.
|
95
|
+
rubygems_version: 3.5.1
|
94
96
|
signing_key:
|
95
97
|
specification_version: 4
|
96
98
|
summary: Queued distributed locks based on Redis.
|