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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5de2915b0828a1c82327dd301b41388cc649b2b922ca18f98cde7e8607753b28
4
- data.tar.gz: da86f22a9ed5a834467ae80d9596c9efb0be1e645546c5b32a8f5d5b1dcec294
3
+ metadata.gz: '0920957ed856cf515d2867ecd6ed7b1911ba53466822f09d5bd98386a5be4296'
4
+ data.tar.gz: 587fa64039e85a633fc2c65d5c0b012d7bfb29b9ccd25177e0895deb33850c38
5
5
  SHA512:
6
- metadata.gz: 1f3a60c00b200f9e474ad0471e8e59541786f6e035df4c4d93d553eb93456fc99f0743970d3154fa8d3878a4e67b03d8d2ae2c16b71336b8d277f886dd4821e4
7
- data.tar.gz: 1b52f00d36d568dce68b14caa0cbfb0e1ec793a7eec9adeb5bbca3c042b8bd224cec5f74488c4f0b34de0b302ec98392584a07e21bd02f106a370f3341ef3a35
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
- - **Major**
604
- - Semantic Error objects for unexpected Redis errors;
605
- - `100%` test coverage;
606
- - per-block-holding-the-lock sidecar `Ractor` and `in progress queue` in RedisDB that will extend
607
- the acquired lock for long-running blocks of code (that invoked "under" the lock
608
- whose ttl may expire before the block execution completes);
609
- - an ability to add custom metadata to the lock and an ability to read this data;
610
- - lock prioritization;
611
- - support for LIFO strategy;
612
- - structured logging;
613
- - **Minor**
614
- - GitHub Actions CI;
615
- - `RedisQueuedLocks::Acquier::Try.try_to_lock` - detailed successful result analization;
616
- - better code stylization and interesting refactorings;
617
- - lock queue expiration (dead queue cleanup);
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.try_lock_start] " \
44
+ "[redis_queued_locks.try_lock.start] " \
45
45
  "lock_key => '#{lock_key}' " \
46
- "acq_id => '#{acquier_id}'"
46
+ "queue_ttl => #{queue_ttl} " \
47
+ "acq_id => '#{acquier_id}' "
47
48
  )
48
49
  end
49
50
  end
50
51
 
51
- # Step 0: watch the lock key changes (and discard acquirement if lock is already acquired)
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.try_lock_rconn_fetched] " \
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
- unless waiting_acquier == acquier_id
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(redis, logger, lock_key, acquier_id, timed, ttl_shift, ttl, &block)
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(redis, logger, lock_key, acquier_id, timed, ttl_shift, ttl, &block)
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
  #
@@ -5,6 +5,6 @@ module RedisQueuedLocks
5
5
  #
6
6
  # @api public
7
7
  # @since 0.0.1
8
- # @version 0.0.28
9
- VERSION = '0.0.28'
8
+ # @version 0.0.30
9
+ VERSION = '0.0.30'
10
10
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redis_queued_locks
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.28
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-21 00:00:00.000000000 Z
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.5.1
110
+ rubygems_version: 3.3.7
111
111
  signing_key:
112
112
  specification_version: 4
113
113
  summary: Queued distributed locks based on Redis.