faulty 0.1.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +9 -0
- data/.travis.yml +4 -2
- data/CHANGELOG.md +37 -1
- data/Gemfile +17 -0
- data/README.md +333 -55
- data/bin/check-version +5 -1
- data/bin/console +1 -1
- data/faulty.gemspec +3 -10
- data/lib/faulty.rb +149 -43
- data/lib/faulty/cache.rb +3 -1
- data/lib/faulty/cache/auto_wire.rb +65 -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 +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 +4 -1
- data/lib/faulty/storage/auto_wire.rb +122 -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 +55 -60
- 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 +13 -118
- data/lib/faulty/scope.rb +0 -117
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Faulty
|
4
|
+
module Storage
|
5
|
+
# A circuit wrapper for storage backends
|
6
|
+
#
|
7
|
+
# This class uses an internal {Circuit} to prevent the storage 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 storage backend
|
19
|
+
# so that you don't introduce cascading failures.
|
20
|
+
# @!attribute [r] notifier
|
21
|
+
# @return [Events::Notifier] A Faulty notifier to use for circuit
|
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 storage [Storage::Interface] The storage backend to wrap
|
43
|
+
# @param options [Hash] Attributes for {Options}
|
44
|
+
# @yield [Options] For setting options in a block
|
45
|
+
def initialize(storage, **options, &block)
|
46
|
+
@storage = storage
|
47
|
+
@options = Options.new(options, &block)
|
48
|
+
end
|
49
|
+
|
50
|
+
%i[entry open reopen close lock unlock reset status history list].each do |method|
|
51
|
+
define_method(method) do |*args|
|
52
|
+
options.circuit.run { @storage.public_send(method, *args) }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# This cache makes any storage fault tolerant, so this is always `true`
|
57
|
+
#
|
58
|
+
# @return [true]
|
59
|
+
def fault_tolerant?
|
60
|
+
@storage.fault_tolerant?
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,207 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Faulty
|
4
|
+
module Storage
|
5
|
+
# An prioritized list of storage backends
|
6
|
+
#
|
7
|
+
# If any backend fails, the next will be tried until one succeeds. This
|
8
|
+
# should typically be used when using a fault-prone backend such as
|
9
|
+
# {Storage::Redis}.
|
10
|
+
#
|
11
|
+
# This is used by {Faulty#initialize} if the `storage` option is set to an
|
12
|
+
# array.
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
# # This storage will try Redis first, then fallback to memory storage
|
16
|
+
# # if Redis is unavailable.
|
17
|
+
# storage = Faulty::Storage::FallbackChain.new([
|
18
|
+
# Faulty::Storage::Redis.new,
|
19
|
+
# Faulty::Storage::Memory.new
|
20
|
+
# ])
|
21
|
+
class FallbackChain
|
22
|
+
attr_reader :options
|
23
|
+
|
24
|
+
# Options for {FallbackChain}
|
25
|
+
#
|
26
|
+
# @!attribute [r] notifier
|
27
|
+
# @return [Events::Notifier] A Faulty notifier
|
28
|
+
Options = Struct.new(
|
29
|
+
:notifier
|
30
|
+
) do
|
31
|
+
include ImmutableOptions
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def required
|
36
|
+
%i[notifier]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Create a new {FallbackChain} to automatically fallback to reliable storage
|
41
|
+
#
|
42
|
+
# @param storages [Array<Storage::Interface>] An array of storage backends.
|
43
|
+
# The primary storage should be specified first. If that one fails,
|
44
|
+
# additional entries will be tried in sequence until one succeeds.
|
45
|
+
# @param options [Hash] Attributes for {Options}
|
46
|
+
# @yield [Options] For setting options in a block
|
47
|
+
def initialize(storages, **options, &block)
|
48
|
+
@storages = storages
|
49
|
+
@options = Options.new(options, &block)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Create a circuit entry in the first available storage backend
|
53
|
+
#
|
54
|
+
# @param (see Interface#entry)
|
55
|
+
# @return (see Interface#entry)
|
56
|
+
def entry(circuit, time, success)
|
57
|
+
send_chain(:entry, circuit, time, success) do |e|
|
58
|
+
options.notifier.notify(:storage_failure, circuit: circuit, action: :entry, error: e)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Open a circuit in the first available storage backend
|
63
|
+
#
|
64
|
+
# @param (see Interface#open)
|
65
|
+
# @return (see Interface#open)
|
66
|
+
def open(circuit, opened_at)
|
67
|
+
send_chain(:open, circuit, opened_at) do |e|
|
68
|
+
options.notifier.notify(:storage_failure, circuit: circuit, action: :open, error: e)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Reopen a circuit in the first available storage backend
|
73
|
+
#
|
74
|
+
# @param (see Interface#reopen)
|
75
|
+
# @return (see Interface#reopen)
|
76
|
+
def reopen(circuit, opened_at, previous_opened_at)
|
77
|
+
send_chain(:reopen, circuit, opened_at, previous_opened_at) do |e|
|
78
|
+
options.notifier.notify(:storage_failure, circuit: circuit, action: :reopen, error: e)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Close a circuit in the first available storage backend
|
83
|
+
#
|
84
|
+
# @param (see Interface#close)
|
85
|
+
# @return (see Interface#close)
|
86
|
+
def close(circuit)
|
87
|
+
send_chain(:close, circuit) do |e|
|
88
|
+
options.notifier.notify(:storage_failure, circuit: circuit, action: :close, error: e)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Lock a circuit in all storage backends
|
93
|
+
#
|
94
|
+
# @param (see Interface#lock)
|
95
|
+
# @return (see Interface#lock)
|
96
|
+
def lock(circuit, state)
|
97
|
+
send_all(:lock, circuit, state)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Unlock a circuit in all storage backends
|
101
|
+
#
|
102
|
+
# @param (see Interface#unlock)
|
103
|
+
# @return (see Interface#unlock)
|
104
|
+
def unlock(circuit)
|
105
|
+
send_all(:unlock, circuit)
|
106
|
+
end
|
107
|
+
|
108
|
+
# Reset a circuit in all storage backends
|
109
|
+
#
|
110
|
+
# @param (see Interface#reset)
|
111
|
+
# @return (see Interface#reset)
|
112
|
+
def reset(circuit)
|
113
|
+
send_all(:reset, circuit)
|
114
|
+
end
|
115
|
+
|
116
|
+
# Get the status of a circuit from the first available storage backend
|
117
|
+
#
|
118
|
+
# @param (see Interface#status)
|
119
|
+
# @return (see Interface#status)
|
120
|
+
def status(circuit)
|
121
|
+
send_chain(:status, circuit) do |e|
|
122
|
+
options.notifier.notify(:storage_failure, circuit: circuit, action: :status, error: e)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Get the history of a circuit from the first available storage backend
|
127
|
+
#
|
128
|
+
# @param (see Interface#history)
|
129
|
+
# @return (see Interface#history)
|
130
|
+
def history(circuit)
|
131
|
+
send_chain(:history, circuit) do |e|
|
132
|
+
options.notifier.notify(:storage_failure, circuit: circuit, action: :history, error: e)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# Get the list of circuits from the first available storage backend
|
137
|
+
#
|
138
|
+
# @param (see Interface#list)
|
139
|
+
# @return (see Interface#list)
|
140
|
+
def list
|
141
|
+
send_chain(:list) do |e|
|
142
|
+
options.notifier.notify(:storage_failure, action: :list, error: e)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# This is fault tolerant if any of the available backends are fault tolerant
|
147
|
+
#
|
148
|
+
# @param (see Interface#fault_tolerant?)
|
149
|
+
# @return (see Interface#fault_tolerant?)
|
150
|
+
def fault_tolerant?
|
151
|
+
@storages.any?(&:fault_tolerant?)
|
152
|
+
end
|
153
|
+
|
154
|
+
private
|
155
|
+
|
156
|
+
# Call a method on the backend and return the first successful result
|
157
|
+
#
|
158
|
+
# Short-circuits, so that if a call succeeds, no additional backends are
|
159
|
+
# called.
|
160
|
+
#
|
161
|
+
# @param method [Symbol] The method to call
|
162
|
+
# @param args [Array] The arguments to send
|
163
|
+
# @raise [AllFailedError] AllFailedError if all backends fail
|
164
|
+
# @return The return value from the first successful call
|
165
|
+
def send_chain(method, *args)
|
166
|
+
errors = []
|
167
|
+
@storages.each do |s|
|
168
|
+
begin
|
169
|
+
return s.public_send(method, *args)
|
170
|
+
rescue StandardError => e
|
171
|
+
errors << e
|
172
|
+
yield e
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
raise AllFailedError.new("#{self.class}##{method} failed for all storage backends", errors)
|
177
|
+
end
|
178
|
+
|
179
|
+
# Call a method on every backend
|
180
|
+
#
|
181
|
+
# @param method [Symbol] The method to call
|
182
|
+
# @param args [Array] The arguments to send
|
183
|
+
# @raise [AllFailedError] AllFailedError if all backends fail
|
184
|
+
# @raise [PartialFailureError] PartialFailureError if some but not all
|
185
|
+
# backends fail
|
186
|
+
# @return [nil]
|
187
|
+
def send_all(method, *args)
|
188
|
+
errors = []
|
189
|
+
@storages.each do |s|
|
190
|
+
begin
|
191
|
+
s.public_send(method, *args)
|
192
|
+
rescue StandardError => e
|
193
|
+
errors << e
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
if errors.empty?
|
198
|
+
nil
|
199
|
+
elsif errors.size < @storages.size
|
200
|
+
raise PartialFailureError.new("#{self.class}##{method} failed for some storage backends", errors)
|
201
|
+
else
|
202
|
+
raise AllFailedError.new("#{self.class}##{method} failed for all storage backends", errors)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
@@ -1,15 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
class Faulty
|
4
4
|
module Storage
|
5
5
|
# A wrapper for storage backends that may raise errors
|
6
6
|
#
|
7
|
-
# {
|
7
|
+
# {Faulty#initialize} automatically wraps all non-fault-tolerant storage backends with
|
8
8
|
# this class.
|
9
9
|
#
|
10
10
|
# If the storage backend raises a `StandardError`, it will be captured and
|
11
11
|
# sent to the notifier.
|
12
12
|
class FaultTolerantProxy
|
13
|
+
extend Forwardable
|
14
|
+
|
13
15
|
attr_reader :options
|
14
16
|
|
15
17
|
# Options for {FaultTolerantProxy}
|
@@ -36,6 +38,53 @@ module Faulty
|
|
36
38
|
@options = Options.new(options, &block)
|
37
39
|
end
|
38
40
|
|
41
|
+
# Wrap a storage backend in a FaultTolerantProxy unless it's already
|
42
|
+
# fault tolerant
|
43
|
+
#
|
44
|
+
# @param storage [Storage::Interface] The storage to maybe wrap
|
45
|
+
# @return [Storage::Interface] The original storage or a {FaultTolerantProxy}
|
46
|
+
def self.wrap(storage, **options, &block)
|
47
|
+
return storage if storage.fault_tolerant?
|
48
|
+
|
49
|
+
new(storage, **options, &block)
|
50
|
+
end
|
51
|
+
|
52
|
+
# @!method lock(circuit, state)
|
53
|
+
# Lock is not called in normal operation, so it doesn't capture errors
|
54
|
+
#
|
55
|
+
# @see Interface#lock
|
56
|
+
# @param (see Interface#lock)
|
57
|
+
# @return (see Interface#lock)
|
58
|
+
#
|
59
|
+
# @!method unlock(circuit)
|
60
|
+
# Unlock is not called in normal operation, so it doesn't capture errors
|
61
|
+
#
|
62
|
+
# @see Interface#unlock
|
63
|
+
# @param (see Interface#unlock)
|
64
|
+
# @return (see Interface#unlock)
|
65
|
+
#
|
66
|
+
# @!method reset(circuit)
|
67
|
+
# Reset is not called in normal operation, so it doesn't capture errors
|
68
|
+
#
|
69
|
+
# @see Interface#reset
|
70
|
+
# @param (see Interface#reset)
|
71
|
+
# @return (see Interface#reset)
|
72
|
+
#
|
73
|
+
# @!method history(circuit)
|
74
|
+
# History is not called in normal operation, so it doesn't capture errors
|
75
|
+
#
|
76
|
+
# @see Interface#history
|
77
|
+
# @param (see Interface#history)
|
78
|
+
# @return (see Interface#history)
|
79
|
+
#
|
80
|
+
# @!method list
|
81
|
+
# List is not called in normal operation, so it doesn't capture errors
|
82
|
+
#
|
83
|
+
# @see Interface#list
|
84
|
+
# @param (see Interface#list)
|
85
|
+
# @return (see Interface#list)
|
86
|
+
def_delegators :@storage, :lock, :unlock, :reset, :history, :list
|
87
|
+
|
39
88
|
# Add a history entry safely
|
40
89
|
#
|
41
90
|
# @see Interface#entry
|
@@ -53,8 +102,8 @@ module Faulty
|
|
53
102
|
# @see Interface#open
|
54
103
|
# @param (see Interface#open)
|
55
104
|
# @return (see Interface#open)
|
56
|
-
def open(circuit)
|
57
|
-
@storage.open(circuit)
|
105
|
+
def open(circuit, opened_at)
|
106
|
+
@storage.open(circuit, opened_at)
|
58
107
|
rescue StandardError => e
|
59
108
|
options.notifier.notify(:storage_failure, circuit: circuit, action: :open, error: e)
|
60
109
|
false
|
@@ -65,8 +114,8 @@ module Faulty
|
|
65
114
|
# @see Interface#reopen
|
66
115
|
# @param (see Interface#reopen)
|
67
116
|
# @return (see Interface#reopen)
|
68
|
-
def reopen(circuit)
|
69
|
-
@storage.reopen(circuit)
|
117
|
+
def reopen(circuit, opened_at, previous_opened_at)
|
118
|
+
@storage.reopen(circuit, opened_at, previous_opened_at)
|
70
119
|
rescue StandardError => e
|
71
120
|
options.notifier.notify(:storage_failure, circuit: circuit, action: :reopen, error: e)
|
72
121
|
false
|
@@ -84,36 +133,6 @@ module Faulty
|
|
84
133
|
false
|
85
134
|
end
|
86
135
|
|
87
|
-
# Since lock is not called in normal operation, it does not capture
|
88
|
-
# errors
|
89
|
-
#
|
90
|
-
# @see Interface#lock
|
91
|
-
# @param (see Interface#lock)
|
92
|
-
# @return (see Interface#lock)
|
93
|
-
def lock(circuit, state)
|
94
|
-
@storage.lock(circuit, state)
|
95
|
-
end
|
96
|
-
|
97
|
-
# Since unlock is not called in normal operation, it does not capture
|
98
|
-
# errors
|
99
|
-
#
|
100
|
-
# @see Interface#unlock
|
101
|
-
# @param (see Interface#unlock)
|
102
|
-
# @return (see Interface#unlock)
|
103
|
-
def unlock(circuit)
|
104
|
-
@storage.unlock(circuit)
|
105
|
-
end
|
106
|
-
|
107
|
-
# Since reset is not called in normal operation, it does not capture
|
108
|
-
# errors
|
109
|
-
#
|
110
|
-
# @see Interface#reset
|
111
|
-
# @param (see Interface#reset)
|
112
|
-
# @return (see Interface#reset)
|
113
|
-
def reset(circuit)
|
114
|
-
@storage.reset(circuit)
|
115
|
-
end
|
116
|
-
|
117
136
|
# Safely get the status of a circuit
|
118
137
|
#
|
119
138
|
# If the backend is unavailable, this returns a stub status that
|
@@ -129,30 +148,6 @@ module Faulty
|
|
129
148
|
stub_status(circuit)
|
130
149
|
end
|
131
150
|
|
132
|
-
# Since history is not called in normal operation, it does not capture
|
133
|
-
# errors
|
134
|
-
#
|
135
|
-
# @see Interface#history
|
136
|
-
# @param (see Interface#history)
|
137
|
-
# @return (see Interface#history)
|
138
|
-
def history(circuit)
|
139
|
-
@storage.history(circuit)
|
140
|
-
end
|
141
|
-
|
142
|
-
# Safely get the list of circuit names
|
143
|
-
#
|
144
|
-
# If the backend is unavailable, this returns an empty array
|
145
|
-
#
|
146
|
-
# @see Interface#list
|
147
|
-
# @param (see Interface#list)
|
148
|
-
# @return (see Interface#list)
|
149
|
-
def list
|
150
|
-
@storage.list
|
151
|
-
rescue StandardError => e
|
152
|
-
options.notifier.notify(:storage_failure, action: :list, error: e)
|
153
|
-
[]
|
154
|
-
end
|
155
|
-
|
156
151
|
# This cache makes any storage fault tolerant, so this is always `true`
|
157
152
|
#
|
158
153
|
# @return [true]
|
@@ -1,11 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
class Faulty
|
4
4
|
module Storage
|
5
5
|
# The default in-memory storage for circuits
|
6
6
|
#
|
7
|
-
# This implementation is
|
8
|
-
#
|
7
|
+
# This implementation is thread-safe and circuit state is shared across
|
8
|
+
# threads. Since state is stored in-memory, this state is not shared across
|
9
|
+
# processes, or persisted across application restarts.
|
9
10
|
#
|
10
11
|
# Circuit state and runs are stored in memory. Although runs have a maximum
|
11
12
|
# size within a circuit, there is no limit on the number of circuits that
|
@@ -18,6 +19,9 @@ module Faulty
|
|
18
19
|
#
|
19
20
|
# This can be used as a reference implementation for storage backends that
|
20
21
|
# store a list of circuit run entries.
|
22
|
+
#
|
23
|
+
# @todo Add a more sophsticated implmentation that can limit the number of
|
24
|
+
# circuits stored.
|
21
25
|
class Memory
|
22
26
|
attr_reader :options
|
23
27
|
|
@@ -83,7 +87,7 @@ module Faulty
|
|
83
87
|
memory = fetch(circuit)
|
84
88
|
memory.runs.borrow do |runs|
|
85
89
|
runs.push([time, success])
|
86
|
-
runs.
|
90
|
+
runs.shift if runs.size > options.max_sample_size
|
87
91
|
end
|
88
92
|
memory.status(circuit.options)
|
89
93
|
end
|