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 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: []