Skip to content

fix: GH-2407 Release JDBC statements as soon as their result is consumed - #2899

Open
Ilya Nemtsev (inemtsev) wants to merge 1 commit into
JetBrains:mainfrom
inemtsev:fix/gh-2407-release-executed-statements
Open

Ilya Nemtsev (inemtsev) wants to merge 1 commit into
JetBrains:mainfrom
inemtsev:fix/gh-2407-release-executed-statements

Conversation

@inemtsev

@inemtsev Ilya Nemtsev (inemtsev) commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Fixes #2407.

Problem

Every executed statement was kept in the transaction's executedStatements list until the transaction ended, and a query's statement was only closed when its iterator reached the last row. A long-running transaction therefore accumulated one PreparedStatement per executed statement. Causing the memory usage to grow considerably.

Description

Description Summary: executedStatements now holds only statements that still own an open result set.

Detailed description:

  • Why: the only way an executable can hand an open cursor to a caller is by returning a ResultApi
  • What:
  1. ResultSet -> JdbcResult change: releasing a statement needs to know which statement wrapper to release, and a raw java.sql.ResultSet can't tell you that. We need to use the JdbcResult wrapper instead which gives us the ability to call releaseResult()
  2. trackResultSet (multiple copies consolidated) -> JdbcTransaction.registerStatement: The counter has to be incremented at the first event and decremented at the second. Both events happen inside JdbcTransaction, so that's where the common code was put.
  3. replace the unconditional transaction.executedStatements.add(statement).: We need to enhance the JdbcResult with owner, so that we can release it later. But, this only makes sense when we have a ResultApi.
  • How: BlockingExecutable.executeIn closes a statement right after the afterExecution interceptors run, unless executeInternal returned a ResultApi. Insert, update, delete, merge, batch statements, and exec(sql) { rs -> } are closed immediately.

Type of Change

Please mark the relevant options with an "X":

  • Bug fix
  • New feature
  • Documentation update

Updates/remove existing public API methods:

  • Is breaking change

Affected databases:

  • MariaDB
  • Mysql5
  • Mysql8
  • Oracle
  • Postgres
  • SqlServer
  • H2
  • SQLite

Checklist

  • Unit tests are in place
  • The build is green (including the Detekt check)
  • All public methods affected by my PR has up to date API docs
  • Documentation for my change is up to date

Related Issues

… is consumed

Every executed statement was kept in the transaction's executedStatements
list until the transaction ended, and a query's statement was only closed
when its iterator reached the last row. A long-running transaction therefore
accumulated one PreparedStatement per executed statement.

Statements that do not hand out a result set (insert, update, delete, exec
with a transform) are now closed right after the interceptors have run.
Statements that do are registered with the transaction and released, and
unregistered, as soon as their result is consumed by an iterator, count(),
or empty(). A statement whose execution throws is closed as well, and the
auxiliary statement used to read SQLite generated keys is closed together
with its parent.

Transaction-end cleanup, closeExecutedStatements(), and the eager cleanup
for drivers without multiple result set support are unchanged.
@inemtsev

Copy link
Copy Markdown
Contributor Author

I ran a few tests with a loop inside a transaction:

Retention harness: one transaction {} running 20,000 iterations of insert → select-by-id → update, sampling used heap after System.gc() and counting PreparedStatements still reachable through a WeakHashMap.

version reachable statements at 20k still open used heap start → end wall time
main (203419a) 60,000 40,000 4 → 53 MB, linear (~2.4 MB per 1,000 iterations) 9.55 s
this branch 0 0 4 → 5 MB, flat 8.69 s (−9%)

environment: (PostgreSQL 18, JDK 21)

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Memory leak in long running transaction

1 participant