diff --git a/.schema/pgdog.schema.json b/.schema/pgdog.schema.json index 62b2fad16..313dc408e 100644 --- a/.schema/pgdog.schema.json +++ b/.schema/pgdog.schema.json @@ -83,6 +83,7 @@ "port": 6432, "prepared_statements": "extended", "prepared_statements_limit": 9223372036854775807, + "prepared_statements_memory_limit": 0, "pub_sub_channel_size": 0, "query_cache_limit": 1000, "query_log": null, @@ -974,6 +975,13 @@ "default": 9223372036854775807, "minimum": 0 }, + "prepared_statements_memory_limit": { + "description": "Approximate memory limit (bytes) for the global prepared statements cache. Statements no client is holding are evicted once the cache grows past it. `0` disables the limit.\n\n**Note:** A limit smaller than the working set causes constant eviction and re-preparation of statements; size it well above what the active workload keeps in flight.\n\n_Default:_ `0`\n\n", + "type": "integer", + "format": "uint", + "default": 0, + "minimum": 0 + }, "pub_sub_channel_size": { "description": "Enables support for pub/sub and configures the size of the background task queue.\n\n", "type": "integer", diff --git a/pgdog-config/src/general.rs b/pgdog-config/src/general.rs index 5630ef054..fcc0c9038 100644 --- a/pgdog-config/src/general.rs +++ b/pgdog-config/src/general.rs @@ -382,6 +382,16 @@ pub struct General { #[serde(default = "General::prepared_statements_limit")] pub prepared_statements_limit: usize, + /// Approximate memory limit (bytes) for the global prepared statements cache. Statements no client is holding are evicted once the cache grows past it. `0` disables the limit. + /// + /// **Note:** A limit smaller than the working set causes constant eviction and re-preparation of statements; size it well above what the active workload keeps in flight. + /// + /// _Default:_ `0` + /// + /// + #[serde(default = "General::prepared_statements_memory_limit")] + pub prepared_statements_memory_limit: usize, + /// Limit on the number of statements saved in the statement cache used to accelerate query parsing. /// /// _Default:_ `50000` @@ -887,6 +897,7 @@ impl Default for General { regex_parser_limit: Self::regex_parser_limit(), query_parser_engine: QueryParserEngine::default(), prepared_statements_limit: Self::prepared_statements_limit(), + prepared_statements_memory_limit: Self::prepared_statements_memory_limit(), query_cache_limit: Self::query_cache_limit(), passthrough_auth: Self::default_passthrough_auth(), connect_timeout: Self::default_connect_timeout(), @@ -1381,6 +1392,10 @@ impl General { Self::env_or_default("PGDOG_PREPARED_STATEMENTS_LIMIT", i64::MAX as usize) } + pub fn prepared_statements_memory_limit() -> usize { + Self::env_or_default("PGDOG_PREPARED_STATEMENTS_MEMORY_LIMIT", 0) + } + pub fn query_cache_limit() -> usize { Self::env_or_default("PGDOG_QUERY_CACHE_LIMIT", 1_000) } @@ -1826,12 +1841,14 @@ mod tests { let _guard = set_env_var("PGDOG_MIRROR_EXPOSURE", "0.5"); let _guard = set_env_var("PGDOG_DNS_TTL", "60000"); let _guard = set_env_var("PGDOG_PUB_SUB_CHANNEL_SIZE", "100"); + let _guard = set_env_var("PGDOG_PREPARED_STATEMENTS_MEMORY_LIMIT", "4294967296"); let _guard = set_env_var("PGDOG_LOG_MIN_DURATION_PARSE", "5"); let _guard = set_env_var("PGDOG_LOG_QUERY_SAMPLE_LENGTH", "200"); assert_eq!(General::broadcast_port(), 7432); assert_eq!(General::openmetrics_port(), Some(9090)); assert_eq!(General::prepared_statements_limit(), 1000); + assert_eq!(General::prepared_statements_memory_limit(), 4294967296); assert_eq!(General::query_cache_limit(), 500); assert_eq!(General::connect_attempts(), 3); assert_eq!(General::mirror_queue(), 256); @@ -1844,6 +1861,7 @@ mod tests { let _guard = remove_env_var("PGDOG_BROADCAST_PORT"); let _guard = remove_env_var("PGDOG_OPENMETRICS_PORT"); let _guard = remove_env_var("PGDOG_PREPARED_STATEMENTS_LIMIT"); + let _guard = remove_env_var("PGDOG_PREPARED_STATEMENTS_MEMORY_LIMIT"); let _guard = remove_env_var("PGDOG_QUERY_CACHE_LIMIT"); let _guard = remove_env_var("PGDOG_CONNECT_ATTEMPTS"); let _guard = remove_env_var("PGDOG_MIRROR_QUEUE"); @@ -1856,6 +1874,7 @@ mod tests { assert_eq!(General::broadcast_port(), General::port() + 1); assert_eq!(General::openmetrics_port(), None); assert_eq!(General::prepared_statements_limit(), i64::MAX as usize); + assert_eq!(General::prepared_statements_memory_limit(), 0); assert_eq!(General::query_cache_limit(), 1_000); assert_eq!(General::connect_attempts(), 1); assert_eq!(General::mirror_queue(), 128); diff --git a/pgdog/src/admin/parser.rs b/pgdog/src/admin/parser.rs index 0b17000cf..cce6ea8ee 100644 --- a/pgdog/src/admin/parser.rs +++ b/pgdog/src/admin/parser.rs @@ -255,6 +255,14 @@ mod tests { assert!(matches!(result, Ok(ParseResult::ResetQueryCache(_)))); } + #[test] + fn parses_reset_prepared_command() { + // The exact string the prepared_statements_limit=0 warning advises: + // if this stops parsing, the advice is broken. + let result = Parser::parse(super::super::reset_prepared::RESET_PREPARED); + assert!(matches!(result, Ok(ParseResult::ResetPrepared(_)))); + } + #[test] fn rejects_unknown_admin_command() { let result = Parser::parse("FOO BAR"); diff --git a/pgdog/src/admin/reset_prepared.rs b/pgdog/src/admin/reset_prepared.rs index a04707626..130e6cc85 100644 --- a/pgdog/src/admin/reset_prepared.rs +++ b/pgdog/src/admin/reset_prepared.rs @@ -1,15 +1,18 @@ //! RESET PREPARED. -use crate::config::config; use crate::frontend::prepared_statements::PreparedStatements; use super::prelude::*; +/// The admin console spelling of this command. The limit-0 warning quotes +/// it, and a parser test keeps the advice parseable. +pub(super) const RESET_PREPARED: &str = "RESET PREPARED"; + pub struct ResetPrepared; #[async_trait] impl Command for ResetPrepared { fn name(&self) -> String { - "RESET PREPARED".into() + RESET_PREPARED.into() } fn parse(_: &str) -> Result { @@ -17,10 +20,10 @@ impl Command for ResetPrepared { } async fn execute(&self) -> Result, Error> { - let config = config(); - PreparedStatements::global() - .write() - .close_unused(config.config.general.prepared_statements_limit); + // Explicit 0: drop everything not in use, whatever the configured + // limit is. With the default (unlimited) limit this would otherwise + // be a no-op. + PreparedStatements::global().write().close_unused(0); Ok(vec![]) } } diff --git a/pgdog/src/admin/set.rs b/pgdog/src/admin/set.rs index cae78e626..f296cc851 100644 --- a/pgdog/src/admin/set.rs +++ b/pgdog/src/admin/set.rs @@ -117,9 +117,25 @@ impl Command for Set { "prepared_statements_limit" => { config.config.general.prepared_statements_limit = self.value.parse()?; - PreparedStatements::global() - .write() - .close_unused(config.config.general.prepared_statements_limit); + if config.config.general.prepared_statements_limit == 0 { + tracing::warn!( + "prepared_statements_limit set to 0, which now means unlimited; \ + to clear the cache, use {}", + super::reset_prepared::RESET_PREPARED, + ); + } + PreparedStatements::global().write().configure( + config.config.general.prepared_statements_limit, + config.config.general.prepared_statements_memory_limit, + ); + } + + "prepared_statements_memory_limit" => { + config.config.general.prepared_statements_memory_limit = self.value.parse()?; + PreparedStatements::global().write().configure( + config.config.general.prepared_statements_limit, + config.config.general.prepared_statements_memory_limit, + ); } "prepared_statements" => { diff --git a/pgdog/src/backend/databases.rs b/pgdog/src/backend/databases.rs index a6e170341..6d2801681 100644 --- a/pgdog/src/backend/databases.rs +++ b/pgdog/src/backend/databases.rs @@ -107,6 +107,12 @@ pub fn init() -> Result<(), Error> { // Resize query cache Cache::resize(config.config.general.query_cache_limit); + // Apply prepared statements cache limits. + PreparedStatements::global().write().configure( + config.config.general.prepared_statements_limit, + config.config.general.prepared_statements_memory_limit, + ); + // Start two-pc manager. let _monitor = Manager::get(); @@ -147,10 +153,12 @@ pub fn reload() -> Result<(), Error> { // Reload TLS connectors. tls::reload()?; - // Remove any unused prepared statements. - PreparedStatements::global() - .write() - .close_unused(new_config.config.general.prepared_statements_limit); + // Apply prepared statements cache limits, dropping anything + // unused over the new caps. + PreparedStatements::global().write().configure( + new_config.config.general.prepared_statements_limit, + new_config.config.general.prepared_statements_memory_limit, + ); // Resize query cache. Cache::resize(new_config.config.general.query_cache_limit); diff --git a/pgdog/src/frontend/prepared_statements/global_cache.rs b/pgdog/src/frontend/prepared_statements/global_cache.rs index 50eed73ba..bcc645406 100644 --- a/pgdog/src/frontend/prepared_statements/global_cache.rs +++ b/pgdog/src/frontend/prepared_statements/global_cache.rs @@ -4,9 +4,12 @@ use crate::{ net::messages::{Parse, RowDescription}, stats::memory::MemoryUsage, }; -use std::{collections::hash_map::HashMap, str::from_utf8}; +use std::{ + collections::{BTreeSet, hash_map::HashMap}, + str::from_utf8, +}; -use fnv::FnvHashSet as HashSet; +use super::str_mem; // Format the globally unique prepared statement // name based on the counter. @@ -29,6 +32,7 @@ impl MemoryUsage for Statement { #[inline] fn memory_usage(&self) -> usize { self.parse.len() + + self.rewrite.as_ref().map(|parse| parse.len()).unwrap_or(0) + if let Some(ref row_description) = self.row_description { row_description.memory_usage() } else { @@ -112,16 +116,29 @@ impl CachedStmt { pub struct GlobalCache { statements: HashMap, names: HashMap, - unused: HashSet, + /// Statements no client is holding, ordered by creation: eviction takes + /// the oldest first, deterministically. + unused: BTreeSet, counter: usize, versions: usize, + /// Maximum number of cached statements (0 = unlimited). Only statements + /// no client holds can be evicted, so the cache can exceed this while + /// they are all in use. + capacity: usize, + /// Approximate memory budget in bytes (0 = unlimited), enforced the same + /// way as `capacity`. + memory_limit: usize, + /// Incremental sum of what the live entries cost; kept in step with every + /// insert and remove so enforcement doesn't rescan the maps. + bytes: usize, } impl MemoryUsage for GlobalCache { + /// The same number the memory limit is enforced against, so the metric + /// and the budget can't drift apart, plus the bookkeeping fields. #[inline] fn memory_usage(&self) -> usize { - self.statements.memory_usage() - + self.names.memory_usage() + self.bytes + self.counter.memory_usage() + self.versions.memory_usage() + self.unused.len() * std::mem::size_of::() @@ -129,6 +146,54 @@ impl MemoryUsage for GlobalCache { } impl GlobalCache { + /// Apply cache limits from configuration, evicting anything over the new + /// caps. A `capacity` or `memory_limit` of 0 disables that limit. + pub fn configure(&mut self, capacity: usize, memory_limit: usize) { + self.capacity = capacity; + self.memory_limit = memory_limit; + self.enforce(); + } + + /// Approximate memory used by the cached statements. + pub fn memory_bytes(&self) -> usize { + self.bytes + } + + /// What an entry adds to the byte total: both map entries, keyed by the + /// global name and the cache key. Kept symmetrical with `entry_removed`. + fn entry_inserted(&mut self, name: &str, statement: &Statement, cached: &CachedStmt) { + self.bytes += str_mem(name) + + statement.memory_usage() + + statement.cache_key.memory_usage() + + cached.memory_usage(); + } + + fn entry_removed(&mut self, name: &str, statement: &Statement, cached: &CachedStmt) { + self.bytes = self.bytes.saturating_sub( + str_mem(name) + + statement.memory_usage() + + statement.cache_key.memory_usage() + + cached.memory_usage(), + ); + } + + fn over_budget(&self) -> bool { + (self.capacity > 0 && self.statements.len() > self.capacity) + || (self.memory_limit > 0 && self.bytes > self.memory_limit) + } + + /// Evict statements nobody holds until the cache fits its limits. If every + /// statement is in use the cache stays over budget: evicting one would + /// break the client using it. + fn enforce(&mut self) { + while self.over_budget() { + let Some(counter) = self.unused.pop_first() else { + break; + }; + self.remove(&global_name(counter)); + } + } + /// Record a Parse message with the global cache and return a globally unique /// name PgDog is using for that statement. /// @@ -158,22 +223,20 @@ impl GlobalCache { version: 0, }; - self.statements.insert( - cache_key.clone(), - CachedStmt { - counter: self.counter, - used: 1, - }, - ); - - self.names.insert( - name.clone(), - Statement { - parse, - cache_key, - ..Default::default() - }, - ); + let cached = CachedStmt { + counter: self.counter, + used: 1, + }; + let statement = Statement { + parse, + cache_key: cache_key.clone(), + ..Default::default() + }; + + self.entry_inserted(&name, &statement, &cached); + self.statements.insert(cache_key, cached); + self.names.insert(name.clone(), statement); + self.enforce(); (true, name) } @@ -194,23 +257,21 @@ impl GlobalCache { version: self.versions, }; - self.statements.insert( - key.clone(), - CachedStmt { - counter: self.counter, - used: 1, - }, - ); + let cached = CachedStmt { + counter: self.counter, + used: 1, + }; + let statement = Statement { + parse, + version: self.versions, + cache_key: key.clone(), + ..Default::default() + }; - self.names.insert( - name.clone(), - Statement { - parse, - version: self.versions, - cache_key: key, - ..Default::default() - }, - ); + self.entry_inserted(&name, &statement, &cached); + self.statements.insert(key, cached); + self.names.insert(name.clone(), statement); + self.enforce(); name } @@ -218,7 +279,12 @@ impl GlobalCache { /// Rewrite prepared statement in the global cache. pub fn rewrite(&mut self, parse: &Parse) { if let Some(stmt) = self.names.get_mut(parse.name()) { + if let Some(old) = stmt.rewrite.take() { + self.bytes = self.bytes.saturating_sub(old.len()); + } + self.bytes += parse.len(); stmt.rewrite = Some(parse.clone()); + self.enforce(); } } @@ -228,17 +294,23 @@ impl GlobalCache { if let Some(ref mut entry) = self.names.get_mut(name) && entry.row_description.is_none() { + self.bytes += row_description.memory_usage(); entry.row_description = Some(row_description); + self.enforce(); } } - /// Clear the global cache. + /// Clear the global cache. Test-only: rolling the name counter back + /// would let a server connection holding an old `__pgdog_N` name be + /// handed a different query under it. + #[cfg(test)] pub fn reset(&mut self) { self.statements.clear(); self.names.clear(); self.unused.clear(); self.counter = 0; self.versions = 0; + self.bytes = 0; } /// Get the query string stored in the global cache @@ -305,19 +377,19 @@ impl GlobalCache { self.remove(name); } else if entry.used == 0 { self.unused.insert(entry.counter); + // The statement just became evictable; if the cache is + // over budget, this is the moment it can shrink. + self.enforce(); } } } } - /// Close all unused statements exceeding capacity. + /// Close unused statements until the cache is down to `capacity` entries, + /// or nothing unused is left. `0` removes every statement not in use; + /// statements clients hold, and the name counter, are never touched, so + /// global names are not reused. pub fn close_unused(&mut self, capacity: usize) -> usize { - if capacity == 0 { - let removed = self.len(); - self.reset(); - return removed; - } - let over = self.len().saturating_sub(capacity); let remove = self.unused.iter().take(over).copied().collect::>(); @@ -332,7 +404,12 @@ impl GlobalCache { /// Remove statement from global cache. fn remove(&mut self, name: &str) { if let Some(stmt) = self.names.remove(name) { - self.statements.remove(&stmt.cache_key()); + let cached = self.statements.remove(&stmt.cache_key()); + debug_assert!(cached.is_some(), "names and statements maps out of sync"); + if let Some(cached) = cached { + self.unused.remove(&cached.counter); + self.entry_removed(name, &stmt, &cached); + } } } @@ -344,6 +421,7 @@ impl GlobalCache { stmt.used = stmt.used.saturating_sub(1); if stmt.used == 0 { self.unused.insert(stmt.counter); + self.enforce(); } } } @@ -362,6 +440,215 @@ impl GlobalCache { mod test { use super::*; + use super::super::str_mem; + use crate::net::messages::Field; + + /// The incremental byte counter must equal a from-scratch recount over the + /// live entries, no matter what sequence of operations got us here. + fn recount(cache: &GlobalCache) -> usize { + cache + .names() + .iter() + .map(|(name, stmt)| str_mem(name) + stmt.memory_usage()) + .sum::() + + cache + .statements() + .iter() + .map(|(key, stmt)| key.memory_usage() + stmt.memory_usage()) + .sum::() + } + + #[test] + fn test_capacity_evicts_unused_on_insert() { + let mut cache = GlobalCache::default(); + cache.configure(10, 0); + + // Ten statements nobody uses anymore. + for i in 0..10 { + let (_, name) = cache.insert(&Parse::named("s", format!("SELECT {i:02}"))); + cache.close(&name); + } + assert_eq!(cache.len(), 10); + + // The next insert pushes the oldest unused one out instead of growing + // the cache. + let (new, name) = cache.insert(&Parse::named("s", "SELECT 'over'")); + assert!(new); + assert_eq!(cache.len(), 10); + assert!(cache.parse(&name).is_some(), "the new statement is cached"); + assert!( + cache.parse("__pgdog_1").is_none(), + "eviction is deterministic: oldest unused goes first" + ); + assert!(cache.parse("__pgdog_2").is_some()); + } + + #[test] + fn test_capacity_never_evicts_statements_in_use() { + let mut cache = GlobalCache::default(); + cache.configure(5, 0); + + let mut names = vec![]; + for i in 0..10 { + let (_, name) = cache.insert(&Parse::named("s", format!("SELECT {i:02}"))); + names.push(name); + } + + // All ten are still held by clients: over capacity, but evicting any + // of them would break the client using it. + assert_eq!(cache.len(), 10); + + // As clients let go, the cache falls back to its capacity. + for name in &names { + cache.close(name); + } + assert_eq!(cache.len(), 5); + } + + #[test] + fn test_memory_limit_evicts_unused() { + // Measure what one entry costs, then budget for about three. + let mut probe = GlobalCache::default(); + let (_, name) = probe.insert(&Parse::named("s", "SELECT 00")); + probe.close(&name); + let per_entry = probe.memory_bytes(); + assert!(per_entry > 0); + + let budget = per_entry * 3 + per_entry / 2; + let mut cache = GlobalCache::default(); + cache.configure(0, budget); + + for i in 0..10 { + let (_, name) = cache.insert(&Parse::named("s", format!("SELECT {i:02}"))); + cache.close(&name); + } + + assert!( + cache.memory_bytes() <= budget, + "cache stays within its memory budget: {} <= {}", + cache.memory_bytes(), + budget + ); + assert_eq!(cache.len(), 3); + } + + #[test] + fn test_memory_limit_never_evicts_statements_in_use() { + let mut cache = GlobalCache::default(); + cache.configure(0, 1); // Nothing fits. + + let (_, name) = cache.insert(&Parse::named("s", "SELECT 1")); + assert_eq!(cache.len(), 1, "a statement in use stays regardless"); + + cache.close(&name); + assert_eq!(cache.len(), 0, "and goes as soon as nobody holds it"); + } + + #[test] + fn test_zero_limits_mean_unlimited() { + let mut cache = GlobalCache::default(); + cache.configure(0, 0); + + for i in 0..1000 { + let (_, name) = cache.insert(&Parse::named("s", format!("SELECT {i:04}"))); + cache.close(&name); + } + + assert_eq!(cache.len(), 1000); + } + + #[test] + fn test_configure_enforces_immediately() { + let mut cache = GlobalCache::default(); + + for i in 0..100 { + let (_, name) = cache.insert(&Parse::named("s", format!("SELECT {i:03}"))); + cache.close(&name); + } + assert_eq!(cache.len(), 100); + + // A reload with a smaller limit shrinks the cache on the spot. + cache.configure(10, 0); + assert_eq!(cache.len(), 10); + } + + #[test] + fn test_decrement_releases_for_eviction() { + let mut cache = GlobalCache::default(); + cache.configure(1, 0); + + let (_, first) = cache.insert(&Parse::named("s", "SELECT 1")); + let (_, second) = cache.insert(&Parse::named("s", "SELECT 2")); + assert_eq!( + cache.len(), + 2, + "both in use: over capacity, nothing to evict" + ); + + // decrement() is the other way a statement gets released. + cache.decrement(&first); + assert_eq!(cache.len(), 1); + assert!( + cache.parse(&second).is_some(), + "the statement still in use survives" + ); + } + + #[test] + fn test_close_unused_zero_keeps_in_use_and_counter() { + let mut cache = GlobalCache::default(); + + let (_, held) = cache.insert(&Parse::named("s", "SELECT 'held'")); + let (_, released) = cache.insert(&Parse::named("s", "SELECT 'released'")); + cache.close(&released); + + assert_eq!(cache.close_unused(0), 1); + assert!(cache.parse(&held).is_some(), "statements in use survive"); + assert!(cache.parse(&released).is_none()); + + // The counter moves on: global names are never reused, so server + // connections holding old names can't be handed a different query. + let (_, next) = cache.insert(&Parse::named("s", "SELECT 'next'")); + assert_eq!(next, "__pgdog_3"); + } + + #[test] + fn test_memory_accounting_survives_mixed_operations() { + let mut cache = GlobalCache::default(); + cache.configure(0, 0); + + let mut names = vec![]; + for i in 0..20 { + let (_, name) = cache.insert(&Parse::named("s", format!("SELECT {i:02}"))); + names.push(name); + } + + // A RowDescription recorded later grows the entry. + cache.insert_row_description(&names[0], RowDescription::new(&[Field::text("x")])); + // So does a rewritten Parse, twice to cover the replacement path. + cache.rewrite(&Parse::named(&names[1], "SELECT 1, 2")); + cache.rewrite(&Parse::named(&names[1], "SELECT 1, 2, 3")); + // Duplicate insert of an existing statement adds nothing. + cache.insert(&Parse::named("s", "SELECT 00")); + // insert_anyway always creates a fresh entry. + let extra = cache.insert_anyway(&Parse::named("s", "SELECT 00")); + + for name in names.iter().chain([&extra]) { + cache.close(name); + } + cache.close(&names[0]); // The duplicate insert above took a second hold. + + assert_eq!(cache.memory_bytes(), recount(&cache)); + + // Evictions subtract what the entries actually cost. + cache.configure(5, 0); + assert_eq!(cache.len(), 5); + assert_eq!(cache.memory_bytes(), recount(&cache)); + + cache.reset(); + assert_eq!(cache.memory_bytes(), 0); + } + #[test] fn test_prep_stmt_cache_close() { let mut cache = GlobalCache::default(); diff --git a/pgdog/src/frontend/prepared_statements/mod.rs b/pgdog/src/frontend/prepared_statements/mod.rs index 310e668d7..f3c45f487 100644 --- a/pgdog/src/frontend/prepared_statements/mod.rs +++ b/pgdog/src/frontend/prepared_statements/mod.rs @@ -184,11 +184,16 @@ pub fn start_maintenance() { }); } -/// Check prepared statements cache for overflows -/// and remove any unused statements exceeding the limit. +/// Re-apply the configured cache limits, evicting anything unused over +/// them. A safety net behind the enforcement that already runs when the +/// cache grows or a statement is released; `0` means unlimited here the +/// same as everywhere else. pub fn run_maintenance() { - let capacity = config().config.general.prepared_statements_limit; - PreparedStatements::global().write().close_unused(capacity); + let general = &config().config.general; + PreparedStatements::global().write().configure( + general.prepared_statements_limit, + general.prepared_statements_memory_limit, + ); } #[cfg(test)] diff --git a/pgdog/src/stats/pools.rs b/pgdog/src/stats/pools.rs index b6755bd8b..bfba341b4 100644 --- a/pgdog/src/stats/pools.rs +++ b/pgdog/src/stats/pools.rs @@ -344,6 +344,17 @@ impl Pools { metric_type: None, })); + metrics.push(Metric::new(PoolMetric { + name: "prepared_statements_memory_limit".into(), + measurements: vec![Measurement { + labels: vec![], + measurement: general.prepared_statements_memory_limit.into(), + }], + help: "Memory limit (bytes) for the prepared statements cache, 0 = unlimited".into(), + unit: None, + metric_type: None, + })); + metrics.push(Metric::new(PoolMetric { name: "query_cache_limit".into(), measurements: vec![Measurement {