/*
* Copyright © 2014-2016 Cask Data, Inc.
*
* 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 co.cask.cdap.internal.app.runtime.workflow;
import co.cask.cdap.api.RuntimeContext;
import co.cask.cdap.api.common.RuntimeArguments;
import co.cask.cdap.api.common.Scope;
import co.cask.cdap.api.workflow.NodeStatus;
import co.cask.cdap.api.workflow.WorkflowNodeState;
import co.cask.cdap.api.workflow.WorkflowSpecification;
import co.cask.cdap.api.workflow.WorkflowToken;
import co.cask.cdap.app.program.Program;
import co.cask.cdap.app.program.Programs;
import co.cask.cdap.app.runtime.Arguments;
import co.cask.cdap.app.runtime.ProgramController;
import co.cask.cdap.app.runtime.ProgramOptions;
import co.cask.cdap.app.runtime.ProgramRunner;
import co.cask.cdap.app.runtime.ProgramRunnerFactory;
import co.cask.cdap.app.runtime.WorkflowTokenProvider;
import co.cask.cdap.common.app.RunIds;
import co.cask.cdap.common.conf.CConfiguration;
import co.cask.cdap.common.lang.ProgramClassLoader;
import co.cask.cdap.internal.app.program.ForwardingProgram;
import co.cask.cdap.internal.app.runtime.AbstractListener;
import co.cask.cdap.internal.app.runtime.BasicArguments;
import co.cask.cdap.internal.app.runtime.ProgramOptionConstants;
import co.cask.cdap.internal.app.runtime.SimpleProgramOptions;
import co.cask.cdap.proto.ProgramType;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.common.io.Closeables;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.SettableFuture;
import com.google.gson.Gson;
import org.apache.twill.common.Threads;
import java.io.Closeable;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import javax.annotation.Nullable;
/**
* An Abstract class implementing {@link ProgramWorkflowRunner}, providing a {@link Runnable} of
* the programs to run in a workflow.
* <p>
* Programs that extend this class (such as {@link MapReduceProgramWorkflowRunner} or
* {@link SparkProgramWorkflowRunner}) can execute their associated programs through the
* {@link AbstractProgramWorkflowRunner#blockForCompletion} by providing the {@link ProgramController} and
* {@link RuntimeContext} that they obtained through the {@link ProgramRunner}.
* </p>
* The {@link RuntimeContext} is blocked until completion of the associated program.
*/
abstract class AbstractProgramWorkflowRunner implements ProgramWorkflowRunner {
private static final Gson GSON = new Gson();
private final CConfiguration cConf;
private final Arguments userArguments;
private final Arguments systemArguments;
private final Program workflowProgram;
private final String nodeId;
private final Map<String, WorkflowNodeState> nodeStates;
protected final WorkflowSpecification workflowSpec;
protected final ProgramRunnerFactory programRunnerFactory;
protected final WorkflowToken token;
AbstractProgramWorkflowRunner(CConfiguration cConf, Program workflowProgram, ProgramOptions workflowProgramOptions,
ProgramRunnerFactory programRunnerFactory, WorkflowSpecification workflowSpec,
WorkflowToken token, String nodeId, Map<String, WorkflowNodeState> nodeStates) {
this.cConf = cConf;
this.userArguments = workflowProgramOptions.getUserArguments();
this.workflowProgram = workflowProgram;
this.programRunnerFactory = programRunnerFactory;
this.workflowSpec = workflowSpec;
this.systemArguments = workflowProgramOptions.getArguments();
this.token = token;
this.nodeId = nodeId;
this.nodeStates = nodeStates;
}
/**
* Returns the {@link ProgramType} supported by this runner.
*/
protected abstract ProgramType getProgramType();
/**
* Rewrites the given {@link Program} to a {@link Program} that represents the {@link ProgramType} as
* returned by {@link #getProgramType()}.
*/
protected abstract Program rewriteProgram(String name, Program program);
@Override
public final Runnable create(String name) {
ProgramRunner programRunner = programRunnerFactory.create(getProgramType());
try {
Program program = rewriteProgram(name, createProgram(programRunner, workflowProgram));
return getProgramRunnable(name, programRunner, program);
} catch (Exception e) {
closeProgramRunner(programRunner);
throw Throwables.propagate(e);
}
}
/**
* Gets a {@link Runnable} for the {@link Program}.
*
* @param name name of the {@link Program}
* @param program the {@link Program}
* @return a {@link Runnable} for this {@link Program}
*/
private Runnable getProgramRunnable(String name, final ProgramRunner programRunner, final Program program) {
Map<String, String> systemArgumentsMap = Maps.newHashMap();
systemArgumentsMap.putAll(systemArguments.asMap());
// Generate the new RunId here for the program running under Workflow
systemArgumentsMap.put(ProgramOptionConstants.RUN_ID, RunIds.generate().getId());
// Add Workflow specific system arguments to be passed to the underlying program
systemArgumentsMap.put(ProgramOptionConstants.WORKFLOW_NAME, workflowSpec.getName());
systemArgumentsMap.put(ProgramOptionConstants.WORKFLOW_RUN_ID,
systemArguments.getOption(ProgramOptionConstants.RUN_ID));
systemArgumentsMap.put(ProgramOptionConstants.WORKFLOW_NODE_ID, nodeId);
systemArgumentsMap.put(ProgramOptionConstants.PROGRAM_NAME_IN_WORKFLOW, name);
systemArgumentsMap.put(ProgramOptionConstants.WORKFLOW_TOKEN, GSON.toJson(token));
final ProgramOptions options = new SimpleProgramOptions(
program.getName(),
new BasicArguments(ImmutableMap.copyOf(systemArgumentsMap)),
new BasicArguments(RuntimeArguments.extractScope(Scope.scopeFor(program.getType().getCategoryName()), name,
userArguments.asMap()))
);
return new Runnable() {
@Override
public void run() {
try {
runAndWait(programRunner, program, options);
} catch (Exception e) {
throw Throwables.propagate(e);
}
}
};
}
/**
* Creates a new {@link Program} instance for the execution by the given {@link ProgramRunner}.
*/
private Program createProgram(ProgramRunner programRunner, Program originalProgram) throws IOException {
ClassLoader classLoader = originalProgram.getClassLoader();
if (!(classLoader instanceof ProgramClassLoader)) {
// If for some reason that the ClassLoader of the original program is not ProgramClassLoader,
// we don't own the program, hence don't close it when execution of the program completed.
return new ForwardingProgram(originalProgram) {
@Override
public void close() throws IOException {
// no-op
}
};
}
return Programs.create(cConf, programRunner, originalProgram.getJarLocation(),
((ProgramClassLoader) classLoader).getDir());
}
private void runAndWait(ProgramRunner programRunner, Program program, ProgramOptions options) throws Exception {
Closeable closeable = createCloseable(programRunner, program);
ProgramController controller;
try {
controller = programRunner.run(program, options);
} catch (Throwable t) {
// If there is any exception when running the program, close the program to release resources.
// Otherwise it will be released when the execution completed.
Closeables.closeQuietly(closeable);
throw t;
}
blockForCompletion(closeable, controller);
if (controller instanceof WorkflowTokenProvider) {
updateWorkflowToken(((WorkflowTokenProvider) controller).getWorkflowToken());
} else {
// This shouldn't happen
throw new IllegalStateException("No WorkflowToken available after program completed: " + program.getId());
}
}
/**
* Adds a listener to the {@link ProgramController} and blocks for completion.
*
* @param closeable a {@link Closeable} to call when the program execution completed
* @param controller the {@link ProgramController} for the program
* @throws Exception if the execution failed
*/
private void blockForCompletion(final Closeable closeable, final ProgramController controller) throws Exception {
// Execute the program.
final SettableFuture<Void> completion = SettableFuture.create();
controller.addListener(new AbstractListener() {
@Override
public void init(ProgramController.State currentState, @Nullable Throwable cause) {
switch (currentState) {
case COMPLETED:
completed();
break;
case KILLED:
killed();
break;
case ERROR:
error(cause);
break;
}
}
@Override
public void completed() {
Closeables.closeQuietly(closeable);
nodeStates.put(nodeId, new WorkflowNodeState(nodeId, NodeStatus.COMPLETED, controller.getRunId().getId(),
null));
completion.set(null);
}
@Override
public void killed() {
Closeables.closeQuietly(closeable);
nodeStates.put(nodeId, new WorkflowNodeState(nodeId, NodeStatus.KILLED, controller.getRunId().getId(), null));
completion.set(null);
}
@Override
public void error(Throwable cause) {
Closeables.closeQuietly(closeable);
nodeStates.put(nodeId, new WorkflowNodeState(nodeId, NodeStatus.FAILED, controller.getRunId().getId(), cause));
completion.setException(cause);
}
}, Threads.SAME_THREAD_EXECUTOR);
// Block for completion.
try {
completion.get();
} catch (ExecutionException e) {
Throwable cause = e.getCause();
if (cause instanceof Exception) {
throw (Exception) cause;
}
throw Throwables.propagate(cause);
} catch (InterruptedException e) {
try {
Futures.getUnchecked(controller.stop());
} catch (Throwable t) {
// no-op
}
// reset the interrupt
Thread.currentThread().interrupt();
}
}
private void updateWorkflowToken(WorkflowToken workflowToken) throws Exception {
((BasicWorkflowToken) token).mergeToken(workflowToken);
}
/**
* Creates a {@link Closeable} that will close the given {@link ProgramRunner} and {@link Program}.
*/
private Closeable createCloseable(final ProgramRunner programRunner, final Program program) {
return new Closeable() {
@Override
public void close() throws IOException {
Closeables.closeQuietly(program);
closeProgramRunner(programRunner);
}
};
}
/**
* Closes the given {@link ProgramRunner} if it implements {@link Closeable}.
*/
private void closeProgramRunner(ProgramRunner programRunner) {
if (programRunner instanceof Closeable) {
Closeables.closeQuietly((Closeable) programRunner);
}
}
}