double_restraint 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +9 -0
- data/MIT-LICENSE +20 -0
- data/README.md +106 -0
- data/VERSION +1 -0
- data/double_restraint.gemspec +32 -0
- data/lib/double_restraint.rb +51 -0
- metadata +78 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 730e777e36c39bfc23ff335c32c5ed3afcc92f939496546a27e26c16cb755a7b
|
4
|
+
data.tar.gz: 68a039424903a839c46defb56535a33a793ab8e044ed62e986ff0b2fe5f4ffef
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: fd3bd50d0e03ba15d9a043e54f877e3c1828d546d54b1f2751adfc5aac0f4c8da88eda18e7af4c7597bf6203272f7748b383e2c24ea548e56e72b3b9a83ac0d8
|
7
|
+
data.tar.gz: 597684fa2b5eefb6f6ae9c5de1fbdeb353846368a209ba606828a2b670a520398400ad3703ae5f0935445d21f3bb58bb62cb6454c045c347dc12b3a897cf5b5c
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
# Changelog
|
2
|
+
All notable changes to this project will be documented in this file.
|
3
|
+
|
4
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
5
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
6
|
+
|
7
|
+
## [1.0.0]
|
8
|
+
### Added
|
9
|
+
- Everything!
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2022 Brian Durand
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
# Double Restraint
|
2
|
+
|
3
|
+
[![Continuous Integration](https://github.com/bdurand/double_restraint/actions/workflows/continuous_integration.yml/badge.svg)](https://github.com/bdurand/double_restraint/actions/workflows/continuous_integration.yml)
|
4
|
+
[![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)
|
5
|
+
|
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/weheartit/restrainer) which requires a Redis server to coordinate processes.
|
7
|
+
|
8
|
+
## Usage
|
9
|
+
|
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 be waiting on the web service and your application will be completely unresponsive.
|
11
|
+
|
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 sevice fail fast. However, if some requests to the service take just a little longer even in a health system, then you will be artificially preventing these requests from succeeding.
|
13
|
+
|
14
|
+
With the [restrainer gem](https://github.com/weheartit/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
|
+
```ruby
|
17
|
+
restrainer = Restrainer.new("MyWebService", limit: 10)
|
18
|
+
begin
|
19
|
+
restainer.throttle do
|
20
|
+
MyWebService.new.call(arguments)
|
21
|
+
end
|
22
|
+
rescue Restrainer::ThrottledError
|
23
|
+
puts "Too many concurrent calls to MyWebService"
|
24
|
+
end
|
25
|
+
```
|
26
|
+
|
27
|
+
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
|
+
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 health system.
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
restraint = DoubleRestraint.new("MyWebService", timeout: 0.5, long_running_timeout: 5.0, long_running_limit: 5)
|
33
|
+
begin
|
34
|
+
restraint.execute do |timeout|
|
35
|
+
MyWebService.new(timeout: timeout).call(arguments)
|
36
|
+
end
|
37
|
+
rescue Restrainer::ThrottledError
|
38
|
+
puts "Too many concurrent calls to MyWebService"
|
39
|
+
end
|
40
|
+
```
|
41
|
+
|
42
|
+
* 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 health system.
|
44
|
+
* The `long_running_limit` value is the maximum number of concurrent requests that are allowed using the higher timeout.
|
45
|
+
|
46
|
+
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
|
+
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 seen any artificially generated errors as long as your `timeout` is set properly.
|
49
|
+
|
50
|
+
The `execute` block **must** be idempotent since it can be run twice by one call to `execute`.
|
51
|
+
|
52
|
+
You can also set a restraint on the initial execution with the lower timeout by specifying the `limit` parameter.
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
restraint = DoubleRestraint.new("MyWebService", limit: 50, timeout: 0.5, long_running_timeout: 5.0, long_running_limit: 5)
|
56
|
+
```
|
57
|
+
|
58
|
+
By default, a timeout is identified by any error that inherits from `Timeout::Error`. You may need to specify what constitutes a timeout error in your block of code, though. For instance, if you code uses Faraday to make an HTTP requests, then you would need to specify that timeouts are identified by `Faraday::TimeoutError`.
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
restraint = DoubleRestraint.new("MyWebService", timeout_errors: [Faraday::TimeoutError], timeout: 0.5, long_running_timeout: 5.0, long_running_limit: 5)
|
62
|
+
```
|
63
|
+
|
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/weheartit/restrainer).
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
# set the global Redis instance
|
68
|
+
Restrainer.redis = Redis.new(url: redis_url)
|
69
|
+
|
70
|
+
# or use a block to specify a value that is yielded at runtime
|
71
|
+
Restrainer.redis{ connection_pool.redis }
|
72
|
+
```
|
73
|
+
|
74
|
+
However, you can also specify the Redis instance directly on the `DoubleRestraint` instance.
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
restraint = DoubleRestraint.new("MyWebService", redis: Redis.new(url: redis_url)), timeout: 0.5, long_running_timeout: 5.0, long_running_limit: 5)
|
78
|
+
```
|
79
|
+
|
80
|
+
## Installation
|
81
|
+
|
82
|
+
Add this line to your application's Gemfile:
|
83
|
+
|
84
|
+
```ruby
|
85
|
+
gem "double_restraint"
|
86
|
+
```
|
87
|
+
|
88
|
+
And then execute:
|
89
|
+
```bash
|
90
|
+
$ bundle
|
91
|
+
```
|
92
|
+
|
93
|
+
Or install it yourself as:
|
94
|
+
```bash
|
95
|
+
$ gem install double_restraint
|
96
|
+
```
|
97
|
+
|
98
|
+
## Contributing
|
99
|
+
|
100
|
+
Open a pull request on GitHub.
|
101
|
+
|
102
|
+
Please use the [standardrb](https://github.com/testdouble/standard) syntax and lint your code with `standardrb --fix` before submitting.
|
103
|
+
|
104
|
+
## License
|
105
|
+
|
106
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.0.0
|
@@ -0,0 +1,32 @@
|
|
1
|
+
Gem::Specification.new do |spec|
|
2
|
+
spec.name = "double_restraint"
|
3
|
+
spec.version = File.read(File.expand_path("VERSION", __dir__)).strip
|
4
|
+
spec.authors = ["Brian Durand"]
|
5
|
+
spec.email = ["bbdurand@gmail.com"]
|
6
|
+
|
7
|
+
spec.summary = "Throttling mechanism for safely dealing with external resources so that latency does not take down your application."
|
8
|
+
spec.homepage = "https://github.com/bdurand/double_restraint"
|
9
|
+
spec.license = "MIT"
|
10
|
+
|
11
|
+
# Specify which files should be added to the gem when it is released.
|
12
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
13
|
+
ignore_files = %w[
|
14
|
+
.
|
15
|
+
Gemfile
|
16
|
+
Gemfile.lock
|
17
|
+
Rakefile
|
18
|
+
bin/
|
19
|
+
spec/
|
20
|
+
]
|
21
|
+
spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do
|
22
|
+
`git ls-files -z`.split("\x0").reject { |f| ignore_files.any? { |path| f.start_with?(path) } }
|
23
|
+
end
|
24
|
+
|
25
|
+
spec.require_paths = ["lib"]
|
26
|
+
|
27
|
+
spec.add_dependency "restrainer"
|
28
|
+
|
29
|
+
spec.add_development_dependency "bundler"
|
30
|
+
|
31
|
+
spec.required_ruby_version = ">= 2.5"
|
32
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "restrainer"
|
4
|
+
|
5
|
+
class DoubleRestraint
|
6
|
+
# @param name [String, Symbol] The name of the restraint.
|
7
|
+
# @param timeout [Numeric] The first timeout that will be yielded to the block.
|
8
|
+
# @param long_running_timeout [Numeric] The timeout that will be yielded to the block if
|
9
|
+
# the block times out the first time it is excuted.
|
10
|
+
# @param long_running_limit [Integer] The maximum number of times the block can be run
|
11
|
+
# with the long running timeout across all processes.
|
12
|
+
# @param limit [Integer] The maximum of number of times the block can be run with the initial
|
13
|
+
# timeout across all processes.
|
14
|
+
# @param timeout_errors [Array<Module>] List of errors that will be considered a timeout.
|
15
|
+
# This needs to be customized depending on what the code in the block could throw to
|
16
|
+
# indicate a timeout has occurred.
|
17
|
+
# @param redis [Redis] Redis connection to use.
|
18
|
+
# If this is not set, the default value set for `Restrainer.redis` will be used.
|
19
|
+
def initialize(name, timeout:, long_running_timeout:, long_running_limit:, limit: nil, timeout_errors: [Timeout::Error], redis: nil)
|
20
|
+
@timeout = timeout
|
21
|
+
@long_running_timeout = long_running_timeout
|
22
|
+
@timeout_errors = Array(timeout_errors)
|
23
|
+
@restrainer = Restrainer.new("DoubleRestrainer(#{name})", limit: limit, redis: redis) if limit
|
24
|
+
@long_running_restrainer = Restrainer.new("DoubleRestrainer(#{name}).long_running", limit: long_running_limit, redis: redis)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Execute a block of code. The block will be yielded with the timeout value. If the block raises
|
28
|
+
# a timeout error, then it will be called again with the long running timeout. The code in the block
|
29
|
+
# must be idempotent since it can be run twice.
|
30
|
+
# @yieldparam [Numeric] the timeout value to use in the block.
|
31
|
+
# @raise [Restrainer::ThrottleError] if too many concurrent processes are trying to use the restraint.
|
32
|
+
def execute
|
33
|
+
begin
|
34
|
+
if @restrainer
|
35
|
+
@restrainer.throttle do
|
36
|
+
yield @timeout
|
37
|
+
end
|
38
|
+
else
|
39
|
+
yield @timeout
|
40
|
+
end
|
41
|
+
rescue => e
|
42
|
+
if @timeout_errors.any? { |error_class| e.is_a?(error_class) }
|
43
|
+
@long_running_restrainer.throttle do
|
44
|
+
yield @long_running_timeout
|
45
|
+
end
|
46
|
+
else
|
47
|
+
raise e
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
metadata
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: double_restraint
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Brian Durand
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-03-28 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: restrainer
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
description:
|
42
|
+
email:
|
43
|
+
- bbdurand@gmail.com
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- CHANGELOG.md
|
49
|
+
- MIT-LICENSE
|
50
|
+
- README.md
|
51
|
+
- VERSION
|
52
|
+
- double_restraint.gemspec
|
53
|
+
- lib/double_restraint.rb
|
54
|
+
homepage: https://github.com/bdurand/double_restraint
|
55
|
+
licenses:
|
56
|
+
- MIT
|
57
|
+
metadata: {}
|
58
|
+
post_install_message:
|
59
|
+
rdoc_options: []
|
60
|
+
require_paths:
|
61
|
+
- lib
|
62
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '2.5'
|
67
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
68
|
+
requirements:
|
69
|
+
- - ">="
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: '0'
|
72
|
+
requirements: []
|
73
|
+
rubygems_version: 3.0.3
|
74
|
+
signing_key:
|
75
|
+
specification_version: 4
|
76
|
+
summary: Throttling mechanism for safely dealing with external resources so that latency
|
77
|
+
does not take down your application.
|
78
|
+
test_files: []
|