Hard work by INTERNET

ベンチャーで働くひとりぼっちWEB開発者が頑張るブログ

ソフトウェアでの業務例外とシステム例外

「例外」という概念について調べていたので理解したことを書いてみる。

プログラミング言語には例外という仕組みがあって、コールスタックを無視して特定の場所にジャンプすることができる。いろんな場所で例外を使ってしまうとプログラムの流れを追うことが困難になってしまうため、例外を使う場所は異常系に限るとよい、とされている。

例外を使う場所を異常系に絞ることでルーチンは正常系だけ記述するだけで済むためシンプルになるということだ。

異常系にのみ適用する、という話なのだけど何が「異常系なのか」を考える必要がある。 異常系といえば、「データベースへの接続エラー」と「ユーザの入力不備」のような2種類に分けることができる。 前者はシステム例外や技術的例外、後者は業務例外と喚ばれている。

業務例外も異常系の一種なのだが、ユーザからアクションによって発生することが多く、発生した事象をユーザにフィードバックしなければならないため、異常系ではあるが、例外としては扱ってはいけない。

システム例外は、ユーザが原因でははいのでフィードバックしても何も意味がない。開発者にエラーを通知するべきなので例外を補足せずにシステムエラーとして投げるべきだ。システムエラーとして投げれば開発者に通知することができるだろう。

まとめ

「例外」を使う場所はシステムの異常系に限ろう。

名前付け大全を読んだ

間違った名前のパターン

  • (1) 名前と実装が一致していない
    • 実装はdestroyしているのに名前はcreateしている
  • (2) 名前の意味が狭すぎる
    • 1のパターンが包括していますが
    • saveという名前なのに、実装ではキャッシュの削除をしている、など
  • (3) 名前の意味が広すぎる
    • saveRecordAndClearCacheという名前なのにキャッシュを削除していない
    • getPageというWEB上のファイルを取得する処理があるとします。getPageだとローカルファイルからなのか、データストアからなのかわかりません。HTTPが確定しているならdownloadPageにしましょう。

命名に関するヒント

  • メソッド名は動詞にするべきか
    • メソッドには「何らかの値を返すことが主目的のもの」「何らかの処理を行い副作用を起こすことが主目的のもの」に分けられるので、メソッドは動詞である必要はない
  • 必要を表現する時
    • 対象が動詞の時はshould
    • 対象が名詞の時はneed

ルフレビュー項目

  • 名前と実装はあっているか?
  • 名前で挙動をカバーできているか?
  • 曖昧な単語を選んでいないか?
    • info
      • 曖昧なデータ構造を指す時に多いので適切な名前をつけるべし
    • check
      • 実行後に何が起きるのかを予測できない。raiseするならraiseを、boolを返すならboolを名前で表現するべし
        • check_paidだったら、boolを返すならpaid?, 支払いが済んでいなくてエラー情報を追加するなら add_error_if_not_paid, 支払いが済んでいない時にエラーを投げるなら raise_if_not_paid
      • chekは言葉が不明瞭であることが問題の根本。結果が曖昧な言葉は避けよう
  • 重要かつ汎用的な単語を使っていないか?
    • group, system...
    • 使うならネームスペースをつけるべし
  • 改修によって現行の名前の意味が変わっていないか?
  • 一言で表現できないか?
    • previuos_value => predecessor
  • 情報量のない言葉を足していないか?
  • 無駄な言い換えをしていないか?
    • text = Article.find(params[:id]).body ではなく、bodyでいいじゃん
  • 対になった言葉を選んでいるか?
    • get, set
    • deep, shallow
    • head, tail
  • スペルミスをしていないか?
  • 過剰な省略をしていないか?
  • 単数形と複数形は間違っていないか?

ActiveSupport::Cacheの豆知識

はじめに

ActiveSupport::Cache は、Rails.cache.read, Rails.cache.writeと書くことでフラグメントキャッシュを扱うクラスです。 さきほど、このクラスのソースコードを読んだので知見を紹介します。(rails4.2時点)

Rails.cache.fetchにブロックを渡すとミスキャッシュした時に書き込んでくれる

