redis_queued_locks 0.0.0 → 0.0.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 612f9339ffba6231bcfe2277af85e2225346f6166eb5bb77da0f507beaeae888
4
- data.tar.gz: 35229b1d26a17a9c46e39e565d11ec416ca0e173f57360765c064327ffa6390a
3
+ metadata.gz: e984603ea8f509f8e5f7f685c356d1f838b16bc950e1de83ff8f66e6afa83fcf
4
+ data.tar.gz: 1b2eee0f32c911caecf08214b2b404d620ffb10501ae72d7721235b64fc02362
5
5
  SHA512:
6
- metadata.gz: 1fdb43047ee4b3ac72d28a244ddc9cb4d6b363ff3aa3852b3cc27cbb77ab0a159ef1a45e7eb13f4e4407449fc7b7383e1aac1775f5f739e6ae2e2fcacc5ced46
7
- data.tar.gz: 2fbf7fb572005090140ccc9a972e4d84d5be0b1a9a2154fc37c1b04f2dcf44abb06ec6e530c1fa36be0f7ddb3795615f6d7626a224a9d3f3fe913cbf3362734f
6
+ metadata.gz: b7e2d574eeeff4a9a235f39ee25bde152880dcd12e7066196a8304e54098480ef05672edd89a6931ece225711b96602f23ce76304c555dbd623d30099e35ca0a
7
+ data.tar.gz: d7f5075bbf136e672ebb1632da5c6a78b2b72886bc1d5e8b0d9425cf38b14f5eef5a097fdc835c78f5f2d26885512279926a33bdbdb559151b875453d5312c9e
data/.rubocop.yml CHANGED
@@ -5,7 +5,7 @@ inherit_gem:
5
5
  - lib/rubocop.rspec.yml
6
6
 
7
7
  AllCops:
8
- TargetRubyVersion: 3.2
8
+ TargetRubyVersion: 3.1
9
9
  NewCops: enable
10
10
  Include:
11
11
  - lib/**/*.rb
@@ -33,3 +33,6 @@ Metrics/AbcSize:
33
33
 
34
34
  Metrics/CyclomaticComplexity:
35
35
  Enabled: false
36
+
37
+ Metrics/PerceivedComplexity:
38
+ Enabled: false
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.2.2
1
+ 3.1.2
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.0.2] - 2024-02-26
4
+ ### Added
5
+ - Instrumentation events:
6
+ - `"redis_queued_locks.lock_obtained"`;
7
+ - `"redis_queued_locks.lock_hold_and_release"`;
8
+ - `"redis_queued_locks.explicit_lock_release"`;
9
+ - `"redis_queued_locks.explicit_all_locks_release"`;
10
+
11
+ ## [0.0.1] - 2024-02-26
12
+
13
+ - Still the initial release version;
14
+ - Downgrade the minimal Ruby version requirement from 3.2 to 3.1;
15
+
3
16
  ## [0.0.0] - 2024-02-25
4
17
 
5
18
  - Initial release
data/README.md CHANGED
@@ -1,3 +1,43 @@
1
1
  # RedisQueuedLocks
2
2
 
3
3
  Distributed lock implementation with "lock acquisition queue" capabilities based on the Redis Database.
