/* * Copyright © 2014-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.runtime; import co.cask.cdap.ArgumentCheckApp; import co.cask.cdap.InvalidFlowOutputApp; import co.cask.cdap.WordCountApp; import co.cask.cdap.api.dataset.lib.cube.AggregationFunction; import co.cask.cdap.api.dataset.lib.cube.TimeValue; import co.cask.cdap.api.flow.flowlet.StreamEvent; 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.Constants; import co.cask.cdap.common.discovery.EndpointStrategy; import co.cask.cdap.common.discovery.RandomEndpointStrategy; import co.cask.cdap.common.namespace.NamespaceAdmin; import co.cask.cdap.common.queue.QueueName; import co.cask.cdap.common.stream.StreamEventCodec; import co.cask.cdap.data2.queue.QueueClientFactory; import co.cask.cdap.data2.queue.QueueEntry; import co.cask.cdap.data2.queue.QueueProducer; import co.cask.cdap.internal.AppFabricTestHelper; import co.cask.cdap.internal.DefaultId; import co.cask.cdap.internal.app.deploy.pipeline.ApplicationWithPrograms; 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.NamespaceMeta; import co.cask.cdap.proto.ProgramType; import co.cask.cdap.runtime.app.PendingMetricTestApp; import co.cask.cdap.test.SlowTests; import co.cask.tephra.Transaction; import co.cask.tephra.TransactionAware; import co.cask.tephra.TransactionSystemClient; import com.google.common.base.Charsets; import com.google.common.base.Supplier; import com.google.common.base.Throwables; 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.reflect.TypeToken; import com.google.gson.Gson; import org.apache.twill.discovery.Discoverable; import org.apache.twill.discovery.DiscoveryServiceClient; import org.apache.twill.discovery.ServiceDiscovered; 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 org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import java.nio.ByteBuffer; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; /** * */ @Category(SlowTests.class) public class FlowTest { @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); } } }; private static final Logger LOG = LoggerFactory.getLogger(FlowTest.class); private static MetricStore metricStore; @BeforeClass public static void init() throws Exception { NamespaceAdmin namespaceAdmin = AppFabricTestHelper.getInjector().getInstance(NamespaceAdmin.class); namespaceAdmin.create(NamespaceMeta.DEFAULT); metricStore = AppFabricTestHelper.getInjector().getInstance(MetricStore.class); } @Test public void testAppWithArgs() throws Exception { final ApplicationWithPrograms app = AppFabricTestHelper.deployApplicationWithManager(ArgumentCheckApp.class, TEMP_FOLDER_SUPPLIER); ProgramRunnerFactory runnerFactory = AppFabricTestHelper.getInjector().getInstance(ProgramRunnerFactory.class); // Only running flow is good. But, in case of service, we need to send something to service as it's lazy loading List<ProgramController> controllers = Lists.newArrayList(); for (final Program program : app.getPrograms()) { ProgramRunner runner = runnerFactory.create(program.getType()); BasicArguments systemArgs = new BasicArguments(ImmutableMap.of(ProgramOptionConstants.RUN_ID, RunIds.generate().getId())); BasicArguments userArgs = new BasicArguments(ImmutableMap.of("arg", "test")); controllers.add(runner.run(program, new SimpleProgramOptions(program.getName(), systemArgs, userArgs))); } TimeUnit.SECONDS.sleep(1); DiscoveryServiceClient discoveryServiceClient = AppFabricTestHelper.getInjector(). getInstance(DiscoveryServiceClient.class); Discoverable discoverable = discoveryServiceClient.discover( String.format("service.%s.%s.%s", DefaultId.NAMESPACE.getId(), "ArgumentCheckApp", "SimpleService")).iterator().next(); URL url = new URL(String.format("http://%s:%d/v3/namespaces/default/apps/%s/services/%s/methods/%s", discoverable.getSocketAddress().getHostName(), discoverable.getSocketAddress().getPort(), "ArgumentCheckApp", "SimpleService", "ping")); HttpURLConnection urlConn = (HttpURLConnection) url.openConnection(); // this would fail had the service been started without the argument (initialize would have thrown) Assert.assertEquals(200, urlConn.getResponseCode()); for (ProgramController controller : controllers) { controller.stop().get(); } } @Test public void testFlow() throws Exception { final ApplicationWithPrograms app = AppFabricTestHelper.deployApplicationWithManager(WordCountApp.class, TEMP_FOLDER_SUPPLIER); ProgramRunnerFactory runnerFactory = AppFabricTestHelper.getInjector().getInstance(ProgramRunnerFactory.class); List<ProgramController> controllers = Lists.newArrayList(); for (final Program program : app.getPrograms()) { // running mapreduce is out of scope of this tests (there's separate unit-test for that) if (program.getType() == ProgramType.MAPREDUCE) { continue; } ProgramRunner runner = runnerFactory.create(program.getType()); BasicArguments systemArgs = new BasicArguments(ImmutableMap.of(ProgramOptionConstants.RUN_ID, RunIds.generate().getId())); controllers.add(runner.run(program, new SimpleProgramOptions(program.getName(), systemArgs, new BasicArguments()))); } TimeUnit.SECONDS.sleep(1); TransactionSystemClient txSystemClient = AppFabricTestHelper.getInjector(). getInstance(TransactionSystemClient.class); QueueName queueName = QueueName.fromStream(app.getId().getNamespaceId(), "text"); QueueClientFactory queueClientFactory = AppFabricTestHelper.getInjector().getInstance(QueueClientFactory.class); QueueProducer producer = queueClientFactory.createProducer(queueName); // start tx to write in queue in tx Transaction tx = txSystemClient.startShort(); ((TransactionAware) producer).startTx(tx); StreamEventCodec codec = new StreamEventCodec(); for (int i = 0; i < 10; i++) { String msg = "Testing message " + i; StreamEvent event = new StreamEvent(ImmutableMap.<String, String>of(), ByteBuffer.wrap(msg.getBytes(Charsets.UTF_8))); producer.enqueue(new QueueEntry(codec.encodePayload(event))); } // commit tx ((TransactionAware) producer).commitTx(); txSystemClient.commit(tx); // Query the service for at most 10 seconds for the expected result Gson gson = new Gson(); DiscoveryServiceClient discoveryServiceClient = AppFabricTestHelper.getInjector(). getInstance(DiscoveryServiceClient.class); ServiceDiscovered serviceDiscovered = discoveryServiceClient.discover( String.format("service.%s.%s.%s", DefaultId.NAMESPACE.getId(), "WordCountApp", "WordFrequencyService")); EndpointStrategy endpointStrategy = new RandomEndpointStrategy(serviceDiscovered); int trials = 0; while (trials++ < 10) { Discoverable discoverable = endpointStrategy.pick(2, TimeUnit.SECONDS); URL url = new URL(String.format("http://%s:%d/v3/namespaces/default/apps/%s/services/%s/methods/%s/%s", discoverable.getSocketAddress().getHostName(), discoverable.getSocketAddress().getPort(), "WordCountApp", "WordFrequencyService", "wordfreq", "text:Testing")); try { HttpURLConnection urlConn = (HttpURLConnection) url.openConnection(); Map<String, Long> responseContent = gson.fromJson( new InputStreamReader(urlConn.getInputStream(), Charsets.UTF_8), new TypeToken<Map<String, Long>>() { }.getType()); LOG.info("Service response: " + responseContent); if (ImmutableMap.of("text:Testing", 10L).equals(responseContent)) { break; } } catch (Throwable t) { LOG.info("Exception when trying to query service.", t); } TimeUnit.SECONDS.sleep(1); } Assert.assertTrue(trials < 10); for (ProgramController controller : controllers) { controller.stop().get(); } } @Test (expected = IllegalArgumentException.class) public void testInvalidOutputEmitter() throws Throwable { try { AppFabricTestHelper.deployApplicationWithManager(InvalidFlowOutputApp.class, TEMP_FOLDER_SUPPLIER); } catch (Exception e) { throw Throwables.getRootCause(e); } } @Test public void testFlowPendingMetric() throws Exception { final ApplicationWithPrograms app = AppFabricTestHelper.deployApplicationWithManager( PendingMetricTestApp.class, TEMP_FOLDER_SUPPLIER); ProgramRunnerFactory runnerFactory = AppFabricTestHelper.getInjector().getInstance(ProgramRunnerFactory.class); File tempFolder = TEMP_FOLDER_SUPPLIER.get(); ProgramController controller = null; for (final Program program : app.getPrograms()) { // running mapreduce is out of scope of this tests (there's separate unit-test for that) if (program.getType() == ProgramType.FLOW) { ProgramRunner runner = runnerFactory.create(program.getType()); BasicArguments systemArgs = new BasicArguments(ImmutableMap.of(ProgramOptionConstants.RUN_ID, RunIds.generate().getId())); controller = runner.run(program, new SimpleProgramOptions( program.getName(), systemArgs, new BasicArguments(ImmutableMap.of("temp", tempFolder.getAbsolutePath(), "count", "4")))); } } Assert.assertNotNull(controller); Map<String, String> tagsForSourceToOne = metricTagsForQueue("source", "ints", "forward-one"); Map<String, String> tagsForSourceToTwo = metricTagsForQueue("source", null, "forward-two"); Map<String, String> tagsForSourceToTwoInts = metricTagsForQueue("source", "ints", "forward-two"); Map<String, String> tagsForSourceToTwoStrings = metricTagsForQueue("source", "strings", "forward-two"); Map<String, String> tagsForOneToSink = metricTagsForQueue("forward-one", "queue", "sink"); Map<String, String> tagsForTwoToSink = metricTagsForQueue("forward-two", "queue", "sink"); Map<String, String> tagsForAllToOne = metricTagsForQueue(null, null, "forward-one"); Map<String, String> tagsForAllToTwo = metricTagsForQueue(null, null, "forward-two"); Map<String, String> tagsForAllToSink = metricTagsForQueue(null, null, "sink"); Map<String, String> tagsForAll = metricTagsForQueue(null, null, null); // each flowlets is waiting for a file to appear in the temp folder. Until then it does not process any event // however, the flowlet driver emits metrics before it calls prcess(), hence it wil always seem as if one event // is already processed (not pending). We will kick off the flowlets one by one to validate the pending metrics. try { // source emits 4, then forward-one reads 1, hence 3 should be pending waitForPending(tagsForSourceToOne, 3, 5000); // wait a little longer as flow needs to start waitForPending(tagsForAllToOne, 3, 100); // wait a little longer as flow needs to start // forward-two receives each of the 4 as a string and an int, but could have read 1 at most per each queue // so there should be either 3 + 4 = 7 pending or 3 + 3 = 6 pending, or 4 + 4 = 8 pending // but we don't know whether the queue pending count will be 4, 3 or 3, 4 or 3, 3 or 4, 4 long intPending = waitForPending(tagsForSourceToTwoInts, 3, 4L, 1000); long stringPending = waitForPending(tagsForSourceToTwoStrings, 3, 4L, 1000); long totalPending = intPending + stringPending; Assert.assertTrue(String.format("Expected the pending events count to be 6, 7 or 8. But it was %d", totalPending), totalPending == 6 || totalPending == 7 || totalPending == 8); waitForPending(tagsForSourceToTwo, 7, 6L, 500); waitForPending(tagsForAllToTwo, 7, 6L, 100); // neither one nor two have emitted, so the total pending should be = 12 - 1 (forward-one) - 1 or 2 (forward-two) // => 10 or 9 events waitForPending(tagsForAll, 10, 9L, 100); // kick on forward-one, it should now consume all its events Assert.assertTrue(new File(tempFolder, "one").createNewFile()); waitForPending(tagsForSourceToOne, 0, 2000); waitForPending(tagsForAllToOne, 0, 100); // sink has received 4 but started to read 1, so it has 3 pending waitForPending(tagsForOneToSink, 3, 1000); waitForPending(tagsForAllToSink, 3, 100); // kick-off forward-two, it should now consume all its integer and string events Assert.assertTrue(new File(tempFolder, "two-i").createNewFile()); Assert.assertTrue(new File(tempFolder, "two-s").createNewFile()); // pending events for all of forward-two's queues should go to zero waitForPending(tagsForSourceToTwoInts, 0, 2000); waitForPending(tagsForSourceToTwoStrings, 0, 1000); waitForPending(tagsForSourceToTwo, 0, 1000); waitForPending(tagsForAllToTwo, 0, 100); // but now sink should have 8 more events waiting waitForPending(tagsForOneToSink, 3, 1000); waitForPending(tagsForTwoToSink, 8, 1000); waitForPending(tagsForAllToSink, 11, 100); // kick off sink, its pending events should now go to zero Assert.assertTrue(new File(tempFolder, "three").createNewFile()); waitForPending(tagsForOneToSink, 0, 2000); waitForPending(tagsForTwoToSink, 0, 2000); waitForPending(tagsForAllToSink, 0, 100); } finally { controller.stop(); } } private static long waitForPending(Map<String, String> tags, long expected, long millis) throws Exception { return waitForPending(tags, expected, null, millis); } private static long waitForPending(Map<String, String> tags, long expected, Long alternative, long millis) throws Exception { long pending = 0L; while (millis >= 0) { pending = getPending(tags); if (pending == expected || alternative != null && pending == alternative) { return pending; } TimeUnit.MILLISECONDS.sleep(50); millis -= 50; } throw new RuntimeException("Timeout reached waiting for pending to reach " + expected + (alternative == null ? "" : " or " + alternative) + " for " + tags + "(actual value is " + pending + ")"); } private static long getPending(Map<String, String> tags) throws Exception { MetricDataQuery metricDataQuery = new MetricDataQuery(0, Integer.MAX_VALUE, Integer.MAX_VALUE, "system.queue.pending", AggregationFunction.SUM, tags, ImmutableList.<String>of()); Collection<MetricTimeSeries> query = metricStore.query(metricDataQuery); if (query.isEmpty()) { return 0; } MetricTimeSeries timeSeries = Iterables.getOnlyElement(query); List<TimeValue> timeValues = timeSeries.getTimeValues(); TimeValue timeValue = Iterables.getOnlyElement(timeValues); return timeValue.getValue(); } private static Map<String, String> metricTagsForQueue(String producer, String queue, String consumer) { Map<String, String> tags = Maps.newHashMap(); tags.put(Constants.Metrics.Tag.NAMESPACE, DefaultId.NAMESPACE.getId()); tags.put(Constants.Metrics.Tag.APP, "PendingMetricTestApp"); tags.put(Constants.Metrics.Tag.FLOW, "TestPendingFlow"); if (producer != null) { tags.put(Constants.Metrics.Tag.PRODUCER, producer); } if (queue != null) { tags.put(Constants.Metrics.Tag.FLOWLET_QUEUE, queue); } if (consumer != null) { tags.put(Constants.Metrics.Tag.CONSUMER, consumer); } return tags; } }