diff --git a/core/src/main/java/com/arangodb/ArangoCursor.java b/core/src/main/java/com/arangodb/ArangoCursor.java index 259b81d73..05c378636 100644 --- a/core/src/main/java/com/arangodb/ArangoCursor.java +++ b/core/src/main/java/com/arangodb/ArangoCursor.java @@ -22,10 +22,12 @@ import com.arangodb.entity.CursorStats; import com.arangodb.entity.CursorWarning; +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 +78,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/core/src/main/java/com/arangodb/ArangoDatabase.java b/core/src/main/java/com/arangodb/ArangoDatabase.java index dfc99ea64..db4c9a6d7 100644 --- a/core/src/main/java/com/arangodb/ArangoDatabase.java +++ b/core/src/main/java/com/arangodb/ArangoDatabase.java @@ -310,6 +310,20 @@ public interface ArangoDatabase extends ArangoSerdeAccessor { */ ArangoCursor cursor(String cursorId, 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 or {@link com.arangodb.util.RawData}) + * @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/core/src/main/java/com/arangodb/entity/MetaAware.java b/core/src/main/java/com/arangodb/entity/MetaAware.java deleted file mode 100644 index a6ddc9e14..000000000 --- a/core/src/main/java/com/arangodb/entity/MetaAware.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.arangodb.entity; - -import java.util.Map; - -/** - * @author Mark Vollmary - */ -public interface MetaAware { - - Map getMeta(); - - void setMeta(final Map meta); - -} diff --git a/core/src/main/java/com/arangodb/internal/ArangoCursorExecute.java b/core/src/main/java/com/arangodb/internal/ArangoCursorExecute.java index 7efc5eef9..209f11cff 100644 --- a/core/src/main/java/com/arangodb/internal/ArangoCursorExecute.java +++ b/core/src/main/java/com/arangodb/internal/ArangoCursorExecute.java @@ -22,15 +22,14 @@ import com.arangodb.internal.cursor.entity.InternalCursorEntity; -import java.util.Map; /** * @author Mark Vollmary */ public interface ArangoCursorExecute { - InternalCursorEntity next(String id, Map meta); + InternalCursorEntity next(String id, String nextBatchId); - void close(String id, Map meta); + void close(String id); } diff --git a/core/src/main/java/com/arangodb/internal/ArangoDatabaseImpl.java b/core/src/main/java/com/arangodb/internal/ArangoDatabaseImpl.java index b17048afa..0ad1abdb1 100644 --- a/core/src/main/java/com/arangodb/internal/ArangoDatabaseImpl.java +++ b/core/src/main/java/com/arangodb/internal/ArangoDatabaseImpl.java @@ -161,7 +161,7 @@ public ArangoCursor query( final String query, final Class type, final Map bindVars, final AqlQueryOptions options) { final InternalRequest request = queryRequest(query, bindVars, options); final HostHandle hostHandle = new HostHandle(); - final InternalCursorEntity result = executor.execute(request, InternalCursorEntity.class, hostHandle); + final InternalCursorEntity result = executor.execute(request, internalCursorEntityDeserializer(), hostHandle); return createCursor(result, type, options, hostHandle); } @@ -183,8 +183,20 @@ public ArangoCursor query(final String query, final Class type) { @Override public ArangoCursor cursor(final String cursorId, final Class type) { final HostHandle hostHandle = new HostHandle(); - final InternalCursorEntity result = executor - .execute(queryNextRequest(cursorId, null, null), InternalCursorEntity.class, hostHandle); + final InternalCursorEntity result = executor.execute( + queryNextRequest(cursorId, null), + internalCursorEntityDeserializer(), + hostHandle); + 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 InternalCursorEntity result = executor.execute( + queryNextByBatchIdRequest(cursorId, nextBatchId, null), + internalCursorEntityDeserializer(), + hostHandle); return createCursor(result, type, null, hostHandle); } @@ -196,13 +208,15 @@ private ArangoCursor createCursor( final ArangoCursorExecute execute = new ArangoCursorExecute() { @Override - public InternalCursorEntity next(final String id, Map meta) { - return executor.execute(queryNextRequest(id, options, meta), InternalCursorEntity.class, hostHandle); + public InternalCursorEntity next(final String id, final String nextBatchId) { + InternalRequest request = nextBatchId == null ? + queryNextRequest(id, options) : queryNextByBatchIdRequest(id, nextBatchId, options); + return executor.execute(request, internalCursorEntityDeserializer(), hostHandle); } @Override - public void close(final String id, Map meta) { - executor.execute(queryCloseRequest(id, options, meta), Void.class, hostHandle); + public void close(final String id) { + executor.execute(queryCloseRequest(id, options), Void.class, hostHandle); } }; diff --git a/core/src/main/java/com/arangodb/internal/ArangoExecutorSync.java b/core/src/main/java/com/arangodb/internal/ArangoExecutorSync.java index 00a6ae6f7..c0f10815f 100644 --- a/core/src/main/java/com/arangodb/internal/ArangoExecutorSync.java +++ b/core/src/main/java/com/arangodb/internal/ArangoExecutorSync.java @@ -21,12 +21,9 @@ package com.arangodb.internal; import com.arangodb.ArangoDBException; -import com.arangodb.entity.MetaAware; import com.arangodb.internal.config.ArangoConfig; import com.arangodb.internal.net.CommunicationProtocol; import com.arangodb.internal.net.HostHandle; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.io.IOException; import java.lang.reflect.Type; @@ -36,8 +33,6 @@ */ public class ArangoExecutorSync extends ArangoExecutor { - private static final Logger LOG = LoggerFactory.getLogger(ArangoExecutorSync.class); - private final CommunicationProtocol protocol; public ArangoExecutorSync(final CommunicationProtocol protocol, final ArangoConfig config) { @@ -64,14 +59,7 @@ public T execute( final InternalResponse response = protocol.execute(interceptRequest(request), hostHandle); interceptResponse(response); - T deserialize = responseDeserializer.deserialize(response); - - if (deserialize instanceof MetaAware) { - LOG.debug("Response is MetaAware {}", deserialize.getClass().getName()); - ((MetaAware) deserialize).setMeta(response.getMeta()); - } - - return deserialize; + return responseDeserializer.deserialize(response); } public void disconnect() { diff --git a/core/src/main/java/com/arangodb/internal/InternalArangoDatabase.java b/core/src/main/java/com/arangodb/internal/InternalArangoDatabase.java index 9fdb8c705..3db44c1f7 100644 --- a/core/src/main/java/com/arangodb/internal/InternalArangoDatabase.java +++ b/core/src/main/java/com/arangodb/internal/InternalArangoDatabase.java @@ -23,6 +23,7 @@ import com.arangodb.entity.*; import com.arangodb.entity.arangosearch.analyzer.SearchAnalyzer; import com.arangodb.internal.ArangoExecutor.ResponseDeserializer; +import com.arangodb.internal.cursor.entity.InternalCursorEntity; import com.arangodb.internal.util.RequestUtils; import com.arangodb.model.*; import com.arangodb.model.arangosearch.*; @@ -156,13 +157,20 @@ protected InternalRequest queryRequest(final String query, final Map meta) { - + protected InternalRequest queryNextRequest(final String id, final AqlQueryOptions options) { final InternalRequest request = request(name, RequestType.POST, PATH_API_CURSOR, id); - request.putHeaderParams(meta); + return completeQueryNextRequest(request, options); + } - final AqlQueryOptions opt = options != null ? options : new AqlQueryOptions(); + protected InternalRequest queryNextByBatchIdRequest(final String id, + final String nextBatchId, + final AqlQueryOptions options) { + final InternalRequest request = request(name, RequestType.POST, PATH_API_CURSOR, id, nextBatchId); + return completeQueryNextRequest(request, options); + } + private InternalRequest completeQueryNextRequest(final InternalRequest request, final AqlQueryOptions options) { + final AqlQueryOptions opt = options != null ? options : new AqlQueryOptions(); if (Boolean.TRUE.equals(opt.getAllowDirtyRead())) { RequestUtils.allowDirtyRead(request); } @@ -170,13 +178,9 @@ protected InternalRequest queryNextRequest(final String id, final AqlQueryOption return request; } - protected InternalRequest queryCloseRequest(final String id, final AqlQueryOptions options, Map meta) { - + protected InternalRequest queryCloseRequest(final String id, final AqlQueryOptions options) { final InternalRequest request = request(name, RequestType.DELETE, PATH_API_CURSOR, id); - request.putHeaderParams(meta); - final AqlQueryOptions opt = options != null ? options : new AqlQueryOptions(); - if (Boolean.TRUE.equals(opt.getAllowDirtyRead())) { RequestUtils.allowDirtyRead(request); } @@ -243,6 +247,15 @@ protected InternalRequest deleteAqlFunctionRequest(final String name, final AqlF return request; } + protected ResponseDeserializer internalCursorEntityDeserializer() { + return response -> { + InternalCursorEntity e = getSerde().deserialize(response.getBody(), InternalCursorEntity.class); + boolean potentialDirtyRead = Boolean.parseBoolean(response.getMeta("X-Arango-Potential-Dirty-Read")); + e.setPontentialDirtyRead(potentialDirtyRead); + return e; + }; + } + protected ResponseDeserializer deleteAqlFunctionResponseDeserializer() { return response -> getSerde().deserialize(response.getBody(), "/deletedCount", Integer.class); } diff --git a/core/src/main/java/com/arangodb/internal/cursor/AbstractArangoIterable.java b/core/src/main/java/com/arangodb/internal/cursor/AbstractArangoIterable.java deleted file mode 100644 index 958446e67..000000000 --- a/core/src/main/java/com/arangodb/internal/cursor/AbstractArangoIterable.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * DISCLAIMER - * - * Copyright 2018 ArangoDB GmbH, Cologne, Germany - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * Copyright holder is ArangoDB GmbH, Cologne, Germany - */ - -package com.arangodb.internal.cursor; - -import com.arangodb.ArangoIterable; - -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - -/** - * @author Mark Vollmary - */ -public abstract class AbstractArangoIterable implements ArangoIterable { - - @Override - public Stream stream() { - return StreamSupport.stream(spliterator(), false); - } - -} diff --git a/core/src/main/java/com/arangodb/internal/cursor/ArangoCursorImpl.java b/core/src/main/java/com/arangodb/internal/cursor/ArangoCursorImpl.java index 2dfdbd194..8112e8ce4 100644 --- a/core/src/main/java/com/arangodb/internal/cursor/ArangoCursorImpl.java +++ b/core/src/main/java/com/arangodb/internal/cursor/ArangoCursorImpl.java @@ -27,39 +27,52 @@ import com.arangodb.internal.ArangoCursorExecute; import com.arangodb.internal.InternalArangoDatabase; import com.arangodb.internal.cursor.entity.InternalCursorEntity; -import com.arangodb.internal.cursor.entity.InternalCursorEntity.Extras; +import com.fasterxml.jackson.databind.JsonNode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Collection; +import java.util.Iterator; +import java.util.NoSuchElementException; import java.util.List; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; /** * @author Mark Vollmary */ -public class ArangoCursorImpl extends AbstractArangoIterable implements ArangoCursor { +public class ArangoCursorImpl implements ArangoCursor { + private final static Logger LOG = LoggerFactory.getLogger(ArangoCursorImpl.class); protected final ArangoCursorIterator iterator; private final Class type; 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 InternalCursorEntity result) { super(); this.execute = execute; this.type = type; - iterator = createIterator(this, db, execute, result); id = result.getId(); - this.isPontentialDirtyRead = Boolean.parseBoolean(result.getMeta().get("x-arango-potential-dirty-read")); + pontentialDirtyRead = result.isPontentialDirtyRead(); + iterator = new ArangoCursorIterator<>(id, type, execute, db, result); + this.allowRetry = result.getNextBatchId() != null; } - protected ArangoCursorIterator createIterator( - final ArangoCursor cursor, - final InternalArangoDatabase db, - final ArangoCursorExecute execute, - final InternalCursorEntity result) { - return new ArangoCursorIterator<>(cursor, execute, db, result); + @Override + public void close() { + if (getId() != null && (allowRetry || iterator.result.getHasMore())) { + getExecute().close(getId()); + } + } + + @Override + public T next() { + return iterator.next(); } @Override @@ -74,66 +87,109 @@ public Class getType() { @Override public Integer getCount() { - return iterator.getResult().getCount(); + return iterator.result.getCount(); } @Override public CursorStats getStats() { - final Extras extra = iterator.getResult().getExtra(); + final InternalCursorEntity.Extras extra = iterator.result.getExtra(); return extra != null ? extra.getStats() : null; } @Override public Collection getWarnings() { - final Extras extra = iterator.getResult().getExtra(); + final InternalCursorEntity.Extras extra = iterator.result.getExtra(); return extra != null ? extra.getWarnings() : null; } @Override public boolean isCached() { - final Boolean cached = iterator.getResult().getCached(); + final Boolean cached = iterator.result.getCached(); return Boolean.TRUE.equals(cached); } - @Override - public void close() { - if (id != null && hasNext()) { - execute.close(id, iterator.getResult().getMeta()); - } - } - @Override public boolean hasNext() { return iterator.hasNext(); } - @Override - public T next() { - return iterator.next(); - } - @Override public List asListRemaining() { final List remaining = new ArrayList<>(); 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 - public void remove() { - throw new UnsupportedOperationException(); + public ArangoIterator iterator() { + return iterator; } @Override - public ArangoIterator iterator() { - return iterator; + public String getNextBatchId() { + return iterator.result.getNextBatchId(); + } + + protected ArangoCursorExecute getExecute() { + return execute; + } + + public Stream stream() { + return StreamSupport.stream(spliterator(), false); + } + + protected static class ArangoCursorIterator implements ArangoIterator { + private final String cursorId; + private final Class type; + private final InternalArangoDatabase db; + private final ArangoCursorExecute execute; + private InternalCursorEntity result; + private Iterator arrayIterator; + + protected ArangoCursorIterator(final String cursorId, final Class type, final ArangoCursorExecute execute, + final InternalArangoDatabase db, final InternalCursorEntity result) { + this.cursorId = cursorId; + this.type = type; + this.execute = execute; + this.db = db; + this.result = result; + arrayIterator = result.getResult().iterator(); + } + + @Override + public boolean hasNext() { + return arrayIterator.hasNext() || result.getHasMore(); + } + + @Override + public T next() { + if (!arrayIterator.hasNext() && Boolean.TRUE.equals(result.getHasMore())) { + result = execute.next(cursorId, result.getNextBatchId()); + arrayIterator = result.getResult().iterator(); + } + if (!hasNext()) { + throw new NoSuchElementException(); + } + return deserialize(db.getSerde().serialize(arrayIterator.next()), type); + } + + private R deserialize(final byte[] result, final Class type) { + return db.getSerde().deserializeUserData(result, type); + } + } } + diff --git a/core/src/main/java/com/arangodb/internal/cursor/ArangoCursorIterator.java b/core/src/main/java/com/arangodb/internal/cursor/ArangoCursorIterator.java deleted file mode 100644 index e6d681045..000000000 --- a/core/src/main/java/com/arangodb/internal/cursor/ArangoCursorIterator.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * DISCLAIMER - * - * Copyright 2016 ArangoDB GmbH, Cologne, Germany - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * Copyright holder is ArangoDB GmbH, Cologne, Germany - */ - -package com.arangodb.internal.cursor; - -import com.arangodb.ArangoCursor; -import com.arangodb.ArangoIterator; -import com.arangodb.internal.ArangoCursorExecute; -import com.arangodb.internal.InternalArangoDatabase; -import com.arangodb.internal.cursor.entity.InternalCursorEntity; -import com.fasterxml.jackson.databind.JsonNode; - -import java.util.Iterator; -import java.util.NoSuchElementException; - -/** - * @param - * @author Mark Vollmary - */ -public class ArangoCursorIterator implements ArangoIterator { - - private final ArangoCursor cursor; - private final InternalArangoDatabase db; - private final ArangoCursorExecute execute; - private InternalCursorEntity result; - private Iterator arrayIterator; - - protected ArangoCursorIterator(final ArangoCursor cursor, final ArangoCursorExecute execute, - final InternalArangoDatabase db, final InternalCursorEntity result) { - super(); - this.cursor = cursor; - this.execute = execute; - this.db = db; - this.result = result; - arrayIterator = result.getResult().iterator(); - } - - public InternalCursorEntity getResult() { - return result; - } - - @Override - public boolean hasNext() { - return arrayIterator.hasNext() || result.getHasMore(); - } - - @Override - public T next() { - if (!arrayIterator.hasNext() && Boolean.TRUE.equals(result.getHasMore())) { - result = execute.next(cursor.getId(), result.getMeta()); - arrayIterator = result.getResult().iterator(); - } - if (!hasNext()) { - throw new NoSuchElementException(); - } - return deserialize(db.getSerde().serialize(arrayIterator.next()), cursor.getType()); - } - - protected R deserialize(final byte[] result, final Class type) { - return db.getSerde().deserializeUserData(result, type); - } - - @Override - public void remove() { - throw new UnsupportedOperationException(); - } - -} diff --git a/core/src/main/java/com/arangodb/internal/cursor/entity/InternalCursorEntity.java b/core/src/main/java/com/arangodb/internal/cursor/entity/InternalCursorEntity.java index 535715e3b..6d479d0fe 100644 --- a/core/src/main/java/com/arangodb/internal/cursor/entity/InternalCursorEntity.java +++ b/core/src/main/java/com/arangodb/internal/cursor/entity/InternalCursorEntity.java @@ -22,20 +22,17 @@ import com.arangodb.entity.CursorStats; import com.arangodb.entity.CursorWarning; -import com.arangodb.entity.MetaAware; import com.fasterxml.jackson.databind.JsonNode; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; -import java.util.Map; /** * @author Mark Vollmary * @see API * Documentation */ -public final class InternalCursorEntity implements MetaAware { +public final class InternalCursorEntity { private final Extras extra = new Extras(); private String id; @@ -43,8 +40,8 @@ public final class InternalCursorEntity implements MetaAware { private Boolean cached; private Boolean hasMore; private JsonNode result; - - private Map meta; + private Boolean pontentialDirtyRead; + private String nextBatchId; public String getId() { return id; @@ -91,25 +88,25 @@ public JsonNode getResult() { return result; } - @Override - public Map getMeta() { - if (meta == null) return Collections.emptyMap(); - return Collections.unmodifiableMap(meta); + /** + * @return true if the result is a potential dirty read + * @since ArangoDB 3.10 + */ + public Boolean isPontentialDirtyRead() { + return pontentialDirtyRead; } - @Override - public void setMeta(Map meta) { - this.meta = cleanupMeta(new HashMap<>(meta)); + public void setPontentialDirtyRead(final Boolean pontentialDirtyRead) { + this.pontentialDirtyRead = pontentialDirtyRead; } /** - * @return remove not allowed (valid storable) meta information + * @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 */ - public Map cleanupMeta(Map meta) { - meta.remove("content-length"); - meta.remove("transfer-encoding"); - meta.remove("x-arango-queue-time-seconds"); - return meta; + public String getNextBatchId() { + return nextBatchId; } public static final class Extras { diff --git a/core/src/main/java/com/arangodb/model/AqlQueryOptions.java b/core/src/main/java/com/arangodb/model/AqlQueryOptions.java index 3aba96dd4..5b12698a4 100644 --- a/core/src/main/java/com/arangodb/model/AqlQueryOptions.java +++ b/core/src/main/java/com/arangodb/model/AqlQueryOptions.java @@ -43,10 +43,6 @@ public final class AqlQueryOptions implements Cloneable { private Boolean allowDirtyRead; private String streamTransactionId; - public AqlQueryOptions() { - super(); - } - public Boolean getCount() { return count; } @@ -489,6 +485,33 @@ public AqlQueryOptions streamTransactionId(final String streamTransactionId) { 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; + } + @Override public AqlQueryOptions clone() { try { @@ -519,6 +542,7 @@ public static final class Options implements Cloneable { private Double maxRuntime; private Boolean fillBlockCache; private String forceOneShardAttributeValue; + private Boolean allowRetry; public Boolean getFailOnWarning() { return failOnWarning; @@ -587,6 +611,10 @@ public Collection getShardIds() { return shardIds; } + public Boolean getAllowRetry() { + return allowRetry; + } + @Override public Options clone() { try { diff --git a/driver/src/main/resources/META-INF/native-image/com.arangodb/arangodb-java-driver/reflect-config.json b/driver/src/main/resources/META-INF/native-image/com.arangodb/arangodb-java-driver/reflect-config.json index 4f9cd16ca..3325fca05 100644 --- a/driver/src/main/resources/META-INF/native-image/com.arangodb/arangodb-java-driver/reflect-config.json +++ b/driver/src/main/resources/META-INF/native-image/com.arangodb/arangodb-java-driver/reflect-config.json @@ -335,12 +335,6 @@ "allDeclaredMethods": true, "allDeclaredConstructors": true }, - { - "name": "com.arangodb.entity.MetaAware", - "allDeclaredFields": true, - "allDeclaredMethods": true, - "allDeclaredConstructors": true - }, { "name": "com.arangodb.entity.arangosearch.analyzer.GeoS2AnalyzerProperties", "allDeclaredFields": true, @@ -1529,12 +1523,6 @@ "allDeclaredMethods": true, "allDeclaredConstructors": true }, - { - "name": "com.arangodb.entity.MetaAware", - "allDeclaredFields": true, - "allDeclaredMethods": true, - "allDeclaredConstructors": true - }, { "name": "com.arangodb.internal.cursor.entity.InternalCursorEntity", "allDeclaredFields": true, diff --git a/driver/src/test/java/com/arangodb/ArangoDatabaseTest.java b/driver/src/test/java/com/arangodb/ArangoDatabaseTest.java index b8b249b00..ff7fdeda2 100644 --- a/driver/src/test/java/com/arangodb/ArangoDatabaseTest.java +++ b/driver/src/test/java/com/arangodb/ArangoDatabaseTest.java @@ -834,25 +834,33 @@ 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", String.class, - new AqlQueryOptions().batchSize(batchSize).count(true)); - 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", Integer.class, + new AqlQueryOptions().batchSize(1)); + 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", Integer.class, + new AqlQueryOptions().batchSize(1).allowRetry(true)); + 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}") @@ -1006,6 +1014,55 @@ BaseDocument.class, new MapBuilder().put("@col", CNAME1).put("test", null).get() 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", String.class, new AqlQueryOptions().allowRetry(true).batchSize(1)); + 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", String.class, new AqlQueryOptions().allowRetry(true).batchSize(1)); + 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", String.class, new AqlQueryOptions().allowRetry(true).batchSize(1)); + 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", String.class, new AqlQueryOptions().allowRetry(true)); + 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/resilience-tests/src/test/java/resilience/retry/RetriableCursorTest.java b/resilience-tests/src/test/java/resilience/retry/RetriableCursorTest.java new file mode 100644 index 000000000..51f7c0c2f --- /dev/null +++ b/resilience-tests/src/test/java/resilience/retry/RetriableCursorTest.java @@ -0,0 +1,56 @@ +package resilience.retry; + +import com.arangodb.ArangoCursor; +import com.arangodb.ArangoDB; +import com.arangodb.ArangoDBException; +import com.arangodb.Protocol; +import com.arangodb.model.AqlQueryOptions; +import eu.rekawek.toxiproxy.model.ToxicDirection; +import eu.rekawek.toxiproxy.model.toxic.Latency; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import resilience.SingleServerTest; + +import java.io.IOException; +import java.util.concurrent.TimeoutException; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; + +/** + * @author Michele Rastelli + */ +class RetriableCursorTest extends SingleServerTest { + + static Stream arangoProvider() { + return Stream.of( + dbBuilder().timeout(1_000).protocol(Protocol.VST).build(), + dbBuilder().timeout(1_000).protocol(Protocol.HTTP_JSON).build(), + dbBuilder().timeout(1_000).protocol(Protocol.HTTP_VPACK).build() + ); + } + + @ParameterizedTest + @MethodSource("arangoProvider") + void retryCursor(ArangoDB arangoDB) throws IOException { + try (ArangoCursor cursor = arangoDB.db() + .query("for i in 1..2 return i", + String.class, + new AqlQueryOptions().batchSize(1).allowRetry(true))) { + + assertThat(cursor.hasNext()).isTrue(); + assertThat(cursor.next()).isEqualTo("1"); + assertThat(cursor.hasNext()).isTrue(); + Latency toxic = getEndpoint().getProxy().toxics().latency("latency", ToxicDirection.DOWNSTREAM, 10_000); + Throwable thrown = catchThrowable(cursor::next); + assertThat(thrown).isInstanceOf(ArangoDBException.class); + assertThat(thrown.getCause()).isInstanceOfAny(TimeoutException.class); + toxic.remove(); + assertThat(cursor.next()).isEqualTo("2"); + assertThat(cursor.hasNext()).isFalse(); + } + arangoDB.shutdown(); + } + +} diff --git a/shaded/src/main/resources/META-INF/native-image/com.arangodb/arangodb-java-driver-shaded/reflect-config.json b/shaded/src/main/resources/META-INF/native-image/com.arangodb/arangodb-java-driver-shaded/reflect-config.json index 4f9cd16ca..3325fca05 100644 --- a/shaded/src/main/resources/META-INF/native-image/com.arangodb/arangodb-java-driver-shaded/reflect-config.json +++ b/shaded/src/main/resources/META-INF/native-image/com.arangodb/arangodb-java-driver-shaded/reflect-config.json @@ -335,12 +335,6 @@ "allDeclaredMethods": true, "allDeclaredConstructors": true }, - { - "name": "com.arangodb.entity.MetaAware", - "allDeclaredFields": true, - "allDeclaredMethods": true, - "allDeclaredConstructors": true - }, { "name": "com.arangodb.entity.arangosearch.analyzer.GeoS2AnalyzerProperties", "allDeclaredFields": true, @@ -1529,12 +1523,6 @@ "allDeclaredMethods": true, "allDeclaredConstructors": true }, - { - "name": "com.arangodb.entity.MetaAware", - "allDeclaredFields": true, - "allDeclaredMethods": true, - "allDeclaredConstructors": true - }, { "name": "com.arangodb.internal.cursor.entity.InternalCursorEntity", "allDeclaredFields": true,