4
+
5
+ ## Instrumentation events
6
+
7
+ - `"redis_queued_locks.lock_obtained"`
8
+ - a moment when the lock was obtained;
9
+ - payload:
10
+ - `ttl` - `integer`/`milliseconds` - lock ttl;
11
+ - `acq_id` - `string` - lock acquier identifier;
12
+ - `lock_key` - `string` - lock name;
13
+ - `ts` - `integer`/`epoch` - the time when the lock was obtaiend;
14
+ - `acq_time` - `float`/`milliseconds` - time spent on lock acquiring;
15
+ - `"redis_queued_locks.lock_hold_and_release"`
16
+ - an event signalizes about the "hold+and+release" process
17
+ when the lock obtained and hold by the block of logic;
18
+ - payload:
19
+ - `hold_time` - `float`/`milliseconds` - lock hold time;
20
+ - `ttl` - `integer`/`milliseconds` - lock ttl;
21
+ - `acq_id` - `string` - lock acquier identifier;
22
+ - `lock_key` - `string` - lock name;
23
+ - `ts` - `integer`/`epoch` - the time when lock was obtained;
24
+ - `acq_time` - `float`/`milliseconds` - time spent on lock acquiring;
25
+ - `rel_time` - `float`/`milliseconds` - time spent on lock "holding + releasing";
26
+ - `"redis_queued_locks.explicit_lock_release"`
27
+ - an event signalizes about the explicit lock release (invoked via `RedisQueuedLock#unlock`);
28
+ - payload:
29
+ - `at` - `integer`/`epoch` - the time when the lock was released;
30
+ - `rel_time` - `float`/`milliseconds` - time spent on lock releasing;
31
+ - `lock_key` - `string` - released lock (lock name);
32
+ - `lock_key_queue` - `string` - released lock queue (lock queue name);
33
+ - `"redis_queued_locks.explicit_all_locks_release"`
34
+ - an event signalizes about the explicit all locks release (invoked viaa `RedisQueuedLock#clear_locks`);
35
+ - payload:
36
+ - `rel_time` - `float`/`milliseconds` - time when the locks was released
37
+ - `at` - `integer`/`epoch` - the time when the operation has ended;
38
+ - `rel_lock_cnt` - `integer` - released lock count;
39
+ - `rel_queue_cnt` - `integer` - released queue count;
40
+
41
+ ## Todo
42
+
43
+ - CI (github actions);
@@ -8,28 +8,40 @@ module RedisQueuedLocks::Acquier::Release
8
8
  # @param redis [RedisClient]
9
9
  # @param lock_key [String]
10
10
  # @param lock_key_queue [String]
11
- # @return [void]
11
+ # @return [Hash<Symbol,Any>] Format: { ok: true/false, result: Any }
12
12
  #
13
13
  # @api private
14
14
  # @since 0.1.0
15
15
  def fully_release_lock(redis, lock_key, lock_key_queue)
16
- redis.multi do |transact|
16
+ result = redis.multi do |transact|
17
17
  transact.call('ZREMRANGEBYSCORE', lock_key_queue, '-inf', '+inf')
18
18
  transact.call('EXPIRE', lock_key, '0')
19
19
  end
20
+
21
+ { ok: true, result: }
20
22
  end
21
23
 
22
24
  # Release all locks: clear all lock queus and expire all locks.
23
25
  #
24
26
  # @param redis [RedisClient]
25
27
  # @param batch_size [Integer]
26
- # @return [void]
28
+ # @return [Hash<Symbol,Any>] Format: { ok: true/false, result: Any }
27
29
  #
28
30
  # @api private
29
31
  # @since 0.1.0
30
32
  def fully_release_all_locks(redis, batch_size)
33
+ rel_queue_cnt = 0
34
+ rel_lock_cnt = 0
35
+
31
36
  # Step A: release all queus and their related locks
32
- redis.scan('MATCH', RedisQueuedLocks::Resource::LOCK_QUEUE_PATTERN) do |lock_queue|
37
+ redis.scan(
38
+ 'MATCH',
39
+ RedisQueuedLocks::Resource::LOCK_QUEUE_PATTERN,
40
+ count: batch_size
41
+ ) do |lock_queue|
42
+ rel_queue_cnt += 1
43
+ rel_lock_cnt += 1
44
+
33
45
  redis.pipelined do |pipeline|
34
46
  pipeline.call('ZREMRANGEBYSCORE', lock_queue, '-inf', '+inf')
35
47
  pipeline.call('EXPIRE', RedisQueuedLocks::Resource.lock_key_from_queue(lock_queue))
@@ -38,9 +50,17 @@ module RedisQueuedLocks::Acquier::Release
38
50
 
39
51
  # Step B: release all locks
40
52
  redis.pipelined do |pipeline|
41
- redis.scan('MATCH', RedisQueuedLocks::Resource::LOCK_PATTERN) do |lock_key|
53
+ rel_lock_cnt += 1
54
+
55
+ redis.scan(
56
+ 'MATCH',
57
+ RedisQueuedLocks::Resource::LOCK_PATTERN,
58
+ count: batch_size
59
+ ) do |lock_key|
42
60
  pipeline.call('EXPIRE', lock_key, '0')
43
61
  end
44
62
  end
