restrainer 1.1.4 → 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 +4 -4
- data/CHANGELOG.md +17 -0
- data/README.md +2 -3
- data/VERSION +1 -1
- data/lib/restrainer.rb +27 -27
- data/restrainer.gemspec +6 -0
- metadata +7 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c140a5c4d377fe5df87028599ec86c507d428657d1c44e8456c83fe4b046c815
|
|
4
|
+
data.tar.gz: 5229e54526f187a07266f15e7af265e91f3a5dc691ceaccb3c2628867b060ba8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 77692da8fd5fd19ccea93bc3903e2322b00959ad32b20b37c7a84a9d37c4213c20d9b7ce1c541247d7f4c7f1ae8dbc870b3c71814c34a610806b968f4cff20cc
|
|
7
|
+
data.tar.gz: 2e21b607d587007bca24bae0602f7d108e447582510e053ee0fe547e635aa3e6d45224a57320776faac152aedba2cb88d8d7ddd490fc91f52a897230169383e5
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@ 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
|
+
|
|
7
24
|
## 1.1.4
|
|
8
25
|
|
|
9
26
|
### Removed
|
data/README.md
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
[](https://github.com/bdurand/restrainer/actions/workflows/continuous_integration.yml)
|
|
2
|
-
[](https://github.com/bdurand/restrainer/actions/workflows/regression_test.yml)
|
|
3
2
|
[](https://github.com/testdouble/standard)
|
|
4
3
|
[](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.
|
|
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 =
|
|
22
|
-
local process_id = ARGV[
|
|
23
|
-
local limit = tonumber(ARGV[
|
|
24
|
-
local ttl = tonumber(ARGV[
|
|
25
|
-
|
|
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
|
-
#
|
|
49
|
-
|
|
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
|
-
|
|
77
|
+
@redis ||= begin
|
|
71
78
|
client = Redis.new
|
|
72
|
-
|
|
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 [
|
|
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
|
|
@@ -196,28 +204,20 @@ class Restrainer
|
|
|
196
204
|
# Remove a process to the currently run set. Returns true if the process was removed.
|
|
197
205
|
def remove_process!(redis, process_id)
|
|
198
206
|
result = redis.zrem(key, process_id)
|
|
199
|
-
result
|
|
200
|
-
result
|
|
207
|
+
result == true || result == 1
|
|
201
208
|
end
|
|
202
209
|
|
|
203
210
|
# Evaluate and execute a Lua script on the redis server.
|
|
204
211
|
def eval_script(redis, process_id, throttle_limit)
|
|
205
|
-
|
|
206
|
-
if sha1.nil?
|
|
207
|
-
sha1 = redis.script(:load, ADD_PROCESS_SCRIPT)
|
|
208
|
-
@add_process_sha1 = sha1
|
|
209
|
-
end
|
|
210
|
-
|
|
212
|
+
script_loaded = false
|
|
211
213
|
begin
|
|
212
|
-
redis.evalsha(
|
|
214
|
+
redis.evalsha(ADD_PROCESS_SCRIPT_SHA1, [key], [process_id, throttle_limit, @timeout])
|
|
213
215
|
rescue Redis::CommandError => e
|
|
214
|
-
if e.message.include?("NOSCRIPT")
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
raise e
|
|
220
|
-
end
|
|
216
|
+
raise if script_loaded || !e.message.include?("NOSCRIPT")
|
|
217
|
+
|
|
218
|
+
redis.script(:load, ADD_PROCESS_SCRIPT)
|
|
219
|
+
script_loaded = true
|
|
220
|
+
retry
|
|
221
221
|
end
|
|
222
222
|
end
|
|
223
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.
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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: []
|