/**
* 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.activemq.artemis.junit;
import java.lang.ref.WeakReference;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.apache.activemq.artemis.api.core.client.ActiveMQClient;
import org.apache.activemq.artemis.core.remoting.impl.invm.InVMConnector;
import org.jboss.logging.Logger;
import org.junit.Assert;
import org.junit.rules.ExternalResource;
/**
* Messaging tests are usually Thread intensive and a thread leak or a server leakage may affect future tests.
* This Rule will prevent Threads leaking from one test into another by checking left over threads.
* This will also clear Client Thread Pools from ActiveMQClient.
*/
public class ThreadLeakCheckRule extends ExternalResource {
private static Logger log = Logger.getLogger(ThreadLeakCheckRule.class);
private static Set<String> knownThreads = new HashSet<>();
boolean enabled = true;
private Map<Thread, StackTraceElement[]> previousThreads;
public void disable() {
enabled = false;
}
/**
* Override to set up your specific external resource.
*
* @throws Throwable if setup fails (which will disable {@code after})
*/
@Override
protected void before() throws Throwable {
// do nothing
previousThreads = Thread.getAllStackTraces();
}
/**
* Override to tear down your specific external resource.
*/
@Override
protected void after() {
ActiveMQClient.clearThreadPools();
InVMConnector.resetThreadPool();
try {
if (enabled) {
boolean failed = true;
boolean failedOnce = false;
long timeout = System.currentTimeMillis() + 60000;
while (failed && timeout > System.currentTimeMillis()) {
failed = checkThread();
if (failed) {
failedOnce = true;
forceGC();
try {
Thread.sleep(500);
} catch (Throwable e) {
}
}
}
if (failed) {
Assert.fail("Thread leaked");
} else if (failedOnce) {
System.out.println("******************** Threads cleared after retries ********************");
System.out.println();
}
} else {
enabled = true;
}
} finally {
// clearing just to help GC
previousThreads = null;
}
}
private static int failedGCCalls = 0;
public static void forceGC() {
if (failedGCCalls >= 10) {
log.info("ignoring forceGC call since it seems System.gc is not working anyways");
return;
}
log.info("#test forceGC");
CountDownLatch finalized = new CountDownLatch(1);
WeakReference<DumbReference> dumbReference = new WeakReference<>(new DumbReference(finalized));
long timeout = System.currentTimeMillis() + 1000;
// A loop that will wait GC, using the minimal time as possible
while (!(dumbReference.get() == null && finalized.getCount() == 0) && System.currentTimeMillis() < timeout) {
System.gc();
System.runFinalization();
try {
finalized.await(100, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
}
}
if (dumbReference.get() != null) {
failedGCCalls++;
log.info("It seems that GC is disabled at your VM");
} else {
// a success would reset the count
failedGCCalls = 0;
}
log.info("#test forceGC Done ");
}
public static void removeKownThread(String name) {
knownThreads.remove(name);
}
public static void addKownThread(String name) {
knownThreads.add(name);
}
private boolean checkThread() {
boolean failedThread = false;
Map<Thread, StackTraceElement[]> postThreads = Thread.getAllStackTraces();
if (postThreads != null && previousThreads != null && postThreads.size() > previousThreads.size()) {
for (Thread aliveThread : postThreads.keySet()) {
if (aliveThread.isAlive() && !isExpectedThread(aliveThread) && !previousThreads.containsKey(aliveThread)) {
if (!failedThread) {
System.out.println("*********************************************************************************");
System.out.println("LEAKING THREADS");
}
failedThread = true;
System.out.println("=============================================================================");
System.out.println("Thread " + aliveThread + " is still alive with the following stackTrace:");
StackTraceElement[] elements = postThreads.get(aliveThread);
for (StackTraceElement el : elements) {
System.out.println(el);
}
}
}
if (failedThread) {
System.out.println("*********************************************************************************");
}
}
return failedThread;
}
/**
* if it's an expected thread... we will just move along ignoring it
*
* @param thread
* @return
*/
private boolean isExpectedThread(Thread thread) {
for (String known : knownThreads) {
if (thread.getName().contains(known)) {
return true;
}
}
return false;
}
protected static class DumbReference {
private CountDownLatch finalized;
public DumbReference(CountDownLatch finalized) {
this.finalized = finalized;
}
@Override
public void finalize() throws Throwable {
finalized.countDown();
super.finalize();
}
}
}