redis_queued_locks 0.0.13 → 0.0.15

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: '0067915f812330a031b51bc20300a13eee31aa2628aa2b26c90524c58d80ab60'
4
- data.tar.gz: f322cbff42c80656ad09d075008c48461db991c153ab04b42e4187f54bcc6bce
3
+ metadata.gz: 3270e2a14a4eed87379d84fe0622f4a231a681fb1e48325a1d6e22becafb5a60
4
+ data.tar.gz: b67274aeec060e1bef07d64933928c5be3632cc68f88f12cbe01b9105ae41478
5
5
  SHA512:
6
- metadata.gz: fbe61d02314ec926529a30cb3771903199897b798f386ff6a5ebb562d3857b92c11d5579a4d6cc9219fa64e152ff1e4f6aeb1ceb98cf8eae094c443b8ec2f6b9
7
- data.tar.gz: 3462b766ffb13ca57d21caa7d4725b076a3dc6c594601288f8c7593cb496f58d3ce4b66082035d8ab4bbfa3eba395db475995cacb3a4eb588b03ab06a25eef27
6
+ metadata.gz: a40531666d37dc44d738fba4e67e027520a58f029c392a46db8a2e465200d4162ad1fdd44e16b17001edab183b8f9f90621d35f6b026a9c2628abc00ab9ea5b3
7
+ data.tar.gz: 40392bb5de7d615e52e4a92b5f193c7c72970d220a9963ef89346364788c2dc1d11ef5664b20d0864dd3b151aba6a1f6bb76b45cdc5c016882dd51883df25281
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.0.15] - 2024-02-28
4
+ ### Added
5
+ - An ability to fail fast if the required lock is already obtained;
6
+
7
+ ## [0.0.14] - 2024-02-28
8
+ ### Changed
9
+ - Minor documentation updates;
10
+
3
11
  ## [0.0.13] - 2024-02-27
4
12
  ### Changed
5
13
  - Minor development updates;
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Distributed locks with "lock acquisition queue" capabilities based on the Redis Database.
4
4
 
5
- Each lock request is put into a request queue and processed in order of their priority (FIFO).
5
+ Each lock request is put into the request queue and processed in order of their priority (FIFO). Each lock request lives some period of time (RTTL) which guarantees that the request queue will never be stacked.
6
6
 
7
7
  ---
8
8
 
@@ -145,6 +145,8 @@ end
145
145
 
146
146
  - If block is passed the obtained lock will be released after the block execution or the lock's ttl (what will happen first);
147
147
  - If block is not passed the obtained lock will be released after lock's ttl;
148
+ - If block is passed the block's yield result will be returned;
149
+ - If block is not passed the lock information will be returned;
148
150
 
