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/lib/faulty/cache/default.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
class Faulty
|
4
4
|
module Cache
|
5
5
|
# The default cache implementation
|
6
6
|
#
|
@@ -11,6 +11,8 @@ module Faulty
|
|
11
11
|
# - If ActiveSupport is available, it will use an `ActiveSupport::Cache::MemoryStore`
|
12
12
|
# - Otherwise it will use a {Faulty::Cache::Null}
|
13
13
|
class Default
|
14
|
+
extend Forwardable
|
15
|
+
|
14
16
|
def initialize
|
15
17
|
@cache = if defined?(::Rails)
|
16
18
|
Cache::Rails.new(::Rails.cache)
|
@@ -21,28 +23,15 @@ module Faulty
|
|
21
23
|
end
|
22
24
|
end
|
23
25
|
|
24
|
-
#
|
26
|
+
# @!method read(key)
|
27
|
+
# (see Faulty::Cache::Interface#read)
|
25
28
|
#
|
26
|
-
#
|
27
|
-
#
|
28
|
-
def read(key)
|
29
|
-
@cache.read(key)
|
30
|
-
end
|
31
|
-
|
32
|
-
# Write to the internal cache
|
29
|
+
# @!method write(key, value, expires_in: expires_in)
|
30
|
+
# (see Faulty::Cache::Interface#write)
|
33
31
|
#
|
34
|
-
#
|
35
|
-
#
|
36
|
-
|
37
|
-
@cache.write(key, value, expires_in: expires_in)
|
38
|
-
end
|
39
|
-
|
40
|
-
# This cache is fault tolerant if the internal one is
|
41
|
-
#
|
42
|
-
# @return [Boolean]
|
43
|
-
def fault_tolerant?
|
44
|
-
@cache.fault_tolerant?
|
45
|
-
end
|
32
|
+
# @!method fault_tolerant
|
33
|
+
# (see Faulty::Cache::Interface#fault_tolerant?)
|
34
|
+
def_delegators :@cache, :read, :write, :fault_tolerant?
|
46
35
|
end
|
47
36
|
end
|
48
37
|
end
|
@@ -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
|
# A default listener that logs Faulty events
|
6
6
|
class LogListener
|
@@ -72,11 +72,10 @@ module Faulty
|
|
72
72
|
end
|
73
73
|
|
74
74
|
def storage_failure(payload)
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
)
|
75
|
+
extra = {}
|
76
|
+
extra[:circuit] = payload[:circuit].name if payload.key?(:circuit)
|
77
|
+
extra[:error] = payload[:error].message
|
78
|
+
log(:error, 'Storage failure', payload[:action], extra)
|
80
79
|
end
|
81
80
|
|
82
81
|
def log(level, msg, action, extra = {})
|
@@ -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
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
class Faulty
|
4
4
|
# The status of a circuit
|
5
5
|
#
|
6
6
|
# Includes information like the state and locks. Also calculates
|
@@ -144,9 +144,10 @@ module Faulty
|
|
144
144
|
|
145
145
|
def finalize
|
146
146
|
raise ArgumentError, "state must be a symbol in #{self.class}::STATES" unless STATES.include?(state)
|
147
|
-
unless lock.nil? || LOCKS.include?(
|
147
|
+
unless lock.nil? || LOCKS.include?(lock)
|
148
148
|
raise ArgumentError, "lock must be a symbol in #{self.class}::LOCKS or nil"
|
149
149
|
end
|
150
|
+
raise ArgumentError, 'opened_at is required if state is open' if state == :open && opened_at.nil?
|
150
151
|
end
|
151
152
|
|
152
153
|
def required
|
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'
|