redis_queued_locks 1.2.0 → 1.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -6,29 +6,24 @@
6
6
  # rubocop:disable Metrics/MethodLength
7
7
  # rubocop:disable Metrics/ClassLength
8
8
  # rubocop:disable Metrics/BlockNesting
9
+ # rubocop:disable Style/IfInsideElse
9
10
  module RedisQueuedLocks::Acquier::AcquireLock
10
11
  require_relative 'acquire_lock/delay_execution'
11
12
  require_relative 'acquire_lock/with_acq_timeout'
12
- require_relative 'acquire_lock/yield_with_expire'
13
+ require_relative 'acquire_lock/yield_expire'
13
14
  require_relative 'acquire_lock/try_to_lock'
14
15
 
15
16
  # @since 1.0.0
16
17
  extend TryToLock
17
18
  # @since 1.0.0
18
19
  extend DelayExecution
19
- # @since 1.0.0
20
- extend YieldWithExpire
20
+ # @since 1.3.0
21
+ extend YieldExpire
21
22
  # @since 1.0.0
22
23
  extend WithAcqTimeout
23
24
  # @since 1.0.0
24
25
  extend RedisQueuedLocks::Utilities
25
26
 
26
- # @return [Integer] Redis time error (in milliseconds).
27
- #
28
- # @api private
29
- # @since 1.0.0
30
- REDIS_TIMESHIFT_ERROR = 2
31
-
32
27
  class << self
33
28
  # @param redis [RedisClient]
34
29
  # Redis connection client.
@@ -72,14 +67,25 @@ module RedisQueuedLocks::Acquier::AcquireLock
72
67
  # - Metadata can not contain reserved lock data keys;
73
68
  # @option logger [::Logger,#debug]
74
69
  # - Logger object used from the configuration layer (see config[:logger]);
75
- # - See RedisQueuedLocks::Logging::VoidLogger for example;
70
+ # - See `RedisQueuedLocks::Logging::VoidLogger` for example;
71
+ # - Supports `SemanticLogger::Logger` (see "semantic_logger" gem)
76
72
  # @option log_lock_try [Boolean]
77
73
  # - should be logged the each try of lock acquiring (a lot of logs can be generated depending
78
74
  # on your retry configurations);
79
75
  # - see `config[:log_lock_try]`;
80
76
  # @option instrument [NilClass,Any]
81
- # - Custom instrumentation data wich will be passed to the instrumenter's payload
82
- # with :instrument key;
77
+ # - Custom instrumentation data wich will be passed to the instrumenter's payload
78
+ # with :instrument key;
79
+ # @option conflict_strategy [Symbol]
80
+ # - The conflict strategy mode for cases when the process that obtained the lock
81
+ # want to acquire this lock again;
82
+ # - By default uses `:wait_for_lock` strategy;
83
+ # - pre-confured in `config[:default_conflict_strategy]`;
84
+ # - Supports:
85
+ # - `:work_through`;
86
+ # - `:extendable_work_through`;
87
+ # - `:wait_for_lock`;
88
+ # - `:dead_locking`;
83
89
  # @param [Block]
84
90
  # A block of code that should be executed after the successfully acquired lock.
85
91
  # @return [RedisQueuedLocks::Data,Hash<Symbol,Any>,yield]
@@ -88,6 +94,7 @@ module RedisQueuedLocks::Acquier::AcquireLock
88
94
  #
89
95
  # @api private
90
96
  # @since 1.0.0
97
+ # @version 1.3.1
91
98
  def acquire_lock(
92
99
  redis,
93
100
  lock_name,
@@ -110,6 +117,7 @@ module RedisQueuedLocks::Acquier::AcquireLock
110
117
  instrument:,
111
118
  logger:,
112
119
  log_lock_try:,
120
+ conflict_strategy:,
113
121
  &block
114
122
  )
115
123
  # Step 0: Prevent argument type incompatabilities
@@ -124,17 +132,23 @@ module RedisQueuedLocks::Acquier::AcquireLock
124
132
  end
