Ruby異常処理テクニック
7856 ワード
ユーザーが通知を読んでいないサイト内のメッセージのように、サイトにアクセスするたびにクエリーしなければならない操作で、データベースのクエリー接続異常やネットワーク接続エラーが発生すると、プログラムは500エラーを報告し、ユーザーは正常にサイトにアクセスできません.通常、この動作は、成功するかどうかにかかわらず、例外的にキャプチャされます.この記事はResilience in Ruby:Handling Failureのまとめを参考にして,著者らはGitHubユーザがデータバンクに通知する実際のシーンに基づいて書いた.
基本的なエラー処理
1つのロード・アイテムのエラーでWebサイト全体が使用できないため、例外処理が必要な場合(一般的なシーンでは、エラーのために500ページを直接与えることはできません)は、通常、プログラム・エラーがユーザーに露出しないように例外キャプチャを使用します.例えばHTTPリクエストを送信すると,インタフェースが利用できない場合にプログラムエラーが発生し,例外を投げ出す必要がある.
上記のコードをirbで実行すると、次のエラー結果が得られます(ポート番号999のウェブサービスにローカルにバインドされていないと仮定します).
このエラーにより、プログラムの実行が中断され、終了します.コードをファイルにコピーしてrubyコマンドを使用して実行し、echo$を使用しますか?コマンド実行結果を表示すると、実行結果が1(失敗)になります.
このエラーを処理するためにRubyは
同じように、一つに置きます.rbファイルで実行し、実行ステータスを表示すると、0(成功)が返されます.
しかし,このような単純な異常捕獲処理も欠陥をもたらした.いくつかのプログラム論理エラーも検出されずにキャプチャされ、例えば「パラメータエラー」のようなエラーはBugを修復するために投げ出されるべきである.例えば、次の例では、requestメソッドは正しいパラメータを渡さず、rubyは依然として正しく実行され、異常は投げ出されなかった.
If you execute this, your script indeed says that it failed and exits with success. 結果出力
この例ではrescueは
ruby resilience.rbは、Failedを出力し、成功状態に戻る.
次の例では、requestメソッドパラメータエラーは、特定の
results in:
異常キャプチャのパッケージングと改善
Rubyは
上のコードはnotificationsメソッドを初歩的にカプセル化したが,Errno::ECONNREFUSED異常が発生してもjsonデータフォーマットとして出力した.改造を続け、
クライアントを呼び出すとnotificationsメソッドは
異常が放出されます.コードを1つのファイルに配置し、irbにrequireを入力してテスト結果は以下の通りです.
基本的なエラー処理
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は
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
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="">}