/******************************************************************************* * Copyright © 2012-2015 eBay Software Foundation * This program is dual licensed under the MIT and Apache 2.0 licenses. * Please see LICENSE for more information. *******************************************************************************/ package com.ebay.jetstream.event.channel.kafka; import static kafka.api.OffsetRequest.EarliestTime; import static kafka.api.OffsetRequest.LatestTime; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.argThat; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import kafka.api.FetchRequest; import kafka.api.PartitionOffsetRequestInfo; import kafka.common.ErrorMapping; import kafka.common.OffsetOutOfRangeException; import kafka.common.TopicAndPartition; import kafka.javaapi.FetchResponse; import kafka.javaapi.OffsetRequest; import kafka.javaapi.OffsetResponse; import kafka.javaapi.consumer.SimpleConsumer; import kafka.javaapi.message.ByteBufferMessageSet; import kafka.message.Message; import kafka.message.MessageAndOffset; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; import org.mockito.ArgumentMatcher; import com.ebay.jetstream.event.JetstreamEvent; public class PartitionReaderTest { // suppose the size of each event is 1000 bytes private static long eachEventInBytes = 1000; private static TestZookeeperServer zkServer; private static KafkaControllerConfig config; private static String configBeanName = "kafkaControllerConfig"; private static KafkaController kafkaController; private static KafkaController.ZkConnector zkConnector; private static TestKafkaServer kafkaBroker0; private static TestKafkaServer kafkaBroker1; private static TestKafkaMessageSerializer serializer; private static String groupId = "testGroup1"; private static String topic = "Topic.test-1"; private static int partition = 1; private static String offsetPath = "/consumers/" + groupId + "/offsets/" + topic + "/" + partition; private static KafkaConsumerConfig consumerConfig; private static PartitionReader reader; @BeforeClass public static void setUp() throws Exception { try { zkServer = new TestZookeeperServer(30000, 2183, 100); zkServer.startup(); } catch (Exception e) { e.printStackTrace(); } String zkConnect = "localhost:2183"; config = new KafkaControllerConfig(); config.setBeanName(configBeanName); config.setRebalanceInterval(3000); config.setRebalanceableWaitInMs(0); config.setZkConnect(zkConnect); kafkaController = new KafkaController(); kafkaController.setConfig(config); kafkaController.init(); zkConnector = kafkaController.getZkConnector(); kafkaBroker0 = new TestKafkaServer("/kafka0/", 9082, 0, zkConnect, 2); kafkaBroker1 = new TestKafkaServer("/kafka1/", 9083, 1, zkConnect, 2); serializer = new TestKafkaMessageSerializer(); consumerConfig = new KafkaConsumerConfig(); consumerConfig.setEnabled(true); consumerConfig.setGroupId(groupId); String partitionStatePath0 = "/brokers/topics/" + topic + "/partitions/" + 0 + "/state"; Map<String, Object> map0 = new HashMap<String, Object>(); map0.put("leader", 0); zkConnector.writeJSON(partitionStatePath0, map0); String partitionStatePath1 = "/brokers/topics/" + topic + "/partitions/" + 1 + "/state"; Map<String, Object> map1 = new HashMap<String, Object>(); map1.put("leader", 1); zkConnector.writeJSON(partitionStatePath1, map1); reader = new PartitionReader(topic, partition, consumerConfig, zkConnector, serializer); } private void mockSimpleConsumerForEarliestOffset( SimpleConsumer mockConsumer, String topic, int partition, long earliestOffset) { TopicAndPartition tp = new TopicAndPartition(topic, partition); OffsetResponse earliestResponse = mock(OffsetResponse.class); when(earliestResponse.hasError()).thenReturn(false); when(earliestResponse.offsets(topic, partition)).thenReturn( new long[] { earliestOffset }); when( mockConsumer .getOffsetsBefore(argThat(new IsEarliestOffsetRequest( tp)))).thenReturn(earliestResponse); } private void mockSimpleConsumerForLatestOffset(SimpleConsumer mockConsumer, String topic, int partition, long latestOffset) { TopicAndPartition tp = new TopicAndPartition(topic, partition); OffsetResponse latestResponse = mock(OffsetResponse.class); when(latestResponse.hasError()).thenReturn(false); when(latestResponse.offsets(topic, partition)).thenReturn( new long[] { latestOffset }); when( mockConsumer .getOffsetsBefore(argThat(new IsLatestOffsetRequest(tp)))) .thenReturn(latestResponse); } private void mockSimpleConsumerForOffsetError(SimpleConsumer mockConsumer, String topic, int partition) { TopicAndPartition tp = new TopicAndPartition(topic, partition); OffsetResponse offsetResponse = mock(OffsetResponse.class); when(offsetResponse.hasError()).thenReturn(true); when(offsetResponse.errorCode(topic, partition)).thenReturn( ErrorMapping.UnknownTopicOrPartitionCode()); when(mockConsumer.getOffsetsBefore(argThat(new IsOffsetRequest()))) .thenReturn(offsetResponse); } private void mockSimpleConsumerForOffsetOutOfRange( SimpleConsumer mockConsumer, String topic, int partition) { FetchResponse fetchResponse = mock(FetchResponse.class); when(fetchResponse.hasError()).thenReturn(true); when(fetchResponse.errorCode(topic, partition)).thenReturn( ErrorMapping.OffsetOutOfRangeCode()); when(mockConsumer.fetch(argThat(new IsFetchRequest()))).thenReturn( fetchResponse); } @SuppressWarnings("unchecked") private void mockSimpleConsumerForFetchException(SimpleConsumer mockConsumer) { when(mockConsumer.fetch(argThat(new IsFetchRequest()))).thenThrow( FetchFailException.class); } private void mockSimpleConsumerForNull(SimpleConsumer mockConsumer) { when(mockConsumer.fetch(argThat(new IsFetchRequest()))) .thenReturn(null); } private void mockSimpleConsumerForRead(SimpleConsumer mockConsumer, String topic, int partition, long readOffset, long readSizeInBytes) { List<MessageAndOffset> list = new ArrayList<MessageAndOffset>(); for (int i = 0; i < readSizeInBytes / eachEventInBytes; i++) { JetstreamEvent event = new JetstreamEvent(); byte[] key = serializer.encodeMessage(event); byte[] payload = serializer.encodeMessage(event); Message msg = mock(Message.class); when(msg.key()).thenReturn(ByteBuffer.wrap(key)); when(msg.payload()).thenReturn(ByteBuffer.wrap(payload)); MessageAndOffset msgOffset = new MessageAndOffset(msg, readOffset + i); list.add(msgOffset); } ByteBufferMessageSet messageSet = mock(ByteBufferMessageSet.class); when(messageSet.iterator()).thenReturn(list.iterator()); FetchResponse fetchResponse = mock(FetchResponse.class); when(fetchResponse.hasError()).thenReturn(false); when(fetchResponse.messageSet(topic, partition)).thenReturn(messageSet); when(mockConsumer.fetch(argThat(new IsFetchRequest()))).thenReturn( fetchResponse); } @Test public void testCompare() { PartitionReader reader0 = new PartitionReader(topic, 0, consumerConfig, zkConnector, serializer); PartitionReader reader1 = new PartitionReader(topic, 1, consumerConfig, zkConnector, serializer); reader0.setNextFetchInMs(10000); reader1.setNextFetchInMs(20000); assertTrue(reader0.compareTo(reader1) < 0); assertTrue(reader1.compareTo(reader0) > 0); reader1.setNextFetchInMs(10000); assertTrue(reader1.compareTo(reader0) == 0); } @Test public void testCalWaitTime() { PartitionReader reader0 = new PartitionReader(topic, 0, consumerConfig, zkConnector, serializer); reader0.setNextFetchInMs(System.currentTimeMillis() - 1000); assertTrue(reader0.calcWaitTime() < 0); reader0.setNextFetchInMs(System.currentTimeMillis() + 1000); assertTrue(reader0.calcWaitTime() > 0); } @Test public void testReadEvents() throws Exception { int expectedCount = (int) (consumerConfig.getBatchSizeBytes() / eachEventInBytes); long readOffset = 1250; reader.setReadOffset(readOffset); SimpleConsumer mockConsumer = mock(SimpleConsumer.class); mockSimpleConsumerForRead(mockConsumer, topic, partition, readOffset, consumerConfig.getBatchSizeBytes()); ReflectFieldUtil.setField(reader, "m_consumer", mockConsumer); reader.setNextBatchSizeBytes(-100); List<JetstreamEvent> events = reader.readEvents(); assertEquals(expectedCount, events.size()); assertEquals(readOffset + expectedCount, reader.getReadOffset()); } @Test public void testInitOffset() throws Exception { long smallest = 100, largest = 10000; SimpleConsumer mockConsumer = mock(SimpleConsumer.class); mockSimpleConsumerForEarliestOffset(mockConsumer, topic, partition, smallest); mockSimpleConsumerForLatestOffset(mockConsumer, topic, partition, largest); ReflectFieldUtil.setField(reader, "m_consumer", mockConsumer); // if offset not in zookeeper, and init with largest zkConnector.delete(offsetPath); reader.setReadOffset(-1); consumerConfig.setAutoOffsetReset("largest"); reader.initOffset(false); long readOffset = reader.getReadOffset(); assertEquals(largest, readOffset); // if offset exist in zookeeper, init with it zkConnector.writeString(offsetPath, String.valueOf(2000)); reader.setReadOffset(-1); consumerConfig.setAutoOffsetReset("largest"); reader.initOffset(false); readOffset = reader.getReadOffset(); assertEquals(2000, readOffset); // if offset not in zookeeper, and init with smallest zkConnector.delete(offsetPath); reader.setReadOffset(-1); consumerConfig.setAutoOffsetReset("smallest"); reader.initOffset(false); readOffset = reader.getReadOffset(); assertEquals(smallest, readOffset); // if a new partition, should always init with smallest although set // largest zkConnector.delete(offsetPath); reader.setReadOffset(-1); consumerConfig.setAutoOffsetReset("largest"); reader.initOffset(true); readOffset = reader.getReadOffset(); assertEquals(smallest, readOffset); // if offset exists, but data illegal zkConnector.writeString(offsetPath, "offset"); boolean thrown = false; try { reader.initOffset(false); } catch (Exception e) { assertTrue(e instanceof RuntimeException); thrown = true; } assertTrue(thrown); } @Test public void testOffsetError() throws Exception { SimpleConsumer mockConsumer = mock(SimpleConsumer.class); mockSimpleConsumerForOffsetError(mockConsumer, topic, partition); ReflectFieldUtil.setField(reader, "m_consumer", mockConsumer); try { reader.resetOffset(); } catch (Exception e) { } verify(mockConsumer, atLeast(2)).getOffsetsBefore( argThat(new IsOffsetRequest())); } @Test public void testResetOffset() throws Exception { long smallest = 100, largest = 10000; SimpleConsumer mockConsumer = mock(SimpleConsumer.class); mockSimpleConsumerForEarliestOffset(mockConsumer, topic, partition, smallest); mockSimpleConsumerForLatestOffset(mockConsumer, topic, partition, largest); ReflectFieldUtil.setField(reader, "m_consumer", mockConsumer); reader.resetOffset(); long readOffset = reader.getReadOffset(); assertEquals(largest, readOffset); consumerConfig.setAutoOffsetReset("smallest"); reader.resetOffset(); readOffset = reader.getReadOffset(); assertEquals(smallest, readOffset); } @Test public void testRevertOffset() throws Exception { long smallest = 100, largest = 10000; SimpleConsumer mockConsumer = mock(SimpleConsumer.class); mockSimpleConsumerForEarliestOffset(mockConsumer, topic, partition, smallest); mockSimpleConsumerForLatestOffset(mockConsumer, topic, partition, largest); ReflectFieldUtil.setField(reader, "m_consumer", mockConsumer); zkConnector.create(offsetPath, false); zkConnector.writeString(offsetPath, String.valueOf(2000)); reader.setReadOffset(3000); assertEquals(3000, reader.getReadOffset()); reader.revertOffset(); assertEquals(2000, reader.getReadOffset()); // if illegal offset in zookeeper zkConnector.writeString(offsetPath, String.valueOf(-2)); reader.setReadOffset(3000); assertEquals(3000, reader.getReadOffset()); consumerConfig.setAutoOffsetReset("largest"); reader.revertOffset(); assertEquals(largest, reader.getReadOffset()); } @Test public void testCommitOffset() { zkConnector.delete(offsetPath); reader.setReadOffset(12345); reader.commitOffset(); reader.setReadOffset(0); reader.revertOffset(); assertEquals(12345, reader.getReadOffset()); } @Test public void testSetToRelease() { reader.setToRelease(); assertFalse(reader.isTaken()); } @Test public void testSetToLost() { reader.setLost(); assertTrue(reader.isLost()); } @Test public void testIdle() throws Exception { reader.fistIdle(); consumerConfig.setIdleTimeInMs(3000); assertTrue(reader.isIdled()); assertTrue(reader.getIdleTime() <= consumerConfig.getIdleTimeInMs()); Thread.sleep(4000); assertTrue(reader.getIdleTime() >= consumerConfig.getIdleTimeInMs()); reader.setOnIdled(); assertTrue(reader.isOnIdled()); reader.fistIdle(); assertTrue(reader.isIdled()); reader.clearIdleStats(); assertFalse(reader.isIdled()); } @Test public void testSetNextBatchSize() throws Exception { int nextBatchSizeBytes = (Integer) ReflectFieldUtil.getField(reader, "m_nextBatchSizeBytes"); assertEquals(consumerConfig.getBatchSizeBytes(), nextBatchSizeBytes); reader.setNextBatchSizeBytes(-100); nextBatchSizeBytes = (Integer) ReflectFieldUtil.getField(reader, "m_nextBatchSizeBytes"); assertEquals(consumerConfig.getBatchSizeBytes(), nextBatchSizeBytes); reader.setNextBatchSizeBytes(10000); nextBatchSizeBytes = (Integer) ReflectFieldUtil.getField(reader, "m_nextBatchSizeBytes"); assertTrue(consumerConfig.getBatchSizeBytes() != nextBatchSizeBytes); assertEquals(10000, nextBatchSizeBytes); } @Test public void testGetTopic() { assertEquals(topic, reader.getTopic()); } @Test public void testGetPartition() { assertEquals(partition, reader.getPartition()); } @Test public void testSkipReadEvents() throws Exception { reader.setNextBatchSizeBytes(0); List<JetstreamEvent> events = reader.readEvents(); assertTrue(events.isEmpty()); int nextBatchSizeBytes = (Integer) ReflectFieldUtil.getField(reader, "m_nextBatchSizeBytes"); assertEquals(consumerConfig.getBatchSizeBytes(), nextBatchSizeBytes); } @Test public void testReadOffsetOutOfRange() throws Exception { long smallest = 100, largest = 10000; SimpleConsumer mockConsumer = mock(SimpleConsumer.class); mockSimpleConsumerForEarliestOffset(mockConsumer, topic, partition, smallest); mockSimpleConsumerForLatestOffset(mockConsumer, topic, partition, largest); mockSimpleConsumerForOffsetOutOfRange(mockConsumer, topic, partition); ReflectFieldUtil.setField(reader, "m_consumer", mockConsumer); boolean thrown = false; try { reader.readEvents(); } catch (Exception e) { assertTrue(e instanceof OffsetOutOfRangeException); thrown = true; } assertTrue(thrown); } @Test public void testFetchException() throws Exception { SimpleConsumer mockConsumer = mock(SimpleConsumer.class); mockSimpleConsumerForFetchException(mockConsumer); ReflectFieldUtil.setField(reader, "m_consumer", mockConsumer); boolean thrown = false; try { reader.readEvents(); } catch (Exception e) { assertTrue(e instanceof RuntimeException); thrown = true; } assertTrue(thrown); } @Test public void testReadEventsAndSkipSomeOldOnes() throws Exception { int expectedCount = (int) (consumerConfig.getBatchSizeBytes() / eachEventInBytes); long readOffset = 1250; reader.setReadOffset(readOffset); SimpleConsumer mockConsumer = mock(SimpleConsumer.class); mockSimpleConsumerForRead(mockConsumer, topic, partition, readOffset - 100, consumerConfig.getBatchSizeBytes()); ReflectFieldUtil.setField(reader, "m_consumer", mockConsumer); reader.setNextBatchSizeBytes(-100); List<JetstreamEvent> events = reader.readEvents(); assertEquals(expectedCount - 100, events.size()); assertEquals(readOffset + expectedCount - 100, reader.getReadOffset()); } @Test public void testReadEventsAndFetchNull() throws Exception { SimpleConsumer mockConsumer = mock(SimpleConsumer.class); this.mockSimpleConsumerForNull(mockConsumer); ReflectFieldUtil.setField(reader, "m_consumer", mockConsumer); try { List<JetstreamEvent> events = reader.readEvents(); } catch (Exception e) { } verify(mockConsumer, atLeast(2)).fetch(argThat(new IsFetchRequest())); } @Test public void testReadEventsAndBlocked() throws Exception { int expectedCount = (int) (consumerConfig.getBatchSizeBytes() / eachEventInBytes); long readOffset = 1250; reader.setReadOffset(readOffset); // cannot read any events although not to the end SimpleConsumer mockConsumer = mock(SimpleConsumer.class); mockSimpleConsumerForRead(mockConsumer, topic, partition, readOffset, 0); long largest = 1000000000; mockSimpleConsumerForLatestOffset(mockConsumer, topic, partition, largest); ReflectFieldUtil.setField(reader, "m_consumer", mockConsumer); reader.setNextBatchSizeBytes(-100); try { List<JetstreamEvent> events = reader.readEvents(); } catch (Exception e) { } verify(mockConsumer, atLeast(2)).fetch(argThat(new IsFetchRequest())); } @Test public void testClose() throws Exception { SimpleConsumer mockConsumer = mock(SimpleConsumer.class); ReflectFieldUtil.setField(reader, "m_consumer", mockConsumer); reader.close(); verify(mockConsumer).close(); } @AfterClass public static void tearDown() throws Exception { kafkaController.shutDown(); zkServer.shutdown(); kafkaBroker0.stop(); kafkaBroker1.stop(); } class IsOffsetRequest extends ArgumentMatcher<OffsetRequest> { @Override public boolean matches(Object argument) { return (argument instanceof OffsetRequest); } } class IsEarliestOffsetRequest extends ArgumentMatcher<OffsetRequest> { private TopicAndPartition tp; public IsEarliestOffsetRequest(TopicAndPartition tp) { this.tp = tp; } @Override public boolean matches(Object argument) { if (!(argument instanceof OffsetRequest)) return false; OffsetRequest req = (OffsetRequest) argument; PartitionOffsetRequestInfo reqInfo = req.underlying().requestInfo() .get(tp).get(); if (reqInfo.time() == EarliestTime()) return true; return false; } } class IsLatestOffsetRequest extends ArgumentMatcher<OffsetRequest> { private TopicAndPartition tp; public IsLatestOffsetRequest(TopicAndPartition tp) { this.tp = tp; } @Override public boolean matches(Object argument) { if (!(argument instanceof OffsetRequest)) return false; OffsetRequest req = (OffsetRequest) argument; PartitionOffsetRequestInfo reqInfo = req.underlying().requestInfo() .get(tp).get(); if (reqInfo.time() == LatestTime()) return true; return false; } } class IsFetchRequest extends ArgumentMatcher<FetchRequest> { @Override public boolean matches(Object argument) { return (argument instanceof FetchRequest); } } class FetchFailException extends Exception { private static final long serialVersionUID = 7394596392413986570L; } }