You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tomcat.apache.org by ma...@apache.org on 2012/03/19 20:28:01 UTC

svn commit: r1302610 - in /tomcat/trunk: java/org/apache/catalina/loader/WebappClassLoader.java test/org/apache/catalina/loader/TestWebappClassLoaderThreadLocalMemoryLeak.java

Author: markt
Date: Mon Mar 19 19:28:01 2012
New Revision: 1302610

URL: http://svn.apache.org/viewvc?rev=1302610&view=rev
Log:
Refactor the ThreadLocal leak tests

Modified:
    tomcat/trunk/java/org/apache/catalina/loader/WebappClassLoader.java
    tomcat/trunk/test/org/apache/catalina/loader/TestWebappClassLoaderThreadLocalMemoryLeak.java

Modified: tomcat/trunk/java/org/apache/catalina/loader/WebappClassLoader.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/loader/WebappClassLoader.java?rev=1302610&r1=1302609&r2=1302610&view=diff
==============================================================================
--- tomcat/trunk/java/org/apache/catalina/loader/WebappClassLoader.java (original)
+++ tomcat/trunk/java/org/apache/catalina/loader/WebappClassLoader.java Mon Mar 19 19:28:01 2012
@@ -41,6 +41,7 @@ import java.security.Permission;
 import java.security.PermissionCollection;
 import java.security.Policy;
 import java.security.PrivilegedAction;
+import java.security.ProtectionDomain;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Enumeration;
@@ -1089,18 +1090,26 @@ public class WebappClassLoader
     // ---------------------------------------------------- ClassLoader Methods
 
 
-     /**
-      * Add the specified URL to the classloader.
-      */
+    /**
+     * Add the specified URL to the classloader.
+     */
     @Override
