mirror of https://github.com/apache/kafka.git
				
				
				
			KAFKA-5862: Remove ZK dependency from Streams reset tool, Part I
Author: Bill Bejeck <bill@confluent.io> Author: bbejeck <bbejeck@gmail.com> Reviewers: Matthias J. Sax <matthias@confluent.io>, Ted Yu <yuzhihong@gmail.com>, Guozhang Wang <wangguoz@gmail.com> Closes #3927 from bbejeck/KAFKA-5862_remove_zk_dependency_from_streams_reset_tool
This commit is contained in:
		
							parent
							
								
									1fd70c7c94
								
							
						
					
					
						commit
						271f6b5aec
					
				|  | @ -53,6 +53,7 @@ | |||
|   </subpackage> | ||||
| 
 | ||||
|   <subpackage name="tools"> | ||||
|     <allow pkg="org.apache.kafka.clients.admin" /> | ||||
|     <allow pkg="kafka.admin" /> | ||||
|     <allow pkg="kafka.javaapi" /> | ||||
|     <allow pkg="kafka.producer" /> | ||||
|  |  | |||
|  | @ -17,19 +17,14 @@ | |||
| package kafka.tools; | ||||
| 
 | ||||
| 
 | ||||
| import joptsimple.OptionException; | ||||
| import joptsimple.OptionParser; | ||||
| import joptsimple.OptionSet; | ||||
| import joptsimple.OptionSpec; | ||||
| import joptsimple.OptionSpecBuilder; | ||||
| import kafka.admin.AdminClient; | ||||
| import kafka.admin.TopicCommand; | ||||
| import kafka.utils.ZkUtils; | ||||
| import org.apache.kafka.clients.admin.AdminClient; | ||||
| import org.apache.kafka.clients.admin.DeleteTopicsResult; | ||||
| import org.apache.kafka.clients.admin.KafkaAdminClient; | ||||
| import org.apache.kafka.clients.consumer.ConsumerConfig; | ||||
| import org.apache.kafka.clients.consumer.KafkaConsumer; | ||||
| import org.apache.kafka.common.KafkaFuture; | ||||
| import org.apache.kafka.common.TopicPartition; | ||||
| import org.apache.kafka.common.annotation.InterfaceStability; | ||||
| import org.apache.kafka.common.security.JaasUtils; | ||||
| import org.apache.kafka.common.serialization.ByteArrayDeserializer; | ||||
| import org.apache.kafka.common.utils.Exit; | ||||
| 
 | ||||
|  | @ -38,8 +33,16 @@ import java.util.ArrayList; | |||
| import java.util.HashSet; | ||||
| import java.util.LinkedList; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
| import java.util.Properties; | ||||
| import java.util.Set; | ||||
| import java.util.concurrent.TimeUnit; | ||||
| 
 | ||||
| import joptsimple.OptionException; | ||||
| import joptsimple.OptionParser; | ||||
| import joptsimple.OptionSet; | ||||
| import joptsimple.OptionSpec; | ||||
| import joptsimple.OptionSpecBuilder; | ||||
| 
 | ||||