125
133
 
126
134
  # Step 0.2: prevent :meta incompatabiltiies (structure)
127
- if meta == ::Hash && (meta.keys.any? do |key|
135
+ if meta.is_a?(::Hash) && (meta.keys.any? do |key|
128
136
  key == 'acq_id' ||
129
137
  key == 'ts' ||
130
138
  key == 'ini_ttl' ||
131
139
  key == 'lock_key' ||
132
- key == 'rem_ttl'
140
+ key == 'rem_ttl' ||
141
+ key == 'spc_ext_ttl' ||
142
+ key == 'spc_cnt' ||
143
+ key == 'l_spc_ext_ini_ttl' ||
144
+ key == 'l_spc_ext_ts' ||
145
+ key == 'l_spc_ts'
133
146
  end)
134
147
  raise(
135
148
  RedisQueuedLocks::ArgumentError,
136
- '`:meta` keys can not overlap reserved lock data keys' \
137
- '"acq_id", "ts", "ini_ttl", "lock_key", "rem_ttl"'
149
+ '`:meta` keys can not overlap reserved lock data keys ' \
150
+ '"acq_id", "ts", "ini_ttl", "lock_key", "rem_ttl", "spc_cnt", ' \
151
+ '"spc_ext_ttl", "l_spc_ext_ini_ttl", "l_spc_ext_ts", "l_spc_ts"'
138
152
  )
139
153
  end
140
154
 
@@ -223,6 +237,7 @@ module RedisQueuedLocks::Acquier::AcquireLock
223
237
  lock_ttl,
224
238
  queue_ttl,
225
239
  fail_fast,
240
+ conflict_strategy,
226
241
  meta
227
242
  ) => { ok:, result: }
228
243
 
@@ -235,26 +250,75 @@ module RedisQueuedLocks::Acquier::AcquireLock
235
250
 
236
251
  # Step 2.1: analyze an acquirement attempt
237
252
  if ok
238
- run_non_critical do
239
- logger.debug do
240
- "[redis_queued_locks.lock_obtained] " \
241
- "lock_key => '#{result[:lock_key]}' " \
242
- "queue_ttl => #{queue_ttl} " \
243
- "acq_id => '#{acquier_id}' " \
244
- "acq_time => #{acq_time} (ms)"
253
+ # Step X: (instrumentation)
254
+ if acq_process[:result][:process] == :extendable_conflict_work_through
255
+ # instrumetnation: (reentrant lock with ttl extension)
256
+ run_non_critical do
257
+ logger.debug do
258
+ "[redis_queued_locks.extendable_reentrant_lock_obtained] " \
259
+ "lock_key => '#{result[:lock_key]}' " \
260
+ "queue_ttl => #{queue_ttl} " \
261
+ "acq_id => '#{acquier_id}' " \
262
+ "acq_time => #{acq_time} (ms)"
263
+ end
245
264
  end
246
- end
247
265
 
