Class: Faulty::Circuit

Inherits:
Object
  • Object
show all
Defined in:
lib/faulty/circuit.rb

Overview

Runs code protected by a circuit breaker

https://www.martinfowler.com/bliki/CircuitBreaker.html

A circuit is intended to protect against repeated calls to a failing external dependency. For example, a vendor API may be failing continuously. In that case, we trip the circuit breaker and stop calling that API for a specified cool-down period.

Once the cool-down passes, we try the API again, and if it succeeds, we reset the circuit.

Why isn't there a timeout option?

Timeout is inherently unsafe, and should not be used blindly. See Why Ruby's timeout is Dangerous.

You should prefer a network timeout like open_timeout and read_timeout, or write your own code to periodically check how long it has been running. If you're sure you want ruby's generic Timeout, you can apply it yourself inside the circuit run block.

Defined Under Namespace

Classes: Options

Constant Summary collapse

CACHE_REFRESH_SUFFIX =
'.faulty_refresh'

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name, **options) {|Options| ... } ⇒ Circuit

Returns a new instance of Circuit.

Parameters:

  • name (String)

    The name of the circuit

  • options (Hash)

    Attributes for Options

Yields:

  • (Options)

    For setting options in a block

Raises:

  • (ArgumentError)


193
194
195
196
197
198
199
200
# File 'lib/faulty/circuit.rb', line 193

def initialize(name, **options, &block)
  raise ArgumentError, 'name must be a String' unless name.is_a?(String)

  @name = name
  @given_options = Options.new(options, &block)
  @pulled_options = nil
  @options_pushed = false
end

Instance Attribute Details

#cacheCache::Interface (readonly)

Cache::Null.new. Unlike Faulty#initialize, this is not wrapped in Faulty::Cache::AutoWire by default.

Returns:



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/faulty/circuit.rb', line 91

Options = Struct.new(
  :cache_expires_in,
  :cache_refreshes_after,
  :cache_refresh_jitter,
  :cool_down,
  :evaluation_window,
  :rate_threshold,
  :sample_threshold,
  :errors,
  :error_mapper,
  :error_module,
  :exclude,
  :cache,
  :notifier,
  :storage,
  :registry
) do
  include ImmutableOptions

  # Get the options stored in the storage backend
  #
  # @return [Hash] A hash of stored options
  def for_storage
    {
      cool_down: cool_down,
      evaluation_window: evaluation_window,
      rate_threshold: rate_threshold,
      sample_threshold: sample_threshold
    }
  end

  def defaults
    {
      cache_expires_in: 86_400,
      cache_refreshes_after: 900,
      cool_down: 300,
      errors: [StandardError],
      error_mapper: Faulty,
      exclude: [],
      evaluation_window: 60,
      rate_threshold: 0.5,
      sample_threshold: 3
    }
  end

  def required
    %i[
      cache
      cool_down
      errors
      error_mapper
      exclude
      evaluation_window
      rate_threshold
      sample_threshold
      notifier
      storage
    ]
  end

  def finalize
    self.cache ||= Cache::Default.new
    self.notifier ||= Events::Notifier.new
    self.storage ||= Storage::Memory.new
    self.errors = [errors] if errors && !errors.is_a?(Array)
    self.exclude = [exclude] if exclude && !exclude.is_a?(Array)

    unless cache_refreshes_after.nil?
      self.cache_refresh_jitter = 0.2 * cache_refreshes_after
    end

    deprecated_error_module
  end

  private

  def deprecated_error_module
    return unless error_module

    Deprecation.method(self.class, :error_module, note: 'See :error_mapper', sunset: '0.9.0')
    self.error_mapper = error_module
  end
end

#cache_expires_inInteger? (readonly)

Returns The number of seconds to keep cached results. A value of nil will keep the cache indefinitely. Default 86400.

Returns:

  • (Integer, nil)

    The number of seconds to keep cached results. A value of nil will keep the cache indefinitely. Default 86400.



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/faulty/circuit.rb', line 91

