Skip to content

Commit 298d4ed

Browse files
feat: add a producer id to BestSolutionChangedEvent (#1913)
Co-authored-by: Lukáš Petrovický <[email protected]>
1 parent 5899f4f commit 298d4ed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+970
-229
lines changed

core/src/build/revapi-differences.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,33 @@
450450
"new": "class ai.timefold.solver.core.config.heuristic.selector.value.ValueSelectorConfig",
451451
"annotation": "@org.jspecify.annotations.NullMarked",
452452
"justification": "Update config"
453+
},
454+
{
455+
"ignore": true,
456+
"code": "java.field.removed",
457+
"old": "field java.util.EventObject.source @ ai.timefold.solver.core.api.solver.event.BestSolutionChangedEvent<Solution_>",
458+
"justification": "BestSolutionChangedEvent no longer extends java.util.EventObject"
459+
},
460+
{
461+
"ignore": true,
462+
"code": "java.method.removed",
463+
"old": "method java.lang.Object java.util.EventObject::getSource() @ ai.timefold.solver.core.api.solver.event.BestSolutionChangedEvent<Solution_>",
464+
"justification": "BestSolutionChangedEvent no longer extends java.util.EventObject"
465+
},
466+
{
467+
"ignore": true,
468+
"code": "java.class.noLongerInheritsFromClass",
469+
"old": "class ai.timefold.solver.core.api.solver.event.BestSolutionChangedEvent<Solution_>",
470+
"new": "class ai.timefold.solver.core.api.solver.event.BestSolutionChangedEvent<Solution_>",
471+
"justification": "BestSolutionChangedEvent no longer extends java.util.EventObject"
472+
},
473+
{
474+
"ignore": true,
475+
"code": "java.class.noLongerImplementsInterface",
476+
"old": "class ai.timefold.solver.core.api.solver.event.BestSolutionChangedEvent<Solution_>",
477+
"new": "class ai.timefold.solver.core.api.solver.event.BestSolutionChangedEvent<Solution_>",
478+
"interface": "java.io.Serializable",
479+
"justification": "BestSolutionChangedEvent no longer extends java.util.EventObject"
453480
}
454481
]
455482
}

core/src/main/java/ai/timefold/solver/core/api/solver/SolverJobBuilder.java

Lines changed: 69 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
import java.util.function.Function;
77

88
import ai.timefold.solver.core.api.domain.solution.PlanningSolution;
9+
import ai.timefold.solver.core.api.solver.event.FinalBestSolutionEvent;
10+
import ai.timefold.solver.core.api.solver.event.FirstInitializedSolutionEvent;
11+
import ai.timefold.solver.core.api.solver.event.NewBestSolutionEvent;
12+
import ai.timefold.solver.core.api.solver.event.SolverJobStartedEvent;
913

