Chapter 9. The OptaPlanner SolverManager

A SolverManager is a facade for one or more Solver instances to simplify solving planning problems in REST and other enterprise services.

Unlike the Solver.solve(…​) method, a SolverManager has the following characteristics:

  • SolverManager.solve(…​) returns immediately: it schedules a problem for asynchronous solving without blocking the calling thread. This avoids timeout issues of HTTP and other technologies.
  • SolverManager.solve(…​) solves multiple planning problems of the same domain, in parallel.

Internally, a SolverManager manages a thread pool of solver threads, which call Solver.solve(…​), and a thread pool of consumer threads, which handle best solution changed events.

In Quarkus and Spring Boot, the SolverManager instance is automatically injected in your code. If you are using a platform other than Quarkus or Spring Boot, build a SolverManager instance with the create(…​) method:

SolverConfig solverConfig = SolverConfig.createFromXmlResource(".../cloudBalancingSolverConfig.xml");
SolverManager<CloudBalance, UUID> solverManager = SolverManager.create(solverConfig, new SolverManagerConfig());

Each problem submitted to the SolverManager.solve(…​) methods must have a unique problem ID. Later calls to getSolverStatus(problemId) or terminateEarly(problemId) use that problem ID to distinguish between planning problems. The problem ID must be an immutable class, such as Long, String, or java.util.UUID.

The SolverManagerConfig class has a parallelSolverCount property that controls how many solvers are run in parallel. For example, if the parallelSolverCount property` is set to 4 and you submit five problems, four problems start solving immediately and the fifth problem starts when one of the first problems ends. If those problems solve for five minutes each, the fifth problem takes 10 minutes to finish. By default, parallelSolverCount is set to AUTO, which resolves to half the CPU cores, regardless of the moveThreadCount of the solvers.

To retrieve the best solution, after solving terminates normally use SolverJob.getFinalBestSolution():

CloudBalance problem1 = ...;
UUID problemId = UUID.randomUUID();
// Returns immediately
SolverJob<CloudBalance, UUID> solverJob = solverManager.solve(problemId, problem1);
...
CloudBalance solution1;
try {
    // Returns only after solving terminates
    solution1 = solverJob.getFinalBestSolution();
} catch (InterruptedException | ExecutionException e) {
    throw ...;
}

However, there are better approaches, both for solving batch problems before a user needs the solution as well as for live solving while a user is actively waiting for the solution.

The current SolverManager implementation runs on a single computer node, but future work aims to distribute solver loads across a cloud.

9.1. Batch solving problems

Batch solving is solving multiple data sets in parallel. Batch solving is particularly useful overnight:

  • There are typically few or no problem changes in the middle of the night. Some organizations enforce a deadline, for example, submit all day off requests before midnight.
  • The solvers can run for much longer, often hours, because nobody is waiting for the results and CPU resources are often cheaper.
  • Solutions are available when employees arrive at work the next working day.

Procedure

To batch solve problems in parallel, limited by parallelSolverCount, call solve(…​) for each data set created the following class:

+

public class TimeTableService {

    private SolverManager<TimeTable, Long> solverManager;

    // Returns immediately, call it for every data set
    public void solveBatch(Long timeTableId) {
        solverManager.solve(timeTableId,
                // Called once, when solving starts
                this::findById,
                // Called once, when solving ends
                this::save);
    }

    public TimeTable findById(Long timeTableId) {...}

    public void save(TimeTable timeTable) {...}

}

9.2. Solve and listen to show progress

When a solver is running while a user is waiting for a solution, the user might need to wait for several minutes or hours before receiving a result. To assure the user that everything is going well, show progress by displaying the best solution and best score attained so far.

Procedure

  1. To handle intermediate best solutions, use solveAndListen(…​):

    public class TimeTableService {
    
        private SolverManager<TimeTable, Long> solverManager;
    
        // Returns immediately
        public void solveLive(Long timeTableId) {
            solverManager.solveAndListen(timeTableId,
                    // Called once, when solving starts
                    this::findById,
                    // Called multiple times, for every best solution change
                    this::save);
        }
    
        public TimeTable findById(Long timeTableId) {...}
    
        public void save(TimeTable timeTable) {...}
    
        public void stopSolving(Long timeTableId) {
            solverManager.terminateEarly(timeTableId);
        }
    
    }

    This implementation is using the database to communicate with the UI, which polls the database. More advanced implementations push the best solutions directly to the UI or a messaging queue.

  2. When the user is satisfied with the intermediate best solution and does not want to wait any longer for a better one, call SolverManager.terminateEarly(problemId).