Options = Struct.new(
  :cache_expires_in,
  :cache_refreshes_after,
  :cache_refresh_jitter,
  :cool_down,
  :evaluation_window,
  :rate_threshold,
  :sample_threshold,
  :errors,
  :error_mapper,
  :error_module,
  :exclude,
  :cache,
  :notifier,
  :storage,
  :registry
) do
  include ImmutableOptions

  # Get the options stored in the storage backend
  #
  # @return [Hash] A hash of stored options
  def for_storage
    {
      cool_down: cool_down,
      evaluation_window: evaluation_window,
      rate_threshold: rate_threshold,
      sample_threshold: sample_threshold
    }
  end

  def defaults
    {
      cache_expires_in: 86_400,
      cache_refreshes_after: 900,
      cool_down: 300,
      errors: [StandardError],
      error_mapper: Faulty,
      exclude: [],
      evaluation_window: 60,
      rate_threshold: 0.5,
      sample_threshold: 3
    }
  end

  def required
    %i[
      cache
      cool_down
      errors
      error_mapper
      exclude
      evaluation_window
      rate_threshold
      sample_threshold
      notifier
      storage
    ]
  end

  def finalize
    self.cache ||= Cache::Default.new
    self.notifier ||= Events::Notifier.new
    self.storage ||= Storage::Memory.new
    self.errors = [errors] if errors && !errors.is_a?(Array)
    self.exclude = [exclude] if exclude && !exclude.is_a?(Array)

    unless cache_refreshes_after.nil?
      self.cache_refresh_jitter = 0.2 * cache_refreshes_after
    end

    deprecated_error_module
  end

  private

  def deprecated_error_module
    return unless error_module

    Deprecation.method(self.class, :error_module, note: 'See :error_mapper', sunset: '0.9.0')
    self.error_mapper = error_module
  end
end

#cache_refresh_jitterInteger (readonly)

Returns The maximum number of seconds to randomly add or subtract from cache_refreshes_after when determining whether to refresh the cache. A non-zero value helps reduce a "thundering herd" cache refresh in most scenarios. Set to 0 to disable jitter. Default 0.2 * cache_refreshes_after.

Returns:

  • (Integer)

    The maximum number of seconds to randomly add or subtract from cache_refreshes_after when determining whether to refresh the cache. A non-zero value helps reduce a "thundering herd" cache refresh in most scenarios. Set to 0 to disable jitter. Default 0.2 * cache_refreshes_after.



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/faulty/circuit.rb', line 91

Options = Struct.new(
  :cache_expires_in,
  :cache_refreshes_after,
  :cache_refresh_jitter,
  :cool_down,
  :evaluation_window,
  :rate_threshold,
  :sample_threshold,
  :errors,
  :error_mapper,
  :error_module,
  :exclude,
  :cache,
  :notifier,
  :storage,
  :registry
) do
  include ImmutableOptions

  # Get the options stored in the storage backend
  #
  # @return [Hash] A hash of stored options
  def for_storage
    {
      cool_down: cool_down,
      evaluation_window: evaluation_window,
      rate_threshold: rate_threshold,
      sample_threshold: sample_threshold
    }
  end

  def defaults
    {
      cache_expires_in: 86_400,
      cache_refreshes_after: 900,
      cool_down: 300,
      errors: [StandardError],
      error_mapper: Faulty,
      exclude: [],
      evaluation_window: 60,
      rate_threshold: 0.5,
      sample_threshold: 3
    }
  end

  def required
    %i[
      cache
      cool_down
      errors
      error_mapper
      exclude
      evaluation_window
      rate_threshold
      sample_threshold
      notifier
      storage
    ]
  end

  def finalize
    self.cache ||= Cache::Default.new
    self.notifier ||= Events::Notifier.new
    self.storage ||= Storage::Memory.new
    self.errors = [errors] if errors && !errors.is_a?(Array)
    self.exclude = [exclude] if exclude && !exclude.is_a?(Array)

    unless cache_refreshes_after.nil?
      self.cache_refresh_jitter = 0.2 * cache_refreshes_after
    end

    deprecated_error_module
  end

  private

  def deprecated_error_module
    return unless error_module

    Deprecation.method(self.class, :error_module, note: 'See :error_mapper', sunset: '0.9.0')
    self.error_mapper = error_module
  end
end

#cache_refreshes_afterInteger? (readonly)

Returns The number of seconds after which we attempt to refresh the cache even if it's not expired. If the circuit fails, we continue serving the value from cache until cache_expires_in. A value of nil disables cache refreshing. Default 900.

Returns:

  • (Integer, nil)

    The number of seconds after which we attempt to refresh the cache even if it's not expired. If the circuit fails, we continue serving the value from cache until cache_expires_in. A value of nil disables cache refreshing. Default 900.



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/faulty/circuit.rb', line 91

