Question

I'm using a gem that's throwing an exception in a background thread as below. I'd like to catch this exception but not sure about how to go about it. How would one go about handling exceptions in library threads?

#this class is in my code
class MQTT
  def self.connect
      @client = Client.connect(options)
  end
ende

This class is in the library which is packaged as a gem, so I technically don't have access to it:

class Client
  def self.connect(*args, &block)
    client = Client.new(*args)
    client.connect(&block)
    return client
  end

  def connect(clientid=nil)
      # Start packet reading thread
      @read_thread = Thread.new(Thread.current) do |parent|
        Thread.current[:parent] = parent
        loop { receive_packet }
      end
  end

  def receive_packet
    begin
      # Poll socket - is there data waiting?
      result = IO.select([@socket], nil, nil, SELECT_TIMEOUT)

      # Pass exceptions up to parent thread
    rescue Exception => exp
      unless @socket.nil?
        @socket.close
        @socket = nil
      end
      Thread.current[:parent].raise(exp)
    end
  end
end
Was it helpful?

Solution

I think you have 3 options.

You could return the exception to the calling thread:

def receive_packet
  raise "Exception in #{Thread.current}"
rescue Exception => exp
  return exp
end

t1 = Thread.new do
  receive_packet
end
puts "t1: #{t1.value.inspect}"

You could catch the exception on joining the thread (note you could reraise here or use an ensure block to make sure your socket is closed):

def receive_packet
  raise "Exception in #{Thread.current}"

rescue Exception => exp
  # reraise the exception
  raise exp
end

t = Thread.new do
  receive_packet
end

begin
  t.join
rescue => e
  puts "Exception caught from joined thread #{e.message} "
end

or you set #abort_on_exception = true so that exceptions kill all threads:

Thread.abort_on_exception = true
begin
  Thread.new do
    receive_packet
  end
  sleep 1
rescue => e
  puts "Exception raised immediately to main thread: #{e.message}"
end

Update Based on what you have above and your comment I guess you need to wait for the threads calling receive_packet to finish. So you would have to join them:

class Client
  def self.connect(*args, &block)
    client = Client.new(*args)
    client.connect(&block)
    return client
  end

  def initialize(args)
    @count = 0
  end

  def connect(clientid=nil)

    puts "Connecting. Thread.current is #{Thread.current}"
    # Start packet reading thread
    @read_thread = Thread.new(Thread.current) do |parent|
      Thread.current[:parent] = parent
      loop { receive_packet }
    end
  end

  def receive_packet
    begin
      # Poll socket - is there data waiting?
      # result = IO.select([@socket], nil, nil, SELECT_TIMEOUT)

      sleep 0.1
      @count += 1

      puts "count is now #{@count}"
      if @count == 3
        raise "WOOT: #{@count}"
      end

      # Pass exceptions up to parent thread
    rescue Exception => exp
      unless @socket.nil?
        @socket.close
        @socket = nil
      end
      puts "Reraising error #{exp.inspect} from #{Thread.current} to #{Thread.current[:parent]}"
      Thread.current[:parent].raise(exp)
    end
  end
end

class MQTT
  def self.connect
    @client = Client.connect(options = {})
  end
end

begin
  MQTT.connect

  Thread.list.each do |t|
    # Wait for the thread to finish if it isn't this thread (i.e. the main thread).
    t.join if t != Thread.current
  end

rescue => e
  puts "Exception from child thread: #{e.inspect}"
end
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top