VOOZH about

URL: https://qiita.com/tasaki-i3/items/6e90c8f037c35375c1c3

⇱ Railsアプリの例外構造パターン #ツンデレ - Qiita


👁 Image
30

Go to list of users who liked

36

Share on X(Twitter)

Share on Facebook

Add to Hatena Bookmark

More than 5 years have passed since last update.

@tasaki-i3(大輔 田崎)

Railsアプリの例外構造パターン

30
Posted at

はじめに

Railsアプリケーションにおけるエラー処理(例外設計)の考え方 - Qiita ← こちらで@jnchitoさんに例外設計について言いたいことはほとんど言われてしまった感が強いのですが、もうちょっとだけこうしたら使いやすいだろうな〜という例外構造パターンについて紹介します。

パターンにはめることで以下のメリットがあるかと思います。

  • 実装者の意図が表現しやすい
  • レビューがしやすい
  • 致命的なエラーを検知しやすい
  • 無秩序に例外クラスが氾濫しにくい

Fatal-Major-Minorエラーパターン

冒頭のリンク記事ではエラーを「システムエラー」と「業務エラー」の2種類に分類していました。ここでは更に細分化して3種類に分類します。

  • システムエラー
    • FatalError
      • システム稼働を継続できない致命的なエラー
  • 業務エラー
    • MajorError
      • リクエスト処理を継続できない重大なエラー
    • MinorError
      • リクエスト処理を継続できる軽微なエラー

👁 例外構造パターン.png

Fatalエラー

業務処理として想定していない事象が発生した場合にFatalエラーを利用します。仮に、本番環境でこのエラーが発生した場合には実行環境やプログラムに何かしらの障害や不具合があると考えられるためいち早く検知して対処する必要があります。(検知の仕方は後述)

Fatalエラーがもし発生した場合は、HTTPレスポンスとしては「500 Internal Server Error」や「503 Service Unavailable」など500番台のステータスコードを返すようにします。

module FatalError
 class Base < StandardError
 def http_status
 :internal_server_error
 end
 end
end

例: インターフェースメソッド

基底クラスでインターフェースメソッドを用意しておくことで、このクラスはどんな機能があるのか何をする役割を持っているのかを宣言することがよくあります。そんなとき、Rubyにはいわゆるインターフェースメソッドと呼ばれるものがありません。そこで代わりに、コールされたら必ず例外を発生させるメソッドを定義することで表現できます。

このときあげる例外にFatalエラーを用います。意味合いとしては「このクラスを継承して具象クラスを作るときには絶対にこのメソッドを実装しなさいよ! さもないと落としちゃうんだからね!!」みたいな、後の実装者へのメッセージとなります。

module FatalError
 class MustBeOrverriden < Base
 end
end

class Device < ActiveRecord::Base
 def device_name
 # インターフェースメソッド。Deviceクラスを継承した具象クラスでは必ずオーバーライドすること
 raise FatalError::MustBeOrverriden
 end
end

class Iphone < Device
 def device_name
 self.device_attributes.name
 end
end

class Android < Device
 # device_nameメソッドの定義漏れ
end

Device.each do |dev|
 puts dev.device_name # FatalError::MustBeOrverriden発生!
end

例: 未知の値

プログラムとして予め想定された値とは違うものが指定された場合にあげる例外にFatalエラーを用います。意味合いとしては「ちょっとそんな値入らないわよ。顔洗って出直して来なさい!」みたいな。

elsif句を使った場合とか、case文では必ずelse句をつけなさいという先人の教え(MISRA-C 15.7とか)がありますが、そのようなときにこの例外を仕込んでおくとよいかと思います。

module FatalError
 class InvalidArgument < Base
 end
end

class Device
 VALID_STATUSES = {
 :deleted => 0,
 :activated => 1,
 }

 def status=(new_status)
 if new_status.is_a?(String)
 super VALID_STATUSES[new_status.underscore.to_sym]
 elsif new_status.is_a?(Symbol)
 super VALID_STATUSES[new_status]
 else
 # 想定外の値が指定されたので例外にする
 raise FatalError::InvalidArgument
 end
 end
end

Majorエラー

ひとつのリクエストを処理している中でユーザーから(プログラムとしては想定した範囲内で)異常な値が指定されており、そのリクエストを処理できない場合にMajorエラーを利用します。

Majorエラーが発生した場合は、HTTPレスポンスとしては「400 Bad Request」や「401 Unauthorized」などの400番台のステータスコードを返すようにします。

module MajorError
 class Base < StandardError
 def http_status
 :bad_request
 end
 end
end

例: 認証エラー

リクエストを処理できないケースには例えば認証エラーが挙げられます。リクエストされても認証NGであれば処理するわけにはいきませんので、そこでリクエスト処理を中止してエラーのレスポンスを返すような場合にMajorエラーを用います。意味合いとしては「勝手にアクセスして来ないでよ! 情報教えてあげたりなんかしないんだから!!」みたいな。

module MajorError
 class Unauthorized < Base
 def http_status
 :unauthorized
 end
 end
