Goで始めるMiddleware

そもそもMiddlewareとは何かと言うと、私の認識ではアプリケーションの処理の前後で何らかの処理を行ったりするもの。例えば、ログを追加したりリクエストのAPIキーを認証したりする。以下の図がわかりやすい。

f:id:takayukinakata:20171124184420p:plain

Laravel 5.0 - Middleware (Filter-style) | MattStauffer.com より引用

とはいえ、上記だけでは具体的にどのように実装したらイメージがつかなかったんだけど、Goの公式のwikiでいろいろ紹介していてわかりやすかったのでこれを元に簡単なサンプルを実装した。

LearnServerProgramming · golang/go Wiki · GitHub

Middlewares in Go: Best practices and examples – Nicolas Merouze

package main

import (
    "fmt"
    "net/http"
    "time"
)

// indexHandler ...
func indexHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Println("Hello, middleware!")
}

// aboutHandler ...
func aboutHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Println("This is midlleware test!!")
}

// loggingHandler ...
func loggingHandler(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        t1 := time.Now()
        next.ServeHTTP(w, r)
        t2 := time.Now()
        fmt.Printf("[%s] %q %v\n", r.Method, r.URL.String(), t2.Sub(t1))
    }
}

func main() {
    http.HandleFunc("/", loggingHandler(indexHandler))
    http.HandleFunc("/about", loggingHandler(aboutHandler))
    http.ListenAndServe(":8888", nil)
}

ポイントは、

  • http.HandlerFuncを返すラッパーを作ること
  • ラッパーの中で引数のhttp.HandlerFuncを呼び出すこと

だと個人的に思った。前述の図との対応としては、処理のイメージとしてはこんな感じだろうか。Middlewareで前処理を行って、アプリケーションの処理( next.ServeHTTP(w, r) )を行って、Middlewareで後処理を行う。

// sampleHandler ...
func sampleHandler(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // 前処理
        next.ServeHTTP(w, r)
        // 後処理
    }
}

まとめ

Middlewareは怖くない

CentOSでユーザ追加

DockerでGoの環境構築する際にCentOSでユーザ追加したのでメモ。

$ useradd -m -d /home/docker -s /bin/bash docker
$ echo "docker:docker" | chpasswd
$ echo "docker ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
  • useradd:新規ユーザの追加
  • chpasswdでパスワードの更新

それぞれ詳細はmanコマンドで確認できる。 追加されたユーザは/etc/passwdファイルで確認できる。なお、passwdファイルの詳細はman 5 passwdで確認できる。

/etc/sudoersファイルでユーザのsudo権限を管理している。dockerで環境構築した際には上記のコマンドで設定できたが、通常ではrootでさえ編集不可のファイルとなっている。こちらもmanコマンドで詳細を確認できる。

sudoersを変更する、よく使う設定例 - それマグで!

感想

普段の作業では設定しないところだったので忘れないようにメモした。そしてmanコマンドってこんなに便利なんだってことがわかった。

Real World HTTP を読んだ

Real World HTTPを読んだ。前に読んだやつ。

www.oreilly.co.jp

HTTPについてそれが作成されてきた歴史と併せて説明されておりわかりやすかった。RFCのリファレンスにもなっており、これを通して初めてRFCの内容を読んだ。ハイパフォーマンスブラウザネットワーキングではパフォーマンスの観点からの説明だけであったのに対して、Real World HTTPでは一般的に使用される機能に関して説明してあり、HTTPの入門書としてもいいんじゃないかと思った。

  • HTTP HEADERの説明が一通りあって、今までスルーしてた部分をフォローできた。
  • Cache-Controlについてもう少し詳しく調べた

リクエスト時の Cache-Control、max-age=0 と no-cache の違い | tech - 氾濫原

RFC 7234 - Hypertext Transfer Protocol (HTTP/1.1): Caching

ちなみにGOによる実装の部分はスルーしたが、最近になってGoを書くようになったので改めて読み直したい。

ハイパフォーマンスブラウザネットワーキングを読んだ

www.oreilly.co.jp

