adaptable_circuit_breaker 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ .ruby-version
11
+ Gemfile.lock
12
+ *.swp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.4.0
5
+ before_install: gem install bundler -v 1.13.7
@@ -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
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in circuit_breaker.gemspec
4
+ gemspec
@@ -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.
@@ -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).
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "circuit_breaker"
5
+
6
+ require "irb"
7
+ IRB.start
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
@@ -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
@@ -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,4 @@
1
+ module CircuitBreaker
2
+ class OpenError < StandardError
3
+ end
4
+ 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
@@ -0,0 +1,3 @@
1
+ module CircuitBreaker
2
+ VERSION = "0.1.0"
3
+ end
Binary file
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: []