rails/springのコードリーディングをしましたー。ということで備忘録。

概要

Springはサーバ、アプリケーション、クライアントに分かれて処理を行います。サーバはクライアントからの接続を受け取り、アプリケーションにコマンド処理を委譲します。アプリケーションはRailsアプリケーションを先に読み込んでおいて、クライアントのリクエストが来たときにコマンドを実行する、ということをやっています。

サーバ側

まずはサーバ側の spring server を叩いたときの動きをコードから追ってみます。bin/springではSpring::Client.runを実行しています。

Spring::Client.runはサブコマンドに応じたクラスをインスタンス化し、callメソッドを呼び出します。serverサブコマンドの場合はSpring::Client::Server#callを呼び出します。

Spring::Server.bootはSpring::Serverをインスタンス化してbootメソッドを呼び出します

サーバ起動のためにpidファイルを書き込んだり諸々の初期設定を行った後、start_serverメソッドを呼び出します。

UNIXServerを起動します。ソケット名(env.socket_name)は環境変数SPRING_SOCKETの値か、テンポラリディレクトリ内のファイルになります。その後、acceptのループに入り、クライアントからの接続を待ち受けつつ、接続要求が来たら接続済みソケットがserveメソッドに渡されます。

serverメソッドではspringのバージョンをクライアントに送ります。クライアントはサーバのSpringとバージョンが合っているかどうかを検証し、合っていなかったらエラーになります。

次にクライアント側がファイルディスクリプタを送ってくるので、UNIXSocket#recv_ioで受け取ります。

クライアント側はさらに実行したいコマンドをJSON形式で送ってきます。最初にJSONのサイズを送り、その後にJSONを送ってきます。JSONの実体としてはargsとdefault_rails_envの属性値を持っています。ここで実行したいコマンドを送っている意味合いとしてはコマンドの実行そのものではなく、rails_env_forによるenv(development, production, test)を決定するためです。

クライアントが送ってきたコマンドがspringで対応しているコマンドであれば、ApplicationManager#runを実行します。

with_childメソッドが実行されます。with_childメソッド内ではsynchronizeで排他制御をしつつ、アプリケーション(コマンドを実際に実行するプロセス)を子プロセスとして起動して、ブロック内の処理を実行します。ブロック内の処理はアプリケーションプロセスにクライアント側が送ってきたファイルディスクリプタを渡します。受け取ると子プロセスはputsしてくるのでブロック内のchild.getsの処理を抜けます。

with_childの処理は、初回はalive?=@pidはnilなのでstartメソッド経由でstart_childメソッドを呼び出します。UNIXSocket.pairで無名のソケットペアを作成し、新しく子プロセス(アプリケーションプロセス)を立ち上げ、そのプロセスの3と4番のファイルディスクリプタにソケットペアの片方とログファイル用のファイルディスクリプタをセットします。これによってサーバプロセスとアプリケーションプロセスがソケットペア経由で通信できるようになります。

子プロセスとして立ち上げるコマンドは require 'spring/application/boot'です。子プロセス用のソケットは親プロセスでは閉じておきます。

アプリケーション側

子プロセスのspring/application/bootが実際にコマンドを実行する常駐プロセスになります。

spring/application/bootはSpring::Applicationをインスタンス化してrunメソッドを実行します。runメソッドは親プロセスから受け取ったクライアントのファイルディスクリプタを受け取って、serveメソッドを実行します。serveメソッドは長いのでかなり省略していますが全体的にこんな感じです↓

クライアントから標準出力、標準エラー出力、標準入力の3つのファイルディスクリプタを受け取り、アプリケーションの標準出力、エラー出力、標準入力として再オープンします。

あとはpreloadでRailsアプリケーションをロードしつつ、クライアントから送られてくるコマンドを実行し、waitメソッドでコマンドが終わるまで待ちます。

クライアント側のコード

次に spring rails console を実行したときの挙動を追ってみます。

springコマンドのサブコマンドがrailsだとSpring::Client::Rails#callを呼び出します。

Spring::Client::Run.callを[“rails_console”]の引数をつけて呼び出します。connectでSpringサーバと接続できればwarm_run、そうでなければcold_runを実行します。Springサーバを明示的に起動していない状態で spring binstubでspringがラップされた状態のbin/railsやbin/rakeを叩くとcold_runが実行されます。cold_runはSpringサーバを立ち上げてからコマンドを実行します。

warm_runもcold_runも最終的にはrunメソッドを叩きます。connect_to_applicationメソッドではサーバにUNIXSocketを渡しており、アプリケーション側にそのソケットが渡されます。この時点ではクライアントとアプリケーションが接続されたことになります。run_commandではクライアントの標準入出力と実行すべきコマンドをアプリケーションに送信します。

application.read.to_iはアプリケーション側で実行したコマンドのステータスコードが返ってくるので、コマンドが実行するまではクライアントは待ちの状態になります。ただし、クライアント側の標準入出力はアプリケーション側と同じになっているので、rails consoleのコマンドではインタラクティブにコマンドを実行することができます。また、forward_signalsによってクライアントに対するシグナルはアプリケーション側に転送されます。

まとめ

  1. Springサーバが立ち上がり、UNIXドメインソケットで待ち受ける
  2. クライアントからのアクセスがあると、サーバはアプリケーション(コマンドを実行する環境)の子プロセスを立ち上げる。既に立ち上がっていればそのアプリケーションを利用する。サーバとアプリケーション間はUnixSocketペアで相互通信を行う。
  3. クライアントからサーバ経由でアプリケーションにUnixSocketペアを渡す
  4. クライアントの標準入出力を3のUnixSocket経由でアプリケーションに渡し、アプリケーションの標準入出力としてセットする。
  5. クライアントがUnixSocket経由でアプリケーションにコマンドを送信し、アプリケーションはforkした上で初期設定を行い、コマンドを実行する。アプリケーションには既にRailsのコードがロードされているので直ぐにコマンドが実行される。クライアントで受け取ったシグナルはアプリケーションに転送される。