/*
* Copyright 2014 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.examples.cloudbalancing.app;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Queue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Test;
import org.optaplanner.core.api.solver.Solver;
import org.optaplanner.core.api.solver.SolverFactory;
import org.optaplanner.core.api.solver.event.BestSolutionChangedEvent;
import org.optaplanner.core.api.solver.event.SolverEventListener;
import org.optaplanner.core.config.solver.termination.TerminationConfig;
import org.optaplanner.examples.cloudbalancing.domain.CloudBalance;
import org.optaplanner.examples.cloudbalancing.domain.CloudProcess;
import org.optaplanner.examples.cloudbalancing.optional.realtime.AddProcessProblemFactChange;
import org.optaplanner.examples.cloudbalancing.persistence.CloudBalancingGenerator;
import org.optaplanner.examples.common.app.LoggingTest;
import static org.junit.Assert.*;
public class CloudBalancingDaemonTest extends LoggingTest {
private Object stageLock = new Object();
private AtomicInteger stageNumber = new AtomicInteger(0);
private CountDownLatch stage1Latch = new CountDownLatch(1);
private CountDownLatch stage2Latch = new CountDownLatch(1);
private CountDownLatch stage3Latch = new CountDownLatch(1);
private Queue<CloudProcess> notYetAddedProcessQueue = new ArrayDeque<>();
private volatile Throwable solverThreadException = null;
@Test(timeout = 600000)
public void daemon() throws InterruptedException {
// In main thread
Solver<CloudBalance> solver = buildSolver();
CloudBalance cloudBalance = buildPlanningProblem(4, 12);
SolverThread solverThread = new SolverThread(solver, cloudBalance);
solverThread.start();
// Wait for the solver thread to start up
waitForNextStage();
// Give the solver thread a chance to terminate and get into the daemon waiting state
Thread.sleep(500);
for (int i = 0; i < 8; i++) {
CloudProcess process = notYetAddedProcessQueue.poll();
solver.addProblemFactChange(new AddProcessProblemFactChange(process));
}
// Wait until those AddProcessChanges are processed
waitForNextStage();
assertEquals(8, (solver.getBestSolution()).getProcessList().size());
// Give the solver thread some time to solve, terminate and get into the daemon waiting state
Thread.sleep(1000);
while (!notYetAddedProcessQueue.isEmpty()) {
CloudProcess process = notYetAddedProcessQueue.poll();
solver.addProblemFactChange(new AddProcessProblemFactChange(process));
}
// Wait until those AddProcessChanges are processed
waitForNextStage();
solver.terminateEarly();
try {
// Wait until the solver thread dies.
solverThread.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("SolverThread did not die yet due to an interruption.", e);
}
assertEquals(true, solver.isEveryProblemFactChangeProcessed());
assertEquals(12, (solver.getBestSolution()).getProcessList().size());
}
private class SolverThread extends Thread implements SolverEventListener<CloudBalance> {
private final Solver<CloudBalance> solver;
private final CloudBalance cloudBalance;
private SolverThread(Solver<CloudBalance> solver, CloudBalance cloudBalance) {
this.solver = solver;
this.cloudBalance = cloudBalance;
}
@Override
public void run() { // In solver thread
solver.addEventListener(this);
nextStage(); // For an empty entity list, there is no bestSolutionChanged() event currently
try {
solver.solve(cloudBalance);
} catch (Throwable e) {
solverThreadException = e;
nextStage();
}
}
@Override
public void bestSolutionChanged(BestSolutionChangedEvent<CloudBalance> event) { // In solver thread
if (event.isEveryProblemFactChangeProcessed()
&& event.getNewBestSolution().getScore().isFeasible()) {
// TODO bestSolutionChanged() is not the most reliable way to control this test's execution:
// With another termination, a Solver can terminate before a feasible best solution event is fired
nextStage();
}
}
}
protected Solver<CloudBalance> buildSolver() {
SolverFactory<CloudBalance> solverFactory = SolverFactory.createFromXmlResource(
CloudBalancingApp.SOLVER_CONFIG);
solverFactory.getSolverConfig().setDaemon(true);
TerminationConfig terminationConfig = new TerminationConfig();
terminationConfig.setBestScoreFeasible(true);
solverFactory.getSolverConfig().setTerminationConfig(terminationConfig);
return solverFactory.buildSolver();
}
private CloudBalance buildPlanningProblem(int computerListSize, int processListSize) {
CloudBalance cloudBalance = new CloudBalancingGenerator().createCloudBalance(computerListSize, processListSize);
notYetAddedProcessQueue.addAll(cloudBalance.getProcessList());
cloudBalance.setProcessList(new ArrayList<>(notYetAddedProcessQueue.size()));
return cloudBalance;
}
private void waitForNextStage() throws InterruptedException {
CountDownLatch latch;
synchronized (stageLock) {
switch (stageNumber.get()) {
case 0:
latch = stage1Latch;
break;
case 1:
latch = stage2Latch;
break;
case 2:
latch = stage3Latch;
break;
default:
throw new IllegalStateException("Unsupported phaseNumber (" + stageNumber.get() + ").");
}
}
latch.await();
if (solverThreadException != null) {
throw new RuntimeException("SolverThread threw an exception.", solverThreadException);
}
// TODO Unlikely race condition: all bestSolutionChanged() could be processed before stageNumber is incremented
int stage;
synchronized (stageLock) {
stage = stageNumber.incrementAndGet();
}
logger.info("==== New testing stage ({}) started. ====", stage);
}
private void nextStage() {
synchronized (stageLock) {
switch (stageNumber.get()) {
case 0:
stage1Latch.countDown();
break;
case 1:
stage2Latch.countDown();
break;
case 2:
stage3Latch.countDown();
break;
}
}
}
}