RubyGemのソースを読む（Unicorn編）

# 前提・下準備

- [RubyGemのソースを読む(設定編)](http://qiita.com/r-fujiwara/items/faf643d6e8abf36cdff5) をちょろっと読んで、設定しとくと良いです。

# Unicornの特徴

- Masterは何もしないことは無いが、Workerが何か死んだら何かしてくれる（これあんまり分かってないです）
- Workerが基本的にリクエストを処理する
- スレッドとかイベントドリブンとか難しいから、なぁ...シンプルにしようや...ってのが哲学っぽいです。

### もっと詳しく哲学とか知りたい人向け

以下とか参考になるのでは無いかと思います

- [unicorn: Rack HTTP server for fast clients and Unix](http://unicorn.bogomips.org/)
- [Unicorn!](https://github.com/blog/517-unicorn)

# 実際に読んで見る

## 起動まで

- とりあえず `git clone` します

```
$ git clone git@github.com:r-fujiwara/django.git
```

- とりあえず `bundle install` して起動します。

```
$ bundle install --path vendor/bundle
$ bundle exec unicorn -c config/unicorn.rb
```

- うまくいくとこんな画面が出ます ~~なんか適当なsinatraのソースからパクって来たんですけど忘れました。~~

![Screen Shot 2016-01-23 at 20.50.40.png](https://qiita-image-store.s3.amazonaws.com/0/46303/5f4e9b1c-603b-2b45-5d64-f4d8163d8597.png)

- ログは、`log/unicorn.stderr.log` に出ますので、以下みたいな感じに書きました。

```
$ tail -f log/unicorn.stderr.log
I, [2016-01-23T20:50:24.622132 #16518]  INFO -- : unlinking existing socket=/tmp/unicorn.sock
I, [2016-01-23T20:50:24.622225 #16518]  INFO -- : listening on addr=/tmp/unicorn.sock fd=10
I, [2016-01-23T20:50:24.622261 #16518]  INFO -- : worker=0 spawning...
I, [2016-01-23T20:50:24.623837 #16518]  INFO -- : master process ready
I, [2016-01-23T20:50:24.623922 #16545]  INFO -- : worker=0 spawned pid=16545
I, [2016-01-23T20:50:24.624088 #16545]  INFO -- : Refreshing Gem list
I, [2016-01-23T20:50:25.057550 #16545]  INFO -- : worker=0 ready
I, [2016-01-23T20:50:26.810465 #16518]  INFO -- : SIGWINCH ignored because we're not daemonized
192.168.33.1 - - [23/Jan/2016:20:50:38 +0900] "GET / HTTP/1.1" 200 1809 0.0244
192.168.33.1 - - [23/Jan/2016:20:50:39 +0900] "GET /favicon.ico HTTP/1.1" 404 512 0.0012
```

## ソース読む手順

- 起動
- ログを読む
- 気になる所に `puts` を挟む
- 再起動
- ログを読む

地道にやれば何となく色々わかってくると思います

## エントリポイント

- 結構細かいオプションはありますが、今回の場合のソースは[bin/unicornの126行目](https://github.com/defunkt/unicorn/blob/master/bin/unicorn#L126)です。

```rb
Unicorn::HttpServer.new(app, options).start.join
```

- 何となくこの時点で

**Unicorn::HttpServer.newでインスタンス生成**
**インスタンスメソッドのstartを読んでいる**
**startにチェーンしてjoinを読んでる**
**だからstartの返り値はインスタンス返しているので、地道にstartとjoinの挙動を追う**

- とか勘がはたらくと良いかもです。
- 最初は地道に読んでましたけど、ある程度アタリをつけながらソースを読んでると、ソースを読むスピードは上がる気がします（気がするだけ）

## ~~[時は来た！それだけだ...](https://www.youtube.com/watch?v=d17c_E7OhMM)~~ これからが本番です

- `Unicorn::HttpServer.new` ですが、ソースは `vendor/bundle/ruby/2.2.0/gems/unicorn-4.9.0/` 配下の `vendor/bundle/ruby/2.2.0/gems/unicorn-4.9.0/lib/unicorn/http_server.rb` にあります。
- なのでここを読んでいきましょう。

### ピックアップすべき点

#### http_server.rb#start

- 色々大事なんですが、ここで大事なのは `spawn_missing_workers` です。

```rb:http_server.rb#start
  def start
    inherit_listeners!
    # this pipe is used to wake us up from select(2) in #join when signals
    # are trapped.  See trap_deferred.
    @self_pipe.replace(Unicorn.pipe)
    @master_pid = $$

    # setup signal handlers before writing pid file in case people get
    # trigger happy and send signals as soon as the pid file exists.
    # Note that signals don't actually get handled until the #join method
    @queue_sigs.each { |sig| trap(sig) { @sig_queue << sig; awaken_master } }
    trap(:CHLD) { awaken_master }

    # write pid early for Mongrel compatibility if we're not inheriting sockets
    # This is needed for compatibility some Monit setups at least.
    # This unfortunately has the side effect of clobbering valid PID if
    # we upgrade and the upgrade breaks during preload_app==true && build_app!
    self.pid = config[:pid]

    build_app! if preload_app
    bind_new_listeners!

    # ここ
    spawn_missing_workers
    self
  end
```

#### http_server.rb#spawn_missing_workers

- ここでやっているのは

**Workerのプロセスを生成**
**Masterのプロセスをfork**
**Workerのプロセスをloopさせる(worker_loop)**

```rb:spawn_missing_workes.rb
  def spawn_missing_workers
    worker_nr = -1
    until (worker_nr += 1) == @worker_processes
      @workers.value?(worker_nr) and next
      worker = Unicorn::Worker.new(worker_nr)
      before_fork.call(self, worker)
      if pid = fork
        @workers[pid] = worker
        worker.atfork_parent
      else
        after_fork_internal
        worker_loop(worker)
        exit
      end
    end
    rescue => e
      @logger.error(e) rescue nil
      exit!
  end
```

- forkがよく分からない人はググるか、後述する[なるほどUnixプロセス ― Rubyで学ぶUnixの基礎](http://tatsu-zine.com/books/naruhounix) を読むといいです。
- ページ数は薄いですが、分かりやすい語り口と短いサンプルコードで個人的には凄く良いと思っています。
- 他の言語でもforkやプロセスの概念は応用が利くと思います。

#### http_server.rb#worker_loop

- ちょっとココらへんの細かい挙動は忘れてしまいましたが、[process_client](https://github.com/defunkt/unicorn/blob/master/lib/unicorn/http_server.rb#L658) が大事です。
- ここが Rackアプリケーションの入り口になります。
- 細かい挙動はガチで`puts`ゲーになると思いますが、基本的には

**Workerプロセスを無限ループさせる**
**process_clientを呼ぶ**

```rb:worker_loop.rb
  # runs inside each forked worker, this sits around and waits
  # for connections and doesn't die until the parent dies (or is
  # given a INT, QUIT, or TERM signal)
  def worker_loop(worker)
    ppid = @master_pid
    readers = init_worker_process(worker)
    nr = 0 # this becomes negative if we need to reopen logs

    # this only works immediately if the master sent us the signal
    # (which is the normal case)
    trap(:USR1) { nr = -65536 }

    ready = readers.dup
    @logger.info "worker=#{worker.nr} ready"

    begin
      nr < 0 and reopen_worker_logs(worker.nr)
      nr = 0
      worker.tick = time_now.to_i
      tmp = ready.dup
      while sock = tmp.shift
        # Unicorn::Worker#kgio_tryaccept is not like accept(2) at all,
        # but that will return false
        if client = sock.kgio_tryaccept
          # ここ
          process_client(client)
          nr += 1
          worker.tick = time_now.to_i
        end
        break if nr < 0
      end

      # make the following bet: if we accepted clients this round,
      # we're probably reasonably busy, so avoid calling select()
      # and do a speculative non-blocking accept() on ready listeners
      # before we sleep again in select().
      unless nr == 0
        tmp = ready.dup
        redo
      end

      ppid == Process.ppid or return

      # timeout used so we can detect parent death:
      worker.tick = time_now.to_i
      ret = IO.select(readers, nil, nil, @timeout) and ready = ret[0]
    rescue => e
      redo if nr < 0 && readers[0]
      Unicorn.log_error(@logger, "listen loop error", e) if readers[0]
    end while readers[0]
  end
```

#### http_server.rb#process_client

- ここは Rackサーバ（Unicorn）からRackアプリケーション(RailsとかSinatraとか)に処理を渡しています。
- 具体的には [562行目](https://github.com/defunkt/unicorn/blob/master/lib/unicorn/http_server.rb#L562)でしょうか
- これは多分Rackに渡しているのはソース読むのだと分かりづらいので、また別途記事を書こうかなと考えています。

```rb
  # once a client is accepted, it is processed in its entirety here
  # in 3 easy steps: read request, call app, write app response
  def process_client(client)
    # ここ
    status, headers, body = @app.call(env = @request.read(client))

    begin
      return if @request.hijacked?

      if 100 == status.to_i
        e100_response_write(client, env)
        status, headers, body = @app.call(env)
        return if @request.hijacked?
      end
      @request.headers? or headers = nil
      http_response_write(client, status, headers, body,
                          @request.response_start_sent)
    ensure
      body.respond_to?(:close) and body.close
    end

    unless client.closed? # rack.hijack may've close this for us
      client.shutdown # in case of fork() in Rack app
      client.close # flush and uncork socket immediately, no keepalive
    end
  rescue => e
    handle_error(client, e)
  end
```

#### http_server.rb#join

- 詳しくは追って無いですが、ソースをざっと読む感じシグナルを受け取った時に、workerの挙動をどういう風に制御するか、ということが書かれていると思います。
- ここらへんはあまり自信がありません...

```rb:http_server.rb#join
  # monitors children and receives signals forever
  # (or until a termination signal is sent).  This handles signals
  # one-at-a-time time and we'll happily drop signals in case somebody
  # is signalling us too often.
  def join
    respawn = true
    last_check = time_now

    proc_name 'master'
    logger.info "master process ready" # test_exec.rb relies on this message
    if @ready_pipe
      begin
        @ready_pipe.syswrite($$.to_s)
      rescue => e
        logger.warn("grandparent died too soon?: #{e.message} (#{e.class})")
      end
      @ready_pipe = @ready_pipe.close rescue nil
    end
    begin
      reap_all_workers
      case @sig_queue.shift
      when nil
        # avoid murdering workers after our master process (or the
        # machine) comes out of suspend/hibernation
        if (last_check + @timeout) >= (last_check = time_now)
          sleep_time = murder_lazy_workers
        else
          sleep_time = @timeout/2.0 + 1
          @logger.debug("waiting #{sleep_time}s after suspend/hibernation")
        end
        maintain_worker_count if respawn
        master_sleep(sleep_time)
      when :QUIT # graceful shutdown
        break
      when :TERM, :INT # immediate shutdown
        stop(false)
        break
      when :USR1 # rotate logs
        logger.info "master reopening logs..."
        Unicorn::Util.reopen_logs
        logger.info "master done reopening logs"
        soft_kill_each_worker(:USR1)
      when :USR2 # exec binary, stay alive in case something went wrong
        reexec
      when :WINCH
        if $stdin.tty?
          logger.info "SIGWINCH ignored because we're not daemonized"
        else
          respawn = false
          logger.info "gracefully stopping all workers"
          soft_kill_each_worker(:QUIT)
          self.worker_processes = 0
        end
      when :TTIN
        respawn = true
        self.worker_processes += 1
      when :TTOU
        self.worker_processes -= 1 if self.worker_processes > 0
      when :HUP
        respawn = true
        if config.config_file
          load_config!
        else # exec binary and exit if there's no config file
          logger.info "config_file not present, reexecuting binary"
          reexec
        end
      end
    rescue => e
      Unicorn.log_error(@logger, "master loop error", e)
    end while true
    stop # gracefully shutdown all workers on our way out
    logger.info "master complete"
    unlink_pid_safe(pid) if pid
  end
```

# 超えられてない壁

## ビルドがgithubのUnicornリポジトリから通らない

- [bundlerでgithub上のunicornを強引に入れる](http://blog.mirakui.com/entry/2012/01/31/145524) を読んで試したりしたんですが、ビルドが通りませんでした。
- 俺の意識が低すぎるのが原因ですね

## kgioとかの処理

- Unicornのネットワーク周りの具体的な処理は [kgio](https://github.com/wsylvest/kgio)がやってるみたいです（多分）
- UnicornのRubyで書かれている部分は結構大まかなロジックを司っている（と思っています。間違ってたら本当にごめんなさい）
- しかしながら大学時代に C言語系の勉強サボって ~~大学にあったXbox で Call of Duty4 Modern Warfareのキャンペーンのベテランをやりこんで、しかも全クリ出来なかったショボいクソだった~~ いたので、あまり同時接続数がどうたらとかは分かりません。
- 悔しいので後々ちゃんと勉強してソース読んで `printf` しまくって挙動を追いまくってやりたいな〜とは思っています

# Reference

## forkとかそういう系

結構Unicornのソースや挙動を追う上で参考になったのが

- [なるほどUnixプロセス ― Rubyで学ぶUnixの基礎](http://tatsu-zine.com/books/naruhounix) です。
- Resqueとかもマルチプロセスモデルなので、Unicornで学んだ知識は応用が効くのでは無いかと思います。

## ソース読んだほうがいい動機とか

- [勉強が出来ない奴はプログラマになれ！（バカだからできる勉強法）](http://d.hatena.ne.jp/amachang/20060804/1154679162)
  - ここで出てくるイメージモデルを作るとか、手元にソースを持ってくるとかは結局やってることあんま変わんないのかなぁと思います。
  - ソース書き換えたりまでしたりはしてないし、その点で言えば俺はまだまだですが。

# 謝辞

- 結構「これわかんないです」みたいなことを相談したらアドバイスくれた先輩達が複数人いました（何か名前出さんでくれって言われたんであえて名前出さないですが）。勝手にここで謝辞します。ありがとうございます。