faulty 0.2.0 → 0.6.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.
data/lib/faulty/status.rb CHANGED
@@ -144,9 +144,10 @@ class 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?(state)
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
@@ -6,6 +6,9 @@ class Faulty
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,107 @@
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
+ # Options for {AutoWire}
10
+ #
11
+ # @!attribute [r] circuit
12
+ # @return [Circuit] A circuit for {CircuitProxy} if one is created.
13
+ # When modifying this, be careful to use only a reliable circuit
14
+ # storage backend so that you don't introduce cascading failures.
15
+ # @!attribute [r] notifier
16
+ # @return [Events::Notifier] A Faulty notifier. If given, listeners are
17
+ # ignored.
18
+ Options = Struct.new(
19
+ :circuit,
20
+ :notifier
21
+ ) do
22
+ include ImmutableOptions
23
+
24
+ private
25
+
26
+ def required
27
+ %i[notifier]
28
+ end
29
+ end
30
+
31
+ class << self
32
+ # Wrap storage backends with sensible defaults
33
+ #
34
+ # If the cache is `nil`, create a new {Memory} storage.
35
+ #
36
+ # If a single storage backend is given and is fault tolerant, leave it
37
+ # unmodified.
38
+ #
39
+ # If a single storage backend is given and is not fault tolerant, wrap it
40
+ # in a {CircuitProxy} and a {FaultTolerantProxy}.
41
+ #
42
+ # If an array of storage backends is given, wrap each non-fault-tolerant
43
+ # entry in a {CircuitProxy} and create a {FallbackChain}. If none of the
44
+ # backends in the array are fault tolerant, also wrap the {FallbackChain}
45
+ # in a {FaultTolerantProxy}.
46
+ #
47
+ # @todo Consider using a {FallbackChain} for non-fault-tolerant storages
48
+ # by default. This would fallback to a {Memory} storage. It would
49
+ # require a more conservative implementation of {Memory} that could
50
+ # limit the number of circuits stored. For now, users need to manually
51
+ # configure fallbacks.
52
+ #
53
+ # @param storage [Interface, Array<Interface>] A storage backed or array
54
+ # of storage backends to setup.
55
+ # @param options [Hash] Attributes for {Options}
56
+ # @yield [Options] For setting options in a block
57
+ def wrap(storage, **options, &block)
58
+ options = Options.new(options, &block)
59
+ if storage.nil?
60
+ Memory.new
61
+ elsif storage.is_a?(Array)
62
+ wrap_array(storage, options)
63
+ elsif !storage.fault_tolerant?
64
+ wrap_one(storage, options)
65
+ else
66
+ storage
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ # Wrap an array of storage backends in a fault-tolerant FallbackChain
73
+ #
74
+ # @param [Array<Storage::Interface>] The array to wrap
75
+ # @param options [Options]
76
+ # @return [Storage::Interface] A fault-tolerant fallback chain
77
+ def wrap_array(array, options)
78
+ FaultTolerantProxy.wrap(FallbackChain.new(
79
+ array.map { |s| s.fault_tolerant? ? s : circuit_proxy(s, options) },
80
+ notifier: options.notifier
81
+ ), notifier: options.notifier)
82
+ end
83
+
84
+ # Wrap one storage backend in fault-tolerant backends
85
+ #
86
+ # @param [Storage::Interface] The storage to wrap
87
+ # @param options [Options]
88
+ # @return [Storage::Interface] A fault-tolerant storage backend
89
+ def wrap_one(storage, options)
90
+ FaultTolerantProxy.new(
91
+ circuit_proxy(storage, options),
92
+ notifier: options.notifier
93
+ )
94
+ end
95
+
96
+ # Wrap storage in a CircuitProxy
97
+ #
98
+ # @param [Storage::Interface] The storage to wrap
99
+ # @param options [Options]
100
+ # @return [CircuitProxy]
101
+ def circuit_proxy(storage, options)
102
+ CircuitProxy.new(storage, circuit: options.circuit, notifier: options.notifier)
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -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
@@ -10,6 +10,8 @@ class Faulty
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 @@ class 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
@@ -45,7 +94,7 @@ class Faulty
45
94
  @storage.entry(circuit, time, success)
46
95
  rescue StandardError => e
47
96
  options.notifier.notify(:storage_failure, circuit: circuit, action: :entry, error: e)
48
- stub_status(circuit)
97
+ []
49
98
  end
50
99
 
51
100
  # Safely mark a circuit as open
@@ -84,36 +133,6 @@ class 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 @@ class 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]