248
- # Step X (instrumentation): lock obtained
249
- run_non_critical do
250
- instrumenter.notify('redis_queued_locks.lock_obtained', {
251
- lock_key: result[:lock_key],
252
- ttl: result[:ttl],
253
- acq_id: result[:acq_id],
254
- ts: result[:ts],
255
- acq_time: acq_time,
256
- instrument:
257
- })
266
+ run_non_critical do
267
+ instrumenter.notify('redis_queued_locks.extendable_reentrant_lock_obtained', {
268
+ lock_key: result[:lock_key],
269
+ ttl: result[:ttl],
270
+ acq_id: result[:acq_id],
271
+ ts: result[:ts],
272
+ acq_time: acq_time,
273
+ instrument:
274
+ })
275
+ end
276
+ elsif acq_process[:result][:process] == :conflict_work_through
277
+ # instrumetnation: (reentrant lock without ttl extension)
278
+ run_non_critical do
279
+ logger.debug do
280
+ "[redis_queued_locks.reentrant_lock_obtained] " \
281
+ "lock_key => '#{result[:lock_key]}' " \
282
+ "queue_ttl => #{queue_ttl} " \
283
+ "acq_id => '#{acquier_id}' " \
284
+ "acq_time => #{acq_time} (ms)"
285
+ end
286
+ end
287
+
288
+ run_non_critical do
289
+ instrumenter.notify('redis_queued_locks.reentrant_lock_obtained', {
290
+ lock_key: result[:lock_key],
291
+ ttl: result[:ttl],
292
+ acq_id: result[:acq_id],
293
+ ts: result[:ts],
294
+ acq_time: acq_time,
295
+ instrument:
296
+ })
297
+ end
298
+ else
299
+ # instrumentation: (classic lock obtain)
300
+ # NOTE: classic is: acq_process[:result][:process] == :lock_obtaining
301
+ run_non_critical do
302
+ logger.debug do
303
+ "[redis_queued_locks.lock_obtained] " \
304
+ "lock_key => '#{result[:lock_key]}' " \
305
+ "queue_ttl => #{queue_ttl} " \
306
+ "acq_id => '#{acquier_id}' " \
307
+ "acq_time => #{acq_time} (ms)"
308
+ end
309
+ end
310
+
311
+ # Step X (instrumentation): lock obtained
312
+ run_non_critical do
313
+ instrumenter.notify('redis_queued_locks.lock_obtained', {
314
+ lock_key: result[:lock_key],
315
+ ttl: result[:ttl],
316
+ acq_id: result[:acq_id],
317
+ ts: result[:ts],
318
+ acq_time: acq_time,
319
+ instrument:
320
+ })
321
+ end
258
322
  end
259
323
 
260
324
  # Step 2.1.a: successfully acquired => build the result
@@ -262,54 +326,73 @@ module RedisQueuedLocks::Acquier::AcquireLock
262
326
  lock_key: result[:lock_key],
263
327
  acq_id: result[:acq_id],
264
328
  ts: result[:ts],
265
- ttl: result[:ttl]
329
+ ttl: result[:ttl],
330
+ process: result[:process]
266
331
  }
267
332
  acq_process[:acquired] = true
268
333
  acq_process[:should_try] = false
269
334
  acq_process[:acq_time] = acq_time
270
335
  acq_process[:acq_end_time] = acq_end_time
271
- elsif fail_fast && acq_process[:result] == :fail_fast_no_try
272
- acq_process[:should_try] = false
273
- if raise_errors
274
- raise(
275
- RedisQueuedLocks::LockAlreadyObtainedError,
276
- "Lock \"#{lock_key}\" is already obtained."
277
- )
278
- end
279
336
  else
280
- # Step 2.1.b: failed acquirement => retry
281
- acq_process[:tries] += 1
282
-
283
- if (retry_count != nil && acq_process[:tries] >= retry_count) || fail_fast
284
- # NOTE:
285
- # - reached the retry limit => quit from the loop
286
- # - should fail fast => quit from the loop
337
+ # Step 2.2: failed to acquire. anylize each case and act in accordance
338
+ if acq_process[:result] == :fail_fast_no_try # Step 2.2.a: fail without try
287
339
  acq_process[:should_try] = false
288
- acq_process[:result] = fail_fast ? :fail_fast_after_try : :retry_limit_reached
289
-
290
- # NOTE:
291
- # - reached the retry limit => dequeue from the lock queue
292
- # - should fail fast => dequeue from the lock queue
293
- acq_dequeue.call
294
340
 
295
- # NOTE: check and raise an error
296
- if fail_fast && raise_errors
341
+ if raise_errors
297
342
  raise(
298
343
  RedisQueuedLocks::LockAlreadyObtainedError,
299
344
  "Lock \"#{lock_key}\" is already obtained."
300
345
  )
