ライブラリのコンテクストを越えた例外の送出はやめよう

  • 2
    Like
  • 0
    Comment

この記事は Sansan アドベントカレンダー ネタ枠のHaiToが去年から引き続きお送りしています。

Introduction

去年の http://qiita.com/HaiTo/items/af0836723ba1ec45de3eIntroduction

fogが唐突にExcon::Errorを返してくるファッキンな部分とかと書いても

こんなことを書いていて、まぁつまりこの記事はこの話です。

Fog?

https://github.com/fog/fog
fog is the Ruby cloud services library, top to bottom:
Fog はクラウドサービスをがばっとラップしていい感じに共通化されている状態で扱える便利ライブラリ1です。
AWSやAzureはもちろん、Sakura cloud やIBMのBluemixとかいろいろななクラウドサービスに対して殆ど共通のインターフェースを提供してくれ、いい感じに扱えます。

大切なことなので二度

例えば使い方として、

# 出典: http://fog.io/
storage = Fog::Storage.new({
  :provider => 'AWS',
  :aws_access_key_id => ACCESS_KEY_ID,
  :aws_secret_access_key => SECRET_ACCESS_KEY
})

storage.get(file_name)
# これでS3からObjectが取れる

ね。簡単でしょ? AWS-SDKが2012年頃はこなれていなかったのかは歴史的経緯の海の飲まれ不明ですが、Sansanの名刺データ化システムの中の画像をとったりする部分はこのFogをさらに僕達がラップして使っています。

何が良くないの?

冒頭でも書きましたが、予期せぬエラーが発生します。

storage.get(file_name)
# raise Excon::Error::InternalServerError 
#       message: Expected(200) <=> Actual(500 InternalServerError)

Excon のエラーがraiseされてきます。
Excon とは https://github.com/excon/excon こちらで、まぁつまりRubyのHTTPクライアントの一つです。
つまり、クラウドサービスをいい感じにラップしてくれるライブラリですが、エラーはそのコンテクストでラップしてくれるわけではなく、HTTPクライアントのエラーをそのまま raise する仕組みになっています。

で、それで何が困るの?

たとえば以下のようなコードになるわけです。

def storage
  @storage ||= Fog::Storage.new({
    :provider => 'AWS',
    :aws_access_key_id => ACCESS_KEY_ID,
    :aws_secret_access_key => SECRET_ACCESS_KEY
  })
end

def image_temporary_url(id, expire: 30.seconds.since.to_i)
  storage.get_object_https_url('awesome-bucket-name', id, expire)
rescue Excon::Error::InternalServerError => e
  retry # 実際には無限Retryになるので良くないのでretriable等を使う。
end

突如として出現する Excon の文字列! 特に Excon を直接使っていないと、このExconが一体何者なのかを考える所から始まります。2
そして、バックトレースを漁っていくとFogの先にその実体が居ることが分かるわけです。
後からコードをさわった人も、「このExconってなんだろう? Gemfileにも書かれてないしなぁ……」となるわけです。
Gemfile.lockを見たり、@storage を見てFogの中を見て……と考古学的な時間が掛かります。

じゃあどうしたいの?

素直にFog::Errorみたいな例外をraiseしてください お願いします何でもはしません。
つまり、以下のようなコードになります。

def storage
  @storage ||= Fog::Storage.new({
    :provider => 'AWS',
    :aws_access_key_id => ACCESS_KEY_ID,
    :aws_secret_access_key => SECRET_ACCESS_KEY
  })
end

def image_temporary_url(id, expire: 30.seconds.since.to_i)
  storage.get_object_https_url('awesome-bucket-name', id, expire)
rescue Fog::Error::InternalServerError
  retry
end

こうするだけで、「あぁFogのエラーなんだな」という事が一瞬で理解出来るようになりますね。
深追いする必要もなく、Fogを使うのを辞めたら素直にこのロジックも消す(あるいはAWS::Errorみたいなのに置き換わる)ことが出来ます。

良い世界ですね。

じゃあコントリビューションしろよ

Fogの場合はライブラリがでかく、また、ライブラリの特性上影響範囲が広く読みきれない(読めば良いんですが)事が目に見えてます。

でも実は取り掛かったら簡単なのかも。

あと英語が苦手なので、ちょっとこれだけのやりとりを頑張るモチベーションも無いです。

Fog捨てたいし

結論

何かライブラリを作ったりした時に、別のGemを使って例外を発生する箇所もあると思いますが、
そのライブラリのコンテクストに応じた例外に置き換えて、改めてraiseするようにする と、皆ハッピーになるかなぁと思います。


今年はモンハンではなくGジェネを楽しんでます


  1. 便利とは言っていない 

  2. rescueはわかりやすさのために入れています