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 @@ -70,6 +70,11 @@ public final class Metadata<Score_> implements Status<Score_> {
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private String failureMessage;

@Schema(nullable = true,
description = "The map-service region resolved when location is auto-select.")
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private String resolvedMapLocation;

public Metadata() {
this((String) null);
}
Expand Down Expand Up @@ -102,6 +107,7 @@ public Metadata(Metadata<Score_> metadata) {
this.parentId = metadata.parentId;
this.originId = metadata.originId;
this.failureMessage = metadata.failureMessage;
this.resolvedMapLocation = metadata.resolvedMapLocation;
}

public String getId() {
Expand Down Expand Up @@ -260,6 +266,14 @@ public void setFailureMessage(String failureMessage) {
this.failureMessage = failureMessage;
}

public String getResolvedMapLocation() {
return resolvedMapLocation;
}

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

@Override
public void solvingStarted() {
if (solverStatus != SolvingStatus.DATASET_COMPUTED
Expand Down
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,24 @@
package ai.timefold.solver.model.definition.internal;

import jakarta.enterprise.context.ApplicationScoped;

/**
* Side-channel used by the maps enricher to publish the resolved map-service location to the
* SolverWorker, which then writes it onto the dataset's {@code Metadata} so it propagates through
* insight events.
*/
@ApplicationScoped
public class MapEnrichmentContext {

private String resolvedMapLocation;

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

public String consumeResolvedMapLocation() {
String value = this.resolvedMapLocation;
this.resolvedMapLocation = null;
return value;
}
}
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.model.definition.api.enrichment.SolverModelEnricher;
import ai.timefold.solver.model.definition.internal.MapEnrichmentContext;
import ai.timefold.solver.model.definition.internal.error.ErrorCodes;
import ai.timefold.solver.model.definition.internal.error.TimefoldRuntimeException;
import ai.timefold.solver.model.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,5 +6,6 @@
import ai.timefold.solver.model.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.model.definition.internal.Headers.X_MAPS_INVALIDATE_MATRIX_HEADER;
import static ai.timefold.solver.model.definition.internal.Headers.X_MAPS_LOCATIONS_CHUNK_BYTES;
import static ai.timefold.solver.model.definition.internal.Headers.X_MAPS_LOCATIONS_NOT_IN_MAP;
import static ai.timefold.solver.model.definition.internal.Headers.X_MAPS_LOCATION_HEADER;
import static ai.timefold.solver.model.definition.internal.Headers.X_MAPS_MATRIX_HASH_HEADER;
import static ai.timefold.solver.model.definition.internal.Headers.X_MAPS_PROVIDER_HEADER;
import static ai.timefold.solver.model.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
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
import ai.timefold.solver.model.definition.api.validation.ModelValidator;
import ai.timefold.solver.model.definition.api.validation.ValidationBuilder;
import ai.timefold.solver.model.definition.api.validation.dto.ValidationResult;
import ai.timefold.solver.model.definition.internal.MapEnrichmentContext;
import ai.timefold.solver.model.definition.internal.error.ErrorCodes;
import ai.timefold.solver.model.definition.internal.error.ItemNotFoundException;
import ai.timefold.solver.model.definition.internal.error.TimefoldRuntimeException;
Expand Down Expand Up @@ -119,6 +120,8 @@ public class SolverWorker {

private final SolverModelEnrichmentDirectorService enrichmentDirectorService;

private final MapEnrichmentContext mapEnrichmentContext;

private final TerminationService terminationService;

private final Emitter<DatasetValidatedEvent> datasetValidatedEventEmitter;
Expand Down Expand Up @@ -173,6 +176,7 @@ public SolverWorker(@ConfigProperty(name = "timefold.application.name") Optional
ModelConvertorBase modelConvertor,
SolverModelEnricherService enricherService,
SolverModelEnrichmentDirectorService enrichmentDirectorService,
MapEnrichmentContext mapEnrichmentContext,
TerminationService terminationService,
ShutdownExecutor shutdownExecutor,
ShutdownOnTerminate shutdownOnTerminate,
Expand All @@ -199,6 +203,7 @@ public SolverWorker(@ConfigProperty(name = "timefold.application.name") Optional
this.modelConvertor = (ModelConvertor) modelConvertor;
this.enricherService = enricherService;
this.enrichmentDirectorService = enrichmentDirectorService;
this.mapEnrichmentContext = mapEnrichmentContext;
this.terminationService = terminationService;
this.shutdownExecutor = shutdownExecutor;
this.shutdownOnTerminate = shutdownOnTerminate;
Expand Down Expand Up @@ -349,6 +354,7 @@ private void computeOutputs(String id) {
ModelConfig modelConfig = Configuration.getSafeModelConfig(configuration);

SolverModel solverModel = createSolverModel(modelInput, modelConfig);
applyResolvedMapLocation(metadata);
solutionManager.update(solverModel);

// Store the updated solution
Expand Down Expand Up @@ -563,6 +569,7 @@ protected SolverModel notifyOnStart(String id, ModelInput modelInput, ModelOutpu
Metadata metadata = storageService.getMetadata(id);

SolverModel solverModel = createSolverModel(modelInput, modelConfig, modelOutput);
applyResolvedMapLocation(metadata);
if (metadata.getSolverStatus() == SolvingStatus.DATASET_COMPUTED
|| metadata.getSolverStatus() == SolvingStatus.SOLVING_SCHEDULED) {
metadata.solvingStarted();
Expand Down Expand Up @@ -612,6 +619,18 @@ private SolverModel enrichModel(SolverModel solverModel) {
: enricherService.enrich(solverModel);
}

private void applyResolvedMapLocation(Metadata metadata) {
String resolved = mapEnrichmentContext.consumeResolvedMapLocation();
if (resolved == null || metadata == null) {
return;
}
metadata.setResolvedMapLocation(resolved);
String configuredLocation = System.getenv(EnvironmentVars.ENV_TIMEFOLD_PLATFORM_MAP_SERVICE_LOCATION);
if (EnvironmentVars.MAP_SERVICE_LOCATION_AUTO_SELECT.equalsIgnoreCase(configuredLocation)) {
LOGGER.info("Auto-select map resolved to '{}' for dataset {}.", resolved, metadata.getId());
}
}

protected void notifyOnInit(String id, SolverModel solverModel, boolean isTerminatedEarly,
EventProducerId eventProducerId) {
LOGGER.debug("Notify run init for id {}", id);
Expand Down
Loading