301
- elsif raise_errors
346
+ end
347
+ elsif acq_process[:result] == :conflict_dead_lock # Step 2.2.b: fail after dead lock
348
+ acq_process[:tries] += 1
349
+ acq_process[:should_try] = false
350
+ acq_process[:result] = :conflict_dead_lock
351
+ acq_dequeue.call
352
+
353
+ if raise_errors
302
354
  raise(
303
- RedisQueuedLocks::LockAcquiermentRetryLimitError,
304
- "Failed to acquire the lock \"#{lock_key}\" " \
305
- "for the given retry_count limit (#{retry_count} times)."
355
+ RedisQueuedLock::ConflictLockObtainError,
356
+ "Lock Conflict: trying to acquire the lock \"#{lock_key}\" " \
357
+ "that is already acquired by the current acquier (acq_id: \"#{acquired_id}\")."
306
358
  )
307
359
  end
308
360
  else
309
- # NOTE:
310
- # delay the exceution in order to prevent chaotic lock-acquire attempts
311
- # and to allow other processes and threads to obtain the lock too.
312
- delay_execution(retry_delay, retry_jitter)
361
+ acq_process[:tries] += 1 # Step RETRY: possible retry case
362
+
363
+ if fail_fast # Step RETRY.A: fail after try
364
+ acq_process[:should_try] = false
365
+ acq_process[:result] = :fail_fast_after_try
366
+ acq_dequeue.call
367
+
368
+ if raise_errors
369
+ raise(
370
+ RedisQueuedLocks::LockAlreadyObtainedError,
371
+ "Lock \"#{lock_key}\" is already obtained."
372
+ )
373
+ end
374
+ else
375
+ # Step RETRY.B: fail cuz the retry count is reached
376
+ if retry_count != nil && acq_process[:tries] >= retry_count
377
+ acq_process[:should_try] = false
378
+ acq_process[:result] = :retry_limit_reached
379
+ acq_dequeue.call
380
+
381
+ if raise_errors
382
+ raise(
383
+ RedisQueuedLocks::LockAcquiermentRetryLimitError,
384
+ "Failed to acquire the lock \"#{lock_key}\" " \
385
+ "for the given retry_count limit (#{retry_count} times)."
386
+ )
387
+ end
388
+ else
389
+ # Step RETRY.X: no significant failures => retry easily :)
390
+ # NOTE:
391
+ # delay the exceution in order to prevent chaotic lock-acquire attempts
392
+ # and to allow other processes and threads to obtain the lock too.
393
+ delay_execution(retry_delay, retry_jitter)
394
+ end
395
+ end
313
396
  end
314
397
  end
315
398
  end
@@ -323,10 +406,18 @@ module RedisQueuedLocks::Acquier::AcquireLock
323
406
  yield_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
324
407
 
325
408
  ttl_shift = (
326
- (yield_time - acq_process[:acq_end_time]) * 1000 - REDIS_TIMESHIFT_ERROR
409
+ (yield_time - acq_process[:acq_end_time]) * 1000 -
410
+ RedisQueuedLocks::Resource::REDIS_TIMESHIFT_ERROR
327
411
  ).ceil(2)
328
412
 
329
- yield_with_expire(
413
+ should_expire =
414
+ acq_process[:result][:process] != :extendable_conflict_work_through &&
415
+ acq_process[:result][:process] != :conflict_work_through
416
+
417
+ should_decrease =
418
+ acq_process[:result][:process] == :extendable_conflict_work_through
419
+
420
+ yield_expire(
330
421
  redis,
331
422
  logger,
332
423
  lock_key,
@@ -335,6 +426,8 @@ module RedisQueuedLocks::Acquier::AcquireLock
335
426
  ttl_shift,
336
427
  ttl,
337
428
  queue_ttl,
429
+ should_expire, # NOTE: should expire the lock after the block execution
430
+ should_decrease, # NOTE: should decrease the lock ttl in reentrant locks?
338
431
  &block
339
432
  )
340
433
  ensure
@@ -343,17 +436,33 @@ module RedisQueuedLocks::Acquier::AcquireLock
343
436
  (acq_process[:rel_time] - acq_process[:acq_end_time]) * 1000
344
437
  ).ceil(2)
345
438
 
