Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion documentation/en/src/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@

- **PAUSE, RESUME, RECONNECT admin commands**: New admin console commands for managing connection pools. `PAUSE [db]` blocks new backend connection acquisition (active transactions continue). `RESUME [db]` lifts the pause and unblocks waiting clients. `RECONNECT [db]` forces connection rotation by incrementing the pool epoch — idle connections are immediately closed and active connections are discarded when returned to the pool. Without arguments, all pools are affected; with a database name, only matching pools. Specifying a nonexistent database returns an error. Use `SHOW POOLS` to see the `paused` status column.

- **`default_min_pool_size` for dynamic auth_query passthrough pools**: New `auth_query.default_min_pool_size` setting controls the minimum number of backend connections maintained per dynamic user pool in passthrough mode. Connections are prewarmed in the background when the pool is first created and replenished by the retain cycle after `server_lifetime` expiry. Pools with `default_min_pool_size > 0` are never garbage-collected. Default is `0` (no prewarm — backward compatible). Note: total backend connections scale as `active_users × default_min_pool_size`.
- **`min_pool_size` for dynamic auth_query passthrough pools**: New `auth_query.min_pool_size` setting controls the minimum number of backend connections maintained per dynamic user pool in passthrough mode. Connections are prewarmed in the background when the pool is first created and replenished by the retain cycle after `server_lifetime` expiry. Pools with `min_pool_size > 0` are never garbage-collected. Default is `0` (no prewarm — backward compatible). Note: total backend connections scale as `active_users × min_pool_size`.

### 3.3.1 <small>Feb 26, 2026</small>

