Expand Up @@ -24,6 +24,7 @@
import org.apache.tinkerpop.gremlin.process.traversal.P;
import org.apache.tinkerpop.gremlin.process.traversal.TextP;
import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__;
import org.apache.tinkerpop.gremlin.process.traversal.util.Metrics;
Expand Down Expand Up @@ -146,6 +147,7 @@
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.EnumMap;
Expand All @@ -162,6 +164,7 @@
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
Expand All @@ -181,6 +184,7 @@
import static org.janusgraph.graphdb.configuration.GraphDatabaseConfiguration.FORCE_INDEX_USAGE;
import static org.janusgraph.graphdb.configuration.GraphDatabaseConfiguration.HARD_MAX_LIMIT;
import static org.janusgraph.graphdb.configuration.GraphDatabaseConfiguration.INITIAL_JANUSGRAPH_VERSION;
import static org.janusgraph.graphdb.configuration.GraphDatabaseConfiguration.LIMIT_BATCH_SIZE;
import static org.janusgraph.graphdb.configuration.GraphDatabaseConfiguration.LOG_BACKEND;
import static org.janusgraph.graphdb.configuration.GraphDatabaseConfiguration.LOG_READ_INTERVAL;
import static org.janusgraph.graphdb.configuration.GraphDatabaseConfiguration.LOG_SEND_DELAY;
Expand Down Expand Up @@ -4545,6 +4549,113 @@ public void testMultiQueryMetricsWhenReadingFromBackend() {
assertTrue(queryProfilerAnnotationIsPresent(t, QueryProfiler.MULTIQUERY_ANNOTATION));
}

@Test
public void testLimitBatchSizeForMultiQuery() {
li-boxuan marked this conversation as resolved.
Show resolved Hide resolved
int numV = 100;
JanusGraphVertex a = graph.addVertex();
JanusGraphVertex[] bs = new JanusGraphVertex[numV];
JanusGraphVertex[] cs = new JanusGraphVertex[numV];
for (int i = 0; i < numV; ++i) {
bs[i] = graph.addVertex();
cs[i] = graph.addVertex();
cs[i].property("foo", "bar");
a.addEdge("knows", bs[i]);
bs[i].addEdge("knows", cs[i]);
}

int barrierSize = 27;
int limit = 40;

// test batching for `out()`
Supplier<GraphTraversal<?, ?>> traversal = () -> graph.traversal().V(bs).barrier(barrierSize).out();
assertEqualResultWithAndWithoutLimitBatchSize(traversal);
clopen(option(USE_MULTIQUERY), true, option(LIMIT_BATCH_SIZE), true);
TraversalMetrics profile = traversal.get().profile().next();
assertEquals(3, countBackendQueriesOfSize(barrierSize * 2, profile.getMetrics()));
assertEquals(1, countBackendQueriesOfSize((numV - 3 * barrierSize) * 2, profile.getMetrics()));
rngcntr marked this conversation as resolved.
Show resolved Hide resolved

// test early abort with limit for `out()`
traversal = () -> graph.traversal().V(bs).barrier(barrierSize).out().limit(limit);
assertEqualResultWithAndWithoutLimitBatchSize(traversal);
clopen(option(USE_MULTIQUERY), true, option(LIMIT_BATCH_SIZE), true);
profile = traversal.get().profile().next();
assertEquals((int) Math.ceil((double) limit / barrierSize), countBackendQueriesOfSize(barrierSize * 2, profile.getMetrics()));

// test batching for `values()`
traversal = () -> graph.traversal().V(cs).barrier(barrierSize).values("foo");
assertEqualResultWithAndWithoutLimitBatchSize(traversal);
clopen(option(USE_MULTIQUERY), true, option(LIMIT_BATCH_SIZE), true);
profile = traversal.get().profile().next();
assertEquals(3, countBackendQueriesOfSize(barrierSize, profile.getMetrics()));
assertEquals(1, countBackendQueriesOfSize(numV - 3 * barrierSize, profile.getMetrics()));

// test early abort with limit for `values()`
traversal = () -> graph.traversal().V(cs).barrier(barrierSize).values("foo").limit(limit);
assertEqualResultWithAndWithoutLimitBatchSize(traversal);
clopen(option(USE_MULTIQUERY), true, option(LIMIT_BATCH_SIZE), true);
profile = traversal.get().profile().next();
assertEquals((int) Math.ceil((double) limit / barrierSize), countBackendQueriesOfSize(barrierSize, profile.getMetrics()));

// test batching with unlimited batch size
traversal = () -> graph.traversal().V(bs).barrier(barrierSize).out();
assertEqualResultWithAndWithoutLimitBatchSize(traversal);
clopen(option(USE_MULTIQUERY), true, option(LIMIT_BATCH_SIZE), false);
profile = traversal.get().profile().next();
assertEquals(0, countBackendQueriesOfSize(barrierSize, profile.getMetrics()));
assertEquals(0, countBackendQueriesOfSize(barrierSize * 2, profile.getMetrics()));
assertEquals(1, countBackendQueriesOfSize(bs.length * 2, profile.getMetrics()));

// test nested VertexStep with unlimited batch size
traversal = () -> graph.traversal().V(bs).barrier(barrierSize).where(__.out());
assertEqualResultWithAndWithoutLimitBatchSize(traversal);
clopen(option(USE_MULTIQUERY), true, option(LIMIT_BATCH_SIZE), false);
profile = traversal.get().profile().next();
assertEquals(0, countBackendQueriesOfSize(barrierSize, profile.getMetrics()));
assertEquals(0, countBackendQueriesOfSize(barrierSize * 2, profile.getMetrics()));
assertEquals(1, countBackendQueriesOfSize(bs.length * 2, profile.getMetrics()));

// test nested VertexStep with non-nested barrier
traversal = () -> graph.traversal().V(bs).barrier(barrierSize).where(__.out());
assertEqualResultWithAndWithoutLimitBatchSize(traversal);
clopen(option(USE_MULTIQUERY), true, option(LIMIT_BATCH_SIZE), true);
profile = traversal.get().profile().next();
assertEquals(3, countBackendQueriesOfSize(barrierSize * 2, profile.getMetrics()));
assertEquals(1, countBackendQueriesOfSize((numV - 3 * barrierSize) * 2, profile.getMetrics()));

// test batching with repeat step
traversal = () -> graph.traversal().V(a).repeat(__.barrier(barrierSize).out()).times(2);
assertEqualResultWithAndWithoutLimitBatchSize(traversal);
clopen(option(USE_MULTIQUERY), true, option(LIMIT_BATCH_SIZE), true);
profile = traversal.get().profile().next();
assertEquals(3, countBackendQueriesOfSize(barrierSize * 2, profile.getMetrics()));
assertEquals(1, countBackendQueriesOfSize((numV - 3 * barrierSize) * 2, profile.getMetrics()));
}

private void assertEqualResultWithAndWithoutLimitBatchSize(Supplier<GraphTraversal<?, ?>> traversal) {
clopen(option(USE_MULTIQUERY), true, option(LIMIT_BATCH_SIZE), true);
final List<?> resultLimitedBatch = traversal.get().toList();
clopen(option(USE_MULTIQUERY), true, option(LIMIT_BATCH_SIZE), false);
final List<?> resultUnimitedBatch = traversal.get().toList();
clopen(option(USE_MULTIQUERY), false);
final List<?> resultNoMultiQuery = traversal.get().toList();

assertEquals(resultLimitedBatch, resultUnimitedBatch);
assertEquals(resultLimitedBatch, resultNoMultiQuery);
}

private long countBackendQueriesOfSize(long size, Collection<? extends Metrics> metrics) {
long count = metrics.stream()
.filter(m -> m.getName().equals("backend-query"))
.map(m -> m.getCounts())
.flatMap(c -> c.values().stream())
.filter(s -> s == size)
.count();
long nestedCount = metrics.stream()
.mapToLong(m -> countBackendQueriesOfSize(size, m.getNested()))
.sum();
return count + nestedCount;
}

