faulty 0.1.2 → 0.4.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/.github/workflows/ci.yml +49 -0
- data/.rubocop.yml +9 -0
- data/CHANGELOG.md +50 -2
- data/Gemfile +22 -0
- data/README.md +836 -220
- data/bin/check-version +5 -1
- data/bin/console +1 -1
- data/faulty.gemspec +4 -11
- data/lib/faulty.rb +157 -43
- data/lib/faulty/cache.rb +3 -1
- data/lib/faulty/cache/auto_wire.rb +58 -0
- data/lib/faulty/cache/circuit_proxy.rb +61 -0
- data/lib/faulty/cache/default.rb +10 -21
- data/lib/faulty/cache/fault_tolerant_proxy.rb +15 -4
- 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 +9 -10
- data/lib/faulty/circuit.rb +10 -5
- data/lib/faulty/error.rb +18 -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 +5 -6
- 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 +3 -2
- data/lib/faulty/storage.rb +4 -1
- data/lib/faulty/storage/auto_wire.rb +107 -0
- data/lib/faulty/storage/circuit_proxy.rb +64 -0
- data/lib/faulty/storage/fallback_chain.rb +207 -0
- data/lib/faulty/storage/fault_tolerant_proxy.rb +51 -56
- data/lib/faulty/storage/interface.rb +1 -1
- data/lib/faulty/storage/memory.rb +8 -4
- data/lib/faulty/storage/redis.rb +75 -13
- data/lib/faulty/version.rb +2 -2
- metadata +18 -122
- data/.travis.yml +0 -44
- data/lib/faulty/scope.rb +0 -117
data/bin/check-version
CHANGED
@@ -3,8 +3,12 @@
|
|
3
3
|
set -e
|
4
4
|
|
5
5
|
tag="$(git describe --abbrev=0 2>/dev/null || echo)"
|
6
|
+
echo "Tag: ${tag}"
|
6
7
|
tag="${tag#v}"
|
8
|
+
echo "Git Version: ${tag}"
|
7
9
|
[ "$tag" = '' ] && exit 0
|
10
|
+
gem_version="$(ruby -r ./lib/faulty/version -e "puts Faulty.version" | tail -n1)"
|
11
|
+
echo "Gem Version: ${gem_version}"
|
8
12
|
|
9
|
-
tag_gt_version
|
13
|
+
tag_gt_version="$(ruby -r ./lib/faulty/version -e "puts Faulty.version >= Gem::Version.new('${tag}')" | tail -n1)"
|
10
14
|
test "$tag_gt_version" = true
|
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 '
|
32
|
-
spec.add_development_dependency 'redis', '~> 3.0'
|
29
|
+
spec.add_development_dependency 'honeybadger', '>= 2.0'
|
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
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'securerandom'
|
4
|
+
require 'forwardable'
|
4
5
|
require 'concurrent-ruby'
|
5
6
|
|
6
7
|
require 'faulty/immutable_options'
|
@@ -9,14 +10,16 @@ require 'faulty/circuit'
|
|
9
10
|
require 'faulty/error'
|
10
11
|
require 'faulty/events'
|
11
12
|
require 'faulty/result'
|
12
|
-
require 'faulty/scope'
|
13
13
|
require 'faulty/status'
|
14
14
|
require 'faulty/storage'
|
15
15
|
|
16
|
-
# The
|
16
|
+
# The {Faulty} class has class-level methods for global state or can be
|
17
|
+
# instantiated to create an independent configuration.
|
17
18
|
#
|
18
|
-
#
|
19
|
-
|
19
|
+
# If you are using global state, call {Faulty#init} during your application's
|
20
|
+
# initialization. This is the simplest way to use {Faulty}. If you prefer, you
|
21
|
+
# can also call {Faulty.new} to create independent {Faulty} instances.
|
22
|
+
class Faulty
|
20
23
|
class << self
|
21
24
|
# Start the Faulty environment
|
22
25
|
#
|
@@ -27,78 +30,79 @@ module Faulty
|
|
27
30
|
# are spawned.
|
28
31
|
#
|
29
32
|
# If you prefer dependency-injection instead of global state, you can skip
|
30
|
-
# init and
|
33
|
+
# `init` and use {Faulty.new} to pass an instance directoy to your
|
34
|
+
# dependencies.
|
31
35
|
#
|
32
|
-
# @param
|
33
|
-
#
|
34
|
-
# @param config [Hash] Attributes for {
|
35
|
-
# @yield [
|
36
|
+
# @param default_name [Symbol] The name of the default instance. Can be set
|
37
|
+
# to `nil` to skip creating a default instance.
|
38
|
+
# @param config [Hash] Attributes for {Faulty::Options}
|
39
|
+
# @yield [Faulty::Options] For setting options in a block
|
36
40
|
# @return [self]
|
37
|
-
def init(
|
38
|
-
raise AlreadyInitializedError if @
|
41
|
+
def init(default_name = :default, **config, &block)
|
42
|
+
raise AlreadyInitializedError if @instances
|
39
43
|
|
40
|
-
@
|
41
|
-
@
|
42
|
-
register(
|
44
|
+
@default_instance = default_name
|
45
|
+
@instances = Concurrent::Map.new
|
46
|
+
register(default_name, new(**config, &block)) unless default_name.nil?
|
43
47
|
self
|
44
48
|
rescue StandardError
|
45
|
-
@
|
49
|
+
@instances = nil
|
46
50
|
raise
|
47
51
|
end
|
48
52
|
|
49
|
-
# Get the default
|
53
|
+
# Get the default instance given during {.init}
|
50
54
|
#
|
51
|
-
# @return [
|
55
|
+
# @return [Faulty, nil] The default instance if it is registered
|
52
56
|
def default
|
53
|
-
raise UninitializedError unless @
|
54
|
-
raise
|
57
|
+
raise UninitializedError unless @instances
|
58
|
+
raise MissingDefaultInstanceError unless @default_instance
|
55
59
|
|
56
|
-
self[@
|
60
|
+
self[@default_instance]
|
57
61
|
end
|
58
62
|
|
59
|
-
# Get
|
63
|
+
# Get an instance by name
|
60
64
|
#
|
61
|
-
# @return [
|
62
|
-
def [](
|
63
|
-
raise UninitializedError unless @
|
65
|
+
# @return [Faulty, nil] The named instance if it is registered
|
66
|
+
def [](name)
|
67
|
+
raise UninitializedError unless @instances
|
64
68
|
|
65
|
-
@
|
69
|
+
@instances[name]
|
66
70
|
end
|
67
71
|
|
68
|
-
# Register
|
72
|
+
# Register an instance to the global Faulty state
|
69
73
|
#
|
70
|
-
# Will not replace an existing
|
71
|
-
# return value if you need to know whether the
|
74
|
+
# Will not replace an existing instance with the same name. Check the
|
75
|
+
# return value if you need to know whether the instance already existed.
|
72
76
|
#
|
73
|
-
# @param name [Symbol] The name of the
|
74
|
-
# @param
|
75
|
-
# @return [
|
77
|
+
# @param name [Symbol] The name of the instance to register
|
78
|
+
# @param instance [Faulty] The instance to register
|
79
|
+
# @return [Faulty, nil] The previously-registered instance of that name if
|
76
80
|
# it already existed, otherwise nil.
|
77
|
-
def register(name,
|
78
|
-
raise UninitializedError unless @
|
81
|
+
def register(name, instance)
|
82
|
+
raise UninitializedError unless @instances
|
79
83
|
|
80
|
-
@
|
84
|
+
@instances.put_if_absent(name, instance)
|
81
85
|
end
|
82
86
|
|
83
|
-
# Get the options for the default
|
87
|
+
# Get the options for the default instance
|
84
88
|
#
|
85
|
-
# @raise
|
86
|
-
# @return [
|
89
|
+
# @raise MissingDefaultInstanceError If the default instance has not been created
|
90
|
+
# @return [Faulty::Options]
|
87
91
|
def options
|
88
92
|
default.options
|
89
93
|
end
|
90
94
|
|
91
|
-
# Get or create a circuit for the default
|
95
|
+
# Get or create a circuit for the default instance
|
92
96
|
#
|
93
|
-
# @raise UninitializedError If the default
|
94
|
-
# @param (see
|
95
|
-
# @yield (see
|
96
|
-
# @return (see
|
97
|
+
# @raise UninitializedError If the default instance has not been created
|
98
|
+
# @param (see Faulty#circuit)
|
99
|
+
# @yield (see Faulty#circuit)
|
100
|
+
# @return (see Faulty#circuit)
|
97
101
|
def circuit(name, **config, &block)
|
98
102
|
default.circuit(name, **config, &block)
|
99
103
|
end
|
100
104
|
|
101
|
-
# Get a list of all circuit names for the default
|
105
|
+
# Get a list of all circuit names for the default instance
|
102
106
|
#
|
103
107
|
# @return [Array<String>] The circuit names
|
104
108
|
def list_circuits
|
@@ -115,4 +119,114 @@ module Faulty
|
|
115
119
|
Time.now.to_i
|
116
120
|
end
|
117
121
|
end
|
122
|
+
|
123
|
+
attr_reader :options
|
124
|
+
|
125
|
+
# Options for {Faulty}
|
126
|
+
#
|
127
|
+
# @!attribute [r] cache
|
128
|
+
# @see Cache::AutoWire
|
129
|
+
# @return [Cache::Interface] A cache backend if you want
|
130
|
+
# to use Faulty's cache support. Automatically wrapped in a
|
131
|
+
# {Cache::AutoWire}. Default `Cache::AutoWire.new`.
|
132
|
+
# @!attribute [r] circuit_defaults
|
133
|
+
# @see Circuit::Options
|
134
|
+
# @return [Hash] A hash of default options to be used when creating
|
135
|
+
# new circuits. See {Circuit::Options} for a full list.
|
136
|
+
# @!attribute [r] storage
|
137
|
+
# @see Storage::AutoWire
|
138
|
+
# @return [Storage::Interface, Array<Storage::Interface>] The storage
|
139
|
+
# backend. Automatically wrapped in a {Storage::AutoWire}, so this can also
|
140
|
+
# be given an array of prioritized backends. Default `Storage::AutoWire.new`.
|
141
|
+
# @!attribute [r] listeners
|
142
|
+
# @see Events::ListenerInterface
|
143
|
+
# @return [Array] listeners Faulty event listeners
|
144
|
+
# @!attribute [r] notifier
|
145
|
+
# @return [Events::Notifier] A Faulty notifier. If given, listeners are
|
146
|
+
# ignored.
|
147
|
+
Options = Struct.new(
|
148
|
+
:cache,
|
149
|
+
:circuit_defaults,
|
150
|
+
:storage,
|
151
|
+
:listeners,
|
152
|
+
:notifier
|
153
|
+
) do
|
154
|
+
include ImmutableOptions
|
155
|
+
|
156
|
+
private
|
157
|
+
|
158
|
+
def finalize
|
159
|
+
self.notifier ||= Events::Notifier.new(listeners || [])
|
160
|
+
self.storage = Storage::AutoWire.wrap(storage, notifier: notifier)
|
161
|
+
self.cache = Cache::AutoWire.wrap(cache, notifier: notifier)
|
162
|
+
end
|
163
|
+
|
164
|
+
def required
|
165
|
+
%i[cache circuit_defaults storage notifier]
|
166
|
+
end
|
167
|
+
|
168
|
+
def defaults
|
169
|
+
{
|
170
|
+
circuit_defaults: {},
|
171
|
+
listeners: [Events::LogListener.new]
|
172
|
+
}
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# Create a new {Faulty} instance
|
177
|
+
#
|
178
|
+
# Note, the process of creating a new instance is not thread safe,
|
179
|
+
# so make sure instances are setup during your application's initialization
|
180
|
+
# phase.
|
181
|
+
#
|
182
|
+
# For the most part, {Faulty} instances are independent, however for some
|
183
|
+
# cache and storage backends, you will need to ensure that the cache keys
|
184
|
+
# and circuit names don't overlap between instances. For example, if using the
|
185
|
+
# {Storage::Redis} storage backend, you should specify different key
|
186
|
+
# prefixes for each instance.
|
187
|
+
#
|
188
|
+
# @see Options
|
189
|
+
# @param options [Hash] Attributes for {Options}
|
190
|
+
# @yield [Options] For setting options in a block
|
191
|
+
def initialize(**options, &block)
|
192
|
+
@circuits = Concurrent::Map.new
|
193
|
+
@options = Options.new(options, &block)
|
194
|
+
end
|
195
|
+
|
196
|
+
# Create or retrieve a circuit
|
197
|
+
#
|
198
|
+
# Within an instance, circuit instances have unique names, so if the given circuit
|
199
|
+
# name already exists, then the existing circuit will be returned, otherwise
|
200
|
+
# a new circuit will be created. If an existing circuit is returned, then
|
201
|
+
# the {options} param and block are ignored.
|
202
|
+
#
|
203
|
+
# @param name [String] The name of the circuit
|
204
|
+
# @param options [Hash] Attributes for {Circuit::Options}
|
205
|
+
# @yield [Circuit::Options] For setting options in a block
|
206
|
+
# @return [Circuit] The new circuit or the existing circuit if it already exists
|
207
|
+
def circuit(name, **options, &block)
|
208
|
+
name = name.to_s
|
209
|
+
@circuits.compute_if_absent(name) do
|
210
|
+
options = circuit_options.merge(options)
|
211
|
+
Circuit.new(name, **options, &block)
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
# Get a list of all circuit names
|
216
|
+
#
|
217
|
+
# @return [Array<String>] The circuit names
|
218
|
+
def list_circuits
|
219
|
+
options.storage.list
|
220
|
+
end
|
221
|
+
|
222
|
+
private
|
223
|
+
|
224
|
+
# Get circuit options from the {Faulty} options
|
225
|
+
#
|
226
|
+
# @return [Hash] The circuit options
|
227
|
+
def circuit_options
|
228
|
+
@options.to_h
|
229
|
+
.select { |k, _v| %i[cache storage notifier].include?(k) }
|
230
|
+
.merge(options.circuit_defaults)
|
231
|
+
end
|
118
232
|
end
|
data/lib/faulty/cache.rb
CHANGED
@@ -1,12 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
class Faulty
|
4
4
|
# The namespace for Faulty caching
|
5
5
|
module Cache
|
6
6
|
end
|
7
7
|
end
|
8
8
|
|
9
|
+
require 'faulty/cache/auto_wire'
|
9
10
|
require 'faulty/cache/default'
|
11
|
+
require 'faulty/cache/circuit_proxy'
|
10
12
|
require 'faulty/cache/fault_tolerant_proxy'
|
11
13
|
require 'faulty/cache/mock'
|
12
14
|
require 'faulty/cache/null'
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Faulty
|
4
|
+
module Cache
|
5
|
+
# Automatically configure a cache backend
|
6
|
+
#
|
7
|
+
# Used by {Faulty#initialize} to setup sensible cache defaults
|
8
|
+
class AutoWire
|
9
|
+
# Options for {AutoWire}
|
10
|
+
#
|
11
|
+
# @!attribute [r] circuit
|
12
|
+
# @return [Circuit] A circuit for {CircuitProxy} if one is created.
|
13
|
+
# When modifying this, be careful to use only a reliable circuit
|
14
|
+
# storage backend so that you don't introduce cascading failures.
|
15
|
+
# @!attribute [r] notifier
|
16
|
+
# @return [Events::Notifier] A Faulty notifier. If given, listeners are
|
17
|
+
# ignored.
|
18
|
+
Options = Struct.new(
|
19
|
+
:circuit,
|
20
|
+
:notifier
|
21
|
+
) do
|
22
|
+
include ImmutableOptions
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def required
|
27
|
+
%i[notifier]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class << self
|
32
|
+
# Wrap a cache backend with sensible defaults
|
33
|
+
#
|
34
|
+
# If the cache is `nil`, create a new {Default}.
|
35
|
+
#
|
36
|
+
# If the backend is not fault tolerant, wrap it in {CircuitProxy} and
|
37
|
+
# {FaultTolerantProxy}.
|
38
|
+
#
|
39
|
+
# @param cache [Interface] A cache backend
|
40
|
+
# @param options [Hash] Attributes for {Options}
|
41
|
+
# @yield [Options] For setting options in a block
|
42
|
+
def wrap(cache, **options, &block)
|
43
|
+
options = Options.new(options, &block)
|
44
|
+
if cache.nil?
|
45
|
+
Cache::Default.new
|
46
|
+
elsif cache.fault_tolerant?
|
47
|
+
cache
|
48
|
+
else
|
49
|
+
Cache::FaultTolerantProxy.new(
|
50
|
+
Cache::CircuitProxy.new(cache, circuit: options.circuit, notifier: options.notifier),
|
51
|
+
notifier: options.notifier
|
52
|
+
)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Faulty
|
4
|
+
module Cache
|
5
|
+
# A circuit wrapper for cache backends
|
6
|
+
#
|
7
|
+
# This class uses an internal {Circuit} to prevent the cache backend from
|
8
|
+
# causing application issues. If the backend fails continuously, this
|
9
|
+
# circuit will trip to prevent cascading failures. This internal circuit
|
10
|
+
# uses an independent in-memory backend by default.
|
11
|
+
class CircuitProxy
|
12
|
+
attr_reader :options
|
13
|
+
|
14
|
+
# Options for {CircuitProxy}
|
15
|
+
#
|
16
|
+
# @!attribute [r] circuit
|
17
|
+
# @return [Circuit] A replacement for the internal circuit. When
|
18
|
+
# modifying this, be careful to use only a reliable circuit storage
|
19
|
+
# backend so that you don't introduce cascading failures.
|
20
|
+
# @!attribute [r] notifier
|
21
|
+
# @return [Events::Notifier] A Faulty notifier to use for failure
|
22
|
+
# notifications. If `circuit` is given, this is ignored.
|
23
|
+
Options = Struct.new(
|
24
|
+
:circuit,
|
25
|
+
:notifier
|
26
|
+
) do
|
27
|
+
include ImmutableOptions
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def finalize
|
32
|
+
raise ArgumentError, 'The circuit or notifier option must be given' unless notifier || circuit
|
33
|
+
|
34
|
+
self.circuit ||= Circuit.new(
|
35
|
+
Faulty::Storage::CircuitProxy.name,
|
36
|
+
notifier: notifier,
|
37
|
+
cache: Cache::Null.new
|
38
|
+
)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# @param cache [Cache::Interface] The cache backend to wrap
|
43
|
+
# @param options [Hash] Attributes for {Options}
|
44
|
+
# @yield [Options] For setting options in a block
|
45
|
+
def initialize(cache, **options, &block)
|
46
|
+
@cache = cache
|
47
|
+
@options = Options.new(options, &block)
|
48
|
+
end
|
49
|
+
|
50
|
+
%i[read write].each do |method|
|
51
|
+
define_method(method) do |*args|
|
52
|
+
options.circuit.run { @cache.public_send(method, *args) }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def fault_tolerant?
|
57
|
+
@cache.fault_tolerant?
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|