63
+
64
+ { ok: true, result: { rel_queue_cnt:, rel_lock_cnt: } }
45
65
  end
46
66
  end
@@ -14,7 +14,7 @@ module RedisQueuedLocks::Acquier::Try
14
14
  #
15
15
  # @api private
16
16
  # @since 0.1.0
17
- # rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity
17
+ # rubocop:disable Metrics/MethodLength
18
18
  def try_to_lock(redis, lock_key, lock_key_queue, acquier_id, acquier_position, ttl, queue_ttl)
19
19
  # Step X: intermediate invocation results
20
20
  inter_result = nil
@@ -111,13 +111,13 @@ module RedisQueuedLocks::Acquier::Try
111
111
  when result == nil || (result.is_a?(::Array) && result.empty?)
112
112
  # Step 7.b: lock is already acquired durign the acquire race => failed to acquire
113
113
  { ok: false, result: :lock_is_acquired_during_acquire_race }
114
- when result.is_a?(::Array) && result.size == 3 # NOTE: 3 is a count of lock commands
114
+ when result.is_a?(::Array) && result.size == 3 # NOTE: 3 is a count of redis lock commands
115
115
  # TODO:
116
116
  # => (!) analyze the command result and do actions with the depending on it;
117
117
  # => (*) at this moment we accept that all comamnds are completed successfully;
118
118
  # => (!) need to analyze:
119
119
  # 1. zpopmin should return our process (array with <acq_id> and <score>)
120
- # 2. hset should return 2 (lock key is added to the redis db with 2 fields)
120
+ # 2. hset should return 2 (lock key is added to the redis as a hashmap with 2 fields)
121
121
  # 3. pexpire should return 1 (expiration time is successfully applied)
122
122
 
123
123
  # Step 7.c: locked! :) let's go! => successfully acquired
@@ -2,6 +2,7 @@
2
2
 
3
3
  # @api private
4
4
  # @since 0.1.0
5
+ # rubocop:disable Metrics/ModuleLength
5
6
  module RedisQueuedLocks::Acquier
6
7
  require_relative 'acquier/try'
7
8
  require_relative 'acquier/delay'
@@ -26,6 +27,7 @@ module RedisQueuedLocks::Acquier
26
27
  # @since 0.1.0
27
28
  REDIS_EXPIRATION_DEVIATION = 2 # NOTE: milliseconds
28
29
 
30
+ # rubocop:disable Metrics/ClassLength
29
31
  class << self
30
32
  # @param redis [RedisClient]
31
33
  # Redis connection client.
@@ -35,10 +37,10 @@ module RedisQueuedLocks::Acquier
35
37
  # The process that want to acquire the lock.
36
38
  # @option thread_id [Integer,String]
37
39
  # The process's thread that want to acquire the lock.
38
- # @option ttl [Integer]
39
- # Lock's time to live (in milliseconds).
40
+ # @option ttl [Integer,NilClass]
41
+ # Lock's time to live (in milliseconds). Nil means "without timeout".
40
42
  # @option queue_ttl [Integer]
41
- # ?
43
+ # Lifetime of the acuier's lock request. In seconds.
42
44
  # @option timeout [Integer]
43
45
  # Time period whe should try to acquire the lock (in seconds).
44
46
  # @option retry_count [Integer]
@@ -47,6 +49,10 @@ module RedisQueuedLocks::Acquier
47
49
  # A time-interval between the each retry (in milliseconds).
48
50
  # @option retry_jitter [Integer]
49
51
  # Time-shift range for retry-delay (in milliseconds).
52
+ # @option raise_errors [Boolean]
53
+ # Raise errors on exceptional cases.
54
+ # @option instrumenter [#notify]
55
+ # See RedisQueuedLocks::Instrument::ActiveSupport for example.
50
56
  # @param [Block]
51
57
  # A block of code that should be executed after the successfully acquired lock.
52
58
  # @return [Hash<Symbol,Any>]
@@ -66,6 +72,8 @@ module RedisQueuedLocks::Acquier
66
72
  retry_count:,
67
73
  retry_delay:,
68
74
  retry_jitter:,
75
+ raise_errors:,
76
+ instrumenter:,
69
77
  &block
70
78
  )