@Test
public void testSimpleTinkerPopTraversal() {
Vertex v1 = graph.addVertex("name", "josh");
Expand Down
Expand Up @@ -262,6 +262,13 @@ public class GraphDatabaseConfiguration {
"performance improvement if there is a non-trivial latency to the backend.",
ConfigOption.Type.MASKABLE, false);

public static final ConfigOption<Boolean> LIMIT_BATCH_SIZE = new ConfigOption<>(QUERY_NS,"limit-batch-size",
"Configure a maximum batch size for queries against the storage backend. This can be used to ensure " +
"responsiveness if batches tend to grow very large. The used batch size is equivalent to the " +
"barrier size of a preceding barrier() step. If a step has no preceding barrier(), the default barrier of TinkerPop " +
"will be inserted. This option only takes effect if query.batch is enabled.",
ConfigOption.Type.MASKABLE, false);

public static final ConfigOption<String> INDEX_SELECT_STRATEGY = new ConfigOption<>(QUERY_NS, "index-select-strategy",
String.format("Name of the index selection strategy or full class name. Following shorthands can be used: <br>" +
"- `%s` (Try all combinations of index candidates and pick up optimal one)<br>" +
Expand Down Expand Up @@ -1134,6 +1141,7 @@ public boolean apply(@Nullable String s) {
private boolean adjustQueryLimit;
private int hardMaxLimit;
private Boolean useMultiQuery;
private boolean limitBatchSize;
private boolean optimizerBackendAccess;
private IndexSelectionStrategy indexSelectionStrategy;
private Boolean batchPropertyPrefetching;
Expand Down Expand Up @@ -1223,6 +1231,10 @@ public boolean useMultiQuery() {
return useMultiQuery;
}

public boolean limitBatchSize() {
return limitBatchSize;
}

public boolean optimizerBackendAccess() {
return optimizerBackendAccess;
}
Expand Down Expand Up @@ -1351,6 +1363,7 @@ private void preLoadConfiguration() {

propertyPrefetching = configuration.get(PROPERTY_PREFETCHING);
useMultiQuery = configuration.get(USE_MULTIQUERY);
limitBatchSize = configuration.get(LIMIT_BATCH_SIZE);
indexSelectionStrategy = Backend.getImplementationClass(configuration, configuration.get(INDEX_SELECT_STRATEGY),
REGISTERED_INDEX_SELECTION_STRATEGIES);
optimizerBackendAccess = configuration.get(OPTIMIZER_BACKEND_ACCESS);
Expand Down
Expand Up @@ -87,6 +87,7 @@
import org.janusgraph.graphdb.tinkerpop.optimize.strategy.AdjacentVertexIsOptimizerStrategy;
import org.janusgraph.graphdb.tinkerpop.optimize.strategy.JanusGraphIoRegistrationStrategy;
import org.janusgraph.graphdb.tinkerpop.optimize.strategy.JanusGraphLocalQueryOptimizerStrategy;
import org.janusgraph.graphdb.tinkerpop.optimize.strategy.JanusGraphMultiQueryStrategy;
import org.janusgraph.graphdb.tinkerpop.optimize.strategy.JanusGraphStepStrategy;
import org.janusgraph.graphdb.transaction.StandardJanusGraphTx;
import org.janusgraph.graphdb.transaction.StandardTransactionBuilder;
Expand Down Expand Up @@ -134,6 +135,7 @@ public class StandardJanusGraph extends JanusGraphBlueprintsGraph {
AdjacentVertexIsOptimizerStrategy.instance(),
AdjacentVertexHasUniquePropertyOptimizerStrategy.instance(),
JanusGraphLocalQueryOptimizerStrategy.instance(),
JanusGraphMultiQueryStrategy.instance(),
JanusGraphStepStrategy.instance(),
JanusGraphIoRegistrationStrategy.instance());

Expand Down
Expand Up @@ -14,40 +14,46 @@

package org.janusgraph.graphdb.tinkerpop.optimize;

import org.apache.tinkerpop.gremlin.process.traversal.step.PathProcessor;
import org.apache.tinkerpop.gremlin.process.traversal.step.map.NoOpBarrierStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.util.EmptyStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.util.ProfileStep;
import org.apache.tinkerpop.gremlin.process.traversal.traverser.TraverserRequirement;
import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalHelper;
import org.janusgraph.core.JanusGraphTransaction;
import org.janusgraph.core.JanusGraphVertex;
import org.janusgraph.graphdb.database.StandardJanusGraph;
import org.janusgraph.graphdb.olap.computer.FulgoraElementTraversal;
import org.janusgraph.graphdb.tinkerpop.JanusGraphBlueprintsGraph;
import org.janusgraph.graphdb.tinkerpop.optimize.step.JanusGraphVertexStep;
import org.janusgraph.graphdb.tinkerpop.optimize.step.MultiQueriable;
import org.janusgraph.graphdb.transaction.StandardJanusGraphTx;
import org.apache.tinkerpop.gremlin.process.traversal.Step;
import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
import org.apache.tinkerpop.gremlin.process.traversal.Traverser;
import org.apache.tinkerpop.gremlin.process.traversal.step.TraversalParent;
import org.apache.tinkerpop.gremlin.process.traversal.step.branch.BranchStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.branch.OptionalStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.branch.RepeatStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.branch.RepeatStep.RepeatEndStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.filter.TraversalFilterStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.map.VertexStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.IdentityStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.SideEffectStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.StartStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.util.EmptyStep;
import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalHelper;
import org.apache.tinkerpop.gremlin.structure.Edge;
import org.apache.tinkerpop.gremlin.structure.Element;
import org.apache.tinkerpop.gremlin.structure.Graph;
import org.apache.tinkerpop.gremlin.structure.Vertex;
import org.apache.tinkerpop.gremlin.structure.util.wrapped.WrappedVertex;
import org.janusgraph.core.JanusGraphTransaction;
import org.janusgraph.core.JanusGraphVertex;
import org.janusgraph.graphdb.database.StandardJanusGraph;
import org.janusgraph.graphdb.olap.computer.FulgoraElementTraversal;
import org.janusgraph.graphdb.tinkerpop.JanusGraphBlueprintsGraph;
import org.janusgraph.graphdb.tinkerpop.optimize.step.JanusGraphVertexStep;
import org.janusgraph.graphdb.transaction.StandardJanusGraphTx;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.Queue;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;

/**
* @author Matthias Broecheler (me@matthiasb.com)
Expand All @@ -58,7 +64,7 @@ public class JanusGraphTraversalUtil {
* These parent steps can benefit from a JanusGraphMultiQueryStep capturing the parent's starts and
* using them to initialise a JanusGraphVertexStep if it's the first step of any child traversal.
*/
private static final List<Class<? extends TraversalParent>> MULTIQUERY_COMPATIBLE_STEPS =
private static final List<Class<? extends TraversalParent>> MULTIQUERY_COMPATIBLE_PARENTS =
Arrays.asList(BranchStep.class, OptionalStep.class, RepeatStep.class, TraversalFilterStep.class);

/**
Expand Down Expand Up @@ -137,45 +143,138 @@ public static JanusGraphTransaction getTx(Traversal.Admin<?, ?> traversal) {
}

/**
* This method searches the traversal for traversal parents which are multiQuery compatible.
* Being multiQuery compatible is not solely determined by the class of the parent step, it
* must also have a vertex step as the first step in one of its local or global children.
* @param traversal The traversal in which to search for multiQuery compatible steps
* @return A list of traversal parents which were multiQuery compatible
* Backtraces the traversal for the position where a MultiQueriable step would expect its corresponding
* JanusGraphMultiQueryStep(s). In case of MultiQueriables nested in RepeatSteps, multiple destinations
* are returned.
* @param multiQueriable The MultiQuery compatible step whose MultiQueryStep positions shall be searched.
* @return The step before which the MultiQueryStep is located or expected.
*/
public static List<Step> getAllMultiQueryPositionsForMultiQueriable(Step<?, ?> multiQueriable) {
rngcntr marked this conversation as resolved.
Show resolved Hide resolved
List<Step> multiQueryStepLocations = new ArrayList<>();
Queue<Step> rawLocations = new LinkedList<>();
Step currentStep = multiQueriable;

do {
rawLocations.add(currentStep);
currentStep = currentStep.getTraversal().getParent().asStep();
} while (currentStep instanceof RepeatStep);

while (!rawLocations.isEmpty()) {
currentStep = rawLocations.poll();
Optional<Step> positionInLocalTraversal = getLocalMultiQueryPositionForStep(currentStep);
if (positionInLocalTraversal.isPresent()) {
multiQueryStepLocations.add(positionInLocalTraversal.get());
} else {
rawLocations.add(currentStep.getTraversal().getParent().asStep());
}
}

return multiQueryStepLocations;
}

/**
* For a MultiQuery compatible step, this method searches the correct position in the step's traversal at which
* a <code>JanusGraphMultiQueryStep</code> should be inserted. Only the traversal of the given step is considered,
* parent and child traversals are not taken into account.
* @param step The MultiQuery compatible step.
* @return The step before which a <code>JanusGraphMultiQueryStep</code> should be inserted.
* @see org.janusgraph.graphdb.tinkerpop.optimize.step.JanusGraphMultiQueryStep
*/
public static List<Step> getMultiQueryCompatibleSteps(final Traversal.Admin<?, ?> traversal) {
final Set<Step> multiQueryCompatibleSteps = new HashSet<>();
for (final Step step : traversal.getSteps()) {
if (isMultiQueryCompatibleStep(step)) {
Step parentStep = step;
((TraversalParent)parentStep).getGlobalChildren().forEach(childTraversal -> getMultiQueryCompatibleStepsFromChildTraversal(childTraversal, parentStep, multiQueryCompatibleSteps));
((TraversalParent)parentStep).getLocalChildren().forEach(childTraversal -> getMultiQueryCompatibleStepsFromChildTraversal(childTraversal, parentStep, multiQueryCompatibleSteps));

if (parentStep instanceof RepeatStep && multiQueryCompatibleSteps.contains(parentStep)) {
RepeatStep repeatStep = (RepeatStep)parentStep;
List<RepeatEndStep> repeatEndSteps = TraversalHelper.getStepsOfClass(RepeatEndStep.class, repeatStep.getRepeatTraversal());
if (repeatEndSteps.size() == 1) {
// Want the RepeatEndStep so the start of one iteration can feed into the next
multiQueryCompatibleSteps.remove(parentStep);
multiQueryCompatibleSteps.add(repeatEndSteps.get(0));
}
}
public static Optional<Step> getLocalMultiQueryPositionForStep(Step<?, ?> step) {
Step currentStep = step;
Step previousStep = step.getPreviousStep();
while (previousStep instanceof SideEffectStep || previousStep instanceof ProfileStep) {
currentStep = previousStep;
previousStep = previousStep.getPreviousStep();
}
if (previousStep instanceof EmptyStep || previousStep instanceof StartStep) {
final Step parentStep = step.getTraversal().getParent().asStep();
if (!(parentStep instanceof RepeatStep) && isMultiQueryCompatibleParent(parentStep)) {
return Optional.empty(); // no position found for JanusGraphMultiQueryStep in this local traversal
} else {
return Optional.of(currentStep); // place JanusGraphMultiQueryStep at the stat of the local traversal
}
} else if (previousStep instanceof NoOpBarrierStep) {
return Optional.of(previousStep);
} else {
return Optional.of(currentStep);
}
return new ArrayList<>(multiQueryCompatibleSteps);
}
private static void getMultiQueryCompatibleStepsFromChildTraversal(Traversal.Admin<?,?> childTraversal, Step parentStep, Set<Step> multiQueryCompatibleSteps) {
Step firstStep = childTraversal.getStartStep();
while (firstStep instanceof StartStep || firstStep instanceof SideEffectStep) {
// Want the next step if this is a side effect
firstStep = firstStep.getNextStep();

/**
* Checks whether this step is a traversal parent for which a preceding <code>JanusGraphMultiQueryStep</code>
* can have a positive impact on the step's child traversals.
* @param step The step to be checked.
* @return <code>true</code> if the step's child traversals can possibly benefit of a preceding
* <code>JanusGraphMultiQueryStep</code>, otherwise <code>false</code>.
*/
public static boolean isMultiQueryCompatibleParent(Step<?, ?> step) {
for (Class<? extends TraversalParent> c : MULTIQUERY_COMPATIBLE_PARENTS) {
if (c.isInstance(step)) {
return true;
}
}
if (firstStep.getClass().isAssignableFrom(VertexStep.class)) {
multiQueryCompatibleSteps.add(parentStep);
return false;
}

/**
* Checks whether a step can profit of a preceding <code>JanusGraphMultiQueryStep</code>.
* @param step The step for which the condition is checked.
* @return <code>true</code> if the step is either a <code>MultiQueriable</code> or is a MultiQuery compatible
* parent step.
* @see MultiQueriable
*/
public static boolean isMultiQueryCompatibleStep(Step<?, ?> step) {
return step instanceof MultiQueriable || isMultiQueryCompatibleParent(step);
}

/**
* This method closely matches the behavior implemented in <code>LazyBarrierStrategy</code> which ensures that
* no <code>NoOpBarrierStep</code>s are inserted if path labels are required. Since the same limitation applies
* to <code>JanusGraphMultiQueryStep</code>s, the definition of legal positions for this step is the same.
* @param step The step which follows the JanusGraphMultiQueryStep to be inserted.
* @return <code>true</code> if no path labels are required for this position, otherwise <code>false</code>.
* @see org.apache.tinkerpop.gremlin.process.traversal.strategy.optimization.LazyBarrierStrategy
*/
public static boolean isLegalMultiQueryPosition(Step<?, ?> step) {
if (step.getTraversal().getTraverserRequirements().contains(TraverserRequirement.PATH)) return false;
boolean labeledPath = false;
Step currentStep = step.getTraversal().getStartStep();
while (!currentStep.equals(step, true)) {
if (step instanceof PathProcessor) {
final Set<String> keepLabels = ((PathProcessor) step).getKeepLabels();
li-boxuan marked this conversation as resolved.
Show resolved Hide resolved
labeledPath &= keepLabels == null || !keepLabels.isEmpty();
}
labeledPath |= !step.getLabels().isEmpty();
currentStep = currentStep.getNextStep();
}
return !labeledPath;
}

/**
* Starting at the given step, this method searches the traversal backwards to find the most recent step which
* matches the given class. Only the traversal of the given step is scanned and parent or child traversals are
* not taken into account.
* @param stepClass The class of the requested step.
* @param start The step from which the search is started.
* @param <S> The class of the requested step.
* @return An Optional which contains the requested step if it was found.
*/
public static <S> Optional<S> getPreviousStepOfClass(final Class<S> stepClass, Step<?,?> start) {
Step currentStep = start;
while (currentStep != null && !currentStep.getClass().equals(stepClass) && !(currentStep instanceof EmptyStep)) {
currentStep = currentStep.getPreviousStep();
}
return currentStep != null && currentStep.getClass().equals(stepClass) ? Optional.of((S) currentStep) : Optional.empty();
}

public static boolean isMultiQueryCompatibleStep(Step<?, ?> currentStep) {
return MULTIQUERY_COMPATIBLE_STEPS.stream().anyMatch(stepClass -> stepClass.isInstance(currentStep));
/**
* Returns a list of steps from the traversal, which match a given predicate.
* @param predicate Whether or not a step should be in the returned list.
* @param traversal The traversal whose steps should be used.
* @return The list of matching steps.
*/
public static List<Step> getSteps(Predicate<Step> predicate, Traversal.Admin<?,?> traversal) {
return traversal.getSteps().stream().filter(predicate).collect(Collectors.toList());
}
porunov marked this conversation as resolved.
Show resolved Hide resolved
}
Expand Up @@ -14,18 +14,18 @@

package org.janusgraph.graphdb.tinkerpop.optimize.step;

import org.apache.tinkerpop.gremlin.process.traversal.Step;
import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
import org.apache.tinkerpop.gremlin.process.traversal.Traverser;
import org.apache.tinkerpop.gremlin.process.traversal.Traverser.Admin;
import org.apache.tinkerpop.gremlin.process.traversal.step.util.AbstractStep;
import org.apache.tinkerpop.gremlin.process.traversal.util.FastNoSuchElementException;
import org.apache.tinkerpop.gremlin.structure.Element;
import org.apache.tinkerpop.gremlin.structure.Vertex;
import org.apache.tinkerpop.gremlin.structure.util.StringFactory;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Collections;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;

/**
* This step can be injected before a traversal parent, such as a union, and will cache the
Expand All @@ -34,50 +34,78 @@
* if initialised with all the starts than just one at a time, so this step allows it to
* request the full set of starts from this step when initialising itself.
*/
public final class JanusGraphMultiQueryStep extends AbstractStep<Vertex, Vertex> {
public final class JanusGraphMultiQueryStep extends AbstractStep<Element, Element> {

private final Set<Traverser.Admin<Vertex>> cachedStarts = new HashSet<>();
private final String forStep;
private boolean cachedStartsAccessed = false;
/**
* All steps that use this step to fill their cache. For example, this could be the
* next JanusGraphVertexStep. If the next step is a MultiQuery compatible parent
* (such as union()), then all of its child traversals can use this cache. Thus,
* there can be more than one client step.
*/
private List<MultiQueriable> clientSteps = new ArrayList<>();
private final boolean limitBatchSize;
private boolean initialized;

public JanusGraphMultiQueryStep(Step<Vertex,?> originalStep) {
super(originalStep.getTraversal());
this.forStep = originalStep.getClass().getSimpleName();
public JanusGraphMultiQueryStep(Traversal.Admin traversal, boolean limitBatchSize) {
super(traversal);
this.limitBatchSize = limitBatchSize;
this.initialized = false;
}

@Override
protected Admin<Vertex> processNextStart() throws NoSuchElementException {
Admin<Vertex> start = this.starts.next();
if (!cachedStarts.contains(start))
{
if (cachedStartsAccessed) {
cachedStarts.clear();
cachedStartsAccessed = false;
public void attachClient(MultiQueriable mq) {
clientSteps.add(mq);
}

private void initialize() {
assert !initialized;
initialized = true;

if (!limitBatchSize && !clientSteps.isEmpty()) { // eagerly cache all starts instead of batching
if (!starts.hasNext()) {
throw FastNoSuchElementException.instance();
}
final List<Traverser.Admin<Vertex>> newStarters = new ArrayList<>();
starts.forEachRemaining(s -> {
newStarters.add(s);
cachedStarts.add(s);
final List<Traverser.Admin<Element>> elements = new ArrayList<>();
starts.forEachRemaining(e -> {
elements.add(e);
if (e.get() instanceof Vertex) {
clientSteps.forEach(client -> client.registerFutureVertexForPrefetching((Vertex) e.get()));
rngcntr marked this conversation as resolved.
Show resolved Hide resolved
}
});
starts.add(newStarters.iterator());
cachedStarts.add(start);
starts.add(elements.iterator());
}
return start;
}

public List<Traverser.Admin<Vertex>> getCachedStarts() {
cachedStartsAccessed = true;
return new ArrayList<>(cachedStarts);
@Override
protected Admin<Element> processNextStart() throws NoSuchElementException {
if (!initialized) {
initialize();
}
Admin<Element> start = this.starts.next();
if (start.get() instanceof Vertex) {
clientSteps.forEach(client -> client.registerFutureVertexForPrefetching((Vertex) start.get()));
li-boxuan marked this conversation as resolved.
Show resolved Hide resolved
}
return start;
}

@Override
public String toString() {
return StringFactory.stepString(this, forStep);
public JanusGraphMultiQueryStep clone() {
JanusGraphMultiQueryStep clone = (JanusGraphMultiQueryStep) super.clone();
clone.clientSteps = new ArrayList<>(clientSteps);
clone.initialized = false;
return clone;
}

@Override
public void reset() {
super.reset();
this.cachedStarts.clear();
this.initialized = false;
}

public boolean isLimitBatchSize() {
return limitBatchSize;
}

public List<MultiQueriable> getClientSteps() {
return Collections.unmodifiableList(clientSteps);
}
}
Expand Up @@ -22,14 +22,15 @@
import org.apache.tinkerpop.gremlin.process.traversal.step.Profiling;
import org.apache.tinkerpop.gremlin.process.traversal.step.map.PropertiesStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.util.HasContainer;
import org.apache.tinkerpop.gremlin.process.traversal.util.FastNoSuchElementException;
import org.apache.tinkerpop.gremlin.process.traversal.util.MutableMetrics;
import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalInterruptedException;
import org.apache.tinkerpop.gremlin.structure.Element;
import org.apache.tinkerpop.gremlin.structure.Property;
import org.apache.tinkerpop.gremlin.structure.Vertex;
import org.apache.tinkerpop.gremlin.structure.util.StringFactory;
import org.apache.tinkerpop.gremlin.structure.util.wrapped.WrappedVertex;
import org.janusgraph.core.BaseVertexQuery;
import org.janusgraph.core.JanusGraphException;
import org.janusgraph.core.JanusGraphMultiVertexQuery;
import org.janusgraph.core.JanusGraphProperty;
import org.janusgraph.core.JanusGraphVertex;
Expand All @@ -43,34 +44,48 @@
import org.janusgraph.graphdb.tinkerpop.profile.TP3ProfileWrapper;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
* @author Matthias Broecheler (me@matthiasb.com)
*/
public class JanusGraphPropertiesStep<E> extends PropertiesStep<E> implements HasStepFolder<Element, E>, Profiling, MultiQueriable<Element,E> {

private boolean initialized = false;
private boolean useMultiQuery = false;
private Set<Vertex> verticesToPrefetch = new HashSet<>();
private Map<JanusGraphVertex, Iterable<? extends JanusGraphProperty>> multiQueryResults = null;
private QueryProfiler queryProfiler = QueryProfiler.NO_OP;

public JanusGraphPropertiesStep(PropertiesStep<E> originalStep) {
super(originalStep.getTraversal(), originalStep.getReturnType(), originalStep.getPropertyKeys());
originalStep.getLabels().forEach(this::addLabel);
this.hasContainers = new ArrayList<>();
this.limit = Query.NO_LIMIT;

if (originalStep instanceof JanusGraphPropertiesStep) {
rngcntr marked this conversation as resolved.
Show resolved Hide resolved
JanusGraphPropertiesStep originalJanusGraphPropertiesStep = (JanusGraphPropertiesStep) originalStep;
this.useMultiQuery = originalJanusGraphPropertiesStep.useMultiQuery;
this.hasContainers = originalJanusGraphPropertiesStep.hasContainers;
this.limit = originalJanusGraphPropertiesStep.limit;
} else {
this.hasContainers = new ArrayList<>();
this.limit = Query.NO_LIMIT;
}
}

@Override
public void setUseMultiQuery(boolean useMultiQuery) {
this.useMultiQuery = useMultiQuery;
}

@Override
public void registerFutureVertexForPrefetching(Vertex futureVertex) {
verticesToPrefetch.add(futureVertex);
}

private <Q extends BaseVertexQuery> Q makeQuery(Q query) {
final String[] keys = getPropertyKeys();
query.keys(keys);
Expand All @@ -91,55 +106,30 @@ private Iterator<E> convertIterator(Iterable<? extends JanusGraphProperty> itera
return (Iterator<E>) Iterators.transform(iterable.iterator(), Property::value);
}

private void initialize() {
assert !initialized;
initialized = true;
assert getReturnType().forProperties() || (orders.isEmpty() && hasContainers.isEmpty());

if (!starts.hasNext()) throw FastNoSuchElementException.instance();
final List<Traverser.Admin<Element>> elements = new ArrayList<>();
starts.forEachRemaining(elements::add);
starts.add(elements.iterator());
assert elements.size() > 0;

useMultiQuery = useMultiQuery && elements.stream().allMatch(e -> e.get() instanceof Vertex);

if (useMultiQuery) {
initializeMultiQuery(elements);
}
}

/**
* This initialisation method is called the first time this instance is used and also when
* an attempt to retrieve a vertex from the cached multiQuery results doesn't find an entry.
* @param vertices A list of vertices with which to initialise the multiQuery
* This initialisation method is called when an attempt to retrieve a vertex from the cached multiQuery results
* doesn't find an entry.
*/
private void initializeMultiQuery(final List<Traverser.Admin<Element>> vertices) {
assert vertices.size() > 0;
final JanusGraphMultiVertexQuery multiQuery = JanusGraphTraversalUtil.getTx(traversal).multiQuery();
vertices.forEach(v -> multiQuery.addVertex((Vertex)v.get()));
private void prefetchNextBatch() {
final JanusGraphMultiVertexQuery multiQuery = JanusGraphTraversalUtil.getTx(getTraversal()).multiQuery();
multiQuery.addAllVertices(verticesToPrefetch);
verticesToPrefetch.clear();
makeQuery(multiQuery);

Map<JanusGraphVertex, Iterable<? extends JanusGraphProperty>> results = multiQuery.properties();
if (multiQueryResults == null) {
multiQueryResults = results;
} else {
multiQueryResults.putAll(results);
try {
multiQueryResults = multiQuery.properties();
} catch (JanusGraphException janusGraphException) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed? The signature of JanusGraphMultiVertexQuery::properties does not say it throws exceptions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I explicitly added that try ... catch because there is some test which asserts that a TraversalInterruptedException is thrown on certain occasions.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you share which test fails? In that case why the old code does not suffer this problem?
Besides, I think we should at least do

if (Thread.interrupted()) throw new TraversalInterruptedException()

otherwise it does not seem to be semantically correct

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the test that fails if no TraversalInterruptedException is thrown:
https://github.com/apache/tinkerpop/blob/6bb5c19e5b3845a1547f079151017861b47e0828/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/TraversalInterruptionTest.java#L121

It creates a sideEffect which lets the first traverser pass and lets each of the succeeding traversers wait 3s before letting them pass. The test then lets the traversal run for five seconds before interrupting the thread which iterates the traversal. The expectation is that the traversal.iterate() call resulted in a TraversalInterruptedException.

I think what happened before this PR is that the JanusGraphVertexStep was still trying to aggregate its starts when the thread was interrupted, but it had not yet started to execute backend queries. Now with this PR, the traversers have already passed the JanusGraphMultiQueryStep before entering the sideEffectStep and thus the JanusGraphVertexStep already knows the set of vertices it needs to prefetch. The exception then somehow occurs during the execution of the MultiQuery.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it. Now it looks good to me. I am just confused why CodeCov complains line 123 is not covered.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Somehow Codecov is still reporting that line 123 is not covered by tests. If you set a debug point on line 123 and run the test locally, do you see it get executed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I did exactly that when I was debugging the exception handling and trying to figure out which Exception has to be caught/thrown. I don't see why Codecov complains here, since the breakpoint definitely triggers.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably not a codecov problem. I downloaded the jacoco report from this GitHub Actions run and loaded into Intellij. It shows that JanusGraphPropertiesStep has 0% coverage.
Can you share which JanusGraph test class would lead to this relevant test?

Copy link
Contributor Author

@rngcntr rngcntr Jun 11, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used this one for debugging. It executes a number of TinkerPop tests, including the one you mentioned. Executing this test class leads to the breakpoint being triggered.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, indeed. I also verified that test indeed was executed in CI. No idea why jacoco does not include JanusGraphPropertiesStep in report...

if (janusGraphException.isCausedBy(InterruptedException.class)) {
throw new TraversalInterruptedException();
}
}
initialized = true;
}

@Override
protected Traverser.Admin<E> processNextStart() {
if (!initialized) initialize();
return super.processNextStart();
}

@Override
protected Iterator<E> flatMap(final Traverser.Admin<Element> traverser) {
if (useMultiQuery) { //it is guaranteed that all elements are vertices
if (useMultiQuery && traverser.get() instanceof Vertex) {
rngcntr marked this conversation as resolved.
Show resolved Hide resolved
if (multiQueryResults == null || !multiQueryResults.containsKey(traverser.get())) {
initializeMultiQuery(Collections.singletonList(traverser));
prefetchNextBatch();
}
return convertIterator(multiQueryResults.get(traverser.get()));
} else if (traverser.get() instanceof JanusGraphVertex || traverser.get() instanceof WrappedVertex) {
Expand Down Expand Up @@ -172,19 +162,6 @@ protected Iterator<E> flatMap(final Traverser.Admin<Element> traverser) {
}
}

@Override
public void reset() {
super.reset();
this.initialized = false;
}

@Override
public JanusGraphPropertiesStep<E> clone() {
final JanusGraphPropertiesStep<E> clone = (JanusGraphPropertiesStep<E>) super.clone();
clone.initialized = false;
return clone;
}

/*
===== HOLDER =====
*/
Expand All @@ -211,7 +188,7 @@ public void orderBy(String key, Order order) {

@Override
public void localOrderBy(List<HasContainer> hasContainers, String key, Order order) {
throw new UnsupportedOperationException("LocalOrderBy is not supported for properties step.");
throw new UnsupportedOperationException("LocalOrderBy is not supported for properties step.");
}

@Override
Expand Down
Expand Up @@ -16,25 +16,18 @@

import com.google.common.base.Preconditions;
import org.apache.tinkerpop.gremlin.process.traversal.Order;
import org.apache.tinkerpop.gremlin.process.traversal.Step;
import org.apache.tinkerpop.gremlin.process.traversal.Traverser;
import org.apache.tinkerpop.gremlin.process.traversal.Traverser.Admin;
import org.apache.tinkerpop.gremlin.process.traversal.step.Profiling;
import org.apache.tinkerpop.gremlin.process.traversal.step.branch.RepeatStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.branch.RepeatStep.RepeatEndStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.map.VertexStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.SideEffectStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.StartStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.util.HasContainer;
import org.apache.tinkerpop.gremlin.process.traversal.step.util.ProfileStep;
import org.apache.tinkerpop.gremlin.process.traversal.util.FastNoSuchElementException;
import org.apache.tinkerpop.gremlin.process.traversal.util.MutableMetrics;
import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalHelper;
import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalInterruptedException;
import org.apache.tinkerpop.gremlin.structure.Element;
import org.apache.tinkerpop.gremlin.structure.Vertex;
import org.apache.tinkerpop.gremlin.structure.util.StringFactory;
import org.janusgraph.core.BaseVertexQuery;
import org.janusgraph.core.JanusGraphElement;
import org.janusgraph.core.JanusGraphException;
import org.janusgraph.core.JanusGraphMultiVertexQuery;
import org.janusgraph.core.JanusGraphVertex;
import org.janusgraph.core.JanusGraphVertexQuery;
Expand All @@ -47,7 +40,6 @@
import org.janusgraph.graphdb.tinkerpop.profile.TP3ProfileWrapper;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
Expand All @@ -59,26 +51,38 @@
*/
public class JanusGraphVertexStep<E extends Element> extends VertexStep<E> implements HasStepFolder<Vertex, E>, Profiling, MultiQueriable<Vertex,E> {

private boolean initialized = false;
private boolean useMultiQuery = false;
private boolean batchPropertyPrefetching = false;
private Set<Vertex> verticesToPrefetch = new HashSet<>();
private Map<JanusGraphVertex, Iterable<? extends JanusGraphElement>> multiQueryResults = null;
private QueryProfiler queryProfiler = QueryProfiler.NO_OP;
private int txVertexCacheSize = 20000;
private JanusGraphMultiQueryStep parentMultiQueryStep;

public JanusGraphVertexStep(VertexStep<E> originalStep) {
super(originalStep.getTraversal(), originalStep.getReturnClass(), originalStep.getDirection(), originalStep.getEdgeLabels());
originalStep.getLabels().forEach(this::addLabel);
this.hasContainers = new ArrayList<>();
this.limit = Query.NO_LIMIT;

if (originalStep instanceof JanusGraphVertexStep) {
JanusGraphVertexStep originalJanusGraphVertexStep = (JanusGraphVertexStep) originalStep;
this.useMultiQuery = originalJanusGraphVertexStep.useMultiQuery;
this.hasContainers = originalJanusGraphVertexStep.hasContainers;
this.limit = originalJanusGraphVertexStep.limit;
} else {
this.hasContainers = new ArrayList<>();
this.limit = Query.NO_LIMIT;
}
}

@Override
public void setUseMultiQuery(boolean useMultiQuery) {
this.useMultiQuery = useMultiQuery;
}

@Override
public void registerFutureVertexForPrefetching(Vertex futureVertex) {
verticesToPrefetch.add(futureVertex);
}

public void setBatchPropertyPrefetching(boolean batchPropertyPrefetching) {
this.batchPropertyPrefetching = batchPropertyPrefetching;
}
Expand All @@ -99,94 +103,32 @@ public <Q extends BaseVertexQuery> Q makeQuery(Q query) {
return query;
}

private void initialize() {
assert !initialized;
initialized = true;
if (useMultiQuery) {
setParentMultiQueryStep();

if (!starts.hasNext()) {
throw FastNoSuchElementException.instance();
}
final List<Traverser.Admin<Vertex>> vertices = new ArrayList<>();
starts.forEachRemaining(vertices::add);
starts.add(vertices.iterator());
initializeMultiQuery(vertices);
}
}

/**
* This initialisation method is called the first time this instance is used and also when
* an attempt to retrieve a vertex from the cached multiQuery results doesn't find an entry.
* If initialised with just a single vertex this might be a drip feed from a parent so it
* will additionally include any cached starts the parent step may have.
* @param vertices A list of vertices with which to initialise the multiQuery
* This initialisation method is called when an attempt to retrieve a vertex from the cached multiQuery results
* doesn't find an entry.
*/
private void initializeMultiQuery(final List<Traverser.Admin<Vertex>> vertices) {
assert vertices.size() > 0;
List<Admin<Vertex>> parentStarts = new ArrayList<>();
if (vertices.size() == 1 && parentMultiQueryStep != null) {
parentStarts = parentMultiQueryStep.getCachedStarts();
}
final JanusGraphMultiVertexQuery multiQuery = JanusGraphTraversalUtil.getTx(traversal).multiQuery();
vertices.forEach(v -> multiQuery.addVertex(v.get()));
parentStarts.forEach(v -> multiQuery.addVertex(v.get()));
private void prefetchNextBatch() {
final JanusGraphMultiVertexQuery multiQuery = JanusGraphTraversalUtil.getTx(getTraversal()).multiQuery();
multiQuery.addAllVertices(verticesToPrefetch);
verticesToPrefetch.clear();
porunov marked this conversation as resolved.
Show resolved Hide resolved
makeQuery(multiQuery);

Map<JanusGraphVertex, Iterable<? extends JanusGraphElement>> results = (Vertex.class.isAssignableFrom(getReturnClass())) ? multiQuery.vertices() : multiQuery.edges();
if (multiQueryResults == null) {
multiQueryResults = results;
} else {
multiQueryResults.putAll(results);
}
}

/**
* Many parent traversals drip feed their start vertices in one at a time. To best exploit
* the multiQuery we need to load all possible starts in one go so this method will attempt
* to find a JanusGraphMultiQueryStep with the starts of the parent, and if found cache it.
*/
private void setParentMultiQueryStep() {
Step firstStep = traversal.getStartStep();
while (firstStep instanceof StartStep || firstStep instanceof SideEffectStep) {
// Want the next step if this is a side effect
firstStep = firstStep.getNextStep();
}
if (this.equals(firstStep)) {
Step<?, ?> parentStep = traversal.getParent().asStep();
if (JanusGraphTraversalUtil.isMultiQueryCompatibleStep(parentStep)) {
Step<?, ?> parentPreviousStep = parentStep.getPreviousStep();
if (parentStep instanceof RepeatStep) {
RepeatStep repeatStep = (RepeatStep)parentStep;
List<RepeatEndStep> repeatEndSteps = TraversalHelper.getStepsOfClass(RepeatEndStep.class, repeatStep.getRepeatTraversal());
if (repeatEndSteps.size() == 1) {
parentPreviousStep = repeatEndSteps.get(0).getPreviousStep();
}
}
if (parentPreviousStep instanceof ProfileStep) {
parentPreviousStep = parentPreviousStep.getPreviousStep();
}
if (parentPreviousStep instanceof JanusGraphMultiQueryStep) {
parentMultiQueryStep = (JanusGraphMultiQueryStep)parentPreviousStep;
}
try {
multiQueryResults = (Vertex.class.isAssignableFrom(getReturnClass())) ? multiQuery.vertices() : multiQuery.edges();
porunov marked this conversation as resolved.
Show resolved Hide resolved
} catch (JanusGraphException janusGraphException) {
if (janusGraphException.isCausedBy(InterruptedException.class)) {
throw new TraversalInterruptedException();
}
}
}

@Override
protected Traverser.Admin<E> processNextStart() {
if (!initialized) initialize();
return super.processNextStart();
}

@Override
protected Iterator<E> flatMap(final Traverser.Admin<Vertex> traverser) {

Iterable<? extends JanusGraphElement> result;

if (useMultiQuery) {
if (multiQueryResults == null || !multiQueryResults.containsKey(traverser.get())) {
initializeMultiQuery(Collections.singletonList(traverser));
prefetchNextBatch(); // current batch is exhausted, fetch new batch
}
result = multiQueryResults.get(traverser.get());
} else {
Expand Down Expand Up @@ -214,19 +156,6 @@ protected Iterator<E> flatMap(final Traverser.Admin<Vertex> traverser) {
return (Iterator<E>) result.iterator();
}

@Override
public void reset() {
super.reset();
this.initialized = false;
}

@Override
public JanusGraphVertexStep<E> clone() {
final JanusGraphVertexStep<E> clone = (JanusGraphVertexStep<E>) super.clone();
clone.initialized = false;
return clone;
}

/*
===== HOLDER =====
*/
Expand Down
Expand Up @@ -15,6 +15,7 @@
package org.janusgraph.graphdb.tinkerpop.optimize.step;

import org.apache.tinkerpop.gremlin.process.traversal.Step;
import org.apache.tinkerpop.gremlin.structure.Vertex;

/**
* @author Matthias Broecheler (me@matthiasb.com)
Expand All @@ -23,4 +24,13 @@ public interface MultiQueriable<S,E> extends Step<S,E> {

void setUseMultiQuery(boolean useMultiQuery);

/**
* Registers a vertex which will pass this step at some point in the future.
* The vertex is typically known because a traverser at that vertex location
* has passed a previous step earlier.
* Using that information, a step can know in advance a set of vertices which
* it will have to handle in the future.
* @param futureVertex The vertex which will reach the step in the future.
*/
void registerFutureVertexForPrefetching(Vertex futureVertex);
}
Expand Up @@ -23,7 +23,6 @@
import org.apache.tinkerpop.gremlin.process.traversal.step.map.EdgeVertexStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.map.PropertiesStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.map.VertexStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.util.EmptyStep;
import org.apache.tinkerpop.gremlin.process.traversal.strategy.AbstractTraversalStrategy;
import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalHelper;
import org.apache.tinkerpop.gremlin.structure.Vertex;
Expand All @@ -32,13 +31,11 @@
import org.janusgraph.graphdb.tinkerpop.optimize.JanusGraphTraversalUtil;
import org.janusgraph.graphdb.tinkerpop.optimize.step.HasStepFolder;
import org.janusgraph.graphdb.tinkerpop.optimize.step.JanusGraphEdgeVertexStep;
import org.janusgraph.graphdb.tinkerpop.optimize.step.JanusGraphMultiQueryStep;
import org.janusgraph.graphdb.tinkerpop.optimize.step.JanusGraphPropertiesStep;
import org.janusgraph.graphdb.tinkerpop.optimize.step.JanusGraphVertexStep;
import org.janusgraph.graphdb.tinkerpop.optimize.step.MultiQueriable;

import java.util.Collections;
import java.util.List;
import java.util.Set;

/**
Expand All @@ -62,31 +59,22 @@ public void apply(final Traversal.Admin<?, ?> traversal) {
return;
}

//If this is a compute graph then we can't apply local traversal optimisation at this stage.
final boolean useMultiQuery = !TraversalHelper.onGraphComputer(traversal) && janusGraph.getConfiguration().useMultiQuery();
boolean batchPropertyPrefetching = janusGraph.getConfiguration().batchPropertyPrefetching();
int txVertexCacheSize = janusGraph.getConfiguration().getTxVertexCacheSize();

/*
====== MULTIQUERY COMPATIBLE STEPS ======
*/

if (useMultiQuery) {
JanusGraphTraversalUtil.getMultiQueryCompatibleSteps(traversal).forEach(originalStep -> {
JanusGraphMultiQueryStep multiQueryStep = new JanusGraphMultiQueryStep(originalStep);
TraversalHelper.insertBeforeStep(multiQueryStep, originalStep, originalStep.getTraversal());
});
}

/*
====== VERTEX STEP ======
*/
applyJanusGraphVertexSteps(traversal, batchPropertyPrefetching, txVertexCacheSize);
applyJanusGraphPropertiesSteps(traversal);
inspectLocalTraversals(traversal);
}

TraversalHelper.getStepsOfClass(VertexStep.class, traversal).forEach(originalStep -> {
private void applyJanusGraphVertexSteps(Admin<?, ?> traversal, boolean batchPropertyPrefetching, int txVertexCacheSize) {
TraversalHelper.getStepsOfAssignableClass(VertexStep.class, traversal).forEach(originalStep -> {
final JanusGraphVertexStep vertexStep = new JanusGraphVertexStep(originalStep);
TraversalHelper.replaceStep(originalStep, vertexStep, traversal);
TraversalHelper.replaceStep(originalStep, vertexStep, originalStep.getTraversal());


if (JanusGraphTraversalUtil.isEdgeReturnStep(vertexStep)) {
HasStepFolder.foldInHasContainer(vertexStep, traversal, traversal);
HasStepFolder.foldInHasContainer(vertexStep, originalStep.getTraversal(), originalStep.getTraversal());
//We cannot fold in orders or ranges since they are not local
}

Expand All @@ -97,42 +85,27 @@ public void apply(final Traversal.Admin<?, ?> traversal) {
vertexStep.setLimit(0, QueryUtil.mergeHighLimits(limit, vertexStep.getHighLimit()));
}

if (useMultiQuery) {
vertexStep.setUseMultiQuery(true);
}

if (janusGraph.getConfiguration().batchPropertyPrefetching()) {
applyBatchPropertyPrefetching(traversal, vertexStep, nextStep, janusGraph.getConfiguration().getTxVertexCacheSize());
if (batchPropertyPrefetching) {
applyBatchPropertyPrefetching(originalStep.getTraversal(), vertexStep, nextStep, txVertexCacheSize);
}
});
}


/*
====== PROPERTIES STEP ======
*/


TraversalHelper.getStepsOfClass(PropertiesStep.class, traversal).forEach(originalStep -> {
private void applyJanusGraphPropertiesSteps(Admin<?, ?> traversal) {
TraversalHelper.getStepsOfAssignableClass(PropertiesStep.class, traversal).forEach(originalStep -> {
final JanusGraphPropertiesStep propertiesStep = new JanusGraphPropertiesStep(originalStep);
TraversalHelper.replaceStep(originalStep, propertiesStep, traversal);

TraversalHelper.replaceStep(originalStep, propertiesStep, originalStep.getTraversal());
rngcntr marked this conversation as resolved.
Show resolved Hide resolved

if (propertiesStep.getReturnType().forProperties()) {
HasStepFolder.foldInHasContainer(propertiesStep, traversal, traversal);
HasStepFolder.foldInHasContainer(propertiesStep, originalStep.getTraversal(), originalStep.getTraversal());
//We cannot fold in orders or ranges since they are not local
}

if (useMultiQuery) {
propertiesStep.setUseMultiQuery(true);
}
});
}

/*
====== EITHER INSIDE LOCAL ======
*/

private void inspectLocalTraversals(final Admin<?, ?> traversal) {
TraversalHelper.getStepsOfClass(LocalStep.class, traversal).forEach(localStep -> {
final Traversal.Admin localTraversal = ((LocalStep<?, ?>) localStep).getLocalChildren().get(0);
final Admin localTraversal = ((LocalStep<?, ?>) localStep).getLocalChildren().get(0);
final Step localStart = localTraversal.getStartStep();

if (localStart instanceof VertexStep) {
Expand All @@ -146,7 +119,7 @@ public void apply(final Traversal.Admin<?, ?> traversal) {
HasStepFolder.foldInRange(vertexStep, JanusGraphTraversalUtil.getNextNonIdentityStep(vertexStep), localTraversal, null);


unfoldLocalTraversal(traversal,localStep,localTraversal,vertexStep,useMultiQuery);
unfoldLocalTraversal(traversal, localStep, localTraversal, vertexStep);
}

if (localStart instanceof PropertiesStep) {
Expand All @@ -160,7 +133,7 @@ public void apply(final Traversal.Admin<?, ?> traversal) {
HasStepFolder.foldInRange(propertiesStep, JanusGraphTraversalUtil.getNextNonIdentityStep(propertiesStep), localTraversal, null);


unfoldLocalTraversal(traversal,localStep,localTraversal,propertiesStep,useMultiQuery);
unfoldLocalTraversal(traversal, localStep, localTraversal, propertiesStep);
}

});
Expand All @@ -172,9 +145,9 @@ public void apply(final Traversal.Admin<?, ?> traversal) {
* loads those properties into the vertex cache with a multiQuery preventing the need to
* go back to the storage back-end for each vertex to fetch the properties.
*
* @param traversal The traversal containing the step
* @param vertexStep The step to potentially apply the optimisation to
* @param nextStep The next step in the traversal
* @param traversal The traversal containing the step
* @param vertexStep The step to potentially apply the optimisation to
* @param nextStep The next step in the traversal
* @param txVertexCacheSize The size of the vertex cache
*/
private void applyBatchPropertyPrefetching(final Admin<?, ?> traversal, final JanusGraphVertexStep vertexStep, final Step nextStep, final int txVertexCacheSize) {
Expand All @@ -194,31 +167,15 @@ else if (nextStep instanceof EdgeVertexStep) {
}

private static void unfoldLocalTraversal(final Traversal.Admin<?, ?> traversal,
LocalStep<?,?> localStep, Traversal.Admin localTraversal,
MultiQueriable vertexStep, boolean useMultiQuery) {
LocalStep<?, ?> localStep, Traversal.Admin localTraversal,
MultiQueriable vertexStep) {
assert localTraversal.asAdmin().getSteps().size() > 0;
if (localTraversal.asAdmin().getSteps().size() == 1) {
//Can replace the entire localStep by the vertex step in the outer traversal
assert localTraversal.getStartStep() == vertexStep;
vertexStep.setTraversal(traversal);
TraversalHelper.replaceStep(localStep, vertexStep, traversal);

if (useMultiQuery) {
vertexStep.setUseMultiQuery(true);
}
}
}

private static boolean isChildOf(Step<?, ?> currentStep, List<Class<? extends Step>> stepClasses) {
Step<?, ?> parent = currentStep.getTraversal().getParent().asStep();
while (!parent.equals(EmptyStep.instance())) {
final Step<?, ?> p = parent;
if(stepClasses.stream().anyMatch(stepClass -> stepClass.isInstance(p))) {
return true;
}
parent = parent.getTraversal().getParent().asStep();
}
return false;
}

private static final Set<Class<? extends ProviderOptimizationStrategy>> PRIORS = Collections.singleton(AdjacentVertexFilterOptimizerStrategy.class);
Expand Down
@@ -0,0 +1,122 @@
// Copyright 2021 JanusGraph Authors
//
// 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.

package org.janusgraph.graphdb.tinkerpop.optimize.strategy;

import org.apache.tinkerpop.gremlin.process.traversal.Step;
import org.apache.tinkerpop.gremlin.process.traversal.Traversal.Admin;
import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategy;
import org.apache.tinkerpop.gremlin.process.traversal.step.filter.DropStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.map.NoOpBarrierStep;
import org.apache.tinkerpop.gremlin.process.traversal.strategy.AbstractTraversalStrategy;
import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalHelper;
import org.janusgraph.graphdb.database.StandardJanusGraph;
import org.janusgraph.graphdb.tinkerpop.optimize.JanusGraphTraversalUtil;
import org.janusgraph.graphdb.tinkerpop.optimize.step.JanusGraphMultiQueryStep;
import org.janusgraph.graphdb.tinkerpop.optimize.step.MultiQueriable;

import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;

/**
* @author Marko A. Rodriguez (https://markorodriguez.com)
* @author Matthias Broecheler (http://matthiasb.com)
*/
public class JanusGraphMultiQueryStrategy extends AbstractTraversalStrategy<TraversalStrategy.ProviderOptimizationStrategy> implements TraversalStrategy.ProviderOptimizationStrategy {

private static final Set<Class<? extends ProviderOptimizationStrategy>> PRIORS = new HashSet<>(Arrays.asList(JanusGraphLocalQueryOptimizerStrategy.class, JanusGraphStepStrategy.class));
li-boxuan marked this conversation as resolved.
Show resolved Hide resolved
private static final JanusGraphMultiQueryStrategy INSTANCE = new JanusGraphMultiQueryStrategy();

private JanusGraphMultiQueryStrategy() {
}

@Override
public void apply(final Admin<?, ?> traversal) {
if (!traversal.getGraph().isPresent()
|| TraversalHelper.onGraphComputer(traversal)
// The LazyBarrierStrategy is not allowed to run on traversals which use drop(). As a precaution,
// this strategy should not run on those traversals either, because it can also insert barrier().
|| !TraversalHelper.getStepsOfAssignableClassRecursively(DropStep.class, traversal).isEmpty()) {
return;
}

final StandardJanusGraph janusGraph = JanusGraphTraversalUtil.getJanusGraph(traversal);
if (janusGraph == null || !janusGraph.getConfiguration().useMultiQuery()) {
return;
}

insertMultiQuerySteps(traversal, janusGraph.getConfiguration().limitBatchSize());
configureMultiQueriables(traversal);
}

/**
* Insert JanusGraphMultiQuerySteps everywhere in the current traversal where MultiQueriable steps could benefit
*
* @param traversal The local traversal layer.
*/
private void insertMultiQuerySteps(final Admin<?, ?> traversal, boolean limitBatchSize) {
JanusGraphTraversalUtil.getSteps(JanusGraphTraversalUtil::isMultiQueryCompatibleStep, traversal).forEach(step -> {
Optional<Step> multiQueryPosition = JanusGraphTraversalUtil.getLocalMultiQueryPositionForStep(step);
if (multiQueryPosition.isPresent() && JanusGraphTraversalUtil.isLegalMultiQueryPosition(multiQueryPosition.get())) {
Step pos = multiQueryPosition.get();
if (limitBatchSize && !(multiQueryPosition.get() instanceof NoOpBarrierStep)) {
NoOpBarrierStep barrier = new NoOpBarrierStep(traversal);
TraversalHelper.insertBeforeStep(barrier, pos, traversal);
pos = barrier;
li-boxuan marked this conversation as resolved.
Show resolved Hide resolved
}
JanusGraphMultiQueryStep multiQueryStep = new JanusGraphMultiQueryStep(traversal, limitBatchSize);
TraversalHelper.insertBeforeStep(multiQueryStep, pos, traversal);
}
});
}

/**
* Looks for MultiQueriables in within the traversal and registers them as clients of their respective
* JanusGraphMultiQuerySteps
*
* @param traversal The local traversal layer.
*/
private void configureMultiQueriables(final Admin<?, ?> traversal) {
TraversalHelper.getStepsOfAssignableClass(MultiQueriable.class, traversal).forEach(multiQueriable -> {
final List<Step> mqPositions = JanusGraphTraversalUtil.getAllMultiQueryPositionsForMultiQueriable(multiQueriable);

// If one position is not legal, this means that the entire step can not use the multiQuery feature.
for (Step mqPos : mqPositions) {
if (!JanusGraphTraversalUtil.isLegalMultiQueryPosition(mqPos)) {
return;
}
}

// MultiQuery is applicable
multiQueriable.setUseMultiQuery(true);
for (Step mqPos : mqPositions) {
final Optional<JanusGraphMultiQueryStep> multiQueryStep =
JanusGraphTraversalUtil.getPreviousStepOfClass(JanusGraphMultiQueryStep.class, mqPos);
multiQueryStep.ifPresent(mqs -> mqs.attachClient(multiQueriable));
}
});
}

@Override
public Set<Class<? extends ProviderOptimizationStrategy>> applyPrior() {
return PRIORS;
}

public static JanusGraphMultiQueryStrategy instance() {
return INSTANCE;
}
}
Expand Up @@ -22,6 +22,7 @@
/**
* @author Ted Wilmes (twilmes@gmail.com)
*/

public class InMemoryMultiQueryGraphProvider extends AbstractJanusGraphProvider {
@Override
public ModifiableConfiguration getJanusGraphConfiguration(String graphName, Class<?> test, String testMethodName) {
Expand Down
Expand Up @@ -57,6 +57,9 @@ public void clopen(Object... settings) {
@Override @Test @Disabled
public void testLocalGraphConfiguration() {}

@Override @Test @Disabled
public void testLimitBatchSizeForMultiQuery() {}
rngcntr marked this conversation as resolved.
Show resolved Hide resolved

@Override @Test @Disabled
public void testMaskableGraphConfig() {}

Expand Down
Expand Up @@ -18,12 +18,7 @@
import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__;
import org.apache.tinkerpop.gremlin.process.traversal.step.branch.ChooseStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.branch.LocalStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.branch.OptionalStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.branch.RepeatStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.branch.UnionStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.filter.TraversalFilterStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.map.OrderGlobalStep;
import org.janusgraph.graphdb.query.profile.QueryProfiler;
import org.janusgraph.graphdb.tinkerpop.optimize.step.JanusGraphPropertiesStep;
Expand All @@ -35,11 +30,8 @@
import static org.apache.tinkerpop.gremlin.process.traversal.Order.desc;
import static org.janusgraph.graphdb.JanusGraphBaseTest.option;
import static org.janusgraph.graphdb.configuration.GraphDatabaseConfiguration.BATCH_PROPERTY_PREFETCHING;
import static org.janusgraph.graphdb.configuration.GraphDatabaseConfiguration.USE_MULTIQUERY;
import static org.janusgraph.testutil.JanusGraphAssert.assertCount;
import static org.janusgraph.testutil.JanusGraphAssert.assertNumStep;
import static org.janusgraph.testutil.JanusGraphAssert.queryProfilerAnnotationIsPresent;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

Expand Down Expand Up @@ -123,58 +115,4 @@ public void testBatchPropertyPrefetching() {
assertFalse(queryProfilerAnnotationIsPresent(t, QueryProfiler.MULTIPREFETCH_ANNOTATION));
}

@Test
public void testMultiQuery() {
clopen(option(USE_MULTIQUERY), true);
makeSampleGraph();

Traversal t = g.V(sv[0]).outE().inV().choose(__.inE("knows").has("weight", 0),__.inE("knows").has("weight", 1), __.inE("knows").has("weight", 2)).profile("~metrics");
assertNumStep(numV * 2, 2, (GraphTraversal)t, ChooseStep.class, JanusGraphVertexStep.class);
assertTrue(queryProfilerAnnotationIsPresent(t, QueryProfiler.MULTIQUERY_ANNOTATION));

t = g.V(sv[0]).outE().inV().union(__.inE("knows").has("weight", 0),__.inE("knows").has("weight", 1),__.inE("knows").has("weight", 2)).profile("~metrics");
assertNumStep(numV * 6, 2, (GraphTraversal)t, UnionStep.class, JanusGraphVertexStep.class);
assertTrue(queryProfilerAnnotationIsPresent(t, QueryProfiler.MULTIQUERY_ANNOTATION));

int[] loop = {0}; // repeat starts from vertex with id 0 and goes in to the sv[0] vertex then loops back out to the vertex with the next id
t = g.V(vs[0], vs[1], vs[2])
.repeat(__.inE("knows")
.outV()
.hasId(sv[0].id())
//.outE("knows")
//.inV()
.out("knows") // TINKERPOP-2342
.sideEffect(e -> loop[0] = e.loops())
.has("id", loop[0]))
.times(numV)
.profile("~metrics");
assertNumStep(3, 1, (GraphTraversal)t, RepeatStep.class);
assertEquals(numV - 1, loop[0]);
assertTrue(queryProfilerAnnotationIsPresent(t, QueryProfiler.MULTIQUERY_ANNOTATION));

t = g.V(vs[0],vs[1],vs[2]).optional(__.inE("knows").has("weight", 0)).profile("~metrics");
assertNumStep(12, 1, (GraphTraversal)t, OptionalStep.class);
assertTrue(queryProfilerAnnotationIsPresent(t, QueryProfiler.MULTIQUERY_ANNOTATION));

t = g.V(vs[0],vs[1],vs[2]).filter(__.inE("knows").has("weight", 0)).profile("~metrics");
assertNumStep(1, 1, (GraphTraversal)t, TraversalFilterStep.class);
assertTrue(queryProfilerAnnotationIsPresent(t, QueryProfiler.MULTIQUERY_ANNOTATION));

assertNumStep(superV * (numV / 5), 2, g.V().has("id", sid).outE("knows").has("weight", 1), JanusGraphStep.class, JanusGraphVertexStep.class);
assertNumStep(superV * (numV / 5 * 2), 2, g.V().has("id", sid).outE("knows").has("weight", P.between(1, 3)), JanusGraphStep.class, JanusGraphVertexStep.class);
assertNumStep(superV * 10, 2, g.V().has("id", sid).local(__.outE("knows").has("weight", P.gte(1)).has("weight", P.lt(3)).limit(10)), JanusGraphStep.class, JanusGraphVertexStep.class);
assertNumStep(superV * 10, 1, g.V().has("id", sid).local(__.outE("knows").has("weight", P.between(1, 3)).order().by("weight", desc).limit(10)), JanusGraphStep.class);
assertNumStep(superV * 10, 0, g.V().has("id", sid).local(__.outE("knows").has("weight", P.between(1, 3)).order().by("weight", desc).limit(10)), LocalStep.class);
assertNumStep(superV * numV, 2, g.V().has("id", sid).values("names"), JanusGraphStep.class, JanusGraphPropertiesStep.class);

//Verify traversal metrics when all reads are from cache (i.e. no backend queries)
t = g.V().has("id", sid).local(__.outE("knows").has("weight", P.between(1, 3)).order().by("weight", desc).limit(10)).profile("~metrics");
assertCount(superV * 10, t);
assertTrue(queryProfilerAnnotationIsPresent(t, QueryProfiler.MULTIQUERY_ANNOTATION));

//Verify that properties also use multi query
t = g.V().has("id", sid).values("names").profile("~metrics");
assertCount(superV * numV, t);
assertTrue(queryProfilerAnnotationIsPresent(t, QueryProfiler.MULTIQUERY_ANNOTATION));
}
}
@@ -0,0 +1,87 @@
// Copyright 2021 JanusGraph Authors
//
// 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.

package org.janusgraph.graphdb.tinkerpop.optimize;

import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
import org.apache.tinkerpop.gremlin.process.traversal.traverser.util.TraverserSet;
import org.janusgraph.graphdb.tinkerpop.optimize.step.JanusGraphMultiQueryStep;
import org.janusgraph.graphdb.tinkerpop.optimize.step.MultiQueriable;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.params.provider.Arguments.arguments;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class JanusGraphMultiQueryStepTest {

@ParameterizedTest
@MethodSource("generateTestParameters")
public void testClone(Traversal.Admin traversal, boolean limitBatchSize, Collection<MultiQueriable> clients) {
JanusGraphMultiQueryStep originalStep = new JanusGraphMultiQueryStep(traversal, limitBatchSize);
clients.forEach(originalStep::attachClient);

JanusGraphMultiQueryStep clone = originalStep.clone();

assertEquals(limitBatchSize, clone.isLimitBatchSize());
assertEquals(originalStep.getClientSteps().size(), clone.getClientSteps().size());
assertTrue(clone.getClientSteps().containsAll(originalStep.getClientSteps()));
assertTrue(originalStep.getClientSteps().containsAll(clone.getClientSteps()));
}

@ParameterizedTest
@MethodSource("generateTestParameters")
public void testReset(Traversal.Admin traversal, boolean limitBatchSize, Collection<MultiQueriable> clients) {
JanusGraphMultiQueryStep originalStep = new JanusGraphMultiQueryStep(traversal, limitBatchSize);
clients.forEach(originalStep::attachClient);

originalStep.reset();

assertEquals(limitBatchSize, originalStep.isLimitBatchSize());
assertEquals(originalStep.getClientSteps().size(), clients.size());
assertTrue(clients.containsAll(originalStep.getClientSteps()));
assertTrue(originalStep.getClientSteps().containsAll(clients));
}

private static Stream<Arguments> generateTestParameters() {
li-boxuan marked this conversation as resolved.
Show resolved Hide resolved
Traversal.Admin mockedTraversal = mock(Traversal.Admin.class);
when(mockedTraversal.getTraverserSetSupplier()).thenReturn(TraverserSet::new);

MultiQueriable mqA = mock(MultiQueriable.class);
MultiQueriable mqB = mock(MultiQueriable.class);

List<MultiQueriable> emptyClientList = Collections.emptyList();
List<MultiQueriable> singleClientList = Collections.singletonList(mqA);
List<MultiQueriable> multiClientList = Arrays.asList(mqA, mqB);

return Arrays.stream(new Arguments[]{
arguments(mockedTraversal, true, emptyClientList),
arguments(mockedTraversal, false, emptyClientList),
arguments(mockedTraversal, true, singleClientList),
arguments(mockedTraversal, false, singleClientList),
arguments(mockedTraversal, true, multiClientList),
arguments(mockedTraversal, false, multiClientList)
});
}
}
@@ -0,0 +1,153 @@
// Copyright 2020 JanusGraph Authors
//
// 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.

package org.janusgraph.graphdb.tinkerpop.optimize;

import org.apache.tinkerpop.gremlin.process.traversal.P;
import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__;
import org.apache.tinkerpop.gremlin.process.traversal.step.branch.ChooseStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.branch.LocalStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.branch.OptionalStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.branch.RepeatStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.branch.UnionStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.filter.TraversalFilterStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.map.NoOpBarrierStep;
import org.apache.tinkerpop.gremlin.structure.Edge;
import org.janusgraph.graphdb.query.profile.QueryProfiler;
import org.janusgraph.graphdb.tinkerpop.optimize.step.JanusGraphMultiQueryStep;
import org.janusgraph.graphdb.tinkerpop.optimize.step.JanusGraphPropertiesStep;
import org.janusgraph.graphdb.tinkerpop.optimize.step.JanusGraphStep;
import org.janusgraph.graphdb.tinkerpop.optimize.step.JanusGraphVertexStep;
import org.janusgraph.graphdb.tinkerpop.optimize.strategy.JanusGraphLocalQueryOptimizerStrategy;
import org.junit.jupiter.api.Test;

import java.util.List;

import static org.apache.tinkerpop.gremlin.process.traversal.Order.desc;
import static org.janusgraph.graphdb.JanusGraphBaseTest.option;
import static org.janusgraph.graphdb.configuration.GraphDatabaseConfiguration.LIMIT_BATCH_SIZE;
import static org.janusgraph.graphdb.configuration.GraphDatabaseConfiguration.USE_MULTIQUERY;
import static org.janusgraph.testutil.JanusGraphAssert.assertNumStep;
import static org.janusgraph.testutil.JanusGraphAssert.assertCount;
import static org.janusgraph.testutil.JanusGraphAssert.queryProfilerAnnotationIsPresent;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class JanusGraphMultiQueryStrategyTest extends OptimizerStrategyTest {

@Test
public void testQueryIsExecutableIfJanusGraphLocalQueryOptimizerStrategyIsEnabled() {
clopen(option(USE_MULTIQUERY), true);
makeSampleGraph();

final List<Edge> normalResults = g.V(sv[0]).outE().inV().choose(__.inE("knows").has("weight", 0), __.inE("knows").has("weight", 1), __.inE("knows").has("weight", 2)).toList();
final List<Edge> resultsWithDisabledStrategy = g.withoutStrategies(JanusGraphLocalQueryOptimizerStrategy.class).V(sv[0]).outE().inV().choose(__.inE("knows").has("weight", 0), __.inE("knows").has("weight", 1), __.inE("knows").has("weight", 2)).toList();

assertEquals(normalResults, resultsWithDisabledStrategy);
}

@Test
public void testNoMultiQueryStepsInsertedIfPathQuery() {
clopen(option(USE_MULTIQUERY), true);
makeSampleGraph();

final GraphTraversal<?,?> traversalWithoutPath = g.V(sv[0]).outE().inV();
assertNumStep(numV, 1, traversalWithoutPath, JanusGraphMultiQueryStep.class);

final GraphTraversal<?,?> traversalWithPath = g.V(sv[0]).outE().inV().path();
assertNumStep(numV, 0, traversalWithPath, JanusGraphMultiQueryStep.class);

final GraphTraversal<?,?> traversalWithNestedPath = g.V(sv[0]).outE().inV().where(__.path());
assertNumStep(numV, 0, traversalWithNestedPath, JanusGraphMultiQueryStep.class);
}

@Test
public void testNoOpBarrierStepInsertedIfNotPresentAndLimitBatchSize() {
clopen(option(USE_MULTIQUERY), true, option(LIMIT_BATCH_SIZE), true);
makeSampleGraph();

final GraphTraversal<?,?> traversalWithoutExplicitBarrier = g.V(sv[0]).outE().inV();
assertNumStep(numV, 1, traversalWithoutExplicitBarrier, NoOpBarrierStep.class);

final GraphTraversal<?,?> traversalWithExplicitBarrier = g.V(sv[0]).barrier(1).outE().inV();
assertNumStep(numV, 1, traversalWithExplicitBarrier, NoOpBarrierStep.class);
}

@Test
public void testNoOpBarrierStepNotInsertedLimitBatchSizeDisabled() {
clopen(option(USE_MULTIQUERY), true, option(LIMIT_BATCH_SIZE), false);
makeSampleGraph();

final GraphTraversal<?,?> traversalWithoutExplicitBarrier = g.V(sv[0]).outE().inV();
assertNumStep(numV, 0, traversalWithoutExplicitBarrier, NoOpBarrierStep.class);

final GraphTraversal<?,?> traversalWithExplicitBarrier = g.V(sv[0]).barrier(1).outE().inV();
assertNumStep(numV, 1, traversalWithExplicitBarrier, NoOpBarrierStep.class);
}

@Test
public void testMultiQuery() {
clopen(option(USE_MULTIQUERY), true);
makeSampleGraph();

Traversal t = g.V(sv[0]).outE().inV().choose(__.inE("knows").has("weight", 0),__.inE("knows").has("weight", 1), __.inE("knows").has("weight", 2)).profile("~metrics");
assertNumStep(numV * 2, 2, (GraphTraversal)t, ChooseStep.class, JanusGraphVertexStep.class);
assertTrue(queryProfilerAnnotationIsPresent(t, QueryProfiler.MULTIQUERY_ANNOTATION));

t = g.V(sv[0]).outE().inV().union(__.inE("knows").has("weight", 0),__.inE("knows").has("weight", 1),__.inE("knows").has("weight", 2)).profile("~metrics");
assertNumStep(numV * 6, 2, (GraphTraversal)t, UnionStep.class, JanusGraphVertexStep.class);
assertTrue(queryProfilerAnnotationIsPresent(t, QueryProfiler.MULTIQUERY_ANNOTATION));

int[] loop = {0}; // repeat starts from vertex with id 0 and goes in to the sv[0] vertex then loops back out to the vertex with the next id
t = g.V(vs[0], vs[1], vs[2])
.repeat(__.inE("knows")
.outV()
.hasId(sv[0].id())
.out("knows") // TINKERPOP-2342
.sideEffect(e -> loop[0] = e.loops())
.has("id", loop[0]))
.times(numV)
.profile("~metrics");
assertNumStep(3, 1, (GraphTraversal)t, RepeatStep.class);
assertEquals(numV - 1, loop[0]);
assertTrue(queryProfilerAnnotationIsPresent(t, QueryProfiler.MULTIQUERY_ANNOTATION));

t = g.V(vs[0],vs[1],vs[2]).optional(__.inE("knows").has("weight", 0)).profile("~metrics");
assertNumStep(12, 1, (GraphTraversal)t, OptionalStep.class);
assertTrue(queryProfilerAnnotationIsPresent(t, QueryProfiler.MULTIQUERY_ANNOTATION));

t = g.V(vs[0],vs[1],vs[2]).filter(__.inE("knows").has("weight", 0)).profile("~metrics");
assertNumStep(1, 1, (GraphTraversal)t, TraversalFilterStep.class);
assertTrue(queryProfilerAnnotationIsPresent(t, QueryProfiler.MULTIQUERY_ANNOTATION));

assertNumStep(superV * (numV / 5), 2, g.V().has("id", sid).outE("knows").has("weight", 1), JanusGraphStep.class, JanusGraphVertexStep.class);
assertNumStep(superV * (numV / 5 * 2), 2, g.V().has("id", sid).outE("knows").has("weight", P.between(1, 3)), JanusGraphStep.class, JanusGraphVertexStep.class);
assertNumStep(superV * 10, 2, g.V().has("id", sid).local(__.outE("knows").has("weight", P.gte(1)).has("weight", P.lt(3)).limit(10)), JanusGraphStep.class, JanusGraphVertexStep.class);
assertNumStep(superV * 10, 1, g.V().has("id", sid).local(__.outE("knows").has("weight", P.between(1, 3)).order().by("weight", desc).limit(10)), JanusGraphStep.class);
assertNumStep(superV * 10, 0, g.V().has("id", sid).local(__.outE("knows").has("weight", P.between(1, 3)).order().by("weight", desc).limit(10)), LocalStep.class);
assertNumStep(superV * numV, 2, g.V().has("id", sid).values("names"), JanusGraphStep.class, JanusGraphPropertiesStep.class);

//Verify traversal metrics when all reads are from cache (i.e. no backend queries)
t = g.V().has("id", sid).local(__.outE("knows").has("weight", P.between(1, 3)).order().by("weight", desc).limit(10)).profile("~metrics");
assertCount(superV * 10, t);
assertTrue(queryProfilerAnnotationIsPresent(t, QueryProfiler.MULTIQUERY_ANNOTATION));

//Verify that properties also use multi query
t = g.V().has("id", sid).values("names").profile("~metrics");
assertCount(superV * numV, t);
assertTrue(queryProfilerAnnotationIsPresent(t, QueryProfiler.MULTIQUERY_ANNOTATION));
}
}
Expand Up @@ -16,19 +16,18 @@

import org.apache.tinkerpop.gremlin.process.traversal.Order;
import org.apache.tinkerpop.gremlin.process.traversal.P;
import org.apache.tinkerpop.gremlin.process.traversal.Step;
import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategies;
import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategy;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.DefaultGraphTraversal;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__;
import org.apache.tinkerpop.gremlin.process.traversal.step.TraversalParent;
import org.apache.tinkerpop.gremlin.process.traversal.step.filter.IsStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.filter.OrStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.filter.RangeGlobalStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.map.GraphStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.map.PropertiesStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.map.VertexStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.util.ElementValueComparator;
import org.apache.tinkerpop.gremlin.process.traversal.step.util.HasContainer;
Expand All @@ -50,18 +49,17 @@
import org.janusgraph.graphdb.query.JanusGraphPredicateUtils;
import org.janusgraph.graphdb.tinkerpop.optimize.step.HasStepFolder;
import org.janusgraph.graphdb.tinkerpop.optimize.step.JanusGraphMultiQueryStep;
import org.janusgraph.graphdb.tinkerpop.optimize.step.JanusGraphPropertiesStep;
import org.janusgraph.graphdb.tinkerpop.optimize.step.JanusGraphStep;
import org.janusgraph.graphdb.tinkerpop.optimize.step.JanusGraphVertexStep;
import org.janusgraph.graphdb.tinkerpop.optimize.strategy.JanusGraphLocalQueryOptimizerStrategy;
import org.janusgraph.graphdb.tinkerpop.optimize.strategy.JanusGraphMultiQueryStrategy;
import org.janusgraph.graphdb.tinkerpop.optimize.strategy.JanusGraphStepStrategy;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.*;
import java.util.stream.Stream;

import static org.apache.tinkerpop.gremlin.process.traversal.P.eq;
Expand Down Expand Up @@ -113,40 +111,21 @@ public void doMultiQueryTest(Traversal original, Traversal expected, Collection<
}

private void applyMultiQueryTraversalSteps(Traversal.Admin<?,?> traversal) {
processVertexSteps(traversal);
processChildTraversals(traversal);
processIsMultiQuerySteps(traversal);
}

private void processVertexSteps(Traversal.Admin<?,?> traversal) {
TraversalHelper.getStepsOfClass(VertexStep.class, traversal).forEach(vertexStep -> {
TraversalHelper.getStepsOfAssignableClassRecursively(VertexStep.class, traversal).forEach(vertexStep -> {
JanusGraphVertexStep janusGraphVertexStep = new JanusGraphVertexStep<>(vertexStep);
TraversalHelper.replaceStep(vertexStep, janusGraphVertexStep, traversal);
TraversalHelper.replaceStep(vertexStep, janusGraphVertexStep, vertexStep.getTraversal());
if (JanusGraphTraversalUtil.isEdgeReturnStep(janusGraphVertexStep)) {
HasStepFolder.foldInHasContainer(janusGraphVertexStep, traversal, traversal);
HasStepFolder.foldInHasContainer(janusGraphVertexStep, vertexStep.getTraversal(), vertexStep.getTraversal());
}
});
}

private void processChildTraversals(Traversal.Admin<?,?> traversal) {
traversal.getSteps().forEach(step -> {
if (JanusGraphTraversalUtil.isMultiQueryCompatibleStep((Step<?, ?>) step)) {
((TraversalParent)step).getGlobalChildren().forEach(child -> applyMultiQueryTraversalSteps(child));
((TraversalParent)step).getLocalChildren().forEach(child -> applyMultiQueryTraversalSteps(child));
}
TraversalHelper.getStepsOfAssignableClassRecursively(PropertiesStep.class, traversal).forEach(vertexStep -> {
JanusGraphPropertiesStep janusGraphPropertiesStep = new JanusGraphPropertiesStep<>(vertexStep);
TraversalHelper.replaceStep(vertexStep, janusGraphPropertiesStep, vertexStep.getTraversal());
});
}

/**
* Replace any place holders like 'is(MQ_OPTIONAL)' with the expected JanusGraphMultiQueryStep
* @param traversal The traversal to operate on
*/
private void processIsMultiQuerySteps(Traversal.Admin<?,?> traversal) {
TraversalHelper.getStepsOfClass(IsStep.class, traversal).forEach(isStep -> {
TraversalHelper.getStepsOfAssignableClassRecursively(IsStep.class, traversal).forEach(isStep -> {
Object expectedStep = isStep.getPredicate().getValue();
Step<Vertex, ?> nextStep = isStep.getNextStep();
if (expectedStep.equals(nextStep.getClass().getSimpleName())) {
TraversalHelper.replaceStep(isStep, new JanusGraphMultiQueryStep(nextStep), traversal);
if (expectedStep.equals(JanusGraphMultiQueryStep.class.getSimpleName())) {
TraversalHelper.replaceStep(isStep, new JanusGraphMultiQueryStep(isStep.getTraversal(), false), isStep.getTraversal());
}
});
}
Expand Down Expand Up @@ -287,39 +266,45 @@ private static Stream<Arguments> generateMultiQueryTestParameters() {
mgmt.makeEdgeLabel("knows").make();
mgmt.commit();

// String constants for expected types of JanusGraphMultiQueryStep
final String MQ_CHOOSE = "ChooseStep";
final String MQ_UNION = "UnionStep";
final String MQ_OPTIONAL = "OptionalStep";
final String MQ_FILTER = "TraversalFilterStep";
final String MQ_REPEAT = "RepeatEndStep";
// String constant for expected JanusGraphMultiQueryStep
final String MQ_STEP = JanusGraphMultiQueryStep.class.getSimpleName();

List<JanusGraphLocalQueryOptimizerStrategy> otherStrategies = Collections.singletonList(JanusGraphLocalQueryOptimizerStrategy.instance());
List<TraversalStrategy.ProviderOptimizationStrategy> otherStrategies = new ArrayList<>(2);
otherStrategies.add(JanusGraphLocalQueryOptimizerStrategy.instance());
otherStrategies.add(JanusGraphMultiQueryStrategy.instance());

return Arrays.stream(new Arguments[]{
arguments(g.V().in("knows").out("knows"),
g_V().is(MQ_STEP).in("knows").is(MQ_STEP).out("knows"), otherStrategies),
arguments(g.V().in("knows").values("weight"),
g_V().is(MQ_STEP).in("knows").is(MQ_STEP).values("weight"), otherStrategies),
// Need two JanusGraphMultiQuerySteps, one for each sub query because caches are flushed when queried
arguments(g.V().choose(__.inE("knows").has("weight", 0),__.inE("knows").has("weight", 1)),
g_V().is(MQ_CHOOSE).choose(__.inE("knows").has("weight", 0),__.inE("knows").has("weight", 1)), otherStrategies),
g_V().is(MQ_STEP).choose(__.inE("knows").has("weight", 0),__.inE("knows").has("weight", 1)), otherStrategies),
arguments(g.V().union(__.inE("knows").has("weight", 0),__.inE("knows").has("weight", 1)),
g_V().is(MQ_UNION).union(__.inE("knows").has("weight", 0),__.inE("knows").has("weight", 1)), otherStrategies),
g_V().is(MQ_STEP).union(__.inE("knows").has("weight", 0),__.inE("knows").has("weight", 1)), otherStrategies),
arguments(g.V().outE().optional(__.inE("knows").has("weight", 0)),
g_V().outE().is(MQ_OPTIONAL).optional(__.inE("knows").has("weight", 0)), otherStrategies),
g_V().is(MQ_STEP).outE().is(MQ_STEP).optional(__.inE("knows").has("weight", 0)), otherStrategies),
arguments(g.V().outE().filter(__.inE("knows").has("weight", 0)),
g_V().outE().is(MQ_FILTER).filter(__.inE("knows").has("weight", 0)), otherStrategies),
// The JanusGraphMultiQueryStep for repeat goes before the RepeatEndStep allowing it to feed its starts to the next iteration
g_V().is(MQ_STEP).outE().is(MQ_STEP).filter(__.inE("knows").has("weight", 0)), otherStrategies),
// An additional JanusGraphMultiQueryStep for repeat goes before the RepeatEndStep allowing it to feed its starts to the next iteration
arguments(g.V().outE("knows").inV().repeat(__.outE("knows").inV().has("weight", 0)).times(10),
g_V().outE("knows").inV().repeat(__.outE("knows").inV().has("weight", 0).is(MQ_REPEAT)).times(10), otherStrategies),
g_V().is(MQ_STEP).outE("knows").inV().is(MQ_STEP).repeat(__.is(MQ_STEP).outE("knows").inV().has("weight", 0)).times(10), otherStrategies),
// Choose does not have a child traversal of JanusGraphVertexStep so won't benefit from JanusGraphMultiQueryStep(ChooseStep)
arguments(g.V().choose(has("weight", lt(3)), __.union(__.inE("knows").has("weight", 0),__.inE("knows").has("weight", 1))),
g_V().choose(has("weight", lt(3)), __.is(MQ_UNION).union(__.inE("knows").has("weight", 0),__.inE("knows").has("weight", 1))), otherStrategies),
g_V().is(MQ_STEP).choose(has("weight", lt(3)), __.union(__.inE("knows").has("weight", 0),__.inE("knows").has("weight", 1))), otherStrategies),
// Choose now has a child traversal of JanusGraphVertexStep and so will benefit from JanusGraphMultiQueryStep(ChooseStep)
arguments(g.V().choose(__.union(__.inE("knows").has("weight", 0),__.inE("knows").has("weight", 1)),__.inE("knows").has("weight", gt(2))),
g_V().is(MQ_CHOOSE).choose(__.is(MQ_UNION).union(__.inE("knows").has("weight", 0),__.inE("knows").has("weight", 1)),__.inE("knows").has("weight", gt(2))), otherStrategies),
g_V().is(MQ_STEP).choose(__.union(__.inE("knows").has("weight", 0),__.inE("knows").has("weight", 1)),__.inE("knows").has("weight", gt(2))), otherStrategies),
// There are 'as' side effect steps preceding the JanusGraphVertexStep
arguments(g.V().choose(has("weight", 0),__.as("true").inE("knows"),__.as("false").inE("knows")),
g_V().is(MQ_CHOOSE).choose(has("weight", 0),__.as("true").inE("knows"),__.as("false").inE("knows")), otherStrategies),
g_V().is(MQ_STEP).choose(has("weight", 0),__.as("true").inE("knows"),__.as("false").inE("knows")), otherStrategies),
// There are 'sideEffect' and 'as' steps preceding the JanusGraphVertexStep
arguments(g.V().choose(has("weight", 0),__.as("true").sideEffect(i -> {}).inE("knows"),__.as("false").sideEffect(i -> {}).inE("knows")),
g_V().is(MQ_CHOOSE).choose(has("weight", 0),__.as("true").sideEffect(i -> {}).inE("knows"),__.as("false").sideEffect(i -> {}).inE("knows")), otherStrategies),
g_V().is(MQ_STEP).choose(has("weight", 0),__.as("true").sideEffect(i -> {}).inE("knows"),__.as("false").sideEffect(i -> {}).inE("knows")), otherStrategies),
// 'local' is not MultiQueryCompatible (at the moment)
arguments(g.V().and(__.inE("knows"), __.inE("knows")),
g_V().and(__.is(MQ_STEP).inE("knows"), __.is(MQ_STEP).inE("knows")), otherStrategies),
});
}
}
1 change: 1 addition & 0 deletions mkdocs.yml
Expand Up @@ -127,6 +127,7 @@ nav:
- Deployment Scenarios: operations/deployment.md
- ConfiguredGraphFactory: operations/configured-graph-factory.md
- Dynamic Graphs: operations/dynamic-graphs.md
- Batch Processing: operations/batch-processing.md
- Bulk Loading: operations/bulk-loading.md
- JanusGraph Cache: operations/cache.md
- Monitoring: operations/monitoring.md
Expand Down