/*
* 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.app.runtime;
import co.cask.cdap.api.app.ApplicationSpecification;
import co.cask.cdap.api.plugin.Plugin;
import co.cask.cdap.app.program.Program;
import co.cask.cdap.app.program.Programs;
import co.cask.cdap.common.ArtifactNotFoundException;
import co.cask.cdap.common.app.RunIds;
import co.cask.cdap.common.conf.CConfiguration;
import co.cask.cdap.common.conf.Constants;
import co.cask.cdap.common.io.Locations;
import co.cask.cdap.common.lang.jar.BundleJarUtil;
import co.cask.cdap.common.utils.DirUtils;
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.internal.app.runtime.artifact.ArtifactDetail;
import co.cask.cdap.internal.app.runtime.artifact.ArtifactRepository;
import co.cask.cdap.internal.app.runtime.artifact.Artifacts;
import co.cask.cdap.internal.app.runtime.service.SimpleRuntimeInfo;
import co.cask.cdap.proto.Id;
import co.cask.cdap.proto.ProgramType;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.base.Throwables;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.collect.Table;
import com.google.common.io.Closeables;
import com.google.common.io.Files;
import com.google.common.util.concurrent.AbstractIdleService;
import org.apache.twill.api.RunId;
import org.apache.twill.common.Threads;
import org.apache.twill.filesystem.Location;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import javax.annotation.Nullable;
/**
* A ProgramRuntimeService that keeps an in memory map for all running programs.
*/
public abstract class AbstractProgramRuntimeService extends AbstractIdleService implements ProgramRuntimeService {
private static final Logger LOG = LoggerFactory.getLogger(AbstractProgramRuntimeService.class);
private static final EnumSet<ProgramController.State> COMPLETED_STATES = EnumSet.of(ProgramController.State.COMPLETED,
ProgramController.State.KILLED,
ProgramController.State.ERROR);
private final CConfiguration cConf;
private final ReadWriteLock runtimeInfosLock;
private final Table<ProgramType, RunId, RuntimeInfo> runtimeInfos;
private final ProgramRunnerFactory programRunnerFactory;
private final ArtifactRepository artifactRepository;
protected AbstractProgramRuntimeService(CConfiguration cConf, ProgramRunnerFactory programRunnerFactory,
ArtifactRepository artifactRepository) {
this.cConf = cConf;
this.runtimeInfosLock = new ReentrantReadWriteLock();
this.runtimeInfos = HashBasedTable.create();
this.programRunnerFactory = programRunnerFactory;
this.artifactRepository = artifactRepository;
}
@Override
public final RuntimeInfo run(Program program, ProgramOptions options) {
ProgramRunner runner = programRunnerFactory.create(program.getType());
Preconditions.checkNotNull(runner, "Fail to get ProgramRunner for type " + program.getType());
RunId runId = RunIds.generate();
ProgramOptions optionsWithRunId = updateProgramOptions(options, runId);
File tempDir = createTempDirectory(program.getId(), runId);
Runnable cleanUpTask = createCleanupTask(tempDir, runner);
try {
ProgramOptions optionsWithPlugins = createPluginSnapshot(optionsWithRunId, program.getId(), tempDir,
program.getApplicationSpecification());
// The Jar Location will be null for some unit-test. It won't be for production.
Location jarLocation = program.getJarLocation();
Program executableProgram = jarLocation == null ? program : createProgram(cConf, runner, jarLocation, tempDir);
cleanUpTask = createCleanupTask(cleanUpTask, executableProgram);
RuntimeInfo runtimeInfo = createRuntimeInfo(runner.run(executableProgram, optionsWithPlugins), program);
monitorProgram(runtimeInfo, cleanUpTask);
return runtimeInfo;
} catch (IOException e) {
cleanUpTask.run();
LOG.error("Exception while trying to createPluginSnapshot", e);
throw Throwables.propagate(e);
}
}
/**
* Creates a {@link Program} for the given {@link ProgramRunner} from the given program jar {@link Location}.
*/
private Program createProgram(CConfiguration cConf, ProgramRunner programRunner,
Location programJarLocation, File tempDir) throws IOException {
// Take a snapshot of the JAR file to avoid program mutation
File programJar = Locations.linkOrCopy(programJarLocation, new File(tempDir, "program.jar"));
// Unpack the JAR file
File unpackedDir = new File(tempDir, "unpacked");
unpackedDir.mkdirs();
BundleJarUtil.unJar(Files.newInputStreamSupplier(programJar), unpackedDir);
return Programs.create(cConf, programRunner, Locations.toLocation(programJar), unpackedDir);
}
private Runnable createCleanupTask(final Object... resources) {
return new Runnable() {
@Override
public void run() {
List<Object> resourceList = new ArrayList<>(Arrays.asList(resources));
Collections.reverse(resourceList);
for (Object resource : resourceList) {
if (resource == null) {
continue;
}
try {
if (resource instanceof File) {
File file = (File) resource;
if (file.isDirectory()) {
DirUtils.deleteDirectoryContents(file);
} else {
file.delete();
}
} else if (resource instanceof Closeable) {
Closeables.closeQuietly((Closeable) resource);
} else if (resource instanceof Runnable) {
((Runnable) resource).run();
}
} catch (Throwable t) {
LOG.warn("Exception when cleaning up resource {}", resource, t);
}
}
}
};
}
/**
* Creates a local temporary directory for this program run.
*/
private File createTempDirectory(Id.Program programId, RunId runId) {
File tempDir = new File(cConf.get(Constants.CFG_LOCAL_DATA_DIR),
cConf.get(Constants.AppFabric.TEMP_DIR)).getAbsoluteFile();
File dir = new File(tempDir, String.format("%s.%s.%s.%s.%s",
programId.getType().name().toLowerCase(),
programId.getNamespaceId(), programId.getApplicationId(),
programId.getId(), runId.getId()));
dir.mkdirs();
return dir;
}
/**
* Return the copy of the {@link ProgramOptions} including locations of plugin artifacts in it.
* @param options the {@link ProgramOptions} in which the locations of plugin artifacts needs to be included
* @param programId Id of the Program
* @param tempDir Temporary Directory to create the plugin artifact snapshot
* @param appSpec program's Application Specification
* @return the copy of the program options with locations of plugin artifacts included in them
*/
private ProgramOptions createPluginSnapshot(ProgramOptions options, Id.Program programId, File tempDir,
@Nullable ApplicationSpecification appSpec) throws IOException {
// appSpec is null in an unit test
if (appSpec == null || appSpec.getPlugins().isEmpty()) {
return options;
}
Set<String> files = Sets.newHashSet();
ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
builder.putAll(options.getArguments().asMap());
for (Map.Entry<String, Plugin> pluginEntry : appSpec.getPlugins().entrySet()) {
Plugin plugin = pluginEntry.getValue();
File destFile = new File(tempDir, Artifacts.getFileName(plugin.getArtifactId()));
// Skip if the file has already been copied.
if (!files.add(destFile.getName())) {
continue;
}
try {
ArtifactDetail detail = artifactRepository.getArtifact(Id.Artifact.from(programId.getNamespace(),
plugin.getArtifactId()));
Files.copy(Locations.newInputSupplier(detail.getDescriptor().getLocation()), destFile);
} catch (ArtifactNotFoundException e) {
throw new IllegalArgumentException(String.format("Artifact %s could not be found", plugin.getArtifactId()), e);
}
}
LOG.debug("Plugin artifacts of {} copied to {}", programId, tempDir.getAbsolutePath());
builder.put(ProgramOptionConstants.PLUGIN_DIR, tempDir.getAbsolutePath());
return new SimpleProgramOptions(options.getName(), new BasicArguments(builder.build()),
options.getUserArguments(), options.isDebug());
}
protected Map<String, String> getExtraProgramOptions() {
return Collections.emptyMap();
}
/**
* Updates the given {@link ProgramOptions} and return a new instance.
* It copies the {@link ProgramOptions} and add all options returned by {@link #getExtraProgramOptions()}.
* It then adds the {@link RunId} to it.
*
* @param options The {@link ProgramOptions} in which the RunId to be included
* @param runId The RunId to be included
* @return the copy of the program options with RunId included in them
*/
private ProgramOptions updateProgramOptions(ProgramOptions options, RunId runId) {
ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
builder.putAll(options.getArguments().asMap());
builder.putAll(getExtraProgramOptions());
builder.put(ProgramOptionConstants.RUN_ID, runId.getId());
return new SimpleProgramOptions(options.getName(), new BasicArguments(builder.build()), options.getUserArguments(),
options.isDebug());
}
protected RuntimeInfo createRuntimeInfo(ProgramController controller, Program program) {
return new SimpleRuntimeInfo(controller, program);
}
protected List<RuntimeInfo> getRuntimeInfos() {
Lock lock = runtimeInfosLock.readLock();
lock.lock();
try {
return ImmutableList.copyOf(runtimeInfos.values());
} finally {
lock.unlock();
}
}
@Override
public RuntimeInfo lookup(Id.Program programId, RunId runId) {
Lock lock = runtimeInfosLock.readLock();
lock.lock();
try {
return runtimeInfos.get(programId.getType(), runId);
} finally {
lock.unlock();
}
}
@Override
public Map<RunId, RuntimeInfo> list(ProgramType type) {
Lock lock = runtimeInfosLock.readLock();
lock.lock();
try {
return ImmutableMap.copyOf(runtimeInfos.row(type));
} finally {
lock.unlock();
}
}
@Override
public Map<RunId, RuntimeInfo> list(final Id.Program program) {
return Maps.filterValues(list(program.getType()), new Predicate<RuntimeInfo>() {
@Override
public boolean apply(RuntimeInfo info) {
return info.getProgramId().equals(program);
}
});
}
@Override
public boolean checkAnyRunning(Predicate<Id.Program> predicate, ProgramType... types) {
for (ProgramType type : types) {
for (Map.Entry<RunId, ProgramRuntimeService.RuntimeInfo> entry : list(type).entrySet()) {
ProgramController.State programState = entry.getValue().getController().getState();
if (programState.isDone()) {
continue;
}
Id.Program programId = entry.getValue().getProgramId();
if (predicate.apply(programId)) {
LOG.trace("Program still running in checkAnyRunning: {} {} {} {}",
programId.getApplicationId(), type, programId.getId(), entry.getValue().getController().getRunId());
return true;
}
}
}
return false;
}
@Override
protected void startUp() throws Exception {
// No-op
}
@Override
protected void shutDown() throws Exception {
// No-op
}
protected void updateRuntimeInfo(ProgramType type, RunId runId, RuntimeInfo runtimeInfo) {
Lock lock = runtimeInfosLock.readLock();
lock.lock();
try {
if (!runtimeInfos.contains(type, runId)) {
monitorProgram(runtimeInfo, createCleanupTask());
}
} finally {
lock.unlock();
}
}
/**
* Starts monitoring a running program.
*
* @param runtimeInfo information about the running program
* @param cleanUpTask task to run when program finished
*/
private void monitorProgram(final RuntimeInfo runtimeInfo, final Runnable cleanUpTask) {
final ProgramController controller = runtimeInfo.getController();
controller.addListener(new AbstractListener() {
@Override
public void init(ProgramController.State currentState, @Nullable Throwable cause) {
if (!COMPLETED_STATES.contains(currentState)) {
add(runtimeInfo);
} else {
cleanUpTask.run();
}
}
@Override
public void completed() {
remove(runtimeInfo, cleanUpTask);
}
@Override
public void killed() {
remove(runtimeInfo, cleanUpTask);
}
@Override
public void error(Throwable cause) {
remove(runtimeInfo, cleanUpTask);
}
}, Threads.SAME_THREAD_EXECUTOR);
}
private void add(RuntimeInfo runtimeInfo) {
Lock lock = runtimeInfosLock.writeLock();
lock.lock();
try {
runtimeInfos.put(runtimeInfo.getType(), runtimeInfo.getController().getRunId(), runtimeInfo);
} finally {
lock.unlock();
}
}
private void remove(RuntimeInfo info, Runnable cleanUpTask) {
Lock lock = runtimeInfosLock.writeLock();
lock.lock();
try {
LOG.debug("Removing RuntimeInfo: {} {} {}",
info.getType(), info.getProgramId().getId(), info.getController().getRunId());
RuntimeInfo removed = runtimeInfos.remove(info.getType(), info.getController().getRunId());
LOG.debug("RuntimeInfo removed: {}", removed);
} finally {
lock.unlock();
cleanUpTask.run();
}
}
protected boolean isRunning(Id.Program programId) {
for (Map.Entry<RunId, RuntimeInfo> entry : list(programId.getType()).entrySet()) {
if (entry.getValue().getProgramId().equals(programId)) {
return true;
}
}
return false;
}
}