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 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.