Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve cancellation of large response in TransModel API #5908

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.opentripplanner.api.common.Message;
import org.opentripplanner.api.error.PlannerError;
import org.opentripplanner.apis.support.mapping.PlannerErrorMapper;
import org.opentripplanner.apis.transmodel.ResponseTooLargeException;
import org.opentripplanner.ext.restapi.mapping.TripPlanMapper;
import org.opentripplanner.ext.restapi.mapping.TripSearchMetadataMapper;
import org.opentripplanner.ext.restapi.model.ElevationMetadata;
Expand Down Expand Up @@ -104,8 +105,8 @@ public Response plan(@Context UriInfo uriInfo, @Context Request grizzlyRequest)
LOG.error("System error - unhandled error case?", e);
response.setError(new PlannerError(Message.SYSTEM_ERROR));
}
} catch (OTPRequestTimeoutException e) {
response.setError(new PlannerError(Message.PROCESSING_TIMEOUT));
} catch (OTPRequestTimeoutException | ResponseTooLargeException e) {
response.setError(new PlannerError(Message.UNPROCESSABLE_REQUEST));
} catch (Exception e) {
LOG.error("System error", e);
response.setError(new PlannerError(Message.SYSTEM_ERROR));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public enum Message {
PLAN_OK(200),
SYSTEM_ERROR(500),

PROCESSING_TIMEOUT(422),
UNPROCESSABLE_REQUEST(422),

GRAPH_UNAVAILABLE(503),

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import static graphql.execution.instrumentation.SimpleInstrumentationContext.noOp;

import graphql.ExecutionResult;
import graphql.execution.AbortExecutionException;
import graphql.execution.instrumentation.Instrumentation;
import graphql.execution.instrumentation.InstrumentationContext;
import graphql.execution.instrumentation.InstrumentationState;
Expand Down Expand Up @@ -47,7 +46,7 @@ public InstrumentationContext<Object> beginFieldFetch(
if (fetched % 10000 == 0) {
LOG.debug("Fetched {} fields", fetched);
if (fetched > maxFieldFetch) {
throw new AbortExecutionException(
throw new ResponseTooLargeException(
"The number of fields in the GraphQL result exceeds the maximum allowed: " + maxFieldFetch
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.opentripplanner.apis.transmodel;

/**
* Exception thrown when the API response exceeds a configurable limit.
*/
public class ResponseTooLargeException extends RuntimeException {

public ResponseTooLargeException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.opentripplanner.apis.transmodel.support.AbortOnTimeoutExecutionStrategy;
import org.opentripplanner.apis.transmodel.support.AbortOnUnprocessableRequestExecutionStrategy;
import org.opentripplanner.apis.transmodel.support.ExecutionResultMapper;
import org.opentripplanner.ext.actuator.MicrometerGraphQLInstrumentation;
import org.opentripplanner.framework.application.OTPFeature;
Expand Down Expand Up @@ -50,7 +50,7 @@ Response executeGraphQL(
int maxNumberOfResultFields,
Iterable<Tag> tracingTags
) {
try (var executionStrategy = new AbortOnTimeoutExecutionStrategy()) {
try (var executionStrategy = new AbortOnUnprocessableRequestExecutionStrategy()) {
variables = ObjectUtils.ifNotNull(variables, new HashMap<>());
var instrumentation = createInstrumentation(maxNumberOfResultFields, tracingTags);
var transmodelRequestContext = createRequestContext(serverContext);
Expand All @@ -69,6 +69,8 @@ Response executeGraphQL(
return ExecutionResultMapper.okResponse(result);
} catch (OTPRequestTimeoutException te) {
return ExecutionResultMapper.timeoutResponse();
} catch (ResponseTooLargeException rtle) {
return ExecutionResultMapper.tooLargeResponse(rtle.getMessage());
} catch (CoercingParseValueException | UnknownOperationException e) {
return ExecutionResultMapper.badRequestResponse(e.getMessage());
} catch (Exception systemError) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,28 @@
import graphql.schema.DataFetchingEnvironment;
import java.io.Closeable;
import java.util.concurrent.CompletableFuture;
import org.opentripplanner.apis.transmodel.ResponseTooLargeException;
import org.opentripplanner.framework.application.OTPRequestTimeoutException;
import org.opentripplanner.framework.logging.ProgressTracker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* To abort fetching data when a timeout occurs we have to rethrow the time-out-exception.
* To abort fetching data when a request is unprocessable (either because the execution times
* out or because the response is too large) we have to rethrow the exception.
* This will prevent unresolved data-fetchers to be called. The exception is not handled
* gracefully.
*/
public class AbortOnTimeoutExecutionStrategy extends AsyncExecutionStrategy implements Closeable {
public class AbortOnUnprocessableRequestExecutionStrategy
extends AsyncExecutionStrategy
implements Closeable {

private static final Logger LOG = LoggerFactory.getLogger(AbortOnTimeoutExecutionStrategy.class);
private static final Logger LOG = LoggerFactory.getLogger(
AbortOnUnprocessableRequestExecutionStrategy.class
);
public static final int LOG_STEPS = 25_000;
private final ProgressTracker timeoutProgressTracker = ProgressTracker.track(
"TIMEOUT! Abort GraphQL query",
"Unprocessable request. Abort GraphQL query",
LOG_STEPS,
-1
);
Expand All @@ -31,15 +37,15 @@ protected <T> CompletableFuture<T> handleFetchingException(
ExecutionStrategyParameters params,
Throwable e
) {
if (e instanceof OTPRequestTimeoutException te) {
logTimeoutProgress();
throw te;
if (e instanceof OTPRequestTimeoutException || e instanceof ResponseTooLargeException) {
logCancellationProgress();
throw (RuntimeException) e;
}
return super.handleFetchingException(environment, params, e);
}

@SuppressWarnings("Convert2MethodRef")
private void logTimeoutProgress() {
private void logCancellationProgress() {
timeoutProgressTracker.startOrStep(m -> LOG.info(m));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ public class ExecutionResultMapper {
private static final ErrorClassification API_PROCESSING_TIMEOUT = ErrorClassification.errorClassification(
"ApiProcessingTimeout"
);

private static final ErrorClassification RESPONSE_TOO_LARGE = ErrorClassification.errorClassification(
"ResponseTooLarge"
);

private static final ErrorClassification BAD_REQUEST_ERROR = ErrorClassification.errorClassification(
"BadRequestError"
);
Expand All @@ -29,13 +34,11 @@ public static Response okResponse(ExecutionResult result) {
}

public static Response timeoutResponse() {
var error = GraphQLError
.newError()
.errorType(API_PROCESSING_TIMEOUT)
.message(OTPRequestTimeoutException.MESSAGE)
.build();
var result = ExecutionResult.newExecutionResult().addError(error).build();
return response(result, OtpHttpStatus.STATUS_UNPROCESSABLE_ENTITY);
return unprocessableResponse(API_PROCESSING_TIMEOUT, OTPRequestTimeoutException.MESSAGE);
}

public static Response tooLargeResponse(String message) {
return unprocessableResponse(RESPONSE_TOO_LARGE, message);
}

public static Response badRequestResponse(String message) {
Expand All @@ -56,4 +59,13 @@ public static Response response(ExecutionResult result, Response.StatusType stat
.entity(GraphQLResponseSerializer.serialize(result))
.build();
}

private static Response unprocessableResponse(
ErrorClassification errorClassification,
String message
) {
var error = GraphQLError.newError().errorType(errorClassification).message(message).build();
var result = ExecutionResult.newExecutionResult().addError(error).build();
return response(result, OtpHttpStatus.STATUS_UNPROCESSABLE_ENTITY);
}
}
3 changes: 2 additions & 1 deletion src/main/resources/Message.properties
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ SYSTEM_ERROR = We're sorry. The trip planner is temporarily unavailable. Please
GRAPH_UNAVAILABLE = We're sorry. The trip planner is temporarily unavailable. Please try again later.

OUTSIDE_BOUNDS = Trip is not possible. You might be trying to plan a trip outside the map data boundary.
PROCESSING_TIMEOUT = The trip planner is taking too long to process your request.
UNPROCESSABLE_REQUEST = The trip planner is taking too long time or consuming too many resources to process your request.

BOGUS_PARAMETER = The request has errors that the server is not willing or able to process.
LOCATION_NOT_ACCESSIBLE = The location was found, but no stops could be found within the search radius.
PATH_NOT_FOUND = No trip found. There may be no transit service within the maximum specified distance or at the specified time, or your start or end point might not be safely accessible.
Expand Down
3 changes: 2 additions & 1 deletion src/main/resources/Message_de.properties
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ PLAN_OK = Success
SYSTEM_ERROR = Es tut uns leid, leider steht der Trip-Planer momentan nicht zur Verfügung. Bitte versuchen Sie es zu einem späteren Zeitpunkt nochmal.

OUTSIDE_BOUNDS = Planung nicht möglich. Vielleicht versuchen sie einen Plan außerhalb der Kartengrenzen zu planen.
PROCESSING_TIMEOUT = Der Trip-Planner braucht zu lange um die Anfrage zu bearbeiten
UNPROCESSABLE_REQUEST = Der Trip-Planner braucht zu lange oder verbraucht zu viele Ressourcen, um die Anfrage zu bearbeiten

BOGUS_PARAMETER = Die Anfrage ist fehlerhaft so dass sie der Server nicht bearbeiten möchte oder kann.
PATH_NOT_FOUND = Planung nicht möglich. Ihr Start- oder Endpunkt könnte nicht erreichbar sein. Bitte stellen sie sicher, dass ihre Anfrage innerhalb der Kartendaten ist.
NO_TRANSIT_TIMES = Keine Fahrzeiten verfügbar. Das Datum kann zu weit in der Vergangenheit oder zu weit in der Zukunft liegen oder es gibt keinen Verkehrsbetrieb zu dem von Ihnen gewählten Zeitpunkt.
Expand Down
3 changes: 2 additions & 1 deletion src/main/resources/Message_es.properties
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ SYSTEM_ERROR = ES-We're sorry. The trip planner is temporarily unavailable. Plea
GRAPH_UNAVAILABLE = ES-We're sorry. The trip planner is temporarily unavailable. Please try again later.

OUTSIDE_BOUNDS = ES-los trip is not possible. You might be trying to plan a trip outside the map data boundary.
PROCESSING_TIMEOUT = ES-los trip planner is taking too long to process your request.
UNPROCESSABLE_REQUEST = ES-los trip planner is taking too long time or consuming many resources to process your request.

BOGUS_PARAMETER = ES-los request has errors that the server is not willing or able to process.
PATH_NOT_FOUND = ES-los trip is not possible. Please check that you plan is within the bound of the map.
NO_TRANSIT_TIMES = ES-Non transit times available. The date may be past or too far in the future or there may not be transit service for your trip at the time you chose.
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/Message_fr.properties
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ SYSTEM_ERROR = Désolé: le calculateur d'itinéraires est temporairement indisp
GRAPH_UNAVAILABLE = Désolé: le calculateur d'itinéraires est temporairement indisponible. Veuillez recommencer ultérieurement.

OUTSIDE_BOUNDS = Impossible de calculer un itinéraire. Vous essayez de planifier un itinéraire hors des limites de la zone couverte.
PROCESSING_TIMEOUT = Le calculateur d'itinéraires prend trop de temps pour gérer votre demande.
UNPROCESSABLE_REQUEST = Le calculateur d'itinéraires prend trop de temps ou de resources pour gérer votre demande.
BOGUS_PARAMETER = Le serveur ne peut pas prendre en compte la requête, elle contient des paramètres erronés.
PATH_NOT_FOUND = Impossible de calculer un itinéraire. Veuillez vérifier que le trajet demandé est inclus dans la zone couverte.
NO_TRANSIT_TIMES = Aucun voyage disponible. Soit la date est trop loin dans le futur, soit il n'y a pas de service ce jour.
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/Message_hu.properties
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ SYSTEM_ERROR = Sajn\u00E1ljuk. Az \u00FAtvonaltervez\u0151 \u00E1tmenetileg
GRAPH_UNAVAILABLE = Sajn\u00E1ljuk. Az \u00FAtvonaltervez\u0151 \u00E1tmenetileg nem el\u00E9rhet\u0151. K\u00E9rj\u00FCk, pr\u00F3b\u00E1lja \u00FAjra k\u00E9s\u0151bb.

OUTSIDE_BOUNDS = Az utaz\u00E1s nem lehets\u00E9ges. Lehet, hogy a t\u00E9rk\u00E9padat-hat\u00E1ron k\u00EDv\u00FCli utat pr\u00F3b\u00E1l megtervezni.
PROCESSING_TIMEOUT = Az utaz\u00E1stervez\u0151 t\u00FAl sok\u00E1ig tart a k\u00E9r\u00E9s feldolgoz\u00E1s\u00E1hoz.
UNPROCESSABLE_REQUEST = Az utaz\u00E1stervez\u0151 t\u00FAl sok\u00E1ig tart a k\u00E9r\u00E9s feldolgoz\u00E1s\u00E1hoz.
BOGUS_PARAMETER = A k\u00E9r\u00E9s olyan hib\u00E1kat tartalmaz, amelyeket a szerver nem hajland\u00F3 vagy nem k\u00E9pes feldolgozni.
LOCATION_NOT_ACCESSIBLE = A helyek megtal\u00E1lhat\u00F3ak, de meg\u00E1ll\u00F3k nem tal\u00E1lhat\u00F3 a keres\u00E9si k\u00F6rzetben.
PATH_NOT_FOUND = Nem tal\u00E1lhat\u00F3 utaz\u00E1s. El\u0151fordulhat, hogy a megadott maxim\u00E1lis t\u00E1vols\u00E1gon bel\u00FCl vagy a megadott id\u0151pontban nincs t\u00F6megk\u00F6zleked\u00E9si szolg\u00E1ltat\u00E1s, vagy a kezd\u0151- vagy v\u00E9gpont nem \u00E9rhet\u0151 el biztons\u00E1gosan.
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/Message_nl.properties
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ SYSTEM_ERROR = Onze excuses. De routeplanner is momenteel niet beschikbaar. Prob
GRAPH_UNAVAILABLE = Onze excuses. De routeplanner is momenteel niet beschikbaar. Probeer later opnieuw.

OUTSIDE_BOUNDS = Deze reis is niet mogelijk. Mogelijk probeert u een reis te plannen buiten het beschikbare gebied
PROCESSING_TIMEOUT = De routeplanner is te lang bezig met uw verzoek.
UNPROCESSABLE_REQUEST = De routeplanner is te lang bezig met uw verzoek.
BOGUS_PARAMETER = Uw verzoek bevat fouten die de server niet wil or kan verwerken.
PATH_NOT_FOUND = Reis is niet mogelijk. Misschien is het startpunt of de bestemming niet veilig toegankelijk. Bijvoorbeeld een straat alleen verbonden met een snelweg.
NO_TRANSIT_TIMES = Geen OV-informatie beschikbaar. De datum is mogelijk te ver in het verleden of te ver in de toekomst. Of er is geen dienst voor uw reis op het moment dat u wilt.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,21 @@ class ExecutionResultMapperTest {
"}"
);

public static final String TOO_LARGE_MESSAGE =
"The number of fields in the GraphQL result exceeds the maximum allowed: 100000";

private static final String TOO_LARGE_RESPONSE = quoteReplace(
"{'" +
"errors':[{" +
"'message':'" +
TOO_LARGE_MESSAGE +
"'," +
"'locations':[]," +
"'extensions':{'classification':'ResponseTooLarge'}" +
"}]" +
"}"
);

public static final String SYSTEM_ERROR_MESSAGE = "A system error!";

public static final String SYSTEM_ERROR_RESPONSE = quoteReplace(
Expand Down Expand Up @@ -62,6 +77,13 @@ void timeoutResponse() {
assertEquals(TIMEOUT_RESPONSE, response.getEntity().toString());
}

@Test
void tooLargeResponse() {
var response = ExecutionResultMapper.tooLargeResponse(TOO_LARGE_MESSAGE);
assertEquals(422, response.getStatus());
assertEquals(TOO_LARGE_RESPONSE, response.getEntity().toString());
}

@Test
void systemErrorResponse() {
var response = ExecutionResultMapper.systemErrorResponse(SYSTEM_ERROR_MESSAGE);
Expand Down
Loading