問題
Rails アプリケーションのとある処理で Segmentation fault がランダムで発生してしまいます この場合、Rails アプリケーションのプロセスごと終了 (abort) してしまうため、エラーハンドリングができません。
やりたいこと
Segmentation fault が発生する可能性がある処理だけ別プロセスで実行し、かつ Segmentation fault の発生を検知したいです
バージョン情報
$ gcc -v
Apple clang version 14.0.0 (clang-1400.0.29.202)
Target: arm64-apple-darwin22.3.0
Thread model: posix
InstalledDir: /Library/Developer/CommandLineTools/usr/bin
$ ruby -v
ruby 3.2.2 (2023-03-30 revision e51014f9c0) [arm64-darwin22]
方法
まず Ruby で Segmentation fault を再現させるためのコードを用意します。実行すると必ず Segmentation fault を発生させる C 言語のファイルを用意して、それをもとに共有ライブラリを作成してから Ruby の fiddle で実行します。
#include <stdlib.h>
void produce_segmentation_fault() {
int *p = NULL;
*p = 1; // raise segmentation fault
}
$ gcc -shared -o produce_segmentation_fault.so produce_segmentation_fault.c
require 'fiddle'
handler = Fiddle.dlopen('./produce_segmentation_fault.so')
produce_segmentation_fault = Fiddle::Function.new(handler['produce_segmentation_fault'], [], Fiddle::TYPE_VOID)
produce_segmentation_fault.call # Segmentation fault が発生して、Ruby プロセスが終了する。
次に fork メソッドで子プロセスを生成し、そこで特定の処理を実行する手段を用意します。このとき、専用の IO オブジェクトを用意して、子プロセスで処理を実行した際の結果を親プロセスで受け取れるようにしています。ただし、オブジェクトのシリアライズ・デシリアライズに Marshal.dump, Marshal.#load メソッドを使っているため、Proc オブジェクトなどシリアライズできないオブジェクトは受け渡しできないことにご注意ください。なお、この実装は以下の記事を参考にしました。
class ChildProcessError < StandardError; end
def execute_in_child_process
read_io, write_io = IO.pipe
pid = fork do
read_io.close
result = yield
Marshal.dump(result, write_io)
exit!(0)
end
write_io.close
result = read_io.read
Process.wait(pid)
raise(ChildProcessError) if result.empty?
Marshal.load(result)
end
以上を組み合わせると、
- 子プロセスで特定の処理を実行して結果を受け取る。
- 子プロセスで Segmentation fault が発生してプロセスが終了した場合に、親プロセスでそれを検知する。
ということができます
require 'fiddle'
handler = Fiddle.dlopen('./produce_segmentation_fault.so')
produce_segmentation_fault = Fiddle::Function.new(handler['produce_segmentation_fault'], [], Fiddle::TYPE_VOID)
execute_in_child_process { 1 + 1 }
#=> 2
execute_in_child_process { produce_segmentation_fault.call }
# ChildProcessError