Skip to content

Commit d725659

Browse files
rsynekCopilot
andauthored
chore: remove storage from REST (#2530)
Fixes TimefoldAI/timefold-solver-enterprise#591. - REST no longer directly uses storage - the logic coordinating storage and events moved to a separate service, `SolverWorkerFacade` - `SolverWorkerFacade` is now unit-tested - one fix along the way: `from-patch` used to catch `IllegalStateException`, but the patching utils throw `IllegalArgumentException` --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent 058c2e6 commit d725659

25 files changed

Lines changed: 1128 additions & 440 deletions

File tree

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
package ai.timefold.solver.service.definition.impl.solver;
2+
3+
import java.util.List;
4+
import java.util.Set;
5+
6+
import ai.timefold.solver.service.definition.api.ModelConfigOverrides;
7+
import ai.timefold.solver.service.definition.api.ModelInput;
8+
import ai.timefold.solver.service.definition.api.ModelOutput;
9+
import ai.timefold.solver.service.definition.api.domain.Configuration;
10+
import ai.timefold.solver.service.definition.api.domain.Metadata;
11+
import ai.timefold.solver.service.definition.api.domain.ModelInputPatchRequest;
12+
import ai.timefold.solver.service.definition.api.domain.ModelRequest;
13+
import ai.timefold.solver.service.definition.api.domain.ModelResponse;
14+
import ai.timefold.solver.service.definition.api.log.LogInfo;
15+
import ai.timefold.solver.service.definition.api.metrics.ModelInputMetrics;
16+
import ai.timefold.solver.service.definition.api.metrics.ModelOutputMetrics;
17+
import ai.timefold.solver.service.definition.api.rest.DatasetSelector;
18+
19+
/**
20+
* Facade used by the REST layer to interact with dataset storage and the solver worker,
21+
* without depending on storage or messaging internals directly.
22+
*/
23+
public interface SolverWorkerFacade {
24+
25+
/***
26+
* Starts solving an existing dataset identified by the given id.
27+
*
28+
* @param id the dataset identifier
29+
* @return the {@link Metadata} associated with the dataset that will be solved
30+
* @throws ai.timefold.solver.service.definition.internal.error.ItemNotFoundException
31+
* if no dataset exists for the given id
32+
*/
33+
<Score_> Metadata<Score_> solveDataset(String id);
34+
35+
/**
36+
* Creates a new dataset derived from an existing one (identified by {@code id}) without starting the solving process.
37+
* The resulting dataset is validated and computed asynchronously.
38+
*
39+
* @param id the identifier of the source (parent) dataset.
40+
* @param select whether to derive from the unsolved input or the solved output of the parent
41+
* @param runName a human-readable name for the new run
42+
* @param tags the set of tags to attach to the new dataset
43+
* @param configuration the configuration to apply; if {@code null}, the parent's configuration is reused
44+
* @return the {@link Metadata} of the newly created dataset
45+
*/
46+
<Score_, ModelConfigurationOverrides_ extends ModelConfigOverrides> Metadata<Score_> createDataset(String id,
47+
DatasetSelector select, String runName, Set<String> tags,
48+
Configuration<ModelConfigurationOverrides_> configuration);
49+
50+
/**
51+
* Creates a new dataset derived from an existing one (identified by {@code id}) and immediately schedules it to be
52+
* solved after validation/compute completes.
53+
*
54+
* @param id the identifier of the source (parent) dataset
55+
* @param select whether to derive from the unsolved input or the solved output of the parent
56+
* @param runName a human-readable name for the new run
57+
* @param tags the set of tags to attach to the new dataset
58+
* @param configuration the configuration to apply; if {@code null}, the parent's configuration is reused
59+
* @return the {@link Metadata} of the newly created dataset
60+
*/
61+
<Score_, ModelConfigurationOverrides_ extends ModelConfigOverrides> Metadata<Score_> createAndSolveDataset(String id,
62+
DatasetSelector select, String runName, Set<String> tags,
63+
Configuration<ModelConfigurationOverrides_> configuration);
64+
65+
/**
66+
* Creates a new dataset from the provided {@link ModelInput} without starting the solving process. The resulting
67+
* dataset is validated and computed asynchronously.
68+
*
69+
* @param runName a human-readable name for the new run
70+
* @param tags the set of tags to attach to the new dataset
71+
* @param modelInput the model input to store as the dataset problem
72+
* @param configuration the configuration to apply to the dataset
73+
* @return the {@link Metadata} of the newly created dataset
74+
*/
75+
<Score_, ModelConfigurationOverrides_ extends ModelConfigOverrides> Metadata<Score_> createDataset(String runName,
76+
Set<String> tags, ModelInput modelInput, Configuration<ModelConfigurationOverrides_> configuration);
77+
78+
/**
79+
* Creates a new dataset from the provided {@link ModelInput} and immediately schedules it to be solved after
80+
* validation/compute completes.
81+
*
82+
* @param runName a human-readable name for the new run
83+
* @param tags the set of tags to attach to the new dataset
84+
* @param modelInput the model input to store as the dataset problem
85+
* @param configuration the configuration to apply to the dataset
86+
* @return the {@link Metadata} of the newly created dataset
87+
*/
88+
<Score_, ModelConfigurationOverrides_ extends ModelConfigOverrides> Metadata<Score_> createAndSolveDataset(String runName,
89+
Set<String> tags, ModelInput modelInput,
90+
Configuration<ModelConfigurationOverrides_> configuration);
91+
92+
/**
93+
* Creates a new dataset by applying a JSON patch to an existing dataset's {@link ModelInput}, without starting
94+
* the solving process. The resulting dataset is validated and computed asynchronously.
95+
*
96+
* @param id the identifier of the source (parent) dataset
97+
* @param select whether to derive from the unsolved input or the solved output of the parent
98+
* @param runName a human-readable name for the new run
99+
* @param modelInputPatchRequest the patch request containing the JSON patch operations and an optional
100+
* {@link Configuration} override
101+
* @return the {@link Metadata} of the newly created dataset
102+
* @throws ai.timefold.solver.service.definition.internal.error.ItemNotFoundException
103+
* if no dataset exists for the given id
104+
*/
105+
<Score_, ModelConfigurationOverrides_ extends ModelConfigOverrides> Metadata<Score_> patchDataset(String id,
106+
DatasetSelector select, String runName,
107+
ModelInputPatchRequest<ModelConfigurationOverrides_> modelInputPatchRequest);
108+
109+
/**
110+
* Creates a new dataset by applying a JSON patch to an existing dataset's {@link ModelInput} and immediately
111+
* schedules it to be solved after validation/compute completes.
112+
*
113+
* @param id the identifier of the source (parent) dataset
114+
* @param select whether to derive from the unsolved input or the solved output of the parent
115+
* @param runName a human-readable name for the new run
116+
* @param modelInputPatchRequest the patch request containing the JSON patch operations and an optional
117+
* {@link Configuration} override
118+
* @return the {@link Metadata} of the newly created dataset
119+
* @throws ai.timefold.solver.service.definition.internal.error.ItemNotFoundException
120+
* if no dataset exists for the given id
121+
*/
122+
<Score_, ModelConfigurationOverrides_ extends ModelConfigOverrides> Metadata<Score_> patchAndSolveDataset(String id,
123+
DatasetSelector select, String runName,
124+
ModelInputPatchRequest<ModelConfigurationOverrides_> modelInputPatchRequest);
125+
126+
/**
127+
* Terminates the ongoing solve for the given dataset (if any) and returns the current model response
128+
*
129+
* @param id the dataset identifier
130+
* @return the {@link ModelResponse} for the dataset after termination
131+
*/
132+
<Score_, ModelOutput_ extends ModelOutput, InputMetrics_ extends ModelInputMetrics, OutputMetrics_ extends ModelOutputMetrics>
133+
ModelResponse<Score_, ModelOutput_, InputMetrics_, OutputMetrics_> terminate(String id);
134+
135+
/**
136+
* Returns the {@link Metadata} for the given dataset, or {@code null} if none exists.
137+
*
138+
* @param id the dataset identifier
139+
* @return the dataset metadata, or {@code null} if not found
140+
*/
141+
<Score_> Metadata<Score_> getMetadata(String id);
142+
143+
/**
144+
* Returns the effective (processed) {@link Configuration} of the given dataset.
145+
*
146+
* @param id the dataset identifier
147+
* @return the effective configuration, or {@code null} if not found
148+
*/
149+
<ModelConfigurationOverrides_ extends ModelConfigOverrides> Configuration<ModelConfigurationOverrides_>
150+
getConfiguration(String id);
151+
152+
/**
153+
* Returns the original (unprocessed) {@link Configuration} of the given dataset, as it was supplied by the caller
154+
* before any post-processing.
155+
*
156+
* @param id the dataset identifier
157+
* @return the unprocessed configuration, or {@code null}if not found
158+
*/
159+
<ModelConfigurationOverrides_ extends ModelConfigOverrides> Configuration<ModelConfigurationOverrides_>
160+
getUnprocessedConfiguration(String id);
161+
162+
/**
163+
* Returns the {@link ModelInput} originally submitted for the given dataset.
164+
*
165+
* @param id the dataset identifier
166+
* @return the model input, or {@code null} if not found
167+
*/
168+
ModelInput getModelInput(String id);
169+
170+
/**
171+
* Returns the {@link ModelInput} representing the solved state of the given dataset (i.e. planning entities updated
172+
* with the best solution).
173+
*
174+
* @param id the dataset identifier
175+
* @return the solved model input, or {@code null} if not available
176+
*/
177+
ModelInput getSolvedModelInput(String id);
178+
179+
/**
180+
* Returns a paginated list of dataset runs.
181+
*
182+
* @param pageNumber the zero-based page index
183+
* @param pageSize the number of runs per page
184+
* @return the list of {@link Metadata} entries for the requested page
185+
*/
186+
<Score_> List<Metadata<Score_>> listRuns(int pageNumber, int pageSize);
187+
188+
/**
189+
* Returns the {@link ModelResponse} for the given dataset.
190+
*
191+
* @param id the dataset identifier
192+
* @return the model response, or {@code null} if not available
193+
*/
194+
<Score_, ModelOutput_ extends ModelOutput, InputMetrics_ extends ModelInputMetrics, OutputMetrics_ extends ModelOutputMetrics>
195+
ModelResponse<Score_, ModelOutput_, InputMetrics_, OutputMetrics_> getModelResponse(String id);
196+
197+
/**
198+
* Returns the original {@link ModelRequest} for the given dataset.
199+
*
200+
* @param id the dataset identifier
201+
* @return the model request, or {@code null} if not found
202+
*/
203+
<ModelInput_ extends ModelInput, ModelConfigurationOverrides_ extends ModelConfigOverrides>
204+
ModelRequest<ModelInput_, ModelConfigurationOverrides_> getModelRequest(String id);
205+
206+
/**
207+
* Returns the solver logs associated with the given dataset.
208+
* The result combines the currently running pod's log file (if present) with any previously persisted log content
209+
*
210+
* @param id the dataset identifier
211+
* @return the aggregated {@link LogInfo}, or {@code null} if no logs are available
212+
*/
213+
LogInfo getLogs(String id);
214+
215+
}

service/json/src/main/java/ai/timefold/solver/service/json/internal/patch/JsonPatch.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public static JsonNode apply(ArrayNode patch, JsonNode source) {
3131
+ "an object or array is required");
3232
}
3333

