4141import org .bson .Document ;
4242import org .bson .codecs .Decoder ;
4343import org .bson .codecs .DocumentCodec ;
44- import org .junit .jupiter .api .Assertions ;
4544import org .junit .jupiter .api .BeforeEach ;
4645import org .junit .jupiter .api .Test ;
4746
4847import java .time .Duration ;
48+ import java .util .concurrent .atomic .AtomicInteger ;
4949
5050import static com .mongodb .internal .operation .OperationUnitSpecification .getMaxWireVersionForServerVersion ;
5151import static java .util .concurrent .TimeUnit .MILLISECONDS ;
52+ import static org .junit .jupiter .api .Assertions .assertEquals ;
53+ import static org .junit .jupiter .api .Assertions .assertNotNull ;
54+ import static org .junit .jupiter .api .Assertions .assertNull ;
55+ import static org .junit .jupiter .api .Assertions .assertTrue ;
5256import static org .mockito .ArgumentMatchers .any ;
5357import static org .mockito .ArgumentMatchers .argThat ;
5458import static org .mockito .ArgumentMatchers .eq ;
@@ -80,11 +84,9 @@ class AsyncCommandCursorTest {
8084 private OperationContext operationContext ;
8185 private TimeoutContext timeoutContext ;
8286 private ServerDescription serverDescription ;
83- private AsyncCursor <Document > coreCursor ;
8487
8588 @ BeforeEach
8689 void setUp () {
87- coreCursor = mock (AsyncCursor .class );
8890 timeoutContext = spy (new TimeoutContext (TimeoutSettings .create (
8991 MongoClientSettings .builder ().timeout (TIMEOUT .toMillis (), MILLISECONDS ).build ())));
9092 operationContext = spy (new OperationContext (
@@ -105,7 +107,7 @@ void setUp() {
105107 serverDescription = mock (ServerDescription .class );
106108 when (operationContext .getTimeoutContext ()).thenReturn (timeoutContext );
107109 doAnswer (invocation -> {
108- SingleResultCallback <AsyncConnection > callback = invocation .getArgument (0 );
110+ SingleResultCallback <AsyncConnection > callback = invocation .getArgument (1 );
109111 callback .onResult (mockConnection , null );
110112 return null ;
111113 }).when (connectionSource ).getConnection (any (), any ());
@@ -126,9 +128,9 @@ void shouldSkipKillsCursorsCommandWhenNetworkErrorOccurs() {
126128
127129 //when
128130 commandBatchCursor .next (operationContext , (result , t ) -> {
129- Assertions . assertNull (result );
130- Assertions . assertNotNull (t );
131- Assertions . assertEquals (MongoSocketException .class , t .getClass ());
131+ assertNull (result );
132+ assertNotNull (t );
133+ assertEquals (MongoSocketException .class , t .getClass ());
132134 });
133135
134136 //then
@@ -151,9 +153,9 @@ void shouldNotSkipKillsCursorsCommandWhenTimeoutExceptionDoesNotHaveNetworkError
151153
152154 //when
153155 commandBatchCursor .next (operationContext , (result , t ) -> {
154- Assertions . assertNull (result );
155- Assertions . assertNotNull (t );
156- Assertions . assertEquals (MongoOperationTimeoutException .class , t .getClass ());
156+ assertNull (result );
157+ assertNotNull (t );
158+ assertEquals (MongoOperationTimeoutException .class , t .getClass ());
157159 });
158160
159161 commandBatchCursor .close (operationContext );
@@ -182,9 +184,9 @@ void shouldSkipKillsCursorsCommandWhenTimeoutExceptionHaveNetworkErrorCause() {
182184
183185 //when
184186 commandBatchCursor .next (operationContext , (result , t ) -> {
185- Assertions . assertNull (result );
186- Assertions . assertNotNull (t );
187- Assertions . assertEquals (MongoOperationTimeoutException .class , t .getClass ());
187+ assertNull (result );
188+ assertNotNull (t );
189+ assertEquals (MongoOperationTimeoutException .class , t .getClass ());
188190 });
189191
190192 commandBatchCursor .close (operationContext );
@@ -199,6 +201,33 @@ void shouldSkipKillsCursorsCommandWhenTimeoutExceptionHaveNetworkErrorCause() {
199201 }
200202
201203
204+ @ Test
205+ void shouldReleaseConnectionBetweenEmptyGetMoreResponses () {
206+ AtomicInteger callCount = new AtomicInteger ();
207+ doAnswer (invocation -> {
208+ SingleResultCallback <BsonDocument > cb = invocation .getArgument (6 );
209+ cb .onResult (new BsonDocument ("cursor" ,
210+ new BsonDocument ("ns" , new BsonString (NAMESPACE .getFullName ()))
211+ .append ("id" , new BsonInt64 (callCount .incrementAndGet () < 3 ? 1 : 0 ))
212+ .append ("nextBatch" , new BsonArrayWrapper <>(new BsonArray ()))), null );
213+ return null ;
214+ }).when (mockConnection ).commandAsync (eq (NAMESPACE .getDatabaseName ()),
215+ argThat (doc -> doc .containsKey ("getMore" )), any (), any (), any (), any (), any ());
216+
217+ when (serverDescription .getType ()).thenReturn (ServerType .STANDALONE );
218+ createBatchCursor ().next (operationContext , (result , t ) -> {
219+ assertNotNull (result );
220+ assertTrue (result .isEmpty ());
221+ assertNull (t );
222+ });
223+
224+ // 2 empty-batch getMores + 1 exhausted getMore = 3 getMores, but the 3rd
225+ // exhausts the cursor (id=0), which makes the cursor break the loop and return an empty result.
226+ verify (mockConnection , times (3 )).release ();
227+ verify (connectionSource , times (3 )).getConnection (any (), any ());
228+ assertEquals (3 , callCount .get ());
229+ }
230+
202231 private AsyncCursor <Document > createBatchCursor () {
203232 return new AsyncCommandCursor <>(
204233 COMMAND_CURSOR_DOCUMENT ,
0 commit comments