(*) 至少,世界上最聪明的计算机科学家还没有找到这样的灵丹妙药。但如果他们能找到一个解决 NP 完全问题的灵丹妙药,那么它就能解决所有 NP 完全问题。
事实上,如果有人能证明这种灵丹妙药是否真的存在,就可以获得 1,000,000 美元的奖励。
|
Lesson
实例分配给Timeslot
和Room
实例以遵守硬调度和软调度约束,例如以下示例:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- Use spring boot parent to use its native profile -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.1</version>
</parent>
<groupId>org.acme</groupId>
<artifactId>spring-boot-school-timetabling</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<version.ai.timefold.solver>1.10.0</version.ai.timefold.solver>
<version.org.springframework.boot>${project.parent.version}</version.org.springframework.boot>
<version.compiler.plugin>3.13.0</version.compiler.plugin>
<version.surefire.plugin>3.2.5</version.surefire.plugin>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${version.org.springframework.boot}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>ai.timefold.solver</groupId>
<artifactId>timefold-solver-bom</artifactId>
<version>${version.ai.timefold.solver}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Spring boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>ai.timefold.solver</groupId>
<artifactId>timefold-solver-spring-boot-starter</artifactId>
</dependency>
<!-- Swagger -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.5.0</version>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ai.timefold.solver</groupId>
<artifactId>timefold-solver-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webflux</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<scope>test</scope>
</dependency>
<!-- UI -->
<!-- No webjar locator; incompatible in native mode;
see https://github.com/spring-projects/spring-framework/issues/27619
and https://github.com/webjars/webjars-locator-core/issues/96
-->
<dependency>
<groupId>ai.timefold.solver</groupId>
<artifactId>timefold-solver-webui</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>5.2.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.6.4</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>font-awesome</artifactId>
<version>5.15.1</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.webjars.npm</groupId>
<artifactId>js-joda</artifactId>
<version>1.11.0</version>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>${version.compiler.plugin}</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>${version.surefire.plugin}</version>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${version.org.springframework.boot}</version>
<executions>
<!-- Repackage the archive produced by maven-jar-plugin into an executable JAR file. -->
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
<configuration>
<!-- optimizedLaunch disables the C2 compiler, which has a massive performance impact -->
<optimizedLaunch>false</optimizedLaunch>
</configuration>
</plugin>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Timeslot
代表授课的时间间隔,例如Monday 10:30 - 11:30
或Tuesday 13:30 - 14:30
。为简单起见,所有时间段的持续时间相同,午餐或其他休息期间没有时间段。
package org.acme.schooltimetabling.domain;
import ai.timefold.solver.core.api.domain.lookup.PlanningId;
import com.fasterxml.jackson.annotation.JsonIdentityInfo;
import com.fasterxml.jackson.annotation.ObjectIdGenerators;
import java.time.DayOfWeek;
import java.time.LocalTime;
@JsonIdentityInfo(scope = Timeslot.class, generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
public class Timeslot {
@PlanningId
private String id;
private DayOfWeek dayOfWeek;
private LocalTime startTime;
private LocalTime endTime;
public Timeslot() {
}
public Timeslot(String id, DayOfWeek dayOfWeek, LocalTime startTime, LocalTime endTime) {
this.id = id;
this.dayOfWeek = dayOfWeek;
this.startTime = startTime;
this.endTime = endTime;
}
public Timeslot(String id, DayOfWeek dayOfWeek, LocalTime startTime) {
this(id, dayOfWeek, startTime, startTime.plusMinutes(50));
}
@Override
public String toString() {
return dayOfWeek + " " + startTime;
}
// ************************************************************************
// Getters and setters
// ************************************************************************
public String getId() {
return id;
}
public DayOfWeek getDayOfWeek() {
return dayOfWeek;
}
public LocalTime getStartTime() {
return startTime;
}
public LocalTime getEndTime() {
return endTime;
}
}
Room
代表授课地点,例如Room A
或Room B
。为简单起见,所有房间均无容量限制,可容纳所有课程。
package org.acme.schooltimetabling.domain;
import ai.timefold.solver.core.api.domain.lookup.PlanningId;
import com.fasterxml.jackson.annotation.JsonIdentityInfo;
import com.fasterxml.jackson.annotation.ObjectIdGenerators;
@JsonIdentityInfo(scope = Room.class, generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
public class Room {
@PlanningId
private String id;
private String name;
public Room() {
}
public Room(String id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return name;
}
// ************************************************************************
// Getters and setters
// ************************************************************************
public String getId() {
return id;
}
public String getName() {
return name;
}
}
Lesson
,一位老师向一组学生讲授一门课程,例如Math by A.Turing for 9th grade
或Chemistry by M.Curie for 10th grade
。如果同一老师每周向同一组学生讲授同一门课程多次,则存在多个Lesson
实例,只能通过 来区分id
。例如,9 年级每周有 6 节数学课。
package org.acme.schooltimetabling.domain;
import ai.timefold.solver.core.api.domain.entity.PlanningEntity;
import ai.timefold.solver.core.api.domain.lookup.PlanningId;
import ai.timefold.solver.core.api.domain.variable.PlanningVariable;
import com.fasterxml.jackson.annotation.JsonIdentityReference;
@PlanningEntity
public class Lesson {
@PlanningId
private String id;
private String subject;
private String teacher;
private String studentGroup;
@JsonIdentityReference
@PlanningVariable
private Timeslot timeslot;
@JsonIdentityReference
@PlanningVariable
private Room room;
public Lesson() {
}
public Lesson(String id, String subject, String teacher, String studentGroup) {
this.id = id;
this.subject = subject;
this.teacher = teacher;
this.studentGroup = studentGroup;
}
public Lesson(String id, String subject, String teacher, String studentGroup, Timeslot timeslot, Room room) {
this(id, subject, teacher, studentGroup);
this.timeslot = timeslot;
this.room = room;
}
@Override
public String toString() {
return subject + "(" + id + ")";
}
// ************************************************************************
// Getters and setters
// ************************************************************************
public String getId() {
return id;
}
public String getSubject() {
return subject;
}
public String getTeacher() {
return teacher;
}
public String getStudentGroup() {
return studentGroup;
}
public Timeslot getTimeslot() {
return timeslot;
}
public void setTimeslot(Timeslot timeslot) {
this.timeslot = timeslot;
}
public Room getRoom() {
return room;
}
public void setRoom(Room room) {
this.room = room;
}
}
HardSoftScore
类来表示分数:
package org.acme.schooltimetabling.solver;
import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore;
import ai.timefold.solver.core.api.score.stream.Constraint;
import ai.timefold.solver.core.api.score.stream.ConstraintFactory;
import ai.timefold.solver.core.api.score.stream.ConstraintProvider;
import ai.timefold.solver.core.api.score.stream.Joiners;
import org.acme.schooltimetabling.domain.Lesson;
import org.acme.schooltimetabling.solver.justifications.*;
import java.time.Duration;
public class TimetableConstraintProvider implements ConstraintProvider {
@Override
public Constraint[] defineConstraints(ConstraintFactory constraintFactory) {
return new Constraint[] {
// Hard constraints
roomConflict(constraintFactory),
teacherConflict(constraintFactory),
studentGroupConflict(constraintFactory),
// Soft constraints
teacherRoomStability(constraintFactory),
teacherTimeEfficiency(constraintFactory),
studentGroupSubjectVariety(constraintFactory)
};
}
Constraint roomConflict(ConstraintFactory constraintFactory) {
// A room can accommodate at most one lesson at the same time.
return constraintFactory
// Select each pair of 2 different lessons ...
.forEachUniquePair(Lesson.class,
// ... in the same timeslot ...
Joiners.equal(Lesson::getTimeslot),
// ... in the same room ...
Joiners.equal(Lesson::getRoom))
// ... and penalize each pair with a hard weight.
.penalize(HardSoftScore.ONE_HARD)
.justifyWith((lesson1, lesson2, score) -> new RoomConflictJustification(lesson1.getRoom(), lesson1, lesson2))
.asConstraint("Room conflict");
}
Constraint teacherConflict(ConstraintFactory constraintFactory) {
// A teacher can teach at most one lesson at the same time.
return constraintFactory
.forEachUniquePair(Lesson.class,
Joiners.equal(Lesson::getTimeslot),
Joiners.equal(Lesson::getTeacher))
.penalize(HardSoftScore.ONE_HARD)
.justifyWith(
(lesson1, lesson2, score) -> new TeacherConflictJustification(lesson1.getTeacher(), lesson1, lesson2))
.asConstraint("Teacher conflict");
}
Constraint studentGroupConflict(ConstraintFactory constraintFactory) {
// A student can attend at most one lesson at the same time.
return constraintFactory
.forEachUniquePair(Lesson.class,
Joiners.equal(Lesson::getTimeslot),
Joiners.equal(Lesson::getStudentGroup))
.penalize(HardSoftScore.ONE_HARD)
.justifyWith((lesson1, lesson2, score) -> new StudentGroupConflictJustification(lesson1.getStudentGroup(), lesson1, lesson2))
.asConstraint("Student group conflict");
}
Constraint teacherRoomStability(ConstraintFactory constraintFactory) {
// A teacher prefers to teach in a single room.
return constraintFactory
.forEachUniquePair(Lesson.class,
Joiners.equal(Lesson::getTeacher))
.filter((lesson1, lesson2) -> lesson1.getRoom() != lesson2.getRoom())
.penalize(HardSoftScore.ONE_SOFT)
.justifyWith((lesson1, lesson2, score) -> new TeacherRoomStabilityJustification(lesson1.getTeacher(), lesson1, lesson2))
.asConstraint("Teacher room stability");
}
Constraint teacherTimeEfficiency(ConstraintFactory constraintFactory) {
// A teacher prefers to teach sequential lessons and dislikes gaps between lessons.
return constraintFactory
.forEach(Lesson.class)
.join(Lesson.class, Joiners.equal(Lesson::getTeacher),
Joiners.equal((lesson) -> lesson.getTimeslot().getDayOfWeek()))
.filter((lesson1, lesson2) -> {
Duration between = Duration.between(lesson1.getTimeslot().getEndTime(),
lesson2.getTimeslot().getStartTime());
return !between.isNegative() && between.compareTo(Duration.ofMinutes(30)) <= 0;
})
.reward(HardSoftScore.ONE_SOFT)
.justifyWith((lesson1, lesson2, score) -> new TeacherTimeEfficiencyJustification(lesson1.getTeacher(), lesson1, lesson2))
.asConstraint("Teacher time efficiency");
}
Constraint studentGroupSubjectVariety(ConstraintFactory constraintFactory) {
// A student group dislikes sequential lessons on the same subject.
return constraintFactory
.forEach(Lesson.class)
.join(Lesson.class,
Joiners.equal(Lesson::getSubject),
Joiners.equal(Lesson::getStudentGroup),
Joiners.equal((lesson) -> lesson.getTimeslot().getDayOfWeek()))
.filter((lesson1, lesson2) -> {
Duration between = Duration.between(lesson1.getTimeslot().getEndTime(),
lesson2.getTimeslot().getStartTime());
return !between.isNegative() && between.compareTo(Duration.ofMinutes(30)) <= 0;
})
.penalize(HardSoftScore.ONE_SOFT)
.justifyWith((lesson1, lesson2, score) -> new StudentGroupSubjectVarietyJustification(lesson1.getStudentGroup(), lesson1, lesson2))
.asConstraint("Student group subject variety");
}
}
Timetable
包装单个数据集的所有Timeslot
、Room
和实例。此外,由于它包含所有课程,每个课程都有特定的规划变量状态,因此它是一个规划解决方案,并且具有分数:Lesson
-4init/0hard/0soft
。-2hard/-3soft
。0hard/-7soft
。package org.acme.schooltimetabling.domain;
import ai.timefold.solver.core.api.domain.solution.PlanningEntityCollectionProperty;
import ai.timefold.solver.core.api.domain.solution.PlanningScore;
import ai.timefold.solver.core.api.domain.solution.PlanningSolution;
import ai.timefold.solver.core.api.domain.solution.ProblemFactCollectionProperty;
import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider;
import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore;
import ai.timefold.solver.core.api.solver.SolverStatus;
import java.util.List;
@PlanningSolution
public class Timetable {
private String name;
@ProblemFactCollectionProperty
@ValueRangeProvider
private List<Timeslot> timeslots;
@ProblemFactCollectionProperty
@ValueRangeProvider
private List<Room> rooms;
@PlanningEntityCollectionProperty
private List<Lesson> lessons;
@PlanningScore
private HardSoftScore score;
// Ignored by Timefold, used by the UI to display solve or stop solving button
private SolverStatus solverStatus;
// No-arg constructor required for Timefold
public Timetable() {
}
public Timetable(String name, HardSoftScore score, SolverStatus solverStatus) {
this.name = name;
this.score = score;
this.solverStatus = solverStatus;
}
public Timetable(String name, List<Timeslot> timeslots, List<Room> rooms, List<Lesson> lessons) {
this.name = name;
this.timeslots = timeslots;
this.rooms = rooms;
this.lessons = lessons;
}
// ************************************************************************
// Getters and setters
// ************************************************************************
public String getName() {
return name;
}
public List<Timeslot> getTimeslots() {
return timeslots;
}
public List<Room> getRooms() {
return rooms;
}
public List<Lesson> getLessons() {
return lessons;
}
public HardSoftScore getScore() {
return score;
}
public SolverStatus getSolverStatus() {
return solverStatus;
}
public void setSolverStatus(SolverStatus solverStatus) {
this.solverStatus = solverStatus;
}
}
package org.acme.schooltimetabling.rest;
import java.util.Collection;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import ai.timefold.solver.core.api.score.analysis.ScoreAnalysis;
import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore;
import ai.timefold.solver.core.api.solver.ScoreAnalysisFetchPolicy;
import ai.timefold.solver.core.api.solver.SolutionManager;
import ai.timefold.solver.core.api.solver.SolverManager;
import ai.timefold.solver.core.api.solver.SolverStatus;
import org.acme.schooltimetabling.domain.Timetable;
import org.acme.schooltimetabling.rest.exception.ErrorInfo;
import org.acme.schooltimetabling.rest.exception.TimetableSolverException;
import org.acme.schooltimetabling.solver.justifications.RoomConflictJustification;
import org.acme.schooltimetabling.solver.justifications.StudentGroupConflictJustification;
import org.acme.schooltimetabling.solver.justifications.StudentGroupSubjectVarietyJustification;
import org.acme.schooltimetabling.solver.justifications.TeacherConflictJustification;
import org.acme.schooltimetabling.solver.justifications.TeacherRoomStabilityJustification;
import org.acme.schooltimetabling.solver.justifications.TeacherTimeEfficiencyJustification;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aot.hint.annotation.RegisterReflectionForBinding;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
@Tag(name = "School Timetables", description = "School timetable service assigning lessons to rooms and timeslots.")
@RestController
@RequestMapping("/timetables")
public class TimetableController {
private static final Logger LOGGER = LoggerFactory.getLogger(TimetableController.class);
private final SolverManager<Timetable, String> solverManager;
private final SolutionManager<Timetable, HardSoftScore> solutionManager;
// TODO: Without any "time to live", the map may eventually grow out of memory.
private final ConcurrentMap<String, Job> jobIdToJob = new ConcurrentHashMap<>();
public TimetableController(SolverManager<Timetable, String> solverManager,
SolutionManager<Timetable, HardSoftScore> solutionManager) {
this.solverManager = solverManager;
this.solutionManager = solutionManager;
}
@Operation(summary = "List the job IDs of all submitted timetables.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "List of all job IDs.",
content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(type = "array", implementation = String.class))) })
@GetMapping
public Collection<String> list() {
return jobIdToJob.keySet();
}
@Operation(summary = "Submit a timetable to start solving as soon as CPU resources are available.")
@ApiResponses(value = {
@ApiResponse(responseCode = "202",
description = "The job ID. Use that ID to get the solution with the other methods.",
content = @Content(mediaType = MediaType.TEXT_PLAIN_VALUE,
schema = @Schema(implementation = String.class))) })
@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.TEXT_PLAIN_VALUE)
public String solve(@RequestBody Timetable problem) {
String jobId = UUID.randomUUID().toString();
jobIdToJob.put(jobId, Job.ofTimetable(problem));
solverManager.solveBuilder()
.withProblemId(jobId)
.withProblemFinder(jobId_ -> jobIdToJob.get(jobId).timetable)
.withBestSolutionConsumer(solution -> jobIdToJob.put(jobId, Job.ofTimetable(solution)))
.withExceptionHandler((jobId_, exception) -> {
jobIdToJob.put(jobId, Job.ofException(exception));
LOGGER.error("Failed solving jobId ({}).", jobId, exception);
})
.run();
return jobId;
}
@Operation(summary = "Submit a timetable to analyze its score.")
@ApiResponses(value = {
@ApiResponse(responseCode = "202",
description = "Resulting score analysis, optionally without constraint matches.",
content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = ScoreAnalysis.class))) })
@PutMapping(value = "/analyze", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
@RegisterReflectionForBinding({
RoomConflictJustification.class,
StudentGroupConflictJustification.class,
StudentGroupSubjectVarietyJustification.class,
TeacherConflictJustification.class,
TeacherRoomStabilityJustification.class,
TeacherTimeEfficiencyJustification.class
})
public ScoreAnalysis<HardSoftScore> analyze(@RequestBody Timetable problem,
@RequestParam(name = "fetchPolicy", required = false) ScoreAnalysisFetchPolicy fetchPolicy) {
return fetchPolicy == null ? solutionManager.analyze(problem) : solutionManager.analyze(problem, fetchPolicy);
}
@Operation(
summary = "Get the solution and score for a given job ID. This is the best solution so far, as it might still be running or not even started.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "The best solution of the timetable so far.",
content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = Timetable.class))),
@ApiResponse(responseCode = "404", description = "No timetable found.",
content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = ErrorInfo.class))),
@ApiResponse(responseCode = "500", description = "Exception during solving a timetable.",
content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = ErrorInfo.class)))
})
@GetMapping(value = "/{jobId}", produces = MediaType.APPLICATION_JSON_VALUE)
public Timetable getTimeTable(
@Parameter(description = "The job ID returned by the POST method.") @PathVariable("jobId") String jobId) {
Timetable timetable = getTimetableAndCheckForExceptions(jobId);
SolverStatus solverStatus = solverManager.getSolverStatus(jobId);
timetable.setSolverStatus(solverStatus);
return timetable;
}
@Operation(
summary = "Get the timetable status and score for a given job ID.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "The timetable status and the best score so far.",
content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = Timetable.class))),
@ApiResponse(responseCode = "404", description = "No timetable found.",
content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = ErrorInfo.class))),
@ApiResponse(responseCode = "500", description = "Exception during solving a timetable.",
content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = ErrorInfo.class)))
})
@GetMapping(value = "/{jobId}/status", produces = MediaType.APPLICATION_JSON_VALUE)
public Timetable getStatus(
@Parameter(description = "The job ID returned by the POST method.") @PathVariable("jobId") String jobId) {
Timetable timetable = getTimetableAndCheckForExceptions(jobId);
SolverStatus solverStatus = solverManager.getSolverStatus(jobId);
return new Timetable(timetable.getName(), timetable.getScore(), solverStatus);
}
private Timetable getTimetableAndCheckForExceptions(String jobId) {
Job job = jobIdToJob.get(jobId);
if (job == null) {
throw new TimetableSolverException(jobId, HttpStatus.NOT_FOUND, "No timetable found.");
}
if (job.exception != null) {
throw new TimetableSolverException(jobId, job.exception);
}
return job.timetable;
}
@Operation(
summary = "Terminate solving for a given job ID. Returns the best solution of the timetable so far, as it might still be running or not even started.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "The best solution of the timetable so far.",
content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = Timetable.class))),
@ApiResponse(responseCode = "404", description = "No timetable found.",
content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = ErrorInfo.class))),
@ApiResponse(responseCode = "500", description = "Exception during solving a timetable.",
content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = ErrorInfo.class)))
})
@DeleteMapping(value = "/{jobId}", produces = MediaType.APPLICATION_JSON_VALUE)
public Timetable terminateSolving(
@Parameter(description = "The job ID returned by the POST method.") @PathVariable("jobId") String jobId) {
// TODO: Replace with .terminateEarlyAndWait(... [, timeout]); see https://github.com/TimefoldAI/timefold-solver/issues/77
solverManager.terminateEarly(jobId);
return getTimeTable(jobId);
}
private record Job(Timetable timetable, Throwable exception) {
static Job ofTimetable(Timetable timetable) {
return new Job(timetable, null);
}
static Job ofException(Throwable error) {
return new Job(null, error);
}
}
}
terminationEarly()
事件,求解器将永远运行。为避免这种情况,请将求解时间限制为五秒。这足够短,可以避免 HTTP 超时。
src/main/resources/application.properties
文件:
# The solver runs only for 5 seconds to avoid a HTTP timeout in this simple implementation.
# It's recommended to run for at least 5 minutes ("5m") otherwise.
timefold.solver.termination.spent-limit=5s