346
- # Step X (instrumentation): lock_hold_and_release
347
- run_non_critical do
348
- instrumenter.notify('redis_queued_locks.lock_hold_and_release', {
349
- hold_time: acq_process[:hold_time],
350
- ttl: acq_process[:lock_info][:ttl],
351
- acq_id: acq_process[:lock_info][:acq_id],
352
- ts: acq_process[:lock_info][:ts],
353
- lock_key: acq_process[:lock_info][:lock_key],
354
- acq_time: acq_process[:acq_time],
355
- instrument:
356
- })
439
+ if acq_process[:result][:process] == :extendable_conflict_work_through ||
440
+ acq_process[:result][:process] == :conflict_work_through
441
+ # Step X (instrumentation): reentrant_lock_hold_completes
442
+ run_non_critical do
443
+ instrumenter.notify('redis_queued_locks.reentrant_lock_hold_completes', {
444
+ hold_time: acq_process[:hold_time],
445
+ ttl: acq_process[:lock_info][:ttl],
446
+ acq_id: acq_process[:lock_info][:acq_id],
447
+ ts: acq_process[:lock_info][:ts],
448
+ lock_key: acq_process[:lock_info][:lock_key],
449
+ acq_time: acq_process[:acq_time],
450
+ instrument:
451
+ })
452
+ end
453
+ else
454
+ # Step X (instrumentation): lock_hold_and_release
455
+ run_non_critical do
456
+ instrumenter.notify('redis_queued_locks.lock_hold_and_release', {
457
+ hold_time: acq_process[:hold_time],
458
+ ttl: acq_process[:lock_info][:ttl],
459
+ acq_id: acq_process[:lock_info][:acq_id],
460
+ ts: acq_process[:lock_info][:ts],
461
+ lock_key: acq_process[:lock_info][:lock_key],
462
+ acq_time: acq_process[:acq_time],
463
+ instrument:
464
+ })
465
+ end
357
466
  end
358
467
  end
359
468
  else
@@ -362,11 +471,12 @@ module RedisQueuedLocks::Acquier::AcquireLock
362
471
  else
363
472
  if acq_process[:result] != :retry_limit_reached &&
364
473
  acq_process[:result] != :fail_fast_no_try &&
365
- acq_process[:result] != :fail_fast_after_try
366
- # NOTE: we have only two situations if lock is not acquired withou fast-fail flag:
367
- # - time limit is reached
368
- # - retry count limit is reached
369
- # In other cases the lock obtaining time and tries count are infinite.
474
+ acq_process[:result] != :fail_fast_after_try &&
475
+ acq_process[:result] != :conflict_dead_lock
476
+ # NOTE: we have only two situations if lock is not acquired without explicit failures:
477
+ # - time limit is reached;
478
+ # - retry count limit is reached;
479
+ # - **(notice: in other cases the lock obtaining time and tries count are infinite)
370
480
  acq_process[:result] = :timeout_reached
371
481
  end
372
482
  # Step 3.b: lock is not acquired (acquier is dequeued by timeout callback)
@@ -379,3 +489,4 @@ end
379
489
  # rubocop:enable Metrics/MethodLength
380
490
  # rubocop:enable Metrics/ClassLength
381
491
  # rubocop:enable Metrics/BlockNesting
492
+ # rubocop:enable Style/IfInsideElse
@@ -12,12 +12,20 @@ module RedisQueuedLocks::Acquier::LockInfo
12
12
  # 'lock_key' => "rql:lock:your_lockname", # acquired lock key
13
13
  # 'acq_id' => "rql:acq:process_id/thread_id", # lock acquier identifier
14
14
  # 'ts' => 123456789.2649841, # <locked at> time stamp (epoch, seconds.microseconds)
15
- # 'ini_ttl' => 123456789, # initial lock key ttl (milliseconds),
15
+ # 'ini_ttl' => 123456789, # initial lock key ttl (milliseconds)
16
16
  # 'rem_ttl' => 123456789, # remaining lock key ttl (milliseconds)
