This is an automated email from the ASF dual-hosted git repository.
colegreer pushed a commit to branch 3.7-dev
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
The following commit(s) were added to refs/heads/3.7-dev by this push:
new e54ae50e4c TINKERPOP-3210 Fix cap() step mid-traversal in OLAP (#3332)
e54ae50e4c is described below
commit e54ae50e4c94db7d76692fc27314bbc59adf2802
Author: Stephen Mallette <[email protected]>
AuthorDate: Mon Mar 23 20:03:13 2026 -0400
TINKERPOP-3210 Fix cap() step mid-traversal in OLAP (#3332)
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 f1ff7a13f8..5b5558d1c3 100644
--- a/CHANGELOG.asciidoc
+++ b/CHANGELOG.asciidoc
@@ -35,6 +35,7 @@
image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima
* 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.
* Added support for Node 22 and 24 alongside Node 20.
+* 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] |