149
151
  ```ruby
150
152
  def lock(
@@ -156,6 +158,7 @@ def lock(
156
158
  retry_delay: config[:retry_delay],
157
159
  retry_jitter: config[:retry_jitter],
158
160
  raise_errors: false,
161
+ fail_fast: false,
159
162
  identity: uniq_identity, # (attr_accessor) calculated during client instantiation via config[:uniq_identifier] proc;
160
163
  &block
161
164
  )
@@ -179,6 +182,11 @@ def lock(
179
182
  - See RedisQueuedLocks::Instrument::ActiveSupport for example.
180
183
  - `raise_errors` - `[Boolean]`
181
184
  - Raise errors on library-related limits such as timeout or retry count limit.
185
+ - `fail_fast` - `[Boolean]`
186
+ - Should the required lock to be checked before the try and exit immidietly if lock is
187
+ already obtained;
188
+ - Should the logic exit immidietly after the first try if the lock was obtained
189
+ by another process while the lock request queue was initially empty;
182
190
  - `identity` - `[String]`
183
191
  - An unique string that is unique per `RedisQueuedLock::Client` instance. Resolves the
184
192
  collisions between the same process_id/thread_id/fiber_id/ractor_id identifiers on different
@@ -191,25 +199,32 @@ def lock(
191
199
  - If block is **not passed** the obtained lock will be released after it's ttl;
192
200
 
193
201
  Return value:
194
- - `[Hash<Symbol,Any>]` Format: `{ ok: true/false, result: Symbol/Hash }`;
195
- - for successful lock obtaining:
196
- ```ruby
197
- {
198
- ok: true,
199
- result: {
200
- lock_key: String, # acquierd lock key ("rql:lock:your_lock_name")
201
- acq_id: String, # acquier identifier ("process_id/thread_id/fiber_id/ractor_id/identity")
202
- ts: Integer, # time (epoch) when lock was obtained (integer)
203
- ttl: Integer # lock's time to live in milliseconds (integer)
202
+
203
+ - If block is passed the block's yield result will be returned;
204
+ - If block is not passed the lock information will be returned;
205
+ - Lock information result:
206
+ - Signature: `[yield, Hash<Symbol,Boolean|Hash<Symbol,Numeric|String>>]`
207
+ - Format: `{ ok: true/false, result: <Symbol|Hash<Symbol,Hash>> }`;
208
+ - for successful lock obtaining:
209
+ ```ruby
210
+ {
211
+ ok: true,
212
+ result: {
213
+ lock_key: String, # acquierd lock key ("rql:lock:your_lock_name")
214
+ acq_id: String, # acquier identifier ("process_id/thread_id/fiber_id/ractor_id/identity")
215
+ ts: Integer, # time (epoch) when lock was obtained (integer)
216
+ ttl: Integer # lock's time to live in milliseconds (integer)
217
+ }
204
218
  }
205
- }
206
- ```
207
- - for failed lock obtaining:
208
- ```ruby
209
- { ok: false, result: :timeout_reached }
210
- { ok: false, result: :retry_count_reached }
211
- { ok: false, result: :unknown }
212
- ```
219
+ ```
220
+ - for failed lock obtaining:
221
+ ```ruby
222
+ { ok: false, result: :timeout_reached }
223
+ { ok: false, result: :retry_count_reached }
224
+ { ok: false, result: :fail_fast_no_try } # see <fail_fast> option
225
+ { ok: false, result: :fail_fast_after_try } # see <fail_fast> option
226
+ { ok: false, result: :unknown }
227
+ ```
213
228
 
214
229
  ---
215
230
 
@@ -229,6 +244,7 @@ def lock!(
229
244
  retry_delay: config[:retry_delay],
230
245
  retry_jitter: config[:retry_jitter],
231
246
  identity: uniq_identity,
247
+ fail_fast: false,
232
248
  &block
233
249
  )
234
250
  ```
@@ -277,6 +293,14 @@ rql.lock_info("your_lock_name")
277
293
  - `acq_id` - `string` - acquier identifier (process_id/thread_id/fiber_id/ractor_id/identity by default);
278
294
  - `score` - `float`/`epoch` - time when the lock request was made (epoch);
279
295
 
296
+ ```
297
+ | Returns an information about the required lock queue by the lock name. The result
298
+ | represnts the ordered lock request queue that is ordered by score (Redis sets) and shows
299
+ | lock acquirers and their position in queue. Async nature with redis communcation can lead
300
+ | the situation when the queue becomes empty during the queue data extraction. So sometimes
301
+ | you can receive the lock queue info with empty queue value (an empty array).
302
+ ```
303
+
280
304
  ```ruby
281
305
  rql.queue_info("your_lock_name")
282
306
 
@@ -327,7 +351,7 @@ def unlock(lock_name)
327
351
  - the lock name that should be released.
328
352
 
329
353
  Return:
330
- - `[Hash<Symbol,Any>]` - Format: `{ ok: true/false, result: Hash<Symbol,Numeric|String> }`;
354
+ - `[Hash<Symbol,Numeric|String>]` - Format: `{ ok: true/false, result: Hash<Symbol,Numeric|String> }`;
331
355
 
332
356
  ```ruby
333
357
  {
@@ -352,10 +376,10 @@ def clear_locks(batch_size: config[:lock_release_batch_size])
352
376
  ```
353
377
 
354
378
  - `batch_size` - `[Integer]`
355
- - batch of cleared locks and lock queus unde the one pipelined redis command;
379
+ - the size of batch of locks and lock queus that should be cleared under the one pipelined redis command at once;
356
380
 
357
381
  Return:
358
- - `[Hash<Symbol,Any>]` - Format: `{ ok: true/false, result: Hash<Symbol,Numeric> }`;
382
+ - `[Hash<Symbol,Numeric>]` - Format: `{ ok: true/false, result: Hash<Symbol,Numeric> }`;
359
383
 
360
384
  ```ruby
361
385
  {
@@ -392,44 +416,44 @@ By default `RedisQueuedLocks::Client` is configured with the void notifier (whic
392
416
 
393
417
  List of instrumentation events
394
418
 
395
- - **redis_queued_locks.lock_obtained**
396
- - **redis_queued_locks.lock_hold_and_release**
397
- - **redis_queued_locks.explicit_lock_release**
398
- - **redis_queued_locks.explicit_all_locks_release**
419
+ - `redis_queued_locks.lock_obtained`
420
+ - `redis_queued_locks.lock_hold_and_release`
421
+ - `redis_queued_locks.explicit_lock_release`
422
+ - `redis_queued_locks.explicit_all_locks_release`
399
423
 
400
424
  Detalized event semantics and payload structure:
401
425
 
402
426
  - `"redis_queued_locks.lock_obtained"`
403
427
  - a moment when the lock was obtained;
404
428
  - payload:
405
- - `ttl` - `integer`/`milliseconds` - lock ttl;
406
- - `acq_id` - `string` - lock acquier identifier;
407
- - `lock_key` - `string` - lock name;
408
- - `ts` - `integer`/`epoch` - the time when the lock was obtaiend;
409
- - `acq_time` - `float`/`milliseconds` - time spent on lock acquiring;
429
+ - `:ttl` - `integer`/`milliseconds` - lock ttl;
430
+ - `:acq_id` - `string` - lock acquier identifier;
431
+ - `:lock_key` - `string` - lock name;
432
+ - `:ts` - `integer`/`epoch` - the time when the lock was obtaiend;
433
+ - `:acq_time` - `float`/`milliseconds` - time spent on lock acquiring;
410
434
  - `"redis_queued_locks.lock_hold_and_release"`
411
435
  - an event signalizes about the "hold+and+release" process
412
436
  when the lock obtained and hold by the block of logic;
413
437
  - payload:
414
- - `hold_time` - `float`/`milliseconds` - lock hold time;
415
- - `ttl` - `integer`/`milliseconds` - lock ttl;
416
- - `acq_id` - `string` - lock acquier identifier;
417
- - `lock_key` - `string` - lock name;
418
- - `ts` - `integer`/`epoch` - the time when lock was obtained;
419
- - `acq_time` - `float`/`milliseconds` - time spent on lock acquiring;
438
+ - `:hold_time` - `float`/`milliseconds` - lock hold time;
439
+ - `:ttl` - `integer`/`milliseconds` - lock ttl;
440
+ - `:acq_id` - `string` - lock acquier identifier;
441
+ - `:lock_key` - `string` - lock name;
442
+ - `:ts` - `integer`/`epoch` - the time when lock was obtained;
443
+ - `:acq_time` - `float`/`milliseconds` - time spent on lock acquiring;
420
444
  - `"redis_queued_locks.explicit_lock_release"`
421
445
  - an event signalizes about the explicit lock release (invoked via `RedisQueuedLock#unlock`);
422
446
  - payload:
423
- - `at` - `integer`/`epoch` - the time when the lock was released;
424
- - `rel_time` - `float`/`milliseconds` - time spent on lock releasing;
425
- - `lock_key` - `string` - released lock (lock name);
426
- - `lock_key_queue` - `string` - released lock queue (lock queue name);
447
+ - `:at` - `integer`/`epoch` - the time when the lock was released;
448
+ - `:rel_time` - `float`/`milliseconds` - time spent on lock releasing;
449
+ - `:lock_key` - `string` - released lock (lock name);
450
+ - `:lock_key_queue` - `string` - released lock queue (lock queue name);
427
451
  - `"redis_queued_locks.explicit_all_locks_release"`
428
452
  - an event signalizes about the explicit all locks release (invoked via `RedisQueuedLock#clear_locks`);
429
453
  - payload:
430
- - `rel_time` - `float`/`milliseconds` - time spent on "realese all locks" operation;
431
- - `at` - `integer`/`epoch` - the time when the operation has ended;
432
- - `rel_keys` - `integer` - released redis keys count (`released queue keys` + `released lock keys`);
454
+ - `:rel_time` - `float`/`milliseconds` - time spent on "realese all locks" operation;
455
+ - `:at` - `integer`/`epoch` - the time when the operation has ended;
456
+ - `:rel_keys` - `integer` - released redis keys count (`released queue keys` + `released lock keys`);
433
457
 
434
458
  ---
435
459
 
@@ -438,7 +462,10 @@ Detalized event semantics and payload structure:
438
462
  - **Major**
439
463
  - Semantic Error objects for unexpected Redis errors;
440
464
  - `100%` test coverage;
441
- - sidecar `Ractor` object and `in progress queue` in RedisDB that will extend an acquired lock for long-running blocks of code (that invoked "under" the lock);
465
+ - per-block-holding-the-lock sidecar `Ractor` and `in progress queue` in RedisDB that will extend
466
+ the acquired lock for long-running blocks of code (that invoked "under" the lock
467
+ whose ttl may expire before the block execution completes);
468
+ - an ability to add custom metadata to the lock and an ability to read this data;
442
469
  - **Minor**
443
470
  - `RedisQueuedLocks::Acquier::Try.try_to_lock` - detailed successful result analization;
444
471
  - better code stylization and interesting refactorings;
@@ -10,112 +10,132 @@ module RedisQueuedLocks::Acquier::Try
10
10
  # @param acquier_position [Numeric]
11
11
  # @param ttl [Integer]
12
12
  # @param queue_ttl [Integer]
13
+ # @param fail_fast [Boolean]
13
14
  # @return [Hash<Symbol,Any>] Format: { ok: true/false, result: Symbol|Hash<Symbol,Any> }
14
15
  #
15
16
  # @api private
16
17
  # @since 0.1.0
17
18
  # rubocop:disable Metrics/MethodLength
18
- def try_to_lock(redis, lock_key, lock_key_queue, acquier_id, acquier_position, ttl, queue_ttl)
19
+ def try_to_lock(
20
+ redis,
21
+ lock_key,
22
+ lock_key_queue,
23
+ acquier_id,
24
+ acquier_position,
25
+ ttl,
26
+ queue_ttl,
27
+ fail_fast
28
+ )
19
29
  # Step X: intermediate invocation results
20
30
  inter_result = nil
21
31
  timestamp = nil
22
32
 
23
33
  # Step 0: watch the lock key changes (and discard acquirement if lock is already acquired)
24
34
  result = redis.multi(watch: [lock_key]) do |transact|
25
- # Step 1: add an acquier to the lock acquirement queue
26
- res = redis.call('ZADD', lock_key_queue, 'NX', acquier_position, acquier_id)
27
-
28
- RedisQueuedLocks.debug(
29
- "Step №1: добавление в очередь (#{acquier_id}). [ZADD to the queue: #{res}]"
30
- )
31
-
32
- # Step 2.1: drop expired acquiers from the lock queue
33
- res = redis.call(
34
- 'ZREMRANGEBYSCORE',
35
- lock_key_queue,
36
- '-inf',
37
- RedisQueuedLocks::Resource.acquier_dead_score(queue_ttl)
38
- )
39
-
40
- RedisQueuedLocks.debug(
41
- "Step №2: дропаем из очереди просроченных ожидающих. [ZREMRANGE: #{res}]"
42
- )
43
-
44
- # Step 3: get the actual acquier waiting in the queue
45
- waiting_acquier = Array(redis.call('ZRANGE', lock_key_queue, '0', '0')).first
35
+ # Fast-Step X0: fail-fast check
36
+ if fail_fast && redis.call('HGET', lock_key, 'acq_id')
37
+ # Fast-Step X1: is lock already obtained. fail fast - no try.
38
+ inter_result = :fail_fast_no_try
39
+ else
40
+ # Step 1: add an acquier to the lock acquirement queue
41
+ res = redis.call('ZADD', lock_key_queue, 'NX', acquier_position, acquier_id)
46
42
 
47
- RedisQueuedLocks.debug(
48
- "Step №3: какой процесс в очереди сейчас ждет. " \
49
- "[ZRANGE <следующий процесс>: #{waiting_acquier} :: <текущий процесс>: #{acquier_id}]"
50
- )
43
+ RedisQueuedLocks.debug(
44
+ "Step №1: добавление в очередь (#{acquier_id}). [ZADD to the queue: #{res}]"
45
+ )
51
46
 
52
- # Step 4: check the actual acquier: is it ours? are we aready to lock?
53
- unless waiting_acquier == acquier_id
54
- # Step ROLLBACK 1.1: our time hasn't come yet. retry!
47
+ # Step 2.1: drop expired acquiers from the lock queue
48
+ res = redis.call(
49
+ 'ZREMRANGEBYSCORE',
50
+ lock_key_queue,
51
+ '-inf',
52
+ RedisQueuedLocks::Resource.acquier_dead_score(queue_ttl)
53
+ )
55
54
 
56
55
  RedisQueuedLocks.debug(
57
- "Step ROLLBACK 1: не одинаковые ключи. выходим. " \
58
- "[Ждет: #{waiting_acquier}. А нужен: #{acquier_id}]"
56
+ "Step №2: дропаем из очереди просроченных ожидающих. [ZREMRANGE: #{res}]"
59
57
  )
60
58
 
61
- inter_result = :acquier_is_not_first_in_queue
62
- else
63
- # NOTE: our time has come! let's try to acquire the lock!
64
-
65
- # Step 5: check if the our lock is already acquired
66
- locked_by_acquier = redis.call('HGET', lock_key, 'acq_id')
59
+ # Step 3: get the actual acquier waiting in the queue
60
+ waiting_acquier = Array(redis.call('ZRANGE', lock_key_queue, '0', '0')).first
67
61
 
68
62
  RedisQueuedLocks.debug(
69
- "Ste5: Ищем требуемый лок. " \
70
- "[HGET<#{lock_key}>: " \
71
- "#{(locked_by_acquier == nil) ? 'не занят' : "занят процессом <#{locked_by_acquier}>"}"
63
+ "Step3: какой процесс в очереди сейчас ждет. " \
64
+ "[ZRANGE <следующий процесс>: #{waiting_acquier} :: <текущий процесс>: #{acquier_id}]"
72
65
  )
73
66
 
74
- if locked_by_acquier
75
- # Step ROLLBACK 2: required lock is stil acquired. retry!
67
+ # Step 4: check the actual acquier: is it ours? are we aready to lock?
68
+ unless waiting_acquier == acquier_id
69
+ # Step ROLLBACK 1.1: our time hasn't come yet. retry!
76
70
 
77
71
  RedisQueuedLocks.debug(
78
- "Step ROLLBACK №2: Ключ уже занят. Ничего не делаем. " \
79
- "[Занят процессом: #{locked_by_acquier}]"
72
+ "Step ROLLBACK №1: не одинаковые ключи. выходим. " \
73
+ "[Ждет: #{waiting_acquier}. А нужен: #{acquier_id}]"
80
74
  )
81
75
 
82
- inter_result = :lock_is_still_acquired
76
+ inter_result = :acquier_is_not_first_in_queue
83
77
  else
84
- # NOTE: required lock is free and ready to be acquired! acquire!
78
+ # NOTE: our time has come! let's try to acquire the lock!
85
79
 
86
- # Step 6.1: remove our acquier from waiting queue
87
- transact.call('ZPOPMIN', lock_key_queue, '1')
88
-
89
- RedisQueuedLocks.debug(
90
- 'Step №4: Забираем наш текущий процесс из очереди. [ZPOPMIN]'
91
- )
80
+ # Step 5: check if the our lock is already acquired
81
+ locked_by_acquier = redis.call('HGET', lock_key, 'acq_id')
92
82
 
93
83
  RedisQueuedLocks.debug(
94
- "===> <FINAL> Step 6: закрепляем лок за процессом [HSET<#{lock_key}>: #{acquier_id}]"
95
- )
96
-
97
- # Step 6.2: acquire a lock and store an info about the acquier
98
- transact.call(
99
- 'HSET',
100
- lock_key,
101
- 'acq_id', acquier_id,
102
- 'ts', (timestamp = Time.now.to_i),
103
- 'ini_ttl', ttl
84
+ "Ste5: Ищем требуемый лок. " \
85
+ "[HGET<#{lock_key}>: " \
86
+ "#{(locked_by_acquier == nil) ? 'не занят' : "занят процессом <#{locked_by_acquier}>"}"
104
87
  )
105
88
 
106
- # Step 6.3: set the lock expiration time in order to prevent "infinite locks"
107
- transact.call('PEXPIRE', lock_key, ttl) # NOTE: in seconds
89
+ if locked_by_acquier
90
+ # Step ROLLBACK 2: required lock is stil acquired. retry!
91
+
92
+ RedisQueuedLocks.debug(
93
+ "Step ROLLBACK №2: Ключ уже занят. Ничего не делаем. " \
94
+ "[Занят процессом: #{locked_by_acquier}]"
95
+ )
96
+
97
+ inter_result = :lock_is_still_acquired
98
+ else
99
+ # NOTE: required lock is free and ready to be acquired! acquire!
100
+
101
+ # Step 6.1: remove our acquier from waiting queue
102
+ transact.call('ZPOPMIN', lock_key_queue, '1')
103
+
104
+ RedisQueuedLocks.debug(
105
+ 'Step №4: Забираем наш текущий процесс из очереди. [ZPOPMIN]'
106
+ )
107
+
108
+ RedisQueuedLocks.debug(
109
+ "===> <FINAL> Step №6: закрепляем лок за процессом [HSET<#{lock_key}>: #{acquier_id}]"
110
+ )
111
+
112
+ # Step 6.2: acquire a lock and store an info about the acquier
113
+ transact.call(
114
+ 'HSET',
115
+ lock_key,
116
+ 'acq_id', acquier_id,
117
+ 'ts', (timestamp = Time.now.to_i),
118
+ 'ini_ttl', ttl
119
+ )
120
+
121
+ # Step 6.3: set the lock expiration time in order to prevent "infinite locks"
122
+ transact.call('PEXPIRE', lock_key, ttl) # NOTE: in milliseconds
123
+ end
108
124
  end
109
125
  end
110
126
  end
111
127
 
112
128
  # Step 7: Analyze the aquirement attempt:
129
+ # rubocop:disable Lint/DuplicateBranch
113
130
  case
131
+ when fail_fast && inter_result == :fail_fast_no_try
132
+ # Step 7.a: lock is still acquired and we should exit from the logic as soon as possible
133
+ { ok: false, result: inter_result }
114
134
  when inter_result == :lock_is_still_acquired || inter_result == :acquier_is_not_first_in_queue
115
- # Step 7.a: lock is still acquired by another process => failed to acquire
135
+ # Step 7.b: lock is still acquired by another process => failed to acquire
116
136
  { ok: false, result: inter_result }
117
137
  when result == nil || (result.is_a?(::Array) && result.empty?)
118
- # Step 7.b: lock is already acquired durign the acquire race => failed to acquire
138
+ # Step 7.c: lock is already acquired durign the acquire race => failed to acquire
119
139
  { ok: false, result: :lock_is_acquired_during_acquire_race }
120
140
  when result.is_a?(::Array) && result.size == 3 # NOTE: 3 is a count of redis lock commands
121
141
  # TODO:
@@ -126,12 +146,13 @@ module RedisQueuedLocks::Acquier::Try
126
146
  # 2. hset should return 2 (lock key is added to the redis as a hashmap with 2 fields)
127
147
  # 3. pexpire should return 1 (expiration time is successfully applied)
128
148
 
129
- # Step 7.c: locked! :) let's go! => successfully acquired
149
+ # Step 7.d: locked! :) let's go! => successfully acquired
130
150
  { ok: true, result: { lock_key: lock_key, acq_id: acquier_id, ts: timestamp, ttl: ttl } }
131
151
  else
132
- # Ste 7.d: unknown behaviour :thinking:
152
+ # Ste 7.3: unknown behaviour :thinking:
133
153
  { ok: false, result: :unknown }
134
154
  end
155
+ # rubocop:enable Lint/DuplicateBranch
135
156
  end
136
157
  # rubocop:enable Metrics/MethodLength, Metrics/PerceivedComplexity
137
158
 
@@ -58,10 +58,14 @@ module RedisQueuedLocks::Acquier
58
58
  # Unique acquire identifier that is also should be unique between processes and pods
59
59
  # on different machines. By default the uniq identity string is
60
60
  # represented as 10 bytes hexstr.
61
+ # @option fail_fast [Boolean]
62
+ # Should the required lock to be checked before the try and exit immidetly if lock is
63
+ # already obtained.
61
64
  # @param [Block]
62
65
  # A block of code that should be executed after the successfully acquired lock.
63
- # @return [Hash<Symbol,Any>]
64
- # Format: { ok: true/false, result: Any }
66
+ # @return [Hash<Symbol,Any>,yield]
67
+ # - Format: { ok: true/false, result: Any }
68
+ # - If block is given the result of block's yeld will be returned.
65
69
  #
66
70
  # @api private
67
71
  # @since 0.1.0
@@ -82,6 +86,7 @@ module RedisQueuedLocks::Acquier
82
86
  raise_errors:,
83
87
  instrumenter:,
84
88
  identity:,
89
+ fail_fast:,
85
90
  &block
86
91
  )
87
92
  # Step 1: prepare lock requirements (generate lock name, calc lock ttl, etc).
@@ -126,7 +131,8 @@ module RedisQueuedLocks::Acquier
126
131
  acquier_id,
127
132
  acquier_position,
128
133
  lock_ttl,
129
- queue_ttl
134
+ queue_ttl,
135
+ fail_fast
130
136
  ) => { ok:, result: }
131
137
 
132
138
  acq_end_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
@@ -157,21 +163,40 @@ module RedisQueuedLocks::Acquier
157
163
  acq_process[:should_try] = false
158
164
  acq_process[:acq_time] = acq_time
159
165
  acq_process[:acq_end_time] = acq_end_time
166
+ elsif fail_fast && acq_process[:result] == :fail_fast_no_try
167
+ acq_process[:should_try] = false
168
+ if raise_errors
169
+ raise(RedisQueuedLocks::LockAlreadyObtainedError, <<~ERROR_MESSAGE.strip)
170
+ Lock "#{lock_key}" is already obtained.
171
+ ERROR_MESSAGE
172
+ end
160
173
  else
161
174
  # Step 2.1.b: failed acquirement => retry
162
175
  acq_process[:tries] += 1
163
176
 
164
- if retry_count != nil && acq_process[:tries] >= retry_count
165
- # NOTE: reached the retry limit => quit from the loop
177
+ if (retry_count != nil && acq_process[:tries] >= retry_count) || fail_fast
178
+ # NOTE:
179
+ # - reached the retry limit => quit from the loop
180
+ # - should fail fast => quit from the loop
166
181
  acq_process[:should_try] = false
167
- acq_process[:result] = :retry_limit_reached
168
- # NOTE: reached the retry limit => dequeue from the lock queue
182
+ acq_process[:result] = fail_fast ? :fail_fast_after_try : :retry_limit_reached
183
+
184
+ # NOTE:
185
+ # - reached the retry limit => dequeue from the lock queue
186
+ # - should fail fast => dequeue from the lock queue
169
187
  acq_dequeue.call
188
+
170
189
  # NOTE: check and raise an error
171
- raise(LockAcquiermentRetryLimitError, <<~ERROR_MESSAGE.strip) if raise_errors
172
- Failed to acquire the lock "#{lock_key}"
173
- for the given retry_count limit (#{retry_count} times).
174
- ERROR_MESSAGE
190
+ if fail_fast && raise_errors
191
+ raise(RedisQueuedLocks::LockAlreadyObtainedError, <<~ERROR_MESSAGE.strip)
192
+ Lock "#{lock_key}" is already obtained.
193
+ ERROR_MESSAGE
194
+ elsif raise_errors
195
+ raise(RedisQueuedLocks::LockAcquiermentRetryLimitError, <<~ERROR_MESSAGE.strip)
196
+ Failed to acquire the lock "#{lock_key}"
197
+ for the given retry_count limit (#{retry_count} times).
198
+ ERROR_MESSAGE
199
+ end
175
200
  else
176
201
  # NOTE:
177
202
  # delay the exceution in order to prevent chaotic attempts
@@ -210,8 +235,10 @@ module RedisQueuedLocks::Acquier
210
235
  { ok: true, result: acq_process[:lock_info] }
211
236
  end
212
237
  else
213
- unless acq_process[:result] == :retry_limit_reached
214
- # NOTE: we have only two situations if lock is not acquired:
238
+ if acq_process[:result] != :retry_limit_reached &&
239
+ acq_process[:result] != :fail_fast_no_try &&
240
+ acq_process[:result] != :fail_fast_after_try
241
+ # NOTE: we have only two situations if lock is not acquired withou fast-fail flag:
215
242
  # - time limit is reached
216
243
  # - retry count limit is reached
217
244
  # In other cases the lock obtaining time and tries count are infinite.
@@ -363,7 +390,7 @@ module RedisQueuedLocks::Acquier
363
390
  # Returns an information about the required lock queue by the lock name. The result
364
391
  # represnts the ordered lock request queue that is ordered by score (Redis sets) and shows
365
392
  # lock acquirers and their position in queue. Async nature with redis communcation can lead
366
- # the sitation when the queue becomes empty during the queue data extraction. So sometimes
393
+ # the sitaution when the queue becomes empty during the queue data extraction. So sometimes
367
394
  # you can receive the lock queue info with empty queue.
368
395
  #
369
396
  # @param redis_client [RedisClient]
@@ -22,8 +22,8 @@ class RedisQueuedLocks::Client
22
22
  # TODO: setting :debug, true/false
23
23
 
24
24
  validate('retry_count') { |val| val == nil || (val.is_a?(::Integer) && val >= 0) }
25
- validate('retry_delay', :integer)
26
- validate('retry_jitter', :integer)
25
+ validate('retry_delay') { |val| val.is_a?(::Integer) && val >= 0 }
26
+ validate('retry_jitter') { |val| val.is_a?(::Integer) && val >= 0 }
27
27
  validate('try_to_lock_timeout') { |val| val == nil || (val.is_a?(::Integer) && val >= 0) }
28
28
  validate('default_lock_tt', :integer)
29
29
  validate('default_queue_ttl', :integer)
@@ -44,6 +44,9 @@ class RedisQueuedLocks::Client
44
44
  # @since 0.1.0
45
45
  attr_accessor :uniq_identity
46
46
 
47
+ # NOTE: attr_access is chosen intentionally in order to have an ability to change
48
+ # uniq_identity values for debug purposes in runtime;
49
+
47
50
  # @param redis_client [RedisClient]
48
51
  # Redis connection manager, which will be used for the lock acquierment and distribution.
49
52
  # It should be an instance of RedisClient.
@@ -81,10 +84,16 @@ class RedisQueuedLocks::Client
81
84
  # Unique acquire identifier that is also should be unique between processes and pods
82
85
  # on different machines. By default the uniq identity string is
83
86
  # represented as 10 bytes hexstr.
87
+ # @option fail_fast [Boolean]
88
+ # - Should the required lock to be checked before the try and exit immidietly if lock is
89
+ # already obtained;
90
+ # - Should the logic exit immidietly after the first try if the lock was obtained
91
+ # by another process while the lock request queue was initially empty;
84
92
  # @param block [Block]
85
93
  # A block of code that should be executed after the successfully acquired lock.
86
94
  # @return [Hash<Symbol,Any>,yield]
87
- # Format: { ok: true/false, result: Symbol/Hash }.
95
+ # - Format: { ok: true/false, result: Symbol/Hash }.
96
+ # - If block is given the result of block's yeld will be returned.
88
97
  #
89
98
  # @api public
90
99
  # @since 0.1.0
@@ -97,6 +106,7 @@ class RedisQueuedLocks::Client
97
106
  retry_delay: config[:retry_delay],
98
107
  retry_jitter: config[:retry_jitter],
99
108
  raise_errors: false,
109
+ fail_fast: false,
100
110
  identity: uniq_identity,
101
111
  &block
102
112
  )
@@ -116,6 +126,7 @@ class RedisQueuedLocks::Client
116
126
  raise_errors:,
117
127
  instrumenter: config[:instrumenter],
118
128
  identity:,
129
+ fail_fast:,
119
130
  &block
120
131
  )
121
132
  end
@@ -128,10 +139,11 @@ class RedisQueuedLocks::Client
128
139
  lock_name,
129
140
  ttl: config[:default_lock_ttl],
130
141
  queue_ttl: config[:default_queue_ttl],
131
- timeout: config[:default_timeout],
142
+ timeout: config[:try_to_lock_timeout],
132
143
  retry_count: config[:retry_count],
133
144
  retry_delay: config[:retry_delay],
134
145
  retry_jitter: config[:retry_jitter],
146
+ fail_fast: false,
135
147
  identity: uniq_identity,
136
148
  &block
137
149
  )
@@ -145,6 +157,7 @@ class RedisQueuedLocks::Client
145
157
  retry_jitter:,
146
158
  raise_errors: true,
147
159
  identity:,
160
+ fail_fast:,
148
161
  &block
149
162
  )
150
163
  end
@@ -9,6 +9,10 @@ module RedisQueuedLocks
9
9
  # @since 0.1.0
10
10
  ArgumentError = Class.new(::ArgumentError)
11
11
 
12
+ # @api public
13
+ # @since 0.1.0
14
+ LockAlreadyObtainedError = Class.new(Error)
15
+
12
16
  # @api public
13
17
  # @since 0.1.0
14
18
  LockAcquiermentTimeoutError = Class.new(Error)
@@ -5,6 +5,6 @@ module RedisQueuedLocks
5
5
  #
6
6
  # @api public
7
7
  # @since 0.0.1
8
- # @version 0.0.13
9
- VERSION = '0.0.13'
8
+ # @version 0.0.15
9
+ VERSION = '0.0.15'
10
10
  end
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.13
4
+ version: 0.0.15
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-27 00:00:00.000000000 Z
11
+ date: 2024-02-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis-client