Skip to content
Open
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 @@ -6,6 +6,8 @@ public class Headers {

public static final String X_MAPS_PROVIDER_HEADER = "X-TF-MAPS-PROVIDER";

public static final String X_MAPS_LOCATION_HEADER = "X-TF-MAPS-LOCATION";

public static final String X_MAPS_CACHE_ID = "X-TF-MAPS-CACHE-ID";

public static final String X_MAPS_RESPONSE_CHUNK_BYTES = "X-TF-MAPS-CHUNK-BYTES";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package ai.timefold.solver.service.definition.internal;

import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class MapEnrichmentContext {

private String resolvedMapLocation;

public void setResolvedMapLocation(String location) {
this.resolvedMapLocation = location;
}

public String getResolvedMapLocation() {
return this.resolvedMapLocation;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ public abstract sealed class AbstractDatasetEvent extends AbstractEvent

private final Metadata metadata;

private String resolvedMapLocation;

protected AbstractDatasetEvent(Metadata metadata) {
super(metadata.getId());
// Safe copy of the run to avoid external modifications.
Expand All @@ -20,6 +22,14 @@ public Metadata getMetadata() {
return metadata;
}

public String getResolvedMapLocation() {
return resolvedMapLocation;
}

public void setResolvedMapLocation(String resolvedMapLocation) {
this.resolvedMapLocation = resolvedMapLocation;
}

@Override
public String toString() {
return getClass().getSimpleName() + "{" +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@ public class EnvironmentVars {
*/
public static final String ENV_TIMEFOLD_TENANT_NAME = "AI_TIMEFOLD_TENANT_NAME";

/**
* Configured map-service location: either a concrete region (e.g. {@code us-georgia}) or the
* sentinel {@link #MAP_SERVICE_LOCATION_AUTO_SELECT} for runtime region resolution.
*/
public static final String ENV_TIMEFOLD_PLATFORM_MAP_SERVICE_LOCATION = "AI_TIMEFOLD_PLATFORM_MAP_SERVICE_LOCATION";

/**
* Sentinel value for {@link #ENV_TIMEFOLD_PLATFORM_MAP_SERVICE_LOCATION}: tells the maps-service
* to auto-select the region at request time based on the locations being routed.
*/
public static final String MAP_SERVICE_LOCATION_AUTO_SELECT = "auto-select";

/**
* Kubernetes API specific environment variables that are set based on execution information like pod and node
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import jakarta.inject.Inject;

import ai.timefold.solver.service.definition.api.enrichment.SolverModelEnricher;
import ai.timefold.solver.service.definition.internal.MapEnrichmentContext;
import ai.timefold.solver.service.definition.internal.error.ErrorCodes;
import ai.timefold.solver.service.definition.internal.error.TimefoldRuntimeException;
import ai.timefold.solver.service.maps.api.model.Location;
Expand All @@ -30,10 +31,14 @@ public class TravelTimeMatrixEnricher implements SolverModelEnricher<LocationsAw

private final MapServiceOptionsSupplier optionsSupplier;

private final MapEnrichmentContext mapEnrichmentContext;

@Inject
public TravelTimeMatrixEnricher(MapService mapService, MapServiceOptionsSupplier optionsSupplier) {
public TravelTimeMatrixEnricher(MapService mapService, MapServiceOptionsSupplier optionsSupplier,
MapEnrichmentContext mapEnrichmentContext) {
this.mapService = mapService;
this.optionsSupplier = optionsSupplier;
this.mapEnrichmentContext = mapEnrichmentContext;
}

@Retry(maxRetries = 5, delay = 1, delayUnit = ChronoUnit.SECONDS, abortOn = {
Expand Down Expand Up @@ -61,6 +66,7 @@ public LocationsAwareSolverModel<?> enrich(LocationsAwareSolverModel<?> solverMo
location.setDistanceMatrix(travelTimeAndDistance.travelTimeAndDistance().distance());
});
solverModel.setLocationsNotInMap(convertIdxToLocations(travelTimeAndDistance.locationsNotInMapIdx(), locations));
mapEnrichmentContext.setResolvedMapLocation(travelTimeAndDistance.resolvedMapLocation());
return solverModel;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@
import ai.timefold.solver.service.maps.service.integration.internal.model.TravelTimeAndDistance;

public record CacheItem(TravelTimeAndDistance travelTimeAndDistance, List<Location> locations, String hash,
List<Integer> locationsOutOfMap) {
List<Integer> locationsOutOfMap, String resolvedMapLocation) {

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import static ai.timefold.solver.service.definition.internal.Headers.X_MAPS_INVALIDATE_MATRIX_HEADER;
import static ai.timefold.solver.service.definition.internal.Headers.X_MAPS_LOCATIONS_CHUNK_BYTES;
import static ai.timefold.solver.service.definition.internal.Headers.X_MAPS_LOCATIONS_NOT_IN_MAP;
import static ai.timefold.solver.service.definition.internal.Headers.X_MAPS_LOCATION_HEADER;
import static ai.timefold.solver.service.definition.internal.Headers.X_MAPS_MATRIX_HASH_HEADER;
import static ai.timefold.solver.service.definition.internal.Headers.X_MAPS_PROVIDER_HEADER;
import static ai.timefold.solver.service.definition.internal.Headers.X_MAPS_RESPONSE_CHUNK_BYTES;
Expand Down Expand Up @@ -135,8 +136,9 @@ public TravelTimeAndDistanceWithMetadata getTravelTimeAndDistance(List<Location>
// If there are no updates, return from cache
LOGGER.info("Distance matrix in cache is up-to-date, returning from cache");
assertLocationsAreInCache(locations);
return new TravelTimeAndDistanceWithMetadata(travelTimeAndDistanceSingleItemCache.get().travelTimeAndDistance(),
travelTimeAndDistanceSingleItemCache.get().locationsOutOfMap());
CacheItem cached = travelTimeAndDistanceSingleItemCache.get();
return new TravelTimeAndDistanceWithMetadata(cached.travelTimeAndDistance(),
cached.locationsOutOfMap(), cached.resolvedMapLocation());
} else {
// If there are updates, process them and update cache
LOGGER.info("Distance matrix in cache is not up-to-date, processing updates");
Expand Down Expand Up @@ -205,7 +207,8 @@ private TravelTimeAndDistanceWithMetadata getFromCacheOrRequest(List<Location> l
if (travelTimeAndDistanceSingleItemCache.isInCache(id)) {
LOGGER.info("Distance matrix without location set name in cache, returning from cache");
CacheItem cacheItem = travelTimeAndDistanceSingleItemCache.get();
return new TravelTimeAndDistanceWithMetadata(cacheItem.travelTimeAndDistance(), cacheItem.locationsOutOfMap());
return new TravelTimeAndDistanceWithMetadata(cacheItem.travelTimeAndDistance(), cacheItem.locationsOutOfMap(),
cacheItem.resolvedMapLocation());
}

// If it does not exist, request from maps-service and store by hash of locations
Expand Down Expand Up @@ -237,6 +240,7 @@ private TravelTimeAndDistanceWithMetadata getAndStoreInCache(List<Location> loca
private TravelTimeAndDistanceWithMetadata processResponseAndStoreInCache(Response response, String localCacheId) {
String matrixHash = response.getHeaderString(X_MAPS_MATRIX_HASH_HEADER);
String provider = response.getHeaderString(X_MAPS_PROVIDER_HEADER);
String resolvedMapLocation = response.getHeaderString(X_MAPS_LOCATION_HEADER);
String tenant = response.getHeaderString(X_TENANT_ID_HEADER);
String cacheId = response.getHeaderString(X_MAPS_CACHE_ID);
String locationsNotInMapString = response.getHeaderString(X_MAPS_LOCATIONS_NOT_IN_MAP);
Expand All @@ -256,11 +260,13 @@ private TravelTimeAndDistanceWithMetadata processResponseAndStoreInCache(Respons
throw new IllegalArgumentException("No provider found to convert travel time and distance response.");
}

TravelTimeAndDistanceWithMetadata travelTimeAndDistance =
TravelTimeAndDistanceWithMetadata raw =
convertResponse(provider, chunkBytes, responseLocations, data, locationsNotInMap);
TravelTimeAndDistanceWithMetadata travelTimeAndDistance = new TravelTimeAndDistanceWithMetadata(
raw.travelTimeAndDistance(), raw.locationsNotInMapIdx(), resolvedMapLocation);
travelTimeAndDistanceSingleItemCache.put(localCacheId,
new CacheItem(travelTimeAndDistance.travelTimeAndDistance(), responseLocations, matrixHash,
locationsNotInMap));
locationsNotInMap, resolvedMapLocation));
return travelTimeAndDistance;

} catch (IllegalDistanceResponseException e) {
Expand All @@ -275,6 +281,7 @@ private TravelTimeAndDistanceWithMetadata processResponseAndStoreInCache(Respons
private TravelTimeAndDistanceWithMetadata processUpdateAndStoreInCache(Response response, String locationSetName) {
String matrixHash = response.getHeaderString(X_MAPS_MATRIX_HASH_HEADER);
String provider = response.getHeaderString(X_MAPS_PROVIDER_HEADER);
String resolvedMapLocation = response.getHeaderString(X_MAPS_LOCATION_HEADER);
String tenant = response.getHeaderString(X_TENANT_ID_HEADER);
String cacheId = response.getHeaderString(X_MAPS_CACHE_ID);
String locationsNotInMapString = response.getHeaderString(X_MAPS_LOCATIONS_NOT_IN_MAP);
Expand All @@ -294,15 +301,18 @@ private TravelTimeAndDistanceWithMetadata processUpdateAndStoreInCache(Response
throw new IllegalArgumentException("No provider found to convert travel time and distance update.");
}

TravelTimeAndDistanceWithMetadata travelTimeAndDistance =
TravelTimeAndDistanceWithMetadata raw =
convertUpdate(provider, chunkBytes, responseLocations, data, cacheItem.locationsOutOfMap(),
locationsNotInMap);
String effectiveMapLocation = resolvedMapLocation != null ? resolvedMapLocation : cacheItem.resolvedMapLocation();
TravelTimeAndDistanceWithMetadata travelTimeAndDistance = new TravelTimeAndDistanceWithMetadata(
raw.travelTimeAndDistance(), raw.locationsNotInMapIdx(), effectiveMapLocation);

List<Location> newLocations = Stream.concat(cacheItem.locations().stream(), responseLocations.stream()).toList();
if (locationSetName != null && matrixHash != null) {
travelTimeAndDistanceSingleItemCache.put(locationSetName,
new CacheItem(travelTimeAndDistance.travelTimeAndDistance(), newLocations, matrixHash,
locationsNotInMap));
locationsNotInMap, effectiveMapLocation));
}
return travelTimeAndDistance;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
package ai.timefold.solver.service.maps.service.client.api;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import jakarta.inject.Inject;

import ai.timefold.solver.service.definition.internal.MapEnrichmentContext;
import ai.timefold.solver.service.maps.api.model.Location;
import ai.timefold.solver.service.maps.api.model.travel.TravelDistance;
import ai.timefold.solver.service.maps.api.model.travel.TravelTime;
import ai.timefold.solver.service.maps.service.client.util.RemoteMapServiceConfigurationProfile;
import ai.timefold.solver.service.maps.service.client.util.SampleModel;
import ai.timefold.solver.service.maps.service.integration.api.LocationsAwareSolverModel;
import ai.timefold.solver.service.maps.service.test.api.MapServiceApiWiremockExtensions;
import ai.timefold.solver.service.maps.service.test.impl.DistanceGetUpdateResponseTransformer;
import ai.timefold.solver.service.maps.service.test.impl.HaversineDistanceResponseTransformer;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
Expand All @@ -28,6 +32,9 @@ public class TravelTimeMatrixEnricherTest {
@Inject
TravelTimeMatrixEnricher enricher;

@Inject
MapEnrichmentContext mapEnrichmentContext;

@Test
void testRemoteConnectionWithMapServer() {
Location l1 = new Location(0, 0);
Expand All @@ -41,6 +48,8 @@ void testRemoteConnectionWithMapServer() {
Assertions.assertThat(enrich.getLocations().getFirst().getTravelTimeTo(l2)).isEqualTo(TravelTime.of(11322L));
Assertions.assertThat(enrich.getLocations().getFirst().getDistanceTo(l2)).isEqualTo(TravelDistance.of(157249L));
Assertions.assertThat(enrich.getLocationsNotInMap()).isEmpty();
Assertions.assertThat(mapEnrichmentContext.getResolvedMapLocation())
.isEqualTo(HaversineDistanceResponseTransformer.RESOLVED_MAP_LOCATION);
}

@Test
Expand Down Expand Up @@ -97,6 +106,8 @@ void testDistanceMatrixWithUpdates() {
Assertions.assertThat(enrich.getLocations().get(1).getDistanceTo(l1)).isEqualTo(TravelDistance.of(157249L));
Assertions.assertThat(enrich.getLocations().get(2).getDistanceTo(l4)).isEqualTo(TravelDistance.of(157178L));
Assertions.assertThat(enrich.getLocationsNotInMap()).isEmpty();
Assertions.assertThat(mapEnrichmentContext.getResolvedMapLocation())
.isEqualTo(HaversineDistanceResponseTransformer.RESOLVED_MAP_LOCATION);
}

@Test
Expand Down Expand Up @@ -153,4 +164,27 @@ void testDistanceMatrixWithLocationsOutOfMap() {
Assertions.assertThat(enrich.getLocations().get(2).getDistanceTo(l4)).isEqualTo(TravelDistance.ZERO);
}

@Test
void updatePathFallsBackToCachedMapLocationWhenHeaderIsMissing() {
// First enrich populates the cache for "with-updates" via the POST full-matrix path,
// which emits X_MAPS_LOCATION_HEADER and cache stores resolvedMapLocation="us-georgia".
SampleModel seed = new SampleModel(DistanceGetUpdateResponseTransformer.UPDATE_AWARE_LOCATION_SET_NAME,
DistanceGetUpdateResponseTransformer.UPDATE_OLD_LOCATIONS);
enricher.enrich(seed);
Assertions.assertThat(mapEnrichmentContext.getResolvedMapLocation())
.isEqualTo(HaversineDistanceResponseTransformer.RESOLVED_MAP_LOCATION);

// Second enrich adds a new location and cache hit triggers the GET-update path.
// The transformer returns a 200 update WITHOUT X_MAPS_LOCATION_HEADER, so
// processUpdateAndStoreInCache must fall back to the value stored in the CacheItem.
List<Location> withNewLocation = new ArrayList<>(DistanceGetUpdateResponseTransformer.UPDATE_OLD_LOCATIONS);
withNewLocation.addAll(DistanceGetUpdateResponseTransformer.UPDATE_NEW_LOCATIONS);
SampleModel updated = new SampleModel(DistanceGetUpdateResponseTransformer.UPDATE_AWARE_LOCATION_SET_NAME,
withNewLocation);
enricher.enrich(updated);

Assertions.assertThat(mapEnrichmentContext.getResolvedMapLocation())
.isEqualTo(HaversineDistanceResponseTransformer.RESOLVED_MAP_LOCATION);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,10 @@
import java.util.List;

public record TravelTimeAndDistanceWithMetadata(TravelTimeAndDistance travelTimeAndDistance,
List<Integer> locationsNotInMapIdx) {
List<Integer> locationsNotInMapIdx, String resolvedMapLocation) {

public TravelTimeAndDistanceWithMetadata(TravelTimeAndDistance travelTimeAndDistance,
List<Integer> locationsNotInMapIdx) {
this(travelTimeAndDistance, locationsNotInMapIdx, null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,10 @@
import java.io.InputStream;
import java.util.List;

public record TravelTimeAndDistanceMatrixResponse(InputStream response, List<Integer> locationsOutOfMapIndexes) {
public record TravelTimeAndDistanceMatrixResponse(InputStream response, List<Integer> locationsOutOfMapIndexes,
String resolvedMapLocation) {

public TravelTimeAndDistanceMatrixResponse(InputStream response, List<Integer> locationsOutOfMapIndexes) {
this(response, locationsOutOfMapIndexes, null);
}
}
Loading
Loading