redis_queued_locks 0.0.2 → 0.0.4

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: e984603ea8f509f8e5f7f685c356d1f838b16bc950e1de83ff8f66e6afa83fcf
4
- data.tar.gz: 1b2eee0f32c911caecf08214b2b404d620ffb10501ae72d7721235b64fc02362
3
+ metadata.gz: 6e1e1a54762132f92451d595938cb467b7b644dffc0817abe99209e1f92160f8
4
+ data.tar.gz: 297eec3d7fcee35d12e7f3509e8e3dfd4b8b3c52889c7570a79cd6a30f45eda4
5
5
  SHA512:
6
- metadata.gz: b7e2d574eeeff4a9a235f39ee25bde152880dcd12e7066196a8304e54098480ef05672edd89a6931ece225711b96602f23ce76304c555dbd623d30099e35ca0a
7
- data.tar.gz: d7f5075bbf136e672ebb1632da5c6a78b2b72886bc1d5e8b0d9425cf38b14f5eef5a097fdc835c78f5f2d26885512279926a33bdbdb559151b875453d5312c9e
6
+ metadata.gz: 6d2c64622bd3b8465a749b5f6a8050d5fd1d5b623990f91421f91004c444baab12114964ded28d5ac0fa43fed56f3e893798feb31b00ab173d536e57fcfdfaa4
7
+ data.tar.gz: 02ee9d8742ffd3a7b16b8c23b8d4c4f58a8cb1b9db4d215ece4bf34de0c79830bd3417186b2d75dbbd48f6845d736361af7503f641e7faa16d139cbe193263d3
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.0.4] - 2024-02-26
4
+ ### Changed
5
+ - changed default configuration values of `RedisQueuedLocks::Client` config;
6
+
7
+ ## [0.0.3] - 2024-02-26
8
+ ### Changed
9
+ - Instrumentation events:
10
+ - `"redis_queued_locks.explicit_all_locks_release"`
11
+ - re-factored with fully pipelined invocation;
12
+ - removed `rel_queue_cnt` and `rel_lock_cnt` because of the pipelined invocation
13
+ misses the concrete results and now we can receive only "released redis keys count";
14
+ - adde `rel_keys` payload data (released redis keys);
15
+
3
16
  ## [0.0.2] - 2024-02-26
4
17
  ### Added
5
18
  - Instrumentation events:
data/README.md CHANGED
@@ -2,6 +2,67 @@
2
2
 
3
3
  Distributed lock implementation with "lock acquisition queue" capabilities based on the Redis Database.
4
4
 
5
+ ## Configuration
6
+
7
+ ```ruby
8
+ redis_client = RedisClient.config.new_pool # NOTE: provide your own RedisClient instance
9
+
10
+ clinet = RedisQueuedLocks::Client.new(redis_client) do |config|
11
+ # (default: 3) (supports nil)
12
+ # - nil means "infinite retries" and you are only limited by the "try_to_lock_timeout" config;
13
+ config.retry_count = 3
14
+
15
+ # (milliseconds) (default: 200)
16
+ config.retry_delay = 200
17
+
18
+ # (milliseconds) (default: 50)
19
+ config.retry_jitter = 50
20
+
21
+ # (seconds) (supports nil)
22
+ # - nil means "no timeout" and you are only limited by "retry_count" config;
23
+ config.try_to_lock_timeout = 10
24
+
25
+ # (milliseconds) (default: 1)
26
+ # - expiration precision. affects the ttl (ttl + precision);
27
+ config.exp_precision = 1
28
+
29
+ # (milliseconds) (default: 5_000)
30
+ # - lock's time to live
31
+ config.default_lock_ttl = 5_000
32
+
33
+ # (seconds) (default: 15)
34
+ # - lock request timeout. after this timeout your lock request in queue will be requeued;
35
+ config.default_queue_ttl = 15
36
+
37
+ # (default: 100)
38
+ # - how many items will be released at a time in RedisQueuedLocks::Client#clear_locks logic;
39
+ # - affects the performancs capabilites (redis, rubyvm);
40
+ config.lock_release_batch_size = 100
41
+
42
+ # (default: RedisQueuedLocks::Instrument::VoidNotifier)
43
+ # - instrumentation layer;
44
+ # - you can provde your own instrumenter with `#notify(event, payload = {})`
45
+ # - event: <string> requried;
46
+ # - payload: <hash> requried;
47
+ # - disabled by default via VoidNotifier;
48
+ config.instrumenter = RedisQueuedLocks::Instrument::ActiveSupport
49
+ end
50
+ ```
51
+
52
+ ## Usage
53
+
54
+ ```ruby
55
+ redis_clinet = RedisClient.config.new_pool # NOTE: provide your ounw RedisClient instance
56
+ rq_lock = RedisQueuedLocks::Client.new(redis_client) do |config|
57
+ # NOTE: some your application-related configs
58
+ end
59
+ ```
60
+
61
+ - `#lock`
62
+ - `#lock!`
63
+ - `#unlock`
64
+ - `#clear_locks`
65
+
5
66
  ## Instrumentation events