TCP, TLS, HTTPといったアプリケーションよりも低いレイヤーについて基本的な構造を説明し、パフォーマンスの観点からエンジニアはどう関わるべきかが書かれている。文章は読みやすかった。読み飛ばした部分もあるが10日間ほどで読み終わった。380ページ。

以下、自分用メモ。

1章 レイテンシ・帯域幅入門

  • ネットワークのパフォーマンスを決める2つの要素がある
  • レイテンシは要するにパケットを送信してから受信するまでの時間。光ファイバーを通る時間やルータの処理時間などの計。
  • 帯域幅は要するに速度(bps)。
  • 光ファイバーを通る時間(propagation delay)は光の速さが29km/sであり、限界が決まっている。
  • 感覚的に数百ミリ秒遅れると人間は遅れを認識する。
  • ラストワンマイルのレイテンシ。要するにパケットが太平洋を渡っても、最後に複数のプロバイダを経由することでレイテンシが発生してしまうこと。
  • ほとんどの場合、帯域幅ではなくレイテンシがパフォーマンスのボトルネック

2章 TCPの構成要素

  • TCP接続で3ウェイハンドシェイクから始まる。ここでもクライアントがデータを送信し始めるまでに1往復分のレイテンシが発生している。
  • フロー制御。データの送信サイズがウィンドウ値で制限されてしまう。データ受信側が処理しきれないデータ量を送信側が送信してしまうのを防ぐための仕組み。
  • スロースタート。データの送信サイズは小さい値から始まって、1往復ごとに送信サイズが大きくなっていく。ネットワーク自体の許容量を超えないようにするための仕組み。
  • 上記のようにデータサイズが制限されることにより発生するレイテンシ(データを余計に送信するためデータの往復が発生するなど)がボトルネックであり、帯域幅はそんなに関係ない。
  • ウィンドウ値を大きくしたり、スロースタートリスタートを無効にしたりするとパフォーマンスにはいいらしい。
  • そもそもとして、CDNを利用してデータを近くでやり取りするようにするといい

3章 UDPの構成要素

  • 受信側に確実に届けるための仕組みがない。このため信頼性は低い。
  • その分、余計なデータの往復などが発生しないため、レイテンシの発生が少ない。
  • NATの話が出てきた。ここでも処理時間(レイテンシ)が発生する
  • 疲れてたので読み飛ばしながら進んだ。

4章 TLS

  • HTTPSのSのやつ。暗号化、認証、データ整合性を提供する。要するに、認証局を利用して、クライアントから送信されたデータが本当にクライアントから送られてきたものかどうかを保証する。逆も同じ。
  • TLSハンドシェイクから通信を始める。最大2往復。
  • 1往復目で使用するTLSのバージョンや多分だけど利用できる認証局をクライアントが送信して、サーバがバージョンや認証局はこれを使います、っていうレスポンスを返す。
  • 2往復目で暗号化のために共通鍵のやり取りを行う。
  • TCPハンドシェイクと合わせると、最大で3往復分のレイテンシが発生する。暗号化の計算によるレイテンシは大したことないらしいので、ここがボトルネックとなる。
  • パケットはすでに光速で近い速度なためここは大きなレイテンシ改善は見込めないが、CDNを利用することで距離を近くすることができる。つまり、CDNTCPTLSハンドシェイクを行わせることでレイテンシ改善が見込める。
  • これが動的コンテンツのやり取りにおいて、キャッシュ機構は使用しないのにCDNを利用する理由。

5章 ワイヤレスネットワーク入門

  • パフォーマンスは周波数帯域幅とシグナル強度に依存する

6章 WiFi

7章 モバイルネットワーク

  • 3G,4G,LTEの規格について
  • RRCは物理層での接続を管理する。TCPハンドシェイクが行われる前段階。ここでもレイテンシが発生する。
  • 読み飛ばした

8章 モバイルネットワークの最適化

  • 読み飛ばした

