mvn clean org.openrewrite.maven:rewrite-maven-plugin:4.44.0:run -Drewrite.recipeArtifactCoordinates=org.optaplanner:optaplanner-migration:9.44.0.Final -Drewrite.activeRecipes=org.optaplanner.migration.ToLatest9
OptaPlanner’s public API classes are backwards compatible (per series), but users often also use impl classes (which are documented in the reference manual too). This upgrade recipe minimizes the pain to upgrade your code and to take advantage of the newest features in OptaPlanner.
Every upgrade note has an indication how likely your code will be affected by that change:
To upgrade from an older version, first apply the previous upgrade recipes. You will find the order of migration steps bellow:
The RHDM version differs from the OptaPlanner version:
RHDM version | OptaPlanner version |
---|---|
7.8 | 7.39 |
7.9 | 7.44 |
7.1 | 7.48 |
7.11 | 8.5 (and 7.52) |
7.12 | 8.11 (and 7.59) |
7.13 | 8.13 (and 7.67) |
Update your code in seconds, with optaplanner-migration
(an OpenRewrite recipe). Try it:
mvn clean org.openrewrite.maven:rewrite-maven-plugin:4.44.0:run -Drewrite.recipeArtifactCoordinates=org.optaplanner:optaplanner-migration:9.44.0.Final -Drewrite.activeRecipes=org.optaplanner.migration.ToLatest9
Note: The -Drewrite.recipeArtifactCoordinates might not work,
use the more verbose pom.xml
approach instead.
It only does upgrade steps with an Automated badge.
@InverseRelationShadowVariable
for non-chained planning variables@InverseRelationShadowVariable
is now also supported for non-chained planning variables,
in which case the inverse property must be Collection
(Set
or List
).
So it’s no longer needed to use a CustomShadowVariable
to implement the bi-directional relationship behaviour.
Before in *.java
:
public class CloudComputer {
...
@CustomShadowVariable(variableListenerClass = MyCustomInverseVariableListener.class,
sources = {@CustomShadowVariable.Source(entityClass = CloudProcess.class, variableName = "computer")})
public List<CloudProcess> getProcessList() {
return processList;
}
}
After in *.java
:
@PlanningEntity // Shadow variable only
public class CloudComputer {
...
@InverseRelationShadowVariable(sourceVariableName = "computer")
public List<CloudProcess> getProcessList() {
return processList;
}
}
Move.toString()
changedTo adhere to Java best practices,
which state that an Object.toString()
should identify an instance and be short
(instead of trying to verbalize its entire state),
the Move.toString()
methods have been modified to mention the old value too (as well as the entity and the new value).
Their notations have also been made consistent:
ChangeMove
: a {v1 -> v2}
SwapMove
: a {v1} <-> b {v2}
PillarChangeMove
: [a, b] {v1 -> v2}
PillarSwapMove
: [a, b] {v1} <-> [c, d, e] {v2}
TailChainSwapMove
: a3 {a2} <-tailChainSwap-> b1 {b0}
SubChainChangeMove
: [a2..a5] {a1 -> b0}
Reversing: [a2..a5] {a1 -reversing-> b0}
SubChainSwapMove
: [a2..a5] {a1} <-> [b1..b3] {b0}
Reversing: [a2..a5] {a1} <-reversing-> [b1..b3] {b0}
This mainly affects the logging output.
In the examples, the toString()
method of planning entities and planning values has been modified accordingly
to avoid mentioning the old value twice:
Before in *.java
:
public class CloudProcess {
...
public String toString() {
return processName + "@" + computer.getName();
}
}
After in *.java
:
public class CloudProcess {
...
public String toString() {
return processName;
}
}
<constructionHeuristic>
: default algorithm changedDefault parameter tweaked: an empty <constructionHeuristic>
changes its default algorithm from FIRST_FIT
to ALLOCATE_ENTITY_FROM_QUEUE
.
If no entity difficulty comparison and no planning value strength comparison is defined, the behavior is exactly the same.
Otherwise, the behaviour is FIRST_FIT_DECREASING
or WEAKEST_FIT(_DECREASING)
.
<solverBenchmarkBluePrintType>
: ALL_CONSTRUCTION_HEURISTIC_TYPES
renamedThe <solverBenchmarkBluePrintType>
ALL_CONSTRUCTION_HEURISTIC_TYPES
has been renamed to EVERY_CONSTRUCTION_HEURISTIC_TYPE
.
The old name ALL_CONSTRUCTION_HEURISTIC_TYPES
is deprecated and will be removed in 7.0.
Before in *BenchmarkConfig.xml
:
<solverBenchmarkBluePrintType>ALL_CONSTRUCTION_HEURISTIC_TYPES</solverBenchmarkBluePrintType>
After in *BenchmarkConfig.xml
:
<solverBenchmarkBluePrintType>EVERY_CONSTRUCTION_HEURISTIC_TYPE</solverBenchmarkBluePrintType>
addProblemFactChange()
resets time spent terminationsIn real-time planning, calling addProblemFactChange()
now resets the time spent terminations too.
It did already reset all other terminations (despite that the docs claimed otherwise).
The docs have also been fixed to reflect reality, which is also the desired behaviour by users.
addProblemFactChange()
keeps the ScoreDirector
In real-time planning, addProblemFactChange()
no longer causes the ScoreDirector
to be replaced
(so it no longer creates a new KieSession
) upon solver restart.
This might expose a hidden bug in your ProblemFactChange
implementation.
Enable environmentMode
FULL_ASSERT
and do a few addProblemFactChange()
calls to validate that there are no such bugs.
ValueRange
: new method isEmpty()
If you implemented a custom ValueRange
, also implement the method isEmpty()
.
Normally, you should not have any need for a custom ValueRange
, because ValueRangeFactory
supports all sensible ranges.
Before in *.java
:
public class MyDoubleValueRange extends AbstractUncountableValueRange<Double> {
...
}
After in *.java
:
public class MyDoubleValueRange extends AbstractUncountableValueRange<Double> {
...
@Override
public boolean isEmpty() {
return from == to;
}
}
If you use multiple planning variables, consider switching to the folded configuration.
Before in *SolverConfig.xml
and *BenchmarkConfig.xml
:
<changeMoveSelector>
<valueSelector>
<variableName>period</variableName>
</valueSelector>
</changeMoveSelector>
<changeMoveSelector>
<valueSelector>
<variableName>room</variableName>
</valueSelector>
</changeMoveSelector>
After in *SolverConfig.xml
and *BenchmarkConfig.xml
:
<changeMoveSelector/>
If you use multiple entity classes, consider switching to the folded configuration.
Before in *SolverConfig.xml
and *BenchmarkConfig.xml
:
<changeMoveSelector>
<entitySelector>
<entityClass>...CoachEntity</entityClass>
</entitySelector>
</changeMoveSelector>
<changeMoveSelector>
<entitySelector>
<entityClass>...ShuttleEntity</entityClass>
</entitySelector>
</changeMoveSelector>
<swapMoveSelector>
<entitySelector>
<entityClass>...CoachEntity</entityClass>
</entitySelector>
</swapMoveSelector>
<swapMoveSelector>
<entitySelector>
<entityClass>...ShuttleEntity</entityClass>
</entitySelector>
</swapMoveSelector>
After in *SolverConfig.xml
and *BenchmarkConfig.xml
:
<changeMoveSelector/>
<swapMoveSelector/>
If your planning solution has a superclass with planner annotations,
those will now be ignored (just like solution subclass annotations are ignored
and just like entity superclass or subclass annotations are ignored unless they are a declared planning entity class too).
Declare the @PlanningSolution
on the superclass instead, the solver will handle subclass instances gracefully
(presuming there are no planner annotations in the subclass).
Before in *.java
:
public abstract class ParentSolution {
@ValueRangeProvider(...)
public List<Computer> getComputers() {...}
}
@PlanningSolution
public class ChildSolution extends ParentSolution {...}
Before in *SolverConfig.xml
and *BenchmarkConfig.xml
:
<solutionClass>...ChildSolution</solutionClass>
After in *.java
:
@PlanningSolution
public abstract class ParentSolution {
@ValueRangeProvider(...)
public List<Computer> getComputers() {...}
}
public class ChildSolution extends ParentSolution {...}
After in *SolverConfig.xml
and *BenchmarkConfig.xml
:
<solutionClass>...ParentSolution</solutionClass>
VariableListener
that changes 2 shadow variables: use variableListenerRef
If a custom VariableListener
changes 2 shadow variables, use the new variableListenerRef
property accordingly
to indicate that the VariableListener
class of another shadow variable also updates this shadow variable:
Before in *.java
:
@PlanningVariable(...)
public Standstill getPreviousStandstill() {
return previousStandstill;
}
@CustomShadowVariable(variableListenerClass = TransportTimeAndCapacityUpdatingVariableListener.class,
sources = {@CustomShadowVariable.Source(variableName = "previousStandstill")})
public Integer getTransportTime() {
return transportTime;
}
@CustomShadowVariable(variableListenerClass = DummyListener.class, sources = ...)
public Integer getCapacity() {
return capacity;
}
After in *.java
:
@PlanningVariable(...)
public Standstill getPreviousStandstill() {
return previousStandstill;
}
@CustomShadowVariable(variableListenerClass = TransportTimeAndCapacityUpdatingVariableListener.class,
sources = {@CustomShadowVariable.Source(variableName = "previousStandstill")})
public Integer getTransportTime() {
return transportTime;
}
@CustomShadowVariable(variableListenerRef = @PlanningVariableReference(variableName = "transportTime"))
public Integer getCapacity() {
return capacity;
}
VariableListeners
no longer trigger chaoticallyVariableListeners
no longer trigger chaotically. This applies to both out of the box shadow variables and custom shadow variables.
Planner now guarantees that the first VariableListener
’s after*()
method triggers AFTER the last genuine variable has been modified.
This means a VariableListener
is no longer exposed to intermediate state.
The before*()
methods still trigger immediately (otherwise they would not be able to capture the source variable’s original state).
Furthermore, Planner guarantees this triggering in stages also for VariableListener
for a shadow variable that depend on earlier shadow variables.
Move
: doMove()
must call triggerVariableListeners()
If you have a custom Move
, its doMove()
method must now call scoreDirector.triggerVariableListeners()
at the end.
In practice, you should have extended AbstractMove
- which does the triggerVariableListeners()
call for you -
but you’ll need to rename your doMove()
method to doMoveOnGenuineVariables()
.
Before in *.java
:
public class CloudComputerChangeMove extends AbstractMove {
...
public void doMove(ScoreDirector scoreDirector) {
CloudBalancingMoveHelper.moveCloudComputer(scoreDirector, cloudProcess, toCloudComputer);
}
}
After in *.java
:
public class CloudComputerChangeMove extends AbstractMove {
...
protected void doMoveOnGenuineVariables(ScoreDirector scoreDirector) {
CloudBalancingMoveHelper.moveCloudComputer(scoreDirector, cloudProcess, toCloudComputer);
}
}
ProblemFactChange
: doChange()
must call triggerVariableListeners()
If you have a ProblemFactChange
, its doChange()
method must now call scoreDirector.triggerVariableListeners()
after every set of changes (before calling calculateScore()
or relying on shadow variables).
Before in *.java
:
public void deleteComputer(final CloudComputer computer) {
doProblemFactChange(new ProblemFactChange() {
public void doChange(ScoreDirector scoreDirector) {
CloudBalance cloudBalance = (CloudBalance) scoreDirector.getWorkingSolution();
// First remove the problem fact from all planning entities that use it
for (CloudProcess process : cloudBalance.getProcessList()) {
if (Objects.equals(process.getComputer(), computer)) {
scoreDirector.beforeVariableChanged(process, "computer");
process.setComputer(null);
scoreDirector.afterVariableChanged(process, "computer");
}
}
...
}
});
}
After in *.java
:
public void deleteComputer(final CloudComputer computer) {
doProblemFactChange(new ProblemFactChange() {
public void doChange(ScoreDirector scoreDirector) {
CloudBalance cloudBalance = (CloudBalance) scoreDirector.getWorkingSolution();
// First remove the problem fact from all planning entities that use it
for (CloudProcess process : cloudBalance.getProcessList()) {
if (Objects.equals(process.getComputer(), computer)) {
scoreDirector.beforeVariableChanged(process, "computer");
process.setComputer(null);
scoreDirector.afterVariableChanged(process, "computer");
}
}
scoreDirector.triggerVariableListeners();
...
}
});
}
CustomPhaseCommand
: changeWorkingSolution()
must call triggerVariableListeners()
If you have a CustomPhaseCommand
, its changeWorkingSolution()
method must now call scoreDirector.triggerVariableListeners()
after every set of changes (before calling calculateScore()
or relying on shadow variables).
Before in *.java
:
public class MyCustomPhase implements CustomPhaseCommand {
public void changeWorkingSolution(ScoreDirector scoreDirector) {
scoreDirector.beforeVariableChanged(processAssignment, "machine");
processAssignment.setMachine(machine);
scoreDirector.afterVariableChanged(processAssignment, "machine");
Score score = scoreDirector.calculateScore();
}
}
After in *.java
:
public class MyCustomPhase implements CustomPhaseCommand {
public void changeWorkingSolution(ScoreDirector scoreDirector) {
scoreDirector.beforeVariableChanged(processAssignment, "machine");
processAssignment.setMachine(machine);
scoreDirector.afterVariableChanged(processAssignment, "machine");
scoreDirector.triggerVariableListeners();
Score score = scoreDirector.calculateScore();
}
}
Move
: read shadow variables firstA custom Move
must now read any shadow variables it needs before its first beforeVariableChanged()
call.
It no longer needs to assign genuine variables to intermediate values to avoid errors in the VariableListeners
that update shadow variables.
All built-in moves that affect chained variables have been greatly simplified due to the new VariableListener
guarantee.
The constructor of ChainedChangeMove
, ChainedSwapMove
, SubChainChangeMove
and SubChainSwapMove
now require the SingletonInverseVariableSupply
parameter.
Before in *.java
:
return new ChainedChangeMove(entity, variableDescriptor, toValue);
After in *.java
:
SingletonInverseVariableSupply inverseVariableSupply = ((InnerScoreDirector) scoreDirector).getSupplyManager()
.demand(new SingletonInverseVariableDemand(variableDescriptor));
return new ChainedChangeMove(entity, variableDescriptor, inverseVariableSupply, toValue);
Furthermore, a ChainedSwapMove
’s constructor now requires a List
instead of Collection
of VariableDescriptor
s.
InnerScoreDirector
: getTrailingEntity()
removedThe method InnerScoreDirector.getTrailingEntity()
has been removed. Use SingletonInverseVariableSupply
instead.
One score rule can now change 2 score levels in its RHS.
Before in *.drl
:
rule "Costly and unfair: part 1"
when
// Complex pattern
then
scoreHolder.addMediumConstraintMatch(kcontext, -1); // Financial cost
end
rule "Costly and unfair: part 2"
when
// Complex pattern (duplication)
then
scoreHolder.addSoftConstraintMatch(kcontext, -1); // Employee happiness cost
end
After in *.drl
:
rule "Costly and unfair"
when
// Complex pattern
then
scoreHolder.addMediumConstraintMatch(kcontext, -1); // Financial cost
scoreHolder.addSoftConstraintMatch(kcontext, -1); // Employee happiness cost
end
Solver
from API: use SolverFactory.createEmpty()
If you build a Solver
entirely from API (not recommended - it’s better to load it partially from XML),
use SolverFactory.createEmpty()
and solverFactory.getSolverConfig()
accordingly.
Before in *.java
:
SolverConfig solverConfig = new SolverConfig();
...
return solverConfig.buildSolver();
After in *.java
:
SolverFactory solverFactory = SolverFactory.createEmpty();
SolverConfig solverConfig = solverFactory.getSolverConfig();
...
return solverFactory.buildSolver();