Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Raising an exception on a thread that is in a Fiber causes a NullPointerException #1463

Closed
cheald opened this Issue · 1 comment

2 participants

Chris Heald Charles Oliver Nutter
Chris Heald

Seeing this periodically in an app that uses Fibers and timeout.rb. Timeout is raising exceptions on threads that are in a Fiber, which is crashing the fiber thread.

    java.lang.NullPointerException
    at org.jruby.ext.fiber.ThreadFiber.yield(ThreadFiber.java:152)
    at org.jruby.ext.fiber.ThreadFiber.yield(ThreadFiber.java:141)
    at org.jruby.ext.fiber.ThreadFiber$INVOKER$s$yield.call(ThreadFiber$INVOKER$s$yield.gen)
    at org.jruby.runtime.callsite.CachingCallSite.call(CachingCallSite.java:134)
    at org.jruby.ast.CallNoArgNode.interpret(CallNoArgNode.java:60)
    at org.jruby.ast.NewlineNode.interpret(NewlineNode.java:105)
    at org.jruby.evaluator.ASTInterpreter.INTERPRET_METHOD(ASTInterpreter.java:74)
    at org.jruby.internal.runtime.methods.InterpretedMethod.call(InterpretedMethod.java:139)
    at org.jruby.runtime.callsite.CachingCallSite.call(CachingCallSite.java:134)
    at org.jruby.ast.VCallNode.interpret(VCallNode.java:88)
    at org.jruby.ast.LocalAsgnNode.interpret(LocalAsgnNode.java:123)
    at org.jruby.ast.NewlineNode.interpret(NewlineNode.java:105)
    at org.jruby.ast.BlockNode.interpret(BlockNode.java:71)
    at org.jruby.evaluator.ASTInterpreter.INTERPRET_METHOD(ASTInterpreter.java:74)
    at org.jruby.internal.runtime.methods.InterpretedMethod.call(InterpretedMethod.java:182)
    at org.jruby.runtime.callsite.CachingCallSite.call(CachingCallSite.java:168)
    at org.jruby.ast.CallOneArgNode.interpret(CallOneArgNode.java:57)
    at org.jruby.ast.NewlineNode.interpret(NewlineNode.java:105)
    at org.jruby.evaluator.ASTInterpreter.INTERPRET_METHOD(ASTInterpreter.java:74)
    at org.jruby.internal.runtime.methods.InterpretedMethod.call(InterpretedMethod.java:182)
    at org.jruby.runtime.callsite.CachingCallSite.call(CachingCallSite.java:168)
    at org.jruby.ast.CallOneArgNode.interpret(CallOneArgNode.java:57)
    at org.jruby.ast.NewlineNode.interpret(NewlineNode.java:105)
    at org.jruby.ast.BlockNode.interpret(BlockNode.java:71)
    at org.jruby.ast.IfNode.interpret(IfNode.java:116)
    at org.jruby.ast.NewlineNode.interpret(NewlineNode.java:105)
    at org.jruby.ast.BlockNode.interpret(BlockNode.java:71)
    at org.jruby.evaluator.ASTInterpreter.INTERPRET_METHOD(ASTInterpreter.java:74)
    at org.jruby.internal.runtime.methods.InterpretedMethod.call(InterpretedMethod.java:225)
    at org.jruby.runtime.callsite.CachingCallSite.call(CachingCallSite.java:202)
    at org.jruby.ast.CallTwoArgNode.interpret(CallTwoArgNode.java:59)
    at org.jruby.ast.CallNoArgNode.interpret(CallNoArgNode.java:60)
    at org.jruby.ast.NewlineNode.interpret(NewlineNode.java:105)
    at org.jruby.evaluator.ASTInterpreter.INTERPRET_METHOD(ASTInterpreter.java:74)
    at org.jruby.internal.runtime.methods.InterpretedMethod.call(InterpretedMethod.java:139)
    at org.jruby.runtime.callsite.CachingCallSite.call(CachingCallSite.java:134)
    at org.jruby.ast.CallNoArgNode.interpret(CallNoArgNode.java:60)
    at org.jruby.ast.NewlineNode.interpret(NewlineNode.java:105)
    at org.jruby.ast.BlockNode.interpret(BlockNode.java:71)
    at org.jruby.evaluator.ASTInterpreter.INTERPRET_METHOD(ASTInterpreter.java:74)
    at org.jruby.internal.runtime.methods.InterpretedMethod.call(InterpretedMethod.java:112)
    at org.jruby.runtime.Helpers$MethodMissingMethod.call(Helpers.java:447)
    at org.jruby.internal.runtime.methods.DynamicMethod.call(DynamicMethod.java:210)
    at org.jruby.runtime.callsite.CachingCallSite.callMethodMissing(CachingCallSite.java:401)
    at org.jruby.runtime.callsite.CachingCallSite.cacheAndCall(CachingCallSite.java:323)
    at org.jruby.runtime.callsite.CachingCallSite.call(CachingCallSite.java:170)
    at org.jruby.ast.CallOneArgNode.interpret(CallOneArgNode.java:57)
    at org.jruby.ast.NewlineNode.interpret(NewlineNode.java:105)
    at org.jruby.ast.BlockNode.interpret(BlockNode.java:71)
    at org.jruby.ast.BlockNode.interpret(BlockNode.java:71)
    at org.jruby.ast.RescueBodyNode.interpret(RescueBodyNode.java:108)
    at org.jruby.ast.RescueNode.handleJavaException(RescueNode.java:204)
    at org.jruby.ast.RescueNode.interpret(RescueNode.java:137)
    at org.jruby.ast.BeginNode.interpret(BeginNode.java:83)
    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.BeginNode.interpret(BeginNode.java:83)
    at org.jruby.ast.NewlineNode.interpret(NewlineNode.java:105)
    at org.jruby.ast.BlockNode.interpret(BlockNode.java:71)
    at org.jruby.evaluator.ASTInterpreter.INTERPRET_METHOD(ASTInterpreter.java:74)
    at org.jruby.internal.runtime.methods.InterpretedMethod.call(InterpretedMethod.java:112)
    at org.jruby.RubyClass.finvoke(RubyClass.java:548)
    at org.jruby.RubyBasicObject.send19(RubyBasicObject.java:1531)
    at org.jruby.RubyBasicObject$INVOKER$i$send19.call(RubyBasicObject$INVOKER$i$send19.gen)
    at org.jruby.RubyKernel.public_send(RubyKernel.java:1963)
    at org.jruby.RubyKernel$INVOKER$s$0$0$public_send.call(RubyKernel$INVOKER$s$0$0$public_send.gen)
    at org.jruby.runtime.callsite.CachingCallSite.cacheAndCall(CachingCallSite.java:286)
    at org.jruby.runtime.callsite.CachingCallSite.callBlock(CachingCallSite.java:81)
    at org.jruby.runtime.callsite.CachingCallSite.call(CachingCallSite.java:85)
    at org.jruby.ast.CallSpecialArgBlockPassNode.interpret(CallSpecialArgBlockPassNode.java:70)
    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.evaluator.ASTInterpreter.INTERPRET_METHOD(ASTInterpreter.java:74)
    at org.jruby.internal.runtime.methods.InterpretedMethod.call(InterpretedMethod.java:204)
    at org.jruby.runtime.callsite.SuperCallSite.callBlock(SuperCallSite.java:190)
    at org.jruby.runtime.callsite.SuperCallSite.call(SuperCallSite.java:197)
    at org.jruby.runtime.callsite.SuperCallSite.callVarargs(SuperCallSite.java:108)
    at org.jruby.ast.SuperNode.interpret(SuperNode.java:115)
    at org.jruby.ast.LocalAsgnNode.interpret(LocalAsgnNode.java:123)
    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.evaluator.ASTInterpreter.INTERPRET_METHOD(ASTInterpreter.java:74)
    at org.jruby.internal.runtime.methods.InterpretedMethod.call(InterpretedMethod.java:182)
    at org.jruby.runtime.callsite.CachingCallSite.cacheAndCall(CachingCallSite.java:326)
    at org.jruby.runtime.callsite.CachingCallSite.call(CachingCallSite.java:170)
    at org.jruby.ast.CallOneArgNode.interpret(CallOneArgNode.java:57)
    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:157)
    at org.jruby.runtime.Interpreted19Block.yieldSpecific(Interpreted19Block.java:130)
    at org.jruby.runtime.Block.yieldSpecific(Block.java:111)
    at org.jruby.ast.ZYieldNode.interpret(ZYieldNode.java:25)
    at org.jruby.ast.NewlineNode.interpret(NewlineNode.java:105)
    at org.jruby.ast.IfNode.interpret(IfNode.java:118)
    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:157)
    at org.jruby.runtime.Interpreted19Block.yieldSpecific(Interpreted19Block.java:130)
    at org.jruby.runtime.Block.yieldSpecific(Block.java:111)
    at org.jruby.ast.ZYieldNode.interpret(ZYieldNode.java:25)
    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.call19(RubyProc.java:271)
    at org.jruby.RubyProc$INVOKER$i$0$0$call19.call(RubyProc$INVOKER$i$0$0$call19.gen)
    at org.jruby.internal.runtime.methods.DynamicMethod.call(DynamicMethod.java:202)
    at org.jruby.internal.runtime.methods.DynamicMethod.call(DynamicMethod.java:198)
    at org.jruby.runtime.callsite.CachingCallSite.call(CachingCallSite.java:134)
    at org.jruby.ast.CallNoArgNode.interpret(CallNoArgNode.java:60)
    at org.jruby.ast.NewlineNode.interpret(NewlineNode.java:105)
    at org.jruby.ast.BlockNode.interpret(BlockNode.java:71)
    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.call19(RubyProc.java:271)
    at org.jruby.RubyProc$INVOKER$i$0$0$call19.call(RubyProc$INVOKER$i$0$0$call19.gen)
    at org.jruby.internal.runtime.methods.DynamicMethod.call(DynamicMethod.java:202)
    at org.jruby.internal.runtime.methods.DynamicMethod.call(DynamicMethod.java:198)
    at org.jruby.runtime.callsite.CachingCallSite.call(CachingCallSite.java:134)
    at org.jruby.ast.CallNoArgNode.interpret(CallNoArgNode.java:60)
    at org.jruby.ast.NewlineNode.interpret(NewlineNode.java:105)
    at org.jruby.ast.BlockNode.interpret(BlockNode.java:71)
    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:157)
    at org.jruby.runtime.Interpreted19Block.yieldSpecific(Interpreted19Block.java:130)
    at org.jruby.runtime.Block.yieldSpecific(Block.java:111)
    at org.jruby.RubyKernel.loop(RubyKernel.java:1519)
    at org.jruby.RubyKernel$INVOKER$s$0$0$loop.call(RubyKernel$INVOKER$s$0$0$loop.gen)
    at org.jruby.runtime.callsite.CachingCallSite.callBlock(CachingCallSite.java:143)
    at org.jruby.runtime.callsite.CachingCallSite.callIter(CachingCallSite.java:154)
    at org.jruby.ast.FCallNoArgBlockNode.interpret(FCallNoArgBlockNode.java:32)
    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.yield(Interpreted19Block.java:177)
    at org.jruby.runtime.Block.yieldArray(Block.java:158)
    at org.jruby.ext.fiber.ThreadFiber$1.run(ThreadFiber.java:197)

