protoboard 0.1.1

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: 3724f36f72e98c1dbb8fd27ee43700e8b878da75
4
+ data.tar.gz: 01aee471ca195a9b014cb3a2edd9888a381db783
5
+ SHA512:
6
+ metadata.gz: 4e81f442a9d343e41cb861cd6bcb2500fead4ab51e24ba3c9375a1cfa8c99979421ab7b10b8bdd582469d648b085795cb2547caaca87a50fb3974d8e7f516a15
7
+ data.tar.gz: 6f74eda66ac8b853881201e614d59e3dd0fd4699e2382ceac90c6cbb50b0d33b3e914410d7d4675baacb094ee18b21e9177616c3b36e07cc4e355949f19c651f
data/.gitignore ADDED
@@ -0,0 +1,13 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
12
+
13
+ .byebug_history
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
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
@@ -0,0 +1,9 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.1
5
+ - 2.2
6
+ - 2.3
7
+ - 2.4
8
+ - 2.5
9
+ before_install: gem install bundler -v 1.16.2
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
+
7
+ # Specify your gem's dependencies in protoboard.gemspec
8
+ gemspec
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
+ [![Build Status](https://travis-ci.org/VAGAScom/protoboard.svg?branch=master)](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
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
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
@@ -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
@@ -0,0 +1,11 @@
1
+ version: '3'
2
+ services:
3
+ redis:
4
+ image: redis:alpine
5
+ volumes:
6
+ - redis-data:/data
7
+ ports:
8
+ - "6379:6379"
9
+
10
+ volumes:
11
+ redis-data:
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,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Protoboard
4
+ module Errors
5
+ class InvalidCallback < StandardError
6
+ DEFAULT_MESSAGE = 'All callbacks should respond to #call and receive one argument'
7
+ def initialize(msg = DEFAULT_MESSAGE); end
8
+ end
9
+ end
10
+ 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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Protoboard
4
+ VERSION = '0.1.1'
5
+ end
@@ -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: []