/*
* Copyright © 2014 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.service;
import co.cask.cdap.api.service.ServiceSpecification;
import co.cask.cdap.app.ApplicationSpecification;
import co.cask.cdap.app.program.Program;
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.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.ProgramRunnerFactory;
import co.cask.cdap.internal.app.runtime.SimpleProgramOptions;
import co.cask.cdap.proto.ProgramType;
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
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.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.api.RuntimeSpecification;
import org.apache.twill.internal.RunIds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* For Running Services in standalone mode.
*/
public class InMemoryServiceRunner implements ProgramRunner {
private static final Logger LOG = LoggerFactory.getLogger(InMemoryServiceRunner.class);
private final Map<RunId, ProgramOptions> programOptions = Maps.newHashMap();
private final ProgramRunnerFactory programRunnerFactory;
@Inject
InMemoryServiceRunner(ProgramRunnerFactory programRunnerFactory) {
this.programRunnerFactory = programRunnerFactory;
}
@Override
public ProgramController run(Program program, ProgramOptions options) {
// Extract and verify parameters
ApplicationSpecification appSpec = program.getSpecification();
Preconditions.checkNotNull(appSpec, "Missing application specification.");
ProgramType processorType = program.getType();
Preconditions.checkNotNull(processorType, "Missing processor type.");
Preconditions.checkArgument(processorType == ProgramType.SERVICE, "Only SERVICE process type is supported.");
ServiceSpecification serviceSpec = appSpec.getServices().get(program.getName());
Preconditions.checkNotNull(serviceSpec, "Missing ServiceSpecification for %s", program.getName());
//RunId for the service
RunId runId = RunIds.generate();
programOptions.put(runId, options);
final Table<String, Integer, ProgramController> serviceRunnables = createRunnables(program, runId, serviceSpec);
return new ServiceProgramController(serviceRunnables, runId, program, serviceSpec);
}
private Table<String, Integer, ProgramController> createRunnables(Program program, RunId runId,
ServiceSpecification serviceSpec) {
Table<String, Integer, ProgramController> runnables = HashBasedTable.create();
try {
for (Map.Entry<String, RuntimeSpecification> entry : serviceSpec.getRunnables().entrySet()) {
int instanceCount = entry.getValue().getResourceSpecification().getInstances();
for (int instanceId = 0; instanceId < instanceCount; instanceId++) {
runnables.put(entry.getKey(), instanceId, startRunnable
(program, createRunnableOptions(entry.getKey(), instanceId, instanceCount, runId)));
}
}
} catch (Throwable t) {
// Need to stop all started runnable here.
try {
Futures.successfulAsList
(Iterables.transform(runnables.values(), new Function<ProgramController,
ListenableFuture<ProgramController>>() {
@Override
public ListenableFuture<ProgramController> apply(ProgramController input) {
return input.stop();
}
}
)
).get();
} catch (Exception e) {
LOG.error("Failed to stop all the runnables");
}
throw Throwables.propagate(t);
}
return runnables;
}
private ProgramController startRunnable(Program program, ProgramOptions runnableOptions) {
return programRunnerFactory.create(ProgramRunnerFactory.Type.RUNNABLE).run(program, runnableOptions);
}
private ProgramOptions createRunnableOptions(String name, int instanceId, int instances, RunId runId) {
// Get the right user arguments.
Arguments userArguments = new BasicArguments();
if (programOptions.containsKey(runId)) {
userArguments = programOptions.get(runId).getUserArguments();
}
return new SimpleProgramOptions(name, new BasicArguments
(ImmutableMap.of(ProgramOptionConstants.INSTANCE_ID, Integer.toString(instanceId),
ProgramOptionConstants.INSTANCES, Integer.toString(instances),
ProgramOptionConstants.RUN_ID, runId.getId())), userArguments);
}
class ServiceProgramController extends AbstractProgramController {
private final Table<String, Integer, ProgramController> runnables;
private final Program program;
private final ServiceSpecification serviceSpec;
private final Lock lock = new ReentrantLock();
ServiceProgramController(Table<String, Integer, ProgramController> runnables,
RunId runId, Program program, ServiceSpecification serviceSpec) {
super(program.getName(), runId);
this.program = program;
this.runnables = runnables;
this.serviceSpec = serviceSpec;
started();
}
public List<ProgramController> getProgramControllers() {
return ImmutableList.copyOf(runnables.values());
}
@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 Service : " + serviceSpec.getName());
lock.lock();
try {
Futures.successfulAsList
(Iterables.transform(runnables.values(), new Function<ProgramController,
ListenableFuture<ProgramController>>() {
@Override
public ListenableFuture<ProgramController> apply(ProgramController input) {
return input.stop();
}
}
)
).get();
} finally {
lock.unlock();
}
LOG.info("Service stopped: " + serviceSpec.getName());
}
@Override
@SuppressWarnings("unchecked")
protected void doCommand(String name, Object value) throws Exception {
if (!ProgramOptionConstants.RUNNABLE_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")));
} catch (Throwable t) {
LOG.error(String.format("Fail to change instances: %s", command), t);
} finally {
lock.unlock();
}
}
/**
* Change the number of instances of the running runnable.
* @param runnableName Name of the runnable
* @param newCount New instance count
* @throws java.util.concurrent.ExecutionException
* @throws InterruptedException
*/
private void changeInstances(String runnableName, final int newCount) throws Exception {
Map<Integer, ProgramController> liveRunnables = runnables.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(runnables.remove(runnableName, instanceId).stop());
}
Futures.allAsList(futures).get();
}
// create more runnable instances, if necessary.
for (int instanceId = liveCount; instanceId < newCount; instanceId++) {
ProgramOptions newProgramOpts = createRunnableOptions(runnableName, instanceId, newCount, getRunId());
ProgramController programController = startRunnable(program, newProgramOpts);
runnables.put(runnableName, instanceId, programController);
}
}
}
}