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:

  1. There is a planning entity with a @PlanningVariable that does not allow unassigned values.

  2. And that planning entity is pinned.

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


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