diff --git a/buildtools/pom.xml b/buildtools/pom.xml
index 183e2f946e4d2..3922aaefc22cb 100644
--- a/buildtools/pom.xml
+++ b/buildtools/pom.xml
@@ -37,28 +37,70 @@
1.81.8
+ 3.0.0-M3
+ 2.14.0
+ 1.7.25
+ 7.3.0
+ 3.11
+ 3.2.4
+
+
+
+ org.apache.logging.log4j
+ log4j-bom
+ ${log4j2.version}
+ pom
+ import
+
+
+
+
org.testngtestng
- 7.3.0
+ ${testng.version}org.apache.logging.log4jlog4j-api
- 2.14.0org.apache.logging.log4jlog4j-core
- 2.14.0org.apache.logging.log4jlog4j-slf4j-impl
- 2.14.0
+
+
+ org.apache.logging.log4j
+ log4j-1.2-api
+
+
+ org.slf4j
+ jcl-over-slf4j
+ ${slf4j.version}
+
+
+ org.apache.commons
+ commons-lang3
+ ${commons-lang3.version}
+
+
+ commons-logging
+ *
+
+
+
+
+
+ io.netty
+ netty-common
+ 4.1.60.Final
+ test
@@ -79,6 +121,45 @@
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ ${maven-shade-plugin.version}
+
+ true
+ true
+ false
+
+
+ org.apache.commons:commons-lang3
+
+
+
+
+ org.apache.commons.lang3
+ org.apache.pulsar.buildtools.shaded.org.apache.commons.lang3
+
+
+
+
+
+ package
+
+ shade
+
+
+
+
+
+ org.apache.rat
+ apache-rat-plugin
+
+
+
+ dependency-reduced-pom.xml
+
+
+
diff --git a/buildtools/src/main/java/org/apache/pulsar/tests/AnnotationListener.java b/buildtools/src/main/java/org/apache/pulsar/tests/AnnotationListener.java
index 7a62fa914354d..b2719b239a69f 100644
--- a/buildtools/src/main/java/org/apache/pulsar/tests/AnnotationListener.java
+++ b/buildtools/src/main/java/org/apache/pulsar/tests/AnnotationListener.java
@@ -24,6 +24,7 @@
import org.testng.IAnnotationTransformer;
import org.testng.annotations.ITestAnnotation;
+import org.testng.internal.annotations.DisabledRetryAnalyzer;
public class AnnotationListener implements IAnnotationTransformer {
@@ -38,7 +39,9 @@ public void transform(ITestAnnotation annotation,
Class testClass,
Constructor testConstructor,
Method testMethod) {
- annotation.setRetryAnalyzer(RetryAnalyzer.class);
+ if (annotation.getRetryAnalyzerClass() == DisabledRetryAnalyzer.class) {
+ annotation.setRetryAnalyzer(RetryAnalyzer.class);
+ }
// Enforce default test timeout
if (annotation.getTimeOut() == 0) {
diff --git a/buildtools/src/main/java/org/apache/pulsar/tests/BetweenTestClassesListenerAdapter.java b/buildtools/src/main/java/org/apache/pulsar/tests/BetweenTestClassesListenerAdapter.java
new file mode 100644
index 0000000000000..7e803f29ef16a
--- /dev/null
+++ b/buildtools/src/main/java/org/apache/pulsar/tests/BetweenTestClassesListenerAdapter.java
@@ -0,0 +1,60 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pulsar.tests;
+
+import org.testng.IClassListener;
+import org.testng.ITestClass;
+import org.testng.ITestContext;
+import org.testng.ITestListener;
+
+/**
+ * TestNG listener adapter for detecting when execution finishes in previous
+ * test class and starts in a new class.
+ */
+abstract class BetweenTestClassesListenerAdapter implements IClassListener, ITestListener {
+ Class> lastTestClass;
+
+ @Override
+ public void onBeforeClass(ITestClass testClass) {
+ checkIfTestClassChanged(testClass.getRealClass());
+ }
+
+ private void checkIfTestClassChanged(Class> testClazz) {
+ if (lastTestClass != testClazz) {
+ onBetweenTestClasses(lastTestClass, testClazz);
+ lastTestClass = testClazz;
+ }
+ }
+
+ @Override
+ public void onFinish(ITestContext context) {
+ if (lastTestClass != null) {
+ onBetweenTestClasses(lastTestClass, null);
+ lastTestClass = null;
+ }
+ }
+
+ /**
+ * Call back hook for adding logic when test execution moves from test class to another.
+ *
+ * @param endedTestClass the test class which has finished execution. null if the started test class is the first
+ * @param startedTestClass the test class which has started execution. null if the ended test class is the last
+ */
+ protected abstract void onBetweenTestClasses(Class> endedTestClass, Class> startedTestClass);
+}
diff --git a/buildtools/src/main/java/org/apache/pulsar/tests/FailFastNotifier.java b/buildtools/src/main/java/org/apache/pulsar/tests/FailFastNotifier.java
new file mode 100644
index 0000000000000..12246773c1411
--- /dev/null
+++ b/buildtools/src/main/java/org/apache/pulsar/tests/FailFastNotifier.java
@@ -0,0 +1,85 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pulsar.tests;
+
+import org.testng.IInvokedMethod;
+import org.testng.IInvokedMethodListener;
+import org.testng.ITestResult;
+import org.testng.SkipException;
+
+/**
+ * Notifies TestNG core skipping remaining tests after first failure has appeared.
+ *
+ * Enabled when -DtestFailFast=true
+ *
+ * This is a workaround for https://issues.apache.org/jira/browse/SUREFIRE-1762 since
+ * the bug makes the built-in fast-fast feature `-Dsurefire.skipAfterFailureCount=1` unusable.
+ * Maven Surefire version 3.0.0-M5 contains the fix, but that version is unusable because of problems
+ * with test output, https://issues.apache.org/jira/browse/SUREFIRE-1827.
+ * It makes the Pulsar integration tests slow and to fail.
+ *
+ * This implementation is based on org.apache.maven.surefire.testng.utils.FailFastNotifier
+ * implementation that is part of the Maven Surefire plugin.
+ *
+ */
+public class FailFastNotifier
+ implements IInvokedMethodListener {
+ private static final boolean FAIL_FAST_ENABLED = Boolean.valueOf(
+ System.getProperty("testFailFast", "true"));
+
+ static class FailFastEventsSingleton {
+ private static final FailFastEventsSingleton INSTANCE = new FailFastEventsSingleton();
+
+ private volatile boolean skipAfterFailure;
+
+ private FailFastEventsSingleton() {
+ }
+
+ public static FailFastEventsSingleton getInstance() {
+ return INSTANCE;
+ }
+
+ public boolean isSkipAfterFailure() {
+ return skipAfterFailure;
+ }
+
+ public void setSkipOnNextTest() {
+ this.skipAfterFailure = true;
+ }
+ }
+
+ static class FailFastSkipException extends SkipException {
+ FailFastSkipException(String skipMessage) {
+ super(skipMessage);
+ reduceStackTrace();
+ }
+ }
+
+ @Override
+ public void beforeInvocation(IInvokedMethod iInvokedMethod, ITestResult iTestResult) {
+ if (FAIL_FAST_ENABLED && FailFastEventsSingleton.getInstance().isSkipAfterFailure()) {
+ throw new FailFastSkipException("Skipped after failure since testFailFast system property is set.");
+ }
+ }
+
+ @Override
+ public void afterInvocation(IInvokedMethod iInvokedMethod, ITestResult iTestResult) {
+
+ }
+}
diff --git a/buildtools/src/main/java/org/apache/pulsar/tests/FastThreadLocalCleanupListener.java b/buildtools/src/main/java/org/apache/pulsar/tests/FastThreadLocalCleanupListener.java
new file mode 100644
index 0000000000000..cd75bfbd4318e
--- /dev/null
+++ b/buildtools/src/main/java/org/apache/pulsar/tests/FastThreadLocalCleanupListener.java
@@ -0,0 +1,61 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pulsar.tests;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Cleanup Thread Local state attach to Netty's FastThreadLocal.
+ */
+public class FastThreadLocalCleanupListener extends BetweenTestClassesListenerAdapter {
+ private static final Logger LOG = LoggerFactory.getLogger(FastThreadLocalCleanupListener.class);
+ private static final boolean FAST_THREAD_LOCAL_CLEANUP_ENABLED =
+ Boolean.valueOf(System.getProperty("testFastThreadLocalCleanup", "true"));
+ private static final String FAST_THREAD_LOCAL_CLEANUP_PACKAGE =
+ System.getProperty("testFastThreadLocalCleanupPackage", "org.apache.pulsar");
+ private static final FastThreadLocalStateCleaner CLEANER = new FastThreadLocalStateCleaner(object -> {
+ if ("*".equals(FAST_THREAD_LOCAL_CLEANUP_PACKAGE)) {
+ return true;
+ }
+ Class> clazz = object.getClass();
+ if (clazz.isArray()) {
+ clazz = clazz.getComponentType();
+ }
+ Package pkg = clazz.getPackage();
+ if (pkg != null && pkg.getName() != null) {
+ return pkg.getName()
+ .startsWith(FAST_THREAD_LOCAL_CLEANUP_PACKAGE);
+ } else {
+ return false;
+ }
+ });
+
+ @Override
+ protected void onBetweenTestClasses(Class> endedTestClass, Class> startedTestClass) {
+ if (FAST_THREAD_LOCAL_CLEANUP_ENABLED && FastThreadLocalStateCleaner.isEnabled()) {
+ LOG.info("Cleaning up FastThreadLocal thread local state.");
+ CLEANER.cleanupAllFastThreadLocals((thread, value) -> {
+ LOG.info("Cleaning FastThreadLocal state for thread {}, instance of class {}, value is {}", thread,
+ value.getClass().getName(), value);
+ });
+ }
+ }
+
+}
diff --git a/buildtools/src/main/java/org/apache/pulsar/tests/FastThreadLocalStateCleaner.java b/buildtools/src/main/java/org/apache/pulsar/tests/FastThreadLocalStateCleaner.java
new file mode 100644
index 0000000000000..b16c7b85792f8
--- /dev/null
+++ b/buildtools/src/main/java/org/apache/pulsar/tests/FastThreadLocalStateCleaner.java
@@ -0,0 +1,141 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pulsar.tests;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Objects;
+import java.util.function.BiConsumer;
+import java.util.function.Predicate;
+import org.apache.commons.lang3.ClassUtils;
+import org.apache.commons.lang3.ThreadUtils;
+import org.apache.commons.lang3.reflect.FieldUtils;
+import org.apache.commons.lang3.reflect.MethodUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Cleanup Thread Local state attach to Netty's FastThreadLocal.
+ *
+ * This is not thread-safe, but that aspect is ignored.
+ */
+public final class FastThreadLocalStateCleaner {
+ private static final Logger LOG = LoggerFactory.getLogger(FastThreadLocalStateCleaner.class);
+ private static final ThreadLocal> SLOW_THREAD_LOCAL_MAP = lookupSlowThreadLocalMap();
+ private static final Class> FAST_THREAD_LOCAL_CLASS;
+ private static final Method GET_THREAD_LOCAL_MAP;
+ private static final Field INDEXED_VARIABLES_FIELD;
+ private static final Object UNSET_OBJECT;
+
+ static {
+ Class> clazz = null;
+ Method getThreadLocalMapMethod = null;
+ Field indexedVariablesField = null;
+ Object unsetObject = null;
+ if (SLOW_THREAD_LOCAL_MAP != null) {
+ try {
+ clazz = ClassUtils.getClass("io.netty.util.concurrent.FastThreadLocalThread");
+ Class> internalThreadLocalMapClass =
+ ClassUtils.getClass("io.netty.util.internal.InternalThreadLocalMap");
+ getThreadLocalMapMethod = MethodUtils
+ .getMatchingAccessibleMethod(clazz, "threadLocalMap");
+ indexedVariablesField = FieldUtils.getDeclaredField(internalThreadLocalMapClass,
+ "indexedVariables", true);
+ Field unsetField = FieldUtils.getField(internalThreadLocalMapClass, "UNSET");
+ unsetObject = unsetField.get(null);
+ } catch (ClassNotFoundException | IllegalAccessException e) {
+ // ignore
+ LOG.debug("Ignoring exception", e);
+ clazz = null;
+ getThreadLocalMapMethod = null;
+ indexedVariablesField = null;
+ unsetObject = null;
+ }
+ }
+ FAST_THREAD_LOCAL_CLASS = clazz;
+ GET_THREAD_LOCAL_MAP = getThreadLocalMapMethod;
+ INDEXED_VARIABLES_FIELD = indexedVariablesField;
+ UNSET_OBJECT = unsetObject;
+ }
+
+ private final Predicate