faulty 0.1.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 +7 -0
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/.rubocop.yml +85 -0
- data/.travis.yml +44 -0
- data/.yardopts +3 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +20 -0
- data/README.md +559 -0
- data/bin/check-version +10 -0
- data/bin/console +12 -0
- data/bin/rspec +29 -0
- data/bin/rubocop +29 -0
- data/bin/yard +29 -0
- data/bin/yardoc +29 -0
- data/bin/yri +29 -0
- data/faulty.gemspec +43 -0
- data/lib/faulty.rb +118 -0
- data/lib/faulty/cache.rb +13 -0
- data/lib/faulty/cache/default.rb +48 -0
- data/lib/faulty/cache/fault_tolerant_proxy.rb +74 -0
- data/lib/faulty/cache/interface.rb +44 -0
- data/lib/faulty/cache/mock.rb +39 -0
- data/lib/faulty/cache/null.rb +23 -0
- data/lib/faulty/cache/rails.rb +37 -0
- data/lib/faulty/circuit.rb +436 -0
- data/lib/faulty/error.rb +66 -0
- data/lib/faulty/events.rb +25 -0
- data/lib/faulty/events/callback_listener.rb +42 -0
- data/lib/faulty/events/listener_interface.rb +18 -0
- data/lib/faulty/events/log_listener.rb +88 -0
- data/lib/faulty/events/notifier.rb +25 -0
- data/lib/faulty/immutable_options.rb +40 -0
- data/lib/faulty/result.rb +150 -0
- data/lib/faulty/scope.rb +117 -0
- data/lib/faulty/status.rb +165 -0
- data/lib/faulty/storage.rb +11 -0
- data/lib/faulty/storage/fault_tolerant_proxy.rb +178 -0
- data/lib/faulty/storage/interface.rb +161 -0
- data/lib/faulty/storage/memory.rb +195 -0
- data/lib/faulty/storage/redis.rb +335 -0
- data/lib/faulty/version.rb +8 -0
- metadata +306 -0
data/lib/faulty/error.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Faulty
|
4
|
+
# The base error for all Faulty errors
|
5
|
+
class FaultyError < StandardError; end
|
6
|
+
|
7
|
+
# Raised if using the global Faulty object without initializing it
|
8
|
+
class UninitializedError < FaultyError
|
9
|
+
def initialize(message = nil)
|
10
|
+
message ||= 'Faulty is not initialized'
|
11
|
+
super(message)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# Raised if {Faulty.init} is called multiple times
|
16
|
+
class AlreadyInitializedError < FaultyError
|
17
|
+
def initialize(message = nil)
|
18
|
+
message ||= 'Faulty is already initialized'
|
19
|
+
super(message)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Raised if getting the default scope without initializing one
|
24
|
+
class MissingDefaultScopeError < FaultyError
|
25
|
+
def initialize(message = nil)
|
26
|
+
message ||= 'No default scope. Create one with init or get your scope with Faulty[:scope_name]'
|
27
|
+
super(message)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# The base error for all errors raised during circuit runs
|
32
|
+
#
|
33
|
+
class CircuitError < FaultyError
|
34
|
+
attr_reader :circuit
|
35
|
+
|
36
|
+
def initialize(message, circuit)
|
37
|
+
message ||= %(circuit error for "#{circuit.name}")
|
38
|
+
@circuit = circuit
|
39
|
+
|
40
|
+
super(message)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Raised when running a circuit that is already open
|
45
|
+
class OpenCircuitError < CircuitError; end
|
46
|
+
|
47
|
+
# Raised when an error occurred while running a circuit
|
48
|
+
#
|
49
|
+
# The `cause` will always be set and will be the internal error
|
50
|
+
#
|
51
|
+
# @see CircuitTrippedError For when the circuit is tripped
|
52
|
+
class CircuitFailureError < CircuitError; end
|
53
|
+
|
54
|
+
# Raised when an error occurred causing a circuit to close
|
55
|
+
#
|
56
|
+
# The `cause` will always be set and will be the internal error
|
57
|
+
class CircuitTrippedError < CircuitError; end
|
58
|
+
|
59
|
+
# Raised if calling get or error on a result without checking it
|
60
|
+
class UncheckedResultError < FaultyError; end
|
61
|
+
|
62
|
+
# Raised if getting the wrong result type.
|
63
|
+
#
|
64
|
+
# For example, calling get on an error result will raise this
|
65
|
+
class WrongResultError < FaultyError; end
|
66
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Faulty
|
4
|
+
# The namespace for Faulty events and event listeners
|
5
|
+
module Events
|
6
|
+
# All possible events that can be raised by Faulty
|
7
|
+
EVENTS = %i[
|
8
|
+
cache_failure
|
9
|
+
circuit_cache_hit
|
10
|
+
circuit_cache_miss
|
11
|
+
circuit_cache_write
|
12
|
+
circuit_closed
|
13
|
+
circuit_failure
|
14
|
+
circuit_opened
|
15
|
+
circuit_reopened
|
16
|
+
circuit_skipped
|
17
|
+
circuit_success
|
18
|
+
storage_failure
|
19
|
+
].freeze
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
require 'faulty/events/callback_listener'
|
24
|
+
require 'faulty/events/notifier'
|
25
|
+
require 'faulty/events/log_listener'
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Faulty
|
4
|
+
module Events
|
5
|
+
# A simple listener implementation that uses callback blocks as handlers
|
6
|
+
#
|
7
|
+
# Each event in {EVENTS} has a method on this class that can be used
|
8
|
+
# to register a callback for that event.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# listener = CallbackListener.new
|
12
|
+
# listener.circuit_opened do |payload|
|
13
|
+
# logger.error(
|
14
|
+
# "Circuit #{payload[:circuit].name} opened: #{payload[:error].message}"
|
15
|
+
# )
|
16
|
+
# end
|
17
|
+
class CallbackListener
|
18
|
+
def initialize
|
19
|
+
@handlers = {}
|
20
|
+
yield self if block_given?
|
21
|
+
end
|
22
|
+
|
23
|
+
# @param (see ListenerInterface#handle)
|
24
|
+
# @return [void]
|
25
|
+
def handle(event, payload)
|
26
|
+
return unless EVENTS.include?(event)
|
27
|
+
return unless @handlers.key?(event)
|
28
|
+
|
29
|
+
@handlers[event].each do |handler|
|
30
|
+
handler.call(payload)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
EVENTS.each do |event|
|
35
|
+
define_method(event) do |&block|
|
36
|
+
@handlers[event] ||= []
|
37
|
+
@handlers[event] << block
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Faulty
|
4
|
+
module Events
|
5
|
+
# The interface required to implement a event listener
|
6
|
+
#
|
7
|
+
# This is for documentation only and is not loaded
|
8
|
+
class ListenerInterface
|
9
|
+
# Handle an event raised by Faulty
|
10
|
+
#
|
11
|
+
# @param event [Symbol] The event name. Will be a member of {EVENTS}.
|
12
|
+
# @param payload [Hash] A hash with keys based on the event type
|
13
|
+
# @return [void]
|
14
|
+
def handle(event, payload)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Faulty
|
4
|
+
module Events
|
5
|
+
# A default listener that logs Faulty events
|
6
|
+
class LogListener
|
7
|
+
attr_reader :logger
|
8
|
+
|
9
|
+
# @param logger A logger similar to stdlib `Logger`. Uses the Rails logger
|
10
|
+
# by default if available, otherwise it creates a new `Logger` to
|
11
|
+
# stderr.
|
12
|
+
def initialize(logger = nil)
|
13
|
+
logger ||= defined?(Rails) ? Rails.logger : ::Logger.new($stderr)
|
14
|
+
@logger = logger
|
15
|
+
end
|
16
|
+
|
17
|
+
# (see ListenerInterface#handle)
|
18
|
+
def handle(event, payload)
|
19
|
+
return unless EVENTS.include?(event)
|
20
|
+
|
21
|
+
send(event, payload) if respond_to?(event, true)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def circuit_cache_hit(payload)
|
27
|
+
log(:debug, 'Circuit cache hit', payload[:circuit].name, key: payload[:key])
|
28
|
+
end
|
29
|
+
|
30
|
+
def circuit_cache_miss(payload)
|
31
|
+
log(:debug, 'Circuit cache miss', payload[:circuit].name, key: payload[:key])
|
32
|
+
end
|
33
|
+
|
34
|
+
def circuit_cache_write(payload)
|
35
|
+
log(:debug, 'Circuit cache write', payload[:circuit].name, key: payload[:key])
|
36
|
+
end
|
37
|
+
|
38
|
+
def circuit_success(payload)
|
39
|
+
log(:debug, 'Circuit succeeded', payload[:circuit].name, state: payload[:status].state)
|
40
|
+
end
|
41
|
+
|
42
|
+
def circuit_failure(payload)
|
43
|
+
log(
|
44
|
+
:error, 'Circuit failed', payload[:circuit].name,
|
45
|
+
state: payload[:status].state,
|
46
|
+
error: payload[:error].message
|
47
|
+
)
|
48
|
+
end
|
49
|
+
|
50
|
+
def circuit_skipped(payload)
|
51
|
+
log(:warn, 'Circuit skipped', payload[:circuit].name)
|
52
|
+
end
|
53
|
+
|
54
|
+
def circuit_opened(payload)
|
55
|
+
log(:error, 'Circuit opened', payload[:circuit].name, error: payload[:error].message)
|
56
|
+
end
|
57
|
+
|
58
|
+
def circuit_reopened(payload)
|
59
|
+
log(:error, 'Circuit reopened', payload[:circuit].name, error: payload[:error].message)
|
60
|
+
end
|
61
|
+
|
62
|
+
def circuit_closed(payload)
|
63
|
+
log(:info, 'Circuit closed', payload[:circuit].name)
|
64
|
+
end
|
65
|
+
|
66
|
+
def cache_failure(payload)
|
67
|
+
log(
|
68
|
+
:error, 'Cache failure', payload[:action],
|
69
|
+
key: payload[:key],
|
70
|
+
error: payload[:error].message
|
71
|
+
)
|
72
|
+
end
|
73
|
+
|
74
|
+
def storage_failure(payload)
|
75
|
+
log(
|
76
|
+
:error, 'Storage failure', payload[:action],
|
77
|
+
circuit: payload[:circuit]&.name,
|
78
|
+
error: payload[:error].message
|
79
|
+
)
|
80
|
+
end
|
81
|
+
|
82
|
+
def log(level, msg, action, extra = {})
|
83
|
+
extra_str = extra.map { |k, v| "#{k}=#{v}" }.join(' ')
|
84
|
+
logger.public_send(level, "#{msg}: #{action} #{extra_str}")
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Faulty
|
4
|
+
module Events
|
5
|
+
# The default event dispatcher for Faulty
|
6
|
+
class Notifier
|
7
|
+
# @param listeners [Array<ListenerInterface>] An array of event listeners
|
8
|
+
def initialize(listeners = [])
|
9
|
+
@listeners = listeners.freeze
|
10
|
+
end
|
11
|
+
|
12
|
+
# Notify all listeners of an event
|
13
|
+
#
|
14
|
+
# @param event [Symbol] The event name
|
15
|
+
# @param payload [Hash] A hash of event payload data. The payload keys
|
16
|
+
# differ between events, but should be consistent across calls for a
|
17
|
+
# single event
|
18
|
+
def notify(event, payload)
|
19
|
+
raise ArgumentError, "Unknown event #{event}" unless EVENTS.include?(event)
|
20
|
+
|
21
|
+
@listeners.each { |l| l.handle(event, payload) }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Faulty
|
4
|
+
# A struct that cannot be modified after initialization
|
5
|
+
module ImmutableOptions
|
6
|
+
# @param hash [Hash] A hash of attributes to initialize with
|
7
|
+
# @yield [self] Yields itself to the block to set options before freezing
|
8
|
+
def initialize(hash)
|
9
|
+
defaults.merge(hash).each { |key, value| self[key] = value }
|
10
|
+
yield self if block_given?
|
11
|
+
finalize
|
12
|
+
required.each do |key|
|
13
|
+
raise ArgumentError, "Missing required attribute #{key}" if self[key].nil?
|
14
|
+
end
|
15
|
+
freeze
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
# A hash of default values to set before yielding to the block
|
21
|
+
#
|
22
|
+
# @return [Hash<Symbol, Object>]
|
23
|
+
def defaults
|
24
|
+
{}
|
25
|
+
end
|
26
|
+
|
27
|
+
# An array of required attributes
|
28
|
+
#
|
29
|
+
# @return [Array<Symbol>]
|
30
|
+
def required
|
31
|
+
[]
|
32
|
+
end
|
33
|
+
|
34
|
+
# Runs before freezing to finalize attribute initialization
|
35
|
+
#
|
36
|
+
# @return [void]
|
37
|
+
def finalize
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Faulty
|
4
|
+
# An approximation of the `Result` type from some strongly-typed languages.
|
5
|
+
#
|
6
|
+
# F#: https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/results
|
7
|
+
#
|
8
|
+
# Rust: https://doc.rust-lang.org/std/result/enum.Result.html
|
9
|
+
#
|
10
|
+
# Since we can't enforce the type at compile-time, we use runtime errors to
|
11
|
+
# check the result for consistency as early as possible. This means we
|
12
|
+
# enforce runtime checks of the result type. This approach does not eliminate
|
13
|
+
# issues, but it does help remind the user to check the result in most cases.
|
14
|
+
#
|
15
|
+
# This is returned from {Circuit#try_run} to allow error handling without
|
16
|
+
# needing to rescue from errors.
|
17
|
+
#
|
18
|
+
# @example
|
19
|
+
# result = Result.new(ok: 'foo')
|
20
|
+
#
|
21
|
+
# # Check the result before calling get
|
22
|
+
# if result.ok?
|
23
|
+
# puts result.get
|
24
|
+
# else
|
25
|
+
# puts result.error.message
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# @example
|
29
|
+
# result = Result.new(error: StandardError.new)
|
30
|
+
# puts result.or_default('fallback') # prints "fallback"
|
31
|
+
#
|
32
|
+
# @example
|
33
|
+
# result = Result.new(ok: 'foo')
|
34
|
+
# result.get # raises UncheckedResultError
|
35
|
+
#
|
36
|
+
# @example
|
37
|
+
# result = Result.new(ok: 'foo')
|
38
|
+
# if result.ok?
|
39
|
+
# result.error.message # raises WrongResultError
|
40
|
+
# end
|
41
|
+
class Result
|
42
|
+
# The constant used to designate that a value is empty
|
43
|
+
#
|
44
|
+
# This is needed to differentiate between an ok `nil` value and
|
45
|
+
# an empty value.
|
46
|
+
#
|
47
|
+
# @private
|
48
|
+
NOTHING = {}.freeze
|
49
|
+
|
50
|
+
# Create a new `Result` with either an ok or error value
|
51
|
+
#
|
52
|
+
# Exactly one parameter must be given, and not both.
|
53
|
+
#
|
54
|
+
# @param ok An ok value
|
55
|
+
# @param error [Error] An error instance
|
56
|
+
def initialize(ok: NOTHING, error: NOTHING) # rubocop:disable Naming/MethodParameterName
|
57
|
+
if ok.equal?(NOTHING) && error.equal?(NOTHING)
|
58
|
+
raise ArgumentError, 'Result must have an ok or error value'
|
59
|
+
end
|
60
|
+
if !ok.equal?(NOTHING) && !error.equal?(NOTHING)
|
61
|
+
raise ArgumentError, 'Result must not have both an ok and error value'
|
62
|
+
end
|
63
|
+
|
64
|
+
@ok = ok
|
65
|
+
@error = error
|
66
|
+
@checked = false
|
67
|
+
end
|
68
|
+
|
69
|
+
# Check if the value is an ok value
|
70
|
+
#
|
71
|
+
# @return [Boolean] True if this result is ok
|
72
|
+
def ok?
|
73
|
+
@checked = true
|
74
|
+
ok_unchecked?
|
75
|
+
end
|
76
|
+
|
77
|
+
# Check if the value is an error value
|
78
|
+
#
|
79
|
+
# @return [Boolean] True if this result is an error
|
80
|
+
def error?
|
81
|
+
!ok?
|
82
|
+
end
|
83
|
+
|
84
|
+
# Get the ok value
|
85
|
+
#
|
86
|
+
# @raise UncheckedResultError if this result was not checked using {#ok?} or {#error?}
|
87
|
+
# @raise WrongResultError if this result is an error
|
88
|
+
# @return The ok value
|
89
|
+
def get
|
90
|
+
validate_checked!('get')
|
91
|
+
unsafe_get
|
92
|
+
end
|
93
|
+
|
94
|
+
# Get the ok value without checking whether it's safe to do so
|
95
|
+
#
|
96
|
+
# @raise WrongResultError if this result is an error
|
97
|
+
# @return The ok value
|
98
|
+
def unsafe_get
|
99
|
+
raise WrongResultError, 'Tried to get value for error result' unless ok_unchecked?
|
100
|
+
|
101
|
+
@ok
|
102
|
+
end
|
103
|
+
|
104
|
+
# Get the error value
|
105
|
+
#
|
106
|
+
# @raise UncheckedResultError if this result was not checked using {#ok?} or {#error?}
|
107
|
+
# @raise WrongResultError if this result is ok
|
108
|
+
def error
|
109
|
+
validate_checked!('error')
|
110
|
+
unsafe_error
|
111
|
+
end
|
112
|
+
|
113
|
+
# Get the error value without checking whether it's safe to do so
|
114
|
+
#
|
115
|
+
# @raise WrongResultError if this result is ok
|
116
|
+
# @return [Error] The error
|
117
|
+
def unsafe_error
|
118
|
+
raise WrongResultError, 'Tried to get error for ok result' if ok_unchecked?
|
119
|
+
|
120
|
+
@error
|
121
|
+
end
|
122
|
+
|
123
|
+
# Get the ok value if this result is ok, otherwise return a default
|
124
|
+
#
|
125
|
+
# @param default The default value. Ignored if a block is given
|
126
|
+
# @yield A block returning the default value
|
127
|
+
# @return The ok value or the default if this result is an error
|
128
|
+
def or_default(default = nil)
|
129
|
+
if ok_unchecked?
|
130
|
+
@ok
|
131
|
+
elsif block_given?
|
132
|
+
yield @error
|
133
|
+
else
|
134
|
+
default
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
private
|
139
|
+
|
140
|
+
def ok_unchecked?
|
141
|
+
!@ok.equal?(NOTHING)
|
142
|
+
end
|
143
|
+
|
144
|
+
def validate_checked!(method)
|
145
|
+
unless @checked
|
146
|
+
raise UncheckedResultError, "Result: Called #{method} without checking ok? or error?"
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|