double_restraint 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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: []