redis_queued_locks 0.0.19 → 0.0.21

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c77f863ddb9f33c75a636ea12281ce1f4df1215d50f43b113b5be5f79b679526
4
- data.tar.gz: 9585690846e55b141e089d26ac160f6481297cfa6f43d21df1403411903221be
3
+ metadata.gz: 2ea008491f8c631aea2677c6571a206db919c0ca5d599b58adc37fa82c372145
4
+ data.tar.gz: 41e5bece44a3b1af1031d28a4d7bfe480641151573b97c89d62841f296cbc3ca
5
5
  SHA512:
6
- metadata.gz: 9e132a1464d92aa59539d5489d5b7d777399de6c38f313cbad063b819e0e32503591532eb010f5c6b87da41096172e9dde8f5fa2fc84623ca2256b84cd3fa98d
7
- data.tar.gz: ea8dc9808a5c16697e5606ce8394c987ceb8df4f561fbf06250bd7669188665f56825a582e9c82240a52b6376767eedda9afeed588f960498fb32070466c512c
6
+ metadata.gz: cf96a4535a3fa8f6c7a62797805263e7957f198997213df65c164b9261d5199526f7dbc6e2a3f5e13a912e79cc7cd718ed193b0f197586cd03f1f3f95d958fca
7
+ data.tar.gz: 46726b0d18cd73d108b57b34872f1369ab63771a7d6c899aee021ab628ced537e6e0a7e255aecee259b902d682f5d13a12519dcc7033b786ef832c035a61f12a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.0.21] - 2024-03-19
4
+ ### Changed
5
+ - `RedisQueuedLocks::Acquier` refactirngs;
6
+
7
+ ## [0.0.20] - 2024-03-14
8
+ ### Added
9
+ - An ability to provide custom metadata to `lock` and `lock!` methods that will be passed
10
+ to the instrumentation level inside the `payload` parameter with `:meta` key;
11
+
3
12
  ## [0.0.19] - 2024-03-12
4
13
  ### Added
5
14
  - An ability to set the invocation time period to the block of code invoked under
data/README.md CHANGED
@@ -36,7 +36,9 @@ Each lock request is put into the request queue and processed in order of their
36
36
 
37
37
  ### Algorithm
38
38
 
39
- - soon
39
+ > Each lock request is put into the request queue and processed in order of their priority (FIFO). Each lock request lives some period of time (RTTL) which guarantees that the request queue will never be stacked.
40
+
41
+ **Soon**: detailed explanation.
40
42
 
41
43
  ---
42
44
 
@@ -174,6 +176,7 @@ def lock(
174
176
  raise_errors: false,
175
177
  fail_fast: false,
176
178
  identity: uniq_identity, # (attr_accessor) calculated during client instantiation via config[:uniq_identifier] proc;
179
+ metadata: nil,
177
180
  &block
178
181
  )
179
182
  ```
@@ -209,6 +212,8 @@ def lock(
209
212
  pods or/and nodes of your application;
210
213
  - It is calculated once during `RedisQueuedLock::Client` instantiation and stored in `@uniq_identity`
211
214
  ivar (accessed via `uniq_dentity` accessor method);
215
+ - `metadata` - `[NilClass,Any]`
216
+ - A custom metadata wich will be passed to the instrumenter's payload with `:meta` key;
212
217
  - `block` - `[Block]`
213
218
  - A block of code that should be executed after the successfully acquired lock.
214
219
  - If block is **passed** the obtained lock will be released after the block execution or it's ttl (what will happen first);
@@ -262,6 +267,7 @@ def lock!(
262
267
  retry_jitter: config[:retry_jitter],
263
268
  identity: uniq_identity,
264
269
  fail_fast: false,
270
+ metadata: nil,
265
271
  &block
266
272
  )
267
273
  ```
@@ -536,6 +542,7 @@ Detalized event semantics and payload structure:
536
542
  - `:lock_key` - `string` - lock name;
537
543
  - `:ts` - `integer`/`epoch` - the time when the lock was obtaiend;
538
544
  - `:acq_time` - `float`/`milliseconds` - time spent on lock acquiring;
