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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -158,6 +163,11 @@ public GtfsApiParameters gtfsApiParameters() {
return gtfsApi;
}

@Nullable
public WarmupParameters warmupParameters() {
return warmupParameters;
}

public NodeAdapter asNodeAdapter() {
return root;
}
Expand Down
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.
Copy link
Copy Markdown
Member

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?

Copy link
Copy Markdown
Contributor Author

@vpaturet vpaturet Apr 23, 2026

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.

"""
)
.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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -99,6 +101,7 @@
VehicleRentalRepositoryModule.class,
VehicleRentalServiceModule.class,
ViaModule.class,
WarmupModule.class,
WorldEnvelopeServiceModule.class,
}
)
Expand Down Expand Up @@ -166,6 +169,8 @@ public interface ConstructApplicationFactory {

DeduplicatorService deduplicatorService();

WarmupLauncher warmupLauncher();

@Component.Builder
interface Builder {
@BindsInstance
Expand Down
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())
);
}
}
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());
}
}
Loading
Loading