これは公式ドキュメントに書いている通り有名だと思います。

      #   cache = ActiveSupport::Cache::MemCacheStore.new
      #   cache.fetch("foo", force: true, raw: true) do
      #     :bar
      #   end

エントリがなかったら save_block_result_to_cache というメソッドが呼ばれていました。

複数エントリを一括に読み書きするメソッドがある

3.times do |i|
  Rails.cache.write("a#{i}", "はい#{i}")
end

という3回ストアへ書き込むコードがありますが、これを1度のアクセスだけで行うメソッドが存在しています。

Rails.cache.write_multi, Rails.cache.read_multiです。 Rails.cache.write_multi(3.times.map {|i| { "a#{i}" => "はい#{i}" } })な感じです。

このインターフェースはAPIドキュメントにも記載されている通り、スーパークラスで定義していますが、本当に1度だけ書き込むのかはキャッシュストアに依存した実装になっています。 RedisCacheStoreでは、ちゃんと1度だけ書き込むようになっていました。

# Writes multiple entries to the cache implementation. Subclasses MAY
# implement this method.
def write_multi_entries
...

キャッシュストアのクラス構成は継承を使っている。

ActiveSupport::Cacheをスーパークラスとして、各キャッシュストアはサブクラスとして定義されています。 これは、共通のIFを持たせたかったためでしょう。 raise NotImplementedErrorというメソッドがスーパークラスの随所で散見されます。

キャッシュストア内からストアの操作には instrumentというラッパーメソッドを使わなければならない

      def write(name, value, options = nil)
        options = merged_options(options)

        instrument(:write, name, options) do
          entry = Entry.new(value, **options.merge(version: normalize_version(name, options)))
          write_entry(normalize_key(name, options), entry, **options)
        end
      end

instrumentというラッパーの中には ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload) { yield(payload) } がコールされていました。 なるほど〜これは、Notificationのsubscriberに通知をするためだったんですね。

instrumentを使わずに操作をしている場合は、subscriberへの通知もれが起きるでしょう。

むすび

そこまで突っ込んで読んではいませんが、全体の構成がわかってきたように思います。

おわり

開発用に使うRailsのキャッシュストラテジーはどれを使ったほうがよいか

activesupportのcache.rbをサラッと読んでわかったことをまとめます。

本文

本番環境だとRedisとかmemcachedにキャッシュを載せていると思いますが、開発環境だと正直何でもいいわけです。 本番で使っているストアと揃えたほうが、地雷を踏まなくても済む可能性があるので、本番と開発で使うキャッシュストアは揃えたほうがいいでしょう。

ところで、ミドルウェアのインストールが不要で今すぐ使える開発用キャッシュストラテジーは3つあります。

  • FileStore
  • LocalCache
  • NullStore

FileStore

ファイルに書き出します。tmp/cache配下に書き込まれるのを確認できると思います。たまにクリアしてあげないともしかしたらディスクを食い尽くすかも。 お手軽ですし、ミドルウェアなしでRedisとかmemcachedのように振る舞うのでよく使われがちです。 ファイルに書き出した中身はMarsharでシリアライズしたバイナリが格納されており、期限も一緒に入っています。 Railsのキャッシュには期限切れのエントリを削除するインターフェイスが提供されており、FileStoreで期限切れエントリ処理を実行すると、すべてのエントリーでデシリアライズをかけて日付を確認するため、IOをめちゃくちゃ食います。

ちなみに、Redisをキャッシュストアに使う場合には、期限切れエントリを削除するメソッドはありません。Redisが定期的にGCするようなのでする必要がないからですね。

LocalCache

これはインメモリキャッシュです。こいつはRackMiddlewareを使って実装されており、キャッシュは1リクエスト内のみ有効です。次のリクエストが来る時には消滅しています。 メモリ上のキャッシュなのでオーバーヘッドが少なく高速に動作するのが特徴です。開発環境だとデータ量が少なくてあまりボトルネックにならないとは思いますが。 また、リクエスト毎にクリアするので、キャッシュによるバグを踏むことはないでしょうが、本番でのみ発生する不具合が起きる可能性があります。

NullStore

キャッシュのインターフェイスを持ちますが、どこにも書き込みをしません。

まとめ