71
79
  # Step 1: prepare lock requirements (generate lock name, calc lock ttl, etc).
@@ -76,11 +84,22 @@ module RedisQueuedLocks::Acquier
76
84
  acquier_position = RedisQueuedLocks::Resource.calc_initial_acquier_position
77
85
 
78
86
  # Step X: intermediate result observer
79
- acq_process = { lock_info: {}, should_try: true, tries: 0, acquired: false, result: nil }
87
+ acq_process = {
88
+ lock_info: {},
89
+ should_try: true,
90
+ tries: 0,
91
+ acquired: false,
92
+ result: nil,
93
+ acq_time: nil, # NOTE: in milliseconds
94
+ hold_time: nil, # NOTE: in milliseconds
95
+ rel_time: nil # NOTE: in milliseconds
96
+ }
80
97
  acq_dequeue = -> { dequeue_from_lock_queue(redis, lock_key_queue, acquier_id) }
81
98
 
82
99
  # Step 2: try to lock with timeout
83
- with_timeout(timeout, lock_key, on_timeout: acq_dequeue) do
100
+ with_timeout(timeout, lock_key, raise_errors, on_timeout: acq_dequeue) do
101
+ acq_start_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
102
+
84
103
  # Step 2.1: caclically try to obtain the lock
85
104
  while acq_process[:should_try]
86
105
  try_to_lock(
@@ -93,15 +112,33 @@ module RedisQueuedLocks::Acquier
93
112
  queue_ttl
94
113
  ) => { ok:, result: }
95
114
 
115
+ acq_end_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
116
+ acq_time = ((acq_end_time - acq_start_time) * 1_000).ceil
117
+
96
118
  # Step X: save the intermediate results to the result observer
97
119
  acq_process[:result] = result
98
120
 
99
121
  # Step 2.1: analyze an acquirement attempt
100
122
  if ok
123
+ # Step X (instrumentation): lock obtained
124
+ instrumenter.notify('redis_queued_locks.lock_obtained', {
125
+ lock_key: result[:lock_key],
126
+ ttl: result[:ttl],
127
+ acq_id: result[:acq_id],
128
+ ts: result[:ts],
129
+ acq_time: acq_time
130
+ })
131
+
101
132
  # Step 2.1.a: successfully acquired => build the result
102
- acq_process[:lock_info] = result
133
+ acq_process[:lock_info] = {
134
+ lock_key: result[:lock_key],
135
+ acq_id: result[:acq_id],
136
+ ts: result[:ts],
137
+ ttl: result[:ttl]
138
+ }
103
139
  acq_process[:acquired] = true
104
140
  acq_process[:should_try] = false
141
+ acq_process[:acq_time] = acq_time
105
142
  else
106
143
  # Step 2.1.b: failed acquirement => retry
107
144
  acq_process[:tries] += 1
@@ -125,7 +162,25 @@ module RedisQueuedLocks::Acquier
125
162
  if acq_process[:acquired]
126
163
  # Step 3.a: acquired successfully => run logic or return the result of acquirement
127
164
  if block_given?
128
- yield_with_expire(redis, lock_key, &block)
165
+ begin
166
+ yield_with_expire(redis, lock_key, instrumenter, &block)
167
+ ensure
168
+ acq_process[:rel_time] = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
169
+ acq_process[:hold_time] = ((
170
+ acq_process[:rel_time] - acq_process[:acq_time]
171
+ ) * 1000).ceil
172
+
173
+ # Step X (instrumentation): lock_hold_and_release
174
+ instrumenter.notify('redis_queued_locks.lock_hold_and_release', {
175
+ hold_time: acq_process[:hold_time],
176
+ rel_time: acq_process[:rel_time],
177
+ ttl: acq_process[:lock_info][:ttl],
178
+ acq_id: acq_process[:lock_info][:acq_id],
179
+ ts: acq_process[:lock_info][:ts],
180
+ lock_key: acq_process[:lock_info][:lock_key],
181
+ acq_time: acq_process[:acq_time]
182
+ })
183
+ end
129
184
  else
130
185
  { ok: true, result: acq_process[:lock_info] }
131
186
  end
@@ -146,15 +201,28 @@ module RedisQueuedLocks::Acquier
146
201
  #
