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/bin/check-version CHANGED
@@ -3,8 +3,12 @@
3
3
  set -e
4
4
 
5
5
  tag="$(git describe --abbrev=0 2>/dev/null || echo)"
6
+ echo "Tag: ${tag}"
6
7
  tag="${tag#v}"
8
+ echo "Git Version: ${tag}"
7
9
  [ "$tag" = '' ] && exit 0
10
+ gem_version="$(ruby -r ./lib/faulty/version -e "puts Faulty.version" | tail -n1)"
11
+ echo "Gem Version: ${gem_version}"
8
12
 
9
- tag_gt_version=$(ruby -r ./lib/faulty/version -e "puts Faulty.version >= Gem::Version.new('${tag}')")
13
+ tag_gt_version="$(ruby -r ./lib/faulty/version -e "puts Faulty.version >= Gem::Version.new('${tag}')" | tail -n1)"
10
14
  test "$tag_gt_version" = true
data/faulty.gemspec CHANGED
@@ -26,8 +26,7 @@ Gem::Specification.new do |spec|
26
26
  # Only essential development tools and dependencies go here.
27
27
  # Other non-essential development dependencies go in the Gemfile.
28
28
  spec.add_development_dependency 'connection_pool', '~> 2.0'
29
- spec.add_development_dependency 'honeybadger', '>= 2.0'
30
- spec.add_development_dependency 'redis', '~> 3.0'
29
+ spec.add_development_dependency 'redis', '>= 3.0'
31
30
  spec.add_development_dependency 'rspec', '~> 3.8'
32
31
  # 0.81 is the last rubocop version with Ruby 2.3 support
33
32
  spec.add_development_dependency 'rubocop', '0.81.0'
data/lib/faulty.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'securerandom'
4
+ require 'forwardable'
4
5
  require 'concurrent-ruby'
5
6
 
6
7
  require 'faulty/immutable_options'
@@ -8,6 +9,7 @@ require 'faulty/cache'
8
9
  require 'faulty/circuit'
9
10
  require 'faulty/error'
10
11
  require 'faulty/events'
12
+ require 'faulty/patch'
11
13
  require 'faulty/result'
12
14
  require 'faulty/status'
13
15
  require 'faulty/storage'
@@ -65,7 +67,7 @@ class Faulty
65
67
  def [](name)
66
68
  raise UninitializedError unless @instances
67
69
 
68
- @instances[name]
70
+ @instances[name.to_s]
69
71
  end
70
72
 
71
73
  # Register an instance to the global Faulty state
@@ -74,13 +76,22 @@ class Faulty
74
76
  # return value if you need to know whether the instance already existed.
75
77
  #
76
78
  # @param name [Symbol] The name of the instance to register
77
- # @param instance [Faulty] The instance to register
79
+ # @param instance [Faulty] The instance to register. If nil, a new instance
80
+ # will be created from the given options or block.
81
+ # @param config [Hash] Attributes for {Faulty::Options}
82
+ # @yield [Faulty::Options] For setting options in a block
78
83
  # @return [Faulty, nil] The previously-registered instance of that name if
79
84
  # it already existed, otherwise nil.
80
- def register(name, instance)
85
+ def register(name, instance = nil, **config, &block)
81
86
  raise UninitializedError unless @instances
82
87
 
83
- @instances.put_if_absent(name, instance)
88
+ if instance
89
+ raise ArgumentError, 'Do not give config options if an instance is given' if !config.empty? || block
90
+ else
91
+ instance = new(**config, &block)
92
+ end
93
+
94
+ @instances.put_if_absent(name.to_s, instance)
84
95
  end
85
96
 
86
97
  # Get the options for the default instance
@@ -124,20 +135,28 @@ class Faulty
124
135
  # Options for {Faulty}
125
136
  #
126
137
  # @!attribute [r] cache
138
+ # @see Cache::AutoWire
127
139
  # @return [Cache::Interface] A cache backend if you want
128
140
  # to use Faulty's cache support. Automatically wrapped in a
