redis_queued_locks 0.0.20 → 0.0.22

Sign up to get free protection for your applications and to get access to all the features.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -0
  3. data/README.md +14 -1
  4. data/lib/redis_queued_locks/acquier/{delay.rb → acquire_lock/delay_execution.rb} +2 -2
  5. data/lib/redis_queued_locks/acquier/{try.rb → acquire_lock/try_to_lock.rb} +1 -1
  6. data/lib/redis_queued_locks/acquier/acquire_lock/with_acq_timeout.rb +29 -0
  7. data/lib/redis_queued_locks/acquier/{yield_expire.rb → acquire_lock/yield_with_expire.rb} +9 -2
  8. data/lib/redis_queued_locks/acquier/acquire_lock.rb +291 -0
  9. data/lib/redis_queued_locks/acquier/extend_lock_ttl.rb +19 -0
  10. data/lib/redis_queued_locks/acquier/is_locked.rb +18 -0
  11. data/lib/redis_queued_locks/acquier/is_queued.rb +18 -0
  12. data/lib/redis_queued_locks/acquier/keys.rb +26 -0
  13. data/lib/redis_queued_locks/acquier/lock_info.rb +57 -0
  14. data/lib/redis_queued_locks/acquier/locks.rb +26 -0
  15. data/lib/redis_queued_locks/acquier/queue_info.rb +50 -0
  16. data/lib/redis_queued_locks/acquier/queues.rb +26 -0
  17. data/lib/redis_queued_locks/acquier/release_all_locks.rb +88 -0
  18. data/lib/redis_queued_locks/acquier/release_lock.rb +76 -0
  19. data/lib/redis_queued_locks/acquier.rb +11 -557
  20. data/lib/redis_queued_locks/client.rb +34 -19
  21. data/lib/redis_queued_locks/instrument/active_support.rb +1 -1
  22. data/lib/redis_queued_locks/instrument/void_notifier.rb +1 -1
  23. data/lib/redis_queued_locks/instrument.rb +3 -3
  24. data/lib/redis_queued_locks/logging/void_logger.rb +43 -0
  25. data/lib/redis_queued_locks/logging.rb +57 -0
  26. data/lib/redis_queued_locks/utilities.rb +16 -0
  27. data/lib/redis_queued_locks/version.rb +2 -2
  28. data/lib/redis_queued_locks.rb +3 -0
  29. metadata +20 -6
  30. data/lib/redis_queued_locks/acquier/release.rb +0 -60
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 281c476c0b1318a3f061e0d22031de92983f4ebd2277bc7e024b2baf998ae559
4
- data.tar.gz: 5547ace054290da08fb1eb26e965e2d0ff68dfc9b5c3df0adb8a77069543b8ef
3
+ metadata.gz: 4360a9af038cc33ca36215d729558bab6810b9c5a4fec7f31df6ca2871b7dd80
4
+ data.tar.gz: 10b80a0f389994fde9a4815c3a5031ae85cee3c61d9c2a02e85d80146208bb99
5
5
  SHA512:
6
- metadata.gz: a971c732be6ef918ae6e9fc70e4456a076b36a50622cda4e0f2b4d5d3f0cb9479e768852e391b02a6420d3de682ccca7de374cf1410b25be37a5d8cf43a4865c
7
- data.tar.gz: ef66b70b7ef28be2b9b2422a94dc89e2fff350ad5d8ce58806f28c7742fa4a3caa5032f64d4817abd52dc4bc2ae77327dcfee79b28f462241c63fb2ddaf992eb
6
+ metadata.gz: 362e7708a37f8716217e08117f072ef34d14886677379d28bef08f80c1a1a82e1309509dfc5fe4f748a3a10268bf340087c83e797ebcd72cb8172594352fb23d
7
+ data.tar.gz: 32a0943ee8c80b8ed745dff8eeea748c2289008d8fb00d3e20c7b464ef14a4d21158066e548bb86cfebc73c5d61a365289b49ae8061bc3ea0339ad7bb3c9894d
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.0.22] - 2024-03-21
4
+ ### Added
5
+ - Logging infrastructure. Initial implementation includes the only debugging features.
6
+
7
+ ## [0.0.21] - 2024-03-19
8
+ ### Changed
9
+ - Refactored `RedisQueuedLocks::Acquier`;
10
+
3
11
  ## [0.0.20] - 2024-03-14
4
12
  ### Added
5
13
  - An ability to provide custom metadata to `lock` and `lock!` methods that will be passed
data/README.md CHANGED
@@ -134,6 +134,16 @@ clinet = RedisQueuedLocks::Client.new(redis_client) do |config|
134
134
  # - it is calculated once per `RedisQueudLocks::Client` instance;
