/*
* Copyright © 2015 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;
import co.cask.cdap.app.program.Program;
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.common.conf.CConfiguration;
import co.cask.cdap.common.conf.Constants;
import co.cask.cdap.internal.app.runtime.AbstractListener;
import co.cask.cdap.internal.app.runtime.AbstractProgramController;
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 com.google.common.base.Function;
import com.google.common.base.Throwables;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Table;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.inject.Inject;
import org.apache.twill.api.RunId;
import org.apache.twill.common.Threads;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* ProgramRunner that can be used to manage multiple in-memory instances of a Program.
*/
public abstract class AbstractInMemoryProgramRunner implements ProgramRunner {
private static final Logger LOG = LoggerFactory.getLogger(AbstractInMemoryProgramRunner.class);
private final String host;
@Inject
protected AbstractInMemoryProgramRunner(CConfiguration cConf) {
this.host = cConf.get(Constants.AppFabric.SERVER_ADDRESS);
}
/**
* Creates a {@link ProgramRunner} that start the type of program that this program runner supports.
*/
protected abstract ProgramRunner createProgramRunner();
/**
* Starts all instances of a Program component.
* @param program The program to run
* @param options options for the program
* @param runId The runId
* @param numInstances number of component instances to start
*/
protected final ProgramController startAll(Program program, ProgramOptions options, RunId runId, int numInstances) {
Table<String, Integer, ProgramController> components = HashBasedTable.create();
try {
for (int instanceId = 0; instanceId < numInstances; instanceId++) {
ProgramOptions componentOptions = createComponentOptions(program.getName(), instanceId,
numInstances, runId, options);
ProgramController controller = createProgramRunner().run(program, componentOptions);
components.put(program.getName(), instanceId, controller);
}
return new InMemoryProgramController(components, program, options, runId);
} catch (Throwable t) {
LOG.error("Failed to start all program instances", t);
try {
// Need to stop all started components
Futures.successfulAsList(
Iterables.transform(components.values(), new Function<ProgramController, ListenableFuture<?>>() {
@Override
public ListenableFuture<?> apply(ProgramController controller) {
return controller.stop();
}
})).get();
throw Throwables.propagate(t);
} catch (Exception e) {
LOG.error("Failed to stop all program instances upon startup failure.", e);
throw Throwables.propagate(e);
}
}
}
private ProgramOptions createComponentOptions(String name, int instanceId, int instances, RunId runId,
ProgramOptions options) {
Map<String, String> systemOptions = Maps.newHashMap();
systemOptions.putAll(options.getArguments().asMap());
systemOptions.put(ProgramOptionConstants.INSTANCE_ID, Integer.toString(instanceId));
systemOptions.put(ProgramOptionConstants.INSTANCES, Integer.toString(instances));
systemOptions.put(ProgramOptionConstants.RUN_ID, runId.getId());
systemOptions.put(ProgramOptionConstants.HOST, host);
return new SimpleProgramOptions(name, new BasicArguments(systemOptions), options.getUserArguments());
}
/**
* ProgramController to manage multiple in-memory instances of a Program.
*/
private final class InMemoryProgramController extends AbstractProgramController {
private final Table<String, Integer, ProgramController> components;
private final Program program;
private final ProgramOptions options;
private final Lock lock = new ReentrantLock();
private final AtomicLong liveComponents;
InMemoryProgramController(Table<String, Integer, ProgramController> components,
Program program, ProgramOptions options, RunId runId) {
super(program.getId(), runId);
this.program = program;
this.components = components;
this.options = options;
this.liveComponents = new AtomicLong(components.size());
started();
monitorComponents();
}
// Add listener to monitor completion/killed status of individual components, so that the program can be marked
// as completed once all the components have completed/killed.
private void monitorComponents() {
for (ProgramController controller : components.values()) {
controller.addListener(new AbstractListener() {
@Override
public void completed() {
if (liveComponents.decrementAndGet() == 0) {
complete();
}
}
}, Threads.SAME_THREAD_EXECUTOR);
}
}
@Override
protected void doSuspend() throws Exception {
// No-op
}
@Override
protected void doResume() throws Exception {
// No-op
}
@Override
protected void doStop() throws Exception {
LOG.info("Stopping Program: {}", program.getName());
lock.lock();
try {
Futures.successfulAsList(
Iterables.transform(components.values(), new Function<ProgramController, ListenableFuture<?>>() {
@Override
public ListenableFuture<?> apply(ProgramController input) {
return input.stop();
}
})).get();
} finally {
lock.unlock();
}
LOG.info("Program stopped: {}", program.getName());
}
@Override
@SuppressWarnings("unchecked")
protected void doCommand(String name, Object value) throws Exception {
if (!ProgramOptionConstants.INSTANCES.equals(name) || !(value instanceof Map)) {
return;
}
Map<String, String> command = (Map<String, String>) value;
lock.lock();
try {
changeInstances(command.get("runnable"),
Integer.valueOf(command.get("newInstances")),
Integer.valueOf(command.get("oldInstances")));
} catch (Throwable t) {
LOG.error(String.format("Fail to change instances: %s", command), t);
throw t;
} finally {
lock.unlock();
}
}
/**
* Change the number of instances of the running runnable.
* @param runnableName Name of the runnable
* @param newCount New instance count
* @param oldCount Old instance count
* @throws java.util.concurrent.ExecutionException
* @throws InterruptedException
*/
private void changeInstances(String runnableName, final int newCount,
// unused but makes the in-memory controller expect the same command as twill
@SuppressWarnings("unused") final int oldCount) throws Exception {
Map<Integer, ProgramController> liveRunnables = components.row(runnableName);
int liveCount = liveRunnables.size();
if (liveCount == newCount) {
return;
}
// stop any extra runnables
if (liveCount > newCount) {
List<ListenableFuture<ProgramController>> futures = Lists.newArrayListWithCapacity(liveCount - newCount);
for (int instanceId = liveCount - 1; instanceId >= newCount; instanceId--) {
futures.add(components.remove(runnableName, instanceId).stop());
}
Futures.allAsList(futures).get();
}
// create more runnable instances, if necessary.
for (int instanceId = liveCount; instanceId < newCount; instanceId++) {
ProgramOptions programOptions = createComponentOptions(runnableName, instanceId, newCount, getRunId(), options);
ProgramController controller = createProgramRunner().run(program, programOptions);
components.put(runnableName, instanceId, controller);
}
liveRunnables = components.row(runnableName);
// Update total instance count for all running runnables
for (Map.Entry<Integer, ProgramController> entry : liveRunnables.entrySet()) {
entry.getValue().command(ProgramOptionConstants.INSTANCES, newCount);
}
}
}
}