[JAVA-448] Tailable Cursor and awaitdata Created: 13/Oct/11  Updated: 11/Sep/19  Resolved: 19/Oct/11

Status: Closed
Project: Java Driver
Component/s: API
Affects Version/s: 2.6.5
Fix Version/s: 2.7

Type: Task Priority: Minor - P4
Reporter: Guy Boertje Assignee: Unassigned
Resolution: Done Votes: 0
Labels: cursor, driver, tailable
Remaining Estimate: Not Specified
Time Spent: Not Specified
Original Estimate: Not Specified
Environment:

Linux 64 bit Ubuntu, Java 7 Jruby 1.6.4 with 2.6.5 of the java driver on Mongo 1.8.3


Backwards Compatibility: Major Change

 Description   

In tests, using a tailable cursor and a capped collection, I can fill the collection with say 10 docs and read them back. If I then ask for next doc, my test never ends. In the test setup if I set a thread to wait for 6 seconds before adding a doc to the collection the test does end. I did a thread dump while the test blocked. See further down

Please confirm that the java driver should eventually return a nil doc assuming the await data option is set. Or confirm that this behaviour is for a future version.

– Dump (some jruby lines removed) –

"ScriptThreadProcess: ./test/collection_test.rb" daemon prio=10 tid=0x0000000002959000 nid=0x55b9 runnable [0x00007f2b2b7c1000]
java.lang.Thread.State: RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.read(SocketInputStream.java:150)
at java.net.SocketInputStream.read(SocketInputStream.java:121)
at java.io.BufferedInputStream.fill(BufferedInputStream.java:235)
at java.io.BufferedInputStream.read1(BufferedInputStream.java:275)
at java.io.BufferedInputStream.read(BufferedInputStream.java:334)

  • locked <0x00000000eab22188> (a java.io.BufferedInputStream)
    at org.bson.io.Bits.readFully(Bits.java:35)
    at org.bson.io.Bits.readFully(Bits.java:28)
    at com.mongodb.Response.<init>(Response.java:39)
    at com.mongodb.DBPort.go(DBPort.java:123)
  • locked <0x00000000eaaf8d88> (a com.mongodb.DBPort)
    at com.mongodb.DBPort.go(DBPort.java:82)
  • locked <0x00000000eaaf8d88> (a com.mongodb.DBPort)
    at com.mongodb.DBPort.call(DBPort.java:72)
    at com.mongodb.DBTCPConnector.call(DBTCPConnector.java:202)
    at com.mongodb.DBTCPConnector.call(DBTCPConnector.java:186)
    at com.mongodb.DBApiLayer$Result._advance(DBApiLayer.java:424)
    at com.mongodb.DBApiLayer$Result.next(DBApiLayer.java:396)
    at com.mongodb.DBApiLayer$Result.next(DBApiLayer.java:397)
    at com.mongodb.DBApiLayer$Result.next(DBApiLayer.java:397)
    at com.mongodb.DBApiLayer$Result.next(DBApiLayer.java:397)
    at com.mongodb.DBApiLayer$Result.next(DBApiLayer.java:397)
    at com.mongodb.DBApiLayer$Result.next(DBApiLayer.java:1)
    at com.mongodb.DBCursor._next(DBCursor.java:445)
    at com.mongodb.DBCursor.next(DBCursor.java:525)
    at com.mongodb.DBCursor.next(DBCursor.java:1)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:601)
    at org.jruby.javasupport.JavaMethod.invokeDirectWithExceptionHandling(JavaMethod.java:508)
    at org.jruby.javasupport.JavaMethod.invokeDirect(JavaMethod.java:368)
    at org.jruby.java.invokers.InstanceMethodInvoker.call(InstanceMethodInvoker.java:50)
    at org.jruby.runtime.callsite.CachingCallSite.call(CachingCallSite.java:133)
    at org.jruby.ast.CallNoArgNode.interpret(CallNoArgNode.java:63)
    at org.jruby.ast.FCallOneArgNode.interpret(FCallOneArgNode.java:36)
    at org.jruby.ast.NewlineNode.interpret(NewlineNode.java:104)
    at org.jruby.evaluator.ASTInterpreter.INTERPRET_METHOD(ASTInterpreter.java:75)
    at org.jruby.internal.runtime.methods.InterpretedMethod.call(InterpretedMethod.java:147)
    at org.jruby.internal.runtime.methods.DefaultMethod.call(DefaultMethod.java:163)
    at org.jruby.runtime.callsite.CachingCallSite.call(CachingCallSite.java:133)
    at org.jruby.ast.VCallNode.interpret(VCallNode.java:86)
    at org.jruby.ast.LocalAsgnNode.interpret(LocalAsgnNode.java:123)
    at org.jruby.ast.NewlineNode.interpret(NewlineNode.java:104)
    at org.jruby.ast.IfNode.interpret(IfNode.java:117)
    at org.jruby.ast.NewlineNode.interpret(NewlineNode.java:104)
    at org.jruby.evaluator.ASTInterpreter.INTERPRET_BLOCK(ASTInterpreter.java:112)
    at org.jruby.runtime.Interpreted19Block.evalBlockBody(Interpreted19Block.java:203)
    at org.jruby.runtime.Interpreted19Block.yield(Interpreted19Block.java:154)
    at org.jruby.runtime.Interpreted19Block.yieldSpecific(Interpreted19Block.java:127)
    at org.jruby.runtime.Block.yieldSpecific(Block.java:99)
    at org.jruby.ast.ZYieldNode.interpret(ZYieldNode.java:25)
    at org.jruby.ast.NewlineNode.interpret(NewlineNode.java:104)
    at org.jruby.ast.RescueNode.executeBody(RescueNode.java:216)
    at org.jruby.ast.RescueNode.interpretWithJavaExceptions(RescueNode.java:120)
    at org.jruby.ast.RescueNode.interpret(RescueNode.java:110)
    at org.jruby.ast.BeginNode.interpret(BeginNode.java:83)
    at org.jruby.ast.NewlineNode.interpret(NewlineNode.java:104)
    at org.jruby.evaluator.ASTInterpreter.INTERPRET_METHOD(ASTInterpreter.java:75)
    at org.jruby.internal.runtime.methods.InterpretedMethod.call(InterpretedMethod.java:212)
    at org.jruby.internal.runtime.methods.DefaultMethod.call(DefaultMethod.java:187)
    at org.jruby.runtime.callsite.CachingCallSite.callBlock(CachingCallSite.java:176)
    at org.jruby.runtime.callsite.CachingCallSite.callIter(CachingCallSite.java:187)
    at org.jruby.ast.FCallOneArgBlockNode.interpret(FCallOneArgBlockNode.java:34)
    at org.jruby.ast.NewlineNode.interpret(NewlineNode.java:104)
    at org.jruby.ast.BlockNode.interpret(BlockNode.java:71)
    at org.jruby.evaluator.ASTInterpreter.INTERPRET_METHOD(ASTInterpreter.java:75)
    at org.jruby.internal.runtime.methods.InterpretedMethod.call(InterpretedMethod.java:147)
    at org.jruby.internal.runtime.methods.DefaultMethod.call(DefaultMethod.java:163)
    at org.jruby.runtime.callsite.CachingCallSite.cacheAndCall(CachingCallSite.java:292)
    at org.jruby.runtime.callsite.CachingCallSite.call(CachingCallSite.java:135)
    at $dot.test.collection_test.block_5$RUBY$_file_(./test/collection_test.rb:941)

