From 405a04d958b1e7a8c997610d761e94c52b782c66 Mon Sep 17 00:00:00 2001 From: Norbert Hu Date: Tue, 10 Feb 2015 21:32:45 -0800 Subject: [PATCH 01/14] Create debian package for 0.8.1 --- debian/changelog | 5 +++++ debian/compat | 1 + debian/control | 14 ++++++++++++++ debian/copyright | 35 +++++++++++++++++++++++++++++++++++ debian/docs | 4 ++++ debian/rules | 40 ++++++++++++++++++++++++++++++++++++++++ debian/source/format | 1 + 7 files changed, 100 insertions(+) create mode 100644 debian/changelog create mode 100644 debian/compat create mode 100644 debian/control create mode 100644 debian/copyright create mode 100644 debian/docs create mode 100755 debian/rules create mode 100644 debian/source/format diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000000000..d93aa3d28228d --- /dev/null +++ b/debian/changelog @@ -0,0 +1,5 @@ +kafka-0.8.1 (0) unstable; urgency=low + + * Create debian package + + -- Norbert Hu Fri, 10 Feb 2015 00:00:00 +0000 diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000000000..45a4fb75db864 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +8 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000000000..a0af28485d403 --- /dev/null +++ b/debian/control @@ -0,0 +1,14 @@ +Source: kafka-0.8.1 +Section: database +Priority: extra +Maintainer: Norbert Hu +Build-Depends: debhelper (>= 8.0.0), openjdk-7-jdk, curl +Standards-Version: 3.9.3 +Homepage: http://kafka.apache.org/ + +Package: kafka-0.8.1 +Architecture: all +Depends: openjdk-7-jre-headless +Description: Distributed, partitioned, replicated commit log service. + Kafka is a distributed, partitioned, replicated commit log service. + It provides the functionality of a messaging system, but with a unique design. diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000000000..920d37f024ffc --- /dev/null +++ b/debian/copyright @@ -0,0 +1,35 @@ +Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: kafka +Source: http://kafka.apache.org/downloads.html + +Files: * +Copyright: 2013 Basho Technologies, Inc +License: Apache License, Version 2.0 + 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. + +Files: debian/* +Copyright: 2014 Aleksey Morarash +License: GPL-2+ + This package is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + . + This package is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + . + You should have received a copy of the GNU General Public License + along with this program. If not, see + . + On Debian systems, the complete text of the GNU General + Public License version 2 can be found in "/usr/share/common-licenses/GPL-2". diff --git a/debian/docs b/debian/docs new file mode 100644 index 0000000000000..86b1f713d483d --- /dev/null +++ b/debian/docs @@ -0,0 +1,4 @@ +HEADER +LICENSE +README.md +NOTICE diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000000000..75950510da046 --- /dev/null +++ b/debian/rules @@ -0,0 +1,40 @@ +#!/usr/bin/make -f + +%: + dh $@ + +SLF4J_VERSION = 1.7.7 +SLF4J = slf4j-$(SLF4J_VERSION) + +override_dh_auto_build: + ./gradlew jar + +# Do not install init script automatically +override_dh_installinit: + +DESTDIR = debian/kafka-0.8.1 +override_dh_auto_install: + install -m 755 -d $(DESTDIR)/etc/kafka + install -m 644 config/*.properties $(DESTDIR)/etc/kafka + install -m 755 -d $(DESTDIR)/usr/lib/kafka + for i in `ls | grep -vE config\|debian\|perf\|examples\|gradle\|system_test`; do \ + cp -r $$i $(DESTDIR)/usr/lib/kafka || exit $$?; \ + done + find $(DESTDIR)/usr/lib/kafka -type f -a \ + \( -name \*.java -o -name \*.class -o \ + -name \*.scala -o -name \*.gradle -o -name \*.MF -o -name \*.html \) \ + -print -delete + for i in `seq 10`; do \ + find $(DESTDIR) -type d -empty -print -exec rmdir '{}' ';' || :; \ + done + find $(DESTDIR)/usr/lib/kafka -type f -a \ + \( -name README\* -o -name LICENSE -o -name NOTICE -o -name HEADER \) \ + -print -delete || : + find $(DESTDIR)/usr/lib/kafka -type d -a \ + \( -name test -o -name src -o -name tmp \) \ + -print -exec rm -rf '{}' ';' || : + ln -s /etc/kafka $(DESTDIR)/usr/lib/kafka/config + ln -s /var/log/kafka $(DESTDIR)/usr/lib/kafka/logs + sed -i 's#/tmp/zookeeper#/var/lib/kafka/zookeeper#' $(DESTDIR)/etc/kafka/zookeeper.properties + sed -i 's#/tmp/kafka-logs#/var/lib/kafka/logs#' $(DESTDIR)/etc/kafka/server.properties + install -m 755 -d $(DESTDIR)/var/lib/kafka $(DESTDIR)/var/log/kafka diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 0000000000000..d3827e75a5cad --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +1.0 From 647fac5824d6e1e5b644770505d6e6f8bc9e5d61 Mon Sep 17 00:00:00 2001 From: Norbert Hu Date: Tue, 10 Feb 2015 21:38:19 -0800 Subject: [PATCH 02/14] Use scala 2.8.0 --- debian/rules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/rules b/debian/rules index 75950510da046..fb8774a5550c7 100755 --- a/debian/rules +++ b/debian/rules @@ -7,7 +7,7 @@ SLF4J_VERSION = 1.7.7 SLF4J = slf4j-$(SLF4J_VERSION) override_dh_auto_build: - ./gradlew jar + ./gradlew -PscalaVersion=2.8.0 jar # Do not install init script automatically override_dh_installinit: From a2767a3e12350351bdd45c88e420936c1d2e4ed9 Mon Sep 17 00:00:00 2001 From: Norbert Hu Date: Wed, 11 Feb 2015 11:03:56 -0800 Subject: [PATCH 03/14] Install 0.8.1 debian package as "leaf" --- debian/rules | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/debian/rules b/debian/rules index fb8774a5550c7..a3e6991929f29 100755 --- a/debian/rules +++ b/debian/rules @@ -7,6 +7,9 @@ SLF4J_VERSION = 1.7.7 SLF4J = slf4j-$(SLF4J_VERSION) override_dh_auto_build: + # Build with scala 2.8.0 so that it matches with the scala version used to build our + # existing kafka 0.7.0 for our kafka7 deployment. The reason why we need this specific + # build is to run the migration tool. ./gradlew -PscalaVersion=2.8.0 jar # Do not install init script automatically @@ -14,27 +17,27 @@ override_dh_installinit: DESTDIR = debian/kafka-0.8.1 override_dh_auto_install: - install -m 755 -d $(DESTDIR)/etc/kafka - install -m 644 config/*.properties $(DESTDIR)/etc/kafka - install -m 755 -d $(DESTDIR)/usr/lib/kafka + install -m 755 -d $(DESTDIR)/etc/kafka-leaf + install -m 644 config/*.properties $(DESTDIR)/etc/kafka-leaf + install -m 755 -d $(DESTDIR)/usr/lib/kafka-leaf for i in `ls | grep -vE config\|debian\|perf\|examples\|gradle\|system_test`; do \ - cp -r $$i $(DESTDIR)/usr/lib/kafka || exit $$?; \ + cp -r $$i $(DESTDIR)/usr/lib/kafka-leaf || exit $$?; \ done - find $(DESTDIR)/usr/lib/kafka -type f -a \ + find $(DESTDIR)/usr/lib/kafka-leaf -type f -a \ \( -name \*.java -o -name \*.class -o \ -name \*.scala -o -name \*.gradle -o -name \*.MF -o -name \*.html \) \ -print -delete for i in `seq 10`; do \ find $(DESTDIR) -type d -empty -print -exec rmdir '{}' ';' || :; \ done - find $(DESTDIR)/usr/lib/kafka -type f -a \ + find $(DESTDIR)/usr/lib/kafka-leaf -type f -a \ \( -name README\* -o -name LICENSE -o -name NOTICE -o -name HEADER \) \ -print -delete || : - find $(DESTDIR)/usr/lib/kafka -type d -a \ + find $(DESTDIR)/usr/lib/kafka-leaf -type d -a \ \( -name test -o -name src -o -name tmp \) \ -print -exec rm -rf '{}' ';' || : - ln -s /etc/kafka $(DESTDIR)/usr/lib/kafka/config - ln -s /var/log/kafka $(DESTDIR)/usr/lib/kafka/logs - sed -i 's#/tmp/zookeeper#/var/lib/kafka/zookeeper#' $(DESTDIR)/etc/kafka/zookeeper.properties - sed -i 's#/tmp/kafka-logs#/var/lib/kafka/logs#' $(DESTDIR)/etc/kafka/server.properties - install -m 755 -d $(DESTDIR)/var/lib/kafka $(DESTDIR)/var/log/kafka + ln -s /etc/kafka-leaf $(DESTDIR)/usr/lib/kafka-leaf/config + ln -s /var/log/kafka-leaf $(DESTDIR)/usr/lib/kafka-leaf/logs + sed -i 's#/tmp/zookeeper#/var/lib/kafka-leaf/zookeeper#' $(DESTDIR)/etc/kafka-leaf/zookeeper.properties + sed -i 's#/tmp/kafka-logs#/var/lib/kafka-leaf/logs#' $(DESTDIR)/etc/kafka-leaf/server.properties + install -m 755 -d $(DESTDIR)/var/lib/kafka-leaf $(DESTDIR)/var/log/kafka-leaf From 0c66bb6d60d7f6a9db8f7866149348766a9e9f7c Mon Sep 17 00:00:00 2001 From: Norbert Hu Date: Sat, 14 Feb 2015 19:40:51 -0800 Subject: [PATCH 04/14] Add jmxtrans-agent to debian package This makes exporting jmx metrics via the deployed debian package much easier. See https://github.com/jmxtrans/jmxtrans-agent NOTE: the inclusion of the jmxtrans-agent jar is mostly for the migration tool as well as the mirror maker, which doesn't expose an easy way to export metrics to graphite --- build.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build.gradle b/build.gradle index 7ff670e5873d5..2f7ef3474cb06 100644 --- a/build.gradle +++ b/build.gradle @@ -218,6 +218,8 @@ project(':core') { compile 'net.sf.jopt-simple:jopt-simple:3.2' compile 'org.xerial.snappy:snappy-java:1.0.5' + runtime 'org.jmxtrans.agent:jmxtrans-agent:1.0.8' + testCompile 'junit:junit:4.1' testCompile 'org.easymock:easymock:3.0' testCompile 'org.objenesis:objenesis:1.2' From 505e2d01d96513217c914f34fa0021c64d09d258 Mon Sep 17 00:00:00 2001 From: Norbert Hu Date: Wed, 11 Mar 2015 17:15:17 -0700 Subject: [PATCH 05/14] Kafka migrator tool is buggy with JDK7 See {T63657} --- debian/control | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/debian/control b/debian/control index a0af28485d403..662a73ae32fcd 100644 --- a/debian/control +++ b/debian/control @@ -2,13 +2,13 @@ Source: kafka-0.8.1 Section: database Priority: extra Maintainer: Norbert Hu -Build-Depends: debhelper (>= 8.0.0), openjdk-7-jdk, curl +Build-Depends: debhelper (>= 8.0.0), openjdk-6-jdk, curl Standards-Version: 3.9.3 Homepage: http://kafka.apache.org/ Package: kafka-0.8.1 Architecture: all -Depends: openjdk-7-jre-headless +Depends: openjdk-6-jre-headless Description: Distributed, partitioned, replicated commit log service. Kafka is a distributed, partitioned, replicated commit log service. It provides the functionality of a messaging system, but with a unique design. From b45239268609b761e07d994764165307466cf470 Mon Sep 17 00:00:00 2001 From: Norbert Hu Date: Fri, 13 Mar 2015 23:42:12 -0700 Subject: [PATCH 06/14] Revert "Kafka migrator tool is buggy with JDK7" This reverts commit 505e2d01d96513217c914f34fa0021c64d09d258. --- debian/control | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/debian/control b/debian/control index 662a73ae32fcd..a0af28485d403 100644 --- a/debian/control +++ b/debian/control @@ -2,13 +2,13 @@ Source: kafka-0.8.1 Section: database Priority: extra Maintainer: Norbert Hu -Build-Depends: debhelper (>= 8.0.0), openjdk-6-jdk, curl +Build-Depends: debhelper (>= 8.0.0), openjdk-7-jdk, curl Standards-Version: 3.9.3 Homepage: http://kafka.apache.org/ Package: kafka-0.8.1 Architecture: all -Depends: openjdk-6-jre-headless +Depends: openjdk-7-jre-headless Description: Distributed, partitioned, replicated commit log service. Kafka is a distributed, partitioned, replicated commit log service. It provides the functionality of a messaging system, but with a unique design. From b96e540a5602adf0b85e83b843d8f850c046bbe5 Mon Sep 17 00:00:00 2001 From: Norbert Hu Date: Wed, 18 Mar 2015 13:07:21 -0700 Subject: [PATCH 07/14] Add arcanist config --- .arcconfig | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .arcconfig diff --git a/.arcconfig b/.arcconfig new file mode 100644 index 0000000000000..b650a7cd84e8f --- /dev/null +++ b/.arcconfig @@ -0,0 +1,5 @@ +{ + "project_id": "kafka-0.8.1", + "conduit_uri": "https://code.uberinternal.com/", + "git.default-relative-commit": "origin/0.8.1" +} From d2d137d320e4776b3a9dbde4b630a4ebc0d5b331 Mon Sep 17 00:00:00 2001 From: Norbert Hu Date: Wed, 18 Mar 2015 14:05:04 -0700 Subject: [PATCH 08/14] Add arc land branch --- .arcconfig | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.arcconfig b/.arcconfig index b650a7cd84e8f..8903ba67eac5d 100644 --- a/.arcconfig +++ b/.arcconfig @@ -1,5 +1,6 @@ { "project_id": "kafka-0.8.1", "conduit_uri": "https://code.uberinternal.com/", - "git.default-relative-commit": "origin/0.8.1" + "git.default-relative-commit": "origin/0.8.1", + "arc.land.onto.default": "0.8.1" } From 998bc66fa3a368b37aa2e31f5e167b4cfe9c62b1 Mon Sep 17 00:00:00 2001 From: Norbert Hu Date: Wed, 18 Mar 2015 14:12:01 -0700 Subject: [PATCH 09/14] Update debian comment Reviewers: grk Reviewed By: grk Differential Revision: https://code.uberinternal.com/D83314 --- debian/rules | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/debian/rules b/debian/rules index a3e6991929f29..3f5872c732be6 100755 --- a/debian/rules +++ b/debian/rules @@ -7,8 +7,8 @@ SLF4J_VERSION = 1.7.7 SLF4J = slf4j-$(SLF4J_VERSION) override_dh_auto_build: - # Build with scala 2.8.0 so that it matches with the scala version used to build our - # existing kafka 0.7.0 for our kafka7 deployment. The reason why we need this specific + # Build with scala 2.8.0 so that it matches with the scala version currently to build + # our existing kafka 0.7.0 for our kafka7 deployment. The reason why we need this specific # build is to run the migration tool. ./gradlew -PscalaVersion=2.8.0 jar From b65536b94705b0b890eb510df5610ab0e05df7e9 Mon Sep 17 00:00:00 2001 From: Seung-Yeoul Yang Date: Wed, 18 Mar 2015 17:57:10 -0700 Subject: [PATCH 10/14] [kafka] better error recovery in migrator tool (part 1) Summary: * Add a new gradle project for the migrator tool * Update .gitignore * KafkaMigrationTool is just copy-pasted. Test Plan: ./gradlew test Reviewers: grk, praveen, vinoth, csoman, norbert Reviewed By: norbert Subscribers: bigpunk, nharkins Maniphest Tasks: T67641 Differential Revision: https://code.uberinternal.com/D83463 --- .gitignore | 24 + build.gradle | 42 +- .../uber/kafka/tools/KafkaMigrationTool.java | 473 ++++++++++++++++++ .../kafka/tools/KafkaMigrationToolTest.java | 15 + migration/src/test/resources/offset_idx | Bin 0 -> 181032 bytes settings.gradle | 2 +- 6 files changed, 551 insertions(+), 5 deletions(-) create mode 100644 migration/src/main/java/com/uber/kafka/tools/KafkaMigrationTool.java create mode 100644 migration/src/test/java/com/uber/kafka/tools/KafkaMigrationToolTest.java create mode 100644 migration/src/test/resources/offset_idx diff --git a/.gitignore b/.gitignore index 553a077d031a3..db830e032cf6c 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,27 @@ project/sbt_project_definition.iml .#* rat.out TAGS + +# Directories # +build/ +core/data + +# OS Files # +.DS_Store + +# Package Files # +*.jar +*.war +*.ear +*.db + +# Intellji Files # +.out +.idea +*.ipr +*.iws +*.iml +*.MF + +# Gradle # +.gradle diff --git a/build.gradle b/build.gradle index 2f7ef3474cb06..3689a4f0f3e44 100644 --- a/build.gradle +++ b/build.gradle @@ -168,12 +168,12 @@ for ( sv in ['2_8_0', '2_9_1', '2_9_2', '2_10_1'] ) { } } -tasks.create(name: "jarAll", dependsOn: ['jar_core_2_8_0', 'jar_core_2_9_1', 'jar_core_2_9_2', 'jar_core_2_10_1', 'clients:jar', 'perf:jar', 'examples:jar', 'contrib:hadoop-consumer:jar', 'contrib:hadoop-producer:jar']) { +tasks.create(name: "jarAll", dependsOn: ['jar_core_2_8_0', 'jar_core_2_9_1', 'jar_core_2_9_2', 'jar_core_2_10_1', 'clients:jar', 'perf:jar', 'migration:jar', 'examples:jar', 'contrib:hadoop-consumer:jar', 'contrib:hadoop-producer:jar']) { } -tasks.create(name: "srcJarAll", dependsOn: ['srcJar_2_8_0', 'srcJar_2_9_1', 'srcJar_2_9_2', 'srcJar_2_10_1', 'clients:srcJar', 'perf:srcJar', 'examples:srcJar', 'contrib:hadoop-consumer:srcJar', 'contrib:hadoop-producer:srcJar']) { } +tasks.create(name: "srcJarAll", dependsOn: ['srcJar_2_8_0', 'srcJar_2_9_1', 'srcJar_2_9_2', 'srcJar_2_10_1', 'clients:srcJar', 'perf:srcJar', 'migration:srcJar', 'examples:srcJar', 'contrib:hadoop-consumer:srcJar', 'contrib:hadoop-producer:srcJar']) { } -tasks.create(name: "docsJarAll", dependsOn: ['docsJar_2_8_0', 'docsJar_2_9_1', 'docsJar_2_9_2', 'docsJar_2_10_1', 'clients:docsJar', 'perf:docsJar', 'examples:docsJar', 'contrib:hadoop-consumer:docsJar', 'contrib:hadoop-producer:docsJar']) { } +tasks.create(name: "docsJarAll", dependsOn: ['docsJar_2_8_0', 'docsJar_2_9_1', 'docsJar_2_9_2', 'docsJar_2_10_1', 'clients:docsJar', 'perf:docsJar', 'migration:docsJar', 'examples:docsJar', 'contrib:hadoop-consumer:docsJar', 'contrib:hadoop-producer:docsJar']) { } tasks.create(name: "testAll", dependsOn: ['test_core_2_8_0', 'test_core_2_9_1', 'test_core_2_9_2', 'test_core_2_10_1', 'clients:test']) { } @@ -181,7 +181,7 @@ tasks.create(name: "testAll", dependsOn: ['test_core_2_8_0', 'test_core_2_9_1', tasks.create(name: "releaseTarGzAll", dependsOn: ['releaseTarGz_2_8_0', 'releaseTarGz_2_9_1', 'releaseTarGz_2_9_2', 'releaseTarGz_2_10_1']) { } -tasks.create(name: "uploadArchivesAll", dependsOn: ['uploadCoreArchives_2_8_0', 'uploadCoreArchives_2_9_1', 'uploadCoreArchives_2_9_2', 'uploadCoreArchives_2_10_1', 'perf:uploadArchives', 'examples:uploadArchives', 'contrib:hadoop-consumer:uploadArchives', 'contrib:hadoop-producer:uploadArchives']) { +tasks.create(name: "uploadArchivesAll", dependsOn: ['uploadCoreArchives_2_8_0', 'uploadCoreArchives_2_9_1', 'uploadCoreArchives_2_9_2', 'uploadCoreArchives_2_10_1', 'perf:uploadArchives', 'migration:uploadArchives', 'examples:uploadArchives', 'contrib:hadoop-consumer:uploadArchives', 'contrib:hadoop-producer:uploadArchives']) { } project(':core') { @@ -296,6 +296,40 @@ project(':perf') { } } +project(':migration') { + archivesBaseName = "kafka-migration" + + dependencies { + compile project(':core') + compile 'com.google.guava:guava:18.0' + + testCompile 'junit:junit:4.1' + + test { + testLogging { + events "passed", "skipped", "failed" + exceptionFormat = 'full' + } + } + + sourceSets { + test { + output.resourcesDir = "build/classes/test" + } + } + } + + configurations { + // manually excludes some unnecessary dependencies + compile.exclude module: 'javax' + compile.exclude module: 'jms' + compile.exclude module: 'jmxri' + compile.exclude module: 'jmxtools' + compile.exclude module: 'mail' + compile.exclude module: 'netty' + } +} + project(':contrib:hadoop-consumer') { archivesBaseName = "kafka-hadoop-consumer" diff --git a/migration/src/main/java/com/uber/kafka/tools/KafkaMigrationTool.java b/migration/src/main/java/com/uber/kafka/tools/KafkaMigrationTool.java new file mode 100644 index 0000000000000..d60cc2952dc55 --- /dev/null +++ b/migration/src/main/java/com/uber/kafka/tools/KafkaMigrationTool.java @@ -0,0 +1,473 @@ +package com.uber.kafka.tools; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; + +import joptsimple.ArgumentAcceptingOptionSpec; +import joptsimple.OptionParser; +import joptsimple.OptionSet; +import joptsimple.OptionSpec; +import joptsimple.OptionSpecBuilder; +import kafka.javaapi.producer.Producer; +import kafka.producer.KeyedMessage; +import kafka.producer.ProducerConfig; +import kafka.utils.Utils; + + +/** + * This is a kafka 0.7 to 0.8 online migration tool used for migrating data from 0.7 to 0.8 cluster. Internally, + * it's composed of a kafka 0.7 consumer and kafka 0.8 producer. The kafka 0.7 consumer consumes data from the + * 0.7 cluster, and the kafka 0.8 producer produces data to the 0.8 cluster. + * + * The 0.7 consumer is loaded from kafka 0.7 jar using a "parent last, child first" java class loader. + * Ordinary class loader is "parent first, child last", and kafka 0.8 and 0.7 both have classes for a lot of + * class names like "kafka.consumer.Consumer", etc., so ordinary java URLClassLoader with kafka 0.7 jar will + * will still load the 0.8 version class. + * + * As kafka 0.7 and kafka 0.8 used different version of zkClient, the zkClient jar used by kafka 0.7 should + * also be used by the class loader. + * + * The user need to provide the configuration file for 0.7 consumer and 0.8 producer. For 0.8 producer, + * the "serializer.class" config is set to "kafka.serializer.DefaultEncoder" by the code. + */ +@SuppressWarnings({"unchecked", "rawtypes"}) +public class KafkaMigrationTool +{ + private static final org.apache.log4j.Logger logger = org.apache.log4j.Logger.getLogger(KafkaMigrationTool.class.getName()); + private static final String KAFKA_07_STATIC_CONSUMER_CLASS_NAME = "kafka.consumer.Consumer"; + private static final String KAFKA_07_CONSUMER_CONFIG_CLASS_NAME = "kafka.consumer.ConsumerConfig"; + private static final String KAFKA_07_CONSUMER_STREAM_CLASS_NAME = "kafka.consumer.KafkaStream"; + private static final String KAFKA_07_CONSUMER_ITERATOR_CLASS_NAME = "kafka.consumer.ConsumerIterator"; + private static final String KAFKA_07_CONSUMER_CONNECTOR_CLASS_NAME = "kafka.javaapi.consumer.ConsumerConnector"; + private static final String KAFKA_07_MESSAGE_AND_METADATA_CLASS_NAME = "kafka.message.MessageAndMetadata"; + private static final String KAFKA_07_MESSAGE_CLASS_NAME = "kafka.message.Message"; + private static final String KAFKA_07_WHITE_LIST_CLASS_NAME = "kafka.consumer.Whitelist"; + private static final String KAFKA_07_TOPIC_FILTER_CLASS_NAME = "kafka.consumer.TopicFilter"; + private static final String KAFKA_07_BLACK_LIST_CLASS_NAME = "kafka.consumer.Blacklist"; + + private static Class KafkaStaticConsumer_07 = null; + private static Class ConsumerConfig_07 = null; + private static Class ConsumerConnector_07 = null; + private static Class KafkaStream_07 = null; + private static Class TopicFilter_07 = null; + private static Class WhiteList_07 = null; + private static Class BlackList_07 = null; + private static Class KafkaConsumerIteratorClass_07 = null; + private static Class KafkaMessageAndMetatDataClass_07 = null; + private static Class KafkaMessageClass_07 = null; + + public static void main(String[] args) throws InterruptedException, IOException { + OptionParser parser = new OptionParser(); + ArgumentAcceptingOptionSpec consumerConfigOpt + = parser.accepts("consumer.config", "Kafka 0.7 consumer config to consume from the source 0.7 cluster. " + "You man specify multiple of these.") + .withRequiredArg() + .describedAs("config file") + .ofType(String.class); + + ArgumentAcceptingOptionSpec producerConfigOpt + = parser.accepts("producer.config", "Producer config.") + .withRequiredArg() + .describedAs("config file") + .ofType(String.class); + + ArgumentAcceptingOptionSpec numProducersOpt + = parser.accepts("num.producers", "Number of producer instances") + .withRequiredArg() + .describedAs("Number of producers") + .ofType(Integer.class) + .defaultsTo(1); + + ArgumentAcceptingOptionSpec zkClient01JarOpt + = parser.accepts("zkclient.01.jar", "zkClient 0.1 jar file") + .withRequiredArg() + .describedAs("zkClient 0.1 jar file required by Kafka 0.7") + .ofType(String.class); + + ArgumentAcceptingOptionSpec kafka07JarOpt + = parser.accepts("kafka.07.jar", "Kafka 0.7 jar file") + .withRequiredArg() + .describedAs("kafka 0.7 jar") + .ofType(String.class); + + ArgumentAcceptingOptionSpec numStreamsOpt + = parser.accepts("num.streams", "Number of consumer streams") + .withRequiredArg() + .describedAs("Number of consumer threads") + .ofType(Integer.class) + .defaultsTo(1); + + ArgumentAcceptingOptionSpec whitelistOpt + = parser.accepts("whitelist", "Whitelist of topics to migrate from the 0.7 cluster") + .withRequiredArg() + .describedAs("Java regex (String)") + .ofType(String.class); + + ArgumentAcceptingOptionSpec blacklistOpt + = parser.accepts("blacklist", "Blacklist of topics to migrate from the 0.7 cluster") + .withRequiredArg() + .describedAs("Java regex (String)") + .ofType(String.class); + + ArgumentAcceptingOptionSpec queueSizeOpt + = parser.accepts("queue.size", "Number of messages that are buffered between the 0.7 consumer and 0.8 producer") + .withRequiredArg() + .describedAs("Queue size in terms of number of messages") + .ofType(Integer.class) + .defaultsTo(10000); + + OptionSpecBuilder helpOpt + = parser.accepts("help", "Print this message."); + + OptionSet options = parser.parse(args); + + if (options.has(helpOpt)) { + parser.printHelpOn(System.out); + System.exit(0); + } + + checkRequiredArgs(parser, options, new OptionSpec[]{consumerConfigOpt, producerConfigOpt, zkClient01JarOpt, kafka07JarOpt}); + int whiteListCount = options.has(whitelistOpt) ? 1 : 0; + int blackListCount = options.has(blacklistOpt) ? 1 : 0; + if(whiteListCount + blackListCount != 1) { + System.err.println("Exactly one of whitelist or blacklist is required."); + System.exit(1); + } + + String kafkaJarFile_07 = options.valueOf(kafka07JarOpt); + String zkClientJarFile = options.valueOf(zkClient01JarOpt); + String consumerConfigFile_07 = options.valueOf(consumerConfigOpt); + int numConsumers = options.valueOf(numStreamsOpt); + String producerConfigFile_08 = options.valueOf(producerConfigOpt); + int numProducers = options.valueOf(numProducersOpt); + final List migrationThreads = new ArrayList(numConsumers); + final List producerThreads = new ArrayList(numProducers); + + try { + File kafkaJar_07 = new File(kafkaJarFile_07); + File zkClientJar = new File(zkClientJarFile); + ParentLastURLClassLoader c1 = new ParentLastURLClassLoader(new URL[] { + kafkaJar_07.toURI().toURL(), + zkClientJar.toURI().toURL() + }); + + /** Construct the 07 consumer config **/ + ConsumerConfig_07 = c1.loadClass(KAFKA_07_CONSUMER_CONFIG_CLASS_NAME); + KafkaStaticConsumer_07 = c1.loadClass(KAFKA_07_STATIC_CONSUMER_CLASS_NAME); + ConsumerConnector_07 = c1.loadClass(KAFKA_07_CONSUMER_CONNECTOR_CLASS_NAME); + KafkaStream_07 = c1.loadClass(KAFKA_07_CONSUMER_STREAM_CLASS_NAME); + TopicFilter_07 = c1.loadClass(KAFKA_07_TOPIC_FILTER_CLASS_NAME); + WhiteList_07 = c1.loadClass(KAFKA_07_WHITE_LIST_CLASS_NAME); + BlackList_07 = c1.loadClass(KAFKA_07_BLACK_LIST_CLASS_NAME); + KafkaMessageClass_07 = c1.loadClass(KAFKA_07_MESSAGE_CLASS_NAME); + KafkaConsumerIteratorClass_07 = c1.loadClass(KAFKA_07_CONSUMER_ITERATOR_CLASS_NAME); + KafkaMessageAndMetatDataClass_07 = c1.loadClass(KAFKA_07_MESSAGE_AND_METADATA_CLASS_NAME); + + Constructor ConsumerConfigConstructor_07 = ConsumerConfig_07.getConstructor(Properties.class); + Properties kafkaConsumerProperties_07 = new Properties(); + kafkaConsumerProperties_07.load(new FileInputStream(consumerConfigFile_07)); + /** Disable shallow iteration because the message format is different between 07 and 08, we have to get each individual message **/ + if(kafkaConsumerProperties_07.getProperty("shallow.iterator.enable", "").equals("true")) { + logger.warn("Shallow iterator should not be used in the migration tool"); + kafkaConsumerProperties_07.setProperty("shallow.iterator.enable", "false"); + } + Object consumerConfig_07 = ConsumerConfigConstructor_07.newInstance(kafkaConsumerProperties_07); + + /** Construct the 07 consumer connector **/ + Method ConsumerConnectorCreationMethod_07 = KafkaStaticConsumer_07.getMethod("createJavaConsumerConnector", ConsumerConfig_07); + final Object consumerConnector_07 = ConsumerConnectorCreationMethod_07.invoke(null, consumerConfig_07); + Method ConsumerConnectorCreateMessageStreamsMethod_07 = ConsumerConnector_07.getMethod( + "createMessageStreamsByFilter", + TopicFilter_07, int.class); + final Method ConsumerConnectorShutdownMethod_07 = ConsumerConnector_07.getMethod("shutdown"); + Constructor WhiteListConstructor_07 = WhiteList_07.getConstructor(String.class); + Constructor BlackListConstructor_07 = BlackList_07.getConstructor(String.class); + Object filterSpec = null; + if(options.has(whitelistOpt)) + filterSpec = WhiteListConstructor_07.newInstance(options.valueOf(whitelistOpt)); + else + filterSpec = BlackListConstructor_07.newInstance(options.valueOf(blacklistOpt)); + + Object retKafkaStreams = ConsumerConnectorCreateMessageStreamsMethod_07.invoke(consumerConnector_07, filterSpec, numConsumers); + + Properties kafkaProducerProperties_08 = new Properties(); + kafkaProducerProperties_08.load(new FileInputStream(producerConfigFile_08)); + kafkaProducerProperties_08.setProperty("serializer.class", "kafka.serializer.DefaultEncoder"); + // create a producer channel instead + int queueSize = options.valueOf(queueSizeOpt); + ProducerDataChannel> producerDataChannel = new ProducerDataChannel>(queueSize); + int threadId = 0; + + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + try { + ConsumerConnectorShutdownMethod_07.invoke(consumerConnector_07); + } catch(Exception e) { + logger.error("Error while shutting down Kafka consumer", e); + } + for(MigrationThread migrationThread : migrationThreads) { + migrationThread.shutdown(); + } + for(ProducerThread producerThread : producerThreads) { + producerThread.shutdown(); + } + for(ProducerThread producerThread : producerThreads) { + producerThread.awaitShutdown(); + } + logger.info("Kafka migration tool shutdown successfully"); + } + }); + + // start consumer threads + for(Object stream: (List)retKafkaStreams) { + MigrationThread thread = new MigrationThread(stream, producerDataChannel, threadId); + threadId ++; + thread.start(); + migrationThreads.add(thread); + } + + String clientId = kafkaProducerProperties_08.getProperty("client.id"); + // start producer threads + for (int i = 0; i < numProducers; i++) { + kafkaProducerProperties_08.put("client.id", clientId + "-" + i); + ProducerConfig producerConfig_08 = new ProducerConfig(kafkaProducerProperties_08); + Producer producer = new Producer(producerConfig_08); + ProducerThread producerThread = new ProducerThread(producerDataChannel, producer, i); + producerThread.start(); + producerThreads.add(producerThread); + } + } + catch (Throwable e){ + System.out.println("Kafka migration tool failed due to: " + Utils.stackTrace(e)); + logger.error("Kafka migration tool failed: ", e); + } + } + + private static void checkRequiredArgs(OptionParser parser, OptionSet options, OptionSpec[] required) throws IOException { + for(OptionSpec arg : required) { + if(!options.has(arg)) { + System.err.println("Missing required argument \"" + arg + "\""); + parser.printHelpOn(System.err); + System.exit(1); + } + } + } + + static class ProducerDataChannel { + private final int producerQueueSize; + private final BlockingQueue producerRequestQueue; + + public ProducerDataChannel(int queueSize) { + producerQueueSize = queueSize; + producerRequestQueue = new ArrayBlockingQueue(producerQueueSize); + } + + public void sendRequest(T data) throws InterruptedException { + producerRequestQueue.put(data); + } + + public T receiveRequest() throws InterruptedException { + return producerRequestQueue.take(); + } + } + + private static class MigrationThread extends Thread { + private final Object stream; + private final ProducerDataChannel> producerDataChannel; + private final int threadId; + private final String threadName; + private final org.apache.log4j.Logger logger; + private CountDownLatch shutdownComplete = new CountDownLatch(1); + private final AtomicBoolean isRunning = new AtomicBoolean(true); + + MigrationThread(Object _stream, ProducerDataChannel> _producerDataChannel, int _threadId) { + stream = _stream; + producerDataChannel = _producerDataChannel; + threadId = _threadId; + threadName = "MigrationThread-" + threadId; + logger = org.apache.log4j.Logger.getLogger(MigrationThread.class.getName()); + this.setName(threadName); + } + + public void run() { + try { + Method MessageGetPayloadMethod_07 = KafkaMessageClass_07.getMethod("payload"); + Method KafkaGetMessageMethod_07 = KafkaMessageAndMetatDataClass_07.getMethod("message"); + Method KafkaGetTopicMethod_07 = KafkaMessageAndMetatDataClass_07.getMethod("topic"); + Method ConsumerIteratorMethod = KafkaStream_07.getMethod("iterator"); + Method KafkaStreamHasNextMethod_07 = KafkaConsumerIteratorClass_07.getMethod("hasNext"); + Method KafkaStreamNextMethod_07 = KafkaConsumerIteratorClass_07.getMethod("next"); + Object iterator = ConsumerIteratorMethod.invoke(stream); + + while (((Boolean) KafkaStreamHasNextMethod_07.invoke(iterator)).booleanValue()) { + Object messageAndMetaData_07 = KafkaStreamNextMethod_07.invoke(iterator); + Object message_07 = KafkaGetMessageMethod_07.invoke(messageAndMetaData_07); + Object topic = KafkaGetTopicMethod_07.invoke(messageAndMetaData_07); + Object payload_07 = MessageGetPayloadMethod_07.invoke(message_07); + int size = ((ByteBuffer)payload_07).remaining(); + byte[] bytes = new byte[size]; + ((ByteBuffer)payload_07).get(bytes); + if(logger.isDebugEnabled()) + logger.debug("Migration thread " + threadId + " sending message of size " + bytes.length + " to topic "+ topic); + KeyedMessage producerData = new KeyedMessage((String)topic, null, bytes); + producerDataChannel.sendRequest(producerData); + } + logger.info("Migration thread " + threadName + " finished running"); + } catch (InvocationTargetException t){ + logger.fatal("Migration thread failure due to root cause ", t.getCause()); + } catch (Throwable t){ + logger.fatal("Migration thread failure due to ", t); + } finally { + shutdownComplete.countDown(); + } + } + + public void shutdown() { + logger.info("Migration thread " + threadName + " shutting down"); + isRunning.set(false); + interrupt(); + try { + shutdownComplete.await(); + } catch(InterruptedException ie) { + logger.warn("Interrupt during shutdown of MigrationThread", ie); + } + logger.info("Migration thread " + threadName + " shutdown complete"); + } + } + + static class ProducerThread extends Thread { + private final ProducerDataChannel> producerDataChannel; + private final Producer producer; + private final int threadId; + private String threadName; + private org.apache.log4j.Logger logger; + private CountDownLatch shutdownComplete = new CountDownLatch(1); + private KeyedMessage shutdownMessage = new KeyedMessage("shutdown", null, null); + + public ProducerThread(ProducerDataChannel> _producerDataChannel, + Producer _producer, + int _threadId) { + producerDataChannel = _producerDataChannel; + producer = _producer; + threadId = _threadId; + threadName = "ProducerThread-" + threadId; + logger = org.apache.log4j.Logger.getLogger(ProducerThread.class.getName()); + this.setName(threadName); + } + + public void run() { + try{ + while(true) { + KeyedMessage data = producerDataChannel.receiveRequest(); + if(!data.equals(shutdownMessage)) { + producer.send(data); + if(logger.isDebugEnabled()) logger.debug("Sending message %s".format(new String(data.message()))); + } + else + break; + } + logger.info("Producer thread " + threadName + " finished running"); + } catch (Throwable t){ + logger.fatal("Producer thread failure due to ", t); + } finally { + shutdownComplete.countDown(); + } + } + + public void shutdown() { + try { + logger.info("Producer thread " + threadName + " shutting down"); + producerDataChannel.sendRequest(shutdownMessage); + } catch(InterruptedException ie) { + logger.warn("Interrupt during shutdown of ProducerThread", ie); + } + } + + public void awaitShutdown() { + try { + shutdownComplete.await(); + producer.close(); + logger.info("Producer thread " + threadName + " shutdown complete"); + } catch(InterruptedException ie) { + logger.warn("Interrupt during shutdown of ProducerThread", ie); + } + } + } + + /** + * A parent-last class loader that will try the child class loader first and then the parent. + * This takes a fair bit of doing because java really prefers parent-first. + */ + private static class ParentLastURLClassLoader extends ClassLoader { + private ChildURLClassLoader childClassLoader; + + /** + * This class allows me to call findClass on a class loader + */ + private static class FindClassClassLoader extends ClassLoader { + public FindClassClassLoader(ClassLoader parent) { + super(parent); + } + @Override + public Class findClass(String name) throws ClassNotFoundException { + return super.findClass(name); + } + } + + /** + * This class delegates (child then parent) for the findClass method for a URLClassLoader. + * We need this because findClass is protected in URLClassLoader + */ + private static class ChildURLClassLoader extends URLClassLoader { + private FindClassClassLoader realParent; + public ChildURLClassLoader( URL[] urls, FindClassClassLoader realParent) { + super(urls, null); + this.realParent = realParent; + } + + @Override + public Class findClass(String name) throws ClassNotFoundException { + try{ + // first try to use the URLClassLoader findClass + return super.findClass(name); + } + catch( ClassNotFoundException e ) { + // if that fails, we ask our real parent class loader to load the class (we give up) + return realParent.loadClass(name); + } + } + } + + public ParentLastURLClassLoader(URL[] urls) { + super(Thread.currentThread().getContextClassLoader()); + childClassLoader = new ChildURLClassLoader(urls, new FindClassClassLoader(this.getParent())); + } + + @Override + protected synchronized Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + try { + // first we try to find a class inside the child class loader + return childClassLoader.findClass(name); + } + catch( ClassNotFoundException e ) { + // didn't find it, try the parent + return super.loadClass(name, resolve); + } + } + } +} + diff --git a/migration/src/test/java/com/uber/kafka/tools/KafkaMigrationToolTest.java b/migration/src/test/java/com/uber/kafka/tools/KafkaMigrationToolTest.java new file mode 100644 index 0000000000000..0155ed13cc490 --- /dev/null +++ b/migration/src/test/java/com/uber/kafka/tools/KafkaMigrationToolTest.java @@ -0,0 +1,15 @@ +package com.uber.kafka.tools; + +import org.junit.Test; + +public class KafkaMigrationToolTest { + + @Test + public void testFoo() throws Exception { + // InputStream is = getClass().getResourceAsStream("/offset_idx"); + // byte[] encodedOffsets = ByteStreams.toByteArray(is); + // Assert.assertEquals(0, encodedOffsets.length); + } + +} + diff --git a/migration/src/test/resources/offset_idx b/migration/src/test/resources/offset_idx new file mode 100644 index 0000000000000000000000000000000000000000..3781d05c7f7332811cac1fae79dca1432ff38ae0 GIT binary patch literal 181032 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