| /** | ||||
|  * {@link StreamsResetter} resets the processing state of a Kafka Streams application so that, for example, you can reprocess its input from scratch. | ||||
|  | @ -68,7 +71,7 @@ public class StreamsResetter { | |||
|     private static final int EXIT_CODE_ERROR = 1; | ||||
| 
 | ||||
|     private static OptionSpec<String> bootstrapServerOption; | ||||
|     private static OptionSpec<String> zookeeperOption; | ||||
|     private static OptionSpecBuilder zookeeperOption; | ||||
|     private static OptionSpec<String> applicationIdOption; | ||||
|     private static OptionSpec<String> inputTopicsOption; | ||||
|     private static OptionSpec<String> intermediateTopicsOption; | ||||
|  | @ -89,52 +92,57 @@ public class StreamsResetter { | |||
| 
 | ||||
|         int exitCode = EXIT_CODE_SUCCESS; | ||||
| 
 | ||||
|         AdminClient adminClient = null; | ||||
|         ZkUtils zkUtils = null; | ||||
|         KafkaAdminClient kafkaAdminClient = null; | ||||
| 
 | ||||
|         try { | ||||
|             parseArguments(args); | ||||
|             dryRun = options.has(dryRunOption); | ||||
| 
 | ||||
|             adminClient = AdminClient.createSimplePlaintext(options.valueOf(bootstrapServerOption)); | ||||
|             final String groupId = options.valueOf(applicationIdOption); | ||||
| 
 | ||||
|             validateNoActiveConsumers(groupId); | ||||
| 
 | ||||
|             zkUtils = ZkUtils.apply(options.valueOf(zookeeperOption), | ||||
|                 30000, | ||||
|                 30000, | ||||
|                 JaasUtils.isZkSecurityEnabled()); | ||||
|             final Properties adminClientProperties = new Properties(); | ||||
|             adminClientProperties.put("bootstrap.servers", options.valueOf(bootstrapServerOption)); | ||||
|             kafkaAdminClient = (KafkaAdminClient) AdminClient.create(adminClientProperties); | ||||
| 
 | ||||
|             allTopics.clear(); | ||||
|             allTopics.addAll(scala.collection.JavaConversions.seqAsJavaList(zkUtils.getAllTopics())); | ||||
| 
 | ||||
| 
 | ||||
|             if (!adminClient.describeConsumerGroup(groupId, 0).consumers().get().isEmpty()) { | ||||
|                 throw new IllegalStateException("Consumer group '" + groupId + "' is still active. " + | ||||
|                             "Make sure to stop all running application instances before running the reset tool."); | ||||
|             } | ||||
|             allTopics.addAll(kafkaAdminClient.listTopics().names().get(60, TimeUnit.SECONDS)); | ||||
| 
 | ||||
|             if (dryRun) { | ||||
|                 System.out.println("----Dry run displays the actions which will be performed when running Streams Reset Tool----"); | ||||
|             } | ||||
|             maybeResetInputAndSeekToEndIntermediateTopicOffsets(); | ||||
|             maybeDeleteInternalTopics(zkUtils); | ||||
|             maybeDeleteInternalTopics(kafkaAdminClient); | ||||
| 
 | ||||
|         } catch (final Throwable e) { | ||||
|             exitCode = EXIT_CODE_ERROR; | ||||
|             System.err.println("ERROR: " + e); | ||||
|             e.printStackTrace(System.err); | ||||
|         } finally { | ||||
|             if (adminClient != null) { | ||||
|                 adminClient.close(); | ||||
|             } | ||||
|             if (zkUtils != null) { | ||||
|                 zkUtils.close(); | ||||
|             if (kafkaAdminClient != null) { | ||||
|                 kafkaAdminClient.close(60, TimeUnit.SECONDS); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return exitCode; | ||||
|     } | ||||
| 
 | ||||
|     private void validateNoActiveConsumers(final String groupId) { | ||||
|         kafka.admin.AdminClient olderAdminClient = null; | ||||
|         try { | ||||
|             olderAdminClient = kafka.admin.AdminClient.createSimplePlaintext(options.valueOf(bootstrapServerOption)); | ||||
|             if (!olderAdminClient.describeConsumerGroup(groupId, 0).consumers().get().isEmpty()) { | ||||
|                 throw new IllegalStateException("Consumer group '" + groupId + "' is still active. " | ||||
|                                                 + "Make sure to stop all running application instances before running the reset tool."); | ||||
|             } | ||||
|         } finally { | ||||
|             if (olderAdminClient != null) { | ||||
|                 olderAdminClient.close(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private void parseArguments(final String[] args) throws IOException { | ||||
| 
 | ||||
|         final OptionParser optionParser = new OptionParser(false); | ||||
|  | @ -148,11 +156,8 @@ public class StreamsResetter { | |||
|             .ofType(String.class) | ||||
|             .defaultsTo("localhost:9092") | ||||
|             .describedAs("urls"); | ||||
|         zookeeperOption = optionParser.accepts("zookeeper", "Zookeeper url with format: HOST:POST") | ||||
|             .withRequiredArg() | ||||
|             .ofType(String.class) | ||||
|             .defaultsTo("localhost:2181") | ||||
|             .describedAs("url"); | ||||
|         zookeeperOption = optionParser.accepts("zookeeper", "Zookeeper option is deprecated by bootstrap.servers, as the reset tool would no longer access Zookeeper directly."); | ||||
| 
 | ||||
|         inputTopicsOption = optionParser.accepts("input-topics", "Comma-separated list of user input topics. For these topics, the tool will reset the offset to the earliest available offset.") | ||||
|             .withRequiredArg() | ||||
|             .ofType(String.class) | ||||
|  | @ -314,30 +319,46 @@ public class StreamsResetter { | |||
|         return options.valuesOf(intermediateTopicsOption).contains(topic); | ||||
|     } | ||||
| 
 | ||||
|     private void maybeDeleteInternalTopics(final ZkUtils zkUtils) { | ||||
|     private void maybeDeleteInternalTopics(final KafkaAdminClient adminClient) { | ||||
| 
 | ||||
|         System.out.println("Deleting all internal/auto-created topics for application " + options.valueOf(applicationIdOption)); | ||||
| 
 | ||||
|         for (final String topic : allTopics) { | ||||
|             if (isInternalTopic(topic)) { | ||||
|                 try { | ||||
|                     if (!dryRun) { | ||||
|                         final TopicCommand.TopicCommandOptions commandOptions = new TopicCommand.TopicCommandOptions(new String[]{ | ||||
|                             "--zookeeper", options.valueOf(zookeeperOption), | ||||
|                             "--delete", "--topic", topic}); | ||||
|                         TopicCommand.deleteTopic(zkUtils, commandOptions); | ||||
|                     } else { | ||||
|                         System.out.println("Topic: " + topic); | ||||
|                     } | ||||
|                 } catch (final RuntimeException e) { | ||||
|                     System.err.println("ERROR: Deleting topic " + topic + " failed."); | ||||
|                     throw e; | ||||
|         List<String> topicsToDelete = new ArrayList<>(); | ||||
|         for (final String listing : allTopics) { | ||||
|             if (isInternalTopic(listing)) { | ||||
|                 if (!dryRun) { | ||||
|                     topicsToDelete.add(listing); | ||||
|                 } else { | ||||
|                     System.out.println("Topic: " + listing); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         if (!dryRun) { | ||||
|             doDelete(topicsToDelete, adminClient); | ||||
|         } | ||||
|         System.out.println("Done."); | ||||
|     } | ||||
| 
 | ||||
|     private void doDelete(final List<String> topicsToDelete, | ||||
|                           final KafkaAdminClient adminClient) { | ||||
|         boolean hasDeleteErrors = false; | ||||
|         final DeleteTopicsResult deleteTopicsResult = adminClient.deleteTopics(topicsToDelete); | ||||
|         final Map<String, KafkaFuture<Void>> results = deleteTopicsResult.values(); | ||||
| 
 | ||||
|         for (final Map.Entry<String, KafkaFuture<Void>> entry : results.entrySet()) { | ||||
|             try { | ||||
|                 entry.getValue().get(30, TimeUnit.SECONDS); | ||||
|             } catch (Exception e) { | ||||
|                 System.err.println("ERROR: deleting topic " + entry.getKey()); | ||||
|                 e.printStackTrace(System.err); | ||||
|                 hasDeleteErrors = true; | ||||
|             } | ||||
|         } | ||||
|         if (hasDeleteErrors) { | ||||
|             throw new RuntimeException("Encountered an error deleting one or more topics"); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     private boolean isInternalTopic(final String topicName) { | ||||
|         return topicName.startsWith(options.valueOf(applicationIdOption) + "-") | ||||
|             && (topicName.endsWith("-changelog") || topicName.endsWith("-repartition")); | ||||
|  |  | |||
|  | @ -16,19 +16,14 @@ | |||
|  */ | ||||
| package org.apache.kafka.streams.integration; | ||||
| 
 | ||||