9章 HTTPの歴史

  • 0.9 => 1.0 => 1.1 => 2.0
  • 0.9のリクエストはGETメソッドとパスの1行で構成される。めちゃシンプル
  • 1.0でキャッシュ(Expires)がレスポンスで返るようになった
  • 1.1でKeep-Alive接続が可能になった
  • 2.0はパフォーマンス向上が主なテーマ

10章 Webパフォーマンス入門

  • JavaScriptの実行はCSSにブロックされる可能性がある。
  • パフォーマンスを計測できるツールを使う、そして計測する。
  • ウォーターフォールチャートがパフォーマンスを計測する上で見やすい
  • ブラウザがやっているパフォーマンス最適化

11章 HTTP 1.x

  • キープアライブ接続がとにかく重要!
    • TCPTLSハンドシェイクを省略できるのでレイテンシ削減に繋がる
  • クライアントは一度に一つのリクエストしか送れない、サーバは一度に一つのリクエストしか処理できない、といったように並列処理ができないという課題がある。HTTPのHoLブロッキング(TCPのHoLブロッキングとは別)。
  • ここを解決できればパフォーマンス改善に繋がる。これをやりたいのがHTTP2.0。

12章 HTTP 2.0

  • パフォーマンス向上に主眼が置かれている
  • バイナリフレーミングレイヤー(?)。これでクライアントから複数のリクエストを一度に送信できるようになるらしい
  • サーバからプッシュで画像などを配信できるようになるらしい

13章 アプリケーション配信最適化

  • 定番のパフォーマンスベストプラクティス
    • DNSルックアップを減らす(キープアライブ接続)
    • TCP接続の再利用(キープアライブ接続)
    • HTTPリダイレクトを減らす(新たにDNSルックアップやTCP接続が必要になるため)
    • 不要なリソースはそもそも配信しない
    • リソースのキャッシュ

14章 ブラウザネットワーク入門

  • 印象に残っていない

15章 XMLHttpRequest

  • ajaxで使うやつ
  • CORSを気にしている

16章 Sever-Sent Events

  • 読み飛ばした
  • サーバ => ブラウザ

17章 WebSocket

  • 読み飛ばした
  • サーバ <=> ブラウザ

18章 WebRTC

  • 読み飛ばした
  • リアルタイム通信で使用する(Web会議など)

Webエンジニアに転職して半年が経った

Webエンジニアに転職して半年が経った。

仕事にも慣れてきたのでこれまでとこれからについて整理しようと思う。

転職前

企業向けのパッケージ開発をしていた。Web経由で使用してもらうものではなく、お客さんのとこに作ったサーバにパッケージをインストールして使ってもらう形態だった。

いわゆるオレオレフレームワークJavaを使って、ビジネスロジックをひたすら書いてた。ちなみにDBはOracle

人間関係とか仕事の出来とかは問題なかったけど、やはりオレオレフレームワークJavaでは何か物足りなかった。オレオレフレームワークの上だと、クラス設計すら何も考えなくていいのでただif文書いてメソッド呼び出して、みたいなことしか書かなくてよかった。

そんな中、インターネットを覗くと、Webエンジニアたちが自分の知らない言葉でしゃべってた。gitとは、デザインパターンとは、HTTPとは、他にもたくさん。まるで話についていけない。自分自身とインターネット上のWebエンジニアを比較して、技術的なことが身についていないことが不安で仕方がなかった。もちろん、そもそも非WebとWebを単純に比較してどうなのとは今は思うが。

というわけで、将来的にエンジニアとしての技術力が足りなくて会社の外では食っていけないと思った。一番大事なのは社内ではなく社外(市場)での評価だと考えていたので、転職することを決意した。

ちなみに、技術的には物足りなかったが、技術以前に問題解決に対する考え方に関しては徹底的に教えてもらっていて、これは本当によかったと今感じている。直属だった先輩、上司にはかなりよくしてもらっていたのでありがたかった。

転職後〜今

新しい会社ではRubyRailsを使ってサイトを作っている。同じフレームワークでもRailsだとググったら情報がたくさん出てくるし、ソースも読めるし、勉強しがいがあって楽しく過ごせている。

