Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How should we rescue Java Throwables in the plugin threads and the pipeline thread #4064

Closed
guyboertje opened this issue Oct 20, 2015 · 23 comments
Labels

Comments

@guyboertje
Copy link
Contributor

Background: any Java Throwable exceptions/errors thrown by JVM, JRuby extensions and JRuby are wrapped by JavaProxy and so are subclassed of Object and not Exception. This means that rescue Exception => e will not rescue them. Only rescue Object => e will catch Java Throwable instances.

This has big implications for how we log all errors and how to decide whether to raise, retry or not nothing.

We should revisit the change that @ph made #3990 and reconsider @jsvd PR #2386

@jsvd
Copy link
Member

jsvd commented Oct 20, 2015

I did some experiments to see how rescue Object behaves under abort_on_exception:

jruby-1.7.22 :001 > Thread.abort_on_exception = true
 => true 
jruby-1.7.22 :022 > begin; t = Thread.new { raise Exception.new }; sleep 5; rescue Object; puts "got exception"; end
got exception
 => nil 
jruby-1.7.22 :023 > begin; t = Thread.new { raise StandardError.new }; sleep 5; rescue Object; puts "got exception"; end
got exception
 => nil 
jruby-1.7.22 :024 > begin; t = Thread.new { raise StandardError }; sleep 5; rescue Object; puts "got exception"; end
got exception
 => nil 
jruby-1.7.22 :025 > begin; t = Thread.new { raise Exception }; sleep 5; rescue Object; puts "got exception"; end
got exception
 => nil 
jruby-1.7.22 :026 > begin; t = Thread.new { raise java.lang.IllegalArgumentException }; sleep 5; rescue Object; puts "got exception"; end
got exception
 => nil 
jruby-1.7.22 :027 > begin; t = Thread.new { raise java.lang.IllegalArgumentException.new }; sleep 5; rescue Object; puts "got exception"; end
Exception in thread "Ruby-0-Thread-24: (irb):27" java.lang.IllegalArgumentException
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:408)
    at org.jruby.javasupport.JavaConstructor.newInstanceDirect(JavaConstructor.java:261)
    at org.jruby.java.invokers.ConstructorInvoker.call(ConstructorInvoker.java:70)
    at org.jruby.java.invokers.ConstructorInvoker.call(ConstructorInvoker.java:156)
    at org.jruby.runtime.callsite.CachingCallSite.callBlock(CachingCallSite.java:143)
    at org.jruby.runtime.callsite.CachingCallSite.call(CachingCallSite.java:149)
    at org.jruby.java.proxies.ConcreteJavaProxy$InitializeMethod.call(ConcreteJavaProxy.java:52)
    at org.jruby.runtime.callsite.CachingCallSite.callBlock(CachingCallSite.java:143)
    at org.jruby.runtime.callsite.CachingCallSite.call(CachingCallSite.java:149)
    at org.jruby.RubyClass.newInstance(RubyClass.java:850)
    at org.jruby.RubyClass$INVOKER$i$newInstance.call(RubyClass$INVOKER$i$newInstance.gen)
    at org.jruby.internal.runtime.methods.JavaMethod$JavaMethodZeroOrNBlock.call(JavaMethod.java:280)
    at org.jruby.java.proxies.ConcreteJavaProxy$NewMethod.call(ConcreteJavaProxy.java:148)
    at org.jruby.runtime.callsite.CachingCallSite.cacheAndCall(CachingCallSite.java:306)
    at org.jruby.runtime.callsite.CachingCallSite.call(CachingCallSite.java:136)
    at org.jruby.ast.CallNoArgNode.interpret(CallNoArgNode.java:60)
    at org.jruby.ast.FCallOneArgNode.interpret(FCallOneArgNode.java:36)
    at org.jruby.ast.NewlineNode.interpret(NewlineNode.java:105)
    at org.jruby.evaluator.ASTInterpreter.INTERPRET_BLOCK(ASTInterpreter.java:112)
    at org.jruby.runtime.Interpreted19Block.evalBlockBody(Interpreted19Block.java:206)
    at org.jruby.runtime.Interpreted19Block.yield(Interpreted19Block.java:194)
    at org.jruby.runtime.Interpreted19Block.call(Interpreted19Block.java:125)
    at org.jruby.runtime.Block.call(Block.java:101)
    at org.jruby.RubyProc.call(RubyProc.java:290)
    at org.jruby.RubyProc.call(RubyProc.java:228)
    at org.jruby.internal.runtime.RubyRunnable.run(RubyRunnable.java:99)
    at java.lang.Thread.run(Thread.java:745)
 => 5 