1014
import org.jspecify.annotations.NonNull;
1115
import org.jspecify.annotations.NullMarked;
@@ -57,41 +61,82 @@ public interface SolverJobBuilder<Solution_, ProblemId_> {
5761
SolverJobBuilder<Solution_, ProblemId_>
5862
withProblemFinder(@NonNull Function<? super ProblemId_, ? extends Solution_> problemFinder);
5963

64+
/**
65+
* As defined by {@link #withBestSolutionEventConsumer(Consumer)}.
66+
*
67+
* @deprecated Use {@link #withBestSolutionEventConsumer(Consumer)} instead.
68+
*/
69+
@Deprecated(forRemoval = true, since = "1.28.0")
70+
@NonNull
71+
default SolverJobBuilder<Solution_, ProblemId_>
72+
withBestSolutionConsumer(@NonNull Consumer<? super Solution_> bestSolutionConsumer) {
73+
return withBestSolutionEventConsumer(event -> bestSolutionConsumer.accept(event.solution()));
74+
}
75+
6076
/**
6177
* Sets the best solution consumer, which may be called multiple times during the solving process.
6278
* <p>
6379
* Don't apply any changes to the solution instance while the solver runs.
6480
* The solver's best solution instance is the same as the one in the event,
6581
* and any modifications may lead to solver corruption due to its internal reuse.
6682
*
67-
* @param bestSolutionConsumer called multiple times for each new best solution on a consumer thread
83+
* @param bestSolutionEventConsumer called multiple times for each new best solution on a consumer thread
6884
* @return this
6985
*/
7086
@NonNull
71-
SolverJobBuilder<Solution_, ProblemId_> withBestSolutionConsumer(@NonNull Consumer<? super Solution_> bestSolutionConsumer);
87+
SolverJobBuilder<Solution_, ProblemId_>
88+
withBestSolutionEventConsumer(@NonNull Consumer<NewBestSolutionEvent<Solution_>> bestSolutionEventConsumer);
89+
90+
/**
91+
* As defined by {@link #withFinalBestSolutionEventConsumer}.
92+
*
93+
* @deprecated Use {@link #withFinalBestSolutionEventConsumer(Consumer)} instead.
94+
*/
95+
@Deprecated(forRemoval = true, since = "1.28.0")
96+
@NonNull
97+
default SolverJobBuilder<Solution_, ProblemId_>
98+
withFinalBestSolutionConsumer(@NonNull Consumer<? super Solution_> finalBestSolutionConsumer) {
99+
return withFinalBestSolutionEventConsumer(event -> finalBestSolutionConsumer.accept(event.solution()));
100+
}
72101

73102
/**
74103
* Sets the final best solution consumer, which is called at the end of the solving process and returns the final
75104
* best solution.
76105
*
77-
* @param finalBestSolutionConsumer called only once at the end of the solving process on a consumer thread
106+
* @param finalBestSolutionEventConsumer called only once at the end of the solving process on a consumer thread
78107
* @return this
79108
*/
80109
@NonNull
81110
SolverJobBuilder<Solution_, ProblemId_>
82-
withFinalBestSolutionConsumer(@NonNull Consumer<? super Solution_> finalBestSolutionConsumer);
111+
withFinalBestSolutionEventConsumer(
112+
@NonNull Consumer<FinalBestSolutionEvent<Solution_>> finalBestSolutionEventConsumer);
83113

84114
/**
85-
* As defined by #withFirstInitializedSolutionConsumer(FirstInitializedSolutionConsumer).
115+
* As defined by {@link #withFirstInitializedSolutionEventConsumer(Consumer)}.
86116
*
87-
* @deprecated Use {@link #withFirstInitializedSolutionConsumer(FirstInitializedSolutionConsumer)} instead.
117+
* @deprecated Use {@link #withFirstInitializedSolutionEventConsumer(Consumer)} instead.
88118
*/
89119
@Deprecated(forRemoval = true, since = "1.19.0")
90120
@NonNull
91121
default SolverJobBuilder<Solution_, ProblemId_>
92122
withFirstInitializedSolutionConsumer(@NonNull Consumer<? super Solution_> firstInitializedSolutionConsumer) {
93-
return withFirstInitializedSolutionConsumer(
94-
(solution, isTerminatedEarly) -> firstInitializedSolutionConsumer.accept(solution));
123+
return withFirstInitializedSolutionEventConsumer(event -> firstInitializedSolutionConsumer.accept(event.solution()));
124+
}
125+
126+
/**
127+
* As defined by {@link #withFirstInitializedSolutionEventConsumer(Consumer)}.
128+
*
129+
* @deprecated Use {@link #withFirstInitializedSolutionEventConsumer(Consumer)} instead.
130+
*
131+
* @param firstInitializedSolutionConsumer called only once before starting the first Local Search phase
132+
* @return this
133+
*/
134+
@Deprecated(forRemoval = true, since = "1.28.0")
135+
@NonNull
136+
default SolverJobBuilder<Solution_, ProblemId_> withFirstInitializedSolutionConsumer(
137+
@NonNull FirstInitializedSolutionConsumer<? super Solution_> firstInitializedSolutionConsumer) {
138+
return withFirstInitializedSolutionEventConsumer(
139+
event -> firstInitializedSolutionConsumer.accept(event.solution(), event.isTerminatedEarly()));
95140
}
96141

97142
/**
@@ -100,20 +145,31 @@ public interface SolverJobBuilder<Solution_, ProblemId_> {
100145
* First initialized solution is the solution at the end of the last phase
101146
* that immediately precedes the first local search phase.
102147
*
103-
* @param firstInitializedSolutionConsumer called only once before starting the first Local Search phase
148+
* @param firstInitializedSolutionEventConsumer called only once before starting the first Local Search phase
104149
* @return this
105150
*/
106-
@NonNull
107-
SolverJobBuilder<Solution_, ProblemId_> withFirstInitializedSolutionConsumer(
108-
@NonNull FirstInitializedSolutionConsumer<? super Solution_> firstInitializedSolutionConsumer);
151+
SolverJobBuilder<Solution_, ProblemId_> withFirstInitializedSolutionEventConsumer(
152+
@NonNull Consumer<FirstInitializedSolutionEvent<Solution_>> firstInitializedSolutionEventConsumer);
153+
154+
/**
155+
* As defined by {@link #withSolverJobStartedEventConsumer(Consumer)}.
156+
*
157+
* @deprecated Use {@link #withSolverJobStartedEventConsumer(Consumer)} instead.
158+
*/
159+
@Deprecated(forRemoval = true, since = "1.28.0")
160+
default SolverJobBuilder<Solution_, ProblemId_>
161+
withSolverJobStartedConsumer(Consumer<? super Solution_> solverJobStartedConsumer) {
162+
return withSolverJobStartedEventConsumer(event -> solverJobStartedConsumer.accept(event.solution()));
163+
}
109164

110165
/**
111166
* Sets the consumer for when the solver starts its solving process.
112167
*
113168
* @param solverJobStartedConsumer never null, called only once when the solver is starting the solving process
114169
* @return this, never null
115170
*/
116-
SolverJobBuilder<Solution_, ProblemId_> withSolverJobStartedConsumer(Consumer<? super Solution_> solverJobStartedConsumer);
171+
SolverJobBuilder<Solution_, ProblemId_>
172+
withSolverJobStartedEventConsumer(Consumer<SolverJobStartedEvent<Solution_>> solverJobStartedConsumer);
117173

118174
/**
119175
* Sets the custom exception handler.
@@ -166,5 +222,4 @@ interface FirstInitializedSolutionConsumer<Solution_> {
166222
void accept(Solution_ solution, boolean isTerminatedEarly);
167223

168224
}
169-
170225
}

core/src/main/java/ai/timefold/solver/core/api/solver/SolverManager.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ public interface SolverManager<Solution_, ProblemId_> extends AutoCloseable {
147147
.withProblemId(problemId)
148148
.withProblem(problem);
149149
if (finalBestSolutionConsumer != null) {
150-
builder.withFinalBestSolutionConsumer(finalBestSolutionConsumer);
150+
builder.withFinalBestSolutionEventConsumer(event -> finalBestSolutionConsumer.accept(event.solution()));
151151
}
152152
return builder.run();
153153
}
@@ -297,7 +297,7 @@ public interface SolverManager<Solution_, ProblemId_> extends AutoCloseable {
297297
return solveBuilder()
298298
.withProblemId(problemId)
299299
.withProblem(problem)
300-
.withBestSolutionConsumer(bestSolutionConsumer)
300+
.withBestSolutionEventConsumer(event -> bestSolutionConsumer.accept(event.solution()))
301301
.run();
302302
}
303303

core/src/main/java/ai/timefold/solver/core/api/solver/event/BestSolutionChangedEvent.java

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package ai.timefold.solver.core.api.solver.event;
22

3-
import java.util.EventObject;
4-
53
import ai.timefold.solver.core.api.domain.solution.PlanningSolution;
64
import ai.timefold.solver.core.api.score.Score;
75
import ai.timefold.solver.core.api.solver.Solver;
@@ -16,9 +14,9 @@
1614
* @param <Solution_> the solution type, the class with the {@link PlanningSolution} annotation
1715
*/
1816
// TODO In Solver 2.0, maybe convert this to an interface.
19-
public class BestSolutionChangedEvent<Solution_> extends EventObject {
20-
17+
public class BestSolutionChangedEvent<Solution_> {
2118
private final Solver<Solution_> solver;
19+
private final EventProducerId producerId;
2220
private final long timeMillisSpent;
2321
private final Solution_ newBestSolution;
2422
private final Score newBestScore;
@@ -31,7 +29,7 @@ public class BestSolutionChangedEvent<Solution_> extends EventObject {
3129
@Deprecated(forRemoval = true, since = "1.22.0")
3230
public BestSolutionChangedEvent(@NonNull Solver<Solution_> solver, long timeMillisSpent,
3331
@NonNull Solution_ newBestSolution, @NonNull Score newBestScore) {
34-
this(solver, timeMillisSpent, newBestSolution, newBestScore, true);
32+
this(solver, EventProducerId.unknown(), timeMillisSpent, newBestSolution, newBestScore, true);
3533
}
3634

3735
/**
@@ -42,8 +40,19 @@ public BestSolutionChangedEvent(@NonNull Solver<Solution_> solver, long timeMill
4240
public BestSolutionChangedEvent(@NonNull Solver<Solution_> solver, long timeMillisSpent,
4341
@NonNull Solution_ newBestSolution, @NonNull Score newBestScore,
4442
boolean isNewBestSolutionInitialized) {
45-
super(solver);
43+
this(solver, EventProducerId.unknown(), timeMillisSpent, newBestSolution, newBestScore, isNewBestSolutionInitialized);
44+
}
45+
46+
/**
47+
* @param timeMillisSpent {@code >= 0L}
48+
* @deprecated Users should not manually construct instances of this event.
49+
*/
50+
@Deprecated(forRemoval = true, since = "1.28.0")
51+
public BestSolutionChangedEvent(@NonNull Solver<Solution_> solver, EventProducerId producerId, long timeMillisSpent,
52+
@NonNull Solution_ newBestSolution, @NonNull Score newBestScore,
53+
boolean isNewBestSolutionInitialized) {
4654
this.solver = solver;
55+
this.producerId = producerId;
4756
this.timeMillisSpent = timeMillisSpent;
4857
this.newBestSolution = newBestSolution;
4958
this.newBestScore = newBestScore;
@@ -58,6 +67,13 @@ public long getTimeMillisSpent() {
5867
return timeMillisSpent;
5968
}
6069

70+
/**
71+
* @return A {@link EventProducerId} identifying what generated the event
72+
*/
73+
public EventProducerId getProducerId() {
74+
return producerId;
75+
}
76+
6177
/**
6278
* Note that:
6379
* <ul>
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package ai.timefold.solver.core.api.solver.event;
2+
3+
import java.util.OptionalInt;
4+
5+
import ai.timefold.solver.core.api.solver.change.ProblemChange;
6+
import ai.timefold.solver.core.config.solver.SolverConfig;
7+
import ai.timefold.solver.core.impl.phase.NoChangePhase;
8+
import ai.timefold.solver.core.impl.phase.PhaseType;
9+
import ai.timefold.solver.core.impl.phase.event.PhaseEventProducerId;
10+
import ai.timefold.solver.core.impl.solver.event.SolveEventProducerId;
11+
12+
import org.jspecify.annotations.NullMarked;
13+
14+
/**
15+
* Identifies the producer of a {@link BestSolutionChangedEvent}.
16+
*/
17+
@NullMarked
18+
public interface EventProducerId {
19+
/**
20+
* An unique string identifying what produced the event, either of the form
21+
* "Event" where "Event" is a string describing the event that cause the update (like "Solving started")
22+
* or "Phase (index)", where "Phase" is a string identifying the type of phase (like "Construction Heuristics")
23+
* and index is the index of the phase in the {@link SolverConfig#getPhaseConfigList()}.
24+
*
25+
* @return An unique string identifying what produced the event.
26+
*/
27+
String producerId();
28+
29+
/**
30+
* A (non-unique) string describing what produced the event.
31+
* Events from different phases of the same type (for example,
32+
* when multiple Construction Heuristics are configured)
33+
* will return the same value.
34+
*
35+
* @return A (non-unique) string describing what produced the event.
36+
*/
37+
String simpleProducerName();
38+
39+
/**
40+
* If present, the index of the phase that produced the event in the {@link SolverConfig#getPhaseConfigList()}.
41+
* Is absent when the producer does not correspond to a phase, for instance,
42+
* an event triggered after {@link ProblemChange} were processed.
43+
*
44+
* @return The index of the corresponding phase in {@link SolverConfig#getPhaseConfigList()},
45+
* or {@link OptionalInt#empty()} if there is no corresponding phase.
46+
*/
47+
OptionalInt phaseIndex();
48+
49+
static EventProducerId unknown() {
50+
return SolveEventProducerId.UNKNOWN;
51+
}
52+
53+
static EventProducerId solvingStarted() {
54+
return SolveEventProducerId.SOLVING_STARTED;
55+
}
56+
57+
static EventProducerId problemChange() {
58+
return SolveEventProducerId.PROBLEM_CHANGE;
59+
}
60+
61+
/**
62+
* @deprecated Deprecated on account of {@link NoChangePhase} having no use.
63+
*/
64+
@Deprecated(forRemoval = true, since = "1.28.0")
65+
static EventProducerId noChange(int phaseIndex) {
66+
return new PhaseEventProducerId(PhaseType.NO_CHANGE, phaseIndex);
67+
}
68+
69+
static EventProducerId constructionHeuristic(int phaseIndex) {
70+
return new PhaseEventProducerId(PhaseType.CONSTRUCTION_HEURISTIC, phaseIndex);
71+
}
72+
73+
static EventProducerId localSearch(int phaseIndex) {
74+
return new PhaseEventProducerId(PhaseType.LOCAL_SEARCH, phaseIndex);
75+
}
76+
77+
static EventProducerId exhaustiveSearch(int phaseIndex) {
78+
return new PhaseEventProducerId(PhaseType.EXHAUSTIVE_SEARCH, phaseIndex);
79+
}
80+
81+
static EventProducerId partitionedSearch(int phaseIndex) {
82+
return new PhaseEventProducerId(PhaseType.PARTITIONED_SEARCH, phaseIndex);
83+
}
84+
85+
static EventProducerId customPhase(int phaseIndex) {
86+
return new PhaseEventProducerId(PhaseType.CUSTOM_PHASE, phaseIndex);
87+
}
88+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package ai.timefold.solver.core.api.solver.event;
2+
3+
import ai.timefold.solver.core.api.domain.solution.PlanningSolution;
4+
5+
/**
6+
* Delivered in a consumer thread at the end of the solving process and contains the final {@link PlanningSolution best
7+
* solution} found.
8+
*
9+
* @param <Solution_> the solution type, the class with the {@link PlanningSolution} annotation
10+
*/
11+
public interface FinalBestSolutionEvent<Solution_> {
12+
/**
13+
* @return the {@link PlanningSolution best solution} found by the solver
14+
*/
15+
Solution_ solution();
16+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package ai.timefold.solver.core.api.solver.event;
2+
3+
import ai.timefold.solver.core.api.domain.solution.PlanningSolution;
4+
5+
/**
6+
* Delivered in a consumer thread at the beginning of the actual optimization process.
7+
* First initialized solution is the solution at the end of the last phase
8+
* that immediately precedes the first local search phase.
9+
*
10+
* @param <Solution_>
11+
*/
12+
public interface FirstInitializedSolutionEvent<Solution_> {
13+
/**
14+
* @return The {@link PlanningSolution initialized solution}
15+
*/
16+
Solution_ solution();
17+
18+
/**
19+
* @return A {@link EventProducerId} identifying what generated the event
20+
*/
21+
EventProducerId producerId();
22+
23+
/**
24+
* @return True if the solver was terminated early, false otherwise
25+
*/
26+
boolean isTerminatedEarly();
27+
}

0 commit comments

Comments
 (0)