redis_queued_locks 0.0.4 → 0.0.6

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 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