ちょっとハマったのでメモ。
Ruby の Timeout ライブラリを使うと、一定の時間が過ぎても終わらない処理を中断することができます。
require 'timeout' def hoge sleep end def main Timeout.timeout(3) do hoge end rescue Timeout::Error => e p 'main: timeout', e.backtrace.first end main
このスクリプトを実行すると3秒たってから終了します。
% ruby t1.rb "main: timeout" "t1.rb:4:in `sleep'"
バックトレースから 4行目の sleep 中でタイムアウトが発生したことがわかります。
次に Timeout.timeout を入れ子にしてみます。
require 'timeout' def hoge Timeout.timeout(5) do sleep end rescue Timeout::Error => e p 'hoge: timeout', e.backtrace.first end def main Timeout.timeout(3) do hoge end rescue Timeout::Error => e p 'main: timeout', e.backtrace.first end main
main 中のタイムアウトは3秒で、hoge 中のタイムアウトは5秒です。 ですので sleep 実行中に main のタイムアウトが発生します。 この場合 main と hoge のどちらの rescue で Timeout::Error をキャッチできるのでしょうか。 プログラムを見ると sleep に近い hoge の rescue でキャッチできそうに思えますが、実行してみると次のようになります。
% ruby t2.rb "main: timeout" "t2.rb:5:in `sleep'"
正解は main の rescue でした。
使い勝手としてはこの方が望ましいでしょう。外側で指定されたタイムアウトが切れた場合に内側のタイムアウト処理が動いてしまっては混乱してしまいますし、使用しているライブラリが内部で Timeout を使用しているかどうかを調べないといけないというのは大変です。
Timeout ライブラリが内部でうまいことやって、利用者にとって自然な振る舞いになるような仕組みになっています。
ところで Timeout.timeout にはタイムアウト時に発生する例外クラスを指定することができます。 何も指定しないと上記のように Timeout::Error が発生します。
ところが、Timeout::Error を渡してみると動きが異なります。
require 'timeout' def hoge Timeout.timeout(5) do sleep end rescue Timeout::Error => e p 'hoge: timeout', e.backtrace.first end def main Timeout.timeout(3, Timeout::Error) do hoge end rescue Timeout::Error => e p 'main: timeout', e.backtrace.first end main
% ruby t3.rb "hoge: timeout" "t3.rb:5:in `sleep'"
hoge の rescue が実行されてしまいました。
Timeout.timeout に例外クラスを指定した場合は、「Timeout ライブラリが内部でうまいことやってる仕組み」が働かず、そのまま指定した例外が発生するためです。
Timeout.timeout を入れ子にしてなくても、次のプログラムを実行すると、
require 'timeout' def hoge sleep rescue p '何か失敗した!' end class OreOreTimeout < StandardError end def main Timeout.timeout(3, OreOreTimeout) do hoge end rescue OreOreTimeout p 'タイムアウト!' end main
「タイムアウト!」ではなく「何か失敗した!」が表示されます。
% ruby t4.rb "何か失敗した!"
OreOreTimeout は StandardError のサブクラスなので rescue で拾われてしまうためです。 StandardError ではなく Exception のサブクラスにすれば rescue で拾われないため、「タイムアウト!」になります。
Timeout.timeout に例外クラスを指定する場合は注意しましょう。