Expand Down
2 changes: 1 addition & 1 deletion pg_doorman.toml
Original file line number Diff line number Diff line change
Expand Up @@ -533,7 +533,7 @@ pool_size = 40
# auth_query.server_user = "app"
# auth_query.server_password = "secret"
# auth_query.default_pool_size = 40
# auth_query.default_min_pool_size = 0
# auth_query.min_pool_size = 0
# auth_query.cache_ttl = 3600000
# auth_query.cache_failure_ttl = 30000
# auth_query.min_interval = 1000
Expand Down
2 changes: 1 addition & 1 deletion pg_doorman.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -578,7 +578,7 @@ pools:
# server_user: "app"
# server_password: "secret"
# default_pool_size: 40
# default_min_pool_size: 0
# min_pool_size: 0
# cache_ttl: "1h"
# cache_failure_ttl: "30s"
# min_interval: "1s"
Expand Down
6 changes: 3 additions & 3 deletions src/app/generate/annotated.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1510,7 +1510,7 @@ fn write_auth_query_commented_example(w: &mut ConfigWriter) {
w.comment(fi, "auth_query.server_user = \"app\"");
w.comment(fi, "auth_query.server_password = \"secret\"");
w.comment(fi, "auth_query.default_pool_size = 40");
w.comment(fi, "auth_query.default_min_pool_size = 0");
w.comment(fi, "auth_query.min_pool_size = 0");
w.comment(fi, "auth_query.cache_ttl = 3600000");
w.comment(fi, "auth_query.cache_failure_ttl = 30000");
w.comment(fi, "auth_query.min_interval = 1000");
Expand All @@ -1528,7 +1528,7 @@ fn write_auth_query_commented_example(w: &mut ConfigWriter) {
w.comment(fi, " server_user: \"app\"");
w.comment(fi, " server_password: \"secret\"");
w.comment(fi, " default_pool_size: 40");
w.comment(fi, " default_min_pool_size: 0");
w.comment(fi, " min_pool_size: 0");
w.comment(fi, " cache_ttl: \"1h\"");
w.comment(fi, " cache_failure_ttl: \"30s\"");
w.comment(fi, " min_interval: \"1s\"");
Expand Down Expand Up @@ -1973,7 +1973,7 @@ mod tests {
"server_user",
"server_password",
"default_pool_size",
"default_min_pool_size",
"min_pool_size",
"cache_ttl",
"cache_failure_ttl",
"min_interval",
Expand Down
2 changes: 1 addition & 1 deletion src/app/generate/docs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ fn write_auth_query_section(out: &mut String) {
"server_user",
"server_password",
"default_pool_size",
"default_min_pool_size",
"min_pool_size",
"cache_ttl",
"cache_failure_ttl",
"min_interval",
Expand Down
4 changes: 2 additions & 2 deletions src/app/generate/fields.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1219,11 +1219,11 @@ fields:
doc: "Pool size for dynamic user data connections (per-user in passthrough mode, shared in dedicated mode)."
default: "40"

default_min_pool_size:
min_pool_size:
config:
en: "Minimum connections to maintain per dynamic user pool (passthrough mode only)."
ru: "Минимальное количество поддерживаемых соединений для каждого динамического пула (только passthrough-режим)."
doc: "Minimum number of backend connections to maintain per dynamic user pool in passthrough mode. Connections are prewarmed when the pool is first created and replenished by the retain cycle. Set to `0` to disable (default). Note: pools with `default_min_pool_size > 0` are never garbage-collected, and total backend connections scale as `active_users × default_min_pool_size`."
doc: "Minimum number of backend connections to maintain per dynamic user pool in passthrough mode. Connections are prewarmed when the pool is first created and replenished by the retain cycle. Set to `0` to disable (default). Note: pools with `min_pool_size > 0` are never garbage-collected, and total backend connections scale as `active_users × min_pool_size`."
default: "0"

cache_ttl:
Expand Down
2 changes: 1 addition & 1 deletion src/auth/auth_query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -692,7 +692,7 @@ mod tests {
server_user: None,
server_password: None,
default_pool_size: 40,
default_min_pool_size: 0,
min_pool_size: 0,
cache_ttl: Duration::from_hours(1),
cache_failure_ttl: Duration::from_secs(30),
min_interval: Duration::from_secs(1),
Expand Down
6 changes: 3 additions & 3 deletions src/config/pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,9 +213,9 @@ impl Pool {
if aq.pool_size == 0 {
return Err(Error::BadConfig("auth_query.pool_size must be > 0".into()));
}
if aq.default_min_pool_size > aq.default_pool_size {
if aq.min_pool_size > aq.default_pool_size {
return Err(Error::BadConfig(
"auth_query: default_min_pool_size must be <= default_pool_size".into(),
"auth_query: min_pool_size must be <= default_pool_size".into(),
));
}
}
Expand Down Expand Up @@ -285,7 +285,7 @@ pub struct AuthQueryConfig {
/// Minimum connections to maintain per dynamic user pool (default: 0 = no prewarm).
/// Only applies in passthrough mode (when server_user is not set).
#[serde(default)]
pub default_min_pool_size: u32,
pub min_pool_size: u32,

/// Max cache age for positive entries (default: "1h").
#[serde(default = "AuthQueryConfig::default_cache_ttl")]
Expand Down
12 changes: 6 additions & 6 deletions src/config/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1014,7 +1014,7 @@ async fn test_auth_query_validate_empty_query() {
server_user: None,
server_password: None,
default_pool_size: 40,
default_min_pool_size: 0,
min_pool_size: 0,
cache_ttl: Duration::from_hours(1),
cache_failure_ttl: Duration::from_secs(30),
min_interval: Duration::from_secs(1),
Expand All @@ -1040,7 +1040,7 @@ async fn test_auth_query_validate_empty_user() {
server_user: None,
server_password: None,
default_pool_size: 40,
default_min_pool_size: 0,
min_pool_size: 0,
cache_ttl: Duration::from_hours(1),
cache_failure_ttl: Duration::from_secs(30),
min_interval: Duration::from_secs(1),
Expand All @@ -1066,7 +1066,7 @@ async fn test_auth_query_validate_server_password_without_server_user() {
server_user: None,
server_password: Some("orphan_password".to_string()),
default_pool_size: 40,
default_min_pool_size: 0,
min_pool_size: 0,
cache_ttl: Duration::from_hours(1),
cache_failure_ttl: Duration::from_secs(30),
min_interval: Duration::from_secs(1),
Expand All @@ -1092,7 +1092,7 @@ async fn test_auth_query_validate_server_user_without_password_ok() {
server_user: Some("backend_user".to_string()),
server_password: None,
default_pool_size: 40,
default_min_pool_size: 0,
min_pool_size: 0,
cache_ttl: Duration::from_hours(1),
cache_failure_ttl: Duration::from_secs(30),
min_interval: Duration::from_secs(1),
Expand All @@ -1115,7 +1115,7 @@ async fn test_auth_query_validate_pool_size_zero() {
server_user: None,
server_password: None,
default_pool_size: 40,
default_min_pool_size: 0,
min_pool_size: 0,
cache_ttl: Duration::from_hours(1),
cache_failure_ttl: Duration::from_secs(30),
min_interval: Duration::from_secs(1),
Expand All @@ -1141,7 +1141,7 @@ async fn test_auth_query_validate_empty_password_ok() {
server_user: None,
server_password: None,
default_pool_size: 40,
default_min_pool_size: 0,
min_pool_size: 0,
cache_ttl: Duration::from_hours(1),
cache_failure_ttl: Duration::from_secs(30),
min_interval: Duration::from_secs(1),
Expand Down
14 changes: 7 additions & 7 deletions src/pool/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1089,8 +1089,8 @@ pub fn create_dynamic_pool(
username: username.to_string(),
password: String::new(),
pool_size: aq_config.default_pool_size,
min_pool_size: if aq_config.default_min_pool_size > 0 {
Some(aq_config.default_min_pool_size)
min_pool_size: if aq_config.min_pool_size > 0 {
Some(aq_config.min_pool_size)
} else {
None
},
Expand Down Expand Up @@ -1209,21 +1209,21 @@ pub fn create_dynamic_pool(
POOLS.store(Arc::new(new_pools));
register_dynamic_pool(&identifier);

// Prewarm: spawn background task to create default_min_pool_size connections
if aq_config.default_min_pool_size > 0 {
// Prewarm: spawn background task to create min_pool_size connections
if aq_config.min_pool_size > 0 {
let pool_clone = conn_pool.clone();
let min = aq_config.default_min_pool_size as usize;
let min = aq_config.min_pool_size as usize;
let pn = pool_name.to_string();
let un = username.to_string();
tokio::spawn(async move {
let created = pool_clone.database.replenish(min).await;
if created > 0 {
info!(
"[pool: {pn}][user: {un}] prewarmed {created} dynamic connection(s) (default_min_pool_size: {min})"
"[pool: {pn}][user: {un}] prewarmed {created} dynamic connection(s) (min_pool_size: {min})"
);
} else {
warn!(
"[pool: {pn}][user: {un}] dynamic prewarm failed (default_min_pool_size: {min})"
"[pool: {pn}][user: {un}] dynamic prewarm failed (min_pool_size: {min})"
);
}
});
Expand Down
2 changes: 1 addition & 1 deletion tests/bdd/auth_query_helper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ fn build_test_config(query: &str, pool_size: u32, database: Option<String>) -> A
server_user: None,
server_password: None,
default_pool_size: 40,
default_min_pool_size: 0,
min_pool_size: 0,
cache_ttl: Duration::from_hours(1),
cache_failure_ttl: Duration::from_secs(30),
min_interval: Duration::from_secs(1),
Expand Down
14 changes: 7 additions & 7 deletions tests/bdd/features/auth-query-min-pool-size.feature
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
@auth-query @auth-query-min-pool-size
Feature: Auth query default_min_pool_size for dynamic passthrough pools
Feature: Auth query min_pool_size for dynamic passthrough pools

Tests that default_min_pool_size prewarmed connections are created and maintained
Tests that min_pool_size prewarmed connections are created and maintained
for dynamic auth_query passthrough pools.

Scenario: Dynamic pool prewarm with default_min_pool_size
Scenario: Dynamic pool prewarm with min_pool_size
Given PostgreSQL started with pg_hba.conf:
"""
local all all trust
Expand Down Expand Up @@ -49,7 +49,7 @@ Feature: Auth query default_min_pool_size for dynamic passthrough pools
password: ""
pool_size: 1
default_pool_size: 5
default_min_pool_size: 2
min_pool_size: 2
cache_ttl: "1h"
cache_failure_ttl: "30s"
min_interval: "0s"
Expand All @@ -59,12 +59,12 @@ Feature: Auth query default_min_pool_size for dynamic passthrough pools
And we send SimpleQuery "SELECT 1" to session "md5_session"
# Wait for prewarm + retain cycles to establish min_pool_size connections
When we sleep for 2000 milliseconds
# Check server connections — should have at least 2 (default_min_pool_size=2)
# Check server connections — should have at least 2 (min_pool_size=2)
When we create admin session "admin1" to pg_doorman as "admin" with password "admin"
And we execute "SHOW SERVERS" on admin session "admin1" and store row count
Then admin session "admin1" row count should be greater than or equal to 2

Scenario: Dynamic pool maintains default_min_pool_size after server_lifetime expiry
Scenario: Dynamic pool maintains min_pool_size after server_lifetime expiry
Given PostgreSQL started with pg_hba.conf:
"""
local all all trust
Expand Down Expand Up @@ -110,7 +110,7 @@ Feature: Auth query default_min_pool_size for dynamic passthrough pools
password: ""
pool_size: 1
default_pool_size: 5
default_min_pool_size: 2
min_pool_size: 2
cache_ttl: "1h"
cache_failure_ttl: "30s"
min_interval: "0s"
Expand Down
4 changes: 2 additions & 2 deletions tests/bdd/features/pool-lifecycle-e2e.feature
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ Feature: Pool lifecycle end-to-end (prewarm → scale up → shrink)
@e2e-auth-query-lifecycle
Scenario: Auth_query pool lifecycle: scale up under load → shrink to 0 after retain
# auth_query dynamic pool with default_pool_size=5, pool-level server_lifetime=1000ms.
# default_min_pool_size is not set (defaults to 0), so no prewarm.
# min_pool_size is not set (defaults to 0), so no prewarm.
# Phase 1: Open 5 concurrent transactions via auth_query user → pool scales to 5.
# Phase 2: Release all, wait for lifetime+retain → pool shrinks to 0.
Given PostgreSQL started with pg_hba.conf:
Expand Down Expand Up @@ -144,6 +144,6 @@ Feature: Pool lifecycle end-to-end (prewarm → scale up → shrink)
And we send SimpleQuery "COMMIT" to session "s5"
# Wait for pool-level server_lifetime (1000ms ±20%) + retain cycles
When we sleep for 4000 milliseconds
# All connections should be closed (default_min_pool_size=0, no prewarm)
# All connections should be closed (min_pool_size=0, no prewarm)
And we execute "SHOW SERVERS" on admin session "admin1" and store row count
Then admin session "admin1" row count should be 0
Loading