Options = Struct.new(
  :cache_expires_in,
  :cache_refreshes_after,
  :cache_refresh_jitter,
  :cool_down,
  :evaluation_window,
  :rate_threshold,
  :sample_threshold,
  :errors,
  :error_mapper,
  :error_module,
  :exclude,
  :cache,
  :notifier,
  :storage,
  :registry
) do
  include ImmutableOptions

  # Get the options stored in the storage backend
  #
  # @return [Hash] A hash of stored options
  def for_storage
    {
      cool_down: cool_down,
      evaluation_window: evaluation_window,
      rate_threshold: rate_threshold,
      sample_threshold: sample_threshold
    }
  end

  def defaults
    {
      cache_expires_in: 86_400,
      cache_refreshes_after: 900,
      cool_down: 300,
      errors: [StandardError],
      error_mapper: Faulty,
      exclude: [],
      evaluation_window: 60,
      rate_threshold: 0.5,
      sample_threshold: 3
    }
  end

  def required
    %i[
      cache
      cool_down
      errors
      error_mapper
      exclude
      evaluation_window
      rate_threshold
      sample_threshold
      notifier
      storage
    ]
  end

  def finalize
    self.cache ||= Cache::Default.new
    self.notifier ||= Events::Notifier.new
    self.storage ||= Storage::Memory.new
    self.errors = [errors] if errors && !errors.is_a?(Array)
    self.exclude = [exclude] if exclude && !exclude.is_a?(Array)

    unless cache_refreshes_after.nil?
      self.cache_refresh_jitter = 0.2 * cache_refreshes_after
    end

    deprecated_error_module
  end

  private

  def deprecated_error_module
    return unless error_module

    Deprecation.method(self.class, :error_module, note: 'See :error_mapper', sunset: '0.9.0')
    self.error_mapper = error_module
  end
end

#cool_downInteger (readonly)

Returns The number of seconds the circuit will stay open after it is tripped. Default 300.

Returns:

  • (Integer)

    The number of seconds the circuit will stay open after it is tripped. Default 300.



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/faulty/circuit.rb', line 91

Options = Struct.new(
  :cache_expires_in,
  :cache_refreshes_after,
  :cache_refresh_jitter,
  :cool_down,
  :evaluation_window,
  :rate_threshold,
  :sample_threshold,
  :errors,
  :error_mapper,
  :error_module,
  :exclude,
  :cache,
  :notifier,
  :storage,
  :registry
) do
  include ImmutableOptions

  # Get the options stored in the storage backend
  #
  # @return [Hash] A hash of stored options
  def for_storage
    {
      cool_down: cool_down,
      evaluation_window: evaluation_window,
      rate_threshold: rate_threshold,
      sample_threshold: sample_threshold
    }
  end

  def defaults
    {
      cache_expires_in: 86_400,
      cache_refreshes_after: 900,
      cool_down: 300,
      errors: [StandardError],
      error_mapper: Faulty,
      exclude: [],
      evaluation_window: 60,
      rate_threshold: 0.5,
      sample_threshold: 3
    }
  end

  def required
    %i[
      cache
      cool_down
      errors
      error_mapper
      exclude
      evaluation_window
      rate_threshold
      sample_threshold
      notifier
      storage
    ]
  end

  def finalize
    self.cache ||= Cache::Default.new
    self.notifier ||= Events::Notifier.new
    self.storage ||= Storage::Memory.new
    self.errors = [errors] if errors && !errors.is_a?(Array)
    self.exclude = [exclude] if exclude && !exclude.is_a?(Array)

    unless cache_refreshes_after.nil?
      self.cache_refresh_jitter = 0.2 * cache_refreshes_after
    end

    deprecated_error_module
  end

  private

  def deprecated_error_module
    return unless error_module

    Deprecation.method(self.class, :error_module, note: 'See :error_mapper', sunset: '0.9.0')
    self.error_mapper = error_module
  end
end

#error_mapperModule, #call (readonly)

Returns Used by patches to set the namespace module for the faulty errors that will be raised. Should be a module or a callable. If given a module, the circuit assumes the module has error classes in that module. If given an object that responds to #call (a proc or lambda), the return value of the callable will be used. The callable is called with (error_name, cause_error, circuit). Default Faulty.

