Affects: \
Please refer to the demo project that replicates the issue.
When using JdbcTemplate.queryForObject()
and subsequently calling the .findFirst()
method, the connection is not getting closed, hense the connection pool runs dry very quickly.
jdbcTemplate.queryForStream("SELECT VALUE FROM TEST WHERE KEY = ?",
(rs, i) -> rs.getString("VALUE"), key)
.findFirst();
The result of the execution of the following code is:
2022-01-30 13:00:38.783 DEBUG 19628 --- [l-1 housekeeper] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Pool stats (total=10, active=10, idle=0, waiting=1)
2022-01-30 13:00:38.783 DEBUG 19628 --- [l-1 housekeeper] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Fill pool skipped, pool is at sufficient level.
2022-01-30 13:00:39.585 DEBUG 19628 --- [ main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Timeout failure stats (total=10, active=10, idle=0, waiting=0)
2022-01-30 13:00:39.585 INFO 19628 --- [ main] ConditionEvaluationReportLoggingListener :
Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2022-01-30 13:00:39.616 ERROR 19628 --- [ main] o.s.boot.SpringApplication : Application run failed
java.lang.IllegalStateException: Failed to execute CommandLineRunner
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:772) ~[spring-boot-2.6.3.jar:2.6.3]
at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:753) ~[spring-boot-2.6.3.jar:2.6.3]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:309) ~[spring-boot-2.6.3.jar:2.6.3]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1303) ~[spring-boot-2.6.3.jar:2.6.3]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1292) ~[spring-boot-2.6.3.jar:2.6.3]
at demo.Demo.main(Demo.java:21) ~[main/:na]
Caused by: org.springframework.jdbc.CannotGetJdbcConnectionException: Failed to obtain JDBC Connection; nested exception is java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 30018ms.
at org.springframework.jdbc.datasource.DataSourceUtils.getConnection(DataSourceUtils.java:83) ~[spring-jdbc-5.3.15.jar:5.3.15]
at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:646) ~[spring-jdbc-5.3.15.jar:5.3.15]
at org.springframework.jdbc.core.JdbcTemplate.queryForStream(JdbcTemplate.java:834) ~[spring-jdbc-5.3.15.jar:5.3.15]
at org.springframework.jdbc.core.JdbcTemplate.queryForStream(JdbcTemplate.java:863) ~[spring-jdbc-5.3.15.jar:5.3.15]
at demo.JdbcRepo.getFromStream(JdbcRepo.java:32) ~[main/:na]
at demo.JdbcRepo$$FastClassBySpringCGLIB$$87a3d4d8.invoke(<generated>) ~[main/:na]
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.3.15.jar:5.3.15]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:783) ~[spring-aop-5.3.15.jar:5.3.15]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-5.3.15.jar:5.3.15]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:753) ~[spring-aop-5.3.15.jar:5.3.15]
at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:137) ~[spring-tx-5.3.15.jar:5.3.15]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.15.jar:5.3.15]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:753) ~[spring-aop-5.3.15.jar:5.3.15]
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:698) ~[spring-aop-5.3.15.jar:5.3.15]
at demo.JdbcRepo$$EnhancerBySpringCGLIB$$c0949ac4.getFromStream(<generated>) ~[main/:na]
at demo.Demo.run(Demo.java:49) ~[main/:na]
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:769) ~[spring-boot-2.6.3.jar:2.6.3]
... 5 common frames omitted
Caused by: java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 30018ms.
at com.zaxxer.hikari.pool.HikariPool.createTimeoutException(HikariPool.java:696) ~[HikariCP-4.0.3.jar:na]
at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:197) ~[HikariCP-4.0.3.jar:na]
at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:162) ~[HikariCP-4.0.3.jar:na]
at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:128) ~[HikariCP-4.0.3.jar:na]
at org.springframework.jdbc.datasource.DataSourceUtils.fetchConnection(DataSourceUtils.java:159) ~[spring-jdbc-5.3.15.jar:5.3.15]
at org.springframework.jdbc.datasource.DataSourceUtils.doGetConnection(DataSourceUtils.java:117) ~[spring-jdbc-5.3.15.jar:5.3.15]
at org.springframework.jdbc.datasource.DataSourceUtils.getConnection(DataSourceUtils.java:80) ~[spring-jdbc-5.3.15.jar:5.3.15]
... 21 common frames omitted
Comment From: sbrannen
As stated in the Javadoc for org.springframework.jdbc.core.JdbcOperations.queryForStream(String, RowMapper<T>, Object...)
, the caller is responsible for closing the stream.
Returns: the result Stream, containing mapped objects, needing to be closed once fully processed (e.g. through a try-with-resources clause)
As you can see in the following code listing, the Stream returned has an onClose()
callback associated with it that will close the connection.
https://github.com/spring-projects/spring-framework/blob/e3529453d1682257162fdfeaa0e3f0c12008a67e/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java#L840-L847
In light of that, I am closing this issue as "works as designed".
As a side note, if you are retrieving a single value from the database, have you considered using queryForObject(...)
with a limit 1
clause in your SQL statement (or whatever syntax is supported by your database to return a single result)?
In other words, why are you using a stream for a single result?
Comment From: arciszewski
Thanks for explanation. I thought that the stream terminal methods also close the internal connection. When I modified my example code to use try-with-resources it worked as expected.
I tried to use the queryForStream(...)
and findFirst()
because in my particular case I know there could be either one or none results, hense I wanted to use Optional
as a return type. And Stream > Optional seemed a reasonable choice.
Comment From: jeanxu1
I am seeing the same issue. it is not that intuitive and I thought it is a bug for connection leak as well.
What is the use case for queryForStream()
then? can you provide example of the right
way of using it?
@sbrannen
Comment From: ryber
I agree with other posters that this violates the principle of least surprise, I would never expect that I would have to close the connection because I don't do that with any other of the methods in that class.
Comment From: lcy2317
Adding Transaction annotation also helps, but I still think queryForStream()
should be avoided using, using jdbctemplate.query()
instead.
Comment From: sbrannen
I tried to use the
queryForStream(...)
andfindFirst()
because in my particular case I know there could be either one or none results, hense I wanted to useOptional
as a return type. And Stream > Optional seemed a reasonable choice.
If you insist on having Optional
semantics when using JdbcTemplate
, see all comments in #30927 for details.
Note, however, that as of Spring Framework 6.1 the recommend way to have Optional
semantics is via the fluent API in the new JdbcClient
. For a concrete example, see https://github.com/spring-projects/spring-framework/issues/30931#issuecomment-1649988290.
Comment From: sbrannen
What is the use case for
queryForStream()
then? can you provide example of theright
way of using it?
I don't believe we have an actual example in the documentation; however, you can see an example in our test suite:
https://github.com/spring-projects/spring-framework/blob/232225b2aab29910f62d7d6a441cdb0af4269177/spring-jdbc/src/test/java/org/springframework/jdbc/core/JdbcTemplateQueryTests.java#L383-L392
Feel free to open a new ticket to suggest that an example be included in the reference manual.
Comment From: sbrannen
I agree with other posters that this violates the principle of least surprise, I would never expect that I would have to close the connection because I don't do that with any other of the methods in that class.
Although it may be surprising, it is sometimes the case that one must close a Stream
in Java.
For example, the Stream
returned from java.nio.file.Files.walk(Path, int, FileVisitOption...)
must also be manually closed (or used in try-with-resources block).
queryForStream(...)
is also one of those cases, and it's documented as such.