redis_queued_locks 0.0.1 → 0.0.3

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: 7f78a90d3830a3797bfd973b8ce48a879ba8400c3daec8827c0c14c25c650bb3
4
- data.tar.gz: a8e09fae141ff16d142234a4200c86f05abea787fb627e23b4187dd11fa06086
3
+ metadata.gz: 5dbd11cefb9d18a816eb4c63fe1178f87325c30d75efc2bc806d29f41994fc2c
4
+ data.tar.gz: 9697f4bf452aaaa7cade45320c50d16b510c657074ba97513c5f8de9c51f6c49
5
5
  SHA512:
6
- metadata.gz: fe28a66ec0d028453a0e43f2e140b907a100a70b8a547af8067f45f60392339dd9d0540ef9b8638d0bc9588dd0152f4e21ac488d031170cb872679c7ed4bc34a
7
- data.tar.gz: 6608fc1f6a9b2b2000254ba0b4a39d5d16c50f9265bcf58c7b138066e08c29c32d31047e3419db09aa0bfda88c09af10111254c3bf2ba35df775248f2d732d63
6
+ metadata.gz: b28ef92375c7eef4d6c43fccb5e44105d653fb00e052edcf67316f8da13578ad7b39086b3799782592b38bf046760f8faa1238b92de293b83eb1e4a7c441413e
7
+ data.tar.gz: dfd3891ae8d0303b88ac95268056addc3b4e03f862a5200166de9553b412eee78b77f42193f133f9dd340793b67928806e2e05451517585c2282fb7e4c871d1f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.0.3] - 2024-02-26
4
+ ### Changed
5
+ - Instrumentation events:
6
+ - `"redis_queued_locks.explicit_all_locks_release"`
7
+ - re-factored with fully pipelined invocation;
8
+ - removed `rel_queue_cnt` and `rel_lock_cnt` because of the pipelined invocation
9
+ misses the concrete results and now we can receive only "released redis keys count";
10
+ - adde `rel_keys` payload data (released redis keys);
11
+
12
+ ## [0.0.2] - 2024-02-26
13
+ ### Added
14
+ - Instrumentation events:
15
+ - `"redis_queued_locks.lock_obtained"`;
16
+ - `"redis_queued_locks.lock_hold_and_release"`;
17
+ - `"redis_queued_locks.explicit_lock_release"`;
18
+ - `"redis_queued_locks.explicit_all_locks_release"`;
19
+
3
20
  ## [0.0.1] - 2024-02-26
4
21
 
5
22
  - Still the initial release version;
data/README.md CHANGED
@@ -5,14 +5,33 @@ Distributed lock implementation with "lock acquisition queue" capabilities based
5
5
  ## Instrumentation events
6
6
 
7
7
  - `"redis_queued_locks.lock_obtained"`
8
- - the moment when the lock was obtained;
8
+ - a moment when the lock was obtained;
9
9
  - payload:
10
10
  - `ttl` - `integer`/`milliseconds` - lock ttl;
11
11
  - `acq_id` - `string` - lock acquier identifier;
12
- - `lock_key` - `string` - lock name ;
12
+ - `lock_key` - `string` - lock name;
13
13
  - `ts` - `integer`/`epoch` - the time when the lock was obtaiend;
14
14
  - `acq_time` - `float`/`milliseconds` - time spent on lock acquiring;
15
-
16
- ## Todo
17
-
18
- - CI (github actions);
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
+ - `"redis_queued_locks.explicit_lock_release"`
26
+ - an event signalizes about the explicit lock release (invoked via `RedisQueuedLock#unlock`);
27
+ - payload:
28
+ - `at` - `integer`/`epoch` - the time when the lock was released;
29
+ - `rel_time` - `float`/`milliseconds` - time spent on lock releasing;
30
+ - `lock_key` - `string` - released lock (lock name);
31
+ - `lock_key_queue` - `string` - released lock queue (lock queue name);
32
+ - `"redis_queued_locks.explicit_all_locks_release"`
33
+ - an event signalizes about the explicit all locks release (invoked via `RedisQueuedLock#clear_locks`);
34
+ - payload:
35
+ - `rel_time` - `float`/`milliseconds` - time spent on the lock release;
36
+ - `at` - `integer`/`epoch` - the time when the operation has ended;
37
+ - `rel_keys` - `integer` - released redis keys count (`released queu keys` + `released lock keys`);
@@ -3,6 +3,8 @@
3
3
  # @api private
4
4
  # @since 0.1.0
5
5
  module RedisQueuedLocks::Acquier::Delay
6
+ # Sleep with random time-shifting (it is necessary for empty-time-slot lock acquirement).
7
+ #
6
8
  # @param retry_delay [Integer] In milliseconds
