javamysqljdbchikaricp

Java Connection Pooling not working properly


I'm building a web project using Tomcat, where I have to fetch some dates from a MySQL database, for which I'm using HikariCP. Unfortunately, after too many fetches I get a SQLTransientConnectionException.

13:34:33.915 [http-nio-8081-exec-14] ERROR de.intranet.data.db.DBAccess - SQL error in getLatestModification for category hausnachrichten: Cannot invoke "java.sql.Connection.prepareStatement(String)" because the return value of "de.intranet.data.db.DBAccess.getConnection()" is null
13:34:33.915 [http-nio-8081-exec-14] DEBUG de.intranet.data.db.DBAccess - Existing getLatestModification for category hausnachrichten: null
13:34:38.957 [http-nio-8081-exec-13] ERROR de.intranet.data.db.DBConnection - Failed to retrieve a connection from the connection pool.
java.sql.SQLTransientConnectionException: HikariPool-2 - Connection is not available, request timed out after 30010ms.
    at com.zaxxer.hikari.pool.HikariPool.createTimeoutException(HikariPool.java:696) ~[HikariCP-5.0.1.jar:?]
    at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:181) ~[HikariCP-5.0.1.jar:?]
    at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:146) ~[HikariCP-5.0.1.jar:?]
    at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:100) ~[HikariCP-5.0.1.jar:?]
    at de.intranet.data.db.DBConnection.getConnection(DBConnection.java:131) [classes/:?]
    at de.intranet.data.db.DBAccess.getLatestModification(DBAccess.java:735) [classes/:?]
    at de.intranet.http.ServletDashboardPrepopulate.doPost(ServletDashboardPrepopulate.java:80) [classes/:?]
    at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:590) [servlet-api.jar:6.0]
    at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658) [servlet-api.jar:6.0]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:205) [catalina.jar:10.1.10]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) [catalina.jar:10.1.10]
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51) [tomcat-websocket.jar:10.1.10]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) [catalina.jar:10.1.10]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) [catalina.jar:10.1.10]
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:166) [catalina.jar:10.1.10]
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90) [catalina.jar:10.1.10]
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:482) [catalina.jar:10.1.10]
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115) [catalina.jar:10.1.10]
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93) [catalina.jar:10.1.10]
    at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:676) [catalina.jar:10.1.10]
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) [catalina.jar:10.1.10]
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:341) [catalina.jar:10.1.10]
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:391) [tomcat-coyote.jar:10.1.10]
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) [tomcat-coyote.jar:10.1.10]
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:894) [tomcat-coyote.jar:10.1.10]
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741) [tomcat-coyote.jar:10.1.10]
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) [tomcat-coyote.jar:10.1.10]
    at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) [tomcat-util.jar:10.1.10]
    at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) [tomcat-util.jar:10.1.10]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-util.jar:10.1.10]
    at java.base/java.lang.Thread.run(Thread.java:1589) [?:?]

To me, it seems like the connections get closed properly, as far as I'm using try-with-resources, when suddenly the SQLException comes:

13:33:28.868 [http-nio-8081-exec-11] DEBUG de.intranet.http.ServletDashboardPrepopulate - Connection Pool Stats - Active: 10, Idle: null
13:33:33.903 [http-nio-8081-exec-14] DEBUG de.intranet.http.ServletDashboardPrepopulate - Connection Pool Stats - Active: 10, Idle: null
13:33:38.934 [http-nio-8081-exec-13] DEBUG de.intranet.http.ServletDashboardPrepopulate - Connection Pool Stats - Active: 10, Idle: null
13:33:43.960 [http-nio-8081-exec-2] DEBUG de.intranet.http.ServletDashboardPrepopulate - Connection Pool Stats - Active: 10, Idle: null
13:33:48.995 [http-nio-8081-exec-16] DEBUG de.intranet.http.ServletDashboardPrepopulate - Connection Pool Stats - Active: 10, Idle: null
13:33:53.856 [http-nio-8081-exec-9] ERROR de.intranet.data.db.DBConnection - Failed to retrieve a connection from the connection pool.
java.sql.SQLTransientConnectionException: HikariPool-2 - Connection is not available, request timed out after 30001ms.
    at com.zaxxer.hikari.pool.HikariPool.createTimeoutException(HikariPool.java:696) ~[HikariCP-5.0.1.jar:?]
