faulty 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,7 +3,7 @@
3
3
 
4
4
  require 'bundler/setup'
5
5
  require 'faulty'
6
- require 'byebug'
6
+ require 'byebug' if Gem.loaded_specs['byebug']
7
7
  require 'irb'
8
8
 
9
9
  # For default cache support
@@ -23,21 +23,14 @@ Gem::Specification.new do |spec|
23
23
 
24
24
  spec.add_runtime_dependency 'concurrent-ruby', '~> 1.0'
25
25
 
26
- spec.add_development_dependency 'activesupport', '>= 4.2'
27
- spec.add_development_dependency 'bundler', '>= 1.17', '< 3'
28
- spec.add_development_dependency 'byebug', '~> 11.0'
26
+ # Only essential development tools and dependencies go here.
27
+ # Other non-essential development dependencies go in the Gemfile.
29
28
  spec.add_development_dependency 'connection_pool', '~> 2.0'
30
- spec.add_development_dependency 'irb', '~> 1.0'
31
- spec.add_development_dependency 'redcarpet', '~> 3.5'
29
+ spec.add_development_dependency 'honeybadger', '>= 2.0'
32
30
  spec.add_development_dependency 'redis', '~> 3.0'
33
31
  spec.add_development_dependency 'rspec', '~> 3.8'
34
- spec.add_development_dependency 'rspec_junit_formatter', '~> 0.4'
35
32
  # 0.81 is the last rubocop version with Ruby 2.3 support
36
33
  spec.add_development_dependency 'rubocop', '0.81.0'
37
34
  spec.add_development_dependency 'rubocop-rspec', '1.38.1'
38
- # For now, code climate doesn't support simplecov 0.18
39
- # https://github.com/codeclimate/test-reporter/issues/413
40
- spec.add_development_dependency 'simplecov', '>= 0.17.1', '< 0.18'
41
35
  spec.add_development_dependency 'timecop', '>= 0.9'
42
- spec.add_development_dependency 'yard', '~> 0.9.25'
43
36
  end
@@ -9,14 +9,16 @@ require 'faulty/circuit'
9
9
  require 'faulty/error'
10
10
  require 'faulty/events'
11
11
  require 'faulty/result'
12
- require 'faulty/scope'
13
12
  require 'faulty/status'
14
13
  require 'faulty/storage'
15
14
 
16
- # The top-level namespace for Faulty
15
+ # The {Faulty} class has class-level methods for global state or can be
16
+ # instantiated to create an independent configuration.
17
17
  #
18
- # Fault-tolerance tools for ruby based on circuit-breakers
19
- module Faulty
18
+ # If you are using global state, call {Faulty#init} during your application's
19
+ # initialization. This is the simplest way to use {Faulty}. If you prefer, you
20
+ # can also call {Faulty.new} to create independent {Faulty} instances.
21
+ class Faulty
20
22
  class << self
21
23
  # Start the Faulty environment
22
24
  #
@@ -27,78 +29,79 @@ module Faulty
27
29
  # are spawned.
28
30
  #
29
31
  # If you prefer dependency-injection instead of global state, you can skip
30
- # init and pass a {Scope} directly to your dependencies.
32
+ # `init` and use {Faulty.new} to pass an instance directoy to your
33
+ # dependencies.
31
34
  #
32
- # @param scope_name [Symbol] The name of the default scope. Can be set to
33
- # `nil` to skip creating a default scope.
34
- # @param config [Hash] Attributes for {Scope::Options}
35
- # @yield [Scope::Options] For setting options in a block
35
+ # @param default_name [Symbol] The name of the default instance. Can be set
36
+ # to `nil` to skip creating a default instance.
37
+ # @param config [Hash] Attributes for {Faulty::Options}
38
+ # @yield [Faulty::Options] For setting options in a block
36
39
  # @return [self]
37
- def init(scope_name = :default, **config, &block)
38
- raise AlreadyInitializedError if @scopes
40
+ def init(default_name = :default, **config, &block)
41
+ raise AlreadyInitializedError if @instances
39
42
 
