redis_queued_locks 1.2.0 → 1.3.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.
@@ -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.0
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
@@ -129,12 +137,18 @@ module RedisQueuedLocks::Acquier::AcquireLock
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.0
9
+ VERSION = '1.3.0'
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.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-04-27 00:00:00.000000000 Z
11
+ date: 2024-05-08 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
@@ -108,7 +108,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
108
108
  - !ruby/object:Gem::Version
109
109
  version: '0'
110
110
  requirements: []
111
- rubygems_version: 3.5.1
111
+ rubygems_version: 3.3.7
112
112
  signing_key:
113
113
  specification_version: 4
114
114
  summary: Queued distributed locks based on Redis.