end

class ApplicationController < ActionController::Base
 rescue_from MajorError::Base do |err|
 response.status = err.http_status
 ....
 end
end

class DevicesController < ApplicationController
 before_filter :authenticated?
 
 private
 
 def authenticated?
 if current_user.auth_token != request.env['HTTP_AUTHORIZATION']
 raise MajorError::Unauthorized
 end
 end
end

Minorエラー

モデルのロジック内などの限られた範囲内でロジックを簡素にする目的で意図して発生させる例外にMinorエラーを用います。この種の例外を発生させた場合は必ず近くでキャッチするようにコーディングルールを決めておくと、raiseしたけどrescueしていないような場合にコードレビュー時に発見しやすくなります。意味合いとしては「ばかぁ。こ、これはわたしのための例外なんだから、あなたのために作ったわけじゃないんだからね!」みたいな。

うん。デレた。

なお、例外処理は一般的に言って処理コストが高いです。単純な条件分岐で代用可能な場合はカッコつけて例外を使わずに地道にif文を書きましょう。

module MinorError
 class Base < StandardError
 def http_status
 :internal_server_error
 end
 end
end

仮にキャッチし忘れてコントローラーのアクションの外まで流出した場合はプログラムエラーですので、Fatalエラーと同様にHTTPレスポンスとしては「500 Internal Server Error」を返すようにします。

例: xxx

ここまで書いておいてなんですが、例が思いつかん (-"-;

Fatalエラーの検知

Fatalエラーは原則として発生してはいけないエラーとして定義しました。なので、本番環境で発生した場合はいち早く知りたい。開発環境であったとしても早めに知ることができれば工程の後戻りが少なくてすみます。

以下ではいくつかの検知方法の例を挙げます。システムや開発工程に合わせて手段を選ぶとよいと思います。

500番台ステータスチェックしてアラート送信

Fatalエラーが発生した場合は、HTTPステータスとして500番台を返すようにしてありますので、apacheやnginxのログやAWSのELBを利用していればCloudWatchで確認することが可能です。なのでRailsアプリには特に仕組みをいれずとも、ログを逐次チェックして500番台のステータスコードがあればアラートをあげるようにします。

参考: CloudWatch Logsを使って500系のレスポンスを検知する方法 | 遍歴プログラマ日記

例外キャッチ時にアラート送信

Rails内でエラー検知&アラート送信をしてしまうなら、コントローラーで例外キャッチしたときの処理の一部として実施してしまうとよいでしょう。

class ApplicationController < ActionController::Base
 rescue_from MinorError::Base, with: :render_internal_server_error
 rescue_from FatalError::Base, with: :render_internal_server_error

 def render_internal_server_error(err)
 response.status = err.http_status
 render xxx

 # ここでアラートメール送信
 # 本番ならシステム管理者宛て
 # 開発環境ならプロジェクトリーダー宛てなど
 end
end

ただ、リクエストの度にメール送信されるのはちょっとやり過ぎな気もしますし、プログラム異常が発生しているのにきちんとアラート送信までできるかどうかちょっと不安が残ります...

ログから抽出してアラート送信

例外キャッチした際にログにキーワードを出力しておきます。キーワードは単純に「*****」とかでもOKです。そして、ログファイルをローテートする直前にログ全体をgrep掛けてキーワードを抽出して、ヒットした場合はアラート送信させます。

即時性を求めていない場合は推奨の方法です。
grepを使いますので、先のキーワードに限らずnilClass参照してNoMethodError例外が発生した場合とかも検知可能ですし、コントローラーでキャッチできないDelayedJobによるバックグラウンド実行時のエラーでも検知可能です。

ログローテート直前に実行するスクリプトは下記のようなもので実現できるかと思います。

APP_NAMES="panel api bg"
STAGE="DEV"
SEND_TO="dev-leader@example.com"
REGEXP="*****|undefined method"


for app_name in $APP_NAMES; do

 egrep -n -B 1 -A 10 "$REGEXP" /path/to/rails/root/$app_name/current/log/*.log > /tmp/fatal_errors_in_$app_name.txt
 if [ $? == 0 ]; then
 cat << _BODY_ > /tmp/fatal_erorrs_mail_body.txt
$STAGE のログに重要なプログラムエラーを検知しました。
 検知対象正規表現: $REGEXP$(cat /tmp/fatal_errors_in_$app_name.txt)_BODY_
 cat /tmp/fatal_erorrs_mail_body.txt | mail -s "[Warning] Fatal errors detected in $STAGE$app_name" $SEND_TO

 fi
done

おわりに

@jnchitoさんの記事に乗っかっただけのような気がハンパないですが、例外構造のパターンについて紹介しました。

冒頭にメリットとして記述しましたが、主に実装者の意図を込めるため、あるいはレビューがしやすくなるためのパターンかと思います。実装者の意図に反して例外があっちゃこっちゃしてしまわないように整理しましょう!

30

Go to list of users who liked

36
0

Go to list of comments

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30

Go to list of users who liked

36