ai.grakn.engine.controller.GraqlController Maven / Gradle / Ivy
/*
* Grakn - A Distributed Semantic Database
* Copyright (C) 2016-2018 Grakn Labs Limited
*
* Grakn is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Grakn is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Grakn. If not, see .
*/
package ai.grakn.engine.controller;
import ai.grakn.GraknTx;
import ai.grakn.GraknTxType;
import ai.grakn.Keyspace;
import ai.grakn.engine.controller.response.ExplanationBuilder;
import ai.grakn.engine.controller.util.Requests;
import ai.grakn.engine.factory.EngineGraknTxFactory;
import ai.grakn.engine.task.postprocessing.PostProcessor;
import ai.grakn.exception.GraknTxOperationException;
import ai.grakn.exception.GraqlQueryException;
import ai.grakn.exception.GraqlSyntaxException;
import ai.grakn.exception.InvalidKBException;
import ai.grakn.exception.TemporaryWriteException;
import ai.grakn.graql.GetQuery;
import ai.grakn.graql.Printer;
import ai.grakn.graql.Query;
import ai.grakn.graql.QueryBuilder;
import ai.grakn.graql.QueryParser;
import ai.grakn.graql.admin.Answer;
import ai.grakn.graql.internal.printer.Printers;
import ai.grakn.graql.internal.query.QueryAnswer;
import ai.grakn.kb.internal.EmbeddedGraknTx;
import ai.grakn.util.REST;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.Timer;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.rholder.retry.Attempt;
import com.github.rholder.retry.RetryException;
import com.github.rholder.retry.RetryListener;
import com.github.rholder.retry.Retryer;
import com.github.rholder.retry.RetryerBuilder;
import com.github.rholder.retry.StopStrategies;
import com.github.rholder.retry.WaitStrategies;
import mjson.Json;
import org.apache.commons.lang.StringUtils;
import org.apache.http.entity.ContentType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import spark.Request;
import spark.Response;
import spark.Service;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static ai.grakn.engine.controller.util.Requests.mandatoryBody;
import static ai.grakn.engine.controller.util.Requests.mandatoryPathParameter;
import static ai.grakn.engine.controller.util.Requests.mandatoryQueryParameter;
import static ai.grakn.engine.controller.util.Requests.queryParameter;
import static ai.grakn.util.REST.Request.Graql.ALLOW_MULTIPLE_QUERIES;
import static ai.grakn.util.REST.Request.Graql.DEFINE_ALL_VARS;
import static ai.grakn.util.REST.Request.Graql.EXECUTE_WITH_INFERENCE;
import static ai.grakn.util.REST.Request.Graql.LOADING_DATA;
import static ai.grakn.util.REST.Request.Graql.QUERY;
import static ai.grakn.util.REST.Request.Graql.TX_TYPE;
import static ai.grakn.util.REST.Request.KEYSPACE_PARAM;
import static ai.grakn.util.REST.Response.ContentType.APPLICATION_JSON;
import static ai.grakn.util.REST.Response.ContentType.APPLICATION_TEXT;
import static com.codahale.metrics.MetricRegistry.name;
import static java.lang.Boolean.parseBoolean;
import static org.apache.http.HttpStatus.SC_OK;
/**
*
* Endpoints used to query the graph using Graql and build a HAL, Graql or Json response.
*
*
* @author Marco Scoppetta, alexandraorth
*/
public class GraqlController implements HttpController {
private static final ObjectMapper mapper = new ObjectMapper();
private static final Logger LOG = LoggerFactory.getLogger(GraqlController.class);
private static final RetryLogger retryLogger = new RetryLogger();
private static final int MAX_RETRY = 10;
private final Printer> printer;
private final EngineGraknTxFactory factory;
private final PostProcessor postProcessor;
private final Timer executeGraql;
private final Timer executeExplanation;
public GraqlController(
EngineGraknTxFactory factory, PostProcessor postProcessor, Printer> printer, MetricRegistry metricRegistry
) {
this.factory = factory;
this.postProcessor = postProcessor;
this.printer = printer;
this.executeGraql = metricRegistry.timer(name(GraqlController.class, "execute-graql"));
this.executeExplanation = metricRegistry.timer(name(GraqlController.class, "execute-explanation"));
}
@Override
public void start(Service spark) {
spark.post(REST.WebPath.KEYSPACE_GRAQL, this::executeGraql);
spark.get(REST.WebPath.KEYSPACE_EXPLAIN, this::explainGraql);
spark.exception(GraqlQueryException.class, (e, req, res) -> handleError(400, e, res));
spark.exception(GraqlSyntaxException.class, (e, req, res) -> handleError(400, e, res));
// Handle invalid type castings and invalid insertions
spark.exception(GraknTxOperationException.class, (e, req, res) -> handleError(422, e, res));
spark.exception(InvalidKBException.class, (e, req, res) -> handleError(422, e, res));
}
@GET
@Path("/kb/{keyspace}/explain")
private String explainGraql(Request request, Response response) throws RetryException, ExecutionException {
Keyspace keyspace = Keyspace.of(mandatoryPathParameter(request, KEYSPACE_PARAM));
String queryString = mandatoryQueryParameter(request, QUERY);
response.status(SC_OK);
return executeFunctionWithRetrying(() -> {
try (GraknTx tx = factory.tx(keyspace, GraknTxType.WRITE); Timer.Context context = executeExplanation.time()) {
Answer answer = tx.graql().infer(true).parser().parseQuery(queryString).execute().stream().findFirst().orElse(new QueryAnswer());
return mapper.writeValueAsString(ExplanationBuilder.buildExplanation(answer));
}
});
}
@POST
@Path("/kb/{keyspace}/graql")
private String executeGraql(Request request, Response response) throws RetryException, ExecutionException {
Keyspace keyspace = Keyspace.of(mandatoryPathParameter(request, KEYSPACE_PARAM));
String queryString = mandatoryBody(request);
//Run the query with reasoning on or off
Optional infer = queryParameter(request, EXECUTE_WITH_INFERENCE).map(Boolean::parseBoolean);
//Allow multiple queries to be executed
boolean multiQuery = parseBoolean(queryParameter(request, ALLOW_MULTIPLE_QUERIES).orElse("false"));
//Define all anonymous variables in the query
Optional defineAllVars = queryParameter(request, DEFINE_ALL_VARS).map(Boolean::parseBoolean);
//Used to check if serialisation of results is needed. When loading we skip this for the sake of speed
boolean skipSerialisation = parseBoolean(queryParameter(request, LOADING_DATA).orElse("false"));
//Check the transaction type to use
GraknTxType txType = queryParameter(request, TX_TYPE)
.map(String::toUpperCase).map(GraknTxType::valueOf).orElse(GraknTxType.WRITE);
//This is used to determine the response format
//TODO: Maybe we should really try to stick with one representation? This would require dashboard console interpreting the json representation
final String acceptType;
if (APPLICATION_TEXT.equals(Requests.getAcceptType(request))) {
acceptType = APPLICATION_TEXT;
} else {
acceptType = APPLICATION_JSON;
}
response.type(APPLICATION_JSON);
//Execute the query and get the results
LOG.debug("Executing graql query: {}", StringUtils.abbreviate(queryString, 100));
LOG.trace("Full query: {}", queryString);
return executeFunctionWithRetrying(() -> {
try (EmbeddedGraknTx> tx = factory.tx(keyspace, txType); Timer.Context context = executeGraql.time()) {
QueryBuilder builder = tx.graql();
infer.ifPresent(builder::infer);
QueryParser parser = builder.parser();
defineAllVars.ifPresent(parser::defineAllVars);
response.status(SC_OK);
return executeQuery(tx, queryString, acceptType, multiQuery, skipSerialisation, parser);
} finally {
LOG.debug("Executed graql query");
}
});
}
private String executeFunctionWithRetrying(Callable callable) throws RetryException, ExecutionException {
try {
Retryer retryer = RetryerBuilder.newBuilder()
.retryIfExceptionOfType(TemporaryWriteException.class)
.withRetryListener(retryLogger)
.withWaitStrategy(WaitStrategies.exponentialWait(100, 5, TimeUnit.MINUTES))
.withStopStrategy(StopStrategies.stopAfterAttempt(MAX_RETRY))
.build();
return retryer.call(callable);
} catch (ExecutionException e) {
Throwable cause = e.getCause();
if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
} else {
throw e;
}
}
}
private static class RetryLogger implements RetryListener {
@Override
public void onRetry(Attempt attempt) {
if (attempt.hasException()) {
LOG.warn("Retrying transaction after {" + attempt.getAttemptNumber() + "} attempts due to exception {" + attempt.getExceptionCause().getMessage() + "}");
}
}
}
/**
* Handle any {@link Exception} that are thrown by the server. Configures and returns
* the correct JSON response with the given status.
*
* @param exception exception thrown by the server
* @param response response to the client
*/
private static void handleError(int status, Exception exception, Response response) {
LOG.error("REST error", exception);
response.status(status);
response.body(Json.object("exception", exception.getMessage()).toString());
response.type(ContentType.APPLICATION_JSON.getMimeType());
}
/**
* Execute a query and return a response in the format specified by the request.
*
* @param tx open transaction to current graph
* @param queryString read query to be executed
* @param acceptType response format that the client will accept
* @param multi execute multiple statements
* @param parser
*/
private String executeQuery(EmbeddedGraknTx> tx, String queryString, String acceptType, boolean multi, boolean skipSerialisation, QueryParser parser) throws JsonProcessingException {
// By default use Jackson printer
Printer> printer = this.printer;
if (APPLICATION_TEXT.equals(acceptType)) printer = Printers.graql(false);
String formatted;
boolean commitQuery = true;
if (multi) {
Stream> query = parser.parseList(queryString);
List> collectedResults = query.map(this::executeAndMonitor).collect(Collectors.toList());
if (skipSerialisation) {
formatted = mapper.writeValueAsString(new Object[collectedResults.size()]);
} else {
formatted = printer.graqlString(collectedResults);
}
} else {
Query> query = parser.parseQuery(queryString);
if (skipSerialisation) {
formatted = "";
} else {
// If acceptType is 'application/text' add new line after every result
if (APPLICATION_TEXT.equals(acceptType)) {
formatted = query.results(printer).collect(Collectors.joining("\n"));
} else {
// If acceptType is 'application/json' map results to JSON representation
formatted = printer.graqlString(executeAndMonitor(query));
}
}
commitQuery = !query.isReadOnly();
}
if (commitQuery) tx.commitSubmitNoLogs().ifPresent(postProcessor::submit);
return formatted;
}
private Object executeAndMonitor(Query> query) {
return query.execute();
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy