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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.rubocop.yml +6 -0
- data/.travis.yml +4 -2
- data/CHANGELOG.md +34 -0
- data/Gemfile +17 -0
- data/README.md +177 -47
- data/bin/console +1 -1
- data/faulty.gemspec +3 -10
- data/lib/faulty.rb +155 -43
- data/lib/faulty/cache.rb +1 -1
- data/lib/faulty/cache/default.rb +1 -1
- data/lib/faulty/cache/fault_tolerant_proxy.rb +2 -2
- data/lib/faulty/cache/interface.rb +1 -1
- data/lib/faulty/cache/mock.rb +1 -1
- data/lib/faulty/cache/null.rb +1 -1
- data/lib/faulty/cache/rails.rb +1 -1
- data/lib/faulty/circuit.rb +1 -1
- data/lib/faulty/error.rb +4 -4
- data/lib/faulty/events.rb +3 -2
- data/lib/faulty/events/callback_listener.rb +1 -1
- data/lib/faulty/events/honeybadger_listener.rb +53 -0
- data/lib/faulty/events/listener_interface.rb +1 -1
- data/lib/faulty/events/log_listener.rb +1 -1
- data/lib/faulty/events/notifier.rb +11 -2
- data/lib/faulty/immutable_options.rb +1 -1
- data/lib/faulty/result.rb +2 -2
- data/lib/faulty/status.rb +1 -1
- data/lib/faulty/storage.rb +1 -1
- data/lib/faulty/storage/fault_tolerant_proxy.rb +8 -10
- data/lib/faulty/storage/interface.rb +1 -1
- data/lib/faulty/storage/memory.rb +2 -2
- data/lib/faulty/storage/redis.rb +9 -9
- data/lib/faulty/version.rb +2 -2
- metadata +14 -123
- data/lib/faulty/scope.rb +0 -117
data/bin/console
CHANGED
data/faulty.gemspec
CHANGED
@@ -23,21 +23,14 @@ Gem::Specification.new do |spec|
|
|
23
23
|
|
24
24
|
spec.add_runtime_dependency 'concurrent-ruby', '~> 1.0'
|
25
25
|
|
26
|
-
|
27
|
-
|
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 '
|
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
|
data/lib/faulty.rb
CHANGED
@@ -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
|
15
|
+
# The {Faulty} class has class-level methods for global state or can be
|
16
|
+
# instantiated to create an independent configuration.
|
17
17
|
#
|
18
|
-
#
|
19
|
-
|
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
|
32
|
+
# `init` and use {Faulty.new} to pass an instance directoy to your
|
33
|
+
# dependencies.
|
31
34
|
#
|
32
|
-
# @param
|
33
|
-
#
|
34
|
-
# @param config [Hash] Attributes for {
|
35
|
-
# @yield [
|
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(
|
38
|
-
raise AlreadyInitializedError if @
|
40
|
+
def init(default_name = :default, **config, &block)
|
41
|
+
raise AlreadyInitializedError if @instances
|
39
42
|
|
40
|
-
@
|
41
|
-
@
|
42
|
-
register(
|
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
|
-
@
|
48
|
+
@instances = nil
|
46
49
|
raise
|
47
50
|
end
|
48
51
|
|
49
|
-
# Get the default
|
52
|
+
# Get the default instance given during {.init}
|
50
53
|
#
|
51
|
-
# @return [
|
54
|
+
# @return [Faulty, nil] The default instance if it is registered
|
52
55
|
def default
|
53
|
-
raise UninitializedError unless @
|
54
|
-
raise
|
56
|
+
raise UninitializedError unless @instances
|
57
|
+
raise MissingDefaultInstanceError unless @default_instance
|
55
58
|
|
56
|
-
self[@
|
59
|
+
self[@default_instance]
|
57
60
|
end
|
58
61
|
|
59
|
-
# Get
|
62
|
+
# Get an instance by name
|
60
63
|
#
|
61
|
-
# @return [
|
62
|
-
def [](
|
63
|
-
raise UninitializedError unless @
|
64
|
+
# @return [Faulty, nil] The named instance if it is registered
|
65
|
+
def [](name)
|
66
|
+
raise UninitializedError unless @instances
|
64
67
|
|
65
|
-
@
|
68
|
+
@instances[name]
|
66
69
|
end
|
67
70
|
|
68
|
-
# Register
|
71
|
+
# Register an instance to the global Faulty state
|
69
72
|
#
|
70
|
-
# Will not replace an existing
|
71
|
-
# return value if you need to know whether the
|
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
|
74
|
-
# @param
|
75
|
-
# @return [
|
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,
|
78
|
-
raise UninitializedError unless @
|
80
|
+
def register(name, instance)
|
81
|
+
raise UninitializedError unless @instances
|
79
82
|
|
80
|
-
@
|
83
|
+
@instances.put_if_absent(name, instance)
|
81
84
|
end
|
82
85
|
|
83
|
-
# Get the options for the default
|
86
|
+
# Get the options for the default instance
|
84
87
|
#
|
85
|
-
# @raise
|
86
|
-
# @return [
|
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
|
94
|
+
# Get or create a circuit for the default instance
|
92
95
|
#
|
93
|
-
# @raise UninitializedError If the default
|
94
|
-
# @param (see
|
95
|
-
# @yield (see
|
96
|
-
# @return (see
|
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
|
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
|
data/lib/faulty/cache.rb
CHANGED
data/lib/faulty/cache/default.rb
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
class Faulty
|
4
4
|
module Cache
|
5
5
|
# A wrapper for cache backends that may raise errors
|
6
6
|
#
|
7
|
-
# {
|
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
|
data/lib/faulty/cache/mock.rb
CHANGED
data/lib/faulty/cache/null.rb
CHANGED
data/lib/faulty/cache/rails.rb
CHANGED
data/lib/faulty/circuit.rb
CHANGED
data/lib/faulty/error.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
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
|
24
|
-
class
|
23
|
+
# Raised if getting the default instance without initializing one
|
24
|
+
class MissingDefaultInstanceError < FaultyError
|
25
25
|
def initialize(message = nil)
|
26
|
-
message ||= 'No default
|
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
|
data/lib/faulty/events.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
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/
|
24
|
+
require 'faulty/events/honeybadger_listener'
|
25
25
|
require 'faulty/events/log_listener'
|
26
|
+
require 'faulty/events/notifier'
|
@@ -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
|