title: Optaplanner: Custom move and multithreading - all solver threads stuck in waiting forever after custom move list is exhausted
body: <p>I am doing a simple time slot (planning entity) -&gt; 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</p>
  <p>This is the move factory:</p>
  <pre><code>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&lt;ShiftsSolution&gt; {
  
      private static Predicate&lt;HalfHourTimeslot&gt; equalTime(HalfHourTimeslot slot2) {
          return slot -&gt; slot.compareTo(slot2) == 0;
      }
  
      private static Predicate&lt;HalfHourTimeslot&gt; sameDayAndTeam(DayOfWeek day, Team team) {
          return slot -&gt; slot.getDayOfWeek().equals(day) &amp;&amp; slot.getTeam().equals(team);
      }
  
      @Override
      public List&lt;? extends Move&lt;ShiftsSolution&gt;&gt; createMoveList(ShiftsSolution solution) {
          List&lt;FarSlotChangeMove&gt; moves = new ArrayList&lt;&gt;();
          List&lt;HalfHourTimeslot&gt; slots = solution.getSlots();
          List&lt;Team&gt; teams = solution.getTeams();
          for (Team fromTeam : teams) {
              for (Team toTeam : teams) {
                  if (fromTeam != toTeam) {
                      for (DayOfWeek day : DayOfWeek.values()) {
                          List&lt;HalfHourTimeslot&gt; fromTeamSlots = slots.stream().filter(sameDayAndTeam(day, fromTeam)).collect(Collectors.toList());
                          List&lt;HalfHourTimeslot&gt; toTeamSlots = slots.stream().filter(sameDayAndTeam(day, toTeam)).collect(Collectors.toList());
  
                          Optional&lt;HalfHourTimeslot&gt; fromMinOpt = fromTeamSlots.stream().min(Comparator.naturalOrder());
                          Optional&lt;HalfHourTimeslot&gt; fromMaxOpt = fromTeamSlots.stream().max(Comparator.naturalOrder());
                          Optional&lt;HalfHourTimeslot&gt; toMaxOpt = toTeamSlots.stream().max(Comparator.naturalOrder());
                          Optional&lt;HalfHourTimeslot&gt; toMinOpt = toTeamSlots.stream().min(Comparator.naturalOrder());
  
                          if (fromMinOpt.isPresent() &amp;&amp; fromMaxOpt.isPresent() &amp;&amp; toMaxOpt.isPresent() &amp;&amp; toMinOpt.isPresent())
                          {
                              HalfHourTimeslot fromMin = fromMinOpt.get();
                              HalfHourTimeslot fromMax = fromMaxOpt.get();
                              HalfHourTimeslot toMax = toMaxOpt.get();
                              HalfHourTimeslot toMin = toMinOpt.get();
  
                              if (toMin.compareTo(fromMin) &lt; 0 &amp;&amp; toMax.compareTo(fromMin) &gt; 0 &amp;&amp; toTeamSlots.stream().noneMatch(equalTime(fromMin)))
                              {
                                  moves.add(new FarSlotChangeMove(fromMin, toTeam));
                              }
  
                              if (toMin.compareTo(fromMax) &lt; 0 &amp;&amp; toMax.compareTo(fromMax) &gt; 0 &amp;&amp; toTeamSlots.stream().noneMatch(equalTime(fromMax)))
                              {
                                  moves.add(new FarSlotChangeMove(fromMax, toTeam));
                              }
                          }
                      }
                  }
              }
          }
          return moves;
      }
  }
  </code></pre>
  <p>And this is the problematic move:</p>
  <pre><code>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&lt;ShiftsSolution&gt; {
  
      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&lt;ShiftsSolution&gt; createUndoMove(ScoreDirector&lt;ShiftsSolution&gt; scoreDirector) {
          return new FarSlotChangeMove(slot, fromTeam);
      }
  
      @Override
      protected void doMoveOnGenuineVariables(ScoreDirector&lt;ShiftsSolution&gt; scoreDirector) {
          scoreDirector.beforeVariableChanged(slot, &quot;team&quot;);
          slot.setTeam(toTeam);
          scoreDirector.afterVariableChanged(slot, &quot;team&quot;);
      }
  
      @Override
      public boolean isMoveDoable(ScoreDirector&lt;ShiftsSolution&gt; 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&lt;ShiftsSolution&gt; destinationScoreDirector) {
          return new FarSlotChangeMove(destinationScoreDirector.lookUpWorkingObject(slot), destinationScoreDirector.lookUpWorkingObject(toTeam));
      }
  
      @Override
      public Collection&lt;?&gt; getPlanningEntities() {
          return Collections.singletonList(slot);
      }
  
      @Override
      public Collection&lt;?&gt; getPlanningValues() {
          return Collections.singletonList(toTeam);
      }
  
      @Override
      public String toString() {
          return slot + &quot; {&quot; + fromTeam + &quot;-&gt;&quot; + toTeam + &quot;}&quot;;
      }
  
      @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) &amp;&amp;
                  Objects.equals(toTeam, other.toTeam) &amp;&amp;
                  Objects.equals(slot, other.slot);
      }
  
      @Override
      public int hashCode() {
          return Objects.hash(slot, toTeam, fromTeam);
      }
  }
  </code></pre>
  <p>After using this move and right after this trace message (after cachedMoveList is empty):</p>
  <pre><code>2022-09-14 20:13:28,568 [main] TRACE     Created cachedMoveList: size (0), moveSelector (MoveListFactory(class pl.medaxtrans.pre.solver.FarSlotChangeMoveFactory)).
  </code></pre>
  <p>All solver threads get blocked in &quot;WAIT&quot; 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:</p>
  <pre><code>                    No doable selected move at step index ... Terminating phase early.
  </code></pre>
  <p>And it ends. I want the exact same behavior with multithreading.</p>