1
0
Fork 0
mirror of https://github.com/mperham/sidekiq.git synced 2022-11-09 13:52:34 -05:00

Merge pull request #325 from subelsky/integrated_exception_handling

extract error handling middleware into processor-level code
This commit is contained in:
Mike Perham 2012-08-02 20:12:59 -07:00
commit e1ee37427b
6 changed files with 176 additions and 65 deletions

View file

@ -0,0 +1,33 @@
require 'sidekiq/util'
module Sidekiq
module ExceptionHandler
extend self
extend Util
def handle(ex,msg)
logger.warn ex
logger.warn ex.backtrace.join("\n")
send_to_airbrake(msg, ex) if defined?(::Airbrake)
send_to_exceptional(msg, ex) if defined?(::Exceptional)
send_to_exception_notifier(msg, ex) if defined?(::ExceptionNotifier)
end
private
def send_to_airbrake(msg, ex)
::Airbrake.notify(ex, :parameters => msg)
end
def send_to_exceptional(msg, ex)
if ::Exceptional::Config.should_send_to_api?
::Exceptional.context(msg)
::Exceptional::Remote.error(::Exceptional::ExceptionData.new(ex))
end
end
def send_to_exception_notifier(msg, ex)
::ExceptionNotifier::Notifier.background_exception_notification(ex, :data => { :message => msg })
end
end
end

View file

@ -1,38 +0,0 @@
require 'sidekiq/util'
module Sidekiq
module Middleware
module Server
class ExceptionHandler
include Util
def call(*args)
yield
rescue => ex
logger.warn ex
logger.warn ex.backtrace.join("\n")
send_to_airbrake(args[1], ex) if defined?(::Airbrake)
send_to_exceptional(args[1], ex) if defined?(::Exceptional)
send_to_exception_notifier(args[1], ex) if defined?(::ExceptionNotifier)
raise
end
private
def send_to_airbrake(msg, ex)
::Airbrake.notify(ex, :parameters => msg)
end
def send_to_exceptional(msg, ex)
if ::Exceptional::Config.should_send_to_api?
::Exceptional.context(msg)
::Exceptional::Remote.error(::Exceptional::ExceptionData.new(ex))
end
end
def send_to_exception_notifier(msg, ex)
::ExceptionNotifier::Notifier.background_exception_notification(ex, :data => { :message => msg })
end
end
end
end
end

View file

@ -2,10 +2,10 @@ require 'celluloid'
require 'sidekiq/util' require 'sidekiq/util'
require 'sidekiq/middleware/server/active_record' require 'sidekiq/middleware/server/active_record'
require 'sidekiq/middleware/server/exception_handler'
require 'sidekiq/middleware/server/retry_jobs' require 'sidekiq/middleware/server/retry_jobs'
require 'sidekiq/middleware/server/logging' require 'sidekiq/middleware/server/logging'
require 'sidekiq/middleware/server/timeout' require 'sidekiq/middleware/server/timeout'
require 'sidekiq/exception_handler'
module Sidekiq module Sidekiq
## ##
@ -17,10 +17,10 @@ module Sidekiq
include Celluloid include Celluloid
exclusive :process exclusive :process
attr_writer :exception_handler
def self.default_middleware def self.default_middleware
Middleware::Chain.new do |m| Middleware::Chain.new do |m|
m.add Middleware::Server::ExceptionHandler
m.add Middleware::Server::Logging m.add Middleware::Server::Logging
m.add Middleware::Server::RetryJobs m.add Middleware::Server::RetryJobs
m.add Middleware::Server::ActiveRecord m.add Middleware::Server::ActiveRecord
@ -43,6 +43,9 @@ module Sidekiq
end end
end end
@boss.processor_done!(current_actor) @boss.processor_done!(current_actor)
rescue StandardError => ex
exception_handler.handle(ex,msg)
raise
end end
# See http://github.com/tarcieri/celluloid/issues/22 # See http://github.com/tarcieri/celluloid/issues/22
@ -93,5 +96,9 @@ module Sidekiq
def hostname def hostname
@h ||= `hostname`.strip @h ||= `hostname`.strip
end end
def exception_handler
@exception_handler ||= Sidekiq::ExceptionHandler
end
end end
end end

View file

