faulty 0.1.0 → 0.2.0

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.
@@ -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