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 +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +72 -45
- data/lib/redis_queued_locks/acquier/try.rb +89 -68
- data/lib/redis_queued_locks/acquier.rb +41 -14
- data/lib/redis_queued_locks/client.rb +17 -4
- data/lib/redis_queued_locks/errors.rb +4 -0
- data/lib/redis_queued_locks/version.rb +2 -2
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3270e2a14a4eed87379d84fe0622f4a231a681fb1e48325a1d6e22becafb5a60
|
4
|
+
data.tar.gz: b67274aeec060e1bef07d64933928c5be3632cc68f88f12cbe01b9105ae41478
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
-
|
195
|
-
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
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
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
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,
|
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
|
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,
|
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
|
-
-
|
396
|
-
-
|
397
|
-
-
|
398
|
-
-
|
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
|
-
-
|
406
|
-
-
|
407
|
-
-
|
408
|
-
-
|
409
|
-
-
|
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
|
-
-
|
415
|
-
-
|
416
|
-
-
|
417
|
-
-
|
418
|
-
-
|
419
|
-
-
|
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
|
-
-
|
424
|
-
-
|
425
|
-
-
|
426
|
-
-
|
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
|
-
-
|
431
|
-
-
|
432
|
-
-
|
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`
|
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(
|
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
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
-
|
48
|
-
|
49
|
-
|
50
|
-
)
|
43
|
+
RedisQueuedLocks.debug(
|
44
|
+
"Step №1: добавление в очередь (#{acquier_id}). [ZADD to the queue: #{res}]"
|
45
|
+
)
|
51
46
|
|
52
|
-
|
53
|
-
|
54
|
-
|
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
|
58
|
-
"[Ждет: #{waiting_acquier}. А нужен: #{acquier_id}]"
|
56
|
+
"Step №2: дропаем из очереди просроченных ожидающих. [ZREMRANGE: #{res}]"
|
59
57
|
)
|
60
58
|
|
61
|
-
|
62
|
-
|
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
|
-
"
|
70
|
-
"[
|
71
|
-
"#{(locked_by_acquier == nil) ? 'не занят' : "занят процессом <#{locked_by_acquier}>"}"
|
63
|
+
"Step №3: какой процесс в очереди сейчас ждет. " \
|
64
|
+
"[ZRANGE <следующий процесс>: #{waiting_acquier} :: <текущий процесс>: #{acquier_id}]"
|
72
65
|
)
|
73
66
|
|
74
|
-
|
75
|
-
|
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 №
|
79
|
-
"[
|
72
|
+
"Step ROLLBACK №1: не одинаковые ключи. выходим. " \
|
73
|
+
"[Ждет: #{waiting_acquier}. А нужен: #{acquier_id}]"
|
80
74
|
)
|
81
75
|
|
82
|
-
inter_result = :
|
76
|
+
inter_result = :acquier_is_not_first_in_queue
|
83
77
|
else
|
84
|
-
# NOTE:
|
78
|
+
# NOTE: our time has come! let's try to acquire the lock!
|
85
79
|
|
86
|
-
# Step
|
87
|
-
|
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
|
-
"
|
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
|
+
"Ste №5: Ищем требуемый лок. " \
|
85
|
+
"[HGET<#{lock_key}>: " \
|
86
|
+
"#{(locked_by_acquier == nil) ? 'не занят' : "занят процессом <#{locked_by_acquier}>"}"
|
104
87
|
)
|
105
88
|
|
106
|
-
|
107
|
-
|
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.
|
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.
|
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.
|
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.
|
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
|
-
#
|
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:
|
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
|
-
|
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
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
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
|
-
|
214
|
-
|
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
|
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'
|
26
|
-
validate('retry_jitter'
|
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[:
|
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)
|
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.
|
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-
|
11
|
+
date: 2024-02-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis-client
|