@headius was able to reproduce it via:

t = Thread.new { begin; f = Fiber.new { sleep 0.2; begin; Fiber.yield; rescue => e; p e; end }; f.resume; rescue => e; p e unless RuntimeError === e; end }; sleep 0.1; t.raise
Charles Oliver Nutter headius referenced this issue from a commit
Charles Oliver Nutter headius Partial fix for #1463.
In order to make a Thread and the fiber it is resuming look like
a single unit, we must make sure that asynchronous exceptions
happening at that boundary propagate the right direction.

Exceptions raised in a thread waiting on Fiber#resume should go
into the fiber and be handled there. Exceptions raised on a fiber
waiting on Fiber.yield should go to the resuming thread and be
handled there. This is done by capturing exceptions in
Fiber#resume, transfer, and yield and propagating them to the
waiting thread/fiber if its queue is still active. If that queue
is not active (the fiber has exited), the exception is propagated
upward in the current fiber/thread.

The unknown case is what should happen to a fiber in which an
exception (for example, a timeout) has triggered.

In MRI, where there's really only one thread, the timeout would go
to the resuming thread and happen arbitrarily in its running code.
The originating fiber would be left undamaged as if nothing had
happened, and would be subject to GC if abandoned or resumable as
normal. In this patch, the original fiber performs the same
exception-propagation loop and goes back to its queue for a
resumed value; this should roughly match MRI's behavior. However,
I am not sure I agree with MRI's behavior, or if MRI's behavior is
correct, then Timeout should *never* be used around Fiber.yield,
since it is impossible to know where and when it will fire.
0c417d1
Charles Oliver Nutter
Owner