6
67
 
7
68
  - `"redis_queued_locks.lock_obtained"`
@@ -22,7 +83,6 @@ Distributed lock implementation with "lock acquisition queue" capabilities based
22
83
  - `lock_key` - `string` - lock name;
23
84
  - `ts` - `integer`/`epoch` - the time when lock was obtained;
24
85
  - `acq_time` - `float`/`milliseconds` - time spent on lock acquiring;
25
- - `rel_time` - `float`/`milliseconds` - time spent on lock "holding + releasing";
26
86
  - `"redis_queued_locks.explicit_lock_release"`
27
87
  - an event signalizes about the explicit lock release (invoked via `RedisQueuedLock#unlock`);
28
88
  - payload:
@@ -31,13 +91,8 @@ Distributed lock implementation with "lock acquisition queue" capabilities based
31
91
  - `lock_key` - `string` - released lock (lock name);
32
92
  - `lock_key_queue` - `string` - released lock queue (lock queue name);
33
93
  - `"redis_queued_locks.explicit_all_locks_release"`
34
- - an event signalizes about the explicit all locks release (invoked viaa `RedisQueuedLock#clear_locks`);
94
+ - an event signalizes about the explicit all locks release (invoked via `RedisQueuedLock#clear_locks`);
35
95
  - payload:
36
- - `rel_time` - `float`/`milliseconds` - time when the locks was released
96
+ - `rel_time` - `float`/`milliseconds` - time spent on "realese all locks" operation;
37
97
  - `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);
98
+ - `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
@@ -30,37 +30,31 @@ module RedisQueuedLocks::Acquier::Release
30
30
  # @api private
31
31
  # @since 0.1.0
32
32
  def fully_release_all_locks(redis, batch_size)
33
- rel_queue_cnt = 0
34
- rel_lock_cnt = 0
35
-
36
- # Step A: release all queus and their related locks
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
-
45
- 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}"
46
41
  pipeline.call('ZREMRANGEBYSCORE', lock_queue, '-inf', '+inf')
47
- pipeline.call('EXPIRE', RedisQueuedLocks::Resource.lock_key_from_queue(lock_queue))
42
+ pipeline.call('EXPIRE', RedisQueuedLocks::Resource.lock_key_from_queue(lock_queue), "0")
48
43
  end
49
- end
50
-
51
- # Step B: release all locks
52
- redis.pipelined do |pipeline|
53
- rel_lock_cnt += 1
54
44
 
45
+ # Step B: release all locks
55
46
  redis.scan(
56
47
  'MATCH',
57
48
  RedisQueuedLocks::Resource::LOCK_PATTERN,
58
49
  count: batch_size
59
50
  ) do |lock_key|
51
+ puts "RELEASE (lock_key): #{lock_key}"
60
52
  pipeline.call('EXPIRE', lock_key, '0')
61
53
  end
62
54
  end
63
55
 
64
- { ok: true, result: { rel_queue_cnt:, rel_lock_cnt: } }
56
+ rel_keys = result.count { |red_res| red_res == 0 }
57
+
58
+ { ok: true, result: { rel_keys: rel_keys } }
65
59
  end
66
60
  end
@@ -43,8 +43,8 @@ module RedisQueuedLocks::Acquier
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]
@@ -113,7 +113,7 @@ module RedisQueuedLocks::Acquier
113
113
  ) => { ok:, result: }
114
114
 
115
115
  acq_end_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
116
- acq_time = ((acq_end_time - acq_start_time) * 1_000).ceil
116
+ acq_time = ((acq_end_time - acq_start_time) * 1_000).ceil(2)
117
117
 
118
118
  # Step X: save the intermediate results to the result observer
119
119
  acq_process[:result] = result
@@ -139,20 +139,20 @@ module RedisQueuedLocks::Acquier
139
139
  acq_process[:acquired] = true
140
140
  acq_process[:should_try] = false
141
141
  acq_process[:acq_time] = acq_time
142
+ acq_process[:acq_end_time] = acq_end_time
142
143
  else
143
144
  # Step 2.1.b: failed acquirement => retry
144
145
  acq_process[:tries] += 1
145
146
 
146
- if acq_process[:tries] >= retry_count
147
+ if retry_count != nil && acq_process[:tries] >= retry_count
147
148
  # NOTE: reached the retry limit => quit from the loop
