faulty 0.1.1 → 0.3.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/.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
@@ -1,14 +1,15 @@
|
|
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
|
11
|
-
# sent to the notifier.
|
11
|
+
# sent to the notifier. Reads errors will return `nil`, and writes will be
|
12
|
+
# a no-op.
|
12
13
|
class FaultTolerantProxy
|
13
14
|
attr_reader :options
|
14
15
|
|
@@ -36,6 +37,16 @@ module Faulty
|
|
36
37
|
@options = Options.new(options, &block)
|
37
38
|
end
|
38
39
|
|
40
|
+
# Wrap a cache in a FaultTolerantProxy unless it's already fault tolerant
|
41
|
+
#
|
42
|
+
# @param cache [Cache::Interface] The cache to maybe wrap
|
43
|
+
# @return [Cache::Interface] The original cache or a {FaultTolerantProxy}
|
44
|
+
def self.wrap(cache, **options, &block)
|
45
|
+
return cache if cache.fault_tolerant?
|
46
|
+
|
47
|
+
new(cache, **options, &block)
|
48
|
+
end
|
49
|
+
|
39
50
|
# Read from the cache safely
|
40
51
|
#
|
41
52
|
# If the backend raises a `StandardError`, this will return `nil`.
|
@@ -58,7 +69,7 @@ module Faulty
|
|
58
69
|
# @return [void]
|
59
70
|
def write(key, value, expires_in: nil)
|
60
71
|
@cache.write(key, value, expires_in: expires_in)
|
61
|
-
rescue StandardError
|
72
|
+
rescue StandardError => e
|
62
73
|
options.notifier.notify(:cache_failure, key: key, action: :write, error: e)
|
63
74
|
nil
|
64
75
|
end
|
data/lib/faulty/cache/mock.rb
CHANGED
data/lib/faulty/cache/null.rb
CHANGED
data/lib/faulty/cache/rails.rb
CHANGED
@@ -1,10 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
class Faulty
|
4
4
|
module Cache
|
5
5
|
# A wrapper for a Rails or ActiveSupport cache
|
6
6
|
#
|
7
7
|
class Rails
|
8
|
+
extend Forwardable
|
9
|
+
|
8
10
|
# @param cache The Rails cache to wrap
|
9
11
|
# @param fault_tolerant [Boolean] Whether the Rails cache is
|
10
12
|
# fault_tolerant. See {#fault_tolerant?} for more details
|
@@ -13,15 +15,12 @@ module Faulty
|
|
13
15
|
@fault_tolerant = fault_tolerant
|
14
16
|
end
|
15
17
|
|
16
|
-
#
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
def write(key, value, expires_in: nil)
|
23
|
-
@cache.write(key, value, expires_in: expires_in)
|
24
|
-
end
|
18
|
+
# @!method read(key)
|
19
|
+
# (see Faulty::Cache::Interface#read)
|
20
|
+
#
|
21
|
+
# @!method write(key, value, expires_in: expires_in)
|
22
|
+
# (see Faulty::Cache::Interface#write)
|
23
|
+
def_delegators :@cache, :read, :write
|
25
24
|
|
26
25
|
# Although ActiveSupport cache implementations are fault-tolerant,
|
27
26
|
# Rails.cache is not guranteed to be fault tolerant. For this reason,
|
data/lib/faulty/circuit.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
class Faulty
|
4
4
|
# Runs code protected by a circuit breaker
|
5
5
|
#
|
6
6
|
# https://www.martinfowler.com/bliki/CircuitBreaker.html
|
@@ -66,14 +66,19 @@ module Faulty
|
|
66
66
|
# @return [Error, Array<Error>] An array of errors that are considered circuit
|
67
67
|
# failures. Default `[StandardError]`.
|
68
68
|
# @!attribute [r] exclude
|
69
|
-
# @return [Error, Array<Error>] An array of errors that will be
|
70
|
-
#
|
69
|
+
# @return [Error, Array<Error>] An array of errors that will not be
|
70
|
+
# captured by Faulty. These errors will not be considered circuit
|
71
|
+
# failures. Default `[]`.
|
71
72
|
# @!attribute [r] cache
|
72
|
-
# @return [Cache::Interface] The cache backend. Default
|
73
|
+
# @return [Cache::Interface] The cache backend. Default
|
74
|
+
# `Cache::Null.new`. Unlike {Faulty#initialize}, this is not wrapped in
|
75
|
+
# {Cache::AutoWire} by default.
|
73
76
|
# @!attribute [r] notifier
|
74
77
|
# @return [Events::Notifier] A Faulty notifier. Default `Events::Notifier.new`
|
75
78
|
# @!attribute [r] storage
|
76
|
-
# @return [Storage::Interface] The storage backend. Default
|
79
|
+
# @return [Storage::Interface] The storage backend. Default
|
80
|
+
# `Storage::Memory.new`. Unlike {Faulty#initialize}, this is not wrapped
|
81
|
+
# in {Storage::AutoWire} by default.
|
77
82
|
Options = Struct.new(
|
78
83
|
:cache_expires_in,
|
79
84
|
:cache_refreshes_after,
|
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
|
@@ -59,8 +59,22 @@ module Faulty
|
|
59
59
|
# Raised if calling get or error on a result without checking it
|
60
60
|
class UncheckedResultError < FaultyError; end
|
61
61
|
|
62
|
+
# An error that wraps multiple other errors
|
63
|
+
class FaultyMultiError < FaultyError
|
64
|
+
def initialize(message, errors)
|
65
|
+
message = "#{message}: #{errors.map(&:message).join(', ')}"
|
66
|
+
super(message)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
62
70
|
# Raised if getting the wrong result type.
|
63
71
|
#
|
64
72
|
# For example, calling get on an error result will raise this
|
65
73
|
class WrongResultError < FaultyError; end
|
74
|
+
|
75
|
+
# Raised if a FallbackChain partially fails
|
76
|
+
class PartialFailureError < FaultyMultiError; end
|
77
|
+
|
78
|
+
# Raised if all FallbackChain backends fail
|
79
|
+
class AllFailedError < FaultyMultiError; end
|
66
80
|
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
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
class Faulty
|
4
4
|
module Events
|
5
5
|
# The default event dispatcher for Faulty
|
6
6
|
class Notifier
|
@@ -11,6 +11,9 @@ module Faulty
|
|
11
11
|
|
12
12
|
# Notify all listeners of an event
|
13
13
|
#
|
14
|
+
# If a listener raises an error while handling an event, that error will
|
15
|
+
# be captured and written to STDERR.
|
16
|
+
#
|
14
17
|
# @param event [Symbol] The event name
|
15
18
|
# @param payload [Hash] A hash of event payload data. The payload keys
|
16
19
|
# differ between events, but should be consistent across calls for a
|
@@ -18,7 +21,13 @@ module Faulty
|
|
18
21
|
def notify(event, payload)
|
19
22
|
raise ArgumentError, "Unknown event #{event}" unless EVENTS.include?(event)
|
20
23
|
|
21
|
-
@listeners.each
|
24
|
+
@listeners.each do |listener|
|
25
|
+
begin
|
26
|
+
listener.handle(event, payload)
|
27
|
+
rescue StandardError => e
|
28
|
+
warn "Faulty listener #{listener.class.name} crashed: #{e.message}"
|
29
|
+
end
|
30
|
+
end
|
22
31
|
end
|
23
32
|
end
|
24
33
|
end
|
data/lib/faulty/result.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
class Faulty
|
4
4
|
# An approximation of the `Result` type from some strongly-typed languages.
|
5
5
|
#
|
6
6
|
# F#: https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/results
|
@@ -53,7 +53,7 @@ module Faulty
|
|
53
53
|
#
|
54
54
|
# @param ok An ok value
|
55
55
|
# @param error [Error] An error instance
|
56
|
-
def initialize(ok: NOTHING, error: NOTHING)
|
56
|
+
def initialize(ok: NOTHING, error: NOTHING)
|
57
57
|
if ok.equal?(NOTHING) && error.equal?(NOTHING)
|
58
58
|
raise ArgumentError, 'Result must have an ok or error value'
|
59
59
|
end
|
data/lib/faulty/status.rb
CHANGED
data/lib/faulty/storage.rb
CHANGED
@@ -1,11 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
class Faulty
|
4
4
|
# The namespace for Faulty storage
|
5
5
|
module Storage
|
6
6
|
end
|
7
7
|
end
|
8
8
|
|
9
|
+
require 'faulty/storage/auto_wire'
|
10
|
+
require 'faulty/storage/circuit_proxy'
|
11
|
+
require 'faulty/storage/fallback_chain'
|
9
12
|
require 'faulty/storage/fault_tolerant_proxy'
|
10
13
|
require 'faulty/storage/memory'
|
11
14
|
require 'faulty/storage/redis'
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Faulty
|
4
|
+
module Storage
|
5
|
+
# Automatically configure a storage backend
|
6
|
+
#
|
7
|
+
# Used by {Faulty#initialize} to setup sensible storage defaults
|
8
|
+
class AutoWire
|
9
|
+
extend Forwardable
|
10
|
+
|
11
|
+
# Options for {AutoWire}
|
12
|
+
Options = Struct.new(
|
13
|
+
:notifier
|
14
|
+
) do
|
15
|
+
include ImmutableOptions
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def required
|
20
|
+
%i[notifier]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Wrap storage backends with sensible defaults
|
25
|
+
#
|
26
|
+
# If the cache is `nil`, create a new {Memory} storage.
|
27
|
+
#
|
28
|
+
# If a single storage backend is given and is fault tolerant, leave it
|
29
|
+
# unmodified.
|
30
|
+
#
|
31
|
+
# If a single storage backend is given and is not fault tolerant, wrap it
|
32
|
+
# in a {CircuitProxy} and a {FaultTolerantProxy}.
|
33
|
+
#
|
34
|
+
# If an array of storage backends is given, wrap each non-fault-tolerant
|
35
|
+
# entry in a {CircuitProxy} and create a {FallbackChain}. If none of the
|
36
|
+
# backends in the array are fault tolerant, also wrap the {FallbackChain}
|
37
|
+
# in a {FaultTolerantProxy}.
|
38
|
+
#
|
39
|
+
# @todo Consider using a {FallbackChain} for non-fault-tolerant storages
|
40
|
+
# by default. This would fallback to a {Memory} storage. It would
|
41
|
+
# require a more conservative implementation of {Memory} that could
|
42
|
+
# limit the number of circuits stored. For now, users need to manually
|
43
|
+
# configure fallbacks.
|
44
|
+
#
|
45
|
+
# @param storage [Interface, Array<Interface>] A storage backed or array
|
46
|
+
# of storage backends to setup.
|
47
|
+
# @param options [Hash] Attributes for {Options}
|
48
|
+
# @yield [Options] For setting options in a block
|
49
|
+
def initialize(storage, **options, &block)
|
50
|
+
@options = Options.new(options, &block)
|
51
|
+
@storage = if storage.nil?
|
52
|
+
Memory.new
|
53
|
+
elsif storage.is_a?(Array)
|
54
|
+
wrap_array(storage)
|
55
|
+
elsif !storage.fault_tolerant?
|
56
|
+
wrap_one(storage)
|
57
|
+
else
|
58
|
+
storage
|
59
|
+
end
|
60
|
+
|
61
|
+
freeze
|
62
|
+
end
|
63
|
+
|
64
|
+
# @!method entry(circuit, time, success)
|
65
|
+
# (see Faulty::Storage::Interface#entry)
|
66
|
+
#
|
67
|
+
# @!method open(circuit, opened_at)
|
68
|
+
# (see Faulty::Storage::Interface#open)
|
69
|
+
#
|
70
|
+
# @!method reopen(circuit, opened_at, previous_opened_at)
|
71
|
+
# (see Faulty::Storage::Interface#reopen)
|
72
|
+
#
|
73
|
+
# @!method close(circuit)
|
74
|
+
# (see Faulty::Storage::Interface#close)
|
75
|
+
#
|
76
|
+
# @!method lock(circuit, state)
|
77
|
+
# (see Faulty::Storage::Interface#lock)
|
78
|
+
#
|
79
|
+
# @!method unlock(circuit)
|
80
|
+
# (see Faulty::Storage::Interface#unlock)
|
81
|
+
#
|
82
|
+
# @!method reset(circuit)
|
83
|
+
# (see Faulty::Storage::Interface#reset)
|
84
|
+
#
|
85
|
+
# @!method status(circuit)
|
86
|
+
# (see Faulty::Storage::Interface#status)
|
87
|
+
#
|
88
|
+
# @!method history(circuit)
|
89
|
+
# (see Faulty::Storage::Interface#history)
|
90
|
+
#
|
91
|
+
# @!method list
|
92
|
+
# (see Faulty::Storage::Interface#list)
|
93
|
+
#
|
94
|
+
def_delegators :@storage,
|
95
|
+
:entry, :open, :reopen, :close, :lock,
|
96
|
+
:unlock, :reset, :status, :history, :list
|
97
|
+
|
98
|
+
def fault_tolerant?
|
99
|
+
true
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
# Wrap an array of storage backends in a fault-tolerant FallbackChain
|
105
|
+
#
|
106
|
+
# @return [Storage::Interface] A fault-tolerant fallback chain
|
107
|
+
def wrap_array(array)
|
108
|
+
FaultTolerantProxy.wrap(FallbackChain.new(
|
109
|
+
array.map { |s| s.fault_tolerant? ? s : CircuitProxy.new(s, notifier: @options.notifier) },
|
110
|
+
notifier: @options.notifier
|
111
|
+
), notifier: @options.notifier)
|
112
|
+
end
|
113
|
+
|
114
|
+
def wrap_one(storage)
|
115
|
+
FaultTolerantProxy.new(
|
116
|
+
CircuitProxy.new(storage, notifier: @options.notifier),
|
117
|
+
notifier: @options.notifier
|
118
|
+
)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|