7
9
  # @param retry_jitter [Integer] In milliseconds
8
10
  # @return [void]
@@ -3,10 +3,10 @@
3
3
  # @api private
4
4
  # @since 0.1.0
5
5
  module RedisQueuedLocks::Acquier::Expire
6
- # @param redis [RedisClient]
7
- # @param lock_key [String]
8
- # @param block [Block]
9
- # @return [void]
6
+ # @param redis [RedisClient] Redis connection manager.
7
+ # @param lock_key [String] Lock key to be expired.
8
+ # @param block [Block] Custom logic that should be invoked unter the obtained lock.
9
+ # @return [Any,NilClass] nil is returned no block parametr is provided.
10
10
  #
11
11
  # @api private
12
12
  # @since 0.1.0
@@ -8,47 +8,53 @@ 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)
31
- # Step A: release all queus and their related locks
32
- redis.scan(
33
- 'MATCH',
34
- RedisQueuedLocks::Resource::LOCK_QUEUE_PATTERN,
35
- count: batch_size
36
- ) do |lock_queue|
37
- redis.pipelined do |pipeline|
33
+ result = redis.pipelined do |pipeline|
34
+ # Step A: release all queus and their related locks
35
+ redis.scan(
36
+ 'MATCH',
37
+ RedisQueuedLocks::Resource::LOCK_QUEUE_PATTERN,
38
+ count: batch_size
39
+ ) do |lock_queue|
40
+ puts "RELEASE (lock_queue): #{lock_queue}"
38
41
  pipeline.call('ZREMRANGEBYSCORE', lock_queue, '-inf', '+inf')
39
- pipeline.call('EXPIRE', RedisQueuedLocks::Resource.lock_key_from_queue(lock_queue))
42
+ pipeline.call('EXPIRE', RedisQueuedLocks::Resource.lock_key_from_queue(lock_queue), "0")
40
43
  end
41
- end
42
44
 
43
- # Step B: release all locks
44
- redis.pipelined do |pipeline|
45
+ # Step B: release all locks
45
46
  redis.scan(
46
47
  'MATCH',
47
48
  RedisQueuedLocks::Resource::LOCK_PATTERN,
48
49
  count: batch_size
49
50
  ) do |lock_key|
51
+ puts "RELEASE (lock_key): #{lock_key}"
50
52
  pipeline.call('EXPIRE', lock_key, '0')
51
53
  end
52
54
  end
55
+
56
+ rel_keys = result.count { |red_res| red_res == 0 }
57
+
58
+ { ok: true, result: { rel_keys: rel_keys } }
53
59
  end
54
60
  end
@@ -37,14 +37,14 @@ module RedisQueuedLocks::Acquier
37
37
  # The process that want to acquire the lock.
38
38
  # @option thread_id [Integer,String]
39
39
  # The process's thread that want to acquire the lock.
40
- # @option ttl [Integer]
41
- # Lock's time to live (in milliseconds).
40
+ # @option ttl [Integer,NilClass]
41
+ # Lock's time to live (in milliseconds). Nil means "without timeout".
42
42
  # @option queue_ttl [Integer]
43
- # ?
43
+ # Lifetime of the acuier's lock request. In seconds.
44
44
  # @option timeout [Integer]
45
45
  # Time period whe should try to acquire the lock (in seconds).
46
- # @option retry_count [Integer]
47
- # How many times we should try to acquire a lock.
46
+ # @option retry_count [Integer,NilClass]
47
+ # How many times we should try to acquire a lock. Nil means "infinite retries".
48
48
  # @option retry_delay [Integer]
49
49
  # A time-interval between the each retry (in milliseconds).
50
50
  # @option retry_jitter [Integer]
@@ -90,7 +90,9 @@ module RedisQueuedLocks::Acquier
90
90
  tries: 0,
91
91
  acquired: false,
92
92
  result: nil,
93
- acq_time: nil # NOTE: in milliseconds
93
+ acq_time: nil, # NOTE: in milliseconds
94
+ hold_time: nil, # NOTE: in milliseconds
95
+ rel_time: nil # NOTE: in milliseconds
94
96
  }
95
97
  acq_dequeue = -> { dequeue_from_lock_queue(redis, lock_key_queue, acquier_id) }
96
98
 
@@ -111,14 +113,14 @@ module RedisQueuedLocks::Acquier
111
113
  ) => { ok:, result: }
112
114
 
113
115
  acq_end_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
114
- acq_time = ((acq_end_time - acq_start_time) * 1_000).ceil
116
+ acq_time = ((acq_end_time - acq_start_time) * 1_000).ceil(2)
115
117
 
116
118
  # Step X: save the intermediate results to the result observer
117
119
  acq_process[:result] = result
118
120
 