17
+ # <additional keys for reentrant locks>:
18
+ # 'spc_cnt' => 2, # lock reentreing count (if lock was used as reentrant lock)
19
+ # 'l_spc_ts' => 123456.1234 # (epoch) <non-extendable reentrant lock obtained at> timestamp
20
+ # 'spc_ext_ttl' => 14500, # (milliseconds) the sum of all ttl extensions
21
+ # 'l_spc_ext_ini_ttl' => 5000, # (milliseconds) the last ttl of reentrant lock
22
+ # 'l_spc_ext_ts' => 123456.789 # (epoch) <extendable reentrant lock obtained at> timestamp
17
23
  # }
18
24
  #
19
25
  # @api private
20
26
  # @since 1.0.0
27
+ # @version 1.3.0
28
+ # rubocop:disable Metrics/MethodLength
21
29
  def lock_info(redis_client, lock_name)
22
30
  lock_key = RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
23
31
 
@@ -48,9 +56,17 @@ module RedisQueuedLocks::Acquier::LockInfo
48
56
  lock_data['ts'] = Float(lock_data['ts'])
49
57
  lock_data['ini_ttl'] = Integer(lock_data['ini_ttl'])
50
58
  lock_data['rem_ttl'] = ((pttl_cmd_res == -1) ? Infinity : pttl_cmd_res)
59
+ lock_data['spc_cnt'] = Integer(lock_data['spc_cnt']) if lock_data['spc_cnt']
60
+ lock_data['l_spc_ts'] = Float(lock_data['l_spc_ts']) if lock_data['l_spc_ts']
61
+ lock_data['spc_ext_ttl'] = Integer(lock_data['spc_ext_ttl']) if lock_data['spc_ext_ttl']
62
+ lock_data['l_spc_ext_ini_ttl'] =
63
+ Integer(lock_data['l_spc_ext_ini_ttl']) if lock_data.key?('l_spc_ext_ini_ttl')
64
+ lock_data['l_spc_ext_ts'] =
65
+ Float(lock_data['l_spc_ext_ts']) if lock_data['l_spc_ext_ts']
51
66
  end
52
67
  end
53
68
  end
54
69
  end
70
+ # rubocop:enable Metrics/MethodLength
55
71
  end
56
72
  end
@@ -45,6 +45,7 @@ module RedisQueuedLocks::Acquier::Locks
45
45
  #
46
46
  # @api private
47
47
  # @since 1.0.0
48
+ # @version 1.3.0
48
49
  # rubocop:disable Metrics/MethodLength
49
50
  def extract_locks_info(redis_client, lock_keys)
50
51
  # TODO: refactor with RedisQueuedLocks::Acquier::LockInfo
@@ -72,6 +73,14 @@ module RedisQueuedLocks::Acquier::Locks
72
73
  lock_data['ts'] = Float(lock_data['ts'])
73
74
  lock_data['ini_ttl'] = Integer(lock_data['ini_ttl'])
74
75
  lock_data['rem_ttl'] = ((pttl_cmd_res == -1) ? Infinity : pttl_cmd_res)
76
+ lock_data['spc_cnt'] = Integer(lock_data['spc_cnt']) if lock_data['spc_cnt']
77
+ lock_data['l_spc_ts'] = Float(lock_data['l_spc_ts']) if lock_data['l_spc_ts']
78
+ lock_data['spc_ext_ttl'] =
79
+ Integer(lock_data['spc_ext_ttl']) if lock_data['spc_ext_ttl']
80
+ lock_data['l_spc_ext_ini_ttl'] =
81
+ Integer(lock_data['l_spc_ext_ini_ttl']) if lock_data.key?('l_spc_ext_ini_ttl')
82
+ lock_data['l_spc_ext_ts'] =
83
+ Float(lock_data['l_spc_ext_ts']) if lock_data['l_spc_ext_ts']
75
84
  end
76
85
  end
77
86
  end
