redis_queued_locks 0.0.1 → 0.0.3

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