119
121
  # Step 2.1: analyze an acquirement attempt
120
122
  if ok
121
- # INSTRUMENT: lock obtained
123
+ # Step X (instrumentation): lock obtained
122
124
  instrumenter.notify('redis_queued_locks.lock_obtained', {
123
125
  lock_key: result[:lock_key],
124
126
  ttl: result[:ttl],
@@ -137,20 +139,20 @@ module RedisQueuedLocks::Acquier
137
139
  acq_process[:acquired] = true
138
140
  acq_process[:should_try] = false
139
141
  acq_process[:acq_time] = acq_time
142
+ acq_process[:acq_end_time] = acq_end_time
140
143
  else
141
144
  # Step 2.1.b: failed acquirement => retry
142
145
  acq_process[:tries] += 1
143
146
 
144
- if acq_process[:tries] >= retry_count
147
+ if retry_count != nil && acq_process[:tries] >= retry_count
145
148
  # NOTE: reached the retry limit => quit from the loop
146
149
  acq_process[:should_try] = false
147
150
  # NOTE: reached the retry limit => dequeue from the lock queue
148
151
  acq_dequeue.call
149
- else
152
+ elsif delay_execution(retry_delay, retry_jitter)
150
153
  # NOTE:
151
154
  # delay the exceution in order to prevent chaotic attempts
152
155
  # and to allow other processes and threads to obtain the lock too.
153
- delay_execution(retry_delay, retry_jitter)
154
156
  end
155
157
  end
156
158
  end
@@ -160,7 +162,26 @@ module RedisQueuedLocks::Acquier
160
162
  if acq_process[:acquired]
161
163
  # Step 3.a: acquired successfully => run logic or return the result of acquirement
162
164
  if block_given?
163
- yield_with_expire(redis, lock_key, &block) # INSTRUMENT: lock release
165
+ begin
166
+ yield_with_expire(redis, lock_key, &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_end_time]) * 1000
171
+ ).ceil(2)
172
+
173
+ # Step X (instrumentation): lock_hold_and_release
174
+ run_non_critical do
175
+ instrumenter.notify('redis_queued_locks.lock_hold_and_release', {
176
+ hold_time: acq_process[:hold_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
184
+ end
164
185
  else
165
186
  { ok: true, result: acq_process[:lock_info] }
166
187
  end
@@ -181,16 +202,30 @@ module RedisQueuedLocks::Acquier
181
202
  #
182
203
  # @param redis [RedisClient] Redis connection client.
183
204
  # @param lock_name [String] The lock name that should be released.
205
+ # @param isntrumenter [#notify] See RedisQueuedLocks::Instrument::ActiveSupport for example.
184
206
  # @return [Hash<Symbol,Any>] Format: { ok: true/false, result: Any }
185
207
  #
186
208
  # @api private
187
209
  # @since 0.1.0
188
- def release_lock!(redis, lock_name)
210
+ def release_lock!(redis, lock_name, instrumenter)
189
211
  lock_key = RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
190
212
  lock_key_queue = RedisQueuedLocks::Resource.prepare_lock_queue(lock_name)
191
213
 
192
- # INSTRUMENT: lock release
193
- result = fully_release_lock(redis, lock_key, lock_key_queue)
214
+ rel_start_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
215
+ fully_release_lock(redis, lock_key, lock_key_queue) => { ok:, result: }
216
+ time_at = Time.now.to_i
217
+ rel_end_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
218
+ rel_time = ((rel_end_time - rel_start_time) * 1_000).ceil(2)
219
+
220
+ run_non_critical do
221
+ instrumenter.notify('redis_queued_locks.explicit_lock_release', {
222
+ lock_key: lock_key,
223
+ lock_key_queue: lock_key_queue,
224
+ rel_time: rel_time,
225
+ at: time_at
226
+ })
227
+ end
228
+
194
229
  { ok: true, result: result }
195
230
  end
196
231
 
@@ -200,13 +235,26 @@ module RedisQueuedLocks::Acquier
200
235
  #
201
236
  # @param redis [RedisClient] Redis connection client.
202
237
  # @param batch_size [Integer] The number of lock keys that should be released in a time.
238
+ # @param isntrumenter [#notify] See RedisQueuedLocks::Instrument::ActiveSupport for example.
203
239
  # @return [Hash<Symbol,Any>] Format: { ok: true/false, result: Any }
204
240
  #
205
241
  # @api private
206
242
  # @since 0.1.0
207
- def release_all_locks!(redis, batch_size)
208
- # INSTRUMENT: all locks released
209
- result = fully_release_all_locks(redis, batch_size)
243
+ def release_all_locks!(redis, batch_size, instrumenter)
244
+ rel_start_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
245
+ fully_release_all_locks(redis, batch_size) => { ok:, result: }
246
+ time_at = Time.now.to_i
247
+ rel_end_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
248
+ rel_time = ((rel_end_time - rel_start_time) * 1_000).ceil(2)
249
+
250
+ run_non_critical do
251
+ instrumenter.notify('redis_queued_locks.explicit_all_locks_release', {
252
+ at: time_at,
253
+ rel_time: rel_time,
254
+ rel_keys: result[:rel_keys]
255
+ })
256
+ end
257
+
210
258
  { ok: true, result: result }
211
259
  end
212
260
 
@@ -235,6 +283,15 @@ module RedisQueuedLocks::Acquier
235
283
  ERROR_MESSAGE
236
284
  end
237
285
  end
286
+
287
+ # @param block [Block]
288
+ # @return [Any]
289
+ #
290
+ # @api private
291
+ # @since 0.1.0
292
+ def run_non_critical(&block)
293
+ yield rescue nil
294
+ end
238
295
  end
239
296
  # rubocop:enable Metrics/ClassLength
240
297
  end
@@ -12,15 +12,15 @@ class RedisQueuedLocks::Client
12
12
  setting :retry_jitter, 50 # NOTE: milliseconds
13
13
  setting :default_timeout, 10 # NOTE: seconds
14
14
  setting :exp_precision, 1 # NOTE: milliseconds
15
- setting :default_lock_ttl, 10_000 # NOTE: milliseconds
16
- setting :default_queue_ttl, 5 # NOTE: seconds
15
+ setting :default_lock_ttl, 5_000 # NOTE: milliseconds
16
+ setting :default_queue_ttl, 30 # NOTE: seconds
17
17
  setting :lock_release_batch_size, 100
18
18
  setting :instrumenter, RedisQueuedLocks::Instrument::VoidNotifier
19
19
 
20
20
  # TODO: setting :logger, Logger.new(IO::NULL)
21
21
  # TODO: setting :debug, true/false
22
22
 
23
- validate('retry_count', :integer)
23
+ validate('retry_count') { |val| val == nil || (val.is_a?(::Integer) && val >= 0) }
24
24
  validate('retry_delay', :integer)
25
25
  validate('retry_jitter', :integer)
26
26
  validate('default_timeout', :integer)
@@ -28,7 +28,7 @@ class RedisQueuedLocks::Client
28
28
  validate('default_lock_tt', :integer)
29
29
  validate('default_queue_ttl', :integer)
30
30
  validate('lock_release_batch_size', :integer)
31
- validate('instrumenter') { |instr| RedisQueuedLocks::Instrument.valid_interface?(instr) }
31
+ validate('instrumenter') { |val| RedisQueuedLocks::Instrument.valid_interface?(val) }
32
32
  end
33
33
 
34
34
  # @return [RedisClient]
@@ -60,11 +60,11 @@ class RedisQueuedLocks::Client
60
60
  # @option ttl [Integer]
61
61
  # Lock's time to live (in milliseconds).
62
62
  # @option queue_ttl [Integer]
63
- # ?
64
- # @option timeout [Integer]
65
- # Time period whe should try to acquire the lock (in seconds).
66
- # @option retry_count [Integer]
67
- # How many times we should try to acquire a lock.
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".
66
+ # @option retry_count [Integer,NilClass]
67
+ # How many times we should try to acquire a lock. Nil means "infinite retries".
68
68
  # @option retry_delay [Integer]
69
69
  # A time-interval between the each retry (in milliseconds).
70
70
  # @option retry_jitter [Integer]
@@ -147,14 +147,23 @@ class RedisQueuedLocks::Client
147
147
  # @api public
148
148
  # @since 0.1.0
149
149
  def unlock(lock_name)
150
- RedisQueuedLocks::Acquier.release_lock!(redis_client, lock_name)
150
+ RedisQueuedLocks::Acquier.release_lock!(
151
+ redis_client,
152
+ lock_name,
153
+ config[:instrumenter]
154
+ )
151
155
  end
152
156
 
157
+ # @option batch_size [Integer]
153
158
  # @return [Hash<Symbol,Any>] Format: { ok: true/false, result: Symbol/Hash }.
154
159
  #
155
160
  # @api public
156
161
  # @since 0.1.0
157
162
  def clear_locks(batch_size: config[:lock_release_batch_size])
158
- 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
+ )
159
168
  end
160
169
  end
@@ -4,6 +4,6 @@ module RedisQueuedLocks
4
4
  # @return [String]
5
5
  #
6
6
  # @api public
7
- # @since 0.0.1
8
- VERSION = '0.0.1'
7
+ # @since 0.0.3
8
+ VERSION = '0.0.3'
9
9
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redis_queued_locks
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rustam Ibragimov