自分たちで運用しているので、アプリケーションを動かすサーバや下のレイヤーであるHTTP, TCP/IPなどについても知っておく必要があり、正直いくら勉強してもなかなか追いつけないでいる。それでもやはり勉強しがいがあるというのは楽しいものであると思った。

入社したての時はWebど素人であり、HTMLも若干怪しいくらいだったが、やっていけば案外なんとかなった。前職で学んだ仕事のやり方のおかげ。頭の中で考えているリスクはほとんどが必要以上にリスクとして認識されている、って話はよく聞くが本当にそうだった。そういえば、前職の先輩に「具体的にここがヤバい、って言えなければそれは大体ヤバくない。必要以上に頭がリスクとして認識しているだけ。」と言われたことがあったのを思い出した。

スキルセット

今現在、自分がわかる範囲はこんな感じだろうか。どれも雰囲気使えるレベルではあるが。

こう書くとなかなか不安になるからたまに列挙するのはいいかも。

今後

次の半年だが、大きく2つのテーマで勉強する。キーワードはRubyと低レイヤー。

一つはRubyRubyでできることを増やす。

基本的な文法はわかったので手を動かしてとにかく書く。正直、仕事で使っているからRubyなのだが、一つの言語で大体のことできるようになると他の言語の習得も容易になるという話をよく聞くので、最初はあえてこだわってRubyを書いていく。

もう一つは低レイヤー。HTTP、TCP/IPなどなどネットワークまわりを中心に。

流行り廃りのない知識がないと将来的に死ぬ気がする。今後死にたくないので勉強する。

md2keyを使った

社内LTでスライド作るのにmd2keyを使った。便利。

github.com

k0kubun.hatenablog.com

太字や取り消し線はまだ使えないらしい

Rich texts · Issue #11 · k0kubun/md2key · GitHub

とのこと。

調べてみた

実装できないかなと思って、applescriptで太字や取り消し線を実現できないか調べてみた。keynote見る限り、文字のスタイルにボールドはあるから案外すぐ見つかるんじゃないかと思った。

結果としては見つけられなかった。リッチテキストのプロパティがcolor, font, sizeしかなかったのでそもそもできないんじゃないか(styleみたいなのがあると期待してたがなかった)。ちなみに色やフォントはうまく指定できた。

rich text n, pl rich text : This provides the base rich text class for all iWork applications.
elements
contains characters, paragraphs, words.
properties
color (color) : The color of the font. Expressed as an RGB value consisting of a list of three color values from 0 to 65535. ex: Blue = {0, 0, 65535}.
font (text) : The name of the font. Can be the PostScript name, such as: “TimesNewRomanPS-ItalicMT”, or display name: “Times New Roman Italic”. TIP: Use the Font Book application get the information about a typeface.
size (integer) : The size of the font.

applescriptをあんまり深追いしたくないのでこの辺でやめる。

Working With Unix Processes を読んだ

Working With Unix Processes (English Edition)

Working With Unix Processes (English Edition)

この前読んだ本と同じ作者が書いている。

Working with TCP Sockets を読んだ - tnakata's blog

こちらもサンプルソースRubyで書かれている。Ruby書いててプロセスのことまだ何もわかってない(言語化できてない)という人にはいいかもしれない。

読もうと思った理由

  • プロセスの概念を言語化したいから。

