Upgrade to the latest version
Timefold Solver public APIs are backwards compatible, but users often also use internal Solver classes which are not guaranteed to stay compatible. This upgrade recipe minimizes the pain to upgrade your code and to take advantage of the newest features in Timefold Solver.
1. Automatic upgrade to the latest version
For many of the upgrade steps mentioned later, we actually provide an upgrade tool that can automatically apply those changes to Java files. This tool is based on OpenRewrite and can be run as a Maven or Gradle plugin. To run the tool, execute the following command in your project directory:
-
Maven
-
Gradle
mvn org.openrewrite.maven:rewrite-maven-plugin:5.40.2:run -Drewrite.recipeArtifactCoordinates=ai.timefold.solver:timefold-solver-migration:1.14.0 -Drewrite.activeRecipes=ai.timefold.solver.migration.ToLatest
curl https://timefold.ai/product/upgrade/upgrade-timefold.gradle > upgrade-timefold.gradle ; gradle -Dorg.gradle.jvmargs=-Xmx2G --init-script upgrade-timefold.gradle rewriteRun -DtimefoldSolverVersion=1.14.0 ; rm upgrade-timefold.gradle
Having done that, you can check the local changes and commit them. Note that none of the upgrade steps could be automatically applied, and it may still be worth your while to read the rest the upgrade recipe below.
For the time being, Kotlin users need to follow the upgrade recipe and apply the steps manually.
2. Manual upgrade recipe
Every upgrade note indicates how likely your code will be affected by that change:
-
Automated: Can be applied automatically using our upgrade tooling.
-
Major: Likely to affect your code.
-
Minor: Unlikely to affect your code, especially if you stick to public API.
-
Recommended: Does not affect backwards compatibility, but you probably want to be aware.
The upgrade recipe often lists the changes as they apply to Java code. We kindly ask Kotlin and Python users to translate the changes accordingly.
2.1. Upgrade from 1.12.0 to 1.13.0
@ConstraintConfiguration
deprecated
Details
@ConstraintConfiguration
has been deprecated and will be removed in a future major version.
Please use constraint weight overrides instead.
Before in *ConstraintProvider.java
:
...
.penalizeConfigurable()
.asConstraint("maxHoursWorked");
...
After in *ConstraintProvider.java
:
...
.penalize(ONE_SOFT)
.asConstraint("maxHoursWorked");
...
Before in *Solution.java
:
...
@ConstraintConfiguration
private MyConstraintConfiguration myConstraintConfiguration;
...
After in *Solution.java
:
...
ConstraintWeightOverrides<HardSoftScore> constraintWeightOverrides;
...
constraintWeightOverrides = ConstraintWeightOverrides.of(
Map.of(
"maxHoursWorked", HardSoftScore.ofSoft(10)
)
);
...
Constraint packages have been deprecated
In the solver, constraints are uniquely identified by their package and name. We have now deprecated the package name and we recommend to keep constraint names unique instead.
Before in *ConstraintProvider.java
:
...
.penalize(ONE_SOFT)
.asConstraint("employees.paris", "maxHoursWorked");
...
After in *ConstraintProvider.java
:
...
.penalize(ONE_SOFT)
.asConstraint("employees.paris.maxHoursWorked");
...
While constraint packages are still supported, they will be removed in a future major version.
ConstraintCollectors.toMap()
now respects the optional merge function
In your constraints, the following code may now behave differently:
...
return constraintFactory.forEach(Entity.class)
.groupBy(
ConstraintCollectors.toMap(
entity -> entity.name(),
entity -> entity.id(),
(entityId1, entityId2) -> Math.max(entityId1, entityId2)
)
)
...
The final argument to the mapping collector is now respected, where previously it was wrongly ignored under certain conditions. This may result in the map being populated differently than before.
2.2. Upgrade from 1.9.0 to 1.10.0
Pinning unassigned entities now fails fast, unless allowed
The solver behavior has changed in the following situation:
-
There is a planning entity with a
@PlanningVariable
that does not allow unassigned values. -
And that planning entity is pinned.
-
And that variable is set to
null
, therefore unassigned.
This situation is both unlikely and erroneous. The solver is asked to require all variables to be assigned, but at the same time one variable is forced unassigned.
Before Timefold Solver 1.10.0, this would result in Construction Heuristics finishing with a negative init
score.
Starting with Timefold Solver 1.10.0, this situation will result in a runtime exception.
Read more about explicitly allowing unassigned values.
Enterprise Edition Maven Repository will soon require authentication
Users of Enterprise Edition will soon need to authenticate to access Timefold’s Maven Repository.
If you are a Timefold customer, a Timefold representative will reach out to you to give you the necessary credentials, as well as sufficient time to make the necessary changes.
If you are not a Timefold customer and you wish to retain your access to the Enterprise Edition artifacts, you can contact us to start your evaluation. There are many benefits to being a Timefold customer.
For more information on setting up the Enterprise Edition Maven Repository, see the Enterprise Edition documentation.
LookupStrategyType
deprecated for removal
LookupStrategyType
is used in multi-threaded solving
to specify how the solver should match entities and facts between parent and child score directors.
The default value is PLANNING_ID_OR_NONE
, which means
that the solver will look up entities by their planning ID.
If the solver doesn’t find anything with that ID, it will throw an exception.
In a future version of Timefold Solver, we will remove the option of configuring the lookup strategy.
The behavior will be fixed to the behavior explained above.
To prepare for this change,
remove the use of @PlanningSolution.lookupStrategyType
and ensure that your planning entities and problem facts have a @PlanningId
-annotated field.
Before in Timetable.java
:
@PlanningSolution(lookUpStrategyType = LookUpStrategyType.PLANNING_ID_OR_NONE)
public class Timetable {
...
}
After in Timetable.java
:
@PlanningSolution
public class Timetable {
...
}
Before in Lesson.java
:
@PlanningEntity
public class Lesson {
private String id;
...
}
After in Lesson.java
:
@PlanningEntity
public class Lesson {
@PlanningId
private String id;
...
}
Removed the examples
module
We have finished the process of removing the Swing-based examples. The legacy examples from the solver codebase have been removed entirely.
You can find better, more modern implementations of these use cases in our quickstarts, including:
-
bed-allocation
, -
conference-scheduling
, -
employee-scheduling
, -
facility-location
, -
flight-crew-scheduling
, -
food-packaging
, -
maintenance-scheduling
, -
meeting-scheduling
, -
order-picking
, -
project-job-scheduling
, -
school-timetabling
, -
sports-league-scheduling
, -
task-assigning
, -
tournament-scheduling
, -
and
vehicle-routing
.
Simplified the quickstarts artifact names
We have simplified and renamed all quickstarts artifactId
names.
For example, the old artifact name timefold-solver-quarkus-vehicle-routing-quickstart
became vehicle-routing
.
2.3. Upgrade from 1.8.0 to 1.9.0
Removed several of the old examples
We have started the process of removing the ancient Swing-based examples.
In the first wave, we have removed the following examples from the examples
module:
-
cloudbalancing
, -
conferencescheduling
, -
curriculumcourse
, -
examination
, -
flightcrewscheduling
, -
machinereassignment
, -
meetingscheduling
, -
nqueens
, -
pas
, -
tsp
, -
and
vehiclerouting
.
You can find better, more modern implementations of these use cases in our quickstarts. The other examples on the list were removed without a replacement as we didn’t see sufficient traction.
Going forward, our intention is to convert every other current example into a quickstart and remove the original Swing-based examples from the solver codebase entirely.
Several internal modules folded into timefold-solver-core
The following JAR files have been merged into timefold-solver-core
:
-
timefold-solver-core-impl
, -
timefold-solver-constraint-streams
.
timefold-solver-core
was previously an empty module that served as an aggregator for the above modules.
Now it contains the source code for both modules directly.
The automatic module name for this module is ai.timefold.solver.core
.
The root package of Constraint Streams implementation classes has changed.
If you have any custom code that references these classes,
you will need to update the imports to point ai.timefold.solver.core.impl.score.stream.bavet
.
Finally, with the folding of these modules into timefold-solver-core
,
the solver no longer relies on `ServiceLoader`s to find implementations of Constraint Streams,
or to find the Enterprise Edition.
None of these changes are likely to affect you, unless you have chosen to depend on internal classes and modules.
2.4. Upgrade from 1.7.0 to 1.8.0
Constraint Verifier: Check your tests if you use the planning list variable
In some cases, especially if you’ve reused our Food Packaging quickstart, you may see your tests failing after the upgrade. This is due to a bug fix in Constraint Streams, which now currently handles values not present in any list variable.
If your code has a shadow entity whose inverse relation shadow variable is a planning list variable and your test leaves that reference null
, the constraints will no longer take that shadow entity into account.
This will result in ConstraintVerifier
failing the test, as the expected number of penalties/rewards will no longer match the actual number.
You can solve this problem by manually assigning a value to the inverse relation shadow variable.
Before in *ConstraintProviderTest.java
:
Job job = new Job("job1", ...);
constraintVerifier.verifyThat(FoodPackagingConstraintProvider::maxEndDateTime)
.given(job)
.penalizesBy(...);
After in *ConstraintProviderTest.java
:
Job job = new Job("job1", ...);
Line line = new Line("line1", ...);
job.setLine(line);
constraintVerifier.verifyThat(FoodPackagingConstraintProvider::maxEndDateTime)
.given(job)
.penalizesBy(...);
The aforementioned quickstart unfortunately did not follow our own guidance on the use of shadow variables, which is why it exposed this bug.
Constraint Streams: Rename forEachIncludingNullVars
to forEachIncludingUnassigned
To better align with the newly introduced support for
unassigned values in list variables, several methods in Constraint Streams
which dealt with null
variable values have been renamed.
Before in *ConstraintProvider.java
:
Constraint myConstraint(ConstraintFactory constraintFactory) {
return constraintFactory.forEachIncludingNullVars(Shift.class)
...;
}
After in *ConstraintProvider.java
:
Constraint myConstraint(ConstraintFactory constraintFactory) {
return constraintFactory.forEachIncludingUnassigned(Shift.class)
...;
}
Similarly, the following methods on UniConstraintStream
have been renamed:
-
ifExistsIncludingNullVars
toifExistsIncludingUnassigned
, -
ifExistsOtherIncludingNullVars
toifExistsOtherIncludingUnassigned
, -
ifNotExistsIncludingNullVars
toifNotExistsIncludingUnassigned
, -
ifNotExistsOtherIncludingNullVars
toifNotExistsOtherIncludingUnassigned
.
On BiConstraintStream
and its Tri
and Quad
counterparts, the following methods have been renamed as well:
-
ifExistsIncludingNullVars
toifExistsIncludingUnassigned
, -
ifNotExistsIncludingNullVars
toifNotExistsIncludingUnassigned
.
Rename nullable
attribute of @PlanningVariable
to allowsUnassigned
To better align with the newly introduced support for
unassigned values in list variables, the nullable
attribute of @PlanningVariable
has been renamed to allowsUnassigned
.
Before in *.java
:
@PlanningVariable(nullable = true)
private Bed bed;
After in *.java
:
@PlanningVariable(allowsUnassigned = true)
private Bed bed;
Constraint Verifier: assertion methods message
argument comes first now
To better align with the newly introduced support for testing justifications and indictments, the assertion methods which accepted a message
argument now have it as the first argument.
Before in *ConstraintProviderTest.java
:
constraintVerifier.verifyThat(MyConstraintProvider::myConstraint)
.given()
.penalizesBy(0, "There should no penalties");
After in *ConstraintProvider.java
:
constraintVerifier.verifyThat(MyConstraintProvider::myConstraint)
.given()
.penalizesBy("There should no penalties", 0);
Similarly to the penalizesBy
method, the following methods were also affected:
-
penalizes
, -
rewards
, -
rewardsWith
.