Rails5 + Pumaを実現する上で気をつけるべき5つの設定

## 今の設定ファイル

```ruby
require 'yaml'

def local_conf
  conf = YAML.load_file('path_to_settings.local.yml')
  conf['puma']
end

directory 'path_to_app_directory'

workers local_conf['workers']
threads_count = local_conf['threads']
threads threads_count, threads_count
phased_restart = local_conf['phased_restart']

preload_app! unless phased_restart
prune_bundler if phased_restart

rackup DefaultRackup
port 3000
worker_timeout local_conf['worker_timeout']
pidfile File.expand_path('tmp/pids/puma.pid')
state_path File.expand_path('tmp/pids/puma.state')
stdout_redirect File.expand_path('log/puma_access.log'), File.expand_path('log/puma_error.log'), true

before_fork do
  ActiveRecord::Base.connection_pool.disconnect! unless phased_restart
end

on_worker_boot do
  unless phased_restart
    ActiveSupport.on_load(:active_record) do
      ActiveRecord::Base.establish_connection
    end
  end
end

plugin :tmp_restart


```

## 5つの設定ポイント
- zero downtimeを実現するための clustered mode設定
- database connection poolings
- logrotate設定
- capistrano-pumaの設定
- prune_bundlerの設定

## zero downtimeを実現するための clustered mode
pumaにおいて、zero downtime、 zero hanging requestを実現するためには、
clustered modeを使う必要がある。clustered modeはworker数を指定するだけで起動する。

```puma.rb
# clustered modeを使わない場合、workersは0とする
workers 2
threads_count = 5
threads threads_count, threads_count
```

workerの数はCPUのコア数と一致させる。

なお、2 worker 5 threadでがっつり負荷をかけたら、2GB以上のメモリを消費した。
少ないメモリで zero downtimeを実現したい場合、unicornの方が適しているかも知れない。
(もちろん、実行しているアプリケーションによって異なる)

https://github.com/puma/puma#clustered-mode

## database connection pools

ActiveRecordのconnectionはworker毎に設定されている。
そのためdatabase connection poolsの値には、1 workerごとの thread数を設定する。
rails デフォルトでは、ENV['RAILS_MAX_THREADS']に値を設定することが[デフォルト](https://github.com/rails/rails/pull/23057/files)となっている。

## logrotate設定について
※ RailsのLoggerにて、rotate & 削除をしている人は気にする必要が無いです.

unicornと違い、Pumaはsignalを送っても puma自体のLogのRotateしか行いません。
そのため、chrono_loggerを使うなり、copytruncateを利用するなり、
今までとは異なる方法で Logrotateを行う必要があります。

## capistrano-pumaの設定について

puma_preload_appをtrueにすると、phased-restartが出来ない。
ので、cluster modeのときは、puma_preload_appをfalseにしないといけない。

```rb
#lib/capistrano/tasks/puma.rake
  task :smart_restart do
    if !puma_preload_app? && puma_workers.to_i > 1
      invoke 'puma:phased-restart'
    else
      invoke 'puma:restart'
    end
  end
```

## Prune Bundlerの設定

ここ多分いちばん大切。
prune bundler は cluster processが読みに行く GEM_FILEを指定するもの。
これをしないと、Gemfile を書き換えても読み込んでくれなくなるし、正しく current directoryに
存在する Gemfile を見てくれず、過去のGemfileが参照できなくなったタイミングで puma のプロセスが落ちてしまう。

```ruby
# launcher.rb
    def prune_bundler
      return unless defined?(Bundler)
      puma = Bundler.rubygems.loaded_specs("puma")
      dirs = puma.require_paths.map { |x| File.join(puma.full_gem_path, x) }
      puma_lib_dir = dirs.detect { |x| File.exist? File.join(x, '../bin/puma-wild') }

      unless puma_lib_dir
        log "! Unable to prune Bundler environment, continuing"
        return
      end

      deps = puma.runtime_dependencies.map do |d|
        spec = Bundler.rubygems.loaded_specs(d.name)
        "#{d.name}:#{spec.version.to_s}"
      end

      log '* Pruning Bundler environment'
      home = ENV['GEM_HOME']
      Bundler.with_clean_env do
        ENV['GEM_HOME'] = home
        ENV['PUMA_BUNDLER_PRUNED'] = '1'
        wild = File.expand_path(File.join(puma_lib_dir, "../bin/puma-wild"))
        args = [Gem.ruby, wild, '-I', dirs.join(':'), deps.join(',')] + @original_argv
        Kernel.exec(*args)
      end
    end
```
処理はこんな感じ。
Bundler.with_clean_env は BUNDLE_ の環境変数を消して yieldを実行するので、
新しいBundlerを読み込もうとしていることが分かる。

なお、prune_bundlerを使いたいときは、preload_app をfalseにして、
workersを2以上にする必要がある。

```ruby
# launcher.rb
    def prune_bundler?
      @options[:prune_bundler] && clustered? && !@options[:preload_app]
    end

    def clustered?
      (@options[:workers] || 0) > 0
    end
```

```
# 確認方法 (by capistrano)
cap staging puma:start
DEBUG [xxxx]       * Pruning Bundler environment
DEBUG [xxxx]       [xxxx] Puma starting in cluster mode...
```

参考資料:
https://devcenter.heroku.com/articles/deploying-rails-applications-with-the-puma-web-server#database-connections
https://devcenter.heroku.com/articles/concurrency-and-database-connections
http://qiita.com/ma2ge/items/1fba78a08e8d3b82b3c2#logger--logrotate
https://github.com/puma/puma/blob/master/DEPLOYMENT.md#restarting