Skip to content
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
10 changes: 10 additions & 0 deletions docs/RouterConfiguration.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ A full list of them can be found in the [RouteRequest](RouteRequest.md).
|    [transferCacheRequests](#transit_transferCacheRequests) | `object[]` | Routing requests to use for pre-filling the stop-to-stop transfer cache. | *Optional* | | 2.3 |
| transmodelApi | `object` | Configuration for the Transmodel GraphQL API. | *Optional* | | 2.1 |
|    [hideFeedId](#transmodelApi_hideFeedId) | `boolean` | Hide the FeedId in all API output, and add it to input. | *Optional* | `false` | na |
|    [maxNumberOfResultFields](#transmodelApi_maxNumberOfResultFields) | `integer` | The maximum number of fields in a GraphQL result | *Optional* | `1000000` | 2.6 |
|    [tracingHeaderTags](#transmodelApi_tracingHeaderTags) | `string[]` | Used to group requests when monitoring OTP. | *Optional* | | na |
| [updaters](UpdaterConfig.md) | `object[]` | Configuration for the updaters that import various types of data into OTP. | *Optional* | | 1.5 |
| [vectorTiles](sandbox/MapboxVectorTilesApi.md) | `object` | Vector tile configuration | *Optional* | | na |
Expand Down Expand Up @@ -423,6 +424,15 @@ Hide the FeedId in all API output, and add it to input.

Only turn this feature on if you have unique ids across all feeds, without the feedId prefix.

<h3 id="transmodelApi_maxNumberOfResultFields">maxNumberOfResultFields</h3>

**Since version:** `2.6` ∙ **Type:** `integer` ∙ **Cardinality:** `Optional` ∙ **Default value:** `1000000`
**Path:** /transmodelApi

The maximum number of fields in a GraphQL result

Enforce rate limiting based on query complexity; Queries that return too much data are cancelled.

<h3 id="transmodelApi_tracingHeaderTags">tracingHeaderTags</h3>

**Since version:** `na` ∙ **Type:** `string[]` ∙ **Cardinality:** `Optional`
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package org.opentripplanner.apis.transmodel;

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;
import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters;
import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters;
import jakarta.validation.constraints.NotNull;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicLong;
import org.opentripplanner.framework.application.OTPRequestTimeoutException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* A GraphQL instrumentation that aborts the execution if the number of fetched fields exceeds
* a configurable limit.
* The instrumentation also periodically checks the OTP request interruption status while the
* query is being processed, giving the possibility to control the request runtime complexity
* both in terms of result size and execution time.
*/
public class MaxFieldsInResultInstrumentation implements Instrumentation {

private static final Logger LOG = LoggerFactory.getLogger(MaxFieldsInResultInstrumentation.class);

/**
* The maximum number of fields that can be present in the GraphQL result.
*/
private final int maxFieldFetch;

private final AtomicLong fieldFetchCounter = new AtomicLong();

public MaxFieldsInResultInstrumentation(int maxFieldFetch) {
this.maxFieldFetch = maxFieldFetch;
}

@Override
public InstrumentationContext<Object> beginFieldFetch(
InstrumentationFieldFetchParameters parameters,
InstrumentationState state
) {
long fetched = fieldFetchCounter.incrementAndGet();
if (fetched % 10000 == 0) {
LOG.debug("Fetched {} fields", fetched);
if (fetched > maxFieldFetch) {
throw new AbortExecutionException(
"The number of fields in the GraphQL result exceeds the maximum allowed: " + maxFieldFetch
);
}
OTPRequestTimeoutException.checkForTimeout();
}
return noOp();
}

@Override
@NotNull
public CompletableFuture<ExecutionResult> instrumentExecutionResult(
ExecutionResult executionResult,
InstrumentationExecutionParameters parameters,
InstrumentationState state
) {
LOG.debug("The GraphQL result contains {} fields", fieldFetchCounter.get());
return Instrumentation.super.instrumentExecutionResult(executionResult, parameters, state);
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
import io.micrometer.core.instrument.Tag;
import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.HeaderParam;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
Expand Down Expand Up @@ -37,6 +35,7 @@ public class TransmodelAPI {

private static GraphQLSchema schema;
private static Collection<String> tracingHeaderTags;
private static int maxNumberOfResultFields;

private final OtpServerRequestContext serverContext;
private final TransmodelGraph index;
Expand Down Expand Up @@ -75,6 +74,7 @@ public static void setUp(
TransitIdMapper.setupFixedFeedId(transitModel.getAgencies());
}
tracingHeaderTags = config.tracingHeaderTags();
maxNumberOfResultFields = config.maxNumberOfResultFields();
GqlUtil gqlUtil = new GqlUtil(transitModel.getTimeZone());
schema = TransmodelGraphQLSchema.create(defaultRouteRequest, gqlUtil);
}
Expand All @@ -83,7 +83,6 @@ public static void setUp(
@Consumes(MediaType.APPLICATION_JSON)
public Response getGraphQL(
HashMap<String, Object> queryParameters,
@HeaderParam("OTPMaxResolves") @DefaultValue("1000000") int maxResolves,
@Context HttpHeaders headers
) {
if (queryParameters == null || !queryParameters.containsKey("query")) {
Expand Down Expand Up @@ -116,24 +115,20 @@ public Response getGraphQL(
serverContext,
variables,
operationName,
maxResolves,
maxNumberOfResultFields,
getTagsFromHeaders(headers)
);
}

@POST
@Consumes("application/graphql")
public Response getGraphQL(
String query,
@HeaderParam("OTPMaxResolves") @DefaultValue("1000000") int maxResolves,
@Context HttpHeaders headers
) {
public Response getGraphQL(String query, @Context HttpHeaders headers) {
return index.executeGraphQL(
query,
serverContext,
null,
null,
maxResolves,
maxNumberOfResultFields,
getTagsFromHeaders(headers)
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,9 @@ public interface TransmodelAPIParameters {
* @see MicrometerGraphQLInstrumentation
*/
Collection<String> tracingHeaderTags();

/**
* The maximum number of fields that can be present in a GraphQL result.
*/
int maxNumberOfResultFields();
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import graphql.ExecutionInput;
import graphql.ExecutionResult;
import graphql.GraphQL;
import graphql.analysis.MaxQueryComplexityInstrumentation;
import graphql.execution.ExecutionStrategy;
import graphql.execution.UnknownOperationException;
import graphql.execution.instrumentation.ChainedInstrumentation;
Expand Down Expand Up @@ -48,12 +47,12 @@ Response executeGraphQL(
OtpServerRequestContext serverContext,
Map<String, Object> variables,
String operationName,
int maxResolves,
int maxNumberOfResultFields,
Iterable<Tag> tracingTags
) {
try (var executionStrategy = new AbortOnTimeoutExecutionStrategy()) {
variables = ObjectUtils.ifNotNull(variables, new HashMap<>());
var instrumentation = createInstrumentation(maxResolves, tracingTags);
var instrumentation = createInstrumentation(maxNumberOfResultFields, tracingTags);
var transmodelRequestContext = createRequestContext(serverContext);
var executionInput = createExecutionInput(
query,
Expand All @@ -78,11 +77,11 @@ Response executeGraphQL(
}
}

private static Instrumentation createInstrumentation(int maxResolves, Iterable<Tag> tracingTags) {
Instrumentation instrumentation = new ChainedInstrumentation(
new MaxQueryComplexityInstrumentation(maxResolves),
new OTPRequestTimeoutInstrumentation()
);
private static Instrumentation createInstrumentation(
int maxNumberOfResultFields,
Iterable<Tag> tracingTags
) {
Instrumentation instrumentation = new MaxFieldsInResultInstrumentation(maxNumberOfResultFields);

if (OTPFeature.ActuatorAPI.isOn()) {
instrumentation =
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package org.opentripplanner.standalone.config.sandbox;

import static org.opentripplanner.standalone.config.framework.json.OtpVersion.NA;
import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_1;
import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_6;

import java.util.Collection;
import java.util.Set;
Expand All @@ -15,18 +15,18 @@ public class TransmodelAPIConfig implements TransmodelAPIParameters {

private final boolean hideFeedId;
private final Collection<String> tracingHeaderTags;
private final int maxNumberOfResultFields;

public TransmodelAPIConfig(String parameterName, NodeAdapter root) {
var c = root
.of("transmodelApi")
.of(parameterName)
.since(V2_1)
.summary("Configuration for the Transmodel GraphQL API.")
.asObject();

hideFeedId =
c
.of("hideFeedId")
.since(NA)
.summary("Hide the FeedId in all API output, and add it to input.")
.description(
"Only turn this feature on if you have unique ids across all feeds, without the " +
Expand All @@ -36,9 +36,19 @@ public TransmodelAPIConfig(String parameterName, NodeAdapter root) {
tracingHeaderTags =
c
.of("tracingHeaderTags")
.since(NA)
.summary("Used to group requests when monitoring OTP.")
.asStringList(Set.of());

maxNumberOfResultFields =
c
.of("maxNumberOfResultFields")
.since(V2_6)
.summary("The maximum number of fields in a GraphQL result")
.description(
"Enforce rate limiting based on query complexity; Queries that return too much data are" +
" cancelled."
)
.asInt(1_000_000);
}

@Override
Expand All @@ -50,4 +60,9 @@ public boolean hideFeedId() {
public Collection<String> tracingHeaderTags() {
return tracingHeaderTags;
}

@Override
public int maxNumberOfResultFields() {
return maxNumberOfResultFields;
}
}