redis_queued_locks 0.0.4 → 0.0.5
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 +3 -0
- data/README.md +256 -11
- 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 +2 -2
- data/lib/redis_queued_locks/errors.rb +1 -1
- 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: 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
data/README.md
CHANGED
@@ -1,8 +1,75 @@
|
|
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
|
6
73
|
|
7
74
|
```ruby
|
8
75
|
redis_client = RedisClient.config.new_pool # NOTE: provide your own RedisClient instance
|
@@ -49,21 +116,173 @@ clinet = RedisQueuedLocks::Client.new(redis_client) do |config|
|
|
49
116
|
end
|
50
117
|
```
|
51
118
|
|
52
|
-
|
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
|
53
131
|
|
54
132
|
```ruby
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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])
|
59
247
|
```
|
60
248
|
|
61
|
-
-
|
62
|
-
-
|
63
|
-
|
64
|
-
|
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
|
65
258
|
|
66
|
-
|
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:
|
67
286
|
|
68
287
|
- `"redis_queued_locks.lock_obtained"`
|
69
288
|
- a moment when the lock was obtained;
|
@@ -96,3 +315,29 @@ end
|
|
96
315
|
- `rel_time` - `float`/`milliseconds` - time spent on "realese all locks" operation;
|
97
316
|
- `at` - `integer`/`epoch` - the time when the operation has ended;
|
98
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
|
@@ -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 }.
|