exception handling: designing robust software in ruby

111
Exception Handling: Designing Robust Software [email protected] 2014/9/27@RailsPacific

Upload: wen-tien-chang

Post on 01-Dec-2014

3.381 views

Category:

Technology


3 download

DESCRIPTION

 

TRANSCRIPT

Page 1: Exception Handling: Designing Robust Software in Ruby

Exception Handling: Designing Robust Software

[email protected] 2014/9/27@RailsPacific

Page 2: Exception Handling: Designing Robust Software in Ruby

About me• 張⽂文鈿 a.k.a. ihower

• http://ihower.tw

• http://twitter.com/ihower

• Instructor at ALPHA Camp

• http://alphacamp.tw

• Rails Developer since 2006

• i.e. Rails 1.1.6 era

Page 3: Exception Handling: Designing Robust Software in Ruby

Agenda

• Why should we care? (5min)

• Exception handling in Ruby (10min)

• Caveat and Guideline (15min)

• Failure handling strategy (15min)

Page 4: Exception Handling: Designing Robust Software in Ruby

I’m standing on the two great books, thanks.

Page 5: Exception Handling: Designing Robust Software in Ruby

1. Why should we care?

Page 6: Exception Handling: Designing Robust Software in Ruby

Feature complete ==

Production ready?

Page 7: Exception Handling: Designing Robust Software in Ruby

All tests pass?

Page 8: Exception Handling: Designing Robust Software in Ruby

Happy Path!

Page 9: Exception Handling: Designing Robust Software in Ruby
Page 10: Exception Handling: Designing Robust Software in Ruby

Time

Cost Quality

Pick Two?

Page 11: Exception Handling: Designing Robust Software in Ruby

abstract software architecture to reduce

development cost

Page 12: Exception Handling: Designing Robust Software in Ruby

Development cost !=

Operational cost

Page 13: Exception Handling: Designing Robust Software in Ruby

– Michael T. Nygard, Release It!

“Don't avoid one-time development expenses at the cost of recurring

operational expenses.”

Page 14: Exception Handling: Designing Robust Software in Ruby

My confession

Page 15: Exception Handling: Designing Robust Software in Ruby

2. Exception handling in Ruby

Page 16: Exception Handling: Designing Robust Software in Ruby

– Steve McConnell, Code Complete

“If code in one routine encounters an unexpected condition that it doesn’t know how to handle, it throws an exception, essentially

throwing up its hands and yelling “I don’t know what to do about this — I sure hope somebody

else knows how to handle it!.”

Page 17: Exception Handling: Designing Robust Software in Ruby

– Devid A. Black, The Well-Grounded Rubyist

“Raising an exception means stopping normal execution of the program and either dealing with the problem that’s been encountered or exiting

the program completely.”

Page 18: Exception Handling: Designing Robust Software in Ruby

1. raise exceptionbegin # do something raise 'An error has occured.' rescue => e puts 'I am rescued.' puts e.message puts e.backtrace.inspect end

1/5

Page 19: Exception Handling: Designing Robust Software in Ruby

raise == fail

begin # do something fail 'An error has occured.' rescue => e puts 'I am rescued.' puts e.message puts e.backtrace.inspect end

Page 20: Exception Handling: Designing Robust Software in Ruby

– Jim Weirich, Rake author

“I almost always use the "fail" keyword. . . [T]he only time I use “raise” is when I am catching an exception and re-raising it,

because here I’m not failing, but explicitly and purposefully raising an exception.”

Page 21: Exception Handling: Designing Robust Software in Ruby

“raise” method signature

!raise(exception_class_or_object, message, backtrace)

Page 22: Exception Handling: Designing Robust Software in Ruby

raise

raise!# is equivalent to:!raise RuntimeError

Page 23: Exception Handling: Designing Robust Software in Ruby

raise(string)

raise 'An error has occured.'!# is equivalent to :!raise RuntimeError, "'An error has occured.'

Page 24: Exception Handling: Designing Robust Software in Ruby

raise(exception, string)

raise RuntimeError, 'An error has occured.'!# is equivalent to :!raise RuntimeError.new('An error has occured.')

Page 25: Exception Handling: Designing Robust Software in Ruby

backtrace (Array of string)

raise RuntimeError, 'An error'!# is equivalent to :!raise RuntimeError, 'An error', caller(0)

Page 26: Exception Handling: Designing Robust Software in Ruby

global $! variable

!

• $!

• $ERROR_INFO

• reset to nil if the exception is rescued

Page 27: Exception Handling: Designing Robust Software in Ruby

2. rescue

rescue SomeError => e # ...end

2/5

