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?
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.