Returns:

  • (Module, #call)

    Used by patches to set the namespace module for the faulty errors that will be raised. Should be a module or a callable. If given a module, the circuit assumes the module has error classes in that module. If given an object that responds to #call (a proc or lambda), the return value of the callable will be used. The callable is called with (error_name, cause_error, circuit). Default Faulty



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/faulty/circuit.rb', line 91

Options = Struct.new(
  :cache_expires_in,
  :cache_refreshes_after,
  :cache_refresh_jitter,
  :cool_down,
  :evaluation_window,
  :rate_threshold,
  :sample_threshold,
  :errors,
  :error_mapper,
  :error_module,
  :exclude,
  :cache,
  :notifier,
  :storage,
  :registry
) do
  include ImmutableOptions

  # Get the options stored in the storage backend
  #
  # @return [Hash] A hash of stored options
  def for_storage
    {
      cool_down: cool_down,
      evaluation_window: evaluation_window,
      rate_threshold: rate_threshold,
      sample_threshold: sample_threshold
    }
  end

  def defaults
    {
      cache_expires_in: 86_400,
      cache_refreshes_after: 900,
      cool_down: 300,
      errors: [StandardError],
      error_mapper: Faulty,
      exclude: [],
      evaluation_window: 60,
      rate_threshold: 0.5,
      sample_threshold: 3
    }
  end

  def required
    %i[
      cache
      cool_down
      errors
      error_mapper
      exclude
      evaluation_window
      rate_threshold
      sample_threshold
      notifier
      storage
    ]
  end

  def finalize
    self.cache ||= Cache::Default.new
    self.notifier ||= Events::Notifier.new
    self.storage ||= Storage::Memory.new
    self.errors = [errors] if errors && !errors.is_a?(Array)
    self.exclude = [exclude] if exclude && !exclude.is_a?(Array)

    unless cache_refreshes_after.nil?
      self.cache_refresh_jitter = 0.2 * cache_refreshes_after
    end

    deprecated_error_module
  end

  private

  def deprecated_error_module
    return unless error_module

    Deprecation.method(self.class, :error_module, note: 'See :error_mapper', sunset: '0.9.0')
    self.error_mapper = error_module
  end
end

#errorsError+ (readonly)

Returns An array of errors that are considered circuit failures. Default [StandardError].

Returns:

  • (Error, Array<Error>)

    An array of errors that are considered circuit failures. Default [StandardError].



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/faulty/circuit.rb', line 91

Options = Struct.new(
  :cache_expires_in,
  :cache_refreshes_after,
  :cache_refresh_jitter,
  :cool_down,
  :evaluation_window,
  :rate_threshold,
  :sample_threshold,
  :errors,
  :error_mapper,
  :error_module,
  :exclude,
  :cache,
  :notifier,
  :storage,
  :registry
) do
  include ImmutableOptions

  # Get the options stored in the storage backend
  #
  # @return [Hash] A hash of stored options
  def for_storage
    {
      cool_down: cool_down,
      evaluation_window: evaluation_window,
      rate_threshold: rate_threshold,
      sample_threshold: sample_threshold
    }
  end

  def defaults
    {
      cache_expires_in: 86_400,
      cache_refreshes_after: 900,
      cool_down: 300,
      errors: [StandardError],
      error_mapper: Faulty,
      exclude: [],
      evaluation_window: 60,
      rate_threshold: 0.5,
      sample_threshold: 3
    }
  end

  def required
    %i[
      cache
      cool_down
      errors
      error_mapper
      exclude
      evaluation_window
      rate_threshold
      sample_threshold
      notifier
      storage
    ]
  end

  def finalize
    self.cache ||= Cache::Default.new
    self.notifier ||= Events::Notifier.new
    self.storage ||= Storage::Memory.new
    self.errors = [errors] if errors && !errors.is_a?(Array)
    self.exclude = [exclude] if exclude && !exclude.is_a?(Array)

    unless cache_refreshes_after.nil?
      self.cache_refresh_jitter = 0.2 * cache_refreshes_after
    end

    deprecated_error_module
  end

  private

  def deprecated_error_module
    return unless error_module

    Deprecation.method(self.class, :error_module, note: 'See :error_mapper', sunset: '0.9.0')
    self.error_mapper = error_module
  end
end

#evaluation_windowInteger (readonly)

Returns The number of seconds of history that will be evaluated to determine the failure rate for a circuit. Default 60.

Returns:

  • (Integer)

    The number of seconds of history that will be evaluated to determine the failure rate for a circuit. Default 60.



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/faulty/circuit.rb', line 91

Options = Struct.new(
  :cache_expires_in,
  :cache_refreshes_after,
  :cache_refresh_jitter,
  :cool_down,
  :evaluation_window,
  :rate_threshold,
  :sample_threshold,
  :errors,
  :error_mapper,
  :error_module,
  :exclude,
  :cache,
  :notifier,
  :storage,
  :registry
) do
  include ImmutableOptions

  # Get the options stored in the storage backend
  #
  # @return [Hash] A hash of stored options
  def for_storage
    {
      cool_down: cool_down,
      evaluation_window: evaluation_window,
      rate_threshold: rate_threshold,
      sample_threshold: sample_threshold
    }
  end

  def defaults
    {
      cache_expires_in: 86_400,
      cache_refreshes_after: 900,
      cool_down: 300,
      errors: [StandardError],
      error_mapper: Faulty,
      exclude: [],
      evaluation_window: 60,
      rate_threshold: 0.5,
      sample_threshold: 3
    }
  end

  def required
    %i[
      cache
      cool_down
      errors
      error_mapper
      exclude
      evaluation_window
      rate_threshold
      sample_threshold
      notifier
      storage
    ]
  end

  def finalize
    self.cache ||= Cache::Default.new
    self.notifier ||= Events::Notifier.new
    self.storage ||= Storage::Memory.new
    self.errors = [errors] if errors && !errors.is_a?(Array)
    self.exclude = [exclude] if exclude && !exclude.is_a?(Array)

    unless cache_refreshes_after.nil?
      self.cache_refresh_jitter = 0.2 * cache_refreshes_after
    end

    deprecated_error_module
  end

  private

  def deprecated_error_module
    return unless error_module

    Deprecation.method(self.class, :error_module, note: 'See :error_mapper', sunset: '0.9.0')
    self.error_mapper = error_module
  end
end

#excludeError+ (readonly)

Returns An array of errors that will not be captured by Faulty. These errors will not be considered circuit failures. Default [].

Returns:

  • (Error, Array<Error>)

    An array of errors that will not be captured by Faulty. These errors will not be considered circuit failures. Default [].



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/faulty/circuit.rb', line 91

Options = Struct.new(
  :cache_expires_in,
  :cache_refreshes_after,
  :cache_refresh_jitter,
  :cool_down,
  :evaluation_window,
  :rate_threshold,
  :sample_threshold,
  :errors,
  :error_mapper,
  :error_module,
  :exclude,
  :cache,
  :notifier,
  :storage,
  :registry
) do
  include ImmutableOptions

  # Get the options stored in the storage backend
  #
  # @return [Hash] A hash of stored options
  def for_storage
    {
      cool_down: cool_down,
      evaluation_window: evaluation_window,
      rate_threshold: rate_threshold,
      sample_threshold: sample_threshold
    }
  end

  def defaults
    {
      cache_expires_in: 86_400,
      cache_refreshes_after: 900,
      cool_down: 300,
      errors: [StandardError],
      error_mapper: Faulty,
      exclude: [],
      evaluation_window: 60,
      rate_threshold: 0.5,
      sample_threshold: 3
    }
  end

  def required
    %i[
      cache
      cool_down
      errors
      error_mapper
      exclude
      evaluation_window
      rate_threshold
      sample_threshold
      notifier
      storage
    ]
  end

  def finalize
    self.cache ||= Cache::Default.new
    self.notifier ||= Events::Notifier.new
    self.storage ||= Storage::Memory.new
    self.errors = [errors] if errors && !errors.is_a?(Array)
    self.exclude = [exclude] if exclude && !exclude.is_a?(Array)

    unless cache_refreshes_after.nil?
      self.cache_refresh_jitter = 0.2 * cache_refreshes_after
    end

    deprecated_error_module
  end

  private

  def deprecated_error_module
    return unless error_module

    Deprecation.method(self.class, :error_module, note: 'See :error_mapper', sunset: '0.9.0')
    self.error_mapper = error_module
  end
end

#nameObject (readonly)

Returns the value of attribute name.



29
30
31
# File 'lib/faulty/circuit.rb', line 29

def name
  @name
end

#notifierEvents::Notifier (readonly)

Returns A Faulty notifier. Default Events::Notifier.new.

Returns:



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/faulty/circuit.rb', line 91

Options = Struct.new(
  :cache_expires_in,
  :cache_refreshes_after,
  :cache_refresh_jitter,
  :cool_down,
  :evaluation_window,
  :rate_threshold,
  :sample_threshold,
  :errors,
  :error_mapper,
  :error_module,
  :exclude,
  :cache,
  :notifier,
  :storage,
  :registry
) do
  include ImmutableOptions

  # Get the options stored in the storage backend
  #
  # @return [Hash] A hash of stored options
  def for_storage
    {
      cool_down: cool_down,
      evaluation_window: evaluation_window,
      rate_threshold: rate_threshold,
      sample_threshold: sample_threshold
    }
  end

  def defaults
    {
      cache_expires_in: 86_400,
      cache_refreshes_after: 900,
      cool_down: 300,
      errors: [StandardError],
      error_mapper: Faulty,
      exclude: [],
      evaluation_window: 60,
      rate_threshold: 0.5,
      sample_threshold: 3
    }
  end

  def required
    %i[
      cache
      cool_down
      errors
      error_mapper
      exclude
      evaluation_window
      rate_threshold
      sample_threshold
      notifier
      storage
    ]
  end

  def finalize
    self.cache ||= Cache::Default.new
    self.notifier ||= Events::Notifier.new
    self.storage ||= Storage::Memory.new
    self.errors = [errors] if errors && !errors.is_a?(Array)
    self.exclude = [exclude] if exclude && !exclude.is_a?(Array)

    unless cache_refreshes_after.nil?
      self.cache_refresh_jitter = 0.2 * cache_refreshes_after
    end

    deprecated_error_module
  end

  private

  def deprecated_error_module
    return unless error_module

    Deprecation.method(self.class, :error_module, note: 'See :error_mapper', sunset: '0.9.0')
    self.error_mapper = error_module
  end
end

#rate_thresholdFloat (readonly)

Returns The minimum failure rate required to trip the circuit. For example, 0.5 requires at least a 50% failure rate to trip. Default 0.5.

Returns:

  • (Float)

    The minimum failure rate required to trip the circuit. For example, 0.5 requires at least a 50% failure rate to trip. Default 0.5.



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/faulty/circuit.rb', line 91

Options = Struct.new(
  :cache_expires_in,
  :cache_refreshes_after,
  :cache_refresh_jitter,
  :cool_down,
  :evaluation_window,
  :rate_threshold,
  :sample_threshold,
  :errors,
  :error_mapper,
  :error_module,
  :exclude,
  :cache,
  :notifier,
  :storage,
  :registry
) do
  include ImmutableOptions

  # Get the options stored in the storage backend
  #
  # @return [Hash] A hash of stored options
  def for_storage
    {
      cool_down: cool_down,
      evaluation_window: evaluation_window,
      rate_threshold: rate_threshold,
      sample_threshold: sample_threshold
    }
  end

  def defaults
    {
      cache_expires_in: 86_400,
      cache_refreshes_after: 900,
      cool_down: 300,
      errors: [StandardError],
      error_mapper: Faulty,
      exclude: [],
      evaluation_window: 60,
      rate_threshold: 0.5,
      sample_threshold: 3
    }
  end

  def required
    %i[
      cache
      cool_down
      errors
      error_mapper
      exclude
      evaluation_window
      rate_threshold
      sample_threshold
      notifier
      storage
    ]
  end

  def finalize
    self.cache ||= Cache::Default.new
    self.notifier ||= Events::Notifier.new
    self.storage ||= Storage::Memory.new
    self.errors = [errors] if errors && !errors.is_a?(Array)
    self.exclude = [exclude] if exclude && !exclude.is_a?(Array)

    unless cache_refreshes_after.nil?
      self.cache_refresh_jitter = 0.2 * cache_refreshes_after
    end

    deprecated_error_module
  end

  private

  def deprecated_error_module
    return unless error_module

    Deprecation.method(self.class, :error_module, note: 'See :error_mapper', sunset: '0.9.0')
    self.error_mapper = error_module
  end
end

#registryCircuitRegistry (readonly)

memoization of circuits.

Returns:



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/faulty/circuit.rb', line 91

Options = Struct.new(
  :cache_expires_in,
  :cache_refreshes_after,
  :cache_refresh_jitter,
  :cool_down,
  :evaluation_window,
  :rate_threshold,
  :sample_threshold,
  :errors,
  :error_mapper,
  :error_module,
  :exclude,
  :cache,
  :notifier,
  :storage,
  :registry
) do
  include ImmutableOptions

  # Get the options stored in the storage backend
  #
  # @return [Hash] A hash of stored options
  def for_storage
    {
      cool_down: cool_down,
      evaluation_window: evaluation_window,
      rate_threshold: rate_threshold,
      sample_threshold: sample_threshold
    }
  end

  def defaults
    {
      cache_expires_in: 86_400,
      cache_refreshes_after: 900,
      cool_down: 300,
      errors: [StandardError],
      error_mapper: Faulty,
      exclude: [],
      evaluation_window: 60,
      rate_threshold: 0.5,
      sample_threshold: 3
    }
  end

  def required
    %i[
      cache
      cool_down
      errors
      error_mapper
      exclude
      evaluation_window
      rate_threshold
      sample_threshold
      notifier
      storage
    ]
  end

  def finalize
    self.cache ||= Cache::Default.new
    self.notifier ||= Events::Notifier.new
    self.storage ||= Storage::Memory.new
    self.errors = [errors] if errors && !errors.is_a?(Array)
    self.exclude = [exclude] if exclude && !exclude.is_a?(Array)

    unless cache_refreshes_after.nil?
      self.cache_refresh_jitter = 0.2 * cache_refreshes_after
    end

    deprecated_error_module
  end

  private

  def deprecated_error_module
    return unless error_module

    Deprecation.method(self.class, :error_module, note: 'See :error_mapper', sunset: '0.9.0')
    self.error_mapper = error_module
  end
end

#sample_thresholdInteger (readonly)

Returns The minimum number of runs required before a circuit can trip. A value of 1 means that the circuit will trip immediately when a failure occurs. Default 3.

Returns:

  • (Integer)

    The minimum number of runs required before a circuit can trip. A value of 1 means that the circuit will trip immediately when a failure occurs. Default 3.



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/faulty/circuit.rb', line 91

Options = Struct.new(
  :cache_expires_in,
  :cache_refreshes_after,
  :cache_refresh_jitter,
  :cool_down,
  :evaluation_window,
  :rate_threshold,
  :sample_threshold,
  :errors,
  :error_mapper,
  :error_module,
  :exclude,
  :cache,
  :notifier,
  :storage,
  :registry
) do
  include ImmutableOptions

  # Get the options stored in the storage backend
  #
  # @return [Hash] A hash of stored options
  def for_storage
    {
      cool_down: cool_down,
      evaluation_window: evaluation_window,
      rate_threshold: rate_threshold,
      sample_threshold: sample_threshold
    }
  end

  def defaults
    {
      cache_expires_in: 86_400,
      cache_refreshes_after: 900,
      cool_down: 300,
      errors: [StandardError],
      error_mapper: Faulty,
      exclude: [],
      evaluation_window: 60,
      rate_threshold: 0.5,
      sample_threshold: 3
    }
  end

  def required
    %i[
      cache
      cool_down
      errors
      error_mapper
      exclude
      evaluation_window
      rate_threshold
      sample_threshold
      notifier
      storage
    ]
  end

  def finalize
    self.cache ||= Cache::Default.new
    self.notifier ||= Events::Notifier.new
    self.storage ||= Storage::Memory.new
    self.errors = [errors] if errors && !errors.is_a?(Array)
    self.exclude = [exclude] if exclude && !exclude.is_a?(Array)

    unless cache_refreshes_after.nil?
      self.cache_refresh_jitter = 0.2 * cache_refreshes_after
    end

    deprecated_error_module
  end

  private

  def deprecated_error_module
    return unless error_module

    Deprecation.method(self.class, :error_module, note: 'See :error_mapper', sunset: '0.9.0')
    self.error_mapper = error_module
  end
end

Instance Method Details

#historyArray<Array>

Get the history of runs of this circuit

The history is an array of tuples where the first value is the run time, and the second value is a boolean which is true if the run was successful.

Returns:

  • (Array<Array>)

    ] An array of tuples of [run_time, is_success]