@guyboertje
Copy link
Contributor Author

@jsvd - how did IRB rescue that java exception and not die?

@guyboertje
Copy link
Contributor Author

More weirdness:
In jirb for...

def runner5
  STDERR.puts Thread.current.inspect
  th = Thread.new do
    begin
      STDERR.puts Thread.current.inspect
      raise java.lang.ArithmeticException.new
    rescue Object => e
      STDERR.puts 'Rescue in child thread', e.class
      raise e
    ensure
      STDERR.puts 'Ensure in child thread'
    end
  end
  th.join
rescue Object => e
  STDERR.puts 'Rescue in main thread', e.class
ensure
  STDERR.puts 'Ensure in main thread'
end

this output:

runner5
#<Thread:0x1b5bc39d run>
#<Thread:0x4cc3e8c6 run>
Rescue in child thread
Java::JavaLang::ArithmeticException
Ensure in child thread
Exception in thread "Ruby-0-Thread-21: (irb):205" java.lang.ArithmeticException
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:422)
    at org.jruby.javasupport.JavaConstructor.newInstanceDirect(JavaConstructor.java:261)
    at org.jruby.java.invokers.ConstructorInvoker.call(ConstructorInvoker.java:70)
    at org.jruby.java.invokers.ConstructorInvoker.call(ConstructorInvoker.java:156)
    at org.jruby.runtime.callsite.CachingCallSite.cacheAndCall(CachingCallSite.java:316)
    at org.jruby.runtime.callsite.CachingCallSite.callBlock(CachingCallSite.java:145)
    at org.jruby.runtime.callsite.CachingCallSite.call(CachingCallSite.java:149)
    at org.jruby.java.proxies.ConcreteJavaProxy$InitializeMethod.call(ConcreteJavaProxy.java:52)
    at org.jruby.runtime.callsite.CachingCallSite.cacheAndCall(CachingCallSite.java:316)
    at org.jruby.runtime.callsite.CachingCallSite.callBlock(CachingCallSite.java:145)
    at org.jruby.runtime.callsite.CachingCallSite.call(CachingCallSite.java:149)
    at org.jruby.RubyClass.newInstance(RubyClass.java:850)
    at org.jruby.RubyClass$INVOKER$i$newInstance.call(RubyClass$INVOKER$i$newInstance.gen)
    at org.jruby.internal.runtime.methods.JavaMethod$JavaMethodZeroOrNBlock.call(JavaMethod.java:280)
    at org.jruby.java.proxies.ConcreteJavaProxy$NewMethod.call(ConcreteJavaProxy.java:148)
    at org.jruby.runtime.callsite.CachingCallSite.cacheAndCall(CachingCallSite.java:306)
    at org.jruby.runtime.callsite.CachingCallSite.call(CachingCallSite.java:136)
    at org.jruby.ast.CallNoArgNode.interpret(CallNoArgNode.java:60)
    at org.jruby.ast.FCallOneArgNode.interpret(FCallOneArgNode.java:36)
    at org.jruby.ast.NewlineNode.interpret(NewlineNode.java:105)
    at org.jruby.ast.BlockNode.interpret(BlockNode.java:71)
    at org.jruby.ast.RescueNode.executeBody(RescueNode.java:221)
    at org.jruby.ast.RescueNode.interpret(RescueNode.java:116)
    at org.jruby.ast.EnsureNode.interpret(EnsureNode.java:96)
    at org.jruby.ast.BeginNode.interpret(BeginNode.java:83)
    at org.jruby.ast.NewlineNode.interpret(NewlineNode.java:105)
    at org.jruby.evaluator.ASTInterpreter.INTERPRET_BLOCK(ASTInterpreter.java:112)
    at org.jruby.runtime.Interpreted19Block.evalBlockBody(Interpreted19Block.java:206)
    at org.jruby.runtime.Interpreted19Block.yield(Interpreted19Block.java:194)
    at org.jruby.runtime.Interpreted19Block.call(Interpreted19Block.java:125)
    at org.jruby.runtime.Block.call(Block.java:101)
    at org.jruby.RubyProc.call(RubyProc.java:290)
    at org.jruby.RubyProc.call(RubyProc.java:228)
    at org.jruby.internal.runtime.RubyRunnable.run(RubyRunnable.java:99)
    at java.lang.Thread.run(Thread.java:745)