148
149
  acq_process[:should_try] = false
149
150
  # NOTE: reached the retry limit => dequeue from the lock queue
150
151
  acq_dequeue.call
151
- else
152
+ elsif delay_execution(retry_delay, retry_jitter)
152
153
  # NOTE:
153
154
  # delay the exceution in order to prevent chaotic attempts
154
155
  # and to allow other processes and threads to obtain the lock too.
155
- delay_execution(retry_delay, retry_jitter)
156
156
  end
157
157
  end
158
158
  end
@@ -163,23 +163,24 @@ module RedisQueuedLocks::Acquier
163
163
  # Step 3.a: acquired successfully => run logic or return the result of acquirement
164
164
  if block_given?
165
165
  begin
166
- yield_with_expire(redis, lock_key, instrumenter, &block)
166
+ yield_with_expire(redis, lock_key, &block)
167
167
  ensure
168
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
169
+ acq_process[:hold_time] = (
170
+ (acq_process[:rel_time] - acq_process[:acq_end_time]) * 1000
171
+ ).ceil(2)
172
172
 
173
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
- })
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
183
184
  end
184
185
  else
185
186
  { ok: true, result: acq_process[:lock_info] }
@@ -211,17 +212,19 @@ module RedisQueuedLocks::Acquier
211
212
  lock_key_queue = RedisQueuedLocks::Resource.prepare_lock_queue(lock_name)
212
213
 
213
214
  rel_start_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
214
- result = fully_release_lock(redis, lock_key, lock_key_queue)
215
+ fully_release_lock(redis, lock_key, lock_key_queue) => { ok:, result: }
215
216
  time_at = Time.now.to_i
216
217
  rel_end_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
217
- rel_time = ((rel_end_time - rel_start_time) * 1_000).ceil
218
+ rel_time = ((rel_end_time - rel_start_time) * 1_000).ceil(2)
218
219
 
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
- })
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
225
228
 
226
229
  { ok: true, result: result }
227
230
  end
@@ -239,17 +242,18 @@ module RedisQueuedLocks::Acquier
239
242
  # @since 0.1.0
240
243
  def release_all_locks!(redis, batch_size, instrumenter)
241
244
  rel_start_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
242
- result = fully_release_all_locks(redis, batch_size)
245
+ fully_release_all_locks(redis, batch_size) => { ok:, result: }
243
246
  time_at = Time.now.to_i
244
247
  rel_end_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
245
- rel_time = ((rel_end_time - rel_start_time) * 1_000).ceil
248
+ rel_time = ((rel_end_time - rel_start_time) * 1_000).ceil(2)
246
249
 
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
- })
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
253
257
 
254
258
  { ok: true, result: result }
255
259
  end
@@ -279,6 +283,15 @@ module RedisQueuedLocks::Acquier
279
283
  ERROR_MESSAGE
280
284
  end
281
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
282
295
  end
283
296
  # rubocop:enable Metrics/ClassLength
284
297
  end
@@ -10,25 +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 :default_timeout, 10 # NOTE: seconds
13
+ setting :try_to_lock_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, 15 # 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
- validate('default_timeout', :integer)
26
+ validate('try_to_lock_timeout') { |val| val == nil || (val.is_a?(::Integer) && val >= 0) }
27
27
  validate('exp_precision', :integer)
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]
@@ -63,8 +63,8 @@ class RedisQueuedLocks::Client
63
63
  # Lifetime of the acuier's lock request. In seconds.
64
64
  # @option timeout [Integer,NilClass]
65
65
  # Time period whe should try to acquire the lock (in seconds). Nil means "without timeout".
66
- # @option retry_count [Integer]
67
- # How many times we should try to acquire a lock.
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]
@@ -86,7 +86,7 @@ class RedisQueuedLocks::Client
86
86
  thread_id: RedisQueuedLocks::Resource.get_thread_id,
87
87
  ttl: config[:default_lock_ttl],
88
88
  queue_ttl: config[:default_queue_ttl],
89
- timeout: config[:default_timeout],
89
+ timeout: config[:try_to_lock_timeout],
90
90
  retry_count: config[:retry_count],
91
91
  retry_delay: config[:retry_delay],
92
92
  retry_jitter: config[:retry_jitter],
@@ -4,6 +4,7 @@ module RedisQueuedLocks
4
4
  # @return [String]
5
5
  #
6
6
  # @api public
7
- # @since 0.0.2
8
- VERSION = '0.0.2'
7
+ # @since 0.0.1
8
+ # @version 0.0.4
9
+ VERSION = '0.0.4'
9
10
  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.2
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rustam Ibragimov