読者です 読者をやめる 読者になる 読者になる

Threadの中で即座に例外をあげる

Ruby

どういうことかというと、Threadを作って、
その中で例外が上がるようなものを作ったとしても、
joinしないと例外が上がりません。
joinを前提にしていないループするプログラムなどは
困ります。
例えばこんなヤツ。
ネットワークが切れたら例外が上がって、
最初から処理をやり直したいって場合。

#!/usr/bin/env ruby
#
require 'net/http'
begin
  http=Net::HTTP.new("d.hatena.ne.jp","80")
  Net::HTTP.version_1_2

  t=Thread.new do
    loop do
      timeout(5) do
        http.start do |http|
          req =Net::HTTP::Post.new("/xibbar")
          response = http.request(req)
        end
      end
      p :thread_loop
      sleep 10
    end
  end

  loop do
    p :mail_loop
    sleep 3
  end
rescue Timeout::Error =>ex
  puts ex.message
  puts "Timeout Error"
  sleep 10
  retry
rescue =>ex
  puts ex.message
  puts "Network unreacheable."
  sleep 10
  retry
end

ネットワークがつながっている場合は、

% ruby sample4.rb
:mail_loop
:thread_loop
:mail_loop
:mail_loop
:mail_loop
:thread_loop
:mail_loop

こんな感じです。
 
んで、このプログラムには重大な欠陥があって、
joinするまで、例外が上がらないのだ。
LANケーブルを外してやってみると、、、

% ruby sample4.rb
:mail_loop
:mail_loop
:mail_loop
:mail_loop
:mail_loop
:mail_loop
:mail_loop
:mail_loop
:mail_loop
:mail_loop
:mail_loop

mainの部分だけループしているのがわかります。
threadのループの中で例外が上がったら
即座にrescueしたいのに、joinするのを待ってしまいます。
これを回避するには、
Thread#abort_on_exception=true
を入れます。こうすると

% ruby sample.rb
:mail_loop
getaddrinfo: nodename nor servname provided, or not known
Network unreacheable.
:mail_loop
getaddrinfo: nodename nor servname provided, or not known
Network unreacheable.
:mail_loop
getaddrinfo: nodename nor servname provided, or not known
Network unreacheable.
:mail_loop
:thread_loop
:mail_loop

となり、意図した挙動になりました。
これは実行してからLANケーブルを入れた挙動です。
 
というわけで、きちんと動作するコード

#!/usr/bin/env ruby
#
require 'net/http'
begin
  http=Net::HTTP.new("d.hatena.ne.jp","80")
  Net::HTTP.version_1_2

  t=Thread.new do
    loop do
      timeout(5) do
        http.start do |http|
          req =Net::HTTP::Post.new("/xibbar")
          response = http.request(req)
        end
      end
      p :thread_loop
      sleep 10
    end
  end
  t.abort_on_exception=true

  loop do
    p :mail_loop
    sleep 3
  end
rescue Timeout::Error =>ex
  puts ex.message
  puts "Timeout Error"
  sleep 10
  retry
rescue =>ex
  puts ex.message
  puts "Network unreacheable."
  sleep 10
  retry
ensure
  t.kill
end

abort_on_exception=を追加して、
ensureでkillするようにしました。