redis_queued_locks 0.0.4 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6e1e1a54762132f92451d595938cb467b7b644dffc0817abe99209e1f92160f8
4
- data.tar.gz: 297eec3d7fcee35d12e7f3509e8e3dfd4b8b3c52889c7570a79cd6a30f45eda4
3
+ metadata.gz: 9e98a72e1519c54d21a6273176a7d8a3bd9fa3033b7f30ca73fdca5a4335341d
4
+ data.tar.gz: b2cf62db6016bd456a379feb3703989cc4a481f2eed4103c5c27a60bec624d46
5
5
  SHA512:
6
- metadata.gz: 6d2c64622bd3b8465a749b5f6a8050d5fd1d5b623990f91421f91004c444baab12114964ded28d5ac0fa43fed56f3e893798feb31b00ab173d536e57fcfdfaa4
7
- data.tar.gz: 02ee9d8742ffd3a7b16b8c23b8d4c4f58a8cb1b9db4d215ece4bf34de0c79830bd3417186b2d75dbbd48f6845d736361af7503f641e7faa16d139cbe193263d3
6
+ metadata.gz: d90c848b5fb00614c3f61719d470d7a2dae008d3b6349c395674fe7bf262feea09795dafc74756a16426f499652e9cb0dcb4eef8b0dc9e2c1cc8abff2a1345d1
7
+ data.tar.gz: ccc6e73175a2c41988303e3ea73feeacf95833b8d318dc1318c91f800d656ea157aeca62d7eba491ca489253a01ba4489f6a4c254c32f54ef3a540c9312ca179
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.0.6] - 2024-02-27
4
+ ### Changed
5
+ - Major documentation updates;
6
+ - `RedisQueuedLock#release_lock!` now returns detaield semantic result;
7
+ - `RediSQueuedLock#release_all_locks!` now returns detailed semantic result;
8
+
9
+ ## [0.0.5] - 2024-02-26
10
+ ### Changed
11
+ - Minor gem update with documentation and configuration updates inside.
12
+
3
13
  ## [0.0.4] - 2024-02-26
4
14
  ### Changed
5
15
  - changed default configuration values of `RedisQueuedLocks::Client` config;
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
- ## Configuration
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,193 @@ clinet = RedisQueuedLocks::Client.new(redis_client) do |config|
49
116
  end