| import kafka.admin.AdminClient; | ||||
| import kafka.server.KafkaConfig$; | ||||
| import kafka.tools.StreamsResetter; | ||||
| import kafka.utils.MockTime; | ||||
| import kafka.utils.ZkUtils; | ||||
| import org.apache.kafka.clients.admin.KafkaAdminClient; | ||||
| import org.apache.kafka.clients.admin.ListTopicsOptions; | ||||
| import org.apache.kafka.clients.consumer.ConsumerConfig; | ||||
| import org.apache.kafka.common.errors.TimeoutException; | ||||
| import org.apache.kafka.common.security.JaasUtils; | ||||
| import org.apache.kafka.common.serialization.LongDeserializer; | ||||
| import org.apache.kafka.common.serialization.LongSerializer; | ||||
| import org.apache.kafka.common.serialization.Serdes; | ||||
| import org.apache.kafka.common.serialization.StringSerializer; | ||||
| import org.apache.kafka.common.utils.Utils; | ||||
| import org.apache.kafka.streams.KafkaStreams; | ||||
| import org.apache.kafka.streams.KeyValue; | ||||
| import org.apache.kafka.streams.StreamsBuilder; | ||||
|  | @ -56,6 +51,12 @@ import java.util.HashSet; | |||
| import java.util.List; | ||||
| import java.util.Properties; | ||||
| import java.util.Set; | ||||
| import java.util.concurrent.TimeUnit; | ||||
| 
 | ||||
