faulty 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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