Exception Handling: Designing Robust Software in Ruby
Upcoming SlideShare
Loading in...5
×
 

Exception Handling: Designing Robust Software in Ruby

on

  • 1,072 views

 

Statistics

Views

Total Views
1,072
Views on SlideShare
860
Embed Views
212

Actions

Likes
23
Downloads
25
Comments
0

9 Embeds 212

http://ihower.tw 92
https://twitter.com 72
http://feedly.com 29
http://www.feedspot.com 5
http://www.hksilicon.com 5
http://digg.com 5
http://tweetedtimes.com 2
https://www.inoreader.com 1
http://www.inoreader.com 1
More...

Accessibility

Categories

Upload Details

Uploaded via as Adobe PDF

Usage Rights

© All Rights Reserved

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Processing…
Post Comment
Edit your comment

Exception Handling: Designing Robust Software in Ruby Exception Handling: Designing Robust Software in Ruby Presentation Transcript

  • Exception Handling: Designing Robust Software ihower@gmail.com 2014/9/27@RailsPacific
  • 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
  • Agenda • Why should we care? (5min) • Exception handling in Ruby (10min) • Caveat and Guideline (15min) • Failure handling strategy (15min) View slide
  • I’m standing on the two great books, thanks. View slide
  • 1. Why should we care?
  • Feature complete == Production ready?
  • All tests pass?
  • Happy Path!
  • Time Cost Quality Pick Two?
  • abstract software architecture to reduce development cost
  • Development cost != Operational cost
  • – Michael T. Nygard, Release It! “Don't avoid one-time development expenses at the cost of recurring operational expenses.”
  • My confession
  • 2. Exception handling 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!.”
  • – 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.”
  • 1. raise exception begin # do something raise 'An error has occured.' rescue => e puts 'I am rescued.' puts e.message puts e.backtrace.inspect end 1/5
  • raise == fail begin # do something fail 'An error has occured.' rescue => e puts 'I am rescued.' puts e.message puts e.backtrace.inspect end
  • – 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.”
  • “raise” method signature ! raise(exception_class_or_object, message, backtrace)
  • raise raise ! # is equivalent to: ! raise RuntimeError
  • raise(string) raise 'An error has occured.' ! # is equivalent to : ! raise RuntimeError, "'An error has occured.'
  • raise(exception, string) raise RuntimeError, 'An error has occured.' ! # is equivalent to : ! raise RuntimeError.new('An error has occured.')
  • backtrace (Array of string) raise RuntimeError, 'An error' ! # is equivalent to : ! raise RuntimeError, 'An error', caller(0)
  • global $! variable ! • $! • $ERROR_INFO • reset to nil if the exception is rescued
  • 2. rescue rescue SomeError => e # ... end 2/5
  • rescue SomeError, SomeOtherError => e # ... end Multiple class or module
  • rescue SomeError => e # ... rescue SomeOtherError => e # ... end stacking rescue order matters
  • rescue # ... end ! # is equivalent to: ! rescue StandardError # ... end
  • Ruby Exception Hierarchy • Exception • NoMemoryError • LoadError • SyntaxError • StandardError -- default for rescue • ArgumentError • IOError • NoMethodError • ….
  • Avoid rescuing Exception class rescue Exception => e # ... end
  • rescue => error # ... end # is equivalent to: ! rescue StandardError => error # ... end
  • support * splatted active_support/core_ext/kernel/reporting.rb suppress(IOError, SystemCallError) do open("NONEXISTENT_FILE") end ! puts 'This code gets executed.'
  • ! def suppress(*exception_classes) yield rescue *exception_classes # nothing to do end support * splatted (cont.) active_support/core_ext/kernel/reporting.rb
  • Like case, it’s support === begin raise "Timeout while reading from socket" rescue errors_with_message(/socket/) puts "Ignoring socket error" end
  • def errors_with_message(pattern) m = Module.new m.singleton_class.instance_eval do define_method(:===) do |e| pattern === e.message end end m end
  • arbitrary block predicate begin raise "Timeout while reading from socket" rescue errors_matching{|e| e.message =~ /socket/} puts "Ignoring socket error" end
  • def errors_matching(&block) m = Module.new m.singleton_class.instance_eval do define_method(:===, &block) end m end
  • – 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.”
  • 3. ensure begin # do something raise 'An error has occured.' rescue => e puts 'I am rescued.' ensure puts 'This code gets executed always.' end 3/5
  • 4. retry be careful “giving up” condition tries = 0 begin tries += 1 puts "Trying #{tries}..." raise "Didn't work" rescue retry if tries < 3 puts "I give up" end 4/5
  • 5. else begin yield rescue puts "Only on error" else puts "Only on success" ensure puts "Always executed" end 5/5
  • Recap • raise • rescue • ensure • retry • else
  • 3. Caveat and Guideline
  • 1. Is the situation truly unexpected? 1/6
  • – 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.”
  • User Input error?
  • 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 => :new end This is bad
  • 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 end end
  • Record not found?
  • begin user = User.find(params[:id) user.do_this rescue ActiveRecord::RecordNotFound # ??? end This is bad
  • Use Null object user = User.find_by_id(params[:id) || NullUser.new user.do_this
  • Replace Exception with Test def execute(command) command.prepare rescue nil command.execute end ! # => ! def execute(command) command.prepare if command.respond_to? :prepare command.execute end
  • – 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.”
  • Spare Handler? begin # main implementation rescue # alternative solution end
  • begin user = User.find(params[:id) rescue ActiveRecord::RecordNotFound user = NullUser.new ensure user.do_this end This is bad
  • user = User.find_by_id(params[:id) || NullUser.new ! user.do_this
  • 2. raise during raise begin raise "Error A" # this will be thrown rescue raise "Error B" end 2/6
  • Wrapped exception ! begin begin raise "Error A" rescue => error raise MyError, "Error B" end rescue => e puts "Current failure: #{e.inspect}" puts "Original failure: #{e.original.inspect}" end
  • Wrapped exception (cont.) class MyError < StandardError attr_reader :original def initialize(msg, original=$!) super(msg) @original = original; set_backtrace original.backtrace end end
  • Example: Rails uses the technique a lot • ActionDispatch::ExceptionWrapper • ActionControllerError::BadRequest, ParseError,SessionRestoreError • ActionView Template::Error
  • 3. raise during ensure begin raise "Error A" ensure raise "Error B” puts "Never execute" end 3/6
  • begin file1.open file2.open raise "Error" ensure file1.close # if there's an error file2.close end
  • a more complex example begin 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
  • 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}" end end
  • 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
  • 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)
  • 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
  • 5. Exception classification module MyLibrary class Error < StandardError end end 5/6
  • exception hierarchy example • StandardError • ActionControllerError • BadRequest • RoutingError • ActionController::UrlGenerationError • MethodNotAllowed • NotImplemented • …
  • smart exception adding more information class MyError < StandardError attr_accessor :code ! def initialize(code) self.code = code end end ! begin raise MyError.new(1) rescue => e puts "Error code: #{e.code}" end
  • 6. Readable exceptional code begin try_something rescue begin try_something_else rescue # ... end end end 6/6
  • Extract it! def foo try_something rescue bar end ! def bar try_something_else # ... rescue # ... end
  • 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
  • 4. Failure handling strategy
  • 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
  • 2. Operational errors v.s. programmer errors https://www.joyent.com/developers/node/design/errors 2/12
  • 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
  • Programmer errors like typo
  • We can handle operational errors • Restore and Cleanup Resource • Handle it directly • Propagate • Retry • Crash • Log it
  • But we can not handle programmer errors
  • 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
  • Level 0: Undefined • Service: Failing implicitly or explicitly • State: Unknown or incorrect • Lifetime: Terminated or continued
  • 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
  • Anti-pattern:
 Dummy handler (eating exceptions) begin #... rescue => e nil end
  • Level 2: State-recovery (weakly tolerant) • Service: Failing explicitly • State: Correct • Lifetime: Continued • How-achieved: • Backward recovery • Cleanup
  • 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.close rescue => e STDERR.puts "Failed to download #{page}" output.close File.delete(file_name) raise end
  • Level 3: Behavior-recovery (strongly tolerant) • Service: Delivered • State: Correct • Lifetime: Continued • How-achieved: • retry, and/or • design diversity, data diversity, and functional diversity
  • 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.
  • 4. Use timeout for any external call begin Timeout::timeout(3) do #... end rescue Timeout::Error => e # ... end 4/12
  • 5. retry with circuit breaker http://martinfowler.com/bliki/CircuitBreaker.html 5/12
  • 6. bulkheads for external service and process begin SomeExternalService.do_request rescue Exception => e logger.error "Error from External Service" logger.error e.message logger.error e.backtrace.join("n") end 6/12
  • ! 7. Failure reporting • A central log server • Email • exception_notification gem • 3-party exception-reporting service • Airbrake, Sentry New Relic…etc 7/12
  • 8. Exception collection class Item def process #... [true, result] rescue => e [false, result] end end ! collections.each do |item| results << item.process end 8/12
  • 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
  • – Brian Kernighan and Rob Pike, The Practice of Programming “In most cases, the caller should determine how to handle an error, not the callee.”
  • def do_something(failure_policy=method(:raise)) #... rescue => e failure_policy.call(e) end ! do_something{ |e| puts e.message }
  • 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
  • 11. Error code v.s. exception • error code problems • it mixes normal code and exception handling • programmer can ignore error code easily 11/12
  • Error code problem prepare_something # return error code trigger_someaction # still run http://yosefk.com/blog/error-codes-vs-exceptions-critical-code-vs-typical-code.html
  • Replace Error Code with Exception (from Refactoring) def withdraw(amount) return -1 if amount > @balance @balance -= amount 0 end ! # => ! def withdraw(amount) raise BalanceError.new if amount > @balance @balance -= amount end
  • 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
  • – 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
  • 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
  • 12. throw/catch flow def halt(*response) #.... throw :halt, response end def invoke res = catch(:halt) { yield } #... end 12/12
  • Recap ! • exception safety • operational errors v.s. programmer errors • robust levels
  • 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
  • Thanks for your listening
  • QUESTIONS! begin thanks! raise if question? rescue puts "Please ask @ihower at twitter" ensure follow(@ihower).get_slides applause! end
  • 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