4747import org .junit .jupiter .params .provider .ValueSource ;
4848
4949import java .time .Duration ;
50+ import java .util .concurrent .atomic .AtomicInteger ;
5051
5152import static com .mongodb .internal .operation .OperationUnitSpecification .getMaxWireVersionForServerVersion ;
5253import static com .mongodb .internal .thread .InterruptionUtil .interruptAndCreateMongoInterruptedException ;
5354import static java .util .concurrent .TimeUnit .MILLISECONDS ;
55+ import static org .junit .jupiter .api .Assertions .assertEquals ;
56+ import static org .junit .jupiter .api .Assertions .assertNotNull ;
57+ import static org .junit .jupiter .api .Assertions .assertNull ;
5458import static org .junit .jupiter .api .Assertions .assertTrue ;
5559import static org .mockito .ArgumentMatchers .any ;
5660import static org .mockito .ArgumentMatchers .argThat ;
@@ -119,17 +123,17 @@ void shouldSkipKillsCursorsCommandWhenNetworkErrorOccurs() {
119123 return null ;
120124 }).when (mockConnection ).commandAsync (eq (NAMESPACE .getDatabaseName ()), any (), any (), any (), any (), any (), any ());
121125 when (serverDescription .getType ()).thenReturn (ServerType .LOAD_BALANCER );
122- AsyncCommandBatchCursor <Document > commandBatchCursor = createBatchCursor (0 );
123126
124127 //when
125- commandBatchCursor .next ((result , t ) -> {
126- Assertions .assertNull (result );
127- Assertions .assertNotNull (t );
128- Assertions .assertEquals (MongoSocketException .class , t .getClass ());
129- });
128+ try (AsyncCommandBatchCursor <Document > commandBatchCursor = createBatchCursor (0 )) {
129+ commandBatchCursor .next ((result , t ) -> {
130+ Assertions .assertNull (result );
131+ Assertions .assertNotNull (t );
132+ Assertions .assertEquals (MongoSocketException .class , t .getClass ());
133+ });
134+ }
130135
131136 //then
132- commandBatchCursor .close ();
133137 verify (mockConnection , times (1 )).commandAsync (eq (NAMESPACE .getDatabaseName ()), any (), any (), any (), any (), any (), any ());
134138 }
135139
@@ -144,17 +148,14 @@ void shouldNotSkipKillsCursorsCommandWhenTimeoutExceptionDoesNotHaveNetworkError
144148 }).when (mockConnection ).commandAsync (eq (NAMESPACE .getDatabaseName ()), any (), any (), any (), any (), any (), any ());
145149 when (serverDescription .getType ()).thenReturn (ServerType .LOAD_BALANCER );
146150
147- AsyncCommandBatchCursor <Document > commandBatchCursor = createBatchCursor (0 );
148-
149151 //when
150- commandBatchCursor .next ((result , t ) -> {
151- Assertions .assertNull (result );
152- Assertions .assertNotNull (t );
153- Assertions .assertEquals (MongoOperationTimeoutException .class , t .getClass ());
154- });
155-
156- commandBatchCursor .close ();
157-
152+ try (AsyncCommandBatchCursor <Document > commandBatchCursor = createBatchCursor (0 )) {
153+ commandBatchCursor .next ((result , t ) -> {
154+ Assertions .assertNull (result );
155+ Assertions .assertNotNull (t );
156+ Assertions .assertEquals (MongoOperationTimeoutException .class , t .getClass ());
157+ });
158+ }
158159
159160 //then
160161 verify (mockConnection , times (2 )).commandAsync (any (),
@@ -175,16 +176,14 @@ void shouldSkipKillsCursorsCommandWhenTimeoutExceptionHaveNetworkErrorCause() {
175176 }).when (mockConnection ).commandAsync (eq (NAMESPACE .getDatabaseName ()), any (), any (), any (), any (), any (), any ());
176177 when (serverDescription .getType ()).thenReturn (ServerType .LOAD_BALANCER );
177178
178- AsyncCommandBatchCursor <Document > commandBatchCursor = createBatchCursor (0 );
179-
180179 //when
181- commandBatchCursor . next (( result , t ) -> {
182- Assertions . assertNull ( result );
183- Assertions .assertNotNull ( t );
184- Assertions .assertEquals ( MongoOperationTimeoutException . class , t . getClass () );
185- } );
186-
187- commandBatchCursor . close ();
180+ try ( AsyncCommandBatchCursor < Document > commandBatchCursor = createBatchCursor ( 0 )) {
181+ commandBatchCursor . next (( result , t ) -> {
182+ Assertions .assertNull ( result );
183+ Assertions .assertNotNull ( t );
184+ Assertions . assertEquals ( MongoOperationTimeoutException . class , t . getClass () );
185+ });
186+ }
188187
189188 //then
190189 verify (mockConnection , times (1 )).commandAsync (any (),
@@ -203,8 +202,6 @@ void closeShouldResetTimeoutContextToDefaultMaxTime() {
203202 try (AsyncCommandBatchCursor <Document > commandBatchCursor = createBatchCursor (maxTimeMS )) {
204203 // verify that the `maxTimeMS` override was applied
205204 timeoutContext .runMaxTimeMS (remainingMillis -> assertTrue (remainingMillis <= maxTimeMS ));
206- } catch (Exception e ) {
207- throw new RuntimeException (e );
208205 }
209206 timeoutContext .runMaxTimeMS (remainingMillis -> {
210207 // verify that the `maxTimeMS` override was reset
@@ -236,8 +233,6 @@ void closeShouldNotResetOriginalTimeout(final boolean disableTimeoutResetWhenClo
236233 Thread .sleep (thirdOfTimeout .toMillis ());
237234 return null ;
238235 });
239- } catch (Exception e ) {
240- throw new RuntimeException (e );
241236 }
242237 verify (mockConnection , times (1 )).release ();
243238 // at this point at least (2 * thirdOfTimeout) have passed
@@ -252,6 +247,34 @@ void closeShouldNotResetOriginalTimeout(final boolean disableTimeoutResetWhenClo
252247 Assertions ::fail );
253248 }
254249
250+ @ Test
251+ void shouldReleaseConnectionBetweenEmptyGetMoreResponses () {
252+ AtomicInteger callCount = new AtomicInteger ();
253+ doAnswer (invocation -> {
254+ SingleResultCallback <BsonDocument > cb = invocation .getArgument (6 );
255+ cb .onResult (new BsonDocument ("cursor" ,
256+ new BsonDocument ("ns" , new BsonString (NAMESPACE .getFullName ()))
257+ .append ("id" , new BsonInt64 (callCount .incrementAndGet () < 3 ? 1 : 0 ))
258+ .append ("nextBatch" , new BsonArrayWrapper <>(new BsonArray ()))), null );
259+ return null ;
260+ }).when (mockConnection ).commandAsync (eq (NAMESPACE .getDatabaseName ()),
261+ argThat (doc -> doc .containsKey ("getMore" )), any (), any (), any (), any (), any ());
262+
263+ when (serverDescription .getType ()).thenReturn (ServerType .STANDALONE );
264+ try (AsyncCommandBatchCursor <Document > commandBatchCursor = createBatchCursor (0 )) {
265+ commandBatchCursor .next ((result , t ) -> {
266+ assertNotNull (result );
267+ assertTrue (result .isEmpty ());
268+ assertNull (t );
269+ });
270+ }
271+
272+ // 2 empty-batch getMores + 1 exhausted getMore = 3 getMores, but the 3rd
273+ // exhausts the cursor (id=0), which makes the cursor break the loop and return an empty result.
274+ verify (mockConnection , times (3 )).release ();
275+ verify (connectionSource , times (3 )).getConnection (any ());
276+ assertEquals (3 , callCount .get ());
277+ }
255278
256279 private AsyncCommandBatchCursor <Document > createBatchCursor (final long maxTimeMS ) {
257280 return new AsyncCommandBatchCursor <Document >(
0 commit comments