]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-2495 Correctly deregister JDBC Driver to prevent memory leaks
authorEvgeny Mandrikov <mandrikov@gmail.com>
Mon, 6 Jun 2011 22:16:39 +0000 (02:16 +0400)
committerEvgeny Mandrikov <mandrikov@gmail.com>
Wed, 8 Jun 2011 17:54:38 +0000 (21:54 +0400)
DriverDatabaseConnector should register only one instance of
DriverProxy in DriverManager and also should perform deregistration.
But this is not enough to prevent memory leaks, so class loader for
JDBC Driver should perform additional efforts for deregistration.

sonar-batch/src/main/java/org/sonar/batch/bootstrap/BootstrapClassLoader.java [deleted file]
sonar-batch/src/main/java/org/sonar/batch/bootstrap/BootstrapModule.java
sonar-batch/src/main/java/org/sonar/batch/bootstrap/JdbcDriverHolder.java [new file with mode: 0644]
sonar-batch/src/main/java/org/sonar/batch/bootstrap/JdbcLeakPrevention.java [new file with mode: 0644]
sonar-batch/src/test/java/org/sonar/batch/bootstrap/BootstrapClassLoaderTest.java
sonar-core/src/main/java/org/sonar/jpa/session/DriverDatabaseConnector.java

diff --git a/sonar-batch/src/main/java/org/sonar/batch/bootstrap/BootstrapClassLoader.java b/sonar-batch/src/main/java/org/sonar/batch/bootstrap/BootstrapClassLoader.java
deleted file mode 100644 (file)
index ca00f9a..0000000
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Sonar, open source software quality management tool.
- * Copyright (C) 2008-2011 SonarSource
- * mailto:contact AT sonarsource DOT com
- *
- * Sonar is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * Sonar 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
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with Sonar; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
- */
-package org.sonar.batch.bootstrap;
-
-import org.sonar.api.utils.SonarException;
-
-import java.io.File;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.net.URLClassLoader;
-
-/**
- * ClassLoader extended with the JDBC Driver hosted on the server-side.
- */
-public class BootstrapClassLoader {
-
-  private URLClassLoader classLoader;
-
-  public BootstrapClassLoader(ArtifactDownloader extensionDownloader) {
-    this(extensionDownloader.downloadJdbcDriver());
-  }
-
-  BootstrapClassLoader(File jdbcDriver) {
-    try {
-      ClassLoader parentClassLoader = BootstrapClassLoader.class.getClassLoader();
-      classLoader = URLClassLoader.newInstance(new URL[]{jdbcDriver.toURI().toURL()}, parentClassLoader);
-
-    } catch (MalformedURLException e) {
-      throw new SonarException("Fail to get URL of : " + jdbcDriver.getAbsolutePath(), e);
-    }
-  }
-
-  public URLClassLoader getClassLoader() {
-    return classLoader;
-  }
-}
index fda540156a8aa2bfedd16a5a2e34e93d858457e9..3127e7a3e506bf96b5a13dde98de9e93e2046038 100644 (file)
@@ -56,9 +56,9 @@ public class BootstrapModule extends Module {
     addComponent(TempDirectories.class);// registered here because used by BootstrapClassLoader
     addComponent(HttpDownloader.class);// registered here because used by BootstrapClassLoader
     addComponent(ArtifactDownloader.class);// registered here because used by BootstrapClassLoader
-    addComponent(BootstrapClassLoader.class);
+    addComponent(JdbcDriverHolder.class);
 
-    URLClassLoader bootstrapClassLoader = getComponent(BootstrapClassLoader.class).getClassLoader();
+    URLClassLoader bootstrapClassLoader = getComponent(JdbcDriverHolder.class).getClassLoader();
     // set as the current context classloader for hibernate, else it does not find the JDBC driver.
     Thread.currentThread().setContextClassLoader(bootstrapClassLoader);
 
diff --git a/sonar-batch/src/main/java/org/sonar/batch/bootstrap/JdbcDriverHolder.java b/sonar-batch/src/main/java/org/sonar/batch/bootstrap/JdbcDriverHolder.java
new file mode 100644 (file)
index 0000000..427ac20
--- /dev/null
@@ -0,0 +1,132 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2011 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * Sonar 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.batch.bootstrap;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.utils.SonarException;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.List;
+
+/**
+ * Contains and provides class loader extended with the JDBC Driver hosted on the server-side.
+ */
+public class JdbcDriverHolder {
+
+  private static Logger LOG = LoggerFactory.getLogger(JdbcDriverHolder.class);
+  private JdbcDriverClassLoader classLoader;
+
+  public JdbcDriverHolder(ArtifactDownloader extensionDownloader) {
+    this(extensionDownloader.downloadJdbcDriver());
+  }
+
+  JdbcDriverHolder(File jdbcDriver) {
+    try {
+      ClassLoader parentClassLoader = JdbcDriverHolder.class.getClassLoader();
+      classLoader = new JdbcDriverClassLoader(jdbcDriver.toURI().toURL(), parentClassLoader);
+
+    } catch (MalformedURLException e) {
+      throw new SonarException("Fail to get URL of : " + jdbcDriver.getAbsolutePath(), e);
+    }
+  }
+
+  public URLClassLoader getClassLoader() {
+    return classLoader;
+  }
+
+  /**
+   * This method automatically invoked by PicoContainer and deregisters JDBC drivers, which were forgotten.
+   * <p>
+   * Dynamically loaded JDBC drivers can not be simply used and this is a well known problem of {@link java.sql.DriverManager},
+   * so <a href="http://stackoverflow.com/questions/288828/how-to-use-a-jdbc-driver-from-an-arbitrary-location">workaround is to use proxy</a>.
+   * However DriverManager also contains memory leak, thus not only proxy, but also original driver must be deregistered,
+   * otherwise our class loader would be kept in memory.
+   * </p>
+   * <p>
+   * This operation contains unnecessary complexity because:
+   * <ul>
+   * <li>DriverManager checks the class loader of the calling class. Thus we can't simply ask it about deregistration.</li>
+   * <li>We can't use reflection against DriverManager, since it would create a dependency on DriverManager implementation,
+   * which can be changed (like it was done - compare Java 1.5 and 1.6).</li>
+   * <li>So we use companion - {@link JdbcLeakPrevention}. But we can't just create an instance,
+   * since it will be loaded by parent class loader and again will not pass DriverManager's check.
+   * So, we load the bytes via our parent class loader, but define the class with this class loader
+   * thus JdbcLeakPrevention looks like our class to the DriverManager.</li>
+   * </li>
+   * </p>
+   */
+  public void stop() {
+    classLoader.clearReferencesJdbc();
+    classLoader = null;
+  }
+
+  private static class JdbcDriverClassLoader extends URLClassLoader {
+
+    public JdbcDriverClassLoader(URL jdbcDriver, ClassLoader parent) {
+      super(new URL[] { jdbcDriver }, parent);
+    }
+
+    public void clearReferencesJdbc() {
+      InputStream is = getResourceAsStream("org/sonar/batch/bootstrap/JdbcLeakPrevention.class");
+      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 = defineClass("org.sonar.batch.bootstrap.JdbcLeakPrevention", classBytes, 0, offset, this.getClass().getProtectionDomain());
+        Object obj = lpClass.newInstance();
+
+        @SuppressWarnings("unchecked")
+        List<String> driverNames = (List<String>) obj.getClass().getMethod("clearJdbcDriverRegistrations").invoke(obj);
+
+        for (String name : driverNames) {
+          LOG.debug("To prevent a memory leak, the JDBC Driver [{}] has been forcibly deregistered", name);
+        }
+      } catch (Exception e) {
+        LOG.warn("JDBC driver deregistration failed", e);
+      } finally {
+        if (is != null) {
+          try {
+            is.close();
+          } catch (IOException ioe) {
+            LOG.warn(ioe.getMessage(), ioe);
+          }
+        }
+      }
+    }
+  }
+
+}
diff --git a/sonar-batch/src/main/java/org/sonar/batch/bootstrap/JdbcLeakPrevention.java b/sonar-batch/src/main/java/org/sonar/batch/bootstrap/JdbcLeakPrevention.java
new file mode 100644 (file)
index 0000000..6da6514
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2011 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * Sonar 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.batch.bootstrap;
+
+import java.sql.Driver;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * Companion of {@link JdbcDriverHolder} and allows it to deregister JDBC drivers.
+ * <p>
+ * Some hacks are involved in the loading of the class - see {@link JdbcDriverHolder#stop()},
+ * so this class can refer to classes only from java.* package and must not be referred from other classes.
+ * Placement and naming of this class and methods are very important, since it loaded and invoked via reflection.
+ * </p>
+ */
+public class JdbcLeakPrevention {
+
+  /**
+   * @return list of names of deregistered drivers
+   */
+  public List<String> clearJdbcDriverRegistrations() throws SQLException {
+    List<String> driverNames = new ArrayList<String>();
+    HashSet<Driver> originalDrivers = new HashSet<Driver>();
+    Enumeration<Driver> drivers = DriverManager.getDrivers();
+    while (drivers.hasMoreElements()) {
+      originalDrivers.add(drivers.nextElement());
+    }
+    drivers = DriverManager.getDrivers();
+    while (drivers.hasMoreElements()) {
+      Driver driver = drivers.nextElement();
+      if (driver.getClass().getClassLoader() != this.getClass().getClassLoader()) {
+        continue;
+      }
+      if (originalDrivers.contains(driver)) {
+        driverNames.add(driver.getClass().getCanonicalName());
+      }
+      DriverManager.deregisterDriver(driver);
+    }
+    return driverNames;
+  }
+
+}
index b28a4c33431c0cbba3bc17cfdad32f62a3458b6c..dfee29db76149274ac461083d56a2c5db548ec05 100644 (file)
@@ -37,7 +37,7 @@ public class BootstrapClassLoaderTest {
     assertNull(getClass().getClassLoader().getResource("foo/foo.txt"));
 
     URL url = getClass().getResource("/org/sonar/batch/bootstrap/BootstrapClassLoaderTest/foo.jar");
-    BootstrapClassLoader classloader = new BootstrapClassLoader(new File(url.toURI()));
+    JdbcDriverHolder classloader = new JdbcDriverHolder(new File(url.toURI()));
     assertNotNull(classloader.getClassLoader());
     assertNotNull(classloader.getClassLoader().getResource("foo/foo.txt"));
     
index 0b11068040ffc38dd287e3d4ec67278e82f33946..00e91fd8f381cb0ad3fcac8972a2289b529fb451 100644 (file)
@@ -27,11 +27,13 @@ import java.sql.Connection;
 import java.sql.Driver;
 import java.sql.DriverManager;
 import java.sql.SQLException;
+import java.util.Enumeration;
 import java.util.Properties;
 
 public class DriverDatabaseConnector extends AbstractDatabaseConnector {
 
   private ClassLoader classloader;
+  private boolean driverProxyRegistered = false;
 
   public DriverDatabaseConnector(Configuration configuration) {
     super(configuration, true);
@@ -74,23 +76,52 @@ public class DriverDatabaseConnector extends AbstractDatabaseConnector {
   }
 
   public Connection getConnection() throws SQLException {
-    try {
-      /*
-       * The sonar batch downloads the JDBC driver in a separated classloader.
-       * This is a well-know problem of java.sql.DriverManager. The workaround
-       * is to use a proxy.
-       * See http://stackoverflow.com/questions/288828/how-to-use-a-jdbc-driver-from-an-arbitrary-location
-       */
-      Driver driver = (Driver) classloader.loadClass(getDriver()).newInstance();
-      DriverManager.registerDriver(new DriverProxy(driver));
-
-    } catch (Exception e) {
-      SQLException ex = new SQLException("SQL driver not found " + getDriver());
-      throw (SQLException) ex.initCause(e);
+    /*
+     * The Sonar batch downloads the JDBC driver in a separated class loader.
+     * This is a well-know problem of java.sql.DriverManager. The workaround
+     * is to use a proxy.
+     * See http://stackoverflow.com/questions/288828/how-to-use-a-jdbc-driver-from-an-arbitrary-location
+     */
+    if (!driverProxyRegistered) {
+      driverProxyRegistered = true;
+      try {
+        Driver driver = (Driver) classloader.loadClass(getDriver()).newInstance();
+        DriverManager.registerDriver(new DriverProxy(driver));
+      } catch (Exception e) {
+        SQLException ex = new SQLException("SQL driver not found " + getDriver());
+        throw (SQLException) ex.initCause(e);
+      }
     }
     return DriverManager.getConnection(getUrl(), getUsername(), getPassword());
   }
 
+  @Override
+  public void stop() {
+    super.stop();
+
+    deregisterDriverProxy();
+  }
+
+  /**
+   * Due to memory leak in DriverManager we also should deregister original driver,
+   * but we can't do it here, because DriverManager checks the class loader of the calling class.
+   * So actually we might have a memory leak, but it supposed to be handled by Sonar batch.
+   */
+  private void deregisterDriverProxy() {
+    Enumeration<Driver> drivers = DriverManager.getDrivers();
+    while (drivers.hasMoreElements()) {
+      Driver driver = drivers.nextElement();
+      if (driver instanceof DriverProxy) {
+        try {
+          DriverManager.deregisterDriver(driver);
+          LOG.debug("JDBC Driver [{}] deregistered", driver);
+        } catch (SQLException e) {
+          LOG.warn("JDBC driver deregistration failed", e);
+        }
+      }
+    }
+  }
+
   @Override
   public void setupEntityManagerFactory(Properties factoryProps) {
     factoryProps.put("hibernate.connection.url", getUrl());
@@ -163,4 +194,4 @@ final class DriverProxy implements Driver {
     org.sonar.jpa.session.DriverProxy other = (org.sonar.jpa.session.DriverProxy) obj;
     return this.target.equals(other.target);
   }
-}
\ No newline at end of file
+}