Ensure in main thread
=> #<Thread:0x4cc3e8c6 dead>

Rescue in child thread is printed
Ensure in child thread is printed
Ensure in main thread is printed
Rescue in main thread in NOT printed
huh?

Maybe it is IRB doing something different
Will try a run

@guyboertje
Copy link
Contributor Author

For a running program same outcome:

$ cat ./runner.rb
Thread.abort_on_exception = true

def runner
  STDERR.puts Thread.current.inspect
  th = Thread.new do
    begin
      STDERR.puts Thread.current.inspect
      raise java.lang.ArithmeticException.new
    rescue Object => e
      STDERR.puts 'Rescue in child thread', e.class
      raise e
    ensure
      STDERR.puts 'Ensure in child thread'
    end
  end
  th.join
rescue Object => e
  STDERR.puts 'Rescue in main thread', e.class
ensure
  STDERR.puts 'Ensure in main thread'
end

runner

$ jruby ./runner.rb
#<Thread:0x52af6cff run>
#<Thread:0x62a73af4 run>
Rescue in child thread
Java::JavaLang::ArithmeticException
Ensure in child thread
Exception in thread "Ruby-0-Thread-2: ./runner.rb:1" java.lang.ArithmeticException
  at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
  at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
  at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
  at java.lang.reflect.Constructor.newInstance(Constructor.java:422)
  at org.jruby.javasupport.JavaConstructor.newInstanceDirect(JavaConstructor.java:261)
  at org.jruby.java.invokers.ConstructorInvoker.call(ConstructorInvoker.java:70)
  at org.jruby.java.invokers.ConstructorInvoker.call(ConstructorInvoker.java:156)
  at org.jruby.runtime.callsite.CachingCallSite.cacheAndCall(CachingCallSite.java:316)
  at org.jruby.runtime.callsite.CachingCallSite.callBlock(CachingCallSite.java:145)
  at org.jruby.runtime.callsite.CachingCallSite.call(CachingCallSite.java:149)
  at org.jruby.java.proxies.ConcreteJavaProxy$InitializeMethod.call(ConcreteJavaProxy.java:52)
  at org.jruby.runtime.callsite.CachingCallSite.cacheAndCall(CachingCallSite.java:316)
  at org.jruby.runtime.callsite.CachingCallSite.callBlock(CachingCallSite.java:145)
  at org.jruby.runtime.callsite.CachingCallSite.call(CachingCallSite.java:149)
  at org.jruby.RubyClass.newInstance(RubyClass.java:850)
  at org.jruby.RubyClass$INVOKER$i$newInstance.call(RubyClass$INVOKER$i$newInstance.gen)
  at org.jruby.internal.runtime.methods.JavaMethod$JavaMethodZeroOrNBlock.call(JavaMethod.java:280)
  at org.jruby.java.proxies.ConcreteJavaProxy$NewMethod.call(ConcreteJavaProxy.java:148)
  at org.jruby.runtime.callsite.CachingCallSite.cacheAndCall(CachingCallSite.java:306)
  at org.jruby.runtime.callsite.CachingCallSite.call(CachingCallSite.java:136)
  at $_dot_.runner.chained_4_rescue_2$RUBY$SYNTHETICrunner(./runner.rb:8)
  at $_dot_.runner.chained_3_ensure_2$RUBY$__ensure__(./runner.rb)
  at $_dot_.runner.block_0$RUBY$runner(./runner.rb:6)
  at $_dot_$runner$block_0$RUBY$runner.call($_dot_$runner$block_0$RUBY$runner)
  at org.jruby.runtime.CompiledBlock19.yield(CompiledBlock19.java:159)
  at org.jruby.runtime.CompiledBlock19.call(CompiledBlock19.java:87)
  at org.jruby.runtime.Block.call(Block.java:101)
  at org.jruby.RubyProc.call(RubyProc.java:290)
  at org.jruby.RubyProc.call(RubyProc.java:228)
  at org.jruby.internal.runtime.RubyRunnable.run(RubyRunnable.java:99)
  at java.lang.Thread.run(Thread.java:745)