382
383
384
# File 'lib/faulty/circuit.rb', line 382

def history
  storage.history(self)
end

#inspectString

Returns Text representation of the circuit.

Returns:

  • (String)

    Text representation of the circuit



176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/faulty/circuit.rb', line 176

def inspect
  interested_opts = %i[
    cache_expires_in
    cache_refreshes_after
    cache_refresh_jitter
    cool_down evaluation_window
    rate_threshold
    sample_threshold
    errors exclude
  ]
  options_text = options.each_pair.map { |k, v| "#{k}: #{v}" if interested_opts.include?(k) }.compact.join(', ')
  %(#<#{self.class.name} name: #{name}, state: #{status.state}, options: { #{options_text} }>)
end

#lock_closed!self

Force the circuit to stay closed until unlocked

Returns:

  • (self)


339
340
341
342
# File 'lib/faulty/circuit.rb', line 339

def lock_closed!
  storage.lock(self, :closed)
  self
end

#lock_open!self

Force the circuit to stay open until unlocked

Returns:

  • (self)


331
332
333
334
# File 'lib/faulty/circuit.rb', line 331

def lock_open!
  storage.lock(self, :open)
  self
end

#optionsOptions

Get the options for this circuit

If this circuit has been run, these will the options exactly as given to new. However, if this circuit has not yet been run, these options will be supplemented by the last-known options from the circuit storage.

Once a circuit is run, the given options are pushed to circuit storage to be persisted.

This is to allow circuit objects to behave as expected in contexts where the exact options for a circuit are not known such as an admin dashboard or in a debug console.

Note that this distinction isn't usually important unless using distributed circuit storage like the Redis storage backend.

Examples:

Faulty.circuit('api', cool_down: 5).run { api.users }
# This status will be calculated using the cool_down of 5 because
# the circuit was already run
Faulty.circuit('api').status
# This status will be calculated using the cool_down in circuit storage
# if it is available instead of using the default value.
Faulty.circuit('api').status
# For typical usage, this behaves as expected, but note that it's
# possible to run into some unexpected behavior when creating circuits
# in unusual ways.

# For example, this status will be calculated using the cool_down in
# circuit storage if it is available despite the given value of 5.
Faulty.circuit('api', cool_down: 5).status
Faulty.circuit('api').run { api.users }
# However now, after the circuit is run, status will be calculated
# using the given cool_down of 5 and the value of 5 will be pushed
# permanently to circuit storage
Faulty.circuit('api').status

Returns:

  • (Options)

    The resolved options



244
245
246
247
248
249
250
# File 'lib/faulty/circuit.rb', line 244

def options
  return @given_options if @options_pushed
  return @pulled_options if @pulled_options

  stored = @given_options.storage.get_options(self)
  @pulled_options = stored ? @given_options.dup_with(stored) : @given_options
end

#reset!self

Reset this circuit to its initial state

This removes the current state, all history, and locks

Returns:

  • (self)


357
358
359
360
361
362
# File 'lib/faulty/circuit.rb', line 357

def reset!
  @options_pushed = false
  @pulled_options = nil
  storage.reset(self)
  self
end

#run(cache: nil) { ... } ⇒ Object

Run a block protected by this circuit

If the circuit is closed, the block will run. Any exceptions raised inside the block will be checked against the error and exclude options to determine whether that error should be captured. If the error is captured, this run will be recorded as a failure.

If the circuit exceeds the failure conditions, this circuit will be tripped and marked as open. Any future calls to run will not execute the block, but instead wait for the cool down period. Once the cool down period passes, the circuit transitions to half-open, and the block will be allowed to run.

If the circuit fails again while half-open, the circuit will be closed for a second cool down period. However, if the circuit completes successfully, the circuit will be closed and reset to its initial state.

When this is run, the given options are persisted to the storage backend.

Parameters:

  • cache (String, nil) (defaults to: nil)

    A cache key, or nil if caching is not desired

Yields:

  • The block to protect with this circuit

Returns:

  • The return value of the block

Raises:

  • If the block raises an error not in the error list, or if the error is excluded.

  • (OpenCircuitError)

    if the circuit is open

  • (CircuitTrippedError)

    if this run causes the circuit to trip. It's possible for concurrent runs to simultaneously trip the circuit if the storage engine is not concurrency-safe.

  • (CircuitFailureError)

    if this run fails, but doesn't cause the circuit to trip



316
317
318
319
320
321
322
323
324
325
326
# File 'lib/faulty/circuit.rb', line 316

def run(cache: nil, &block)
  push_options
  cached_value = cache_read(cache)
  # return cached unless cached.nil?
  return cached_value if !cached_value.nil? && !cache_should_refresh?(cache)

  current_status = status
  return run_skipped(cached_value) unless current_status.can_run?

  run_exec(current_status, cached_value, cache, &block)
end

#statusStatus

Get the current status of the circuit

This method is not safe for concurrent operations, so it's unsafe to check this method and make runtime decisions based on that. However, it's useful for getting a non-synchronized snapshot of a circuit.

Returns:



371
372
373
# File 'lib/faulty/circuit.rb', line 371

def status
  storage.status(self)
end

#try_run(**options) { ... } ⇒ Result<Object, Error>

Run the circuit as with #run, but return a Result

This is syntax sugar for running a circuit and rescuing an error

Examples:

result = Faulty.circuit(:api).try_run do
  api.get
end

response = if result.ok?
  result.get
else
  { error: result.error.message }
end
# The Result object has a fetch method that can return a default value
# if an error occurs
result = Faulty.circuit(:api).try_run do
  api.get
end.fetch({})

Parameters:

  • cache (String, nil)

    A cache key, or nil if caching is not desired

Yields:

  • The block to protect with this circuit

Returns:

  • (Result<Object, Error>)

    A result where the ok value is the return value of the block, or the error value is an error captured by the circuit.

Raises:

  • If the block raises an error not in the error list, or if the error is excluded.



281
282
283
284
285
# File 'lib/faulty/circuit.rb', line 281

def try_run(**options, &block)
  Result.new(ok: run(**options, &block))
rescue FaultyError => e
  Result.new(error: e)
end

#unlock!self

Remove any open or closed locks

Returns:

  • (self)


347
348
349
350
# File 'lib/faulty/circuit.rb', line 347

def unlock!
  storage.unlock(self)
  self
end