概要
Ruby によるマルチプロセスプログラミングでは、子プロセスの異常終了を親から知ることは可能だが、子プロセスが raise した exception を Ruby の標準エラーハンドリングで扱うことはできない。
つまり、親子プロセス間でエラー内容の共有を行いたい場合、何らかの工夫をする必要がある。
そこで、プロセス間通信 (Interprocess Communication) を実現可能なIO.pipe
を用いてプロセス間でのエラー情報共有を行った。
基本
まずはエラー情報の共有を行わず、子プロセスが異常終了していた場合に StandardError で落ちるようにしてみた。
pids = []
pids << fork do
puts 'forked!'
end
pids << fork do
fail 'failed!'
end
results = Process.waitall
results.each do |r|
fail unless pids.include?(r[0]) && r[1].success?
end
Process.waitall
は全ての子プロセスの終了を待つメソッドで、返り値は「子プロセスの pid と終了ステータス (Process::Status) の配列の配列」となっている。
上記のコードであれば具体的には以下のような値が格納されることになる。
pry(main)> results
=> [[8409, #<Process::Status: pid 8409 exit 0>], [8412, #<Process::Status: pid 8412 exit 1>]]
プログラムの実行結果は以下の通りとなる。
$ ruby sample.rb
forked!
sample.rb:8:in `block in <main>': failed! (RuntimeError)
from sample.rb:7:in `fork'
from sample.rb:7:in `<main>'
sample.rb:14:in `block in <main>': unhandled exception
from sample.rb:13:in `each'
子プロセスのエラー情報が出力されてはいるが、親プロセスからその情報を扱うことはできない。
子プロセス内でエラーハンドリング
このままだと気持ち悪いので、子プロセス内でエラーハンドリングを行うようにプログラムを修正する。
pids = []
pids << fork do
puts 'forked!'
end
pids << fork do
begin
fail 'failed!'
rescue => e
exit 1
end
end
results = Process.waitall
results.each do |r|
fail unless pids.include?(r[0]) && r[1].success?
end
実行結果は以下のとおりとなる。
$ ruby sample.rb
forked!
sample.rb:18:in `block in <main>': unhandled exception
from sample.rb:17:in `each'
from sample.rb:17:in `<main>'
問題なく子プロセスの異常終了が感知できている。
IO.pipe
IO.pipe は Linux/Unix のパイプのようなものを実現してくれるメソッドである。
rd, wr = IO.pipe wr.write "ping" wr.close rd.read #=> "ping"
IO.pipe の戻り値は2つの IO オブジェクトで、それぞれ読み込み用と書き込み用である。これら2つのオブジェクトを使用すると、パイプ機能を実現することができる。
プロセス間でのエラー情報共有
fork する前に IO.pipe を呼び出せば、子プロセスには親プロセスの IO オブジェクトのコピーが渡されることになる。
このコピーされたオブジェクトを通して、プロセス間通信を実現することができる。
rd, wr = IO.pipe
pids = []
pids << fork do
# 子プロセスでは書き込みしか行わない
rd.close
puts 'forked!'
end
pids << fork do
rd.close
begin
fail 'failed!'
rescue => e
# 今回の例では"クラス名,メッセージ\n"という形式を用いる
wr.write("#{e.class},#{e.message}\n")
exit 1
end
end
# 親プロセスでは読み込みしか行わない
wr.close
results = Process.waitall
# メッセージが格納されている場合
unless rd.eof?
# 格納した形式に応じて文字列をパースしエラーを生成
error_class, error_message = rd.read.split("\n").first.split(',')
raise Module.const_get(error_class).new(error_message)
end
results.each do |r|
fail unless pids.include?(r[0]) && r[1].success?
end
実際に実行してみると、以下のような結果となる。
$ ruby sample.rb
forked!
sample.rb:27:in `<main>': failed! (RuntimeError)
これで無事、親子プロセス間でエラー内容の共有を行うことができた。