redis_queued_locks 1.6.0 → 1.7.0

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.
@@ -2,16 +2,20 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.0.0
5
+ # @version 1.7.0
5
6
  # rubocop:disable Metrics/ModuleLength
6
7
  # rubocop:disable Metrics/MethodLength
7
8
  # rubocop:disable Metrics/ClassLength
8
9
  # rubocop:disable Metrics/BlockNesting
9
10
  # rubocop:disable Style/IfInsideElse
10
11
  module RedisQueuedLocks::Acquier::AcquireLock
12
+ require_relative 'acquire_lock/log_visitor'
13
+ require_relative 'acquire_lock/instr_visitor'
11
14
  require_relative 'acquire_lock/delay_execution'
12
15
  require_relative 'acquire_lock/with_acq_timeout'
13
16
  require_relative 'acquire_lock/yield_expire'
14
17
  require_relative 'acquire_lock/try_to_lock'
18
+ require_relative 'acquire_lock/dequeue_from_lock_queue'
15
19
 
16
20
  # @since 1.0.0
17
21
  extend TryToLock
@@ -21,8 +25,8 @@ module RedisQueuedLocks::Acquier::AcquireLock
21
25
  extend YieldExpire
22
26
  # @since 1.0.0
23
27
  extend WithAcqTimeout
24
- # @since 1.0.0
25
- extend RedisQueuedLocks::Utilities
28
+ # @since 1.7.0
29
+ extend DequeueFromLockQueue
26
30
 
27
31
  class << self
28
32
  # @param redis [RedisClient]
@@ -86,6 +90,16 @@ module RedisQueuedLocks::Acquier::AcquireLock
86
90
  # - `:extendable_work_through`;
87
91
  # - `:wait_for_lock`;
88
92
  # - `:dead_locking`;
93
+ # @option access_strategy [Symbol]
94
+ # - The way in which the lock will be obtained;
95
+ # - By default it uses `:queued` strategy;
96
+ # - Supports following strategies:
97
+ # - `:queued` (FIFO): the classic queued behavior (default), your lock will be
98
+ # obitaned if you are first in queue and the required lock is free;
99
+ # - `:random` (RANDOM): obtain a lock without checking the positions in the queue
100
+ # (but with checking the limist, retries, timeouts and so on). if lock is
101
+ # free to obtain - it will be obtained;
102
+ # - pre-configured in `config[:default_access_strategy]`;
89
103
  # @option log_sampling_enabled [Boolean]
90
104
  # - enables <log sampling>: only the configured percent of RQL cases will be logged;
91
105
  # - disabled by default;
@@ -129,7 +143,7 @@ module RedisQueuedLocks::Acquier::AcquireLock
129
143
  #
130
144
  # @api private
131
145
  # @since 1.0.0
132
- # @version 1.6.0
146
+ # @version 1.7.0
133
147
  def acquire_lock(
134
148
  redis,
135
149
  lock_name,
@@ -153,6 +167,7 @@ module RedisQueuedLocks::Acquier::AcquireLock
153
167
  logger:,
154
168
  log_lock_try:,
155
169
  conflict_strategy:,
170
+ access_strategy:,
156
171
  log_sampling_enabled:,
157
172
  log_sampling_percent:,
158
173
  log_sampler:,
@@ -236,19 +251,16 @@ module RedisQueuedLocks::Acquier::AcquireLock
236
251
  lock_key_queue,
237
252
  queue_ttl,
238
253
  acquier_id,
254
+ access_strategy,
239
255
  log_sampled,
240
256
  instr_sampled
241
257
  )
242
258
  end
243
259
 
244
- run_non_critical do
245
- logger.debug do
246
- "[redis_queued_locks.start_lock_obtaining] " \
247
- "lock_key => '#{lock_key}' " \
248
- "queue_ttl => #{queue_ttl} " \
249
- "acq_id => '#{acquier_id}'"
250
- end
251
- end if log_sampled
260
+ LogVisitor.start_lock_obtaining(
261
+ logger, log_sampled,
262
+ lock_key, queue_ttl, acquier_id, access_strategy
263
+ )
252
264
 
253
265
  # Step 2: try to lock with timeout
254
266
  with_acq_timeout(timeout, lock_key, raise_errors, on_timeout: acq_dequeue) do
@@ -256,28 +268,21 @@ module RedisQueuedLocks::Acquier::AcquireLock
256
268
 
257
269
  # Step 2.1: cyclically try to obtain the lock
258
270
  while acq_process[:should_try]
