Ruby異常処理テクニック

7856 ワード

ユーザーが通知を読んでいないサイト内のメッセージのように、サイトにアクセスするたびにクエリーしなければならない操作で、データベースのクエリー接続異常やネットワーク接続エラーが発生すると、プログラムは500エラーを報告し、ユーザーは正常にサイトにアクセスできません.通常、この動作は、成功するかどうかにかかわらず、例外的にキャプチャされます.この記事はResilience in Ruby:Handling Failureのまとめを参考にして,著者らはGitHubユーザがデータバンクに通知する実際のシーンに基づいて書いた.
基本的なエラー処理
1つのロード・アイテムのエラーでWebサイト全体が使用できないため、例外処理が必要な場合(一般的なシーンでは、エラーのために500ページを直接与えることはできません)は、通常、プログラム・エラーがユーザーに露出しないように例外キャプチャを使用します.例えばHTTPリクエストを送信すると,インタフェースが利用できない場合にプログラムエラーが発生し,例外を投げ出す必要がある.
require "net/http"
Net::HTTP.new("localhost", 9999).request(Net::HTTP::Get.new("/"))

上記のコードをirbで実行すると、次のエラー結果が得られます(ポート番号999のウェブサービスにローカルにバインドされていないと仮定します).
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. 結果出力failedをRubyコマンドで実行し、正常に終了しました(echo$?出力結果0)
$ ruby resilience.rb
Failed
$ 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 `
' $ echo $? 1

異常キャプチャのパッケージングと改善
Rubyはbeginrescueのキャプチャ異常を提供しているが、カプセル化しないと、優雅ではないゴミコードが多くなる.依然として例であり、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
  end
end

client = Client.new
p client.notifications

上のコードはnotificationsメソッドを初歩的にカプセル化したが,Errno::ECONNREFUSED異常が発生してもjsonデータフォーマットとして出力した.改造を続け、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
  end
end

client = Client.new
response = client.notifications

if 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
  end
end

client = Client.new
response = client.notifications
# response.notifications would raise error because ok? was not checked

クライアントを呼び出すとnotificationsメソッドはclient.ok?を実行する前に、
raise "ok? must be checked prior to accessing response data"

異常が放出されます.コードを1つのファイルに配置し、irbにrequireを入力してテスト結果は以下の通りです.
2.3.0 :001 > require_relative "./test.rb"
 => true
2.3.0 :002 > client = Client.new
 => #<0x007fabcb241430>
2.3.0 :003 > response = client.notifications
 => #<:notificationsresponse:0x007fabcb239550 failed="" to="" open="" tcp="" connection="" localhost:3000="" refused="" connect="" for="" port="">, @notifications={:code=>400, :message=>#<:econnrefused: failed="" to="" open="" tcp="" connection="" localhost:3000="" refused="" connect="" for="" port="">}>
2.3.0 :004 > response.notifications
RuntimeError: 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 `
' 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=>#<:econnrefused: failed="" to="" open="" tcp="" connection="" localhost:3000="" refused="" connect="" for="" port="">}