545
+ - `:meta` - `nil`/`Any` - custom metadata passed to the `lock`/`lock!` method;
539
546
  - `"redis_queued_locks.lock_hold_and_release"`
540
547
  - an event signalizes about the "hold+and+release" process
541
548
  when the lock obtained and hold by the block of logic;
@@ -546,6 +553,7 @@ Detalized event semantics and payload structure:
546
553
  - `:lock_key` - `string` - lock name;
547
554
  - `:ts` - `integer`/`epoch` - the time when lock was obtained;
548
555
  - `:acq_time` - `float`/`milliseconds` - time spent on lock acquiring;
556
+ - `:meta` - `nil`/`Any` - custom metadata passed to the `lock`/`lock!` method;
549
557
  - `"redis_queued_locks.explicit_lock_release"`
550
558
  - an event signalizes about the explicit lock release (invoked via `RedisQueuedLock#unlock`);
551
559
  - payload:
@@ -571,10 +579,13 @@ Detalized event semantics and payload structure:
571
579
  the acquired lock for long-running blocks of code (that invoked "under" the lock
572
580
  whose ttl may expire before the block execution completes);
573
581
  - an ability to add custom metadata to the lock and an ability to read this data;
582
+ - lock prioritization;
583
+ - support for LIFO strategy;
574
584
  - **Minor**
575
585
  - GitHub Actions CI;
576
586
  - `RedisQueuedLocks::Acquier::Try.try_to_lock` - detailed successful result analization;
577
587
  - better code stylization and interesting refactorings;
588
+ - lock queue expiration (dead queue cleanup);
578
589
 
579
590
  ---
580
591
 
@@ -2,8 +2,8 @@
2
2
 
3
3
  # @api private
4
4
  # @since 0.1.0
5
- module RedisQueuedLocks::Acquier::Delay
6
- # Sleep with random time-shifting (it is necessary for empty-time-slot lock acquirement).
5
+ module RedisQueuedLocks::Acquier::AcquireLock::DelayExecution
6
+ # Sleep with random time-shifting (it is necessary for empty lock-acquirement time slots).
7
7
  #
8
8
  # @param retry_delay [Integer] In milliseconds
9
9
  # @param retry_jitter [Integer] In milliseconds
@@ -2,7 +2,7 @@
2
2
 
3
3
  # @api private
4
4
  # @since 0.1.0
5
- module RedisQueuedLocks::Acquier::Try
5
+ module RedisQueuedLocks::Acquier::AcquireLock::TryToLock
6
6
  # @param redis [RedisClient]
7
7
  # @param lock_key [String]
