restrainer 1.1.3 → 1.1.5

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: fd2f59427326450c5159980282e4d463d275dac6fceaa2a31423a5433ff1502f
4
- data.tar.gz: ddf65e8c8915e920a9689f78bce879d6feae920740890acc7c3df5f5c8ccb2d4
3
+ metadata.gz: c140a5c4d377fe5df87028599ec86c507d428657d1c44e8456c83fe4b046c815
4
+ data.tar.gz: 5229e54526f187a07266f15e7af265e91f3a5dc691ceaccb3c2628867b060ba8
5
5
  SHA512:
6
- metadata.gz: 4166271f5f617a48286024f8e57ab7ebc662c0edfd357ced9e4e40d3998b975db1abcb4db6a1c8de2922ffc57ace7593e752798f39a6f552659877a146fff1b5
7
- data.tar.gz: 032357eb1992399ba5d10681b7194e667d852ca000b51dcc6ac592f8d10b4734306225208c918d9c3f73faec11adf690a6d0a1ffccefab57deef09438854437a
6
+ metadata.gz: 77692da8fd5fd19ccea93bc3903e2322b00959ad32b20b37c7a84a9d37c4213c20d9b7ce1c541247d7f4c7f1ae8dbc870b3c71814c34a610806b968f4cff20cc
7
+ data.tar.gz: 2e21b607d587007bca24bae0602f7d108e447582510e053ee0fe547e635aa3e6d45224a57320776faac152aedba2cb88d8d7ddd490fc91f52a897230169383e5
data/CHANGELOG.md CHANGED
@@ -4,6 +4,29 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## 1.1.5
8
+
9
+ ### Fixed
10
+
11
+ - Expired process cleanup now uses the Redis server clock instead of the client clock,
12
+ so clock skew between application hosts can no longer reap active locks or allow the
13
+ limit to be exceeded.
14
+ - `release!` now always returns a boolean; previously it could return a truthy `0` with
15
+ Redis clients that return integer replies from `ZREM`.
16
+ - The Lua script SHA1 is now precomputed as a constant. Previously each `Restrainer`
17
+ instance loaded the script on its first lock because the intended class level cache
18
+ was never populated.
19
+ - The sorted set key is now passed to the Lua script via `KEYS` instead of `ARGV`,
20
+ making the script compatible with Redis Cluster slot routing.
21
+ - The `NOSCRIPT` recovery path now retries only once instead of retrying indefinitely.
22
+ - Fixed constructor examples in the README that raised `ArgumentError`.
23
+
24
+ ## 1.1.4
25
+
26
+ ### Removed
27
+
28
+ - Removed unimplemented method stub.
29
+
7
30
  ## 1.1.3
8
31
 
9
32
  ### Added
