Skip to content

feat(prepared_statements): add new ttl config - #1351

Open
meskill wants to merge 3 commits into
mainfrom
meskill-2026-08-06-feat-prepared_statements---uml
Open

feat(prepared_statements): add new ttl config#1351
meskill wants to merge 3 commits into
mainfrom
meskill-2026-08-06-feat-prepared_statements---uml

Conversation

@meskill

@meskill meskill commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Fixes #1319

TTL for prepared statements with jitter - when the deadline for specific PS is reached it will be reprepared on the first use.

@codecov

codecov Bot commented Aug 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.74214% with 6 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
pgdog/src/backend/server.rs 95.52% 3 Missing ⚠️
pgdog/src/util/time.rs 96.49% 2 Missing ⚠️
pgdog/src/backend/prepared_statements.rs 99.48% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!


/// A statement info prepared on this connection.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LocalStatement {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: let's use pub(crate) going forward and maybe add a linter entry for this if possible. This allows the compiler to detect dead code, while pub is considered exportable (pgdog is also a library), so the compiler can't detect if it's used or not.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did this, especially since this shouldn't be pub at all. Updated other parts as well.

But, for the issue: that's not the issue per se that pgdog is a library, but that it's a library that pubs everything

pgdog/pgdog/src/lib.rs

Lines 8 to 28 in 6eb1b2a

pub mod admin;
pub mod api;
pub mod auth;
pub mod backend;
pub mod cli;
pub mod config;
pub mod frontend;
pub mod healthcheck;
pub mod net;
pub mod plugin;
pub mod sighup;
pub mod state;
pub mod stats;
pub(crate) mod sync;
pub mod tasks;
#[cfg(test)]
pub mod test_utils;
#[cfg(feature = "tui")]
pub mod tui;
pub mod unique_id;
pub mod util;

so, if we drop all this pubs from lib.rs the warn will be triggered on the pub items as well, since they be pub, but inside private modules.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah, we should do that in a separate PR for sure.

Comment thread pgdog/src/backend/prepared_statements.rs Outdated
/// to run something before actual client's requests
fn check_prepared(&mut self, name: &str) -> Result<Option<Prepare>, Error> {
// Ignore if we already have a Parse in progress.
if self.parses.iter().any(|s| s == name) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: this is hot, maybe worth using memchr?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure memchr is helpful here, since we want to validate if the name is present in vec and memchr is helpful to do the search in bytes.

moving to mental backlog related to perf about parse overall

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah memchr will be useful for comparing prepared statement names because it'll use vectorized instructions to load multiple bytes of the string at a time and compare them quicker. It's useful for comparing longer strings faster, while the default implementation will do it one byte at a time in a loop.

Definitely not a level of optimization we should care about since self.parses is gonna be empty 9 times out of 10, and if it isn't, it will contain I think at most one entry? I'm not even sure why it's an array tbh.

Anyway, 👍

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe, but the memchr was actually looking more like search optimized library and the cmp logic inside the std led me to mentions of memcmp from libc, that should be also heavily optimized and maybe vectorized.
Maybe Rust is already fast, I'm not sure


let entry = self.local_cache.get(name);
let expired =
self.config.ttl.is_some() && entry.is_some_and(|entry| entry.expired(Instant::now()));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: check if the caller has Instant already in scope (we call the timer when the query starts executing). It may be worth passing it down because calling Instant::now is not free. That being said, I think it's insignificant compared to the IO time we're about to do.

I have seen Instant::now show up on flamegraphs though, so just double check the overhead (it might be invisible or extremely small, so may not be worth it).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't look there are relevant callers that could provide Instant. I'll put the performance report later

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks significant!
image

13% of check_prepared and that's not visible when the ttl is disabled.

But, before making the conclusions, we need to remind ourselves how does the sampling profiler works. And such profile makes samples of the current stack traces at some moment of time to eventually collect how many times the code was inside the specific function. And the result for the call is basically number of samples where the function was present / number of all samples.

So the next screen shows the bigger picture for execution:
image

>2k samples for execution inside of which only 2 samples got the Instant::now(). And still it doesn't mean the Instant::now were executing ~0.1% of time, but rather that for some samples we've got, in that exact time the Instant::now was active. It's not free, it uses some CPU for sure, but I'm not sure we should worry about it.

I verified the TPS with pg_bench and the enabled TTL was actually showing better numbers (insignificantly). The microbenchmark shows that Instant::now costs 10-40ns on my machine (hard to tell exactly, I guess the benchmark itself will also use the clocks that should be embedded there). It's negligible with ms-kind of latency we have with network connections.

There could be caveats here and there, but overall I don't think we are even close to the state when we should worry about Instant::now

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool! I've seen pgbouncer optimize it away with a cached call per iteration of the event loop, so that's why it's kinda top of mind for me, but this might of been pre-vdso1, when getting the current time was expensive (it was a syscall). These days, I think it's just one hop into libc.

FWIW, we can't cache the time this way because we use Tokio and I don't think it gives us a callback for each runtime tick, so we end up passing it around wherever it's easy...and not, when it's not.

@@ -418,7 +520,7 @@ impl PreparedStatements {
/// or failed to parse.
pub(crate) fn remove(&mut self, name: &str) -> bool {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

todo: we should really switch to Bytes for prepared statement names so we don't have to allocate that name over and over. Not important for this PR, just something to maybe put in our backlog.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe the strings are not the issue per se, but the cloning is definitely could be bad and Bytes could be simpler for this. Worth another pr, I agree

Comment thread pgdog-config/src/general.rs Outdated
///
/// **Note:** Expired statements are closed and prepared again the next time they are used. This stops stale execution plans from staying in Postgres. Set to `0` to let statements stay prepared forever.
///
/// _Default:_ `300000` (5 minutes)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's keep this disabled at first to make it an opt-in feature. As a follow up, we should change our prepared statements defaults across the entire config and make that part of a release with specific notes / reasoning.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

made it's Option for now

@levkk levkk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only thing we should change is prepared_statements_ttl should be None by default to make this feature opt-in.

Everything else is just food for thought.

@meskill
meskill force-pushed the meskill-2026-08-06-feat-prepared_statements---uml branch from e32936f to dec0966 Compare August 13, 2026 11:29

@levkk levkk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚢

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.

Add TTL to prepared statements

2 participants