11 Сентября 2020

Spring Data поиск connection leak

Очень много приходилось работать со Spring Data Repositories, и проблема с соединениями с БД была не такой уже и редкой. То забудешь сессию закрыть, то что-то пойдет не так и нужный метод не вызывается и поиск ошибок, когда заканчиваются соединения с БД превращаются в настоящую проблему.

Я обычно использую postgresql для продуктива и h2db для тестирования, примеры буду приводить для postgresql.

Вот такой командой можно получить максимальное количество допустимых соединений СУБД. Причем нужно понимать что если у вас на одном сервере несколько бд, то соединения считаются в общем, что нужно учитывать для настройки пулов.

select setting::int max_conn from pg_settings where name=$$max_connections$$

Запрос, которым можно получить число активных соединений.

SELECT sum(numbackends) FROM pg_stat_database;

После очередных изменений стали появляться ошибки.

    org.hibernate.exception.JDBCConnectionException: Could not open connection

Не мог понять почему через некоторое время приложение просто падало. Понятно, что заканчиваются соединения в пуле, осталось разобраться из-за чего они заканчиваются.

При настройке источника данных (javax.sql.DataSource) подключение нужно настроить с использованием пула. Пул HikariCP (com.zaxxer.HikariCP) поддерживает механизм поиска connection leak. Для того чтобы его включить, нужно при настройке пула задать таймаут через который пул будет понимать, что соединение зависло и не закрылось.

Значение таймаута задается в миллисекундах. Получилась вот такая конфигурация:

    HikariConfig config = new HikariConfig();
    config.setMaximumPoolSize(100);
    config.setIdleTimeout(30000);
    config.setLeakDetectionThreshold(30000); // <-- таймаут для включения поиска connection leak
    config.setConnectionTimeout(TimeUnit.SECONDS.toMillis(50));
    config.setDataSourceClassName(dataSourceProperties.getDriverClassName());
    config.addDataSourceProperty("url", dataSourceProperties.getUrl());
    if (dataSourceProperties.getUsername() != null) {
       config.addDataSourceProperty("user", dataSourceProperties.getUsername());
    } else {
       config.addDataSourceProperty("user", ""); // HikariCP doesn't allow null user
    }
    if (dataSourceProperties.getPassword() != null) {
       config.addDataSourceProperty("password", dataSourceProperties.getPassword());
    } else {
       config.addDataSourceProperty("password", ""); // HikariCP doesn't allow null password
    }
    return new HikariDataSource(config);

После настройки пула в логе стали появляться исключения

ProxyLeakTask: Connection leak detection triggered for connection

Стектрейс должен привести к методу службы или репозитория где возник leak. Как же я удивился, когда это были обычные методы для чтения.

public interface CardFileRepository extends JpaRepository<CardFile,Long> {
    Stream<CardFile> findAllByCard(Card card);
}

Дальше вспоминаем, что Stream в отличае от того же List мало того, что может быть прочитан только один раз, так еще и вычисление значения элемента Stream ленивое, то есть выполняется при обращении. В документации spring написано, что обязательно нужно закрывать Stream вручную через вызов метода close или использовать try-with-resources.

В статье по использованию объектов spring-data тоже пишут закрывайте stream. Как это часто бывает, полезная статья вышла уже после того, как ты ошибок понаделал и героически их починил.