redis_queued_locks 0.0.0 → 0.0.2

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: 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