読んでて勉強になった部分

  • プロセスとはUnix上での処理単位。どんなプログラムもプロセスの中で実行される。
    • 例えば、irbを実行するとプロセスが一つ立ち上がる。psコマンドを実行することで立ち上がったプロセスの存在を確認できる。
  • Unixでは全てはFileとして扱われる。
    • test.rbといったファイルやソケットも全てファイル。RubyではIOクラスが該当する。
    • ターミナルでのコマンドで出てくる標準入力、標準出力、標準エラー出力も同様にファイルである。
  • プロセスはforkでコピーできる(子プロセスが作られる)。並列処理に関係するところ。
  • 子プロセスが終了したのに親プロセスがProcess.waitを実行しないと、子プロセスのメモリは解放されないままになる。Zombie Processという。
  • シグナルという概念がある。プロセス間、もしくはキーボードからプロセスへシグナルを送り、プロセスを終わらせることができる。
    • Ctrl+Cによるプロセスの強制終了やProcess.kill
    • ちょっと自信がないが、ソケットを利用したやり取りとは異なる概念。
  • プロセス間の通信で代表的なものはパイプとソケット。どちらもデータの流れはSTREAM(データの始まりと終わりの目印がない)。
  • STREAMの反対はMESSAGE。UNIXソケットで利用される。
  • Daemon Processesとはバックグラウンドで走っているプロセスのこと。
  • 全てのプロセスの親としてinitプロセスがある。
  • execrc(2)で現在のプロセスを他のプロセスに変えることができる。Rubyのexecはexecrc(2)のラッパー。なおプロセスを変えた後に元のプロセスに自動で戻ることはない。
  • Ruby のexecは元のプロセスのfile descriptorsを全て閉じる。メモリリークを防ぐため。
  • PreforkingはCopy on Writeによってメモリを効率的に使用できる。
    • CoWはコピーしたプロセスを使う(たしかacceptした)タイミングでメモリがコピーされるので効率的とのこと。

読んでてよくわからなかったところは以下の通り。

  • Zombie ProcessやDaemon Processが実際にどのようなときに使われるのか
  • 仕事でやってることとどう結びつければよいか

Working with TCP Sockets を読んだ

Working With TCP Sockets (English Edition)

Working With TCP Sockets (English Edition)

一通り読んだけど結構勉強になった。サンプルソースRubyで書かれているから、Rubyを使っていてWebサーバとかソケットとかのことについて基礎から勉強したいって人にはいいと思う。

読もうと思った動機

以下に書いたわからないことを言語化したいから

  • ネットワークプログラミングについて基礎的なことをまるで知らない。
  • Railsを使っているとWebrickとかPumaとかnginxとかいろんなWebサーバが出てくるけど違いがわからない。
  • そもそもWebサーバの仕組みを知らない。

勉強になった部分

  • serverやclientがどのようなlifecycleを送るのかについて
    • server : create, bind, listen, accept, close
    • client : create, bind, connect, close
  • RubyのSocketライブラリはsystem callを呼んでいること
  • Befferがいい感じにデータ量を調整してくれること
  • forkやthreadを利用することで並列接続を実現していること
  • WebrickやPumaやnginxのような様々なサーバが存在するけど、違いは並列接続を実現させる方法だということ(ざっくり)

逆に理解できなかったところ

  • non-blocking IO が理解できた気でいるけど自信ない
  • Preforkingがどうして効率的なのか
  • Eventedは何が何だか、Hybridも

'rails -v'コマンドは何をしているのか

rails -v'コマンドは何をやっているのか

rails -vコマンドがどのような仕組みになっているか気になったので調べてみる。RailsのバージョンはRails 5.0.1

which railsコマンドで実行ファイルの場所を取得する。

$ which rails
~/.rbenv/shims/rails

rbenvを使用している場合には上記が実行ファイルとなるが、上記のファイルはただのラッパーのようである。ここでは詳細は追わないが、以下のリンクが後で理解するときに参考になりそう。

memo.sugyan.com

実際の実行ファイルをrbenv which railsコマンドで取得する。

$ rbenv which rails
~/.rbenv/versions/2.3.1/bin/rails

~/.rbenv/versions/2.3.1/bin/railsの中身を見ると、

(省略)

gem 'railties', version
load Gem.bin_path('railties', 'rails', version)

#[1] pry(main)> Gem.bin_path('railties', 'rails')
#=> "~/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/railties-5.0.1/exe/rails"

となっており、railtiesというgemのrailsという実行ファイルを実行している。 /exe/railsを見てみる。

#!/usr/bin/env ruby

git_path = File.expand_path('../../../.git', __FILE__)

if File.exist?(git_path)
  railties_path = File.expand_path('../../lib', __FILE__)
  $:.unshift(railties_path)
end
require "rails/cli"

