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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +206 -47
- data/lib/redis_queued_locks/acquier/acquire_lock/try_to_lock.rb +235 -8
- data/lib/redis_queued_locks/acquier/acquire_lock/yield_expire.rb +115 -0
- data/lib/redis_queued_locks/acquier/acquire_lock.rb +194 -83
- data/lib/redis_queued_locks/acquier/lock_info.rb +17 -1
- data/lib/redis_queued_locks/acquier/locks.rb +9 -0
- data/lib/redis_queued_locks/client.rb +29 -2
- data/lib/redis_queued_locks/errors.rb +4 -0
- data/lib/redis_queued_locks/logging.rb +1 -1
- data/lib/redis_queued_locks/resource.rb +6 -0
- data/lib/redis_queued_locks/version.rb +2 -2
- metadata +4 -4
- data/lib/redis_queued_locks/acquier/acquire_lock/yield_with_expire.rb +0 -72
@@ -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/
|
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.
|
20
|
-
extend
|
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
|
-
#
|
82
|
-
#
|
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
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
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
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
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.
|
281
|
-
acq_process[:
|
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
|
-
|
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
|
-
|
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
|
-
|
304
|
-
"
|
305
|
-
"
|
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
|
-
#
|
310
|
-
|
311
|
-
|
312
|
-
|
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 -
|
409
|
+
(yield_time - acq_process[:acq_end_time]) * 1000 -
|
410
|
+
RedisQueuedLocks::Resource::REDIS_TIMESHIFT_ERROR
|
327
411
|
).ceil(2)
|
328
412
|
|
329
|
-
|
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
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
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
|
-
|
367
|
-
#
|
368
|
-
# -
|
369
|
-
#
|
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
|
-
#
|
111
|
-
#
|
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
|
@@ -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
|
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.
|
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-
|
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/
|
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.
|
111
|
+
rubygems_version: 3.3.7
|
112
112
|
signing_key:
|
113
113
|
specification_version: 4
|
114
114
|
summary: Queued distributed locks based on Redis.
|