nginxのlocationの優先順位を誤解していた

最近nginxを触っていて、locationの優先順位、主に^~の有無による前方一致の扱いについて誤解していたことに気づいた。

まず正解から。locationは以下の流れで決定される

  1. 完全一致チェック -> location = path {}
    • locationとURIが完全一致すれば、この設定で確定
  2. 前方一致チェック -> location path {} または location ^~ path {}
    • ^~ の有無や記述の順番は優先度に影響しない。最長一致するものが選ばれる
      • ^~ ありのlocationが選ばれた場合、この設定で確定
      • ^~ なしのlocationが選ばれた場合、保留して正規表現チェックに移る
  3. 正規表現チェック -> location ~* regexp または location ~ regexp {}
    • 設定ファイルに記述された順番でマッチングしていく
      • マッチするものがあれば、その設定で確定
      • マッチするものがなければ、保留していた前方一致の設定を使う

例えばこんな設定をしたとして

        location /hoge/ {
            return 403;
        }
        location ^~ /hoge/images/ {
            root /var/www/html;
        }
        location ^~ /fuga/ {
            return 403;
        }
        location /fuga/images/ {
            root /var/www/html;
        }
        location ~ \.jpg$ {
            root /data/;
        }

色々なリクエストを送ってみると、こんな結果になる

リクエスURI 前方一致の結果 結果
/hoge/images/a.jpg ^~ /hoge/images/ 前方一致の結果で確定し /var/www/html/hoge/images/a.jpg が返される
/hoge/images/b.png ^~ /hoge/images/ 前方一致の結果で確定し/var/www/html/hoge/images/b.png が返される
/hoge/a.jpg /hoge/ 前方一致の結果は保留され、正規表現にマッチするので /data/hoge/a.jpg が返される
/hoge/b.png /hoge/ 前方一致の結果は保留されるが、マッチする正規表現がないので前方一致の結果が使われて403エラーとなる
/fuga/images/a.jpg /fuga/images/ 前方一致の結果は保留され、正規表現にマッチするので /data/fuga/images/a.jpg が返される
/fuga/images/b.png /fuga/images/ 前方一致の結果は保留されるが、マッチする正規表現がないので /var/www/html/fuga/images/b.png が返される
/fuga/a.jpg ^~ /fuga/ 前方一致の結果で確定し、403エラー
/fuga/b.png ^~ /fuga/ 前方一致の結果で確定し、403エラー

誤解していたのはlocationのマッチング処理順が

  1. =の完全一致
  2. ^~ありの前方一致
  3. 正規表現
  4. ^~なしの前方一致

であると思い込んでいたこと。実際は2の段階で^~のあるものないもの両方がチェックされるので ^~ にマッチするlocationが存在しても、^~なしのlocationが最長一致で選ばれて正規表現マッチに移行する可能性がある。

自分は ^~ をあまり使わないので誤解していても実用上問題なかったのだが、これは非常によろしくない。公式のドキュメントはちゃんと読みましょう。

で、nginxのドキュメントを読むと・・・

nginx first checks locations defined using the prefix strings (prefix locations). Among them, the location with the longest matching prefix is selected and remembered.

最初に前方一致マッチで一番長くマッチするやつを選んで記憶する

Then regular expressions are checked,

次に正規表現をチェック

If the longest matching prefix location has the “^~” modifier then regular expressions are not checked.

最長マッチしたlocationが ^~ を持っていたら正規表現はチェックしない

using the “=” modifier it is possible to define an exact match of URI and location. If an exact match is found, the search terminates.

=を使うとlocationとURIの完全一致を定義できて、完全一致した場合は検索を終了する

Scalaのimplicitパラメータは何のための機能?

最近Scalaのコード読むことが増えたんですが、implicitパラメータのついた関数が多用されるとメッチャコード読みにくいんすよねアレ。このドワンゴのScala研修テキストにあるようなコード

この3つのメソッドは共通してConnection型を引数に取るのに、呼びだす度に明示的にConnectionオブジェクトを渡さなければならず面倒で仕方ありません。ここでimplicit parameterの出番です。

というコレね

def useDatabase1(....)(implicit conn: Connection)
def useDatabase2(....)(implicit conn: Connection)
def useDatabase3(....)(implicit conn: Connection)

たかだか変数名のタイプ数文字減らせるだけで何が嬉しいんすか?何の得があるんすか?って話っすよ。書く方は楽できていいかもしれないけど、こんなの読む方はたまったもんじゃない。

メソッド利用者側のコードから見た場合、引数のconnectionが明示されていれば、この関数の呼び出しでDBアクセスしているんだなって一目瞭然なわけじゃないすか。でも省略してしまったら、その情報が欠落するので、いちいち定義にジャンプして確認しなければDBアクセスしてるかどうかもわからないんです。スロークエリ出てるしSQLちょっと直そっかなとか思った時にストレスマッハになるやつですよコレ。一見DBなんて知りませんわーって顔して実はDBアクセスしてやがったって糞ビッチコードっすよ。ま、この例は useDatabase なんて名前付いてるんでビッチだってわかるけど、普通はそんなメソッド名つけないからね。

コードってのは書く時間より読まれる時間の方が長いものなのだから、多少早く書けたところで読むのを妨げるようなコードはトータルで見たらマイナスにしかならんのですよ。

しかしなー、言語デザイナーってのはだいたい超頭がいい人なわけで、そんな人が引数省略できますスゴイでしょwなーんて機能を入れるとは思えんのですよね。つまり俺がなんか根本的に勘違いしている可能性の方が高い。んで

とかツイートしたらScalaの強い人から教えていただけました。メッチャ参考になります、神か。

んー、やっぱ本来の趣旨から外れてんじゃんねコレ。スライド見るにimplicitパラメータの本来の目的は関数の多相性のため。ナンセンスな例だが意図するところはこんな感じのコードらしい。

trait Same[A] {
  def equals(x:A, y:A): Boolean
}

object Main {

  implicit val sameInt = new Same[Int] {
    def equals(x:Int, y:Int) = x == y
  }
  implicit val sameStr = new Same[String] {
    def equals(x:String, y:String) = x equals y
  }

  def main(args:Array[String]) = {
    Console.println(isSame(1, 2))
    Console.println(isSame("3", "3"))
  }

  def isSame[A](x:A, y:A)(implicit s:Same[A]) = {
    s.equals(x, y)
  }
}

これは可読性を損ねることはない。この場合コードを読み解く上で、どういう実装がなされているか、型ごとにどういう違いがあるか、スライドで言うところのhowなんて気にする必要ないからね。しかしDBアクセスするメソッドの場合、そのhowが大切なんすよ。DBアクセスしてるという事実そのものがコードの本筋であって読み解く上で重要なんすよね。それをimplicitで隠蔽されて読みやすいわけがない。

ちなみにscalaドキュメントのimplicit parameterの項でも多相性で説明している。いちいち引数かくの面倒だから省略なんてのは邪悪なimplicitではないかと思えてきたんだが、Scala界隈ではどういう位置付けになってるの?