Skip to content

Non-persistent prepared statements not closed in PostgreSQL #3850

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
ThomWright opened this issue May 1, 2025 · 1 comment · May be fixed by #3863
Open

Non-persistent prepared statements not closed in PostgreSQL #3850

ThomWright opened this issue May 1, 2025 · 1 comment · May be fixed by #3863
Labels
bug db:postgres Related to PostgreSQL

Comments

@ThomWright
Copy link

ThomWright commented May 1, 2025

I have found these related issues/pull requests

I could not find an existing issue.

Description

The docs on Query::persistent() state:

    /// If `false`, the prepared statement will be closed after execution.

So I was expecting sqlx to close the prepared statement after execution.

However, when I set persistent to false while using PgConnection, I see the following behaviour:

  1. The statement gets prepared.
  2. The statement never enters the client-side statement cache.
  3. Unless I am much mistaken, the prepared statement never gets closed.

This can lead to a memory leak in the database, since the same query can get repeatedly prepared and never closed.

Question

Instead of using incrementing IDs, would it help to have a deterministic query text -> ID mapping, and always cache this ID in the LRU statement cache? I believe I have seen this in other connection pools.

In this case, this would result in an error from PostgreSQL when trying to re-prepare the same query with the same ID/name. This is significantly quicker/easier to detect than a memory leak.

Reproduction steps

for _ in 0..10 {
    let q = sqlx::query("SELECT (1)");
    let q = q.persistent(false);
    q.execute(&mut *conn).await.expect("query should succeed");
}

Produces the following logs in the database:

2025-05-01 15:28:31.704 UTC [932] LOG:  execute sqlx_s_1: SELECT (1)
2025-05-01 15:28:31.706 UTC [932] LOG:  execute sqlx_s_2: SELECT (1)
2025-05-01 15:28:31.708 UTC [932] LOG:  execute sqlx_s_3: SELECT (1)
2025-05-01 15:28:31.710 UTC [932] LOG:  execute sqlx_s_4: SELECT (1)
2025-05-01 15:28:31.711 UTC [932] LOG:  execute sqlx_s_5: SELECT (1)
2025-05-01 15:28:31.713 UTC [932] LOG:  execute sqlx_s_6: SELECT (1)
2025-05-01 15:28:31.714 UTC [932] LOG:  execute sqlx_s_7: SELECT (1)
2025-05-01 15:28:31.717 UTC [932] LOG:  execute sqlx_s_8: SELECT (1)
2025-05-01 15:28:31.719 UTC [932] LOG:  execute sqlx_s_9: SELECT (1)
2025-05-01 15:28:31.721 UTC [932] LOG:  execute sqlx_s_10: SELECT (1)

Note how the sqlx_s_<ID> increments every time. Without setting q.persistent(false), you will only see sqlx_s_1;

I've found it difficult to prove that the statement is not being closed/deallocated from looking at the database logs, but I see that Close::Statement(id) is not called anywhere except PgConnection::get_or_prepare(), when a statement is evicted from the LRU statement cache.

SQLx version

0.8.3

Enabled SQLx features

runtime-tokio-native-tls, postgres, derive

Database server and version

PostgreSQL 13

Operating system

MacOS

Rust version

1.86.0

@ThomWright ThomWright added the bug label May 1, 2025
@abonander abonander added the db:postgres Related to PostgreSQL label May 12, 2025
@abonander
Copy link
Collaborator

These should probably just use the unnamed statement, honestly. Parse -> Bind -> Execute -> Close in one shot.

@ThomWright ThomWright linked a pull request May 16, 2025 that will close this issue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug db:postgres Related to PostgreSQL
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants