redis_queued_locks 0.0.20 → 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: 281c476c0b1318a3f061e0d22031de92983f4ebd2277bc7e024b2baf998ae559
4
- data.tar.gz: 5547ace054290da08fb1eb26e965e2d0ff68dfc9b5c3df0adb8a77069543b8ef
3
+ metadata.gz: 2ea008491f8c631aea2677c6571a206db919c0ca5d599b58adc37fa82c372145
4
+ data.tar.gz: 41e5bece44a3b1af1031d28a4d7bfe480641151573b97c89d62841f296cbc3ca
5
5
  SHA512:
6
- metadata.gz: a971c732be6ef918ae6e9fc70e4456a076b36a50622cda4e0f2b4d5d3f0cb9479e768852e391b02a6420d3de682ccca7de374cf1410b25be37a5d8cf43a4865c
7
- data.tar.gz: ef66b70b7ef28be2b9b2422a94dc89e2fff350ad5d8ce58806f28c7742fa4a3caa5032f64d4817abd52dc4bc2ae77327dcfee79b28f462241c63fb2ddaf992eb
6
+ metadata.gz: cf96a4535a3fa8f6c7a62797805263e7957f198997213df65c164b9261d5199526f7dbc6e2a3f5e13a912e79cc7cd718ed193b0f197586cd03f1f3f95d958fca
7
+ data.tar.gz: 46726b0d18cd73d108b57b34872f1369ab63771a7d6c899aee021ab628ced537e6e0a7e255aecee259b902d682f5d13a12519dcc7033b786ef832c035a61f12a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.0.21] - 2024-03-19
4
+ ### Changed
5
+ - `RedisQueuedLocks::Acquier` refactirngs;
6
+
3
7
  ## [0.0.20] - 2024-03-14
4
8
  ### Added
5
9
  - An ability to provide custom metadata to `lock` and `lock!` methods that will be passed
data/README.md CHANGED
@@ -213,7 +213,7 @@ def lock(
213
213
  - It is calculated once during `RedisQueuedLock::Client` instantiation and stored in `@uniq_identity`
214
214
  ivar (accessed via `uniq_dentity` accessor method);
215
215
  - `metadata` - `[NilClass,Any]`
216
- - A custom metadata wich will be passed to the instrumenter's payload with :meta key;
216
+ - A custom metadata wich will be passed to the instrumenter's payload with `:meta` key;
217
217
  - `block` - `[Block]`
218
218
  - A block of code that should be executed after the successfully acquired lock.
219
219
  - If block is **passed** the obtained lock will be released after the block execution or it's ttl (what will happen first);
@@ -580,10 +580,12 @@ Detalized event semantics and payload structure:
580
580
  whose ttl may expire before the block execution completes);
581
581
  - an ability to add custom metadata to the lock and an ability to read this data;
582
582
  - lock prioritization;
583
+ - support for LIFO strategy;
583
584
  - **Minor**
584
585
  - GitHub Actions CI;
585
586
  - `RedisQueuedLocks::Acquier::Try.try_to_lock` - detailed successful result analization;
586
587
  - better code stylization and interesting refactorings;
588
+ - lock queue expiration (dead queue cleanup);
587
589
 
588
590
  ---
589
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