129
- # {Cache::FaultTolerantProxy}. Default `Cache::Default.new`.
141
+ # {Cache::AutoWire}. Default `Cache::AutoWire.new`.
142
+ # @!attribute [r] circuit_defaults
143
+ # @see Circuit::Options
144
+ # @return [Hash] A hash of default options to be used when creating
145
+ # new circuits. See {Circuit::Options} for a full list.
130
146
  # @!attribute [r] storage
131
- # @return [Storage::Interface] The storage backend.
132
- # Automatically wrapped in a {Storage::FaultTolerantProxy}.
133
- # Default `Storage::Memory.new`.
147
+ # @see Storage::AutoWire
148
+ # @return [Storage::Interface, Array<Storage::Interface>] The storage
149
+ # backend. Automatically wrapped in a {Storage::AutoWire}, so this can also
150
+ # be given an array of prioritized backends. Default `Storage::AutoWire.new`.
134
151
  # @!attribute [r] listeners
152
+ # @see Events::ListenerInterface
135
153
  # @return [Array] listeners Faulty event listeners
136
154
  # @!attribute [r] notifier
137
155
  # @return [Events::Notifier] A Faulty notifier. If given, listeners are
138
156
  # ignored.
139
157
  Options = Struct.new(
140
158
  :cache,
159
+ :circuit_defaults,
141
160
  :storage,
142
161
  :listeners,
143
162
  :notifier
@@ -148,24 +167,17 @@ class Faulty
148
167
 
149
168
  def finalize
150
169
  self.notifier ||= Events::Notifier.new(listeners || [])
151
-
152
- self.storage ||= Storage::Memory.new
153
- unless storage.fault_tolerant?
154
- self.storage = Storage::FaultTolerantProxy.new(storage, notifier: notifier)
155
- end
156
-
157
- self.cache ||= Cache::Default.new
158
- unless cache.fault_tolerant?
159
- self.cache = Cache::FaultTolerantProxy.new(cache, notifier: notifier)
160
- end
170
+ self.storage = Storage::AutoWire.wrap(storage, notifier: notifier)
171
+ self.cache = Cache::AutoWire.wrap(cache, notifier: notifier)
161
172
  end
162
173
 
163
174
  def required
164
- %i[cache storage notifier]
175
+ %i[cache circuit_defaults storage notifier]
165
176
  end
166
177
 
167
178
  def defaults
168
179
  {
180
+ circuit_defaults: {},
169
181
  listeners: [Events::LogListener.new]
170
182
  }
171
183
  end
@@ -204,8 +216,8 @@ class Faulty
204
216
  # @return [Circuit] The new circuit or the existing circuit if it already exists
205
217
  def circuit(name, **options, &block)
206
218
  name = name.to_s
207
- options = options.merge(circuit_options)
208
219
  @circuits.compute_if_absent(name) do
220
+ options = circuit_options.merge(options)
209
221
  Circuit.new(name, **options, &block)
210
222
  end
211
223
  end
@@ -223,8 +235,8 @@ class Faulty
223
235
  #
224
236
  # @return [Hash] The circuit options
225
237
  def circuit_options
226
- options = @options.to_h
227
- options.delete(:listeners)
228
- options
238
+ @options.to_h
239
+ .select { |k, _v| %i[cache storage notifier].include?(k) }
240
+ .merge(options.circuit_defaults)
229
241
  end
230
242
  end
data/lib/faulty/cache.rb CHANGED
@@ -6,7 +6,9 @@ class Faulty
6
6
  end
7
7
  end
8
8
 
9
+ require 'faulty/cache/auto_wire'
9
10
  require 'faulty/cache/default'
11
+ require 'faulty/cache/circuit_proxy'
10
12
  require 'faulty/cache/fault_tolerant_proxy'
11
13
  require 'faulty/cache/mock'
12
14
  require 'faulty/cache/null'
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Faulty
4
+ module Cache
5
+ # Automatically configure a cache backend
6
+ #
7
+ # Used by {Faulty#initialize} to setup sensible cache 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 a cache backend with sensible defaults
33
+ #
34
+ # If the cache is `nil`, create a new {Default}.
35
+ #
36
+ # If the backend is not fault tolerant, wrap it in {CircuitProxy} and
37
+ # {FaultTolerantProxy}.
38
+ #
39
+ # @param cache [Interface] A cache backend
40
+ # @param options [Hash] Attributes for {Options}
41
+ # @yield [Options] For setting options in a block
42
+ def wrap(cache, **options, &block)
43
+ options = Options.new(options, &block)
44
+ if cache.nil?
45
+ Cache::Default.new
46
+ elsif cache.fault_tolerant?
47
+ cache
48
+ else
49
+ Cache::FaultTolerantProxy.new(
50
+ Cache::CircuitProxy.new(cache, circuit: options.circuit, notifier: options.notifier),
51
+ notifier: options.notifier
52
+ )
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Faulty
4
+ module Cache
5
+ # A circuit wrapper for cache backends
6
+ #
7
+ # This class uses an internal {Circuit} to prevent the cache 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 circuit storage
19
+ # backend so that you don't introduce cascading failures.
20
+ # @!attribute [r] notifier
21
+ # @return [Events::Notifier] A Faulty notifier to use for failure
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 cache [Cache::Interface] The cache backend to wrap
43
+ # @param options [Hash] Attributes for {Options}
44
+ # @yield [Options] For setting options in a block
45
+ def initialize(cache, **options, &block)
46
+ @cache = cache
47
+ @options = Options.new(options, &block)
48
+ end
49
+
50
+ %i[read write].each do |method|
51
+ define_method(method) do |*args|
52
+ options.circuit.run { @cache.public_send(method, *args) }
53
+ end
54
+ end
55
+
56
+ def fault_tolerant?
57
+ @cache.fault_tolerant?
58
+ end
59
+ end
60
+ end
61
+ end
@@ -11,6 +11,8 @@ class 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 @@ class Faulty
21
23
  end
22
24
  end
23
25
 
24
- # Read from the internal cache by key
26
+ # @!method read(key)
27
+ # (see Faulty::Cache::Interface#read)
25
28
  #
26
- # @param (see Cache::Interface#read)
27
- # @return (see Cache::Interface#read)
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
- # @param (see Cache::Interface#read)
35
- # @return (see Cache::Interface#read)
36
- def write(key, value, expires_in: nil)
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
@@ -8,7 +8,8 @@ class Faulty
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 @@ class 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 @@ class 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
@@ -5,6 +5,8 @@ class Faulty
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 @@ class Faulty
13
15
  @fault_tolerant = fault_tolerant
14
16
  end
15
17
 
16
- # (see Interface#read)
17
- def read(key)
18
- @cache.read(key)
19
- end
20
-
21
- # (see Interface#read)
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,
@@ -50,6 +50,9 @@ class Faulty
50
50
  # @!attribute [r] cool_down
51
51
  # @return [Integer] The number of seconds the circuit will
52
52
  # stay open after it is tripped. Default 300.
53
+ # @!attribute [r] error_module
54
+ # @return [Module] Used by patches to set the namespace module for
55
+ # the faulty errors that will be raised. Default `Faulty`
53
56
  # @!attribute [r] evaluation_window
54
57
  # @return [Integer] The number of seconds of history that
55
58
  # will be evaluated to determine the failure rate for a circuit.
@@ -66,14 +69,19 @@ class Faulty
66
69
  # @return [Error, Array<Error>] An array of errors that are considered circuit
67
70
  # failures. Default `[StandardError]`.
68
71
  # @!attribute [r] exclude
69
- # @return [Error, Array<Error>] An array of errors that will be captured and
70
- # considered circuit failures. Default `[]`.
72
+ # @return [Error, Array<Error>] An array of errors that will not be
73
+ # captured by Faulty. These errors will not be considered circuit
74
+ # failures. Default `[]`.
71
75
  # @!attribute [r] cache
72
- # @return [Cache::Interface] The cache backend. Default `Cache::Null.new`
76
+ # @return [Cache::Interface] The cache backend. Default
77
+ # `Cache::Null.new`. Unlike {Faulty#initialize}, this is not wrapped in
78
+ # {Cache::AutoWire} by default.
73
79
  # @!attribute [r] notifier
74
80
  # @return [Events::Notifier] A Faulty notifier. Default `Events::Notifier.new`
75
81
  # @!attribute [r] storage
76
- # @return [Storage::Interface] The storage backend. Default `Storage::Memory.new`
82
+ # @return [Storage::Interface] The storage backend. Default
83
+ # `Storage::Memory.new`. Unlike {Faulty#initialize}, this is not wrapped
84
+ # in {Storage::AutoWire} by default.
77
85
  Options = Struct.new(
78
86
  :cache_expires_in,
79
87
  :cache_refreshes_after,
@@ -83,6 +91,7 @@ class Faulty
83
91
  :rate_threshold,
84
92
  :sample_threshold,
85
93
  :errors,
94
+ :error_module,
86
95
  :exclude,
87
96
  :cache,
88
97
  :notifier,
@@ -98,6 +107,7 @@ class Faulty
98
107
  cache_refreshes_after: 900,
99
108
  cool_down: 300,
100
109
  errors: [StandardError],
110
+ error_module: Faulty,
101
111
  exclude: [],
102
112
  evaluation_window: 60,
103
113
  rate_threshold: 0.5,
@@ -110,6 +120,7 @@ class Faulty
110
120
  cache
111
121
  cool_down
112
122
  errors
123
+ error_module
113
124
  exclude
114
125
  evaluation_window
115
126
  rate_threshold
@@ -208,9 +219,11 @@ class Faulty
208
219
  cached_value = cache_read(cache)
209
220
  # return cached unless cached.nil?
210
221
  return cached_value if !cached_value.nil? && !cache_should_refresh?(cache)
211
- return run_skipped(cached_value) unless status.can_run?
212
222
 
213
- run_exec(cached_value, cache, &block)
223
+ current_status = status
224
+ return run_skipped(cached_value) unless current_status.can_run?
225
+
226
+ run_exec(current_status, cached_value, cache, &block)
214
227
  end
215
228
 
216
229
  # Force the circuit to stay open until unlocked
@@ -277,7 +290,7 @@ class Faulty
277
290
  # @return The result from cache if available
278
291
  def run_skipped(cached_value)
279
292
  skipped!
280
- raise OpenCircuitError.new(nil, self) if cached_value.nil?
293
+ raise options.error_module::OpenCircuitError.new(nil, self) if cached_value.nil?
281
294
 
282
295
  cached_value
283
296
  end
@@ -287,26 +300,27 @@ class Faulty
287
300
  # @param cached_value The cached value if one is available
288
301
  # @param cache_key [String, nil] The cache key if one is given
289
302
  # @return The run result
290
- def run_exec(cached_value, cache_key)
303
+ def run_exec(status, cached_value, cache_key)
291
304
  result = yield
292
- success!
305
+ success!(status)
293
306
  cache_write(cache_key, result)
294
307
  result
295
308
  rescue *options.errors => e
296
309
  raise if options.exclude.any? { |ex| e.is_a?(ex) }
297
310
 
298
311
  if cached_value.nil?
299
- raise CircuitTrippedError.new(nil, self) if failure!(e)
312
+ raise options.error_module::CircuitTrippedError.new(nil, self) if failure!(status, e)
300
313
 
301
- raise CircuitFailureError.new(nil, self)
314
+ raise options.error_module::CircuitFailureError.new(nil, self)
302
315
  else
303
316
  cached_value
304
317
  end
305
318
  end
306
319
 
307
320
  # @return [Boolean] True if the circuit transitioned to closed
308
- def success!
309
- status = storage.entry(self, Faulty.current_time, true)
321
+ def success!(status)
322
+ entries = storage.entry(self, Faulty.current_time, true)
323
+ status = Status.from_entries(entries, **status.to_h)
310
324
  closed = false
311
325
  closed = close! if should_close?(status)
312
326
 
@@ -315,8 +329,9 @@ class Faulty
315
329
  end
316
330
 
317
331
  # @return [Boolean] True if the circuit transitioned to open
318
- def failure!(error)
319
- status = storage.entry(self, Faulty.current_time, false)
332
+ def failure!(status, error)
333
+ entries = storage.entry(self, Faulty.current_time, false)
334
+ status = Status.from_entries(entries, **status.to_h)
320
335
  options.notifier.notify(:circuit_failure, circuit: self, status: status, error: error)
321
336
 
322
337
  opened = if status.half_open?