| import kafka.admin.AdminClient; | ||||
| import kafka.server.KafkaConfig$; | ||||
| import kafka.tools.StreamsResetter; | ||||
| import kafka.utils.MockTime; | ||||
| 
 | ||||
| import static org.hamcrest.CoreMatchers.equalTo; | ||||
| import static org.hamcrest.MatcherAssert.assertThat; | ||||
|  | @ -94,6 +95,7 @@ public class ResetIntegrationTest { | |||
| 
 | ||||
|     private static int testNo = 0; | ||||
|     private static AdminClient adminClient = null; | ||||
|     private static KafkaAdminClient kafkaAdminClient = null; | ||||
| 
 | ||||
|     private final MockTime mockTime = CLUSTER.time; | ||||
|     private final WaitUntilConsumerGroupGotClosed consumerGroupInactive = new WaitUntilConsumerGroupGotClosed(); | ||||
|  | @ -104,6 +106,11 @@ public class ResetIntegrationTest { | |||
|             adminClient.close(); | ||||
|             adminClient = null; | ||||
|         } | ||||
| 
 | ||||
|         if (kafkaAdminClient != null) { | ||||
|             kafkaAdminClient.close(10, TimeUnit.SECONDS); | ||||
|             kafkaAdminClient = null; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Before | ||||
|  | @ -114,6 +121,12 @@ public class ResetIntegrationTest { | |||
|             adminClient = AdminClient.createSimplePlaintext(CLUSTER.bootstrapServers()); | ||||
|         } | ||||
| 
 | ||||
|         if (kafkaAdminClient == null) { | ||||
|             Properties props = new Properties(); | ||||
|             props.put("bootstrap.servers", CLUSTER.bootstrapServers()); | ||||
|             kafkaAdminClient =  (KafkaAdminClient) org.apache.kafka.clients.admin.AdminClient.create(props); | ||||
|         } | ||||
| 
 | ||||
|         // busy wait until cluster (ie, ConsumerGroupCoordinator) is available | ||||
|         while (true) { | ||||
|             Thread.sleep(50); | ||||
|  | @ -338,20 +351,20 @@ public class ResetIntegrationTest { | |||
|     } | ||||
| 
 | ||||
|     private void cleanGlobal(final String intermediateUserTopic) { | ||||
|         // leaving --zookeeper arg here to ensure tool works if users add it | ||||
|         final String[] parameters; | ||||
|         if (intermediateUserTopic != null) { | ||||
|             parameters = new String[]{ | ||||
|                 "--application-id", APP_ID + testNo, | ||||
|                 "--bootstrap-servers", CLUSTER.bootstrapServers(), | ||||
|                 "--zookeeper", CLUSTER.zKConnectString(), | ||||
|                 "--input-topics", INPUT_TOPIC, | ||||
|                 "--intermediate-topics", INTERMEDIATE_USER_TOPIC | ||||
|                 "--intermediate-topics", INTERMEDIATE_USER_TOPIC, | ||||
|                 "--zookeeper", "localhost:2181" | ||||
|             }; | ||||
|         } else { | ||||
|             parameters = new String[]{ | ||||
|                 "--application-id", APP_ID + testNo, | ||||
|                 "--bootstrap-servers", CLUSTER.bootstrapServers(), | ||||
|                 "--zookeeper", CLUSTER.zKConnectString(), | ||||
|                 "--input-topics", INPUT_TOPIC | ||||
|             }; | ||||
|         } | ||||
|  | @ -363,7 +376,7 @@ public class ResetIntegrationTest { | |||
|         Assert.assertEquals(0, exitCode); | ||||
|     } | ||||
| 
 | ||||
|     private void assertInternalTopicsGotDeleted(final String intermediateUserTopic) { | ||||
|     private void assertInternalTopicsGotDeleted(final String intermediateUserTopic) throws Exception { | ||||
|         final Set<String> expectedRemainingTopicsAfterCleanup = new HashSet<>(); | ||||
|         expectedRemainingTopicsAfterCleanup.add(INPUT_TOPIC); | ||||
|         if (intermediateUserTopic != null) { | ||||
|  | @ -374,25 +387,13 @@ public class ResetIntegrationTest { | |||
|         expectedRemainingTopicsAfterCleanup.add(OUTPUT_TOPIC_2_RERUN); | ||||
|         expectedRemainingTopicsAfterCleanup.add("__consumer_offsets"); | ||||
| 
 | ||||
|         Set<String> allTopics; | ||||
|         ZkUtils zkUtils = null; | ||||
|         try { | ||||
|             zkUtils = ZkUtils.apply(CLUSTER.zKConnectString(), | ||||
|                 30000, | ||||
|                 30000, | ||||
|                 JaasUtils.isZkSecurityEnabled()); | ||||
|         final Set<String> allTopics = new HashSet<>(); | ||||
| 
 | ||||
|             do { | ||||
|                 Utils.sleep(100); | ||||
|                 allTopics = new HashSet<>(); | ||||
|                 allTopics.addAll(scala.collection.JavaConversions.seqAsJavaList(zkUtils.getAllTopics())); | ||||
|             } while (allTopics.size() != expectedRemainingTopicsAfterCleanup.size()); | ||||
|         } finally { | ||||
|             if (zkUtils != null) { | ||||
|                 zkUtils.close(); | ||||
|             } | ||||
|         } | ||||
|         final ListTopicsOptions listTopicsOptions = new ListTopicsOptions(); | ||||
|         listTopicsOptions.listInternal(true); | ||||
|         allTopics.addAll(kafkaAdminClient.listTopics(listTopicsOptions).names().get(30000, TimeUnit.MILLISECONDS)); | ||||
|         assertThat(allTopics, equalTo(expectedRemainingTopicsAfterCleanup)); | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     private class WaitUntilConsumerGroupGotClosed implements TestCondition { | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue