/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.optaplanner.core.impl.partitionedsearch;
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
import java.util.concurrent.Semaphore;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import org.optaplanner.core.api.domain.solution.PlanningSolution;
import org.optaplanner.core.config.heuristic.policy.HeuristicConfigPolicy;
import org.optaplanner.core.config.phase.PhaseConfig;
import org.optaplanner.core.config.solver.recaller.BestSolutionRecallerConfig;
import org.optaplanner.core.impl.heuristic.move.Move;
import org.optaplanner.core.impl.partitionedsearch.event.PartitionedSearchPhaseLifecycleListener;
import org.optaplanner.core.impl.partitionedsearch.partitioner.SolutionPartitioner;
import org.optaplanner.core.impl.partitionedsearch.queue.PartitionQueue;
import org.optaplanner.core.impl.partitionedsearch.scope.PartitionChangeMove;
import org.optaplanner.core.impl.partitionedsearch.scope.PartitionedSearchPhaseScope;
import org.optaplanner.core.impl.partitionedsearch.scope.PartitionedSearchStepScope;
import org.optaplanner.core.impl.phase.AbstractPhase;
import org.optaplanner.core.impl.phase.Phase;
import org.optaplanner.core.impl.score.director.InnerScoreDirector;
import org.optaplanner.core.impl.solver.ChildThreadType;
import org.optaplanner.core.impl.solver.recaller.BestSolutionRecaller;
import org.optaplanner.core.impl.solver.scope.DefaultSolverScope;
import org.optaplanner.core.impl.solver.termination.ChildThreadPlumbingTermination;
import org.optaplanner.core.impl.solver.termination.OrCompositeTermination;
import org.optaplanner.core.impl.solver.termination.Termination;
/**
* Default implementation of {@link PartitionedSearchPhase}.
* @param <Solution_> the solution type, the class with the {@link PlanningSolution} annotation
*/
public class DefaultPartitionedSearchPhase<Solution_> extends AbstractPhase<Solution_>
implements PartitionedSearchPhase<Solution_>, PartitionedSearchPhaseLifecycleListener<Solution_> {
protected SolutionPartitioner<Solution_> solutionPartitioner;
protected ThreadFactory threadFactory;
protected Integer runnablePartThreadLimit;
protected List<PhaseConfig> phaseConfigList;
protected HeuristicConfigPolicy configPolicy;
public DefaultPartitionedSearchPhase(int phaseIndex, String logIndentation,
BestSolutionRecaller<Solution_> bestSolutionRecaller, Termination termination) {
super(phaseIndex, logIndentation, bestSolutionRecaller, termination);
}
public void setSolutionPartitioner(SolutionPartitioner<Solution_> solutionPartitioner) {
this.solutionPartitioner = solutionPartitioner;
}
public void setThreadFactory(ThreadFactory threadFactory) {
this.threadFactory = threadFactory;
}
public void setRunnablePartThreadLimit(Integer runnablePartThreadLimit) {
this.runnablePartThreadLimit = runnablePartThreadLimit;
}
public void setPhaseConfigList(List<PhaseConfig> phaseConfigList) {
this.phaseConfigList = phaseConfigList;
}
public void setConfigPolicy(HeuristicConfigPolicy configPolicy) {
this.configPolicy = configPolicy;
}
@Override
public String getPhaseTypeString() {
return "Partitioned Search";
}
// ************************************************************************
// Worker methods
// ************************************************************************
@Override
public void solve(DefaultSolverScope<Solution_> solverScope) {
PartitionedSearchPhaseScope<Solution_> phaseScope = new PartitionedSearchPhaseScope<>(solverScope);
List<Solution_> partList = solutionPartitioner.splitWorkingSolution(
solverScope.getScoreDirector(), runnablePartThreadLimit);
int partCount = partList.size();
phaseScope.setPartCount(partCount);
phaseStarted(phaseScope);
ThreadPoolExecutor threadPoolExecutor = createThreadPoolExecutor();
if (threadPoolExecutor.getMaximumPoolSize() < partCount) {
throw new IllegalStateException(
"The threadPoolExecutor's maximumPoolSize (" + threadPoolExecutor.getMaximumPoolSize()
+ ") is less than the partCount (" + partCount + "), so some partitions will starve.\n"
+ "Normally this impossible because the threadPoolExecutor should be unbounded:"
+ " Use runnablePartThreadLimit (" + runnablePartThreadLimit
+ ") instead to avoid CPU hogging and live locks.");
}
ChildThreadPlumbingTermination childThreadPlumbingTermination = new ChildThreadPlumbingTermination();
PartitionQueue<Solution_> partitionQueue = new PartitionQueue<>(partCount);
Semaphore runnablePartThreadSemaphore
= runnablePartThreadLimit == null ? null : new Semaphore(runnablePartThreadLimit);
try {
for (ListIterator<Solution_> it = partList.listIterator(); it.hasNext();) {
int partIndex = it.nextIndex();
Solution_ part = it.next();
PartitionSolver<Solution_> partitionSolver = buildPartitionSolver(
childThreadPlumbingTermination, runnablePartThreadSemaphore, solverScope);
partitionSolver.addEventListener(event -> {
InnerScoreDirector<Solution_> childScoreDirector = partitionSolver.solverScope.getScoreDirector();
PartitionChangeMove<Solution_> move = PartitionChangeMove.createMove(childScoreDirector);
InnerScoreDirector<Solution_> parentScoreDirector = solverScope.getScoreDirector();
move = move.rebase(parentScoreDirector);
partitionQueue.addMove(partIndex, move);
});
threadPoolExecutor.submit(() -> {
try {
partitionSolver.solve(part);
partitionQueue.addFinish(partIndex);
} catch (Throwable throwable) {
// Any Exception or even Error that happens here (on a partition thread) must be stored
// in the partitionQueue in order to be propagated to the solver thread
partitionQueue.addExceptionThrown(partIndex, throwable);
}
});
}
for (PartitionChangeMove<Solution_> step : partitionQueue) {
PartitionedSearchStepScope<Solution_> stepScope = new PartitionedSearchStepScope<>(phaseScope);
stepStarted(stepScope);
stepScope.setStep(step);
if (logger.isDebugEnabled()) {
stepScope.setStepString(step.toString());
}
doStep(stepScope);
stepEnded(stepScope);
phaseScope.setLastCompletedStepScope(stepScope);
}
} finally {
// If a partition thread throws an Exception, it is propagated here
// but the other partition threads won't finish any time soon, so we need to ask them to terminate
childThreadPlumbingTermination.terminateChildren();
try {
// First wait for solvers to terminate voluntarily (because we have just issued children termination)
if (!threadPoolExecutor.awaitTermination(1, TimeUnit.SECONDS)) {
// Some solvers refused to complete. Busy threads will be interrupted in the finally block
logger.warn("{}Partitioned Search threadPoolExecutor didn't terminate within timeout (1 second).",
logIndentation);
}
} catch (InterruptedException e) {
// Interrupted while waiting for thread pool termination. Busy threads will be interrupted
// in the finally block
Thread.currentThread().interrupt();
throw new IllegalStateException("Thread pool shutdown was interrupted.", e);
} finally {
threadPoolExecutor.shutdownNow();
}
}
phaseEnded(phaseScope);
}
private ThreadPoolExecutor createThreadPoolExecutor() {
// Based on Executors.newCachedThreadPool(...)
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<>(),
threadFactory);
}
public PartitionSolver<Solution_> buildPartitionSolver(
ChildThreadPlumbingTermination childThreadPlumbingTermination, Semaphore runnablePartThreadSemaphore,
DefaultSolverScope<Solution_> solverScope) {
Termination partTermination = new OrCompositeTermination(childThreadPlumbingTermination,
termination.createChildThreadTermination(solverScope, ChildThreadType.PART_THREAD));
BestSolutionRecaller<Solution_> bestSolutionRecaller = new BestSolutionRecallerConfig()
.buildBestSolutionRecaller(configPolicy.getEnvironmentMode());
List<Phase<Solution_>> phaseList = new ArrayList<>(phaseConfigList.size());
int partPhaseIndex = 0;
for (PhaseConfig phaseConfig : phaseConfigList) {
phaseList.add(phaseConfig.buildPhase(partPhaseIndex, configPolicy, bestSolutionRecaller, partTermination));
partPhaseIndex++;
}
// TODO create PartitionSolverScope alternative to deal with 3 layer terminations
DefaultSolverScope<Solution_> partSolverScope
= solverScope.createChildThreadSolverScope(ChildThreadType.PART_THREAD);
partSolverScope.setRunnableThreadSemaphore(runnablePartThreadSemaphore);
return new PartitionSolver<>(partTermination, bestSolutionRecaller, phaseList, partSolverScope);
}
protected void doStep(PartitionedSearchStepScope<Solution_> stepScope) {
Move<Solution_> nextStep = stepScope.getStep();
nextStep.doMove(stepScope.getScoreDirector());
calculateWorkingStepScore(stepScope, nextStep);
bestSolutionRecaller.processWorkingSolutionDuringStep(stepScope);
}
@Override
public void phaseStarted(PartitionedSearchPhaseScope<Solution_> phaseScope) {
super.phaseStarted(phaseScope);
}
@Override
public void stepStarted(PartitionedSearchStepScope<Solution_> stepScope) {
super.stepStarted(stepScope);
}
@Override
public void stepEnded(PartitionedSearchStepScope<Solution_> stepScope) {
super.stepEnded(stepScope);
PartitionedSearchPhaseScope<Solution_> phaseScope = stepScope.getPhaseScope();
if (logger.isDebugEnabled()) {
logger.debug("{} PS step ({}), time spent ({}), score ({}), {} best score ({}), picked move ({}).",
logIndentation,
stepScope.getStepIndex(),
phaseScope.calculateSolverTimeMillisSpentUpToNow(),
stepScope.getScore(),
(stepScope.getBestScoreImproved() ? "new" : " "), phaseScope.getBestScore(),
stepScope.getStepString());
}
}
@Override
public void phaseEnded(PartitionedSearchPhaseScope<Solution_> phaseScope) {
super.phaseEnded(phaseScope);
phaseScope.endingNow();
logger.info("{}Partitioned Search phase ({}) ended: time spent ({}), best score ({}),"
+ " score calculation speed ({}/sec), step total ({}), partCount ({}), runnablePartThreadLimit ({}).",
logIndentation,
phaseIndex,
phaseScope.calculateSolverTimeMillisSpentUpToNow(),
phaseScope.getBestScore(),
phaseScope.getPhaseScoreCalculationSpeed(),
phaseScope.getNextStepIndex(),
phaseScope.getPartCount(),
runnablePartThreadLimit);
}
}