Skip to content

Commit 01b920d

Browse files
spmalletteCole-Greer
authored andcommitted
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.
1 parent e9540e3 commit 01b920d

8 files changed

Lines changed: 90 additions & 3 deletions

File tree

CHANGELOG.asciidoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima
3535
* Added `SessionedChildClient` that borrows connections from a different `Client` for use with `Sessions`.
3636
* Added `reuseConnectionsForSessions` to Java GLV settings to decide whether to use `SessionedChildClient` for remote transactions.
3737
* Added support for Node 22 and 24 alongside Node 20.
38+
* Fixed `cap()` step throwing an error when used mid-traversal in OLAP.
3839
3940
[[release-3-7-5]]
4041
=== TinkerPop 3.7.5 (Release Date: November 12, 2025)

gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/computer/traversal/MemoryTraversalSideEffects.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,12 @@ public TraversalSideEffects getSideEffects() {
5757
@Override
5858
public void set(final String key, final Object value) {
5959
this.sideEffects.set(key, value);
60-
if (null != this.memory)
60+
61+
// looks like calls to this method are only permitted during setup/terminate (i.e. masterState)
62+
// during worker execution (e.g. cap() firing lazily via a downstream local step), skip the
63+
// memory write to avoid IllegalArgumentException from the distributed memory implementation.
64+
// see TINKERPOP-3210 for an example of how this fails.
65+
if (null != this.memory && this.phase.masterState())
6166
this.memory.set(key, value);
6267
}
6368

gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/computer/traversal/TraversalVertexProgram.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -329,8 +329,13 @@ public boolean terminate(final Memory memory) {
329329
MasterExecutor.processMemory(this.traversalMatrix, memory, toProcessTraversers, completedBarriers);
330330
// process all results from barriers locally and when elements are touched, put them in remoteActiveTraversers
331331
MasterExecutor.processTraversers(this.traversal, this.traversalMatrix, toProcessTraversers, remoteActiveTraversers, haltedTraversers, this.haltedTraverserStrategy);
332-
// tell parallel barriers that might not have been active in the last round that they are no longer active
333-
memory.set(COMPLETED_BARRIERS, completedBarriers);
332+
// tell parallel barriers that might not have been active in the last round that they are no longer active.
333+
// accumulate all previously-completed barriers: worker clones start with done=false and need done() called
334+
// for every barrier ever completed (not just the most recent ones) to prevent stale lazy re-evaluation.
335+
// see TINKERPOP-3210 for the lazy cap() re-firing that motivated this change.
336+
final Set<String> allCompletedBarriers = new HashSet<>(memory.get(COMPLETED_BARRIERS));
337+
allCompletedBarriers.addAll(completedBarriers);
338+
memory.set(COMPLETED_BARRIERS, allCompletedBarriers);
334339
if (!remoteActiveTraversers.isEmpty() ||
335340
completedBarriers.stream().map(this.traversalMatrix::getStepById).filter(step -> step instanceof LocalBarrier).findAny().isPresent()) {
336341
// send active traversers back to workers

gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1483,6 +1483,9 @@ private static IDictionary<string, List<Func<GraphTraversalSource, IDictionary<s
14831483
{"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")}},
14841484
{"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")}},
14851485
{"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")}},
1486+
{"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>()}},
1487+
{"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()}},
1488+
{"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()}},
14861489
{"g_V_fail", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Fail()}},
14871490
{"g_V_failXmsgX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Fail("msg")}},
14881491
{"g_V_unionXout_failX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Union<object>(__.Out(),__.Fail())}},

gremlin-go/driver/cucumber/gremlin.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1454,6 +1454,9 @@ var translationMap = map[string][]func(g *gremlingo.GraphTraversalSource, p map[
14541454
"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")}},
14551455
"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")}},
14561456
"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")}},
1457+
"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()}},
1458+
"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()}},
1459+
"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()}},
14571460
"g_V_fail": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Fail()}},
14581461
"g_V_failXmsgX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Fail("msg")}},
14591462
"g_V_unionXout_failX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Union(gremlingo.T__.Out(), gremlingo.T__.Fail())}},

gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/gremlin.js

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gremlin-python/src/main/python/tests/feature/gremlin.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1456,6 +1456,8 @@
14561456
'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'))],
14571457
'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'))],
14581458
'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'))],
1459+
'g_V_aggregateXaX_capXaX_unfold_both': [(lambda g:g.V().aggregate('a').cap('a').unfold().both())],
1460+
'g_V_aggregateXaX_capXaX_unfold_barrier_both': [(lambda g:g.V().aggregate('a').cap('a').unfold().barrier().both())],
14591461
'g_V_fail': [(lambda g:g.V().fail())],
14601462
'g_V_failXmsgX': [(lambda g:g.V().fail('msg'))],
14611463
'g_V_unionXout_failX': [(lambda g:g.V().union(__.out(),__.fail()))],

gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/sideEffect/Aggregate.feature

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,3 +576,69 @@ Feature: Step - aggregate()
576576
Then the result should be unordered
577577
| result |
578578
| d[35].i |
579+
580+
Scenario: g_V_repeatXaggregateXaXX_timesX2X_capXaX_unfold
581+
Given the modern graph
582+
And the traversal of
583+
"""
584+
g.V().repeat(__.aggregate("a")).times(2).cap("a").unfold()
585+
"""
586+
When iterated to list
587+
Then the result should be unordered
588+
| result |
589+
| v[marko] |
590+
| v[marko] |
591+
| v[vadas] |
592+
| v[vadas] |
593+
| v[lop] |
594+
| v[lop] |
595+
| v[josh] |
596+
| v[josh] |
597+
| v[ripple] |
598+
| v[ripple] |
599+
| v[peter] |
600+
| v[peter] |
601+
602+
Scenario: g_V_aggregateXaX_capXaX_unfold_both
603+
Given the modern graph
604+
And the traversal of
605+
"""
606+
g.V().aggregate("a").cap("a").unfold().both()
607+
"""
608+
When iterated to list
609+
Then the result should be unordered
610+
| result |
611+
| v[marko] |
612+
| v[marko] |
613+
| v[marko] |
614+
| v[vadas] |
615+
| v[josh] |
616+
| v[josh] |
617+
| v[josh] |
618+
| v[lop] |
619+
| v[lop] |
620+
| v[lop] |
621+
| v[peter] |
622+
| v[ripple] |
623+
624+
Scenario: g_V_aggregateXaX_capXaX_unfold_barrier_both
625+
Given the modern graph
626+
And the traversal of
627+
"""
628+
g.V().aggregate("a").cap("a").unfold().barrier().both()
629+
"""
630+
When iterated to list
631+
Then the result should be unordered
632+
| result |
633+
| v[marko] |
634+
| v[marko] |
635+
| v[marko] |
636+
| v[vadas] |
637+
| v[josh] |
638+
| v[josh] |
639+
| v[josh] |
640+
| v[lop] |
641+
| v[lop] |
642+
| v[lop] |
643+
| v[peter] |
644+
| v[ripple] |

0 commit comments

Comments
 (0)