Page 28: Exception Handling: Designing Robust Software in Ruby

rescue SomeError, SomeOtherError => e # ...end

Multiple class or module

Page 29: Exception Handling: Designing Robust Software in Ruby

rescue SomeError => e # ...rescue SomeOtherError => e # ...end

stacking rescue order matters

Page 30: Exception Handling: Designing Robust Software in Ruby

rescue # ...end!# is equivalent to:!rescue StandardError # ...end

Page 31: Exception Handling: Designing Robust Software in Ruby

Ruby Exception Hierarchy• Exception

• NoMemoryError

• LoadError

• SyntaxError

• StandardError -- default for rescue

• ArgumentError

• IOError

• NoMethodError

• ….

Page 32: Exception Handling: Designing Robust Software in Ruby

Avoid rescuing Exception class

rescue Exception => e # ...end

Page 33: Exception Handling: Designing Robust Software in Ruby

rescue => error # ...end # is equivalent to:!rescue StandardError => error # ...end

Page 34: Exception Handling: Designing Robust Software in Ruby

support * splatted active_support/core_ext/kernel/reporting.rb

suppress(IOError, SystemCallError) do open("NONEXISTENT_FILE")end!puts 'This code gets executed.'

Page 35: Exception Handling: Designing Robust Software in Ruby

!def suppress(*exception_classes) yieldrescue *exception_classes # nothing to doend

support * splatted (cont.) active_support/core_ext/kernel/reporting.rb

Page 36: Exception Handling: Designing Robust Software in Ruby

Like case, it’s support ===

begin raise "Timeout while reading from socket"rescue errors_with_message(/socket/) puts "Ignoring socket error"end

Page 37: Exception Handling: Designing Robust Software in Ruby

def errors_with_message(pattern) m = Module.new m.singleton_class.instance_eval do define_method(:===) do |e| pattern === e.message end end mend

Page 38: Exception Handling: Designing Robust Software in Ruby

arbitrary block predicate

begin raise "Timeout while reading from socket"rescue errors_matching{|e| e.message =~ /socket/} puts "Ignoring socket error"end

Page 39: Exception Handling: Designing Robust Software in Ruby

def errors_matching(&block) m = Module.new m.singleton_class.instance_eval do define_method(:===, &block) end mend

Page 40: Exception Handling: Designing Robust Software in Ruby

– Bertrand Meyer, Object Oriented Software Construction

“In practice, the rescue clause should be a short sequence of simple instructions designed to bring the object back to a stable state and to

either retry the operation or terminate with failure.”

Page 41: Exception Handling: Designing Robust Software in Ruby

3. ensurebegin # do something raise 'An error has occured.' rescue => e puts 'I am rescued.' ensure puts 'This code gets executed always.'end

3/5

Page 42: Exception Handling: Designing Robust Software in Ruby

4. retry be careful “giving up” condition

tries = 0begin tries += 1 puts "Trying #{tries}..." raise "Didn't work"rescue retry if tries < 3 puts "I give up"end

4/5

Page 43: Exception Handling: Designing Robust Software in Ruby

5. elsebegin yieldrescue puts "Only on error"else puts "Only on success"ensure puts "Always executed"end

5/5

Page 44: Exception Handling: Designing Robust Software in Ruby

Recap• raise

• rescue

• ensure

• retry

• else

Page 45: Exception Handling: Designing Robust Software in Ruby

3. Caveat and Guideline

Page 46: Exception Handling: Designing Robust Software in Ruby

1. Is the situation truly unexpected?

1/6

Page 47: Exception Handling: Designing Robust Software in Ruby

– Dave Thomas and Andy Hunt, The Pragmatic Programmer

“ask yourself, 'Will this code still run if I remove all the exception handlers?" If the answer is "no", then maybe exceptions are

being used in non-exceptional circumstances.”

Page 48: Exception Handling: Designing Robust Software in Ruby

User Input error?

Page 49: Exception Handling: Designing Robust Software in Ruby

def create @user = User.new params[:user] @user.save! redirect_to user_path(@user)rescue ActiveRecord::RecordNotSaved flash[:notice] = 'Unable to create user' render :action => :newend

This is bad

Page 50: Exception Handling: Designing Robust Software in Ruby

def create @user = User.new params[:user] if @user.save redirect_to user_path(@user) else flash[:notice] = 'Unable to create user' render :action => :new endend

Page 51: Exception Handling: Designing Robust Software in Ruby

Record not found?

Page 52: Exception Handling: Designing Robust Software in Ruby