34-
if (patch.size() == 0) {
34+
if (patch.isEmpty()) {
3535
return source;
3636
}
3737

@@ -61,7 +61,7 @@ protected static JsonNode perform(ObjectNode operation, JsonNode doc) {
6161
throw new IllegalArgumentException("Invalid \"path\" property: " + pathNode);
6262
}
6363
String path = pathNode.asText();
64-
if (path.length() != 0 && path.charAt(0) != '/') {
64+
if (!path.isEmpty() && path.charAt(0) != '/') {
6565
throw new IllegalArgumentException("Invalid \"path\" property: " + path);
6666
}
6767

@@ -166,7 +166,7 @@ protected static JsonNode add(JsonNode doc, String path, JsonNode value) {
166166
* @return the patched JSON document
167167
*/
168168
protected static JsonNode remove(JsonNode doc, String path) {
169-
if (path.equals("")) {
169+
if (path.isEmpty()) {
170170
if (doc.isObject()) {
171171
ObjectNode docObject = (ObjectNode) doc;
172172
docObject.removeAll();

service/maps/service-rest/src/main/java/ai/timefold/solver/service/maps/service/rest/impl/AbstractMapsModelAPIResource.java

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,8 @@
2020
import ai.timefold.solver.service.definition.api.metrics.ModelOutputMetrics;
2121
import ai.timefold.solver.service.definition.api.validation.Issue;
2222
import ai.timefold.solver.service.definition.api.validation.ModelValidator;
23+
import ai.timefold.solver.service.definition.impl.solver.SolverWorkerFacade;
2324
import ai.timefold.solver.service.definition.impl.validation.ValidationIssueTypeCatalog;
24-
import ai.timefold.solver.service.definition.internal.events.DatasetCreatedEvent;
25-
import ai.timefold.solver.service.definition.internal.events.DatasetValidateComputeCommand;
26-
import ai.timefold.solver.service.definition.internal.events.SolveStartCommand;
27-
import ai.timefold.solver.service.definition.internal.events.SolveTerminateCommand;
28-
import ai.timefold.solver.service.definition.internal.storage.AbstractStorageService;
2925
import ai.timefold.solver.service.maps.api.model.Waypoints;
3026
import ai.timefold.solver.service.maps.service.integration.impl.WaypointsService;
3127
import ai.timefold.solver.service.rest.impl.AbstractModelAPIResource;
@@ -36,15 +32,10 @@
3632
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
3733
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
3834
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
39-
import org.eclipse.microprofile.reactive.messaging.Emitter;
40-
41-
import com.fasterxml.jackson.databind.ObjectMapper;
4235

4336
import io.quarkus.runtime.annotations.RegisterForReflection;
44-
import io.smallrye.reactive.messaging.MutinyEmitter;
4537

4638
@RegisterForReflection
47-
@SuppressWarnings({ "rawtypes", "unchecked" })
4839
public class AbstractMapsModelAPIResource<ModelInput_ extends ModelInput, ModelOutput_ extends ModelOutput, ModelConfigurationOverrides_ extends ModelConfigOverrides, Score_ extends Score<?>, InputMetrics_ extends ModelInputMetrics, OutputMetrics_ extends ModelOutputMetrics, Justification_ extends ModelConstraintJustification, ValidationIssue_ extends Issue>
4940
extends
5041
AbstractModelAPIResource<ModelInput_, ModelOutput_, ModelConfigurationOverrides_, Score_, InputMetrics_, OutputMetrics_, Justification_, ValidationIssue_> {
@@ -56,17 +47,10 @@ public AbstractMapsModelAPIResource() {
5647
}
5748

5849
public AbstractMapsModelAPIResource(ModelValidator<ModelInput_, ModelConfigurationOverrides_> modelValidator,
59-
AbstractStorageService storageService,
60-
Emitter<DatasetCreatedEvent> datasetPostedEventEmitter,
61-
Emitter<DatasetValidateComputeCommand> datasetValidateComputeCommandEmitter,
62-
Emitter<SolveStartCommand> scheduleStartEmitter,
63-
MutinyEmitter<SolveTerminateCommand> scheduleTerminateEmitter,
64-
ObjectMapper mapper,
50+
SolverWorkerFacade solverWorkerFacade,
6551
ValidationIssueTypeCatalog validationIssueTypeCatalog,
6652
WaypointsService waypointsService) {
67-
super(modelValidator, storageService, datasetPostedEventEmitter,
68-
datasetValidateComputeCommandEmitter, scheduleStartEmitter, scheduleTerminateEmitter, mapper,
69-
validationIssueTypeCatalog);
53+
super(modelValidator, solverWorkerFacade, validationIssueTypeCatalog);
7054
this.waypointsService = waypointsService;
7155
}
7256

Original file line numberDiff line numberDiff line change
@@ -1,21 +1,12 @@
11
package ai.timefold.solver.service.quarkus.deployment.rest;
22

3-
import java.util.Map;
4-
53
import ai.timefold.solver.service.definition.api.validation.ModelValidator;
4+
import ai.timefold.solver.service.definition.impl.solver.SolverWorkerFacade;
65
import ai.timefold.solver.service.definition.impl.validation.ValidationIssueTypeCatalog;
7-
import ai.timefold.solver.service.definition.internal.events.DatasetCreatedEvent;
8-
import ai.timefold.solver.service.definition.internal.events.DatasetValidateComputeCommand;
9-
import ai.timefold.solver.service.definition.internal.events.SolveStartCommand;
10-
import ai.timefold.solver.service.definition.internal.events.SolveTerminateCommand;
11-
import ai.timefold.solver.service.definition.internal.events.SolverChannels;
12-
import ai.timefold.solver.service.definition.internal.storage.AbstractStorageService;
136
import ai.timefold.solver.service.quarkus.deployment.builditem.ModelComponentsBuildItem;
147

158
import org.jboss.jandex.DotName;
169

17-
import com.fasterxml.jackson.databind.ObjectMapper;
18-
1910
import io.quarkus.gizmo.SignatureBuilder;
2011
import io.quarkus.gizmo.Type;
2112
import io.quarkus.gizmo.Type.ParameterizedType;
@@ -46,50 +37,16 @@ public String constructorSignature(ModelComponentsBuildItem modelComponents) {
4637
.addParameterType(Type.parameterizedType(Type.classType(ModelValidator.class),
4738
Type.classType(modelComponents.getModelInput().name()),
4839
Type.classType(modelComponents.getModelConfigOverrides().name())))
49-
.addParameterType(Type.parameterizedType(Type.classType(AbstractStorageService.class),
50-
Type.classType(modelComponents.getModelInput().name()),
51-
Type.classType(modelComponents.getModelConfigOverrides().name()),
52-
Type.classType(modelComponents.getModelInputMetrics().name()),
53-
Type.classType(modelComponents.getModelOutputMetrics().name()),
54-
Type.classType(modelComponents.getModelOutput().name()),
55-
Type.classType(modelComponents.getModelScoreClass().name()),
56-
Type.classType(modelComponents.getModelConstraintJustification().name())))
57-
.addParameterType(
58-
Type.parameterizedType(Type.classType(EMITTER_CLASS_NAME),
59-
Type.classType(DatasetCreatedEvent.class)))
60-
.addParameterType(
61-
Type.parameterizedType(Type.classType(EMITTER_CLASS_NAME),
62-
Type.classType(DatasetValidateComputeCommand.class)))
63-
.addParameterType(
64-
Type.parameterizedType(Type.classType(EMITTER_CLASS_NAME),
65-
Type.classType(SolveStartCommand.class)))
66-
.addParameterType(
67-
Type.parameterizedType(Type.classType(MUTINY_EMITTER_CLASS_NAME),
68-
Type.classType(SolveTerminateCommand.class)))
69-
.addParameterType(Type.classType(ObjectMapper.class))
40+
.addParameterType(Type.classType(SolverWorkerFacade.class))
7041
.addParameterType(Type.classType(ValidationIssueTypeCatalog.class))
7142
.build();
7243
}
7344

7445
@Override
7546
public String[] constructorParameterTypes(ModelComponentsBuildItem modelComponents) {
7647
return new String[] { ModelValidator.class.getCanonicalName(),
77-
AbstractStorageService.class.getCanonicalName(),
78-
EMITTER_CLASS_NAME,
79-
EMITTER_CLASS_NAME,
80-
EMITTER_CLASS_NAME,
81-
MUTINY_EMITTER_CLASS_NAME,
82-
ObjectMapper.class.getCanonicalName(),
48+
SolverWorkerFacade.class.getCanonicalName(),
8349
ValidationIssueTypeCatalog.class.getCanonicalName(),
8450
};
8551
}
86-
87-
@Override
88-
public Map<String, Integer> channelConstructorParameterIndices() {
89-
return Map.of(
90-
SolverChannels.DATASET_CREATED, 2,
91-
SolverChannels.DATASET_VALIDATE_COMPUTE, 3,
92-
SolverChannels.START, 4,
93-
SolverChannels.TERMINATE, 5);
94-
}
9552
}

0 commit comments

Comments
 (0)