3つとも書き込み場所と挙動がすべて違います。開発環境でキャッシュ系の不具合が起きたら特定できるように把握しておきましょう。

記事タイトルへの回答ですが、デザイナーとかにわたす環境だったらNullStoreにしておいて、自分が使う環境だったらFileStoreを使います。

おわり

activeresourceで脆弱性が見つかった

activeresouceとは

Railsアプリケーション間の通信をRESTで行うためのラッパーライブラリのようだ。 自分はactiveresouceを使うようなソフトウェアの開発は経験無し。 https://rubygems.org/gems/activeresource

本文

間違っているかもしれないんだけど、バグレポートを見ている感じだと、finderメソッドの引数に任意の文字列を入力することで、ディレクトリトラバーサルが引き起こす脆弱性だ。 しかし、この脆弱性を使った攻撃が成立するには、アプリケーションがfinderメソッドの引数にパラメータをそのまま渡すような実装をしている必要がある。 そんな人はいないしょ、、、、と思うけど、ActiveRecordチックに使えるライブラリのようなので、params[:id]をそのまま渡すという実装をよくやっているんだろうと思う。 これはかなり大きな穴だ。

この脆弱性を持つバージョンは前バージョンとのこと。修正版は5.1.1に出ている。 使っている人はアップデートを急げ!

https://groups.google.com/forum/#!topic/ruby-security-ann/r0TL_YzNSR0

以上。

rubyweekly#483

rubyweekly.com

Ruby 2.7 Commentary from Two Ruby Core Team Members

クックパッドのフルタイムで働くrubyコミッター2人による解説記事を英訳した記事。英語の鍛錬のついで読みたい。 この英訳記事を投稿した人が CTO Cookpad Ltdらしい。CTOはパンダの人では?と思ったけど海外の子会社なのかな。

sourcediving.com

The Ruby Reference (Now Updated for Ruby 2.7)

ruby2.7のドキュメント。https://github.com/rubyreferences/rubyref がソースになってる。 2.7のことしか書いていない!

How Well Do You REALLY Know Ruby's Exception System?

例外の実装について書かれた本。無料って書いてあったのでダウンロードしてみた。 ダウンロードには名前とメールアドレスの入力が必要でメールにダウンロードURLがついている。ただし、メーリングリストへの登録される。 全ページ43枚の英語で書かれてた。

Ruby 2.7.0's Rails Ruby Bench Speed Unchanged from 2.6

2.6から2.7へはパフォーマンスは変わらなかったよ。安定している。という記事。

Ruby 2.7 Reverts The Deprecation of The 'Flip-Flop' Operator

2.6から非推奨になったflip-flop 構文を2.7では非推奨を扱いをやめたよ、という記事。 warning: flip-flop is deprecatedがでなくなった。

誰も使っていないから消そうぜって書いたら実は使われていた、というのが取りやめの理由みたい。 Feature #5400: Remove flip-flops in 2.0 - Ruby master - Ruby Issue Tracking System 僕もflip-flop構文を知りませんでした。

Jobs

Software Engineer, Full-Stack (Ruby/Rails/JS)

ヘルスケア系会社の求人って書いていた。フルスタックエンジニアを求めているらしい。 サンフランシスコかリモートワーク。 海外の求人おもしろい。

Senior Rails Developer - Amsterdam (Netherlands)

これも求人。アムステルダム。バックエンドがメインでフロントエンドの知識もあるといいかもって。 競争力のある給与と無期限の有給休暇、つよい。

Find a Job Through Vettery

独自のマッチングアルゴリズムを使って企業と労働者を直接繋げる求人サイト。 NETFLIXの求人もありそう。

Articles & Tutorials

On Ruby 2.7 Pattern Matching After 10 Months of Elixir

エリクサーのパターンマッチングとRuby2.7から入ったパターンマッチングを比べる、という記事。 結論だけ読んでみたけど、そこそこ好評っぽい。

More Fiber Benchmarking

Fiberのパフォーマンスを調べたって記事。

直接Fiberってそんなに使うことってある??? 僕は並列処理したいと思ったらparallel(gem)使っている。。。

How to Migrate Large Database Tables Without Headaches