50
117
  ```
51
118
 
52
- ## Usage
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
- redis_clinet = RedisClient.config.new_pool # NOTE: provide your ounw RedisClient instance
56
- rq_lock = RedisQueuedLocks::Client.new(redis_client) do |config|
57
- # NOTE: some your application-related configs
58
- end
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: :timeout_reached }
190
+ { ok: false, result: :retry_count_reached }
191
+ { ok: false, result: :unknown }
192
+ ```
193
+
194
+
195
+ ---
196
+
197
+ #### #lock! - exceptional lock obtaining
198
+
199
+ - fails when (and with):
200
+ - (`RedisQueuedLocks::LockAcquiermentTimeoutError`) `timeout` limit reached before lock is obtained;
201
+ - (`RedisQueuedLocks::LockAcquiermentRetryLimitError`) `retry_count` limit reached before lock is obtained;
202
+
203
+ ```ruby
204
+ def lock!(
205
+ lock_name,
206
+ process_id: RedisQueuedLocks::Resource.get_process_id,
207
+ thread_id: RedisQueuedLocks::Resource.get_thread_id,
208
+ ttl: config[:default_lock_ttl],
209
+ queue_ttl: config[:default_queue_ttl],
210
+ timeout: config[:default_timeout],
211
+ retry_count: config[:retry_count],
212
+ retry_delay: config[:retry_delay],
213
+ retry_jitter: config[:retry_jitter],
214
+ &block
215
+ )
216
+ ```
217
+
218
+ See `#lock` method [documentation](#lock---obtain-a-lock).
219
+
220
+ ---
221
+
222
+ #### #unlock - release a lock
223
+
224
+ - release the concrete lock with lock request queue;
225
+ - queue will be relased first;
226
+
227
+ ```ruby
228
+ def unlock(lock_name)
229
+ ```
230
+
231
+ - `lock_name` - `[String]`
232
+ - the lock name that should be released.
233
+
234
+ Return:
235
+ - `[Hash<Symbol,Any>]` - Format: `{ ok: true/false, result: Hash<Symbol,Numeric|String> }`;
236
+
237
+ ```ruby
238
+ {
239
+ ok: true,
240
+ result: {
241
+ rel_time: 0.02, # time spent to lock release (in seconds)
242
+ rel_key: "rql:lock:your_lock_name", # released lock key
243
+ rel_queue: "rql:lock_queue:your_lock_name" # released lock key queue
244
+ }
245
+ }
246
+ ```
247
+
248
+ ---
249
+
250
+ #### #clear_locks - release all locks and lock queues
251
+
252
+ - release all obtained locks and related lock request queues;
253
+ - queues will be released first;
254
+
255
+ ```ruby
256
+ def clear_locks(batch_size: config[:lock_release_batch_size])
257
+ ```
258
+
259
+ - `batch_size` - `[Integer]`
260
+ - batch of cleared locks and lock queus unde the one pipelined redis command;
261
+
262
+ Return:
263
+ - `[Hash<Symbol,Any>]` - Format: `{ ok: true/false, result: Hash<Symbol,Numeric> }`;
264
+
265
+ ```ruby
266
+ {
267
+ ok: true,
268
+ result: {
269
+ rel_time: 3.07, # time spent to release all locks and related lock queues
270
+ rel_key_cnt: 100_500 # released redis keys (released locks + released lock queues)
271
+ }
272
+ }
59
273
  ```
60
274
 
61
- - `#lock`
62
- - `#lock!`
63
- - `#unlock`
64
- - `#clear_locks`
275
+ ---
65
276
 
66
- ## Instrumentation events
277
+ ## Instrumentation
278
+
279
+ An instrumentation layer is incapsulated in `instrumenter` object stored in configurations.
280
+
281
+ Instrumenter object should provide `notify(event, payload)` method with following signarue:
282
+
283
+ - `event` - `string`;
284
+ - `payload` - `hash<Symbol,Any>`;
285
+
286
+ `redis_queued_locks` provides two instrumenters:
287
+
288
+ - `RedisQueuedLocks::Instrument::ActiveSupport` - `ActiveSupport::Notifications` instrumenter
289
+ that instrument events via `ActiveSupport::Notifications` api;
290
+ - `RedisQueuedLocks::Instrument::VoidNotifier` - instrumenter that does nothing;
291
+
292
+ By default `RedisQueuedLocks::Client` is configured with the void notifier (which means "instrumentation is disabled").
293
+
294
+ ---
295
+
296
+ ### Instrumentation Events
297
+
298
+ List of instrumentation events
299
+
300
+ - **redis_queued_locks.lock_obtained**
301
+ - **redis_queued_locks.lock_hold_and_release**
302
+ - **redis_queued_locks.explicit_lock_release**
303
+ - **redis_queued_locks.explicit_all_locks_release**
304
+
305
+ Detalized event semantics and payload structure:
67
306
 
68
307
  - `"redis_queued_locks.lock_obtained"`
69
308
  - a moment when the lock was obtained;
@@ -96,3 +335,29 @@ end
96
335
  - `rel_time` - `float`/`milliseconds` - time spent on "realese all locks" operation;
97
336
  - `at` - `integer`/`epoch` - the time when the operation has ended;
98
337
  - `rel_keys` - `integer` - released redis keys count (`released queu keys` + `released lock keys`);
338
+
339
+ ---
340
+
341
+ ## TODO
342
+
343
+ - `RedisQueuedLocks::Acquier::Try.try_to_lock` - detailed successful result analization;
344
+ - `100%` test coverage;
345
+ - better code stylization and interesting refactorings :)
346
+
347
+ ---
348
+
349
+ ## Contributing
350
+
351
+ - Fork it ( https://github.com/0exp/redis_queued_locks )
352
+ - Create your feature branch (`git checkout -b feature/my-new-feature`)
353
+ - Commit your changes (`git commit -am '[feature_context] Add some feature'`)
354
+ - Push to the branch (`git push origin feature/my-new-feature`)
355
+ - Create new Pull Request
356
+
357
+ ## License
358
+
359
+ Released under MIT License.
360
+
361
+ ## Authors
362
+
363
+ [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
@@ -60,7 +60,7 @@ module RedisQueuedLocks::Acquier
60
60
  #
61
61
  # @api private
62
62
  # @since 0.1.0
63
- # rubocop:disable Metrics/MethodLength
63
+ # rubocop:disable Metrics/MethodLength, Metrics/BlockNesting
64
64
  def acquire_lock!(
65
65
  redis,
66
66
  lock_name,
@@ -147,12 +147,19 @@ module RedisQueuedLocks::Acquier
147
147
  if retry_count != nil && acq_process[:tries] >= retry_count
148
148
  # NOTE: reached the retry limit => quit from the loop
149
149
  acq_process[:should_try] = false
150
+ acq_process[:result] = :retry_limit_reached
150
151
  # NOTE: reached the retry limit => dequeue from the lock queue
151
152
  acq_dequeue.call
152
- elsif delay_execution(retry_delay, retry_jitter)
153
+ # NOTE: check and raise an error
154
+ raise(LockAcquiermentRetryLimitError, <<~ERROR_MESSAGE.strip) if raise_errors
155
+ Failed to acquire the lock "#{lock_key}"
156
+ for the given retry_count limit (#{retry_count} times).
157
+ ERROR_MESSAGE
158
+ else
153
159
  # NOTE:
154
160
  # delay the exceution in order to prevent chaotic attempts
155
161
  # and to allow other processes and threads to obtain the lock too.
162
+ delay_execution(retry_delay, retry_jitter)
156
163
  end
157
164
  end
158
165
  end
@@ -186,12 +193,18 @@ module RedisQueuedLocks::Acquier
186
193
  { ok: true, result: acq_process[:lock_info] }
187
194
  end
188
195
  else
189
- # Step 3.b: lock is not acquired:
190
- # => drop itslef from the queue and return the reason of the failed acquirement
196
+ unless acq_process[:result] == :retry_limit_reached
197
+ # NOTE: we have only two situations if lock is not acquired:
198
+ # - time limit is reached
199
+ # - retry count limit is reached
200
+ # In other cases the lock obtaining time and tries count are infinite.
201
+ acq_process[:result] = :timeout_reached
202
+ end
203
+ # Step 3.b: lock is not acquired (acquier is dequeued by timeout callback)
191
204
  { ok: false, result: acq_process[:result] }
192
205
  end
193
206
  end
194
- # rubocop:enable Metrics/MethodLength
207
+ # rubocop:enable Metrics/MethodLength, Metrics/BlockNesting
195
208
 
196
209
  # Release the concrete lock:
197
210
  # - 1. clear lock queue: al; related processes released
@@ -203,7 +216,7 @@ module RedisQueuedLocks::Acquier
203
216
  # @param redis [RedisClient] Redis connection client.
204
217
  # @param lock_name [String] The lock name that should be released.
205
218
  # @param isntrumenter [#notify] See RedisQueuedLocks::Instrument::ActiveSupport for example.
206
- # @return [Hash<Symbol,Any>] Format: { ok: true/false, result: Any }
219
+ # @return [Hash<Symbol,Any>] Format: { ok: true/false, result: Hash<Symbil,Numeric|String> }
207
220
  #
208
221
  # @api private
209
222
  # @since 0.1.0
@@ -226,7 +239,7 @@ module RedisQueuedLocks::Acquier
226
239
  })
227
240
  end
228
241
 
229
- { ok: true, result: result }
242
+ { ok: true, result: { rel_time: rel_time, rel_key: lock_key, rel_queue: lock_key_queue } }
230
243
  end
231
244
 
232
245
  # Release all locks:
@@ -236,7 +249,7 @@ module RedisQueuedLocks::Acquier
236
249
  # @param redis [RedisClient] Redis connection client.
237
250
  # @param batch_size [Integer] The number of lock keys that should be released in a time.
238
251
  # @param isntrumenter [#notify] See RedisQueuedLocks::Instrument::ActiveSupport for example.
239
- # @return [Hash<Symbol,Any>] Format: { ok: true/false, result: Any }
252
+ # @return [Hash<Symbol,Any>] Format: { ok: true/false, result: Hash<Symbol,Numeric> }
240
253
  #
241
254
  # @api private
242
255
  # @since 0.1.0
@@ -255,7 +268,7 @@ module RedisQueuedLocks::Acquier
255
268
  })
256
269
  end
257
270
 
258
- { ok: true, result: result }
271
+ { ok: true, result: { rel_key_cnt: result[:rel_keys], rel_time: rel_time } }
259
272
  end
260
273
 
261
274
  private
@@ -52,7 +52,7 @@ class RedisQueuedLocks::Client
52
52
  end
53
53
 
54
54
  # @param lock_name [String]
55
- # Lock name to be acquier.
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 }.
@@ -15,5 +15,5 @@ module RedisQueuedLocks
15
15
 
16
16
  # @api public
17
17
  # @since 0.1.0
18
- LockAcquiermentLimitError = Class.new(Error)
18
+ LockAcquiermentRetryLimitError = Class.new(Error)
19
19
  end
@@ -5,6 +5,6 @@ module RedisQueuedLocks
5
5
  #
6
6
  # @api public
7
7
  # @since 0.0.1
8
- # @version 0.0.4
9
- VERSION = '0.0.4'
8
+ # @version 0.0.6
9
+ VERSION = '0.0.6'
10
10
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redis_queued_locks
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rustam Ibragimov