adaptable_circuit_breaker 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +127 -0
- data/Rakefile +6 -0
- data/bin/console +7 -0
- data/bin/setup +6 -0
- data/circuit_breaker.gemspec +34 -0
- data/examples/example.rb +27 -0
- data/examples/example_redis.rb +32 -0
- data/lib/circuit_breaker.rb +117 -0
- data/lib/circuit_breaker/failure.rb +35 -0
- data/lib/circuit_breaker/memory.rb +53 -0
- data/lib/circuit_breaker/open_error.rb +4 -0
- data/lib/circuit_breaker/redis.rb +111 -0
- data/lib/circuit_breaker/version.rb +3 -0
- data/remote-loop.gif +0 -0
- data/remote.gif +0 -0
- metadata +179 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 4a20ac15b930fde9e3f1024f91615331f9ddb170
|
4
|
+
data.tar.gz: 15b94b99a7f58b151ac1bd2805c9ca6367106f50
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7c556e4fb765ec54887c9085f9cdfa481db38e7b1c1e87e50af075f5f37b0fde870408fd344c1d822e076d8a62a6192c85ab956f53f709aa83d382d4eeddc9a9
|
7
|
+
data.tar.gz: ecf4f43903c327eb860eecbe197f5c2b74fd3a100666179c5911922294c28bab771959ca885cc8d349d84322aaeac18953ec32e5bbbc13df6434ea08e1aeb386
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
2
|
+
|
3
|
+
## Our Pledge
|
4
|
+
|
5
|
+
In the interest of fostering an open and welcoming environment, we as
|
6
|
+
contributors and maintainers pledge to making participation in our project and
|
7
|
+
our community a harassment-free experience for everyone, regardless of age, body
|
8
|
+
size, disability, ethnicity, gender identity and expression, level of experience,
|
9
|
+
nationality, personal appearance, race, religion, or sexual identity and
|
10
|
+
orientation.
|
11
|
+
|
12
|
+
## Our Standards
|
13
|
+
|
14
|
+
Examples of behavior that contributes to creating a positive environment
|
15
|
+
include:
|
16
|
+
|
17
|
+
* Using welcoming and inclusive language
|
18
|
+
* Being respectful of differing viewpoints and experiences
|
19
|
+
* Gracefully accepting constructive criticism
|
20
|
+
* Focusing on what is best for the community
|
21
|
+
* Showing empathy towards other community members
|
22
|
+
|
23
|
+
Examples of unacceptable behavior by participants include:
|
24
|
+
|
25
|
+
* The use of sexualized language or imagery and unwelcome sexual attention or
|
26
|
+
advances
|
27
|
+
* Trolling, insulting/derogatory comments, and personal or political attacks
|
28
|
+
* Public or private harassment
|
29
|
+
* Publishing others' private information, such as a physical or electronic
|
30
|
+
address, without explicit permission
|
31
|
+
* Other conduct which could reasonably be considered inappropriate in a
|
32
|
+
professional setting
|
33
|
+
|
34
|
+
## Our Responsibilities
|
35
|
+
|
36
|
+
Project maintainers are responsible for clarifying the standards of acceptable
|
37
|
+
behavior and are expected to take appropriate and fair corrective action in
|
38
|
+
response to any instances of unacceptable behavior.
|
39
|
+
|
40
|
+
Project maintainers have the right and responsibility to remove, edit, or
|
41
|
+
reject comments, commits, code, wiki edits, issues, and other contributions
|
42
|
+
that are not aligned to this Code of Conduct, or to ban temporarily or
|
43
|
+
permanently any contributor for other behaviors that they deem inappropriate,
|
44
|
+
threatening, offensive, or harmful.
|
45
|
+
|
46
|
+
## Scope
|
47
|
+
|
48
|
+
This Code of Conduct applies both within project spaces and in public spaces
|
49
|
+
when an individual is representing the project or its community. Examples of
|
50
|
+
representing a project or community include using an official project e-mail
|
51
|
+
address, posting via an official social media account, or acting as an appointed
|
52
|
+
representative at an online or offline event. Representation of a project may be
|
53
|
+
further defined and clarified by project maintainers.
|
54
|
+
|
55
|
+
## Enforcement
|
56
|
+
|
57
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
58
|
+
reported by contacting the project team at anthony.ross@validic.com. All
|
59
|
+
complaints will be reviewed and investigated and will result in a response that
|
60
|
+
is deemed necessary and appropriate to the circumstances. The project team is
|
61
|
+
obligated to maintain confidentiality with regard to the reporter of an incident.
|
62
|
+
Further details of specific enforcement policies may be posted separately.
|
63
|
+
|
64
|
+
Project maintainers who do not follow or enforce the Code of Conduct in good
|
65
|
+
faith may face temporary or permanent repercussions as determined by other
|
66
|
+
members of the project's leadership.
|
67
|
+
|
68
|
+
## Attribution
|
69
|
+
|
70
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
71
|
+
available at [http://contributor-covenant.org/version/1/4][version]
|
72
|
+
|
73
|
+
[homepage]: http://contributor-covenant.org
|
74
|
+
[version]: http://contributor-covenant.org/version/1/4/
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2017 Anthony Ross
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
# CircuitBreaker
|
2
|
+
|
3
|
+
A lightweight ruby gem that implements the famous [Michael Nygard](https://www.martinfowler.com/bliki/CircuitBreaker.html) circuit breaker pattern.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
This gem is purposely not on RubyGems.org yet as I'm still finalizing the API. If you wish to help me get to it's first stable release, please do!
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'circuit_breaker', github: 'allcentury/circuit_breaker'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle install
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
### In Memory
|
22
|
+
|
23
|
+
Here's an example of how you could use the breaker while making routine calls to a third party service such as Twitter:
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
require 'circuit_breaker'
|
27
|
+
require 'logger'
|
28
|
+
|
29
|
+
@logger = Logger.new(STDOUT)
|
30
|
+
|
31
|
+
handles = ["joe", "jane", "mary", "steve"]
|
32
|
+
|
33
|
+
def get_tweets(twitter_handle)
|
34
|
+
http_result = ["Success!", "Fail"].sample
|
35
|
+
raise RuntimeError.new("Failed to fetch tweets for #{twitter_handle}") if http_result == "Fail"
|
36
|
+
@logger.info "#{http_result} getting tweets for #{twitter_handle}"
|
37
|
+
end
|
38
|
+
|
39
|
+
breaker = CircuitBreaker::Memory.new do |cb|
|
40
|
+
cb.circuit = -> (twitter_handle) { get_tweets(twitter_handle) }
|
41
|
+
cb.failure_limit = 2
|
42
|
+
cb.reset_timeout = 5
|
43
|
+
end
|
44
|
+
|
45
|
+
handles.each do |handle|
|
46
|
+
begin
|
47
|
+
breaker.call(handle)
|
48
|
+
rescue CircuitBreaker::OpenError
|
49
|
+
@logger.warn "Circuit is open - unable to make calls for #{handle}"
|
50
|
+
sleep breaker.reset_timeout
|
51
|
+
end
|
52
|
+
end
|
53
|
+
```
|
54
|
+
|
55
|
+
You will see output similar to:
|
56
|
+
```
|
57
|
+
W, [2017-02-12T20:49:12.374971 #85900] WARN -- : [RuntimeError] - Failed to fetch tweets for joe
|
58
|
+
W, [2017-02-12T20:49:12.375049 #85900] WARN -- : [RuntimeError] - Failed to fetch tweets for jane
|
59
|
+
I, [2017-02-12T20:49:17.380771 #85900] INFO -- : Success! getting tweets for steve
|
60
|
+
I, [2017-02-12T20:49:17.380865 #85900] INFO -- : Circuit closed
|
61
|
+
```
|
62
|
+
|
63
|
+
Notice that we had two failures in a row for joe and jane. The circuit breaker was configured to only allow for 2 failures via the `failuire_limit` method. If another call comes in after two failures, it will raise a `CircuitBreaker::OpenError` error. The only way the circuit breaker will be closed again is if the `reset_timeout` period has lapsed. In our loop we catch the `CircuitBreaker::OpenError` exception and sleep (don't sleep in production - this is just an example) to allow the Circuit to close. You can see the timestamp of this log,
|
64
|
+
|
65
|
+
```
|
66
|
+
I, [2017-02-12T20:49:17.380771 #85900] INFO -- : Success! getting tweets for steve
|
67
|
+
```
|
68
|
+
is 5+ seconds after the last error which exceeds the `reset_timeout` - that's why the breaker allowed the method invocation to go get steve's tweets.
|
69
|
+
|
70
|
+
|
71
|
+
### Redis
|
72
|
+
|
73
|
+
In an distributed environment the in memory solution of the circuit breaker creates quite a bit of unnecessary work. If you can imagine 5 servers all running their own circuit breakers, the `failure_limit` has just increased by a factor of 5. Ideally, we want server1's failures and server2's failures to be included for similar breakers. We do this by using redis where the state of the breaker and the failures are persisted. Redis is a great choice for this especially since most distributed systems have a redis instance in use.
|
74
|
+
|
75
|
+
You can visualize a few servers that were originally in a closed state moving to open upon failures as such:
|
76
|
+
|
77
|
+
![img](https://s3.postimg.org/stxckap03/ezgif_com_video_to_gif.gif)
|
78
|
+
|
79
|
+
You can set up the `CircuitBreaker` to use the redis adapter like this:
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
breaker = CircuitBreaker::Redis.new do |cb|
|
83
|
+
cb.circuit = -> (twitter_handle) { get_tweets(twitter_handle) }
|
84
|
+
cb.client = redis
|
85
|
+
cb.namespace = "get_tweets"
|
86
|
+
cb.failure_limit = 2
|
87
|
+
cb.reset_timeout = 5
|
88
|
+
end
|
89
|
+
```
|
90
|
+
|
91
|
+
You need 2 additional parameters(compared to the `Memory` adapter), they are defined as such:
|
92
|
+
|
93
|
+
- `client` - an instance of a `Redis` client. This gem does not have a hard dependency on a particular redis client but for testing I've used [redis-rb](https://github.com/redis/redis-rb). Whatever you pass in here simply has to implement a few redis commands such as `sadd`, `del`, `smembers`, `get` and `set`. The client will ensure these exist before the breaker can be instantiated.
|
94
|
+
- `namespace` - A unique name that will be used across servers to sync `state` and `failures`. I'd recommend `class_name:some_method` or whatever is special about what's being invoked in the `circuit`.
|
95
|
+
|
96
|
+
### Roll Your Own
|
97
|
+
|
98
|
+
The goal of this project is to help you implement a circuit breaker pattern and be agnostic to the persistence layer. I did it in memory and in redis both as working implementations to make the gem usable out of the box. There are other in memory data stores that would work really well with this and so you can easily implement your own.
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
class MyPreferredAdapter
|
102
|
+
include CircuitBreaker
|
103
|
+
end
|
104
|
+
```
|
105
|
+
|
106
|
+
## Forthcoming
|
107
|
+
|
108
|
+
1. A middleware in Sidekiq using this gem
|
109
|
+
2. Better in memory support for async tasks
|
110
|
+
3. More examples
|
111
|
+
4. More documentation
|
112
|
+
|
113
|
+
|
114
|
+
## Development
|
115
|
+
|
116
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
117
|
+
|
118
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
119
|
+
|
120
|
+
## Contributing
|
121
|
+
|
122
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/allcentury/circuit_breaker. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
123
|
+
|
124
|
+
|
125
|
+
## License
|
126
|
+
|
127
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/bin/console
ADDED
data/bin/setup
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'circuit_breaker/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "adaptable_circuit_breaker"
|
8
|
+
spec.version = CircuitBreaker::VERSION
|
9
|
+
spec.authors = ["Anthony Ross"]
|
10
|
+
spec.email = ["anthony.s.ross@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Circuit Breaker pattern}
|
13
|
+
spec.description = %q{The Circuit Breaker pattern is used to prevent constant fail-over from spotty remote systems}
|
14
|
+
spec.homepage = ""
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
18
|
+
f.match(%r{^(test|spec|features)/})
|
19
|
+
end
|
20
|
+
spec.bindir = "exe"
|
21
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
22
|
+
spec.require_paths = ["lib"]
|
23
|
+
|
24
|
+
spec.metadata["yard.run"] = "yri"
|
25
|
+
|
26
|
+
spec.add_development_dependency "bundler", "~> 1.13"
|
27
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
28
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
29
|
+
spec.add_development_dependency "pry", "~> 0.10"
|
30
|
+
spec.add_development_dependency "timecop", "~> 0.8"
|
31
|
+
spec.add_development_dependency "simplecov", "~> 0.13"
|
32
|
+
spec.add_development_dependency "yard", "~> 0.9"
|
33
|
+
spec.add_development_dependency "redis", "~> 3.3"
|
34
|
+
end
|
data/examples/example.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'circuit_breaker'
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
@logger = Logger.new(STDOUT)
|
5
|
+
|
6
|
+
handles = ["joe", "jane", "mary", "steve"]
|
7
|
+
|
8
|
+
def get_tweets(twitter_handle)
|
9
|
+
http_result = ["Success!", "Fail"].sample
|
10
|
+
raise RuntimeError.new("Failed to fetch tweets for #{twitter_handle}") if http_result == "Fail"
|
11
|
+
@logger.info "#{http_result} getting tweets for #{twitter_handle}"
|
12
|
+
end
|
13
|
+
|
14
|
+
breaker = CircuitBreaker::Memory.new do |cb|
|
15
|
+
cb.circuit = -> (twitter_handle) { get_tweets(twitter_handle) }
|
16
|
+
cb.failure_limit = 2
|
17
|
+
cb.reset_timeout = 5
|
18
|
+
end
|
19
|
+
|
20
|
+
handles.each do |handle|
|
21
|
+
begin
|
22
|
+
breaker.call(handle)
|
23
|
+
rescue CircuitBreaker::OpenError
|
24
|
+
@logger.warn "Circuit is open - unable to make calls for #{handle}"
|
25
|
+
sleep breaker.reset_timeout
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'circuit_breaker'
|
2
|
+
require 'logger'
|
3
|
+
require 'redis'
|
4
|
+
|
5
|
+
@logger = Logger.new(STDOUT)
|
6
|
+
|
7
|
+
handles = ["joe", "jane", "mary", "steve"]
|
8
|
+
|
9
|
+
def get_tweets(twitter_handle)
|
10
|
+
http_result = ["Success!", "Fail"].sample
|
11
|
+
raise RuntimeError.new("Failed to fetch tweets for #{twitter_handle}") if http_result == "Fail"
|
12
|
+
@logger.info "#{http_result} getting tweets for #{twitter_handle}"
|
13
|
+
end
|
14
|
+
|
15
|
+
redis = Redis.new
|
16
|
+
|
17
|
+
breaker = CircuitBreaker::Redis.new do |cb|
|
18
|
+
cb.circuit = -> (twitter_handle) { get_tweets(twitter_handle) }
|
19
|
+
cb.client = redis
|
20
|
+
cb.namespace = "get_tweets"
|
21
|
+
cb.failure_limit = 2
|
22
|
+
cb.reset_timeout = 5
|
23
|
+
end
|
24
|
+
|
25
|
+
handles.each do |handle|
|
26
|
+
begin
|
27
|
+
breaker.call(handle)
|
28
|
+
rescue CircuitBreaker::OpenError
|
29
|
+
@logger.warn "Circuit is open - unable to make calls for #{handle}"
|
30
|
+
sleep breaker.reset_timeout
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require "circuit_breaker/version"
|
2
|
+
require "circuit_breaker/failure"
|
3
|
+
require 'circuit_breaker/open_error'
|
4
|
+
require 'circuit_breaker/memory'
|
5
|
+
require 'circuit_breaker/redis'
|
6
|
+
require 'logger'
|
7
|
+
|
8
|
+
module CircuitBreaker
|
9
|
+
# Calls the circuit proc/lambda if the circuit is closed or half-open
|
10
|
+
#
|
11
|
+
# @param args [Array<Object>] Any number of Objects to be called with the circuit block.
|
12
|
+
# @return [Void, CircuitBreaker::Open] No usable return value if successful, but will raise an error if failure_limit is reached or if the circuit is open
|
13
|
+
def call(*args)
|
14
|
+
check_reset_timeout
|
15
|
+
raise OpenError if open?
|
16
|
+
do_run(args, &circuit)
|
17
|
+
end
|
18
|
+
|
19
|
+
# @return [Integer] The count of current failures
|
20
|
+
def failure_count
|
21
|
+
failures.size
|
22
|
+
end
|
23
|
+
|
24
|
+
# @return [Boolean] Whether the circuit is open
|
25
|
+
def open?
|
26
|
+
state == :open
|
27
|
+
end
|
28
|
+
|
29
|
+
# @return [Boolean] Whether the circuit is closed
|
30
|
+
def closed?
|
31
|
+
state == :closed
|
32
|
+
end
|
33
|
+
|
34
|
+
# @return [Boolean] Whether the circuit is half-open
|
35
|
+
def half_open?
|
36
|
+
state == :half_open
|
37
|
+
end
|
38
|
+
|
39
|
+
# @return [Array<CircuitBreaker::Failure>] a list of current failures
|
40
|
+
def failures
|
41
|
+
must_implement(:failures)
|
42
|
+
end
|
43
|
+
|
44
|
+
# @param failure [Array<CircuitBreaker::Failure>] a list of failures
|
45
|
+
# @return [void]
|
46
|
+
def failures=(failure)
|
47
|
+
must_implement(:failures=)
|
48
|
+
end
|
49
|
+
|
50
|
+
# @param failure [CircuitBreaker::Failure] a list of failures
|
51
|
+
# @return [void]
|
52
|
+
def add_failure(failure)
|
53
|
+
must_implement(:add_failure)
|
54
|
+
end
|
55
|
+
|
56
|
+
# @return [Symbol] - either :open, :closed, :half-open
|
57
|
+
def state
|
58
|
+
must_implement(:state)
|
59
|
+
end
|
60
|
+
|
61
|
+
# @param state [Symbol] - either :open, :closed, :half-open
|
62
|
+
# @return [void]
|
63
|
+
def state=(state)
|
64
|
+
must_implement(:state=)
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def must_implement(arg)
|
70
|
+
raise NotImplementedError.new("You must implement #{arg}.")
|
71
|
+
end
|
72
|
+
|
73
|
+
def do_run(args)
|
74
|
+
yield *args
|
75
|
+
reset_failures
|
76
|
+
rescue => e
|
77
|
+
handle_failure(e)
|
78
|
+
end
|
79
|
+
|
80
|
+
def check_reset_timeout
|
81
|
+
return if !open?
|
82
|
+
if reset_period_lapsed?
|
83
|
+
self.state = :half_open
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def reset_period_lapsed?
|
88
|
+
(Time.now.utc - failures.last.timestamp) > reset_timeout
|
89
|
+
end
|
90
|
+
|
91
|
+
def reset_failures
|
92
|
+
self.failures = []
|
93
|
+
self.state = :closed
|
94
|
+
logger.info "Circuit closed"
|
95
|
+
end
|
96
|
+
|
97
|
+
def handle_failure(e)
|
98
|
+
failure = Failure.new(e)
|
99
|
+
add_failure(failure)
|
100
|
+
logger.warn failure.to_s
|
101
|
+
if half_open? || failures.size >= failure_limit
|
102
|
+
self.state = :open
|
103
|
+
else
|
104
|
+
self.state = :closed
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def run_validations
|
109
|
+
logger_methods = [:debug, :info, :warn, :error]
|
110
|
+
if !logger_methods.all? { |e| logger.respond_to?(e) }
|
111
|
+
raise NotImplementedError.new("Your logger must respond to #{logger_methods}")
|
112
|
+
end
|
113
|
+
if !circuit.respond_to?(:call)
|
114
|
+
raise NotImplementedError.new("Your circuit must respond to #call")
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'json'
|
2
|
+
module CircuitBreaker
|
3
|
+
class Failure
|
4
|
+
def self.from_json(json)
|
5
|
+
failure = JSON.parse(json)
|
6
|
+
error = Object.const_get(failure["error"])
|
7
|
+
error = error.new(failure["message"])
|
8
|
+
new(error, Time.parse(failure["timestamp"]))
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(error, recorded = Time.now.utc)
|
12
|
+
@error = error
|
13
|
+
@recorded = recorded
|
14
|
+
end
|
15
|
+
|
16
|
+
def timestamp
|
17
|
+
recorded
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_s
|
21
|
+
"[#{error.class}] - #{error.message}"
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_json
|
25
|
+
JSON.generate({
|
26
|
+
error: error.class,
|
27
|
+
message: error.message,
|
28
|
+
timestamp: timestamp.to_s
|
29
|
+
})
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
attr_reader :error, :recorded
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module CircuitBreaker
|
2
|
+
class Memory
|
3
|
+
include CircuitBreaker
|
4
|
+
|
5
|
+
# The main runner, must respond to #call
|
6
|
+
# @return [Proc/Lambda] the runner
|
7
|
+
attr_accessor :circuit
|
8
|
+
# The count of failures
|
9
|
+
# @return [Integer] the amount of failures to permit. Defaults to 10 seconds
|
10
|
+
attr_accessor :failure_limit
|
11
|
+
# The amount of time in seconds before a breaker should reset if currently open. Defaults to 5
|
12
|
+
# @return [Integer]
|
13
|
+
attr_accessor :reset_timeout
|
14
|
+
# The current logger
|
15
|
+
# @return [Object] - The logger sent in at initialization. Defaults to ruby's std Logger class
|
16
|
+
attr_accessor :logger
|
17
|
+
# The current state
|
18
|
+
# @return [Symbol] should always return either :open, :closed or :half-open.
|
19
|
+
# Use the helper methods in lib/circuit_breaker.rb for more readable code.
|
20
|
+
attr_accessor :state
|
21
|
+
# The current failures
|
22
|
+
# @return [Array<CircuitBreaker::Failure>] - a list of failures serialized.
|
23
|
+
attr_accessor :failures
|
24
|
+
#
|
25
|
+
# @example create a new breaker
|
26
|
+
# breaker = CircuitBreaker::Memory.new do |cb|
|
27
|
+
# cb.circuit = -> (arg) { my_method(arg) }
|
28
|
+
# cb.failure_limit = 2
|
29
|
+
# cb.reset_timeout = 5
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# @yieldparam circuit - (look to {#circuit})
|
33
|
+
# @yieldparam failure_limit - (look to {#failure_limit})
|
34
|
+
# @yieldparam reset_timeout - (look to {#reset_timeout})
|
35
|
+
# @yieldparam logger - (look to {#logger})
|
36
|
+
# @return [CircuitBreaker::Memory] the object.
|
37
|
+
def initialize(&block)
|
38
|
+
yield self
|
39
|
+
@failure_limit ||= 5
|
40
|
+
@reset_timeout ||= 10
|
41
|
+
@logger ||= Logger.new(STDOUT)
|
42
|
+
@state = :closed
|
43
|
+
@failures = []
|
44
|
+
run_validations
|
45
|
+
end
|
46
|
+
|
47
|
+
# (look to {CircuitBreaker::add_failure})
|
48
|
+
def add_failure(failure)
|
49
|
+
failures << failure
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
module CircuitBreaker
|
2
|
+
class Redis
|
3
|
+
include CircuitBreaker
|
4
|
+
# (look to {CircuitBreaker::Memory#circuit})
|
5
|
+
attr_accessor :circuit
|
6
|
+
# (look to {CircuitBreaker::Memory#failure_limit})
|
7
|
+
attr_accessor :failure_limit
|
8
|
+
# (look to {CircuitBreaker::Memory#reset_timeout})
|
9
|
+
attr_accessor :reset_timeout
|
10
|
+
# (look to {CircuitBreaker::Memory#logger})
|
11
|
+
attr_accessor :logger
|
12
|
+
# (look to {CircuitBreaker::Memory#state})
|
13
|
+
attr_accessor :state
|
14
|
+
# (look to {CircuitBreaker::Memory#failures})
|
15
|
+
attr_accessor :failures
|
16
|
+
# A unique name that will be used across servers to
|
17
|
+
# sync state and failures. I'd recommend `your_class.name:your_method_name` or whatever
|
18
|
+
# is special about what's being invoked in the `circuit`. See examples/example_redis.rb
|
19
|
+
# @return [String] - namespace given
|
20
|
+
attr_accessor :namespace
|
21
|
+
|
22
|
+
# An instance of an redis client. This library does not have a
|
23
|
+
# hard dependency on a particular redis client but for testing I've used
|
24
|
+
# [redis-rb](https://github.com/redis/redis-rb). Whatever you pass in here simply has to
|
25
|
+
# implement a few redis commands such as `sadd`, `del`, `smembers`, `get` and `set`.
|
26
|
+
# The client will ensure these exist before the breaker can be instantiated.
|
27
|
+
# @return [Object] - redis client given
|
28
|
+
attr_accessor :client
|
29
|
+
|
30
|
+
# The main class to instantiate the CircuitBraker class.
|
31
|
+
#
|
32
|
+
# @example create a new breaker
|
33
|
+
# breaker = CircuitBreaker::Redis.new do |cb|
|
34
|
+
# cb.circuit = -> (arg) { my_method(arg) }
|
35
|
+
# cb.failure_limit = 2
|
36
|
+
# cb.reset_timeout = 5
|
37
|
+
# cb.client = redis_client
|
38
|
+
# cb.namespace = "some_key"
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# @yieldparam circuit - (look to {#circuit})
|
42
|
+
# @yieldparam failure_limit - (look to {#failure_limit})
|
43
|
+
# @yieldparam reset_timeout - (look to {#reset_timeout})
|
44
|
+
# @yieldparam logger - (look to {#logger})
|
45
|
+
# @yieldparam client - (look to {#client})
|
46
|
+
# @yieldparam namespace - (look to {#namespace})
|
47
|
+
# @return [CircuitBreaker::Redis] the object.
|
48
|
+
def initialize
|
49
|
+
yield self
|
50
|
+
@client = client
|
51
|
+
@namespace = namespace
|
52
|
+
@failure_limit ||= 5
|
53
|
+
@reset_timeout ||= 10
|
54
|
+
@logger = Logger.new(STDOUT)
|
55
|
+
run_validations
|
56
|
+
@namespace = "circuit_breaker:#{namespace}"
|
57
|
+
end
|
58
|
+
|
59
|
+
def state
|
60
|
+
redis_state = client.get(state_namespace)
|
61
|
+
return redis_state.to_sym if redis_state
|
62
|
+
# if there is no state stored in redis, set it.
|
63
|
+
self.state = :closed
|
64
|
+
end
|
65
|
+
|
66
|
+
def state=(state)
|
67
|
+
client.set(state_namespace, state.to_s)
|
68
|
+
end
|
69
|
+
|
70
|
+
def failures
|
71
|
+
redis_fails = client.smembers(fail_namespace)
|
72
|
+
return redis_fails.map { |f| Failure.from_json(f) } if redis_fails
|
73
|
+
# if there are no failures in redis, set it to empty
|
74
|
+
self.failures = []
|
75
|
+
[]
|
76
|
+
end
|
77
|
+
|
78
|
+
def add_failure(failure)
|
79
|
+
client.sadd(fail_namespace, failure.to_json)
|
80
|
+
end
|
81
|
+
|
82
|
+
# failures= requires we replace what is currently in redis
|
83
|
+
# with the new value so we delete all entries first then add
|
84
|
+
def failures=(failures)
|
85
|
+
client.del(fail_namespace)
|
86
|
+
failures.each { |f| client.sadd(fail_namespace, f.to_json) }
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def fail_namespace
|
92
|
+
"#{namespace}:failures"
|
93
|
+
end
|
94
|
+
|
95
|
+
def state_namespace
|
96
|
+
"#{namespace}:state"
|
97
|
+
end
|
98
|
+
|
99
|
+
def run_validations
|
100
|
+
# call super to ensure module has what it needs
|
101
|
+
super
|
102
|
+
redis_commands = [:smembers, :get, :set, :sadd, :del]
|
103
|
+
if !redis_commands.all? { |c| client.respond_to?(c) }
|
104
|
+
raise NotImplementedError.new("Missing Methods. Your client must implement: #{redis_commands}")
|
105
|
+
end
|
106
|
+
if !namespace
|
107
|
+
raise NotImplementedError.new("Missing namespace")
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
data/remote-loop.gif
ADDED
Binary file
|
data/remote.gif
ADDED
Binary file
|
metadata
ADDED
@@ -0,0 +1,179 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: adaptable_circuit_breaker
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Anthony Ross
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-03-02 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.13'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.13'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: pry
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0.10'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0.10'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: timecop
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0.8'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0.8'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: simplecov
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0.13'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0.13'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: yard
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0.9'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0.9'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: redis
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '3.3'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '3.3'
|
125
|
+
description: The Circuit Breaker pattern is used to prevent constant fail-over from
|
126
|
+
spotty remote systems
|
127
|
+
email:
|
128
|
+
- anthony.s.ross@gmail.com
|
129
|
+
executables: []
|
130
|
+
extensions: []
|
131
|
+
extra_rdoc_files: []
|
132
|
+
files:
|
133
|
+
- ".gitignore"
|
134
|
+
- ".rspec"
|
135
|
+
- ".travis.yml"
|
136
|
+
- CODE_OF_CONDUCT.md
|
137
|
+
- Gemfile
|
138
|
+
- LICENSE.txt
|
139
|
+
- README.md
|
140
|
+
- Rakefile
|
141
|
+
- bin/console
|
142
|
+
- bin/setup
|
143
|
+
- circuit_breaker.gemspec
|
144
|
+
- examples/example.rb
|
145
|
+
- examples/example_redis.rb
|
146
|
+
- lib/circuit_breaker.rb
|
147
|
+
- lib/circuit_breaker/failure.rb
|
148
|
+
- lib/circuit_breaker/memory.rb
|
149
|
+
- lib/circuit_breaker/open_error.rb
|
150
|
+
- lib/circuit_breaker/redis.rb
|
151
|
+
- lib/circuit_breaker/version.rb
|
152
|
+
- remote-loop.gif
|
153
|
+
- remote.gif
|
154
|
+
homepage: ''
|
155
|
+
licenses:
|
156
|
+
- MIT
|
157
|
+
metadata:
|
158
|
+
yard.run: yri
|
159
|
+
post_install_message:
|
160
|
+
rdoc_options: []
|
161
|
+
require_paths:
|
162
|
+
- lib
|
163
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
164
|
+
requirements:
|
165
|
+
- - ">="
|
166
|
+
- !ruby/object:Gem::Version
|
167
|
+
version: '0'
|
168
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
169
|
+
requirements:
|
170
|
+
- - ">="
|
171
|
+
- !ruby/object:Gem::Version
|
172
|
+
version: '0'
|
173
|
+
requirements: []
|
174
|
+
rubyforge_project:
|
175
|
+
rubygems_version: 2.6.8
|
176
|
+
signing_key:
|
177
|
+
specification_version: 4
|
178
|
+
summary: Circuit Breaker pattern
|
179
|
+
test_files: []
|