1X3Z5}7uCs}mvN>QIQQOg?9 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 literal 0 HcmV?d00001 diff --git a/settings.gradle b/settings.gradle index 6041784d6f84c..3b6dd91b31c47 100644 --- a/settings.gradle +++ b/settings.gradle @@ -14,4 +14,4 @@ // limitations under the License. apply from: file('scala.gradle') -include 'core', 'perf', 'contrib:hadoop-consumer', 'contrib:hadoop-producer', 'examples', 'clients' +include 'core', 'perf', 'migration', 'contrib:hadoop-consumer', 'contrib:hadoop-producer', 'examples', 'clients' From ce43cf232fa02769ce774c46aa4b391c5224f471 Mon Sep 17 00:00:00 2001 From: Seung-Yeoul Yang Date: Wed, 18 Mar 2015 18:12:08 -0700 Subject: [PATCH 11/14] [kafka] better error recovery in migration tool (part 2) Summary: * check topic whiltelist/blacklist before running the migration tool * fix corrupted offsets upon detection * fail fast on erorrs and sigterm Test plan: * Added unit tests * Manually tested that corrupted offsets are fixed Reviewers: norbert, grk, praveen, csoman, vinoth Subscribers: nharkins, bigpunk Maniphest Tasks: T67641 Differential Revision: https://code.uberinternal.com/D84682 --- build.gradle | 12 + config/tools-log4j.properties | 6 +- .../uber/kafka/tools/Kafka7LatestOffsets.java | 14 ++ .../kafka/tools/Kafka7LatestOffsetsImpl.java | 64 ++++++ .../uber/kafka/tools/Kafka7OffsetFixer.java | 110 +++++++++ .../uber/kafka/tools/KafkaMigrationTool.java | 217 ++++++++++++++---- .../uber/kafka/tools/MigrationContext.java | 54 +++++ .../com/uber/kafka/tools/MigrationUtils.java | 85 +++++++ .../com/uber/kafka/tools/OffsetIndex.java | 71 ++++++ .../kafka/tools/Kafka7OffsetFixerTest.java | 112 +++++++++ .../kafka/tools/KafkaMigrationToolTest.java | 15 -- .../uber/kafka/tools/MigrationUtilsTest.java | 45 ++++ .../com/uber/kafka/tools/OffsetIndexTest.java | 63 +++++ .../uber/kafka/tools/OffsetIndexTestUtil.java | 39 ++++ 14 files changed, 847 insertions(+), 60 deletions(-) create mode 100644 migration/src/main/java/com/uber/kafka/tools/Kafka7LatestOffsets.java create mode 100644 migration/src/main/java/com/uber/kafka/tools/Kafka7LatestOffsetsImpl.java create mode 100644 migration/src/main/java/com/uber/kafka/tools/Kafka7OffsetFixer.java create mode 100644 migration/src/main/java/com/uber/kafka/tools/MigrationContext.java create mode 100644 migration/src/main/java/com/uber/kafka/tools/MigrationUtils.java create mode 100644 migration/src/main/java/com/uber/kafka/tools/OffsetIndex.java create mode 100644 migration/src/test/java/com/uber/kafka/tools/Kafka7OffsetFixerTest.java delete mode 100644 migration/src/test/java/com/uber/kafka/tools/KafkaMigrationToolTest.java create mode 100644 migration/src/test/java/com/uber/kafka/tools/MigrationUtilsTest.java create mode 100644 migration/src/test/java/com/uber/kafka/tools/OffsetIndexTest.java create mode 100644 migration/src/test/java/com/uber/kafka/tools/OffsetIndexTestUtil.java diff --git a/build.gradle b/build.gradle index 3689a4f0f3e44..e475fe8959350 100644 --- a/build.gradle +++ b/build.gradle @@ -304,6 +304,8 @@ project(':migration') { compile 'com.google.guava:guava:18.0' testCompile 'junit:junit:4.1' + testCompile 'org.mockito:mockito-core:1.+' + testCompile 'commons-io:commons-io:2.4' test { testLogging { @@ -317,6 +319,16 @@ project(':migration') { output.resourcesDir = "build/classes/test" } } + + tasks.create(name: "copyDependantLibs", type: Copy) { + into "$buildDir/dependant-libs-${scalaVersion}" + from configurations.runtime + } + + + jar { + dependsOn 'copyDependantLibs' + } } configurations { diff --git a/config/tools-log4j.properties b/config/tools-log4j.properties index 7924049014983..aea6f47330590 100644 --- a/config/tools-log4j.properties +++ b/config/tools-log4j.properties @@ -13,10 +13,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -log4j.rootLogger=WARN, stdout +log4j.rootLogger=WARN, stdout log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout -log4j.appender.stdout.layout.ConversionPattern=[%d] %p %m (%c)%n +log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %t %-5p %c{1}:%L - %m%n + +log4j.logger.com.uber.kafka=INFO diff --git a/migration/src/main/java/com/uber/kafka/tools/Kafka7LatestOffsets.java b/migration/src/main/java/com/uber/kafka/tools/Kafka7LatestOffsets.java new file mode 100644 index 0000000000000..e98ddf20c8ea5 --- /dev/null +++ b/migration/src/main/java/com/uber/kafka/tools/Kafka7LatestOffsets.java @@ -0,0 +1,14 @@ +// Copyright (c) 2015 Uber Technologies, Inc. All rights reserved. +// @author Seung-Yeoul Yang (syyang@uber.com) + +package com.uber.kafka.tools; + +public interface Kafka7LatestOffsets { + + /** + * Returns the latest offset for a given Kafka 0.7 topic and partition. + */ + long get(String topic, int partition); + + void close(); +} diff --git a/migration/src/main/java/com/uber/kafka/tools/Kafka7LatestOffsetsImpl.java b/migration/src/main/java/com/uber/kafka/tools/Kafka7LatestOffsetsImpl.java new file mode 100644 index 0000000000000..580091543533e --- /dev/null +++ b/migration/src/main/java/com/uber/kafka/tools/Kafka7LatestOffsetsImpl.java @@ -0,0 +1,64 @@ +// Copyright (c) 2015 Uber Technologies, Inc. All rights reserved. +// @author Seung-Yeoul Yang (syyang@uber.com) + +package com.uber.kafka.tools; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; + +/** + * Wrapper around Kafka 0.7 SimpleConsumer for fetching the latest + * offset. The shenanigans with class loader is necessary because + * of class name collisions between Kafka 0.7 and 0.8. + */ +class Kafka7LatestOffsetsImpl implements Kafka7LatestOffsets { + + private static final long LATEST_OFFSET = -1L; + private static final String KAFKA_07_STATIC_SIMPLE_CONSUMER_CLASS_NAME = + "kafka.javaapi.consumer.SimpleConsumer"; + private static final int SO_TIMEOUT_MS = 10 * 1000; + private static final int BUFFER_SIZE_BYTES = 1000 * 1024; + private static final String LEAF_KAFKA_07_HOST = "localhost"; + private static final int LEAF_KAKFA_07_PORT = 9093; + + private final Object simpleConsumer_07; + private final Method simpleConsumerGetOffsetBeforeMethod_07; + private final Method simpleConsumerCloseMethod_07; + + Kafka7LatestOffsetsImpl(ClassLoader cl) throws Exception { + Class simpleConsumerClass_07 = cl.loadClass(KAFKA_07_STATIC_SIMPLE_CONSUMER_CLASS_NAME); + Constructor simpleConsumerConstructor_07 = simpleConsumerClass_07.getConstructor( + String.class, int.class, int.class, int.class); + simpleConsumer_07 = simpleConsumerConstructor_07.newInstance( + LEAF_KAFKA_07_HOST, LEAF_KAKFA_07_PORT, SO_TIMEOUT_MS, BUFFER_SIZE_BYTES); + simpleConsumerGetOffsetBeforeMethod_07 = simpleConsumerClass_07.getMethod( + "getOffsetsBefore", String.class, int.class, long.class, int.class); + simpleConsumerCloseMethod_07 = simpleConsumerClass_07.getMethod("close"); + } + + @Override + public long get(String topic, int partition) { + long[] offsets = null; + try { + offsets = (long[]) simpleConsumerGetOffsetBeforeMethod_07.invoke( + simpleConsumer_07, topic, partition, LATEST_OFFSET, 1); + } catch (Exception e) { + throw new RuntimeException(e); + } + if (offsets.length < 1) { + throw new RuntimeException("Failed to find latest offset for " + + topic + "_" + partition); + } + return offsets[0]; + } + + @Override + public void close() { + try { + simpleConsumerCloseMethod_07.invoke(simpleConsumer_07); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + +} diff --git a/migration/src/main/java/com/uber/kafka/tools/Kafka7OffsetFixer.java b/migration/src/main/java/com/uber/kafka/tools/Kafka7OffsetFixer.java new file mode 100644 index 0000000000000..a40f2343e0acb --- /dev/null +++ b/migration/src/main/java/com/uber/kafka/tools/Kafka7OffsetFixer.java @@ -0,0 +1,110 @@ +// Copyright (c) 2015 Uber Technologies, Inc. All rights reserved. +// @author Seung-Yeoul Yang (syyang@uber.com) + +package com.uber.kafka.tools; + +import java.io.File; +import java.util.Date; +import java.util.concurrent.TimeUnit; + +import org.I0Itec.zkclient.ZkClient; +import org.apache.log4j.Logger; + +import com.google.common.base.Preconditions; + +/** + * Fixes corrupt Kafka 0.7 offset using offset index file (containing a list of latest + * offsets written out periodically by a cron job). + */ +public class Kafka7OffsetFixer { + + private static final Logger LOGGER = Logger.getLogger(Kafka7OffsetFixer.class); + + private static final long OFFSET_INDEX_MAX_AGE_MS = TimeUnit.MINUTES.toMillis(10L); + + private static final int PARTITION_0 = 0; + + private static final String LEAF_KAFKA07_ZK_HOST = "localhost:2182"; + + private static final String MIRRORMAKER_PATH = "/var/mirrormaker"; + private static final String CURRENT_OFFSET_PATH = "/consumers/%s/offsets/%s/0-0"; + + private final Kafka7LatestOffsets latestOffsets; + private final ZkClient zkClient; + private final String mirrorMakerPath; + + public Kafka7OffsetFixer(Kafka7LatestOffsets latestOffsetProvider) { + this(latestOffsetProvider, MIRRORMAKER_PATH); + } + + public Kafka7OffsetFixer(Kafka7LatestOffsets latestOffsets, String mirrorMakerPath) { + this(latestOffsets, mirrorMakerPath, + MigrationUtils.get().newZkClient(LEAF_KAFKA07_ZK_HOST)); + } + + public Kafka7OffsetFixer(Kafka7LatestOffsets latestOffsets, + String mirrorMakerPath, ZkClient zkClient) { + this.latestOffsets = latestOffsets; + this.mirrorMakerPath = mirrorMakerPath; + this.zkClient = zkClient; + } + + public void close() { + try { + latestOffsets.close(); + zkClient.close(); + } catch (Exception e) { + LOGGER.warn("Failed to clean-up Kafka offset fixer", e); + } + } + + public static String getCurrentOffsetZkPath(String consumerGroup, String topic) { + return String.format(CURRENT_OFFSET_PATH, consumerGroup, topic); + } + + public void fixOffset(String consumerGroup, String topic) { + Preconditions.checkNotNull(topic, "Topic can't be null"); + // Load offset index. + File offsetIndexFile = new File(mirrorMakerPath, topic + "_0"); + String offsetIndexPath = offsetIndexFile.getPath(); + if (!offsetIndexFile.exists()) { + throw new RuntimeException("Offset index file doesn't exist at path: " + + offsetIndexPath); + } + // Check whether offset index is stale. + if (offsetIndexFile.lastModified() < System.currentTimeMillis() - OFFSET_INDEX_MAX_AGE_MS) { + throw new RuntimeException("Offset index file is stale. Path: " + offsetIndexPath + + ", last modified time: " + new Date(offsetIndexFile.lastModified())); + } + + LOGGER.info("Loading offset file for topic: " + topic + ", path: " + offsetIndexPath); + OffsetIndex index = OffsetIndex.load(offsetIndexPath); + + // Get current offset from kafka leaf zookeeper. + String currentOffsetPath = getCurrentOffsetZkPath(consumerGroup, topic); + LOGGER.info("Reading current offset for topic: " + topic + ", zk path: " + currentOffsetPath); + if (!zkClient.exists(currentOffsetPath)) { + throw new RuntimeException("Missing current offset for " + topic + "at zk path: " + + currentOffsetPath); + } + byte[] currentOffsetBytes = zkClient.readData(currentOffsetPath); + long currentOffset = Long.parseLong(new String(currentOffsetBytes)); + LOGGER.info("Current offset: " + currentOffset + ", topic: " + topic); + + // Find the next valid offset. + long newOffset = index.getNextOffset(currentOffset); + if (newOffset == OffsetIndex.LATEST_OFFSET) { + // Resolve latest offset to actual offset. + newOffset = latestOffsets.get(topic, PARTITION_0); + LOGGER.info("Resolved latest offset to " + newOffset + ", topic " + topic); + } + + // Update the current offset in zookeeper. + byte[] newOffsetBytes = Long.toString(newOffset).getBytes(); + zkClient.writeData(currentOffsetPath, newOffsetBytes); + + LOGGER.info(String.format("Updated offset for %s from %d to %d", + topic, currentOffset, newOffset)); + } + +} diff --git a/migration/src/main/java/com/uber/kafka/tools/KafkaMigrationTool.java b/migration/src/main/java/com/uber/kafka/tools/KafkaMigrationTool.java index d60cc2952dc55..dcd18455ac52e 100644 --- a/migration/src/main/java/com/uber/kafka/tools/KafkaMigrationTool.java +++ b/migration/src/main/java/com/uber/kafka/tools/KafkaMigrationTool.java @@ -12,10 +12,11 @@ import java.util.ArrayList; import java.util.List; import java.util.Properties; +import java.util.Set; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.TimeUnit; import joptsimple.ArgumentAcceptingOptionSpec; import joptsimple.OptionParser; @@ -27,22 +28,16 @@ import kafka.producer.ProducerConfig; import kafka.utils.Utils; +import org.apache.log4j.AppenderSkeleton; +import org.apache.log4j.Level; +import org.apache.log4j.Logger; +import org.apache.log4j.spi.LoggingEvent; + +import com.google.common.base.Joiner; + /** - * This is a kafka 0.7 to 0.8 online migration tool used for migrating data from 0.7 to 0.8 cluster. Internally, - * it's composed of a kafka 0.7 consumer and kafka 0.8 producer. The kafka 0.7 consumer consumes data from the - * 0.7 cluster, and the kafka 0.8 producer produces data to the 0.8 cluster. - * - * The 0.7 consumer is loaded from kafka 0.7 jar using a "parent last, child first" java class loader. - * Ordinary class loader is "parent first, child last", and kafka 0.8 and 0.7 both have classes for a lot of - * class names like "kafka.consumer.Consumer", etc., so ordinary java URLClassLoader with kafka 0.7 jar will - * will still load the 0.8 version class. - * - * As kafka 0.7 and kafka 0.8 used different version of zkClient, the zkClient jar used by kafka 0.7 should - * also be used by the class loader. - * - * The user need to provide the configuration file for 0.7 consumer and 0.8 producer. For 0.8 producer, - * the "serializer.class" config is set to "kafka.serializer.DefaultEncoder" by the code. + * kafka 0.7 to 0.8 online migration tool based on kafka.tools.KafkaMigrationTool. */ @SuppressWarnings({"unchecked", "rawtypes"}) public class KafkaMigrationTool @@ -58,6 +53,7 @@ public class KafkaMigrationTool private static final String KAFKA_07_WHITE_LIST_CLASS_NAME = "kafka.consumer.Whitelist"; private static final String KAFKA_07_TOPIC_FILTER_CLASS_NAME = "kafka.consumer.TopicFilter"; private static final String KAFKA_07_BLACK_LIST_CLASS_NAME = "kafka.consumer.Blacklist"; + private static final String KAFKA_07_CONSUMER_GROUP_PROPERTY = "groupid"; private static Class KafkaStaticConsumer_07 = null; private static Class ConsumerConfig_07 = null; @@ -70,7 +66,50 @@ public class KafkaMigrationTool private static Class KafkaMessageAndMetatDataClass_07 = null; private static Class KafkaMessageClass_07 = null; + /** + * Snoops into error logs, and tracks topics whose offsets are invalid. This hack is + * necessary because while high-level consumer (i.e. ConsumerIterator.hasNext()) throws + * an exception when it encounters a message with invalid offset, the exception doesn't + * contain any info about the topic associated with the message. + */ + private static class InvalidMessageAppender extends AppenderSkeleton { + private static final String FETCH_RUNNABLE_ERROR_PREFIX = "error in FetcherRunnable for "; + + private final MigrationContext context; + + public InvalidMessageAppender(MigrationContext context) { + this.context = context; + } + + @Override + protected void append(LoggingEvent event) { + if (event.getLevel() != Level.ERROR) { + return; + } + String message = event.getRenderedMessage(); + if (message == null || !message.startsWith(FETCH_RUNNABLE_ERROR_PREFIX)) { + return; + } + int beginIdx = FETCH_RUNNABLE_ERROR_PREFIX.length(); + int endIdx = message.indexOf(':'); + // e.g. error in FetcherRunnable for foo:0-0: fetched offset = 12: consumed offset = 25 + String topic = message.substring(beginIdx, endIdx); + context.addTopicWithCorruptOffset(topic); + logger.info("Detected topic with invalid offset: " + topic); + } + + @Override + public void close() { } + + @Override + public boolean requiresLayout() { return false; } + } + public static void main(String[] args) throws InterruptedException, IOException { + final MigrationContext context = new MigrationContext(); + InvalidMessageAppender foo = new InvalidMessageAppender(context); + Logger.getRootLogger().addAppender(foo); + OptionParser parser = new OptionParser(); ArgumentAcceptingOptionSpec consumerConfigOpt = parser.accepts("consumer.config", "Kafka 0.7 consumer config to consume from the source 0.7 cluster. " + "You man specify multiple of these.") @@ -103,6 +142,12 @@ public static void main(String[] args) throws InterruptedException, IOException .describedAs("kafka 0.7 jar") .ofType(String.class); + ArgumentAcceptingOptionSpec kafka08ZKHostsOpt + = parser.accepts("kafka08.zookeeper.connect", "Zookeeper chroot path for the 0.8 cluster") + .withRequiredArg() + .describedAs("Zookeeper chroot path for the 0.8 cluster") + .ofType(String.class); + ArgumentAcceptingOptionSpec numStreamsOpt = parser.accepts("num.streams", "Number of consumer streams") .withRequiredArg() @@ -139,7 +184,8 @@ public static void main(String[] args) throws InterruptedException, IOException System.exit(0); } - checkRequiredArgs(parser, options, new OptionSpec[]{consumerConfigOpt, producerConfigOpt, zkClient01JarOpt, kafka07JarOpt}); + checkRequiredArgs(parser, options, new OptionSpec[]{ + consumerConfigOpt, producerConfigOpt, zkClient01JarOpt, kafka07JarOpt, kafka08ZKHostsOpt}); int whiteListCount = options.has(whitelistOpt) ? 1 : 0; int blackListCount = options.has(blacklistOpt) ? 1 : 0; if(whiteListCount + blackListCount != 1) { @@ -153,13 +199,14 @@ public static void main(String[] args) throws InterruptedException, IOException int numConsumers = options.valueOf(numStreamsOpt); String producerConfigFile_08 = options.valueOf(producerConfigOpt); int numProducers = options.valueOf(numProducersOpt); + String kafka08ZKHosts = options.valueOf(kafka08ZKHostsOpt); final List migrationThreads = new ArrayList(numConsumers); final List producerThreads = new ArrayList(numProducers); try { File kafkaJar_07 = new File(kafkaJarFile_07); File zkClientJar = new File(zkClientJarFile); - ParentLastURLClassLoader c1 = new ParentLastURLClassLoader(new URL[] { + final ParentLastURLClassLoader c1 = new ParentLastURLClassLoader(new URL[] { kafkaJar_07.toURI().toURL(), zkClientJar.toURI().toURL() }); @@ -177,7 +224,7 @@ public static void main(String[] args) throws InterruptedException, IOException KafkaMessageAndMetatDataClass_07 = c1.loadClass(KAFKA_07_MESSAGE_AND_METADATA_CLASS_NAME); Constructor ConsumerConfigConstructor_07 = ConsumerConfig_07.getConstructor(Properties.class); - Properties kafkaConsumerProperties_07 = new Properties(); + final Properties kafkaConsumerProperties_07 = new Properties(); kafkaConsumerProperties_07.load(new FileInputStream(consumerConfigFile_07)); /** Disable shallow iteration because the message format is different between 07 and 08, we have to get each individual message **/ if(kafkaConsumerProperties_07.getProperty("shallow.iterator.enable", "").equals("true")) { @@ -196,10 +243,17 @@ public static void main(String[] args) throws InterruptedException, IOException Constructor WhiteListConstructor_07 = WhiteList_07.getConstructor(String.class); Constructor BlackListConstructor_07 = BlackList_07.getConstructor(String.class); Object filterSpec = null; - if(options.has(whitelistOpt)) - filterSpec = WhiteListConstructor_07.newInstance(options.valueOf(whitelistOpt)); - else - filterSpec = BlackListConstructor_07.newInstance(options.valueOf(blacklistOpt)); + if (options.has(whitelistOpt)) { + String whitelist = MigrationUtils.get().rewriteTopicWhitelist( + kafka08ZKHosts, options.valueOf(whitelistOpt)); + logger.info("Whitelist after rewrite: " + whitelist); + filterSpec = WhiteListConstructor_07.newInstance(whitelist); + } else { + String blacklist = MigrationUtils.get().rewriteTopicBlacklist( + kafka08ZKHosts, options.valueOf(blacklistOpt)); + logger.info("Blacklist after rewrite: " + blacklist); + filterSpec = BlackListConstructor_07.newInstance(blacklist); + } Object retKafkaStreams = ConsumerConnectorCreateMessageStreamsMethod_07.invoke(consumerConnector_07, filterSpec, numConsumers); @@ -208,12 +262,13 @@ public static void main(String[] args) throws InterruptedException, IOException kafkaProducerProperties_08.setProperty("serializer.class", "kafka.serializer.DefaultEncoder"); // create a producer channel instead int queueSize = options.valueOf(queueSizeOpt); - ProducerDataChannel> producerDataChannel = new ProducerDataChannel>(queueSize); + ProducerDataChannel> producerDataChannel = new ProducerDataChannel>(context, queueSize); int threadId = 0; Runtime.getRuntime().addShutdownHook(new Thread() { @Override public void run() { + logger.info("Shutting down migration tool..."); try { ConsumerConnectorShutdownMethod_07.invoke(consumerConnector_07); } catch(Exception e) { @@ -228,13 +283,43 @@ public void run() { for(ProducerThread producerThread : producerThreads) { producerThread.awaitShutdown(); } + if (context.failed()) { + Set topics = context.getTopicsWithCorruptOffset(); + if (!topics.isEmpty()) { + String consumerGroup = kafkaConsumerProperties_07.getProperty( + KAFKA_07_CONSUMER_GROUP_PROPERTY); + logger.info("Fixing corrupt offsets for the following topics: " + + Joiner.on(", ").join(topics)); + + Kafka7OffsetFixer fixer = null; + try { + Kafka7LatestOffsets latestOffsets = new Kafka7LatestOffsetsImpl(c1); + fixer = new Kafka7OffsetFixer(latestOffsets); + for (String topic : topics) { + fixer.fixOffset(consumerGroup, topic); + } + } catch (Throwable t) { + logger.error("Unexpected failure when fixing corrupt offset", t); + } finally { + try { + if (fixer != null) { + fixer.close(); + } + } catch (Throwable t) { + logger.warn("Unexpected failure when closing offset fixer", t); + } + } + } + } logger.info("Kafka migration tool shutdown successfully"); } }); // start consumer threads + logger.info("Starting " + numConsumers + " threads"); for(Object stream: (List)retKafkaStreams) { - MigrationThread thread = new MigrationThread(stream, producerDataChannel, threadId); + MigrationThread thread = new MigrationThread( + context, stream, producerDataChannel, threadId); threadId ++; thread.start(); migrationThreads.add(thread); @@ -242,19 +327,31 @@ public void run() { String clientId = kafkaProducerProperties_08.getProperty("client.id"); // start producer threads + logger.info("Starting " + numProducers + " producer threads"); for (int i = 0; i < numProducers; i++) { kafkaProducerProperties_08.put("client.id", clientId + "-" + i); ProducerConfig producerConfig_08 = new ProducerConfig(kafkaProducerProperties_08); Producer producer = new Producer(producerConfig_08); - ProducerThread producerThread = new ProducerThread(producerDataChannel, producer, i); + ProducerThread producerThread = new ProducerThread( + context, producerDataChannel, producer, i); producerThread.start(); producerThreads.add(producerThread); } + + // Block while the migration tool is running. We need to call + // System.exit(0) below to force trigger the shutdown hook to be + // called since SimpleConsumer internally runs a user thread. + while (!context.failed()) { + Thread.sleep(100L); + } } catch (Throwable e){ System.out.println("Kafka migration tool failed due to: " + Utils.stackTrace(e)); logger.error("Kafka migration tool failed: ", e); + context.setFailed(); } + + System.exit(0); } private static void checkRequiredArgs(OptionParser parser, OptionSet options, OptionSpec[] required) throws IOException { @@ -268,20 +365,40 @@ private static void checkRequiredArgs(OptionParser parser, OptionSet options, Op } static class ProducerDataChannel { + private static final long OFFER_INTERVAL_MS = 100L; + private static final long POLL_INTERVAL_MS = 100L; + + private final MigrationContext context; private final int producerQueueSize; private final BlockingQueue producerRequestQueue; - public ProducerDataChannel(int queueSize) { + public ProducerDataChannel(MigrationContext context, int queueSize) { + this.context = context; producerQueueSize = queueSize; producerRequestQueue = new ArrayBlockingQueue(producerQueueSize); } public void sendRequest(T data) throws InterruptedException { - producerRequestQueue.put(data); + while (!context.failed()) { + if (producerRequestQueue.offer(data, OFFER_INTERVAL_MS, TimeUnit.MILLISECONDS)) { + return; + } + } + throw new RuntimeException("Migration failed. Failed to offer request"); } public T receiveRequest() throws InterruptedException { - return producerRequestQueue.take(); + while (!context.failed()) { + T data = producerRequestQueue.poll(POLL_INTERVAL_MS, TimeUnit.MILLISECONDS); + if (data != null) { + return data; + } + } + throw new RuntimeException("Migration failed. Failed to get poll request"); + } + + public int size() { + return producerRequestQueue.size(); } } @@ -292,9 +409,12 @@ private static class MigrationThread extends Thread { private final String threadName; private final org.apache.log4j.Logger logger; private CountDownLatch shutdownComplete = new CountDownLatch(1); - private final AtomicBoolean isRunning = new AtomicBoolean(true); + private final MigrationContext context; - MigrationThread(Object _stream, ProducerDataChannel> _producerDataChannel, int _threadId) { + MigrationThread(MigrationContext context, Object _stream, + ProducerDataChannel> _producerDataChannel, + int _threadId) { + this.context = context; stream = _stream; producerDataChannel = _producerDataChannel; threadId = _threadId; @@ -313,7 +433,7 @@ public void run() { Method KafkaStreamNextMethod_07 = KafkaConsumerIteratorClass_07.getMethod("next"); Object iterator = ConsumerIteratorMethod.invoke(stream); - while (((Boolean) KafkaStreamHasNextMethod_07.invoke(iterator)).booleanValue()) { + while (!context.failed() && ((Boolean) KafkaStreamHasNextMethod_07.invoke(iterator)).booleanValue()) { Object messageAndMetaData_07 = KafkaStreamNextMethod_07.invoke(iterator); Object message_07 = KafkaGetMessageMethod_07.invoke(messageAndMetaData_07); Object topic = KafkaGetTopicMethod_07.invoke(messageAndMetaData_07); @@ -321,16 +441,21 @@ public void run() { int size = ((ByteBuffer)payload_07).remaining(); byte[] bytes = new byte[size]; ((ByteBuffer)payload_07).get(bytes); - if(logger.isDebugEnabled()) - logger.debug("Migration thread " + threadId + " sending message of size " + bytes.length + " to topic "+ topic); + if(logger.isDebugEnabled()) { + logger.debug("Migration thread " + threadId + " sending message of size " + + bytes.length + " to topic " + topic); + } + KeyedMessage producerData = new KeyedMessage((String)topic, null, bytes); producerDataChannel.sendRequest(producerData); } logger.info("Migration thread " + threadName + " finished running"); } catch (InvocationTargetException t){ logger.fatal("Migration thread failure due to root cause ", t.getCause()); + context.setFailed(); } catch (Throwable t){ logger.fatal("Migration thread failure due to ", t); + context.setFailed(); } finally { shutdownComplete.countDown(); } @@ -338,7 +463,6 @@ public void run() { public void shutdown() { logger.info("Migration thread " + threadName + " shutting down"); - isRunning.set(false); interrupt(); try { shutdownComplete.await(); @@ -353,14 +477,16 @@ static class ProducerThread extends Thread { private final ProducerDataChannel> producerDataChannel; private final Producer producer; private final int threadId; + private final MigrationContext context; private String threadName; private org.apache.log4j.Logger logger; private CountDownLatch shutdownComplete = new CountDownLatch(1); private KeyedMessage shutdownMessage = new KeyedMessage("shutdown", null, null); - public ProducerThread(ProducerDataChannel> _producerDataChannel, - Producer _producer, - int _threadId) { + public ProducerThread(MigrationContext context, + ProducerDataChannel> _producerDataChannel, + Producer _producer, int _threadId) { + this.context = context; producerDataChannel = _producerDataChannel; producer = _producer; threadId = _threadId; @@ -371,18 +497,21 @@ public ProducerThread(ProducerDataChannel> _produce public void run() { try{ - while(true) { + while(!context.failed()) { KeyedMessage data = producerDataChannel.receiveRequest(); if(!data.equals(shutdownMessage)) { producer.send(data); - if(logger.isDebugEnabled()) logger.debug("Sending message %s".format(new String(data.message()))); - } - else + if(logger.isDebugEnabled()) { + logger.debug("Sending message " + new String(data.message())); + } + } else { break; + } } logger.info("Producer thread " + threadName + " finished running"); } catch (Throwable t){ logger.fatal("Producer thread failure due to ", t); + context.setFailed(); } finally { shutdownComplete.countDown(); } @@ -391,7 +520,9 @@ public void run() { public void shutdown() { try { logger.info("Producer thread " + threadName + " shutting down"); - producerDataChannel.sendRequest(shutdownMessage); + if (!context.failed()) { + producerDataChannel.sendRequest(shutdownMessage); + } } catch(InterruptedException ie) { logger.warn("Interrupt during shutdown of ProducerThread", ie); } @@ -412,7 +543,7 @@ public void awaitShutdown() { * A parent-last class loader that will try the child class loader first and then the parent. * This takes a fair bit of doing because java really prefers parent-first. */ - private static class ParentLastURLClassLoader extends ClassLoader { + static class ParentLastURLClassLoader extends ClassLoader { private ChildURLClassLoader childClassLoader; /** diff --git a/migration/src/main/java/com/uber/kafka/tools/MigrationContext.java b/migration/src/main/java/com/uber/kafka/tools/MigrationContext.java new file mode 100644 index 0000000000000..142c0b8fb1e10 --- /dev/null +++ b/migration/src/main/java/com/uber/kafka/tools/MigrationContext.java @@ -0,0 +1,54 @@ +// Copyright (c) 2015 Uber Technologies, Inc. All rights reserved. +// @author Seung-Yeoul Yang (syyang@uber.com) + +package com.uber.kafka.tools; + +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; + +/** + * Context object for migration job. Used for coordination + * of migrator and producer threads. + * + * This class is thread-safe. + */ +public class MigrationContext { + + private final AtomicBoolean failed; + private final Set topicsWithCorruptOffset; + + public MigrationContext() { + this.failed = new AtomicBoolean(false); + this.topicsWithCorruptOffset = Sets.newHashSet(); + } + + /** + * Returns true if migration job failed; false otherwise. + */ + public boolean failed() { + return failed.get(); + } + + public + void setFailed() { + failed.set(true); + } + + /** + * Returns a set of Kafka 0.7 topics with corrupt offsets. + */ + public Set getTopicsWithCorruptOffset() { + synchronized (topicsWithCorruptOffset) { + return ImmutableSet.copyOf(topicsWithCorruptOffset); + } + } + + public void addTopicWithCorruptOffset(String topic) { + synchronized (topicsWithCorruptOffset) { + topicsWithCorruptOffset.add(topic); + } + } +} diff --git a/migration/src/main/java/com/uber/kafka/tools/MigrationUtils.java b/migration/src/main/java/com/uber/kafka/tools/MigrationUtils.java new file mode 100644 index 0000000000000..014a80d9c34c9 --- /dev/null +++ b/migration/src/main/java/com/uber/kafka/tools/MigrationUtils.java @@ -0,0 +1,85 @@ +// Copyright (c) 2015 Uber Technologies, Inc. All rights reserved. +// @author Seung-Yeoul Yang (syyang@uber.com) + +package com.uber.kafka.tools; + +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import kafka.utils.ZkUtils; + +import org.I0Itec.zkclient.ZkClient; +import org.I0Itec.zkclient.serialize.BytesPushThroughSerializer; +import org.apache.log4j.Logger; + +import scala.collection.Iterator; + +import com.google.common.base.Joiner; +import com.google.common.collect.Lists; + +/** + * Utility methods for Kafka 0.7 to Kafak 0.8 migrator. + */ +public class MigrationUtils { + + private static final Logger LOGGER = Logger.getLogger(MigrationUtils.class); + + private static final int ZK_CONN_TIMEOUT_MS = 5 * 1000; + private static final int ZK_SOCKET_TIMEOUT_MS = 30 * 1000; + + private static final Joiner OR_DELIMITER = Joiner.on('|'); + + private static final MigrationUtils INSTANCE = new MigrationUtils(); + + // For tests. + MigrationUtils() {} + + public static MigrationUtils get() { + return INSTANCE; + } + + public String rewriteTopicWhitelist(String kafka08ZKHosts, String whitelist) { + return getTopicList(kafka08ZKHosts, whitelist, true); + } + + public String rewriteTopicBlacklist(String kafka08ZKHosts, String blacklist) { + return getTopicList(kafka08ZKHosts, blacklist, false); + } + + public ZkClient newZkClient(String zkServers) { + return new ZkClient(zkServers, ZK_CONN_TIMEOUT_MS, ZK_SOCKET_TIMEOUT_MS, + new BytesPushThroughSerializer()); + } + + private String getTopicList(String kafka08ZKHosts, String topicList, boolean isWhitelist) { + Pattern pattern = Pattern.compile(topicList); + List allTopics = getAllTopicsInKafka08(kafka08ZKHosts); + List filteredTopics = Lists.newArrayList(); + for (String topic : allTopics) { + Matcher matcher = pattern.matcher(topic); + if (matcher.find() ^ !isWhitelist) { + filteredTopics.add(topic); + } else { + LOGGER.warn("Attempting to migrate topic that doesn't exist in " + + "kafka8, topic: " + topic); + } + } + return OR_DELIMITER.join(filteredTopics); + } + + public List getAllTopicsInKafka08(String kafka08ZKHosts) { + ZkClient zkClient = newZkClient(kafka08ZKHosts); + try { + Iterator allTopics = ZkUtils.getAllTopics(zkClient).toIterator(); + List res = Lists.newArrayList(); + while (allTopics.hasNext()) { + res.add(allTopics.next()); + } + return res; + } finally { + zkClient.close(); + } + } + +} diff --git a/migration/src/main/java/com/uber/kafka/tools/OffsetIndex.java b/migration/src/main/java/com/uber/kafka/tools/OffsetIndex.java new file mode 100644 index 0000000000000..b5a93401049c0 --- /dev/null +++ b/migration/src/main/java/com/uber/kafka/tools/OffsetIndex.java @@ -0,0 +1,71 @@ +// Copyright (c) 2015 Uber Technologies, Inc. All rights reserved. +// @author Seung-Yeoul Yang (syyang@uber.com) + +package com.uber.kafka.tools; + +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Collections; +import java.util.List; + +import com.google.common.base.Preconditions; +import com.google.common.collect.Lists; +import com.google.common.io.Files; + +/** + * Represents a list of latest Kafka offsets in increasing order. The latest offsets for every + * topic are periodically written to a file on every leaf Kafka host by a cron job. The file is + * encoded as two parallel timestamp and offsets arrays. Timestamps are encoded as 4-bytes + * little endian unsigned ints, and offsets 8-bytes little endian unsigned longs. + * + * See https://code.uberinternal.com/diffusion/SO/browse/master/sortsol/meta_client.py for + * more details. + */ +public class OffsetIndex { + + public static final long LATEST_OFFSET = -2; + + private static final int SIZE_OF_INT = 4; + private static final int SIZE_OF_LONG = 8; + + private final List offsets; + + public OffsetIndex(byte[] encodedOffsets) { + Preconditions.checkArgument(encodedOffsets.length > 0, "Encoded offsets are empty"); + Preconditions.checkArgument(encodedOffsets.length % 3 == 0, "Invalid encoded offsets"); + final int size = encodedOffsets.length / 3 / SIZE_OF_INT; + + offsets = Lists.newArrayListWithCapacity(size); + + // Skip timestamps since we don't use them. + int bufOffset = SIZE_OF_INT * size; + int bufLen = SIZE_OF_LONG * size; + ByteBuffer buffer = ByteBuffer.wrap(encodedOffsets, bufOffset, bufLen) + .order(ByteOrder.LITTLE_ENDIAN); + for (int i = 0; i < size; i++) { + long offset = buffer.getLong(); + offsets.add(offset); + } + Collections.sort(offsets); + } + + public long getNextOffset(long offset) { + int idx = Collections.binarySearch(offsets, offset); + if (idx >= 0) { + return offsets.get(idx + 1); + } + int insertionPoint = (idx + 1) * -1; + return insertionPoint >= offsets.size() ? LATEST_OFFSET : offsets.get(insertionPoint); + } + + public static OffsetIndex load(String path) { + try { + byte[] encodedOffsets = Files.toByteArray(new File(path)); + return new OffsetIndex(encodedOffsets); + } catch (IOException e) { + throw new RuntimeException("Failed to read encoded bytes from " + path, e); + } + } +} diff --git a/migration/src/test/java/com/uber/kafka/tools/Kafka7OffsetFixerTest.java b/migration/src/test/java/com/uber/kafka/tools/Kafka7OffsetFixerTest.java new file mode 100644 index 0000000000000..7ee14bdf3ce83 --- /dev/null +++ b/migration/src/test/java/com/uber/kafka/tools/Kafka7OffsetFixerTest.java @@ -0,0 +1,112 @@ +// Copyright (c) 2015 Uber Technologies, Inc. All rights reserved. +// @author Seung-Yeoul Yang (syyang@uber.com) + +package com.uber.kafka.tools; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +import org.I0Itec.zkclient.ZkClient; +import org.apache.commons.io.FileUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import com.google.common.collect.ImmutableList; +import com.google.common.io.Files; +import com.uber.kafka.tools.OffsetIndexTestUtil.Offset; + +/** + * Tests for {@link com.uber.kafka.tools.Kafka7OffsetFixer} + */ +public class Kafka7OffsetFixerTest { + + private static final String TEST_CONSUMER_GROUP = "test_fixer_consumer"; + private static final String TEST_TOPIC = "test_fixer_topic"; + + private Kafka7LatestOffsets latestOffsets; + private ZkClient zkClient; + private File mirrorkMakerDir; + private String mirrorMakerPath; + private String currentOffsetZkPath; + private List offsets; + private byte[] offsetsBytes; + + @Before + public void setUp() throws Exception { + latestOffsets = mock(Kafka7LatestOffsets.class); + zkClient = mock(ZkClient.class); + mirrorkMakerDir = Files.createTempDir(); + mirrorMakerPath = mirrorkMakerDir.getPath(); + currentOffsetZkPath = Kafka7OffsetFixer.getCurrentOffsetZkPath( + TEST_CONSUMER_GROUP, TEST_TOPIC); + + // Set up offset index file. + offsets = ImmutableList.of( + new Offset(3, 30L), + new Offset(4, 40L), + new Offset(5, 50L), + new Offset(1, 10L), + new Offset(2, 20L) + ); + offsetsBytes = OffsetIndexTestUtil.toByteArray(offsets); + File offsetIndexFile = new File(mirrorkMakerDir, TEST_TOPIC + "_0"); + Files.write(offsetsBytes, offsetIndexFile); + } + + @After + public void tearDown() throws IOException { + // Clean up offset index file. + FileUtils.deleteDirectory(mirrorkMakerDir); + } + + @Test + public void testBasic() { + final long CURRENT_OFFSET = 25L; + + // Mock zkClient + when(zkClient.exists(currentOffsetZkPath)).thenReturn(true); + byte[] currentOffsetBytes = Long.toString(CURRENT_OFFSET).getBytes(); + when(zkClient.readData(currentOffsetZkPath)).thenReturn(currentOffsetBytes); + + // Run fixer + Kafka7OffsetFixer fixer = new Kafka7OffsetFixer( + latestOffsets, mirrorMakerPath, zkClient); + fixer.fixOffset(TEST_CONSUMER_GROUP, TEST_TOPIC); + + // Verify that the new offset should be 30 since it's the next + // biggest offset after the current offset 25 in the offset index. + byte[] newOffset = Long.toString(30L).getBytes(); + verify(zkClient).writeData(currentOffsetZkPath, newOffset); + } + + @Test + public void testFixWithLatestOffset() { + final long LATEST_OFFSET = 60L; + final long CURRENT_OFFSET = 55L; + + // Mock zkClient + when(zkClient.exists(currentOffsetZkPath)).thenReturn(true); + byte[] currentOffsetBytes = Long.toString(CURRENT_OFFSET).getBytes(); + when(zkClient.readData(currentOffsetZkPath)).thenReturn(currentOffsetBytes); + + // Mock Kafka7LatestOffsets + when(latestOffsets.get(TEST_TOPIC, 0)).thenReturn(LATEST_OFFSET); + + // Run fixer + Kafka7OffsetFixer fixer = new Kafka7OffsetFixer( + latestOffsets, mirrorMakerPath, zkClient); + fixer.fixOffset(TEST_CONSUMER_GROUP, TEST_TOPIC); + + // Verify that the new offset is latest offset since the current + // offset 55 is bigger than any other offsets in the offset index. + byte[] newOffset = Long.toString(LATEST_OFFSET).getBytes(); + verify(zkClient).writeData(currentOffsetZkPath, newOffset); + } + +} diff --git a/migration/src/test/java/com/uber/kafka/tools/KafkaMigrationToolTest.java b/migration/src/test/java/com/uber/kafka/tools/KafkaMigrationToolTest.java deleted file mode 100644 index 0155ed13cc490..0000000000000 --- a/migration/src/test/java/com/uber/kafka/tools/KafkaMigrationToolTest.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.uber.kafka.tools; - -import org.junit.Test; - -public class KafkaMigrationToolTest { - - @Test - public void testFoo() throws Exception { - // InputStream is = getClass().getResourceAsStream("/offset_idx"); - // byte[] encodedOffsets = ByteStreams.toByteArray(is); - // Assert.assertEquals(0, encodedOffsets.length); - } - -} - diff --git a/migration/src/test/java/com/uber/kafka/tools/MigrationUtilsTest.java b/migration/src/test/java/com/uber/kafka/tools/MigrationUtilsTest.java new file mode 100644 index 0000000000000..ade3793b97856 --- /dev/null +++ b/migration/src/test/java/com/uber/kafka/tools/MigrationUtilsTest.java @@ -0,0 +1,45 @@ +// Copyright (c) 2015 Uber Technologies, Inc. All rights reserved. +// @author Seung-Yeoul Yang (syyang@uber.com) + +package com.uber.kafka.tools; + +import static org.junit.Assert.assertEquals; + +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +import com.google.common.collect.ImmutableList; + +/** + * Tests for {@link com.uber.kafka.tools.MigrationUtils} + */ +public class MigrationUtilsTest { + + private static final String TEST_ZK_HOSTS = "localhost:2182"; + private static final List KAFAK08_TOPICS = ImmutableList.of("a", "b", "c"); + + private MigrationUtils utils; + + @Before + public void setUp() { + utils = new MigrationUtils() { + @Override + public List getAllTopicsInKafka08(String kafka08ZKHosts) { + return KAFAK08_TOPICS; + } + }; + } + + @Test + public void testRewriteWhitelist() { + assertEquals("a", utils.rewriteTopicWhitelist(TEST_ZK_HOSTS, "a|d")); + } + + @Test + public void testRewriteBlacklist() { + assertEquals("b|c", utils.rewriteTopicBlacklist(TEST_ZK_HOSTS, "a|d")); + } + +} diff --git a/migration/src/test/java/com/uber/kafka/tools/OffsetIndexTest.java b/migration/src/test/java/com/uber/kafka/tools/OffsetIndexTest.java new file mode 100644 index 0000000000000..2c58208bbcbad --- /dev/null +++ b/migration/src/test/java/com/uber/kafka/tools/OffsetIndexTest.java @@ -0,0 +1,63 @@ +// Copyright (c) 2015 Uber Technologies, Inc. All rights reserved. +// @author Seung-Yeoul Yang (syyang@uber.com) + +package com.uber.kafka.tools; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +import com.google.common.collect.ImmutableList; +import com.uber.kafka.tools.OffsetIndexTestUtil.Offset; + +/** + * Tests for {@link com.uber.kafka.tools.OffsetIndex}. + **/ +public class OffsetIndexTest { + + private List offsets; + private byte[] offsetsBytes; + + @Before + public void setUp() { + offsets = ImmutableList.of( + new Offset(3, 30L), + new Offset(4, 40L), + new Offset(5, 50L), + new Offset(1, 10L), + new Offset(2, 20L) + ); + offsetsBytes = OffsetIndexTestUtil.toByteArray(offsets); + } + + @Test + public void testSimple() { + OffsetIndex index = new OffsetIndex(offsetsBytes); + assertEquals(10L, index.getNextOffset(5L)); + assertEquals(30L, index.getNextOffset(20L)); + assertEquals(50L, index.getNextOffset(43L)); + assertEquals(OffsetIndex.LATEST_OFFSET, index.getNextOffset(55L)); + } + + @Test + public void testEmptyOffsetIndex() { + try { + OffsetIndex index = new OffsetIndex(new byte[]{}); + fail("Loading empty encoded offsets should fail"); + } catch (IllegalArgumentException e) { + // Expected. + } + } + + @Test + public void testLoadFromFile() { + String path = getClass().getResource("/offset_idx").getPath(); + OffsetIndex index = OffsetIndex.load(path); + assertEquals(9777L, index.getNextOffset(0L)); + } + +} diff --git a/migration/src/test/java/com/uber/kafka/tools/OffsetIndexTestUtil.java b/migration/src/test/java/com/uber/kafka/tools/OffsetIndexTestUtil.java new file mode 100644 index 0000000000000..26e9c250d605a --- /dev/null +++ b/migration/src/test/java/com/uber/kafka/tools/OffsetIndexTestUtil.java @@ -0,0 +1,39 @@ +// Copyright (c) 2015 Uber Technologies, Inc. All rights reserved. +// @author Seung-Yeoul Yang (syyang@uber.com) + +package com.uber.kafka.tools; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.List; + +public class OffsetIndexTestUtil { + + private static final int SIZE_OF_INT = 4; + private static final int SIZE_OF_LONG = 8; + + public static class Offset { + public final int ts; + public final long offset; + + public Offset(int ts, long offset) { + this.ts = ts; + this.offset = offset; + } + } + + public static byte[] toByteArray(List offsets) { + int bufLen = offsets.size() * (SIZE_OF_INT + SIZE_OF_LONG); + byte[] encodedBytes = new byte[bufLen]; + ByteBuffer buf = ByteBuffer.wrap(encodedBytes).order(ByteOrder.LITTLE_ENDIAN); + for (Offset offset : offsets) { + buf.putInt(offset.ts); + } + for (Offset offset : offsets) { + buf.putLong(offset.offset); + } + return encodedBytes; + } + + private OffsetIndexTestUtil() {} +} From d6a25ef7e74f5cf74346700e80e05e6a9f6370c4 Mon Sep 17 00:00:00 2001 From: Seung-Yeoul Yang Date: Wed, 18 Mar 2015 18:12:08 -0700 Subject: [PATCH 12/14] [kafka] better error recovery in migration tool (part 2) Summary: * check topic whiltelist/blacklist before running the migration tool * fix corrupted offsets upon detection * fail fast on erorrs and sigterm Test plan: * Added unit tests * Manually tested that corrupted offsets are fixed Reviewers: norbert, grk, praveen, csoman, vinoth Subscribers: nharkins, bigpunk Maniphest Tasks: T67641 Differential Revision: https://code.uberinternal.com/D84682 --- .../java/com/uber/kafka/tools/KafkaMigrationTool.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/migration/src/main/java/com/uber/kafka/tools/KafkaMigrationTool.java b/migration/src/main/java/com/uber/kafka/tools/KafkaMigrationTool.java index dcd18455ac52e..625f3a82dbea9 100644 --- a/migration/src/main/java/com/uber/kafka/tools/KafkaMigrationTool.java +++ b/migration/src/main/java/com/uber/kafka/tools/KafkaMigrationTool.java @@ -316,7 +316,11 @@ public void run() { }); // start consumer threads +<<<<<<< HEAD logger.info("Starting " + numConsumers + " threads"); +======= + logger.info("Starting " + numConsumers + " migration threads"); +>>>>>>> [kafka] better error recovery in migration tool (part 2) for(Object stream: (List)retKafkaStreams) { MigrationThread thread = new MigrationThread( context, stream, producerDataChannel, threadId); @@ -425,6 +429,7 @@ private static class MigrationThread extends Thread { public void run() { try { + int count = 0; Method MessageGetPayloadMethod_07 = KafkaMessageClass_07.getMethod("payload"); Method KafkaGetMessageMethod_07 = KafkaMessageAndMetatDataClass_07.getMethod("message"); Method KafkaGetTopicMethod_07 = KafkaMessageAndMetatDataClass_07.getMethod("topic"); @@ -441,11 +446,11 @@ public void run() { int size = ((ByteBuffer)payload_07).remaining(); byte[] bytes = new byte[size]; ((ByteBuffer)payload_07).get(bytes); - if(logger.isDebugEnabled()) { - logger.debug("Migration thread " + threadId + " sending message of size " + + if(count % 100 == 0) { + logger.info("Migration thread " + threadId + " sending message of size " + bytes.length + " to topic " + topic); } - + count++; KeyedMessage producerData = new KeyedMessage((String)topic, null, bytes); producerDataChannel.sendRequest(producerData); } From d257ad6bef663693c3de96562189fdf83a444ed8 Mon Sep 17 00:00:00 2001 From: Seung-Yeoul Yang Date: Mon, 23 Mar 2015 18:13:32 -0700 Subject: [PATCH 13/14] foobar --- foo | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 foo diff --git a/foo b/foo new file mode 100644 index 0000000000000..e69de29bb2d1d From ccb63bd8940f9c02416e4a66925d7d048f1fc3a8 Mon Sep 17 00:00:00 2001 From: Seung-Yeoul Yang Date: Mon, 23 Mar 2015 18:13:32 -0700 Subject: [PATCH 14/14] foobar --- .../main/java/com/uber/kafka/tools/KafkaMigrationTool.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/migration/src/main/java/com/uber/kafka/tools/KafkaMigrationTool.java b/migration/src/main/java/com/uber/kafka/tools/KafkaMigrationTool.java index 625f3a82dbea9..cc059d5429200 100644 --- a/migration/src/main/java/com/uber/kafka/tools/KafkaMigrationTool.java +++ b/migration/src/main/java/com/uber/kafka/tools/KafkaMigrationTool.java @@ -316,11 +316,7 @@ public void run() { }); // start consumer threads -<<<<<<< HEAD - logger.info("Starting " + numConsumers + " threads"); -======= logger.info("Starting " + numConsumers + " migration threads"); ->>>>>>> [kafka] better error recovery in migration tool (part 2) for(Object stream: (List)retKafkaStreams) { MigrationThread thread = new MigrationThread( context, stream, producerDataChannel, threadId);