Author: slaurent Date: Thu Dec 9 22:11:27 2010 New Revision: 1044145 URL: http://svn.apache.org/viewvc?rev=1044145&view=rev Log: bug 49159: Improve ThreadLocal memory leak clean-up https://issues.apache.org/bugzilla/show_bug.cgi?id=49159
Use a dedicated thread when calling web application code when it is started and stopped (calls to Listeners, Filters, Servlets). Added: tomcat/trunk/java/org/apache/tomcat/util/threads/DedicatedThreadExecutor.java (with props) tomcat/trunk/test/org/apache/tomcat/util/threads/ tomcat/trunk/test/org/apache/tomcat/util/threads/DedicatedThreadExecutorTest.java (with props) Modified: tomcat/trunk/java/org/apache/catalina/core/StandardContext.java Modified: tomcat/trunk/java/org/apache/catalina/core/StandardContext.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/core/StandardContext.java?rev=1044145&r1=1044144&r2=1044145&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/catalina/core/StandardContext.java (original) +++ tomcat/trunk/java/org/apache/catalina/core/StandardContext.java Thu Dec 9 22:11:27 2010 @@ -37,6 +37,7 @@ import java.util.Map; import java.util.Set; import java.util.Stack; import java.util.TreeMap; +import java.util.concurrent.Callable; import javax.management.ListenerNotFoundException; import javax.management.MBeanNotificationInfo; @@ -117,6 +118,7 @@ import org.apache.tomcat.JarScanner; import org.apache.tomcat.util.ExceptionUtils; import org.apache.tomcat.util.modeler.Registry; import org.apache.tomcat.util.scan.StandardJarScanner; +import org.apache.tomcat.util.threads.DedicatedThreadExecutor; /** * Standard implementation of the <b>Context</b> interface. Each @@ -4966,6 +4968,7 @@ public class StandardContext extends Con } } + DedicatedThreadExecutor temporaryExecutor = new DedicatedThreadExecutor(); try { // Create context attributes that will be required @@ -4992,7 +4995,21 @@ public class StandardContext extends Con // Configure and call application event listeners if (ok) { - if (!listenerStart()) { + // we do it in a dedicated thread for memory leak protection, in + // case the Listeners registers some ThreadLocals that they + // forget to cleanup + Boolean listenerStarted = + temporaryExecutor.execute(new Callable<Boolean>() { + public Boolean call() throws Exception { + ClassLoader old = bindThread(); + try { + return listenerStart(); + } finally { + unbindThread(old); + } + } + }); + if (!listenerStarted) { log.error( "Error listenerStart"); ok = false; } @@ -5013,20 +5030,48 @@ public class StandardContext extends Con // Configure and call application filters if (ok) { - if (!filterStart()) { - log.error( "Error filterStart"); + // we do it in a dedicated thread for memory leak protection, in + // case the Filters register some ThreadLocals that they forget + // to cleanup + Boolean filterStarted = + temporaryExecutor.execute(new Callable<Boolean>() { + public Boolean call() throws Exception { + ClassLoader old = bindThread(); + try { + return filterStart(); + } finally { + unbindThread(old); + } + } + }); + if (!filterStarted) { + log.error("Error filterStart"); ok = false; } } // Load and initialize all "load on startup" servlets if (ok) { - loadOnStartup(findChildren()); + // we do it in a dedicated thread for memory leak protection, in + // case the Servlets register some ThreadLocals that they forget + // to cleanup + temporaryExecutor.execute(new Callable<Void>() { + public Void call() throws Exception { + ClassLoader old = bindThread(); + try { + loadOnStartup(findChildren()); + return null; + } finally { + unbindThread(old); + } + } + }); } } finally { // Unbinding thread unbindThread(oldCCL); + temporaryExecutor.shutdown(); } // Set available status depending upon startup success @@ -5159,23 +5204,38 @@ public class StandardContext extends Con try { // Stop our child containers, if any - Container[] children = findChildren(); - for (int i = 0; i < children.length; i++) { - children[i].stop(); - } - - // Stop our filters - filterStop(); - - // Stop ContainerBackgroundProcessor thread - super.threadStop(); - - if ((manager != null) && (manager instanceof Lifecycle)) { - ((Lifecycle) manager).stop(); - } - - // Stop our application listeners - listenerStop(); + final Container[] children = findChildren(); + // we do it in a dedicated thread for memory leak protection, in + // case some webapp code registers some ThreadLocals that they + // forget to cleanup + DedicatedThreadExecutor.executeInOwnThread( + new Callable<Void>() { + public Void call() throws Exception { + ClassLoader old = bindThread(); + try { + for (int i = 0; i < children.length; i++) { + children[i].stop(); + } + + // Stop our filters + filterStop(); + + // Stop ContainerBackgroundProcessor thread + threadStop(); + + if ((manager != null) && + (manager instanceof Lifecycle)) { + ((Lifecycle) manager).stop(); + } + + // Stop our application listeners + listenerStop(); + return null; + }finally{ + unbindThread(old); + } + } + }); // Finalize our character set mapper setCharsetMapper(null); Added: tomcat/trunk/java/org/apache/tomcat/util/threads/DedicatedThreadExecutor.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/tomcat/util/threads/DedicatedThreadExecutor.java?rev=1044145&view=auto ============================================================================== --- tomcat/trunk/java/org/apache/tomcat/util/threads/DedicatedThreadExecutor.java (added) +++ tomcat/trunk/java/org/apache/tomcat/util/threads/DedicatedThreadExecutor.java Thu Dec 9 22:11:27 2010 @@ -0,0 +1,137 @@ +/* + * 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.tomcat.util.threads; + +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ThreadFactory; + +/** + * A utility class to execute a {...@link Callable} in a dedicated thread. + * It can be used either with an instance to reuse the same thread for each call + * to {...@link #execute(Callable)} or with the static method + * {...@link #executeInOwnThread(Callable)}. When using an instance, + * {...@link #shutdown()} must be called when the instance is no longer needed to + * dispose of the dedicated thread. + */ +public class DedicatedThreadExecutor { + private final SingleThreadFactory singleThreadFactory = + new SingleThreadFactory(); + private final ExecutorService executorService = + Executors.newSingleThreadExecutor(singleThreadFactory); + + /** + * Executes the given {...@link Callable} with the thread spawned for the + * current {...@link DedicatedThreadExecutor} instance, and returns its result. + * + * @param <V> + * the type of the returned value + * @param callable + * @return + */ + public <V> V execute(final Callable<V> callable) { + final Future<V> futureTask = executorService.submit(callable); + + boolean interrupted = false; + V result; + while (true) { + try { + result = futureTask.get(); + break; + } catch (InterruptedException e) { + // keep waiting + interrupted = true; + } catch (ExecutionException e) { + throw new RuntimeException(e); + } + } + if (interrupted) { + // set interruption status so that the caller may react to it + Thread.currentThread().interrupt(); + } + return result; + } + + /** + * Stops the dedicated thread and waits for its death. + */ + public void shutdown() { + executorService.shutdown(); + if (singleThreadFactory.singleThread != null) { + boolean interrupted = false; + while (true) { + try { + singleThreadFactory.singleThread.join(); + singleThreadFactory.singleThread = null; + break; + } catch (InterruptedException e) { + // keep waiting + interrupted = true; + } + } + if (interrupted) { + // set interruption status so that the caller may react to it + Thread.currentThread().interrupt(); + } + } + } + + /** + * Executes the given {...@link Callable} in a new thread and returns the + * result after the thread is stopped. + * + * @param <V> + * @param callable + * @return + */ + public static <V> V executeInOwnThread( + final Callable<V> callable) { + DedicatedThreadExecutor executor = new DedicatedThreadExecutor(); + try { + return executor.execute(callable); + } finally { + executor.shutdown(); + } + + } + + // we use a ThreadFactory so that we can later call Thread.join(). + // Indeed, calling shutdown() on an ExecutorService will eventually stop the + // thread but it might still be alive when execute() returns (race + // condition). + // This can lead to false alarms about potential memory leaks because the + // thread may have a web application class loader for its context class + // loader. + private class SingleThreadFactory implements ThreadFactory { + private volatile Thread singleThread; + + @Override + public Thread newThread(Runnable r) { + if (singleThread != null) { + throw new IllegalStateException( + "should not have been called more than once"); + } + singleThread = new Thread(r); + singleThread.setDaemon(true); + return singleThread; + } + + } +} Propchange: tomcat/trunk/java/org/apache/tomcat/util/threads/DedicatedThreadExecutor.java ------------------------------------------------------------------------------ svn:eol-style = native Propchange: tomcat/trunk/java/org/apache/tomcat/util/threads/DedicatedThreadExecutor.java ------------------------------------------------------------------------------ svn:keywords = Author Date Id Revision Added: tomcat/trunk/test/org/apache/tomcat/util/threads/DedicatedThreadExecutorTest.java URL: http://svn.apache.org/viewvc/tomcat/trunk/test/org/apache/tomcat/util/threads/DedicatedThreadExecutorTest.java?rev=1044145&view=auto ============================================================================== --- tomcat/trunk/test/org/apache/tomcat/util/threads/DedicatedThreadExecutorTest.java (added) +++ tomcat/trunk/test/org/apache/tomcat/util/threads/DedicatedThreadExecutorTest.java Thu Dec 9 22:11:27 2010 @@ -0,0 +1,70 @@ +/* + * 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.tomcat.util.threads; + +import java.util.concurrent.Callable; + +import junit.framework.TestCase; + +public class DedicatedThreadExecutorTest extends TestCase { + private Thread dedicatedThread; + + public void testExecute() { + final Thread testingThread = Thread.currentThread(); + DedicatedThreadExecutor executor = new DedicatedThreadExecutor(); + Long result = executor.execute(new Callable<Long>() { + @Override + public Long call() throws Exception { + dedicatedThread = Thread.currentThread(); + DedicatedThreadExecutorTest.assertNotSame(testingThread, + dedicatedThread); + return 123L; + } + }); + assertEquals(123, result.longValue()); + + //check that the same thread is reused + executor.execute(new Callable<Void>() { + @Override + public Void call() throws Exception { + DedicatedThreadExecutorTest.assertSame(dedicatedThread, + Thread.currentThread()); + return null; + } + }); + + executor.shutdown(); + assertFalse(dedicatedThread.isAlive()); + } + + public void testExecuteInOwnThread() { + final Thread testingThread = Thread.currentThread(); + Long result = + DedicatedThreadExecutor.executeInOwnThread(new Callable<Long>() { + @Override + public Long call() throws Exception { + dedicatedThread = Thread.currentThread(); + DedicatedThreadExecutorTest.assertNotSame(testingThread, + dedicatedThread); + return 456L; + } + }); + assertEquals(456, result.longValue()); + assertFalse(dedicatedThread.isAlive()); + } + +} Propchange: tomcat/trunk/test/org/apache/tomcat/util/threads/DedicatedThreadExecutorTest.java ------------------------------------------------------------------------------ svn:eol-style = native Propchange: tomcat/trunk/test/org/apache/tomcat/util/threads/DedicatedThreadExecutorTest.java ------------------------------------------------------------------------------ svn:keywords = Author Date Id Revision --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org