breakers 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 +52 -0
- data/.rspec +2 -0
- data/.rubocop.yml +217 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/LICENSE.md +31 -0
- data/README.md +163 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/breakers.gemspec +36 -0
- data/lib/breakers.rb +29 -0
- data/lib/breakers/client.rb +21 -0
- data/lib/breakers/outage.rb +93 -0
- data/lib/breakers/service.rb +116 -0
- data/lib/breakers/uptime_middleware.rb +87 -0
- data/lib/breakers/version.rb +3 -0
- metadata +222 -0
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
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
data/Gemfile
ADDED
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
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
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
|
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: []
|