类似用户未读通知的站内消息,每次访问站点时都必须查询的操作,如果出现数据库查询连接异常或网络连接错误,那么程序就会报 500 错误,用户不能正常访问站点。通常的做法是不管成功与否,这种行为都要进行异常捕获。
本篇文章是参考 Resilience in Ruby: Handling Failure 的总结,作者基于 GitHub 用户通知数据分库的实际场景而写。
基本的错误处理
由于某一个加载项出错而导致整个网址不可用,有必要对异常处理时(常见的场景就是用户未读消息,不能因为出错而直接给出 500 页面)通常是使用异常捕获,不让程序错误暴露给用户。例如发送 HTTP 请求,接口不可用时会出现程序报错,这是就需要抛出异常。
require "net/http"Net::HTTP.new("localhost", 9999).request(Net::HTTP::Get.new("/"))
如果你把上面的代码在 irb 里面运行,会得到下面的报错结果(假设本地没有绑定在端口号为 999 的web 服务)
Errno::ECONNREFUSED: Connection refused - connect(2) for "localhost" port 9999
这个错误导致程序运行中断并退出。如果你把代码拷贝到文件中并使用 ruby 命令执行,然后再使用 echo $? 命令查看执行结果,你就会看到执行结果为 1(失败)
$ ruby resilience.rb ... [snip several lines of error and backtrace] ...$ echo $?1
为了处理这个错误,Ruby 提供了 begin/rescue
require "net/http"begin Net::HTTP.new("localhost", 9999).request(Net::HTTP::Get.new("/")) puts "Succeeded"rescue puts "Failed"end# Outputs: Failed
同样,放在一个 .rb 文件中执行,并查看执行状态,你就会看到返回 0 (成功)
$ ruby resilience.rb Failed $ echo $?0
但是,这样简单地异常捕获处理也带来了缺陷。就是一些程序逻辑错误也会被捕获而不被发现,例如“参数错误”这样的错误应该被抛出来,以便修复 Bug 。例如下面的例子中,request 方法没有传递正确的参数,ruby 依然正确执行而并没有抛出异常。
require "net/http"begin Net::HTTP.new("localhost", 9999).request() puts "Succeeded"rescue puts "Failed"end# Outputs: Failed
If you execute this, your script indeed says that it failed and exits with success.
用 Ruby 命令执行结果输出 failed
并成功地退出(echo $? 输出结果 0)
$ ruby resilience.rbFailed$ echo $?0
在这个例子中 rescue 刚好屏蔽了 ArgumentError
错误。由于开发者编写代码错误,但在报错信息中,request 方法没有传参的错误没有得到反馈,无法快速定位出错原因。
在开发时,编写代码错误这样的低级不应该被捕获,尽可能地捕获具体的异常。
require "net/http"begin Net::HTTP.new("localhost", 9999).request(Net::HTTP::Get.new("/")) puts "Succeeded"rescue Errno::ECONNREFUSED puts "Failed"end# Outputs: Failed
ruby resilience.rb ,输出 Failed 并返回成功状态。
$ ruby resilience.rb Failed $ echo $?0
下面的例子中,request 方法参数错误,由于我们捕捉了具体的 Errno::ECONNREFUSED
异常,所以如果 request 参数错误不会被捕获,运行代码时会报错。
require "net/http"begin Net::HTTP.new("localhost", 9999).request() puts "Succeeded"rescue Errno::ECONNREFUSED puts "Failed"end
results in:
$ ruby resilience.rb /Users/jnunemaker/.rbenv/versions/2.2.5/lib/ruby/2.2.0/net/http.rb:1373:in `request': wrong number of arguments (0 for 1..2) (ArgumentError) from resilience.rb:3:in `<main>'$ echo $? 1
异常捕获的封装和改进
尽管 Ruby 提供了 begin
和 rescue
捕获异常,但是如果不封装的话,会导致很多不优雅的垃圾代码。
依然是举例子,一个捕获 HTTP 连接异常的请求,HTTP 请求连接成功返回 JSON
require "json"require "net/http"class Client # Returns Hash of notifications data for successful response. # Returns nil if notification data cannot be retrieved. def notifications begin request = Net::HTTP::Get.new("/") http = Net::HTTP.new("localhost", 9999) response = http.request(request) JSON.parse(response.body) rescue Errno::ECONNREFUSED # what should we return here??? end endendclient = Client.new p client.notifications
上面的代码初步封装了一个 notifications 方法,但我们输出为json 数据格式,即使 Errno::ECONNREFUSED 异常出现。
继续改造一下,添加了个 NotificationsResponse
类。
require "json"require "net/http"class Client class NotificationsResponse attr_reader :notifications, :error def initialize(&block) @error = false @notifications = begin yield rescue Errno::ECONNREFUSED => error @error = error {status: 400, message: @error.to_s} # sensible default end end def ok? @error == false end end def notifications NotificationsResponse.new do request = Net::HTTP::Get.new("/") http = Net::HTTP.new("localhost", 9999) http_response = http.request(request) JSON.parse(http_response.body) end endendclient = Client.new response = client.notificationsif response.ok? # Do something with notifications like show them as a list...else # Communicate that things went wrong to the caller or user.end
通过 response.ok?
方法的返回,我们可以在 if else 写相应的业务逻辑。
为了防止调用 Client
类的 notifications
前没有调用 NotificationsResponse
的 ok?
方法, 继续改造一下。
require "json"require "net/http"class Client class NotificationsResponse attr_reader :error def initialize(&block) @error = false @notifications = begin yield rescue Errno::ECONNREFUSED => error @error = error {} # sensible default end end def ok? @ok_predicate_checked = true @error == false end def notifications unless @ok_predicate_checked raise "ok? must be checked prior to accessing response data" end @notifications end end def notifications NotificationsResponse.new do request = Net::HTTP::Get.new("/") http = Net::HTTP.new("localhost", 9999) response = http.request(request) JSON.parse(response.body) end endendclient = Client.new response = client.notifications# response.notifications would raise error because ok? was not checked
如果调用 client.notifications 方法之前没有执行 client.ok?
,
raise "ok? must be checked prior to accessing response data"
则会抛出异常。
把代码放在一个文件中,并在 irb 中 require 进来测试结果如下:
2.3.0 :001 > require_relative "./test.rb" => true2.3.0 :002 > client = Client.new => #<Client:0x007fabcb241430>2.3.0 :003 > response = client.notifications => #<Client::NotificationsResponse:0x007fabcb239550 @error=#<Errno::ECONNREFUSED: Failed to open TCP connection to localhost:3000 (Connection refused - connect(2) for "localhost" port 3000)>, @notifications={:code=>400, :message=>#<Errno::ECONNREFUSED: Failed to open TCP connection to localhost:3000 (Connection refused - connect(2) for "localhost" port 3000)>}>2.3.0 :004 > response.notificationsRuntimeError: ok? must be checked prior to accessing response data from /Users/hww/test.rb:25:in `notifications' from (irb):4 from /Users/hww/.rvm/rubies/ruby-2.3.0/bin/irb:11:in `<main>' 2.3.0 :005 > response.instance_variable_get(:@ok_predicate_checked) => nil 2.3.0 :006 > response.ok? => false 2.3.0 :007 > response.instance_variable_get(:@ok_predicate_checked) => true 2.3.0 :008 > response.notifications => {:code=>400, :message=>#<Errno::ECONNREFUSED: Failed to open TCP connection to localhost:3000 (Connection refused - connect(2) for "localhost" port 3000)>}
作者:黄文威
链接:https://www.jianshu.com/p/7de1c670077a