上位のフォルダに.gitがあれば、自分自身のlibが探索されるように探索パス($:)にパスを追加している。 そしてrequire "rails/cli"cli.rbを実行している。/lib/rails/cli.rbが読み込まれるはずだが、念のためにrequire "rails/cli"の上の行にputs $:を差し込んで~/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/railties-5.0.1/libが含まれているかどうか確認した。ちゃんとあったので多分これが読み込まれているはず。

/lib/rails/cli.rbを見てみる。

require 'rails/app_loader'

# If we are inside a Rails application this method performs an exec and thus
# the rest of this script is not run.
Rails::AppLoader.exec_app

require 'rails/ruby_version_check'
Signal.trap("INT") { puts; exit(1) }

if ARGV.first == 'plugin'
  ARGV.shift
  require 'rails/commands/plugin'
else
  require 'rails/commands/application'
end

Rails::AppLoader.exec_appを詳しく見るため、/lib/rails/app_loader.rbを見る。

require 'pathname'
require 'rails/version'

module Rails
  module AppLoader # :nodoc:
    extend self

    RUBY = Gem.ruby
    EXECUTABLES = ['bin/rails', 'script/rails']

(省略)

    def exec_app
      original_cwd = Dir.pwd

      loop do
        if exe = find_executable
          contents = File.read(exe)

          if contents =~ /(APP|ENGINE)_PATH/
            exec RUBY, exe, *ARGV
            break # non reachable, hack to be able to stub exec in the test suite
          elsif exe.end_with?('bin/rails') && contents.include?('This file was generated by Bundler')
            $stderr.puts(BUNDLER_WARNING)
            Object.const_set(:APP_PATH, File.expand_path('config/application', Dir.pwd))
            require File.expand_path('../boot', APP_PATH)
            require 'rails/commands'
            break
          end
        end

        # If we exhaust the search there is no executable, this could be a
        # call to generate a new application, so restore the original cwd.
        Dir.chdir(original_cwd) and return if Pathname.new(Dir.pwd).root?

        # Otherwise keep moving upwards in search of an executable.
        Dir.chdir('..')
      end
    end

    def find_executable
      EXECUTABLES.find { |exe| File.file?(exe) }
    end
(省略)

if exe = find_executableは現在自分がRails::Rootにいるならtrue、そうでないならfalseを返す。

カレントディレクトリがRails::Root以外

ここでは、一旦homeディレクトリにいるとして続きを見ていく。

cli.rbに戻ると、今回のコマンドの引数は-vなのでrequire 'rails/commands/application'が実行されることがわかる。 /lib/rails/commands/application.rbでは、args = Rails::Generators::ARGVScrubber.new(ARGV).prepare!が実行されている。

require 'rails/generators'
require 'rails/generators/rails/app/app_generator'

module Rails
  module Generators
    class AppGenerator # :nodoc:
      # We want to exit on failure to be kind to other libraries
      # This is only when accessing via CLI
      def self.exit_on_failure?
        true
      end
    end
  end
end

args = Rails::Generators::ARGVScrubber.new(ARGV).prepare!
Rails::Generators::AppGenerator.start args

Rails::Generators::ARGVScrubber.new(ARGV).prepare!を詳しく見るために、/lib/rails/generators/rails/app/app_generator.rbを見る。

    # This class handles preparation of the arguments before the AppGenerator is
    # called. The class provides version or help information if they were
    # requested, and also constructs the railsrc file (used for extra configuration
    # options).
    #
    # This class should be called before the AppGenerator is required and started
    # since it configures and mutates ARGV correctly.
    class ARGVScrubber # :nodoc:
      def initialize(argv = ARGV)
        @argv = argv
      end

      def prepare!
        handle_version_request!(@argv.first)
        handle_invalid_command!(@argv.first, @argv) do
          handle_rails_rc!(@argv.drop(1))
        end
      end

(中略)

      private

        def handle_version_request!(argument)
          if ['--version', '-v'].include?(argument)
            require 'rails/version'
            puts "Rails #{Rails::VERSION::STRING}"
            exit(0)
          end
        end

