This is an automated email from the ASF dual-hosted git repository.
kenhuuu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
The following commit(s) were added to refs/heads/master by this push:
new 06da5d4485 Enable autocommit in GremlinServer (#3423)
06da5d4485 is described below
commit 06da5d44856f6d95c432fa42c8c7dd8335a6b5dc
Author: Ken Hu <[email protected]>
AuthorDate: Tue Jun 2 12:48:34 2026 -0700
Enable autocommit in GremlinServer (#3423)
This behaves the same as the TraversalOpProcessor would have
in the 3.x line. All traversals are now transactional if the
underlying Graph supports transactions. Traversals that aren't
explicitly in a transaction are now wrapped into their own
implicit transaction and the server will autocommit on sucess
and rollback on failure.
Assisted-by: Kiro:claude-opus-4-6
---
docs/src/dev/provider/index.asciidoc | 28 ++++++-
docs/src/reference/gremlin-applications.asciidoc | 22 ++++--
docs/src/reference/the-traversal.asciidoc | 10 +--
.../server/handler/HttpGremlinEndpointHandler.java | 37 ++++++---
.../GremlinDriverTransactionIntegrateTest.java | 27 +++++++
.../server/GremlinServerHttpIntegrateTest.java | 91 ++++++++++++++++++----
6 files changed, 175 insertions(+), 40 deletions(-)
diff --git a/docs/src/dev/provider/index.asciidoc
b/docs/src/dev/provider/index.asciidoc
index 7654807a90..f1c1bc6446 100644
--- a/docs/src/dev/provider/index.asciidoc
+++ b/docs/src/dev/provider/index.asciidoc
@@ -1260,8 +1260,32 @@ Graph transactions are typically `ThreadLocal`-bound.
The server maintains a sin
to ensure all operations execute on the same thread. This is an important
implementation detail for graph system
providers whose `Transaction` implementation relies on thread-local state.
-NOTE: Non-transactional requests (those without a `transactionId`) are not
affected by any of the transaction-specific
-behavior described above. They continue to operate exactly as before.
+==== Implicit Transaction Management
+
+Implicit transactions (requests without a `transactionId`) are not affected by
any of the explicit transaction-specific
+behavior described above. All graphs participate in implicit transactions
regardless of whether they support explicit
+transactions. The difference lies in what the server does at the boundaries:
+
+When the graph supports explicit transactions, the server auto-commits and
rolls back implicit transactions:
+
+* Before processing: roll back any stale open transaction to prevent state
leakage between requests.
+* On success: commit the transaction after the traversal has been fully
iterated and serialized.
+* On error: roll back the transaction so that partial mutations are not
persisted.
+
+When the graph does not support explicit transactions, writes are immediately
durable. If a failure occurs
+mid-traversal, the graph may be left in a partially mutated state since
rollback is not possible.
+
+Graph system providers implementing their own server or HTTP endpoint should
replicate the auto-commit/rollback behavior
+when their graph implementation supports explicit transactions. The commit
occurs after serialization is complete but
+before the final response is flushed to the client, so the client only
receives a success response after the data is
+committed. Note that because HTTP responses use chunked transfer encoding, the
initial HTTP status is sent as 200 OK
+before iteration begins. If the commit itself fails, the error will appear in
the response body status field and in
+the trailing HTTP headers rather than as a non-200 HTTP status code. Clients
should therefore check the response body
+status (and trailing headers) to confirm that the transaction was committed
successfully.
+
+IMPORTANT: The auto-commit/rollback logic must only apply to implicit
transactions. Requests that carry a
+`transactionId` are part of an explicit, client-managed transaction and must
not be auto-committed or auto-rolled-back
+by the server.
=== HTTP Request Interceptor
diff --git a/docs/src/reference/gremlin-applications.asciidoc
b/docs/src/reference/gremlin-applications.asciidoc
index 7a5925eb0a..18391c5506 100644
--- a/docs/src/reference/gremlin-applications.asciidoc
+++ b/docs/src/reference/gremlin-applications.asciidoc
@@ -2242,14 +2242,20 @@ above with the use of the `maximumSize`.
[[considering-transactions]]
==== Considering Transactions
-Non-transactional requests (those without a `transactionId`) behave as
self-contained units of work where the graph's
-own transaction semantics apply. Each traversal executes within its own
transaction as managed by the graph
-implementation itself. Transactional requests participate in a transaction
opened via `g.tx().begin()`, where the
-client explicitly controls the lifecycle through `g.tx().commit()` and
`g.tx().rollback()`.
-
-IMPORTANT: Understand the transactional capabilities of the graph configured
in Gremlin Server. For example, a basic
-`TinkerGraph` does not support transactions. Use `TinkerTransactionGraph` or
another transaction-capable graph
-implementation. Attempting to begin a transaction on a non-transactional graph
will result in an error.
+Every traversal executes within a transaction. TinkerPop distinguishes between
two modes:
+
+* *Implicit transactions* are the default. Each traversal submitted through
`g` is a self-contained unit of work. All
+graphs participate in implicit transactions. When the graph supports explicit
transactions, implicit transactions are
+auto-committed on success and rolled back on error, providing atomicity. When
the graph does not support explicit
+transactions, writes are immediately durable and cannot be rolled back if a
failure occurs mid-traversal, potentially
+leaving the graph in a partially mutated state. In either case, a simple
`g.addV('person')` will be persisted on
+success without requiring an explicit `commit()`.
+* *Explicit transactions* are opened via `g.tx().begin()`, where the client
controls the lifecycle through
+`g.tx().commit()` and `g.tx().rollback()`. Multiple traversals can participate
in the same explicit transaction.
+
+IMPORTANT: Not all graph implementations support explicit transactions (for
example, basic `TinkerGraph` does not).
+Use `TinkerTransactionGraph` or another graph implementation that supports
explicit transactions. Attempting to begin
+an explicit transaction on a graph that does not support them will result in
an error.
Two settings in the Gremlin Server YAML control transaction resource usage:
diff --git a/docs/src/reference/the-traversal.asciidoc
b/docs/src/reference/the-traversal.asciidoc
index a081e209b3..ab49ae2614 100644
--- a/docs/src/reference/the-traversal.asciidoc
+++ b/docs/src/reference/the-traversal.asciidoc
@@ -87,11 +87,11 @@ try {
The above example is straightforward and represents a good starting point for
discussing the nuances of transactions
in relation to the usage convention and graph provider caveats alluded to
earlier.
-Focusing on remote contexts first, note that it is still possible to issue
traversals from `g`, but those will have a
-transaction scope outside of `gtx` and will simply behave as self-contained
units of work where the graph's own
-transaction semantics apply (i.e. one traversal is one transaction). Each
isolated transaction will require its own
-`Transaction` object. Multiple `begin()` calls on the same `Transaction`
object are not permitted and will throw an
-`IllegalStateException`:
+Focusing on remote contexts first, note that it is still possible to issue
traversals from `g`, but those will
+operate as implicit transactions (as opposed to the explicit transaction
opened by `gtx`) and simply behave as
+self-contained units of work (i.e. one traversal is one implicit transaction).
Each explicit transaction requires its
+own `Transaction` object. Multiple `begin()` calls on the same `Transaction`
object are not permitted and will throw
+an `IllegalStateException`:
[source,java]
----
diff --git
a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpGremlinEndpointHandler.java
b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpGremlinEndpointHandler.java
index f0cb2b9ea2..b520464d68 100644
---
a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpGremlinEndpointHandler.java
+++
b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpGremlinEndpointHandler.java
@@ -422,19 +422,30 @@ public class HttpGremlinEndpointHandler extends
SimpleChannelInboundHandler<Requ
}
final Bindings mergedBindings = mergeBindingsFromRequest(context, new
SimpleBindings(graphManager.getAsBindings()));
- final Object result = scriptEngine.eval(message.getGremlin(),
mergedBindings);
- final String bulkingSetting =
context.getChannelHandlerContext().channel().attr(StateKey.REQUEST_HEADERS).get().get(Tokens.BULK_RESULTS);
- // bulking only applies if it's gremlin-lang, and per request token
setting takes precedence over header setting.
- // The serializer check is temporarily needed because GraphSON hasn't
been removed yet and doesn't support bulking.
- final boolean bulking = language.equals("gremlin-lang") && serializer
instanceof GraphBinaryMessageSerializerV4 ?
- (args.containsKey(Tokens.BULK_RESULTS) ?
- Objects.equals(args.get(Tokens.BULK_RESULTS), "true") :
- Objects.equals(bulkingSetting, "true")) :
- false;
+ // resolve the graph for auto-transaction management on
non-transactional requests
+ final String g = message.getField(Tokens.ARGS_G);
+ final TraversalSource ts = g != null ?
graphManager.getTraversalSource(g) : null;
+ final Graph graph = ts != null ? ts.getGraph() : null;
+ final boolean autoCommit = (context.getTransactionId() == null) &&
(graph != null) &&
+ graph.features().graph().supportsTransactions();
+
+ // rollback any stale open transaction before processing
+ if (autoCommit && graph.tx().isOpen()) graph.tx().rollback();
Iterator itty = null;
try {
+ final Object result = scriptEngine.eval(message.getGremlin(),
mergedBindings);
+
+ final String bulkingSetting =
context.getChannelHandlerContext().channel().attr(StateKey.REQUEST_HEADERS).get().get(Tokens.BULK_RESULTS);
+ // bulking only applies if it's gremlin-lang, and per request
token setting takes precedence over header setting.
+ // The serializer check is temporarily needed because GraphSON
hasn't been removed yet and doesn't support bulking.
+ final boolean bulking = language.equals("gremlin-lang") &&
serializer instanceof GraphBinaryMessageSerializerV4 ?
+ (args.containsKey(Tokens.BULK_RESULTS) ?
+ Objects.equals(args.get(Tokens.BULK_RESULTS),
"true") :
+ Objects.equals(bulkingSetting, "true")) :
+ false;
+
if (bulking) {
// optimization for driver requests
((Traversal.Admin<?, ?>) result).applyStrategies();
@@ -444,7 +455,11 @@ public class HttpGremlinEndpointHandler extends
SimpleChannelInboundHandler<Requ
itty = IteratorUtils.asIterator(result);
handleIterator(context, itty, serializer, false);
}
- } catch (Exception ex) {
+
+ if (autoCommit && graph.tx().isOpen()) graph.tx().commit();
+ } catch (Throwable t) {
+ if (autoCommit && graph.tx().isOpen()) graph.tx().rollback();
+
// TINKERPOP-3144 ensure Traversals are closed when exception
thrown.
if (itty instanceof TraverserIterator) {
CloseableIterator.closeIterator(((TraverserIterator)
itty).getTraversal());
@@ -452,7 +467,7 @@ public class HttpGremlinEndpointHandler extends
SimpleChannelInboundHandler<Requ
CloseableIterator.closeIterator(itty);
}
- throw ex;
+ throw t;
}
}
diff --git
a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinDriverTransactionIntegrateTest.java
b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinDriverTransactionIntegrateTest.java
index 3cb37d06a1..af35cf4392 100644
---
a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinDriverTransactionIntegrateTest.java
+++
b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinDriverTransactionIntegrateTest.java
@@ -647,4 +647,31 @@ public class GremlinDriverTransactionIntegrateTest extends
AbstractGremlinServer
gtx.tx().rollback();
}
}
+
+ @Test
+ public void shouldAutoCommitNonTransactionalMutatingTraversal() throws
Exception {
+ final Client client = cluster.connect().alias(GTX);
+
+ // add a vertex without explicit transaction management - should be
auto-committed
+ client.submit("g.addV('person').property('name','alice')").all().get();
+
+ // verify persisted on a subsequent request
+ assertEquals(1L,
client.submit("g.V().hasLabel('person').count()").all().get().get(0).getLong());
+ }
+
+ @Test
+ public void shouldAutoRollbackOnFailedNonTransactionalMutatingTraversal()
throws Exception {
+ final Client client = cluster.connect().alias(GTX);
+
+ // submit a traversal that mutates then fails - should be rolled back
+ try {
+ client.submit("g.addV('person').fail()").all().get();
+ fail("Expected an exception");
+ } catch (Exception ex) {
+ // expected
+ }
+
+ // verify nothing was persisted
+ assertEquals(0L,
client.submit("g.V().hasLabel('person').count()").all().get().get(0).getLong());
+ }
}
diff --git
a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerHttpIntegrateTest.java
b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerHttpIntegrateTest.java
index 895b09b75c..7d724975b9 100644
---
a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerHttpIntegrateTest.java
+++
b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerHttpIntegrateTest.java
@@ -111,6 +111,8 @@ public class GremlinServerHttpIntegrateTest extends
AbstractGremlinServerIntegra
settings.maxRequestContentLength = 31;
break;
case "should200OnPOSTTransactionalGraph":
+ case "shouldRollbackOnFailedMutatingTraversal":
+ case "shouldCommitMutatingTraversalWithEmptyResult":
useTinkerTransactionGraph(settings);
break;
case "should200OnPOSTTransactionalGraphInStrictMode":
@@ -489,36 +491,97 @@ public class GremlinServerHttpIntegrateTest extends
AbstractGremlinServerIntegra
}
}
- /*@Test disabled for now as current implementation doesn't support
implicit transactions.
+ @Test
public void should200OnPOSTTransactionalGraph() throws Exception {
final CloseableHttpClient httpclient = HttpClients.createDefault();
+
+ // add a vertex without an explicit transaction - should be
auto-committed
final HttpPost httppost = new
HttpPost(TestClientFactory.createURLString());
httppost.addHeader("Content-Type", "application/json");
- httppost.setEntity(new
StringEntity("{\"gremlin\":\"graph.addVertex('name','stephen');g.V().count()\"}",
Consts.UTF_8));
+ httppost.setEntity(new StringEntity(
+
"{\"gremlin\":\"g.addV('person').property('name','stephen')\",\"g\":\"g\"}",
Consts.UTF_8));
try (final CloseableHttpResponse response =
httpclient.execute(httppost)) {
assertEquals(200, response.getStatusLine().getStatusCode());
- assertEquals("application/json",
response.getEntity().getContentType().getValue());
- final String json = EntityUtils.toString(response.getEntity());
- final JsonNode node = mapper.readTree(json);
- assertEquals(1,
node.get("result").get(TOKEN_DATA).get(GraphSONTokens.VALUEPROP).get(0).get(GraphSONTokens.VALUEPROP).intValue());
+ EntityUtils.consume(response.getEntity());
}
- final HttpGet httpget = new
HttpGet(TestClientFactory.createURLString("?gremlin=g.V().count()"));
- httpget.addHeader("Accept", "application/json");
+ // verify the vertex is visible on subsequent requests from
potentially different threads
+ final HttpPost countPost = new
HttpPost(TestClientFactory.createURLString());
+ countPost.addHeader("Content-Type", "application/json");
+ countPost.setEntity(new
StringEntity("{\"gremlin\":\"g.V().count()\",\"g\":\"g\"}", Consts.UTF_8));
- // execute this a bunch of times so that there's a good chance a
different thread on the server processes
- // the request
for (int ix = 0; ix < 100; ix++) {
- try (final CloseableHttpResponse response =
httpclient.execute(httpget)) {
+ try (final CloseableHttpResponse response =
httpclient.execute(countPost)) {
assertEquals(200, response.getStatusLine().getStatusCode());
- assertEquals("application/json",
response.getEntity().getContentType().getValue());
final String json = EntityUtils.toString(response.getEntity());
final JsonNode node = mapper.readTree(json);
- assertEquals(1,
node.get("result").get(TOKEN_DATA).get(GraphSONTokens.VALUEPROP).get(0).get(GraphSONTokens.VALUEPROP).intValue());
+ assertEquals(1, node.get("result").get(TOKEN_DATA)
+ .get(GraphSONTokens.VALUEPROP).get(0)
+ .get(GraphSONTokens.VALUEPROP).intValue());
}
}
- } */
+ }
+
+ @Test
+ public void shouldRollbackOnFailedMutatingTraversal() throws Exception {
+ final CloseableHttpClient httpclient = HttpClients.createDefault();
+
+ // submit a traversal that adds a vertex then fails - should be rolled
back
+ final HttpPost httppost = new
HttpPost(TestClientFactory.createURLString());
+ httppost.addHeader("Content-Type", "application/json");
+ httppost.setEntity(new
StringEntity("{\"gremlin\":\"g.addV('person').fail()\",\"g\":\"g\"}",
Consts.UTF_8));
+
+ try (final CloseableHttpResponse response =
httpclient.execute(httppost)) {
+ // the fail() error appears in the response body, not the HTTP
status
+ }
+
+ // verify the vertex was not persisted
+ final HttpPost countPost = new
HttpPost(TestClientFactory.createURLString());
+ countPost.addHeader("Content-Type", "application/json");
+ countPost.setEntity(new StringEntity(
+
"{\"gremlin\":\"g.V().hasLabel('person').count()\",\"g\":\"g\"}",
Consts.UTF_8));
+
+ try (final CloseableHttpResponse response =
httpclient.execute(countPost)) {
+ assertEquals(200, response.getStatusLine().getStatusCode());
+ final String json = EntityUtils.toString(response.getEntity());
+ final JsonNode node = mapper.readTree(json);
+ assertEquals(0, node.get("result").get(TOKEN_DATA)
+ .get(GraphSONTokens.VALUEPROP).get(0)
+ .get(GraphSONTokens.VALUEPROP).intValue());
+ }
+ }
+
+ @Test
+ public void shouldCommitMutatingTraversalWithEmptyResult() throws
Exception {
+ final CloseableHttpClient httpclient = HttpClients.createDefault();
+
+ // g.addV() followed by iterate-style consumption returns no results
but should still commit
+ final HttpPost httppost = new
HttpPost(TestClientFactory.createURLString());
+ httppost.addHeader("Content-Type", "application/json");
+ httppost.setEntity(new StringEntity(
+
"{\"gremlin\":\"g.addV('person').property('name','p').iterate()\",\"g\":\"g\"}",
Consts.UTF_8));
+
+ try (final CloseableHttpResponse response =
httpclient.execute(httppost)) {
+ assertEquals(200, response.getStatusLine().getStatusCode());
+ EntityUtils.consume(response.getEntity());
+ }
+
+ // verify the vertex was committed despite the empty result set
+ final HttpPost countPost2 = new
HttpPost(TestClientFactory.createURLString());
+ countPost2.addHeader("Content-Type", "application/json");
+ countPost2.setEntity(new StringEntity(
+
"{\"gremlin\":\"g.V().hasLabel('person').count()\",\"g\":\"g\"}",
Consts.UTF_8));
+
+ try (final CloseableHttpResponse response =
httpclient.execute(countPost2)) {
+ assertEquals(200, response.getStatusLine().getStatusCode());
+ final String json = EntityUtils.toString(response.getEntity());
+ final JsonNode node = mapper.readTree(json);
+ assertEquals(1, node.get("result").get(TOKEN_DATA)
+ .get(GraphSONTokens.VALUEPROP).get(0)
+ .get(GraphSONTokens.VALUEPROP).intValue());
+ }
+ }
@Test
public void should200OnPOSTTransactionalGraphInStrictMode() throws
Exception {