breakers 0.1.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
+ SHA1:
3
+ metadata.gz: 60d7df3e3a44461c64b30339dd20504836c1458e
4
+ data.tar.gz: a958353efaaa1fec1b980fb1d166b3a365576590
5
+ SHA512:
6
+ metadata.gz: 863f61f5192713f9fe300c7300b64c2eeb30c17e00c888e1f4b70843ea21347c3128cd5dab4cdafbc4018148e7f0e7f14d9f6639bc6c3d2a29df2a8821712ed6
7
+ data.tar.gz: c9ed0d652b46945975d130d05baea3af3f55af2630157403f3235486880fbd0aea04be41dce60c4e7ed9db36a658f5576a60352d1ab92078bc68d62e48ee50bc
data/.gitignore ADDED
@@ -0,0 +1,52 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /spec/examples.txt
9
+ /test/tmp/
10
+ /test/version_tmp/
11
+ /tmp/
12
+
13
+ # Used by dotenv library to load environment variables.
14
+ # .env
15
+
16
+ ## Specific to RubyMotion:
17
+ .dat*
18
+ .repl_history
19
+ build/
20
+ *.bridgesupport
21
+ build-iPhoneOS/
22
+ build-iPhoneSimulator/
23
+
24
+ ## Specific to RubyMotion (use of CocoaPods):
25
+ #
26
+ # We recommend against adding the Pods directory to your .gitignore. However
27
+ # you should judge for yourself, the pros and cons are mentioned at:
28
+ # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
29
+ #
30
+ # vendor/Pods/
31
+
32
+ ## Documentation cache and generated files:
33
+ /.yardoc/
34
+ /_yardoc/
35
+ /doc/
36
+ /rdoc/
37
+
38
+ ## Environment normalization:
39
+ /.bundle/
40
+ /vendor/bundle
41
+ /lib/bundler/man/
42
+
43
+ # for a library or gem, you might want to ignore these files since the code is
44
+ # intended to run in multiple environments; otherwise, check them in:
45
+ Gemfile.lock
46
+ .ruby-version
47
+ .ruby-gemset
48
+
49
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
50
+ .rvmrc
51
+
52
+ /.byebug_history
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.rubocop.yml ADDED
@@ -0,0 +1,217 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.3
3
+ Include:
4
+ - 'Rakefile'
5
+
6
+ Metrics/LineLength:
7
+ Max: 140
8
+
9
+ # Removes the requirement for using double quotes only for string interpolation.
10
+ Style/StringLiterals:
11
+ Enabled: true
12
+
13
+ # These complexity and length metrics tend to require a bunch of high-touch refactoring
14
+ # in existing projects. Leaving them high for now, and we can slowly lower them to standard
15
+ # levels in the near future.
16
+ Metrics/ModuleLength:
17
+ Max: 200
18
+
19
+ Metrics/ClassLength:
20
+ Max: 230
21
+
22
+ Metrics/MethodLength:
23
+ Max: 50
24
+
25
+ Metrics/AbcSize:
26
+ Max: 75
27
+
28
+ Metrics/CyclomaticComplexity:
29
+ Max: 20
30
+
31
+ Metrics/PerceivedComplexity:
32
+ Max: 20
33
+
34
+ # Allow long keyword parameter lists
35
+ Metrics/ParameterLists:
36
+ Max: 15
37
+ CountKeywordArgs: false
38
+
39
+ # This enforces bad style and can break things.
40
+ # See: https://github.com/bbatsov/rubocop/issues/2614
41
+ Performance/Casecmp:
42
+ Enabled: false
43
+
44
+ # This requires the use of alias rather than alias_method, which seems totally arbitrary
45
+ Style/Alias:
46
+ Enabled: false
47
+
48
+ # This cop enforces that submodules/subclasses be defined like this:
49
+ #
50
+ # class Foo::Bar
51
+ #
52
+ # rather than like this:
53
+ #
54
+ # module Foo
55
+ # class Bar
56
+ #
57
+ # This is actually semantically different, and there are valid reasons for wanting to use the latter
58
+ # form because of the way the former does funky stuff to the namespace.
59
+ Style/ClassAndModuleChildren:
60
+ Enabled: false
61
+
62
+ # This forces you to use class instance variables rather than class variables, which seems pretty
63
+ # situation-specific
64
+ Style/ClassVars:
65
+ Enabled: false
66
+
67
+ # This makes you do things like this:
68
+ # variable = if test
69
+ # 'abc-123'
70
+ # else
71
+ # 'def-456'
72
+ # end
73
+ #
74
+ # I think this is harder to read than assigning the variable within the conditional.
75
+ Style/ConditionalAssignment:
76
+ Enabled: false
77
+
78
+ # This cop forces you to put a return at the beginning of a block of code rather than having an if statement
79
+ # whose body carries to the end of the function. For example:
80
+ #
81
+ # def foo
82
+ # ...
83
+ # if test
84
+ # ...
85
+ # end
86
+ # end
87
+ #
88
+ # would be considered bad, and the cop would force you to put a `return if !test` before that block and
89
+ # then remove the if. The problem is that this hides intent, since the if test does have a purpose in
90
+ # readability, and it could also be easier for future changes to miss the return statement and add code
91
+ # after it expecting it to be executed.
92
+ Style/GuardClause:
93
+ Enabled: false
94
+
95
+ # This is pretty much the same thing as the one above. Inside a loop, it forces you to use next to skip
96
+ # iteration rather than using an if block that runs to the end of the loop, and it suffers from the same
97
+ # problems as above.
98
+ Style/Next:
99
+ Enabled: false
100
+
101
+ Style/IndentArray:
102
+ EnforcedStyle: consistent
103
+
104
+ # This forces you to change simple if/unless blocks to the conditional form like: `return 2 if badness`.
105
+ # Unfortunately there are a number of cases where it makes sense to use the block form even for simple statements,
106
+ # and the modifier form can be easy to miss when scanning code.
107
+ Style/IfUnlessModifier:
108
+ Enabled: false
109
+
110
+ # This requires you to implement respond_to_missing? anywhere that you implement method_missing, but I think that
111
+ # is a lof of a pain.
112
+ Style/MethodMissing:
113
+ Enabled: false
114
+
115
+ # This cop forces the use of unless in all negated if statements. Since unless is a source of so many arguments
116
+ # and there seems to be no purpose in enforcing its use, disable it.
117
+ Style/NegatedIf:
118
+ Enabled: false
119
+
120
+ # This will force you to use methods like .positive? and .zero? rather than > 0 and == 0. But why?
121
+ Style/NumericPredicate:
122
+ Enabled: false
123
+
124
+ # This one enforces that functions with names like has_value? be renamed to value?. There are many cases where
125
+ # doing so would make the code more difficult to parse.
126
+ Style/PredicateName:
127
+ Enabled: false
128
+
129
+ # By default this will force you to use specific names for arguments for enumerable and other methods,
130
+ # which I don't understand even a little bit.
131
+ Style/SingleLineBlockParams:
132
+ Methods: []
133
+
134
+ # This rule disallows you from parenthesizing the test in ternary operations, so that:
135
+ # (p == 100) ? 'success' : ''
136
+ # must be written as:
137
+ # p == 100 ? 'success' : ''
138
+ # I can't possibly be the only one who finds the latter a bit harder to read, can I?
139
+ Style/TernaryParentheses:
140
+ Enabled: false
141
+
142
+ # Allow trivial methods that have ? at the end.
143
+ Style/TrivialAccessors:
144
+ AllowPredicates: true
145
+
146
+ # It's ok to make a small array of words without using a %w
147
+ Style/WordArray:
148
+ MinSize: 5
149
+
150
+ # Some people really like to put lines at the beginning and end of class bodies, while other people
151
+ # really don't. It doesn't really seem to matter.
152
+ Style/EmptyLinesAroundClassBody:
153
+ Enabled: false
154
+
155
+ # This forces you to put a comment like this at the top of every single file:
156
+ # frozen_string_literal: true
157
+ # In Ruby 3, string literals will be frozen by default, so doing so future-proofs
158
+ # the code, but in the meantime it's a huge pain in the ass.
159
+ Style/FrozenStringLiteralComment:
160
+ Enabled: false
161
+
162
+ # this forces you to use the lambda keyword rather than -> for multiline lambdas, which seems totally arbitrary
163
+ Style/Lambda:
164
+ Enabled: false
165
+
166
+ # Force indentation for milti-line expressions and method calls
167
+ Style/MultilineOperationIndentation:
168
+ EnforcedStyle: indented
169
+
170
+ Style/MultilineMethodCallIndentation:
171
+ EnforcedStyle: indented
172
+
173
+ # This disallows the use of $1, $2 from regular expressions, which seems to make no sense whatsoever
174
+ Style/PerlBackrefs:
175
+ Enabled: false
176
+
177
+ # This enforces that multi-line array literals do not end in a comma. For example:
178
+ #
179
+ # foo = [
180
+ # 1,
181
+ # 2
182
+ # ]
183
+ Style/TrailingCommaInLiteral:
184
+ EnforcedStyleForMultiline: no_comma
185
+
186
+ # Same as above but for method arguments rather than array entries.
187
+ Style/TrailingCommaInArguments:
188
+ EnforcedStyleForMultiline: no_comma
189
+
190
+ # This forces you to replace things like: `[1, 2, 3].length == 0` with `[1,2,3].empty?`. The problem is that
191
+ # not all things that implement length also implement empty? so you will get errors that cannot be resolved,
192
+ # and the cop will encourage you to do things that are incorrect.
193
+ Style/ZeroLengthPredicate:
194
+ Enabled: false
195
+
196
+ # Enforce alignment of multi-line assignments to be like this:
197
+ # variable = if test
198
+ # ...
199
+ # end
200
+ Lint/EndAlignment:
201
+ AlignWith: variable
202
+
203
+ # This cop will require you to replace or prefix method arguments that go unused with underscores. The problem
204
+ # is that while seeming to solve no problem this could easily cause issues where someone editing the code to
205
+ # begin using the variable forgets to remove the underscore. Also, if you replace the argument with _, then
206
+ # information about the meaning of that argument is lost.
207
+ Lint/UnusedMethodArgument:
208
+ Enabled: false
209
+
210
+ # Same as above but with block arguments.
211
+ Lint/UnusedBlockArgument:
212
+ Enabled: false
213
+
214
+ # This cop forces all rescue blocks to do something with the exception. Sometimes you just have an exception
215
+ # you want to rescue but do nothing about.
216
+ Lint/HandleExceptions:
217
+ Enabled: false
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.1
5
+ before_install: gem install bundler -v 1.13.1
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in breakers.gemspec
4
+ gemspec
data/LICENSE.md ADDED
@@ -0,0 +1,31 @@
1
+ As a work of the United States Government, this project is in the
2
+ public domain within the United States.
3
+
4
+ Additionally, we waive copyright and related rights in the work
5
+ worldwide through the CC0 1.0 Universal public domain dedication.
6
+
7
+ ## CC0 1.0 Universal Summary
8
+
9
+ This is a human-readable summary of the [Legal Code (read the full text)](https://creativecommons.org/publicdomain/zero/1.0/legalcode).
10
+
11
+ ### No Copyright
12
+
13
+ The person who associated a work with this deed has dedicated the work to
14
+ the public domain by waiving all of his or her rights to the work worldwide
15
+ under copyright law, including all related and neighboring rights, to the
16
+ extent allowed by law.
17
+
18
+ You can copy, modify, distribute and perform the work, even for commercial
19
+ purposes, all without asking permission.
20
+
21
+ ### Other Information
22
+
23
+ In no way are the patent or trademark rights of any person affected by CC0,
24
+ nor are the rights that other persons may have in the work or in how the
25
+ work is used, such as publicity or privacy rights.
26
+
27
+ Unless expressly stated otherwise, the person who associated a work with
28
+ this deed makes no warranties about the work, and disclaims liability for
29
+ all uses of the work, to the fullest extent permitted by applicable law.
30
+ When using or citing the work, you should not imply endorsement by the
31
+ author or the affirmer.
data/README.md ADDED
@@ -0,0 +1,163 @@
1
+ # Breakers
2
+
3
+ Breakers is a Ruby gem that implements the circuit breaker pattern for Ruby using a Faraday middleware. It is designed to handle the case
4
+ where your app communicates with one or more backend services over HTTP and those services could possibly go down. Data about the success
5
+ and failure of requests is recorded in Redis, and the gem uses this to determine when an outage occurs. While a service is marked as down,
6
+ requests will continue to flow through occasionally to check if it has returned to being alive.
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ ```ruby
13
+ gem 'breakers'
14
+ ```
15
+
16
+ And then execute:
17
+
18
+ $ bundle
19
+
20
+ Or install it yourself as:
21
+
22
+ $ gem install breakers
23
+
24
+ ## Quick Start
25
+
26
+ ```ruby
27
+ service = Breakers::Service.new(
28
+ name: 'messaging',
29
+ request_matcher: proc { |request_env| request_env.url.host =~ /.*messaging\.va\.gov/ }
30
+ )
31
+
32
+ client = Breakers::Client.new(redis_connection: redis, services: [service])
33
+
34
+ Breakers.set_client(client)
35
+
36
+ connection = Faraday.new do |conn|
37
+ conn.use :breakers
38
+ conn.adapter Faraday.default_adapter
39
+ end
40
+
41
+ response = connection.get 'http://messaging.va.gov/query'
42
+ ```
43
+
44
+ This will track all requests to messaging.va.gov and will stop sending requests to it for one minute when the error rate reaches 50% over a
45
+ two minute period.
46
+
47
+ ## Usage
48
+
49
+ For more advanced usage and an explanation of the code above, keep reading.
50
+
51
+ ### Services
52
+
53
+ In an application where you rely on a number of backend services with different endpoints, outage characteristics, and levels of reliability,
54
+ breakers lets you configure each of those services globally and then apply a Faraday middleware that uses them to track changes. Services
55
+ are defined like this:
56
+
57
+ ```ruby
58
+ service = Breakers::Service.new(
59
+ name: 'messaging',
60
+ request_matcher: proc { |request_env| request_env.url.host =~ /.*messaging\.va\.gov/ },
61
+ seconds_before_retry: 60,
62
+ error_threshold: 50
63
+ )
64
+ ```
65
+
66
+ The name parameter is used for logging and reporting only. On each request, the block will be called with the request's environment, and
67
+ the block should return true if the service applies to it.
68
+
69
+ Each service can be further configured with the following:
70
+
71
+ * `seconds_before_retry` - The number of seconds to wait before sending a new request when an outage is reported. Every N seconds, a new request will be sent, and if it succeeds the outage will be ended. Defaults to 60.
72
+ * `error_threshold` - The percentage of errors over which an outage will be reported. Defaults to 50.
73
+ * `data_retention_seconds` - The number of seconds for which data will be stored in Redis for successful and unsuccessful request counts. See below for information on the structure of data within Redis. Defaults to 30 days.
74
+
75
+ ### Client
76
+
77
+ A Breakers::Client is the data structure that contains all of the information needed to operate the system, and it provides a query API for
78
+ accessing the current state. It is initialized with a redis connection and one or more services, with options for a set of plugins and a logger:
79
+
80
+ ```ruby
81
+ client = Breakers::Client.new(
82
+ redis_connection: redis,
83
+ services: [service],
84
+ logger: logger,
85
+ plugins: [plugin]
86
+ )
87
+ ```
88
+
89
+ The logger should conform to Ruby's Logger API. See more information on plugins below.
90
+
91
+ ### Global Configuration
92
+
93
+ The client can be configured globally with:
94
+
95
+ ```ruby
96
+ Breakers.set_client(client)
97
+ ```
98
+
99
+ In a Rails app, it makes sense to create the services and client in an initializer and then apply them with this call. If you would like to
100
+ namespace the data in Redis with a prefix, you can make that happen with:
101
+
102
+ ```ruby
103
+ Breakers.redis_prefix = 'custom-'
104
+ ```
105
+
106
+ The default prefix is brk-.
107
+
108
+ ### Using the Middleware
109
+
110
+ Once the global configuration is in place, use the middleware as you would normally in Faraday:
111
+
112
+ ```ruby
113
+ Faraday.new('http://va.gov') do |conn|
114
+ conn.use :breakers
115
+ conn.adapter Faraday.default_adapter
116
+ end
117
+ ```
118
+
119
+ ### Logging
120
+
121
+ The client takes an optional `logger:` argument that can accept an object that conforms to Ruby's Logger interface. If provided, it will
122
+ log on request errors and outage beginnings and endings.
123
+
124
+ ### Plugins
125
+
126
+ If you would like to track events in another way, you can also pass plugins to the client with the `plugins:` argument. Plugins should
127
+ be instances that implement the following interface:
128
+
129
+ ```ruby
130
+ class ExamplePlugin
131
+ def on_outage_begin(outage); end
132
+
133
+ def on_outage_end(outage); end
134
+
135
+ def on_error(service, request_env, response_env); end
136
+
137
+ def on_success(service, request_env, response_env); end
138
+ end
139
+ ```
140
+
141
+ It's ok for your plugin to implement only part of this interface.
142
+
143
+ ### Redis Data Structure
144
+
145
+ Data is stored in Redis with the following structure:
146
+
147
+ * {prefix}-{service_name}-errors-{unix_timestamp} - A set of keys that store the number of errors by service for each minute. By default these are kept for one month, but you can customize that timestamp with the `data_retention_seconds` argument when creating a service.
148
+ * {prefix}-{service_name}-successes-{unix_timestamp} - Same as above but counts for successful requests.
149
+ * {prefix}-{service_name}-outages - A sorted set that stores the actual outages. The sort value is the unix timestamp at which the outage occurred, and each entry stores a JSON document containing the start and end times for the outage.
150
+
151
+ ## Development
152
+
153
+ 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.
154
+
155
+ 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).
156
+
157
+ ## Contributing
158
+
159
+ Bug reports and pull requests are welcome on GitHub at https://github.com/department-of-veterans-affairs/breakers.
160
+
161
+ ## License
162
+
163
+ The gem is available as open source under the terms of the Creative Commons Zero 1.0 Universal License.
data/Rakefile ADDED
@@ -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
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'breakers'
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require 'irb'
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/breakers.gemspec ADDED
@@ -0,0 +1,36 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'breakers/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'breakers'
8
+ spec.version = Breakers::VERSION
9
+ spec.authors = ['Aubrey Holland']
10
+ spec.email = ['aubrey@adhocteam.us']
11
+
12
+ spec.summary = 'Handle outages to backend systems with a Faraday middleware'
13
+ spec.description = 'This is a Faraday middleware that detects backend outages and reacts to them'
14
+ spec.homepage = 'https://github.com/department-of-veterans-affairs/breakers'
15
+ spec.license = 'CC0-1.0'
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.add_dependency 'faraday', ['>= 0.7.4', '< 0.10']
25
+ spec.add_dependency 'multi_json', '~> 1.0'
26
+
27
+ spec.add_development_dependency 'bundler', '~> 1.0'
28
+ spec.add_development_dependency 'byebug', '~> 9.0'
29
+ spec.add_development_dependency 'fakeredis', '~> 0.6.0'
30
+ spec.add_development_dependency 'rake', '~> 11.0'
31
+ spec.add_development_dependency 'rspec', '~> 3.0'
32
+ spec.add_development_dependency 'rubocop', '0.43.0'
33
+ spec.add_development_dependency 'simplecov', '~> 0.12.0'
34
+ spec.add_development_dependency 'timecop', '~> 0.8.0'
35
+ spec.add_development_dependency 'webmock', '~> 2.1'
36
+ end
data/lib/breakers.rb ADDED
@@ -0,0 +1,29 @@
1
+ require 'breakers/client'
2
+ require 'breakers/outage'
3
+ require 'breakers/service'
4
+ require 'breakers/uptime_middleware'
5
+ require 'breakers/version'
6
+
7
+ require 'faraday'
8
+
9
+ module Breakers
10
+ Faraday::Middleware.register_middleware(breakers: lambda { UptimeMiddleware })
11
+
12
+ # rubocop:disable Style/AccessorMethodName
13
+ def self.set_client(client)
14
+ @client = client
15
+ end
16
+ # rubocop:enable Style/AccessorMethodName
17
+
18
+ def self.client
19
+ @client
20
+ end
21
+
22
+ def self.redis_prefix=(prefix)
23
+ @redis_prefix = prefix
24
+ end
25
+
26
+ def self.redis_prefix
27
+ @redis_prefix || 'brk-'
28
+ end
29
+ end
@@ -0,0 +1,21 @@
1
+ module Breakers
2
+ class Client
3
+ attr_reader :services
4
+ attr_reader :plugins
5
+ attr_reader :redis_connection
6
+ attr_reader :logger
7
+
8
+ def initialize(redis_connection:, services:, plugins: nil, logger: nil)
9
+ @redis_connection = redis_connection
10
+ @services = Array(services)
11
+ @plugins = Array(plugins)
12
+ @logger = logger
13
+ end
14
+
15
+ def service_for_request(request_env:)
16
+ @services.find do |service|
17
+ service.handles_request?(request_env)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,93 @@
1
+ require 'multi_json'
2
+
3
+ module Breakers
4
+ class Outage
5
+ attr_reader :service
6
+ attr_reader :body
7
+
8
+ def self.find_last(service:)
9
+ data = Breakers.client.redis_connection.zrange(outages_key(service: service), -1, -1)[0]
10
+ data && new(service: service, data: data)
11
+ end
12
+
13
+ def self.in_range(service:, start_time:, end_time:)
14
+ data = Breakers.client.redis_connection.zrangebyscore(
15
+ outages_key(service: service),
16
+ start_time.to_i,
17
+ end_time.to_i
18
+ )
19
+ data.map { |item| new(service: service, data: item) }
20
+ end
21
+
22
+ def self.create(service:)
23
+ data = MultiJson.dump(start_time: Time.now.utc.to_i)
24
+ Breakers.client.redis_connection.zadd(outages_key(service: service), Time.now.utc.to_i, data)
25
+
26
+ Breakers.client.logger&.error(msg: 'Breakers outage beginning', service: service.name)
27
+
28
+ Breakers.client.plugins.each do |plugin|
29
+ plugin.on_outage_begin(Outage.new(service: service, data: data)) if plugin.respond_to?(:on_outage_begin)
30
+ end
31
+ end
32
+
33
+ def self.outages_key(service:)
34
+ "#{Breakers.redis_prefix}#{service.name}-outages"
35
+ end
36
+
37
+ def initialize(service:, data:)
38
+ @body = MultiJson.load(data)
39
+ @service = service
40
+ end
41
+
42
+ def ended?
43
+ @body.key?('end_time')
44
+ end
45
+
46
+ def end!
47
+ new_body = @body.dup
48
+ new_body['end_time'] = Time.now.utc.to_i
49
+ replace_body(body: new_body)
50
+
51
+ Breakers.client.logger&.info(msg: 'Breakers outage ending', service: @service.name)
52
+ Breakers.client.plugins.each do |plugin|
53
+ plugin.on_outage_end(self) if plugin.respond_to?(:on_outage_begin)
54
+ end
55
+ end
56
+
57
+ def start_time
58
+ @body['start_time'] && Time.at(@body['start_time']).utc
59
+ end
60
+
61
+ def end_time
62
+ @body['end_time'] && Time.at(@body['end_time']).utc
63
+ end
64
+
65
+ def last_test_time
66
+ (@body['last_test_time'] && Time.at(@body['last_test_time']).utc) || start_time
67
+ end
68
+
69
+ def update_last_test_time!
70
+ new_body = @body.dup
71
+ new_body['last_test_time'] = Time.now.utc.to_i
72
+ replace_body(body: new_body)
73
+ end
74
+
75
+ def ready_for_retest?(wait_seconds:)
76
+ (Time.now.utc - last_test_time) > wait_seconds
77
+ end
78
+
79
+ protected
80
+
81
+ def key
82
+ "#{Breakers.redis_prefix}#{@service.name}-outages"
83
+ end
84
+
85
+ def replace_body(body:)
86
+ Breakers.client.redis_connection.multi do
87
+ Breakers.client.redis_connection.zrem(key, MultiJson.dump(@body))
88
+ Breakers.client.redis_connection.zadd(key, start_time.to_i, MultiJson.dump(body))
89
+ end
90
+ @body = body
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,116 @@
1
+ module Breakers
2
+ class Service
3
+ DEFAULT_OPTS = {
4
+ seconds_before_retry: 60,
5
+ error_threshold: 50,
6
+ data_retention_seconds: 60 * 60 * 24 * 30
7
+ }.freeze
8
+
9
+ def initialize(opts)
10
+ @configuration = DEFAULT_OPTS.merge(opts)
11
+ end
12
+
13
+ def name
14
+ @configuration[:name]
15
+ end
16
+
17
+ def handles_request?(request_env)
18
+ @configuration[:request_matcher].call(request_env)
19
+ end
20
+
21
+ def seconds_before_retry
22
+ @configuration[:seconds_before_retry]
23
+ end
24
+
25
+ def add_error
26
+ increment_key(key: errors_key)
27
+ maybe_create_outage
28
+ end
29
+
30
+ def add_success
31
+ increment_key(key: successes_key)
32
+ end
33
+
34
+ def last_outage
35
+ Outage.find_last(service: self)
36
+ end
37
+
38
+ def outages_in_range(start_time:, end_time:)
39
+ Outage.in_range(
40
+ service: self,
41
+ start_time: start_time,
42
+ end_time: end_time
43
+ )
44
+ end
45
+
46
+ def successes_in_range(start_time:, end_time:, sample_seconds: 3600)
47
+ values_in_range(start_time: start_time, end_time: end_time, type: :successes, sample_seconds: sample_seconds)
48
+ end
49
+
50
+ def errors_in_range(start_time:, end_time:, sample_seconds: 3600)
51
+ values_in_range(start_time: start_time, end_time: end_time, type: :errors, sample_seconds: sample_seconds)
52
+ end
53
+
54
+ protected
55
+
56
+ def errors_key(time: nil)
57
+ "#{Breakers.redis_prefix}#{name}-errors-#{align_time_on_minute(time: time).to_i}"
58
+ end
59
+
60
+ def successes_key(time: nil)
61
+ "#{Breakers.redis_prefix}#{name}-successes-#{align_time_on_minute(time: time).to_i}"
62
+ end
63
+
64
+ def values_in_range(start_time:, end_time:, type:, sample_seconds:)
65
+ start_time = align_time_on_minute(time: start_time)
66
+ end_time = align_time_on_minute(time: end_time)
67
+ keys = []
68
+ times = []
69
+ while start_time <= end_time
70
+ times << start_time
71
+ if type == :errors
72
+ keys << errors_key(time: start_time)
73
+ elsif type == :successes
74
+ keys << successes_key(time: start_time)
75
+ end
76
+ start_time += sample_seconds
77
+ end
78
+ Breakers.client.redis_connection.mget(keys).each_with_index.map do |value, idx|
79
+ { count: value.to_i, time: times[idx] }
80
+ end
81
+ end
82
+
83
+ def increment_key(key:)
84
+ Breakers.client.redis_connection.multi do
85
+ Breakers.client.redis_connection.incr(key)
86
+ Breakers.client.redis_connection.expire(key, @configuration[:data_retention_seconds])
87
+ end
88
+ end
89
+
90
+ # Take the current or given time and round it down to the nearest minute
91
+ def align_time_on_minute(time: nil)
92
+ time = (time || Time.now.utc).to_i
93
+ time - (time % 60)
94
+ end
95
+
96
+ def maybe_create_outage
97
+ data = Breakers.client.redis_connection.multi do
98
+ Breakers.client.redis_connection.get(errors_key(time: Time.now.utc))
99
+ Breakers.client.redis_connection.get(errors_key(time: Time.now.utc - 60))
100
+ Breakers.client.redis_connection.get(successes_key(time: Time.now.utc))
101
+ Breakers.client.redis_connection.get(successes_key(time: Time.now.utc - 60))
102
+ end
103
+ failure_count = data[0].to_i + data[1].to_i
104
+ success_count = data[2].to_i + data[3].to_i
105
+
106
+ if failure_count > 0 && success_count == 0
107
+ Outage.create(service: self)
108
+ else
109
+ failure_rate = failure_count / (failure_count + success_count).to_f
110
+ if failure_rate >= @configuration[:error_threshold] / 100.0
111
+ Outage.create(service: self)
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,87 @@
1
+ require 'faraday'
2
+ require 'multi_json'
3
+
4
+ module Breakers
5
+ class UptimeMiddleware < Faraday::Middleware
6
+ def initialize(app)
7
+ super(app)
8
+ end
9
+
10
+ def call(request_env)
11
+ service = Breakers.client.service_for_request(request_env: request_env)
12
+
13
+ if !service
14
+ return @app.call(request_env)
15
+ end
16
+
17
+ last_outage = service.last_outage
18
+
19
+ if last_outage && !last_outage.ended?
20
+ if last_outage.ready_for_retest?(wait_seconds: service.seconds_before_retry)
21
+ handle_request(service: service, request_env: request_env, current_outage: last_outage)
22
+ else
23
+ outage_response(outage: last_outage, service: service)
24
+ end
25
+ else
26
+ handle_request(service: service, request_env: request_env)
27
+ end
28
+ end
29
+
30
+ protected
31
+
32
+ def outage_response(outage:, service:)
33
+ Faraday::Response.new.tap do |response|
34
+ response.finish(
35
+ status: 503,
36
+ body: "Outage detected on #{service.name} beginning at #{outage.start_time.to_i}",
37
+ response_headers: {}
38
+ )
39
+ end
40
+ end
41
+
42
+ def handle_request(service:, request_env:, current_outage: nil)
43
+ return @app.call(request_env).on_complete do |response_env|
44
+ if response_env.status >= 500
45
+ handle_error(
46
+ service: service,
47
+ request_env: request_env,
48
+ response_env: response_env,
49
+ error: response_env.status,
50
+ current_outage: current_outage
51
+ )
52
+ else
53
+ service.add_success
54
+ current_outage&.end!
55
+
56
+ Breakers.client.plugins.each do |plugin|
57
+ plugin.on_success(service, request_env, response_env) if plugin.respond_to?(:on_success)
58
+ end
59
+ end
60
+ end
61
+ rescue => e
62
+ handle_error(
63
+ service: service,
64
+ request_env: request_env,
65
+ response_env: nil,
66
+ error: "#{e.class.name} - #{e.message}",
67
+ current_outage: current_outage
68
+ )
69
+ raise
70
+ end
71
+
72
+ def handle_error(service:, request_env:, response_env:, error:, current_outage: nil)
73
+ service.add_error
74
+ current_outage&.update_last_test_time!
75
+
76
+ Breakers.client.logger&.warn(
77
+ msg: 'Breakers failed request',
78
+ service: service.name,
79
+ url: request_env.url.to_s,
80
+ error: error
81
+ )
82
+ Breakers.client.plugins.each do |plugin|
83
+ plugin.on_error(service, request_env, response_env) if plugin.respond_to?(:on_error)
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,3 @@
1
+ module Breakers
2
+ VERSION = '0.1.0'.freeze
3
+ end
metadata ADDED
@@ -0,0 +1,222 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: breakers
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Aubrey Holland
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-10-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 0.7.4
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '0.10'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: 0.7.4
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '0.10'
33
+ - !ruby/object:Gem::Dependency
34
+ name: multi_json
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.0'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: bundler
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.0'
61
+ - !ruby/object:Gem::Dependency
62
+ name: byebug
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '9.0'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '9.0'
75
+ - !ruby/object:Gem::Dependency
76
+ name: fakeredis
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: 0.6.0
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: 0.6.0
89
+ - !ruby/object:Gem::Dependency
90
+ name: rake
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '11.0'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '11.0'
103
+ - !ruby/object:Gem::Dependency
104
+ name: rspec
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '3.0'
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '3.0'
117
+ - !ruby/object:Gem::Dependency
118
+ name: rubocop
119
+ requirement: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - '='
122
+ - !ruby/object:Gem::Version
123
+ version: 0.43.0
124
+ type: :development
125
+ prerelease: false
126
+ version_requirements: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - '='
129
+ - !ruby/object:Gem::Version
130
+ version: 0.43.0
131
+ - !ruby/object:Gem::Dependency
132
+ name: simplecov
133
+ requirement: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - "~>"
136
+ - !ruby/object:Gem::Version
137
+ version: 0.12.0
138
+ type: :development
139
+ prerelease: false
140
+ version_requirements: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - "~>"
143
+ - !ruby/object:Gem::Version
144
+ version: 0.12.0
145
+ - !ruby/object:Gem::Dependency
146
+ name: timecop
147
+ requirement: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - "~>"
150
+ - !ruby/object:Gem::Version
151
+ version: 0.8.0
152
+ type: :development
153
+ prerelease: false
154
+ version_requirements: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - "~>"
157
+ - !ruby/object:Gem::Version
158
+ version: 0.8.0
159
+ - !ruby/object:Gem::Dependency
160
+ name: webmock
161
+ requirement: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - "~>"
164
+ - !ruby/object:Gem::Version
165
+ version: '2.1'
166
+ type: :development
167
+ prerelease: false
168
+ version_requirements: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - "~>"
171
+ - !ruby/object:Gem::Version
172
+ version: '2.1'
173
+ description: This is a Faraday middleware that detects backend outages and reacts
174
+ to them
175
+ email:
176
+ - aubrey@adhocteam.us
177
+ executables: []
178
+ extensions: []
179
+ extra_rdoc_files: []
180
+ files:
181
+ - ".gitignore"
182
+ - ".rspec"
183
+ - ".rubocop.yml"
184
+ - ".travis.yml"
185
+ - Gemfile
186
+ - LICENSE.md
187
+ - README.md
188
+ - Rakefile
189
+ - bin/console
190
+ - bin/setup
191
+ - breakers.gemspec
192
+ - lib/breakers.rb
193
+ - lib/breakers/client.rb
194
+ - lib/breakers/outage.rb
195
+ - lib/breakers/service.rb
196
+ - lib/breakers/uptime_middleware.rb
197
+ - lib/breakers/version.rb
198
+ homepage: https://github.com/department-of-veterans-affairs/breakers
199
+ licenses:
200
+ - CC0-1.0
201
+ metadata: {}
202
+ post_install_message:
203
+ rdoc_options: []
204
+ require_paths:
205
+ - lib
206
+ required_ruby_version: !ruby/object:Gem::Requirement
207
+ requirements:
208
+ - - ">="
209
+ - !ruby/object:Gem::Version
210
+ version: '0'
211
+ required_rubygems_version: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - ">="
214
+ - !ruby/object:Gem::Version
215
+ version: '0'
216
+ requirements: []
217
+ rubyforge_project:
218
+ rubygems_version: 2.5.1
219
+ signing_key:
220
+ specification_version: 4
221
+ summary: Handle outages to backend systems with a Faraday middleware
222
+ test_files: []