double_restraint 1.0.1 → 1.0.2
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 +16 -3
- data/README.md +27 -11
- data/VERSION +1 -1
- data/double_restraint.gemspec +7 -1
- data/lib/double_restraint.rb +32 -20
- metadata +10 -11
- /data/{MIT-LICENSE → MIT-LICENSE.txt} +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ea9dbcbe1d5da4480324c5dcfa993aaa72e58786430200ee0aaedbd844d341ba
|
|
4
|
+
data.tar.gz: 42201e52acde9d9ca90b5c0c4ded0350af3910f7aca3e8f97c59010929284030
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: de78ad1954fc63e5936f0d26d4d02daf188cb594dc366b8dcf62a8941ee574fd06d7d1200ffb88ace43a4ab3c919a945ad738d70ef4af7d0dfea3b1f1db32ffe
|
|
7
|
+
data.tar.gz: 31dc3d878fde21db6c4a636129bb6acc8ef2b11573b0ded621b840968c456ec5b9f2972a8fc104c978af32dc3a43abcc92d358eaa56bebc411d2b82a81b7a761
|
data/CHANGELOG.md
CHANGED
|
@@ -4,10 +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
|
-
##
|
|
7
|
+
## 1.0.2
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Timeout errors raised by the underlying `Restrainer` Redis calls no longer trigger the long running retry; only timeout errors raised by the block itself do. Previously a matching error raised while releasing the throttle could re-run a block that had already succeeded.
|
|
12
|
+
- Timeout errors that are not `StandardError` subclasses now correctly trigger the long running retry.
|
|
13
|
+
- `limit: 0` now means no executions are allowed (matching `Restrainer` semantics) instead of silently meaning unlimited. Use `limit: nil` (the default) for no limit.
|
|
14
|
+
- The orphaned lock expiration on the underlying restrainers is now derived from `long_running_timeout` so that blocks legitimately running longer than 60 seconds no longer allow the concurrency limits to be exceeded.
|
|
15
|
+
|
|
16
|
+
## 1.0.1
|
|
17
|
+
|
|
8
18
|
### Added
|
|
19
|
+
|
|
9
20
|
- Methods to expose the pool limits, timeouts and current in use sizes.
|
|
10
21
|
|
|
11
|
-
##
|
|
22
|
+
## 1.0.0
|
|
23
|
+
|
|
12
24
|
### Added
|
|
13
|
-
|
|
25
|
+
|
|
26
|
+
- Initial release.
|
data/README.md
CHANGED
|
@@ -2,21 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/bdurand/double_restraint/actions/workflows/continuous_integration.yml)
|
|
4
4
|
[](https://github.com/testdouble/standard)
|
|
5
|
+
[](https://badge.fury.io/rb/double_restraint)
|
|
5
6
|
|
|
6
|
-
This gem implements a pattern for interacting with external services in a way that prevents performance issues with those services from taking down your application. It builds atop the [restrainer gem](https://github.com/
|
|
7
|
+
This gem implements a pattern for interacting with external services in a way that prevents performance issues with those services from taking down your application. It builds atop the [restrainer gem](https://github.com/bdurand/restrainer) which requires a Redis server to coordinate processes.
|
|
7
8
|
|
|
8
9
|
## Usage
|
|
9
10
|
|
|
10
|
-
Suppose you have a web application that calls a web service for something, and at some point that web service starts to have latency issues and requests take several seconds to return. Eventually most your application threads will be
|
|
11
|
+
Suppose you have a web application that calls a web service for something, and at some point that web service starts to have latency issues and requests take several seconds to return. Eventually most of your application threads will be waiting on the web service and your application will be completely unresponsive.
|
|
11
12
|
|
|
12
|
-
If the external service uses timeouts, you could mitigate the issue of locking up your application by setting a low timeout so that requests to the
|
|
13
|
+
If the external service uses timeouts, you could mitigate the issue of locking up your application by setting a low timeout so that requests to the service fail fast. However, if some requests to the service take just a little longer even in a healthy system, then you will be artificially preventing these requests from succeeding.
|
|
13
14
|
|
|
14
|
-
With the [restrainer gem](https://github.com/
|
|
15
|
+
With the [restrainer gem](https://github.com/bdurand/restrainer) you can throttle the number of concurrent requests to a service so that, if there is a problem with that service, only a limited number of application threads would be affected:
|
|
15
16
|
|
|
16
17
|
```ruby
|
|
17
18
|
restrainer = Restrainer.new("MyWebService", limit: 10)
|
|
18
19
|
begin
|
|
19
|
-
|
|
20
|
+
restrainer.throttle do
|
|
20
21
|
MyWebService.new.call(arguments)
|
|
21
22
|
end
|
|
22
23
|
rescue Restrainer::ThrottledError
|
|
@@ -26,7 +27,7 @@ end
|
|
|
26
27
|
|
|
27
28
|
However, this can lead to problems if you set the limit too low. You could end up in a situation where peak traffic sends more requests than the limit you set. This will end up artificially limiting the external calls and returning errors to users.
|
|
28
29
|
|
|
29
|
-
This gem combines both solutions and lets you set two levels of timeouts and a limit on how many concurrent requests can use the longer timeout. You can be more aggressive with both your fail fast timeout and the limit on concurrent processes without affecting requests in a
|
|
30
|
+
This gem combines both solutions and lets you set two levels of timeouts and a limit on how many concurrent requests can use the longer timeout. You can be more aggressive with both your fail fast timeout and the limit on concurrent processes without affecting requests in a healthy system.
|
|
30
31
|
|
|
31
32
|
```ruby
|
|
32
33
|
restraint = DoubleRestraint.new("MyWebService", timeout: 0.5, long_running_timeout: 5.0, long_running_limit: 5)
|
|
@@ -40,12 +41,12 @@ end
|
|
|
40
41
|
```
|
|
41
42
|
|
|
42
43
|
* The `timeout` value should be set to a low value that works for most requests in a healthy system.
|
|
43
|
-
* The `long_running_timeout` value should be set to a higher value that works for all requests in a
|
|
44
|
+
* The `long_running_timeout` value should be set to a higher value that works for all requests in a healthy system.
|
|
44
45
|
* The `long_running_limit` value is the maximum number of concurrent requests that are allowed using the higher timeout.
|
|
45
46
|
|
|
46
47
|
The `execute` call will call the block with the `timeout` value. If the block raises a timeout error, then it will be called again with the `long_running_timeout` value inside a `Restrainer`. If there are too many concurrent requests, then a `Restrainer::ThrottledError` will be raised.
|
|
47
48
|
|
|
48
|
-
The effect of this is that if there are latency issues in `MyWebService`, then the requests will fail fast. Only a handful of requests will be allowed to execute with the higher timeout value so the impact on the overall system will be very limited. On a healthy system, you shouldn't
|
|
49
|
+
The effect of this is that if there are latency issues in `MyWebService`, then the requests will fail fast. Only a handful of requests will be allowed to execute with the higher timeout value so the impact on the overall system will be very limited. On a healthy system, you shouldn't see any artificially generated errors as long as your `timeout` is set properly.
|
|
49
50
|
|
|
50
51
|
The `execute` block **must** be idempotent since it can be run twice by one call to `execute`.
|
|
51
52
|
|
|
@@ -61,7 +62,7 @@ By default, a timeout is identified by any error that inherits from `Timeout::Er
|
|
|
61
62
|
restraint = DoubleRestraint.new("MyWebService", timeout_errors: [Faraday::TimeoutError], timeout: 0.5, long_running_timeout: 5.0, long_running_limit: 5)
|
|
62
63
|
```
|
|
63
64
|
|
|
64
|
-
Finally, you need to specify the Redis instance to use. By default this uses the value specified for the [restrainer gem](https://github.com/
|
|
65
|
+
Finally, you need to specify the Redis instance to use. By default this uses the value specified for the [restrainer gem](https://github.com/bdurand/restrainer).
|
|
65
66
|
|
|
66
67
|
```ruby
|
|
67
68
|
# set the global Redis instance
|
|
@@ -74,10 +75,25 @@ Restrainer.redis{ connection_pool.redis }
|
|
|
74
75
|
However, you can also specify the Redis instance directly on the `DoubleRestraint` instance.
|
|
75
76
|
|
|
76
77
|
```ruby
|
|
77
|
-
restraint = DoubleRestraint.new("MyWebService", redis: Redis.new(url: redis_url)
|
|
78
|
+
restraint = DoubleRestraint.new("MyWebService", redis: Redis.new(url: redis_url), timeout: 0.5, long_running_timeout: 5.0, long_running_limit: 5)
|
|
78
79
|
```
|
|
79
80
|
|
|
80
|
-
You can peek at the current pool sizes as well if
|
|
81
|
+
You can peek at the current pool sizes as well if you want:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
# Number of processes currently using a slot in the default pool
|
|
85
|
+
restraint.default_pool_size
|
|
86
|
+
|
|
87
|
+
# Number of processes currently using a slot in the long running pool
|
|
88
|
+
restraint.long_running_pool_size
|
|
89
|
+
|
|
90
|
+
# Get the percentage capacity being used as a whole. Note that this only
|
|
91
|
+
# makes sense if a limit is set on the default pool (default_pool_limit
|
|
92
|
+
# returns -1 when the pool is unlimited).
|
|
93
|
+
total_pool_used = restraint.default_pool_size + restraint.long_running_pool_size
|
|
94
|
+
total_pool_capacity = restraint.default_pool_limit + restraint.long_running_pool_limit
|
|
95
|
+
total_pool_used.to_f / total_pool_capacity
|
|
96
|
+
```
|
|
81
97
|
|
|
82
98
|
## Installation
|
|
83
99
|
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.2
|
data/double_restraint.gemspec
CHANGED
|
@@ -8,6 +8,12 @@ Gem::Specification.new do |spec|
|
|
|
8
8
|
spec.homepage = "https://github.com/bdurand/double_restraint"
|
|
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[
|
|
@@ -24,7 +30,7 @@ Gem::Specification.new do |spec|
|
|
|
24
30
|
|
|
25
31
|
spec.require_paths = ["lib"]
|
|
26
32
|
|
|
27
|
-
spec.add_dependency "restrainer"
|
|
33
|
+
spec.add_dependency "restrainer", ">= 1.1.3"
|
|
28
34
|
|
|
29
35
|
spec.add_development_dependency "bundler"
|
|
30
36
|
|
data/lib/double_restraint.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "timeout"
|
|
3
4
|
require "restrainer"
|
|
4
5
|
|
|
5
6
|
class DoubleRestraint
|
|
@@ -8,11 +9,11 @@ class DoubleRestraint
|
|
|
8
9
|
# @param name [String, Symbol] The name of the restraint.
|
|
9
10
|
# @param timeout [Numeric] The first timeout that will be yielded to the block.
|
|
10
11
|
# @param long_running_timeout [Numeric] The timeout that will be yielded to the block if
|
|
11
|
-
# the block times out the first time it is
|
|
12
|
-
# @param long_running_limit [Integer] The maximum number of
|
|
12
|
+
# the block times out the first time it is executed.
|
|
13
|
+
# @param long_running_limit [Integer] The maximum number of concurrent executions of the block
|
|
13
14
|
# with the long running timeout across all processes.
|
|
14
|
-
# @param limit [Integer] The maximum of
|
|
15
|
-
# timeout across all processes.
|
|
15
|
+
# @param limit [Integer] The maximum number of concurrent executions of the block with the
|
|
16
|
+
# initial timeout across all processes. If this is nil, then no limit will be applied.
|
|
16
17
|
# @param timeout_errors [Array<Module>] List of errors that will be considered a timeout.
|
|
17
18
|
# This needs to be customized depending on what the code in the block could throw to
|
|
18
19
|
# indicate a timeout has occurred.
|
|
@@ -22,9 +23,15 @@ class DoubleRestraint
|
|
|
22
23
|
@timeout = timeout
|
|
23
24
|
@long_running_timeout = long_running_timeout
|
|
24
25
|
@timeout_errors = Array(timeout_errors)
|
|
25
|
-
limit = -1 if limit.
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
limit = -1 if limit.nil?
|
|
27
|
+
# Slots in the underlying restrainers expire as a safeguard against orphaned locks,
|
|
28
|
+
# so the expiration must comfortably exceed the longest time a block can legitimately
|
|
29
|
+
# hold a slot.
|
|
30
|
+
restrainer_timeout = [60, long_running_timeout.to_f * 2].max
|
|
31
|
+
# The restrainer names use "DoubleRestrainer" rather than "DoubleRestraint" for
|
|
32
|
+
# backward compatibility with the Redis keys created by earlier versions of the gem.
|
|
33
|
+
@restrainer = Restrainer.new("DoubleRestrainer(#{name})", limit: limit, timeout: restrainer_timeout, redis: redis)
|
|
34
|
+
@long_running_restrainer = Restrainer.new("DoubleRestrainer(#{name}).long_running", limit: long_running_limit, timeout: restrainer_timeout, redis: redis)
|
|
28
35
|
end
|
|
29
36
|
|
|
30
37
|
# Execute a block of code. The block will be yielded with the timeout value. If the block raises
|
|
@@ -32,21 +39,26 @@ class DoubleRestraint
|
|
|
32
39
|
# must be idempotent since it can be run twice.
|
|
33
40
|
#
|
|
34
41
|
# @yieldparam [Numeric] the timeout value to use in the block.
|
|
35
|
-
# @
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
rescue
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
# @return [Object] the value returned by the block.
|
|
43
|
+
# @raise [Restrainer::ThrottledError] if too many concurrent processes are trying to use the restraint.
|
|
44
|
+
def execute
|
|
45
|
+
timed_out = false
|
|
46
|
+
result = @restrainer.throttle do
|
|
47
|
+
yield @timeout
|
|
48
|
+
rescue *@timeout_errors
|
|
49
|
+
# Just flag the timeout here so the retry happens after the throttle
|
|
50
|
+
# block exits and releases its slot in the default pool.
|
|
51
|
+
timed_out = true
|
|
52
|
+
nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
if timed_out
|
|
56
|
+
result = @long_running_restrainer.throttle do
|
|
57
|
+
yield @long_running_timeout
|
|
48
58
|
end
|
|
49
59
|
end
|
|
60
|
+
|
|
61
|
+
result
|
|
50
62
|
end
|
|
51
63
|
|
|
52
64
|
# Get the current size of the default pool. This can be useful in
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: double_restraint
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.2
|
|
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: restrainer
|
|
@@ -16,14 +15,14 @@ dependencies:
|
|
|
16
15
|
requirements:
|
|
17
16
|
- - ">="
|
|
18
17
|
- !ruby/object:Gem::Version
|
|
19
|
-
version:
|
|
18
|
+
version: 1.1.3
|
|
20
19
|
type: :runtime
|
|
21
20
|
prerelease: false
|
|
22
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
22
|
requirements:
|
|
24
23
|
- - ">="
|
|
25
24
|
- !ruby/object:Gem::Version
|
|
26
|
-
version:
|
|
25
|
+
version: 1.1.3
|
|
27
26
|
- !ruby/object:Gem::Dependency
|
|
28
27
|
name: bundler
|
|
29
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -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: []
|
|
@@ -46,7 +44,7 @@ extensions: []
|
|
|
46
44
|
extra_rdoc_files: []
|
|
47
45
|
files:
|
|
48
46
|
- CHANGELOG.md
|
|
49
|
-
- MIT-LICENSE
|
|
47
|
+
- MIT-LICENSE.txt
|
|
50
48
|
- README.md
|
|
51
49
|
- VERSION
|
|
52
50
|
- double_restraint.gemspec
|
|
@@ -54,8 +52,10 @@ files:
|
|
|
54
52
|
homepage: https://github.com/bdurand/double_restraint
|
|
55
53
|
licenses:
|
|
56
54
|
- MIT
|
|
57
|
-
metadata:
|
|
58
|
-
|
|
55
|
+
metadata:
|
|
56
|
+
homepage_uri: https://github.com/bdurand/double_restraint
|
|
57
|
+
source_code_uri: https://github.com/bdurand/double_restraint
|
|
58
|
+
changelog_uri: https://github.com/bdurand/double_restraint/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: Throttling mechanism for safely dealing with external resources so that latency
|
|
77
76
|
does not take down your application.
|
|
File without changes
|