259
- run_non_critical do
260
- logger.debug do
261
- "[redis_queued_locks.start_try_to_lock_cycle] " \
262
- "lock_key => '#{lock_key}' " \
263
- "queue_ttl => #{queue_ttl} " \
264
- "acq_id => '{#{acquier_id}'"
265
- end
266
- end if log_sampled
271
+
272
+ LogVisitor.start_try_to_lock_cycle(
273
+ logger, log_sampled,
274
+ lock_key, queue_ttl, acquier_id, access_strategy
275
+ )
267
276
 
268
277
  # Step 2.X: check the actual score: is it in queue ttl limit or not?
269
278
  if RedisQueuedLocks::Resource.dead_score_reached?(acquier_position, queue_ttl)
270
279
  # Step 2.X.X: dead score reached => re-queue the lock request with the new score;
271
280
  acquier_position = RedisQueuedLocks::Resource.calc_initial_acquier_position
272
281
 
273
- run_non_critical do
274
- logger.debug do
275
- "[redis_queued_locks.dead_score_reached__reset_acquier_position] " \
276
- "lock_key => '#{lock_key} " \
277
- "queue_ttl => #{queue_ttl} " \
278
- "acq_id => '#{acquier_id}'"
279
- end
280
- end if log_sampled
282
+ LogVisitor.dead_score_reached__reset_acquier_position(
283
+ logger, log_sampled,
284
+ lock_key, queue_ttl, acquier_id, access_strategy
285
+ )
281
286
  end
282
287
 