This appears to be fixed now by 0c417d1.

There were two issues.

  1. When a thread, waiting on Fiber#resume for a fiber, receives an asynchronous exception, it would exit the resume logic and clear the "prev" pointer from the target thread back to its resumer. The target fiber would continue to run, eventually reaching Fiber.yield, at which point the null prev would cause a NullPointerException.
  2. When a fiber, waiting on Fiber.yield, receives an asynchronous exception, it would receive that exception and start running. If the exception were rescued, it could reach another Fiber.yield causing the same NPE as in (2). If the exception were not rescued, it would propagate out of the fiber.

Both of these issues were fixed by the same modification to resume and yield. When a thread or fiber is waiting on another thread or fiber, exceptions it receives asynchronously need to be forwarded on to the target thread or fiber. Only if the target fiber is dead should the exception be propagated up the call stack as normal.

This issue illustrates more difficult issues with having fibers implemented using threads, but the fix seems to be working well and has so far been confirmed by @cheald. We'll call this fixed, but we should probably prioritize looking into alternative implementations of Fiber for JRuby 9k.

Charles Oliver Nutter headius closed this
Charles Oliver Nutter headius was assigned
David Grayson DavidEGrayson referenced this issue from a commit in DavidEGrayson/jruby
David Grayson DavidEGrayson Fixed an infinite loop in 'rake spec:regression:int'. Fibers are not …
…available in 1.8 mode so the GH-1463 spec should just be skipped.
52e6823
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.