JRUBY
it "should find using a tailable cursor" do
tail = Mongo::Cursor.new(@capped, :timeout => false, :tailable => true,
:await_data => true, :order => [['$natural', 1]])

10.times do
assert tail.next_document
end

Thread.new(@capped) do |col|
sleep 6
1.times { col.insert({:n => 100}) }
end

assert true, tail.alive?

  1. blocks forever unless using thread to insert
    #assert_nil tail.next_document
  1. blocks until thread inserts docs
    assert tail.next_document

assert tail.next_document
end



 Comments   
Comment by Guy Boertje [ 19/Oct/11 ]

Much appreciated. I have a modified fork of the driver from commit tagged 2.6.5, which commits will I have to cherry pick to have this behavour in my fork? I know its a long shot. I modded to include the change for Symbol in BasicBSONCallback as per JAVA-441. I have tried cherrypicking Brendans commit but it segfaulted, though it could be my bad as I'm not great at the whole build-to-jar thing.

Comment by Antoine Girbal [ 19/Oct/11 ]

The fix here keeps the looping for the AWAIT case as fixed in JAVA-155.
But without AWAIT the hasNext() method will return as false without blocking.
The client can still use the cursor at a later time.
Tests were added for both modes.

Comment by auto [ 19/Oct/11 ]

Author:

{u'login': u'agirbal', u'name': u'agirbal', u'email': u'antoine@10gen.com'}

Message: JAVA-448: Tailable Cursor and awaitdata
Branch: master
https://github.com/mongodb/mongo-java-driver/commit/c715a138eca38234d08d1d2f730ecec45e4de325

