-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Add application warmup feature to run routing queries during startup #7509
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
Merged
vpaturet
merged 6 commits into
opentripplanner:dev-2.x
from
entur:jvm-warmup-during-startup
Apr 23, 2026
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
f630a9a
Add application warmup feature to run routing queries during startup
vpaturet 197f992
Move warmup feature into its own top-level package
vpaturet 8f24b93
Make WarmupParameters a record produced by WarmupConfig
vpaturet 6d26463
Merge remote-tracking branch 'opentripplanner/dev-2.x' into jvm-warmu…
vpaturet 4af3536
Apply review feedback: DRY coordinate parsing and shorten WarmupParam…
vpaturet bfe7115
Apply review feedback: package layout, strategy naming, and log forma…
vpaturet File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
122 changes: 122 additions & 0 deletions
122
...cation/src/main/java/org/opentripplanner/standalone/config/routerconfig/WarmupConfig.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<StreetMode> DEFAULT_ACCESS_MODES = List.of( | ||
| StreetMode.WALK, | ||
| StreetMode.CAR_TO_PARK | ||
| ); | ||
| private static final List<StreetMode> 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<StreetMode> 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<StreetMode> 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); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
110 changes: 110 additions & 0 deletions
110
application/src/main/java/org/opentripplanner/warmup/GtfsWarmupQueryExecutor.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<StreetMode> accessModes, | ||
| List<StreetMode> 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<String, Object> 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()) | ||
| ); | ||
| } | ||
| } |
44 changes: 44 additions & 0 deletions
44
application/src/main/java/org/opentripplanner/warmup/ModeCombinations.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<StreetMode> accessModes; | ||
| private final List<StreetMode> egressModes; | ||
|
|
||
| ModeCombinations(List<StreetMode> accessModes, List<StreetMode> 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()); | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm confused - how is this related to the updaters?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This feature takes advantage of the time window between the end of the raptor data indexing and the priming of the last updater to run warm-up queries. During this time window, the system is mostly busy with I/O and CPU is available for warming up Raptor/A*/GraphQL.
The signal to stop the warm-up is the priming of the last updater.