redis_queued_locks 1.2.0 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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.
|