org.apache.tinkerpop.gremlin.server.handler.AbstractSession Maven / Gradle / Ivy
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.tinkerpop.gremlin.server.handler;
import com.codahale.metrics.Timer;
import groovy.lang.GroovyRuntimeException;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.tinkerpop.gremlin.driver.Client;
import org.apache.tinkerpop.gremlin.driver.MessageSerializer;
import org.apache.tinkerpop.gremlin.driver.Tokens;
import org.apache.tinkerpop.gremlin.driver.message.RequestMessage;
import org.apache.tinkerpop.gremlin.driver.message.ResponseMessage;
import org.apache.tinkerpop.gremlin.driver.message.ResponseStatusCode;
import org.apache.tinkerpop.gremlin.driver.ser.MessageTextSerializer;
import org.apache.tinkerpop.gremlin.groovy.engine.GremlinExecutor;
import org.apache.tinkerpop.gremlin.groovy.jsr223.TimedInterruptTimeoutException;
import org.apache.tinkerpop.gremlin.jsr223.GremlinScriptEngine;
import org.apache.tinkerpop.gremlin.jsr223.JavaTranslator;
import org.apache.tinkerpop.gremlin.process.traversal.Bytecode;
import org.apache.tinkerpop.gremlin.process.traversal.Failure;
import org.apache.tinkerpop.gremlin.process.traversal.GraphOp;
import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
import org.apache.tinkerpop.gremlin.process.traversal.TraversalSource;
import org.apache.tinkerpop.gremlin.process.traversal.strategy.verification.VerificationException;
import org.apache.tinkerpop.gremlin.process.traversal.util.BytecodeHelper;
import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalInterruptedException;
import org.apache.tinkerpop.gremlin.server.GraphManager;
import org.apache.tinkerpop.gremlin.server.GremlinServer;
import org.apache.tinkerpop.gremlin.server.Settings;
import org.apache.tinkerpop.gremlin.server.auth.AuthenticatedUser;
import org.apache.tinkerpop.gremlin.util.ExceptionHelper;
import org.apache.tinkerpop.gremlin.server.util.TraverserIterator;
import org.apache.tinkerpop.gremlin.structure.Graph;
import org.apache.tinkerpop.gremlin.structure.Transaction;
import org.apache.tinkerpop.gremlin.structure.util.TemporaryException;
import org.apache.tinkerpop.gremlin.util.iterator.IteratorUtils;
import org.codehaus.groovy.control.MultipleCompilationErrorsException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.script.Bindings;
import javax.script.ScriptException;
import javax.script.SimpleBindings;
import java.io.InterruptedIOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Stream;
import static org.apache.tinkerpop.gremlin.process.traversal.GraphOp.TX_COMMIT;
import static org.apache.tinkerpop.gremlin.process.traversal.GraphOp.TX_ROLLBACK;
/**
* A base implementation of {@link Session} which offers some common functionality that matches typical Gremlin Server
* request response expectations for script, bytecode and graph operations. The class is designed to be extended but
* take care in understanding the way that different methods are called as they do depend on one another a bit. It
* maybe best to examine the source code to determine how best to use this class or to extend from the higher order
* classes of {@link SingleTaskSession} or {@link MultiTaskSession}.
*/
public abstract class AbstractSession implements Session, AutoCloseable {
private static final Logger logger = LoggerFactory.getLogger(AbstractSession.class);
private static final Logger auditLogger = LoggerFactory.getLogger(GremlinServer.AUDIT_LOGGER_NAME);
private final boolean sessionIdOnRequest;
private final Channel initialChannel;
private final boolean transactionManaged;
private final String sessionId;
private final AtomicReference> sessionCancelFuture = new AtomicReference<>();
private final AtomicReference> sessionFuture = new AtomicReference<>();
private long actualTimeoutLengthWhenClosed = 0;
/**
* The session thread is a reference to the thread that is running the session and should be set by an
* implementation as the first line of the {@link #run()} method.
*/
protected Thread sessionThread;
protected final boolean maintainStateAfterException;
protected final AtomicReference closeReason = new AtomicReference<>();
protected final GraphManager graphManager;
protected final ConcurrentMap sessions;
protected final Set aliasesUsedBySession = new HashSet<>();
protected final AtomicBoolean sessionTaskStarted = new AtomicBoolean(false);
/**
* The reason that a particular session closed. The reason for the close is generally not important as a
* final disposition for the {@link Session} instance and is more useful in aiding flow control during the
* close process.
*/
protected enum CloseReason {
/**
* The session exits in a fashion that did not precipitate from some form of interruption, timeout or
* exception, i.e. it is simply allowed to process to an exit through its normal execution flow. This status
* may or may not be possible given the context of the implementation. For example, a {@link MultiTaskSession}
* needs to be interrupted to stop processing.
*/
EXIT_PROCESSING,
/**
* The session was interrupted by the channel closing, which can be something initiated by closing the
* {@link Client} or might be triggered by the server. This may not be considered an error situation and
* depending on context, might be similar to a {@link #EXIT_PROCESSING} termination.
*/
CHANNEL_CLOSED,
/**
* The session encountered an exception related to execution like a script error, traversal iteration problem,
* serialization issue, etc.
*/
PROCESSING_EXCEPTION,
/**
* The session was interrupted by the session lifetime timeout.
*/
SESSION_TIMEOUT,
/**
* The session was interrupted by the request timeout.
*/
REQUEST_TIMEOUT
}
AbstractSession(final SessionTask sessionTask, final String sessionId,
final boolean transactionManaged,
final ConcurrentMap sessions) {
// this only applies to sessions
this.maintainStateAfterException = (boolean) sessionTask.getRequestMessage().
optionalArgs(Tokens.ARGS_MAINTAIN_STATE_AFTER_EXCEPTION).orElse(false);
this.sessionIdOnRequest = sessionTask.getRequestMessage().optionalArgs(Tokens.ARGS_SESSION).isPresent();
this.transactionManaged = transactionManaged;
this.sessionId = sessionId;
this.initialChannel = sessionTask.getChannelHandlerContext().channel();
// close session if the channel closes to cleanup and close transactions
this.initialChannel.closeFuture().addListener(f -> {
if (closeReason.compareAndSet(null, CloseReason.CHANNEL_CLOSED)) {
close();
}
});
this.sessions = sessions;
this.graphManager = sessionTask.getGraphManager();
}
protected synchronized void cancel(final boolean mayInterruptIfRunning) {
final FutureTask> sf = (FutureTask) sessionFuture.get();
if (sf != null && !sf.isDone()) {
sf.cancel(mayInterruptIfRunning);
if (!sessionTaskStarted.get()) {
sendTimeoutResponseForUncommencedTask();
}
}
}
public boolean isTransactionManaged() {
return transactionManaged;
}
@Override
public String getSessionId() {
return sessionId;
}
public boolean isBoundTo(final Channel channel) {
return channel == initialChannel;
}
public long getActualTimeoutLengthWhenClosed() {
return actualTimeoutLengthWhenClosed;
}
public Optional getCloseReason() {
return Optional.ofNullable(closeReason.get());
}
/**
* Gets the script engine from the cached one in the {@link GremlinExecutor}.
*/
public GremlinScriptEngine getScriptEngine(final SessionTask sessionTask, final String language) {
return sessionTask.getGremlinExecutor().getScriptEngineManager().getEngineByName(language);
}
/**
* Respond to the client with the specific timeout response for this Session implementation.
* This is for situations where the Session hasn't started running.
*/
protected abstract void sendTimeoutResponseForUncommencedTask();
@Override
public void setSessionCancelFuture(final ScheduledFuture> f) {
if (!sessionCancelFuture.compareAndSet(null, f))
throw new IllegalStateException("Session cancellation future is already set");
}
@Override
public void setSessionFuture(final Future> f) {
if (!sessionFuture.compareAndSet(null, f))
throw new IllegalStateException("Session future is already set");
}
@Override
public synchronized void triggerTimeout(final long timeout, final boolean causedBySession) {
// triggering timeout triggers the stop of the session which will end in close()
// for final cleanup
final Future> f = sessionFuture.get();
if (f != null && !f.isDone()) {
if (closeReason.compareAndSet(null, causedBySession ? CloseReason.SESSION_TIMEOUT : CloseReason.REQUEST_TIMEOUT)) {
actualTimeoutLengthWhenClosed = timeout;
// if caused by a session timeout for a MultiTaskSession OR if it is a request timeout for a
// SingleTaskSession request then we can just straight cancel() the session instance
if (causedBySession || !sessionIdOnRequest)
cancel(true);
else {
// in both MultiTaskSession and SingleTaskSession the thread gets set immediately at the start
// of run() as it should (though "single" has little need for it). As triggerTimeout() for a
// request MultiTaskSession can only be called AFTER we are deep in the run() there should be
// no chance of race conditions or situations where the sessionThread is null at this point.
if (sessionThread != null) {
sessionThread.interrupt();
} else {
logger.debug("{} is a {} which cannot be interrupted as the thread running the session has not " +
"been set - please check the implementation if this is not desirable",
sessionId, this.getClass().getSimpleName());
}
}
}
}
}
protected void process(final SessionTask sessionTask) throws SessionException {
final RequestMessage msg = sessionTask.getRequestMessage();
final Map args = msg.getArgs();
final Object gremlinToExecute = args.get(Tokens.ARGS_GREMLIN);
// for strict transactions track the aliases used so that we can commit them and only them on close()
if (sessionTask.getSettings().strictTransactionManagement)
msg.optionalArgs(Tokens.ARGS_ALIASES).ifPresent(m -> aliasesUsedBySession.addAll(((Map) m).values()));
final Timer.Context timer = getMetricsTimer(sessionTask);
try {
// itty is optional as Bytecode could be a "graph operation" rather than a Traversal. graph operations
// don't need to be iterated and handle their own lifecycle
final Optional> itty = gremlinToExecute instanceof Bytecode ?
fromBytecode(sessionTask, (Bytecode) gremlinToExecute) :
Optional.of(fromScript(sessionTask, (String) gremlinToExecute));
processAuditLog(sessionTask.getSettings(), sessionTask.getChannelHandlerContext(), gremlinToExecute);
if (itty.isPresent())
handleIterator(sessionTask, itty.get());
} catch (Throwable t) {
handleException(sessionTask, t);
} finally {
timer.stop();
}
}
protected void handleException(final SessionTask sessionTask, final Throwable t) throws SessionException {
if (t instanceof SessionException) throw (SessionException) t;
final Optional possibleSpecialException = determineIfSpecialException(t);
if (possibleSpecialException.isPresent()) {
final Throwable special = possibleSpecialException.get();
final ResponseMessage.Builder specialResponseMsg = ResponseMessage.build(sessionTask.getRequestMessage()).
statusMessage(special.getMessage()).
statusAttributeException(special);
if (special instanceof TemporaryException) {
specialResponseMsg.code(ResponseStatusCode.SERVER_ERROR_TEMPORARY);
} else if (special instanceof Failure) {
final Failure failure = (Failure) special;
specialResponseMsg.code(ResponseStatusCode.SERVER_ERROR_FAIL_STEP).
statusAttribute(Tokens.STATUS_ATTRIBUTE_FAIL_STEP_MESSAGE, failure.format());
}
throw new SessionException(special.getMessage(), specialResponseMsg.create());
}
final Throwable root = ExceptionHelper.getRootCause(t);
if (root instanceof TimedInterruptTimeoutException) {
// occurs when the TimedInterruptCustomizerProvider is in play
final String msg = String.format("A timeout occurred within the script during evaluation of [%s] - consider increasing the limit given to TimedInterruptCustomizerProvider",
sessionTask.getRequestMessage().getRequestId());
throw new SessionException(msg, root, ResponseMessage.build(sessionTask.getRequestMessage())
.code(ResponseStatusCode.SERVER_ERROR_TIMEOUT)
.statusMessage("Timeout during script evaluation triggered by TimedInterruptCustomizerProvider")
.create());
}
if (root instanceof TimeoutException) {
final String errorMessage = String.format("Script evaluation exceeded the configured threshold for request [%s]",
sessionTask.getRequestMessage().getRequestId());
throw new SessionException(errorMessage, root, ResponseMessage.build(sessionTask.getRequestMessage())
.code(ResponseStatusCode.SERVER_ERROR_TIMEOUT)
.statusMessage(t.getMessage())
.create());
}
if (root instanceof InterruptedException ||
root instanceof TraversalInterruptedException ||
root instanceof InterruptedIOException) {
String msg = "Processing interrupted but the reason why was not known";
switch (closeReason.get()) {
case CHANNEL_CLOSED:
msg = "Processing interrupted because the channel was closed";
break;
case SESSION_TIMEOUT:
msg = String.format("Session closed - %s - sessionLifetimeTimeout of %s ms exceeded", sessionId, actualTimeoutLengthWhenClosed);
break;
case REQUEST_TIMEOUT:
msg = String.format("Evaluation exceeded timeout threshold of %s ms", actualTimeoutLengthWhenClosed);
break;
}
final ResponseStatusCode code = closeReason.get() == CloseReason.SESSION_TIMEOUT || closeReason.get() == CloseReason.REQUEST_TIMEOUT ?
ResponseStatusCode.SERVER_ERROR_TIMEOUT : ResponseStatusCode.SERVER_ERROR;
throw new SessionException(msg, root, ResponseMessage.build(sessionTask.getRequestMessage())
.code(code)
.statusMessage(msg).create());
}
if (root instanceof MultipleCompilationErrorsException && root.getMessage().contains("Method too large") &&
((MultipleCompilationErrorsException) root).getErrorCollector().getErrorCount() == 1) {
final String errorMessage = String.format("The Gremlin statement that was submitted exceeds the maximum compilation size allowed by the JVM, please split it into multiple smaller statements - %s", trimMessage(sessionTask.getRequestMessage()));
logger.warn(errorMessage);
throw new SessionException(errorMessage, root, ResponseMessage.build(sessionTask.getRequestMessage())
.code(ResponseStatusCode.SERVER_ERROR_EVALUATION)
.statusMessage(errorMessage)
.statusAttributeException(root).create());
}
// GroovyRuntimeException will hit a pretty wide range of eval type errors, like MissingPropertyException,
// CompilationFailedException, MissingMethodException, etc. If more specific handling is required then
// try to catch it earlier above.
if (root instanceof GroovyRuntimeException ||
root instanceof VerificationException ||
root instanceof ScriptException) {
throw new SessionException(root.getMessage(), root, ResponseMessage.build(sessionTask.getRequestMessage())
.code(ResponseStatusCode.SERVER_ERROR_EVALUATION)
.statusMessage(root.getMessage())
.statusAttributeException(root).create());
}
throw new SessionException(root.getClass().getSimpleName() + ": " + root.getMessage(), root,
ResponseMessage.build(sessionTask.getRequestMessage())
.code(ResponseStatusCode.SERVER_ERROR)
.statusAttributeException(root)
.statusMessage(root.getMessage()).create());
}
/**
* Used to decrease the size of a Gremlin script that triggered a "method too large" exception so that it
* doesn't log a massive text string nor return a large error message.
*/
private RequestMessage trimMessage(final RequestMessage msg) {
final RequestMessage trimmedMsg = RequestMessage.from(msg).create();
if (trimmedMsg.getArgs().containsKey(Tokens.ARGS_GREMLIN))
trimmedMsg.getArgs().put(Tokens.ARGS_GREMLIN, trimmedMsg.getArgs().get(Tokens.ARGS_GREMLIN).toString().substring(0, 1021) + "...");
return trimmedMsg;
}
/**
* Check if any exception in the chain is {@link TemporaryException} or {@link Failure} then respond with the
* right error code so that the client knows to retry.
*/
protected static Optional determineIfSpecialException(final Throwable ex) {
return Stream.of(ExceptionUtils.getThrowables(ex)).
filter(i -> i instanceof TemporaryException || i instanceof Failure).findFirst();
}
/**
* Removes the session from the session list and cancels the future that manages the lifetime of the session.
*/
@Override
public synchronized void close() {
// already closing/closed
if (!sessions.containsKey(sessionId)) return;
sessions.remove(sessionId);
if (sessionCancelFuture.get() != null) {
final ScheduledFuture> f = sessionCancelFuture.get();
if (!f.isDone()) f.cancel(true);
}
}
/**
* Constructs an {@code Iterator} from the results of a script evaluation provided in the {@link SessionTask}.
*
* @param sessionTask The session task which can be used as a context in constructing the {@code Iterator}
* @param script The script extracted by the calling method from the {@code sessionTask}
*/
protected Iterator> fromScript(final SessionTask sessionTask, final String script) throws Exception {
final RequestMessage msg = sessionTask.getRequestMessage();
final Map args = msg.getArgs();
final String language = args.containsKey(Tokens.ARGS_LANGUAGE) ? (String) args.get(Tokens.ARGS_LANGUAGE) : "gremlin-groovy";
return IteratorUtils.asIterator(getScriptEngine(sessionTask, language).eval(
script, mergeBindingsFromRequest(sessionTask, getWorkerBindings())));
}
/**
* Constructs an {@code Iterator} from {@link Bytecode} provided in the {@link SessionTask}. If the {@link Bytecode}
* is found to evalute to a {@link GraphOp} then it is processed and an empty {@code Optional} is returned.
*
* @param sessionTask The session task which can be used as a context in constructing the {@code Iterator}
* @param bytecode The {@link Bytecode} extracted by the calling method from the {@code sessionTask}
*/
protected Optional> fromBytecode(final SessionTask sessionTask, final Bytecode bytecode) throws Exception {
final RequestMessage msg = sessionTask.getRequestMessage();
final Traversal.Admin, ?> traversal;
final Map aliases = (Map) msg.optionalArgs(Tokens.ARGS_ALIASES).get();
final GraphManager graphManager = sessionTask.getGraphManager();
final String traversalSourceName = aliases.entrySet().iterator().next().getValue();
final TraversalSource g = graphManager.getTraversalSource(traversalSourceName);
// handle bytecode based graph operations like commit/rollback commands
if (BytecodeHelper.isGraphOperation(bytecode)) {
handleGraphOperation(sessionTask, bytecode, g.getGraph());
return Optional.empty();
} else {
final Optional lambdaLanguage = BytecodeHelper.getLambdaLanguage(bytecode);
if (!lambdaLanguage.isPresent())
traversal = JavaTranslator.of(g).translate(bytecode);
else {
final SimpleBindings bindings = new SimpleBindings();
bindings.put(traversalSourceName, g);
traversal = sessionTask.getGremlinExecutor().getScriptEngineManager().
getEngineByName(lambdaLanguage.get()).eval(bytecode, bindings, traversalSourceName);
}
// compile the traversal - without it getEndStep() has nothing in it
traversal.applyStrategies();
return Optional.of(new TraverserIterator(traversal));
}
}
protected Bindings getWorkerBindings() throws SessionException {
return new SimpleBindings(graphManager.getAsBindings());
}
protected Bindings mergeBindingsFromRequest(final SessionTask sessionTask, final Bindings bindings) throws SessionException {
// alias any global bindings to a different variable.
final RequestMessage msg = sessionTask.getRequestMessage();
if (msg.getArgs().containsKey(Tokens.ARGS_ALIASES)) {
final Map aliases = (Map) msg.getArgs().get(Tokens.ARGS_ALIASES);
for (Map.Entry aliasKv : aliases.entrySet()) {
boolean found = false;
// first check if the alias refers to a Graph instance
final Graph graph = sessionTask.getGraphManager().getGraph(aliasKv.getValue());
if (null != graph) {
bindings.put(aliasKv.getKey(), graph);
found = true;
}
// if the alias wasn't found as a Graph then perhaps it is a TraversalSource - it needs to be
// something
if (!found) {
final TraversalSource ts = sessionTask.getGraphManager().getTraversalSource(aliasKv.getValue());
if (null != ts) {
bindings.put(aliasKv.getKey(), ts);
found = true;
}
}
// this validation is important to calls to GraphManager.commit() and rollback() as they both
// expect that the aliases supplied are valid
if (!found) {
final String error = String.format("Could not alias [%s] to [%s] as [%s] not in the Graph or TraversalSource global bindings",
aliasKv.getKey(), aliasKv.getValue(), aliasKv.getValue());
throw new SessionException(error, ResponseMessage.build(msg)
.code(ResponseStatusCode.REQUEST_ERROR_INVALID_REQUEST_ARGUMENTS).statusMessage(error).create());
}
}
} else {
// there's no bindings so determine if that's ok with Gremlin Server
if (sessionTask.getSettings().strictTransactionManagement) {
final String error = "Gremlin Server is configured with strictTransactionManagement as 'true' - the 'aliases' arguments must be provided";
throw new SessionException(error, ResponseMessage.build(msg)
.code(ResponseStatusCode.REQUEST_ERROR_INVALID_REQUEST_ARGUMENTS).statusMessage(error).create());
}
}
// add any bindings to override any other supplied
Optional.ofNullable((Map) msg.getArgs().get(Tokens.ARGS_BINDINGS)).ifPresent(bindings::putAll);
return bindings;
}
/**
* Provides a generic way of iterating a result set back to the client.
*
* @param sessionTask The Gremlin Server {@link SessionTask} object containing settings, request message, etc.
* @param itty The result to iterator
*/
protected void handleIterator(final SessionTask sessionTask, final Iterator> itty) throws InterruptedException {
final ChannelHandlerContext nettyContext = sessionTask.getChannelHandlerContext();
final RequestMessage msg = sessionTask.getRequestMessage();
final Settings settings = sessionTask.getSettings();
boolean warnOnce = false;
// sessionless requests are always transaction managed, but in-session requests are configurable.
final boolean managedTransactionsForRequest = transactionManaged ?
true : (Boolean) msg.getArgs().getOrDefault(Tokens.ARGS_MANAGE_TRANSACTION, false);
// we have an empty iterator - happens on stuff like: g.V().iterate()
if (!itty.hasNext()) {
final Map attributes = generateStatusAttributes(sessionTask,ResponseStatusCode.NO_CONTENT, itty);
// as there is nothing left to iterate if we are transaction managed then we should execute a
// commit here before we send back a NO_CONTENT which implies success
if (managedTransactionsForRequest)
closeTransaction(sessionTask, Transaction.Status.COMMIT);
sessionTask.writeAndFlush(ResponseMessage.build(msg)
.code(ResponseStatusCode.NO_CONTENT)
.statusAttributes(attributes)
.create());
return;
}
// the batch size can be overridden by the request
final int resultIterationBatchSize = (Integer) msg.optionalArgs(Tokens.ARGS_BATCH_SIZE)
.orElse(settings.resultIterationBatchSize);
List
© 2015 - 2025 Weber Informatics LLC | Privacy Policy