巨大テーブルをmigrateする方法が書いてる。 alter tableを使うと内容によっては書き込みロックするから、トリガーを使って新しく挿入する行は新しいテーブルにも挿入する。 既存データはバルクインサートする感じのスクリプトが書いている。 で、行のコピーが完全に終わったら一瞬ダウンした後にテーブルを入れ替える、みたいな感じっぽい。 ペルコナツールやんけ

Extracting a Tidy PORO From a Messy Active Record Model

リファクタリングの話。ActiveRecordを継承したクラスにベタッとメソッドを定義するんじゃなくて、Plain Old Ruby Object(PORO) に切り出そうぜって記事。

書いていることは下記とだいたい似ていると思う。

tech.medpeer.co.jp

Why Rails' default_scope Bad?

どうしてdefault_scopeは悪いのか、というRailsエンジニア誰が同意するであろう事実を言語化していう記事。 僕はアップグレードでいつも挙動が変わっていて近寄りたくない、みたいな印象でした。 この記事で書いている本当にそうで、検索条件が競合するので、associationを使えなくてバグの温床になってました。

Inheritance and Abstract Class Pattern for Rails Controllers

controllerで継承を使ってシンプルにしよう、という記事。 管理画面とかであたりまえに使っているので、はい、という感じがした。

Five Rails Performance Tips for Performance 'Noobs'

パフォーマンス改善の話。ほとんどの原因がデータベースだよねぇ、、、。

An Introduction to Ruby's 'Fibers'

Filberの解説記事。Theadとの対比している。 記事にも触れらているけどアプリケーションでは使わないライブラリだよって書いてあって「わかる」ってなった。

Code and Tools

ActiveRecord Adapter for Amazon Aurora Serverless

名前の通り、サーバレスAurora用のActiveRecord。 mysql2は不要。utf8mb4使える。サーバレスAuroraはMySQL5.6互換のみなんだよな。 今はPostgeSQLのAuroraには未対応らしい。 今後の動向に注目。

jetsというLambdaで動くrailsライクフレームがあるんだけど、それとは繋がるのかねぇ。

Forme: HTML Form Generation Library

他のライブラリに依存していないフォームビルダー。

Rack::Cache: Drop-in HTTP Caching for Ruby Webapps

rack層でキャッシュできるgemらしい。

CDN使っていれば不要なやつか?いまいちピンと来ていない。

Down: Streaming HTTP Downloads using net/http, http.rb or wget

ストリーミングでダウンロードするgem。 どんな感じで使うのかいまいち想像できないけど、フロントエンドにデータの断片を継続して送る時に使うのかな?

FString: Python-esque 'F-Strings' for Ruby

Pythonみたいな文字列展開ができるようになるgem。 内部でevalしているし、遅くなるしれないし、十分テストはしていないよってREADMEに書いてあった。

所感

多かったので大変でした。

ActiveRecordのorderメソッドで is null を使いたい

github.com

SQLのorder by句にActiveRecord経由で is null を使っていて、バージョンを6に上げたら警告が出てきた。よくわからん。

DEPRECATION WARNING: Dangerous query method (method whose arguments are used as raw SQL) called with non-attribute argument(s): "\"psets\".\"order\" is null, \"psets\".\"order\"". Non-attribute arguments will be disallowed in Rails 6.1. This method should not be called with user-provided values, such as request parameters or model attributes. Known-safe values can be passed by wrapping them in Arel.sql(). (called from show at /Users/martijn/dev/course-site2/app/controllers/user_controller.rb:26)

そもそも、この方法はStackOverflowにも書かれていた方法で間違っていないと思う。 is null を使えるようにQueryクラスを拡張してほしい。

みたいなこと書かれた。

これに対して、その警告は Arel.sqlでラップすると収まるぜ。つまり、 Pset.order(Arel.sql("order IS NULL"), :order)`って書けばいい。というコメントが書かれた。

レスポンスへのコメントに対して>>1 が、いま出ている警告がよくわからないのでプルリクを投げたぜ。これはクローズするぜ。

github.com

そのプルリクでは、non-attribute argumentというキーワードを raw SQL as argumentに置き換えようとしており、オープンしてから17日経過しているが、レビューされる気配はない。

おわり