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 a migration 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.28.0:run -Drewrite.recipeArtifactCoordinates=ai.timefold.solver:timefold-solver-migration:1.9.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.9.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 migration 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. Kotlin users should translate the changes accordingly.

2.1. Upgrade from 1.9.0 to 1.10.0

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;
    ...

}

2.2. 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 many of these use cases in our quickstarts. The other examples on the list were removed without 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.3. 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::dueDateTime)
    .given(job)
    .penalizesBy(...);

After in *ConstraintProviderTest.java:

Job job = new Job("job1",  ...);
Line line = new Line("line1", ...);
job.setLine(line);

constraintVerifier.verifyThat(FoodPackagingConstraintProvider::dueDateTime)
    .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 to ifExistsIncludingUnassigned,

  • ifExistsOtherIncludingNullVars to ifExistsOtherIncludingUnassigned,

  • ifNotExistsIncludingNullVars to ifNotExistsIncludingUnassigned,

  • ifNotExistsOtherIncludingNullVars to ifNotExistsOtherIncludingUnassigned.

On BiConstraintStream and its Tri and Quad counterparts, the following methods have been renamed as well:

  • ifExistsIncludingNullVars to ifExistsIncludingUnassigned,

  • ifNotExistsIncludingNullVars to ifNotExistsIncludingUnassigned.


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.