Skip to content

Commit

Permalink
Merge 892c4af into d1024b7
Browse files Browse the repository at this point in the history
  • Loading branch information
zeisler committed Apr 4, 2016
2 parents d1024b7 + 892c4af commit 6e01424
Show file tree
Hide file tree
Showing 7 changed files with 114 additions and 34 deletions.
31 changes: 29 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Check out [stoplight-admin][] for controlling your stoplights.
- [Custom fallback](#custom-fallback)
- [Custom threshold](#custom-threshold)
- [Custom timeout](#custom-timeout)
- [Custom error handler](#custom-error-handler)
- [Rails](#rails)
- [Setup](#setup)
- [Data store](#data-store)
Expand Down Expand Up @@ -139,8 +140,8 @@ light.color
# => "green"
```

The following errors are always whitelisted: `NoMemoryError`, `ScriptError`,
`SecurityError`, `SignalException`, `SystemExit`, and `SystemStackError`.
The following errors are always whitelisted: `NoMemoryError`, `SignalException`,
`Interrupt`, and `SystemExit`.

Whitelisted errors take precedence over [blacklisted errors](#blacklisted-errors).

Expand Down Expand Up @@ -255,6 +256,32 @@ timeout to `Float::INFINITY`. To make automatic recovery instantaneous, set the
timeout to `0` seconds. Note that this is not recommended, as it effectively
replaces the red state with yellow.

### Custom error handler

Stoplight defaults to using the whitelisted_errors and the blacklisted_errors
to handle errors. If you have a custom strategy pass a `Proc` or a callable to
`with_error_handler`. When called it passes error, whitelist, and blacklist as
keyword args. To act as a whitelist re-raise the error, otherwise do nothing
to ask as a blacklist.

``` rb
light = Stoplight('example-custom-error-handler') { fail }
.with_error_handler -> (error:, whitelisted_errors:, blacklisted_errors:) do
if error !== RuntimeError
raise error
end
end
# => #<Stoplight::Light:...>
light.run
# RuntimeError:
light.run
# RuntimeError:
light.run
# RuntimeError:
light.color
# => "green"
```

### Rails

Stoplight was designed to wrap Rails actions with minimal effort. Here's an
Expand Down
20 changes: 12 additions & 8 deletions lib/stoplight/default.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,7 @@

module Stoplight
module Default
WHITELISTED_ERRORS = [
NoMemoryError,
ScriptError,
SecurityError,
SignalException,
SystemExit,
SystemStackError
].freeze
WHITELISTED_ERRORS = [].freeze

BLACKLISTED_ERRORS = [].freeze

Expand All @@ -32,5 +25,16 @@ module Default
THRESHOLD = 3

TIMEOUT = 60.0

# Taken from rspec-support
module AllExceptionsExceptOnesWeMustNotRescue
# These exceptions are dangerous to rescue as rescuing them
# would interfere with things we should not interfere with.
AVOID_RESCUING = [NoMemoryError, SignalException, Interrupt, SystemExit]

def self.===(exception)
AVOID_RESCUING.none? { |ar| ar === exception }
end
end
end
end
9 changes: 9 additions & 0 deletions lib/stoplight/light.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ class Light # rubocop:disable Style/Documentation
attr_reader :threshold
# @return [Float]
attr_reader :timeout
# @return [Proc]
attr_reader :error_handler

class << self
# @return [DataStore::Base]
Expand Down Expand Up @@ -111,5 +113,12 @@ def with_timeout(timeout)
@timeout = timeout
self
end

# @param error_handler [Proc]
# @return [self]
def with_error_handler(error_handler)
@error_handler = error_handler
self
end
end
end
11 changes: 8 additions & 3 deletions lib/stoplight/light/runnable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def run_code(on_success, on_failure)
failures = clear_failures
on_success.call(failures) if on_success
result
rescue *[StandardError].concat(blacklisted_errors) => error
rescue Default::AllExceptionsExceptOnesWeMustNotRescue => error
handle_error(error, on_failure)
end

Expand All @@ -62,8 +62,13 @@ def not_blacklisted_error?(error)
end

def handle_error(error, on_failure)
raise error if whitelisted_errors.any? { |klass| error.is_a?(klass) }
raise error if not_blacklisted_error?(error)
default_error_handler = -> (error:, **_) do
raise error if whitelisted_errors.any? { |klass| error.is_a?(klass) }
raise error if not_blacklisted_error?(error)
end
(error_handler || default_error_handler).call(error: error,
whitelisted_errors: whitelisted_errors,
blacklisted_errors: blacklisted_errors)
size = record_failure(error)
on_failure.call(size, error) if on_failure
raise error unless fallback
Expand Down
6 changes: 1 addition & 5 deletions spec/stoplight/default_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

it 'contains exception classes' do
Stoplight::Default::WHITELISTED_ERRORS.each do |whitelisted_error|
expect(whitelisted_error).to be < Exception
expect(whitelisted_error).to be < StandardError
end
end

Expand All @@ -28,10 +28,6 @@
expect(Stoplight::Default::BLACKLISTED_ERRORS).to be_an(Array)
end

it 'is empty' do
expect(Stoplight::Default::BLACKLISTED_ERRORS).to be_empty
end

it 'is frozen' do
expect(Stoplight::Default::BLACKLISTED_ERRORS).to be_frozen
end
Expand Down
63 changes: 47 additions & 16 deletions spec/stoplight/light/runnable_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -151,66 +151,75 @@ def random_string
end
end

context 'when the error is a blacklisted of Exception class' do
let(:error_class) { Class.new(Exception) }
let(:blacklisted_errors) { [error.class] }
context 'when the error is not blacklisted' do
let(:blacklisted_errors) { [RuntimeError] }

before { subject.with_blacklisted_errors(blacklisted_errors) }

it 'records the failure' do
it 'does not record the failure' do
expect(subject.data_store.get_failures(subject).size).to eql(0)
begin
subject.run
rescue error.class
nil
end
expect(subject.data_store.get_failures(subject).size).to eql(1)
expect(subject.data_store.get_failures(subject).size).to eql(0)
end
end

context 'when the error is not blacklisted' do
let(:blacklisted_errors) { [RuntimeError] }
context 'when the list of blacklisted errors is empty' do
let(:blacklisted_errors) { [] }

before { subject.with_blacklisted_errors(blacklisted_errors) }

it 'does not record the failure' do
it 'records the failure' do
expect(subject.data_store.get_failures(subject).size).to eql(0)
begin
subject.run
rescue error.class
nil
end
expect(subject.data_store.get_failures(subject).size).to eql(0)
expect(subject.data_store.get_failures(subject).size).to eql(1)
end
end

context 'when the list of blacklisted errors is empty' do
let(:blacklisted_errors) { [] }
context 'when the error is both whitelisted and blacklisted' do
let(:whitelisted_errors) { [error.class] }
let(:blacklisted_errors) { [error.class] }

before { subject.with_blacklisted_errors(blacklisted_errors) }
before do
subject
.with_whitelisted_errors(whitelisted_errors)
.with_blacklisted_errors(blacklisted_errors)
end

it 'records the failure' do
it 'does not record the failure' do
expect(subject.data_store.get_failures(subject).size).to eql(0)
begin
subject.run
rescue error.class
nil
end
expect(subject.data_store.get_failures(subject).size).to eql(1)
expect(subject.data_store.get_failures(subject).size).to eql(0)
end
end

context 'when the error is both whitelisted and blacklisted' do
context 'when using user provided error handler' do
let(:whitelisted_errors) { [error.class] }
let(:blacklisted_errors) { [error.class] }
let(:error_handler){double("error_handlers")}

before do
subject
.with_error_handler(error_handler)
.with_whitelisted_errors(whitelisted_errors)
.with_blacklisted_errors(blacklisted_errors)
end

it 'does not record the failure' do
it 'does record the failure' do
allow(error_handler).to receive(:call) do |error:, **args|
raise error
end
expect(subject.data_store.get_failures(subject).size).to eql(0)
begin
subject.run
Expand All @@ -219,6 +228,28 @@ def random_string
end
expect(subject.data_store.get_failures(subject).size).to eql(0)
end

it 'sends the correct arguments to custom error handler' do
expect(error_handler).to receive(:call).with(error: error,
blacklisted_errors: blacklisted_errors,
whitelisted_errors: whitelisted_errors)
begin
subject.run
rescue error.class
nil
end
end

it 'does not record the failure' do
allow(error_handler).to receive(:call).and_return(true)
expect(subject.data_store.get_failures(subject).size).to eql(0)
begin
subject.run
rescue error.class
nil
end
expect(subject.data_store.get_failures(subject).size).to eql(1)
end
end

context 'with a fallback' do
Expand Down
8 changes: 8 additions & 0 deletions spec/stoplight/light_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -206,4 +206,12 @@
expect(light.timeout).to eql(timeout)
end
end

describe '#with_error_handler' do
it 'sets the error handler' do
error_handler = -> () { }
light.with_error_handler(error_handler)
expect(light.error_handler).to eql(error_handler)
end
end
end

0 comments on commit 6e01424

Please sign in to comment.