Ensure in main thread

@guyboertje
Copy link
Contributor Author

Same results when begin rescue is in the main code:

$ cat ./runner.rb
Thread.abort_on_exception = true

begin
  STDERR.puts Thread.current.inspect
  th = Thread.new do
    begin
      STDERR.puts Thread.current.inspect
      raise java.lang.ArithmeticException.new
    rescue Object => e
      STDERR.puts 'Rescue in child thread', e.class
      raise e
    ensure
      STDERR.puts 'Ensure in child thread'
    end
  end
  th.join

rescue Object => e
  STDERR.puts 'Rescue in main thread', e.class
ensure
  STDERR.puts 'Ensure in main thread'
end

@headius
Copy link

headius commented Oct 21, 2015

Ok, it looks like we made a decision some time ago (caa7f5ad7b8d8148384be33981f7cf3880377df0) to only propagate subclasses of java.lang.Error on the main thread for abort_on_exception = true. That was probably a conservative approach; there's always concerns about re-raising exceptions on a thread that does not expect them.

However, I think it may have been too conservative.

It should be an expectation of any code calling into Ruby that it could raise any exception, even checked exceptions normally not expected on that thread. This is due to abort_on_exception, but also do to Ruby's dynamic nature; it's not possible to see all exceptions code might raise.

Here's a proposed diff that allows all Throwable to propagate out of a child thread through the main thread:

diff --git a/core/src/main/java/org/jruby/RubyThread.java b/core/src/main/java/org/jruby/RubyThread.java
index 2e39b1d..72bb924 100644
--- a/core/src/main/java/org/jruby/RubyThread.java
+++ b/core/src/main/java/org/jruby/RubyThread.java
@@ -1217,7 +1217,7 @@ public class RubyThread extends RubyObject implements ExecutionContext {
         assert isCurrent();

         Ruby runtime = getRuntime();
-        if (abortOnException(runtime) && exception instanceof Error) {
+        if (abortOnException(runtime)) {
             // re-propagate on main thread
             runtime.getThreadService().getMainThread().raise(JavaUtil.convertJavaToUsableRubyObject(runtime, exception));
         } else {

@headius
Copy link

headius commented Oct 21, 2015

A possible workaround, by setting the default uncaught exception handler for a given Ruby thread (untested code):

native_thread = JRuby.reference(Thread.current).nativeThread
native_thread.set_uncaught_exception_handler {|th, t| Thread.main.raise t}

Similar could be done in Java with a bit less magic and a bit less overhead.

Edit: fixed set_uncaught_exception_handler method call.

@guyboertje
Copy link
Contributor Author

After debugging with @headius on IRC this works:

Thread.abort_on_exception = true

begin
  STDERR.puts Thread.current.inspect
  th = Thread.new do

    JRuby.reference(Thread.current).nativeThread.set_uncaught_exception_handler {|th, t| Thread.main.raise t}

    begin
      STDERR.puts Thread.current.inspect
      raise java.lang.ArithmeticException.new
    rescue Object => e
      STDERR.puts 'Rescue in child thread', e.class
      raise e
    ensure
      STDERR.puts 'Ensure in child thread'
    end
  end
  th.join

rescue Object => e
  STDERR.puts 'Rescue in main thread', e.class
ensure
  STDERR.puts 'Ensure in main thread'
end

gives output:

#<Thread:0x67b92f0a run>
#<Thread:0x7edcab05 run>
Rescue in child thread
Java::JavaLang::ArithmeticException
Ensure in child thread
Rescue in main thread
Java::JavaLang::ArithmeticException
Ensure in main thread

@headius
Copy link

headius commented Oct 21, 2015

Now, I don't necessarily recommend this workaround for general use, but within a controlled environment like Logstash it's less of a concern.

@guyboertje
Copy link
Contributor Author

Sure. It means that every child thread we define will have to have that extra line.

@guyboertje
Copy link
Contributor Author

I am going to send a failing spec to the jruby team - maybe we will have a fix in 1.7.23 and 9000

Should we wait until then?

@guyboertje
Copy link
Contributor Author

We can as part of #2386 in the child thread handler also rescue Object => e and convert e into a Ruby Exception or our own CaughtJavaException < Exception and re-raise that, then we will not need the JRuby patch or work around.

@jsvd WDYT?

@jordansissel
Copy link
Contributor

@guyboertje I think your proposal is good - to rescue Object and reraise it wrapped in something ilke a CaughtJavaException.

@andrewvc
Copy link
Contributor

As a style concern should we write code like

begin
  do_something_sketchy
rescue StandardError => e
  do_something_with(e.backtrace)
rescue Object => e
  do_something_with(e.stacktrace)
end

since IIRC the interfaces are different?

@colinsurprenant
Copy link
Contributor

I am not sure why the Java set_uncaught_exception_handler is necessary here.

Why can't we just wrap the worker(s) in a rescue block similar to @ph suggestion in #3990 but instead of just plain re-raising, we wrap that exception in a WorkerException with something like:

class WorkerException < Exception
  attr_reader :cause

  def initialize(message = nil, cause = nil)
    super(message)
    @cause = cause
  end

  def to_s
    "#{super.inspect}, cause=#{@cause.inspect}"
  end
end

def filterworker
  LogStash::Util.set_thread_name("|worker")
  begin
    ...
  rescue Exception => e
    raise(WorkerException.new(e.message, e))
  ensure
    ...
  end
end

@guyboertje and your example above could be something like that:

Thread.abort_on_exception = true

class WorkerException < Exception
  attr_reader :cause

  def initialize(message = nil, cause = nil)
    super(message)
    @cause = cause
  end

  def to_s
    "#{super.inspect}, cause=#{@cause.inspect}"
  end
end


begin
  STDERR.puts Thread.current.inspect

  th = Thread.new do
    begin
      STDERR.puts "Child thread #{Thread.current.inspect}"
      raise java.lang.ArithmeticException.new("foo")
    rescue Exception => e
      STDERR.puts "Rescue in child thread #{e.inspect}"
      raise WorkerException.new(e.message, e)
    ensure
      STDERR.puts 'Ensure in child thread'
    end
  end

  th.join
  sleep(5)
rescue Exception => e
  STDERR.puts "Rescue in main thread #{e.inspect}"
ensure
  STDERR.puts 'Ensure in main thread'
end

Am I missing something here?

@guyboertje
Copy link
Contributor Author

@colinsurprenant - the workaround to set the uncaught... was a suggestion from headius. I dont think its necessary. Your suggestion is similar to mine and @andrewvc added to it too.

This is ultimately something that should go into @jsvd PR #2386

@colinsurprenant
Copy link
Contributor

@guyboertje ah, sorry, I missed your comment about CaughtJavaException - in any case, I'd vote for this solution. Also, note that if doing this, using rescue Object is not necessary.

@guyboertje
Copy link
Contributor Author

@colin - rescue Object is necessary. Can you be certain that the error raised in the tests is truly a java based exception and not one that looks like it is.

Looking at the jruby code and on IRC with headius a true java exception e.g. OOM, UOE or NPE etc is not converted to a RubyException. See this https://github.com/jruby/jruby/blob/1.7.22/core/src/main/java/org/jruby/RubyThread.java#L1211 and https://github.com/jruby/jruby/blob/1.7.22/core/src/main/java/org/jruby/runtime/Helpers.java#L3015

headius asked me to provide a test case showing this problem. His proposal will fix this problem.

@colinsurprenant
Copy link
Contributor

Here's the result of all exception handling combinations for both java.lang.Exception and java.lang.Error base Java exceptions on 1.7.22 + Java 8.

First I created this simple Java exception generator class:

public class Foo {
  public static void javaThrow(String className) throws Throwable {
    Class clazz = Class.forName(className);
    throw (Throwable)clazz.newInstance();
  }
}

1- thread rescued & re-raised exception

require "java"
java_import "Foo"

Thread.abort_on_exception = true

begin
  STDERR.puts("main thread #{Thread.current.inspect}")

  th = Thread.new do
    begin
      STDERR.puts("rescued block child thread #{Thread.current.inspect} throwing #{ARGV[0]}")
      Foo.javaThrow(ARGV[0])
    rescue Exception => e
      STDERR.puts("rescued block rescue in child thread #{e.inspect}")
      raise(e)
    ensure
      STDERR.puts("rescued block ensure in child thread")
    end
  end
  th.join

  sleep(5)
rescue Exception => e
  STDERR.puts("main thread rescue Exception #{e.inspect}")
rescue Object => e
  STDERR.puts("main thread rescue Object #{e.inspect}")
ensure
  STDERR.puts("main thread ensure")
end

1.1 - java.lang.Error base exception (java.lang.OutOfMemoryError)

$ ruby rescued.rb java.lang.OutOfMemoryError
main thread #<Thread:0x234bef66 run>
rescued block child thread #<Thread:0x21ff0d13 run> throwing java.lang.OutOfMemoryError
rescued block rescue in child thread java.lang.OutOfMemoryError
rescued block ensure in child thread
main thread rescue Exception java.lang.OutOfMemoryError
main thread ensure

1.2 - java.lang.Exception base exception (java.lang.ArithmeticException)

$ ruby rescued.rb java.lang.ArithmeticException
main thread #<Thread:0x234bef66 run>
rescued block child thread #<Thread:0x364b937 run> throwing java.lang.ArithmeticException
rescued block rescue in child thread java.lang.ArithmeticException
rescued block ensure in child thread
Exception in thread "Ruby-0-Thread-2: rescued.rb:1" java.lang.ArithmeticException
    at java.lang.reflect.Constructor.newInstance(java/lang/reflect/Constructor.java:422)
    at java.lang.Class.newInstance(java/lang/Class.java:442)
    at Foo.javaThrow(Foo.java:4)
    at java.lang.reflect.Method.invoke(java/lang/reflect/Method.java:497)
    at rescued.(root)(rescued.rb:12)
    at rescued.(root)(rescued.rb:12)
    at rescued.(root)(rescued.rb:10)
    at rescued.(root)(rescued.rb:10)
    at java.lang.Thread.run(java/lang/Thread.java:745)
main thread ensure

2- thread unrescued exception

require "java"
java_import "Foo"

Thread.abort_on_exception = true

begin
  STDERR.puts("main thread #{Thread.current.inspect}")

  th = Thread.new do
    begin
      STDERR.puts("unrescued block child thread #{Thread.current.inspect} throwing #{ARGV[0]}")
      Foo.javaThrow(ARGV[0])
    ensure
      STDERR.puts("unrescued block ensure in child thread")
    end
  end
  th.join

  sleep(5)
rescue Object => e
  STDERR.puts("main thread rescue Object #{e.inspect}")
rescue Exception => e
  STDERR.puts("main thread rescue Exception #{e.inspect}")
ensure
  STDERR.puts("main thread ensure")
end

2.1 - java.lang.Error base exception (java.lang.OutOfMemoryError)

$ ruby unrescued.rb java.lang.OutOfMemoryError
main thread #<Thread:0x234bef66 run>
unrescued block child thread #<Thread:0x5cf60573 run> throwing java.lang.OutOfMemoryError
unrescued block ensure in child thread
main thread rescue Exception java.lang.OutOfMemoryError
main thread ensure

2.2 - java.lang.Exception base exception (java.lang.ArithmeticException)

$ ruby unrescued.rb java.lang.ArithmeticException
main thread #<Thread:0x234bef66 run>
unrescued block child thread #<Thread:0x1719c8b7 run> throwing java.lang.ArithmeticException
unrescued block ensure in child thread
Exception in thread "Ruby-0-Thread-2: unrescued.rb:1" java.lang.ArithmeticException
    at java.lang.reflect.Constructor.newInstance(java/lang/reflect/Constructor.java:422)
    at java.lang.Class.newInstance(java/lang/Class.java:442)
    at Foo.javaThrow(Foo.java:4)
    at java.lang.reflect.Method.invoke(java/lang/reflect/Method.java:497)
    at unrescued.__ensure__(unrescued.rb:12)
    at unrescued.__ensure__(unrescued.rb:12)
    at unrescued.(root)(unrescued.rb:10)
    at unrescued.(root)(unrescued.rb:10)
    at java.lang.Thread.run(java/lang/Thread.java:745)
main thread ensure

conclusion

  • all Java exception are always rescuable using rescue Exception in the child thread.
  • Java exceptions subclass of java.lang.Exception are never rescuable in the main thread, regardless if rescued and re-raised in the child thread or not rescued at all.
  • Java exceptions subclass of java.lang.Error are always rescuable using rescue Exception in both the child and main thread.

So the correct solution here IMO is to simply always wrap child thread (worker) exceptions in an exception wrapper class for re-raising. Here's a working example using a proper child thread rescue, wrap & re-raise with a java.lang.Exception:

require "java"
java_import "Foo"

Thread.abort_on_exception = true

class WrappedException < Exception
  attr_reader :cause

  def initialize(cause)
    super(cause.message)
    @cause = cause
  end

  def to_s
    "#{super.inspect}, cause=#{@cause.inspect}"
  end
end

begin
  STDERR.puts("main thread #{Thread.current.inspect}")

  th = Thread.new do
    begin
      STDERR.puts("rescued block child thread #{Thread.current.inspect} throwing #{ARGV[0]}")
      Foo.javaThrow(ARGV[0])
    rescue Exception => e
      STDERR.puts("rescued block rescue in child thread #{e.inspect}")
      raise(WrappedException.new(e))
    ensure
      STDERR.puts("rescued block ensure in child thread")
    end
  end
  th.join

  sleep(5)
rescue Exception => e
  STDERR.puts("main thread rescue Exception #{e.inspect}")
rescue Object => e
  STDERR.puts("main thread rescue Object #{e.inspect}")
ensure
  STDERR.puts("main thread ensure")
end
$ ruby rescued.rb java.lang.ArithmeticException
main thread #<Thread:0x234bef66 run>
rescued block child thread #<Thread:0x31c13ab6 run> throwing java.lang.ArithmeticException
rescued block rescue in child thread java.lang.ArithmeticException
rescued block ensure in child thread
main thread rescue Exception #<WrappedException: "", cause=java.lang.ArithmeticException>
main thread ensure

@guyboertje
Copy link
Contributor Author

Thank you @colinsurprenant for your detailed investigation. I was wrong about the rescue Object.

As can be seen from the above, it is necessary that we revisit all the plugin error handling blocks.

This means we need to reconsider @jsvd PR #2386 and that we need to look at how the main pipeline thread handles exceptions thrown from input and output threads and not just worker threads.

@colinsurprenant
Copy link
Contributor

+1 on revisiting error handling & #2386
+1 on @headius's proposed JRuby patch to consistently propagate all exceptions in the main thread. We can certainly propose a spec/test case for this?

@guyboertje
Copy link
Contributor Author

@colinsurprenant - @headius asked me to provide a spec/test case illustrating the problem. Initially looked at https://github.com/jruby/jruby/blob/master/spec/java_integration/exceptions/rescue_spec.rb. I see no obvious way to include a java class that throws a raw java exception.

@guyboertje
Copy link
Contributor Author

Closing this. Moved discussion to #4127

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

6 participants