JRuby+Thorで-Xmx指定したりdaemonizeしたりする
JRuby 1.7.4でのお話。
JRubyでなんらかのサーバプログラムを作ろうと思うと、やることはいろいろあるが、とにかくヒープ容量 -Xmx2g みたいなのを指定できるようにしないとお話にならないし、だいたいデフォルトだと client VM になってしまう。普通のjavaだと -server -Xmx2g だがjrubyではこれを -J-server -J-Xmx2g みたいにして渡す。
が、これはjrubyコマンドのオプションとして渡すため、たとえば rubygem に実行ファイルを含めて配布したいときには普通には指定できない。実行ファイルが起動したとき、そのJVMでは既にヒープ容量が決まってしまっているからだ。
またdaemonizeしたい、というケースでもこれは割と困る。shが使えれば一発だけどrubygemの実行ファイルはすべてrubyスクリプトとして実行されちゃうんだよね……。
なので、解決方法としては以下のようになる。コマンド名を awesome_one としよう。
- awesome_one start で起動する
- 渡されたオプション(-Xmxなど)をピックアップする
- jruby コマンド経由で自分自身を実行(exec)しなおす
ユーザに叩かせたいのはサブコマンド start だが、ここに「実行しなおす」時用のコードも含めて全部突っ込んでしまうとユーザに起動されたときと「実行しなおし」で起動されたときを何らかの方法(環境変数? 隠しオプション?)で区別しないといけないし、コードがたいへんみにくくなる。ので、サブコマンドは分けてしまおう。実行しなおし用は actualstart とでもしておこうか。
つまり 'awesome_one start' -> '(jruby -Xmx..) awesome_one actualstart' のような起動順序の関係になる。
しかし start と actualstart では使えるコマンドラインオプションは共通でなければならない。でないと起動時に Thor に怒られる。のでこれをDRY化する。
いっぽう awesome_one help したときに actualstart の方が見えてしまうとちょっと不恰好だ。隠しておきたい。
ということで、最終的にはこんな感じでコードを書いた。これで rubygem として配布できる thor なdaemonが作れる。
実際には pidfile をコマンドラインオプションで指定できるようにしたり、標準出力のリダイレクト先(awesome_one.out)を指定できるようにしたり、あれやこれや色々やらないといけないけど。まあ骨格としてはこれでいいんじゃないでしょうか。ついでに stop コマンドも付けておいた。
module AwesomeOne module CLIUtil def self.start_options(klass) option :daemonize, :type => :boolean, :default => false option :host, :type => :string, :default => '0.0.0.0' option :port, :type => :numeric, :default => 10080 # other options for 'start' and 'actualstart' ... end end class CLI < Thor # for start CLIUtil.start_options(self) # add options option :version, :type => :boolean, :default => false # start specific options here. desc "start [options]", "start this awesome server process" def start(*options) # -Xmx などがinvalid optionとしてThorに怒られるのを防ぐためここで任意の引数を受け取れる必要がある ARGV.shift # discard "start" # ARGVには解析済みオプションも含めて入力されたものが全て入ってる、サブコマンドも next_argv = ["actualstart"] jruby_options = ["-J-server"] ARGV.each do |arg| if arg =~ /^(-X.*)$/ jruby_options.push "-J#{$1}" else next_argv.push arg end end # this file is "lib/awesome_one/cli.rb" and bin file is "bin/awesome_one" binpath = File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "bin", "awesome_one")) args = jruby_options + [binpath] + next_argv if options[:daemonize] out_file_path = '/tmp/awesome_one.out' # to check child process execution File.open(out_file_path, 'w'){|file| file.write "write from parent process"} pid_file_path = '/tmp/awesome_one.pid' pid_file = File.open(pid_file_path, 'w') pid = spawn("jruby", *args, :pgroup => 0) pid_file.write pid.to_s pid_file.close # wait until child process successfully executed waiting_child = true while waiting_child && sleep(1) out_file_text = File.open(out_file_path){|f| f.readline} waiting_child = false if out_file_text =~ /working on #{pid}/ end Process.detach(pid) else exec('jruby', *args) end end # for actualstart CLIUtil.start_options(self) # add options as same as 'start' desc "actualstart [options]", "hidden subcommand", :hide => true # hide in help def actualstart if options[:daemonize] Dir.chdir("/") out_file_path = '/tmp/awesome_one.out' STDIN.reopen("/dev/null") out_file = File.open(out_file_path, 'w') STDOUT.reopen(out_file) STDERR.reopen(out_file) puts "working on #{$PID}" end # ... actual server process code ... end desc "stop", "stop daemonized process" def stop unless test(?r, "/tmp/awesome_one.pid") puts "Cannot find pid file" exit(1) end pid = File.open("/tmp/awesome_one.pid"){|f| f.read}.to_i timeout = Time.now + 5 # magic! waiting = true Process.kill(:TERM, pid) begin while waiting && Time.now < timeout sleep(0.5) status = Process.waitpid(pid, Process::WNOHANG) if status waiting = false end end rescue Errno::ECHILD waiting = false end if waiting puts "Faild to stop server #{pid}" exit(1) else File.unlink("/tmp/awesome_one.pid") end end end end
これで、JRubyでも、rubygems.orgで配れる、JVMのオプションも指定できてdaemonizeもできるサーバプログラムが書ける。
べんり!