40
- @default_scope = scope_name
41
- @scopes = Concurrent::Map.new
42
- register(scope_name, Scope.new(**config, &block)) unless scope_name.nil?
43
+ @default_instance = default_name
44
+ @instances = Concurrent::Map.new
45
+ register(default_name, new(**config, &block)) unless default_name.nil?
43
46
  self
44
47
  rescue StandardError
45
- @scopes = nil
48
+ @instances = nil
46
49
  raise
47
50
  end
48
51
 
49
- # Get the default scope given during {.init}
52
+ # Get the default instance given during {.init}
50
53
  #
51
- # @return [Scope, nil] The default scope if it is registered
54
+ # @return [Faulty, nil] The default instance if it is registered
52
55
  def default
53
- raise UninitializedError unless @scopes
54
- raise MissingDefaultScopeError unless @default_scope
56
+ raise UninitializedError unless @instances
57
+ raise MissingDefaultInstanceError unless @default_instance
55
58
 
56
- self[@default_scope]
59
+ self[@default_instance]
57
60
  end
58
61
 
59
- # Get a scope by name
62
+ # Get an instance by name
60
63
  #
61
- # @return [Scope, nil] The named scope if it is registered
62
- def [](scope_name)
63
- raise UninitializedError unless @scopes
64
+ # @return [Faulty, nil] The named instance if it is registered
65
+ def [](name)
66
+ raise UninitializedError unless @instances
64
67
 
65
- @scopes[scope_name]
68
+ @instances[name]
66
69
  end
67
70
 
68
- # Register a scope to the global Faulty state
71
+ # Register an instance to the global Faulty state
69
72
  #
70
- # Will not replace an existing scope with the same name. Check the
71
- # return value if you need to know whether the scope already existed.
73
+ # Will not replace an existing instance with the same name. Check the
74
+ # return value if you need to know whether the instance already existed.
72
75
  #
73
- # @param name [Symbol] The name of the scope to register
74
- # @param scope [Scope] The scope to register
75
- # @return [Scope, nil] The previously-registered scope of that name if
76
+ # @param name [Symbol] The name of the instance to register
77
+ # @param instance [Faulty] The instance to register
78
+ # @return [Faulty, nil] The previously-registered instance of that name if
76
79
  # it already existed, otherwise nil.
77
- def register(name, scope)
78
- raise UninitializedError unless @scopes
80
+ def register(name, instance)
81
+ raise UninitializedError unless @instances
79
82
 
80
- @scopes.put_if_absent(name, scope)
83
+ @instances.put_if_absent(name, instance)
81
84
  end
82
85
 
83
- # Get the options for the default scope
86
+ # Get the options for the default instance
84
87
  #
85
- # @raise MissingDefaultScopeError If the default scope has not been created
86
- # @return [Scope::Options]
88
+ # @raise MissingDefaultInstanceError If the default instance has not been created
89
+ # @return [Faulty::Options]
87
90
  def options
88
91
  default.options
89
92
  end
90
93
 
91
- # Get or create a circuit for the default scope
94
+ # Get or create a circuit for the default instance
92
95
  #
93
- # @raise UninitializedError If the default scope has not been created
94
- # @param (see Scope#circuit)
95
- # @yield (see Scope#circuit)
96
- # @return (see Scope#circuit)
96
+ # @raise UninitializedError If the default instance has not been created
97
+ # @param (see Faulty#circuit)
98
+ # @yield (see Faulty#circuit)
99
+ # @return (see Faulty#circuit)
97
100
  def circuit(name, **config, &block)
98
101
  default.circuit(name, **config, &block)
99
102
  end
100
103
 
101
- # Get a list of all circuit names for the default scope
104
+ # Get a list of all circuit names for the default instance
102
105
  #
103
106
  # @return [Array<String>] The circuit names
104
107
  def list_circuits
@@ -115,4 +118,113 @@ module Faulty
115
118
  Time.now.to_i
116
119
  end
117
120
  end