@@ -22,6 +22,7 @@ class RedisQueuedLocks::Client
22
22
  setting :log_lock_try, false
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
+ setting :default_conflict_strategy, :wait_for_lock
25
26
 
26
27
  validate('retry_count') { |val| val == nil || (val.is_a?(::Integer) && val >= 0) }
27
28
  validate('retry_delay') { |val| val.is_a?(::Integer) && val >= 0 }
@@ -36,6 +37,14 @@ class RedisQueuedLocks::Client
36
37
  validate('log_lock_try', :boolean)
37
38
  validate('dead_request_ttl') { |val| val.is_a?(::Integer) && val > 0 }
38
39
  validate('is_timed_by_default', :boolean)
40
+ validate('default_conflict_strategy') do |val|
41
+ # rubocop:disable Layout/MultilineOperationIndentation
42
+ val == :work_through ||
43
+ val == :extendable_work_through ||
44
+ val == :wait_for_lock ||
45
+ val == :dead_locking
46
+ # rubocop:enable Layout/MultilineOperationIndentation
47
+ end
39
48
  end
40
49
 
41
50
  # @return [RedisClient]
@@ -96,19 +105,31 @@ class RedisQueuedLocks::Client
96
105
  # already obtained;
97
106
  # - Should the logic exit immidietly after the first try if the lock was obtained
98
107
  # by another process while the lock request queue was initially empty;
108
+ # @option conflict_strategy [Symbol]
109
+ # - The conflict strategy mode for cases when the process that obtained the lock
110
+ # want to acquire this lock again;
111
+ # - By default uses `:wait_for_lock` strategy;
112
+ # - pre-confured in `config[:default_conflict_strategy]`;
113
+ # - Supports:
114
+ # - `:work_through` - continue working under the lock without lock's TTL extension;
115
+ # - `:extendable_work_through` - continue working under the lock with lock's TTL extension;
116
+ # - `:wait_for_lock` - (default) - work in classic way
117
+ # (with timeouts, retry delays, retry limits, etc - in classic way :));
118
+ # - `:dead_locking` - fail with deadlock exception;
99
119
  # @option meta [NilClass,Hash<String|Symbol,Any>]
100
120
  # - A custom metadata wich will be passed to the lock data in addition to the existing data;
101
121
  # - Metadata can not contain reserved lock data keys;
102
122
  # @option logger [::Logger,#debug]
103
123
  # - Logger object used from the configuration layer (see config[:logger]);
104
124
  # - See `RedisQueuedLocks::Logging::VoidLogger` for example;
125
+ # - Supports `SemanticLogger::Logger` (see "semantic_logger" gem)
105
126
  # @option log_lock_try [Boolean]
106
127
  # - should be logged the each try of lock acquiring (a lot of logs can
107
128
  # be generated depending on your retry configurations);
108
129
  # - see `config[:log_lock_try]`;
109
130
  # @option instrument [NilClass,Any]
110
- # - Custom instrumentation data wich will be passed to the instrumenter's payload
111
- # with :instrument key;
131
+ # - Custom instrumentation data wich will be passed to the instrumenter's payload
132
+ # with :instrument key;
112
133
  # @param block [Block]
113
134
  # A block of code that should be executed after the successfully acquired lock.
114
135
  # @return [RedisQueuedLocks::Data,Hash<Symbol,Any>,yield]
@@ -117,6 +138,7 @@ class RedisQueuedLocks::Client
117
138
  #
118
139
  # @api public
119
140
  # @since 1.0.0
