/*
* 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.runtime.worker;
import co.cask.cdap.AppWithWorker;
import co.cask.cdap.api.common.RuntimeArguments;
import co.cask.cdap.api.dataset.DatasetDefinition;
import co.cask.cdap.api.dataset.lib.KeyValueTable;
import co.cask.cdap.api.dataset.lib.cube.AggregationFunction;
import co.cask.cdap.api.metrics.MetricDataQuery;
import co.cask.cdap.api.metrics.MetricStore;
import co.cask.cdap.api.metrics.MetricTimeSeries;
import co.cask.cdap.app.program.Program;
import co.cask.cdap.app.runtime.ProgramController;
import co.cask.cdap.app.runtime.ProgramRunner;
import co.cask.cdap.app.runtime.ProgramRunnerFactory;
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.utils.Tasks;
import co.cask.cdap.data.dataset.SystemDatasetInstantiator;
import co.cask.cdap.data2.dataset2.DatasetFramework;
import co.cask.cdap.data2.dataset2.DynamicDatasetCache;
import co.cask.cdap.data2.dataset2.SingleThreadDatasetCache;
import co.cask.cdap.data2.transaction.TransactionExecutorFactory;
import co.cask.cdap.internal.AppFabricTestHelper;
import co.cask.cdap.internal.DefaultId;
import co.cask.cdap.internal.TempFolder;
import co.cask.cdap.internal.app.deploy.pipeline.ApplicationWithPrograms;
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.DatasetSpecificationSummary;
import co.cask.cdap.proto.Id;
import co.cask.cdap.proto.id.NamespaceId;
import co.cask.cdap.test.SlowTests;
import co.cask.tephra.TransactionExecutor;
import co.cask.tephra.TransactionManager;
import co.cask.tephra.TransactionSystemClient;
import co.cask.tephra.TxConstants;
import com.google.common.base.Supplier;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableMap;
import com.google.inject.Injector;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.twill.common.Threads;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.junit.rules.TemporaryFolder;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
/**
* Tests running worker programs.
*/
@Category(SlowTests.class)
public class WorkerProgramRunnerTest {
private static final TempFolder TEMP_FOLDER = new TempFolder();
private static Injector injector;
private static TransactionExecutorFactory txExecutorFactory;
private static TransactionManager txService;
private static DatasetFramework dsFramework;
private static DynamicDatasetCache datasetCache;
private static MetricStore metricStore;
private static Collection<ProgramController> runningPrograms = new HashSet<>();
@ClassRule
public static TemporaryFolder tmpFolder = new TemporaryFolder();
private static final Supplier<File> TEMP_FOLDER_SUPPLIER = new Supplier<File>() {
@Override
public File get() {
try {
return tmpFolder.newFolder();
} catch (IOException e) {
throw Throwables.propagate(e);
}
}
};
@BeforeClass
public static void beforeClass() {
// we are only gonna do long-running transactions here. Set the tx timeout to a ridiculously low value.
// that will test that the long-running transactions actually bypass that timeout.
CConfiguration conf = CConfiguration.create();
conf.set(Constants.CFG_LOCAL_DATA_DIR, TEMP_FOLDER.newFolder("data").getAbsolutePath());
conf.setInt(TxConstants.Manager.CFG_TX_TIMEOUT, 1);
conf.setInt(TxConstants.Manager.CFG_TX_CLEANUP_INTERVAL, 2);
injector = AppFabricTestHelper.getInjector(conf);
txService = injector.getInstance(TransactionManager.class);
txExecutorFactory = injector.getInstance(TransactionExecutorFactory.class);
dsFramework = injector.getInstance(DatasetFramework.class);
datasetCache = new SingleThreadDatasetCache(
new SystemDatasetInstantiator(dsFramework, WorkerProgramRunnerTest.class.getClassLoader(), null),
injector.getInstance(TransactionSystemClient.class),
NamespaceId.DEFAULT, DatasetDefinition.NO_ARGUMENTS, null, null);
metricStore = injector.getInstance(MetricStore.class);
txService.startAndWait();
}
@AfterClass
public static void afterClass() throws Exception {
txService.stopAndWait();
}
@After
public void after() throws Throwable {
// stop all running programs
for (ProgramController controller : runningPrograms) {
stopProgram(controller);
}
// cleanup user data (only user datasets)
for (DatasetSpecificationSummary spec : dsFramework.getInstances(DefaultId.NAMESPACE)) {
dsFramework.deleteInstance(Id.DatasetInstance.from(DefaultId.NAMESPACE, spec.getName()));
}
}
@Test
public void testWorkerDatasetWithMetrics() throws Throwable {
final ApplicationWithPrograms app =
AppFabricTestHelper.deployApplicationWithManager(AppWithWorker.class, TEMP_FOLDER_SUPPLIER);
ProgramController controller = startProgram(app, AppWithWorker.TableWriter.class);
// validate worker wrote the "initialize" and "run" rows
final TransactionExecutor executor = txExecutorFactory.createExecutor(datasetCache);
// wait at most 5 seconds until the "RUN" row is set (indicates the worker has started running)
Tasks.waitFor(AppWithWorker.RUN, new Callable<String>() {
@Override
public String call() throws Exception {
return executor.execute(
new Callable<String>() {
@Override
public String call() throws Exception {
KeyValueTable kvTable = datasetCache.getDataset(AppWithWorker.DATASET);
return Bytes.toString(kvTable.read(AppWithWorker.RUN));
}
});
}
}, 5, TimeUnit.SECONDS);
stopProgram(controller);
txExecutorFactory.createExecutor(datasetCache.getTransactionAwares()).execute(
new TransactionExecutor.Subroutine() {
@Override
public void apply() throws Exception {
KeyValueTable kvTable = datasetCache.getDataset(AppWithWorker.DATASET);
Assert.assertEquals(AppWithWorker.RUN, Bytes.toString(kvTable.read(AppWithWorker.RUN)));
Assert.assertEquals(AppWithWorker.INITIALIZE, Bytes.toString(kvTable.read(AppWithWorker.INITIALIZE)));
Assert.assertEquals(AppWithWorker.STOP, Bytes.toString(kvTable.read(AppWithWorker.STOP)));
}
});
// validate that the table emitted metrics
Tasks.waitFor(3L, new Callable<Long>() {
@Override
public Long call() throws Exception {
Collection<MetricTimeSeries> metrics =
metricStore.query(new MetricDataQuery(
0,
System.currentTimeMillis() / 1000L,
Integer.MAX_VALUE,
"system." + Constants.Metrics.Name.Dataset.OP_COUNT,
AggregationFunction.SUM,
ImmutableMap.of(Constants.Metrics.Tag.NAMESPACE, DefaultId.NAMESPACE.getId(),
Constants.Metrics.Tag.APP, AppWithWorker.NAME,
Constants.Metrics.Tag.WORKER, AppWithWorker.WORKER,
Constants.Metrics.Tag.DATASET, AppWithWorker.DATASET),
Collections.<String>emptyList()));
if (metrics.isEmpty()) {
return 0L;
}
Assert.assertEquals(1, metrics.size());
MetricTimeSeries ts = metrics.iterator().next();
Assert.assertEquals(1, ts.getTimeValues().size());
return ts.getTimeValues().get(0).getValue();
}
}, 5L, TimeUnit.SECONDS, 50L, TimeUnit.MILLISECONDS);
}
private ProgramController startProgram(ApplicationWithPrograms app, Class<?> programClass)
throws Throwable {
final AtomicReference<Throwable> errorCause = new AtomicReference<>();
final ProgramController controller = submit(app, programClass, RuntimeArguments.NO_ARGUMENTS);
runningPrograms.add(controller);
controller.addListener(new AbstractListener() {
@Override
public void error(Throwable cause) {
errorCause.set(cause);
}
@Override
public void killed() {
errorCause.set(new RuntimeException("Killed"));
}
}, Threads.SAME_THREAD_EXECUTOR);
Tasks.waitFor(ProgramController.State.ALIVE, new Callable<ProgramController.State>() {
@Override
public ProgramController.State call() throws Exception {
Throwable t = errorCause.get();
if (t != null) {
Throwables.propagateIfInstanceOf(t, Exception.class);
throw Throwables.propagate(t);
}
return controller.getState();
}
}, 30, TimeUnit.SECONDS);
return controller;
}
private void stopProgram(ProgramController controller) throws Throwable {
final AtomicReference<Throwable> errorCause = new AtomicReference<>();
final CountDownLatch complete = new CountDownLatch(1);
controller.addListener(new AbstractListener() {
@Override
public void error(Throwable cause) {
complete.countDown();
errorCause.set(cause);
}
@Override
public void completed() {
complete.countDown();
}
@Override
public void killed() {
complete.countDown();
}
}, Threads.SAME_THREAD_EXECUTOR);
controller.stop();
complete.await(30, TimeUnit.SECONDS);
runningPrograms.remove(controller);
Throwable t = errorCause.get();
if (t != null) {
throw t;
}
}
private ProgramController submit(ApplicationWithPrograms app,
Class<?> programClass,
Map<String, String> userArgs) throws ClassNotFoundException {
ProgramRunnerFactory runnerFactory = injector.getInstance(ProgramRunnerFactory.class);
Program program = getProgram(app, programClass);
Assert.assertNotNull(program);
ProgramRunner runner = runnerFactory.create(program.getType());
BasicArguments systemArgs = new BasicArguments(ImmutableMap.of(ProgramOptionConstants.RUN_ID,
RunIds.generate().getId()));
return runner.run(program, new SimpleProgramOptions(program.getName(), systemArgs, new BasicArguments(userArgs)));
}
private Program getProgram(ApplicationWithPrograms app, Class<?> programClass) throws ClassNotFoundException {
for (Program p : app.getPrograms()) {
if (programClass.getCanonicalName().equals(p.getMainClass().getCanonicalName())) {
return p;
}
}
return null;
}
}