背景

有一个通知服务,需要使用Mandrill的提供的邮件服务给客户发送邮件。开发时使用了mandrill_mailer这个GEM来实现和Mandrill服务的交互。

今天监控程序报警提及昨天的邮件发送量比平时低了不少。上线查了一下日志,发现是代码中一处exception没处理好,导致程序崩溃了。

分析

Ruby 中捕获非指定类型的异常,会使用如下方式来捕获异常。

1
2
3
begin
rescue => e
end

根据官方文档的说明,不指定exception类型时,rescue默认捕获的是StandardError类型的exception。

在我这个问题中,由于Mandrill的服务出现了问题,发送请求后,没有返回正常的json格式,而是返回了504 Gateway Time-out的一段HTML, Gem mandrill_mailer解析JSON失败,抛出了自定义的Mandrill::Error的异常。

Gem中抛异常的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
begin
error_info = JSON.parse(body)
if error_info['status'] != 'error' or not error_info['name']
raise Error, "We received an unexpected error: #{body}"
end
if error_map[error_info['name']]
raise error_map[error_info['name']], error_info['message']
else
raise Error, error_info['message']
end
rescue JSON::ParserError
raise Error, "We received an unexpected error: #{body}"
end

Error异常定义的代码

1
2
3
4
5
module Mandrill
class Error < Exception
end
class ValidationError < Error
end

可以看到,Mandrill::Error这个类型,是直接继承自Exception的。而在原有的代码中,没有指定exception的类型,捕获不到这个exception,导致程序崩溃了。

在代码中指定固定类型的exception,解决该问题

1
2
3
begin
rescue StandardError, Mandrill::Error => e
end

深挖

Ruby中关于如何throw和catch exception,stackoverflowruby-doc.org上的这几个网页值得细读

梳理了一下,罗列如下:

  • 三种常用的exception类
  • RuntimeErrorStandardError的一个子类,而StandardError又是Exception的一个子类。
  • 直接raise,抛出的是RuntimeError的异常
  • 默认的rescue,捕获的是StandardError的异常
  • Exception包含了全部的异常,ruby中全部异常都可以使用rescue Exception => e这种写法来捕获。
  • 但强烈建议不要直接粗暴的指定基类Exception来捕获异常
    • rescue Exception时,会捕获所有的异常,包括SyntaxError, LoadErrorSignalException
    • 捕获了SignalException,会导致除了kill -9的其他信号量失效,包括Ctrl + C
    • 捕获SyntaxError,调用eval时,即使失败了也不会有提示
  • 编写库和Gem的自定义异常时,强烈反对直接继承自基类Exception,因为这会导致使用者在没有指定异常类型时,无法成功捕获异常,导致程序崩溃。

小实验

  1. raise字符串,rescue不指定异常类型

    代码:

    1
    2
    3
    4
    5
    begin
    raise "error occur"
    rescue
    puts "here is string exception"
    end

    运行结果:

    1
    here is string exception
  2. raise Exception, rescue Exception

    代码:

    1
    2
    3
    4
    5
    begin
    raise Exception.new("error occur")
    rescue Exception
    puts "here is Exception exception"
    end

    运行结果:

    1
    here is Exception exception
  3. raise字符串,rescue Exception

    代码:

    1
    2
    3
    4
    5
    begin
    raise "error occur"
    rescue Exception
    puts "here is exception exception"
    end

    运行结果:

    1
    2
    # rescue Exception可以捕获任何异常
    here is exception exception
  4. raise Exception, rescue不指定异常类型

    代码:

    1
    2
    3
    4
    5
    begin
    raise Exception.new("error occur")
    rescue
    puts "here is string exception"
    end

    运行结果:

    1
    2
    3
    4
    # 没有捕获到异常,程序崩溃了
    Exception: error occur
    from (irb):39
    from /Users/carlshen/.rvm/rubies/ruby-2.3.0/bin/irb:11:in `<main>'

留言