redis_queued_locks 0.0.29 → 0.0.32
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +28 -1
- data/README.md +56 -27
- data/lib/redis_queued_locks/acquier/acquire_lock/try_to_lock.rb +39 -5
- data/lib/redis_queued_locks/acquier/acquire_lock/with_acq_timeout.rb +5 -3
- data/lib/redis_queued_locks/acquier/acquire_lock/yield_with_expire.rb +18 -4
- data/lib/redis_queued_locks/acquier/acquire_lock.rb +89 -18
- data/lib/redis_queued_locks/acquier/lock_info.rb +12 -13
- data/lib/redis_queued_locks/acquier/queue_info.rb +6 -6
- data/lib/redis_queued_locks/client.rb +16 -8
- data/lib/redis_queued_locks/resource.rb +14 -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: d33fd5af73686a34765eb95c54fe9dc28b107a01768e41b2a28150dffdc72a60
|
4
|
+
data.tar.gz: 0ac04f452fbd00b01df535a48dd724e360ed66f42459e33fbf989427531c6b6a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 419d36772547c3a7010353d6589b62c442cd7e2575a1c96fcf8ebcee9be83c002a44de7a5dc6de8faa6e32c76f044bf5eaf659ddaf156e8eb3093587dab54e90
|
7
|
+
data.tar.gz: 28ba2de724d7d7540d967e9ade24ca6d65126af526224ae90f418c04e58618ff1aa856911d9eef363b004272fda73a0ce5ddf83a52460e4d78f09dabdd9fba3f
|
data/CHANGELOG.md
CHANGED
@@ -1,8 +1,35 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.0.32] - 2024-03-26
|
4
|
+
### Added
|
5
|
+
- Support for custom metadata that merged to the lock data. This data also returned from `RedisQueudLocks::Client#lock_info` method;
|
6
|
+
- Custom metadata shou;d be represented as a `key => value` `Hash` data (or `NilClass` instead);
|
7
|
+
- Custom metadata values is returned as raw data from Redis (commonly as strings);
|
8
|
+
- Custom metadata can not contain reserved lock data keys;
|
9
|
+
- Reduced some memory consuption;
|
10
|
+
### Changed
|
11
|
+
- `RedisQueuedLocks::Client#lock_info` - has keys is changed from `Symbol` type to `String` type;
|
12
|
+
- `RedisQueuedLocks::Client#queue_info` - hash keys is changed from `Symbol` type to `String` type;
|
13
|
+
|
14
|
+
## [0.0.31] - 2024-03-25
|
15
|
+
### Changed
|
16
|
+
- `:metadata` renamed to `:instrument` in order to reflect it's domain area;
|
17
|
+
- `:metadata` is renamed to `:meta` and reserved for the future updates;
|
18
|
+
|
19
|
+
## [0.0.30] - 2024-03-23
|
20
|
+
### Fixed
|
21
|
+
- Re-enqueue problem: fixed a problem when the expired lock requests were infinitly re-added to the lock queue
|
22
|
+
and immediately removed from the lock queue rather than being re-positioned. It happens when the lock request
|
23
|
+
ttl reached the queue ttl, and the new request now had the dead score forever (fix: it's score now will be correctly
|
24
|
+
recalculated from the current time at the dead score time moment);
|
25
|
+
### Added
|
26
|
+
- Logging: more detailed logs to the `RedisQueuedLocks::Acquier::AcquierLock` logic and it's sub-modules:
|
27
|
+
- added new logs;
|
28
|
+
- added `queue_ttl` to each log;
|
29
|
+
|
3
30
|
## [0.0.29] - 2024-03-23
|
4
31
|
### Added
|
5
|
-
- Logging: more detailed logs to `RedisQueuedLocks::Acquier::AcquireLock::TryToLock`;
|
32
|
+
- Logging: added more detailed logs to `RedisQueuedLocks::Acquier::AcquireLock::TryToLock`;
|
6
33
|
|
7
34
|
## [0.0.28] - 2024-03-21
|
8
35
|
### Added
|
data/README.md
CHANGED
@@ -2,7 +2,9 @@
|
|
2
2
|
|
3
3
|
Distributed locks with "lock acquisition queue" capabilities based on the Redis Database.
|
4
4
|
|
5
|
-
|
5
|
+
Provides flexible invocation flow, parametrized limits (lock request ttl, lock ttls, queue ttls, fast failing, etc), logging and instrumentation.
|
6
|
+
|
7
|
+
Each lock request is put into the request queue (each lock is hosted by it's own queue separately from other queues) and processed in order of their priority (FIFO). Each lock request lives some period of time (RTTL) which guarantees the request queue will never be stacked.
|
6
8
|
|
7
9
|
---
|
8
10
|
|
@@ -14,7 +16,7 @@ Each lock request is put into the request queue and processed in order of their
|
|
14
16
|
- [Configuration](#configuration)
|
15
17
|
- [Usage](#usage)
|
16
18
|
- [lock](#lock---obtain-a-lock)
|
17
|
-
- [lock!](#lock---
|
19
|
+
- [lock!](#lock---exceptional-lock-obtaining)
|
18
20
|
- [lock_info](#lock_info)
|
19
21
|
- [queue_info](#queue_info)
|
20
22
|
- [locked?](#locked)
|
@@ -36,7 +38,7 @@ Each lock request is put into the request queue and processed in order of their
|
|
36
38
|
|
37
39
|
### Algorithm
|
38
40
|
|
39
|
-
> 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.
|
41
|
+
> Each lock request is put into the request queue (each lock is hosted by it's own queue separately from other queues) 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.
|
40
42
|
|
41
43
|
**Soon**: detailed explanation.
|
42
44
|
|
@@ -194,7 +196,8 @@ def lock(
|
|
194
196
|
raise_errors: false,
|
195
197
|
fail_fast: false,
|
196
198
|
identity: uniq_identity, # (attr_accessor) calculated during client instantiation via config[:uniq_identifier] proc;
|
197
|
-
|
199
|
+
meta: nil,
|
200
|
+
instrument: nil,
|
198
201
|
logger: config[:logger],
|
199
202
|
log_lock_try: config[:log_lock_try],
|
200
203
|
&block
|
@@ -232,8 +235,11 @@ def lock(
|
|
232
235
|
pods or/and nodes of your application;
|
233
236
|
- It is calculated once during `RedisQueuedLock::Client` instantiation and stored in `@uniq_identity`
|
234
237
|
ivar (accessed via `uniq_dentity` accessor method);
|
235
|
-
- `
|
236
|
-
- A custom metadata wich will be passed to the
|
238
|
+
- `meta` - `[NilClass,Hash<String|Symbol,Any>]`
|
239
|
+
- A custom metadata wich will be passed to the lock data in addition to the existing data;
|
240
|
+
- Custom metadata can not contain reserved lock data keys (such as `lock_key`, `acq_id`, `ts`, `ini_ttl`, `rem_ttl`);
|
241
|
+
- `instrument` - `[NilClass,Any]`
|
242
|
+
- Custom instrumentation data wich will be passed to the instrumenter's payload with :instrument key;
|
237
243
|
- `logger` - `[::Logger,#debug]`
|
238
244
|
- Logger object used from the configuration layer (see config[:logger]);
|
239
245
|
- See `RedisQueuedLocks::Logging::VoidLogger` for example;
|
@@ -293,7 +299,8 @@ def lock!(
|
|
293
299
|
retry_jitter: config[:retry_jitter],
|
294
300
|
identity: uniq_identity,
|
295
301
|
fail_fast: false,
|
296
|
-
|
302
|
+
meta: nil,
|
303
|
+
instrument: nil,
|
297
304
|
logger: config[:logger],
|
298
305
|
log_lock_try: config[:log_lock_try],
|
299
306
|
&block
|
@@ -309,22 +316,42 @@ See `#lock` method [documentation](#lock---obtain-a-lock).
|
|
309
316
|
- get the lock information;
|
310
317
|
- returns `nil` if lock does not exist;
|
311
318
|
- lock data (`Hash<Symbol,String|Integer>`):
|
312
|
-
- `lock_key` - `string` - lock key in redis;
|
313
|
-
- `acq_id` - `string` - acquier identifier (process_id/thread_id/fiber_id/ractor_id/identity);
|
314
|
-
- `ts` - `integer`/`epoch` - the time lock was obtained;
|
315
|
-
- `init_ttl` - `integer` - (milliseconds) initial lock key ttl;
|
316
|
-
- `rem_ttl` - `integer` - (milliseconds) remaining lock key ttl;
|
319
|
+
- `"lock_key"` - `string` - lock key in redis;
|
320
|
+
- `"acq_id"` - `string` - acquier identifier (process_id/thread_id/fiber_id/ractor_id/identity);
|
321
|
+
- `"ts"` - `integer`/`epoch` - the time lock was obtained;
|
322
|
+
- `"init_ttl"` - `integer` - (milliseconds) initial lock key ttl;
|
323
|
+
- `"rem_ttl"` - `integer` - (milliseconds) remaining lock key ttl;
|
324
|
+
- custom metadata keys - `String` - custom metadata passed to the `lock`/`lock!`
|
325
|
+
methods via `meta:` keyword argument (see [lock]((#lock---obtain-a-lock)) method documentation);
|
326
|
+
|
327
|
+
```ruby
|
328
|
+
# without custom metadata
|
329
|
+
rql.lock_info("your_lock_name")
|
330
|
+
|
331
|
+
# =>
|
332
|
+
{
|
333
|
+
"lock_key" => "rql:lock:your_lock_name",
|
334
|
+
"acq_id" => "rql:acq:123/456/567/678/374dd74324",
|
335
|
+
"ts" => 123456789,
|
336
|
+
"ini_ttl" => 123456789,
|
337
|
+
"rem_ttl" => 123456789
|
338
|
+
}
|
339
|
+
```
|
317
340
|
|
318
341
|
```ruby
|
342
|
+
# with custom metadata
|
343
|
+
rql.lock("your_lock_name", meta: { "kek" => "pek", "bum" => 123 })
|
319
344
|
rql.lock_info("your_lock_name")
|
320
345
|
|
321
346
|
# =>
|
322
347
|
{
|
323
|
-
lock_key
|
324
|
-
acq_id
|
325
|
-
ts
|
326
|
-
ini_ttl
|
327
|
-
rem_ttl
|
348
|
+
"lock_key" => "rql:lock:your_lock_name",
|
349
|
+
"acq_id" => "rql:acq:123/456/567/678/374dd74324",
|
350
|
+
"ts" => 123456789,
|
351
|
+
"ini_ttl" => 123456789,
|
352
|
+
"rem_ttl" => 123456789,
|
353
|
+
"kek" => "pek",
|
354
|
+
"bum" => "123" # NOTE: returned as a raw string directly from Redis
|
328
355
|
}
|
329
356
|
```
|
330
357
|
|
@@ -339,10 +366,10 @@ rql.lock_info("your_lock_name")
|
|
339
366
|
- represents the acquier identifier and their score as an array of hashes;
|
340
367
|
- returns `nil` if lock queue does not exist;
|
341
368
|
- lock queue data (`Hash<Symbol,String|Array<Hash<Symbol,String|Numeric>>`):
|
342
|
-
- `lock_queue` - `string` - lock queue key in redis;
|
343
|
-
- `queue` - `array` - an array of lock requests (array of hashes):
|
344
|
-
- `acq_id` - `string` - acquier identifier (process_id/thread_id/fiber_id/ractor_id/identity by default);
|
345
|
-
- `score` - `float`/`epoch` - time when the lock request was made (epoch);
|
369
|
+
- `"lock_queue"` - `string` - lock queue key in redis;
|
370
|
+
- `"queue"` - `array` - an array of lock requests (array of hashes):
|
371
|
+
- `"acq_id"` - `string` - acquier identifier (process_id/thread_id/fiber_id/ractor_id/identity by default);
|
372
|
+
- `"score"` - `float`/`epoch` - time when the lock request was made (epoch);
|
346
373
|
|
347
374
|
```
|
348
375
|
| Returns an information about the required lock queue by the lock name. The result
|
@@ -357,11 +384,11 @@ rql.queue_info("your_lock_name")
|
|
357
384
|
|
358
385
|
# =>
|
359
386
|
{
|
360
|
-
lock_queue
|
361
|
-
queue
|
362
|
-
{ acq_id
|
363
|
-
{ acq_id
|
364
|
-
{ acq_id
|
387
|
+
"lock_queue" => "rql:lock_queue:your_lock_name",
|
388
|
+
"queue" => [
|
389
|
+
{ "acq_id" => "rql:acq:123/456/567/678/fa76df9cc2", "score" => 1},
|
390
|
+
{ "acq_id" => "rql:acq:123/567/456/679/c7bfcaf4f9", "score" => 2},
|
391
|
+
{ "acq_id" => "rql:acq:555/329/523/127/7329553b11", "score" => 3},
|
365
392
|
# ...etc
|
366
393
|
]
|
367
394
|
}
|
@@ -604,7 +631,8 @@ Detalized event semantics and payload structure:
|
|
604
631
|
- `100%` test coverage;
|
605
632
|
- per-block-holding-the-lock sidecar `Ractor` and `in progress queue` in RedisDB that will extend
|
606
633
|
the acquired lock for long-running blocks of code (that invoked "under" the lock
|
607
|
-
whose ttl may expire before the block execution completes)
|
634
|
+
whose ttl may expire before the block execution completes). It only makes sens for non-`timed` locks
|
635
|
+
(for those locks where otaned with `timed: false` option);
|
608
636
|
- an ability to add custom metadata to the lock and an ability to read this data;
|
609
637
|
- lock prioritization;
|
610
638
|
- support for LIFO strategy;
|
@@ -613,6 +641,7 @@ Detalized event semantics and payload structure:
|
|
613
641
|
- `RedisQueuedLocks::Acquier::Try.try_to_lock` - detailed successful result analization;
|
614
642
|
- better code stylization and interesting refactorings;
|
615
643
|
- dead queue cleanup;
|
644
|
+
- statistics with UI;
|
616
645
|
- support for `Dragonfly` DB backend;
|
617
646
|
- support for `Garnet` DB backend;
|
618
647
|
|
@@ -17,6 +17,7 @@ module RedisQueuedLocks::Acquier::AcquireLock::TryToLock
|
|
17
17
|
# @param ttl [Integer]
|
18
18
|
# @param queue_ttl [Integer]
|
19
19
|
# @param fail_fast [Boolean]
|
20
|
+
# @param meta [NilClass,Hash<String|Symbol,Any>]
|
20
21
|
# @return [Hash<Symbol,Any>] Format: { ok: true/false, result: Symbol|Hash<Symbol,Any> }
|
21
22
|
#
|
22
23
|
# @api private
|
@@ -32,7 +33,8 @@ module RedisQueuedLocks::Acquier::AcquireLock::TryToLock
|
|
32
33
|
acquier_position,
|
33
34
|
ttl,
|
34
35
|
queue_ttl,
|
35
|
-
fail_fast
|
36
|
+
fail_fast,
|
37
|
+
meta
|
36
38
|
)
|
37
39
|
# Step X: intermediate invocation results
|
38
40
|
inter_result = nil
|
@@ -43,23 +45,26 @@ module RedisQueuedLocks::Acquier::AcquireLock::TryToLock
|
|
43
45
|
logger.debug(
|
44
46
|
"[redis_queued_locks.try_lock.start] " \
|
45
47
|
"lock_key => '#{lock_key}' " \
|
46
|
-
"
|
48
|
+
"queue_ttl => #{queue_ttl} " \
|
49
|
+
"acq_id => '#{acquier_id}' "
|
47
50
|
)
|
48
51
|
end
|
49
52
|
end
|
50
53
|
|
51
|
-
# Step
|
54
|
+
# Step X: start to work with lock acquiring
|
52
55
|
result = redis.with do |rconn|
|
53
56
|
if log_lock_try
|
54
57
|
run_non_critical do
|
55
58
|
logger.debug(
|
56
59
|
"[redis_queued_locks.try_lock.rconn_fetched] " \
|
57
60
|
"lock_key => '#{lock_key}' " \
|
61
|
+
"queue_ttl => #{queue_ttl} " \
|
58
62
|
"acq_id => '#{acquier_id}'"
|
59
63
|
)
|
60
64
|
end
|
61
65
|
end
|
62
66
|
|
67
|
+
# Step 0: watch the lock key changes (and discard acquirement if lock is already acquired)
|
63
68
|
rconn.multi(watch: [lock_key]) do |transact|
|
64
69
|
# Fast-Step X0: fail-fast check
|
65
70
|
if fail_fast && rconn.call('HGET', lock_key, 'acq_id')
|
@@ -74,6 +79,7 @@ module RedisQueuedLocks::Acquier::AcquireLock::TryToLock
|
|
74
79
|
logger.debug(
|
75
80
|
"[redis_queued_locks.try_lock.acq_added_to_queue] " \
|
76
81
|
"lock_key => '#{lock_key}' " \
|
82
|
+
"queue_ttl => #{queue_ttl} " \
|
77
83
|
"acq_id => '#{acquier_id}'"
|
78
84
|
)
|
79
85
|
end
|
@@ -96,6 +102,7 @@ module RedisQueuedLocks::Acquier::AcquireLock::TryToLock
|
|
96
102
|
logger.debug(
|
97
103
|
"[redis_queued_locks.try_lock.remove_expired_acqs] " \
|
98
104
|
"lock_key => '#{lock_key}' " \
|
105
|
+
"queue_ttl => #{queue_ttl} " \
|
99
106
|
"acq_id => '#{acquier_id}'"
|
100
107
|
)
|
101
108
|
end
|
@@ -113,6 +120,7 @@ module RedisQueuedLocks::Acquier::AcquireLock::TryToLock
|
|
113
120
|
logger.debug(
|
114
121
|
"[redis_queued_locks.try_lock.get_first_from_queue] " \
|
115
122
|
"lock_key => '#{lock_key}' " \
|
123
|
+
"queue_ttl => #{queue_ttl} " \
|
116
124
|
"acq_id => '#{acquier_id}' " \
|
117
125
|
"first_acq_id_in_queue => '#{waiting_acquier}'"
|
118
126
|
)
|
@@ -124,8 +132,28 @@ module RedisQueuedLocks::Acquier::AcquireLock::TryToLock
|
|
124
132
|
"[ZRANGE <следующий процесс>: #{waiting_acquier} :: <текущий процесс>: #{acquier_id}]"
|
125
133
|
)
|
126
134
|
|
135
|
+
# Step PRE-4.x: check if the request time limit is reached
|
136
|
+
# (when the current try self-removes itself from queue (queue ttl has come))
|
137
|
+
if waiting_acquier == nil
|
138
|
+
if log_lock_try
|
139
|
+
run_non_critical do
|
140
|
+
logger.debug(
|
141
|
+
"[redis_queued_locks.try_lock.exit__queue_ttl_reached] " \
|
142
|
+
"lock_key => '#{lock_key}' " \
|
143
|
+
"queue_ttl => #{queue_ttl} " \
|
144
|
+
"acq_id => '#{acquier_id}'"
|
145
|
+
)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
RedisQueuedLocks.debug(
|
150
|
+
"Step PRE-ROLLBACK №0: достигли лимита времени эквайра лока (queue ttl). выходим. " \
|
151
|
+
"[Наша позиция: #{acquier_id}. queue_ttl: #{queue_ttl}]"
|
152
|
+
)
|
153
|
+
|
154
|
+
inter_result = :dead_score_reached
|
127
155
|
# Step 4: check the actual acquier: is it ours? are we aready to lock?
|
128
|
-
|
156
|
+
elsif waiting_acquier != acquier_id
|
129
157
|
# Step ROLLBACK 1.1: our time hasn't come yet. retry!
|
130
158
|
|
131
159
|
if log_lock_try
|
@@ -133,6 +161,7 @@ module RedisQueuedLocks::Acquier::AcquireLock::TryToLock
|
|
133
161
|
logger.debug(
|
134
162
|
"[redis_queued_locks.try_lock.exit__no_first] " \
|
135
163
|
"lock_key => '#{lock_key}' " \
|
164
|
+
"queue_ttl => #{queue_ttl} " \
|
136
165
|
"acq_id => '#{acquier_id}' " \
|
137
166
|
"first_acq_id_in_queue => '#{waiting_acquier}'"
|
138
167
|
)
|
@@ -167,6 +196,7 @@ module RedisQueuedLocks::Acquier::AcquireLock::TryToLock
|
|
167
196
|
logger.debug(
|
168
197
|
"[redis_queued_locks.try_lock.exit__still_obtained] " \
|
169
198
|
"lock_key => '#{lock_key}' " \
|
199
|
+
"queue_ttl => #{queue_ttl} " \
|
170
200
|
"acq_id => '#{acquier_id}' " \
|
171
201
|
"first_acq_id_in_queue => '#{waiting_acquier}' " \
|
172
202
|
"locked_by_acq_id => '#{locked_by_acquier}'"
|
@@ -202,7 +232,8 @@ module RedisQueuedLocks::Acquier::AcquireLock::TryToLock
|
|
202
232
|
lock_key,
|
203
233
|
'acq_id', acquier_id,
|
204
234
|
'ts', (timestamp = Time.now.to_f),
|
205
|
-
'ini_ttl', ttl
|
235
|
+
'ini_ttl', ttl,
|
236
|
+
*(meta.to_a if meta != nil)
|
206
237
|
)
|
207
238
|
|
208
239
|
# Step 6.3: set the lock expiration time in order to prevent "infinite locks"
|
@@ -213,6 +244,7 @@ module RedisQueuedLocks::Acquier::AcquireLock::TryToLock
|
|
213
244
|
logger.debug(
|
214
245
|
"[redis_queued_locks.try_lock.run__free_to_acquire] " \
|
215
246
|
"lock_key => '#{lock_key}' " \
|
247
|
+
"queue_ttl => #{queue_ttl} " \
|
216
248
|
"acq_id => '#{acquier_id}'"
|
217
249
|
)
|
218
250
|
end
|
@@ -229,6 +261,8 @@ module RedisQueuedLocks::Acquier::AcquireLock::TryToLock
|
|
229
261
|
when fail_fast && inter_result == :fail_fast_no_try
|
230
262
|
# Step 7.a: lock is still acquired and we should exit from the logic as soon as possible
|
231
263
|
RedisQueuedLocks::Data[ok: false, result: inter_result]
|
264
|
+
when inter_result == :dead_score_reached
|
265
|
+
RedisQueuedLocks::Data[ok: false, result: inter_result]
|
232
266
|
when inter_result == :lock_is_still_acquired || inter_result == :acquier_is_not_first_in_queue
|
233
267
|
# Step 7.b: lock is still acquired by another process => failed to acquire
|
234
268
|
RedisQueuedLocks::Data[ok: false, result: inter_result]
|
@@ -21,9 +21,11 @@ module RedisQueuedLocks::Acquier::AcquireLock::WithAcqTimeout
|
|
21
21
|
on_timeout.call unless on_timeout == nil
|
22
22
|
|
23
23
|
if raise_errors
|
24
|
-
raise(
|
25
|
-
|
26
|
-
|
24
|
+
raise(
|
25
|
+
RedisQueuedLocks::LockAcquiermentTimeoutError,
|
26
|
+
"Failed to acquire the lock \"#{lock_key}\" " \
|
27
|
+
"for the given timeout (#{timeout} seconds)."
|
28
|
+
)
|
27
29
|
end
|
28
30
|
end
|
29
31
|
end
|
@@ -13,12 +13,23 @@ module RedisQueuedLocks::Acquier::AcquireLock::YieldWithExpire
|
|
13
13
|
# @param timed [Boolean] Should the lock be wrapped by Tiemlout with with lock's ttl
|
14
14
|
# @param ttl_shift [Float] Lock's TTL shifting. Should affect block's ttl. In millisecodns.
|
15
15
|
# @param ttl [Integer,NilClass] Lock's time to live (in ms). Nil means "without timeout".
|
16
|
+
# @param queue_ttl [Integer] Lock request lifetime.
|
16
17
|
# @param block [Block] Custom logic that should be invoked unter the obtained lock.
|
17
18
|
# @return [Any,NilClass] nil is returned no block parametr is provided.
|
18
19
|
#
|
19
20
|
# @api private
|
20
21
|
# @since 0.1.0
|
21
|
-
def yield_with_expire(
|
22
|
+
def yield_with_expire(
|
23
|
+
redis,
|
24
|
+
logger,
|
25
|
+
lock_key,
|
26
|
+
acquier_id,
|
27
|
+
timed,
|
28
|
+
ttl_shift,
|
29
|
+
ttl,
|
30
|
+
queue_ttl,
|
31
|
+
&block
|
32
|
+
)
|
22
33
|
if block_given?
|
23
34
|
if timed && ttl != nil
|
24
35
|
timeout = ((ttl - ttl_shift) / 1000.0).yield_self { |time| (time < 0) ? 0.0 : time }
|
@@ -32,6 +43,7 @@ module RedisQueuedLocks::Acquier::AcquireLock::YieldWithExpire
|
|
32
43
|
logger.debug(
|
33
44
|
"[redis_queued_locks.expire_lock] " \
|
34
45
|
"lock_key => '#{lock_key}' " \
|
46
|
+
"queue_ttl => #{queue_ttl} " \
|
35
47
|
"acq_id => '#{acquier_id}'"
|
36
48
|
)
|
37
49
|
end
|
@@ -51,8 +63,10 @@ module RedisQueuedLocks::Acquier::AcquireLock::YieldWithExpire
|
|
51
63
|
def yield_with_timeout(timeout, lock_key, lock_ttl, &block)
|
52
64
|
::Timeout.timeout(timeout, &block)
|
53
65
|
rescue ::Timeout::Error
|
54
|
-
raise(
|
55
|
-
|
56
|
-
|
66
|
+
raise(
|
67
|
+
RedisQueuedLocks::TimedLockTimeoutError,
|
68
|
+
"Passed <timed> block of code exceeded " \
|
69
|
+
"the lock TTL (lock: \"#{lock_key}\", ttl: #{lock_ttl})"
|
70
|
+
)
|
57
71
|
end
|
58
72
|
end
|
@@ -67,8 +67,9 @@ module RedisQueuedLocks::Acquier::AcquireLock
|
|
67
67
|
# @option fail_fast [Boolean]
|
68
68
|
# Should the required lock to be checked before the try and exit immidetly if lock is
|
69
69
|
# already obtained.
|
70
|
-
# @option
|
71
|
-
# - A custom metadata wich will be passed to the
|
70
|
+
# @option meta [NilClass,Hash<String|Symbol,Any>]
|
71
|
+
# - A custom metadata wich will be passed to the lock data in addition to the existing data;
|
72
|
+
# - Metadata can not contain reserved lock data keys;
|
72
73
|
# @option logger [::Logger,#debug]
|
73
74
|
# - Logger object used from the configuration layer (see config[:logger]);
|
74
75
|
# - See RedisQueuedLocks::Logging::VoidLogger for example;
|
@@ -76,6 +77,9 @@ module RedisQueuedLocks::Acquier::AcquireLock
|
|
76
77
|
# - should be logged the each try of lock acquiring (a lot of logs can be generated depending
|
77
78
|
# on your retry configurations);
|
78
79
|
# - see `config[:log_lock_try]`;
|
80
|
+
# @option instrument [NilClass,Any]
|
81
|
+
# - Custom instrumentation data wich will be passed to the instrumenter's payload
|
82
|
+
# with :instrument key;
|
79
83
|
# @param [Block]
|
80
84
|
# A block of code that should be executed after the successfully acquired lock.
|
81
85
|
# @return [RedisQueuedLocks::Data,Hash<Symbol,Any>,yield]
|
@@ -102,11 +106,38 @@ module RedisQueuedLocks::Acquier::AcquireLock
|
|
102
106
|
instrumenter:,
|
103
107
|
identity:,
|
104
108
|
fail_fast:,
|
105
|
-
|
109
|
+
meta:,
|
110
|
+
instrument:,
|
106
111
|
logger:,
|
107
112
|
log_lock_try:,
|
108
113
|
&block
|
109
114
|
)
|
115
|
+
# Step 0: Prevent argument type incompatabilities
|
116
|
+
# Step 0.1: prevent :meta incompatabiltiies (type)
|
117
|
+
case meta # NOTE: do not ask why case/when is used here
|
118
|
+
when Hash, NilClass then nil
|
119
|
+
else
|
120
|
+
raise(
|
121
|
+
RedisQueuedLocks::ArgumentError,
|
122
|
+
"`:meta` argument should be a type of NilClass or Hash, got #{meta.class}."
|
123
|
+
)
|
124
|
+
end
|
125
|
+
|
126
|
+
# Step 0.2: prevent :meta incompatabiltiies (structure)
|
127
|
+
if meta == ::Hash && (meta.keys.any? do |key|
|
128
|
+
key == 'acq_id' ||
|
129
|
+
key == 'ts' ||
|
130
|
+
key == 'ini_ttl' ||
|
131
|
+
key == 'lock_key' ||
|
132
|
+
key == 'rem_ttl'
|
133
|
+
end)
|
134
|
+
raise(
|
135
|
+
RedisQueuedLocks::ArgumentError,
|
136
|
+
'`:meta` keys can not overlap reserved lock data keys' \
|
137
|
+
'"acq_id", "ts", "ini_ttl", "lock_key", "rem_ttl"'
|
138
|
+
)
|
139
|
+
end
|
140
|
+
|
110
141
|
# Step 1: prepare lock requirements (generate lock name, calc lock ttl, etc).
|
111
142
|
acquier_id = RedisQueuedLocks::Resource.acquier_identifier(
|
112
143
|
process_id,
|
@@ -140,6 +171,7 @@ module RedisQueuedLocks::Acquier::AcquireLock
|
|
140
171
|
logger.debug(
|
141
172
|
"[redis_queued_locks.start_lock_obtaining] " \
|
142
173
|
"lock_key => '#{lock_key}' " \
|
174
|
+
"queue_ttl => #{queue_ttl} " \
|
143
175
|
"acq_id => '#{acquier_id}'"
|
144
176
|
)
|
145
177
|
end
|
@@ -150,6 +182,30 @@ module RedisQueuedLocks::Acquier::AcquireLock
|
|
150
182
|
|
151
183
|
# Step 2.1: caclically try to obtain the lock
|
152
184
|
while acq_process[:should_try]
|
185
|
+
run_non_critical do
|
186
|
+
logger.debug(
|
187
|
+
"[redis_queued_locks.start_try_to_lock_cycle] " \
|
188
|
+
"lock_key => '#{lock_key}' " \
|
189
|
+
"queue_ttl => #{queue_ttl} " \
|
190
|
+
"acq_id => '{#{acquier_id}'"
|
191
|
+
)
|
192
|
+
end
|
193
|
+
|
194
|
+
# Step 2.X: check the actual score: is it in queue ttl limit or not?
|
195
|
+
if RedisQueuedLocks::Resource.dead_score_reached?(acquier_position, queue_ttl)
|
196
|
+
# Step 2.X.X: dead score reached => re-queue the lock request with the new score;
|
197
|
+
acquier_position = RedisQueuedLocks::Resource.calc_initial_acquier_position
|
198
|
+
|
199
|
+
run_non_critical do
|
200
|
+
logger.debug(
|
201
|
+
"[redis_queued_locks.dead_score_reached__reset_acquier_position] " \
|
202
|
+
"lock_key => '#{lock_key} " \
|
203
|
+
"queue_ttl => #{queue_ttl} " \
|
204
|
+
"acq_id => '#{acquier_id}'"
|
205
|
+
)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
153
209
|
try_to_lock(
|
154
210
|
redis,
|
155
211
|
logger,
|
@@ -160,7 +216,8 @@ module RedisQueuedLocks::Acquier::AcquireLock
|
|
160
216
|
acquier_position,
|
161
217
|
lock_ttl,
|
162
218
|
queue_ttl,
|
163
|
-
fail_fast
|
219
|
+
fail_fast,
|
220
|
+
meta
|
164
221
|
) => { ok:, result: }
|
165
222
|
|
166
223
|
acq_end_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
@@ -176,6 +233,7 @@ module RedisQueuedLocks::Acquier::AcquireLock
|
|
176
233
|
logger.debug(
|
177
234
|
"[redis_queued_locks.lock_obtained] " \
|
178
235
|
"lock_key => '#{result[:lock_key]}' " \
|
236
|
+
"queue_ttl => #{queue_ttl} " \
|
179
237
|
"acq_id => '#{acquier_id}' " \
|
180
238
|
"acq_time => #{acq_time} (ms)"
|
181
239
|
)
|
@@ -189,7 +247,7 @@ module RedisQueuedLocks::Acquier::AcquireLock
|
|
189
247
|
acq_id: result[:acq_id],
|
190
248
|
ts: result[:ts],
|
191
249
|
acq_time: acq_time,
|
192
|
-
|
250
|
+
instrument:
|
193
251
|
})
|
194
252
|
end
|
195
253
|
|
@@ -207,9 +265,10 @@ module RedisQueuedLocks::Acquier::AcquireLock
|
|
207
265
|
elsif fail_fast && acq_process[:result] == :fail_fast_no_try
|
208
266
|
acq_process[:should_try] = false
|
209
267
|
if raise_errors
|
210
|
-
raise(
|
211
|
-
|
212
|
-
|
268
|
+
raise(
|
269
|
+
RedisQueuedLocks::LockAlreadyObtainedError,
|
270
|
+
"Lock \"#{lock_key}\" is already obtained."
|
271
|
+
)
|
213
272
|
end
|
214
273
|
else
|
215
274
|
# Step 2.1.b: failed acquirement => retry
|
@@ -229,18 +288,20 @@ module RedisQueuedLocks::Acquier::AcquireLock
|
|
229
288
|
|
230
289
|
# NOTE: check and raise an error
|
231
290
|
if fail_fast && raise_errors
|
232
|
-
raise(
|
233
|
-
|
234
|
-
|
291
|
+
raise(
|
292
|
+
RedisQueuedLocks::LockAlreadyObtainedError,
|
293
|
+
"Lock \"#{lock_key}\" is already obtained."
|
294
|
+
)
|
235
295
|
elsif raise_errors
|
236
|
-
raise(
|
237
|
-
|
238
|
-
|
239
|
-
|
296
|
+
raise(
|
297
|
+
RedisQueuedLocks::LockAcquiermentRetryLimitError,
|
298
|
+
"Failed to acquire the lock \"#{lock_key}\" " \
|
299
|
+
"for the given retry_count limit (#{retry_count} times)."
|
300
|
+
)
|
240
301
|
end
|
241
302
|
else
|
242
303
|
# NOTE:
|
243
|
-
# delay the exceution in order to prevent chaotic attempts
|
304
|
+
# delay the exceution in order to prevent chaotic lock-acquire attempts
|
244
305
|
# and to allow other processes and threads to obtain the lock too.
|
245
306
|
delay_execution(retry_delay, retry_jitter)
|
246
307
|
end
|
@@ -255,7 +316,17 @@ module RedisQueuedLocks::Acquier::AcquireLock
|
|
255
316
|
begin
|
256
317
|
yield_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
257
318
|
ttl_shift = ((yield_time - acq_process[:acq_end_time]) * 1000).ceil(2)
|
258
|
-
yield_with_expire(
|
319
|
+
yield_with_expire(
|
320
|
+
redis,
|
321
|
+
logger,
|
322
|
+
lock_key,
|
323
|
+
acquier_id,
|
324
|
+
timed,
|
325
|
+
ttl_shift,
|
326
|
+
ttl,
|
327
|
+
queue_ttl,
|
328
|
+
&block
|
329
|
+
)
|
259
330
|
ensure
|
260
331
|
acq_process[:rel_time] = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
261
332
|
acq_process[:hold_time] = (
|
@@ -271,7 +342,7 @@ module RedisQueuedLocks::Acquier::AcquireLock
|
|
271
342
|
ts: acq_process[:lock_info][:ts],
|
272
343
|
lock_key: acq_process[:lock_info][:lock_key],
|
273
344
|
acq_time: acq_process[:acq_time],
|
274
|
-
|
345
|
+
instrument:
|
275
346
|
})
|
276
347
|
end
|
277
348
|
end
|
@@ -6,14 +6,14 @@ module RedisQueuedLocks::Acquier::LockInfo
|
|
6
6
|
class << self
|
7
7
|
# @param redis_client [RedisClient]
|
8
8
|
# @param lock_name [String]
|
9
|
-
# @return [Hash<
|
9
|
+
# @return [Hash<String,String|Numeric>,NilClass]
|
10
10
|
# - `nil` is returned when lock key does not exist or expired;
|
11
11
|
# - result format: {
|
12
|
-
# lock_key
|
13
|
-
# acq_id
|
14
|
-
# ts
|
15
|
-
# ini_ttl
|
16
|
-
# rem_ttl
|
12
|
+
# 'lock_key' => "rql:lock:your_lockname", # acquired lock key
|
13
|
+
# 'acq_id' => "rql:acq:process_id/thread_id", # lock acquier identifier
|
14
|
+
# 'ts' => 123456789.2649841, # <locked at> time stamp (epoch, seconds.microseconds)
|
15
|
+
# 'ini_ttl' => 123456789, # initial lock key ttl (milliseconds),
|
16
|
+
# 'rem_ttl' => 123456789, # remaining lock key ttl (milliseconds)
|
17
17
|
# }
|
18
18
|
#
|
19
19
|
# @api private
|
@@ -43,13 +43,12 @@ module RedisQueuedLocks::Acquier::LockInfo
|
|
43
43
|
# NOTE: the result of MULTI-command is an array of results of each internal command
|
44
44
|
# - result[0] (HGETALL) (Hash<String,String>)
|
45
45
|
# - result[1] (PTTL) (Integer)
|
46
|
-
|
47
|
-
lock_key
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
}
|
46
|
+
hget_cmd_res.tap do |lock_data|
|
47
|
+
lock_data['lock_key'] = lock_key
|
48
|
+
lock_data['ts'] = Float(lock_data['ts'])
|
49
|
+
lock_data['ini_ttl'] = Integer(lock_data['ini_ttl'])
|
50
|
+
lock_data['rem_ttl'] = ((pttl_cmd_res == -1) ? Infinity : pttl_cmd_res)
|
51
|
+
end
|
53
52
|
end
|
54
53
|
end
|
55
54
|
end
|
@@ -12,13 +12,13 @@ module RedisQueuedLocks::Acquier::QueueInfo
|
|
12
12
|
#
|
13
13
|
# @param redis_client [RedisClient]
|
14
14
|
# @param lock_name [String]
|
15
|
-
# @return [Hash<
|
15
|
+
# @return [Hash<String|Array<Hash<String,String|Numeric>>,NilClass]
|
16
16
|
# - `nil` is returned when lock queue does not exist;
|
17
17
|
# - result format: {
|
18
|
-
# lock_queue
|
18
|
+
# "lock_queue" => "rql:lock_queue:your_lock_name", # lock queue key in redis,
|
19
19
|
# queue: [
|
20
|
-
# { acq_id
|
21
|
-
# { acq_id
|
20
|
+
# { "acq_id" => "rql:acq:process_id/thread_id", "score" => 123 },
|
21
|
+
# { "acq_id" => "rql:acq:process_id/thread_id", "score" => 456 },
|
22
22
|
# ] # ordered set (by score) with information about an acquier and their position in queue
|
23
23
|
# }
|
24
24
|
#
|
@@ -38,8 +38,8 @@ module RedisQueuedLocks::Acquier::QueueInfo
|
|
38
38
|
if exists_cmd_res == 1
|
39
39
|
# NOTE: queue existed during the piepline invocation
|
40
40
|
{
|
41
|
-
lock_queue
|
42
|
-
queue
|
41
|
+
'lock_queue' => lock_key_queue,
|
42
|
+
'queue' => zrange_cmd_res.map { |val| { 'acq_id' => val[0], 'score' => val[1] } }
|
43
43
|
}
|
44
44
|
else
|
45
45
|
# NOTE: queue did not exist during the pipeline invocation
|
@@ -93,8 +93,9 @@ class RedisQueuedLocks::Client
|
|
93
93
|
# already obtained;
|
94
94
|
# - Should the logic exit immidietly after the first try if the lock was obtained
|
95
95
|
# by another process while the lock request queue was initially empty;
|
96
|
-
# @option
|
97
|
-
# - A custom metadata wich will be passed to the
|
96
|
+
# @option meta [NilClass,Hash<String|Symbol,Any>]
|
97
|
+
# - A custom metadata wich will be passed to the lock data in addition to the existing data;
|
98
|
+
# - Metadata can not contain reserved lock data keys;
|
98
99
|
# @option logger [::Logger,#debug]
|
99
100
|
# - Logger object used from the configuration layer (see config[:logger]);
|
100
101
|
# - See `RedisQueuedLocks::Logging::VoidLogger` for example;
|
@@ -102,6 +103,9 @@ class RedisQueuedLocks::Client
|
|
102
103
|
# - should be logged the each try of lock acquiring (a lot of logs can
|
103
104
|
# be generated depending on your retry configurations);
|
104
105
|
# - see `config[:log_lock_try]`;
|
106
|
+
# @option instrument [NilClass,Any]
|
107
|
+
# - Custom instrumentation data wich will be passed to the instrumenter's payload
|
108
|
+
# with :instrument key;
|
105
109
|
# @param block [Block]
|
106
110
|
# A block of code that should be executed after the successfully acquired lock.
|
107
111
|
# @return [RedisQueuedLocks::Data,Hash<Symbol,Any>,yield]
|
@@ -122,9 +126,10 @@ class RedisQueuedLocks::Client
|
|
122
126
|
raise_errors: false,
|
123
127
|
fail_fast: false,
|
124
128
|
identity: uniq_identity,
|
125
|
-
|
129
|
+
meta: nil,
|
126
130
|
logger: config[:logger],
|
127
131
|
log_lock_try: config[:log_lock_try],
|
132
|
+
instrument: nil,
|
128
133
|
&block
|
129
134
|
)
|
130
135
|
RedisQueuedLocks::Acquier::AcquireLock.acquire_lock(
|
@@ -145,9 +150,10 @@ class RedisQueuedLocks::Client
|
|
145
150
|
instrumenter: config[:instrumenter],
|
146
151
|
identity:,
|
147
152
|
fail_fast:,
|
148
|
-
|
153
|
+
meta:,
|
149
154
|
logger: config[:logger],
|
150
155
|
log_lock_try: config[:log_lock_try],
|
156
|
+
instrument:,
|
151
157
|
&block
|
152
158
|
)
|
153
159
|
end
|
@@ -167,9 +173,10 @@ class RedisQueuedLocks::Client
|
|
167
173
|
retry_jitter: config[:retry_jitter],
|
168
174
|
fail_fast: false,
|
169
175
|
identity: uniq_identity,
|
170
|
-
|
176
|
+
meta: nil,
|
171
177
|
logger: config[:logger],
|
172
178
|
log_lock_try: config[:log_lock_try],
|
179
|
+
instrument: nil,
|
173
180
|
&block
|
174
181
|
)
|
175
182
|
lock(
|
@@ -184,7 +191,8 @@ class RedisQueuedLocks::Client
|
|
184
191
|
raise_errors: true,
|
185
192
|
identity:,
|
186
193
|
fail_fast:,
|
187
|
-
|
194
|
+
meta:,
|
195
|
+
instrument:,
|
188
196
|
&block
|
189
197
|
)
|
190
198
|
end
|
@@ -224,7 +232,7 @@ class RedisQueuedLocks::Client
|
|
224
232
|
end
|
225
233
|
|
226
234
|
# @param lock_name [String]
|
227
|
-
# @return [Hash,NilClass]
|
235
|
+
# @return [Hash<String,String|Numeric>,NilClass]
|
228
236
|
#
|
229
237
|
# @api public
|
230
238
|
# @since 0.1.0
|
@@ -233,7 +241,7 @@ class RedisQueuedLocks::Client
|
|
233
241
|
end
|
234
242
|
|
235
243
|
# @param lock_name [String]
|
236
|
-
# @return [Hash,NilClass]
|
244
|
+
# @return [Hash<String|Array<Hash<String,String|Numeric>>,NilClass]
|
237
245
|
#
|
238
246
|
# @api public
|
239
247
|
# @since 0.1.0
|
@@ -82,6 +82,20 @@ module RedisQueuedLocks::Resource
|
|
82
82
|
Time.now.to_f - queue_ttl
|
83
83
|
end
|
84
84
|
|
85
|
+
# @param acquier_position [Float]
|
86
|
+
# A time (epoch, seconds.microseconds) that represents
|
87
|
+
# the acquier position in lock request queue.
|
88
|
+
# @parma queue_ttl [Integer]
|
89
|
+
# In second.
|
90
|
+
# @return [Boolean]
|
91
|
+
# Is the lock request time limit has reached or not.
|
92
|
+
#
|
93
|
+
# @api private
|
94
|
+
# @since 0.1.0
|
95
|
+
def dead_score_reached?(acquier_position, queue_ttl)
|
96
|
+
(acquier_position + queue_ttl) < Time.now.to_f
|
97
|
+
end
|
98
|
+
|
85
99
|
# @param lock_queue [String]
|
86
100
|
# @return [String]
|
87
101
|
#
|
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.32
|
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-03-
|
11
|
+
date: 2024-03-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis-client
|