data/README.md CHANGED
@@ -1,5 +1,4 @@
1
1
  [![Continuous Integration](https://github.com/bdurand/restrainer/actions/workflows/continuous_integration.yml/badge.svg)](https://github.com/bdurand/restrainer/actions/workflows/continuous_integration.yml)
2
- [![Regression Test](https://github.com/bdurand/restrainer/actions/workflows/regression_test.yml/badge.svg)](https://github.com/bdurand/restrainer/actions/workflows/regression_test.yml)
3
2
  [![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)
4
3
  [![Gem Version](https://badge.fury.io/rb/restrainer.svg)](https://badge.fury.io/rb/restrainer)
5
4
 
@@ -50,7 +49,7 @@ Restrainer.redis = redis_client
50
49
  You can also pass in a `Redis` instance in the constructor.
51
50
 
52
51
  ```ruby
53
- restrainer = Restrainer.new(limit: 5, redis: my_redis)
52
+ restrainer = Restrainer.new(:my_service, limit: 5, redis: my_redis)
54
53
  ```
55
54
 
56
55
  ### Internals
@@ -60,7 +59,7 @@ To protect against situations where a process is killed without a chance to clea
60
59
  The timeout can be set by the timeout option on the constructor. If you have any timeouts set on the services being called in the block, you should set the Restrainer timeout to a slightly higher value.
61
60
 
62
61
  ```ruby
63
- restrainer = Restrainer.new(:my_service, 100, timeout: 10)
62
+ restrainer = Restrainer.new(:my_service, limit: 100, timeout: 10)
64
63
  ```
65
64
 
66
65
  This gem does clean up after itself nicely, so that it won't ever leave unused data lying around in redis.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.1.3
1
+ 1.1.5
data/lib/restrainer.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "digest"
3
4
  require "redis"
4
5
  require "securerandom"
5
6
 
@@ -17,12 +18,17 @@ class Restrainer
17
18
  attr_reader :name, :limit
18
19
 
19
20
  ADD_PROCESS_SCRIPT = <<-LUA
21
+ redis.replicate_commands()
22
+
20
23
  -- Parse arguments
21
- local sorted_set = ARGV[1]
22
- local process_id = ARGV[2]
23
- local limit = tonumber(ARGV[3])
24
- local ttl = tonumber(ARGV[4])
25
- local now = tonumber(ARGV[5])
24
+ local sorted_set = KEYS[1]
25
+ local process_id = ARGV[1]
26
+ local limit = tonumber(ARGV[2])
27
+ local ttl = tonumber(ARGV[3])
28
+
29
+ -- Use the Redis server clock so all clients share a consistent view of time.
30
+ local time = redis.call('time')
31
+ local now = tonumber(time[1]) + (tonumber(time[2]) / 1000000)
26
32
 
27
33
  -- Get count of current processes. If more than the max, check if any of the processes have timed out
28
34
  -- and try again.
@@ -45,8 +51,9 @@ class Restrainer
45
51
  return process_count
46
52
  LUA
47
53
 
48
- # This class level variable will be used to load the SHA1 of the script at runtime.
49
- @add_process_sha1 = nil
54
+ # SHA1 digest of the script used to call it with EVALSHA. The script itself is
55
+ # only loaded to the server if it isn't already cached there.
56
+ ADD_PROCESS_SCRIPT_SHA1 = Digest::SHA1.hexdigest(ADD_PROCESS_SCRIPT).freeze
50
57
 
51
58
  # This error will be thrown when the throttle block can't be executed.
52
59
  class ThrottledError < StandardError
@@ -67,9 +74,9 @@ class Restrainer
67
74
  if block
68
75
  @redis = block
69
76
  else
70
- unless @redis
77
+ @redis ||= begin
71
78
  client = Redis.new
72
- @redis = lambda { client }
79
+ lambda { client }
73
80
  end
74
81
  @redis.call
75
82
  end
@@ -113,7 +120,7 @@ class Restrainer
113
120
  #
114
121
  # @param limit [Integer] The maximum number of processes that can be executing
115
122
  # at any one time. Defaults to the value passed to the constructor.
116
- # @return [void]
123
+ # @return [Object] The value returned by the block.
117
124
  # @raise [Restrainer::ThrottledError] If the throttle would be exceeded.
118
125
  def throttle(limit: nil)
119
126
  limit ||= self.limit
@@ -137,7 +144,8 @@ class Restrainer
137
144
  # passed, a unique value will be generated.
138
145
  # @param limit [Integer] The maximum number of processes that can be executing
139
146
  # at any one time. Defaults to the value passed to the constructor.
140
- # @return [String] The process identifier.
147
+ # @return [String, nil] The process identifier, or nil if the limit is negative
148
+ # (i.e. the throttle is wide open and there is nothing to release).
141
149
  # @raise [Restrainer::ThrottledError] If the throttle would be exceeded.
142
150
  def lock!(process_id = nil, limit: nil)
143
151
  process_id ||= SecureRandom.uuid
@@ -151,9 +159,6 @@ class Restrainer
151
159
  process_id
152
160
  end
153
161
 
154
- def lock(process_id = nil, limit: nil)
155
- end
156
-
157
162
  # Release one of the allowed processes. You must pass in a process id
158
163
  # returned by the lock method.
159
164
  #
@@ -199,28 +204,20 @@ class Restrainer
199
204
  # Remove a process to the currently run set. Returns true if the process was removed.
200
205
  def remove_process!(redis, process_id)
201
206
  result = redis.zrem(key, process_id)
202
- result = true if result == 1
203
- result
207
+ result == true || result == 1
204
208
  end
205
209
 
206
210
  # Evaluate and execute a Lua script on the redis server.
207
211
  def eval_script(redis, process_id, throttle_limit)
208
- sha1 = @add_process_sha1
209
- if sha1.nil?
210
- sha1 = redis.script(:load, ADD_PROCESS_SCRIPT)
211
- @add_process_sha1 = sha1
212
- end
213
-
212
+ script_loaded = false
214
213
  begin
215
- redis.evalsha(sha1, [], [key, process_id, throttle_limit, @timeout, Time.now.to_f])
214
+ redis.evalsha(ADD_PROCESS_SCRIPT_SHA1, [key], [process_id, throttle_limit, @timeout])
216
215
  rescue Redis::CommandError => e
217
- if e.message.include?("NOSCRIPT")
218
- sha1 = redis.script(:load, ADD_PROCESS_SCRIPT)
219
- @add_process_sha1 = sha1
220
- retry
221
- else
222
- raise e
223
- end
216
+ raise if script_loaded || !e.message.include?("NOSCRIPT")
217
+
218
+ redis.script(:load, ADD_PROCESS_SCRIPT)
219
+ script_loaded = true
220
+ retry
224
221
  end
225
222
  end
226
223
  end
data/restrainer.gemspec CHANGED
@@ -8,6 +8,12 @@ Gem::Specification.new do |spec|
8
8
  spec.homepage = "https://github.com/bdurand/restrainer"
9
9
  spec.license = "MIT"
10
10
 
11
+ spec.metadata = {
12
+ "homepage_uri" => spec.homepage,
13
+ "source_code_uri" => spec.homepage,
14
+ "changelog_uri" => "#{spec.homepage}/blob/main/CHANGELOG.md"
15
+ }
16
+
11
17
  # Specify which files should be added to the gem when it is released.
12
18
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
13
19
  ignore_files = %w[
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: restrainer
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.3
4
+ version: 1.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Durand
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2023-12-22 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: redis
@@ -38,7 +37,6 @@ dependencies:
38
37
  - - ">="
39
38
  - !ruby/object:Gem::Version
40
39
  version: '0'
41
- description:
42
40
  email:
43
41
  - bbdurand@gmail.com
44
42
  executables: []
@@ -54,8 +52,10 @@ files:
54
52
  homepage: https://github.com/bdurand/restrainer
55
53
  licenses:
56
54
  - MIT
57
- metadata: {}
58
- post_install_message:
55
+ metadata:
56
+ homepage_uri: https://github.com/bdurand/restrainer
57
+ source_code_uri: https://github.com/bdurand/restrainer
58
+ changelog_uri: https://github.com/bdurand/restrainer/blob/main/CHANGELOG.md
59
59
  rdoc_options: []
60
60
  require_paths:
61
61
  - lib
@@ -70,8 +70,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
70
70
  - !ruby/object:Gem::Version
71
71
  version: '0'
72
72
  requirements: []
73
- rubygems_version: 3.4.20
74
- signing_key:
73
+ rubygems_version: 4.0.3
75
74
  specification_version: 4
76
75
  summary: Code for throttling workloads so as not to overwhelm external services
77
76
  test_files: []