This is an automated email from the ASF dual-hosted git repository. spmallette pushed a commit to branch TINKERPOP-2635 in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
commit 04884ecb901d0e3868a0afc482694e6fbf7d8458 Author: Stephen Mallette <[email protected]> AuthorDate: Tue Nov 30 13:57:30 2021 -0500 TINKERPOP-2635 Add fail() step --- CHANGELOG.asciidoc | 2 + docs/src/dev/developer/for-committers.asciidoc | 1 + docs/src/reference/the-traversal.asciidoc | 37 ++++++ docs/src/upgrade/release-3.6.x.asciidoc | 23 +++- .../tinkerpop/gremlin/console/Console.groovy | 124 +++++++++++++++------ .../language/grammar/GremlinBaseVisitor.java | 13 +++ .../language/grammar/TraversalMethodVisitor.java | 17 +++ .../gremlin/process/traversal/Failure.java | 101 +++++++++++++++++ .../gremlin/process/traversal/Traverser.java | 9 ++ .../traversal/dsl/graph/GraphTraversal.java | 54 +++++++-- .../gremlin/process/traversal/dsl/graph/__.java | 14 +++ .../traversal/step/sideEffect/FailStep.java | 87 +++++++++++++++ .../traverser/B_LP_NL_O_S_SE_SL_Traverser.java | 5 + .../traverser/B_LP_O_S_SE_SL_Traverser.java | 1 - .../traverser/B_NL_O_S_SE_SL_Traverser.java | 1 - .../traversal/traverser/B_O_S_SE_SL_Traverser.java | 7 ++ .../traverser/LP_NL_O_OB_P_S_SE_SL_Traverser.java | 6 + .../traverser/LP_NL_O_OB_S_SE_SL_Traverser.java | 6 + .../traverser/LP_O_OB_S_SE_SL_Traverser.java | 1 - .../traverser/NL_O_OB_S_SE_SL_Traverser.java | 6 + .../traverser/O_OB_S_SE_SL_Traverser.java | 9 +- .../Process/Traversal/GraphTraversal.cs | 27 +++++ .../src/Gremlin.Net/Process/Traversal/__.cs | 16 +++ .../Gherkin/CommonSteps.cs | 53 ++++++--- .../Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs | 3 + .../apache/tinkerpop/gremlin/driver/Tokens.java | 15 +++ .../gremlin/driver/message/ResponseStatusCode.java | 11 ++ .../ast/VarAsBindingASTTransformation.groovy | 3 + .../lib/process/graph-traversal.js | 11 ++ .../test/cucumber/feature-steps.js | 8 +- .../gremlin-javascript/test/cucumber/gremlin.js | 3 + gremlin-language/src/main/antlr4/Gremlin.g4 | 6 + .../gremlin_python/process/graph_traversal.py | 14 +++ .../src/main/python/radish/feature_steps.py | 18 ++- gremlin-python/src/main/python/radish/gremlin.py | 3 + .../gremlin/server/handler/AbstractSession.java | 31 ++++-- .../gremlin/server/op/AbstractEvalOpProcessor.java | 25 +++-- .../gremlin/server/op/AbstractOpProcessor.java | 9 +- .../server/op/session/SessionOpProcessor.java | 67 ++++++++--- .../server/op/traversal/TraversalOpProcessor.java | 47 +++++--- .../gremlin/server/GremlinServerIntegrateTest.java | 16 +++ gremlin-test/features/sideEffect/Fail.feature | 46 ++++++++ .../tinkerpop/gremlin/features/StepDefinition.java | 20 +++- 43 files changed, 847 insertions(+), 129 deletions(-) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index b2e256b..782f4e3 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -25,6 +25,8 @@ limitations under the License. * Changed TinkerGraph to allow identifiers to be heterogeneous when filtering. * Prevented values of `T` to `property()` from being `null`. +* Added `fail()` step. +* Improved Gherkin test framework to allow for asserting traversal exceptions as a behavior. * Fixed query indentation for profile metrics where indent levels were not being respected. * `TraversalOpProcessor` no longer accepts a `String` representation of `Bytecode` for the "gremlin" argument which was left to support older versions of the drivers. * Removed requirement that "ids" used to filter vertices and edges need to be all of a single type. diff --git a/docs/src/dev/developer/for-committers.asciidoc b/docs/src/dev/developer/for-committers.asciidoc index c3c3453..e9e5f9b 100644 --- a/docs/src/dev/developer/for-committers.asciidoc +++ b/docs/src/dev/developer/for-committers.asciidoc @@ -436,6 +436,7 @@ The "Then" options handle the assertion of the result. There are several options * "the result should have a count of _xxx_" - assumes a list value in the result and counts the number of values in it * "the result should be empty" - no results +* "the traversal will raise an error" - an exception is thrown as a result of traversal iteration * "the result should be ordered" - the exact results and should appear in the order presented * "the result should be unordered" - the exact results but can appear any order * "the result should be of" - results can be any of the specified values and in any order (use when guarantees diff --git a/docs/src/reference/the-traversal.asciidoc b/docs/src/reference/the-traversal.asciidoc index a694818..2eee51d 100644 --- a/docs/src/reference/the-traversal.asciidoc +++ b/docs/src/reference/the-traversal.asciidoc @@ -1228,6 +1228,43 @@ g.V().hasLabel('person').outE().identity().inV().count().is(gt(5)).explain() For traversal profiling information, please see <<profile-step,`profile()`>>-step. +[[fail-step]] +=== Fail Step + +The `fail()`-step provides a way to force a traversal to immediately fail with an exception. This feature is often +helpful during debugging purposes and for validating certain conditions prior to continuing with traversal execution. + +[source,text] +---- +gremlin> g.V().has('person','name','peter').fold(). +......1> coalesce(unfold(), +......2> fail('peter should exist')). +......3> property('k',100) +==>v[6] +gremlin> g.V().has('person','name','stephen').fold(). +......1> coalesce(unfold(), +......2> fail('stephen should exist')). +......3> property('k',100) +fail() Step Triggered +=========================================================================================================================== +Message > stephen should exist +Traverser> [] + Bulk > 1 +Traversal> fail() +Parent > CoalesceStep [V().has("person","name","stephen").fold().coalesce(__.unfold(),__.fail()).property("k",(int) 100)] +Metadata > {} +=========================================================================================================================== +---- + +The code example above exemplifies the latter use case where there is essentially an assertion that there is a vertex +with a particular "name" value prior to updating the property "k" and explicitly failing when that vertex is not found. + +*Additional References* + +link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#fail--++[`fail()`], +link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#fail-java.lang.String-++[`fail(String)`] + + [[fold-step]] === Fold Step diff --git a/docs/src/upgrade/release-3.6.x.asciidoc b/docs/src/upgrade/release-3.6.x.asciidoc index 8fb8290..aa508c8 100644 --- a/docs/src/upgrade/release-3.6.x.asciidoc +++ b/docs/src/upgrade/release-3.6.x.asciidoc @@ -47,7 +47,28 @@ It is worth noting that `gremlin-groovy` utilized the DSL annotations to constru link:https://tinkerpop.apache.org/docs/3.6.0/reference/#credentials-dsl[Credentials DSL] so the `gremlin-annotations` package is now explicitly associated to `gremlin-groovy` but as an `<optional>` dependency. -See:link:https://issues.apache.org/jira/browse/TINKERPOP-2411[TINKERPOP-2411] +See: link:https://issues.apache.org/jira/browse/TINKERPOP-2411[TINKERPOP-2411] + +==== fail() Step + +The new `fail()` step provides a way to immediately terminate a traversal with a runtime exception. In the Gremlin +Console, the exception will be rendered as follows which helps provide some context to the failure: + +[source,text] +---- +gremlin> g.V().fail("nope!") +fail() Step Triggered +===================== +Message > nope! +Traverser> v[1] + Bulk > 1 +Traversal> V().fail() +Metadata > {} +===================== +---- + +See: link:https://issues.apache.org/jira/browse/TINKERPOP-2635[TINKERPOP-2635], +link:https://tinkerpop.apache.org/docs/3.6.0/reference/#fail-step[Reference Documentation] ==== Null for T diff --git a/gremlin-console/src/main/groovy/org/apache/tinkerpop/gremlin/console/Console.groovy b/gremlin-console/src/main/groovy/org/apache/tinkerpop/gremlin/console/Console.groovy index c47bbf3..bf5fe0c 100644 --- a/gremlin-console/src/main/groovy/org/apache/tinkerpop/gremlin/console/Console.groovy +++ b/gremlin-console/src/main/groovy/org/apache/tinkerpop/gremlin/console/Console.groovy @@ -36,6 +36,13 @@ import org.apache.tinkerpop.gremlin.jsr223.CoreGremlinPlugin import org.apache.tinkerpop.gremlin.jsr223.GremlinPlugin import org.apache.tinkerpop.gremlin.jsr223.ImportCustomizer import org.apache.tinkerpop.gremlin.jsr223.console.RemoteException +import org.apache.tinkerpop.gremlin.process.traversal.Failure +import org.apache.tinkerpop.gremlin.process.traversal.Step +import org.apache.tinkerpop.gremlin.process.traversal.Traverser +import org.apache.tinkerpop.gremlin.process.traversal.step.util.EmptyStep +import org.apache.tinkerpop.gremlin.process.traversal.translator.GroovyTranslator +import org.apache.tinkerpop.gremlin.process.traversal.traverser.B_LP_NL_O_P_S_SE_SL_Traverser +import org.apache.tinkerpop.gremlin.process.traversal.traverser.TraverserRequirement import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalExplanation import org.apache.tinkerpop.gremlin.structure.Edge import org.apache.tinkerpop.gremlin.structure.T @@ -346,47 +353,54 @@ class Console { if (err instanceof Throwable) { try { final Throwable e = (Throwable) err - String message = e.getMessage() - if (null != message) { - message = message.replace("startup failed:", "") - io.err.println(Colorizer.render(Preferences.errorColor, message.trim())) + + // special rendering for Failure + if ((err instanceof Failure)) { + def fail = (Failure) err + io.err.println(Colorizer.render(Preferences.errorColor, fail.format())) } else { - io.err.println(Colorizer.render(Preferences.errorColor,e)) - } + String message = e.getMessage() + if (null != message) { + message = message.replace("startup failed:", "") + io.err.println(Colorizer.render(Preferences.errorColor, message.trim())) + } else { + io.err.println(Colorizer.render(Preferences.errorColor, e)) + } - // provide a hint in the case of a stackoverflow as it can be common when running large Gremlin - // scripts and it isn't immediately apparent what the error might mean in this context especially - // if the user isn't familiar with the JVM. it really can only be a hint since we can't be completely - // sure it arose as a result of a long Gremlin traversal. - if (err instanceof StackOverflowError) { - io.err.println(Colorizer.render(Preferences.errorColor, - "A StackOverflowError can indicate that the Gremlin traversal being executed is too long. If " + - "you have a single Gremlin statement that is \"long\", you may break it up into " + - "multiple separate commands, re-write the traversal to operate on a stream of " + - "input via inject() rather than literals, or attempt to increase the -Xss setting" + - "of the Gremlin Console by modifying gremlin.sh.")); - } + // provide a hint in the case of a stackoverflow as it can be common when running large Gremlin + // scripts and it isn't immediately apparent what the error might mean in this context especially + // if the user isn't familiar with the JVM. it really can only be a hint since we can't be completely + // sure it arose as a result of a long Gremlin traversal. + if (err instanceof StackOverflowError) { + io.err.println(Colorizer.render(Preferences.errorColor, + "A StackOverflowError can indicate that the Gremlin traversal being executed is too long. If " + + "you have a single Gremlin statement that is \"long\", you may break it up into " + + "multiple separate commands, re-write the traversal to operate on a stream of " + + "input via inject() rather than literals, or attempt to increase the -Xss setting" + + "of the Gremlin Console by modifying gremlin.sh.")); + } - if (interactive) { - io.err.println(Colorizer.render(Preferences.infoColor,"Type ':help' or ':h' for help.")) - io.err.print(Colorizer.render(Preferences.errorColor, "Display stack trace? [yN]")) - io.err.flush() - String line = new BufferedReader(io.in).readLine() - if (null == line) - line = "" - io.err.print(line.trim()) - io.err.println() - if (line.trim().equals("y") || line.trim().equals("Y")) { - if (err instanceof RemoteException && err.remoteStackTrace.isPresent()) { - io.err.print(err.remoteStackTrace.get()) - io.err.flush() - } else { - e.printStackTrace(io.err) + if (interactive) { + io.err.println(Colorizer.render(Preferences.infoColor, "Type ':help' or ':h' for help.")) + io.err.print(Colorizer.render(Preferences.errorColor, "Display stack trace? [yN]")) + io.err.flush() + String line = new BufferedReader(io.in).readLine() + if (null == line) + line = "" + io.err.print(line.trim()) + io.err.println() + if (line.trim().equals("y") || line.trim().equals("Y")) { + if (err instanceof RemoteException && err.remoteStackTrace.isPresent()) { + io.err.print(err.remoteStackTrace.get()) + io.err.flush() + } else { + e.printStackTrace(io.err) + } } + } else { + e.printStackTrace(io.err) + System.exit(1) } - } else { - e.printStackTrace(io.err) - System.exit(1) } } catch (Exception ignored) { io.err.println(Colorizer.render(Preferences.errorColor, "An undefined error has occurred: " + err)) @@ -402,6 +416,44 @@ class Console { return null } + private def writeTraverserToErrorLines(Traverser t, List errorLines) { + // every traverser has an object so toString() that. pad with spaces to cover "side-effects" width + errorLines << "Traverser> $t" + + def optGenerator = t.asAdmin().generator + if (optGenerator.isPresent()) { + def width = "Traverser".length() + def generator = optGenerator.get() + if (generator.providedRequirements.contains(TraverserRequirement.BULK)) { + errorLines << " Bulk".padRight(width) + "> " + t.bulk() + } + + if (generator.providedRequirements.contains(TraverserRequirement.SACK)) { + errorLines << " Sack".padRight(width) + "> " + t.sack() + } + + if (generator.providedRequirements.contains(TraverserRequirement.PATH)) { + errorLines << " Path".padRight(width) + "> " + t.path() + } + + if (generator.providedRequirements.contains(TraverserRequirement.SINGLE_LOOP) || + generator.providedRequirements.contains(TraverserRequirement.NESTED_LOOP)) { + // flatten loops/names if present + def loopNames = t.asAdmin().loopNames + def loopsLine = loopNames.isEmpty() ? t.loops() : loopNames.collect { [(it): t.loops(it)]} + errorLines << " Loops".padRight(width) + "> " + loopsLine + } + + if (generator.providedRequirements.contains(TraverserRequirement.SIDE_EFFECTS)) { + // convert side-effects to a map + def sideEffects = t.asAdmin().sideEffects + def keys = sideEffects.keys() + errorLines << " S/E".padRight(width) + "> " + keys.collectEntries { [(it): sideEffects.get(it)]} + } + + } + } + private static String buildResultPrompt() { final String groovyshellProperty = System.getProperty("gremlin.prompt") if (groovyshellProperty != null) diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/GremlinBaseVisitor.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/GremlinBaseVisitor.java index 95f2874..faef075 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/GremlinBaseVisitor.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/GremlinBaseVisitor.java @@ -348,6 +348,19 @@ public class GremlinBaseVisitor<T> extends AbstractParseTreeVisitor<T> implement * {@inheritDoc} */ @Override public T visitTraversalMethod_emit_Traversal(final GremlinParser.TraversalMethod_emit_TraversalContext ctx) { notImplemented(ctx); return null; } + + /** + * {@inheritDoc} + */ + @Override + public T visitTraversalMethod_fail_Empty(final GremlinParser.TraversalMethod_fail_EmptyContext ctx) { notImplemented(ctx); return null; } + + /** + * {@inheritDoc} + */ + @Override + public T visitTraversalMethod_fail_String(final GremlinParser.TraversalMethod_fail_StringContext ctx) { notImplemented(ctx); return null; } + /** * {@inheritDoc} */ diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalMethodVisitor.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalMethodVisitor.java index 67258ce..7682232 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalMethodVisitor.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalMethodVisitor.java @@ -29,6 +29,7 @@ import org.apache.tinkerpop.gremlin.structure.Direction; import org.apache.tinkerpop.gremlin.structure.T; import org.apache.tinkerpop.gremlin.structure.VertexProperty; +import java.util.Map; import java.util.function.BiFunction; import static org.apache.tinkerpop.gremlin.process.traversal.SackFunctions.Barrier.normSack; @@ -453,6 +454,22 @@ public class TraversalMethodVisitor extends TraversalRootVisitor<GraphTraversal> * {@inheritDoc} */ @Override + public Traversal visitTraversalMethod_fail_Empty(final GremlinParser.TraversalMethod_fail_EmptyContext ctx) { + return this.graphTraversal.fail(); + } + + /** + * {@inheritDoc} + */ + @Override + public Traversal visitTraversalMethod_fail_String(final GremlinParser.TraversalMethod_fail_StringContext ctx) { + return this.graphTraversal.fail(GenericLiteralVisitor.getStringLiteral(ctx.stringLiteral())); + } + + /** + * {@inheritDoc} + */ + @Override public GraphTraversal visitTraversalMethod_filter_Predicate(final GremlinParser.TraversalMethod_filter_PredicateContext ctx) { return graphTraversal.filter(TraversalPredicateVisitor.getInstance().visitTraversalPredicate(ctx.traversalPredicate())); } diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/Failure.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/Failure.java new file mode 100644 index 0000000..8e11c87 --- /dev/null +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/Failure.java @@ -0,0 +1,101 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.tinkerpop.gremlin.process.traversal; + +import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.FailStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.util.EmptyStep; +import org.apache.tinkerpop.gremlin.process.traversal.translator.GroovyTranslator; +import org.apache.tinkerpop.gremlin.process.traversal.traverser.TraverserRequirement; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +public interface Failure { + + static Translator.ScriptTranslator TRANSLATOR = GroovyTranslator.of(""); + + String getMessage(); + + Map<String,Object> getMetadata(); + + Traverser.Admin getTraverser(); + + Traversal.Admin getTraversal(); + + /** + * Gets the {@code Failure} as a formatted string representation. + */ + public default String format() { + final List<String> lines = new ArrayList<>(); + final Step parentStep = (Step) getTraversal().getParent(); + + lines.add(String.format("Message > %s", getMessage())); + lines.add(String.format("Traverser> %s", getTraverser().toString())); + + final TraverserGenerator generator = getTraversal().getTraverserGenerator(); + final Traverser.Admin traverser = getTraverser(); + if (generator.getProvidedRequirements().contains(TraverserRequirement.BULK)) { + lines.add(String.format(" Bulk > %s", traverser.bulk())); + } + if (generator.getProvidedRequirements().contains(TraverserRequirement.SACK)) { + lines.add(String.format(" Sack > %s", traverser.sack())); + } + if (generator.getProvidedRequirements().contains(TraverserRequirement.PATH)) { + lines.add(String.format(" Path > %s", traverser.path())); + } + if (generator.getProvidedRequirements().contains(TraverserRequirement.SINGLE_LOOP) || + generator.getProvidedRequirements().contains(TraverserRequirement.NESTED_LOOP) ) { + final Set<String> loopNames = traverser.getLoopNames(); + final String loopsLine = loopNames.isEmpty() ? + String.valueOf(traverser.asAdmin().loops()) : + loopNames.stream().collect(Collectors.toMap(loopName -> loopName, traverser::loops)).toString(); + lines.add(String.format(" Loops > %s", loopsLine)); + } + if (generator.getProvidedRequirements().contains(TraverserRequirement.SIDE_EFFECTS)) { + final TraversalSideEffects tse = traverser.getSideEffects(); + final Set<String> keys = tse.keys(); + lines.add(String.format(" S/E > %s", keys.stream().collect(Collectors.toMap(k -> k, tse::get)))); + } + + // removes the starting period so that "__.out()" simply presents as "out()" + lines.add(String.format("Traversal> %s", TRANSLATOR.translate(getTraversal()).getScript().substring(1))); + + // not sure there is a situation where fail() would be used where it was not wrapped in a parent, + // but on the odd case that it is it can be handled + if (parentStep != EmptyStep.instance()) { + lines.add(String.format("Parent > %s [%s]", + parentStep.getClass().getSimpleName(), TRANSLATOR.translate(parentStep.getTraversal()).getScript().substring(1))); + } + + lines.add(String.format("Metadata > %s", getMetadata())); + + final int longestLineLength = lines.stream().mapToInt(String::length).max().getAsInt(); + final String separatorLine = String.join("", Collections.nCopies(longestLineLength, "=")); + lines.add(0, separatorLine); + lines.add(0, "fail() Step Triggered"); + lines.add(separatorLine); + + return String.join(System.lineSeparator(), lines); + } +} diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/Traverser.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/Traverser.java index fc58c62..19eb689 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/Traverser.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/Traverser.java @@ -22,7 +22,9 @@ import org.apache.tinkerpop.gremlin.process.traversal.step.map.LoopsStep; import org.apache.tinkerpop.gremlin.structure.util.Attachable; import java.io.Serializable; +import java.util.Collections; import java.util.Comparator; +import java.util.Optional; import java.util.Set; import java.util.function.Function; @@ -325,5 +327,12 @@ public interface Traverser<T> extends Serializable, Comparable<Traverser<T>>, Cl * @return the set of tags associated with the traverser. */ public Set<String> getTags(); + + /** + * If this traverser supports loops then return the loop names if any. + */ + public default Set<String> getLoopNames() { + return Collections.emptySet(); + } } } diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.java index 8d11664..891ead0 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.java @@ -25,6 +25,7 @@ import org.apache.tinkerpop.gremlin.process.computer.traversal.step.map.PageRank import org.apache.tinkerpop.gremlin.process.computer.traversal.step.map.PeerPressureVertexProgramStep; import org.apache.tinkerpop.gremlin.process.computer.traversal.step.map.ProgramVertexProgramStep; import org.apache.tinkerpop.gremlin.process.computer.traversal.step.map.ShortestPathVertexProgramStep; +import org.apache.tinkerpop.gremlin.process.traversal.Failure; import org.apache.tinkerpop.gremlin.process.traversal.Order; import org.apache.tinkerpop.gremlin.process.traversal.P; import org.apache.tinkerpop.gremlin.process.traversal.Path; @@ -126,6 +127,7 @@ import org.apache.tinkerpop.gremlin.process.traversal.step.map.UnfoldStep; import org.apache.tinkerpop.gremlin.process.traversal.step.map.VertexStep; import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.AddPropertyStep; import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.AggregateGlobalStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.FailStep; import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.GroupCountSideEffectStep; import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.GroupSideEffectStep; import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.IdentityStep; @@ -1254,6 +1256,18 @@ public interface GraphTraversal<S, E> extends Traversal<S, E> { } /** + * Filter all traversers in the traversal. This step has narrow use cases and is primarily intended for use as a + * signal to remote servers that {@link #iterate()} was called. While it may be directly used, it is often a sign + * that a traversal should be re-written in another form. + * + * @return the updated traversal with respective {@link NoneStep}. + */ + @Override + default GraphTraversal<S, E> none() { + return (GraphTraversal<S, E>) Traversal.super.none(); + } + + /** * Ensures that at least one of the provided traversals yield a result. * * @param orTraversals filter traversals where at least one must be satisfied @@ -2154,6 +2168,33 @@ public interface GraphTraversal<S, E> extends Traversal<S, E> { } /** + * When triggered, immediately throws a {@code RuntimeException} which implements the {@link Failure} interface. + * The traversal will be terminated as a result. + * + * @return the traversal with an appended {@link FailStep}. + * @see <a href="http://tinkerpop.apache.org/docs/${project.version}/reference/#fail-step" target="_blank">Reference Documentation - Fail Step</a> + * @since 3.6.0 + */ + public default GraphTraversal<S, E> fail() { + this.asAdmin().getBytecode().addStep(Symbols.fail); + return this.asAdmin().addStep(new FailStep<>(this.asAdmin())); + } + + /** + * When triggered, immediately throws a {@code RuntimeException} which implements the {@link Failure} interface. + * The traversal will be terminated as a result. + * + * @param message the error message to include in the exception + * @return the traversal with an appended {@link FailStep}. + * @see <a href="http://tinkerpop.apache.org/docs/${project.version}/reference/#fail-step" target="_blank">Reference Documentation - Fail Step</a> + * @since 3.6.0 + */ + public default GraphTraversal<S, E> fail(final String message) { + this.asAdmin().getBytecode().addStep(Symbols.fail, message); + return this.asAdmin().addStep(new FailStep<>(this.asAdmin(), message)); + } + + /** * Aggregates the emanating paths into a {@link Tree} data structure. * * @param sideEffectKey the name of the side-effect key that will hold the tree @@ -2222,18 +2263,6 @@ public interface GraphTraversal<S, E> extends Traversal<S, E> { } /** - * Filter all traversers in the traversal. This step has narrow use cases and is primarily intended for use as a - * signal to remote servers that {@link #iterate()} was called. While it may be directly used, it is often a sign - * that a traversal should be re-written in another form. - * - * @return the updated traversal with respective {@link NoneStep}. - */ - @Override - default GraphTraversal<S, E> none() { - return (GraphTraversal<S, E>) Traversal.super.none(); - } - - /** * Sets a {@link Property} value and related meta properties if supplied, if supported by the {@link Graph} * and if the {@link Element} is a {@link VertexProperty}. This method is the long-hand version of * {@link #property(Object, Object, Object...)} with the difference that the {@link VertexProperty.Cardinality} @@ -3156,6 +3185,7 @@ public interface GraphTraversal<S, E> extends Traversal<S, E> { @Deprecated public static final String store = "store"; public static final String aggregate = "aggregate"; + public static final String fail = "fail"; public static final String subgraph = "subgraph"; public static final String barrier = "barrier"; public static final String index = "index"; diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/__.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/__.java index aab83f4..08a4c07 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/__.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/__.java @@ -912,6 +912,20 @@ public class __ { } /** + * @see GraphTraversal#fail() + */ + public static <A> GraphTraversal<A, A> fail() { + return __.<A>start().fail(); + } + + /** + * @see GraphTraversal#fail(String) + */ + public static <A> GraphTraversal<A, A> fail(final String message) { + return __.<A>start().fail(message); + } + + /** * @see GraphTraversal#group(String) */ public static <A> GraphTraversal<A, A> group(final String sideEffectKey) { diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/FailStep.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/FailStep.java new file mode 100644 index 0000000..48741f8 --- /dev/null +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/FailStep.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.tinkerpop.gremlin.process.traversal.step.sideEffect; + +import org.apache.tinkerpop.gremlin.process.traversal.Failure; +import org.apache.tinkerpop.gremlin.process.traversal.Traversal; +import org.apache.tinkerpop.gremlin.process.traversal.Traverser; + +import java.util.Collections; +import java.util.Map; + +/** + * Triggers an immediate failure of the traversal by throwing a {@code RuntimeException}. The exception thrown must + * implement the {@link Failure} interface. + */ +public class FailStep<S> extends SideEffectStep<S> { + + protected String message; + protected Map<String,Object> metadata; + + public FailStep(final Traversal.Admin traversal) { + this(traversal, "fail() step triggered"); + } + + public FailStep(final Traversal.Admin traversal, final String message) { + this(traversal, message, Collections.emptyMap()); + } + + public FailStep(final Traversal.Admin traversal, final String message, final Map<String,Object> metadata) { + super(traversal); + this.message = message; + this.metadata = metadata; + } + + @Override + protected void sideEffect(final Traverser.Admin<S> traverser) { + throw new FailException(traversal, traverser, message, metadata); + } + + /** + * Default {@link Failure} implementation that is thrown by {@link FailStep}. + */ + public static class FailException extends RuntimeException implements Failure { + private final Map<String,Object> metadata; + private final Traversal.Admin traversal; + private final Traverser.Admin traverser; + + public FailException(final Traversal.Admin traversal, final Traverser.Admin traverser, + final String message, final Map<String,Object> metadata) { + super(message); + this.metadata = metadata; + this.traversal = traversal; + this.traverser = traverser; + } + + @Override + public Map<String, Object> getMetadata() { + return metadata; + } + + @Override + public Traverser.Admin getTraverser() { + return traverser; + } + + @Override + public Traversal.Admin getTraversal() { + return traversal; + } + } +} diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/traverser/B_LP_NL_O_S_SE_SL_Traverser.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/traverser/B_LP_NL_O_S_SE_SL_Traverser.java index 59287ac..401bfd2 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/traverser/B_LP_NL_O_S_SE_SL_Traverser.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/traverser/B_LP_NL_O_S_SE_SL_Traverser.java @@ -23,6 +23,7 @@ import org.apache.tinkerpop.gremlin.process.traversal.Step; import org.apache.tinkerpop.gremlin.process.traversal.traverser.util.LabelledCounter; import java.util.Iterator; +import java.util.Set; import java.util.Stack; public class B_LP_NL_O_S_SE_SL_Traverser<T> extends B_LP_O_S_SE_SL_Traverser<T> { @@ -65,6 +66,10 @@ public class B_LP_NL_O_S_SE_SL_Traverser<T> extends B_LP_O_S_SE_SL_Traverser<T> this.loopNames.put(loopName, lc); } } + @Override + public Set<String> getLoopNames() { + return loopNames.keySet(); + } @Override public void incrLoops() { diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/traverser/B_LP_O_S_SE_SL_Traverser.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/traverser/B_LP_O_S_SE_SL_Traverser.java index 3a2aa38..1687909 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/traverser/B_LP_O_S_SE_SL_Traverser.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/traverser/B_LP_O_S_SE_SL_Traverser.java @@ -19,7 +19,6 @@ package org.apache.tinkerpop.gremlin.process.traversal.traverser; import org.apache.tinkerpop.gremlin.process.traversal.Path; -import org.apache.tinkerpop.gremlin.process.traversal.Pop; import org.apache.tinkerpop.gremlin.process.traversal.Step; import org.apache.tinkerpop.gremlin.process.traversal.Traverser; import org.apache.tinkerpop.gremlin.process.traversal.step.util.ImmutablePath; diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/traverser/B_NL_O_S_SE_SL_Traverser.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/traverser/B_NL_O_S_SE_SL_Traverser.java index 20ed1be..6e18d12 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/traverser/B_NL_O_S_SE_SL_Traverser.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/traverser/B_NL_O_S_SE_SL_Traverser.java @@ -18,7 +18,6 @@ */ package org.apache.tinkerpop.gremlin.process.traversal.traverser; -import org.apache.commons.collections.MapIterator; import org.apache.commons.collections.map.ReferenceMap; import org.apache.tinkerpop.gremlin.process.traversal.Step; import org.apache.tinkerpop.gremlin.process.traversal.traverser.util.LabelledCounter; diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/traverser/B_O_S_SE_SL_Traverser.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/traverser/B_O_S_SE_SL_Traverser.java index 57fa47a..904d8c7 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/traverser/B_O_S_SE_SL_Traverser.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/traverser/B_O_S_SE_SL_Traverser.java @@ -22,7 +22,9 @@ import org.apache.tinkerpop.gremlin.process.traversal.Step; import org.apache.tinkerpop.gremlin.process.traversal.TraversalSideEffects; import org.apache.tinkerpop.gremlin.process.traversal.Traverser; +import java.util.Collections; import java.util.Objects; +import java.util.Set; /** * @author Marko A. Rodriguez (http://markorodriguez.com) @@ -72,6 +74,11 @@ public class B_O_S_SE_SL_Traverser<T> extends B_O_Traverser<T> { } @Override + public Set<String> getLoopNames() { + return Collections.singleton(loopName); + } + + @Override public void initialiseLoops(final String stepLabel , final String loopName){ this.loopName = loopName; } diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/traverser/LP_NL_O_OB_P_S_SE_SL_Traverser.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/traverser/LP_NL_O_OB_P_S_SE_SL_Traverser.java index 3cb1dd3..77157b8 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/traverser/LP_NL_O_OB_P_S_SE_SL_Traverser.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/traverser/LP_NL_O_OB_P_S_SE_SL_Traverser.java @@ -24,6 +24,7 @@ import org.apache.tinkerpop.gremlin.process.traversal.Step; import org.apache.tinkerpop.gremlin.process.traversal.traverser.util.LabelledCounter; import java.util.Iterator; +import java.util.Set; import java.util.Stack; public class LP_NL_O_OB_P_S_SE_SL_Traverser<T> extends LP_O_OB_P_S_SE_SL_Traverser<T> { @@ -130,6 +131,11 @@ public class LP_NL_O_OB_P_S_SE_SL_Traverser<T> extends LP_O_OB_P_S_SE_SL_Travers super.merge(other); } + @Override + public Set<String> getLoopNames() { + return loopNames.keySet(); + } + ///////////////// @Override diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/traverser/LP_NL_O_OB_S_SE_SL_Traverser.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/traverser/LP_NL_O_OB_S_SE_SL_Traverser.java index a9cd7fb..14c2fe3 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/traverser/LP_NL_O_OB_S_SE_SL_Traverser.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/traverser/LP_NL_O_OB_S_SE_SL_Traverser.java @@ -24,6 +24,7 @@ import org.apache.tinkerpop.gremlin.process.traversal.Step; import org.apache.tinkerpop.gremlin.process.traversal.traverser.util.LabelledCounter; import java.util.Iterator; +import java.util.Set; import java.util.Stack; public class LP_NL_O_OB_S_SE_SL_Traverser<T> extends LP_O_OB_S_SE_SL_Traverser<T> { @@ -58,6 +59,11 @@ public class LP_NL_O_OB_S_SE_SL_Traverser<T> extends LP_O_OB_S_SE_SL_Traverser<T } @Override + public Set<String> getLoopNames() { + return loopNames.keySet(); + } + + @Override public void initialiseLoops(final String stepLabel, final String loopName) { if (this.nestedLoops.empty() || !this.nestedLoops.peek().hasLabel(stepLabel)) { final LabelledCounter lc = new LabelledCounter(stepLabel, (short) 0); diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/traverser/LP_O_OB_S_SE_SL_Traverser.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/traverser/LP_O_OB_S_SE_SL_Traverser.java index f6bb9cc..97038fe 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/traverser/LP_O_OB_S_SE_SL_Traverser.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/traverser/LP_O_OB_S_SE_SL_Traverser.java @@ -20,7 +20,6 @@ package org.apache.tinkerpop.gremlin.process.traversal.traverser; import org.apache.tinkerpop.gremlin.process.traversal.Path; -import org.apache.tinkerpop.gremlin.process.traversal.Pop; import org.apache.tinkerpop.gremlin.process.traversal.Step; import org.apache.tinkerpop.gremlin.process.traversal.Traverser; import org.apache.tinkerpop.gremlin.process.traversal.step.util.ImmutablePath; diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/traverser/NL_O_OB_S_SE_SL_Traverser.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/traverser/NL_O_OB_S_SE_SL_Traverser.java index 72f43fe..c30e893 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/traverser/NL_O_OB_S_SE_SL_Traverser.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/traverser/NL_O_OB_S_SE_SL_Traverser.java @@ -24,6 +24,7 @@ import org.apache.tinkerpop.gremlin.process.traversal.Step; import org.apache.tinkerpop.gremlin.process.traversal.traverser.util.LabelledCounter; import java.util.Iterator; +import java.util.Set; import java.util.Stack; public class NL_O_OB_S_SE_SL_Traverser<T> extends O_OB_S_SE_SL_Traverser<T> { @@ -130,6 +131,11 @@ public class NL_O_OB_S_SE_SL_Traverser<T> extends O_OB_S_SE_SL_Traverser<T> { super.merge(other); } + @Override + public Set<String> getLoopNames() { + return loopNames.keySet(); + } + ///////////////// @Override diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/traverser/O_OB_S_SE_SL_Traverser.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/traverser/O_OB_S_SE_SL_Traverser.java index 601bcda..73dc644 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/traverser/O_OB_S_SE_SL_Traverser.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/traverser/O_OB_S_SE_SL_Traverser.java @@ -23,7 +23,9 @@ import org.apache.tinkerpop.gremlin.process.traversal.Step; import org.apache.tinkerpop.gremlin.process.traversal.TraversalSideEffects; import org.apache.tinkerpop.gremlin.process.traversal.Traverser; +import java.util.Collections; import java.util.Objects; +import java.util.Set; /** * @author Marko A. Rodriguez (http://markorodriguez.com) @@ -78,13 +80,18 @@ public class O_OB_S_SE_SL_Traverser<T> extends O_Traverser<T> { @Override public int loops(final String loopName) { - if (loopName == null || this.loopName != null && this.loopName.equals(loopName)) + if (loopName == null || this.loopName.equals(loopName)) return this.loops; else throw new IllegalArgumentException("Loop name not defined: " + loopName); } @Override + public Set<String> getLoopNames() { + return Collections.singleton(loopName); + } + + @Override public void initialiseLoops(final String stepLabel , final String loopName){ this.loopName = loopName; } diff --git a/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GraphTraversal.cs b/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GraphTraversal.cs index 328e506..ebd1cdf 100644 --- a/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GraphTraversal.cs +++ b/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GraphTraversal.cs @@ -543,6 +543,33 @@ namespace Gremlin.Net.Process.Traversal } /// <summary> + /// Adds the fail step to this <see cref="GraphTraversal{SType, EType}" />. + /// </summary> + public GraphTraversal<S, E> Fail () + { + Bytecode.AddStep("fail"); + return Wrap<S, E>(this); + } + + /// <summary> + /// Adds the fail step to this <see cref="GraphTraversal{SType, EType}" />. + /// </summary> + public GraphTraversal<S, E> Fail (string msg) + { + Bytecode.AddStep("fail", msg); + return Wrap<S, E>(this); + } + + /// <summary> + /// Adds the fail step to this <see cref="GraphTraversal{SType, EType}" />. + /// </summary> + public GraphTraversal<S, E> Fail (string msg, IDictionary<string,object> m) + { + Bytecode.AddStep("fail", msg, m); + return Wrap<S, E>(this); + } + + /// <summary> /// Adds the filter step to this <see cref="GraphTraversal{SType, EType}" />. /// </summary> public GraphTraversal<S, E> Filter (IPredicate predicate) diff --git a/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/__.cs b/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/__.cs index fa1c027..acad588 100644 --- a/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/__.cs +++ b/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/__.cs @@ -364,6 +364,22 @@ namespace Gremlin.Net.Process.Traversal } /// <summary> + /// Spawns a <see cref="GraphTraversal{SType, EType}" /> and adds the fail step to that traversal. + /// </summary> + public static GraphTraversal<object, object> Fail() + { + return new GraphTraversal<object, object>().Fail(); + } + + /// <summary> + /// Spawns a <see cref="GraphTraversal{SType, EType}" /> and adds the fail step to that traversal. + /// </summary> + public static GraphTraversal<object, object> Fail(string msg) + { + return new GraphTraversal<object, object>().Fail(msg); + } + + /// <summary> /// Spawns a <see cref="GraphTraversal{SType, EType}" /> and adds the filter step to that traversal. /// </summary> public static GraphTraversal<object, object> Filter(IPredicate predicate) diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/CommonSteps.cs b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/CommonSteps.cs index 11e71c8..37a758a 100644 --- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/CommonSteps.cs +++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/CommonSteps.cs @@ -47,6 +47,7 @@ namespace Gremlin.Net.IntegrationTest.Gherkin private readonly IDictionary<string, object> _parameters = new Dictionary<string, object>(); private ITraversal _traversal; private object[] _result; + private Exception _error = null; private static readonly JsonSerializerOptions JsonDeserializingOptions = new JsonSerializerOptions {PropertyNamingPolicy = JsonNamingPolicy.CamelCase}; @@ -157,11 +158,18 @@ namespace Gremlin.Net.IntegrationTest.Gherkin } ITraversal t = _traversal; var list = new List<object>(); - while (t.MoveNext()) + try { - list.Add(t.Current); + while (t.MoveNext()) + { + list.Add(t.Current); + } + _result = list.ToArray(); + } + catch (Exception ex) + { + _error = ex; } - _result = list.ToArray(); } [When("iterated next")] @@ -171,21 +179,34 @@ namespace Gremlin.Net.IntegrationTest.Gherkin { throw new InvalidOperationException("Traversal should be set before iterating"); } - _traversal.MoveNext(); - var result = _traversal.Current; - switch (result) + + try { - case null: - _result = null; - return; - case object[] arrayResult: - _result = arrayResult; - return; - case IEnumerable enumerableResult: - _result = enumerableResult.Cast<object>().ToArray(); - return; + _traversal.MoveNext(); + var result = _traversal.Current; + switch (result) + { + case null: + _result = null; + return; + case object[] arrayResult: + _result = arrayResult; + return; + case IEnumerable enumerableResult: + _result = enumerableResult.Cast<object>().ToArray(); + return; + } + } + catch (Exception ex) + { + _error = ex; } - throw new InvalidCastException($"Can not convert instance of {result.GetType()} to object[]"); + } + + [Then("the traversal will raise an error")] + public void TraversalWillRaiseError() + { + Assert.NotNull(_error); } [Then("the result should be (\\w+)")] diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs index 40bae6a..4f809e3 100644 --- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs +++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs @@ -715,6 +715,9 @@ namespace Gremlin.Net.IntegrationTest.Gherkin {"g_V_aggregateXlocal_aX_byXoutEXcreatedX_countX_out_out_aggregateXlocal_aX_byXinEXcreatedX_weight_sumX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Aggregate(Scope.Local,"a").By(__.OutE("created").Count()).Out().Out().Aggregate(Scope.Local,"a").By(__.InE("created").Values<object>("weight").Sum<object>()).Cap<object>("a")}}, {"g_V_aggregateXxX_byXvaluesXageX_isXgtX29XXX_capXxX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Aggregate("x").By(__.Values<object>("age").Is(P.Gt(29))).Cap<object>("x")}}, {"g_V_aggregateXxX_byXout_order_byXnameXX_capXxX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Aggregate("x").By(__.Out().Order().By("name")).Cap<object>("x")}}, + {"g_V_fail", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Fail()}}, + {"g_V_failXmsgX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Fail("msg")}}, + {"g_V_unionXout_failX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Union<object>(__.Out(),__.Fail())}}, {"g_V_group_byXnameX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Group<object,object>().By("name")}}, {"g_V_group_byXageX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Group<object,object>().By("age")}}, {"g_V_group_byXnameX_by", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Group<object,object>().By("name").By()}}, diff --git a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/Tokens.java b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/Tokens.java index c5936a2..ba9b6fd 100644 --- a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/Tokens.java +++ b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/Tokens.java @@ -19,6 +19,7 @@ package org.apache.tinkerpop.gremlin.driver; import org.apache.tinkerpop.gremlin.driver.message.ResponseMessage; +import org.apache.tinkerpop.gremlin.process.traversal.Failure; import org.apache.tinkerpop.gremlin.process.traversal.TraversalSource; import org.apache.tinkerpop.gremlin.structure.Graph; @@ -96,8 +97,22 @@ public final class Tokens { public static final String VAL_TRAVERSAL_SOURCE_ALIAS = "g"; + /** + * The value of this key holds a string representation of the data held by a {@link Failure} as produced by + * {@link Failure#format()}. + */ + public static final String STATUS_ATTRIBUTE_FAIL_STEP_MESSAGE = "failStepMessage"; + + /** + * Refers to the hierarchy of exception names for a particular exception thrown on the server. + */ public static final String STATUS_ATTRIBUTE_EXCEPTIONS = "exceptions"; + + /** + * Refers to the stacktrace for an exception thrown on the server + */ public static final String STATUS_ATTRIBUTE_STACK_TRACE = "stackTrace"; + /** * A {@link ResultSet#statusAttributes()} key for user-facing warnings. * <p> diff --git a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/message/ResponseStatusCode.java b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/message/ResponseStatusCode.java index 4f43f65..88fce82 100644 --- a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/message/ResponseStatusCode.java +++ b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/message/ResponseStatusCode.java @@ -18,6 +18,9 @@ */ package org.apache.tinkerpop.gremlin.driver.message; +import org.apache.tinkerpop.gremlin.process.traversal.Failure; +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal; + import java.util.HashMap; import java.util.Iterator; import java.util.Map; @@ -114,6 +117,14 @@ public enum ResponseStatusCode { SERVER_ERROR(500), /** + * A server error that is produced when the {@link GraphTraversal#fail()} step is triggered. The returned exception + * will include information consistent with the {@link Failure} interface. + * + * @since 3.6.0 + */ + SERVER_ERROR_FAIL_STEP(595), + + /** * A server error that indicates that the client should retry the request. A graph will typically return this error * when a transaction fails due to a locking exception or some other sort of concurrent modification. In other * words, the request was likely valid but the state of the server at the particular time the request arrived diff --git a/gremlin-groovy/src/main/groovy/org/apache/tinkerpop/gremlin/groovy/jsr223/ast/VarAsBindingASTTransformation.groovy b/gremlin-groovy/src/main/groovy/org/apache/tinkerpop/gremlin/groovy/jsr223/ast/VarAsBindingASTTransformation.groovy index 5cee02b..1da973d 100644 --- a/gremlin-groovy/src/main/groovy/org/apache/tinkerpop/gremlin/groovy/jsr223/ast/VarAsBindingASTTransformation.groovy +++ b/gremlin-groovy/src/main/groovy/org/apache/tinkerpop/gremlin/groovy/jsr223/ast/VarAsBindingASTTransformation.groovy @@ -115,6 +115,9 @@ class VarAsBindingASTTransformation implements ASTTransformation { case GraphTraversal.Symbols.by: if (i == 1) bindingValue = new PropertyExpression(new ClassExpression(new ClassNode(Order)), "desc") break + case GraphTraversal.Symbols.fail: + if (i == 1) bindingValue = new MethodCallExpression(new ClassExpression(new ClassNode(Collections)), "emptyMap", new TupleExpression()) + break } def bindingExpression = createBindingFromVar(entry.text, bindingVariableName, bindingValue) bindingExpression.sourcePosition = entry diff --git a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/process/graph-traversal.js b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/process/graph-traversal.js index e45e0e2..6c49ec5 100644 --- a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/process/graph-traversal.js +++ b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/process/graph-traversal.js @@ -500,6 +500,16 @@ class GraphTraversal extends Traversal { this.bytecode.addStep('emit', args); return this; } + + /** + * Graph traversal fa method. + * @param {...Object} args + * @returns {GraphTraversal} + */ + fail(...args) { + this.bytecode.addStep('fail', args); + return this; + } /** * Graph traversal filter method. @@ -1325,6 +1335,7 @@ const statics = { drop: (...args) => callOnEmptyTraversal('drop', args), elementMap: (...args) => callOnEmptyTraversal('elementMap', args), emit: (...args) => callOnEmptyTraversal('emit', args), + fail: (...args) => callOnEmptyTraversal('fail', args), filter: (...args) => callOnEmptyTraversal('filter', args), flatMap: (...args) => callOnEmptyTraversal('flatMap', args), fold: (...args) => callOnEmptyTraversal('fold', args), diff --git a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/feature-steps.js b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/feature-steps.js index dd5c562..02e7bfc 100644 --- a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/feature-steps.js +++ b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/feature-steps.js @@ -128,7 +128,7 @@ Given(/^using the parameter (.+) defined as "(.+)"$/, function (paramName, strin }); When('iterated to list', function () { - return this.traversal.toList().then(list => this.result = list); + return this.traversal.toList().then(list => this.result = list).catch(err => this.result = err); }); When('iterated next', function () { @@ -138,7 +138,11 @@ When('iterated next', function () { // Compare using the objects array this.result = this.result.objects; } - }); + }).catch(err => this.result = err); +}); + +Then('the traversal will raise an error', function() { + expect(this.result).to.be.a.instanceof(Error); }); Then(/^the result should be (\w+)$/, function assertResult(characterizedAs, resultTable) { diff --git a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/gremlin.js b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/gremlin.js index 6f49924..d303f97 100644 --- a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/gremlin.js +++ b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/gremlin.js @@ -703,6 +703,9 @@ const gremlins = { g_V_aggregateXlocal_aX_byXoutEXcreatedX_countX_out_out_aggregateXlocal_aX_byXinEXcreatedX_weight_sumX: [function({g}) { return g.V().aggregate(Scope.local,"a").by(__.outE("created").count()).out().out().aggregate(Scope.local,"a").by(__.inE("created").values("weight").sum()).cap("a") }], g_V_aggregateXxX_byXvaluesXageX_isXgtX29XXX_capXxX: [function({g}) { return g.V().aggregate("x").by(__.values("age").is(P.gt(29))).cap("x") }], g_V_aggregateXxX_byXout_order_byXnameXX_capXxX: [function({g}) { return g.V().aggregate("x").by(__.out().order().by("name")).cap("x") }], + g_V_fail: [function({g}) { return g.V().fail() }], + g_V_failXmsgX: [function({g}) { return g.V().fail("msg") }], + g_V_unionXout_failX: [function({g}) { return g.V().union(__.out(),__.fail()) }], g_V_group_byXnameX: [function({g}) { return g.V().group().by("name") }], g_V_group_byXageX: [function({g}) { return g.V().group().by("age") }], g_V_group_byXnameX_by: [function({g}) { return g.V().group().by("name").by() }], diff --git a/gremlin-language/src/main/antlr4/Gremlin.g4 b/gremlin-language/src/main/antlr4/Gremlin.g4 index a1ebb53..4cbc8a4 100644 --- a/gremlin-language/src/main/antlr4/Gremlin.g4 +++ b/gremlin-language/src/main/antlr4/Gremlin.g4 @@ -240,6 +240,7 @@ traversalMethod | traversalMethod_subgraph | traversalMethod_sum | traversalMethod_tail + | traversalMethod_fail | traversalMethod_timeLimit | traversalMethod_times | traversalMethod_to @@ -668,6 +669,11 @@ traversalMethod_tail | 'tail' LPAREN integerLiteral RPAREN #traversalMethod_tail_long ; +traversalMethod_fail + : 'fail' LPAREN RPAREN #traversalMethod_fail_Empty + | 'fail' LPAREN stringLiteral RPAREN #traversalMethod_fail_String + ; + traversalMethod_timeLimit : 'timeLimit' LPAREN integerLiteral RPAREN ; diff --git a/gremlin-python/src/main/python/gremlin_python/process/graph_traversal.py b/gremlin-python/src/main/python/gremlin_python/process/graph_traversal.py index b56988b..770821e 100644 --- a/gremlin-python/src/main/python/gremlin_python/process/graph_traversal.py +++ b/gremlin-python/src/main/python/gremlin_python/process/graph_traversal.py @@ -253,6 +253,10 @@ class GraphTraversal(Traversal): self.bytecode.add_step("emit", *args) return self + def fail(self, *args): + self.bytecode.add_step("fail", *args) + return self + def filter_(self, *args): self.bytecode.add_step("filter", *args) return self @@ -679,6 +683,10 @@ class __(object, metaclass=MagicType): return cls.graph_traversal(None, None, Bytecode()).emit(*args) @classmethod + def fail(cls, *args): + return cls.graph_traversal(None, None, Bytecode()).fail(*args) + + @classmethod def filter_(cls, *args): return cls.graph_traversal(None, None, Bytecode()).filter_(*args) @@ -1039,6 +1047,10 @@ def emit(*args): return __.emit(*args) +def fail(*args): + return __.fail(*args) + + def filter_(*args): return __.filter_(*args) @@ -1355,6 +1367,8 @@ statics.add_static('elementMap', elementMap) statics.add_static('emit', emit) +statics.add_static('fail', fail) + statics.add_static('filter_', filter_) statics.add_static('flatMap', flatMap) diff --git a/gremlin-python/src/main/python/radish/feature_steps.py b/gremlin-python/src/main/python/radish/feature_steps.py index 9fd8cf6..dbc9944 100644 --- a/gremlin-python/src/main/python/radish/feature_steps.py +++ b/gremlin-python/src/main/python/radish/feature_steps.py @@ -95,8 +95,12 @@ def translate_traversal(step): def iterate_the_traversal(step): if step.context.ignore: return - - step.context.result = list(map(lambda x: _convert_results(x), step.context.traversal.toList())) + + try: + step.context.result = list(map(lambda x: _convert_results(x), step.context.traversal.toList())) + step.context.failed = False + except: + step.context.failed = True @when("iterated next") @@ -104,8 +108,16 @@ def next_the_traversal(step): if step.context.ignore: return - step.context.result = list(map(lambda x: _convert_results(x), step.context.traversal.next())) + try: + step.context.result = list(map(lambda x: _convert_results(x), step.context.traversal.next())) + step.context.failed = False + except: + step.context.failed = True + +@then("the traversal will raise an error") +def raise_an_error(step): + assert_that(step.context.failed, equal_to(True)) @then("the result should be {characterized_as:w}") def assert_result(step, characterized_as): diff --git a/gremlin-python/src/main/python/radish/gremlin.py b/gremlin-python/src/main/python/radish/gremlin.py index 652d491..13900d6 100644 --- a/gremlin-python/src/main/python/radish/gremlin.py +++ b/gremlin-python/src/main/python/radish/gremlin.py @@ -688,6 +688,9 @@ world.gremlins = { 'g_V_aggregateXlocal_aX_byXoutEXcreatedX_countX_out_out_aggregateXlocal_aX_byXinEXcreatedX_weight_sumX': [(lambda g:g.V().aggregate(Scope.local,'a').by(__.outE('created').count()).out().out().aggregate(Scope.local,'a').by(__.inE('created').weight.sum_()).cap('a'))], 'g_V_aggregateXxX_byXvaluesXageX_isXgtX29XXX_capXxX': [(lambda g:g.V().aggregate('x').by(__.age.is_(P.gt(29))).cap('x'))], 'g_V_aggregateXxX_byXout_order_byXnameXX_capXxX': [(lambda g:g.V().aggregate('x').by(__.out().order().by('name')).cap('x'))], + 'g_V_fail': [(lambda g:g.V().fail())], + 'g_V_failXmsgX': [(lambda g:g.V().fail('msg'))], + 'g_V_unionXout_failX': [(lambda g:g.V().union(__.out(),__.fail()))], 'g_V_group_byXnameX': [(lambda g:g.V().group().by('name'))], 'g_V_group_byXageX': [(lambda g:g.V().group().by('age'))], 'g_V_group_byXnameX_by': [(lambda g:g.V().group().by('name').by())], diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/AbstractSession.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/AbstractSession.java index 65c26be..ed63dc6 100644 --- a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/AbstractSession.java +++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/AbstractSession.java @@ -35,6 +35,7 @@ import org.apache.tinkerpop.gremlin.groovy.jsr223.TimedInterruptTimeoutException import org.apache.tinkerpop.gremlin.jsr223.GremlinScriptEngine; import org.apache.tinkerpop.gremlin.jsr223.JavaTranslator; import org.apache.tinkerpop.gremlin.process.traversal.Bytecode; +import org.apache.tinkerpop.gremlin.process.traversal.Failure; import org.apache.tinkerpop.gremlin.process.traversal.GraphOp; import org.apache.tinkerpop.gremlin.process.traversal.Traversal; import org.apache.tinkerpop.gremlin.process.traversal.TraversalSource; @@ -278,14 +279,20 @@ public abstract class AbstractSession implements Session, AutoCloseable { protected void handleException(final SessionTask sessionTask, final Throwable t) throws SessionException { if (t instanceof SessionException) throw (SessionException) t; - final Optional<Throwable> possibleTemporaryException = determineIfTemporaryException(t); - if (possibleTemporaryException.isPresent()) { - final Throwable temporaryException = possibleTemporaryException.get(); - throw new SessionException(temporaryException.getMessage(), t, - ResponseMessage.build(sessionTask.getRequestMessage()) - .code(ResponseStatusCode.SERVER_ERROR_TEMPORARY) - .statusMessage(temporaryException.getMessage()) - .statusAttributeException(temporaryException).create()); + final Optional<Throwable> possibleSpecialException = determineIfSpecialException(t); + if (possibleSpecialException.isPresent()) { + final Throwable special = possibleSpecialException.get(); + final ResponseMessage.Builder specialResponseMsg = ResponseMessage.build(sessionTask.getRequestMessage()). + statusMessage(special.getMessage()). + statusAttributeException(special); + if (special instanceof TemporaryException) { + specialResponseMsg.code(ResponseStatusCode.SERVER_ERROR_TEMPORARY); + } else if (special instanceof Failure) { + final Failure failure = (Failure) special; + specialResponseMsg.code(ResponseStatusCode.SERVER_ERROR_FAIL_STEP). + statusAttribute(Tokens.STATUS_ATTRIBUTE_FAIL_STEP_MESSAGE, failure.format()); + } + throw new SessionException(special.getMessage(), specialResponseMsg.create()); } final Throwable root = ExceptionUtils.getRootCause(t); @@ -373,12 +380,12 @@ public abstract class AbstractSession implements Session, AutoCloseable { } /** - * Check if any exception in the chain is TemporaryException then we should respond with the right error code so - * that the client knows to retry. + * Check if any exception in the chain is {@link TemporaryException} or {@link Failure} then respond with the + * right error code so that the client knows to retry. */ - protected Optional<Throwable> determineIfTemporaryException(final Throwable ex) { + protected static Optional<Throwable> determineIfSpecialException(final Throwable ex) { return Stream.of(ExceptionUtils.getThrowables(ex)). - filter(i -> i instanceof TemporaryException).findFirst(); + filter(i -> i instanceof TemporaryException || i instanceof Failure).findFirst(); } /** diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/op/AbstractEvalOpProcessor.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/op/AbstractEvalOpProcessor.java index 2a8ecbe..cbd14a1 100644 --- a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/op/AbstractEvalOpProcessor.java +++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/op/AbstractEvalOpProcessor.java @@ -25,6 +25,7 @@ import org.apache.tinkerpop.gremlin.driver.message.ResponseMessage; import org.apache.tinkerpop.gremlin.driver.message.ResponseStatusCode; import org.apache.tinkerpop.gremlin.groovy.engine.GremlinExecutor; import org.apache.tinkerpop.gremlin.groovy.jsr223.TimedInterruptTimeoutException; +import org.apache.tinkerpop.gremlin.process.traversal.Failure; import org.apache.tinkerpop.gremlin.process.traversal.Operator; import org.apache.tinkerpop.gremlin.process.traversal.Order; import org.apache.tinkerpop.gremlin.process.traversal.Pop; @@ -38,6 +39,7 @@ import org.apache.tinkerpop.gremlin.server.Context; import org.apache.tinkerpop.gremlin.server.GremlinServer; import org.apache.tinkerpop.gremlin.server.Settings; import org.apache.tinkerpop.gremlin.server.util.MetricManager; +import org.apache.tinkerpop.gremlin.structure.util.TemporaryException; import org.apache.tinkerpop.gremlin.util.function.ThrowingConsumer; import org.apache.tinkerpop.gremlin.util.iterator.IteratorUtils; import org.codehaus.groovy.control.MultipleCompilationErrorsException; @@ -272,13 +274,22 @@ public abstract class AbstractEvalOpProcessor extends AbstractOpProcessor { timerContext.stop(); if (t != null) { - // if any exception in the chain is TemporaryException then we should respond with the right error - // code so that the client knows to retry - final Optional<Throwable> possibleTemporaryException = determineIfTemporaryException(t); - if (possibleTemporaryException.isPresent()) { - ctx.writeAndFlush(ResponseMessage.build(msg).code(ResponseStatusCode.SERVER_ERROR_TEMPORARY) - .statusMessage(possibleTemporaryException.get().getMessage()) - .statusAttributeException(possibleTemporaryException.get()).create()); + // if any exception in the chain is TemporaryException or Failure then we should respond with the + // right error code so that the client knows to retry + final Optional<Throwable> possibleSpecialException = determineIfSpecialException(t); + if (possibleSpecialException.isPresent()) { + final Throwable special = possibleSpecialException.get(); + final ResponseMessage.Builder specialResponseMsg = ResponseMessage.build(msg). + statusMessage(special.getMessage()). + statusAttributeException(special); + if (special instanceof TemporaryException) { + specialResponseMsg.code(ResponseStatusCode.SERVER_ERROR_TEMPORARY); + } else if (special instanceof Failure) { + final Failure failure = (Failure) special; + specialResponseMsg.code(ResponseStatusCode.SERVER_ERROR_FAIL_STEP). + statusAttribute(Tokens.STATUS_ATTRIBUTE_FAIL_STEP_MESSAGE, failure.format()); + } + ctx.writeAndFlush(specialResponseMsg.create()); } else if (t instanceof OpProcessorException) { ctx.writeAndFlush(((OpProcessorException) t).getResponseMessage()); } else if (t instanceof TimedInterruptTimeoutException) { diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/op/AbstractOpProcessor.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/op/AbstractOpProcessor.java index 1498c51..c11af3b 100644 --- a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/op/AbstractOpProcessor.java +++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/op/AbstractOpProcessor.java @@ -26,6 +26,7 @@ import org.apache.tinkerpop.gremlin.driver.message.RequestMessage; import org.apache.tinkerpop.gremlin.driver.message.ResponseMessage; import org.apache.tinkerpop.gremlin.driver.message.ResponseStatusCode; import org.apache.tinkerpop.gremlin.driver.ser.MessageTextSerializer; +import org.apache.tinkerpop.gremlin.process.traversal.Failure; import org.apache.tinkerpop.gremlin.server.Context; import org.apache.tinkerpop.gremlin.server.GraphManager; import org.apache.tinkerpop.gremlin.server.OpProcessor; @@ -67,12 +68,12 @@ public abstract class AbstractOpProcessor implements OpProcessor { } /** - * Check if any exception in the chain is TemporaryException then we should respond with the right error code so - * that the client knows to retry. + * Check if any exception in the chain is {@link TemporaryException} or {@link Failure} then respond with the + * right error code so that the client knows to retry. */ - protected static Optional<Throwable> determineIfTemporaryException(final Throwable ex) { + protected static Optional<Throwable> determineIfSpecialException(final Throwable ex) { return Stream.of(ExceptionUtils.getThrowables(ex)). - filter(i -> i instanceof TemporaryException).findFirst(); + filter(i -> i instanceof TemporaryException || i instanceof Failure).findFirst(); } /** diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/op/session/SessionOpProcessor.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/op/session/SessionOpProcessor.java index 6a18645..a5df693 100644 --- a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/op/session/SessionOpProcessor.java +++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/op/session/SessionOpProcessor.java @@ -27,6 +27,7 @@ import org.apache.tinkerpop.gremlin.driver.message.ResponseStatusCode; import org.apache.tinkerpop.gremlin.groovy.jsr223.GroovyCompilerGremlinPlugin; import org.apache.tinkerpop.gremlin.jsr223.JavaTranslator; import org.apache.tinkerpop.gremlin.process.traversal.Bytecode; +import org.apache.tinkerpop.gremlin.process.traversal.Failure; import org.apache.tinkerpop.gremlin.process.traversal.Traversal; import org.apache.tinkerpop.gremlin.process.traversal.TraversalSource; import org.apache.tinkerpop.gremlin.process.traversal.util.BytecodeHelper; @@ -46,6 +47,7 @@ import org.apache.tinkerpop.gremlin.server.util.TraverserIterator; import org.apache.tinkerpop.gremlin.structure.Graph; import org.apache.tinkerpop.gremlin.structure.io.graphson.GraphSONMapper; import org.apache.tinkerpop.gremlin.structure.io.graphson.GraphSONVersion; +import org.apache.tinkerpop.gremlin.structure.util.TemporaryException; import org.apache.tinkerpop.gremlin.util.function.ThrowingConsumer; import org.apache.tinkerpop.shaded.jackson.databind.ObjectMapper; import org.slf4j.Logger; @@ -419,13 +421,22 @@ public class SessionOpProcessor extends AbstractEvalOpProcessor { if (ex instanceof UndeclaredThrowableException) t = t.getCause(); - // if any exception in the chain is TemporaryException then we should respond with the right error - // code so that the client knows to retry - final Optional<Throwable> possibleTemporaryException = determineIfTemporaryException(ex); - if (possibleTemporaryException.isPresent()) { - context.writeAndFlush(ResponseMessage.build(msg).code(ResponseStatusCode.SERVER_ERROR_TEMPORARY) - .statusMessage(possibleTemporaryException.get().getMessage()) - .statusAttributeException(possibleTemporaryException.get()).create()); + // if any exception in the chain is TemporaryException or Failure then we should respond with the + // right error code so that the client knows to retry + final Optional<Throwable> possibleSpecialException = determineIfSpecialException(ex); + if (possibleSpecialException.isPresent()) { + final Throwable special = possibleSpecialException.get(); + final ResponseMessage.Builder specialResponseMsg = ResponseMessage.build(msg). + statusMessage(special.getMessage()). + statusAttributeException(special); + if (special instanceof TemporaryException) { + specialResponseMsg.code(ResponseStatusCode.SERVER_ERROR_TEMPORARY); + } else if (special instanceof Failure) { + final Failure failure = (Failure) special; + specialResponseMsg.code(ResponseStatusCode.SERVER_ERROR_FAIL_STEP). + statusAttribute(Tokens.STATUS_ATTRIBUTE_FAIL_STEP_MESSAGE, failure.format()); + } + context.writeAndFlush(specialResponseMsg.create()); } else if (t instanceof InterruptedException || t instanceof TraversalInterruptedException) { final String errorMessage = String.format("A timeout occurred during traversal evaluation of [%s] - consider increasing the limit given to evaluationTimeout", msg); logger.warn(errorMessage); @@ -441,11 +452,22 @@ public class SessionOpProcessor extends AbstractEvalOpProcessor { onError(graph, context); } } catch (Exception ex) { - final Optional<Throwable> possibleTemporaryException = determineIfTemporaryException(ex); - if (possibleTemporaryException.isPresent()) { - context.writeAndFlush(ResponseMessage.build(msg).code(ResponseStatusCode.SERVER_ERROR_TEMPORARY) - .statusMessage(possibleTemporaryException.get().getMessage()) - .statusAttributeException(possibleTemporaryException.get()).create()); + // if any exception in the chain is TemporaryException or Failure then we should respond with the + // right error code so that the client knows to retry + final Optional<Throwable> possibleSpecialException = determineIfSpecialException(ex); + if (possibleSpecialException.isPresent()) { + final Throwable special = possibleSpecialException.get(); + final ResponseMessage.Builder specialResponseMsg = ResponseMessage.build(msg). + statusMessage(special.getMessage()). + statusAttributeException(special); + if (special instanceof TemporaryException) { + specialResponseMsg.code(ResponseStatusCode.SERVER_ERROR_TEMPORARY); + } else if (special instanceof Failure) { + final Failure failure = (Failure) special; + specialResponseMsg.code(ResponseStatusCode.SERVER_ERROR_FAIL_STEP). + statusAttribute(Tokens.STATUS_ATTRIBUTE_FAIL_STEP_MESSAGE, failure.format()); + } + context.writeAndFlush(specialResponseMsg.create()); } else { logger.warn(String.format("Exception processing a Traversal on request [%s].", msg.getRequestId()), ex); context.writeAndFlush(ResponseMessage.build(msg).code(ResponseStatusCode.SERVER_ERROR) @@ -501,11 +523,22 @@ public class SessionOpProcessor extends AbstractEvalOpProcessor { .create()); } catch (Exception ex) { - final Optional<Throwable> possibleTemporaryException = determineIfTemporaryException(ex); - if (possibleTemporaryException.isPresent()) { - context.writeAndFlush(ResponseMessage.build(msg).code(ResponseStatusCode.SERVER_ERROR_TEMPORARY) - .statusMessage(possibleTemporaryException.get().getMessage()) - .statusAttributeException(possibleTemporaryException.get()).create()); + // if any exception in the chain is TemporaryException or Failure then we should respond with the + // right error code so that the client knows to retry + final Optional<Throwable> possibleSpecialException = determineIfSpecialException(ex); + if (possibleSpecialException.isPresent()) { + final Throwable special = possibleSpecialException.get(); + final ResponseMessage.Builder specialResponseMsg = ResponseMessage.build(msg). + statusMessage(special.getMessage()). + statusAttributeException(special); + if (special instanceof TemporaryException) { + specialResponseMsg.code(ResponseStatusCode.SERVER_ERROR_TEMPORARY); + } else if (special instanceof Failure) { + final Failure failure = (Failure) special; + specialResponseMsg.code(ResponseStatusCode.SERVER_ERROR_FAIL_STEP). + statusAttribute(Tokens.STATUS_ATTRIBUTE_FAIL_STEP_MESSAGE, failure.format()); + } + context.writeAndFlush(specialResponseMsg.create()); } else { logger.warn(String.format("Exception processing a Traversal on request [%s].", msg.getRequestId()), ex); context.writeAndFlush(ResponseMessage.build(msg).code(ResponseStatusCode.SERVER_ERROR) diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/op/traversal/TraversalOpProcessor.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/op/traversal/TraversalOpProcessor.java index 80be01f..620332f 100644 --- a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/op/traversal/TraversalOpProcessor.java +++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/op/traversal/TraversalOpProcessor.java @@ -27,6 +27,7 @@ import org.apache.tinkerpop.gremlin.driver.message.ResponseMessage; import org.apache.tinkerpop.gremlin.driver.message.ResponseStatusCode; import org.apache.tinkerpop.gremlin.jsr223.JavaTranslator; import org.apache.tinkerpop.gremlin.process.traversal.Bytecode; +import org.apache.tinkerpop.gremlin.process.traversal.Failure; import org.apache.tinkerpop.gremlin.process.traversal.Traversal; import org.apache.tinkerpop.gremlin.process.traversal.TraversalSource; import org.apache.tinkerpop.gremlin.process.traversal.util.BytecodeHelper; @@ -38,13 +39,13 @@ import org.apache.tinkerpop.gremlin.server.OpProcessor; import org.apache.tinkerpop.gremlin.server.Settings; import org.apache.tinkerpop.gremlin.server.auth.AuthenticatedUser; import org.apache.tinkerpop.gremlin.server.handler.Frame; -import org.apache.tinkerpop.gremlin.server.handler.SessionException; import org.apache.tinkerpop.gremlin.server.handler.StateKey; import org.apache.tinkerpop.gremlin.server.op.AbstractOpProcessor; import org.apache.tinkerpop.gremlin.server.op.OpProcessorException; import org.apache.tinkerpop.gremlin.server.util.MetricManager; import org.apache.tinkerpop.gremlin.server.util.TraverserIterator; import org.apache.tinkerpop.gremlin.structure.Graph; +import org.apache.tinkerpop.gremlin.structure.util.TemporaryException; import org.apache.tinkerpop.gremlin.util.function.ThrowingConsumer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -222,13 +223,22 @@ public class TraversalOpProcessor extends AbstractOpProcessor { if (ex instanceof UndeclaredThrowableException) t = t.getCause(); - // if any exception in the chain is TemporaryException then we should respond with the right error - // code so that the client knows to retry - final Optional<Throwable> possibleTemporaryException = determineIfTemporaryException(ex); - if (possibleTemporaryException.isPresent()) { - context.writeAndFlush(ResponseMessage.build(msg).code(ResponseStatusCode.SERVER_ERROR_TEMPORARY) - .statusMessage(possibleTemporaryException.get().getMessage()) - .statusAttributeException(possibleTemporaryException.get()).create()); + // if any exception in the chain is TemporaryException or Failure then we should respond with the + // right error code so that the client knows to retry + final Optional<Throwable> possibleSpecialException = determineIfSpecialException(ex); + if (possibleSpecialException.isPresent()) { + final Throwable special = possibleSpecialException.get(); + final ResponseMessage.Builder specialResponseMsg = ResponseMessage.build(msg). + statusMessage(special.getMessage()). + statusAttributeException(special); + if (special instanceof TemporaryException) { + specialResponseMsg.code(ResponseStatusCode.SERVER_ERROR_TEMPORARY); + } else if (special instanceof Failure) { + final Failure failure = (Failure) special; + specialResponseMsg.code(ResponseStatusCode.SERVER_ERROR_FAIL_STEP). + statusAttribute(Tokens.STATUS_ATTRIBUTE_FAIL_STEP_MESSAGE, failure.format()); + } + context.writeAndFlush(specialResponseMsg.create()); } else if (t instanceof InterruptedException || t instanceof TraversalInterruptedException) { final String errorMessage = String.format("A timeout occurred during traversal evaluation of [%s] - consider increasing the limit given to evaluationTimeout", msg); logger.warn(errorMessage); @@ -244,11 +254,22 @@ public class TraversalOpProcessor extends AbstractOpProcessor { onError(graph, context); } } catch (Exception ex) { - final Optional<Throwable> possibleTemporaryException = determineIfTemporaryException(ex); - if (possibleTemporaryException.isPresent()) { - context.writeAndFlush(ResponseMessage.build(msg).code(ResponseStatusCode.SERVER_ERROR_TEMPORARY) - .statusMessage(possibleTemporaryException.get().getMessage()) - .statusAttributeException(possibleTemporaryException.get()).create()); + // if any exception in the chain is TemporaryException or Failure then we should respond with the + // right error code so that the client knows to retry + final Optional<Throwable> possibleSpecialException = determineIfSpecialException(ex); + if (possibleSpecialException.isPresent()) { + final Throwable special = possibleSpecialException.get(); + final ResponseMessage.Builder specialResponseMsg = ResponseMessage.build(msg). + statusMessage(special.getMessage()). + statusAttributeException(special); + if (special instanceof TemporaryException) { + specialResponseMsg.code(ResponseStatusCode.SERVER_ERROR_TEMPORARY); + } else if (special instanceof Failure) { + final Failure failure = (Failure) special; + specialResponseMsg.code(ResponseStatusCode.SERVER_ERROR_FAIL_STEP). + statusAttribute(Tokens.STATUS_ATTRIBUTE_FAIL_STEP_MESSAGE, failure.format()); + } + context.writeAndFlush(specialResponseMsg.create()); } else { logger.warn(String.format("Exception processing a Traversal on request [%s].", msg.getRequestId()), ex); context.writeAndFlush(ResponseMessage.build(msg).code(ResponseStatusCode.SERVER_ERROR) diff --git a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerIntegrateTest.java b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerIntegrateTest.java index 2f88ddc..dd9b682 100644 --- a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerIntegrateTest.java +++ b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerIntegrateTest.java @@ -1083,4 +1083,20 @@ public class GremlinServerIntegrateTest extends AbstractGremlinServerIntegration assertEquals(ResponseStatusCode.SERVER_ERROR_TEMPORARY, ((ResponseException) t).getResponseStatusCode()); } } + + @Test + public void shouldGenerateFailureErrorResponseStatusCode() throws Exception { + final Cluster cluster = TestClientFactory.build().create(); + final Client client = cluster.connect(); + + try { + client.submit("g.inject(0).fail('make it stop')").all().get(); + fail("Should have tanked since we used fail() step"); + } catch (Exception ex) { + final Throwable t = ex.getCause(); + assertThat(t, instanceOf(ResponseException.class)); + assertEquals("make it stop", t.getMessage()); + assertEquals(ResponseStatusCode.SERVER_ERROR_FAIL_STEP, ((ResponseException) t).getResponseStatusCode()); + } + } } diff --git a/gremlin-test/features/sideEffect/Fail.feature b/gremlin-test/features/sideEffect/Fail.feature new file mode 100644 index 0000000..53f2d56 --- /dev/null +++ b/gremlin-test/features/sideEffect/Fail.feature @@ -0,0 +1,46 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + +@StepClassSideEffect @StepFail +Feature: Step - fail() + + Scenario: g_V_fail + Given the modern graph + And the traversal of + """ + g.V().fail() + """ + When iterated to list + Then the traversal will raise an error + + Scenario: g_V_failXmsgX + Given the modern graph + And the traversal of + """ + g.V().fail("msg") + """ + When iterated to list + Then the traversal will raise an error + + Scenario: g_V_unionXout_failX + Given the modern graph + And the traversal of + """ + g.V().union(out(), fail()) + """ + When iterated to list + Then the traversal will raise an error diff --git a/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/features/StepDefinition.java b/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/features/StepDefinition.java index 6b644a0..7138d73 100644 --- a/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/features/StepDefinition.java +++ b/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/features/StepDefinition.java @@ -53,6 +53,7 @@ import static org.hamcrest.collection.IsIterableContainingInOrder.contains; import static org.hamcrest.core.Every.everyItem; import static org.hamcrest.core.IsInstanceOf.instanceOf; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.fail; import java.math.BigDecimal; @@ -81,6 +82,7 @@ public final class StepDefinition { private final Map<String, String> stringParameters = new HashMap<>(); private Traversal traversal; private Object result; + private Throwable error; private static final Pattern edgeTripletPattern = Pattern.compile("(.+)-(.+)->(.+)"); private static final Pattern ioPattern = Pattern.compile("g\\.io\\(\"(.*)\"\\).*"); private List<Pair<Pattern, Function<String,String>>> stringMatcherConverters = new ArrayList<Pair<Pattern, Function<String,String>>>() {{ @@ -204,6 +206,7 @@ public final class StepDefinition { } if (result != null) result = null; + if (error != null) error = null; } @After @@ -243,12 +246,20 @@ public final class StepDefinition { @When("iterated to list") public void iteratedToList() { - result = traversal.toList(); + try { + result = traversal.toList(); + } catch (Exception ex) { + error = ex; + } } @When("iterated next") public void iteratedNext() { - result = traversal.next(); + try { + result = traversal.next(); + } catch (Exception ex) { + error = ex; + } } @Then("the result should be unordered") @@ -305,6 +316,11 @@ public final class StepDefinition { assertEquals(0, IteratorUtils.count((Collection) result)); } + @Then("the traversal will raise an error") + public void theTraversalWillRaiseAnError() { + assertNotNull(error); + } + ////////////////////////////////////////////// @Given("an unsupported test")