@ -0,0 +1,104 @@
require 'helper'
require 'sidekiq'
require 'sidekiq/exception_handler'
require 'stringio'
require 'logger'
ExceptionHandlerTestException = Class.new(StandardError)
TEST_EXCEPTION = ExceptionHandlerTestException.new("Something didn't work!")
def invoke_exception(args)
raise TEST_EXCEPTION
rescue ExceptionHandlerTestException => e
Sidekiq::ExceptionHandler.handle(e,args)
end
class TestExceptionHandler < MiniTest::Unit::TestCase
describe "with mock logger" do
before do
@old_logger = Sidekiq.logger
@str_logger = StringIO.new
Sidekiq.logger = Logger.new(@str_logger)
end
after do
Sidekiq.logger = @old_logger
end
it "logs the exception to Sidekiq.logger" do
invoke_exception(:a => 1)
@str_logger.rewind
log = @str_logger.readlines
assert_match /Something didn't work!/, log[0], "didn't include the exception message"
assert_match /test\/test_exception_handler.rb/, log[1], "didn't include the backtrace"
end
end
describe "with fake Airbrake" do
before do
::Airbrake = MiniTest::Mock.new
end
after do
Object.send(:remove_const, "Airbrake") # HACK should probably inject Airbrake etc into this class in the future
end
it "notifies Airbrake" do
::Airbrake.expect(:notify,nil,[TEST_EXCEPTION,:parameters => { :a => 1 }])
invoke_exception(:a => 1)
::Airbrake.verify
end
end
describe "with fake ExceptionNotifier" do
before do
::ExceptionNotifier = Module.new
::ExceptionNotifier::Notifier = MiniTest::Mock.new
end
after do
Object.send(:remove_const, "ExceptionNotifier")
end
it "notifies ExceptionNotifier" do
::ExceptionNotifier::Notifier.expect(:background_exception_notification,nil,[TEST_EXCEPTION, :data => { :message => { :b => 2 } }])
invoke_exception(:b => 2)
::ExceptionNotifier::Notifier.verify
end
end
describe "with fake Exceptional" do
before do
::Exceptional = Class.new do
def self.context(msg)
@msg = msg
end
def self.check_context
@msg
end
end
::Exceptional::Config = MiniTest::Mock.new
::Exceptional::Remote = MiniTest::Mock.new
::Exceptional::ExceptionData = MiniTest::Mock.new
end
after do
Object.send(:remove_const, "Exceptional")
end
it "notifies Exceptional" do
::Exceptional::Config.expect(:should_send_to_api?,true)
exception_data = MiniTest::Mock.new
::Exceptional::Remote.expect(:error,nil,[exception_data])
::Exceptional::ExceptionData.expect(:new,exception_data,[TEST_EXCEPTION])
invoke_exception(:c => 3)
assert_equal({:c => 3},::Exceptional.check_context,"did not record arguments properly")
::Exceptional::Config.verify
::Exceptional::Remote.verify
::Exceptional::ExceptionData.verify
end
end
end

View file

@ -9,18 +9,6 @@ class TestMiddleware < MiniTest::Unit::TestCase
Sidekiq.redis = REDIS Sidekiq.redis = REDIS
end end
it 'handles errors' do
handler = Sidekiq::Middleware::Server::ExceptionHandler.new
assert_raises ArgumentError do
handler.call('', { :a => 1 }, 'default') do
raise ArgumentError
end
end
assert_equal 1, $errors.size
assert_equal({ :a => 1 }, $errors[0][:parameters])
end
class CustomMiddleware class CustomMiddleware
def initialize(name, recorder) def initialize(name, recorder)
@name = name @name = name
@ -83,10 +71,3 @@ class TestMiddleware < MiniTest::Unit::TestCase
end end
end end
end end
class FakeAirbrake
def self.notify(ex, hash)
$errors << hash
end
end
Airbrake = FakeAirbrake

View file

@ -2,31 +2,55 @@ require 'helper'
require 'sidekiq/processor' require 'sidekiq/processor'
class TestProcessor < MiniTest::Unit::TestCase class TestProcessor < MiniTest::Unit::TestCase
TestException = Class.new(StandardError)
TEST_EXCEPTION = TestException.new("kerboom!")
describe 'with mock setup' do describe 'with mock setup' do
before do before do
$invokes = 0 $invokes = 0
$errors = []
@boss = MiniTest::Mock.new @boss = MiniTest::Mock.new
@processor = ::Sidekiq::Processor.new(@boss)
Celluloid.logger = nil Celluloid.logger = nil
Sidekiq.redis = REDIS Sidekiq.redis = REDIS
@exception_handler = MiniTest::Mock.new
end end
class MockWorker class MockWorker
include Sidekiq::Worker include Sidekiq::Worker
def perform(args) def perform(args)
raise "kerboom!" if args == 'boom' raise TEST_EXCEPTION if args == 'boom'
$invokes += 1 $invokes += 1
end end
end end
it 'processes as expected' do it 'processes as expected' do
msg = Sidekiq.dump_json({ 'class' => MockWorker.to_s, 'args' => ['myarg'] }) msg = Sidekiq.dump_json({ 'class' => MockWorker.to_s, 'args' => ['myarg'] })
processor = ::Sidekiq::Processor.new(@boss) @boss.expect(:processor_done!, nil, [@processor])
@boss.expect(:processor_done!, nil, [processor]) @processor.process(msg, 'default')
processor.process(msg, 'default')
@boss.verify @boss.verify
assert_equal 1, $invokes assert_equal 1, $invokes
assert_equal 0, $errors.size end
it 'passes exceptions to ExceptionHandler' do
msg = Sidekiq.dump_json({ 'class' => MockWorker.to_s, 'args' => ['boom'] })
@processor.exception_handler = @exception_handler
@exception_handler.expect(:handle,nil,[TEST_EXCEPTION,{"class"=>"TestProcessor::MockWorker", "args"=>["boom"]}])
@processor.process(msg, 'default') rescue TestException
@exception_handler.verify
assert_equal 0, $invokes
end
it 're-raises exceptions after handling' do
msg = Sidekiq.dump_json({ 'class' => MockWorker.to_s, 'args' => ['boom'] })
re_raise = false
begin
@processor.process(msg, 'default')
rescue TestException
re_raise = true
end
assert re_raise, "does not re-raise exceptions after handling"
end end
end end
end end