8
8
  # @param lock_key_queue [String]
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
4
+ # @since 0.1.0
5
+ module RedisQueuedLocks::Acquier::AcquireLock::WithAcqTimeout
6
+ # @param timeout [NilClass,Integer]
7
+ # Time period after which the logic will fail with timeout error.
8
+ # @param lock_key [String]
9
+ # Lock name.
10
+ # @param raise_errors [Boolean]
11
+ # Raise erros on exceptional cases.
12
+ # @option on_timeout [Proc,NilClass]
13
+ # Callback invoked on Timeout::Error.
14
+ # @return [Any]
15
+ #
16
+ # @api private
17
+ # @since 0.1.0
18
+ def with_acq_timeout(timeout, lock_key, raise_errors, on_timeout: nil, &block)
19
+ ::Timeout.timeout(timeout, &block)
20
+ rescue ::Timeout::Error
21
+ on_timeout.call unless on_timeout == nil
22
+
23
+ if raise_errors
24
+ raise(RedisQueuedLocks::LockAcquiermentTimeoutError, <<~ERROR_MESSAGE.strip)
25
+ Failed to acquire the lock "#{lock_key}" for the given timeout (#{timeout} seconds).
26
+ ERROR_MESSAGE
27
+ end
28
+ end
29
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  # @api private
4
4
  # @since 0.1.0
5
- module RedisQueuedLocks::Acquier::YieldExpire
5
+ module RedisQueuedLocks::Acquier::AcquireLock::YieldWithExpire
6
6
  # @param redis [RedisClient] Redis connection manager.
7
7
  # @param lock_key [String] Lock key to be expired.
8
8
  # @param timed [Boolean] Should the lock be wrapped by Tiemlout with with lock's ttl
@@ -0,0 +1,270 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
4
+ # @since 0.1.0
5
+ # rubocop:disable Metrics/ModuleLength
6
+ # rubocop:disable Metrics/MethodLength
7
+ # rubocop:disable Metrics/ClassLength
8
+ # rubocop:disable Metrics/BlockNesting
9
+ module RedisQueuedLocks::Acquier::AcquireLock
10
+ require_relative 'acquire_lock/delay_execution'
11
+ require_relative 'acquire_lock/with_acq_timeout'
12
+ require_relative 'acquire_lock/yield_with_expire'
13
+ require_relative 'acquire_lock/try_to_lock'
14
+
15
+ # @since 0.1.0
16
+ extend TryToLock
17
+ # @since 0.1.0
18
+ extend DelayExecution
19
+ # @since 0.1.0
20
+ extend YieldWithExpire
21
+ # @since 0.1.0
22
+ extend WithAcqTimeout
23
+ # @since 0.1.0
24
+ extend RedisQueuedLocks::Utilities
25
+
26
+ # @return [Integer] Redis expiration error (in milliseconds).
27
+ #
28
+ # @api private
29
+ # @since 0.1.0
30
+ REDIS_EXPIRE_ERROR = 1
31
+
32
+ class << self
33
+ # @param redis [RedisClient]
34
+ # Redis connection client.
35
+ # @param lock_name [String]
36
+ # Lock name to be acquier.
37
+ # @option process_id [Integer,String]
38
+ # The process that want to acquire a lock.
39
+ # @option thread_id [Integer,String]
40
+ # The process's thread that want to acquire a lock.
41
+ # @option fiber_id [Integer,String]
42
+ # A current fiber that want to acquire a lock.
43
+ # @option ractor_id [Integer,String]
44
+ # The current ractor that want to acquire a lock.
45
+ # @option ttl [Integer,NilClass]
46
+ # Lock's time to live (in milliseconds). Nil means "without timeout".
47
+ # @option queue_ttl [Integer]
48
+ # Lifetime of the acuier's lock request. In seconds.
49
+ # @option timeout [Integer]
50
+ # Time period whe should try to acquire the lock (in seconds).
51
+ # @option timed [Boolean]
52
+ # Limit the invocation time period of the passed block of code by the lock's TTL.
53
+ # @option retry_count [Integer,NilClass]
54
+ # How many times we should try to acquire a lock. Nil means "infinite retries".
55
+ # @option retry_delay [Integer]
56
+ # A time-interval between the each retry (in milliseconds).
57
+ # @option retry_jitter [Integer]
58
+ # Time-shift range for retry-delay (in milliseconds).
59
+ # @option raise_errors [Boolean]
60
+ # Raise errors on exceptional cases.
61
+ # @option instrumenter [#notify]
62
+ # See RedisQueuedLocks::Instrument::ActiveSupport for example.
63
+ # @option identity [String]
64
+ # Unique acquire identifier that is also should be unique between processes and pods
65
+ # on different machines. By default the uniq identity string is
66
+ # represented as 10 bytes hexstr.
67
+ # @option fail_fast [Boolean]
68
+ # Should the required lock to be checked before the try and exit immidetly if lock is
69
+ # already obtained.
70
+ # @option metadata [NilClass,Any]
71
+ # - A custom metadata wich will be passed to the instrumenter's payload with :meta key;
72
+ # @param [Block]
73
+ # A block of code that should be executed after the successfully acquired lock.
74
+ # @return [RedisQueuedLocks::Data,Hash<Symbol,Any>,yield]
75
+ # - Format: { ok: true/false, result: Any }
76
+ # - If block is given the result of block's yeld will be returned.
77
+ #
78
+ # @api private
79
+ # @since 0.1.0
80
+ def acquire_lock(
81
+ redis,
82
+ lock_name,
83
+ process_id:,
84
+ thread_id:,
85
+ fiber_id:,
86
+ ractor_id:,
87
+ ttl:,
88
+ queue_ttl:,
89
+ timeout:,
90
+ timed:,
91
+ retry_count:,
92
+ retry_delay:,
93
+ retry_jitter:,
94
+ raise_errors:,
95
+ instrumenter:,
96
+ identity:,
97
+ fail_fast:,
98
+ metadata:,
99
+ &block
100
+ )
101
+ # Step 1: prepare lock requirements (generate lock name, calc lock ttl, etc).
102
+ acquier_id = RedisQueuedLocks::Resource.acquier_identifier(
103
+ process_id,
104
+ thread_id,
105
+ fiber_id,
106
+ ractor_id,
107
+ identity
108
+ )
109
+ # NOTE:
110
+ # - think aobut the redis expiration error
111
+ # - (ttl - REDIS_EXPIRE_ERROR).yield_self { |val| (val == 0) ? ttl : val }
112
+ lock_ttl = ttl
113
+ lock_key = RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
114
+ lock_key_queue = RedisQueuedLocks::Resource.prepare_lock_queue(lock_name)
115
+ acquier_position = RedisQueuedLocks::Resource.calc_initial_acquier_position
116
+
117
+ # Step X: intermediate result observer
118
+ acq_process = {
119
+ lock_info: {},
120
+ should_try: true,
121
+ tries: 0,
122
+ acquired: false,
123
+ result: nil,
124
+ acq_time: nil, # NOTE: in milliseconds
125
+ hold_time: nil, # NOTE: in milliseconds
126
+ rel_time: nil # NOTE: in milliseconds
127
+ }
128
+ acq_dequeue = -> { dequeue_from_lock_queue(redis, lock_key_queue, acquier_id) }
129
+
130
+ # Step 2: try to lock with timeout
131
+ with_acq_timeout(timeout, lock_key, raise_errors, on_timeout: acq_dequeue) do
132
+ acq_start_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
133
+
134
+ # Step 2.1: caclically try to obtain the lock
135
+ while acq_process[:should_try]
136
+ try_to_lock(
137
+ redis,
138
+ lock_key,
139
+ lock_key_queue,
140
+ acquier_id,
141
+ acquier_position,
142
+ lock_ttl,
143
+ queue_ttl,
144
+ fail_fast
145
+ ) => { ok:, result: }
146
+
147
+ acq_end_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
148
+ acq_time = ((acq_end_time - acq_start_time) * 1_000).ceil(2)
149
+
150
+ # Step X: save the intermediate results to the result observer
151
+ acq_process[:result] = result
152
+ acq_process[:acq_end_time] = acq_end_time
153
+
154
+ # Step 2.1: analyze an acquirement attempt
155
+ if ok
156
+ # Step X (instrumentation): lock obtained
157
+ instrumenter.notify('redis_queued_locks.lock_obtained', {
158
+ lock_key: result[:lock_key],
159
+ ttl: result[:ttl],
160
+ acq_id: result[:acq_id],
161
+ ts: result[:ts],
162
+ acq_time: acq_time,
163
+ meta: metadata
164
+ })
165
+
166
+ # Step 2.1.a: successfully acquired => build the result
167
+ acq_process[:lock_info] = {
168
+ lock_key: result[:lock_key],
169
+ acq_id: result[:acq_id],
170
+ ts: result[:ts],
171
+ ttl: result[:ttl]
172
+ }
173
+ acq_process[:acquired] = true
174
+ acq_process[:should_try] = false
175
+ acq_process[:acq_time] = acq_time
176
+ acq_process[:acq_end_time] = acq_end_time
177
+ elsif fail_fast && acq_process[:result] == :fail_fast_no_try
178
+ acq_process[:should_try] = false
179
+ if raise_errors
180
+ raise(RedisQueuedLocks::LockAlreadyObtainedError, <<~ERROR_MESSAGE.strip)
181
+ Lock "#{lock_key}" is already obtained.
182
+ ERROR_MESSAGE
183
+ end
184
+ else
185
+ # Step 2.1.b: failed acquirement => retry
186
+ acq_process[:tries] += 1
187
+
188
+ if (retry_count != nil && acq_process[:tries] >= retry_count) || fail_fast
189
+ # NOTE:
190
+ # - reached the retry limit => quit from the loop
191
+ # - should fail fast => quit from the loop
192
+ acq_process[:should_try] = false
193
+ acq_process[:result] = fail_fast ? :fail_fast_after_try : :retry_limit_reached
194
+
195
+ # NOTE:
196
+ # - reached the retry limit => dequeue from the lock queue
197
+ # - should fail fast => dequeue from the lock queue
198
+ acq_dequeue.call
199
+
200
+ # NOTE: check and raise an error
201
+ if fail_fast && raise_errors
202
+ raise(RedisQueuedLocks::LockAlreadyObtainedError, <<~ERROR_MESSAGE.strip)
203
+ Lock "#{lock_key}" is already obtained.
204
+ ERROR_MESSAGE
205
+ elsif raise_errors
206
+ raise(RedisQueuedLocks::LockAcquiermentRetryLimitError, <<~ERROR_MESSAGE.strip)
207
+ Failed to acquire the lock "#{lock_key}"
208
+ for the given retry_count limit (#{retry_count} times).
209
+ ERROR_MESSAGE
210
+ end
211
+ else
212
+ # NOTE:
213
+ # delay the exceution in order to prevent chaotic attempts
214
+ # and to allow other processes and threads to obtain the lock too.
215
+ delay_execution(retry_delay, retry_jitter)
216
+ end
217
+ end
218
+ end
219
+ end
220
+
221
+ # Step 3: analyze acquirement result
222
+ if acq_process[:acquired]
223
+ # Step 3.a: acquired successfully => run logic or return the result of acquirement
224
+ if block_given?
225
+ begin
226
+ yield_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
227
+ ttl_shift = ((yield_time - acq_process[:acq_end_time]) * 1000).ceil(2)
228
+ yield_with_expire(redis, lock_key, timed, ttl_shift, ttl, &block)
229
+ ensure
230
+ acq_process[:rel_time] = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
231
+ acq_process[:hold_time] = (
232
+ (acq_process[:rel_time] - acq_process[:acq_end_time]) * 1000
233
+ ).ceil(2)
234
+
235
+ # Step X (instrumentation): lock_hold_and_release
236
+ run_non_critical do
237
+ instrumenter.notify('redis_queued_locks.lock_hold_and_release', {
238
+ hold_time: acq_process[:hold_time],
239
+ ttl: acq_process[:lock_info][:ttl],
240
+ acq_id: acq_process[:lock_info][:acq_id],
241
+ ts: acq_process[:lock_info][:ts],
242
+ lock_key: acq_process[:lock_info][:lock_key],
243
+ acq_time: acq_process[:acq_time],
244
+ meta: metadata
245
+ })
246
+ end
247
+ end
248
+ else
249
+ RedisQueuedLocks::Data[ok: true, result: acq_process[:lock_info]]
250
+ end
251
+ else
252
+ if acq_process[:result] != :retry_limit_reached &&
253
+ acq_process[:result] != :fail_fast_no_try &&
254
+ acq_process[:result] != :fail_fast_after_try
255
+ # NOTE: we have only two situations if lock is not acquired withou fast-fail flag:
256
+ # - time limit is reached
257
+ # - retry count limit is reached
258
+ # In other cases the lock obtaining time and tries count are infinite.
259
+ acq_process[:result] = :timeout_reached
260
+ end
261
+ # Step 3.b: lock is not acquired (acquier is dequeued by timeout callback)
262
+ RedisQueuedLocks::Data[ok: false, result: acq_process[:result]]
263
+ end
264
+ end
265
+ end
266
+ end
267
+ # rubocop:enable Metrics/ModuleLength
268
+ # rubocop:enable Metrics/MethodLength
269
+ # rubocop:enable Metrics/ClassLength
270
+ # rubocop:enable Metrics/BlockNesting
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
4
+ # @since 0.1.0
5
+ module RedisQueuedLocks::Acquier::ExtendLockTTL
6
+ class << self
7
+ # @param redis_client [RedisClient]
8
+ # @param lock_name [String]
9
+ # @param milliseconds [Integer]
10
+ # @return [?]
11
+ #
12
+ # @api private
13
+ # @since 0.1.0
14
+ def extend_lock_ttl(redis_client, lock_name, milliseconds)
15
+ # TODO: realize
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
4
+ # @since 0.1.0
5
+ module RedisQueuedLocks::Acquier::IsLocked
6
+ class << self
7
+ # @param redis_client [RedisClient]
8
+ # @param lock_name [String]
9
+ # @return [Boolean]
10
+ #
11
+ # @api private
12
+ # @since 0.1.0
13
+ def locked?(redis_client, lock_name)
14
+ lock_key = RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
15
+ redis_client.call('EXISTS', lock_key) == 1
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
4
+ # @since 0.1.0
5
+ module RedisQueuedLocks::Acquier::IsQueued
6
+ class << self
7
+ # @param redis_client [RedisClient]
8
+ # @param lock_name [String]
9
+ # @return [Boolean]
10
+ #
11
+ # @api private
12
+ # @since 0.1.0
13
+ def queued?(redis_client, lock_name)
14
+ lock_key_queue = RedisQueuedLocks::Resource.prepare_lock_queue(lock_name)
15
+ redis_client.call('EXISTS', lock_key_queue) == 1
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
4
+ # @since 0.1.0
5
+ module RedisQueuedLocks::Acquier::Keys
6
+ class << self
7
+ # @param redis_client [RedisClient]
8
+ # @option scan_size [Integer]
9
+ # @return [Array<String>]
10
+ #
11
+ # @api private
12
+ # @since 0.1.0
13
+ def keys(redis_client, scan_size:)
14
+ Set.new.tap do |keys|
15
+ redis_client.scan(
16
+ 'MATCH',
17
+ RedisQueuedLocks::Resource::KEY_PATTERN,
18
+ count: scan_size
19
+ ) do |key|
20
+ # TODO: reduce unnecessary iterations
21
+ keys.add(key)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
4
+ # @since 0.1.0
5
+ module RedisQueuedLocks::Acquier::LockInfo
6
+ class << self
7
+ # @param redis_client [RedisClient]
8
+ # @param lock_name [String]
9
+ # @return [Hash<Symbol,String|Numeric>,NilClass]
10
+ # - `nil` is returned when lock key does not exist or expired;
11
+ # - result format: {
12
+ # lock_key: "rql:lock:your_lockname", # acquired lock key
13
+ # acq_id: "rql:acq:process_id/thread_id", # lock acquier identifier
14
+ # ts: 123456789, # <locked at> time stamp (epoch)
15
+ # ini_ttl: 123456789, # initial lock key ttl (milliseconds),
16
+ # rem_ttl: 123456789, # remaining lock key ttl (milliseconds)
17
+ # }
18
+ #
19
+ # @api private
20
+ # @since 0.1.0
21
+ def lock_info(redis_client, lock_name)
22
+ lock_key = RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
23
+
24
+ result = redis_client.multi(watch: [lock_key]) do |transact|
25
+ transact.call('HGETALL', lock_key)
26
+ transact.call('PTTL', lock_key)
27
+ end
28
+
29
+ if result == nil
30
+ # NOTE:
31
+ # - nil result means that during transaction invocation the lock is changed (CAS):
32
+ # - lock is expired;
33
+ # - lock is released;
34
+ # - lock is expired + re-obtained;
35
+ nil
36
+ else
37
+ hget_cmd_res = result[0]
38
+ pttl_cmd_res = result[1]
39
+
40
+ if hget_cmd_res == {} || pttl_cmd_res == -2 # NOTE: key does not exist
41
+ nil
42
+ else
43
+ # NOTE: the result of MULTI-command is an array of results of each internal command
44
+ # - result[0] (HGETALL) (Hash<String,String>)
45
+ # - result[1] (PTTL) (Integer)
46
+ {
47
+ lock_key: lock_key,
48
+ acq_id: hget_cmd_res['acq_id'],
49
+ ts: Integer(hget_cmd_res['ts']),
50
+ ini_ttl: Integer(hget_cmd_res['ini_ttl']),
51
+ rem_ttl: ((pttl_cmd_res == -1) ? Infinity : pttl_cmd_res)
52
+ }
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
4
+ # @since 0.1.0
5
+ module RedisQueuedLocks::Acquier::Locks
6
+ class << self
7
+ # @param redis_client [RedisClient]
8
+ # @option scan_size [Integer]
9
+ # @return [Set<String>]
10
+ #
11
+ # @api private
12
+ # @since 0.1.0
13
+ def locks(redis_client, scan_size:)
14
+ Set.new.tap do |lock_keys|
15
+ redis_client.scan(
16
+ 'MATCH',
17
+ RedisQueuedLocks::Resource::LOCK_PATTERN,
18
+ count: scan_size
19
+ ) do |lock_key|
20
+ # TODO: reduce unnecessary iterations
21
+ lock_keys.add(lock_key)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
4
+ # @since 0.1.0
5
+ module RedisQueuedLocks::Acquier::QueueInfo
6
+ class << self
7
+ # Returns an information about the required lock queue by the lock name. The result
8
+ # represnts the ordered lock request queue that is ordered by score (Redis sets) and shows
9
+ # lock acquirers and their position in queue. Async nature with redis communcation can lead
10
+ # the sitaution when the queue becomes empty during the queue data extraction. So sometimes
11
+ # you can receive the lock queue info with empty queue.
12
+ #
13
+ # @param redis_client [RedisClient]
14
+ # @param lock_name [String]
15
+ # @return [Hash<Symbol,String|Array<Hash<Symbol,String|Float>>,NilClass]
16
+ # - `nil` is returned when lock queue does not exist;
17
+ # - result format: {
18
+ # lock_queue: "rql:lock_queue:your_lock_name", # lock queue key in redis,
19
+ # queue: [
20
+ # { acq_id: "rql:acq:process_id/thread_id", score: 123 },
21
+ # { acq_id: "rql:acq:process_id/thread_id", score: 456 },
22
+ # ] # ordered set (by score) with information about an acquier and their position in queue
23
+ # }
24
+ #
25
+ # @api private
26
+ # @since 0.1.0
27
+ def queue_info(redis_client, lock_name)
28
+ lock_key_queue = RedisQueuedLocks::Resource.prepare_lock_queue(lock_name)
29
+
30
+ result = redis_client.pipelined do |pipeline|
31
+ pipeline.call('EXISTS', lock_key_queue)
32
+ pipeline.call('ZRANGE', lock_key_queue, '0', '-1', 'WITHSCORES')
33
+ end
34
+
35
+ exists_cmd_res = result[0]
36
+ zrange_cmd_res = result[1]
37
+
38
+ if exists_cmd_res == 1
39
+ # NOTE: queue existed during the piepline invocation
40
+ {
41
+ lock_queue: lock_key_queue,
42
+ queue: zrange_cmd_res.map { |val| { acq_id: val[0], score: val[1] } }
43
+ }
44
+ else
45
+ # NOTE: queue did not exist during the pipeline invocation
46
+ nil
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
4
+ # @since 0.1.0
5
+ module RedisQueuedLocks::Acquier::Queues
6
+ class << self
7
+ # @param redis_client [RedisClient]
8
+ # @param scan_size [Integer]
9
+ # @return [Set<String>]
10
+ #
11
+ # @api private
12
+ # @since 0.1.0
13
+ def queues(redis_client, scan_size:)
14
+ Set.new.tap do |lock_queues|
15
+ redis_client.scan(
16
+ 'MATCH',
17
+ RedisQueuedLocks::Resource::LOCK_QUEUE_PATTERN,
18
+ count: scan_size
19
+ ) do |lock_queue|
20
+ # TODO: reduce unnecessary iterations
21
+ lock_queues.add(lock_queue)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end