Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deregister JDBC drivers during undeploy of a war deployment #21221

Closed
BenICE opened this issue Apr 29, 2020 · 6 comments
Closed

Deregister JDBC drivers during undeploy of a war deployment #21221

BenICE opened this issue Apr 29, 2020 · 6 comments
Assignees
Labels
type: enhancement A general enhancement
Milestone

Comments

@BenICE
Copy link

BenICE commented Apr 29, 2020

Hi,

following up on the registration of JDBC drivers in Spring Boot (especially #2612).

As I can see it, no Spring Boot Application (e.g. 2.2.6) deregisters its JDBC driver.
Standard Spring Boot created with Spring boot initializr & In Memory H2 database, will leave its driver behind, which leads to a memory leak as the class loader cannot be GCed.

Why is there no huge outcry?

I guess these are the most standard cases, but if you run it in a JBoss environment you will not have much fun as the server will quickly rise an OOM after some deployments.

Workaround (which I deployed) is to deregister the driver in a context listener, when the context is destroyed (much like https://stackoverflow.com/questions/3320400/to-prevent-a-memory-leak-the-jdbc-driver-has-been-forcibly-unregistered/23912257#23912257).

Sure, we could relay on the Tomcat memory leak protection or deploy the database driver in the application server, but I would assume a War-file produced by Spring Boot should run everywhere.

What is your take on it?

Best

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Apr 29, 2020
@wilkinsona
Copy link
Member

@BenICE Thanks for opening a follow-up issue. As I requested on #2612, can you please provide a minimal sample that reproduces the problem?

@wilkinsona wilkinsona added the status: waiting-for-feedback We need additional information before we can continue label Apr 29, 2020
@BenICE
Copy link
Author

BenICE commented Apr 29, 2020

Sorry, it is really nothing special.
Just a Spring Boot app (created by the initializr) & application.properties to configure the datasource.
Deploy this app in a stand alone tomcat and you will see that tomcat (e.g. latest 9) need to tidy up after us. Do this in a JBoss (e.g. latest 7.3) and you will create a memory leak.

https://github.com/BenICE/exampleMemoryLeak <-- hope this is the right way to share it.

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Apr 29, 2020
@wilkinsona
Copy link
Member

Thanks for the sample, @BenICE. Unfortunately, I don't think there's much that we can do about this.

H2 automatically registers itself in the static initialiser of org.h2.Driver. It does this by calling its own load() method. While public, this method is documented as INTERNAL. There's a corresponding unload() method that deregisters the driver. It too is labelled as INTERNAL and H2 never calls it itself. Furthermore, the javadoc of the org.h2.Driver class says that "an application should not use this class directly". Given this, it's not clear to me how H2 is intended to be used in a way that does not cause a memory leak unless you ignore the documentation and call internal API.

A further complication is that Spring Boot itself does not load org.h2.Driver or create an instance of it. It's Hikari that does this when creating its DriverDataSource. It might be possible for a change to be made to Hikari to deregister the Driver instance that it creates when the connection pool is being closed, however I don't think this could be used safely with H2 is it maintains internal state for its registration and this will get out of sync if its Driver is deregistered from the DriverManager by any mechanism other than calling H2's internal unload() method.

I think you have a few options:

  • Switch to a database other than H2
  • Use a Tomcat- or JBoss-managed data source retrieved from JNDI
  • Add some code to your app to clean up H2 as you've suggested above

FWIW, I would implement that last of these by overriding onStartup in your SpringBootServletInitializer sub-class and registering a ServletContextListener:

@Override
public void onStartup(ServletContext servletContext) throws ServletException {
	servletContext.addListener(new ServletContextListener() {

		@Override
		public void contextDestroyed(ServletContextEvent sce) {
			org.h2.Driver.unload();
		}
			
	});
	super.onStartup(servletContext);
}

I've resorted to using internal API to avoid potential problems with H2's internal state getting out of sync with that of DriverManager.

@wilkinsona wilkinsona added for: external-project For an external project and not something we can fix status: declined A suggestion or change that we don't feel we should currently apply and removed status: feedback-provided Feedback has been provided status: waiting-for-triage An issue we've not yet triaged labels May 1, 2020
@BenICE
Copy link
Author

BenICE commented May 1, 2020

Shit sorry...I lead you on a goose chase. I just took H2 as an example.
Yes, they have their internal state, which is messy.

But you have the same behaviour as well with e.g. the maria DB driver. In my opinion it concerns all JDBC drivers.

@wilkinsona
Copy link
Member

Ideally, JDBC drivers would ship with a ServletContainerInitializer so that they can automatically unregister themselves when the servlet context is being destroyed. This would avoid any problems with internal state getting out of sync with DriverManager state and fix the problem for everyone in any servlet environment. Interestingly, a number of drivers ship with OSGi BundleActivators to address the same problem in an OSGi environment.

Failing that, fixing the problem at the container level is the next best option, but it limits the impact of the fix to those that are using a particular container.

Lastly, we could fix it in Spring Boot by adding code that mimics what Tomcat does in JdbcLeakPrevention. Something like this would work, I believe, in any servlet container:

@Override
public void contextDestroyed(ServletContextEvent sce) {
	for (Driver driver: Collections.list(DriverManager.getDrivers())) {
		if (driver.getClass().getClassLoader() == sce.getServletContext().getClassLoader()) {
			try {
				DriverManager.deregisterDriver(driver);
			} catch (SQLException ex) {
				// Continue
			}
		}
	}
}

JdbcLeakPrevention contains a comment about DriverManager.getDrivers() causing additional registration. As far as I can tell, that's no longer the case (on Java 8 at least).

The code above would break any Driver that keeps its own internal registration state as it would think that it is registered when it is not. If we can guarantee that we perform deregistration after the application context has closed, or at least after all beans have been destroyed, this probably won't cause a problem as the Driver's class is about to get garbage collected as a result of the web app being undeployed.

I'm wavering a bit on this one now. Let's see what the rest of the team thinks.

@wilkinsona wilkinsona reopened this May 1, 2020
@wilkinsona wilkinsona added for: team-attention An issue we'd like other members of the team to review status: waiting-for-feedback We need additional information before we can continue and removed for: external-project For an external project and not something we can fix status: declined A suggestion or change that we don't feel we should currently apply labels May 1, 2020
@philwebb philwebb added type: enhancement A general enhancement and removed for: team-attention An issue we'd like other members of the team to review status: waiting-for-feedback We need additional information before we can continue labels May 6, 2020
@philwebb philwebb modified the milestones: 2.3.x, 2.3.0 May 6, 2020
@wilkinsona wilkinsona changed the title Managed JDBC driver is not deregistered Deregister JDBC drivers during undeploy of a war deployment May 13, 2020
@BenICE
Copy link
Author

BenICE commented May 13, 2020

Thanks a lot for adding this feature to the base!
Awesome, nice and quick support :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: enhancement A general enhancement
Projects
None yet
Development

No branches or pull requests

4 participants