Skip to content

Commit

Permalink
Merge pull request #2 from undees/trip-or-ignore-specific-exceptions
Browse files Browse the repository at this point in the history
Allow callers to trip on or ignore specific exceptions
  • Loading branch information
djspinmonkey committed Mar 1, 2016
2 parents b0808c4 + 4209a9e commit d81baef
Show file tree
Hide file tree
Showing 3 changed files with 98 additions and 4 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,16 @@ proc = ->(*args) do
end

breaker = CircuitBreakage::Breaker.new(proc)

# These options are required.
breaker.failure_threshold = 3 # only 3 failures before tripping circuit
breaker.duration = 10 # 10 seconds before retry
breaker.timeout = 0.5 # 500 milliseconds allowed before auto-fail

# These options are, uh, optional.
breaker.only_trip_on = [ExpensiveFailureException]
breaker.never_trip_on = [CheapUnimportantFailureException]

begin
breaker.call(*some_args) # args are passed through to the proc
rescue CircuitBreakage::CircuitOpen
Expand Down
14 changes: 10 additions & 4 deletions lib/circuit_breakage/breaker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,21 @@ class CircuitTimeout < RuntimeError; end
class Breaker
attr_accessor :failure_count, :last_failed, :state, :block
attr_accessor :failure_threshold, :duration, :timeout, :last_exception
attr_accessor :only_trip_on, :never_trip_on

DEFAULT_FAILURE_THRESHOLD = 5 # Number of failures required to trip circuit
DEFAULT_DURATION = 300 # Number of seconds the circuit stays tripped
DEFAULT_TIMEOUT = 10 # Number of seconds before the call times out
DEFAULT_FAILURE_THRESHOLD = 5 # Number of failures required to trip circuit
DEFAULT_DURATION = 300 # Number of seconds the circuit stays tripped
DEFAULT_TIMEOUT = 10 # Number of seconds before the call times out
DEFAULT_ONLY_TRIP_ON = [Exception] # Exceptions that trigger the breaker
DEFAULT_NEVER_TRIP_ON = [] # Exceptions that won't trigger the breaker

def initialize(block=nil)
self.block = block
self.failure_threshold = DEFAULT_FAILURE_THRESHOLD
self.duration = DEFAULT_DURATION
self.timeout = DEFAULT_TIMEOUT
self.only_trip_on = DEFAULT_ONLY_TRIP_ON
self.never_trip_on = DEFAULT_NEVER_TRIP_ON
self.failure_count ||= 0
self.last_failed ||= 0
self.state ||= 'closed'
Expand Down Expand Up @@ -55,7 +60,8 @@ def do_call(*args, &block_arg)
handle_success

return ret_value
rescue Exception => e
rescue *self.only_trip_on => e
raise if never_trip_on.any? { |t| e.instance_of?(t) }
handle_failure(e)
end

Expand Down
82 changes: 82 additions & 0 deletions spec/breaker_spec.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
module CircuitBreakage
class VerySpecificException < StandardError
end

describe Breaker do
let(:breaker) { Breaker.new(block) }
let(:block) { ->(x) { return x } }
Expand Down Expand Up @@ -78,6 +81,85 @@ module CircuitBreakage
expect { breaker.call(arg) }.to raise_exception(CircuitBreakage::CircuitTimeout)
end
end

context 'with specific exceptions defined' do
before do
breaker.only_trip_on = [VerySpecificException]
end

context 'and the call fails with one of the specific exceptions' do
let(:block) { ->(_) { raise VerySpecificException } }

it { is_expected.to change { breaker.failure_count }.by(1) }
it { is_expected.to change { breaker.last_failed } }
it { is_expected.to change { breaker.last_exception }.from(nil) }

it 'raises the exception that caused the failure' do
expect { breaker.call(arg) }.to raise_exception(VerySpecificException)
end
end

context 'and the call fails with a different exception (including timeouts)' do
let(:block) { ->(_) { raise Timeout::Error } }

it { is_expected.not_to change { breaker.failure_count } }
it { is_expected.not_to change { breaker.last_failed } }
it { is_expected.not_to change { breaker.last_exception } }

it 'raises the exception that caused the failure' do
expect { breaker.call(arg) }.to raise_exception(Timeout::Error)
end
end
end

context 'with specific exceptions excluded' do
before do
breaker.never_trip_on = [VerySpecificException]
end

context 'and the call fails with one of the specific exceptions' do
let(:block) { ->(_) { raise VerySpecificException } }

it { is_expected.not_to change { breaker.failure_count } }
it { is_expected.not_to change { breaker.last_failed } }
it { is_expected.not_to change { breaker.last_exception } }

it 'raises the exception that caused the failure' do
expect { breaker.call(arg) }.to raise_exception(VerySpecificException)
end
end

context 'and the call fails with a different exception' do
let(:block) { ->(_) { raise RuntimeError } }

it { is_expected.to change { breaker.failure_count }.by(1) }
it { is_expected.to change { breaker.last_failed } }
it { is_expected.to change { breaker.last_exception }.from(nil) }

it 'raises the exception that caused the failure' do
expect { breaker.call(arg) }.to raise_exception(RuntimeError)
end
end
end

context 'with overlapping whitelisted and blacklisted exceptions' do
before do
breaker.only_trip_on = [StandardError]
breaker.never_trip_on = [VerySpecificException] # inherits from StandardError
end

context 'and the call fails with an overlapped exception' do
let(:block) { ->(_) { raise VerySpecificException } }

it { is_expected.not_to change { breaker.failure_count } }
it { is_expected.not_to change { breaker.last_failed } }
it { is_expected.not_to change { breaker.last_exception } }

it 'raises the exception that caused the failure' do
expect { breaker.call(arg) }.to raise_exception(VerySpecificException)
end
end
end
end

context 'when the circuit is open' do
Expand Down

0 comments on commit d81baef

Please sign in to comment.