I am doing a simple time slot (planning entity) -> team (planning var) assignment task. All work fine while using only one thread. Also multithreading works fine if one custom move I have is not involved, but after it's used the solver gets stuck forever with no message. All the custom moves are created before every STEP. Full assert doesn't say anything. It looks like deadlock for me. I am using optaplanner 8.25.0.Final with Java 17
This is the move factory:
package pl.medaxtrans.pre.solver;
import org.optaplanner.core.impl.heuristic.move.Move;
import org.optaplanner.core.impl.heuristic.selector.move.factory.MoveListFactory;
import pl.medaxtrans.common.domain.Team;
import pl.medaxtrans.pre.domain.HalfHourTimeslot;
import pl.medaxtrans.pre.domain.ShiftsSolution;
import java.time.DayOfWeek;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class FarSlotChangeMoveFactory implements MoveListFactory<ShiftsSolution> {
private static Predicate<HalfHourTimeslot> equalTime(HalfHourTimeslot slot2) {
return slot -> slot.compareTo(slot2) == 0;
}
private static Predicate<HalfHourTimeslot> sameDayAndTeam(DayOfWeek day, Team team) {
return slot -> slot.getDayOfWeek().equals(day) && slot.getTeam().equals(team);
}
@Override
public List<? extends Move<ShiftsSolution>> createMoveList(ShiftsSolution solution) {
List<FarSlotChangeMove> moves = new ArrayList<>();
List<HalfHourTimeslot> slots = solution.getSlots();
List<Team> teams = solution.getTeams();
for (Team fromTeam : teams) {
for (Team toTeam : teams) {
if (fromTeam != toTeam) {
for (DayOfWeek day : DayOfWeek.values()) {
List<HalfHourTimeslot> fromTeamSlots = slots.stream().filter(sameDayAndTeam(day, fromTeam)).collect(Collectors.toList());
List<HalfHourTimeslot> toTeamSlots = slots.stream().filter(sameDayAndTeam(day, toTeam)).collect(Collectors.toList());
Optional<HalfHourTimeslot> fromMinOpt = fromTeamSlots.stream().min(Comparator.naturalOrder());
Optional<HalfHourTimeslot> fromMaxOpt = fromTeamSlots.stream().max(Comparator.naturalOrder());
Optional<HalfHourTimeslot> toMaxOpt = toTeamSlots.stream().max(Comparator.naturalOrder());
Optional<HalfHourTimeslot> toMinOpt = toTeamSlots.stream().min(Comparator.naturalOrder());
if (fromMinOpt.isPresent() && fromMaxOpt.isPresent() && toMaxOpt.isPresent() && toMinOpt.isPresent())
{
HalfHourTimeslot fromMin = fromMinOpt.get();
HalfHourTimeslot fromMax = fromMaxOpt.get();
HalfHourTimeslot toMax = toMaxOpt.get();
HalfHourTimeslot toMin = toMinOpt.get();
if (toMin.compareTo(fromMin) < 0 && toMax.compareTo(fromMin) > 0 && toTeamSlots.stream().noneMatch(equalTime(fromMin)))
{
moves.add(new FarSlotChangeMove(fromMin, toTeam));
}
if (toMin.compareTo(fromMax) < 0 && toMax.compareTo(fromMax) > 0 && toTeamSlots.stream().noneMatch(equalTime(fromMax)))
{
moves.add(new FarSlotChangeMove(fromMax, toTeam));
}
}
}
}
}
}
return moves;
}
}
And this is the problematic move:
package pl.medaxtrans.pre.solver;
import org.optaplanner.core.api.score.director.ScoreDirector;
import org.optaplanner.core.impl.heuristic.move.AbstractMove;
import pl.medaxtrans.common.domain.Team;
import pl.medaxtrans.pre.domain.HalfHourTimeslot;
import pl.medaxtrans.pre.domain.ShiftsSolution;
import java.util.Collection;
import java.util.Collections;
import java.util.Objects;
public class FarSlotChangeMove extends AbstractMove<ShiftsSolution> {
private final HalfHourTimeslot slot;
private final Team toTeam;
private final Team fromTeam;
public FarSlotChangeMove(HalfHourTimeslot slot, Team toTeam) {
this.slot = slot;
this.toTeam = toTeam;
this.fromTeam = slot.getTeam();
}
@Override
protected AbstractMove<ShiftsSolution> createUndoMove(ScoreDirector<ShiftsSolution> scoreDirector) {
return new FarSlotChangeMove(slot, fromTeam);
}
@Override
protected void doMoveOnGenuineVariables(ScoreDirector<ShiftsSolution> scoreDirector) {
scoreDirector.beforeVariableChanged(slot, "team");
slot.setTeam(toTeam);
scoreDirector.afterVariableChanged(slot, "team");
}
@Override
public boolean isMoveDoable(ScoreDirector<ShiftsSolution> scoreDirector) {
// we don't check overlaps here, because it is already checked by the move factory
return !toTeam.equals(fromTeam);
}
@Override
public FarSlotChangeMove rebase(ScoreDirector<ShiftsSolution> destinationScoreDirector) {
return new FarSlotChangeMove(destinationScoreDirector.lookUpWorkingObject(slot), destinationScoreDirector.lookUpWorkingObject(toTeam));
}
@Override
public Collection<?> getPlanningEntities() {
return Collections.singletonList(slot);
}
@Override
public Collection<?> getPlanningValues() {
return Collections.singletonList(toTeam);
}
@Override
public String toString() {
return slot + " {" + fromTeam + "->" + toTeam + "}";
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final FarSlotChangeMove other = (FarSlotChangeMove) o;
return Objects.equals(fromTeam, other.fromTeam) &&
Objects.equals(toTeam, other.toTeam) &&
Objects.equals(slot, other.slot);
}
@Override
public int hashCode() {
return Objects.hash(slot, toTeam, fromTeam);
}
}
After using this move and right after this trace message (after cachedMoveList is empty):
2022-09-14 20:13:28,568 [main] TRACE Created cachedMoveList: size (0), moveSelector (MoveListFactory(class pl.medaxtrans.pre.solver.FarSlotChangeMoveFactory)).
All solver threads get blocked in "WAIT" mode and they never stop waiting. What are they waiting for and what did I do wrong? This is expected that this move list ends at some point and I want solver to stop solving then. Why is it not happening as opposed to single thread solving? On the other hand while solving with one thread only I get:
No doable selected move at step index ... Terminating phase early.
And it ends. I want the exact same behavior with multithreading.