LoginSignup
6
6

More than 5 years have passed since last update.

親子プロセス間でエラー情報を共有する方法

Last updated at Posted at 2015-07-02

概要

Ruby によるマルチプロセスプログラミングでは、子プロセスの異常終了を親から知ることは可能だが、子プロセスが raise した exception を Ruby の標準エラーハンドリングで扱うことはできない。

つまり、親子プロセス間でエラー内容の共有を行いたい場合、何らかの工夫をする必要がある。

そこで、プロセス間通信 (Interprocess Communication) を実現可能なIO.pipeを用いてプロセス間でのエラー情報共有を行った。

基本

まずはエラー情報の共有を行わず、子プロセスが異常終了していた場合に StandardError で落ちるようにしてみた。

sample.rb
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'

子プロセスのエラー情報が出力されてはいるが、親プロセスからその情報を扱うことはできない。

子プロセス内でエラーハンドリング

このままだと気持ち悪いので、子プロセス内でエラーハンドリングを行うようにプログラムを修正する。

sample.rb
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"

Process spawning patterns

IO.pipe の戻り値は2つの IO オブジェクトで、それぞれ読み込み用と書き込み用である。これら2つのオブジェクトを使用すると、パイプ機能を実現することができる。

プロセス間でのエラー情報共有

fork する前に IO.pipe を呼び出せば、子プロセスには親プロセスの IO オブジェクトのコピーが渡されることになる。

このコピーされたオブジェクトを通して、プロセス間通信を実現することができる。

sample.rb
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)

これで無事、親子プロセス間でエラー内容の共有を行うことができた。

参考文献

6
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
6