困っていたこと
Railsで構築したサービスで、modelの更新に応じてJSファイルを生成し直す処理があり、当然ながらそのJSを圧縮しています。その生成+圧縮+配置は sidekiq で処理しているのですが、この処理がフリーズするんですね。。最初はS3への配置で停まってるんだろうなぁと思っていたのですが、調べてみたら圧縮の部分でした。
原因
JSファイルの圧縮は Rails の asset pipeline と同様、Uglifier を使っていました。Ugilifier は ExecJSに処理を委譲していて、ExecJS が使う JSエンジンが The Ruby Racer でした。
で、The Ruby Racerはスレッドセーフだけどデッドロックの問題があるようで(参考)。。。
※実際に問題があった環境のバージョン等は次のとおりです。
(therubyracer以外は関係ないかと思いますが)
- rails : 3.2.14
- sidekiq : 2.7.5
- uglifier : 2.1.1
- therubyracer : 0.11.4
- sprockets : 2.2.1
- execjs : 1.4.0
Node.jsに任せて回避できました
ExecJSのソースを見てみると、JS用のランタイムを切り替えられるようになっているんですね。
module ExecJS
module Runtimes
Disabled = DisabledRuntime.new
RubyRacer = RubyRacerRuntime.new
RubyRhino = RubyRhinoRuntime.new
Johnson = JohnsonRuntime.new
Mustang = MustangRuntime.new
Node = ExternalRuntime.new(...)
JavaScriptCore = ExternalRuntime.new(...)
SpiderMonkey = Spidermonkey = ExternalRuntime.new(...)
JScript = ExternalRuntime.new(...)
Nodeだけじゃなくて色々と使えるライブラリがあるのですが、今回は開発と運用の両環境に入っていたNodeを採用しました。で、一発で上手くいったので他のランタイムは試していません。。他のでも上手くいったよーという方は是非教えてください!
ちなみにNodeが入っていない環境だと、こんな例外を出力して教えてくれます。
.../lib/execjs/runtimes.rb:66:in `from_environment':Node.js (V8) runtime is not available on this system (ExecJS::RuntimeUnavailable)
手順
sidekiqの起動時に環境変数で指定する
$ EXECJS_RUNTIME=Node sidekiq
基本は ExecJS が使うJSランタイムを Node にするだけだから簡単な話なのですが、この方法に落ち着くまでちょっとはまりました。ExecJS の中を見てみると
def self.autodetect
puts "#{from_environment}"
from_environment || best_available ||
raise(RuntimeUnavailable, "Could not find a JavaScript runtime. " +
"See https://github.com/sstephenson/execjs for a list of available runtimes.")
end
def self.best_available
runtimes.reject(&:deprecated?).find(&:available?)
end
def self.from_environment
if name = ENV["EXECJS_RUNTIME"]
if runtime = const_get(name)
if runtime.available?
runtime if runtime.available?
else
raise RuntimeUnavailable, "#{runtime.name} runtime is not available on this system"
end
elsif !name.empty?
raise RuntimeUnavailable, "#{name} runtime is not defined"
end
end
end
ってな感じになっていたので、initializerで指定しようとしました。
ENV["EXECJS_RUNTIME"] = "Node" #ダメだった
でもこれじゃダメでした。ExecJSがJSランタイムを選択するのは lib のロード時だけなので、initializersの読み込み前なんです。で、結局はプロセスの起動時に環境変数で指定するというこの方法に落ち着きました。
$ EXECJS_RUNTIME=Node sidekiq
ひとまず、めでたし。他のライブラリ/ランタイムの方が早いよorリソース消耗しないよーという情報があったら是非教えてくださいっ