bundle execを殺す
「バグの希釈」の項目を見て思い出した。
2015年に書いたコードの中で、一番意味不明で気が利いてたなーと自分では思っているやつは「lib/bundler/setup.rb
に空のファイルを置く」というものだったりする。会社で少し説明したんだけど、あまりうけなかったのでここに書いておこう。
ユビレジで販売している製品の中にユビレジエクステンションというのがあって、簡単に言うとRaspberry Piに電源とWi-FiアダプタとUSBハブを付けたもので、HTTPでJSONをPOSTするとレシートが印刷できる機械になっている。ふつーにDebianが動いていて、Rubyで書いたHTTPサーバとかLED点滅のためのdaemonとかレシート印刷のためのdaemonとかが動いている。問題は、特定の処理でHTTPサーバとかを再起動するときに、なんか上手く行かない子がいることが、出荷後に判明したことだった。調べてみると、再起動が上手くいかないというのは、タイムアウトしていることがわかった。
なんで上手くいく子と上手くいかない子がいるのかは良くわかっていない。SDカードを読み書きしながら動作しているので、なんかの拍子にSDカードがなんかダメになるんだと思う。SDカードをdd
でコピーして試すと平気で動いたりする。(これは本題ではない。)
さて、再起動と一言で言ってもなにに時間がかかっているのだろうか。主にprintfデバッグなどを繰り返したところ、Rubyプログラムの先頭にたどり着くまでに時間がかかっていることがわかった。どうしようもないじゃんそれ……とかいいつついろいろ試行錯誤していると、
$ bundle exec ruby -e "puts :hello"
と、
$ ruby -e "puts :hello"
で実行時間が大きく違うことがわかる。後者の方が速い。まあ当たり前感がある。Bundlerを起動してからrubyを起動するのと、rubyだけ起動するのでは、後者の方が速いのは道理だ。そして、出荷済のバージョンではうっかりbundle exec
しているのだった。
また、普通にGemfile
とかGemfile.lock
とかがある場所でbunde exec
を複数同時に実行するとめちゃくちゃ遅くなることもわかった。きっと、複数のrubyが一斉にbundler経由でgemをロードすると、CPUを使いすぎるとかディスクアクセスが多すぎるとかで、なかなか終わらないんだろう。ちなみにbundle exec
しないで3つのrubyを実行すると、あんまり遅くない。(「めちゃくちゃ」というのは「3つ同時に起動すると10倍時間がかかるようになる」くらいの感じ。「あんまり」というのは、3〜4倍くらいの感じ。)
$ bundle exec ruby -e "puts :hello" & bundle exec ruby -e "puts :hello" & bundle exec ruby -e "puts :hello"
これで、「普通に起動したときには上手く起動するのに、(特定の場面で)サービスを再起動したときには上手くいかない」理由もわかった。普通に起動するときには、LED点滅daemonだけは先に起動してから(起動中にLEDを点滅させたいので)レシート印刷daemonとHTTPサーバを起動するが、問題となっている再起動のときには3つを一斉に起動している。2つ同時起動の場合はなんとかタイムアウトせずにすむが、3つだとダメなことがある、そんなラインにタイムアウトを設定してしまったのだろう。
どうやって直したら良いだろうか。いくつか方法がある。
- サービス起動時の
bundle exec
をやめる - サービスを3つ同時に再起動しないようにする
- タイムアウトを長くする
どれでも良い。簡単に直せる。
……まだ出荷してなかったらね。そして既に出荷は開始されているのだった。
実は、こういうこともあろうかと、最近のバージョンにはiPadアプリ経由で更新ができる仕組みを仕込んである。あるんだけど、上に挙げたやつは、どれもソフトウェアアップデートでは更新ができないファイルに書かれたプログラムなのだった。うーむ。
どうなっているのか、もう少しちゃんと説明しよう。
- アプリケーションの本体は1個のディレクトリにすべて格納されている。
bin
とかlib
とかvendor
とかがあって、/var/ubiregi/1.3
とかに保存されて、/var/ubiregi/current
みたいなリンクを張る。 /etc/init.d
に置かれた起動スクリプトがbundle exec ruby -I /var/ubiregi/current/lib /var/ubiregi/current/bin/a.rb
などとしてサービスを起動する- ソフトウェアアップデートは
/usr/local/ubiregi
以下に展開すると良い感じになるようなディレクトリをtgzにしたものの形で処理される - ソフトウェアアップデートはファイルの展開しかしないので、
/etc/init.d
以下のファイルを変更することはできない - ソフトウェアアップデートはソフトウェアアップデートdaemonによって処理され、このソフトウェアアップデートdaemonをソフトウェアアップデートすることはできない
そういうわけで「bundle exec
で実行されたアプリケーションが、rubyが実行されるまでの間に、bundle exec
をなかったことにする」そんな方法がないか考えることになった。(回答は、LOAD_PATH
が通っている場所にbundler/setup.rb
を置くことである。最初に書いた通り。)
bundle exec
がやっていることを見ると、次の二つである。
bundle exec
で実行する実行ファイルをGemfile.lock
を見ながら探すRUBYOPT
環境変数に-rbundler/setup
を設定する
今回は1.は関係ない。RUBYOPT
ってなんだったっけ。
なるほど。そして-r
はrequire
だ。つまり、bundle exec
すると
require 'bundler/setup'
と書かなくて良くなるわけだ。ってことはlib/bundler/setup.rb
に空のファイルを置いておけば、Bundlerの処理をスキップできるのではないだろうか。問題は、このRUBYOPT
とコマンドライン引数の-I
のどっちが先に処理されるかである。(サービス起動のコマンドをもう一度。)
$ bundle exec ruby -I /var/ubiregi/current/lib /var/ubiregi/current/bin/a.rb
-I
の処理がRUBYOPT
よりも後なら、-rbundler/setup
は標準ライブラリを見に行ってしまうので、本物のbundler/setup.rb
をロードしてしまう。-I
の処理が先なら、/var/ubiregi/current/lib/bundler/setup.rb
をロードするので、アプリケーションで乗っ取ることができる。rubyのマニュアルにはなんとも書いてないので、実際に実行して試してみるしかない。
-I
が先だった。
というわけで、無事Bundlerを無効にする方法が見つかって、ソフトウェアアップデートによってこの問題は解決されたのだった。bundler/setup
がなくなるので、Bundlerの手を借りずにgemをロードしないといけないけど、それはStandaloneモードで解決できる。
--standalone[=<list>]
bundle-install(1) - Install the dependencies specified in your Gemfile
(ちなみにStandaloneモードだと起動がかなり速いので、Gemfile.lock
の処理がなんか変なことやってて遅い気がする。わからないけど。)
そういうわけで、空のファイルを追加する謎のコミットによって問題が解決されたのだった。(うっかり不必要にbundle exec
してたのが本当の問題なんだけど。)あと、あんまりやらないとは思うけど、うっかりbundler/setup.rb
みたいなファイルを作るとbundle exec
が動かなくなるかもしれないので、注意しましょう。