-     protected void addURL(URL url) {
-         super.addURL(url);
-         hasExternalRepositories = true;
-         repositoryURLs = null;
-     }
+    protected void addURL(URL url) {
+        super.addURL(url);
+        hasExternalRepositories = true;
+        repositoryURLs = null;
+    }
 
 
     /**
+     * Expose this method for use by the unit tests.
+     */
+    protected final Class<?> doDefineClass(String name, byte[] b, int off, int len,
+            ProtectionDomain protectionDomain) {
+        return super.defineClass(name, b, off, len, protectionDomain);
+    }
+
+    /**
      * Find the specified class in our local repositories, if possible.  If
      * not found, throw <code>ClassNotFoundException</code>.
      *
@@ -2568,6 +2577,16 @@ public class WebappClassLoader
             }
             cl = cl.getParent();
         }
+
+        if (o instanceof Collection<?>) {
+            Iterator<?> iter = ((Collection<?>) o).iterator();
+            while (iter.hasNext()) {
+                Object entry = iter.next();
+                if (loadedByThisOrChild(entry)) {
+                    return true;
+                }
+            }
+        }
         return false;
     }
 

Modified: tomcat/trunk/test/org/apache/catalina/loader/TestWebappClassLoaderThreadLocalMemoryLeak.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/test/org/apache/catalina/loader/TestWebappClassLoaderThreadLocalMemoryLeak.java?rev=1302610&r1=1302609&r2=1302610&view=diff
==============================================================================
--- tomcat/trunk/test/org/apache/catalina/loader/TestWebappClassLoaderThreadLocalMemoryLeak.java (original)
+++ tomcat/trunk/test/org/apache/catalina/loader/TestWebappClassLoaderThreadLocalMemoryLeak.java Mon Mar 19 19:28:01 2012
@@ -17,17 +17,21 @@
 package org.apache.catalina.loader;
 
 import java.io.IOException;
-import java.util.Arrays;
-import java.util.List;
+import java.io.InputStream;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.logging.Filter;
+import java.util.logging.LogManager;
+import java.util.logging.LogRecord;
 
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
+
+import org.junit.Assert;
 import org.junit.Test;
 
 import org.apache.catalina.Context;
+import org.apache.catalina.core.JreMemoryLeakPreventionListener;
+import org.apache.catalina.core.StandardHost;
 import org.apache.catalina.startup.Tomcat;
 import org.apache.catalina.startup.TomcatBaseTest;
 import org.apache.tomcat.util.buf.ByteChunk;
@@ -35,119 +39,188 @@ import org.apache.tomcat.util.buf.ByteCh
 public class TestWebappClassLoaderThreadLocalMemoryLeak extends TomcatBaseTest {
 
     @Test
-    public void testThreadLocalLeak() throws Exception {
+    public void testThreadLocalLeak1() throws Exception {
 
         Tomcat tomcat = getTomcatInstance();
+        // Need to make sure we see a leak for the right reasons
+        tomcat.getServer().addLifecycleListener(
+                new JreMemoryLeakPreventionListener());
 
         // Must have a real docBase - just use temp
         Context ctx = tomcat.addContext("",
                 System.getProperty("java.io.tmpdir"));
 
-        Tomcat.addServlet(ctx, "leakServlet", new LeakingServlet());
-        ctx.addServletMapping("/leak1", "leakServlet");
+        Tomcat.addServlet(ctx, "leakServlet1",
+                "org.apache.tomcat.unittest.TesterLeakingServlet1");
+        ctx.addServletMapping("/leak1", "leakServlet1");
 
-        Tomcat.addServlet(ctx, "leakServlet2", new LeakingServlet2());
-        ctx.addServletMapping("/leak2", "leakServlet2");
 
         tomcat.start();
 
-        // This will trigger the timer & thread creation
-        ByteChunk chunk = getUrl("http://localhost:" + getPort() + "/leak1");
-        System.out.print("First Threadlocal test response " + chunk.toString());
-
-        chunk = getUrl("http://localhost:" + getPort() + "/leak2");
-        System.out
-                .print("Second Threadlocal test response " + chunk.toString());
+        // Configure logging filter to check leak message appears
+        LogValidationFilter f = new LogValidationFilter(
+                "The web application [] created a ThreadLocal with key of");
+        LogManager.getLogManager().getLogger(
+                "org.apache.catalina.loader.WebappClassLoader").setFilter(f);
+
+        // Need to force loading of all web application classes via the web
+        // application class loader
+        loadClass("TesterCounter",
+                (WebappClassLoader) ctx.getLoader().getClassLoader());
+        loadClass("TesterLeakingServlet1",
+                (WebappClassLoader) ctx.getLoader().getClassLoader());
+
+        // This will trigger the ThreadLocal creation
+        int rc = getUrl("http://localhost:" + getPort() + "/leak1",
+                new ByteChunk(), null);
+
+        // Make sure request is OK
+        Assert.assertEquals(HttpServletResponse.SC_OK, rc);
 
-        // Stop the context
+        // Destroy the context
         ctx.stop();
+        tomcat.getHost().removeChild(ctx);
+        ctx = null;
 
-        // If the thread still exists, we have a thread/memory leak
-        try {
-            Thread.sleep(1000);
-        } catch (InterruptedException ie) {
-            // ignore
-        }
+        // Make sure we have a memory leak
+        String[] leaks = ((StandardHost) tomcat.getHost())
+                .findReloadedContextMemoryLeaks();
+        Assert.assertNotNull(leaks);
+        Assert.assertTrue(leaks.length > 0);
+
+        // Make sure the message was logged
+        Assert.assertEquals(1, f.getMessageCount());
     }
 
-    class LeakingServlet extends HttpServlet {
 
-        private static final long serialVersionUID = 1L;
+    @Test
+    public void testThreadLocalLeak2() throws Exception {
 
-        private ThreadLocal<MyCounter> myThreadLocal = new ThreadLocal<MyCounter>();
+        Tomcat tomcat = getTomcatInstance();
+        // Need to make sure we see a leak for the right reasons
+        tomcat.getServer().addLifecycleListener(
+                new JreMemoryLeakPreventionListener());
 
-        @Override
-        protected void doGet(HttpServletRequest request,
-                HttpServletResponse response) throws ServletException,
-                IOException {
-
-            MyCounter counter = myThreadLocal.get();
-            if (counter == null) {
-                counter = new MyCounter();
-                myThreadLocal.set(counter);
-            }
+        // Must have a real docBase - just use temp
+        Context ctx = tomcat.addContext("",
+                System.getProperty("java.io.tmpdir"));
 
-            response.getWriter().println(
-                    "The current thread served this servlet "
-                            + counter.getCount() + " times");
-            counter.increment();
-        }
+        Tomcat.addServlet(ctx, "leakServlet2",
+                "org.apache.tomcat.unittest.TesterLeakingServlet2");
+        ctx.addServletMapping("/leak2", "leakServlet2");
 
-        @Override
-        public void destroy() {
-            super.destroy();
-            // normally not needed, just to make my point
-            myThreadLocal = null;
-        }
-    }
+        tomcat.start();
 
-    class LeakingServlet2 extends HttpServlet {
+        // Configure logging filter to check leak message appears
+        LogValidationFilter f = new LogValidationFilter(
+                "The web application [] created a ThreadLocal with key of");
+        LogManager.getLogManager().getLogger(
+                "org.apache.catalina.loader.WebappClassLoader").setFilter(f);
+
+        // Need to force loading of all web application classes via the web
+        // application class loader
+        loadClass("TesterCounter",
+                (WebappClassLoader) ctx.getLoader().getClassLoader());
+        loadClass("TesterThreadScopedHolder",
+                (WebappClassLoader) ctx.getLoader().getClassLoader());
+        loadClass("TesterLeakingServlet2",
+                (WebappClassLoader) ctx.getLoader().getClassLoader());
+
+        // This will trigger the ThreadLocal creation
+        int rc = getUrl("http://localhost:" + getPort() + "/leak2",
+                new ByteChunk(), null);
 
-        private static final long serialVersionUID = 1L;
+        // Make sure request is OK
+        Assert.assertEquals(HttpServletResponse.SC_OK, rc);
 
-        @Override
-        protected void doGet(HttpServletRequest request,
-                HttpServletResponse response) throws ServletException,
-                IOException {
-
-            List<MyCounter> counterList = ThreadScopedHolder.getFromHolder();
-            MyCounter counter;
-            if (counterList == null) {
-                counter = new MyCounter();
-                ThreadScopedHolder.saveInHolder(Arrays.asList(counter));
-            } else {
-                counter = counterList.get(0);
-            }
+        // Destroy the context
+        ctx.stop();
+        tomcat.getHost().removeChild(ctx);
+        ctx = null;
 
-            response.getWriter().println(
-                    "The current thread served this servlet "
-                            + counter.getCount() + " times");
-            counter.increment();
-        }
+        // Make sure we have a memory leak
+        String[] leaks = ((StandardHost) tomcat.getHost())
+                .findReloadedContextMemoryLeaks();
+        Assert.assertNotNull(leaks);
+        Assert.assertTrue(leaks.length > 0);
+
+        // Make sure the message was logged
+        Assert.assertEquals(1, f.getMessageCount());
     }
 
-    static class ThreadScopedHolder {
-        private static final ThreadLocal<List<MyCounter>> threadLocal =
-                new ThreadLocal<List<MyCounter>>();
 
-        public static void saveInHolder(List<MyCounter> o) {
-            threadLocal.set(o);
+    /**
+     * Utility method to ensure that classes are loaded by the
+     * WebappClassLoader. We can't just create classes since they will be loaded
+     * by the current class loader rather than the WebappClassLoader. This would
+     * mean that no leak occurred making the test for a leak rather pointless
+     * So, we load the bytes via the current class loader but define the class
+     * with the WebappClassLoader.
+     *
+     * This method assumes that all classes are in the current package.
+     */
+    private void loadClass(String name, WebappClassLoader cl) throws Exception {
+
+        InputStream is = cl.getResourceAsStream(
+                "org/apache/tomcat/unittest/" + name + ".class");
+        // We know roughly how big the class will be (~ 1K) so allow 2k as a
+        // starting point
+        byte[] classBytes = new byte[2048];
+        int offset = 0;
+        try {
+            int read = is.read(classBytes, offset, classBytes.length-offset);
+            while (read > -1) {
+                offset += read;
+                if (offset == classBytes.length) {
+                    // Buffer full - double size
+                    byte[] tmp = new byte[classBytes.length * 2];
+                    System.arraycopy(classBytes, 0, tmp, 0, classBytes.length);
+                    classBytes = tmp;
+                }
+                read = is.read(classBytes, offset, classBytes.length-offset);
+            }
+            Class<?> lpClass = cl.doDefineClass(
+                    "org.apache.tomcat.unittest." + name, classBytes, 0,
+                    offset, cl.getClass().getProtectionDomain());
+            // Make sure we can create an instance
+            Object obj = lpClass.newInstance();
+            obj.toString();
+        } finally {
+            if (is != null) {
+                try {
+                    is.close();
+                } catch (IOException ioe) {
+                    // Ignore
+                }
+            }
         }
+    }
 
-        public static List<MyCounter> getFromHolder() {
-            return threadLocal.get();
+
+    private class LogValidationFilter implements Filter {
+
+        private String targetMessage;
+        private AtomicInteger messageCount = new AtomicInteger(0);
+
+
+        public LogValidationFilter(String targetMessage) {
+            this.targetMessage = targetMessage;
         }
-    }
 
-    class MyCounter {
-        private int count = 0;
 
-        public void increment() {
-            count++;
+        public int getMessageCount() {
+            return messageCount.get();
         }
 
-        public int getCount() {
-            return count;
+
+        @Override
+        public boolean isLoggable(LogRecord record) {
+            String msg = record.getMessage();
+            if (msg != null && msg.contains(targetMessage)) {
+                messageCount.incrementAndGet();
+            }
+
+            return true;
         }
     }
 }
\ No newline at end of file



---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org