141
+ # @version 1.3.0
120
142
  def lock(
121
143
  lock_name,
122
144
  ttl: config[:default_lock_ttl],
@@ -128,6 +150,7 @@ class RedisQueuedLocks::Client
128
150
  retry_jitter: config[:retry_jitter],
129
151
  raise_errors: false,
130
152
  fail_fast: false,
153
+ conflict_strategy: config[:default_conflict_strategy],
131
154
  identity: uniq_identity,
132
155
  meta: nil,
133
156
  logger: config[:logger],
@@ -153,6 +176,7 @@ class RedisQueuedLocks::Client
153
176
  instrumenter: config[:instrumenter],
154
177
  identity:,
155
178
  fail_fast:,
179
+ conflict_strategy:,
156
180
  meta:,
157
181
  logger: config[:logger],
158
182
  log_lock_try: config[:log_lock_try],
@@ -165,6 +189,7 @@ class RedisQueuedLocks::Client
165
189
  #
166
190
  # @api public
167
191
  # @since 1.0.0
192
+ # @version 1.3.0
168
193
  def lock!(
169
194
  lock_name,
170
195
  ttl: config[:default_lock_ttl],
@@ -175,6 +200,7 @@ class RedisQueuedLocks::Client
175
200
  retry_delay: config[:retry_delay],
176
201
  retry_jitter: config[:retry_jitter],
177
202
  fail_fast: false,
203
+ conflict_strategy: config[:default_conflict_strategy],
178
204
  identity: uniq_identity,
179
205
  meta: nil,
180
206
  logger: config[:logger],
@@ -198,6 +224,7 @@ class RedisQueuedLocks::Client
198
224
  log_lock_try:,
199
225
  meta:,
200
226
  instrument:,
227
+ conflict_strategy:,
201
228
  &block
202
229
  )
203
230
  end
@@ -24,4 +24,8 @@ module RedisQueuedLocks
24
24
  # @api pulic
25
25
  # @since 1.0.0
26
26
  TimedLockTimeoutError = Class.new(Error)
27
+
28
+ # @api public
29
+ # @since 1.3.0
30
+ ConflictLockObtainError = Class.new(Error)
27
31
  end
@@ -16,7 +16,7 @@ module RedisQueuedLocks::Logging
16
16
  return true if logger.is_a?(::Logger)
17
17
 
18
18
  # NOTE:
19
- # - convinient/conventional way to support the popular`semantic_logger` library
19
+ # - convinient/conventional way to support the popular `semantic_logger` library
20
20
  # - see https://logger.rocketjob.io/
21
21
  # - see https://github.com/reidmorrison/semantic_logger
22
22
  return true if defined?(::SemanticLogger::Logger) && logger.is_a?(::SemanticLogger::Logger)
@@ -21,6 +21,12 @@ module RedisQueuedLocks::Resource
21
21
  # @since 1.0.0
22
22
  LOCK_QUEUE_PATTERN = 'rql:lock_queue:*'
23
23
 
24
+ # @return [Integer] Redis time error (in milliseconds).
25
+ #
26
+ # @api private
27
+ # @since 1.3.0
28
+ REDIS_TIMESHIFT_ERROR = 2
29
+
24
30
  class << self
25
31
  # Returns 16-byte unique identifier. It is used for uniquely
26
32
  # identify current process between different nodes/pods of your application
@@ -5,6 +5,6 @@ module RedisQueuedLocks
5
5
  #
6
6
  # @api public
7
7
  # @since 0.0.1
8
- # @version 1.2.0
9
- VERSION = '1.2.0'
8
+ # @version 1.3.1
9
+ VERSION = '1.3.1'
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: 1.2.0
4
+ version: 1.3.1
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-04-27 00:00:00.000000000 Z
11
+ date: 2024-05-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis-client
@@ -60,7 +60,7 @@ files:
60
60
  - lib/redis_queued_locks/acquier/acquire_lock/delay_execution.rb
61
61
  - lib/redis_queued_locks/acquier/acquire_lock/try_to_lock.rb
62
62
  - lib/redis_queued_locks/acquier/acquire_lock/with_acq_timeout.rb
63
- - lib/redis_queued_locks/acquier/acquire_lock/yield_with_expire.rb
63
+ - lib/redis_queued_locks/acquier/acquire_lock/yield_expire.rb
64
64
  - lib/redis_queued_locks/acquier/clear_dead_requests.rb
65
65
  - lib/redis_queued_locks/acquier/extend_lock_ttl.rb
66
66
  - lib/redis_queued_locks/acquier/is_locked.rb