(rest is same as above)
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
    try {
        // Initialize a map to store category names and their corresponding latest
        // modification dates
        Map<String, String> dateMap = new HashMap<>();

        // Retrieve the ServletContext and DBAccess instance from the request
        ServletContext servletContext = request.getServletContext();
        DBAccess dbAccess = (DBAccess) servletContext.getAttribute("dbConnection");
        // Define a date format for formatting the modification dates in the response
        SimpleDateFormat sdf = new SimpleDateFormat("dd.MM.yyyy");

        try {
            Map<String, Integer> poolStats = dbAccess.getConnectionPoolStats();
            logger.debug("Connection Pool Stats - Active: {}, Idle: {}", poolStats.get("active"),
                    poolStats.get("idle"));
        } catch (Exception e) {
            logger.error("Error retrieving connection pool stats: {}", e.getMessage());
        }

        // Iterate through each file category and retrieve the latest modification date
        for (DBFilesTableCategory category : DBFilesTableCategory.values()) {
            Date lastDate = dbAccess.getLatestModification(category);
            // If a modification date is available, add it to the dateMap
            if (lastDate != null) {
                dateMap.put(category.name(), sdf.format(lastDate));
            } else {
                dateMap.put(category.name(), "Keine Dateien vorhanden");
            }
        }
        // Convert the dateMap to a JSON response using Gson
        Gson gson = new Gson();
        String jsonResponse = gson.toJson(dateMap);
        // Write the JSON response to the HttpServletResponse
        response.getWriter().write(jsonResponse);

    } catch (Exception e) {
        // Log any exceptions that occur during the processing of the request
        logger.error("Error while prepopulating dashboard: {}", e.getMessage());

        // Send an internal server error response to the client
        response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
    }
}
public class DBAccess extends DBConnection {
    public Date getLatestModification(DBFilesTableCategory category) {
        // Variable to store the latest modification date
        Date latestDate = null;
        // Convert the category enum to a string
        String categoryString = category.toString();
        // SQL query to retrieve the maximum upload date for files in the specified
        // category
        String query = "SELECT MAX(f_uploadDate) AS latestDate FROM files WHERE f_category = ?";
        try (Connection connection = this.getConnection();
                PreparedStatement preparedStatement = this.getConnection().prepareStatement(query)) {

            // Set the category as a parameter in the prepared statement
            preparedStatement.setString(1, categoryString);
            logger.debug("Executing query: {}", preparedStatement);
            // Execute the query and retrieve the result set
            try (ResultSet resultSet = preparedStatement.executeQuery()) {
                // If a row is returned, retrieve the latest date
                if (resultSet.next()) {
                    latestDate = resultSet.getDate("latestDate");
                }
            } catch (SQLException e) {
                // Handle any SQL exceptions by printing the stack trace
                logger.error("SQL error in getLatestModification for category {}: {}", categoryString, e.getMessage());
            }
        } catch (Exception e) {
            // Handle any SQL exceptions by printing the stack trace
            logger.error("SQL error in getLatestModification for category {}: {}", categoryString, e.getMessage());
        }
        logger.debug("Existing getLatestModification for category {}: {}", categoryString, latestDate);
        // Return the latest modification date, or null if no files are found
        return latestDate;
    }
}

Maybe there is a problem in how I start the connections in the initialization of the instance?

public class DBConnection {

    // The HikariConfig object for configuring the connection pool
    private HikariConfig config;
    private HikariDataSource dataSource;

    // Static initializer for loading the MySQL JDBC driver
    static {
        try {
            Class.forName("com.mysql.cj.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            throw new RuntimeException("MySQL JDBC driver not found", e);
        }
    }

    public DBConnection(String url, String username, String password) {
        // Create a new DBAccess instance with the specified database connection details
        config = new HikariConfig();

        // Set the URL, username, and password for the database connection
        config.setJdbcUrl(url);
        config.setUsername(username);
        config.setPassword(password);
        config.addDataSourceProperty("cachePrepStmts", "true");
        config.addDataSourceProperty("prepStmtCacheSize", "250");
        config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");

        dataSource = new HikariDataSource(config);
    }
}
@WebListener
public class WebServletContextListener implements ServletContextListener {
    // Attribute name for the database connection
    private static final String DB_CONNECTION_ATTR = "dbConnection";

    @Override
    public void contextInitialized(ServletContextEvent event) {
        // Pass the servlet context to ConfigUtil
        ConfigUtil.setServletContext(event.getServletContext());

        // Create a new DBAccess instance with the specified database connection details
        DBAccess dbConnection = new DBAccess(ConfigUtil.getDatabaseUrl(), ConfigUtil.getDatabaseUsername(),
                ConfigUtil.getDatabasePassword());
        // Set the DBAccess instance as a servlet context attribute
        event.getServletContext().setAttribute(DB_CONNECTION_ATTR, dbConnection);
    }

The logs of the database are completely empty and I don't know what I can do. Maybe some of you have an idea on how to isolate the error or even had similar problems before and have a solution.


Solution

  • You are leaking connections here:

    try (Connection connection = this.getConnection();
         PreparedStatement preparedStatement = this.getConnection().prepareStatement(query)) {
    

    This fragment opens two connections but only one is declared as resource that is to be closed. The second resource is the PreparedStatement that is created on the second connection.

    Try-with-resources can only call close() on variables that you declare in the resource specification block. When you write this.getConnection().prepareStatement(query) you get a new connection but don't assign it to a variable. Try-with-resources therefore cannot close it.

    The solution is to open only one connection and to use that connection to call prepareStatement():

    try (Connection connection = this.getConnection();
         PreparedStatement preparedStatement = connection.prepareStatement(query)) {