135
135
  # - expects the proc object;
136
136
  config.uniq_identifier = -> { RedisQueuedLocks::Resource.calc_uniq_identity }
137
+
138
+ # (default: RedisQueuedLocks::Logging::VoidLogger)
139
+ # - the logger object;
140
+ # - should implement `debug(progname = nil, &block)` (minimal requirement) or be an instance of Ruby's `::Logger` class/subclass;
141
+ # - at this moment the only debug logs are realised in 3 cases:
142
+ # - start of lock obtaining: "[redis_queud_locks.start_lock_obtaining] lock_key => 'rql:lock:your_lock'"
143
+ # - finish of the lock obtaining: "[redis_queued_locks.lock_obtained] lock_key => 'rql:lock:your_lock' acq_time => 123.456 (ms)"
144
+ # - start of the lock expiration after `yield`: "[redis_queud_locks.expire_lock] lock_key => 'rql:lock:your_lock'"
145
+ # - by default uses VoidLogger that does nothing;
146
+ config.logger = RedisQueuedLocks::Logging::VoidLogger
137
147
  end
138
148
  ```
139
149
 
@@ -213,7 +223,7 @@ def lock(
213
223
  - It is calculated once during `RedisQueuedLock::Client` instantiation and stored in `@uniq_identity`
214
224
  ivar (accessed via `uniq_dentity` accessor method);
215
225
  - `metadata` - `[NilClass,Any]`
216
- - A custom metadata wich will be passed to the instrumenter's payload with :meta key;
226
+ - A custom metadata wich will be passed to the instrumenter's payload with `:meta` key;
217
227
  - `block` - `[Block]`
218
228
  - A block of code that should be executed after the successfully acquired lock.
219
229
  - If block is **passed** the obtained lock will be released after the block execution or it's ttl (what will happen first);
@@ -580,10 +590,13 @@ Detalized event semantics and payload structure:
580
590
  whose ttl may expire before the block execution completes);
581
591
  - an ability to add custom metadata to the lock and an ability to read this data;
582
592
  - lock prioritization;
593
+ - support for LIFO strategy;
594
+ - structured logging;
583
595
  - **Minor**
584
596
  - GitHub Actions CI;
585
597
  - `RedisQueuedLocks::Acquier::Try.try_to_lock` - detailed successful result analization;
586
598
  - better code stylization and interesting refactorings;
599
+ - lock queue expiration (dead queue cleanup);
587
600
 
588
601
  ---
589
602
 
@@ -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,8 +2,12 @@
2
2
 
3
3
  # @api private
4
4
  # @since 0.1.0
5
- module RedisQueuedLocks::Acquier::YieldExpire
5
+ module RedisQueuedLocks::Acquier::AcquireLock::YieldWithExpire
6
+ # @since 0.1.0
7
+ extend RedisQueuedLocks::Utilities
8
+
6
9
  # @param redis [RedisClient] Redis connection manager.
10
+ # @param logger [#debug] Logger object.
7
11
  # @param lock_key [String] Lock key to be expired.
8
12
  # @param timed [Boolean] Should the lock be wrapped by Tiemlout with with lock's ttl
9
13
  # @param ttl_shift [Float] Lock's TTL shifting. Should affect block's ttl. In millisecodns.
@@ -13,7 +17,7 @@ module RedisQueuedLocks::Acquier::YieldExpire
13
17
  #
14
18
  # @api private
15
19
  # @since 0.1.0
16
- def yield_with_expire(redis, lock_key, timed, ttl_shift, ttl, &block)
20
+ def yield_with_expire(redis, logger, lock_key, timed, ttl_shift, ttl, &block)
17
21
  if block_given?
18
22
  if timed && ttl != nil
19
23
  timeout = ((ttl - ttl_shift) / 1000.0).yield_self { |time| (time < 0) ? 0.0 : time }
@@ -23,6 +27,9 @@ module RedisQueuedLocks::Acquier::YieldExpire
23
27
  end
24
28
  end
25
29
  ensure
30
+ run_non_critical do
31
+ logger.debug("[redis_queued_locks.expire_lock] lock_key => '#{lock_key}'")
32
+ end
26
33
  redis.call('EXPIRE', lock_key, '0')
27
34
  end
28
35
 
@@ -0,0 +1,291 @@
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
+ # @option logger [#debug]
73
+ # - Logger object used from `configuration` layer (see config[:logger]);
74
+ # - See RedisQueuedLocks::Logging::VoidLogger for example;
75
+ # @param [Block]
76
+ # A block of code that should be executed after the successfully acquired lock.
77
+ # @return [RedisQueuedLocks::Data,Hash<Symbol,Any>,yield]
78
+ # - Format: { ok: true/false, result: Any }
79
+ # - If block is given the result of block's yeld will be returned.
80
+ #
81
+ # @api private
82
+ # @since 0.1.0
83
+ def acquire_lock(
84
+ redis,
85
+ lock_name,
86
+ process_id:,
87
+ thread_id:,
88
+ fiber_id:,
89
+ ractor_id:,
90
+ ttl:,
91
+ queue_ttl:,
92
+ timeout:,
93
+ timed:,
94
+ retry_count:,
95
+ retry_delay:,
96
+ retry_jitter:,
97
+ raise_errors:,
98
+ instrumenter:,
99
+ identity:,
100
+ fail_fast:,
101
+ metadata:,
102
+ logger:,
103
+ &block
104
+ )
105
+ # Step 1: prepare lock requirements (generate lock name, calc lock ttl, etc).
106
+ acquier_id = RedisQueuedLocks::Resource.acquier_identifier(
107
+ process_id,
108
+ thread_id,
109
+ fiber_id,
110
+ ractor_id,
111
+ identity
112
+ )
113
+ # NOTE:
114
+ # - think aobut the redis expiration error
115
+ # - (ttl - REDIS_EXPIRE_ERROR).yield_self { |val| (val == 0) ? ttl : val }
116
+ lock_ttl = ttl
117
+ lock_key = RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
118
+ lock_key_queue = RedisQueuedLocks::Resource.prepare_lock_queue(lock_name)
119
+ acquier_position = RedisQueuedLocks::Resource.calc_initial_acquier_position
120
+
121
+ # Step X: intermediate result observer
122
+ acq_process = {
123
+ lock_info: {},
124
+ should_try: true,
125
+ tries: 0,
126
+ acquired: false,
127
+ result: nil,
128
+ acq_time: nil, # NOTE: in milliseconds
129
+ hold_time: nil, # NOTE: in milliseconds
130
+ rel_time: nil # NOTE: in milliseconds
131
+ }
132
+ acq_dequeue = -> { dequeue_from_lock_queue(redis, lock_key_queue, acquier_id) }
133
+
134
+ run_non_critical do
135
+ logger.debug(
136
+ "[redis_queued_locks.start_lock_obtaining] " \
137
+ "lock_key => '#{lock_key}'"
138
+ )
139
+ end
140
+
141
+ # Step 2: try to lock with timeout
142
+ with_acq_timeout(timeout, lock_key, raise_errors, on_timeout: acq_dequeue) do
143
+ acq_start_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
144
+
145
+ # Step 2.1: caclically try to obtain the lock
146
+ while acq_process[:should_try]
147
+ try_to_lock(
148
+ redis,
149
+ lock_key,
150
+ lock_key_queue,
151
+ acquier_id,
152
+ acquier_position,
153
+ lock_ttl,
154
+ queue_ttl,
155
+ fail_fast
156
+ ) => { ok:, result: }
157
+
158
+ acq_end_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
159
+ acq_time = ((acq_end_time - acq_start_time) * 1_000).ceil(2)
160
+
161
+ # Step X: save the intermediate results to the result observer
162
+ acq_process[:result] = result
163
+ acq_process[:acq_end_time] = acq_end_time
164
+
165
+ # Step 2.1: analyze an acquirement attempt
166
+ if ok
167
+ run_non_critical do
168
+ logger.debug(
169
+ "[redis_queued_locks.lock_obtained] " \
170
+ "lock_key => '#{result[:lock_key]}'" \
171
+ "acq_time => #{acq_time} (ms)"
172
+ )
173
+ end
174
+
175
+ # Step X (instrumentation): lock obtained
176
+ run_non_critical do
177
+ instrumenter.notify('redis_queued_locks.lock_obtained', {
178
+ lock_key: result[:lock_key],
179
+ ttl: result[:ttl],
180
+ acq_id: result[:acq_id],
181
+ ts: result[:ts],
182
+ acq_time: acq_time,
183
+ meta: metadata
184
+ })
185
+ end
186
+
187
+ # Step 2.1.a: successfully acquired => build the result
188
+ acq_process[:lock_info] = {
189
+ lock_key: result[:lock_key],
190
+ acq_id: result[:acq_id],
191
+ ts: result[:ts],
192
+ ttl: result[:ttl]
193
+ }
194
+ acq_process[:acquired] = true
195
+ acq_process[:should_try] = false
196
+ acq_process[:acq_time] = acq_time
197
+ acq_process[:acq_end_time] = acq_end_time
198
+ elsif fail_fast && acq_process[:result] == :fail_fast_no_try
199
+ acq_process[:should_try] = false
200
+ if raise_errors
201
+ raise(RedisQueuedLocks::LockAlreadyObtainedError, <<~ERROR_MESSAGE.strip)
202
+ Lock "#{lock_key}" is already obtained.
203
+ ERROR_MESSAGE
204
+ end
205
+ else
206
+ # Step 2.1.b: failed acquirement => retry
207
+ acq_process[:tries] += 1
208
+
209
+ if (retry_count != nil && acq_process[:tries] >= retry_count) || fail_fast
210
+ # NOTE:
211
+ # - reached the retry limit => quit from the loop
212
+ # - should fail fast => quit from the loop
213
+ acq_process[:should_try] = false
214
+ acq_process[:result] = fail_fast ? :fail_fast_after_try : :retry_limit_reached
215
+
216
+ # NOTE:
217
+ # - reached the retry limit => dequeue from the lock queue
218
+ # - should fail fast => dequeue from the lock queue
219
+ acq_dequeue.call
220
+
221
+ # NOTE: check and raise an error
222
+ if fail_fast && raise_errors
223
+ raise(RedisQueuedLocks::LockAlreadyObtainedError, <<~ERROR_MESSAGE.strip)
224
+ Lock "#{lock_key}" is already obtained.
225
+ ERROR_MESSAGE
226
+ elsif raise_errors
227
+ raise(RedisQueuedLocks::LockAcquiermentRetryLimitError, <<~ERROR_MESSAGE.strip)
228
+ Failed to acquire the lock "#{lock_key}"
229
+ for the given retry_count limit (#{retry_count} times).
230
+ ERROR_MESSAGE
231
+ end
232
+ else
233
+ # NOTE:
234
+ # delay the exceution in order to prevent chaotic attempts
235
+ # and to allow other processes and threads to obtain the lock too.
236
+ delay_execution(retry_delay, retry_jitter)
237
+ end
238
+ end
239
+ end
240
+ end
241
+
242
+ # Step 3: analyze acquirement result
243
+ if acq_process[:acquired]
244
+ # Step 3.a: acquired successfully => run logic or return the result of acquirement
245
+ if block_given?
246
+ begin
247
+ yield_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
248
+ ttl_shift = ((yield_time - acq_process[:acq_end_time]) * 1000).ceil(2)
249
+ yield_with_expire(redis, logger, lock_key, timed, ttl_shift, ttl, &block)
250
+ ensure
251
+ acq_process[:rel_time] = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
252
+ acq_process[:hold_time] = (
253
+ (acq_process[:rel_time] - acq_process[:acq_end_time]) * 1000
254
+ ).ceil(2)
255
+
256
+ # Step X (instrumentation): lock_hold_and_release
257
+ run_non_critical do
258
+ instrumenter.notify('redis_queued_locks.lock_hold_and_release', {
259
+ hold_time: acq_process[:hold_time],
260
+ ttl: acq_process[:lock_info][:ttl],
261
+ acq_id: acq_process[:lock_info][:acq_id],
262
+ ts: acq_process[:lock_info][:ts],
263
+ lock_key: acq_process[:lock_info][:lock_key],
264
+ acq_time: acq_process[:acq_time],
265
+ meta: metadata
266
+ })
267
+ end
268
+ end
269
+ else
270
+ RedisQueuedLocks::Data[ok: true, result: acq_process[:lock_info]]
271
+ end
272
+ else
273
+ if acq_process[:result] != :retry_limit_reached &&
274
+ acq_process[:result] != :fail_fast_no_try &&
275
+ acq_process[:result] != :fail_fast_after_try
276
+ # NOTE: we have only two situations if lock is not acquired withou fast-fail flag:
277
+ # - time limit is reached
278
+ # - retry count limit is reached
279
+ # In other cases the lock obtaining time and tries count are infinite.
280
+ acq_process[:result] = :timeout_reached
281
+ end
282
+ # Step 3.b: lock is not acquired (acquier is dequeued by timeout callback)
283
+ RedisQueuedLocks::Data[ok: false, result: acq_process[:result]]
284
+ end
285
+ end
286
+ end
287
+ end
288
+ # rubocop:enable Metrics/ModuleLength
289
+ # rubocop:enable Metrics/MethodLength
290
+ # rubocop:enable Metrics/ClassLength
291
+ # rubocop:enable Metrics/BlockNesting
@@ -0,0 +1,19 @@
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
+ # @param logger [#debug]
11
+ # @return [?]
12
+ #
13
+ # @api private
14
+ # @since 0.1.0
15
+ def extend_lock_ttl(redis_client, lock_name, milliseconds, logger)
16
+ # TODO: realize
17
+ end
18
+ end
19
+ 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