package com.neverwinterdp.kafka.producer; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; import java.util.LinkedList; import java.util.List; import org.junit.Ignore; import org.junit.Test; import com.beust.jcommander.JCommander; import com.neverwinterdp.kafka.tool.KafkaMessageCheckTool; import com.neverwinterdp.kafka.tool.KafkaTool; public class KafkaProducerTopicRebalanceBugUnitTest extends AbstractBugsUnitTest { /* * For simple re-balance, we start with two brokers, add a new broker and make it leader * For total re-balance we start with two brokers then move the data to two new brokers. * */ @Override public int getKafkaBrokers() { return 4; } //Here we investigate what happens when topic is in brokers 1,2 with broker 1 as leader. //We then swap and have broker 2 as leader. //Does default producer lose messages? //Update. This tests fails. This shows that the default kafka writer does not lose messages on swapping leader @Test @Ignore public void testLeaderSwap() throws Exception { //create topic on brokers 1,2 String[] args = { "--create", "--partition", "1", "--replication-factor", "2", "--topic", NAME, "--zookeeper", cluster.getZKConnect(), "--replica-assignment", "1:2" }; KafkaTool tool = new KafkaTool(NAME, cluster.getZKConnect()); tool.connect(); tool.createTopic(args); // while writing, re-balance topic to have isr {2,1} and have 2 as leader DefaultKafkaWriter writer = createKafkaWriter(); MessageFailDebugCallback failDebugCallback = new MessageFailDebugCallback(); int[] newBrokers = {2,1}; int leaderId1 = 0; int leaderId2 = 0; for (int i = 0; i < NUM_OF_SENT_MESSAGES; i++) { writer.send(NAME, 0, "key-" + i, "test-1-" + i, failDebugCallback, 5000); if (i == 5) { leaderId1 = tool.findPartitionMetadata(NAME, 0).leader().id(); KafkaLeaderElector leaderElector = new KafkaLeaderElector(NAME, 0, tool, newBrokers); new Thread(leaderElector).start(); } } writer.close(); // Thread.sleep(5000); String[] checkArgs = { "--topic", NAME, "--consume-max", Integer.toString(NUM_OF_SENT_MESSAGES), "--zk-connect", cluster.getZKConnect(), }; KafkaMessageCheckTool checkTool = new KafkaMessageCheckTool(checkArgs); new JCommander(checkTool, checkArgs); checkTool.runAsDeamon(); if (checkTool.waitForTermination(10000)) { checkTool.setInterrupt(true); Thread.sleep(3000); } leaderId2 = tool.findPartitionMetadata(NAME, 0).leader().id(); System.out.println("initial leader " + leaderId1); System.out.println("new leader " + leaderId2); //ensure that leader election worked assertNotEquals(leaderId1, leaderId2); assertEquals(2, leaderId2); tool.close(); System.out.println("send done, failed message count = " + failDebugCallback.failedCount); System.out.println("read messages = " + checkTool.getMessageCounter().getTotal()); assertTrue(checkTool.getMessageCounter().getTotal() < NUM_OF_SENT_MESSAGES); } /** * This unit test show that the kafka producer loses messages when a simple topic rebalance is done. * It doesn't have the capability to write to the new leader as the new leader was not among the initial list. * * While writing to kafka we move the leader to a broker that wasn't in the initial bootstrap.servers list * @throws Exception */ @Test public void testSimpleTopicRebalance() throws Exception { //create topic on brokers 2,3 String[] args = { "--create", "--partition", "1", "--replication-factor", "2", "--topic", NAME, "--zookeeper", cluster.getZKConnect(), "--replica-assignment", "2:3" }; KafkaTool tool = new KafkaTool(NAME, cluster.getZKConnect()); tool.connect(); tool.createTopic(args); // while writing, re-balance topic to have isr {1,2,3} and have 1 as leader DefaultKafkaWriter writer = createKafkaWriter(); MessageFailDebugCallback failDebugCallback = new MessageFailDebugCallback(); int[] brokers= { 1, 2, 3 }; int leaderId1 = 0; int leaderId2 = 0; for (int i = 0; i < NUM_OF_SENT_MESSAGES; i++) { writer.send(NAME, 0, "key-" + i, "test-1-" + i, failDebugCallback, 5000); if (i == 5) { leaderId1 = tool.findPartitionMetadata(NAME, 0).leader().id(); KafkaLeaderElector leaderElector = new KafkaLeaderElector(NAME, 0, tool, brokers); new Thread(leaderElector).start(); } } writer.close(); // Thread.sleep(5000); String[] checkArgs = { "--topic", NAME, "--consume-max", Integer.toString(NUM_OF_SENT_MESSAGES), "--zk-connect", cluster.getZKConnect(), }; KafkaMessageCheckTool checkTool = new KafkaMessageCheckTool(checkArgs); new JCommander(checkTool, checkArgs); checkTool.runAsDeamon(); if (checkTool.waitForTermination(10000)) { checkTool.setInterrupt(true); Thread.sleep(3000); } leaderId2 = tool.findPartitionMetadata(NAME, 0).leader().id(); System.out.println("initial leader " + leaderId1); System.out.println("new leader " + leaderId2); //ensure that leader election worked assertNotEquals(leaderId1, leaderId2); assertEquals(1, leaderId2); tool.close(); System.out.println("Sent message count = " + NUM_OF_SENT_MESSAGES); System.out.println("send done, failed message count = " + failDebugCallback.failedCount); System.out.println("read messages = " + checkTool.getMessageCounter().getTotal()); assertTrue(checkTool.getMessageCounter().getTotal() < NUM_OF_SENT_MESSAGES); } /** * This unit test show that the kafka producer loses messages when topic is rebalanced to new brokers. * It doesn't have the capability to 'know' the new leader as the new leader was not among the initial list. * * While writing to kafka we move the topic/partition to new brokers */ @Test public void testTotalTopicRebalance() throws Exception { //create topic on brokers 1,2 String[] args = { "--create", "--partition", "1", "--replication-factor", "2", "--topic", NAME, "--zookeeper", cluster.getZKConnect(), "--replica-assignment", "1:2" }; KafkaTool tool = new KafkaTool(NAME, cluster.getZKConnect()); tool.connect(); tool.createTopic(args); DefaultKafkaWriter writer = createKafkaWriter(); MessageFailDebugCallback failDebugCallback = new MessageFailDebugCallback(); int leaderId1 = 0; int leaderId2 = 0; for (int i = 0; i < NUM_OF_SENT_MESSAGES; i++) { writer.send(NAME, 0, "key-" + i, "test-1-" + i, failDebugCallback, 5000); if (i == 5) { leaderId1 = tool.findPartitionMetadata(NAME, 0).leader().id(); System.out.println("leader1 " + leaderId1); KafkaTopicRebalancer leaderKiller = new KafkaTopicRebalancer(NAME, 0, tool); new Thread(leaderKiller).start(); } } writer.close(); String[] checkArgs = { "--topic", NAME, "--consume-max", Integer.toString(NUM_OF_SENT_MESSAGES), "--zk-connect", cluster.getZKConnect(), }; KafkaMessageCheckTool checkTool = new KafkaMessageCheckTool(checkArgs); new JCommander(checkTool, checkArgs); //KafkaMessageCheckTool checkTool = new KafkaMessageCheckTool(cluster.getZKConnect(), "test", NUM_OF_SENT_MESSAGES); checkTool.runAsDeamon(); if (checkTool.waitForTermination(10000)) { checkTool.setInterrupt(true); Thread.sleep(3000); } leaderId2 = tool.findPartitionMetadata(NAME, 0).leader().id(); System.out.println("initial leader " + leaderId1); System.out.println("new leader " + leaderId2); //ensure that leader election worked assertNotEquals(leaderId1, leaderId2); assertEquals(3, leaderId2); System.out.println("send done, failed message count = " + failDebugCallback.failedCount); System.out.println("read messages = " + checkTool.getMessageCounter().getTotal()); assertTrue(checkTool.getMessageCounter().getTotal() < NUM_OF_SENT_MESSAGES); } class KafkaTopicRebalancer implements Runnable { private int partition; private String topic; List<Object> remainingBrokers; private KafkaTool tool; public KafkaTopicRebalancer(String topic, int partition, KafkaTool tool) { super(); this.topic = topic; this.partition = partition; this.tool = tool; remainingBrokers = getRemainingBrokers(); } @Override public void run() { try { boolean success = tool.reassignPartition(topic, partition, remainingBrokers); logger.info("reasign " + success); Thread.sleep(500); } catch (Exception e) { e.printStackTrace(); } } //a list of broker id's //move topic to brokers 3,4 private List<Object> getRemainingBrokers() { List<Object> brokerIds = new LinkedList<Object>(); brokerIds.add(3); brokerIds.add(4); return brokerIds; } public void setNewBrokers(int... i) { remainingBrokers.clear(); for (int j = 0; j < i.length; j++) { remainingBrokers.add(i[j]); } System.out.println("remaining brokers"); } } class KafkaLeaderElector implements Runnable { private int partition; private String topic; private KafkaTool tool; private int[] brokers; public KafkaLeaderElector(String topic, int partition, KafkaTool tool, int[] newBrokers) { super(); this.topic = topic; this.partition = partition; this.tool = tool; this.brokers= newBrokers; } @Override public void run() { try { KafkaTopicRebalancer topicRebalancer = new KafkaTopicRebalancer(topic, partition, tool); topicRebalancer.setNewBrokers(brokers); topicRebalancer.run(); tool.moveLeaderToPreferredReplica(topic, partition); } catch (Exception e) { e.printStackTrace(); } } } }