Comment by Brendan W. McAdams [ 19/Oct/11 ]

Tailable cursors w/ no data but not in await mode will no longer block and sleep but instead return null.

Comment by auto [ 19/Oct/11 ]

Author:

{u'login': u'bwmcadams', u'name': u'Brendan W. McAdams', u'email': u'brendan@10gen.com'}

Message: JAVA-448 - Tailable Cursor behavior while not in AWAIT mode

  • Fixed non-await empty tailable cursor behavior to be more consistently inline with other drivers & user expectations.
    Instead of forcing a sleep of 500 milliseconds on "no data", we instead when tailable catch an empty cursor and return null instead. This should be more safely non blocking for users who need to roll their own event driven code, for which the sleep breaks logic.
    Branch: master
    https://github.com/mongodb/mongo-java-driver/commit/456b8667b004e8e12d600f811d0ab62caf5d987d
Comment by Antoine Girbal [ 18/Oct/11 ]

when QUERYOPTION_AWAITDATA is on, it behaves properly: blocks in the hasNext() loop.
when it is off, code still blocks in the loop, doing a sleep(500).
So in both cases it's the same behavior.
But without awaitdata, hasNext() should return right away as false.
Client can decide to poll again at a later time.

Comment by Brendan W. McAdams [ 18/Oct/11 ]

There's one relevant bit above that try{Thread.sleep however though:

// have a tailable cursor

if ( ( _flags & Bytes.RESULTFLAG_AWAITCAPABLE ) > 0 && ( queryOptions & Bytes.QUERYOPTION_AWAITDATA ) > 0 )
return true;

Which should behave correctly there, and not block. Basically it checks that both the server is await capable and AWAIT was set, and returns immediately. OTHERWISE it blocks...

Comment by Antoine Girbal [ 18/Oct/11 ]

I think there is some bad code in the driver, this loops:

public boolean hasNext(){
while ( true )

{ if ( _cur.hasNext() ) return true; if ( ! _curResult.hasGetMore( _options ) ) return false; _advance(); }

}

public boolean hasGetMore( int queryOptions ){
...
try

{ Thread.sleep( 500 ); }

catch ( Exception e ){}

return true;
}

This result in hasNext() being stuck in a loop even without AWAIT.
I dont know why it was done that way, maybe it was implemented before AWAIT was available.

Comment by Antoine Girbal [ 18/Oct/11 ]

I did a quick test and indeed the request blocks even without AWAIT_DATA.
At first that seems like a bug on the server side.
But you are saying it is fine with the ruby driver? In that case there is a problem in java driver..

Comment by Guy Boertje [ 18/Oct/11 ]

I will double check. I recall that the original test did not set AWAITDATA but still blocked.

This is the test

adds 10 docs in setup

should "find using a tailable cursor" do
tail = Cursor.new(@capped, :tailable => true, :order => [['$natural', 1]])
10.times do
assert tail.next_document
end
assert_nil tail.next_document <------ blocks here
@capped.insert({:n => 100})
assert tail.next_document
end

Comment by Antoine Girbal [ 18/Oct/11 ]

I am not sure I fully understand the need.
If you need to return right away, why not use TAILABLE but without AWAITDATA?
Then you can potentially implement some wait time client side, e.g. a loop that polls and sleeps, with a timeout

Comment by Guy Boertje [ 16/Oct/11 ]

This presents a bit of a problem because the ruby driver returns a nil document immediately. I have looked at the RabbitMq Java driver - it has a QueuingConsumer class that allows for a timeout when waiting for a delivery. A tailable cursor is similar and I suggest that this driver should provide similar semantics. I encourage you to look at the class I mentioned.

Using a tailable cursor in an event driven way needs a loop with a callback and the loop needs to exit occasionally (to shutdown perhaps). I think this the loop, timeout and callback invocation are driver side duties.

For now I am going be invasive and, in another thread after a timeout period, put a 'poison' doc into the collection from the cursor and return nil from the next method if it receives a 'poison' doc.

Comment by Antoine Girbal [ 14/Oct/11 ]

I have not tested the behavior, but it seems this is independent of the driver and it's a server feature.
If using awaitdata, it's telling the server to block until at least 1 new document is available.
The doc says that eventually a timeout will make the server return null, but that timeout may be long like 30min.

Generated at Thu Feb 08 08:52:18 UTC 2024 using Jira 9.7.1#970001-sha1:2222b88b221c4928ef0de3161136cc90c8356a66.