Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Exception Handling: Designing Robust Software in Ruby

  • Login to see the comments

Exception Handling: Designing Robust Software in Ruby

  1. 1. Exception Handling: Designing Robust Software ihower@gmail.com 2014/9/27@RailsPacific
  2. 2. 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
  3. 3. Agenda • Why should we care? (5min) • Exception handling in Ruby (10min) • Caveat and Guideline (15min) • Failure handling strategy (15min)
  4. 4. I’m standing on the two great books, thanks.
  5. 5. 1. Why should we care?
  6. 6. Feature complete == Production ready?
  7. 7. All tests pass?
  8. 8. Happy Path!
  9. 9. Time Cost Quality Pick Two?
  10. 10. abstract software architecture to reduce development cost
  11. 11. Development cost != Operational cost
  12. 12. – Michael T. Nygard, Release It! “Don't avoid one-time development expenses at the cost of recurring operational expenses.”
  13. 13. My confession
  14. 14. 2. Exception handling in Ruby
  15. 15. – 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!.”
  16. 16. – 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.”
  17. 17. 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
  18. 18. raise == fail begin # do something fail 'An error has occured.' rescue => e puts 'I am rescued.' puts e.message puts e.backtrace.inspect end
  19. 19. – 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.”
  20. 20. “raise” method signature ! raise(exception_class_or_object, message, backtrace)
  21. 21. raise raise ! # is equivalent to: ! raise RuntimeError
  22. 22. raise(string) raise 'An error has occured.' ! # is equivalent to : ! raise RuntimeError, "'An error has occured.'
  23. 23. raise(exception, string) raise RuntimeError, 'An error has occured.' ! # is equivalent to : ! raise RuntimeError.new('An error has occured.')
  24. 24. backtrace (Array of string) raise RuntimeError, 'An error' ! # is equivalent to : ! raise RuntimeError, 'An error', caller(0)
  25. 25. global $! variable ! • $! • $ERROR_INFO • reset to nil if the exception is rescued
  26. 26. 2. rescue rescue SomeError => e # ... end 2/5
  27. 27. rescue SomeError, SomeOtherError => e # ... end Multiple class or module
  28. 28. rescue SomeError => e # ... rescue SomeOtherError => e # ... end stacking rescue order matters
  29. 29. rescue # ... end ! # is equivalent to: ! rescue StandardError # ... end
  30. 30. Ruby Exception Hierarchy • Exception • NoMemoryError • LoadError • SyntaxError • StandardError -- default for rescue • ArgumentError • IOError • NoMethodError • ….
  31. 31. Avoid rescuing Exception class rescue Exception => e # ... end
  32. 32. rescue => error # ... end # is equivalent to: ! rescue StandardError => error # ... end
  33. 33. support * splatted active_support/core_ext/kernel/reporting.rb suppress(IOError, SystemCallError) do open("NONEXISTENT_FILE") end ! puts 'This code gets executed.'
  34. 34. ! def suppress(*exception_classes) yield rescue *exception_classes # nothing to do end support * splatted (cont.) active_support/core_ext/kernel/reporting.rb
  35. 35. Like case, it’s support === begin raise "Timeout while reading from socket" rescue errors_with_message(/socket/) puts "Ignoring socket error" end
  36. 36. 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
  37. 37. arbitrary block predicate begin raise "Timeout while reading from socket" rescue errors_matching{|e| e.message =~ /socket/} puts "Ignoring socket error" end
  38. 38. def errors_matching(&block) m = Module.new m.singleton_class.instance_eval do define_method(:===, &block) end m end
  39. 39. – 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.”
  40. 40. 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
  41. 41. 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
  42. 42. 5. else begin yield rescue puts "Only on error" else puts "Only on success" ensure puts "Always executed" end 5/5
  43. 43. Recap • raise • rescue • ensure • retry • else
  44. 44. 3. Caveat and Guideline
  45. 45. 1. Is the situation truly unexpected? 1/6
  46. 46. – 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.”
  47. 47. User Input error?
  48. 48. 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
  49. 49. 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
  50. 50. Record not found?
  51. 51. begin user = User.find(params[:id) user.do_this rescue ActiveRecord::RecordNotFound # ??? end This is bad
  52. 52. Use Null object user = User.find_by_id(params[:id) || NullUser.new user.do_this
  53. 53. 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
  54. 54. – 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.”
  55. 55. Spare Handler? begin # main implementation rescue # alternative solution end
  56. 56. begin user = User.find(params[:id) rescue ActiveRecord::RecordNotFound user = NullUser.new ensure user.do_this end This is bad
  57. 57. user = User.find_by_id(params[:id) || NullUser.new ! user.do_this
  58. 58. 2. raise during raise begin raise "Error A" # this will be thrown rescue raise "Error B" end 2/6
  59. 59. 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
  60. 60. Wrapped exception (cont.) class MyError < StandardError attr_reader :original def initialize(msg, original=$!) super(msg) @original = original; set_backtrace original.backtrace end end
  61. 61. Example: Rails uses the technique a lot • ActionDispatch::ExceptionWrapper • ActionControllerError::BadRequest, ParseError,SessionRestoreError • ActionView Template::Error
  62. 62. 3. raise during ensure begin raise "Error A" ensure raise "Error B” puts "Never execute" end 3/6
  63. 63. begin file1.open file2.open raise "Error" ensure file1.close # if there's an error file2.close end
  64. 64. 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
  65. 65. 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
  66. 66. 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
  67. 67. 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)
  68. 68. 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
  69. 69. 5. Exception classification module MyLibrary class Error < StandardError end end 5/6
  70. 70. exception hierarchy example • StandardError • ActionControllerError • BadRequest • RoutingError • ActionController::UrlGenerationError • MethodNotAllowed • NotImplemented • …
  71. 71. 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
  72. 72. 6. Readable exceptional code begin try_something rescue begin try_something_else rescue # ... end end end 6/6
  73. 73. Extract it! def foo try_something rescue bar end ! def bar try_something_else # ... rescue # ... end
  74. 74. 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
  75. 75. 4. Failure handling strategy
  76. 76. 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
  77. 77. 2. Operational errors v.s. programmer errors https://www.joyent.com/developers/node/design/errors 2/12
  78. 78. 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
  79. 79. Programmer errors like typo
  80. 80. We can handle operational errors • Restore and Cleanup Resource • Handle it directly • Propagate • Retry • Crash • Log it
  81. 81. But we can not handle programmer errors
  82. 82. 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
  83. 83. Level 0: Undefined • Service: Failing implicitly or explicitly • State: Unknown or incorrect • Lifetime: Terminated or continued
  84. 84. 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
  85. 85. Anti-pattern:
 Dummy handler (eating exceptions) begin #... rescue => e nil end
  86. 86. Level 2: State-recovery (weakly tolerant) • Service: Failing explicitly • State: Correct • Lifetime: Continued • How-achieved: • Backward recovery • Cleanup
  87. 87. 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
  88. 88. Level 3: Behavior-recovery (strongly tolerant) • Service: Delivered • State: Correct • Lifetime: Continued • How-achieved: • retry, and/or • design diversity, data diversity, and functional diversity
  89. 89. 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.
  90. 90. 4. Use timeout for any external call begin Timeout::timeout(3) do #... end rescue Timeout::Error => e # ... end 4/12
  91. 91. 5. retry with circuit breaker http://martinfowler.com/bliki/CircuitBreaker.html 5/12
  92. 92. 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
  93. 93. ! 7. Failure reporting • A central log server • Email • exception_notification gem • 3-party exception-reporting service • Airbrake, Sentry New Relic…etc 7/12
  94. 94. 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
  95. 95. 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
  96. 96. – Brian Kernighan and Rob Pike, The Practice of Programming “In most cases, the caller should determine how to handle an error, not the callee.”
  97. 97. def do_something(failure_policy=method(:raise)) #... rescue => e failure_policy.call(e) end ! do_something{ |e| puts e.message }
  98. 98. 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
  99. 99. 11. Error code v.s. exception • error code problems • it mixes normal code and exception handling • programmer can ignore error code easily 11/12
  100. 100. 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
  101. 101. 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
  102. 102. 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
  103. 103. – 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
  104. 104. 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
  105. 105. 12. throw/catch flow def halt(*response) #.... throw :halt, response end def invoke res = catch(:halt) { yield } #... end 12/12
  106. 106. Recap ! • exception safety • operational errors v.s. programmer errors • robust levels
  107. 107. 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
  108. 108. Thanks for your listening
  109. 109. QUESTIONS! begin thanks! raise if question? rescue puts "Please ask @ihower at twitter" ensure follow(@ihower).get_slides applause! end
  110. 110. 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

×