Search code examples
requestmicroservicessolveroptaplannerdata-corruption

OptaPlanner: timetable resuming generates a wrong solution, even if the generation starts from where it stopped


I am using Java Spring Boot and OptaPlanner to generate a timetable with almost 20 constraints. At the initial generation, everything works fine. The score showed by the OptaPlanner logging messages matches the solution received, but when I want to resume the generation, the solution contains a lot of problems (like the constraints are not respected anymore) although the generation starts from where it has stopped and it continues initializing or finding a best solution.

My project is divided into two microservices: one that communicates with the UI and keeps the database, and the other receives data from the first when a request for starting/resuming the generation is done and generates the schedule using OptaPlanner. I use the same request for starting/resuming the generation.

This is how my project works: the UI makes the requests for starting, resuming, stopping the generation and getting the timetable. These requests are handled by the first microservice, which uses WebClient to send new requests to the second microservice. Here, the timetable will be generated after asking for some data from the database.

Here is the method for starting/resuming the generation from the second microservice:

@PostMapping("startSolver")
public ResponseEntity<?> startSolver(@PathVariable String organizationId) {
    try {
        SolverConfig solverConfig = SolverConfig.createFromXmlResource("solver/timeTableSolverConfig.xml");
        SolverFactory<TimeTable> solverFactory = new DefaultSolverFactory<>(solverConfig);
        this.solverManager = SolverManager.create(solverFactory);
        this.solverManager.solveAndListen(TimeTableService.SINGLETON_TIME_TABLE_ID,
                id -> timeTableService.findById(id, UUID.fromString(organizationId)),
                timeTable -> timeTableService.updateModifiedLessons(timeTable, organizationId));

        return new ResponseEntity<>("Solving has successfully started", HttpStatus.OK);
    } catch(OptaPlannerException exception) {
        System.out.println("OptaPlanner exception - " + exception.getMessage());
        return utils.generateResponse(exception.getMessage(), HttpStatus.CONFLICT);
    }
}

-> findById(...) method make a request to the first microservice, expecting to receive all data needed by constraints for generation (lists of planning entities, planning variables and all other useful data)

public TimeTable findById(Long id, UUID organizationId) {
    SolverDataDTO solverDataDTO = webClient.get()
            .uri("http://localhost:8080/smart-planner/org/{organizationId}/optaplanner-solver/getSolverData",
                    organizationId)
            .retrieve()
            .onStatus(HttpStatus::isError, error -> {
                        LOGGER.error(extractExceptionMessage("findById.fetchFails", "findById()"));
                        return Mono.error(new OptaPlannerException(
                                extractExceptionMessage("findById.fetchFails", "")));
                    })
            .bodyToMono(SolverDataDTO.class)
            .block();

    TimeTable timeTable = new TimeTable();

   /.. populating all lists from TimeTable with the one received in solverDataDTO ../

    return timeTable;
}

-> updateModifiedLessons(...) method send to the first microservice the list of all generated planning entities with the corresponding planning variables assigned

public void updateModifiedLessons(TimeTable timeTable, String organizationId) {
    List<ScheduleSlot> slots = new ArrayList<>(timeTable.getScheduleSlotList());
    List<SolverScheduleSlotDTO> solverScheduleSlotDTOs =
            scheduleSlotConverter.convertModelsToSolverDTOs(slots);

    String executionMessage = webClient.post()
            .uri("http://localhost:8080/smart-planner/org/{organizationId}/optaplanner-solver/saveTimeTable",
                    organizationId)
            .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
            .body(Mono.just(solverScheduleSlotDTOs), SolverScheduleSlotDTO.class)
            .retrieve()
            .onStatus(HttpStatus::isError, error -> {
                        LOGGER.error(extractExceptionMessage("saveSlots.savingFails", "updateModifiedLessons()"));
                        return Mono.error(new OptaPlannerException(
                                extractExceptionMessage("saveSlots.savingFails", "")));
                    })
            .bodyToMono(String.class)
            .block();
}

Solution

  • I would probably start by making sure that the solution you save to the DB after the first run of startSolver() is the same (in terms of Java equality), including the assignments of planning variables to values, as the solution you retrieve via findById() at the beginning of the second run.