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
commit 9bc27d1d095b7daa8e7ce896ce2468f67bb67b45 Author: Ken Hu <[email protected]> AuthorDate: Tue Jun 9 10:04:54 2026 -0700 Improve error for divergent transaction "g" value CTR Assisted-by: Claude Code:claude-opus-4-6 --- .../server/handler/HttpGremlinEndpointHandler.java | 11 ++++++++++- .../server/transaction/TransactionManager.java | 5 +++-- .../server/transaction/UnmanagedTransaction.java | 11 +++++++++++ .../tinkerpop/gremlin/server/util/GremlinError.java | 15 +++++++++++++++ .../GremlinServerHttpTransactionIntegrateTest.java | 19 ++++++++++++++----- 5 files changed, 53 insertions(+), 8 deletions(-) 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 8bc6e79c3e..0c64aca813 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 @@ -228,10 +228,19 @@ public class HttpGremlinEndpointHandler extends SimpleChannelInboundHandler<Requ doBegin(requestCtx); } else if (txId != null) { + final String requestedSource = requestMessage.getField(Tokens.ARGS_G); + final Optional<UnmanagedTransaction> txOpt = transactionManager.get(txId); + if (txOpt.isEmpty()) { + throw new ProcessingException(GremlinError.transactionNotFound(txId)); + } + final UnmanagedTransaction tx = txOpt.get(); + if (!tx.getTraversalSourceName().equals(requestedSource)) { + throw new ProcessingException(GremlinError.traversalSourceMismatch(tx.getTraversalSourceName(), requestedSource)); + } // This check makes sure that the underlying Graph is already open to stop a closed transaction // from re-opening due to the default autostart nature of transactions. This occurs in cases where a // transactional traversal is submitted after a commit/rollback. - final Graph g = graphManager.getTraversalSource(requestMessage.getField(Tokens.ARGS_G)).getGraph(); + final Graph g = graphManager.getTraversalSource(requestedSource).getGraph(); if ((!g.tx().isOpen())) { throw new ProcessingException(GremlinError.transactionNotFound(txId)); } diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/transaction/TransactionManager.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/transaction/TransactionManager.java index 6c4e8b0f76..6901664c15 100644 --- a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/transaction/TransactionManager.java +++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/transaction/TransactionManager.java @@ -93,7 +93,7 @@ public class TransactionManager { throw Graph.Exceptions.transactionsNotSupported(); } - final UnmanagedTransaction txCtx = createTransactionContext(ts.getGraph()); + final UnmanagedTransaction txCtx = createTransactionContext(traversalSourceName, ts.getGraph()); logger.debug("Transaction {} created for source {}", txCtx.getTransactionId(), traversalSourceName); return txCtx; } @@ -111,7 +111,7 @@ public class TransactionManager { * Creates a unique transaction ID, retrying on the unlikely UUID collision. The newly created * {@link UnmanagedTransaction} is inserted into the transactions map. */ - private UnmanagedTransaction createTransactionContext(final Graph graph) { + private UnmanagedTransaction createTransactionContext(final String traversalSourceName, final Graph graph) { String txId; UnmanagedTransaction ctx; @@ -120,6 +120,7 @@ public class TransactionManager { ctx = new UnmanagedTransaction( txId, this, + traversalSourceName, graph, scheduledExecutorService, transactionTimeoutMs, diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/transaction/UnmanagedTransaction.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/transaction/UnmanagedTransaction.java index 0b98b9b846..c3d3b8a274 100644 --- a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/transaction/UnmanagedTransaction.java +++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/transaction/UnmanagedTransaction.java @@ -47,6 +47,7 @@ public class UnmanagedTransaction { private static final Logger logger = LoggerFactory.getLogger(UnmanagedTransaction.class); private final String transactionId; + private final String traversalSourceName; private final TransactionManager manager; private final Graph graph; private final ScheduledExecutorService scheduledExecutorService; @@ -66,18 +67,21 @@ public class UnmanagedTransaction { * * @param transactionId The unique identifier for this transaction * @param transactionManager The manager that owns this transaction's lifecycle + * @param traversalSourceName The traversal source name bound at begin time * @param graph The graph instance for this transaction * @param scheduledExecutorService Scheduler for timeout management * @param transactionTimeout Timeout in milliseconds before auto-rollback */ public UnmanagedTransaction(final String transactionId, final TransactionManager transactionManager, + final String traversalSourceName, final Graph graph, final ScheduledExecutorService scheduledExecutorService, final long transactionTimeout, final long perGraphClose) { logger.debug("New transaction context established for {}", transactionId); this.transactionId = transactionId; + this.traversalSourceName = traversalSourceName; this.manager = transactionManager; this.graph = graph; this.scheduledExecutorService = scheduledExecutorService; @@ -96,6 +100,13 @@ public class UnmanagedTransaction { return transactionId; } + /** + * Returns the traversal source name bound at begin time. + */ + public String getTraversalSourceName() { + return traversalSourceName; + } + /** * Resets the timeout for this transaction. Called on each request. */ diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/util/GremlinError.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/util/GremlinError.java index cbdfa36de8..e0e74c26d0 100644 --- a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/util/GremlinError.java +++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/util/GremlinError.java @@ -250,4 +250,19 @@ public class GremlinError { final String message = String.format("Script engine [%s] is not available.", language); return new GremlinError(HttpResponseStatus.BAD_REQUEST, message, "InvalidRequestException"); } + + /** + * Creates an error for when a transactional request uses a different traversal source + * name than the one bound at begin time. + * + * @param expected The traversal source name bound at transaction begin + * @param actual The traversal source name in the current request + * @return A GremlinError with appropriate message and status code + */ + public static GremlinError traversalSourceMismatch(final String expected, final String actual) { + final String message = String.format( + "Transaction traversal source alias mismatch: transaction bound to '%s' but request used '%s'", + expected, actual); + return new GremlinError(HttpResponseStatus.BAD_REQUEST, message, "TransactionException"); + } } diff --git a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerHttpTransactionIntegrateTest.java b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerHttpTransactionIntegrateTest.java index 8aa919cad8..156b2773dd 100644 --- a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerHttpTransactionIntegrateTest.java +++ b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerHttpTransactionIntegrateTest.java @@ -105,6 +105,15 @@ public class GremlinServerHttpTransactionIntegrateTest extends AbstractGremlinSe case "shouldRollbackAbandonedTransaction": settings.transactionTimeout = 300; break; + case "shouldRejectMismatchedGraphAliasInTransaction": { + final Settings.GraphSettings gs = new Settings.GraphSettings(); + gs.configuration = "conf/tinkertransactiongraph-empty.properties"; + final Settings.TraversalSourceSettings ts = new Settings.TraversalSourceSettings(); + ts.name = "gTxEmpty"; + gs.traversalSources.add(ts); + settings.graphs.put("tx2", gs); + break; + } } return settings; } @@ -517,11 +526,11 @@ public class GremlinServerHttpTransactionIntegrateTest extends AbstractGremlinSe public void shouldRejectMismatchedGraphAliasInTransaction() throws Exception { final String txId = beginTx(client, GTX); - // send a request with the same txId but a different graph alias - try (final CloseableHttpResponse r = submitInTx(client, txId, "g.V().count()", "gclassic")) { - final int status = r.getStatusLine().getStatusCode(); - assertTrue("Expected error status for alias mismatch, got " + status, - status == 400 || status == 404 || status == 500); + // send a request with the same txId but a different transactional graph alias + try (final CloseableHttpResponse r = submitInTx(client, txId, "g.V().count()", "gTxEmpty")) { + assertEquals(400, r.getStatusLine().getStatusCode()); + final String msg = extractStatusMessage(r); + assertTrue(msg.contains("alias")); } }
