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 d43ef50b59b4a2daf488d108f4f6f4eb9b98fadd Author: Ken Hu <[email protected]> AuthorDate: Tue Jun 9 20:06:54 2026 -0700 Centralize transaction request classification in Context CTR The match is now case-sensitive to stay consistent with how the grammar parses the same string. Assisted-by: Claude Code:claude-opus-4-8 --- .../apache/tinkerpop/gremlin/server/Context.java | 58 +++++++++++++++++++++- .../server/handler/HttpGremlinEndpointHandler.java | 37 +++----------- 2 files changed, 63 insertions(+), 32 deletions(-) diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/Context.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/Context.java index 10148abe08..c7c5a610e3 100644 --- a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/Context.java +++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/Context.java @@ -53,6 +53,7 @@ public class Context { private final long requestTimeout; private final String materializeProperties; private final Object gremlinArgument; + private final RequestType requestType; private HttpGremlinEndpointHandler.RequestState requestState; private final AtomicBoolean startedResponse = new AtomicBoolean(false); private ScheduledFuture<?> timeoutExecutor = null; @@ -80,7 +81,9 @@ public class Context { this.scheduledExecutorService = scheduledExecutorService; // order of calls matter as one depends on the next - this.gremlinArgument = requestMessage.getGremlin(); + final String gremlin = requestMessage.getGremlin(); + this.gremlinArgument = gremlin; + this.requestType = RequestType.fromGremlin(gremlin); this.requestState = requestState; this.requestTimeout = determineTimeout(); this.materializeProperties = determineMaterializeProperties(); @@ -128,6 +131,27 @@ public class Context { return transactionId; } + /** + * Returns {@code true} if this request is a transaction begin control command. + */ + public boolean isTransactionBegin() { + return requestType == RequestType.BEGIN_TX; + } + + /** + * Returns {@code true} if this request is a transaction commit control command. + */ + public boolean isTransactionCommit() { + return requestType == RequestType.COMMIT_TX; + } + + /** + * Returns {@code true} if this request is a transaction rollback control command. + */ + public boolean isTransactionRollback() { + return requestType == RequestType.ROLLBACK_TX; + } + public void setTransactionId(final String transactionId) { this.transactionId = transactionId; } @@ -233,4 +257,36 @@ public class Context { public void setRequestState(HttpGremlinEndpointHandler.RequestState requestState) { this.requestState = requestState; } + + /** + * Classifies an HTTP request by the kind of work it performs. Transaction control requests reuse the canonical + * Gremlin idioms ({@code g.tx().begin()} etc.) as protocol signals rather than evaluating them, because the + * ThreadLocal-bound nature of graph transactions forces the server to route the underlying graph operation onto + * a dedicated per-transaction thread instead of the shared eval path. The match is therefore against fixed, + * case-sensitive tokens — the same spelling the script engine would accept — not a grammar parse. + */ + private enum RequestType { + BEGIN_TX, + COMMIT_TX, + ROLLBACK_TX, + EVAL; + + private static final String BEGIN = "g.tx().begin()"; + private static final String COMMIT = "g.tx().commit()"; + private static final String ROLLBACK = "g.tx().rollback()"; + + /** + * Classifies a gremlin string into a {@link RequestType}. Leading/trailing whitespace is tolerated, but the + * token match is case-sensitive so that the routing decision cannot disagree with how the script engine would + * parse the same string. + */ + private static RequestType fromGremlin(final String gremlin) { + if (gremlin == null) return EVAL; + final String trimmed = gremlin.trim(); + if (trimmed.equals(BEGIN)) return BEGIN_TX; + if (trimmed.equals(COMMIT)) return COMMIT_TX; + if (trimmed.equals(ROLLBACK)) return ROLLBACK_TX; + return EVAL; + } + } } 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 390dd85d05..40f0d2b79f 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 @@ -218,8 +218,7 @@ public class HttpGremlinEndpointHandler extends SimpleChannelInboundHandler<Requ // These guards prevent any obvious failures from returning 200 OK early by detecting them here and // throwing before any other processing starts so the user gets a better error code. final String txId = requestCtx.getTransactionId(); - final String gremlin = requestMessage.getGremlin(); - if (isTransactionBegin(gremlin)) { + if (requestCtx.isTransactionBegin()) { // If this is a begin transaction request then we need to create the Transaction ID first since the // dual-transmission expectation means the response header below should contain it. @@ -244,7 +243,7 @@ public class HttpGremlinEndpointHandler extends SimpleChannelInboundHandler<Requ if ((!g.tx().isOpen())) { throw new ProcessingException(GremlinError.transactionNotFound(txId)); } - } else if ((txId == null) && (isTransactionCommit(gremlin) || isTransactionRollback(gremlin))) { + } else if ((txId == null) && (requestCtx.isTransactionCommit() || requestCtx.isTransactionRollback())) { // Logically, commit/rollback should only be allowed on a transactional request. throw new ProcessingException(GremlinError.transactionalControlRequiresTransaction()); } @@ -309,7 +308,7 @@ public class HttpGremlinEndpointHandler extends SimpleChannelInboundHandler<Requ }); try { - final boolean isBeginTransactionRequest = isTransactionBegin(requestMessage.getGremlin()); + final boolean isBeginTransactionRequest = requestCtx.isTransactionBegin(); final Future<?> executionFuture = ((requestCtx.getTransactionId() != null) && !isBeginTransactionRequest) ? transactionManager.get(requestCtx.getTransactionId()).get().submit(evalFuture) : requestCtx.getGremlinExecutor().getExecutorService().submit(evalFuture); @@ -355,11 +354,11 @@ public class HttpGremlinEndpointHandler extends SimpleChannelInboundHandler<Requ // Early guard against fake or incorrect transaction IDs. if ((txId != null) && transaction.isEmpty()) throw new ProcessingException(GremlinError.transactionNotFound(txId)); - if (isTransactionBegin(request.getGremlin())) { + if (requestContext.isTransactionBegin()) { runBegin(requestContext, transaction.get(), serializer); - } else if (isTransactionCommit(request.getGremlin())) { + } else if (requestContext.isTransactionCommit()) { handleGraphOp(requestContext, txId, Transaction::commit, serializer); - } else if (isTransactionRollback(requestContext.getRequestMessage().getGremlin())) { + } else if (requestContext.isTransactionRollback()) { handleGraphOp(requestContext, txId, Transaction::rollback, serializer); } else { // Both transactional and non-transactional traversals follow this path for response chunking. @@ -491,30 +490,6 @@ public class HttpGremlinEndpointHandler extends SimpleChannelInboundHandler<Requ } } - /** - * Detects if the gremlin script is a transaction begin command. - */ - private boolean isTransactionBegin(final String gremlin) { - if (gremlin == null) return false; - return gremlin.trim().equalsIgnoreCase("g.tx().begin()"); - } - - /** - * Detects if the gremlin script is a transaction commit command. - */ - private boolean isTransactionCommit(final String gremlin) { - if (gremlin == null) return false; - return gremlin.trim().equalsIgnoreCase("g.tx().commit()"); - } - - /** - * Detects if the gremlin script is a transaction rollback command. - */ - private boolean isTransactionRollback(final String gremlin) { - if (gremlin == null) return false; - return gremlin.trim().equalsIgnoreCase("g.tx().rollback()"); - } - /** * Handle begin by creating an {@link UnmanagedTransaction} and submitting the open to its executor. */
