/**
* diqube: Distributed Query Base.
*
* Copyright (C) 2015 Bastian Gloeckle
*
* This file is part of diqube.
*
* diqube is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.diqube.execution;
import java.util.Collection;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import org.diqube.data.table.TableShard;
import org.diqube.executionenv.ExecutionEnvironment;
import org.diqube.executionenv.VersionedExecutionEnvironment;
import org.diqube.queries.QueryRegistry;
import org.diqube.queries.QueryUuid;
import org.diqube.queries.QueryUuid.QueryUuidThreadState;
import org.diqube.threads.ExecutorManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This is a plan that is ready to be executed, either on a query master node or a query remote node. In case it is
* executed on a query remote, this is based on a specific {@link TableShard}.
*
* <p>
* This class basically holds the various {@link ExecutablePlanStep}s from which each will be executed in its own thread
* which is provided by an {@link Executor} on the call to {@link #executeAsynchronously(Executor)}. An instance
* additionally holds the _default_ {@link ExecutionEnvironment} which is used to hold temporary information while the
* plan is being executed - information that needs to be shared between the {@link ExecutablePlanStep}s. See JavaDoc of
* {@link ExecutionEnvironment} and {@link VersionedExecutionEnvironment} for more information.
*
* <p>
* An ExecutablePlan executes one part of the overall execution for a query (= a diql string sent by the user). Each
* {@link ExecutablePlan} therefore belongs to a query, which has a query UUID. As the execution of each query is
* distributed across the diqube cluster, each cluster node participating in the execution (query master and each query
* remote) has an execution UUID for identifying the things that are being executed together. Those two UUIDs can be
* fetched from {@link QueryUuid} class while in a thread that is currently executing some part of this
* {@link ExecutablePlan}.
*
* @author Bastian Gloeckle
*/
public class ExecutablePlan {
private static final Logger logger = LoggerFactory.getLogger(ExecutablePlan.class);
private Collection<ExecutablePlanStep> steps;
private ExecutionEnvironment defaultEnv;
private ExecutablePlanInfo info;
private ColumnVersionManager columnVersionManager;
/* package */ ExecutablePlan(ExecutionEnvironment defaultEnv, Collection<ExecutablePlanStep> steps,
ExecutablePlanInfo info, ColumnVersionManager columnVersionManager) {
this.defaultEnv = defaultEnv;
this.steps = steps;
this.info = info;
this.columnVersionManager = columnVersionManager;
}
public Collection<ExecutablePlanStep> getSteps() {
return steps;
}
public ExecutionEnvironment getDefaultExecutionEnvironment() {
return defaultEnv;
}
/**
* Execute all steps asynchronously using the given new {@link ExecutorService}.
*
* @param executor
* The {@link Executor} to be used to execute the steps. This Executor should have
* {@link #preferredExecutorServiceSize()} free threads, as specific threads might block while executing a
* specific step (as the step is e.g. waiting for additional data to be provided by other steps). If there a
* not enough threads available, it is not guaranteed that there will be no deadlock. Additionally, all
* threads of the executor should have the correct {@link QueryUuidThreadState} set (e.g. by having it
* created by
* {@link ExecutorManager#newQueryFixedThreadPoolWithTimeout(int, String, java.util.UUID, java.util.UUID)}).
* @return A Future that can be used to query the state of the computation of all steps. Note that
* {@link Future#cancel(boolean)} will not work on the returned future, stop the executor passed to this
* method instead (one might want to use
* {@link ExecutorManager#shutdownEverythingOfQueryExecution(java.util.UUID)} and
* {@link QueryRegistry#unregisterQueryExecution(java.util.UUID)}).
*/
public Future<Void> executeAsynchronously(Executor executor) {
logger.trace("Executing asynchronously {} on shard starting at row Id {}.", this,
defaultEnv.getFirstRowIdInShard());
ExecutionFuture future = new ExecutionFuture(steps.size());
// first: initialize all steps
AtomicInteger initializedCount = new AtomicInteger(0);
Object initializedSync = new Object();
for (ExecutablePlanStep step : steps) {
executor.execute(new Runnable() {
@Override
public void run() {
step.initialize();
logger.trace("A new step was initialized (shard starting at {}).", defaultEnv.getFirstRowIdInShard());
initializedCount.incrementAndGet();
synchronized (initializedSync) {
initializedSync.notifyAll();
}
}
});
}
// wait until all are initialized.
while (initializedCount.get() < steps.size()) {
synchronized (initializedSync) {
logger.trace("Waiting until all steps are initialized (shard starting at {}).",
defaultEnv.getFirstRowIdInShard());
try {
initializedSync.wait(1000);
} catch (InterruptedException e) {
// interrupted, exit quietly
}
}
}
logger.trace("All steps initialized, starting to execute (shard starting at {}).",
defaultEnv.getFirstRowIdInShard());
// start executing the steps.
for (ExecutablePlanStep step : steps)
executor.execute(new Runnable() {
@Override
public void run() {
boolean isException = false;
try {
step.run();
} catch (RuntimeException e) {
future.oneStepIsException();
isException = true;
throw e;
} finally {
if (!isException)
future.oneStepIsDone();
}
}
});
return future;
}
/**
* @return Number of threads that should be available to use when calling
* {@link #executeAsynchronously(ExecutorService)}.
*/
public int preferredExecutorServiceSize() {
return steps.size();
}
/**
* @return Information about what this {@link ExecutablePlan} will do when executed.
*/
public ExecutablePlanInfo getInfo() {
return info;
}
/**
* @return The {@link ColumnVersionManager} that is used when this ExecutablePlan is executed. Could be
* <code>null</code>.
*/
public ColumnVersionManager getColumnVersionManager() {
return columnVersionManager;
}
public static class ExecutionFuture implements Future<Void> {
private boolean cancelled = false;
private int numberOfStepsToWaitFor;
private AtomicInteger stepsDone = new AtomicInteger(0);
private Object getWait = new Object();
private boolean isException = false;
public ExecutionFuture(int numberOfStepsToWaitFor) {
this.numberOfStepsToWaitFor = numberOfStepsToWaitFor;
}
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
cancelled = !isDone();
return false;
}
@Override
public boolean isCancelled() {
return cancelled;
}
@Override
public boolean isDone() {
return numberOfStepsToWaitFor == stepsDone.intValue();
}
@Override
public Void get() throws InterruptedException, ExecutionException {
while (!isDone()) {
if (isException)
throw new ExecutionException("There was an exception executing the plan", null);
synchronized (getWait) {
getWait.wait(500);
}
}
return null;
}
@Override
public Void get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
synchronized (getWait) {
if (isDone())
return null;
if (isException)
throw new ExecutionException("There was an exception executing the plan", null);
getWait.wait(unit.toMillis(timeout));
}
if (isDone())
return null;
if (isException)
throw new ExecutionException("There was an exception executing the plan", null);
throw new TimeoutException("ExecutablePlan not executed yet");
}
private void oneStepIsDone() {
stepsDone.incrementAndGet();
synchronized (getWait) {
getWait.notifyAll();
}
}
private void oneStepIsException() {
isException = true;
synchronized (getWait) {
getWait.notifyAll();
}
}
}
@Override
public String toString() {
return "ExecutablePlan[steps=" + steps + "]";
}
}