redis_queued_locks 0.0.3 → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +308 -2
- data/lib/redis_queued_locks/acquier/release.rb +0 -2
- data/lib/redis_queued_locks/acquier.rb +5 -0
- data/lib/redis_queued_locks/client.rb +6 -6
- data/lib/redis_queued_locks/errors.rb +1 -1
- data/lib/redis_queued_locks/version.rb +3 -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: 1044ea4b8236b241132017ea9b9f2f8ac280c584be1158a9e7b52b15aa58d4ef
|
4
|
+
data.tar.gz: 5e6dc2141b05055f3641d88c9c181521ed6ad94100f6007573628e6aef7cee17
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8d8c0ac83545d663f0d558dc258364348a100e34c9636bf9663d8c021154c38c660ee626019f08bc5607c4ce01fc731a49c1fae3f5dc6e28d3a5d2ea41292d02
|
7
|
+
data.tar.gz: 1bbc2cfc740ff6eb3fa6e458dc5ed9dd4509885427eaf7e5eb8c2e74a605652dc9c39586d7282442befb1f199d0b84ff3ac573dfa43bfcc86bcb117f165e70ca
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,12 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.0.5] - 2024-02-26
|
4
|
+
- Minor update with documentation and configuration updates inside.
|
5
|
+
|
6
|
+
## [0.0.4] - 2024-02-26
|
7
|
+
### Changed
|
8
|
+
- changed default configuration values of `RedisQueuedLocks::Client` config;
|
9
|
+
|
3
10
|
## [0.0.3] - 2024-02-26
|
4
11
|
### Changed
|
5
12
|
- Instrumentation events:
|
data/README.md
CHANGED
@@ -1,8 +1,288 @@
|
|
1
1
|
# RedisQueuedLocks
|
2
2
|
|
3
3
|
Distributed lock implementation with "lock acquisition queue" capabilities based on the Redis Database.
|
4
|
+
Each lock request is put into a request queue and processed in order of their priority (FIFO).
|
4
5
|
|
5
|
-
|
6
|
+
---
|
7
|
+
|
8
|
+
## Table of Contents
|
9
|
+
|
10
|
+
- [Algorithm](#algorithm)
|
11
|
+
- [Installation](#installation)
|
12
|
+
- [Setup](#setup)
|
13
|
+
- [Configuration](#configuration)
|
14
|
+
- [Usage](#usage)
|
15
|
+
- [lock](#lock---obtain-a-lock)
|
16
|
+
- [lock!](#lock---exeptional-lock-obtaining)
|
17
|
+
- [unlock](#unlock---release-a-lock)
|
18
|
+
- [clear_locks](#clear_locks---release-all-locks-and-lock-queues)
|
19
|
+
- [Instrumentation](#instrumentation)
|
20
|
+
- [Instrumentation Events](#instrumentation-events)
|
21
|
+
- [TODO](#todo)
|
22
|
+
- [Contributing](#contributing)
|
23
|
+
- [License](#license)
|
24
|
+
- [Authors](#authors)
|
25
|
+
|
26
|
+
---
|
27
|
+
|
28
|
+
### Algorithm
|
29
|
+
|
30
|
+
- soon;
|
31
|
+
|
32
|
+
---
|
33
|
+
|
34
|
+
### Installation
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
gem 'redis_queued_locks'
|
38
|
+
```
|
39
|
+
|
40
|
+
```shell
|
41
|
+
bundle install
|
42
|
+
# --- or ---
|
43
|
+
gem install redis_queued_locks
|
44
|
+
```
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
require 'redis_queued_locks'
|
48
|
+
```
|
49
|
+
|
50
|
+
---
|
51
|
+
|
52
|
+
### Setup
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
require 'redis_queued_locks'
|
56
|
+
|
57
|
+
# Step 1: initialize RedisClient instance
|
58
|
+
redis_clinet = RedisClient.config.new_pool # NOTE: provide your ounw RedisClient instance
|
59
|
+
|
60
|
+
# Step 2: initialize RedisQueuedLock::Client instance
|
61
|
+
rq_lock_client = RedisQueuedLocks::Client.new(redis_client) do |config|
|
62
|
+
# NOTE:
|
63
|
+
# - some your application-related configs;
|
64
|
+
# - for documentation see <Configuration> section in readme;
|
65
|
+
end
|
66
|
+
|
67
|
+
# Step 3: start to work with locks :)
|
68
|
+
```
|
69
|
+
|
70
|
+
---
|
71
|
+
|
72
|
+
### Configuration
|
73
|
+
|
74
|
+
```ruby
|
75
|
+
redis_client = RedisClient.config.new_pool # NOTE: provide your own RedisClient instance
|
76
|
+
|
77
|
+
clinet = RedisQueuedLocks::Client.new(redis_client) do |config|
|
78
|
+
# (default: 3) (supports nil)
|
79
|
+
# - nil means "infinite retries" and you are only limited by the "try_to_lock_timeout" config;
|
80
|
+
config.retry_count = 3
|
81
|
+
|
82
|
+
# (milliseconds) (default: 200)
|
83
|
+
config.retry_delay = 200
|
84
|
+
|
85
|
+
# (milliseconds) (default: 50)
|
86
|
+
config.retry_jitter = 50
|
87
|
+
|
88
|
+
# (seconds) (supports nil)
|
89
|
+
# - nil means "no timeout" and you are only limited by "retry_count" config;
|
90
|
+
config.try_to_lock_timeout = 10
|
91
|
+
|
92
|
+
# (milliseconds) (default: 1)
|
93
|
+
# - expiration precision. affects the ttl (ttl + precision);
|
94
|
+
config.exp_precision = 1
|
95
|
+
|
96
|
+
# (milliseconds) (default: 5_000)
|
97
|
+
# - lock's time to live
|
98
|
+
config.default_lock_ttl = 5_000
|
99
|
+
|
100
|
+
# (seconds) (default: 15)
|
101
|
+
# - lock request timeout. after this timeout your lock request in queue will be requeued;
|
102
|
+
config.default_queue_ttl = 15
|
103
|
+
|
104
|
+
# (default: 100)
|
105
|
+
# - how many items will be released at a time in RedisQueuedLocks::Client#clear_locks logic;
|
106
|
+
# - affects the performancs capabilites (redis, rubyvm);
|
107
|
+
config.lock_release_batch_size = 100
|
108
|
+
|
109
|
+
# (default: RedisQueuedLocks::Instrument::VoidNotifier)
|
110
|
+
# - instrumentation layer;
|
111
|
+
# - you can provde your own instrumenter with `#notify(event, payload = {})`
|
112
|
+
# - event: <string> requried;
|
113
|
+
# - payload: <hash> requried;
|
114
|
+
# - disabled by default via VoidNotifier;
|
115
|
+
config.instrumenter = RedisQueuedLocks::Instrument::ActiveSupport
|
116
|
+
end
|
117
|
+
```
|
118
|
+
|
119
|
+
---
|
120
|
+
|
121
|
+
### Usage
|
122
|
+
|
123
|
+
- [lock](#lock---obtain-a-lock)
|
124
|
+
- [lock!](#lock---exeptional-lock-obtaining)
|
125
|
+
- [unlock](#unlock---release-a-lock)
|
126
|
+
- [clear_locks](#clear_locks---release-all-locks-and-lock-queues)
|
127
|
+
|
128
|
+
---
|
129
|
+
|
130
|
+
#### lock - obtain a lock
|
131
|
+
|
132
|
+
```ruby
|
133
|
+
def lock(
|
134
|
+
lock_name,
|
135
|
+
process_id: RedisQueuedLocks::Resource.get_process_id,
|
136
|
+
thread_id: RedisQueuedLocks::Resource.get_thread_id,
|
137
|
+
ttl: config[:default_lock_ttl],
|
138
|
+
queue_ttl: config[:default_queue_ttl],
|
139
|
+
timeout: config[:try_to_lock_timeout],
|
140
|
+
retry_count: config[:retry_count],
|
141
|
+
retry_delay: config[:retry_delay],
|
142
|
+
retry_jitter: config[:retry_jitter],
|
143
|
+
raise_errors: false,
|
144
|
+
&block
|
145
|
+
)
|
146
|
+
```
|
147
|
+
|
148
|
+
- `lock_name` - `[String]`
|
149
|
+
- Lock name to be obtained.
|
150
|
+
- `process_id` - `[Integer,String]`
|
151
|
+
- The process that want to acquire the lock.
|
152
|
+
- `thread_id` - `[Integer,String]`
|
153
|
+
- The process's thread that want to acquire the lock.
|
154
|
+
- `ttl` [Integer]
|
155
|
+
- Lock's time to live (in milliseconds).
|
156
|
+
- `queue_ttl` - `[Integer]`
|
157
|
+
- Lifetime of the acuier's lock request. In seconds.
|
158
|
+
- `timeout` - `[Integer,NilClass]`
|
159
|
+
- Time period whe should try to acquire the lock (in seconds). Nil means "without timeout".
|
160
|
+
- `retry_count` - `[Integer,NilClass]`
|
161
|
+
- How many times we should try to acquire a lock. Nil means "infinite retries".
|
162
|
+
- `retry_delay` - `[Integer]`
|
163
|
+
- A time-interval between the each retry (in milliseconds).
|
164
|
+
- `retry_jitter` - `[Integer]`
|
165
|
+
- Time-shift range for retry-delay (in milliseconds).
|
166
|
+
- `instrumenter` - `[#notify]`
|
167
|
+
- See RedisQueuedLocks::Instrument::ActiveSupport for example.
|
168
|
+
- `raise_errors` - `[Boolean]`
|
169
|
+
- Raise errors on library-related limits such as timeout or failed lock obtain.
|
170
|
+
- `block` - `[Block]`
|
171
|
+
- A block of code that should be executed after the successfully acquired lock.
|
172
|
+
|
173
|
+
Return value:
|
174
|
+
- `[Hash<Symbol,Any>]` Format: `{ ok: true/false, result: Symbol/Hash }`;
|
175
|
+
- for successful lock obtaining:
|
176
|
+
```ruby
|
177
|
+
{
|
178
|
+
ok: true,
|
179
|
+
result: {
|
180
|
+
lock_key: String, # acquierd lock key ("rql:lock:your_lock_name")
|
181
|
+
acq_id: String, # acquier identifier ("your_process_id/your_thread_id")
|
182
|
+
ts: Integer, # time (epoch) when lock was obtained (integer)
|
183
|
+
ttl: Integer # lock's time to live in milliseconds (integer)
|
184
|
+
}
|
185
|
+
}
|
186
|
+
```
|
187
|
+
- for failed lock obtaining:
|
188
|
+
```ruby
|
189
|
+
{ ok: false, result: :acquier_is_not_first_in_queue }
|
190
|
+
{ ok: false, result: :lock_is_still_acquired }
|
191
|
+
{ ok: false, result: :lock_is_acquired_during_acquire_race }
|
192
|
+
{ ok: false, result: :unknown }
|
193
|
+
```
|
194
|
+
|
195
|
+
|
196
|
+
---
|
197
|
+
|
198
|
+
#### #lock! - exceptional lock obtaining
|
199
|
+
|
200
|
+
- fails when:
|
201
|
+
- `timeout` limit reached before lock is obtained;
|
202
|
+
- `retry_count` limit reached before lock is obtained;
|
203
|
+
|
204
|
+
```ruby
|
205
|
+
def lock!(
|
206
|
+
lock_name,
|
207
|
+
process_id: RedisQueuedLocks::Resource.get_process_id,
|
208
|
+
thread_id: RedisQueuedLocks::Resource.get_thread_id,
|
209
|
+
ttl: config[:default_lock_ttl],
|
210
|
+
queue_ttl: config[:default_queue_ttl],
|
211
|
+
timeout: config[:default_timeout],
|
212
|
+
retry_count: config[:retry_count],
|
213
|
+
retry_delay: config[:retry_delay],
|
214
|
+
retry_jitter: config[:retry_jitter],
|
215
|
+
&block
|
216
|
+
)
|
217
|
+
```
|
218
|
+
|
219
|
+
See `#lock` method [documentation](#lock---obtain-a-lock).
|
220
|
+
|
221
|
+
---
|
222
|
+
|
223
|
+
#### #unlock - release a lock
|
224
|
+
|
225
|
+
- release the concrete lock with lock request queue;
|
226
|
+
- queue will be relased first;
|
227
|
+
|
228
|
+
```ruby
|
229
|
+
def unlock(lock_name)
|
230
|
+
```
|
231
|
+
|
232
|
+
- `lock_name` - `[String]`
|
233
|
+
- the lock name that should be released.
|
234
|
+
|
235
|
+
Return:
|
236
|
+
- `[Hash<Symbol,Any>]` - Format: `{ ok: true/false, result: Symbol/Hash }`;
|
237
|
+
|
238
|
+
---
|
239
|
+
|
240
|
+
#### #clear_locks - release all locks and lock queues
|
241
|
+
|
242
|
+
- release all obtained locks and related lock request queues;
|
243
|
+
- queues will be released first;
|
244
|
+
|
245
|
+
```ruby
|
246
|
+
def clear_locks(batch_size: config[:lock_release_batch_size])
|
247
|
+
```
|
248
|
+
|
249
|
+
- `batch_size` - `[Integer]`
|
250
|
+
- batch of cleared locks and lock queus unde the one pipelined redis command;
|
251
|
+
|
252
|
+
Return:
|
253
|
+
- `[Hash<Symbol,Any>]` - Format: `{ ok: true/false, result: Symbol/Hash }`;
|
254
|
+
|
255
|
+
---
|
256
|
+
|
257
|
+
## Instrumentation
|
258
|
+
|
259
|
+
An instrumentation layer is incapsulated in `instrumenter` object stored in configurations.
|
260
|
+
|
261
|
+
Instrumenter object should provide `notify(event, payload)` method with following signarue:
|
262
|
+
|
263
|
+
- `event` - `string`;
|
264
|
+
- `payload` - `hash<Symbol,Any>`;
|
265
|
+
|
266
|
+
`redis_queued_locks` provides two instrumenters:
|
267
|
+
|
268
|
+
- `RedisQueuedLocks::Instrument::ActiveSupport` - `ActiveSupport::Notifications` instrumenter
|
269
|
+
that instrument events via `ActiveSupport::Notifications` api;
|
270
|
+
- `RedisQueuedLocks::Instrument::VoidNotifier` - instrumenter that does nothing;
|
271
|
+
|
272
|
+
By default `RedisQueuedLocks::Client` is configured with the void notifier (which means "instrumentation is disabled").
|
273
|
+
|
274
|
+
---
|
275
|
+
|
276
|
+
### Instrumentation Events
|
277
|
+
|
278
|
+
List of instrumentation events
|
279
|
+
|
280
|
+
- **redis_queued_locks.lock_obtained**
|
281
|
+
- **redis_queued_locks.lock_hold_and_release**
|
282
|
+
- **redis_queued_locks.explicit_lock_release**
|
283
|
+
- **redis_queued_locks.explicit_all_locks_release**
|
284
|
+
|
285
|
+
Detalized event semantics and payload structure:
|
6
286
|
|
7
287
|
- `"redis_queued_locks.lock_obtained"`
|
8
288
|
- a moment when the lock was obtained;
|
@@ -32,6 +312,32 @@ Distributed lock implementation with "lock acquisition queue" capabilities based
|
|
32
312
|
- `"redis_queued_locks.explicit_all_locks_release"`
|
33
313
|
- an event signalizes about the explicit all locks release (invoked via `RedisQueuedLock#clear_locks`);
|
34
314
|
- payload:
|
35
|
-
- `rel_time` - `float`/`milliseconds` - time spent on
|
315
|
+
- `rel_time` - `float`/`milliseconds` - time spent on "realese all locks" operation;
|
36
316
|
- `at` - `integer`/`epoch` - the time when the operation has ended;
|
37
317
|
- `rel_keys` - `integer` - released redis keys count (`released queu keys` + `released lock keys`);
|
318
|
+
|
319
|
+
---
|
320
|
+
|
321
|
+
## TODO
|
322
|
+
|
323
|
+
- `RedisQueuedLocks::Acquier::Try.try_to_lock` - detailed successful result analization;
|
324
|
+
- `100%` test coverage;
|
325
|
+
- better code stylization and interesting refactorings :)
|
326
|
+
|
327
|
+
---
|
328
|
+
|
329
|
+
## Contributing
|
330
|
+
|
331
|
+
- Fork it ( https://github.com/0exp/redis_queued_locks )
|
332
|
+
- Create your feature branch (`git checkout -b feature/my-new-feature`)
|
333
|
+
- Commit your changes (`git commit -am '[feature_context] Add some feature'`)
|
334
|
+
- Push to the branch (`git push origin feature/my-new-feature`)
|
335
|
+
- Create new Pull Request
|
336
|
+
|
337
|
+
## License
|
338
|
+
|
339
|
+
Released under MIT License.
|
340
|
+
|
341
|
+
## Authors
|
342
|
+
|
343
|
+
[Rustam Ibragimov](https://github.com/0exp)
|
@@ -37,7 +37,6 @@ module RedisQueuedLocks::Acquier::Release
|
|
37
37
|
RedisQueuedLocks::Resource::LOCK_QUEUE_PATTERN,
|
38
38
|
count: batch_size
|
39
39
|
) do |lock_queue|
|
40
|
-
puts "RELEASE (lock_queue): #{lock_queue}"
|
41
40
|
pipeline.call('ZREMRANGEBYSCORE', lock_queue, '-inf', '+inf')
|
42
41
|
pipeline.call('EXPIRE', RedisQueuedLocks::Resource.lock_key_from_queue(lock_queue), "0")
|
43
42
|
end
|
@@ -48,7 +47,6 @@ module RedisQueuedLocks::Acquier::Release
|
|
48
47
|
RedisQueuedLocks::Resource::LOCK_PATTERN,
|
49
48
|
count: batch_size
|
50
49
|
) do |lock_key|
|
51
|
-
puts "RELEASE (lock_key): #{lock_key}"
|
52
50
|
pipeline.call('EXPIRE', lock_key, '0')
|
53
51
|
end
|
54
52
|
end
|
@@ -149,6 +149,11 @@ module RedisQueuedLocks::Acquier
|
|
149
149
|
acq_process[:should_try] = false
|
150
150
|
# NOTE: reached the retry limit => dequeue from the lock queue
|
151
151
|
acq_dequeue.call
|
152
|
+
# NOTE: check and raise an error
|
153
|
+
raise(LockAcquiermentRetryLimitError, <<~ERROR_MESSAGE.strip) if raise_errors
|
154
|
+
Failed to acquire the lock "#{lock_key}"
|
155
|
+
for the given retry_count limit (#{retry_count} times).
|
156
|
+
ERROR_MESSAGE
|
152
157
|
elsif delay_execution(retry_delay, retry_jitter)
|
153
158
|
# NOTE:
|
154
159
|
# delay the exceution in order to prevent chaotic attempts
|
@@ -10,10 +10,10 @@ 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 :try_to_lock_timeout, 10 # NOTE: seconds
|
14
14
|
setting :exp_precision, 1 # NOTE: milliseconds
|
15
15
|
setting :default_lock_ttl, 5_000 # NOTE: milliseconds
|
16
|
-
setting :default_queue_ttl,
|
16
|
+
setting :default_queue_ttl, 15 # NOTE: seconds
|
17
17
|
setting :lock_release_batch_size, 100
|
18
18
|
setting :instrumenter, RedisQueuedLocks::Instrument::VoidNotifier
|
19
19
|
|
@@ -23,7 +23,7 @@ class RedisQueuedLocks::Client
|
|
23
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
|
-
validate('
|
26
|
+
validate('try_to_lock_timeout') { |val| val == nil || (val.is_a?(::Integer) && val >= 0) }
|
27
27
|
validate('exp_precision', :integer)
|
28
28
|
validate('default_lock_tt', :integer)
|
29
29
|
validate('default_queue_ttl', :integer)
|
@@ -52,7 +52,7 @@ class RedisQueuedLocks::Client
|
|
52
52
|
end
|
53
53
|
|
54
54
|
# @param lock_name [String]
|
55
|
-
# Lock name to be
|
55
|
+
# Lock name to be obtained.
|
56
56
|
# @option process_id [Integer,String]
|
57
57
|
# The process that want to acquire the lock.
|
58
58
|
# @option thread_id [Integer,String]
|
@@ -73,7 +73,7 @@ class RedisQueuedLocks::Client
|
|
73
73
|
# See RedisQueuedLocks::Instrument::ActiveSupport for example.
|
74
74
|
# @option raise_errors [Boolean]
|
75
75
|
# Raise errors on library-related limits such as timeout or failed lock obtain.
|
76
|
-
# @param [Block]
|
76
|
+
# @param block [Block]
|
77
77
|
# A block of code that should be executed after the successfully acquired lock.
|
78
78
|
# @return [Hash<Symbol,Any>]
|
79
79
|
# Format: { ok: true/false, result: Symbol/Hash }.
|
@@ -86,7 +86,7 @@ class RedisQueuedLocks::Client
|
|
86
86
|
thread_id: RedisQueuedLocks::Resource.get_thread_id,
|
87
87
|
ttl: config[:default_lock_ttl],
|
88
88
|
queue_ttl: config[:default_queue_ttl],
|
89
|
-
timeout: config[:
|
89
|
+
timeout: config[:try_to_lock_timeout],
|
90
90
|
retry_count: config[:retry_count],
|
91
91
|
retry_delay: config[:retry_delay],
|
92
92
|
retry_jitter: config[:retry_jitter],
|