Search code examples
exceptioncrystal-langfibers

How to handle fiber exception outside of fiber?


Sometimes you need to work with unmaintained, old, dirty, huge and sort of libraries that can be dangerous for our program.

Is there have the best practices for execution this code in a safe way?

Recently I found (probably on my knowledge and experience level) non-catching exception. The common practice that I used until today is to wrap an code into Fiber, capture exception inside and sending out through Channel. For now it is not work (I can't put Yield or Proc inside Fiber).

The dangerous lib can be looks like common class with method that encapsulates the Fiber with Fiber.yield for swap execution to other fibers right now. In real life this Fiber may contain inside work with IO, it does not matter.

class LibDangerous
  def exec_remote
    spawn do
      raise IO::Error.new
    end
    Fiber.yield
  end
end

Wrapper that should be handle exception consists of nested methods over begin ... rescue. I call methods from the top-level and from the last wrapper method I return lib method that always blow up the program even with block begin ... rescue.

class Wrapper
  def capture
    begin
      yield self
    rescue
      puts "rescued from :capture"
    end
  end

  def guard
    begin
    capture do |this|
      yield this
    end
    rescue
      puts "rescued from :guard"
    end
  end

  def run
    begin
      yield LibDangerous.new
    rescue ex
      puts "rescued from :run"
    end
  end
end

This seems to be because you need to handle the exception at the same level where it happened, but I can't modify the code of someone else's library for a variety of reasons.

wrapper = Wrapper.new

result = wrapper.guard do |sandbox|
  begin
    sandbox.run do |library|
      library.exec_remote
    end
  rescue
    puts "rescued from top-level"
  end
end

Boom! (this code on play.crystal-lang.org)

Unhandled exception in spawn:  (IO::Error)
  from /eval:4:7 in '->'
  from /usr/lib/crystal/fiber.cr:255:3 in 'run'
  from /usr/lib/crystal/fiber.cr:92:34 in '->'
  from ???

It may happens due to swap executable contexts: my code and exception are in different contexts and can't interact? If you remove the Fiber then the Exception is caught as usual.

Is it possible to solve this without modifying the original library?


Solution

  • No, you cannot handle this without patching into the original faulty code. However Crystal's open class system makes this possible entirely from your side until the upstream behavior is fixed, you can just redefine the method in your code.

    Please note that this is only a problem in terms of dealing with the fact that the operation failed. In case you can obtain that information otherwise, for example by waiting on the result with a timeout using select, or you simply don't care if the operation succeeded or not, the only real problem is that of a bit of log spam. A fiber, that's not the main fiber, crashing doesn't take down your program! (see https://play.crystal-lang.org/#/r/98da)

    Why is this? Raising an exception means walking up the current stack until a handler is found. When you see "Unhanded exception", that's just the default handler Crystal puts at the root of each stack. Now what is a fiber? It's a separate stack! So raising inside a fiber does not unwind any other stacks, especially not your main fiber's.