prepare!メソッド内で呼び出しているhandle_version_request!Railsのバージョンを表示していることがわかった。

これでRails::Root以外の場所からrails -vコマンドを打った時の流れがわかった。

カレントディレクトリがRails::Root

次にRails::Rootにいる場合を見ていきます。Rails::AppLoader.exec_appを再び見ていくと、bin/railsをreadしている。

#!/usr/bin/env ruby
APP_PATH = File.expand_path('../../config/application', __FILE__)
require_relative '../config/boot'
require 'rails/commands'

APP_PATHという文字列が含まれているので、Rails::AppLoader.exec_app

          if contents =~ /(APP|ENGINE)_PATH/
            exec RUBY, exe, *ARGV

により、bin/railsRubyスクリプトとして実行される。bin/rails../config/bootrails/commandsを実行する。

# ../config/boot

ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)

require 'bundler/setup' # Set up gems listed in the Gemfile.

詳細までは追いませんが、gemを使用可能にできるようロードパスを更新してるみたい。

Bundler: The best way to manage a Ruby application's gems

# rails/commands

ARGV << '--help' if ARGV.empty?

aliases = {
  "g"  => "generate",
  "d"  => "destroy",
  "c"  => "console",
  "s"  => "server",
  "db" => "dbconsole",
  "r"  => "runner",
  "t"  => "test"
}

command = ARGV.shift
command = aliases[command] || command

require 'rails/commands/commands_tasks'

Rails::CommandsTasks.new(ARGV).run_command!(command)

railsコマンドに続けて入力した引数をここで変数として取り扱っている。rails sといったエイリアスもここで定義されている。そして、rails/commands/commands_tasksをrequireしている。rails/commands/commands_tasksでは、

require 'rails/commands/rake_proxy'

module Rails
  # This is a class which takes in a rails command and initiates the appropriate
  # initiation sequence.
  #
  # Warning: This class mutates ARGV because some commands require manipulating
  # it before they are run.
  class CommandsTasks # :nodoc:
    include Rails::RakeProxy

    attr_reader :argv

(中略)

    def initialize(argv)
      @argv = argv
    end

    def run_command!(command)
      command = parse_command(command)

      if COMMAND_WHITELIST.include?(command)
        send(command)
      else
        run_rake_task(command)
      end
    end

(中略)

    def version
      argv.unshift '--version'
      require_command!("application")
    end

(中略)

    private

(中略)

      def require_command!(command)
        require "rails/commands/#{command}"
      end

(中略)

      def parse_command(command)
        case command
        when '--version', '-v'
          'version'
        when '--help', '-h'
          'help'
        else
          command
        end
      end
  end
end

となっており、Rails::CommandsTasksを定義している。 Rails::CommandsTasks.new(ARGV).run_command!(command)Rails::CommandsTasksインスタンスを作り、run_command!メソッドを実行している。run_command!では、まずparse_commandエイリアスを取り扱い、versionコマンドとしてまとめて扱うようにしている。そして、versionメソッド→require_command!メソッドの順に実行され、最終的にrails/commands/applicationを実行する。

ここから先は、Rails::Root以外から実行した場合と同じになる。

Rubyではなぜmix-inでクラスメソッドを引き継げないのか

includeと継承の違い

includeメソッドによりモジュールをinclude(Mix-in)してそのモジュールのインスタンスメソッドを使えるようになります。クラスメソッドは使えません。

instance method Module#include (Ruby 2.4.0)

一方、継承では親となるクラスのインスタンスメソッド・クラスメソッドの両方を使えるようになります。

module Foo
  def foo_instance_method
    "foo_instance_method"
  end
  def self.foo_class_method
    "foo_class_method"
  end
end

class SuperBoo
  def boo_instance_method
    "boo_instance_method"
  end
  def self.boo_class_method
    "boo_class_method"
  end
end

class Boo < SuperBoo
  include Foo
end

boo = Boo.new
boo.boo_instance_method #=> "boo_instance_method"
boo.foo_instance_method #=> "foo_instance_method"

