redis_queued_locks 0.0.28 → 0.0.30
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +15 -0
- data/README.md +15 -17
- data/lib/redis_queued_locks/acquier/acquire_lock/try_to_lock.rb +102 -7
- data/lib/redis_queued_locks/acquier/acquire_lock/yield_with_expire.rb +13 -1
- data/lib/redis_queued_locks/acquier/acquire_lock.rb +38 -2
- data/lib/redis_queued_locks/resource.rb +14 -0
- data/lib/redis_queued_locks/version.rb +2 -2
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '0920957ed856cf515d2867ecd6ed7b1911ba53466822f09d5bd98386a5be4296'
|
4
|
+
data.tar.gz: 587fa64039e85a633fc2c65d5c0b012d7bfb29b9ccd25177e0895deb33850c38
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 388e0d43c66464672bbe019dc0f26e3bac44be0cc3e5dcdb502ccd9b2d0b82931de2608c6b7219b2c190b9024197a0cbf1263a7cc7d4f8ed6568480ce6120930
|
7
|
+
data.tar.gz: 56158aca2424c21fc3fcbeebbd33625559aceca82143c2e18700334c6485e919d4005f4391ba3a07b0839e9122f57b9c5d223ebfd4bc38bdd4f61b58bc94f34f
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,20 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.0.30] - 2024-03-23
|
4
|
+
### Fixed
|
5
|
+
- Re-enqueue problem: fixed a problem when the expired lock requests were infinitly re-added to the lock queue
|
6
|
+
and immediately removed from the lock queue rather than being re-positioned. It happens when the lock request
|
7
|
+
ttl reached the queue ttl, and the new request now had the dead score forever (fix: it's score now will be correctly
|
8
|
+
recalculated from the current time at the dead score time moment);
|
9
|
+
### Added
|
10
|
+
- Logging: more detailed logs to the `RedisQueuedLocks::Acquier::AcquierLock` logic and it's sub-modules:
|
11
|
+
- added new logs;
|
12
|
+
- added `queue_ttl` to each log;
|
13
|
+
|
14
|
+
## [0.0.29] - 2024-03-23
|
15
|
+
### Added
|
16
|
+
- Logging: added more detailed logs to `RedisQueuedLocks::Acquier::AcquireLock::TryToLock`;
|
17
|
+
|
3
18
|
## [0.0.28] - 2024-03-21
|
4
19
|
### Added
|
5
20
|
- Logging: added `acq_id` to every log message;
|
data/README.md
CHANGED
@@ -600,23 +600,21 @@ Detalized event semantics and payload structure:
|
|
600
600
|
|
601
601
|
## Roadmap
|
602
602
|
|
603
|
-
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
- support for `Dragonfly` DB backend;
|
619
|
-
- support for `Garnet` DB backend;
|
603
|
+
- Semantic Error objects for unexpected Redis errors;
|
604
|
+
- `100%` test coverage;
|
605
|
+
- per-block-holding-the-lock sidecar `Ractor` and `in progress queue` in RedisDB that will extend
|
606
|
+
the acquired lock for long-running blocks of code (that invoked "under" the lock
|
607
|
+
whose ttl may expire before the block execution completes);
|
608
|
+
- an ability to add custom metadata to the lock and an ability to read this data;
|
609
|
+
- lock prioritization;
|
610
|
+
- support for LIFO strategy;
|
611
|
+
- structured logging;
|
612
|
+
- GitHub Actions CI;
|
613
|
+
- `RedisQueuedLocks::Acquier::Try.try_to_lock` - detailed successful result analization;
|
614
|
+
- better code stylization and interesting refactorings;
|
615
|
+
- dead queue cleanup;
|
616
|
+
- support for `Dragonfly` DB backend;
|
617
|
+
- support for `Garnet` DB backend;
|
620
618
|
|
621
619
|
---
|
622
620
|
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
# @api private
|
4
4
|
# @since 0.1.0
|
5
|
-
# rubocop:disable Metrics/ModuleLength
|
5
|
+
# rubocop:disable Metrics/ModuleLength, Metrics/BlockNesting
|
6
6
|
module RedisQueuedLocks::Acquier::AcquireLock::TryToLock
|
7
7
|
# @since 0.1.0
|
8
8
|
extend RedisQueuedLocks::Utilities
|
@@ -41,25 +41,28 @@ module RedisQueuedLocks::Acquier::AcquireLock::TryToLock
|
|
41
41
|
if log_lock_try
|
42
42
|
run_non_critical do
|
43
43
|
logger.debug(
|
44
|
-
"[redis_queued_locks.
|
44
|
+
"[redis_queued_locks.try_lock.start] " \
|
45
45
|
"lock_key => '#{lock_key}' " \
|
46
|
-
"
|
46
|
+
"queue_ttl => #{queue_ttl} " \
|
47
|
+
"acq_id => '#{acquier_id}' "
|
47
48
|
)
|
48
49
|
end
|
49
50
|
end
|
50
51
|
|
51
|
-
# Step
|
52
|
+
# Step X: start to work with lock acquiring
|
52
53
|
result = redis.with do |rconn|
|
53
54
|
if log_lock_try
|
54
55
|
run_non_critical do
|
55
56
|
logger.debug(
|
56
|
-
"[redis_queued_locks.
|
57
|
+
"[redis_queued_locks.try_lock.rconn_fetched] " \
|
57
58
|
"lock_key => '#{lock_key}' " \
|
59
|
+
"queue_ttl => #{queue_ttl} " \
|
58
60
|
"acq_id => '#{acquier_id}'"
|
59
61
|
)
|
60
62
|
end
|
61
63
|
end
|
62
64
|
|
65
|
+
# Step 0: watch the lock key changes (and discard acquirement if lock is already acquired)
|
63
66
|
rconn.multi(watch: [lock_key]) do |transact|
|
64
67
|
# Fast-Step X0: fail-fast check
|
65
68
|
if fail_fast && rconn.call('HGET', lock_key, 'acq_id')
|
@@ -69,6 +72,17 @@ module RedisQueuedLocks::Acquier::AcquireLock::TryToLock
|
|
69
72
|
# Step 1: add an acquier to the lock acquirement queue
|
70
73
|
res = rconn.call('ZADD', lock_key_queue, 'NX', acquier_position, acquier_id)
|
71
74
|
|
75
|
+
if log_lock_try
|
76
|
+
run_non_critical do
|
77
|
+
logger.debug(
|
78
|
+
"[redis_queued_locks.try_lock.acq_added_to_queue] " \
|
79
|
+
"lock_key => '#{lock_key}' " \
|
80
|
+
"queue_ttl => #{queue_ttl} " \
|
81
|
+
"acq_id => '#{acquier_id}'"
|
82
|
+
)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
72
86
|
RedisQueuedLocks.debug(
|
73
87
|
"Step №1: добавление в очередь (#{acquier_id}). [ZADD to the queue: #{res}]"
|
74
88
|
)
|
@@ -81,6 +95,17 @@ module RedisQueuedLocks::Acquier::AcquireLock::TryToLock
|
|
81
95
|
RedisQueuedLocks::Resource.acquier_dead_score(queue_ttl)
|
82
96
|
)
|
83
97
|
|
98
|
+
if log_lock_try
|
99
|
+
run_non_critical do
|
100
|
+
logger.debug(
|
101
|
+
"[redis_queued_locks.try_lock.remove_expired_acqs] " \
|
102
|
+
"lock_key => '#{lock_key}' " \
|
103
|
+
"queue_ttl => #{queue_ttl} " \
|
104
|
+
"acq_id => '#{acquier_id}'"
|
105
|
+
)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
84
109
|
RedisQueuedLocks.debug(
|
85
110
|
"Step №2: дропаем из очереди просроченных ожидающих. [ZREMRANGE: #{res}]"
|
86
111
|
)
|
@@ -88,15 +113,59 @@ module RedisQueuedLocks::Acquier::AcquireLock::TryToLock
|
|
88
113
|
# Step 3: get the actual acquier waiting in the queue
|
89
114
|
waiting_acquier = Array(rconn.call('ZRANGE', lock_key_queue, '0', '0')).first
|
90
115
|
|
116
|
+
if log_lock_try
|
117
|
+
run_non_critical do
|
118
|
+
logger.debug(
|
119
|
+
"[redis_queued_locks.try_lock.get_first_from_queue] " \
|
120
|
+
"lock_key => '#{lock_key}' " \
|
121
|
+
"queue_ttl => #{queue_ttl} " \
|
122
|
+
"acq_id => '#{acquier_id}' " \
|
123
|
+
"first_acq_id_in_queue => '#{waiting_acquier}'"
|
124
|
+
)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
91
128
|
RedisQueuedLocks.debug(
|
92
129
|
"Step №3: какой процесс в очереди сейчас ждет. " \
|
93
130
|
"[ZRANGE <следующий процесс>: #{waiting_acquier} :: <текущий процесс>: #{acquier_id}]"
|
94
131
|
)
|
95
132
|
|
133
|
+
# Step PRE-4.x: check if the request time limit is reached
|
134
|
+
# (when the current try self-removes itself from queue (queue ttl has come))
|
135
|
+
if waiting_acquier == nil
|
136
|
+
if log_lock_try
|
137
|
+
run_non_critical do
|
138
|
+
logger.debug(
|
139
|
+
"[redis_queued_locks.try_lock.exit__queue_ttl_reached] " \
|
140
|
+
"lock_key => '#{lock_key}' " \
|
141
|
+
"queue_ttl => #{queue_ttl} " \
|
142
|
+
"acq_id => '#{acquier_id}'"
|
143
|
+
)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
RedisQueuedLocks.debug(
|
148
|
+
"Step PRE-ROLLBACK №0: достигли лимита времени эквайра лока (queue ttl). выходим. " \
|
149
|
+
"[Наша позиция: #{acquier_id}. queue_ttl: #{queue_ttl}]"
|
150
|
+
)
|
151
|
+
|
152
|
+
inter_result = :dead_score_reached
|
96
153
|
# Step 4: check the actual acquier: is it ours? are we aready to lock?
|
97
|
-
|
154
|
+
elsif waiting_acquier != acquier_id
|
98
155
|
# Step ROLLBACK 1.1: our time hasn't come yet. retry!
|
99
156
|
|
157
|
+
if log_lock_try
|
158
|
+
run_non_critical do
|
159
|
+
logger.debug(
|
160
|
+
"[redis_queued_locks.try_lock.exit__no_first] " \
|
161
|
+
"lock_key => '#{lock_key}' " \
|
162
|
+
"queue_ttl => #{queue_ttl} " \
|
163
|
+
"acq_id => '#{acquier_id}' " \
|
164
|
+
"first_acq_id_in_queue => '#{waiting_acquier}'"
|
165
|
+
)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
100
169
|
RedisQueuedLocks.debug(
|
101
170
|
"Step ROLLBACK №1: не одинаковые ключи. выходим. " \
|
102
171
|
"[Ждет: #{waiting_acquier}. А нужен: #{acquier_id}]"
|
@@ -120,6 +189,19 @@ module RedisQueuedLocks::Acquier::AcquireLock::TryToLock
|
|
120
189
|
if locked_by_acquier
|
121
190
|
# Step ROLLBACK 2: required lock is stil acquired. retry!
|
122
191
|
|
192
|
+
if log_lock_try
|
193
|
+
run_non_critical do
|
194
|
+
logger.debug(
|
195
|
+
"[redis_queued_locks.try_lock.exit__still_obtained] " \
|
196
|
+
"lock_key => '#{lock_key}' " \
|
197
|
+
"queue_ttl => #{queue_ttl} " \
|
198
|
+
"acq_id => '#{acquier_id}' " \
|
199
|
+
"first_acq_id_in_queue => '#{waiting_acquier}' " \
|
200
|
+
"locked_by_acq_id => '#{locked_by_acquier}'"
|
201
|
+
)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
123
205
|
RedisQueuedLocks.debug(
|
124
206
|
"Step ROLLBACK №2: Ключ уже занят. Ничего не делаем. " \
|
125
207
|
"[Занят процессом: #{locked_by_acquier}]"
|
@@ -153,6 +235,17 @@ module RedisQueuedLocks::Acquier::AcquireLock::TryToLock
|
|
153
235
|
|
154
236
|
# Step 6.3: set the lock expiration time in order to prevent "infinite locks"
|
155
237
|
transact.call('PEXPIRE', lock_key, ttl) # NOTE: in milliseconds
|
238
|
+
|
239
|
+
if log_lock_try
|
240
|
+
run_non_critical do
|
241
|
+
logger.debug(
|
242
|
+
"[redis_queued_locks.try_lock.run__free_to_acquire] " \
|
243
|
+
"lock_key => '#{lock_key}' " \
|
244
|
+
"queue_ttl => #{queue_ttl} " \
|
245
|
+
"acq_id => '#{acquier_id}'"
|
246
|
+
)
|
247
|
+
end
|
248
|
+
end
|
156
249
|
end
|
157
250
|
end
|
158
251
|
end
|
@@ -165,6 +258,8 @@ module RedisQueuedLocks::Acquier::AcquireLock::TryToLock
|
|
165
258
|
when fail_fast && inter_result == :fail_fast_no_try
|
166
259
|
# Step 7.a: lock is still acquired and we should exit from the logic as soon as possible
|
167
260
|
RedisQueuedLocks::Data[ok: false, result: inter_result]
|
261
|
+
when inter_result == :dead_score_reached
|
262
|
+
RedisQueuedLocks::Data[ok: false, result: inter_result]
|
168
263
|
when inter_result == :lock_is_still_acquired || inter_result == :acquier_is_not_first_in_queue
|
169
264
|
# Step 7.b: lock is still acquired by another process => failed to acquire
|
170
265
|
RedisQueuedLocks::Data[ok: false, result: inter_result]
|
@@ -210,4 +305,4 @@ module RedisQueuedLocks::Acquier::AcquireLock::TryToLock
|
|
210
305
|
RedisQueuedLocks::Data[ok: true, result: result]
|
211
306
|
end
|
212
307
|
end
|
213
|
-
# rubocop:enable Metrics/ModuleLength
|
308
|
+
# rubocop:enable Metrics/ModuleLength, Metrics/BlockNesting
|
@@ -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
|
@@ -140,6 +140,7 @@ module RedisQueuedLocks::Acquier::AcquireLock
|
|
140
140
|
logger.debug(
|
141
141
|
"[redis_queued_locks.start_lock_obtaining] " \
|
142
142
|
"lock_key => '#{lock_key}' " \
|
143
|
+
"queue_ttl => #{queue_ttl} " \
|
143
144
|
"acq_id => '#{acquier_id}'"
|
144
145
|
)
|
145
146
|
end
|
@@ -150,6 +151,30 @@ module RedisQueuedLocks::Acquier::AcquireLock
|
|
150
151
|
|
151
152
|
# Step 2.1: caclically try to obtain the lock
|
152
153
|
while acq_process[:should_try]
|
154
|
+
run_non_critical do
|
155
|
+
logger.debug(
|
156
|
+
"[redis_queued_locks.start_try_to_lock_cycle] " \
|
157
|
+
"lock_key => '#{lock_key}' " \
|
158
|
+
"queue_ttl => #{queue_ttl} " \
|
159
|
+
"acq_id => '{#{acquier_id}'"
|
160
|
+
)
|
161
|
+
end
|
162
|
+
|
163
|
+
# Step 2.X: check the actual score: is it in queue ttl limit or not?
|
164
|
+
if RedisQueuedLocks::Resource.dead_score_reached?(acquier_position, queue_ttl)
|
165
|
+
# Step 2.X.X: dead score reached => re-queue the lock request with the new score;
|
166
|
+
acquier_position = RedisQueuedLocks::Resource.calc_initial_acquier_position
|
167
|
+
|
168
|
+
run_non_critical do
|
169
|
+
logger.debug(
|
170
|
+
"[redis_queued_locks.dead_score_reached__reset_acquier_position] " \
|
171
|
+
"lock_key => '#{lock_key} " \
|
172
|
+
"queue_ttl => #{queue_ttl} " \
|
173
|
+
"acq_id => '#{acquier_id}'"
|
174
|
+
)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
153
178
|
try_to_lock(
|
154
179
|
redis,
|
155
180
|
logger,
|
@@ -176,6 +201,7 @@ module RedisQueuedLocks::Acquier::AcquireLock
|
|
176
201
|
logger.debug(
|
177
202
|
"[redis_queued_locks.lock_obtained] " \
|
178
203
|
"lock_key => '#{result[:lock_key]}' " \
|
204
|
+
"queue_ttl => #{queue_ttl} " \
|
179
205
|
"acq_id => '#{acquier_id}' " \
|
180
206
|
"acq_time => #{acq_time} (ms)"
|
181
207
|
)
|
@@ -240,7 +266,7 @@ module RedisQueuedLocks::Acquier::AcquireLock
|
|
240
266
|
end
|
241
267
|
else
|
242
268
|
# NOTE:
|
243
|
-
# delay the exceution in order to prevent chaotic attempts
|
269
|
+
# delay the exceution in order to prevent chaotic lock-acquire attempts
|
244
270
|
# and to allow other processes and threads to obtain the lock too.
|
245
271
|
delay_execution(retry_delay, retry_jitter)
|
246
272
|
end
|
@@ -255,7 +281,17 @@ module RedisQueuedLocks::Acquier::AcquireLock
|
|
255
281
|
begin
|
256
282
|
yield_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
257
283
|
ttl_shift = ((yield_time - acq_process[:acq_end_time]) * 1000).ceil(2)
|
258
|
-
yield_with_expire(
|
284
|
+
yield_with_expire(
|
285
|
+
redis,
|
286
|
+
logger,
|
287
|
+
lock_key,
|
288
|
+
acquier_id,
|
289
|
+
timed,
|
290
|
+
ttl_shift,
|
291
|
+
ttl,
|
292
|
+
queue_ttl,
|
293
|
+
&block
|
294
|
+
)
|
259
295
|
ensure
|
260
296
|
acq_process[:rel_time] = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
261
297
|
acq_process[:hold_time] = (
|
@@ -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.30
|
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-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis-client
|
@@ -107,7 +107,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
107
107
|
- !ruby/object:Gem::Version
|
108
108
|
version: '0'
|
109
109
|
requirements: []
|
110
|
-
rubygems_version: 3.
|
110
|
+
rubygems_version: 3.3.7
|
111
111
|
signing_key:
|
112
112
|
specification_version: 4
|
113
113
|
summary: Queued distributed locks based on Redis.
|