147
202
  # @param redis [RedisClient] Redis connection client.
148
203
  # @param lock_name [String] The lock name that should be released.
204
+ # @param isntrumenter [#notify] See RedisQueuedLocks::Instrument::ActiveSupport for example.
149
205
  # @return [Hash<Symbol,Any>] Format: { ok: true/false, result: Any }
150
206
  #
151
207
  # @api private
152
208
  # @since 0.1.0
153
- def release_lock!(redis, lock_name)
209
+ def release_lock!(redis, lock_name, instrumenter)
154
210
  lock_key = RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
155
211
  lock_key_queue = RedisQueuedLocks::Resource.prepare_lock_queue(lock_name)
156
212
 
213
+ rel_start_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
157
214
  result = fully_release_lock(redis, lock_key, lock_key_queue)
215
+ time_at = Time.now.to_i
216
+ rel_end_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
217
+ rel_time = ((rel_end_time - rel_start_time) * 1_000).ceil
218
+
219
+ instrumenter.notify('redis_queued_locks.explicit_lock_release', {
220
+ lock_key: lock_key,
221
+ lock_key_queue: lock_key_queue,
222
+ rel_time: rel_time,
223
+ at: time_at
224
+ })
225
+
158
226
  { ok: true, result: result }
159
227
  end
160
228
 
@@ -164,31 +232,54 @@ module RedisQueuedLocks::Acquier
164
232
  #
165
233
  # @param redis [RedisClient] Redis connection client.
166
234
  # @param batch_size [Integer] The number of lock keys that should be released in a time.
235
+ # @param isntrumenter [#notify] See RedisQueuedLocks::Instrument::ActiveSupport for example.
167
236
  # @return [Hash<Symbol,Any>] Format: { ok: true/false, result: Any }
168
237
  #
169
238
  # @api private
170
239
  # @since 0.1.0
171
- def release_all_locks!(redis, batch_size)
240
+ def release_all_locks!(redis, batch_size, instrumenter)
241
+ rel_start_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
172
242
  result = fully_release_all_locks(redis, batch_size)
243
+ time_at = Time.now.to_i
244
+ rel_end_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
245
+ rel_time = ((rel_end_time - rel_start_time) * 1_000).ceil
246
+
247
+ instrumenter.notify('redis_queued_locks.explicit_all_locks_release', {
248
+ at: time_at,
249
+ rel_time: rel_time,
250
+ rel_lock_cnt: result[:rel_lock_cnt],
251
+ rel_queue_cnt: result[:rel_queue_cnt]
252
+ })
253
+
173
254
  { ok: true, result: result }
174
255
  end
175
256
 
176
257
  private
177
258
 
178
- # @param timeout [Integer] Time period after which the logic will fail with timeout error.
179
- # @param lock_key [String] Lock name.
180
- # @option on_timeout [Proc,NilClass] Callback invoked on Timeout::Error.
259
+ # @param timeout [NilClass,Integer]
260
+ # Time period after which the logic will fail with timeout error.
261
+ # @param lock_key [String]
262
+ # Lock name.
263
+ # @param raise_errors [Boolean]
264
+ # Raise erros on exceptional cases.
265
+ # @option on_timeout [Proc,NilClass]
266
+ # Callback invoked on Timeout::Error.
181
267
  # @return [Any]
182
268
  #
183
269
  # @api private
184
270
  # @since 0.1.0
185
- def with_timeout(timeout, lock_key, on_timeout: nil, &block)
271
+ def with_timeout(timeout, lock_key, raise_errors, on_timeout: nil, &block)
186
272
  ::Timeout.timeout(timeout, &block)
187
273
  rescue ::Timeout::Error
188
274
  on_timeout.call unless on_timeout == nil
