diff --git a/application/src/main/java/org/opentripplanner/standalone/config/RouterConfig.java b/application/src/main/java/org/opentripplanner/standalone/config/RouterConfig.java index 5f88433c771..6866d371ba9 100644 --- a/application/src/main/java/org/opentripplanner/standalone/config/RouterConfig.java +++ b/application/src/main/java/org/opentripplanner/standalone/config/RouterConfig.java @@ -8,6 +8,7 @@ import com.fasterxml.jackson.databind.node.MissingNode; import java.io.Serializable; import java.util.List; +import javax.annotation.Nullable; import org.opentripplanner.apis.gtfs.GtfsApiParameters; import org.opentripplanner.ext.flex.FlexParameters; import org.opentripplanner.ext.ojp.config.OjpApiConfig; @@ -22,10 +23,12 @@ import org.opentripplanner.standalone.config.routerconfig.TransitRoutingConfig; import org.opentripplanner.standalone.config.routerconfig.UpdatersConfig; import org.opentripplanner.standalone.config.routerconfig.VectorTileConfig; +import org.opentripplanner.standalone.config.routerconfig.WarmupConfig; import org.opentripplanner.standalone.config.sandbox.FlexConfig; import org.opentripplanner.standalone.config.sandbox.GtfsApiConfig; import org.opentripplanner.standalone.config.sandbox.TransmodelAPIConfig; import org.opentripplanner.updater.UpdatersParameters; +import org.opentripplanner.warmup.api.WarmupParameters; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -58,6 +61,7 @@ public class RouterConfig implements Serializable { private final VectorTileConfig vectorTileConfig; private final TriasApiParameters triasApiParameters; private final OjpApiParameters ojpApiParameters; + private final WarmupParameters warmupParameters; public RouterConfig(JsonNode node, String source, boolean logUnusedParams) { this(new NodeAdapter(node, source), logUnusedParams); @@ -88,6 +92,7 @@ public RouterConfig(JsonNode node, String source, boolean logUnusedParams) { this.triasApiParameters = TriasApiConfig.mapParameters("triasApi", root); this.ojpApiParameters = OjpApiConfig.mapParameters("ojpApi", root); this.flexConfig = new FlexConfig(root, "flex"); + this.warmupParameters = WarmupConfig.mapWarmupConfig("warmup", root); if (logUnusedParams && LOG.isWarnEnabled()) { root.logAllWarnings(LOG::warn); @@ -158,6 +163,11 @@ public GtfsApiParameters gtfsApiParameters() { return gtfsApi; } + @Nullable + public WarmupParameters warmupParameters() { + return warmupParameters; + } + public NodeAdapter asNodeAdapter() { return root; } diff --git a/application/src/main/java/org/opentripplanner/standalone/config/routerconfig/WarmupConfig.java b/application/src/main/java/org/opentripplanner/standalone/config/routerconfig/WarmupConfig.java new file mode 100644 index 00000000000..e63f8477afb --- /dev/null +++ b/application/src/main/java/org/opentripplanner/standalone/config/routerconfig/WarmupConfig.java @@ -0,0 +1,122 @@ +package org.opentripplanner.standalone.config.routerconfig; + +import static org.opentripplanner.standalone.config.framework.json.EnumMapper.docEnumValueList; +import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_10; + +import java.util.List; +import javax.annotation.Nullable; +import org.opentripplanner.standalone.config.framework.json.NodeAdapter; +import org.opentripplanner.street.geometry.WgsCoordinate; +import org.opentripplanner.street.model.StreetMode; +import org.opentripplanner.warmup.api.WarmupApi; +import org.opentripplanner.warmup.api.WarmupParameters; + +/** + * Maps the {@code warmup} section of {@code router-config.json} into a {@link WarmupParameters} + * record consumed by the warmup module. + */ +public final class WarmupConfig { + + private static final List DEFAULT_ACCESS_MODES = List.of( + StreetMode.WALK, + StreetMode.CAR_TO_PARK + ); + private static final List DEFAULT_EGRESS_MODES = List.of( + StreetMode.WALK, + StreetMode.WALK + ); + + private WarmupConfig() {} + + @Nullable + public static WarmupParameters mapWarmupConfig(String parameterName, NodeAdapter root) { + if (!root.exist(parameterName)) { + return null; + } + + var c = root + .of(parameterName) + .since(V2_10) + .summary("Configure application warmup by running transit searches during startup.") + .description( + """ + When configured, OTP runs transit routing queries between the given locations + during startup. This warms up the application (JIT compilation, GraphQL schema + caches, routing data structures, etc.) before production traffic arrives. + Queries start after the Raptor transit data is created and stop when all updaters + are primed (the health endpoint would return "UP"). + If no updaters are configured, no warmup queries are run. + """ + ) + .asObject(); + + WarmupApi api = c + .of("api") + .since(V2_10) + .summary("Which GraphQL API to use for warmup queries.") + .description(docEnumValueList(WarmupApi.values())) + .asEnum(WarmupApi.TRANSMODEL); + + WgsCoordinate from = mapCoordinate(c, "from", "Origin location for warmup searches.", "origin"); + WgsCoordinate to = mapCoordinate( + c, + "to", + "Destination location for warmup searches.", + "destination" + ); + + var accessModeStrings = c + .of("accessModes") + .since(V2_10) + .summary("Access modes to cycle through in warmup queries.") + .description( + "Ordered list of `StreetMode` values used as access modes. " + + "Each entry is paired with the egress mode at the same index. " + + docEnumValueList(StreetMode.values()) + ) + .asStringList(DEFAULT_ACCESS_MODES.stream().map(Enum::name).toList()); + List accessModes = accessModeStrings + .stream() + .map(s -> StreetMode.valueOf(s)) + .toList(); + + var egressModeStrings = c + .of("egressModes") + .since(V2_10) + .summary("Egress modes to cycle through in warmup queries.") + .description( + "Ordered list of `StreetMode` values used as egress modes. " + + "Each entry is paired with the access mode at the same index. " + + docEnumValueList(StreetMode.values()) + ) + .asStringList(DEFAULT_EGRESS_MODES.stream().map(Enum::name).toList()); + List egressModes = egressModeStrings + .stream() + .map(s -> StreetMode.valueOf(s)) + .toList(); + + if (accessModes.size() != egressModes.size()) { + throw new IllegalArgumentException( + "warmup.accessModes and warmup.egressModes must have the same number of entries, " + + "got %d access modes and %d egress modes.".formatted( + accessModes.size(), + egressModes.size() + ) + ); + } + + return new WarmupParameters(api, from, to, accessModes, egressModes); + } + + private static WgsCoordinate mapCoordinate( + NodeAdapter parent, + String name, + String summary, + String noun + ) { + var node = parent.of(name).since(V2_10).summary(summary).asObject(); + double lat = node.of("lat").since(V2_10).summary("Latitude of the " + noun + ".").asDouble(); + double lon = node.of("lon").since(V2_10).summary("Longitude of the " + noun + ".").asDouble(); + return new WgsCoordinate(lat, lon); + } +} diff --git a/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java b/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java index de0d61c5537..0789b4e890e 100644 --- a/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java +++ b/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java @@ -206,6 +206,9 @@ private void setupTransitRoutingServer() { routerConfig().updaterConfig() ); + // Start application warmup — runs routing queries to warm up the application + factory.warmupLauncher().start(); + initEllipsoidToGeoidDifference(); initializeTransferCache(routerConfig().transitTuningConfig(), timetableRepository()); diff --git a/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java b/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java index 343e8cda451..88e492516e9 100644 --- a/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java +++ b/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java @@ -66,6 +66,8 @@ import org.opentripplanner.transit.service.TimetableRepository; import org.opentripplanner.transit.service.TransitService; import org.opentripplanner.updater.trip.TimetableSnapshotManager; +import org.opentripplanner.warmup.WarmupLauncher; +import org.opentripplanner.warmup.configure.WarmupModule; /** * A Factory used by the Dagger dependency injection system to create the components of OTP, which @@ -99,6 +101,7 @@ VehicleRentalRepositoryModule.class, VehicleRentalServiceModule.class, ViaModule.class, + WarmupModule.class, WorldEnvelopeServiceModule.class, } ) @@ -166,6 +169,8 @@ public interface ConstructApplicationFactory { DeduplicatorService deduplicatorService(); + WarmupLauncher warmupLauncher(); + @Component.Builder interface Builder { @BindsInstance diff --git a/application/src/main/java/org/opentripplanner/warmup/GtfsWarmupQueryExecutor.java b/application/src/main/java/org/opentripplanner/warmup/GtfsWarmupQueryExecutor.java new file mode 100644 index 00000000000..33a8ee0bd7c --- /dev/null +++ b/application/src/main/java/org/opentripplanner/warmup/GtfsWarmupQueryExecutor.java @@ -0,0 +1,110 @@ +package org.opentripplanner.warmup; + +import graphql.ExecutionInput; +import graphql.GraphQL; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import org.opentripplanner.apis.gtfs.GraphQLRequestContext; +import org.opentripplanner.apis.gtfs.mapping.routerequest.AccessModeMapper; +import org.opentripplanner.apis.gtfs.mapping.routerequest.EgressModeMapper; +import org.opentripplanner.apis.support.graphql.OtpDataFetcherExceptionHandler; +import org.opentripplanner.standalone.api.OtpServerRequestContext; +import org.opentripplanner.street.geometry.WgsCoordinate; +import org.opentripplanner.street.model.StreetMode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class GtfsWarmupQueryExecutor implements WarmupQueryStrategy { + + private static final Logger LOG = LoggerFactory.getLogger(GtfsWarmupQueryExecutor.class); + + static final String QUERY = """ + query( + $fromLat: CoordinateValue!, $fromLon: CoordinateValue!, + $toLat: CoordinateValue!, $toLon: CoordinateValue!, + $dateTime: PlanDateTimeInput!, + $accessMode: PlanAccessMode!, $egressMode: PlanEgressMode! + ) { + planConnection( + origin: { location: { coordinate: { latitude: $fromLat, longitude: $fromLon } } } + destination: { location: { coordinate: { latitude: $toLat, longitude: $toLon } } } + dateTime: $dateTime + modes: { transit: { access: [$accessMode], egress: [$egressMode] } } + ) { + edges { + node { + start + end + legs { + mode + duration + from { name lat lon } + to { name lat lon } + route { shortName } + legGeometry { points } + } + } + } + } + } + """; + + private final GraphQL graphQL; + private final GraphQLRequestContext requestContext; + private final ModeCombinations modeCombinations; + + GtfsWarmupQueryExecutor( + OtpServerRequestContext context, + List accessModes, + List egressModes + ) { + this.requestContext = GraphQLRequestContext.ofServerContext(context); + this.graphQL = GraphQL.newGraphQL(context.gtfsSchema()) + .defaultDataFetcherExceptionHandler(new OtpDataFetcherExceptionHandler()) + .build(); + this.modeCombinations = new ModeCombinations(accessModes, egressModes); + } + + @Override + public boolean execute(WgsCoordinate from, WgsCoordinate to, boolean arriveBy, int queryCount) { + var variables = buildVariables(from, to, arriveBy, queryCount); + + var input = ExecutionInput.newExecutionInput() + .query(QUERY) + .context(requestContext) + .variables(variables) + .locale(Locale.US) + .build(); + + var result = graphQL.execute(input); + if (!result.getErrors().isEmpty()) { + LOG.warn("Warmup query had GraphQL errors: {}", result.getErrors()); + return false; + } + return true; + } + + Map buildVariables( + WgsCoordinate from, + WgsCoordinate to, + boolean arriveBy, + int queryCount + ) { + var now = Instant.now().atOffset(ZoneOffset.UTC).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); + var dateTime = arriveBy ? Map.of("latestArrival", now) : Map.of("earliestDeparture", now); + + return Map.ofEntries( + Map.entry("fromLat", from.latitude()), + Map.entry("fromLon", from.longitude()), + Map.entry("toLat", to.latitude()), + Map.entry("toLon", to.longitude()), + Map.entry("dateTime", dateTime), + Map.entry("accessMode", AccessModeMapper.map(modeCombinations.access(queryCount)).name()), + Map.entry("egressMode", EgressModeMapper.map(modeCombinations.egress(queryCount)).name()) + ); + } +} diff --git a/application/src/main/java/org/opentripplanner/warmup/ModeCombinations.java b/application/src/main/java/org/opentripplanner/warmup/ModeCombinations.java new file mode 100644 index 00000000000..6b83fa49733 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/warmup/ModeCombinations.java @@ -0,0 +1,44 @@ +package org.opentripplanner.warmup; + +import java.util.List; +import org.opentripplanner.street.model.StreetMode; + +/** + * Access/egress mode pairs to cycle through during warmup. Entries {@code i} of the access and + * egress lists form one pair. The cycle is driven by a caller-supplied counter; modulo arithmetic + * maps the counter to a pair so warmup queries iterate through every combination before repeating. + */ +class ModeCombinations { + + private final List accessModes; + private final List egressModes; + + ModeCombinations(List accessModes, List egressModes) { + if (accessModes.size() != egressModes.size()) { + throw new IllegalArgumentException( + "accessModes and egressModes must have the same size, got %d and %d.".formatted( + accessModes.size(), + egressModes.size() + ) + ); + } + this.accessModes = List.copyOf(accessModes); + this.egressModes = List.copyOf(egressModes); + } + + int size() { + return accessModes.size(); + } + + StreetMode access(int counter) { + return accessModes.get(indexFor(counter)); + } + + StreetMode egress(int counter) { + return egressModes.get(indexFor(counter)); + } + + private int indexFor(int counter) { + return Math.floorMod(counter - 1, size()); + } +} diff --git a/application/src/main/java/org/opentripplanner/warmup/TransmodelWarmupQueryExecutor.java b/application/src/main/java/org/opentripplanner/warmup/TransmodelWarmupQueryExecutor.java new file mode 100644 index 00000000000..4cfb6f85d85 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/warmup/TransmodelWarmupQueryExecutor.java @@ -0,0 +1,126 @@ +package org.opentripplanner.warmup; + +import graphql.ExecutionInput; +import graphql.GraphQL; +import graphql.schema.GraphQLSchema; +import java.util.List; +import java.util.Map; +import org.opentripplanner.apis.transmodel.TransmodelRequestContext; +import org.opentripplanner.apis.transmodel.model.EnumTypes; +import org.opentripplanner.apis.transmodel.support.AbortOnUnprocessableRequestExecutionStrategy; +import org.opentripplanner.standalone.api.OtpServerRequestContext; +import org.opentripplanner.street.geometry.WgsCoordinate; +import org.opentripplanner.street.model.StreetMode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class TransmodelWarmupQueryExecutor implements WarmupQueryStrategy { + + private static final Logger LOG = LoggerFactory.getLogger(TransmodelWarmupQueryExecutor.class); + + static final String QUERY = """ + query( + $fromLat: Float!, $fromLon: Float!, + $toLat: Float!, $toLon: Float!, + $arriveBy: Boolean!, + $accessMode: StreetMode!, $egressMode: StreetMode! + ) { + trip( + from: { coordinates: { latitude: $fromLat, longitude: $fromLon } } + to: { coordinates: { latitude: $toLat, longitude: $toLon } } + arriveBy: $arriveBy + modes: { accessMode: $accessMode, egressMode: $egressMode } + ) { + tripPatterns { + duration + legs { + mode + duration + fromPlace { name } + toPlace { name } + line { publicCode } + pointsOnLink { points } + } + } + } + } + """; + + private final OtpServerRequestContext serverContext; + private final TransmodelRequestContext requestContext; + private final GraphQLSchema schema; + private final ModeCombinations modeCombinations; + + TransmodelWarmupQueryExecutor( + OtpServerRequestContext context, + List accessModes, + List egressModes + ) { + this.serverContext = context; + this.schema = context.transmodelSchema(); + this.requestContext = new TransmodelRequestContext( + context, + context.routingService(), + context.transitService(), + context.empiricalDelayService() + ); + this.modeCombinations = new ModeCombinations(accessModes, egressModes); + } + + @Override + public boolean execute(WgsCoordinate from, WgsCoordinate to, boolean arriveBy, int queryCount) { + var variables = buildVariables(from, to, arriveBy, queryCount); + + var input = ExecutionInput.newExecutionInput() + .query(QUERY) + .context(requestContext) + .root(serverContext) + .variables(variables) + .build(); + + // The AbortOnUnprocessableRequestExecutionStrategy has per-query state + // (ProgressTracker) and must be created fresh for each execution. + try (var strategy = new AbortOnUnprocessableRequestExecutionStrategy()) { + var graphQL = GraphQL.newGraphQL(schema).queryExecutionStrategy(strategy).build(); + var result = graphQL.execute(input); + if (!result.getErrors().isEmpty()) { + LOG.warn("Warmup query had GraphQL errors: {}", result.getErrors()); + return false; + } + return true; + } + } + + private static String toGraphQLName(StreetMode mode) { + return EnumTypes.STREET_MODE.getValues() + .stream() + .filter(v -> v.getValue() == mode) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("No TransModel mapping for " + mode)) + .getName(); + } + + Map buildVariables( + WgsCoordinate from, + WgsCoordinate to, + boolean arriveBy, + int queryCount + ) { + return Map.of( + "fromLat", + from.latitude(), + "fromLon", + from.longitude(), + "toLat", + to.latitude(), + "toLon", + to.longitude(), + "arriveBy", + arriveBy, + "accessMode", + toGraphQLName(modeCombinations.access(queryCount)), + "egressMode", + toGraphQLName(modeCombinations.egress(queryCount)) + ); + } +} diff --git a/application/src/main/java/org/opentripplanner/warmup/WarmupLauncher.java b/application/src/main/java/org/opentripplanner/warmup/WarmupLauncher.java new file mode 100644 index 00000000000..9c8f7e69bd2 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/warmup/WarmupLauncher.java @@ -0,0 +1,73 @@ +package org.opentripplanner.warmup; + +import javax.annotation.Nullable; +import org.opentripplanner.standalone.api.OtpServerRequestContext; +import org.opentripplanner.transit.service.TimetableRepository; +import org.opentripplanner.updater.GraphUpdaterManager; +import org.opentripplanner.warmup.api.WarmupParameters; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Launches the application warmup background thread. + *

+ * Injected dependencies come from the Dagger {@link + * org.opentripplanner.warmup.configure.WarmupModule}. The launcher decides whether a warmup run + * is applicable (parameters present, updaters present, selected API enabled) and, when it is, + * starts a daemon thread running a {@link WarmupWorker}. + */ +public class WarmupLauncher { + + private static final Logger LOG = LoggerFactory.getLogger(WarmupLauncher.class); + + @Nullable + private final WarmupParameters parameters; + + private final OtpServerRequestContext serverContext; + private final TimetableRepository timetableRepository; + + public WarmupLauncher( + @Nullable WarmupParameters parameters, + OtpServerRequestContext serverContext, + TimetableRepository timetableRepository + ) { + this.parameters = parameters; + this.serverContext = serverContext; + this.timetableRepository = timetableRepository; + } + + /** + * Start the application warmup thread if configured and applicable. + *

+ * No warmup is started if parameters are null (warmup section absent in router-config.json), + * if no updaters are configured (health probe would immediately return "UP"), or if the + * selected API schema is not available. + */ + public void start() { + if (parameters == null) { + return; + } + GraphUpdaterManager updaterManager = timetableRepository.getUpdaterManager(); + if (updaterManager == null) { + LOG.info("Application warmup configured but no updaters found. Skipping warmup."); + return; + } + var schema = switch (parameters.api()) { + case TRANSMODEL -> serverContext.transmodelSchema(); + case GTFS -> serverContext.gtfsSchema(); + }; + if (schema == null) { + LOG.warn( + "Application warmup configured for {} API, but the schema is not available. " + + "Is the corresponding API feature enabled?", + parameters.api() + ); + return; + } + var worker = new WarmupWorker(parameters, serverContext, () -> updaterManager); + var thread = new Thread(worker, "app-warmup"); + thread.setDaemon(true); + thread.start(); + LOG.info("Application warmup thread started."); + } +} diff --git a/application/src/main/java/org/opentripplanner/warmup/WarmupQueryStrategy.java b/application/src/main/java/org/opentripplanner/warmup/WarmupQueryStrategy.java new file mode 100644 index 00000000000..3a3fe2e4652 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/warmup/WarmupQueryStrategy.java @@ -0,0 +1,14 @@ +package org.opentripplanner.warmup; + +import org.opentripplanner.street.geometry.WgsCoordinate; + +/** Strategy for executing warmup queries against a specific GraphQL API. */ +interface WarmupQueryStrategy { + /** + * Execute one warmup query. The strategy picks access/egress modes and any other per-query + * parameters from the given {@code queryCount}, so the caller only needs a running counter. + * + * @return true if the query executed without GraphQL errors. + */ + boolean execute(WgsCoordinate from, WgsCoordinate to, boolean arriveBy, int queryCount); +} diff --git a/application/src/main/java/org/opentripplanner/warmup/WarmupWorker.java b/application/src/main/java/org/opentripplanner/warmup/WarmupWorker.java new file mode 100644 index 00000000000..da90f05dca7 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/warmup/WarmupWorker.java @@ -0,0 +1,134 @@ +package org.opentripplanner.warmup; + +import java.time.Duration; +import java.time.Instant; +import java.util.function.Supplier; +import org.opentripplanner.standalone.api.OtpServerRequestContext; +import org.opentripplanner.updater.GraphUpdaterStatus; +import org.opentripplanner.utils.time.DurationUtils; +import org.opentripplanner.warmup.api.WarmupParameters; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Runs GraphQL trip queries in a background thread during OTP startup to warm up the + * application before production traffic arrives. + *

+ * The worker sends sequential queries through the configured GraphQL API (TransModel or GTFS), + * exercising the full stack: GraphQL parsing, data fetchers, routing (Raptor + A*), itinerary + * filtering, and response serialization. This warms up JIT compilation, GraphQL schema caches, + * routing data structures, and other lazily initialized components. It alternates between + * depart-at / arrive-by and cycles through access/egress modes (walk, bike, car-to-park). + *

+ * It starts after Raptor transit data is created and stops when the health probe + * reports "UP" (all updaters primed). + */ +class WarmupWorker implements Runnable { + + private static final Logger LOG = LoggerFactory.getLogger(WarmupWorker.class); + private static final int MAX_QUERIES = 20; + + private final WarmupParameters parameters; + private final WarmupQueryStrategy queryStrategy; + private final Supplier updaterStatusProvider; + + WarmupWorker( + WarmupParameters parameters, + OtpServerRequestContext serverContext, + Supplier updaterStatusProvider + ) { + this.parameters = parameters; + this.updaterStatusProvider = updaterStatusProvider; + this.queryStrategy = switch (parameters.api()) { + case TRANSMODEL -> new TransmodelWarmupQueryExecutor( + serverContext, + parameters.accessModes(), + parameters.egressModes() + ); + case GTFS -> new GtfsWarmupQueryExecutor( + serverContext, + parameters.accessModes(), + parameters.egressModes() + ); + }; + } + + @Override + public void run() { + LOG.info( + "Application warmup started. Sending {} GraphQL trip queries from {} to {}.", + parameters.api(), + parameters.from(), + parameters.to() + ); + + var startTime = Instant.now(); + int queryCount = 0; + int failureCount = 0; + + try { + while (queryCount < MAX_QUERIES) { + if (isHealthy()) { + LOG.info( + "Application warmup complete: {} queries ({} failures) in {}. All updaters primed.", + queryCount, + failureCount, + DurationUtils.durationToStr(Duration.between(startTime, Instant.now())) + ); + return; + } + + queryCount++; + boolean arriveBy = queryCount % 2 == 0; + if (!executeQuery(queryCount, arriveBy)) { + failureCount++; + } + } + + LOG.info( + "Application warmup reached maximum of {} queries ({} failures) in {}" + + " before all updaters were primed.", + MAX_QUERIES, + failureCount, + DurationUtils.durationToStr(Duration.between(startTime, Instant.now())) + ); + } catch (Throwable e) { + LOG.error("Application warmup terminated by error after {} queries.", queryCount, e); + } + } + + /** @return true if the query succeeded without errors, false otherwise. */ + private boolean executeQuery(int queryCount, boolean arriveBy) { + var queryStart = Instant.now(); + try { + boolean success = queryStrategy.execute( + parameters.from(), + parameters.to(), + arriveBy, + queryCount + ); + var elapsed = Duration.between(queryStart, Instant.now()); + LOG.info( + "Warmup query #{} completed in {}.", + queryCount, + DurationUtils.durationToStr(elapsed) + ); + return success; + } catch (Exception e) { + var elapsed = Duration.between(queryStart, Instant.now()); + LOG.info( + "Warmup query #{} failed in {}: {}", + queryCount, + DurationUtils.durationToStr(elapsed), + e.getMessage() + ); + LOG.debug("Warmup query #{} exception detail", queryCount, e); + return false; + } + } + + private boolean isHealthy() { + var status = updaterStatusProvider.get(); + return status == null || status.listUnprimedUpdaters().isEmpty(); + } +} diff --git a/application/src/main/java/org/opentripplanner/warmup/api/WarmupApi.java b/application/src/main/java/org/opentripplanner/warmup/api/WarmupApi.java new file mode 100644 index 00000000000..96cf590c2d9 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/warmup/api/WarmupApi.java @@ -0,0 +1,25 @@ +package org.opentripplanner.warmup.api; + +import org.opentripplanner.core.model.doc.DocumentedEnum; + +/** Which GraphQL API to use for warmup queries. */ +public enum WarmupApi implements DocumentedEnum { + TRANSMODEL("Use the TransModel GraphQL API for warmup queries."), + GTFS("Use the GTFS GraphQL API for warmup queries."); + + private final String description; + + WarmupApi(String description) { + this.description = description; + } + + @Override + public String typeDescription() { + return "Which GraphQL API to use for warmup queries."; + } + + @Override + public String enumValueDescription() { + return description; + } +} diff --git a/application/src/main/java/org/opentripplanner/warmup/api/WarmupParameters.java b/application/src/main/java/org/opentripplanner/warmup/api/WarmupParameters.java new file mode 100644 index 00000000000..e0aec217f97 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/warmup/api/WarmupParameters.java @@ -0,0 +1,23 @@ +package org.opentripplanner.warmup.api; + +import java.util.List; +import org.opentripplanner.street.geometry.WgsCoordinate; +import org.opentripplanner.street.model.StreetMode; + +/** + * Parameters for the application warmup feature. + *

+ * See the configuration for documentation on the parameter fields. + */ +public record WarmupParameters( + WarmupApi api, + WgsCoordinate from, + WgsCoordinate to, + List accessModes, + List egressModes +) { + public WarmupParameters { + accessModes = List.copyOf(accessModes); + egressModes = List.copyOf(egressModes); + } +} diff --git a/application/src/main/java/org/opentripplanner/warmup/configure/WarmupModule.java b/application/src/main/java/org/opentripplanner/warmup/configure/WarmupModule.java new file mode 100644 index 00000000000..ac553f9d2aa --- /dev/null +++ b/application/src/main/java/org/opentripplanner/warmup/configure/WarmupModule.java @@ -0,0 +1,39 @@ +package org.opentripplanner.warmup.configure; + +import dagger.Module; +import dagger.Provides; +import jakarta.inject.Singleton; +import javax.annotation.Nullable; +import org.opentripplanner.standalone.api.OtpServerRequestContext; +import org.opentripplanner.standalone.config.RouterConfig; +import org.opentripplanner.transit.service.TimetableRepository; +import org.opentripplanner.warmup.WarmupLauncher; +import org.opentripplanner.warmup.api.WarmupParameters; + +/** + * Dagger wiring for the application warmup feature. + *

+ * Provides the {@link WarmupParameters} binding (mapped from the JSON config section by {@code + * WarmupConfig}) and the {@link WarmupLauncher} that {@link + * org.opentripplanner.standalone.configure.ConstructApplication} uses to start the warmup thread + * after Raptor transit data and updaters have been set up. + */ +@Module +public class WarmupModule { + + @Provides + @Nullable + static WarmupParameters provideWarmupParameters(RouterConfig routerConfig) { + return routerConfig.warmupParameters(); + } + + @Provides + @Singleton + static WarmupLauncher provideWarmupLauncher( + @Nullable WarmupParameters parameters, + OtpServerRequestContext serverContext, + TimetableRepository timetableRepository + ) { + return new WarmupLauncher(parameters, serverContext, timetableRepository); + } +} diff --git a/application/src/main/java/org/opentripplanner/warmup/package.md b/application/src/main/java/org/opentripplanner/warmup/package.md new file mode 100644 index 00000000000..a750913b469 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/warmup/package.md @@ -0,0 +1,34 @@ +# Application Warmup + +Runs GraphQL trip queries in a background thread during OTP startup to warm up the application +(JIT compilation, GraphQL schema caches, routing data structures, etc.) before production traffic +arrives. + +## Lifecycle + +1. `ConstructApplication` obtains a `WarmupLauncher` from the Dagger factory + (`configure.WarmupModule` wires it) and calls `start()` after Raptor transit data is created + and updaters are configured. +2. A daemon thread sends sequential queries through the configured GraphQL API (TransModel or GTFS), + exercising the full stack: GraphQL parsing, data fetchers, routing (Raptor + A*), itinerary + filtering, and response serialization. +3. It alternates between depart-at / arrive-by and cycles through access/egress modes. +4. The thread stops when the health probe reports "UP" (all updaters primed). + +## Design + +The public API of the module lives in the `api` subpackage and exposes only value objects: +`WarmupParameters` (the configured parameter values) and `WarmupApi` (which GraphQL API to exercise). +`WarmupConfig` (in `standalone.config.routerconfig`) reads the JSON config section and produces a +`WarmupParameters` instance. + +`WarmupQueryStrategy` is the strategy interface with two implementations: +- `TransmodelWarmupQueryExecutor` -- builds and executes TransModel `trip` queries. +- `GtfsWarmupQueryExecutor` -- builds and executes GTFS `planConnection` queries. + +Each executor owns a `ModeCombinations` helper that holds the configured access/egress mode lists +and maps a running query counter to the next access/egress pair via modulo arithmetic. + +## Configuration + +Configured via the `warmup` section in `router-config.json`. See `WarmupConfig` for details. diff --git a/application/src/test/java/org/opentripplanner/standalone/config/routerconfig/WarmupConfigTest.java b/application/src/test/java/org/opentripplanner/standalone/config/routerconfig/WarmupConfigTest.java new file mode 100644 index 00000000000..9e690a991f4 --- /dev/null +++ b/application/src/test/java/org/opentripplanner/standalone/config/routerconfig/WarmupConfigTest.java @@ -0,0 +1,55 @@ +package org.opentripplanner.standalone.config.routerconfig; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.opentripplanner.standalone.config.framework.json.JsonSupport.jsonNodeForTest; + +import java.util.List; +import org.junit.jupiter.api.Test; +import org.opentripplanner.standalone.config.framework.json.NodeAdapter; +import org.opentripplanner.street.model.StreetMode; + +class WarmupConfigTest { + + @Test + void defaultModesWhenAbsent() { + var root = createNodeAdapter( + """ + { + warmup: { + from: { lat: 59.91, lon: 10.75 }, + to: { lat: 59.95, lon: 10.76 } + } + } + """ + ); + var config = WarmupConfig.mapWarmupConfig("warmup", root); + assertNotNull(config); + assertEquals(List.of(StreetMode.WALK, StreetMode.CAR_TO_PARK), config.accessModes()); + assertEquals(List.of(StreetMode.WALK, StreetMode.WALK), config.egressModes()); + } + + @Test + void mismatchedModeListSizesThrows() { + var root = createNodeAdapter( + """ + { + warmup: { + from: { lat: 59.91, lon: 10.75 }, + to: { lat: 59.95, lon: 10.76 }, + accessModes: ["WALK", "BIKE", "CAR_TO_PARK"], + egressModes: ["WALK", "BIKE"] + } + } + """ + ); + assertThrows(IllegalArgumentException.class, () -> + WarmupConfig.mapWarmupConfig("warmup", root) + ); + } + + private static NodeAdapter createNodeAdapter(String jsonText) { + return new NodeAdapter(jsonNodeForTest(jsonText), "Test"); + } +} diff --git a/application/src/test/java/org/opentripplanner/warmup/WarmupQueryValidationTest.java b/application/src/test/java/org/opentripplanner/warmup/WarmupQueryValidationTest.java new file mode 100644 index 00000000000..a6857db3b89 --- /dev/null +++ b/application/src/test/java/org/opentripplanner/warmup/WarmupQueryValidationTest.java @@ -0,0 +1,56 @@ +package org.opentripplanner.warmup; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import graphql.parser.Parser; +import graphql.schema.GraphQLSchema; +import graphql.validation.Validator; +import java.util.Locale; +import org.junit.jupiter.api.Test; +import org.opentripplanner._support.time.ZoneIds; +import org.opentripplanner.api.model.transit.DefaultFeedIdMapper; +import org.opentripplanner.apis.gtfs.SchemaFactory; +import org.opentripplanner.apis.support.graphql.injectdoc.ApiDocumentationProfile; +import org.opentripplanner.apis.transmodel.TransmodelGraphQLSchemaFactory; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.TransitTuningParametersTestFactory; +import org.opentripplanner.routing.api.request.RouteRequest; + +/** + * Validates that the warmup GraphQL queries are syntactically and structurally valid + * against the actual schemas. This catches field renames, removed arguments, or + * invalid enum values at build time rather than at runtime. + */ +class WarmupQueryValidationTest { + + private static final GraphQLSchema GTFS_SCHEMA = SchemaFactory.createSchemaWithDefaultInjection( + RouteRequest.defaultValue() + ); + + private static final GraphQLSchema TRANSMODEL_SCHEMA = new TransmodelGraphQLSchemaFactory( + RouteRequest.defaultValue(), + ZoneIds.OSLO, + TransitTuningParametersTestFactory.forTest(), + new DefaultFeedIdMapper(), + ApiDocumentationProfile.DEFAULT + ).create(); + + @Test + void transmodelQueryIsValid() { + var errors = new Validator().validateDocument( + TRANSMODEL_SCHEMA, + Parser.parse(TransmodelWarmupQueryExecutor.QUERY), + Locale.ROOT + ); + assertEquals(0, errors.size(), errors.toString()); + } + + @Test + void gtfsQueryIsValid() { + var errors = new Validator().validateDocument( + GTFS_SCHEMA, + Parser.parse(GtfsWarmupQueryExecutor.QUERY), + Locale.ROOT + ); + assertEquals(0, errors.size(), errors.toString()); + } +} diff --git a/application/src/test/resources/standalone/config/router-config.json b/application/src/test/resources/standalone/config/router-config.json index fd3d00cbf25..b454e817954 100644 --- a/application/src/test/resources/standalone/config/router-config.json +++ b/application/src/test/resources/standalone/config/router-config.json @@ -473,6 +473,13 @@ "maxPrimingIdleTime": "1s" } ], + "warmup": { + "api": "transmodel", + "from": { "lat": 59.9139, "lon": 10.7522 }, + "to": { "lat": 59.95, "lon": 10.76 }, + "accessModes": ["WALK", "CAR_TO_PARK"], + "egressModes": ["WALK", "WALK"] + }, "rideHailingServices": [ { "type": "uber-car-hailing", diff --git a/doc/user/RouterConfiguration.md b/doc/user/RouterConfiguration.md index ba4a481e6b4..b0083600435 100644 --- a/doc/user/RouterConfiguration.md +++ b/doc/user/RouterConfiguration.md @@ -31,51 +31,61 @@ A full list of them can be found in the [RouteRequest](RouteRequest.md). -| Config Parameter | Type | Summary | Req./Opt. | Default Value | Since | -|-------------------------------------------------------------------------------------------|:---------------------:|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:----------:|---------------|:-----:| -| [configVersion](#configVersion) | `string` | Deployment version of the *router-config.json*. | *Optional* | | 2.1 | -| [flex](sandbox/Flex.md) | `object` | Configuration for flex routing. | *Optional* | | 2.1 | -| gtfsApi | `object` | Configuration for the GTFS GraphQL API. | *Optional* | | 2.8 | -|    [tracingTags](#gtfsApi_tracingTags) | `string[]` | Used to group requests based on headers or query parameters when monitoring OTP. | *Optional* | | na | -| [ojpApi](sandbox/OjpApi.md) | `object` | Configuration for the OJP API. | *Optional* | | 2.9 | -| [rideHailingServices](sandbox/RideHailing.md) | `object[]` | Configuration for interfaces to external ride hailing services like Uber. | *Optional* | | 2.3 | -| [routingDefaults](RouteRequest.md) | `object` | The default parameters for the routing query. | *Optional* | | 2.0 | -| [server](#server) | `object` | Configuration for router server. | *Optional* | | 2.4 | -|    [apiDocumentationProfile](#server_apiDocumentationProfile) | `enum` | List of available custom documentation profiles. A profile is used to inject custom documentation like type and field description or a deprecated reason. Currently, ONLY the Transmodel API supports this feature. | *Optional* | `"default"` | 2.7 | -|    [apiProcessingTimeout](#server_apiProcessingTimeout) | `duration` | Maximum processing time for an API request | *Optional* | `"PT-1S"` | 2.4 | -|    [httpResponseTimeMetrics](#server_httpResponseTimeMetrics) | `object` | Configuration for HTTP response time metrics. | *Optional* | | 2.9 | -|    [traceParameters](#server_traceParameters) | `object[]` | Trace OTP request using HTTP request/response parameter(s) combined with logging. | *Optional* | | 2.4 | -|          generateIdIfMissing | `boolean` | If `true` a unique value is generated if no http request header is provided, or the value is missing. | *Optional* | `false` | 2.4 | -|          httpRequestHeader | `string` | The header-key to use when fetching the trace parameter value | *Optional* | | 2.4 | -|          httpResponseHeader | `string` | The header-key to use when saving the value back into the http response | *Optional* | | 2.4 | -|          [logKey](#server_traceParameters_0_logKey) | `string` | The log event key used. | *Optional* | | 2.4 | -| timetableUpdates | `object` | Global configuration for timetable updaters. | *Optional* | | 2.2 | -|    [maxSnapshotFrequency](#timetableUpdates_maxSnapshotFrequency) | `duration` | How long a snapshot should be cached. | *Optional* | `"PT1S"` | 2.2 | -|    purgeExpiredData | `boolean` | Should expired real-time data be purged from the graph. Apply to GTFS-RT and Siri updates. | *Optional* | `true` | 2.2 | -| [transit](#transit) | `object` | Configuration for transit searches with RAPTOR. | *Optional* | | na | -|    [iterationDepartureStepInSeconds](#transit_iterationDepartureStepInSeconds) | `integer` | Step for departure times between each RangeRaptor iterations. | *Optional* | `60` | na | -|    [maxNumberOfTransfers](#transit_maxNumberOfTransfers) | `integer` | This parameter is used to allocate enough memory space for Raptor. | *Optional* | `12` | na | -|    [maxSearchWindow](#transit_maxSearchWindow) | `duration` | Upper limit of the request parameter searchWindow. | *Optional* | `"PT24H"` | 2.4 | -|    [scheduledTripBinarySearchThreshold](#transit_scheduledTripBinarySearchThreshold) | `integer` | This threshold is used to determine when to perform a binary trip schedule search. | *Optional* | `50` | na | -|    [searchThreadPoolSize](#transit_searchThreadPoolSize) | `integer` | Split a travel search in smaller jobs and run them in parallel to improve performance. | *Optional* | `0` | na | -|    [transferCacheMaxSize](#transit_transferCacheMaxSize) | `integer` | The maximum number of distinct transfers parameters to cache pre-calculated transfers for. | *Optional* | `25` | na | -|    [dynamicSearchWindow](#transit_dynamicSearchWindow) | `object` | The dynamic search window coefficients used to calculate the EDT, LAT and SW. | *Optional* | | 2.1 | -|       [maxWindow](#transit_dynamicSearchWindow_maxWindow) | `duration` | Upper limit for the search-window calculation. | *Optional* | `"PT3H"` | 2.2 | -|       [minTransitTimeCoefficient](#transit_dynamicSearchWindow_minTransitTimeCoefficient) | `double` | The coefficient to multiply with `minTransitTime`. | *Optional* | `0.5` | 2.1 | -|       [minWaitTimeCoefficient](#transit_dynamicSearchWindow_minWaitTimeCoefficient) | `double` | The coefficient to multiply with `minWaitTime`. | *Optional* | `0.5` | 2.1 | -|       [minWindow](#transit_dynamicSearchWindow_minWindow) | `duration` | The constant minimum duration for a raptor-search-window. | *Optional* | `"PT40M"` | 2.2 | -|       [stepMinutes](#transit_dynamicSearchWindow_stepMinutes) | `integer` | Used to set the steps the search-window is rounded to. | *Optional* | `10` | 2.1 | -|    [pagingSearchWindowAdjustments](#transit_pagingSearchWindowAdjustments) | `duration[]` | The provided array of durations is used to increase the search-window for the next/previous page. | *Optional* | | na | -|    [stopBoardAlightDuringTransferCost](#transit_stopBoardAlightDuringTransferCost) | `enum map of integer` | Costs for boarding and alighting during transfers at stops with a given transfer priority. | *Optional* | | 2.0 | -|    [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 | -| [triasApi](sandbox/TriasApi.md) | `object` | Configuration for the TRIAS API. | *Optional* | | 2.8 | -| [updaters](Realtime-Updaters.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 | -| [vehicleRentalServiceDirectory](sandbox/VehicleRentalServiceDirectory.md) | `object` | Configuration for the vehicle rental service directory using GBFS v3 manifest. | *Optional* | | 2.0 | +| Config Parameter | Type | Summary | Req./Opt. | Default Value | Since | +|-------------------------------------------------------------------------------------------|:---------------------:|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:----------:|----------------|:-----:| +| [configVersion](#configVersion) | `string` | Deployment version of the *router-config.json*. | *Optional* | | 2.1 | +| [flex](sandbox/Flex.md) | `object` | Configuration for flex routing. | *Optional* | | 2.1 | +| gtfsApi | `object` | Configuration for the GTFS GraphQL API. | *Optional* | | 2.8 | +|    [tracingTags](#gtfsApi_tracingTags) | `string[]` | Used to group requests based on headers or query parameters when monitoring OTP. | *Optional* | | na | +| [ojpApi](sandbox/OjpApi.md) | `object` | Configuration for the OJP API. | *Optional* | | 2.9 | +| [rideHailingServices](sandbox/RideHailing.md) | `object[]` | Configuration for interfaces to external ride hailing services like Uber. | *Optional* | | 2.3 | +| [routingDefaults](RouteRequest.md) | `object` | The default parameters for the routing query. | *Optional* | | 2.0 | +| [server](#server) | `object` | Configuration for router server. | *Optional* | | 2.4 | +|    [apiDocumentationProfile](#server_apiDocumentationProfile) | `enum` | List of available custom documentation profiles. A profile is used to inject custom documentation like type and field description or a deprecated reason. Currently, ONLY the Transmodel API supports this feature. | *Optional* | `"default"` | 2.7 | +|    [apiProcessingTimeout](#server_apiProcessingTimeout) | `duration` | Maximum processing time for an API request | *Optional* | `"PT-1S"` | 2.4 | +|    [httpResponseTimeMetrics](#server_httpResponseTimeMetrics) | `object` | Configuration for HTTP response time metrics. | *Optional* | | 2.9 | +|    [traceParameters](#server_traceParameters) | `object[]` | Trace OTP request using HTTP request/response parameter(s) combined with logging. | *Optional* | | 2.4 | +|          generateIdIfMissing | `boolean` | If `true` a unique value is generated if no http request header is provided, or the value is missing. | *Optional* | `false` | 2.4 | +|          httpRequestHeader | `string` | The header-key to use when fetching the trace parameter value | *Optional* | | 2.4 | +|          httpResponseHeader | `string` | The header-key to use when saving the value back into the http response | *Optional* | | 2.4 | +|          [logKey](#server_traceParameters_0_logKey) | `string` | The log event key used. | *Optional* | | 2.4 | +| timetableUpdates | `object` | Global configuration for timetable updaters. | *Optional* | | 2.2 | +|    [maxSnapshotFrequency](#timetableUpdates_maxSnapshotFrequency) | `duration` | How long a snapshot should be cached. | *Optional* | `"PT1S"` | 2.2 | +|    purgeExpiredData | `boolean` | Should expired real-time data be purged from the graph. Apply to GTFS-RT and Siri updates. | *Optional* | `true` | 2.2 | +| [transit](#transit) | `object` | Configuration for transit searches with RAPTOR. | *Optional* | | na | +|    [iterationDepartureStepInSeconds](#transit_iterationDepartureStepInSeconds) | `integer` | Step for departure times between each RangeRaptor iterations. | *Optional* | `60` | na | +|    [maxNumberOfTransfers](#transit_maxNumberOfTransfers) | `integer` | This parameter is used to allocate enough memory space for Raptor. | *Optional* | `12` | na | +|    [maxSearchWindow](#transit_maxSearchWindow) | `duration` | Upper limit of the request parameter searchWindow. | *Optional* | `"PT24H"` | 2.4 | +|    [scheduledTripBinarySearchThreshold](#transit_scheduledTripBinarySearchThreshold) | `integer` | This threshold is used to determine when to perform a binary trip schedule search. | *Optional* | `50` | na | +|    [searchThreadPoolSize](#transit_searchThreadPoolSize) | `integer` | Split a travel search in smaller jobs and run them in parallel to improve performance. | *Optional* | `0` | na | +|    [transferCacheMaxSize](#transit_transferCacheMaxSize) | `integer` | The maximum number of distinct transfers parameters to cache pre-calculated transfers for. | *Optional* | `25` | na | +|    [dynamicSearchWindow](#transit_dynamicSearchWindow) | `object` | The dynamic search window coefficients used to calculate the EDT, LAT and SW. | *Optional* | | 2.1 | +|       [maxWindow](#transit_dynamicSearchWindow_maxWindow) | `duration` | Upper limit for the search-window calculation. | *Optional* | `"PT3H"` | 2.2 | +|       [minTransitTimeCoefficient](#transit_dynamicSearchWindow_minTransitTimeCoefficient) | `double` | The coefficient to multiply with `minTransitTime`. | *Optional* | `0.5` | 2.1 | +|       [minWaitTimeCoefficient](#transit_dynamicSearchWindow_minWaitTimeCoefficient) | `double` | The coefficient to multiply with `minWaitTime`. | *Optional* | `0.5` | 2.1 | +|       [minWindow](#transit_dynamicSearchWindow_minWindow) | `duration` | The constant minimum duration for a raptor-search-window. | *Optional* | `"PT40M"` | 2.2 | +|       [stepMinutes](#transit_dynamicSearchWindow_stepMinutes) | `integer` | Used to set the steps the search-window is rounded to. | *Optional* | `10` | 2.1 | +|    [pagingSearchWindowAdjustments](#transit_pagingSearchWindowAdjustments) | `duration[]` | The provided array of durations is used to increase the search-window for the next/previous page. | *Optional* | | na | +|    [stopBoardAlightDuringTransferCost](#transit_stopBoardAlightDuringTransferCost) | `enum map of integer` | Costs for boarding and alighting during transfers at stops with a given transfer priority. | *Optional* | | 2.0 | +|    [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 | +| [triasApi](sandbox/TriasApi.md) | `object` | Configuration for the TRIAS API. | *Optional* | | 2.8 | +| [updaters](Realtime-Updaters.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 | +| [vehicleRentalServiceDirectory](sandbox/VehicleRentalServiceDirectory.md) | `object` | Configuration for the vehicle rental service directory using GBFS v3 manifest. | *Optional* | | 2.0 | +| [warmup](#warmup) | `object` | Configure application warmup by running transit searches during startup. | *Optional* | | 2.10 | +|    [api](#warmup_api) | `enum` | Which GraphQL API to use for warmup queries. | *Optional* | `"transmodel"` | 2.10 | +|    [accessModes](#warmup_accessModes) | `string[]` | Access modes to cycle through in warmup queries. | *Optional* | | 2.10 | +|    [egressModes](#warmup_egressModes) | `string[]` | Egress modes to cycle through in warmup queries. | *Optional* | | 2.10 | +|    from | `object` | Origin location for warmup searches. | *Optional* | | 2.10 | +|       lat | `double` | Latitude of the origin. | *Required* | | 2.10 | +|       lon | `double` | Longitude of the origin. | *Required* | | 2.10 | +|    to | `object` | Destination location for warmup searches. | *Optional* | | 2.10 | +|       lat | `double` | Latitude of the destination. | *Required* | | 2.10 | +|       lon | `double` | Longitude of the destination. | *Required* | | 2.10 | @@ -482,6 +492,131 @@ Enforce rate limiting based on query complexity; Queries that return too much da Used to group requests when monitoring OTP. +

warmup

+ +**Since version:** `2.10` ∙ **Type:** `object` ∙ **Cardinality:** `Optional` +**Path:** / + +Configure application warmup by running transit searches during startup. + +When configured, OTP runs transit routing queries between the given locations +during startup. This warms up the application (JIT compilation, GraphQL schema +caches, routing data structures, etc.) before production traffic arrives. +Queries start after the Raptor transit data is created and stop when all updaters +are primed (the health endpoint would return "UP"). +If no updaters are configured, no warmup queries are run. + + +

api

+ +**Since version:** `2.10` ∙ **Type:** `enum` ∙ **Cardinality:** `Optional` ∙ **Default value:** `"transmodel"` +**Path:** /warmup +**Enum values:** `transmodel` | `gtfs` + +Which GraphQL API to use for warmup queries. + + - `transmodel` Use the TransModel GraphQL API for warmup queries. + - `gtfs` Use the GTFS GraphQL API for warmup queries. + + +

accessModes

+ +**Since version:** `2.10` ∙ **Type:** `string[]` ∙ **Cardinality:** `Optional` +**Path:** /warmup + +Access modes to cycle through in warmup queries. + +Ordered list of `StreetMode` values used as access modes. Each entry is paired with the egress mode at the same index. - `not-set` + - `walk` Walking some or all of the way of the route. + - `bike` Cycling for the entirety of the route or taking a bicycle onto the public transport and cycling from the arrival station to the destination. + + Taking a bicycle onto transit is only possible if information about the permission to do so is supplied in the source data. In GTFS this field + is called `bikesAllowed`. + - `bike-to-park` Leaving the bicycle at the departure station and walking from the arrival station to the destination. + This mode needs to be combined with at least one transit mode otherwise it behaves like an ordinary bicycle journey. + + _Prerequisite:_ Bicycle parking stations present in the OSM file and visible to OTP by enabling the property `staticBikeParkAndRide` during graph build. + - `bike-rental` Taking a rented, shared-mobility bike for part or the entirety of the route. + + _Prerequisite:_ Vehicle or station locations need to be added to OTP from dynamic data feeds. + See [Configuring GBFS](GBFS-Config.md) on how to add one. + - `scooter-rental` Walking to a scooter rental point, riding a scooter to a scooter rental drop-off point, and walking the rest of the way. + This can include scooter rental at fixed locations or free-floating services. + + _Prerequisite:_ Vehicle or station locations need to be added to OTP from dynamic data feeds. + See [Configuring GBFS](GBFS-Config.md) on how to add one. + - `car` Driving your own car the entirety of the route. + This can be combined with transit, where will return routes with a [Kiss & Ride](https://en.wikipedia.org/wiki/Park_and_ride#Kiss_and_ride_/_kiss_and_fly) component. + This means that the car is not parked in a permanent parking area but rather the passenger is dropped off (for example, at an airport) and the driver continues driving the car away from the drop off location. + - `car-to-park` Driving a car to the park-and-ride facilities near a station and taking publictransport. + This mode needs to be combined with at least one transit mode otherwise, it behaves like an ordinary car journey. + _Prerequisite:_ Park-and-ride areas near the stations need to be present in the OSM input file. + - `car-pickup` Walking to a pickup point along the road, driving to a drop-off point along the road, and walking the rest of the way.
This can include various taxi-services or kiss & ride. + - `car-rental` Walk to a car rental point, drive to a car rental drop-off point and walk the rest of the way. + This can include car rental at fixed locations or free-floating services. + + _Prerequisite:_ Vehicle or station locations need to be added to OTP from dynamic data feeds. + See [Configuring GBFS](GBFS-Config.md) on how to add one. + - `car-hailing` Using a car hailing app like Uber or Lyft to get to a train station or all the way to the destination. + + See [the sandbox documentation](sandbox/RideHailing.md) on how to configure it. + - `carpool` Carpool or rideshare with other passengers going in the same direction. + + This is the request mode for enabling carpooling in street route searches. + + Use this _street_ mode, if your data source for trips is SIRI, not GTFS static. + - `flexible` Encompasses all types of on-demand and flexible transportation for example GTFS Flex or NeTEx Flexible Stop Places. + + +

egressModes

+ +**Since version:** `2.10` ∙ **Type:** `string[]` ∙ **Cardinality:** `Optional` +**Path:** /warmup + +Egress modes to cycle through in warmup queries. + +Ordered list of `StreetMode` values used as egress modes. Each entry is paired with the access mode at the same index. - `not-set` + - `walk` Walking some or all of the way of the route. + - `bike` Cycling for the entirety of the route or taking a bicycle onto the public transport and cycling from the arrival station to the destination. + + Taking a bicycle onto transit is only possible if information about the permission to do so is supplied in the source data. In GTFS this field + is called `bikesAllowed`. + - `bike-to-park` Leaving the bicycle at the departure station and walking from the arrival station to the destination. + This mode needs to be combined with at least one transit mode otherwise it behaves like an ordinary bicycle journey. + + _Prerequisite:_ Bicycle parking stations present in the OSM file and visible to OTP by enabling the property `staticBikeParkAndRide` during graph build. + - `bike-rental` Taking a rented, shared-mobility bike for part or the entirety of the route. + + _Prerequisite:_ Vehicle or station locations need to be added to OTP from dynamic data feeds. + See [Configuring GBFS](GBFS-Config.md) on how to add one. + - `scooter-rental` Walking to a scooter rental point, riding a scooter to a scooter rental drop-off point, and walking the rest of the way. + This can include scooter rental at fixed locations or free-floating services. + + _Prerequisite:_ Vehicle or station locations need to be added to OTP from dynamic data feeds. + See [Configuring GBFS](GBFS-Config.md) on how to add one. + - `car` Driving your own car the entirety of the route. + This can be combined with transit, where will return routes with a [Kiss & Ride](https://en.wikipedia.org/wiki/Park_and_ride#Kiss_and_ride_/_kiss_and_fly) component. + This means that the car is not parked in a permanent parking area but rather the passenger is dropped off (for example, at an airport) and the driver continues driving the car away from the drop off location. + - `car-to-park` Driving a car to the park-and-ride facilities near a station and taking publictransport. + This mode needs to be combined with at least one transit mode otherwise, it behaves like an ordinary car journey. + _Prerequisite:_ Park-and-ride areas near the stations need to be present in the OSM input file. + - `car-pickup` Walking to a pickup point along the road, driving to a drop-off point along the road, and walking the rest of the way.
This can include various taxi-services or kiss & ride. + - `car-rental` Walk to a car rental point, drive to a car rental drop-off point and walk the rest of the way. + This can include car rental at fixed locations or free-floating services. + + _Prerequisite:_ Vehicle or station locations need to be added to OTP from dynamic data feeds. + See [Configuring GBFS](GBFS-Config.md) on how to add one. + - `car-hailing` Using a car hailing app like Uber or Lyft to get to a train station or all the way to the destination. + + See [the sandbox documentation](sandbox/RideHailing.md) on how to configure it. + - `carpool` Carpool or rideshare with other passengers going in the same direction. + + This is the request mode for enabling carpooling in street route searches. + + Use this _street_ mode, if your data source for trips is SIRI, not GTFS static. + - `flexible` Encompasses all types of on-demand and flexible transportation for example GTFS Flex or NeTEx Flexible Stop Places. + + @@ -961,6 +1096,25 @@ Used to group requests when monitoring OTP. "maxPrimingIdleTime" : "1s" } ], + "warmup" : { + "api" : "transmodel", + "from" : { + "lat" : 59.9139, + "lon" : 10.7522 + }, + "to" : { + "lat" : 59.95, + "lon" : 10.76 + }, + "accessModes" : [ + "WALK", + "CAR_TO_PARK" + ], + "egressModes" : [ + "WALK", + "WALK" + ] + }, "rideHailingServices" : [ { "type" : "uber-car-hailing",