Boo.boo_class_method #=> "boo_class_method"
Boo.foo_class_method #=> NoMethodError: undefined method `foo_class_method' for Boo:Class

上記でBooクラスがFooモジュールをincludeしたことでBooクラスの継承チェーンにFooモジュールが組み込まれます。

Boo.ancestors #=> [Boo, Foo, SuperBoo, Object, PP::ObjectMixin, Kernel, BasicObject]

ここだけ見ると、includeは継承の一種かなと思ってしまいます。違いが出るのは特異クラスの継承チェーンを見たときです。

Boo.singleton_class.ancestors #=> [#<Class:Boo>, #<Class:SuperBoo>, #<Class:Object>, #<Class:BasicObject>, Class, Module, Object, PP::ObjectMixin, Kernel, BasicObject]

上記で確認できるように、includeしたモジュールは特異クラスの継承チェーンには出てきません。このため、includeしたモジュールのクラスメソッドを使用できません。つまり、includeと継承は一見似ているようですが全く異なる処理となります。

includeしたモジュールのクラスメソッドも使いたい

上記の通り、includeしたモジュールからはクラスメソッドを引き継げません。このため、ある責務を持ったモジュールのクラスメソッドを複数のクラスへ引き継ぎたいという場合にどうするかという問題が出てきます。この場合はincludedを使った実装をするのが一般的です。

qiita.com

module Foo
  def foo_instance_method
    "foo_instance_method"
  end
  def self.included(base)
    base.extend(ClassMethods)
  end
  module ClassMethods
    def foo_class_method
      "foo_class_method"
    end
  end
end

class SuperBoo
  def boo_instance_method
    "boo_instance_method"
  end
  def self.boo_class_method
    "boo_class_method"
  end
end

class Boo < SuperBoo
  include Foo
end

Boo.foo_class_method #=> "foo_class_method"

上記の実装はRailsでも使用されています。

なぜincludeでクラスメソッドを継承しないのか

そもそもなぜincludedを使用するようなめんどくさいことをしないとincludeしたモジュールのクラスメソッドを引き継げないのでしょうか。

上記で述べたようにincludeでクラスメソッドを引き継ぐ、という要望は普通にあるみたいです。であれば、Rubyの仕様としてincludeしたモジュールのクラスメソッドも引き継ぐというのはありだったんじゃないか、ということを考えました。その方がincludedを使用せずともクラスメソッドが使えるようになるから楽ですよね。

多重継承と同じになってしまうから、というのも考えましたがまあこれは違うかと。複数のモジュールをincludeしても継承しても、継承チェーンの中での順番は確定するからです。

とはいえ、includeでクラスメソッドを継承しないという選択をしているからには何かしらのメリットがあるということです。このメリットが何か私にはわからなかったので調べてみることにしました。

includeが使用されるそもそもの想定とは

Ruby forumの過去のスレッドで同じようなことを疑問に思った人がスレッドを立てていました。

Why the lack of mixing-in support for Class methods? - Ruby Forum

疑問に対するmatzさんの回答もありました。

https://www.ruby-forum.com/topic/68638

引用します。

Mix-in is used for several purposes and some of them can be hindered by inheriting class methods, for example, I don’t want to spill internal methods when including the Math module. I am not against for some kind of inclusion that inherits class methods as well, but it should be separated from the current #include behavior.

以下、matzさんの考えへの私の解釈です。

Mix-inが使用される想定(例えば、後述するMathモジュールみたいな使い方)がいくつかあるんだけど、クラスメソッドを継承したらそれがうまくいかない。 例えば、Mathモジュールを継承するときにクラスメソッドまで継承したら、includeしたクラスでメソッド名(名前空間)がかなり侵されてしまうのを避けたい。Mathモジュールの使い方を見ると、インスタンスメソッドとクラスメソッドにおそらく同名のメソッドを持っています。MathモジュールをincludeすればMathモジュールのクラスメソッドは不要になりますが、この時クラスメソッドも継承していると不要になったクラスメソッドの分だけincludeしたクラスでメソッド名の衝突が起きてしまう恐れがあります。