protoboard 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.rspec +3 -0
- data/.rubocop.yml +26 -0
- data/.travis.yml +9 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +51 -0
- data/LICENSE.txt +21 -0
- data/README.md +154 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/docker-compose.yml +11 -0
- data/lib/protoboard.rb +32 -0
- data/lib/protoboard/adapters/base_adapter.rb +58 -0
- data/lib/protoboard/adapters/stoplight_adapter.rb +104 -0
- data/lib/protoboard/circuit.rb +27 -0
- data/lib/protoboard/circuit_breaker.rb +115 -0
- data/lib/protoboard/circuit_execution.rb +22 -0
- data/lib/protoboard/circuit_proxy_factory.rb +35 -0
- data/lib/protoboard/configuration.rb +19 -0
- data/lib/protoboard/errors/invalid_callback.rb +10 -0
- data/lib/protoboard/helpers/services_healthcheck_generator.rb +40 -0
- data/lib/protoboard/helpers/validate_callbacks.rb +18 -0
- data/lib/protoboard/refinements/string_extensions.rb +12 -0
- data/lib/protoboard/version.rb +5 -0
- data/protoboard.gemspec +43 -0
- metadata +171 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 3724f36f72e98c1dbb8fd27ee43700e8b878da75
|
4
|
+
data.tar.gz: 01aee471ca195a9b014cb3a2edd9888a381db783
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4e81f442a9d343e41cb861cd6bcb2500fead4ab51e24ba3c9375a1cfa8c99979421ab7b10b8bdd582469d648b085795cb2547caaca87a50fb3974d8e7f516a15
|
7
|
+
data.tar.gz: 6f74eda66ac8b853881201e614d59e3dd0fd4699e2382ceac90c6cbb50b0d33b3e914410d7d4675baacb094ee18b21e9177616c3b36e07cc4e355949f19c651f
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
AllCops:
|
2
|
+
TargetRubyVersion: 2.3
|
3
|
+
|
4
|
+
Documentation:
|
5
|
+
Enabled: false
|
6
|
+
|
7
|
+
AsciiComments:
|
8
|
+
Enabled: false
|
9
|
+
|
10
|
+
Style/ClassAndModuleChildren:
|
11
|
+
Enabled: false
|
12
|
+
|
13
|
+
Metrics/LineLength:
|
14
|
+
Max: 120
|
15
|
+
|
16
|
+
Metrics/BlockLength:
|
17
|
+
Exclude:
|
18
|
+
- spec/**/*
|
19
|
+
- protoboard.gemspec
|
20
|
+
|
21
|
+
Metrics/MethodLength:
|
22
|
+
Max: 15
|
23
|
+
|
24
|
+
Naming/UncommunicativeMethodParamName:
|
25
|
+
Exclude:
|
26
|
+
- spec/**/*
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
protoboard (0.1.0)
|
5
|
+
dry-configurable (~> 0.7.0)
|
6
|
+
stoplight (~> 2.1.3)
|
7
|
+
|
8
|
+
GEM
|
9
|
+
remote: https://rubygems.org/
|
10
|
+
specs:
|
11
|
+
concurrent-ruby (1.0.5)
|
12
|
+
diff-lcs (1.3)
|
13
|
+
docile (1.3.1)
|
14
|
+
dry-configurable (0.7.0)
|
15
|
+
concurrent-ruby (~> 1.0)
|
16
|
+
json (2.1.0)
|
17
|
+
rake (10.5.0)
|
18
|
+
redis (3.3.5)
|
19
|
+
rspec (3.7.0)
|
20
|
+
rspec-core (~> 3.7.0)
|
21
|
+
rspec-expectations (~> 3.7.0)
|
22
|
+
rspec-mocks (~> 3.7.0)
|
23
|
+
rspec-core (3.7.1)
|
24
|
+
rspec-support (~> 3.7.0)
|
25
|
+
rspec-expectations (3.7.0)
|
26
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
27
|
+
rspec-support (~> 3.7.0)
|
28
|
+
rspec-mocks (3.7.0)
|
29
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
30
|
+
rspec-support (~> 3.7.0)
|
31
|
+
rspec-support (3.7.1)
|
32
|
+
simplecov (0.16.1)
|
33
|
+
docile (~> 1.1)
|
34
|
+
json (>= 1.8, < 3)
|
35
|
+
simplecov-html (~> 0.10.0)
|
36
|
+
simplecov-html (0.10.2)
|
37
|
+
stoplight (2.1.3)
|
38
|
+
|
39
|
+
PLATFORMS
|
40
|
+
ruby
|
41
|
+
|
42
|
+
DEPENDENCIES
|
43
|
+
bundler (~> 1.16)
|
44
|
+
protoboard!
|
45
|
+
rake (~> 10.0)
|
46
|
+
redis (~> 3.2)
|
47
|
+
rspec (~> 3.0)
|
48
|
+
simplecov
|
49
|
+
|
50
|
+
BUNDLED WITH
|
51
|
+
1.16.2
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2018 TODO: Write your name
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,154 @@
|
|
1
|
+
# Protoboard
|
2
|
+
|
3
|
+
[](https://travis-ci.org/VAGAScom/protoboard)
|
4
|
+
|
5
|
+
Protoboard abstracts the way you use Circuit Breaker allowing you to easily use it with any Ruby Object, under the hood it uses the gem [stoplight](https://github.com/orgsync/stoplight) to create the circuits.
|
6
|
+
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
Add this line to your application's Gemfile:
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
gem 'protoboard'
|
14
|
+
```
|
15
|
+
|
16
|
+
And then execute:
|
17
|
+
|
18
|
+
$ bundle
|
19
|
+
|
20
|
+
Or install it yourself as:
|
21
|
+
|
22
|
+
$ gem install protoboard
|
23
|
+
|
24
|
+
## Usage
|
25
|
+
|
26
|
+
The usage is really simple, just include `Protoboard::CircuitBreaker` and register your circuit.
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
class MyFooService
|
30
|
+
include Protoboard::CircuitBreaker
|
31
|
+
|
32
|
+
register_circuits [:some_method],
|
33
|
+
options: {
|
34
|
+
service: 'my_cool_service',
|
35
|
+
open_after: 2,
|
36
|
+
cool_off_after: 3
|
37
|
+
}
|
38
|
+
def some_method
|
39
|
+
# Something that can break
|
40
|
+
end
|
41
|
+
end
|
42
|
+
```
|
43
|
+
|
44
|
+
You can also define a fallback and callbacks for the circuit.
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
class MyFooService
|
48
|
+
include Protoboard::CircuitBreaker
|
49
|
+
|
50
|
+
register_circuits [:some_method],
|
51
|
+
fallback: -> (error) { 'Do Something' }
|
52
|
+
on_before: [->(ce) { Something.notify("Circuit #{ce.circuit.name}") }, ->(_) {}],
|
53
|
+
on_after: [->(ce) { Something.notify("It fails with #{ce.error}") if ce.fail? }],
|
54
|
+
options: {
|
55
|
+
service: 'my_cool_service',
|
56
|
+
open_after: 2,
|
57
|
+
cool_off_after: 3
|
58
|
+
}
|
59
|
+
def some_method
|
60
|
+
# Something that can break
|
61
|
+
end
|
62
|
+
end
|
63
|
+
```
|
64
|
+
|
65
|
+
Also if you want to add more than one method in the same class and customize the circuit name:
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
class Foo4
|
69
|
+
include Protoboard::CircuitBreaker
|
70
|
+
|
71
|
+
register_circuits({ some_method: 'my_custom_name', other_method: 'my_other_custom_name' },
|
72
|
+
options: {
|
73
|
+
service: 'my_service_name',
|
74
|
+
open_after: 2,
|
75
|
+
cool_off_after: 3
|
76
|
+
})
|
77
|
+
def some_method
|
78
|
+
'OK'
|
79
|
+
end
|
80
|
+
end
|
81
|
+
```
|
82
|
+
|
83
|
+
### Callbacks
|
84
|
+
|
85
|
+
Any callback should receive one argument that it will be an instance of `CircuitExecution` class, that object will respond to the following methods:
|
86
|
+
|
87
|
+
* `state` returns the current state of the circuit execution (`:not_started`, `:success` or `:fail`)
|
88
|
+
* `value` returns the result value of the circuit execution, if the circuit fail the value will be `nil`.
|
89
|
+
* `error` returns the error raised when the circuit fail, it will be `nil` if no error occurred
|
90
|
+
* `circuit` returns a circuit instance which has the options configured in `register_circuits` (`name`, `service`, `open_after` and `cool_off_after`)
|
91
|
+
* `fail?` returns `true` if the execution failed
|
92
|
+
|
93
|
+
P.S: In before calbacks the state will always be `:not_started` and in after callbacks the state can be either `:fail` or `:success`
|
94
|
+
|
95
|
+
### Check Services and Circuits Status
|
96
|
+
|
97
|
+
If you want to check the services and circuits registered in Protoboard you can use `Protoboard::CircuitBreaker.services_healthcheck`, it will return a hash with the status of all circuits:
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
{
|
101
|
+
'services' => {
|
102
|
+
'my_service_name' => {
|
103
|
+
'circuits' => {
|
104
|
+
'my_circuit1' => 'OK',
|
105
|
+
'my_circuit2' => 'NOT_OK'
|
106
|
+
}
|
107
|
+
}
|
108
|
+
}
|
109
|
+
}
|
110
|
+
|
111
|
+
```
|
112
|
+
|
113
|
+
|
114
|
+
## Configuration
|
115
|
+
|
116
|
+
In configuration you can customize the adapter options and set callbacks and configurations for all the Protoboard Circuits:
|
117
|
+
|
118
|
+
```ruby
|
119
|
+
|
120
|
+
Protoboard.configure do |config|
|
121
|
+
config.adapter = Protoboard::Adapters::StoplightAdapter
|
122
|
+
|
123
|
+
config.adapter.configure do |adapter|
|
124
|
+
adapter.data_store = :redis # Default is :memory
|
125
|
+
adapter.redis_host = 'localhost'
|
126
|
+
adapter.redis_port = 1234
|
127
|
+
end
|
128
|
+
|
129
|
+
#Global callbacks
|
130
|
+
config.callbacks.before = [->(_) {}]
|
131
|
+
config.callbacks.after = [MyCallableObject.new, ->(_) {}]
|
132
|
+
end
|
133
|
+
|
134
|
+
```
|
135
|
+
|
136
|
+
The available options are:
|
137
|
+
|
138
|
+
* `adapter =` Sets the adapter, `Protoboard::Adapters::StoplightAdapter` is the default
|
139
|
+
* `callbacks.before =` Receives an array of callables, lambdas, procs or any object that responds to `call` and receives one argument. It will be called before each circuit execution.
|
140
|
+
* `callbacks.after =` Receives an array of callables, lambdas, procs or any object that responds to `call` and receives one argument. It will be called after each circuit execution.
|
141
|
+
|
142
|
+
### StoplightAdapter Options
|
143
|
+
|
144
|
+
* `datastore =` Sets the datastore(:redis or :memory). The default option is :memory
|
145
|
+
* `redis_host=` Sets the redis host
|
146
|
+
* `redis_port=` Sets the redis port
|
147
|
+
|
148
|
+
## Contributing
|
149
|
+
|
150
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/VAGAScom/protoboard.
|
151
|
+
|
152
|
+
## License
|
153
|
+
|
154
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'protoboard'
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
11
|
+
# require "pry"
|
12
|
+
# Pry.start
|
13
|
+
|
14
|
+
require 'irb'
|
15
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/docker-compose.yml
ADDED
data/lib/protoboard.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry-configurable'
|
4
|
+
require 'protoboard/circuit_execution'
|
5
|
+
require 'protoboard/adapters/base_adapter'
|
6
|
+
require 'protoboard/adapters/stoplight_adapter'
|
7
|
+
|
8
|
+
require 'protoboard/version'
|
9
|
+
require 'protoboard/helpers/validate_callbacks'
|
10
|
+
require 'protoboard/refinements/string_extensions'
|
11
|
+
require 'protoboard/helpers/services_healthcheck_generator'
|
12
|
+
require 'protoboard/configuration'
|
13
|
+
require 'protoboard/circuit_breaker'
|
14
|
+
require 'protoboard/circuit'
|
15
|
+
require 'protoboard/circuit_proxy_factory'
|
16
|
+
require 'protoboard/errors/invalid_callback'
|
17
|
+
|
18
|
+
##
|
19
|
+
# This module is the entry to get or set the configuration needed by the gem.
|
20
|
+
module Protoboard
|
21
|
+
##
|
22
|
+
# Returns the current configuration
|
23
|
+
def self.config
|
24
|
+
Protoboard::Configuration
|
25
|
+
end
|
26
|
+
|
27
|
+
##
|
28
|
+
# Does the configuration needed by the gem
|
29
|
+
def self.configure(&block)
|
30
|
+
config.configure(&block)
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Protoboard
|
4
|
+
module Adapters
|
5
|
+
##
|
6
|
+
# This class is responsible to encapsulate every action that are commom between all adapters
|
7
|
+
class BaseAdapter
|
8
|
+
class << self
|
9
|
+
##
|
10
|
+
# Manages the execution of the code intended to run before circuit execution
|
11
|
+
def execute_before_circuit_callbacks(circuit_execution)
|
12
|
+
before_global_callback(circuit_execution)
|
13
|
+
before_circuit_callback(circuit_execution)
|
14
|
+
end
|
15
|
+
|
16
|
+
##
|
17
|
+
# Manages the execution of the code intended to run after circuit execution
|
18
|
+
def execute_after_circuit_callbacks(circuit_execution)
|
19
|
+
after_global_callback(circuit_execution)
|
20
|
+
after_circuit_callback(circuit_execution)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
##
|
25
|
+
# Calls the code intended to run before all circuit execution
|
26
|
+
def before_global_callback(circuit_execution)
|
27
|
+
Protoboard.config.callbacks.before.each do |callback|
|
28
|
+
callback.call(circuit_execution)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
##
|
33
|
+
# Calls the code intended to run before a circuit execution
|
34
|
+
def before_circuit_callback(circuit_execution)
|
35
|
+
circuit_execution.circuit.on_before.each do |callback|
|
36
|
+
callback.call(circuit_execution)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
##
|
41
|
+
# Calls the code intended to run after all circuit execution
|
42
|
+
def after_global_callback(circuit_execution)
|
43
|
+
Protoboard.config.callbacks.after.each do |callback|
|
44
|
+
callback.call(circuit_execution)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
##
|
49
|
+
# Calls the code intended to run after a circuit execution
|
50
|
+
def after_circuit_callback(circuit_execution)
|
51
|
+
circuit_execution.circuit.on_after.each do |callback|
|
52
|
+
callback.call(circuit_execution)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'stoplight'
|
4
|
+
module Protoboard
|
5
|
+
module Adapters
|
6
|
+
##
|
7
|
+
# This class manages every aspect for the execution of a circuit using the gem stoplight
|
8
|
+
class StoplightAdapter < BaseAdapter
|
9
|
+
##
|
10
|
+
# This class represents the configuration needed to configure the gem stoplight.
|
11
|
+
class Configuration
|
12
|
+
extend Dry::Configurable
|
13
|
+
|
14
|
+
setting :data_store, :memory
|
15
|
+
setting :redis_host
|
16
|
+
setting :redis_port
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize
|
20
|
+
prepare_data_store
|
21
|
+
end
|
22
|
+
|
23
|
+
class << self
|
24
|
+
##
|
25
|
+
# This methods is used to make it easier to access adapter configurations
|
26
|
+
def configure(&block)
|
27
|
+
Configuration.configure(&block)
|
28
|
+
end
|
29
|
+
|
30
|
+
##
|
31
|
+
# This method is used to make it easier to access adapter data store configuration
|
32
|
+
def data_store
|
33
|
+
Configuration.config.data_store
|
34
|
+
end
|
35
|
+
|
36
|
+
##
|
37
|
+
# This method is used to make it easier to access adapter redis host configuration
|
38
|
+
def redis_host
|
39
|
+
Configuration.config.redis_host
|
40
|
+
end
|
41
|
+
|
42
|
+
##
|
43
|
+
# This method is used to make it easier to access adapter redis port configuration
|
44
|
+
def redis_port
|
45
|
+
Configuration.config.redis_port
|
46
|
+
end
|
47
|
+
|
48
|
+
##
|
49
|
+
# Runs the circuit using stoplight
|
50
|
+
def run_circuit(circuit, &block)
|
51
|
+
prepare_data_store
|
52
|
+
|
53
|
+
circuit_execution = Protoboard::CircuitExecution.new(circuit)
|
54
|
+
|
55
|
+
execute_before_circuit_callbacks(circuit_execution)
|
56
|
+
|
57
|
+
stoplight = Stoplight(circuit.name, &block)
|
58
|
+
.with_threshold(circuit.open_after)
|
59
|
+
.with_cool_off_time(circuit.cool_off_after)
|
60
|
+
|
61
|
+
stoplight.with_fallback(&circuit.fallback) if circuit.fallback
|
62
|
+
value = stoplight.run
|
63
|
+
|
64
|
+
circuit_execution = ::Protoboard::CircuitExecution.new(circuit, state: :success, value: value)
|
65
|
+
execute_after_circuit_callbacks(circuit_execution)
|
66
|
+
|
67
|
+
value
|
68
|
+
rescue StandardError => exception
|
69
|
+
circuit_execution = Protoboard::CircuitExecution.new(circuit, state: :fail, error: exception)
|
70
|
+
execute_after_circuit_callbacks(circuit_execution)
|
71
|
+
|
72
|
+
raise circuit_execution.error if circuit_execution.fail?
|
73
|
+
end
|
74
|
+
|
75
|
+
# Returns the state of a circuit
|
76
|
+
#
|
77
|
+
# ==== States returned
|
78
|
+
#
|
79
|
+
# * +OK+ - when that stoplight circuit is green
|
80
|
+
# * +NOT_OK+ - when that stoplight circuit is yellow or red
|
81
|
+
def check_state(circuit_name)
|
82
|
+
mapper = { 'yellow' => 'NOT_OK', 'green' => 'OK', 'red' => 'NOT_OK' }
|
83
|
+
mapper[Stoplight(circuit_name).color]
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def prepare_data_store
|
89
|
+
@prepare_data_store ||= case Configuration.config.data_store
|
90
|
+
when :redis
|
91
|
+
require 'redis'
|
92
|
+
redis_host = Configuration.config.redis_host
|
93
|
+
redis_port = Configuration.config.redis_port
|
94
|
+
redis = Redis.new(host: redis_host, port: redis_port)
|
95
|
+
data_store = Stoplight::DataStore::Redis.new(redis)
|
96
|
+
Stoplight::Light.default_data_store = data_store
|
97
|
+
else
|
98
|
+
Stoplight::Light.default_data_store = Stoplight::DataStore::Memory.new
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Protoboard
|
4
|
+
##
|
5
|
+
# This class represents a circuit.
|
6
|
+
class Circuit
|
7
|
+
attr_reader :name, :service,
|
8
|
+
:method_name, :timeout,
|
9
|
+
:open_after, :cool_off_after,
|
10
|
+
:on_before, :on_after,
|
11
|
+
:fallback
|
12
|
+
|
13
|
+
def initialize(**options)
|
14
|
+
@name = options.fetch(:name)
|
15
|
+
@service = options.fetch(:service)
|
16
|
+
@method_name = options.fetch(:method_name)
|
17
|
+
@timeout = options.fetch(:timeout)
|
18
|
+
@open_after = options.fetch(:open_after)
|
19
|
+
@cool_off_after = options.fetch(:cool_off_after)
|
20
|
+
@fallback = options[:fallback]
|
21
|
+
@on_before = options.fetch(:on_before, [])
|
22
|
+
@on_after = options.fetch(:on_after, [])
|
23
|
+
rescue KeyError => error
|
24
|
+
raise ArgumentError, "Missing required arguments: #{error.message}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Protoboard
|
4
|
+
##
|
5
|
+
# This module is responsible to manage the circuits.
|
6
|
+
module CircuitBreaker
|
7
|
+
module ClassMethods
|
8
|
+
##
|
9
|
+
# Registers a list of circuits to be executed
|
10
|
+
#
|
11
|
+
# ==== Attributes
|
12
|
+
#
|
13
|
+
# * +circuit_methods+ - An array of symbols representing the names of the methods to be on a circuit
|
14
|
+
# or a hash containing a key with a symbol representing the name of the method and the value as the name of the circuit.
|
15
|
+
# * +on_before+ - An array of callable objects with code to be executed before each circuit runs
|
16
|
+
# * +on_after+ - An array of callable objects with code to be executed after each circuit runs
|
17
|
+
# * +options+ - A hash containing the options needed for the circuit to execute
|
18
|
+
# * +:service+ - A string representing the name of the service for the circuit
|
19
|
+
# * +:open_after+ - An integer representing the number of errors to occur for the circuit to be opened
|
20
|
+
# * +:cool_off_after+ - An integer representing the number of successful requests to occur for the circuit to be closed
|
21
|
+
#
|
22
|
+
# ==== Example
|
23
|
+
# options: {
|
24
|
+
# service: 'my_cool_service',
|
25
|
+
# open_after: 2,
|
26
|
+
# cool_off_after: 3
|
27
|
+
# }
|
28
|
+
# ====
|
29
|
+
#
|
30
|
+
# * +fallback+ - A callable object with code to be executed as an alternative plan if the code of the circuit fails
|
31
|
+
def register_circuits(circuit_methods, on_before: [], on_after: [], options:, fallback: nil)
|
32
|
+
Protoboard::Helpers::VALIDATE_CALLBACKS.call(on_before)
|
33
|
+
Protoboard::Helpers::VALIDATE_CALLBACKS.call(on_after)
|
34
|
+
|
35
|
+
circuits = Protoboard::CircuitBreaker.create_circuits(
|
36
|
+
circuit_methods,
|
37
|
+
options.merge(
|
38
|
+
fallback: fallback,
|
39
|
+
on_before: on_before,
|
40
|
+
on_after: on_after
|
41
|
+
)
|
42
|
+
)
|
43
|
+
|
44
|
+
circuits.each do |circuit|
|
45
|
+
Protoboard::CircuitBreaker.add_circuit circuit
|
46
|
+
end
|
47
|
+
|
48
|
+
proxy_module = Protoboard::CircuitBreaker.create_circuit_proxy(circuits, name)
|
49
|
+
prepend proxy_module
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class << self
|
54
|
+
##
|
55
|
+
# Returns a hash with the +circuits+ names and its states.
|
56
|
+
def services_healthcheck
|
57
|
+
Protoboard::Helpers::ServicesHealthcheckGenerator.new.call
|
58
|
+
end
|
59
|
+
|
60
|
+
##
|
61
|
+
# Returns a list of registered +circuits+.
|
62
|
+
def registered_circuits
|
63
|
+
circuits
|
64
|
+
end
|
65
|
+
|
66
|
+
##
|
67
|
+
# Adds a +circuit+ to the list of registered +circuits+.
|
68
|
+
def add_circuit(circuit)
|
69
|
+
circuits << circuit
|
70
|
+
end
|
71
|
+
|
72
|
+
##
|
73
|
+
# Returns a list of +circuits+.
|
74
|
+
def circuits
|
75
|
+
@circuits ||= []
|
76
|
+
end
|
77
|
+
|
78
|
+
##
|
79
|
+
# Calls the module responsible for creating the proxy module that will execute the circuit.
|
80
|
+
def create_circuit_proxy(circuits, class_name)
|
81
|
+
CircuitProxyFactory.create_module(circuits, class_name)
|
82
|
+
end
|
83
|
+
|
84
|
+
##
|
85
|
+
# Creates a new +circuit+.
|
86
|
+
def create_circuits(circuit_methods, options)
|
87
|
+
circuit_hash = case circuit_methods
|
88
|
+
when Array
|
89
|
+
circuit_methods.reduce({}) do |memo, value|
|
90
|
+
memo.merge(value.to_sym => "#{formatted_namespace}#{options[:service]}\##{value}")
|
91
|
+
end
|
92
|
+
when Hash
|
93
|
+
circuit_methods
|
94
|
+
else
|
95
|
+
raise ArgumentError, 'Invalid input for circuit methods'
|
96
|
+
end
|
97
|
+
circuit_hash.map do |circuit_method, circuit_name|
|
98
|
+
Circuit.new({ name: circuit_name, method_name: circuit_method }.merge(options))
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def included(klass)
|
103
|
+
klass.extend(ClassMethods)
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
##
|
109
|
+
# Formats the namespace considering the configuration given when the gem starts
|
110
|
+
def formatted_namespace
|
111
|
+
!Protoboard.config.namespace.empty? ? "#{Protoboard.config.namespace}/" : ''
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Protoboard
|
4
|
+
##
|
5
|
+
# This class represents a circuit execution.
|
6
|
+
class CircuitExecution
|
7
|
+
STATES = %i[not_started success fail].freeze
|
8
|
+
|
9
|
+
attr_reader :circuit, :state, :value, :error
|
10
|
+
|
11
|
+
def initialize(circuit, state: :pending, value: nil, error: nil)
|
12
|
+
@circuit = circuit
|
13
|
+
@state = state
|
14
|
+
@value = value
|
15
|
+
@error = error
|
16
|
+
end
|
17
|
+
|
18
|
+
def fail?
|
19
|
+
@state == :fail
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Protoboard
|
4
|
+
##
|
5
|
+
# This module is responsible to manage a proxy module that executes the circuit.
|
6
|
+
module CircuitProxyFactory
|
7
|
+
class << self
|
8
|
+
using Protoboard::Refinements::StringExtensions
|
9
|
+
##
|
10
|
+
# Creates the module that executes the circuit
|
11
|
+
def create_module(circuits, class_name)
|
12
|
+
module_name = infer_module_name(class_name, circuits.map(&:method_name))
|
13
|
+
proxy_module = Module.new
|
14
|
+
|
15
|
+
proxy_module.instance_exec do
|
16
|
+
circuits.each do |circuit|
|
17
|
+
define_method(circuit.method_name) do |*args|
|
18
|
+
Protoboard.config.adapter.run_circuit(circuit) { super(*args) }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
Protoboard.const_set(module_name, proxy_module)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
##
|
29
|
+
# Formats the module name
|
30
|
+
def infer_module_name(class_name, methods)
|
31
|
+
"#{methods.map(&:to_s).map { |method| method.camelize }.join}#{class_name.split('::').join('')}CircuitProxy"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Protoboard
|
4
|
+
##
|
5
|
+
# This class represents the configuration needed to run the gem.
|
6
|
+
class Configuration
|
7
|
+
extend Dry::Configurable
|
8
|
+
|
9
|
+
setting :adapter, Protoboard::Adapters::StoplightAdapter, reader: true
|
10
|
+
|
11
|
+
setting :namespace, '', reader: true
|
12
|
+
|
13
|
+
setting :callbacks, reader: true do
|
14
|
+
setting :before, [], reader: true, &Protoboard::Helpers::VALIDATE_CALLBACKS
|
15
|
+
|
16
|
+
setting :after, [], reader: true, &Protoboard::Helpers::VALIDATE_CALLBACKS
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Protoboard
|
4
|
+
module Helpers
|
5
|
+
##
|
6
|
+
# This class is responsible to generate information about the +circuits+ added
|
7
|
+
class ServicesHealthcheckGenerator
|
8
|
+
|
9
|
+
##
|
10
|
+
# Verifies the list of +circuits+ added and returns a hash with the +circuits names+ and its states.
|
11
|
+
#
|
12
|
+
# ==== Examples
|
13
|
+
# 'services' => {
|
14
|
+
# 'my_service_name' => {
|
15
|
+
# 'circuits' => {
|
16
|
+
# 'my_service_name#some_method' => 'OK',
|
17
|
+
# 'my_custom_name' => 'NOT_OK'
|
18
|
+
# }
|
19
|
+
# }
|
20
|
+
# }
|
21
|
+
# ====
|
22
|
+
#
|
23
|
+
def call
|
24
|
+
circuits_hash = Protoboard::CircuitBreaker.registered_circuits.map do |circuit|
|
25
|
+
state = Protoboard.config.adapter.check_state(circuit.name)
|
26
|
+
|
27
|
+
{ name: circuit.name, status: state, service: circuit.service }
|
28
|
+
end
|
29
|
+
services_hash = circuits_hash
|
30
|
+
.group_by { |circuit| circuit[:service] }
|
31
|
+
.map do |service, circuits_hash|
|
32
|
+
circuits = circuits_hash.each_with_object({}) { |circuit, memo| memo[circuit[:name]] = circuit[:status] }
|
33
|
+
{ service => { 'circuits' => circuits } }
|
34
|
+
end.reduce(:merge)
|
35
|
+
|
36
|
+
{ 'services' => services_hash }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Protoboard
|
4
|
+
module Helpers
|
5
|
+
VALIDATE_CALLBACKS = lambda do |callbacks|
|
6
|
+
callbacks.each do |callback|
|
7
|
+
case callback
|
8
|
+
when Proc
|
9
|
+
raise Errors::InvalidCallback if callback.arity != 1
|
10
|
+
else
|
11
|
+
raise Errors::InvalidCallback if !callback.respond_to?(:call) || callback.method(:call).arity != 1
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
callbacks
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Protoboard
|
2
|
+
module Refinements
|
3
|
+
module StringExtensions
|
4
|
+
refine String do
|
5
|
+
def camelize
|
6
|
+
string = sub(/^[a-z\d]*/) { $&.capitalize }
|
7
|
+
string.gsub(/(?:_|(\/))([a-z\d]*)/) { "#{$1}#{$2.capitalize}" }.gsub('/', '::')
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
data/protoboard.gemspec
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
lib = File.expand_path('lib', __dir__)
|
5
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
6
|
+
require 'protoboard/version'
|
7
|
+
|
8
|
+
Gem::Specification.new do |spec|
|
9
|
+
spec.name = 'protoboard'
|
10
|
+
spec.version = Protoboard::VERSION
|
11
|
+
spec.authors = ['Carlos Atkinson', 'Kelly Bhering']
|
12
|
+
spec.email = ['carlos.atks@gmail.com', 'kellybhering@hotmail.com']
|
13
|
+
|
14
|
+
spec.summary = 'Protoboard abstracts the way you use Circuit Breaker' \
|
15
|
+
'allowing you to easily use it with any Ruby Object'
|
16
|
+
# spec.description = %q{TODO: Write a longer description or delete this line.}
|
17
|
+
# spec.homepage = "TODO: Put your gem's website or public repo URL here."
|
18
|
+
spec.license = 'MIT'
|
19
|
+
|
20
|
+
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
21
|
+
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
22
|
+
# if spec.respond_to?(:metadata)
|
23
|
+
# spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com'"
|
24
|
+
# else
|
25
|
+
# raise 'RubyGems 2.0 or newer is required to protect against ' \
|
26
|
+
# 'public gem pushes.'
|
27
|
+
# end
|
28
|
+
|
29
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
30
|
+
f.match(%r{^(test|spec|features)/})
|
31
|
+
end
|
32
|
+
spec.bindir = 'exe'
|
33
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
34
|
+
spec.require_paths = ['lib']
|
35
|
+
|
36
|
+
spec.add_dependency 'dry-configurable', '~> 0.7.0'
|
37
|
+
spec.add_dependency 'stoplight', '~> 2.1.3'
|
38
|
+
spec.add_development_dependency 'bundler', '~> 1.16'
|
39
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
40
|
+
spec.add_development_dependency 'redis', '~> 3.2'
|
41
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
42
|
+
spec.add_development_dependency 'simplecov'
|
43
|
+
end
|
metadata
ADDED
@@ -0,0 +1,171 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: protoboard
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Carlos Atkinson
|
8
|
+
- Kelly Bhering
|
9
|
+
autorequire:
|
10
|
+
bindir: exe
|
11
|
+
cert_chain: []
|
12
|
+
date: 2018-06-05 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: dry-configurable
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - "~>"
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: 0.7.0
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - "~>"
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: 0.7.0
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: stoplight
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - "~>"
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: 2.1.3
|
35
|
+
type: :runtime
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - "~>"
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: 2.1.3
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: bundler
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - "~>"
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '1.16'
|
49
|
+
type: :development
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - "~>"
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '1.16'
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: rake
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - "~>"
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '10.0'
|
63
|
+
type: :development
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - "~>"
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '10.0'
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: redis
|
72
|
+
requirement: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - "~>"
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '3.2'
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - "~>"
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '3.2'
|
84
|
+
- !ruby/object:Gem::Dependency
|
85
|
+
name: rspec
|
86
|
+
requirement: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - "~>"
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '3.0'
|
91
|
+
type: :development
|
92
|
+
prerelease: false
|
93
|
+
version_requirements: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - "~>"
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '3.0'
|
98
|
+
- !ruby/object:Gem::Dependency
|
99
|
+
name: simplecov
|
100
|
+
requirement: !ruby/object:Gem::Requirement
|
101
|
+
requirements:
|
102
|
+
- - ">="
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: '0'
|
105
|
+
type: :development
|
106
|
+
prerelease: false
|
107
|
+
version_requirements: !ruby/object:Gem::Requirement
|
108
|
+
requirements:
|
109
|
+
- - ">="
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: '0'
|
112
|
+
description:
|
113
|
+
email:
|
114
|
+
- carlos.atks@gmail.com
|
115
|
+
- kellybhering@hotmail.com
|
116
|
+
executables: []
|
117
|
+
extensions: []
|
118
|
+
extra_rdoc_files: []
|
119
|
+
files:
|
120
|
+
- ".gitignore"
|
121
|
+
- ".rspec"
|
122
|
+
- ".rubocop.yml"
|
123
|
+
- ".travis.yml"
|
124
|
+
- Gemfile
|
125
|
+
- Gemfile.lock
|
126
|
+
- LICENSE.txt
|
127
|
+
- README.md
|
128
|
+
- Rakefile
|
129
|
+
- bin/console
|
130
|
+
- bin/setup
|
131
|
+
- docker-compose.yml
|
132
|
+
- lib/protoboard.rb
|
133
|
+
- lib/protoboard/adapters/base_adapter.rb
|
134
|
+
- lib/protoboard/adapters/stoplight_adapter.rb
|
135
|
+
- lib/protoboard/circuit.rb
|
136
|
+
- lib/protoboard/circuit_breaker.rb
|
137
|
+
- lib/protoboard/circuit_execution.rb
|
138
|
+
- lib/protoboard/circuit_proxy_factory.rb
|
139
|
+
- lib/protoboard/configuration.rb
|
140
|
+
- lib/protoboard/errors/invalid_callback.rb
|
141
|
+
- lib/protoboard/helpers/services_healthcheck_generator.rb
|
142
|
+
- lib/protoboard/helpers/validate_callbacks.rb
|
143
|
+
- lib/protoboard/refinements/string_extensions.rb
|
144
|
+
- lib/protoboard/version.rb
|
145
|
+
- protoboard.gemspec
|
146
|
+
homepage:
|
147
|
+
licenses:
|
148
|
+
- MIT
|
149
|
+
metadata: {}
|
150
|
+
post_install_message:
|
151
|
+
rdoc_options: []
|
152
|
+
require_paths:
|
153
|
+
- lib
|
154
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
155
|
+
requirements:
|
156
|
+
- - ">="
|
157
|
+
- !ruby/object:Gem::Version
|
158
|
+
version: '0'
|
159
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
160
|
+
requirements:
|
161
|
+
- - ">="
|
162
|
+
- !ruby/object:Gem::Version
|
163
|
+
version: '0'
|
164
|
+
requirements: []
|
165
|
+
rubyforge_project:
|
166
|
+
rubygems_version: 2.6.14
|
167
|
+
signing_key:
|
168
|
+
specification_version: 4
|
169
|
+
summary: Protoboard abstracts the way you use Circuit Breakerallowing you to easily
|
170
|
+
use it with any Ruby Object
|
171
|
+
test_files: []
|