/* * 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.data2.transaction.stream; import co.cask.cdap.api.flow.flowlet.StreamEvent; import co.cask.cdap.common.conf.CConfiguration; import co.cask.cdap.common.conf.Constants; import co.cask.cdap.common.namespace.NamespacedLocationFactory; import co.cask.cdap.data.file.FileWriter; import co.cask.cdap.data.stream.StreamFileWriterFactory; import co.cask.cdap.data2.queue.ConsumerConfig; import co.cask.cdap.data2.queue.DequeueResult; import co.cask.cdap.data2.queue.DequeueStrategy; import co.cask.cdap.data2.queue.QueueClientFactory; import co.cask.cdap.proto.Id; import co.cask.cdap.test.SlowTests; import co.cask.tephra.TransactionAware; import co.cask.tephra.TransactionContext; import co.cask.tephra.TransactionSystemClient; import com.google.common.base.Charsets; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.common.primitives.Longs; import org.junit.Assert; import org.junit.Test; import org.junit.experimental.categories.Category; import java.io.IOException; import java.util.Collection; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.concurrent.TimeUnit; /** * */ public abstract class StreamConsumerTestBase { protected static CConfiguration cConf = CConfiguration.create(); protected static final Id.Namespace TEST_NAMESPACE = Id.Namespace.from("streamConsumerTestNamespace"); protected static final Id.Namespace OTHER_NAMESPACE = Id.Namespace.from("otherNamespace"); private static final Comparator<StreamEvent> STREAM_EVENT_COMPARATOR = new Comparator<StreamEvent>() { @Override public int compare(StreamEvent o1, StreamEvent o2) { int cmp = Longs.compare(o1.getTimestamp(), o2.getTimestamp()); if (cmp != 0) { return cmp; } return o1.getBody().compareTo(o2.getBody()); } }; protected abstract QueueClientFactory getQueueClientFactory(); protected abstract StreamConsumerFactory getConsumerFactory(); protected abstract StreamAdmin getStreamAdmin(); protected abstract TransactionSystemClient getTransactionClient(); protected abstract StreamFileWriterFactory getFileWriterFactory(); protected static void setupNamespaces(NamespacedLocationFactory namespacedLocationFactory) throws IOException { namespacedLocationFactory.get(TEST_NAMESPACE).mkdirs(); namespacedLocationFactory.get(OTHER_NAMESPACE).mkdirs(); } @Test public void testNamespacedStreamConsumers() throws Exception { // Test two consumers for two streams with the same name, but in different namespaces. Their consumption should be // independent of the other. String stream = "testNamespacedStreamConsumers"; Id.Stream streamId = Id.Stream.from(TEST_NAMESPACE, stream); Id.Stream otherStreamId = Id.Stream.from(OTHER_NAMESPACE, stream); StreamAdmin streamAdmin = getStreamAdmin(); streamAdmin.create(streamId); streamAdmin.create(otherStreamId); StreamConfig streamConfig = streamAdmin.getConfig(streamId); StreamConfig otherStreamConfig = streamAdmin.getConfig(otherStreamId); // Writes 5 events to both streams writeEvents(streamConfig, "Testing ", 5); writeEvents(otherStreamConfig, "Testing ", 5); streamAdmin.configureInstances(streamId, 0L, 1); streamAdmin.configureInstances(otherStreamId, 0L, 1); StreamConsumerFactory consumerFactory = getConsumerFactory(); StreamConsumer consumer = consumerFactory.create(streamId, "fifo.rollback", new ConsumerConfig(0L, 0, 1, DequeueStrategy.FIFO, null)); StreamConsumer otherConsumer = consumerFactory.create(otherStreamId, "fifo.rollback", new ConsumerConfig(0L, 0, 1, DequeueStrategy.FIFO, null)); // Try to dequeue using both consumers TransactionContext context = createTxContext(consumer); TransactionContext otherContext = createTxContext(otherConsumer); context.start(); otherContext.start(); // Consume events from the stream in the default namespace DequeueResult<StreamEvent> result0 = consumer.poll(1, 1, TimeUnit.SECONDS); Assert.assertEquals("Testing 0", Charsets.UTF_8.decode(result0.iterator().next().getBody()).toString()); context.finish(); context.start(); result0 = consumer.poll(1, 1, TimeUnit.SECONDS); Assert.assertEquals("Testing 1", Charsets.UTF_8.decode(result0.iterator().next().getBody()).toString()); context.finish(); context.start(); result0 = consumer.poll(1, 1, TimeUnit.SECONDS); Assert.assertEquals("Testing 2", Charsets.UTF_8.decode(result0.iterator().next().getBody()).toString()); context.finish(); context.start(); // Even though a stream with the same name has already consumed 3 events, the otherConsumer is for a stream in a // different namespace, so it will still be on the initial event. DequeueResult<StreamEvent> result1 = otherConsumer.poll(1, 1, TimeUnit.SECONDS); Assert.assertEquals("Testing 0", Charsets.UTF_8.decode(result1.iterator().next().getBody()).toString()); otherContext.finish(); otherContext.start(); result0 = consumer.poll(1, 1, TimeUnit.SECONDS); result1 = otherConsumer.poll(1, 1, TimeUnit.SECONDS); Assert.assertEquals("Testing 3", Charsets.UTF_8.decode(result0.iterator().next().getBody()).toString()); Assert.assertEquals("Testing 1", Charsets.UTF_8.decode(result1.iterator().next().getBody()).toString()); // Commit both context.finish(); otherContext.finish(); consumer.close(); otherConsumer.close(); } @Test public void testFIFORollback() throws Exception { String stream = "testFIFORollback"; Id.Stream streamId = Id.Stream.from(TEST_NAMESPACE, stream); StreamAdmin streamAdmin = getStreamAdmin(); streamAdmin.create(streamId); StreamConfig streamConfig = streamAdmin.getConfig(streamId); // Writes 5 events writeEvents(streamConfig, "Testing ", 5); streamAdmin.configureInstances(streamId, 0L, 2); StreamConsumerFactory consumerFactory = getConsumerFactory(); StreamConsumer consumer0 = consumerFactory.create(streamId, "fifo.rollback", new ConsumerConfig(0L, 0, 2, DequeueStrategy.FIFO, null)); StreamConsumer consumer1 = consumerFactory.create(streamId, "fifo.rollback", new ConsumerConfig(0L, 1, 2, DequeueStrategy.FIFO, null)); // Try to dequeue using both consumers TransactionContext context0 = createTxContext(consumer0); TransactionContext context1 = createTxContext(consumer1); context0.start(); context1.start(); DequeueResult<StreamEvent> result0 = consumer0.poll(1, 1, TimeUnit.SECONDS); DequeueResult<StreamEvent> result1 = consumer1.poll(1, 1, TimeUnit.SECONDS); Assert.assertEquals("Testing 0", Charsets.UTF_8.decode(result0.iterator().next().getBody()).toString()); Assert.assertEquals("Testing 1", Charsets.UTF_8.decode(result1.iterator().next().getBody()).toString()); // Commit the first one, rollback the second one. context0.finish(); context1.abort(); // Dequeue again with the consuemrs context0.start(); context1.start(); result0 = consumer0.poll(1, 1, TimeUnit.SECONDS); result1 = consumer1.poll(1, 1, TimeUnit.SECONDS); // Expect consumer 0 keep proceeding while consumer 1 will retry with what it claimed in previous transaction. // This is the optimization in FIFO mode to avoid going back and rescanning. Assert.assertEquals("Testing 2", Charsets.UTF_8.decode(result0.iterator().next().getBody()).toString()); Assert.assertEquals("Testing 1", Charsets.UTF_8.decode(result1.iterator().next().getBody()).toString()); // Commit both context0.finish(); context1.finish(); consumer0.close(); consumer1.close(); } @Test public void testFIFOReconfigure() throws Exception { String stream = "testReconfigure"; Id.Stream streamId = Id.Stream.from(TEST_NAMESPACE, stream); StreamAdmin streamAdmin = getStreamAdmin(); streamAdmin.create(streamId); StreamConfig streamConfig = streamAdmin.getConfig(streamId); // Writes 5 events writeEvents(streamConfig, "Testing ", 5); // Configure 3 consumers. streamAdmin.configureInstances(streamId, 0L, 3); StreamConsumerFactory consumerFactory = getConsumerFactory(); // Starts three consumers List<StreamConsumer> consumers = Lists.newArrayList(); for (int i = 0; i < 3; i++) { consumers.add(consumerFactory.create(streamId, "fifo.reconfigure", new ConsumerConfig(0L, i, 3, DequeueStrategy.FIFO, null))); } List<TransactionContext> txContexts = Lists.newArrayList(); for (StreamConsumer consumer : consumers) { txContexts.add(createTxContext(consumer)); } for (TransactionContext txContext : txContexts) { txContext.start(); } // Consumer an item from each consumer, but only have the first one commit. for (int i = 0; i < consumers.size(); i++) { DequeueResult<StreamEvent> result = consumers.get(i).poll(1, 1, TimeUnit.SECONDS); Assert.assertEquals("Testing " + i, Charsets.UTF_8.decode(result.iterator().next().getBody()).toString()); if (i == 0) { txContexts.get(i).finish(); } else { txContexts.get(i).abort(); } } for (StreamConsumer consumer : consumers) { consumer.close(); } // Reconfigure to have two consumers. streamAdmin.configureInstances(streamId, 0L, 2); consumers.clear(); for (int i = 0; i < 2; i++) { consumers.add(consumerFactory.create(streamId, "fifo.reconfigure", new ConsumerConfig(0L, i, 2, DequeueStrategy.FIFO, null))); } txContexts.clear(); for (StreamConsumer consumer : consumers) { txContexts.add(createTxContext(consumer)); } // Consumer an item from each consumer, they should see all four items. Set<String> messages = Sets.newTreeSet(); boolean done; do { for (TransactionContext txContext : txContexts) { txContext.start(); } done = true; for (int i = 0; i < consumers.size(); i++) { DequeueResult<StreamEvent> result = consumers.get(i).poll(1, 1, TimeUnit.SECONDS); if (result.isEmpty()) { continue; } done = false; messages.add(Charsets.UTF_8.decode(result.iterator().next().getBody()).toString()); txContexts.get(i).finish(); } } while (!done); Assert.assertEquals(4, messages.size()); int count = 1; for (String msg : messages) { Assert.assertEquals("Testing " + count, msg); count++; } for (StreamConsumer consumer : consumers) { consumer.close(); } } @Test public void testTTL() throws Exception { String stream = "testTTL"; Id.Stream streamId = Id.Stream.from(TEST_NAMESPACE, stream); StreamAdmin streamAdmin = getStreamAdmin(); // Create stream with ttl of 1 day final long ttl = TimeUnit.DAYS.toMillis(1); final long currentTime = System.currentTimeMillis(); final long increment = TimeUnit.SECONDS.toMillis(1); final long approxEarliestNonExpiredTime = currentTime - TimeUnit.HOURS.toMillis(1); Properties streamProperties = new Properties(); streamProperties.setProperty(Constants.Stream.TTL, Long.toString(ttl)); streamProperties.setProperty(Constants.Stream.PARTITION_DURATION, Long.toString(ttl)); streamAdmin.create(streamId, streamProperties); StreamConfig streamConfig = streamAdmin.getConfig(streamId); streamAdmin.configureInstances(streamId, 0L, 1); StreamConsumerFactory consumerFactory = getConsumerFactory(); Assert.assertEquals(ttl, streamConfig.getTTL()); Assert.assertEquals(ttl, streamConfig.getPartitionDuration()); Set<StreamEvent> expectedEvents = Sets.newTreeSet(STREAM_EVENT_COMPARATOR); FileWriter<StreamEvent> writer = getFileWriterFactory().create(streamConfig, 0); try { // Write 10 expired messages writeEvents(streamConfig, "Old event ", 20, new IncrementingClock(0, 1)); // Write 5 non-expired messages expectedEvents.addAll(writeEvents(streamConfig, "New event ", 12, new IncrementingClock(approxEarliestNonExpiredTime, increment))); } finally { writer.close(); } // Dequeue from stream. Should only get the 5 unexpired events. StreamConsumer consumer = consumerFactory.create(streamId, stream, new ConsumerConfig(0L, 0, 1, DequeueStrategy.FIFO, null)); try { verifyEvents(consumer, expectedEvents); TransactionContext txContext = createTxContext(consumer); txContext.start(); try { // Should be no more pending events DequeueResult<StreamEvent> result = consumer.poll(1, 2, TimeUnit.SECONDS); Assert.assertTrue(result.isEmpty()); } finally { txContext.finish(); } } finally { consumer.close(); } } @Test public void testTTLMultipleEventsWithSameTimestamp() throws Exception { String stream = "testTTLMultipleEventsWithSameTimestamp"; Id.Stream streamId = Id.Stream.from(TEST_NAMESPACE, stream); StreamAdmin streamAdmin = getStreamAdmin(); // Create stream with ttl of 1 day final long ttl = TimeUnit.DAYS.toMillis(1); final long currentTime = System.currentTimeMillis(); final long increment = TimeUnit.SECONDS.toMillis(1); final long approxEarliestNonExpiredTime = currentTime - TimeUnit.HOURS.toMillis(1); Properties streamProperties = new Properties(); streamProperties.setProperty(Constants.Stream.TTL, Long.toString(ttl)); streamProperties.setProperty(Constants.Stream.PARTITION_DURATION, Long.toString(ttl)); streamAdmin.create(streamId, streamProperties); StreamConfig streamConfig = streamAdmin.getConfig(streamId); streamAdmin.configureInstances(streamId, 0L, 1); StreamConsumerFactory consumerFactory = getConsumerFactory(); Assert.assertEquals(ttl, streamConfig.getTTL()); Assert.assertEquals(ttl, streamConfig.getPartitionDuration()); // Write 100 expired messages to stream with expired timestamp writeEvents(streamConfig, "Old event ", 10, new ConstantClock(0)); // Write 500 non-expired messages to stream with timestamp approxEarliestNonExpiredTime..currentTime Set<StreamEvent> expectedEvents = Sets.newTreeSet(STREAM_EVENT_COMPARATOR); FileWriter<StreamEvent> writer = getFileWriterFactory().create(streamConfig, 0); try { expectedEvents.addAll(writeEvents(writer, "New event pre-flush ", 20, new IncrementingClock(approxEarliestNonExpiredTime, increment, 5))); writer.flush(); expectedEvents.addAll(writeEvents(writer, "New event post-flush ", 20, new IncrementingClock(approxEarliestNonExpiredTime + 1, increment, 5))); } finally { writer.close(); } StreamConsumer consumer = consumerFactory.create(streamId, stream, new ConsumerConfig(0L, 0, 1, DequeueStrategy.FIFO, null)); verifyEvents(consumer, expectedEvents); TransactionContext txContext = createTxContext(consumer); txContext.start(); try { // Should be no more pending events DequeueResult<StreamEvent> result = consumer.poll(1, 1, TimeUnit.SECONDS); Assert.assertTrue(result.isEmpty()); } finally { txContext.finish(); } consumer.close(); } @Category(SlowTests.class) @Test public void testTTLStartingFile() throws Exception { String stream = "testTTLStartingFile"; Id.Stream streamId = Id.Stream.from(TEST_NAMESPACE, stream); StreamAdmin streamAdmin = getStreamAdmin(); // Create stream with ttl of 3 seconds and partition duration of 3 seconds final long ttl = TimeUnit.SECONDS.toMillis(3); Properties streamProperties = new Properties(); streamProperties.setProperty(Constants.Stream.TTL, Long.toString(ttl)); streamProperties.setProperty(Constants.Stream.PARTITION_DURATION, Long.toString(ttl)); streamAdmin.create(streamId, streamProperties); StreamConfig streamConfig = streamAdmin.getConfig(streamId); streamAdmin.configureGroups(streamId, ImmutableMap.of(0L, 1, 1L, 1)); StreamConsumerFactory consumerFactory = getConsumerFactory(); StreamConsumer consumer = consumerFactory.create(streamId, stream, new ConsumerConfig(0L, 0, 1, DequeueStrategy.FIFO, null)); StreamConsumer newConsumer; Set<StreamEvent> expectedEvents = Sets.newTreeSet(STREAM_EVENT_COMPARATOR); try { // Create a new consumer for second consumer verification. // Need to create consumer before write event because in HBase, creation of consumer took couple seconds. newConsumer = consumerFactory.create(streamId, stream, new ConsumerConfig(1L, 0, 1, DequeueStrategy.FIFO, null)); // write 20 events in a partition that will be expired due to sleeping the TTL writeEvents(streamConfig, "Phase 0 expired event ", 20); Thread.sleep(ttl); verifyEvents(consumer, expectedEvents); // also verify for a new consumer try { verifyEvents(newConsumer, expectedEvents); } finally { newConsumer.close(); } // Create a new consumer for second consumer verification (with clean state) // Need to create consumer before write event because in HBase, creation of consumer took couple seconds. streamAdmin.configureGroups(streamId, ImmutableMap.of(0L, 1)); streamAdmin.configureGroups(streamId, ImmutableMap.of(0L, 1, 1L, 1)); newConsumer = consumerFactory.create(streamId, stream, new ConsumerConfig(1L, 0, 1, DequeueStrategy.FIFO, null)); // write 20 events in a partition and read it back immediately. They shouldn't expired. expectedEvents.addAll(writeEvents(streamConfig, "Phase 1 non-expired event ", 20)); verifyEvents(consumer, expectedEvents); // also verify for a new consumer try { verifyEvents(newConsumer, expectedEvents); } finally { newConsumer.close(); } // Create a new consumer for second consumer verification (with clean state) // Need to create consumer before write event because in HBase, creation of consumer took couple seconds. streamAdmin.configureGroups(streamId, ImmutableMap.of(0L, 1)); streamAdmin.configureGroups(streamId, ImmutableMap.of(0L, 1, 1L, 1)); newConsumer = consumerFactory.create(streamId, stream, new ConsumerConfig(1L, 0, 1, DequeueStrategy.FIFO, null)); // write 20 events in a partition that will be expired due to sleeping the TTL // This will write to a new partition different then the first batch write. // Also, because it sleep TTL time, the previous batch write would also get expired. expectedEvents.clear(); writeEvents(streamConfig, "Phase 2 expired event ", 20); Thread.sleep(ttl); verifyEvents(consumer, expectedEvents); // also verify for a new consumer try { verifyEvents(newConsumer, expectedEvents); } finally { newConsumer.close(); } // Create a new consumer for second consumer verification (with clean state) // Need to create consumer before write event because in HBase, creation of consumer took couple seconds. streamAdmin.configureGroups(streamId, ImmutableMap.of(0L, 1)); streamAdmin.configureGroups(streamId, ImmutableMap.of(0L, 1, 1L, 1)); newConsumer = consumerFactory.create(streamId, stream, new ConsumerConfig(1L, 0, 1, DequeueStrategy.FIFO, null)); // write 20 events in a partition and read it back immediately. They shouldn't expire. expectedEvents.addAll(writeEvents(streamConfig, "Phase 3 non-expired event ", 20)); verifyEvents(consumer, expectedEvents); // also verify for a new consumer try { verifyEvents(newConsumer, expectedEvents); } finally { newConsumer.close(); } // Should be no more pending events expectedEvents.clear(); verifyEvents(consumer, expectedEvents); } finally { consumer.close(); } } private List<StreamEvent> writeEvents(StreamConfig streamConfig, String msgPrefix, int count, Clock clock) throws IOException { FileWriter<StreamEvent> writer = getFileWriterFactory().create(streamConfig, 0); try { return writeEvents(writer, msgPrefix, count, clock); } finally { writer.close(); } } private List<StreamEvent> writeEvents(FileWriter<StreamEvent> streamWriter, String msgPrefix, int count, Clock clock) throws IOException { Map<String, String> headers = ImmutableMap.of(); List<StreamEvent> result = Lists.newLinkedList(); for (int i = 0; i < count; i++) { String msg = msgPrefix + i; StreamEvent event = new StreamEvent(headers, Charsets.UTF_8.encode(msg), clock.getTime()); result.add(event); streamWriter.append(event); } return result; } private List<StreamEvent> writeEvents(StreamConfig streamConfig, String msgPrefix, int count) throws IOException { return writeEvents(streamConfig, msgPrefix, count, new Clock()); } private void verifyEvents(StreamConsumer consumer, Collection<StreamEvent> expectedEvents) throws Exception { TransactionContext txContext = createTxContext(consumer); txContext.start(); try { Set<StreamEvent> actualEvents = Sets.newTreeSet(STREAM_EVENT_COMPARATOR); int size = expectedEvents.size(); DequeueResult<StreamEvent> result = consumer.poll(size == 0 ? 1 : size, 1, TimeUnit.SECONDS); Iterables.addAll(actualEvents, result); Assert.assertEquals(expectedEvents, actualEvents); } finally { txContext.finish(); } } private TransactionContext createTxContext(TransactionAware... txAwares) { return new TransactionContext(getTransactionClient(), txAwares); } private class Clock { public long getTime() { return System.currentTimeMillis(); } } private class ConstantClock extends Clock { private long time; private ConstantClock(long time) { this.time = time; } @Override public long getTime() { return time; } } private class IncrementingClock extends Clock { private final int repeatsPerTimestamp; private final long increment; private int currentRepeat; private long current; public IncrementingClock(long start, long increment, int repeatsPerTimestamp) { Preconditions.checkArgument(repeatsPerTimestamp > 0); this.increment = increment; this.repeatsPerTimestamp = repeatsPerTimestamp; this.current = start; this.currentRepeat = 0; } public IncrementingClock(long start, long increment) { this(start, increment, 1); } @Override public long getTime() { final long result = current; if (currentRepeat % repeatsPerTimestamp == 0) { current += increment; } currentRepeat++; return result; } } }