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
が、これがうまく動作しません。
UnicornでSIGHUPをハンドリングしている箇所を読んで見ると、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を認識できるということのようです。(詳しく追ってないので、もしかしたら間違っているかも)