feat(prepared_statements): add new ttl config - #1351
Conversation
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
|
|
||
| /// A statement info prepared on this connection. | ||
| #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||
| pub struct LocalStatement { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
Lines 8 to 28 in 6eb1b2a
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.
There was a problem hiding this comment.
Oh yeah, we should do that in a separate PR for sure.
| /// 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) { |
There was a problem hiding this comment.
question: this is hot, maybe worth using memchr?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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, 👍
There was a problem hiding this comment.
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())); |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
It doesn't look there are relevant callers that could provide Instant. I'll put the performance report later
There was a problem hiding this comment.
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:

>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
There was a problem hiding this comment.
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 { | |||
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
| /// | ||
| /// **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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
made it's Option for now
levkk
left a comment
There was a problem hiding this comment.
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.
e32936f to
dec0966
Compare

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