/** * Copyright 2016-2017 Sixt GmbH & Co. Autovermietung KG * 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 com.sixt.service.framework.kafka.messaging; import com.palantir.docker.compose.DockerComposeRule; import com.palantir.docker.compose.configuration.ProjectName; import com.palantir.docker.compose.connection.DockerPort; import com.sixt.service.framework.IntegrationTest; import com.sixt.service.framework.OrangeContext; import com.sixt.service.framework.ServiceProperties; import com.sixt.service.framework.servicetest.helper.DockerComposeHelper; import com.sixt.service.framework.util.Sleeper; import org.apache.commons.lang3.RandomStringUtils; import org.joda.time.Duration; import org.junit.ClassRule; import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.experimental.categories.Category; import org.junit.rules.Timeout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import static org.junit.Assert.assertTrue; @Ignore("This test is not automated and meant to be run manually: you kill/restart some Docker containers, look to the logs and see what happens.") @Category(IntegrationTest.class) public class KafkaFailoverIntegrationTest { private static final Logger logger = LoggerFactory.getLogger(KafkaFailoverIntegrationTest.class); @Rule public Timeout globalTimeout = Timeout.seconds(60 * 20); @ClassRule public static DockerComposeRule docker = DockerComposeRule.builder() .file("src/test/resources/docker-compose-kafkafailover-integrationtest.yml") .saveLogsTo("build/dockerCompose/logs") .projectName(ProjectName.random()) .waitingForService("kafka1", (container) -> DockerComposeHelper.waitForKafka( "build/dockerCompose/logs/kafka1.log"), Duration.standardMinutes(2)) .waitingForService("kafka2", (container) -> DockerComposeHelper.waitForKafka( "build/dockerCompose/logs/kafka2.log"), Duration.standardMinutes(2)) .waitingForService("kafka3", (container) -> DockerComposeHelper.waitForKafka( "build/dockerCompose/logs/kafka3.log"), Duration.standardMinutes(2)) .build(); @Test public void manualKafkaTest() throws InterruptedException { ServiceProperties serviceProperties = fillServiceProperties(); // Topics are created with 3 partitions - see docker-compose-kafkafailover-integrationtest.yml Topic ping = new Topic("ping"); Topic pong = new Topic("pong"); AtomicInteger sentMessages = new AtomicInteger(0); AtomicInteger sendFailures = new AtomicInteger(0); AtomicInteger recievedMessages = new AtomicInteger(0); Producer producer = new ProducerFactory(serviceProperties).createProducer(); final AtomicBoolean produceMessages = new AtomicBoolean(true); // Produce messages until test tells producer to stop. ExecutorService producerExecutor = Executors.newSingleThreadExecutor(); producerExecutor.submit( new Runnable() { @Override public void run() { OrangeContext context = new OrangeContext(); Sleeper sleeper = new Sleeper(); while (produceMessages.get()) { try { String key = RandomStringUtils.randomAscii(5); SayHelloToCmd payload = SayHelloToCmd.newBuilder().setName(key).build(); Message request = Messages.requestFor(ping, pong, key, payload, context); producer.send(request); sentMessages.incrementAndGet(); sleeper.sleepNoException(1000); } catch (Throwable t) { sendFailures.incrementAndGet(); logger.error("Caught exception in producer loop", t); } } } } ); Consumer consumer = consumerFactoryWithHandler(serviceProperties, SayHelloToCmd.class, new MessageHandler<SayHelloToCmd>() { @Override public void onMessage(Message<SayHelloToCmd> message, OrangeContext context) { recievedMessages.incrementAndGet(); } } ).consumerForTopic(ping, new DiscardFailedMessages()); // Wait to allow manual fiddling with Kafka. Sync with global test timeout above. Thread.sleep(2* 60 * 1000); produceMessages.set(false); producer.shutdown(); Thread.sleep(10_000); consumer.shutdown(); logger.info("sentMessages: " + sentMessages.get()); logger.info("sendFailures: " + sendFailures.get()); logger.info("recievedMessages: " + recievedMessages.get()); } @Ignore @Test public void producerSendsToNonExistingTopic() { ServiceProperties serviceProperties = fillServiceProperties(); Topic cruft = new Topic("cruft"); Topic lard = new Topic("lard"); Producer producer = new ProducerFactory(serviceProperties).createProducer(); String key = RandomStringUtils.randomAscii(5); SayHelloToCmd payload = SayHelloToCmd.newBuilder().setName(key).build(); Message request = Messages.requestFor(cruft, lard, key, payload, new OrangeContext()); producer.send(request); // Results: // 1.) NO topic auto creation i.e. KAFKA_AUTO_CREATE_TOPICS_ENABLE = false // 2017-04-12 18:14:41,239 [Time-limited test] DEBUG c.s.s.f.kafka.messaging.Producer - Sending message com.sixt.service.framework.kafka.messaging.SayHelloToCmd with key O+oRQ to topic cruft // loads of: 2017-04-12 18:14:41,340 [kafka-producer-network-thread | producer-2] WARN o.apache.kafka.clients.NetworkClient - Error while fetching metadata with correlation id 0 : {cruft=UNKNOWN_TOPIC_OR_PARTITION} // and finally: org.apache.kafka.common.errors.TimeoutException: Failed to update metadata after 60000 ms. // 2.) WITH topic auto creation i.e. KAFKA_AUTO_CREATE_TOPICS_ENABLE = true // 2017-04-12 18:18:24,488 [Time-limited test] DEBUG c.s.s.f.kafka.messaging.Producer - Sending message com.sixt.service.framework.kafka.messaging.SayHelloToCmd with key uXdJ~ to topic cruft // one: 2017-04-12 18:18:24,638 [kafka-producer-network-thread | producer-2] WARN o.apache.kafka.clients.NetworkClient - Error while fetching metadata with correlation id 0 : {cruft=LEADER_NOT_AVAILABLE // and finally: success } @Ignore @Test public void consumerSubscribesToNonExistingTopic() throws InterruptedException { ServiceProperties serviceProperties = fillServiceProperties(); Topic cruft = new Topic("krufty"); CountDownLatch latch = new CountDownLatch(1); Consumer consumer = consumerFactoryWithHandler(serviceProperties, SayHelloToCmd.class, new MessageHandler<SayHelloToCmd>() { @Override public void onMessage(Message<SayHelloToCmd> message, OrangeContext context) { latch.countDown(); } } ).consumerForTopic(cruft, new DiscardFailedMessages()); Producer producer = new ProducerFactory(serviceProperties).createProducer(); String key = RandomStringUtils.randomAscii(5); SayHelloToCmd payload = SayHelloToCmd.newBuilder().setName(key).build(); Message request = Messages.requestFor(cruft, cruft, key, payload, new OrangeContext()); producer.send(request); assertTrue(latch.await(1, TimeUnit.MINUTES)); producer.shutdown(); consumer.shutdown(); // Results: // 1.) WITH topic auto creation i.e. KAFKA_AUTO_CREATE_TOPICS_ENABLE = true // All ok, needs to discover coordinator etc. // 2.) NO topic auto creation i.e. KAFKA_AUTO_CREATE_TOPICS_ENABLE = false //2017-04-12 18:27:16,701 [pool-9-thread-1] INFO c.s.s.f.kafka.messaging.Consumer - Consumer in group kruftmeister-com.sixt.service.unknown subscribed to topic kruftmeister //2017-04-12 18:27:16,852 [pool-9-thread-1] WARN o.apache.kafka.clients.NetworkClient - Error while fetching metadata with correlation id 1 : {kruftmeister=UNKNOWN_TOPIC_OR_PARTITION} //2017-04-12 18:27:18,876 [pool-9-thread-1] WARN o.apache.kafka.clients.NetworkClient - Error while fetching metadata with correlation id 40 : {kruftmeister=UNKNOWN_TOPIC_OR_PARTITION} //2017-04-12 18:27:18,889 [pool-9-thread-1] INFO o.a.k.c.c.i.AbstractCoordinator - Discovered coordinator 172.19.0.3:9092 (id: 2147482646 rack: null) for group kruftmeister-com.sixt.service.unknown. //2017-04-12 18:27:18,892 [pool-9-thread-1] INFO o.a.k.c.c.i.ConsumerCoordinator - Revoking previously assigned partitions [] for group kruftmeister-com.sixt.service.unknown //2017-04-12 18:27:18,894 [pool-9-thread-1] DEBUG c.s.s.f.kafka.messaging.Consumer - ConsumerRebalanceListener.onPartitionsRevoked on [] //2017-04-12 18:27:18,917 [pool-9-thread-1] INFO o.a.k.c.c.i.AbstractCoordinator - (Re-)joining group kruftmeister-com.sixt.service.unknown //2017-04-12 18:27:18,937 [pool-9-thread-1] INFO o.a.k.c.c.i.AbstractCoordinator - Marking the coordinator 172.19.0.3:9092 (id: 2147482646 rack: null) dead for group kruftmeister-com.sixt.service.unknown //2017-04-12 18:27:19,041 [pool-9-thread-1] INFO o.a.k.c.c.i.AbstractCoordinator - Discovered coordinator 172.19.0.3:9092 (id: 2147482646 rack: null) for group kruftmeister-com.sixt.service.unknown. //2017-04-12 18:27:19,041 [pool-9-thread-1] INFO o.a.k.c.c.i.AbstractCoordinator - (Re-)joining group kruftmeister-com.sixt.service.unknown //2017-04-12 18:27:19,135 [pool-9-thread-1] INFO o.a.k.c.c.i.AbstractCoordinator - Successfully joined group kruftmeister-com.sixt.service.unknown with generation 1 //2017-04-12 18:27:19,135 [pool-9-thread-1] INFO o.a.k.c.c.i.ConsumerCoordinator - Setting newly assigned partitions [] for group kruftmeister-com.sixt.service.unknown //2017-04-12 18:27:19,135 [pool-9-thread-1] DEBUG c.s.s.f.kafka.messaging.Consumer - ConsumerRebalanceListener.onPartitionsAssigned on [] // -> assigned to a topic with no partitions? } private ServiceProperties fillServiceProperties() { DockerPort kafka1 = docker.containers().container("kafka1").port(9092); DockerPort kafka2 = docker.containers().container("kafka2").port(9092); DockerPort kafka3 = docker.containers().container("kafka2").port(9092); StringBuilder kafkaServer = new StringBuilder(); kafkaServer.append(kafka2.inFormat("$HOST:$EXTERNAL_PORT")); kafkaServer.append(","); kafkaServer.append(kafka1.inFormat("$HOST:$EXTERNAL_PORT")); kafkaServer.append(","); kafkaServer.append(kafka3.inFormat("$HOST:$EXTERNAL_PORT")); String[] args = new String[2]; args[0] = "-" + ServiceProperties.KAFKA_SERVER_KEY; args[1] = kafkaServer.toString(); ServiceProperties serviceProperties = new ServiceProperties(); serviceProperties.initialize(args); return serviceProperties; } private <T extends com.google.protobuf.Message> ConsumerFactory consumerFactoryWithHandler(ServiceProperties serviceProperties, Class<T> messageType, MessageHandler<T> handler) { TypeDictionary typeDictionary = new TypeDictionary(); ReflectionTypeDictionaryFactory reflectionCruft = new ReflectionTypeDictionaryFactory(null); typeDictionary.putAllParsers(reflectionCruft.populateParsersFromClasspath()); typeDictionary.putHandler(MessageType.of(messageType), handler); ConsumerFactory consumerFactory = new ConsumerFactory(serviceProperties, typeDictionary, null, null); return consumerFactory; } }