diff --git a/src/main/java/com/arangodb/ArangoCursor.java b/src/main/java/com/arangodb/ArangoCursor.java index 734fc57b8..292cbf15f 100644 --- a/src/main/java/com/arangodb/ArangoCursor.java +++ b/src/main/java/com/arangodb/ArangoCursor.java @@ -22,10 +22,13 @@ import com.arangodb.entity.CursorEntity.Stats; import com.arangodb.entity.CursorEntity.Warning; +import com.arangodb.model.AqlQueryOptions; + import java.io.Closeable; import java.util.Collection; import java.util.List; +import java.util.NoSuchElementException; /** * @author Mark Vollmary @@ -76,4 +79,27 @@ public interface ArangoCursor extends ArangoIterable, ArangoIterator, C */ boolean isPotentialDirtyRead(); + /** + * @return The ID of the batch after the current one. The first batch has an ID of 1 and the value is incremented by + * 1 with every batch. Only set if the allowRetry query option is enabled. + * @since ArangoDB 3.11 + */ + String getNextBatchId(); + + /** + * Returns the next element in the iteration. + *

+ * If the cursor allows retries (see {@link AqlQueryOptions#allowRetry(Boolean)}), then it is safe to retry invoking + * this method in case of I/O exceptions (which are actually thrown as {@link com.arangodb.ArangoDBException} with + * cause {@link java.io.IOException}). + *

+ * If the cursor does not allow retries (default), then it is not safe to retry invoking this method in case of I/O + * exceptions, since the request to fetch the next batch is not idempotent (i.e. the cursor may advance multiple + * times on the server). + * + * @return the next element in the iteration + * @throws NoSuchElementException if the iteration has no more elements + */ + @Override + T next(); } diff --git a/src/main/java/com/arangodb/ArangoDatabase.java b/src/main/java/com/arangodb/ArangoDatabase.java index 8be1bf642..ff73ee5d6 100644 --- a/src/main/java/com/arangodb/ArangoDatabase.java +++ b/src/main/java/com/arangodb/ArangoDatabase.java @@ -338,6 +338,20 @@ ArangoCursor query(String query, Map bindVars, AqlQueryOp */ ArangoCursor cursor(String cursorId, Class type) throws ArangoDBException; + /** + * Return an cursor from the given cursor-ID if still existing + * + * @param cursorId The ID of the cursor + * @param type The type of the result (POJO class, VPackSlice, String for JSON, or Collection/List/Map) + * @param nextBatchId The ID of the next cursor batch (set only if cursor allows retries, see + * {@link AqlQueryOptions#allowRetry(Boolean)} + * @return cursor of the results + * @see API Documentation + * @since ArangoDB 3.11 + */ + ArangoCursor cursor(String cursorId, Class type, String nextBatchId); + /** * Explain an AQL query and return information about it * diff --git a/src/main/java/com/arangodb/async/ArangoDatabaseAsync.java b/src/main/java/com/arangodb/async/ArangoDatabaseAsync.java index c7ec2b89b..647006459 100644 --- a/src/main/java/com/arangodb/async/ArangoDatabaseAsync.java +++ b/src/main/java/com/arangodb/async/ArangoDatabaseAsync.java @@ -20,6 +20,7 @@ package com.arangodb.async; +import com.arangodb.ArangoCursor; import com.arangodb.ArangoDBException; import com.arangodb.ArangoSerializationAccessor; import com.arangodb.DbName; @@ -331,6 +332,20 @@ CompletableFuture> query( */ CompletableFuture> cursor(final String cursorId, final Class type); + /** + * Return an cursor from the given cursor-ID if still existing + * + * @param cursorId The ID of the cursor + * @param type The type of the result (POJO class, VPackSlice, String for JSON, or Collection/List/Map) + * @param nextBatchId The ID of the next cursor batch (set only if cursor allows retries, see + * {@link AqlQueryOptions#allowRetry(Boolean)} + * @return cursor of the results + * @see API Documentation + * @since ArangoDB 3.11 + */ + CompletableFuture> cursor(String cursorId, Class type, String nextBatchId); + /** * Explain an AQL query and return information about it * diff --git a/src/main/java/com/arangodb/async/internal/ArangoDatabaseAsyncImpl.java b/src/main/java/com/arangodb/async/internal/ArangoDatabaseAsyncImpl.java index 9913a38c9..597da7512 100644 --- a/src/main/java/com/arangodb/async/internal/ArangoDatabaseAsyncImpl.java +++ b/src/main/java/com/arangodb/async/internal/ArangoDatabaseAsyncImpl.java @@ -223,6 +223,17 @@ public CompletableFuture> cursor(final String cursorId, return execution.thenApply(result -> createCursor(result, type, null, hostHandle)); } + @Override + public CompletableFuture> cursor(final String cursorId, final Class type, + final String nextBatchId) { + final HostHandle hostHandle = new HostHandle(); + final CompletableFuture execution = executor.execute(queryNextByBatchIdRequest(cursorId, + nextBatchId, null, + null), + CursorEntity.class, hostHandle); + return execution.thenApply(result -> createCursor(result, type, null, hostHandle)); + } + private ArangoCursorAsync createCursor( final CursorEntity result, final Class type, @@ -230,9 +241,10 @@ private ArangoCursorAsync createCursor( final HostHandle hostHandle) { return new ArangoCursorAsyncImpl<>(this, new ArangoCursorExecute() { @Override - public CursorEntity next(final String id, Map meta) { - final CompletableFuture result = executor.execute(queryNextRequest(id, options, meta), - CursorEntity.class, hostHandle); + public CursorEntity next(final String id, Map meta, String nextBatchId) { + Request request = nextBatchId == null ? + queryNextRequest(id, options, meta) : queryNextByBatchIdRequest(id, nextBatchId, options, meta); + final CompletableFuture result = executor.execute(request, CursorEntity.class, hostHandle); try { return result.get(); } catch (InterruptedException | ExecutionException e) { diff --git a/src/main/java/com/arangodb/entity/CursorEntity.java b/src/main/java/com/arangodb/entity/CursorEntity.java index a696ad521..bf0b3c5f3 100644 --- a/src/main/java/com/arangodb/entity/CursorEntity.java +++ b/src/main/java/com/arangodb/entity/CursorEntity.java @@ -43,6 +43,7 @@ public class CursorEntity implements Entity, MetaAware { private VPackSlice result; private Map meta; + private String nextBatchId; public String getId() { return id; @@ -94,6 +95,10 @@ public Map getMeta() { return meta; } + public String getNextBatchId() { + return nextBatchId; + } + /** * @return remove not allowed (valid storable) meta information */ diff --git a/src/main/java/com/arangodb/internal/ArangoCursorExecute.java b/src/main/java/com/arangodb/internal/ArangoCursorExecute.java index 455844113..967f1a802 100644 --- a/src/main/java/com/arangodb/internal/ArangoCursorExecute.java +++ b/src/main/java/com/arangodb/internal/ArangoCursorExecute.java @@ -30,7 +30,7 @@ */ public interface ArangoCursorExecute { - CursorEntity next(String id, Map meta) throws ArangoDBException; + CursorEntity next(String id, Map meta, String nextBatchId) throws ArangoDBException; void close(String id, Map meta) throws ArangoDBException; diff --git a/src/main/java/com/arangodb/internal/ArangoDatabaseImpl.java b/src/main/java/com/arangodb/internal/ArangoDatabaseImpl.java index 49501f903..f025ecf14 100644 --- a/src/main/java/com/arangodb/internal/ArangoDatabaseImpl.java +++ b/src/main/java/com/arangodb/internal/ArangoDatabaseImpl.java @@ -200,6 +200,14 @@ public ArangoCursor cursor(final String cursorId, final Class type) th return createCursor(result, type, null, hostHandle); } + @Override + public ArangoCursor cursor(final String cursorId, final Class type, final String nextBatchId) { + final HostHandle hostHandle = new HostHandle(); + final CursorEntity result = executor + .execute(queryNextByBatchIdRequest(cursorId, nextBatchId, null, null), CursorEntity.class, hostHandle); + return createCursor(result, type, null, hostHandle); + } + private ArangoCursor createCursor( final CursorEntity result, final Class type, @@ -208,8 +216,10 @@ private ArangoCursor createCursor( final ArangoCursorExecute execute = new ArangoCursorExecute() { @Override - public CursorEntity next(final String id, Map meta) { - return executor.execute(queryNextRequest(id, options, meta), CursorEntity.class, hostHandle); + public CursorEntity next(String id, Map meta, String nextBatchId) { + Request request = nextBatchId == null ? + queryNextRequest(id, options, meta) : queryNextByBatchIdRequest(id, nextBatchId, options, meta); + return executor.execute(request, CursorEntity.class, hostHandle); } @Override diff --git a/src/main/java/com/arangodb/internal/InternalArangoDatabase.java b/src/main/java/com/arangodb/internal/InternalArangoDatabase.java index 34610d61d..3cff34a63 100644 --- a/src/main/java/com/arangodb/internal/InternalArangoDatabase.java +++ b/src/main/java/com/arangodb/internal/InternalArangoDatabase.java @@ -182,10 +182,17 @@ protected Request queryRequest( return request; } - protected Request queryNextRequest(final String id, final AqlQueryOptions options, Map meta) { - + protected Request queryNextRequest(String id, AqlQueryOptions options, Map meta) { final Request request = request(dbName, RequestType.POST, PATH_API_CURSOR, id); + return completeQueryNextRequest(request, options, meta); + } + + protected Request queryNextByBatchIdRequest(String id, String nextBatchId, AqlQueryOptions options, Map meta) { + final Request request = request(dbName, RequestType.POST, PATH_API_CURSOR, id, nextBatchId); + return completeQueryNextRequest(request, options, meta); + } + private Request completeQueryNextRequest(Request request, AqlQueryOptions options, Map meta) { if (meta != null) { request.getHeaderParam().putAll(meta); } diff --git a/src/main/java/com/arangodb/internal/cursor/ArangoCursorImpl.java b/src/main/java/com/arangodb/internal/cursor/ArangoCursorImpl.java index 1c86ceefe..971af4f86 100644 --- a/src/main/java/com/arangodb/internal/cursor/ArangoCursorImpl.java +++ b/src/main/java/com/arangodb/internal/cursor/ArangoCursorImpl.java @@ -29,6 +29,8 @@ import com.arangodb.entity.CursorEntity.Warning; import com.arangodb.internal.ArangoCursorExecute; import com.arangodb.internal.InternalArangoDatabase; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Collection; @@ -38,12 +40,14 @@ * @author Mark Vollmary */ public class ArangoCursorImpl extends AbstractArangoIterable implements ArangoCursor { + private final static Logger LOG = LoggerFactory.getLogger(ArangoCursorImpl.class); private final Class type; protected final ArangoCursorIterator iterator; private final String id; private final ArangoCursorExecute execute; - private final boolean isPontentialDirtyRead; + private final boolean pontentialDirtyRead; + private final boolean allowRetry; public ArangoCursorImpl(final InternalArangoDatabase db, final ArangoCursorExecute execute, final Class type, final CursorEntity result) { @@ -52,7 +56,8 @@ public ArangoCursorImpl(final InternalArangoDatabase db, final ArangoCurso this.type = type; iterator = createIterator(this, db, execute, result); id = result.getId(); - this.isPontentialDirtyRead = Boolean.parseBoolean(result.getMeta().get("X-Arango-Potential-Dirty-Read")); + this.pontentialDirtyRead = Boolean.parseBoolean(result.getMeta().get("X-Arango-Potential-Dirty-Read")); + this.allowRetry = result.getNextBatchId() != null; } protected ArangoCursorIterator createIterator( @@ -98,7 +103,7 @@ public boolean isCached() { @Override public void close() { - if (id != null && hasNext()) { + if (getId() != null && (allowRetry || iterator.getResult().getHasMore())) { execute.close(id, iterator.getResult().getMeta()); } } @@ -119,12 +124,17 @@ public List asListRemaining() { while (hasNext()) { remaining.add(next()); } + try { + close(); + } catch (final Exception e) { + LOG.warn("Could not close cursor: ", e); + } return remaining; } @Override public boolean isPotentialDirtyRead() { - return isPontentialDirtyRead; + return pontentialDirtyRead; } @Override @@ -144,4 +154,8 @@ public void foreach(final Consumer action) { } } + public String getNextBatchId() { + return iterator.getResult().getNextBatchId(); + } + } diff --git a/src/main/java/com/arangodb/internal/cursor/ArangoCursorIterator.java b/src/main/java/com/arangodb/internal/cursor/ArangoCursorIterator.java index b67ed820d..ecccc1226 100644 --- a/src/main/java/com/arangodb/internal/cursor/ArangoCursorIterator.java +++ b/src/main/java/com/arangodb/internal/cursor/ArangoCursorIterator.java @@ -44,9 +44,9 @@ public class ArangoCursorIterator implements ArangoIterator { private final InternalArangoDatabase db; private final ArangoCursorExecute execute; - protected ArangoCursorIterator(final ArangoCursor cursor, final ArangoCursorExecute execute, + protected ArangoCursorIterator(final ArangoCursor cursor, + final ArangoCursorExecute execute, final InternalArangoDatabase db, final CursorEntity result) { - super(); this.cursor = cursor; this.execute = execute; this.db = db; @@ -66,7 +66,7 @@ public boolean hasNext() { @Override public T next() { if (!arrayIterator.hasNext() && result.getHasMore()) { - result = execute.next(cursor.getId(), result.getMeta()); + result = execute.next(cursor.getId(), result.getMeta(), result.getNextBatchId()); arrayIterator = result.getResult().arrayIterator(); } if (!hasNext()) { diff --git a/src/main/java/com/arangodb/model/AqlQueryOptions.java b/src/main/java/com/arangodb/model/AqlQueryOptions.java index 57ad8d77a..2dc856623 100644 --- a/src/main/java/com/arangodb/model/AqlQueryOptions.java +++ b/src/main/java/com/arangodb/model/AqlQueryOptions.java @@ -425,6 +425,33 @@ public AqlQueryOptions forceOneShardAttributeValue(final String forceOneShardAtt return this; } + public Boolean getAllowRetry() { + return getOptions().allowRetry; + } + + /** + * @param allowRetry Set this option to true to make it possible to retry fetching the latest batch from a cursor. + *

+ * This makes possible to safely retry invoking {@link com.arangodb.ArangoCursor#next()} in + * case of I/O exceptions (which are actually thrown as {@link com.arangodb.ArangoDBException} + * with cause {@link java.io.IOException}) + *

+ * If set to false (default), then it is not safe to retry invoking + * {@link com.arangodb.ArangoCursor#next()} in case of I/O exceptions, since the request to + * fetch the next batch is not idempotent (i.e. the cursor may advance multiple times on the + * server). + *

+ * Note: once you successfully received the last batch, you should call + * {@link com.arangodb.ArangoCursor#close()} so that the server does not unnecessary keep the + * batch until the cursor times out ({@link AqlQueryOptions#ttl(Integer)}). + * @return options + * @since ArangoDB 3.11 + */ + public AqlQueryOptions allowRetry(final Boolean allowRetry) { + getOptions().allowRetry = allowRetry; + return this; + } + private Options getOptions() { if (options == null) { options = new Options(); @@ -463,6 +490,7 @@ public static class Options implements Serializable, Cloneable { private Double maxRuntime; private Boolean fillBlockCache; private String forceOneShardAttributeValue; + private Boolean allowRetry; protected Optimizer getOptimizer() { if (optimizer == null) { diff --git a/src/test/java/com/arangodb/ArangoDatabaseTest.java b/src/test/java/com/arangodb/ArangoDatabaseTest.java index 9099bd9e6..bb4315f5f 100644 --- a/src/test/java/com/arangodb/ArangoDatabaseTest.java +++ b/src/test/java/com/arangodb/ArangoDatabaseTest.java @@ -832,25 +832,32 @@ void queryWithMaxWarningCount(ArangoDatabase db) { @ParameterizedTest(name = "{index}") @MethodSource("dbs") void queryCursor(ArangoDatabase db) { - final int numbDocs = 10; - for (int i = 0; i < numbDocs; i++) { - db.collection(CNAME1).insertDocument(new BaseDocument(), null); - } - - final int batchSize = 5; - final ArangoCursor cursor = db.query("for i in " + CNAME1 + " return i._id", null, - new AqlQueryOptions().batchSize(batchSize).count(true), String.class); - assertThat((Object) cursor).isNotNull(); - assertThat(cursor.getCount()).isGreaterThanOrEqualTo(numbDocs); - - final ArangoCursor cursor2 = db.cursor(cursor.getId(), String.class); - assertThat((Object) cursor2).isNotNull(); - assertThat(cursor2.getCount()).isGreaterThanOrEqualTo(numbDocs); - assertThat((Iterator) cursor2).hasNext(); + ArangoCursor cursor = db.query("for i in 1..4 return i", new AqlQueryOptions().batchSize(1), Integer.class); + List result = new ArrayList<>(); + result.add(cursor.next()); + result.add(cursor.next()); + ArangoCursor cursor2 = db.cursor(cursor.getId(), Integer.class); + result.add(cursor2.next()); + result.add(cursor2.next()); + assertThat(cursor2.hasNext()).isFalse(); + assertThat(result).containsExactly(1, 2, 3, 4); + } - for (int i = 0; i < batchSize; i++, cursor.next()) { - assertThat((Iterator) cursor).hasNext(); - } + @ParameterizedTest(name = "{index}") + @MethodSource("dbs") + void queryCursorRetry(ArangoDatabase db) throws IOException { + assumeTrue(isAtLeastVersion(3, 11)); + ArangoCursor cursor = db.query("for i in 1..4 return i", + new AqlQueryOptions().batchSize(1).allowRetry(true), Integer.class); + List result = new ArrayList<>(); + result.add(cursor.next()); + result.add(cursor.next()); + ArangoCursor cursor2 = db.cursor(cursor.getId(), Integer.class, cursor.getNextBatchId()); + result.add(cursor2.next()); + result.add(cursor2.next()); + cursor2.close(); + assertThat(cursor2.hasNext()).isFalse(); + assertThat(result).containsExactly(1, 2, 3, 4); } @ParameterizedTest(name = "{index}") @@ -996,6 +1003,55 @@ void queryAllowDirtyRead(ArangoDatabase db) throws IOException { cursor.close(); } + @ParameterizedTest(name = "{index}") + @MethodSource("arangos") + void queryAllowRetry(ArangoDB arangoDB) throws IOException { + assumeTrue(isAtLeastVersion(3, 11)); + final ArangoCursor cursor = arangoDB.db() + .query("for i in 1..2 return i", new AqlQueryOptions().allowRetry(true).batchSize(1), String.class); + assertThat(cursor.asListRemaining()).containsExactly("1", "2"); + } + + @ParameterizedTest(name = "{index}") + @MethodSource("arangos") + void queryAllowRetryClose(ArangoDB arangoDB) throws IOException { + assumeTrue(isAtLeastVersion(3, 11)); + final ArangoCursor cursor = arangoDB.db() + .query("for i in 1..2 return i", new AqlQueryOptions().allowRetry(true).batchSize(1), String.class); + assertThat(cursor.hasNext()).isTrue(); + assertThat(cursor.next()).isEqualTo("1"); + assertThat(cursor.hasNext()).isTrue(); + assertThat(cursor.next()).isEqualTo("2"); + assertThat(cursor.hasNext()).isFalse(); + cursor.close(); + } + + @ParameterizedTest(name = "{index}") + @MethodSource("arangos") + void queryAllowRetryCloseBeforeLatestBatch(ArangoDB arangoDB) throws IOException { + assumeTrue(isAtLeastVersion(3, 11)); + final ArangoCursor cursor = arangoDB.db() + .query("for i in 1..2 return i", new AqlQueryOptions().allowRetry(true).batchSize(1), String.class); + assertThat(cursor.hasNext()).isTrue(); + assertThat(cursor.next()).isEqualTo("1"); + assertThat(cursor.hasNext()).isTrue(); + cursor.close(); + } + + @ParameterizedTest(name = "{index}") + @MethodSource("arangos") + void queryAllowRetryCloseSingleBatch(ArangoDB arangoDB) throws IOException { + assumeTrue(isAtLeastVersion(3, 11)); + final ArangoCursor cursor = arangoDB.db() + .query("for i in 1..2 return i", new AqlQueryOptions().allowRetry(true), String.class); + assertThat(cursor.hasNext()).isTrue(); + assertThat(cursor.next()).isEqualTo("1"); + assertThat(cursor.hasNext()).isTrue(); + assertThat(cursor.next()).isEqualTo("2"); + assertThat(cursor.hasNext()).isFalse(); + cursor.close(); + } + @ParameterizedTest(name = "{index}") @MethodSource("dbs") void explainQuery(ArangoDatabase db) { diff --git a/src/test/java/com/arangodb/async/ArangoDatabaseTest.java b/src/test/java/com/arangodb/async/ArangoDatabaseTest.java index de76da620..c84be23f5 100644 --- a/src/test/java/com/arangodb/async/ArangoDatabaseTest.java +++ b/src/test/java/com/arangodb/async/ArangoDatabaseTest.java @@ -20,8 +20,7 @@ package com.arangodb.async; -import com.arangodb.ArangoDBException; -import com.arangodb.DbName; +import com.arangodb.*; import com.arangodb.entity.AqlExecutionExplainEntity.ExecutionPlan; import com.arangodb.entity.*; import com.arangodb.entity.AqlParseEntity.AstNode; @@ -627,28 +626,32 @@ void queryWithCache() throws InterruptedException, ExecutionException { @Test void queryCursor() throws InterruptedException, ExecutionException { - try { - db.createCollection(COLLECTION_NAME, null).get(); - final int numbDocs = 10; - for (int i = 0; i < numbDocs; i++) { - db.collection(COLLECTION_NAME).insertDocument(new BaseDocument(), null).get(); - } - - final int batchSize = 5; - final ArangoCursorAsync cursor = db.query("for i in db_test return i._id", null, - new AqlQueryOptions().batchSize(batchSize).count(true), String.class).get(); - assertThat(cursor.getCount()).isEqualTo(numbDocs); - - final ArangoCursorAsync cursor2 = db.cursor(cursor.getId(), String.class).get(); - assertThat(cursor2.getCount()).isEqualTo(numbDocs); - assertThat(cursor2.hasNext()).isTrue(); - - for (int i = 0; i < batchSize; i++, cursor.next()) { - assertThat(cursor.hasNext()).isTrue(); - } - } finally { - db.collection(COLLECTION_NAME).drop().get(); - } + ArangoCursor cursor = db.query("for i in 1..4 return i", new AqlQueryOptions().batchSize(1), + Integer.class).get(); + List result = new ArrayList<>(); + result.add(cursor.next()); + result.add(cursor.next()); + ArangoCursor cursor2 = db.cursor(cursor.getId(), Integer.class).get(); + result.add(cursor2.next()); + result.add(cursor2.next()); + assertThat(cursor2.hasNext()).isFalse(); + assertThat(result).containsExactly(1, 2, 3, 4); + } + + @Test + void queryCursorRetry() throws IOException, ExecutionException, InterruptedException { + assumeTrue(isAtLeastVersion(3, 11)); + ArangoCursor cursor = db.query("for i in 1..4 return i", + new AqlQueryOptions().batchSize(1).allowRetry(true), Integer.class).get(); + List result = new ArrayList<>(); + result.add(cursor.next()); + result.add(cursor.next()); + ArangoCursor cursor2 = db.cursor(cursor.getId(), Integer.class, cursor.getNextBatchId()).get(); + result.add(cursor2.next()); + result.add(cursor2.next()); + cursor2.close(); + assertThat(cursor2.hasNext()).isFalse(); + assertThat(result).containsExactly(1, 2, 3, 4); } @Test @@ -724,6 +727,51 @@ void queryClose() throws IOException, InterruptedException, ExecutionException { } + @Test + void queryAllowRetry() throws IOException, ExecutionException, InterruptedException { + assumeTrue(isAtLeastVersion(3, 11)); + final ArangoCursor cursor = arangoDB.db() + .query("for i in 1..2 return i", new AqlQueryOptions().allowRetry(true).batchSize(1), String.class).get(); + assertThat(cursor.asListRemaining()).containsExactly("1", "2"); + } + + @Test + void queryAllowRetryClose() throws IOException, ExecutionException, InterruptedException { + assumeTrue(isAtLeastVersion(3, 11)); + final ArangoCursor cursor = arangoDB.db() + .query("for i in 1..2 return i", new AqlQueryOptions().allowRetry(true).batchSize(1), String.class).get(); + assertThat(cursor.hasNext()).isTrue(); + assertThat(cursor.next()).isEqualTo("1"); + assertThat(cursor.hasNext()).isTrue(); + assertThat(cursor.next()).isEqualTo("2"); + assertThat(cursor.hasNext()).isFalse(); + cursor.close(); + } + + @Test + void queryAllowRetryCloseBeforeLatestBatch() throws IOException, ExecutionException, InterruptedException { + assumeTrue(isAtLeastVersion(3, 11)); + final ArangoCursor cursor = arangoDB.db() + .query("for i in 1..2 return i", new AqlQueryOptions().allowRetry(true).batchSize(1), String.class).get(); + assertThat(cursor.hasNext()).isTrue(); + assertThat(cursor.next()).isEqualTo("1"); + assertThat(cursor.hasNext()).isTrue(); + cursor.close(); + } + + @Test + void queryAllowRetryCloseSingleBatch() throws IOException, ExecutionException, InterruptedException { + assumeTrue(isAtLeastVersion(3, 11)); + final ArangoCursor cursor = arangoDB.db() + .query("for i in 1..2 return i", new AqlQueryOptions().allowRetry(true), String.class).get(); + assertThat(cursor.hasNext()).isTrue(); + assertThat(cursor.next()).isEqualTo("1"); + assertThat(cursor.hasNext()).isTrue(); + assertThat(cursor.next()).isEqualTo("2"); + assertThat(cursor.hasNext()).isFalse(); + cursor.close(); + } + @Test void explainQuery() throws InterruptedException, ExecutionException { arangoDB.db().explainQuery("for i in 1..1 return i", null, null)