Tomcat DBCP leak
Рубрика: Development, Java | 26 June 2007, 08:12 | juriy
Хочу описать небольшие, но крайне неприятные грабли, по которым мне довелось потоптаться. Грабли заключаются в том, что Tomcat не закрывает открытые соединения DBCP пула после undeploy/stop/redeploy.
Эффект следующий: вы спокойно отлаживаете какой-нибудь проектик, который использует DBCP. Через часа полтора-два отладки, приходит злой DBA и сообщает, что вы “совсем охренели по 700 сессий держать”. Если для пользователя, под которым вы ходите в БД установлено ограничение по количеству сессий, то вы получаете ошибку вроде “Exceeded simultaneous session per user limit” ну или что-то вроде того.
Чтобы повторить этот эффект необходимо произвести следующие действия:
1. Проверить количество подключений к серверу. На Oracle можно это сделать таким вот нехитрым запросом:
select t.MACHINE, count(*)
from v$session t
where t.USERNAME = 'myuser'
group by t.MACHINE
2. Поднять Tomcat, задеплоить приложение, и выполнить запрос к сервлету, который использует пул.
3. Снова проверяем количество сессий – число должно увеличиться на значение, указанное в параметре initialSize конфигурации пула:
<parameter>
<name>initialSize</name>
<value>1</value>
</parameter>
4. Выполнить undeploy/deploy приложения, выполнить еще один запрос к сервлету и снова промониторить количество сессий.Вы увидите, что количество сессий снова увеличилось, нисмотря на то, что приложение было переустановлено.Разработчики говорят “это не баг – это фича”. Какая, дескать, нашему Tomcat’у разница, что за ресурсы вы там открываете. Сами открыли – сами закрывайте.
Без проблем, отвечаем мы и начинаем писать закрывалку ресурсов DBCP.
Для начала в web.xml объявим listener’а.
<listener>
<listener-class>ResourceManager</listener-class>
</listener>
Теперь, собственно, его код:
import java.util.Enumeration;import javax.naming.Binding;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.sql.DataSource;
import org.apache.commons.dbcp.BasicDataSource;
import org.apache.log4j.Logger;
public final class ResourceManager implements ServletContextListener {
Logger logger = Logger.getLogger("ResourceManager");
public ResourceManager() {
}
public void contextInitialized(ServletContextEvent event) {
logger.info("Startup");
}
public void contextDestroyed(ServletContextEvent event) {
logger.info("Shutdown");
closeDbcpPool();
}
private void closeDbcpPool() {
Context initCtx;
try {
initCtx = new InitialContext();
Enumeration bindings = initCtx.listBindings("java:comp/env/jdbc/pool");
while (bindings.hasMoreElements()) {
Binding binding = null;
try {
binding = (Binding) bindings.nextElement();
DataSource someDateSource = (DataSource) initCtx.lookup(
"java:comp/env/jdbc/pool/" + binding.getName());
if (someDateSource instanceof BasicDataSource) {
BasicDataSource basicDS = (BasicDataSource)someDateSource;
basicDS.close();
logger.info(binding.getName() + " removed");
}
} catch (Exception e) {
if (binding != null)
logger.warn("Error closing " + binding.getName() +
" : " + e.getMessage());
else
logger.warn("Error closing pool " +
" : " + e.getMessage());
}
}
} catch (NamingException e) {
e.printStackTrace();
}
}
}
Повторяем описанный эксперимент – видно, что теперь все сессии закрыты.
К сожалению – не все так гладко. Сессии, которые назодятся в состоянии active не будут закрыты. Чтобы проеврить – делаем сервлет, который выполняет запрос достаточно долго. Если нет желания выдумывать такой запрос то можно просто написать
while (resultSet.next()) {
try {
Thread.sleep(1000);
} catch (Exception e) {
}
}
Теперь повторяем эксперимент. После undeploy на БД останется открытой одна лишняя сессия (это и есть наш “долгий” запрос). А на страничке статуса сервера видно, что один поток работает уж очень долго.
Другими словами, мне не удалось полностью обезопасить себя от “потоков-зомби” после закрытия приложения. Хорошая новость – после отработки запроса HTTP поток спокойно вернется обратно в пул.
C другой стороны, если объект Connection закрыть в отдельном потоке во время исполнения запроса, то соединение спокойно закроется. При этом в цикле выборки (while (resultSet.next())) будет поймана исключительная ситуация.

Вот спасибо! Недавно напоролся на эти грабли, думал это что-то у меня не так, пока с помощью JMX не убедился, что Tomcat не закрывает пул соединений после остановки приложения, причём такая “фича” как в 5 так и в 6.
Спасибо за таблетку :-)
Если приложение нестабильно то может следует задуматься, чтобы отвязать его от пула соеденений? Расшарить пул на уровне сервера как глобальный JNDI ресурс, тогда деплой/андеплой наздоровье пока из Tomcat вся память не вытечет. При том даже расшаренный пул можно коректно закрыть http://www.javatalks.ru/ftopic13967.php