283
288
  try_to_lock(
@@ -292,6 +297,7 @@ module RedisQueuedLocks::Acquier::AcquireLock
292
297
  queue_ttl,
293
298
  fail_fast,
294
299
  conflict_strategy,
300
+ access_strategy,
295
301
  meta,
296
302
  log_sampled,
297
303
  instr_sampled
@@ -309,72 +315,38 @@ module RedisQueuedLocks::Acquier::AcquireLock
309
315
  # Step X: (instrumentation)
310
316
  if acq_process[:result][:process] == :extendable_conflict_work_through
311
317
  # instrumetnation: (reentrant lock with ttl extension)
312
- run_non_critical do
313
- logger.debug do
314
- "[redis_queued_locks.extendable_reentrant_lock_obtained] " \
315
- "lock_key => '#{result[:lock_key]}' " \
316
- "queue_ttl => #{queue_ttl} " \
317
- "acq_id => '#{acquier_id}' " \
318
- "acq_time => #{acq_time} (ms)"
319
- end
320
- end if log_sampled
321
-
322
- run_non_critical do
323
- instrumenter.notify('redis_queued_locks.extendable_reentrant_lock_obtained', {
324
- lock_key: result[:lock_key],
325
- ttl: result[:ttl],
326
- acq_id: result[:acq_id],
327
- ts: result[:ts],
328
- acq_time: acq_time,
329
- instrument:
330
- })
331
- end if instr_sampled
318
+ LogVisitor.extendable_reentrant_lock_obtained(
319
+ logger, log_sampled,
320
+ result[:lock_key], queue_ttl, acquier_id, acq_time, access_strategy
321
+ )
322
+ InstrVisitor.extendable_reentrant_lock_obtained(
323
+ instrumenter, instr_sampled,
324
+ result[:lock_key], result[:ttl], result[:acq_id], result[:ts], acq_time,
325
+ instrument
326
+ )
332
327
  elsif acq_process[:result][:process] == :conflict_work_through
333
328
  # instrumetnation: (reentrant lock without ttl extension)
334
- run_non_critical do
335
- logger.debug do
336
- "[redis_queued_locks.reentrant_lock_obtained] " \
337
- "lock_key => '#{result[:lock_key]}' " \
338
- "queue_ttl => #{queue_ttl} " \
339
- "acq_id => '#{acquier_id}' " \
340
- "acq_time => #{acq_time} (ms)"
341
- end
342
- end if log_sampled
343
-
344
- run_non_critical do
345
- instrumenter.notify('redis_queued_locks.reentrant_lock_obtained', {
346
- lock_key: result[:lock_key],
347
- ttl: result[:ttl],
348
- acq_id: result[:acq_id],
349
- ts: result[:ts],
350
- acq_time: acq_time,
351
- instrument:
352
- })
353
- end if instr_sampled
329
+ LogVisitor.reentrant_lock_obtained(
330
+ logger, log_sampled,
331
+ result[:lock_key], queue_ttl, acquier_id, acq_time, access_strategy
332
+ )
333
+ InstrVisitor.reentrant_lock_obtained(
334
+ instrumenter, instr_sampled,
335
+ result[:lock_key], result[:ttl], result[:acq_id], result[:ts], acq_time,
336
+ instrument
337
+ )
354
338
  else
355
339
  # instrumentation: (classic lock obtain)
356
340
  # NOTE: classic is: acq_process[:result][:process] == :lock_obtaining
357
- run_non_critical do
358
- logger.debug do
359
- "[redis_queued_locks.lock_obtained] " \
360
- "lock_key => '#{result[:lock_key]}' " \
361
- "queue_ttl => #{queue_ttl} " \
362
- "acq_id => '#{acquier_id}' " \
363
- "acq_time => #{acq_time} (ms)"
364
- end
365
- end if log_sampled
366
-
367
- # Step X (instrumentation): lock obtained
368
- run_non_critical do
369
- instrumenter.notify('redis_queued_locks.lock_obtained', {
370
- lock_key: result[:lock_key],
371
- ttl: result[:ttl],
372
- acq_id: result[:acq_id],
373
- ts: result[:ts],
374
- acq_time: acq_time,
375
- instrument:
376
- })
377
- end if instr_sampled
341
+ LogVisitor.lock_obtained(
342
+ logger, log_sampled,
343
+ result[:lock_key], queue_ttl, acquier_id, acq_time, access_strategy
344
+ )
345
+ InstrVisitor.lock_obtained(
346
+ instrumenter, instr_sampled,
347
+ result[:lock_key], result[:ttl], result[:acq_id], result[:ts], acq_time,
348
+ instrument
349
+ )
378
350
  end
379
351
 
380
352
  # Step 2.1.a: successfully acquired => build the result
@@ -478,6 +450,7 @@ module RedisQueuedLocks::Acquier::AcquireLock
478
450
  logger,
479
451
  lock_key,
480
452
  acquier_id,
453
+ access_strategy,
481
454
  timed,
482
455
  ttl_shift,
483
456
  ttl,
@@ -499,30 +472,30 @@ module RedisQueuedLocks::Acquier::AcquireLock
499
472
  if acq_process[:result][:process] == :extendable_conflict_work_through ||
500
473
  acq_process[:result][:process] == :conflict_work_through
501
474
  # Step X (instrumentation): reentrant_lock_hold_completes
502
- run_non_critical do
503
- instrumenter.notify('redis_queued_locks.reentrant_lock_hold_completes', {
504
- hold_time: acq_process[:hold_time],
505
- ttl: acq_process[:lock_info][:ttl],
506
- acq_id: acq_process[:lock_info][:acq_id],
507
- ts: acq_process[:lock_info][:ts],
508
- lock_key: acq_process[:lock_info][:lock_key],
509
- acq_time: acq_process[:acq_time],
510
- instrument:
511
- })
512
- end if instr_sampled
475
+ InstrVisitor.reentrant_lock_hold_completes(
476
+ instrumenter,
477
+ instr_sampled,
478
+ acq_process[:lock_info][:lock_key],
479
+ acq_process[:lock_info][:ttl],
480
+ acq_process[:lock_info][:acq_id],
481
+ acq_process[:lock_info][:ts],
482
+ acq_process[:acq_time],
483
+ acq_process[:hold_time],
484
+ instrument
485
+ )
513
486
  else
514
487
  # Step X (instrumentation): lock_hold_and_release
515
- run_non_critical do
516
- instrumenter.notify('redis_queued_locks.lock_hold_and_release', {
517
- hold_time: acq_process[:hold_time],
518
- ttl: acq_process[:lock_info][:ttl],
519
- acq_id: acq_process[:lock_info][:acq_id],
520
- ts: acq_process[:lock_info][:ts],
521
- lock_key: acq_process[:lock_info][:lock_key],
522
- acq_time: acq_process[:acq_time],
523
- instrument:
524
- })
525
- end if instr_sampled
488
+ InstrVisitor.lock_hold_and_release(
489
+ instrumenter,
490
+ instr_sampled,
491
+ acq_process[:lock_info][:lock_key],
492
+ acq_process[:lock_info][:ttl],
493
+ acq_process[:lock_info][:acq_id],
494
+ acq_process[:lock_info][:ts],
495
+ acq_process[:lock_info][:lock_key],
496
+ acq_process[:acq_time],
497
+ instrument
498
+ )
526
499
  end
527
500
  end
528
501
  else
@@ -23,6 +23,7 @@ class RedisQueuedLocks::Client
23
23
  setting :dead_request_ttl, (1 * 24 * 60 * 60 * 1000) # NOTE: 1 day in milliseconds
24
24
  setting :is_timed_by_default, false
25
25
  setting :default_conflict_strategy, :wait_for_lock
26
+ setting :default_access_strategy, :queued
26
27
  setting :log_sampling_enabled, false
27
28
  setting :log_sampling_percent, 15
28
29
  setting :log_sampler, RedisQueuedLocks::Logging::Sampler
@@ -57,6 +58,12 @@ class RedisQueuedLocks::Client
57
58
  val == :dead_locking
58
59
  # rubocop:enable Layout/MultilineOperationIndentation
59
60
  end
61
+ validate('default_access_strategy') do |val|
62
+ # rubocop:disable Layout/MultilineOperationIndentation
63
+ val == :queued ||
64
+ val == :random
65
+ # rubocop:enable Layout/MultilineOperationIndentation
66
+ end
60
67
  end
61
68
 
62
69
  # @return [RedisClient]
@@ -126,6 +133,16 @@ class RedisQueuedLocks::Client
126
133
  # - `:wait_for_lock` - (default) - work in classic way
127
134
  # (with timeouts, retry delays, retry limits, etc - in classic way :));
128
135
  # - `:dead_locking` - fail with deadlock exception;
136
+ # @option access_strategy [Symbol]
137
+ # - The way in which the lock will be obtained;
138
+ # - By default it uses `:queued` strategy;
139
+ # - Supports following strategies:
140
+ # - `:queued` (FIFO): the classic queued behavior (default), your lock will be
141
+ # obitaned if you are first in queue and the required lock is free;
142
+ # - `:random` (RANDOM): obtain a lock without checking the positions in the queue
143
+ # (but with checking the limist, retries, timeouts and so on). if lock is
144
+ # free to obtain - it will be obtained;
145
+ # - pre-configured in `config[:default_access_strategy]`;
129
146
  # @option meta [NilClass,Hash<String|Symbol,Any>]
130
147
  # - A custom metadata wich will be passed to the lock data in addition to the existing data;
131
148
  # - Metadata can not contain reserved lock data keys;
@@ -189,7 +206,7 @@ class RedisQueuedLocks::Client
189
206
  #
190
207
  # @api public
191
208
  # @since 1.0.0
192
- # @version 1.6.0
209
+ # @version 1.7.0
193
210
  # rubocop:disable Metrics/MethodLength
194
211
  def lock(
195
212
  lock_name,
@@ -203,6 +220,7 @@ class RedisQueuedLocks::Client
203
220
  raise_errors: false,
204
221
  fail_fast: false,
205
222
  conflict_strategy: config[:default_conflict_strategy],
223
+ access_strategy: config[:default_access_strategy],
206
224
  identity: uniq_identity,
207
225
  meta: nil,
208
226
  logger: config[:logger],
@@ -236,6 +254,7 @@ class RedisQueuedLocks::Client
236
254
  identity:,
237
255
  fail_fast:,
238
256
  conflict_strategy:,
257
+ access_strategy:,
239
258
  meta:,
240
259
  logger:,
241
260
  log_lock_try:,
@@ -255,7 +274,7 @@ class RedisQueuedLocks::Client
255
274
  #
256
275
  # @api public
257
276
  # @since 1.0.0
258
- # @version 1.6.0
277
+ # @version 1.7.0
259
278
  # rubocop:disable Metrics/MethodLength
260
279
  def lock!(
261
280
  lock_name,
@@ -268,6 +287,7 @@ class RedisQueuedLocks::Client
268
287
  retry_jitter: config[:retry_jitter],
269
288
  fail_fast: false,
270
289
  conflict_strategy: config[:default_conflict_strategy],
290
+ access_strategy: config[:default_access_strategy],
271
291
  identity: uniq_identity,
272
292
  instrumenter: config[:instrumenter],
273
293
  meta: nil,
@@ -300,6 +320,7 @@ class RedisQueuedLocks::Client
300
320
  instrument:,
301
321
  instrumenter:,
302
322
  conflict_strategy:,
323
+ access_strategy:,
303
324
  log_sampling_enabled:,
304
325
  log_sampling_percent:,
305
326
  log_sampler:,
@@ -2,7 +2,6 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.0.0
5
- # @version 1.5.0
6
5
  module RedisQueuedLocks::Utilities
7
6
  module_function
8
7
 
@@ -5,6 +5,6 @@ module RedisQueuedLocks
5
5
  #
6
6
  # @api public
7
7
  # @since 0.0.1
8
- # @version 1.6.0
9
- VERSION = '1.6.0'
8
+ # @version 1.7.0
9
+ VERSION = '1.7.0'
10
10
  end
@@ -0,0 +1 @@
1
+ # frozen_string_literal: true
@@ -10,9 +10,29 @@ Gem::Specification.new do |spec|
10
10
  spec.authors = ['Rustam Ibragimov']
11
11
  spec.email = ['iamdaiver@gmail.com']
12
12
 
13
- spec.summary = 'Queued distributed locks based on Redis.'
14
- spec.description = 'Distributed locks with "lock acquisition queue" ' \
15
- 'capabilities based on the Redis Database.'
13
+ spec.summary =
14
+ 'Distributed locks with "prioritized lock acquisition queue" ' \
15
+ 'capabilities based on the Redis Database.'
16
+
17
+ spec.description =
18
+ 'Distributed locks with "prioritized lock acquisition queue" capabilities ' \
19
+ 'based on the Redis Database. ' \
20
+ # ---
21
+ 'Each lock request is put into the request queue ' \
22
+ "(each lock is hosted by it's own queue separately from other queues) and processed " \
23
+ 'in order of their priority (FIFO). ' \
24
+ # ---
25
+ 'Each lock request lives some period of time (RTTL) ' \
26
+ '(with requeue capabilities) which guarantees the request queue will never be stacked. ' \
27
+ # ---
28
+ 'In addition to the classic `queued` (FIFO) strategy RQL supports ' \
29
+ '`random` (RANDOM) lock obtaining strategy when any acquirer from the lock queue ' \
30
+ 'can obtain the lock regardless the position in the queue. ' \
31
+ # ---
32
+ 'Provides flexible invocation flow, parametrized limits ' \
33
+ '(lock request ttl, lock ttl, queue ttl, lock attempts limit, fast failing, etc), ' \
34
+ 'logging and instrumentation.'
35
+
16
36
  spec.homepage = 'https://github.com/0exp/redis_queued_locks'
17
37
  spec.license = 'MIT'
18
38
 
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: 1.6.0
4
+ version: 1.7.0
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-05-25 00:00:00.000000000 Z
11
+ date: 2024-06-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis-client
@@ -38,8 +38,8 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0.28'
41
- description: Distributed locks with "lock acquisition queue" capabilities based on
42
- the Redis Database.
41
+ description: 'Distributed locks with "prioritized lock acquisition queue" capabilities
42
+ based on the Redis Database. '
43
43
  email:
44
44
  - iamdaiver@gmail.com
45
45
  executables: []
@@ -58,9 +58,15 @@ files:
58
58
  - lib/redis_queued_locks/acquier.rb
59
59
  - lib/redis_queued_locks/acquier/acquire_lock.rb
60
60
  - lib/redis_queued_locks/acquier/acquire_lock/delay_execution.rb
61
+ - lib/redis_queued_locks/acquier/acquire_lock/dequeue_from_lock_queue.rb
62
+ - lib/redis_queued_locks/acquier/acquire_lock/dequeue_from_lock_queue/log_visitor.rb
63
+ - lib/redis_queued_locks/acquier/acquire_lock/instr_visitor.rb
64
+ - lib/redis_queued_locks/acquier/acquire_lock/log_visitor.rb
61
65
  - lib/redis_queued_locks/acquier/acquire_lock/try_to_lock.rb
66
+ - lib/redis_queued_locks/acquier/acquire_lock/try_to_lock/log_visitor.rb
62
67
  - lib/redis_queued_locks/acquier/acquire_lock/with_acq_timeout.rb
63
68
  - lib/redis_queued_locks/acquier/acquire_lock/yield_expire.rb
69
+ - lib/redis_queued_locks/acquier/acquire_lock/yield_expire/log_visitor.rb
64
70
  - lib/redis_queued_locks/acquier/clear_dead_requests.rb
65
71
  - lib/redis_queued_locks/acquier/extend_lock_ttl.rb
66
72
  - lib/redis_queued_locks/acquier/is_locked.rb
@@ -87,6 +93,7 @@ files:
87
93
  - lib/redis_queued_locks/resource.rb
88
94
  - lib/redis_queued_locks/utilities.rb
89
95
  - lib/redis_queued_locks/version.rb
96
+ - lib/redis_queued_locks/watcher.rb
90
97
  - redis_queued_locks.gemspec
91
98
  homepage: https://github.com/0exp/redis_queued_locks
92
99
  licenses:
@@ -113,5 +120,6 @@ requirements: []
113
120
  rubygems_version: 3.3.7
114
121
  signing_key:
115
122
  specification_version: 4
116
- summary: Queued distributed locks based on Redis.
123
+ summary: Distributed locks with "prioritized lock acquisition queue" capabilities
124
+ based on the Redis Database.
117
125
  test_files: []