189
- raise(RedisQueuedLocks::LockAcquiermentTimeoutError, <<~ERROR_MESSAGE.strip)
190
- Failed to acquire the lock "#{lock_key}" for the given timeout (#{timeout} seconds).
191
- ERROR_MESSAGE
275
+
276
+ if raise_errors
277
+ raise(RedisQueuedLocks::LockAcquiermentTimeoutError, <<~ERROR_MESSAGE.strip)
278
+ Failed to acquire the lock "#{lock_key}" for the given timeout (#{timeout} seconds).
279
+ ERROR_MESSAGE
280
+ end
192
281
  end
193
282
  end
283
+ # rubocop:enable Metrics/ClassLength
194
284
  end
285
+ # rubocop:enable Metrics/ModuleLength
@@ -10,23 +10,25 @@ class RedisQueuedLocks::Client
10
10
  setting :retry_count, 3
11
11
  setting :retry_delay, 200 # NOTE: milliseconds
12
12
  setting :retry_jitter, 50 # NOTE: milliseconds
13
- setting :acquire_timeout, 10 # NOTE: seconds
13
+ setting :default_timeout, 10 # NOTE: seconds
14
14
  setting :exp_precision, 1 # NOTE: milliseconds
15
15
  setting :default_lock_ttl, 10_000 # NOTE: milliseconds
16
16
  setting :default_queue_ttl, 5 # NOTE: seconds
17
17
  setting :lock_release_batch_size, 100
18
+ setting :instrumenter, RedisQueuedLocks::Instrument::VoidNotifier
18
19
 
19
20
  # TODO: setting :logger, Logger.new(IO::NULL)
20
21
  # TODO: setting :debug, true/false
21
22
 
22
- validate 'retry_count', :integer
23
- validate 'retry_delay', :integer
24
- validate 'retry_jitter', :integer
25
- validate 'acquire_timeout', :integer
26
- validate 'exp_precision', :integer
27
- validate 'default_lock_tt', :integer
28
- validate 'default_queue_ttl', :integer
29
- validate 'lock_release_batch_size', :integer
23
+ validate('retry_count', :integer)
24
+ validate('retry_delay', :integer)
25
+ validate('retry_jitter', :integer)
26
+ validate('default_timeout', :integer)
27
+ validate('exp_precision', :integer)
28
+ validate('default_lock_tt', :integer)
29
+ validate('default_queue_ttl', :integer)
30
+ validate('lock_release_batch_size', :integer)
31
+ validate('instrumenter') { |instr| RedisQueuedLocks::Instrument.valid_interface?(instr) }
30
32
  end
31
33
 
32
34
  # @return [RedisClient]
@@ -58,15 +60,19 @@ class RedisQueuedLocks::Client
58
60
  # @option ttl [Integer]
59
61
  # Lock's time to live (in milliseconds).
60
62
  # @option queue_ttl [Integer]
61
- # ?
62
- # @option timeout [Integer]
63
- # Time period whe should try to acquire the lock (in seconds).
63
+ # Lifetime of the acuier's lock request. In seconds.
64
+ # @option timeout [Integer,NilClass]
65
+ # Time period whe should try to acquire the lock (in seconds). Nil means "without timeout".
64
66
  # @option retry_count [Integer]
65
67
  # How many times we should try to acquire a lock.
66
68
  # @option retry_delay [Integer]
67
69
  # A time-interval between the each retry (in milliseconds).
68
70
  # @option retry_jitter [Integer]
69
71
  # Time-shift range for retry-delay (in milliseconds).
72
+ # @option instrumenter [#notify]
73
+ # See RedisQueuedLocks::Instrument::ActiveSupport for example.
74
+ # @option raise_errors [Boolean]
75
+ # Raise errors on library-related limits such as timeout or failed lock obtain.
70
76
  # @param [Block]
71
77
  # A block of code that should be executed after the successfully acquired lock.
72
78
  # @return [Hash<Symbol,Any>]
@@ -80,10 +86,11 @@ class RedisQueuedLocks::Client
80
86
  thread_id: RedisQueuedLocks::Resource.get_thread_id,
81
87
  ttl: config[:default_lock_ttl],
82
88
  queue_ttl: config[:default_queue_ttl],
83
- timeout: config[:acquire_timeout],
89
+ timeout: config[:default_timeout],
84
90
  retry_count: config[:retry_count],
85
91
  retry_delay: config[:retry_delay],
86
92
  retry_jitter: config[:retry_jitter],
93
+ raise_errors: false,
87
94
  &block
88
95
  )
89
96
  RedisQueuedLocks::Acquier.acquire_lock!(
@@ -97,24 +104,66 @@ class RedisQueuedLocks::Client
97
104
  retry_count:,
98
105
  retry_delay:,
99
106
  retry_jitter:,
107
+ raise_errors:,
108
+ instrumenter: config[:instrumenter],
109
+ &block
110
+ )
111
+ end
112
+
113
+ # @note See #lock method signature.
114
+ #
115
+ # @api public
116
+ # @since 0.1.0
117
+ def lock!(
118
+ lock_name,
119
+ process_id: RedisQueuedLocks::Resource.get_process_id,
120
+ thread_id: RedisQueuedLocks::Resource.get_thread_id,
121
+ ttl: config[:default_lock_ttl],
122
+ queue_ttl: config[:default_queue_ttl],
123
+ timeout: config[:default_timeout],
124
+ retry_count: config[:retry_count],
125
+ retry_delay: config[:retry_delay],
126
+ retry_jitter: config[:retry_jitter],
127
+ &block
128
+ )
129
+ lock(
130
+ lock_name,
131
+ process_id:,
132
+ thread_id:,
133
+ ttl:,
134
+ queue_ttl:,
135
+ timeout:,
136
+ retry_count:,
137
+ retry_delay:,
138
+ retry_jitter:,
139
+ raise_errors: true,
100
140
  &block
101
141
  )
102
142
  end
103
143
 
104
144
  # @param lock_name [String] The lock name that should be released.
105
- # @return [?]
145
+ # @return [Hash<Symbol,Any>] Format: { ok: true/false, result: Symbol/Hash }.
106
146
  #
107
147
  # @api public
108
148
  # @since 0.1.0
109
149
  def unlock(lock_name)
110
- RedisQueuedLocks::Acquier.release_lock!(redis_client, lock_name)
150
+ RedisQueuedLocks::Acquier.release_lock!(
151
+ redis_client,
152
+ lock_name,
153
+ config[:instrumenter]
154
+ )
111
155
  end
112
156
 
113
- # @return [?]
157
+ # @option batch_size [Integer]
158
+ # @return [Hash<Symbol,Any>] Format: { ok: true/false, result: Symbol/Hash }.
114
159
  #
115
160
  # @api public
116
161
  # @since 0.1.0
117
162
  def clear_locks(batch_size: config[:lock_release_batch_size])
118
- RedisQueuedLocks::Acquier.release_all_locks!(redis_client, batch_size)
163
+ RedisQueuedLocks::Acquier.release_all_locks!(
164
+ redis_client,
165
+ batch_size,
166
+ config[:instrumenter]
167
+ )
119
168
  end
120
169
  end
@@ -10,7 +10,7 @@ module RedisQueuedLocks::Debugger
10
10
  # @api private
11
11
  # @since 0.1.0
12
12
  DEBUG_ENABLED_METHOD = <<~METHOD_DECLARATION.strip.freeze
13
- def debug(message) = STDOUT.write_nonblock("#\{message}\n")
13
+ def debug(message) = STDOUT.write("#\{message}\n")
14
14
  METHOD_DECLARATION
15
15
 
16
16
  # @return [String]
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api public
4
+ # @since 0.1.0
5
+ module RedisQueuedLocks::Instrument::ActiveSupport
6
+ class << self
7
+ # @param event [String]
8
+ # @param payload [Hash<String|Symbol,Any>]
9
+ # @return [void]
10
+ #
11
+ # @api private
12
+ # @since 0.1.0
13
+ def notify(event, payload = {})
14
+ ::ActiveSupport::Notifications.instrument(event, payload)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api public
4
+ # @since 0.1.0
5
+ module RedisQueuedLocks::Instrument::VoidNotifier
6
+ class << self
7
+ # @param event [String]
8
+ # @param payload [Hash<String|Symbol,Any>]
9
+ # @return [void]
10
+ #
11
+ # @api private
12
+ # @since 0.1.0
13
+ def notify(event, payload = {}); end
14
+ end
15
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api public
4
+ # @since 0.1.0
5
+ module RedisQueuedLocks::Instrument
6
+ require_relative 'instrument/void_notifier'
7
+ require_relative 'instrument/active_support'
8
+
9
+ class << self
10
+ # @param instrumenter [Class,Module,Object]
11
+ # @return [Boolean]
12
+ #
13
+ # @api public
14
+ # @since 0.1.0
15
+ def valid_interface?(instrumenter)
16
+ if instrumenter == RedisQueuedLocks::Instrument::ActiveSupport
17
+ # NOTE: active_support should be required in your app
18
+ defined?(::ActiveSupport::Notifications)
19
+ elsif instrumenter.respond_to?(:notify)
20
+ # NOTE: the method signature should be (event, payload). Supported variants:
21
+ # => [[:req, :event], [:req, :payload]]
22
+ # => [[:req, :event], [:opt, :payload]]
23
+ # => [[:opt, :event], [:opt, :payload]]
24
+
25
+ m_obj = instrumenter.method(:notify)
26
+ m_sig = m_obj.parameters
27
+
28
+ f_prm = m_sig[0][0]
29
+ s_prm = m_sig[1][0]
30
+
31
+ if m_sig.size == 2
32
+ # rubocop:disable Layout/MultilineOperationIndentation
33
+ # NOTE: check the signature vairants
34
+ f_prm == :req && s_prm == :req ||
35
+ f_prm == :req && s_prm == :opt ||
36
+ f_prm == :opt && s_prm == :opt
37
+ # rubocop:enable Layout/MultilineOperationIndentation
38
+ else
39
+ # NOTE: incompatible signature
40
+ false
41
+ end
42
+ else
43
+ # NOTE: no required method :notify
44
+ false
45
+ end
46
+ end
47
+ end
48
+ end
@@ -4,6 +4,6 @@ module RedisQueuedLocks
4
4
  # @return [String]
5
5
  #
6
6
  # @api public
7
- # @since 0.0.0
8
- VERSION = '0.0.0'
7
+ # @since 0.0.2
8
+ VERSION = '0.0.2'
9
9
  end
@@ -12,7 +12,7 @@ module RedisQueuedLocks
12
12
  require_relative 'redis_queued_locks/debugger'
13
13
  require_relative 'redis_queued_locks/resource'
14
14
  require_relative 'redis_queued_locks/acquier'
15
- require_relative 'redis_queued_locks/instrumentation'
15
+ require_relative 'redis_queued_locks/instrument'
16
16
  require_relative 'redis_queued_locks/client'
17
17
 
18
18
  # @since 0.1.0
@@ -3,7 +3,7 @@
3
3
  require_relative 'lib/redis_queued_locks/version'
4
4
 
5
5
  Gem::Specification.new do |spec|
6
- spec.required_ruby_version = '>= 3.2.0'
6
+ spec.required_ruby_version = '>= 3.1'
7
7
 
8
8
  spec.name = 'redis_queued_locks'
9
9
  spec.version = RedisQueuedLocks::VERSION
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redis_queued_locks
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rustam Ibragimov
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-02-25 00:00:00.000000000 Z
11
+ date: 2024-02-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis-client
@@ -64,7 +64,9 @@ files:
64
64
  - lib/redis_queued_locks/debugger.rb
65
65
  - lib/redis_queued_locks/debugger/interface.rb
66
66
  - lib/redis_queued_locks/errors.rb
67
- - lib/redis_queued_locks/instrumentation.rb
67
+ - lib/redis_queued_locks/instrument.rb
68
+ - lib/redis_queued_locks/instrument/active_support.rb
69
+ - lib/redis_queued_locks/instrument/void_notifier.rb
68
70
  - lib/redis_queued_locks/resource.rb
69
71
  - lib/redis_queued_locks/version.rb
70
72
  - redis_queued_locks.gemspec
@@ -83,14 +85,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
83
85
  requirements:
84
86
  - - ">="
85
87
  - !ruby/object:Gem::Version
86
- version: 3.2.0
88
+ version: '3.1'
87
89
  required_rubygems_version: !ruby/object:Gem::Requirement
88
90
  requirements:
89
91
  - - ">="
90
92
  - !ruby/object:Gem::Version
91
93
  version: '0'
92
94
  requirements: []
93
- rubygems_version: 3.5.3
95
+ rubygems_version: 3.5.1
94
96
  signing_key:
95
97
  specification_version: 4
96
98
  summary: Queued distributed locks based on Redis.
@@ -1,6 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # @api private
4
- # @since 0.1.0
5
- module RedisQueuedLocks::Instrumentation
6
- end