bundler環境で動いてるunicornでgem が更新されない話

TL;DR

Unicornをpreload_app=falseで運用してる場合は、

before_exec do |server|
  ENV["BUNDLE_GEMFILE"] = File.join(project_home, "Gemfile")
end

の設定をした上で、SIGUSR2 を使いましょう。 capistrano3-unicornを利用しているなら、下記の通りです。

after 'deploy:publishing', 'deploy:restart'
namespace :deploy do
  task :restart do
    invoke 'unicorn:restart'
  end
end

発生していた問題

新しいgemをGemfileに追加してcapistranoでデプロイするも、なぜか新しいgemを認識してくれない。

## unicorn.log

E, [2016-09-08T15:02:36.583698 #5571] ERROR -- : uninitialized constant ExceptionNotifier::Rake (NameError)
/home/myapp/myapp/releases/20160908060023/config/initializers/exception_notification.rb:10:in `<top (required)>'

(以下略)

設定

preload_appはfalseで運用しているため、capistrano3-unicornのREADMEに従って、config/deploy.rbには下記の通り記述しています。

after 'deploy:publishing', 'deploy:restart'
namespace :deploy do
  task :restart do
    invoke 'unicorn:reload'
  end
end

この設定により cap production deploy を実行すると、SIGHUP をunicornに送ることになります。

Running /usr/bin/env kill -s HUP `cat /home/myapp/myapp/current/tmp/pids/unicorn.pid` on ***.***.***.***

原因

2つほど誤解(というか理解不足)がありました。

誤解その1

http://unicorn.bogomips.org/Sandbox.html に従って、before_exec で ENV["BUNDLE_GEMFILE"] をセットしていました。

before_exec do |server|
  ENV["BUNDLE_GEMFILE"] = File.join(project_home, "Gemfile")
end

が、これがうまく動作しません。

UnicornSIGHUPをハンドリングしている箇所を読んで見ると、preload_app=false では before_exec を実行しないことが判明。。。

誤解その2

そうは言っても https://unicorn.bogomips.org/SIGNALS.html のHUPには

When reloading the application, Gem.refresh will be called so updated code for your application can pick up newly installed RubyGems.

と書かれているじゃないか、と思うわけです。

実際、unicornコードやログを見ても、Gem.refresh を呼び出しているようにも見えます。

I, [2016-09-09T11:49:21.648309 #5967]  INFO -- : worker=0 spawned pid=5967
I, [2016-09-09T11:49:21.667362 #5967]  INFO -- : Refreshing Gem list
I, [2016-09-09T11:49:23.931829 #30636]  INFO -- : reaped #<Process::Status: pid 14112 exit 0> worker=1
I, [2016-09-09T11:49:23.933808 #5970]  INFO -- : worker=1 spawned pid=5970
I, [2016-09-09T11:49:23.934109 #5970]  INFO -- : Refreshing Gem list

が、どうも疑わしいので Bundler のコードを追いかけてみました。

    # Because Bundler has a static view of what specs are available,
    # we don't #refresh, so stub it out.
    def replace_refresh
      gem_class = (class << Gem; self; end)
      redefine_method(gem_class, :refresh) {}
    end

空っぽに置き換えられていることが判明。。。

どうすればよいか?

USR2 シグナルを使えばOKです。

Unicornのmasterプロセスは USR2 シグナルを受取ると、自身をforkして unicornコマンドをexecします。

I, [2016-09-09T16:54:40.831653 #10698]  INFO -- : executing ["/home/myapp/myapp/shared/bundle/ruby/2.2.0/bin/unicorn", "-c", "/home/myapp/myapp/current/config/unicorn/sandbox.rb", "-E", "deployment", "-D", {12=>#<Kgio::TCPServer:fd 12>}] (in /home/myapp/myapp/releases/20160909075156)

コードを見ると、execの直前でbefore_execを実行しているので、

before_exec do |server|
  ENV["BUNDLE_GEMFILE"] = File.join(project_home, "Gemfile")
end

でBUNDLE_GEMFILEをセットしておけばよいです。これで新しいGemfileをexecするプロセスで認識できます(この時点でcurrent symlinkは切り替え済み)。

なお、execで実行されるunicornのbinはGem.bin_pathを呼び出すのですが、BundlerがこのコードによりGem.bin_pathを書き換えているので新しいgemを認識できるということのようです。(詳しく追ってないので、もしかしたら間違っているかも)