121
+
122
+ attr_reader :options
123
+
124
+ # Options for {Faulty}
125
+ #
126
+ # @!attribute [r] cache
127
+ # @return [Cache::Interface] A cache backend if you want
128
+ # to use Faulty's cache support. Automatically wrapped in a
129
+ # {Cache::FaultTolerantProxy}. Default `Cache::Default.new`.
130
+ # @!attribute [r] storage
131
+ # @return [Storage::Interface] The storage backend.
132
+ # Automatically wrapped in a {Storage::FaultTolerantProxy}.
133
+ # Default `Storage::Memory.new`.
134
+ # @!attribute [r] listeners
135
+ # @return [Array] listeners Faulty event listeners
136
+ # @!attribute [r] notifier
137
+ # @return [Events::Notifier] A Faulty notifier. If given, listeners are
138
+ # ignored.
139
+ Options = Struct.new(
140
+ :cache,
141
+ :storage,
142
+ :listeners,
143
+ :notifier
144
+ ) do
145
+ include ImmutableOptions
146
+
147
+ private
148
+
149
+ def finalize
150
+ self.notifier ||= Events::Notifier.new(listeners || [])
151
+
152
+ self.storage ||= Storage::Memory.new
153
+ unless storage.fault_tolerant?
154
+ self.storage = Storage::FaultTolerantProxy.new(storage, notifier: notifier)
155
+ end
156
+
157
+ self.cache ||= Cache::Default.new
158
+ unless cache.fault_tolerant?
159
+ self.cache = Cache::FaultTolerantProxy.new(cache, notifier: notifier)
160
+ end
161
+ end
162
+
163
+ def required
164
+ %i[cache storage notifier]
165
+ end
166
+
167
+ def defaults
168
+ {
169
+ listeners: [Events::LogListener.new]
170
+ }
171
+ end
172
+ end
173
+
174
+ # Create a new {Faulty} instance
175
+ #
176
+ # Note, the process of creating a new instance is not thread safe,
177
+ # so make sure instances are setup during your application's initialization
178
+ # phase.
179
+ #
180
+ # For the most part, {Faulty} instances are independent, however for some
181
+ # cache and storage backends, you will need to ensure that the cache keys
182
+ # and circuit names don't overlap between instances. For example, if using the
183
+ # {Storage::Redis} storage backend, you should specify different key
184
+ # prefixes for each instance.
185
+ #
186
+ # @see Options
187
+ # @param options [Hash] Attributes for {Options}
188
+ # @yield [Options] For setting options in a block
189
+ def initialize(**options, &block)
190
+ @circuits = Concurrent::Map.new
191
+ @options = Options.new(options, &block)
192
+ end
193
+
194
+ # Create or retrieve a circuit
195
+ #
196
+ # Within an instance, circuit instances have unique names, so if the given circuit
197
+ # name already exists, then the existing circuit will be returned, otherwise
198
+ # a new circuit will be created. If an existing circuit is returned, then
199
+ # the {options} param and block are ignored.
200
+ #
201
+ # @param name [String] The name of the circuit
202
+ # @param options [Hash] Attributes for {Circuit::Options}
203
+ # @yield [Circuit::Options] For setting options in a block
204
+ # @return [Circuit] The new circuit or the existing circuit if it already exists
205
+ def circuit(name, **options, &block)
206
+ name = name.to_s
207
+ options = options.merge(circuit_options)
208
+ @circuits.compute_if_absent(name) do
209
+ Circuit.new(name, **options, &block)
210
+ end
211
+ end
212
+
213
+ # Get a list of all circuit names
214
+ #
215
+ # @return [Array<String>] The circuit names
216
+ def list_circuits
217
+ options.storage.list
218
+ end
219
+
220
+ private
221
+
222
+ # Get circuit options from the {Faulty} options
223
+ #
224
+ # @return [Hash] The circuit options
225
+ def circuit_options
226
+ options = @options.to_h
227
+ options.delete(:listeners)
228
+ options
229
+ end
118
230
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Faulty
3
+ class Faulty
4
4
  # The namespace for Faulty caching
5
5
  module Cache
6
6
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Faulty
3
+ class Faulty
4
4
  module Cache
