This is an automated email from the ASF dual-hosted git repository. spmallette pushed a commit to branch TINKERPOP-3210 in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
commit 0793f22f0e3a52f7741117c912c52a55ea8f3491 Author: Stephen Mallette <[email protected]> AuthorDate: Wed Mar 18 11:18:31 2026 -0400 TINKERPOP-3210 Fix cap() step mid-traversal in OLAP Added check to memory.set() to ensure masterState() in MemoryTraversalSideEffects. Changed TraversalVertexProgram to gather all completed barriers across all rounds which prevented stale lazy re-evaluation. --- CHANGELOG.asciidoc | 1 + .../traversal/MemoryTraversalSideEffects.java | 7 ++- .../computer/traversal/TraversalVertexProgram.java | 9 ++- .../Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs | 3 + gremlin-go/driver/cucumber/gremlin.go | 3 + .../gremlin-javascript/test/cucumber/gremlin.js | 2 + .../src/main/python/tests/feature/gremlin.py | 2 + .../test/features/sideEffect/Aggregate.feature | 66 ++++++++++++++++++++++ 8 files changed, 90 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 555e70a604..782ff71bcf 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -32,6 +32,7 @@ image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima * Added `closeSessionPostGraphOp` to the Gremlin Server settings to indicate that the `Session` should be closed on either a successful commit or rollback. * Added `SessionedChildClient` that borrows connections from a different `Client` for use with `Sessions`. * Added `reuseConnectionsForSessions` to Java GLV settings to decide whether to use `SessionedChildClient` for remote transactions. +* Fixed `cap()` step throwing an error when used mid-traversal in OLAP. [[release-3-7-5]] === TinkerPop 3.7.5 (Release Date: November 12, 2025) diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/computer/traversal/MemoryTraversalSideEffects.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/computer/traversal/MemoryTraversalSideEffects.java index 9bc91ad69e..9d180b4ec8 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/computer/traversal/MemoryTraversalSideEffects.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/computer/traversal/MemoryTraversalSideEffects.java @@ -57,7 +57,12 @@ public final class MemoryTraversalSideEffects implements TraversalSideEffects { @Override public void set(final String key, final Object value) { this.sideEffects.set(key, value); - if (null != this.memory) + + // looks like calls to this method are only permitted during setup/terminate (i.e. masterState) + // during worker execution (e.g. cap() firing lazily via a downstream local step), skip the + // memory write to avoid IllegalArgumentException from the distributed memory implementation. + // see TINKERPOP-3210 for an example of how this fails. + if (null != this.memory && this.phase.masterState()) this.memory.set(key, value); } diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/computer/traversal/TraversalVertexProgram.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/computer/traversal/TraversalVertexProgram.java index 0273302e9b..c247fb26e3 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/computer/traversal/TraversalVertexProgram.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/computer/traversal/TraversalVertexProgram.java @@ -329,8 +329,13 @@ public final class TraversalVertexProgram implements VertexProgram<TraverserSet< MasterExecutor.processMemory(this.traversalMatrix, memory, toProcessTraversers, completedBarriers); // process all results from barriers locally and when elements are touched, put them in remoteActiveTraversers MasterExecutor.processTraversers(this.traversal, this.traversalMatrix, toProcessTraversers, remoteActiveTraversers, haltedTraversers, this.haltedTraverserStrategy); - // tell parallel barriers that might not have been active in the last round that they are no longer active - memory.set(COMPLETED_BARRIERS, completedBarriers); + // tell parallel barriers that might not have been active in the last round that they are no longer active. + // accumulate all previously-completed barriers: worker clones start with done=false and need done() called + // for every barrier ever completed (not just the most recent ones) to prevent stale lazy re-evaluation. + // see TINKERPOP-3210 for the lazy cap() re-firing that motivated this change. + final Set<String> allCompletedBarriers = new HashSet<>(memory.get(COMPLETED_BARRIERS)); + allCompletedBarriers.addAll(completedBarriers); + memory.set(COMPLETED_BARRIERS, allCompletedBarriers); if (!remoteActiveTraversers.isEmpty() || completedBarriers.stream().map(this.traversalMatrix::getStepById).filter(step -> step instanceof LocalBarrier).findAny().isPresent()) { // send active traversers back to workers diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs index e483a0a6b6..739b6a04fd 100644 --- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs +++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs @@ -1483,6 +1483,9 @@ namespace Gremlin.Net.IntegrationTest.Gherkin {"g_withSideEffectXa_xx1_addAllX_V_aggregateXlocal_aX_byXageX_capXaX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.WithSideEffect("a",p["xx1"],Operator.AddAll).V().Aggregate(Scope.Local,"a").By("age").Cap<object>("a")}}, {"g_withSideEffectXa_xx1_assignX_V_aggregateXaX_byXageX_capXaX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.WithSideEffect("a",p["xx1"],Operator.Assign).V().Aggregate("a").By("age").Cap<object>("a")}}, {"g_withSideEffectXa_xx1_assignX_V_order_byXageX_aggregateXlocal_aX_byXageX_capXaX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.WithSideEffect("a",p["xx1"],Operator.Assign).V().Order().By("age").Aggregate(Scope.Local,"a").By("age").Cap<object>("a")}}, + {"g_V_repeatXaggregateXaXX_timesX2X_capXaX_unfold", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Repeat(__.Aggregate("a")).Times(2).Cap<object>("a").Unfold<object>()}}, + {"g_V_aggregateXaX_capXaX_unfold_both", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Aggregate("a").Cap<object>("a").Unfold<object>().Both()}}, + {"g_V_aggregateXaX_capXaX_unfold_barrier_both", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Aggregate("a").Cap<object>("a").Unfold<object>().Barrier().Both()}}, {"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())}}, diff --git a/gremlin-go/driver/cucumber/gremlin.go b/gremlin-go/driver/cucumber/gremlin.go index 71a5ba599a..bd619f592a 100644 --- a/gremlin-go/driver/cucumber/gremlin.go +++ b/gremlin-go/driver/cucumber/gremlin.go @@ -1454,6 +1454,9 @@ var translationMap = map[string][]func(g *gremlingo.GraphTraversalSource, p map[ "g_withSideEffectXa_xx1_addAllX_V_aggregateXlocal_aX_byXageX_capXaX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.WithSideEffect("a", p["xx1"], gremlingo.Operator.AddAll).V().Aggregate(gremlingo.Scope.Local, "a").By("age").Cap("a")}}, "g_withSideEffectXa_xx1_assignX_V_aggregateXaX_byXageX_capXaX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.WithSideEffect("a", p["xx1"], gremlingo.Operator.Assign).V().Aggregate("a").By("age").Cap("a")}}, "g_withSideEffectXa_xx1_assignX_V_order_byXageX_aggregateXlocal_aX_byXageX_capXaX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.WithSideEffect("a", p["xx1"], gremlingo.Operator.Assign).V().Order().By("age").Aggregate(gremlingo.Scope.Local, "a").By("age").Cap("a")}}, + "g_V_repeatXaggregateXaXX_timesX2X_capXaX_unfold": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Repeat(gremlingo.T__.Aggregate("a")).Times(2).Cap("a").Unfold()}}, + "g_V_aggregateXaX_capXaX_unfold_both": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Aggregate("a").Cap("a").Unfold().Both()}}, + "g_V_aggregateXaX_capXaX_unfold_barrier_both": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Aggregate("a").Cap("a").Unfold().Barrier().Both()}}, "g_V_fail": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Fail()}}, "g_V_failXmsgX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Fail("msg")}}, "g_V_unionXout_failX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Union(gremlingo.T__.Out(), gremlingo.T__.Fail())}}, 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 1403e37d53..58da9767c5 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 @@ -1474,6 +1474,8 @@ const gremlins = { g_withSideEffectXa_xx1_addAllX_V_aggregateXlocal_aX_byXageX_capXaX: [function({g, xx1}) { return g.withSideEffect("a",xx1,Operator.addAll).V().aggregate(Scope.local,"a").by("age").cap("a") }], g_withSideEffectXa_xx1_assignX_V_aggregateXaX_byXageX_capXaX: [function({g, xx1}) { return g.withSideEffect("a",xx1,Operator.assign).V().aggregate("a").by("age").cap("a") }], g_withSideEffectXa_xx1_assignX_V_order_byXageX_aggregateXlocal_aX_byXageX_capXaX: [function({g, xx1}) { return g.withSideEffect("a",xx1,Operator.assign).V().order().by("age").aggregate(Scope.local,"a").by("age").cap("a") }], + g_V_aggregateXaX_capXaX_unfold_both: [function({g}) { return g.V().aggregate("a").cap("a").unfold().both() }], + g_V_aggregateXaX_capXaX_unfold_barrier_both: [function({g}) { return g.V().aggregate("a").cap("a").unfold().barrier().both() }], 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()) }], diff --git a/gremlin-python/src/main/python/tests/feature/gremlin.py b/gremlin-python/src/main/python/tests/feature/gremlin.py index a543860a70..ed40a40218 100644 --- a/gremlin-python/src/main/python/tests/feature/gremlin.py +++ b/gremlin-python/src/main/python/tests/feature/gremlin.py @@ -1456,6 +1456,8 @@ world.gremlins = { 'g_withSideEffectXa_xx1_addAllX_V_aggregateXlocal_aX_byXageX_capXaX': [(lambda g, xx1=None:g.with_side_effect('a',xx1,Operator.add_all).V().aggregate(Scope.local,'a').by('age').cap('a'))], 'g_withSideEffectXa_xx1_assignX_V_aggregateXaX_byXageX_capXaX': [(lambda g, xx1=None:g.with_side_effect('a',xx1,Operator.assign).V().aggregate('a').by('age').cap('a'))], 'g_withSideEffectXa_xx1_assignX_V_order_byXageX_aggregateXlocal_aX_byXageX_capXaX': [(lambda g, xx1=None:g.with_side_effect('a',xx1,Operator.assign).V().order().by('age').aggregate(Scope.local,'a').by('age').cap('a'))], + 'g_V_aggregateXaX_capXaX_unfold_both': [(lambda g:g.V().aggregate('a').cap('a').unfold().both())], + 'g_V_aggregateXaX_capXaX_unfold_barrier_both': [(lambda g:g.V().aggregate('a').cap('a').unfold().barrier().both())], '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()))], diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/sideEffect/Aggregate.feature b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/sideEffect/Aggregate.feature index 7267bedd5d..7c327d8374 100644 --- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/sideEffect/Aggregate.feature +++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/sideEffect/Aggregate.feature @@ -576,3 +576,69 @@ Feature: Step - aggregate() Then the result should be unordered | result | | d[35].i | + + Scenario: g_V_repeatXaggregateXaXX_timesX2X_capXaX_unfold + Given the modern graph + And the traversal of + """ + g.V().repeat(__.aggregate("a")).times(2).cap("a").unfold() + """ + When iterated to list + Then the result should be unordered + | result | + | v[marko] | + | v[marko] | + | v[vadas] | + | v[vadas] | + | v[lop] | + | v[lop] | + | v[josh] | + | v[josh] | + | v[ripple] | + | v[ripple] | + | v[peter] | + | v[peter] | + + Scenario: g_V_aggregateXaX_capXaX_unfold_both + Given the modern graph + And the traversal of + """ + g.V().aggregate("a").cap("a").unfold().both() + """ + When iterated to list + Then the result should be unordered + | result | + | v[marko] | + | v[marko] | + | v[marko] | + | v[vadas] | + | v[josh] | + | v[josh] | + | v[josh] | + | v[lop] | + | v[lop] | + | v[lop] | + | v[peter] | + | v[ripple] | + + Scenario: g_V_aggregateXaX_capXaX_unfold_barrier_both + Given the modern graph + And the traversal of + """ + g.V().aggregate("a").cap("a").unfold().barrier().both() + """ + When iterated to list + Then the result should be unordered + | result | + | v[marko] | + | v[marko] | + | v[marko] | + | v[vadas] | + | v[josh] | + | v[josh] | + | v[josh] | + | v[lop] | + | v[lop] | + | v[lop] | + | v[peter] | + | v[ripple] |
