Rubyのシグナルトラップを使う時のマナー

シグナルトラップの中からLoggerに書き込むと、 log writing failed. can't be called from trap context という出力が出て、書き込みに失敗します。
これはtrapの中でmutexを使っているせい、らしいです。

require "logger"
logger = Logger.new($stdout)

trap "TERM" do
  logger.info "got signal"
end

Process.kill :TERM, $$

昔は、mutexを使っていない、ただの File#wirte を行う https://rubygems.org/gems/mono_logger をtrapの中から使うというワークアラウンドが用いられていました。
今も一部で現役かもしれませんが、現代ではtrapの中からloggerへ書き込むことは避ける方向にあると思います。少なくともsidekiqはtrapの中でmono_loggerは使っておらず後述の実装を採用しています。

https://blog.tmtms.net/entry/2014/09/23/ruby-signal
昔の回避方法は、trap内でローカル変数でのフラグを立てて、そのフラグをポーリングしていたようですが、今はpipeを用いるのがオシャレだと思います。(N=1)

実装例は以下のとおりです。
基本的な方針は、trapの中ではpipe経由で文字列を送信して、メインスレッドで例外を投げて次の処理に繋げます。

self_read, self_write = IO.pipe
%w(TERM INT USR1 USR2).each do |sig|
  begin
    trap sig do
      self_write.puts(sig)
    end
  rescue Interrupt
    logger.info "シグナルを受け付けました"
  end
end

# ここでThreadを作るとかする

begin
  while(readable_io = IO.select([self_read]))
    signal = readable_io.first[0].gets.strip
      case signal
      when 'USR2', 'INT', 'TERM'
        raise Interrupt
    end
  end
rescue Interrupt
  # 終了処理をする
end

この実装ではtrapの中でmutexに触らず、最低限のことだけやるので安全です。

おわり