5
5
  # The default cache implementation
6
6
  #
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Faulty
3
+ class Faulty
4
4
  module Cache
5
5
  # A wrapper for cache backends that may raise errors
6
6
  #
7
- # {Scope} automatically wraps all non-fault-tolerant cache backends with
7
+ # {Faulty#initialize} automatically wraps all non-fault-tolerant cache backends with
8
8
  # this class.
9
9
  #
10
10
  # If the cache backend raises a `StandardError`, it will be captured and
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Faulty
3
+ class Faulty
4
4
  module Cache
5
5
  # The interface required for a cache backend implementation
6
6
  #
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Faulty
3
+ class Faulty
4
4
  module Cache
5
5
  # A mock cache for testing
6
6
  #
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Faulty
3
+ class Faulty
4
4
  module Cache
5
5
  # A cache backend that does nothing
6
6
  #
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Faulty
3
+ class Faulty
4
4
  module Cache
5
5
  # A wrapper for a Rails or ActiveSupport cache
6
6
  #
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Faulty
3
+ class Faulty
4
4
  # Runs code protected by a circuit breaker
5
5
  #
6
6
  # https://www.martinfowler.com/bliki/CircuitBreaker.html
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Faulty
3
+ class Faulty
4
4
  # The base error for all Faulty errors
5
5
  class FaultyError < StandardError; end
6
6
 
@@ -20,10 +20,10 @@ module Faulty
20
20
  end
21
21
  end
22
22
 
23
- # Raised if getting the default scope without initializing one
24
- class MissingDefaultScopeError < FaultyError
23
+ # Raised if getting the default instance without initializing one
24
+ class MissingDefaultInstanceError < FaultyError
25
25
  def initialize(message = nil)
26
- message ||= 'No default scope. Create one with init or get your scope with Faulty[:scope_name]'
26
+ message ||= 'No default instance. Create one with init or get your instance with Faulty[:name]'
27
27
  super(message)
28
28
  end
29
29
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Faulty
3
+ class Faulty
4
4
  # The namespace for Faulty events and event listeners
5
5
  module Events
6
6
  # All possible events that can be raised by Faulty
@@ -21,5 +21,6 @@ module Faulty
21
21
  end
22
22
 
23
23
  require 'faulty/events/callback_listener'
24
- require 'faulty/events/notifier'
24
+ require 'faulty/events/honeybadger_listener'
25
25
  require 'faulty/events/log_listener'
26
+ require 'faulty/events/notifier'
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Faulty
3
+ class Faulty
4
4
  module Events
5
5
  # A simple listener implementation that uses callback blocks as handlers
6
6
  #
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Faulty
4
+ module Events
5
+ # Reports circuit errors to Honeybadger
6
+ #
7
+ # https://www.honeybadger.io/
8
+ #
9
+ # The honeybadger gem must be available.
10
+ class HoneybadgerListener
11
+ # (see ListenerInterface#handle)
12
+ def handle(event, payload)
13
+ return unless EVENTS.include?(event)
14
+
15
+ send(event, payload) if respond_to?(event, true)
16
+ end
17
+
18
+ private
19
+
20
+ def circuit_failure(payload)
21
+ _circuit_error(payload)
22
+ end
23
+
24
+ def circuit_opened(payload)
25
+ _circuit_error(payload)
26
+ end
27
+
28
+ def circuit_reopened(payload)
29
+ _circuit_error(payload)
30
+ end
31
+
32
+ def cache_failure(payload)
33
+ Honeybadger.notify(payload[:error], context: {
34
+ action: payload[:action],
35
+ key: payload[:key]
36
+ })
37
+ end
38
+
39
+ def storage_failure(payload)
40
+ Honeybadger.notify(payload[:error], context: {
41
+ action: payload[:action],
42
+ circuit: payload[:circuit]&.name
43
+ })
44
+ end
45
+
46
+ def _circuit_error(payload)
47
+ Honeybadger.notify(payload[:error], context: {
48
+ circuit: payload[:circuit].name
49
+ })
50
+ end
51
+ end
52
+ end
53
+ end