begin user = User.find(params[:id) user.do_thisrescue ActiveRecord::RecordNotFound # ???end

This is bad

Page 53: Exception Handling: Designing Robust Software in Ruby

Use Null object

user = User.find_by_id(params[:id) || NullUser.newuser.do_this

Page 54: Exception Handling: Designing Robust Software in Ruby

Replace Exception with Testdef execute(command) command.prepare rescue nil command.executeend!# =>!def execute(command) command.prepare if command.respond_to? :prepare command.executeend

Page 55: Exception Handling: Designing Robust Software in Ruby

– Martin Folwer, Refactoring

“Exceptions should be used for exceptional behaviour. They should not acts as substitute

for conditional tests. If you can reasonably expect the caller to check the condition before calling the operation, you should provide a test,

and the caller should use it.”

Page 56: Exception Handling: Designing Robust Software in Ruby

Spare Handler?begin # main implementationrescue # alternative solutionend

Page 57: Exception Handling: Designing Robust Software in Ruby

begin user = User.find(params[:id)rescue ActiveRecord::RecordNotFound user = NullUser.newensure user.do_thisend

This is bad

Page 58: Exception Handling: Designing Robust Software in Ruby

user = User.find_by_id(params[:id) || NullUser.new!user.do_this

Page 59: Exception Handling: Designing Robust Software in Ruby

2. raise during raise

begin raise "Error A" # this will be thrownrescue raise "Error B"end

2/6

Page 60: Exception Handling: Designing Robust Software in Ruby

Wrapped exception!begin begin raise "Error A" rescue => error raise MyError, "Error B" endrescue => e puts "Current failure: #{e.inspect}" puts "Original failure: #{e.original.inspect}"end

Page 61: Exception Handling: Designing Robust Software in Ruby

Wrapped exception (cont.)

class MyError < StandardError attr_reader :original def initialize(msg, original=$!) super(msg) @original = original; set_backtrace original.backtrace endend

Page 62: Exception Handling: Designing Robust Software in Ruby

Example: Rails uses the technique a lot

• ActionDispatch::ExceptionWrapper

• ActionControllerError::BadRequest, ParseError,SessionRestoreError

• ActionView Template::Error

Page 63: Exception Handling: Designing Robust Software in Ruby

3. raise during ensure

begin raise "Error A"ensure raise "Error B” puts "Never execute"end

3/6

Page 64: Exception Handling: Designing Robust Software in Ruby

begin file1.open file2.open raise "Error"ensure file1.close # if there's an error file2.closeend

Page 65: Exception Handling: Designing Robust Software in Ruby

a more complex examplebegin r1 = Resource.new(1) r2 = Resource.new(2) r2.run r1.run rescue => e raise "run error: #{e.message}"ensure r2.close r1.close end

Page 66: Exception Handling: Designing Robust Software in Ruby

class Resource attr_accessor :id! def initialize(id) self.id = id end! def run puts "run #{self.id}" raise "run error: #{self.id}" end! def close puts "close #{self.id}" raise "close error: #{self.id}" endend

Page 67: Exception Handling: Designing Robust Software in Ruby

begin r1 = Resource.new(1) r2 = Resource.new(2) r2.run r1.run # raise exception!!! rescue => e raise "run error: #{e.message}"ensure r2.close # raise exception!!! r1.close # never execute! end

Page 68: Exception Handling: Designing Robust Software in Ruby

Result lost original r1 exception and fail to close r2

run 1 run 2 close 1 double_raise.rb:15:in `close': close error: 1 (RuntimeError)

Page 69: Exception Handling: Designing Robust Software in Ruby

4. Exception is your method interface too

• For library, either you document your exception, or you should consider no-raise library API

• return values (error code)

• provide fallback callback

4/6

Page 70: Exception Handling: Designing Robust Software in Ruby

5. Exception classification

module MyLibrary class Error < StandardError endend

5/6

Page 71: Exception Handling: Designing Robust Software in Ruby

exception hierarchy example

• StandardError

• ActionControllerError

• BadRequest

• RoutingError

• ActionController::UrlGenerationError

• MethodNotAllowed

• NotImplemented

• …

Page 72: Exception Handling: Designing Robust Software in Ruby

smart exception adding more information

class MyError < StandardError attr_accessor :code! def initialize(code) self.code = code endend!begin raise MyError.new(1)rescue => e puts "Error code: #{e.code}"end

Page 73: Exception Handling: Designing Robust Software in Ruby

6. Readable exceptional code

begin try_something rescue begin try_something_else rescue # ... end endend

6/6

Page 74: Exception Handling: Designing Robust Software in Ruby

Extract it!def foo try_somethingrescue barend!def bar try_something_else # ...rescue # ...end

Page 75: Exception Handling: Designing Robust Software in Ruby

Recap• Use exception when you need

• Wrap exception when re-raising

• Avoid raising during ensure

• Exception is your method interface too

• Classify your exceptions

• Readable exceptional code

Page 76: Exception Handling: Designing Robust Software in Ruby

4. Failure handling strategy

Page 77: Exception Handling: Designing Robust Software in Ruby

1. Exception Safety• no guarantee

• The weak guarantee (no-leak): If an exception is raised, the object will be left in a consistent state.

• The strong guarantee (a.k.a. commit-or-rollback, all-or-nothing): If an exception is raised, the object will be rolled back to its beginning state.

• The nothrow guarantee (failure transparency): No exceptions will be raised from the method. If an exception is raised during the execution of the method it will be handled internally.

1/12

Page 78: Exception Handling: Designing Robust Software in Ruby

2. Operational errors v.s.

programmer errors

https://www.joyent.com/developers/node/design/errors 2/12

Page 79: Exception Handling: Designing Robust Software in Ruby

Operational errors• failed to connect to server

• failed to resolve hostname

• request timeout

• server returned a 500 response

• socket hang-up

• system is out of memory

Page 80: Exception Handling: Designing Robust Software in Ruby

Programmer errors like typo

Page 81: Exception Handling: Designing Robust Software in Ruby

We can handle operational errors

• Restore and Cleanup Resource

• Handle it directly

• Propagate

• Retry

• Crash

• Log it

Page 82: Exception Handling: Designing Robust Software in Ruby

But we can not handle programmer errors

Page 83: Exception Handling: Designing Robust Software in Ruby

3. Robust levels

• Robustness: the ability of a system to resist change without adapting its initial stable configuration.

• There’re four robust levels

http://en.wikipedia.org/wiki/Robustness 3/12

Page 84: Exception Handling: Designing Robust Software in Ruby

Level 0: Undefined

• Service: Failing implicitly or explicitly

• State: Unknown or incorrect

• Lifetime: Terminated or continued

Page 85: Exception Handling: Designing Robust Software in Ruby

Level 1: Error-reporting (Failing fast)

• Service: Failing explicitly

• State: Unknown or incorrect

• Lifetime: Terminated or continued

• How-achieved:

• Propagating all unhandled exceptions, and

• Catching and reporting them in the main program

Page 86: Exception Handling: Designing Robust Software in Ruby

Anti-pattern:Dummy handler (eating exceptions)

begin #... rescue => e nilend

Page 87: Exception Handling: Designing Robust Software in Ruby

Level 2: State-recovery (weakly tolerant)

• Service: Failing explicitly

• State: Correct

• Lifetime: Continued

• How-achieved:

• Backward recovery

• Cleanup

Page 88: Exception Handling: Designing Robust Software in Ruby

require 'open-uri'page = "titles"file_name = "#{page}.html"web_page = open("https://pragprog.com/#{page}")output = File.open(file_name, "w")begin while line = web_page.gets output.puts line end output.closerescue => e STDERR.puts "Failed to download #{page}" output.close File.delete(file_name) raiseend

Page 89: Exception Handling: Designing Robust Software in Ruby

Level 3: Behavior-recovery (strongly tolerant)

• Service: Delivered

• State: Correct

• Lifetime: Continued

• How-achieved:

• retry, and/or

• design diversity, data diversity, and functional diversity

Page 90: Exception Handling: Designing Robust Software in Ruby

Improve exception handling incrementally

• Level 0 is bad

• it’s better we require all method has Level 1

• Level 2 for critical operation. e.g storage/database operation

• Level 3 for critical feature or customer requires. it means cost * 2 because we must have two solution everywhere.

Page 91: Exception Handling: Designing Robust Software in Ruby

4. Use timeout for any external call

begin Timeout::timeout(3) do #... endrescue Timeout::Error => e # ...end

4/12

Page 92: Exception Handling: Designing Robust Software in Ruby

5. retry with circuit breaker

http://martinfowler.com/bliki/CircuitBreaker.html 5/12

Page 93: Exception Handling: Designing Robust Software in Ruby

6. bulkheads for external service and process

begin SomeExternalService.do_requestrescue Exception => e logger.error "Error from External Service" logger.error e.message logger.error e.backtrace.join("\n")end

6/12

Page 94: Exception Handling: Designing Robust Software in Ruby

!

7. Failure reporting

• A central log server

• Email

• exception_notification gem

• 3-party exception-reporting service

• Airbrake, Sentry New Relic…etc

7/12

Page 95: Exception Handling: Designing Robust Software in Ruby

8. Exception collectionclass Item def process #... [true, result] rescue => e [false, result] end end!collections.each do |item| results << item.processend 8/12

Page 96: Exception Handling: Designing Robust Software in Ruby

9. caller-supplied fallback strategy

h.fetch(:optional){ "default value" }h.fetch(:required){ raise "Required not found" }!arr.fetch(999){ "default value" }arr.detect( lambda{ "default value" }) { |x| x == "target" }

9/12

Page 97: Exception Handling: Designing Robust Software in Ruby

– Brian Kernighan and Rob Pike, The Practice of Programming

“In most cases, the caller should determine how to handle an error, not the callee.”

Page 98: Exception Handling: Designing Robust Software in Ruby

def do_something(failure_policy=method(:raise)) #...rescue => e failure_policy.call(e) end!do_something{ |e| puts e.message }

Page 99: Exception Handling: Designing Robust Software in Ruby

10. Avoid unexpected termination

• rescue all exceptions at the outermost call stacks.

• Rails does a great job here

• developer will see all exceptions at development mode.

• end user will not see exceptions at production mode

10/12

Page 100: Exception Handling: Designing Robust Software in Ruby

11. Error code v.s. exception

• error code problems

• it mixes normal code and exception handling

• programmer can ignore error code easily

11/12

Page 101: Exception Handling: Designing Robust Software in Ruby

Error code problem

prepare_something # return error codetrigger_someaction # still run

http://yosefk.com/blog/error-codes-vs-exceptions-critical-code-vs-typical-code.html

Page 102: Exception Handling: Designing Robust Software in Ruby

Replace Error Code with Exception (from Refactoring)

def withdraw(amount) return -1 if amount > @balance @balance -= amount 0end!# =>!def withdraw(amount) raise BalanceError.new if amount > @balance @balance -= amountend

Page 103: Exception Handling: Designing Robust Software in Ruby

Why does Go not have exceptions?

• We believe that coupling exceptions to a control structure, as in the try-catch-finally idiom, results in convoluted code. It also tends to encourage programmers to label too many ordinary errors, such as failing to open a file, as exceptional. Go takes a different approach. For plain error handling, Go's multi-value returns make it easy to report an error without overloading the return value. A canonical error type, coupled with Go's other features, makes error handling pleasant but quite different from that in other languages.

https://golang.org/doc/faq#exceptions

Page 104: Exception Handling: Designing Robust Software in Ruby

– Raymond Chen

“It's really hard to write good exception-based code since you have to check every single line

of code (indeed, every sub-expression) and think about what exceptions it might raise and

how your code will react to it.”

http://blogs.msdn.com/b/oldnewthing/archive/2005/01/14/352949.aspx

Page 105: Exception Handling: Designing Robust Software in Ruby

When use error code?!

• In low-level code which you must know the behavior in every possible situation

• error codes may be better

• Otherwise we have to know every exception that can be thrown by every line in the method to know what it will do

http://stackoverflow.com/questions/253314/exceptions-or-error-codes

Page 106: Exception Handling: Designing Robust Software in Ruby

12. throw/catch flowdef halt(*response) #.... throw :halt, responseend def invoke res = catch(:halt) { yield } #...end

12/12

Page 107: Exception Handling: Designing Robust Software in Ruby

Recap!

• exception safety

• operational errors v.s. programmer errors

• robust levels

Page 108: Exception Handling: Designing Robust Software in Ruby

Recap (cont.)• use timeout

• retry with circuit breaker pattern

• bulkheads

• failure reporting

• exception collection

• caller-supplied fallback strategy

• avoid unexpected termination

• error code

• throw/catch

Page 109: Exception Handling: Designing Robust Software in Ruby

Thanks for your listening

Page 110: Exception Handling: Designing Robust Software in Ruby

QUESTIONS!

begin thanks! raise if question?rescue puts "Please ask @ihower at twitter"ensure follow(@ihower).get_slides applause!end

Page 111: Exception Handling: Designing Robust Software in Ruby

Reference• 例外處理設計的逆襲

• Exceptional Ruby http://avdi.org/talks/exceptional-ruby-2011-02-04/

• Release it!

• Programming Ruby

• The Well-Grounded Rubyist, 2nd

• Refactoring

• The Pragmatic Programmer

• Code Complete 2

• https://www.joyent.com/developers/node/design/errors

• http://robots.thoughtbot.com/save-bang-your-head-active-record-will-drive-you-mad

• http://code.tutsplus.com/articles/writing-robust-web-applications-the-lost-art-of-exception-handling--net-36395

• http://blogs.msdn.com/b/oldnewthing/archive/2005/01/14/352949.aspx