redis_queued_locks 0.0.28 → 0.0.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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.
|