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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3137184f5a89b895fe347d8080957e08a57974e91d5a68189d0eb157afd7c3eb
4
- data.tar.gz: 7b059f5de5cde2af1634eed3aeae540afd7edfc5ba564781789649098e53e0a1
3
+ metadata.gz: d33fd5af73686a34765eb95c54fe9dc28b107a01768e41b2a28150dffdc72a60
4
+ data.tar.gz: 0ac04f452fbd00b01df535a48dd724e360ed66f42459e33fbf989427531c6b6a
5
5
  SHA512:
6
- metadata.gz: 4821b6c0fc142d90fae314aa11c92069be9223cc4a61be077ff6e19278fae97f920d31b590c5688f3614159a2f5c9d9d1792d8a6aee50f51d7b24f83ea1a9298
7
- data.tar.gz: 80a0de485f263459c52d230cc0e604d0c742db235a6a9b57053cc84001e9e146cfc35b9c31aff5e1ef5a2796692559f95fe94db39350720277fb219ef95a2685
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
- 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.
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---exeptional-lock-obtaining)
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
- metadata: nil,
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
- - `metadata` - `[NilClass,Any]`
236
- - A custom metadata wich will be passed to the instrumenter's payload with `:meta` key;
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
- metadata: nil,
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: "rql:lock:your_lock_name",
324
- acq_id: "rql:acq:123/456/567/678/374dd74324",
325
- ts: 123456789,
326
- ini_ttl: 123456789,
327
- rem_ttl: 123456789
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: "rql:lock_queue:your_lock_name",
361
- queue: [
362
- { acq_id: "rql:acq:123/456/567/678/fa76df9cc2", score: 1},
363
- { acq_id: "rql:acq:123/567/456/679/c7bfcaf4f9", score: 2},
364
- { acq_id: "rql:acq:555/329/523/127/7329553b11", score: 3},
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
- "acq_id => '#{acquier_id}'"
48
+ "queue_ttl => #{queue_ttl} " \
49
+ "acq_id => '#{acquier_id}' "
47
50
  )
48
51
  end
49
52
  end
50
53
 
51
- # Step 0: watch the lock key changes (and discard acquirement if lock is already acquired)
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
- unless waiting_acquier == acquier_id
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(RedisQueuedLocks::LockAcquiermentTimeoutError, <<~ERROR_MESSAGE.strip)
25
- Failed to acquire the lock "#{lock_key}" for the given timeout (#{timeout} seconds).
26
- ERROR_MESSAGE
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(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
@@ -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(RedisQueuedLocks::TimedLockTimeoutError, <<~ERROR_MESSAGE)
55
- Passed <timed> block of code exceeded the lock TTL (lock: "#{lock_key}", ttl: #{lock_ttl})
56
- ERROR_MESSAGE
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 metadata [NilClass,Any]
71
- # - A custom metadata wich will be passed to the instrumenter's payload with :meta key;
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
- metadata:,
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
- meta: metadata
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(RedisQueuedLocks::LockAlreadyObtainedError, <<~ERROR_MESSAGE.strip)
211
- Lock "#{lock_key}" is already obtained.
212
- ERROR_MESSAGE
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(RedisQueuedLocks::LockAlreadyObtainedError, <<~ERROR_MESSAGE.strip)
233
- Lock "#{lock_key}" is already obtained.
234
- ERROR_MESSAGE
291
+ raise(
292
+ RedisQueuedLocks::LockAlreadyObtainedError,
293
+ "Lock \"#{lock_key}\" is already obtained."
294
+ )
235
295
  elsif raise_errors
236
- raise(RedisQueuedLocks::LockAcquiermentRetryLimitError, <<~ERROR_MESSAGE.strip)
237
- Failed to acquire the lock "#{lock_key}"
238
- for the given retry_count limit (#{retry_count} times).
239
- ERROR_MESSAGE
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(redis, logger, lock_key, acquier_id, timed, ttl_shift, ttl, &block)
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
- meta: metadata
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<Symbol,String|Numeric>,NilClass]
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: "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)
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: lock_key,
48
- acq_id: hget_cmd_res['acq_id'],
49
- ts: Float(hget_cmd_res['ts']),
50
- ini_ttl: Integer(hget_cmd_res['ini_ttl']),
51
- rem_ttl: ((pttl_cmd_res == -1) ? Infinity : pttl_cmd_res)
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<Symbol,String|Array<Hash<Symbol,String|Float>>,NilClass]
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: "rql:lock_queue:your_lock_name", # lock queue key in redis,
18
+ # "lock_queue" => "rql:lock_queue:your_lock_name", # lock queue key in redis,
19
19
  # queue: [
20
- # { acq_id: "rql:acq:process_id/thread_id", score: 123 },
21
- # { acq_id: "rql:acq:process_id/thread_id", score: 456 },
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: lock_key_queue,
42
- queue: zrange_cmd_res.map { |val| { acq_id: val[0], score: val[1] } }
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 metadata [NilClass,Any]
97
- # - A custom metadata wich will be passed to the instrumenter's payload with :meta key;
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
- metadata: nil,
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
- metadata:,
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
- metadata: nil,
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
- metadata:,
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
  #
@@ -5,6 +5,6 @@ module RedisQueuedLocks
5
5
  #
6
6
  # @api public
7
7
  # @since 0.0.1
8
- # @version 0.0.29
9
- VERSION = '0.0.29'
8
+ # @version 0.0.32
9
+ VERSION = '0.0.32'
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.29
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-23 00:00:00.000000000 Z
11
+ date: 2024-03-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis-client