redis_queued_locks 0.0.20 → 0.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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