From 1cb17c907318c5f9b46be4eea293d9aec83ef5ae Mon Sep 17 00:00:00 2001 From: dmitrivasilyev Date: Thu, 30 Apr 2026 12:36:23 +0300 Subject: [PATCH 01/13] fix(bench): expose notify_idle_returned to coordinator_benchmarks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bench measures the cost of notify_idle_returned, but the helper was pub(crate) — and Cargo compiles benches as a separate consumer crate, so the bench failed to build with E0624 ("private method") even on the current master. Mark it #[doc(hidden)] pub: still excluded from the documented public surface of the lib crate, but reachable from the bench. The helper itself is unchanged. --- src/pool/pool_coordinator.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pool/pool_coordinator.rs b/src/pool/pool_coordinator.rs index 8bb5f2ad4..5a9e46857 100644 --- a/src/pool/pool_coordinator.rs +++ b/src/pool/pool_coordinator.rs @@ -518,7 +518,8 @@ impl PoolCoordinator { /// `server_lifetime` ages a connection out — the waiter would then /// timeout into Phase D even though the cross-pool system had headroom /// every few milliseconds. - pub(crate) fn notify_idle_returned(&self) { + #[doc(hidden)] + pub fn notify_idle_returned(&self) { self.connection_returned.notify_one(); } From dc9a7493c180c767cfd136dc4df4ec9afe1a2912 Mon Sep 17 00:00:00 2001 From: dmitrivasilyev Date: Thu, 30 Apr 2026 12:36:36 +0300 Subject: [PATCH 02/13] chore(deps): bump Rust toolchain to 1.95.0 and tokio to 1.52.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Foundation for the 3.7.0 upgrade — get the toolchain and async runtime to current stable so the next steps (edition 2024 migration, clippy fixes) don't have to deal with year-old tooling. Rust pinned to 1.95.0 (was 1.87.0); tokio to 1.52.1 (was 1.49.0 in the lockfile). The lockfile picks up matching minor bumps in libc, mio, socket2, and tokio-macros that the new tokio requires. --- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 4 ++-- rust-toolchain.toml | 2 +- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 625209aa5..eaff820a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1494,9 +1494,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.172" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "linked-hash-map" @@ -1598,13 +1598,13 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.3" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1928,7 +1928,7 @@ dependencies = [ "sha-1", "sha2", "smallvec", - "socket2 0.6.1", + "socket2 0.6.3", "stringprep", "syslog", "tempfile", @@ -2747,12 +2747,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -3100,9 +3100,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.49.0" +version = "1.52.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" dependencies = [ "bytes", "libc", @@ -3110,16 +3110,16 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.1", + "socket2 0.6.3", "tokio-macros", "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index f55539216..d248df139 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "pg_doorman" version = "3.6.3" edition = "2021" -rust-version = "1.87.0" +rust-version = "1.95.0" license = "MIT" @@ -30,7 +30,7 @@ quanta = "0.12" arc-swap = "1.7.1" toml = "0.8" prometheus = "0.14.0" -tokio = { version = "1.48.4", features = ["rt-multi-thread", "fs", "parking_lot", "sync", "io-util", "net", "macros", "signal", "time"] } +tokio = { version = "1.52.1", features = ["rt-multi-thread", "fs", "parking_lot", "sync", "io-util", "net", "macros", "signal", "time"] } exitcode = "1.1.2" pin-project = "1" bytes = "1.10.1" diff --git a/rust-toolchain.toml b/rust-toolchain.toml index b8889a3bb..f25b5b140 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.87.0" +channel = "1.95.0" From e98cfbd47fe849d7fc7f799dc9eb061b5b783dbd Mon Sep 17 00:00:00 2001 From: dmitrivasilyev Date: Thu, 30 Apr 2026 13:26:07 +0300 Subject: [PATCH 03/13] chore: migrate to edition 2024 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step 2 of the 3.7.0 upgrade. Bumps the project to Rust edition 2024 and adapts source to the new language defaults. Two production paths required real code changes: - std::env::remove_var becomes unsafe in 2024 (it can race with other threads reading the environment); both call sites in run_server are now wrapped in unsafe blocks. - unsafe fn bodies are no longer implicit unsafe blocks; each unsafe fn in the daemon module now opens an explicit unsafe block over its body. The remaining source edits are pattern-binding cleanups that 2024's match ergonomics make redundant (if let Some(ref x) collapses to if let Some(x)). The bulk of the diff is rustfmt under edition 2024 — formatter defaults shifted (import grouping, trailing-comment alignment). Note: cargo clippy --deny warnings does not pass yet on this commit. Edition 2024 enables collapsible_if-style diagnostics on the new if-let-chains, and rustc 1.95 surfaces several manual_checked_ops, collapsible_match, and similar lints — all addressed in the next commit (C3). --- Cargo.toml | 2 +- benches/cache_benchmarks.rs | 2 +- benches/coordinator_benchmarks.rs | 2 +- benches/hash_benchmarks.rs | 2 +- benches/log_benchmarks.rs | 2 +- benches/memory_benchmarks.rs | 2 +- benches/pool_anticipation_benchmarks.rs | 4 +- src/admin/commands.rs | 4 +- src/admin/show.rs | 8 +- src/app/config.rs | 2 +- src/app/generate/annotated.rs | 20 +- src/app/generate/docs.rs | 257 ++++++++++++++---- src/app/logger.rs | 2 +- src/app/mod.rs | 4 +- src/app/server.rs | 24 +- src/auth/auth_query.rs | 4 +- src/auth/hba.rs | 2 +- src/auth/hba_eval_tests.rs | 2 +- src/auth/mod.rs | 48 +++- src/auth/pam.rs | 6 +- src/auth/scram.rs | 14 +- src/auth/scram_client.rs | 8 +- src/auth/tests.rs | 2 +- src/bin/patroni_proxy/config.rs | 27 +- src/bin/patroni_proxy/main.rs | 4 +- src/bin/patroni_proxy/port.rs | 4 +- src/bin/patroni_proxy/stream.rs | 2 +- .../tests/bdd/mock_backend_helper.rs | 2 +- .../patroni_proxy/tests/bdd/proxy_helper.rs | 2 +- src/client/core.rs | 4 +- src/client/entrypoint.rs | 2 +- src/client/migration.rs | 12 +- src/client/mod.rs | 4 +- src/client/protocol.rs | 20 +- src/client/startup.rs | 6 +- src/client/transaction.rs | 14 +- src/client/util.rs | 2 +- src/config/byte_size.rs | 2 +- src/config/duration.rs | 2 +- src/config/mod.rs | 2 +- src/config/pool.rs | 9 +- src/config/tests.rs | 4 +- src/daemon/lib.rs | 236 +++++++++------- src/messages/extended.rs | 2 +- src/messages/mod.rs | 6 +- src/messages/protocol.rs | 6 +- src/messages/socket.rs | 16 +- src/messages/tests.rs | 4 +- src/pool/dynamic.rs | 14 +- src/pool/eviction.rs | 2 +- src/pool/fallback.rs | 2 +- src/pool/gc.rs | 4 +- src/pool/inner.rs | 12 +- src/pool/mod.rs | 10 +- src/pool/pool_coordinator.rs | 6 +- src/pool/retain.rs | 14 +- src/pool/server_pool.rs | 9 +- src/prometheus/metrics.rs | 23 +- src/prometheus/server.rs | 4 +- src/server/mod.rs | 2 +- src/server/prepared_statement_cache.rs | 7 +- src/server/protocol_io.rs | 34 ++- src/server/server_backend.rs | 18 +- src/server/startup_error.rs | 2 +- src/stats/client.rs | 4 +- src/stats/mod.rs | 4 +- src/stats/pool.rs | 6 +- src/stats/server.rs | 4 +- src/utils/core_affinity.rs | 8 +- src/utils/rate_limit.rs | 2 +- tests/bdd/extended/admin_session.rs | 2 +- tests/bdd/pg_connection.rs | 2 +- tests/bdd/pool_bench_helper.rs | 2 +- tests/bdd/postgres_helper.rs | 2 +- tests/bdd/shell_helper.rs | 6 +- 75 files changed, 630 insertions(+), 392 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d248df139..1ccdc43da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "pg_doorman" version = "3.6.3" -edition = "2021" +edition = "2024" rust-version = "1.95.0" license = "MIT" diff --git a/benches/cache_benchmarks.rs b/benches/cache_benchmarks.rs index 4150ff81b..3d7d7cde3 100644 --- a/benches/cache_benchmarks.rs +++ b/benches/cache_benchmarks.rs @@ -1,4 +1,4 @@ -use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; +use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main}; use std::sync::Arc; use pg_doorman::auth::auth_query::{AuthQueryCache, PasswordFetcher}; diff --git a/benches/coordinator_benchmarks.rs b/benches/coordinator_benchmarks.rs index 87e505005..ef1ca003a 100644 --- a/benches/coordinator_benchmarks.rs +++ b/benches/coordinator_benchmarks.rs @@ -1,4 +1,4 @@ -use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; +use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main}; use std::sync::Arc; use tokio::sync::Semaphore; diff --git a/benches/hash_benchmarks.rs b/benches/hash_benchmarks.rs index 5a89557e0..fa60b4f12 100644 --- a/benches/hash_benchmarks.rs +++ b/benches/hash_benchmarks.rs @@ -1,4 +1,4 @@ -use criterion::{criterion_group, criterion_main, Criterion, Throughput}; +use criterion::{Criterion, Throughput, criterion_group, criterion_main}; use std::collections::hash_map::DefaultHasher; use std::hash::Hasher; use xxhash_rust::xxh3::Xxh3; diff --git a/benches/log_benchmarks.rs b/benches/log_benchmarks.rs index 4d0c5c87c..a53b1ad47 100644 --- a/benches/log_benchmarks.rs +++ b/benches/log_benchmarks.rs @@ -1,4 +1,4 @@ -use criterion::{criterion_group, criterion_main, Criterion, Throughput}; +use criterion::{Criterion, Throughput, criterion_group, criterion_main}; use log::Log; use std::io::Write; use std::sync::atomic::{AtomicU64, Ordering}; diff --git a/benches/memory_benchmarks.rs b/benches/memory_benchmarks.rs index 8001857b8..78243f0b9 100644 --- a/benches/memory_benchmarks.rs +++ b/benches/memory_benchmarks.rs @@ -1,5 +1,5 @@ use bytes::BytesMut; -use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; +use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main}; fn bytesmut_allocation(c: &mut Criterion) { let mut group = c.benchmark_group("bytesmut_alloc"); diff --git a/benches/pool_anticipation_benchmarks.rs b/benches/pool_anticipation_benchmarks.rs index b87397ee9..3e2f604d5 100644 --- a/benches/pool_anticipation_benchmarks.rs +++ b/benches/pool_anticipation_benchmarks.rs @@ -19,9 +19,9 @@ //! them private avoids leaking test/bench scaffolding into the public API //! surface, and the duplication is twenty trivial lines. -use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; -use std::sync::atomic::{AtomicUsize, Ordering}; +use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main}; use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; use std::time::Duration; use tokio::sync::Notify; diff --git a/src/admin/commands.rs b/src/admin/commands.rs index b8d1a24b3..981657d00 100644 --- a/src/admin/commands.rs +++ b/src/admin/commands.rs @@ -10,7 +10,7 @@ use crate::errors::Error; use crate::messages::protocol::{command_complete, data_row, row_description}; use crate::messages::socket::write_all_half; use crate::messages::types::DataType; -use crate::pool::{get_all_pools, ClientServerMap, PoolMap}; +use crate::pool::{ClientServerMap, PoolMap, get_all_pools}; /// Reload the configuration file without restarting the process. pub async fn reload(stream: &mut T, client_server_map: ClientServerMap) -> Result<(), Error> @@ -133,7 +133,7 @@ async fn check_db_has_pools( where T: tokio::io::AsyncWrite + std::marker::Unpin, { - if let Some(ref db_name) = db { + if let Some(db_name) = db { if !pools.keys().any(|id| id.db == *db_name) { admin_error_response( stream, diff --git a/src/admin/show.rs b/src/admin/show.rs index 215b3f763..a81a26e62 100644 --- a/src/admin/show.rs +++ b/src/admin/show.rs @@ -6,20 +6,20 @@ use std::sync::atomic::Ordering; use bytes::{BufMut, BytesMut}; use crate::app::log_level; -use crate::config::{get_config, VERSION}; +use crate::config::{VERSION, get_config}; use crate::errors::Error; use crate::messages::protocol::{command_complete, data_row, row_description}; use crate::messages::socket::write_all_half; use crate::messages::types::DataType; -use crate::pool::{get_all_pools, AUTH_QUERY_STATE, COORDINATORS, DYNAMIC_POOLS}; +use crate::pool::{AUTH_QUERY_STATE, COORDINATORS, DYNAMIC_POOLS, get_all_pools}; use crate::stats::client::{CLIENT_STATE_ACTIVE, CLIENT_STATE_IDLE}; #[cfg(target_os = "linux")] use crate::stats::get_socket_states_count; use crate::stats::pool::PoolStats; use crate::stats::server::{SERVER_STATE_ACTIVE, SERVER_STATE_IDLE}; use crate::stats::{ - get_client_stats, get_server_stats, CANCEL_CONNECTION_COUNTER, PLAIN_CONNECTION_COUNTER, - TLS_CONNECTION_COUNTER, TOTAL_CONNECTION_COUNTER, + CANCEL_CONNECTION_COUNTER, PLAIN_CONNECTION_COUNTER, TLS_CONNECTION_COUNTER, + TOTAL_CONNECTION_COUNTER, get_client_stats, get_server_stats, }; /// Column-oriented statistics. diff --git a/src/app/config.rs b/src/app/config.rs index 0d6f9e6b6..b9a85f1ce 100644 --- a/src/app/config.rs +++ b/src/app/config.rs @@ -1,7 +1,7 @@ use log::error; use std::io::{self, IsTerminal, Write}; -use crate::config::{get_config, Config}; +use crate::config::{Config, get_config}; use tokio::runtime::Builder; use crate::app::args::Args; diff --git a/src/app/generate/annotated.rs b/src/app/generate/annotated.rs index 3c9f3f085..266f38659 100644 --- a/src/app/generate/annotated.rs +++ b/src/app/generate/annotated.rs @@ -24,11 +24,7 @@ pub(crate) struct I18n { impl I18n { pub(crate) fn get(&self, russian: bool) -> &str { - if russian { - &self.ru - } else { - &self.en - } + if russian { &self.ru } else { &self.en } } } @@ -217,11 +213,7 @@ impl ConfigWriter { /// Select English or Russian text. fn t<'a>(&self, en: &'a str, ru: &'a str) -> &'a str { - if self.russian { - ru - } else { - en - } + if self.russian { ru } else { en } } /// Write a comment line with given indent level (2 spaces per level for YAML). @@ -1346,8 +1338,12 @@ fn write_pool_users(w: &mut ConfigWriter, pool_name: &str, users: &[User]) { match w.format { ConfigFormat::Toml => { w.separator(fi, f.section_title("pool_users_toml").get(w.russian)); - let en_msg = format!("Users are defined with numeric indices: [pools.{pool_name}.users.0], [pools.{pool_name}.users.1], etc."); - let ru_msg = format!("Пользователи задаются с числовыми индексами: [pools.{pool_name}.users.0], [pools.{pool_name}.users.1] и т.д."); + let en_msg = format!( + "Users are defined with numeric indices: [pools.{pool_name}.users.0], [pools.{pool_name}.users.1], etc." + ); + let ru_msg = format!( + "Пользователи задаются с числовыми индексами: [pools.{pool_name}.users.0], [pools.{pool_name}.users.1] и т.д." + ); w.comment(fi, w.t(&en_msg, &ru_msg)); w.comment(fi, f.text("pool_users_unique").get(w.russian)); } diff --git a/src/app/generate/docs.rs b/src/app/generate/docs.rs index 9c8fb76e7..7d14685d2 100644 --- a/src/app/generate/docs.rs +++ b/src/app/generate/docs.rs @@ -5,7 +5,7 @@ use std::fmt::Write; -use super::annotated::{FieldsData, FIELDS}; +use super::annotated::{FIELDS, FieldsData}; /// Generate the general settings reference doc. pub fn generate_general_doc() -> String { @@ -26,7 +26,10 @@ pub fn generate_pool_doc() -> String { let mut out = String::with_capacity(8 * 1024); let _ = writeln!(out, "## Pool Settings\n"); - let _ = writeln!(out, "Each record in the pool is the name of the virtual database that the pg-doorman client can connect to.\n"); + let _ = writeln!( + out, + "Each record in the pool is the name of the virtual database that the pg-doorman client can connect to.\n" + ); let _ = writeln!( out, "```toml\n[pools.exampledb] # Declaring the 'exampledb' database\n```\n" @@ -44,7 +47,10 @@ pub fn generate_prometheus_doc() -> String { let mut out = String::with_capacity(8 * 1024); let _ = writeln!(out, "# Prometheus Settings\n"); - let _ = writeln!(out, "pg_doorman includes a Prometheus metrics exporter that provides detailed insights into the performance and behavior of your connection pools. This document describes how to enable and use the Prometheus metrics exporter, as well as the available metrics.\n"); + let _ = writeln!( + out, + "pg_doorman includes a Prometheus metrics exporter that provides detailed insights into the performance and behavior of your connection pools. This document describes how to enable and use the Prometheus metrics exporter, as well as the available metrics.\n" + ); write_prometheus_fields(&mut out, f); write_prometheus_metrics_section(&mut out); @@ -87,8 +93,14 @@ fn write_config_format_section(out: &mut String) { out, "* **YAML** (`.yaml`, `.yml`) - The primary and recommended format for new configurations." ); - let _ = writeln!(out, "* **TOML** (`.toml`) - Supported for backward compatibility with existing configurations.\n"); - let _ = writeln!(out, "The format is automatically detected based on the file extension. Both formats support the same configuration options and can be used interchangeably.\n"); + let _ = writeln!( + out, + "* **TOML** (`.toml`) - Supported for backward compatibility with existing configurations.\n" + ); + let _ = writeln!( + out, + "The format is automatically detected based on the file extension. Both formats support the same configuration options and can be used interchangeably.\n" + ); write_config_examples(out); write_generate_command(out); @@ -111,7 +123,10 @@ fn write_config_examples(out: &mut String) { fn write_generate_command(out: &mut String) { let _ = writeln!(out, "### Generate Command\n"); - let _ = writeln!(out, "The `generate` command can output configuration in either format. The format is determined by the output file extension. By default, the generated config includes detailed inline comments explaining every parameter.\n"); + let _ = writeln!( + out, + "The `generate` command can output configuration in either format. The format is determined by the output file extension. By default, the generated config includes detailed inline comments explaining every parameter.\n" + ); let _ = writeln!( out, "```bash\n# Generate YAML configuration (recommended)\npg_doorman generate --output config.yaml\n\n# Generate TOML configuration (for backward compatibility)\npg_doorman generate --output config.toml\n\n# Generate a complete reference config without PG connection\npg_doorman generate --reference --output config.yaml\n\n# Generate reference config with Russian comments\npg_doorman generate --reference --ru --output config.yaml\n\n# Generate config without comments (plain serialization)\npg_doorman generate --no-comments --output config.yaml\n```\n" @@ -119,18 +134,30 @@ fn write_generate_command(out: &mut String) { let _ = writeln!(out, "| Flag | Description |"); let _ = writeln!(out, "|------|-------------|"); - let _ = writeln!(out, "| `--no-comments` | Disable inline comments in generated config (by default, comments are included) |"); - let _ = writeln!(out, "| `--reference` | Generate a complete reference config with example values, no PostgreSQL connection needed |"); + let _ = writeln!( + out, + "| `--no-comments` | Disable inline comments in generated config (by default, comments are included) |" + ); + let _ = writeln!( + out, + "| `--reference` | Generate a complete reference config with example values, no PostgreSQL connection needed |" + ); let _ = writeln!( out, "| `--russian-comments`, `--ru` | Generate comments in Russian for quick start guide |" ); - let _ = writeln!(out, "| `--format`, `-f` | Output format: `yaml` (default) or `toml`. If `--output` is specified, format is auto-detected from file extension. This flag overrides auto-detection |\n"); + let _ = writeln!( + out, + "| `--format`, `-f` | Output format: `yaml` (default) or `toml`. If `--output` is specified, format is auto-detected from file extension. This flag overrides auto-detection |\n" + ); } fn write_include_files(out: &mut String) { let _ = writeln!(out, "### Include Files\n"); - let _ = writeln!(out, "Include files can be in either format, and you can mix formats. For example, a YAML main config can include TOML files and vice versa:\n"); + let _ = writeln!( + out, + "Include files can be in either format, and you can mix formats. For example, a YAML main config can include TOML files and vice versa:\n" + ); let _ = writeln!( out, "```yaml\ninclude:\n files:\n - \"pools.yaml\"\n - \"users.toml\"\n```\n" @@ -139,7 +166,10 @@ fn write_include_files(out: &mut String) { fn write_human_readable_section(out: &mut String) { let _ = writeln!(out, "## Human-Readable Values\n"); - let _ = writeln!(out, "pg_doorman supports human-readable formats for duration and byte size values, while maintaining backward compatibility with numeric values.\n"); + let _ = writeln!( + out, + "pg_doorman supports human-readable formats for duration and byte size values, while maintaining backward compatibility with numeric values.\n" + ); let _ = writeln!(out, "### Duration Format\n"); let _ = writeln!(out, "Duration values can be specified as:\n"); let _ = writeln!( @@ -166,7 +196,10 @@ fn write_human_readable_section(out: &mut String) { ); let _ = writeln!(out, "**Examples:**"); - let _ = writeln!(out, "```yaml\ngeneral:\n # All these are equivalent (3 seconds):\n # connect_timeout: 3000 # backward compatible (milliseconds)\n # connect_timeout: \"3s\" # human-readable\n # connect_timeout: \"3000ms\" # explicit milliseconds\n connect_timeout: \"3s\"\n idle_timeout: \"10m\" # 10 minutes\n server_lifetime: \"1h\" # 1 hour\n```\n"); + let _ = writeln!( + out, + "```yaml\ngeneral:\n # All these are equivalent (3 seconds):\n # connect_timeout: 3000 # backward compatible (milliseconds)\n # connect_timeout: \"3s\" # human-readable\n # connect_timeout: \"3000ms\" # explicit milliseconds\n connect_timeout: \"3s\"\n idle_timeout: \"10m\" # 10 minutes\n server_lifetime: \"1h\" # 1 hour\n```\n" + ); let _ = writeln!(out, "### Byte Size Format\n"); let _ = writeln!(out, "Byte size values can be specified as:\n"); @@ -194,7 +227,10 @@ fn write_human_readable_section(out: &mut String) { "Note: Uses binary prefixes (1 KB = 1024 bytes, not 1000 bytes).\n" ); let _ = writeln!(out, "**Examples:**"); - let _ = writeln!(out, "```yaml\ngeneral:\n # All these are equivalent (256 MB):\n # max_memory_usage: 268435456 # backward compatible (bytes)\n # max_memory_usage: \"256MB\" # human-readable\n # max_memory_usage: \"256M\" # short form\n max_memory_usage: \"256MB\"\n unix_socket_buffer_size: \"1MB\" # 1 MB\n worker_stack_size: \"8MB\" # 8 MB\n```\n"); + let _ = writeln!( + out, + "```yaml\ngeneral:\n # All these are equivalent (256 MB):\n # max_memory_usage: 268435456 # backward compatible (bytes)\n # max_memory_usage: \"256MB\" # human-readable\n # max_memory_usage: \"256M\" # short form\n max_memory_usage: \"256MB\"\n unix_socket_buffer_size: \"1MB\" # 1 MB\n worker_stack_size: \"8MB\" # 8 MB\n```\n" + ); } fn write_general_fields(out: &mut String, f: &FieldsData) { @@ -299,15 +335,36 @@ fn write_auth_query_section(out: &mut String) { let f = &*FIELDS; let _ = writeln!(out, "## Auth Query Settings\n"); - let _ = writeln!(out, "The `auth_query` section enables dynamic user authentication by querying a PostgreSQL database for credentials at connection time. This allows pg_doorman to authenticate users without listing them statically in the configuration file.\n"); - let _ = writeln!(out, "```yaml\npools:\n mydb:\n auth_query:\n query: \"SELECT passwd FROM pg_shadow WHERE usename = $1\"\n user: \"doorman_auth\"\n password: \"auth_password\"\n```\n"); + let _ = writeln!( + out, + "The `auth_query` section enables dynamic user authentication by querying a PostgreSQL database for credentials at connection time. This allows pg_doorman to authenticate users without listing them statically in the configuration file.\n" + ); + let _ = writeln!( + out, + "```yaml\npools:\n mydb:\n auth_query:\n query: \"SELECT passwd FROM pg_shadow WHERE usename = $1\"\n user: \"doorman_auth\"\n password: \"auth_password\"\n```\n" + ); let _ = writeln!(out, "There are two modes of operation:\n"); - let _ = writeln!(out, "- **Dedicated mode** (`server_user` is set): All dynamically authenticated users share a single connection pool that connects to PostgreSQL as `server_user`. This is the simplest setup and works well when all users need the same backend access."); - let _ = writeln!(out, "- **Passthrough mode** (`server_user` is not set): Each dynamically authenticated user gets their own connection pool that connects to PostgreSQL using their own credentials (MD5 pass-the-hash or SCRAM ClientKey passthrough). This preserves per-user identity on the backend.\n"); - let _ = writeln!(out, "Static users (defined in the `users` section) are always checked first. The auth_query is only used when the username is not found among static users.\n"); + let _ = writeln!( + out, + "- **Dedicated mode** (`server_user` is set): All dynamically authenticated users share a single connection pool that connects to PostgreSQL as `server_user`. This is the simplest setup and works well when all users need the same backend access." + ); + let _ = writeln!( + out, + "- **Passthrough mode** (`server_user` is not set): Each dynamically authenticated user gets their own connection pool that connects to PostgreSQL using their own credentials (MD5 pass-the-hash or SCRAM ClientKey passthrough). This preserves per-user identity on the backend.\n" + ); + let _ = writeln!( + out, + "Static users (defined in the `users` section) are always checked first. The auth_query is only used when the username is not found among static users.\n" + ); let _ = writeln!(out, "```admonish warning title=\"Security Recommendation\""); - let _ = writeln!(out, "The `user` that runs auth queries needs access to password hashes (e.g. from `pg_shadow`). **Do not use a superuser** for this purpose. Instead, create a `SECURITY DEFINER` function owned by a superuser and a dedicated role with minimal privileges:\n"); - let _ = writeln!(out, "~~~sql\n-- Create a dedicated role for auth queries\nCREATE ROLE doorman_auth LOGIN PASSWORD 'strong_password';\n\n-- Create a SECURITY DEFINER function (runs with owner's privileges)\nCREATE OR REPLACE FUNCTION pg_doorman_get_auth(p_usename TEXT)\nRETURNS TABLE (usename name, passwd text)\nLANGUAGE sql SECURITY DEFINER SET search_path = pg_catalog AS\n$$\n SELECT usename, passwd FROM pg_shadow WHERE usename = p_usename;\n$$;\n\n-- Grant execute only to the dedicated role\nREVOKE ALL ON FUNCTION pg_doorman_get_auth(TEXT) FROM PUBLIC;\nGRANT EXECUTE ON FUNCTION pg_doorman_get_auth(TEXT) TO doorman_auth;\n~~~\n\nThen use this function in the `query` parameter:\n\n~~~yaml\nauth_query:\n query: \"SELECT * FROM pg_doorman_get_auth($1)\"\n user: \"doorman_auth\"\n password: \"strong_password\"\n~~~\n```\n"); + let _ = writeln!( + out, + "The `user` that runs auth queries needs access to password hashes (e.g. from `pg_shadow`). **Do not use a superuser** for this purpose. Instead, create a `SECURITY DEFINER` function owned by a superuser and a dedicated role with minimal privileges:\n" + ); + let _ = writeln!( + out, + "~~~sql\n-- Create a dedicated role for auth queries\nCREATE ROLE doorman_auth LOGIN PASSWORD 'strong_password';\n\n-- Create a SECURITY DEFINER function (runs with owner's privileges)\nCREATE OR REPLACE FUNCTION pg_doorman_get_auth(p_usename TEXT)\nRETURNS TABLE (usename name, passwd text)\nLANGUAGE sql SECURITY DEFINER SET search_path = pg_catalog AS\n$$\n SELECT usename, passwd FROM pg_shadow WHERE usename = p_usename;\n$$;\n\n-- Grant execute only to the dedicated role\nREVOKE ALL ON FUNCTION pg_doorman_get_auth(TEXT) FROM PUBLIC;\nGRANT EXECUTE ON FUNCTION pg_doorman_get_auth(TEXT) TO doorman_auth;\n~~~\n\nThen use this function in the `query` parameter:\n\n~~~yaml\nauth_query:\n query: \"SELECT * FROM pg_doorman_get_auth($1)\"\n user: \"doorman_auth\"\n password: \"strong_password\"\n~~~\n```\n" + ); // Individual parameters from fields.yaml let fields = [ @@ -357,9 +414,18 @@ fn write_user_fields(out: &mut String, f: &FieldsData) { out, "`````admonish info title=\"Passthrough Authentication\"" ); - let _ = writeln!(out, "By default, PgDoorman uses **passthrough authentication**: the client's cryptographic proof (MD5 hash or SCRAM ClientKey) is automatically reused to authenticate to PostgreSQL. No plaintext passwords in config needed.\n"); - let _ = writeln!(out, "Set `server_username` and `server_password` **only** when the backend PostgreSQL user differs from the pool username (e.g., username mapping or JWT auth):\n"); - let _ = writeln!(out, "```yaml\nusers:\n - username: \"app_user\" # client-facing name\n password: \"md5...\" # hash for client authentication\n server_username: \"pg_app_user\" # different backend PostgreSQL user\n server_password: \"plaintext_pwd\" # plaintext password for that user\n```\n`````\n"); + let _ = writeln!( + out, + "By default, PgDoorman uses **passthrough authentication**: the client's cryptographic proof (MD5 hash or SCRAM ClientKey) is automatically reused to authenticate to PostgreSQL. No plaintext passwords in config needed.\n" + ); + let _ = writeln!( + out, + "Set `server_username` and `server_password` **only** when the backend PostgreSQL user differs from the pool username (e.g., username mapping or JWT auth):\n" + ); + let _ = writeln!( + out, + "```yaml\nusers:\n - username: \"app_user\" # client-facing name\n password: \"md5...\" # hash for client authentication\n server_username: \"pg_app_user\" # different backend PostgreSQL user\n server_password: \"plaintext_pwd\" # plaintext password for that user\n```\n`````\n" + ); } // --------------------------------------------------------------------------- @@ -368,8 +434,14 @@ fn write_user_fields(out: &mut String, f: &FieldsData) { fn write_prometheus_fields(out: &mut String, f: &FieldsData) { let _ = writeln!(out, "## Enabling Prometheus Metrics\n"); - let _ = writeln!(out, "To enable the Prometheus metrics exporter, add the following to your configuration file:\n"); - let _ = writeln!(out, "```yaml\nprometheus:\n enabled: true\n host: \"0.0.0.0\" # The host on which the metrics server will listen\n port: 9127 # The port on which the metrics server will listen\n```\n"); + let _ = writeln!( + out, + "To enable the Prometheus metrics exporter, add the following to your configuration file:\n" + ); + let _ = writeln!( + out, + "```yaml\nprometheus:\n enabled: true\n host: \"0.0.0.0\" # The host on which the metrics server will listen\n port: 9127 # The port on which the metrics server will listen\n```\n" + ); let _ = writeln!(out, "### Configuration Options\n"); @@ -394,9 +466,18 @@ fn write_prometheus_fields(out: &mut String, f: &FieldsData) { fn write_prometheus_metrics_section(out: &mut String) { let _ = writeln!(out, "## Configuring Prometheus\n"); - let _ = writeln!(out, "Add the following job to your Prometheus configuration to scrape metrics from pg_doorman:\n"); - let _ = writeln!(out, "```yaml\nscrape_configs:\n - job_name: 'pg_doorman'\n static_configs:\n - targets: [':9127']\n```\n"); - let _ = writeln!(out, "Replace `` with the hostname or IP address of your pg_doorman instance.\n"); + let _ = writeln!( + out, + "Add the following job to your Prometheus configuration to scrape metrics from pg_doorman:\n" + ); + let _ = writeln!( + out, + "```yaml\nscrape_configs:\n - job_name: 'pg_doorman'\n static_configs:\n - targets: [':9127']\n```\n" + ); + let _ = writeln!( + out, + "Replace `` with the hostname or IP address of your pg_doorman instance.\n" + ); let _ = writeln!(out, "## Available Metrics\n"); let _ = writeln!(out, "pg_doorman exposes the following metrics:\n"); @@ -405,40 +486,82 @@ fn write_prometheus_metrics_section(out: &mut String) { let _ = writeln!(out, "### System Metrics\n"); let _ = writeln!(out, "| Metric | Description |"); let _ = writeln!(out, "|--------|-------------|"); - let _ = writeln!(out, "| `pg_doorman_total_memory` | Total memory allocated to the pg_doorman process in bytes. Monitors the memory footprint of the application. |\n"); + let _ = writeln!( + out, + "| `pg_doorman_total_memory` | Total memory allocated to the pg_doorman process in bytes. Monitors the memory footprint of the application. |\n" + ); // Connection Metrics let _ = writeln!(out, "### Connection Metrics\n"); let _ = writeln!(out, "| Metric | Description |"); let _ = writeln!(out, "|--------|-------------|"); - let _ = writeln!(out, "| `pg_doorman_connection_count` | Counter of new connections by type handled by pg_doorman. Types include: 'plain' (unencrypted connections), 'tls' (encrypted connections), 'cancel' (connection cancellation requests), and 'total' (sum of all connections). |\n"); + let _ = writeln!( + out, + "| `pg_doorman_connection_count` | Counter of new connections by type handled by pg_doorman. Types include: 'plain' (unencrypted connections), 'tls' (encrypted connections), 'cancel' (connection cancellation requests), and 'total' (sum of all connections). |\n" + ); // Socket Metrics let _ = writeln!(out, "### Socket Metrics (Linux only)\n"); let _ = writeln!(out, "| Metric | Description |"); let _ = writeln!(out, "|--------|-------------|"); - let _ = writeln!(out, "| `pg_doorman_sockets` | Counter of sockets used by pg_doorman by socket type. Types include: 'tcp' (IPv4 TCP sockets), 'tcp6' (IPv6 TCP sockets), 'unix' (Unix domain sockets), and 'unknown' (sockets of unrecognized type). Only available on Linux systems. |\n"); + let _ = writeln!( + out, + "| `pg_doorman_sockets` | Counter of sockets used by pg_doorman by socket type. Types include: 'tcp' (IPv4 TCP sockets), 'tcp6' (IPv6 TCP sockets), 'unix' (Unix domain sockets), and 'unknown' (sockets of unrecognized type). Only available on Linux systems. |\n" + ); // Pool Metrics let _ = writeln!(out, "### Pool Metrics\n"); let _ = writeln!(out, "| Metric | Description |"); let _ = writeln!(out, "|--------|-------------|"); - let _ = writeln!(out, "| `pg_doorman_pools_clients` | Number of clients in connection pools by status, user, and database. Status values include: 'idle' (connected but not executing queries), 'waiting' (waiting for a server connection), and 'active' (currently executing queries). Helps monitor connection pool utilization and client distribution. |"); - let _ = writeln!(out, "| `pg_doorman_pools_servers` | Number of servers in connection pools by status, user, and database. Status values include: 'active' (actively serving clients) and 'idle' (available for new connections). Helps monitor server availability and load distribution. |"); - let _ = writeln!(out, "| `pg_doorman_pools_bytes` | Total bytes transferred through connection pools by direction, user, and database. Direction values include: 'received' (bytes received from clients) and 'sent' (bytes sent to clients). Useful for monitoring network traffic and identifying high-volume connections. |\n"); - let _ = writeln!(out, "| `pg_doorman_pool_size` | Configured maximum pool size per user and database. Useful for calculating remaining pool capacity together with pg_doorman_pools_servers. |\n"); + let _ = writeln!( + out, + "| `pg_doorman_pools_clients` | Number of clients in connection pools by status, user, and database. Status values include: 'idle' (connected but not executing queries), 'waiting' (waiting for a server connection), and 'active' (currently executing queries). Helps monitor connection pool utilization and client distribution. |" + ); + let _ = writeln!( + out, + "| `pg_doorman_pools_servers` | Number of servers in connection pools by status, user, and database. Status values include: 'active' (actively serving clients) and 'idle' (available for new connections). Helps monitor server availability and load distribution. |" + ); + let _ = writeln!( + out, + "| `pg_doorman_pools_bytes` | Total bytes transferred through connection pools by direction, user, and database. Direction values include: 'received' (bytes received from clients) and 'sent' (bytes sent to clients). Useful for monitoring network traffic and identifying high-volume connections. |\n" + ); + let _ = writeln!( + out, + "| `pg_doorman_pool_size` | Configured maximum pool size per user and database. Useful for calculating remaining pool capacity together with pg_doorman_pools_servers. |\n" + ); // Query and Transaction Metrics let _ = writeln!(out, "### Query and Transaction Metrics\n"); let _ = writeln!(out, "| Metric | Description |"); let _ = writeln!(out, "|--------|-------------|"); - let _ = writeln!(out, "| `pg_doorman_pools_queries_percentile` | Query execution time percentiles by user and database. Percentile values include: '99', '95', '90', and '50' (median). Values are in milliseconds. Helps identify slow queries and performance trends across different users and databases. |"); - let _ = writeln!(out, "| `pg_doorman_pools_transactions_percentile` | Transaction execution time percentiles by user and database. Percentile values include: '99', '95', '90', and '50' (median). Values are in milliseconds. Helps monitor transaction performance and identify long-running transactions that might impact database performance. |"); - let _ = writeln!(out, "| `pg_doorman_pools_transactions_count` | Counter of transactions executed in connection pools by user and database. Helps track transaction volume and identify users or databases with high transaction rates. |"); - let _ = writeln!(out, "| `pg_doorman_pools_transactions_total_time` | Total time spent executing transactions in connection pools by user and database. Values are in milliseconds. Helps monitor overall transaction performance and identify users or databases with high transaction execution times. |"); - let _ = writeln!(out, "| `pg_doorman_pools_queries_count` | Counter of queries executed in connection pools by user and database. Helps track query volume and identify users or databases with high query rates. |"); - let _ = writeln!(out, "| `pg_doorman_pools_queries_total_time` | Total time spent executing queries in connection pools by user and database. Values are in milliseconds. Helps monitor overall query performance and identify users or databases with high query execution times. |"); - let _ = writeln!(out, "| `pg_doorman_pools_avg_wait_time` | Average wait time for clients in connection pools by user and database. Values are in milliseconds. Helps monitor client wait times and identify potential bottlenecks. |\n"); + let _ = writeln!( + out, + "| `pg_doorman_pools_queries_percentile` | Query execution time percentiles by user and database. Percentile values include: '99', '95', '90', and '50' (median). Values are in milliseconds. Helps identify slow queries and performance trends across different users and databases. |" + ); + let _ = writeln!( + out, + "| `pg_doorman_pools_transactions_percentile` | Transaction execution time percentiles by user and database. Percentile values include: '99', '95', '90', and '50' (median). Values are in milliseconds. Helps monitor transaction performance and identify long-running transactions that might impact database performance. |" + ); + let _ = writeln!( + out, + "| `pg_doorman_pools_transactions_count` | Counter of transactions executed in connection pools by user and database. Helps track transaction volume and identify users or databases with high transaction rates. |" + ); + let _ = writeln!( + out, + "| `pg_doorman_pools_transactions_total_time` | Total time spent executing transactions in connection pools by user and database. Values are in milliseconds. Helps monitor overall transaction performance and identify users or databases with high transaction execution times. |" + ); + let _ = writeln!( + out, + "| `pg_doorman_pools_queries_count` | Counter of queries executed in connection pools by user and database. Helps track query volume and identify users or databases with high query rates. |" + ); + let _ = writeln!( + out, + "| `pg_doorman_pools_queries_total_time` | Total time spent executing queries in connection pools by user and database. Values are in milliseconds. Helps monitor overall query performance and identify users or databases with high query execution times. |" + ); + let _ = writeln!( + out, + "| `pg_doorman_pools_avg_wait_time` | Average wait time for clients in connection pools by user and database. Values are in milliseconds. Helps monitor client wait times and identify potential bottlenecks. |\n" + ); // Auth Query Metrics let _ = writeln!(out, "### Auth Query Metrics\n"); @@ -448,22 +571,46 @@ fn write_prometheus_metrics_section(out: &mut String) { ); let _ = writeln!(out, "| Metric | Description |"); let _ = writeln!(out, "|--------|-------------|"); - let _ = writeln!(out, "| `pg_doorman_auth_query_cache` | Auth query cache metrics by type and database. Types include: `entries` (current cached credentials), `hits` (cache lookups that found a valid entry), `misses` (cache lookups that required a PostgreSQL fetch), `refetches` (re-fetches triggered by auth failure with stale credentials), `rate_limited` (re-fetch attempts that were rate-limited by `min_interval`). |"); - let _ = writeln!(out, "| `pg_doorman_auth_query_auth` | Auth query authentication outcomes by result and database. Results include: `success` (successful authentication) and `failure` (wrong password or credential mismatch). |"); - let _ = writeln!(out, "| `pg_doorman_auth_query_executor` | Auth query executor metrics by type and database. Types include: `queries` (total queries executed against PostgreSQL to fetch credentials) and `errors` (queries that failed due to connection or execution errors). |"); - let _ = writeln!(out, "| `pg_doorman_auth_query_dynamic_pools` | Auth query dynamic pool lifecycle metrics by type and database. Types include: `current` (currently active dynamic pools), `created` (total pools created since startup), `destroyed` (total pools garbage-collected or removed on RELOAD). Only relevant in passthrough mode. |\n"); + let _ = writeln!( + out, + "| `pg_doorman_auth_query_cache` | Auth query cache metrics by type and database. Types include: `entries` (current cached credentials), `hits` (cache lookups that found a valid entry), `misses` (cache lookups that required a PostgreSQL fetch), `refetches` (re-fetches triggered by auth failure with stale credentials), `rate_limited` (re-fetch attempts that were rate-limited by `min_interval`). |" + ); + let _ = writeln!( + out, + "| `pg_doorman_auth_query_auth` | Auth query authentication outcomes by result and database. Results include: `success` (successful authentication) and `failure` (wrong password or credential mismatch). |" + ); + let _ = writeln!( + out, + "| `pg_doorman_auth_query_executor` | Auth query executor metrics by type and database. Types include: `queries` (total queries executed against PostgreSQL to fetch credentials) and `errors` (queries that failed due to connection or execution errors). |" + ); + let _ = writeln!( + out, + "| `pg_doorman_auth_query_dynamic_pools` | Auth query dynamic pool lifecycle metrics by type and database. Types include: `current` (currently active dynamic pools), `created` (total pools created since startup), `destroyed` (total pools garbage-collected or removed on RELOAD). Only relevant in passthrough mode. |\n" + ); // Server Metrics let _ = writeln!(out, "### Server Metrics\n"); let _ = writeln!(out, "| Metric | Description |"); let _ = writeln!(out, "|--------|-------------|"); - let _ = writeln!(out, "| `pg_doorman_servers_prepared_hits` | Counter of prepared statement hits in databases backends by user and database. Helps track the effectiveness of prepared statements in reducing query parsing overhead. |"); - let _ = writeln!(out, "| `pg_doorman_servers_prepared_misses` | Counter of prepared statement misses in databases backends by user and database. Helps identify queries that could benefit from being prepared to improve performance. |\n"); + let _ = writeln!( + out, + "| `pg_doorman_servers_prepared_hits` | Counter of prepared statement hits in databases backends by user and database. Helps track the effectiveness of prepared statements in reducing query parsing overhead. |" + ); + let _ = writeln!( + out, + "| `pg_doorman_servers_prepared_misses` | Counter of prepared statement misses in databases backends by user and database. Helps identify queries that could benefit from being prepared to improve performance. |\n" + ); // Grafana Dashboard let _ = writeln!(out, "## Grafana Dashboard\n"); - let _ = writeln!(out, "You can create a Grafana dashboard to visualize these metrics. Here's a simple example of panels you might want to include:\n"); - let _ = writeln!(out, "1. Connection counts by type\n2. Memory usage over time\n3. Client and server counts by pool\n4. Query and transaction performance percentiles\n5. Network traffic by pool\n"); + let _ = writeln!( + out, + "You can create a Grafana dashboard to visualize these metrics. Here's a simple example of panels you might want to include:\n" + ); + let _ = writeln!( + out, + "1. Connection counts by type\n2. Memory usage over time\n3. Client and server counts by pool\n4. Query and transaction performance percentiles\n5. Network traffic by pool\n" + ); // Example Queries let _ = writeln!(out, "## Example Queries\n"); @@ -479,7 +626,10 @@ fn write_prometheus_metrics_section(out: &mut String) { ); let _ = writeln!(out, "### Pool Utilization\n"); - let _ = writeln!(out, "```\nsum by (database) (pg_doorman_pools_clients{{status=\"active\"}}) / sum by (database) (pg_doorman_pools_servers{{status=\"active\"}} + pg_doorman_pools_servers{{status=\"idle\"}})\n```\n"); + let _ = writeln!( + out, + "```\nsum by (database) (pg_doorman_pools_clients{{status=\"active\"}}) / sum by (database) (pg_doorman_pools_servers{{status=\"active\"}} + pg_doorman_pools_servers{{status=\"idle\"}})\n```\n" + ); let _ = writeln!(out, "### Slow Queries\n"); let _ = writeln!( @@ -491,7 +641,10 @@ fn write_prometheus_metrics_section(out: &mut String) { let _ = writeln!(out, "```\npg_doorman_pools_avg_wait_time\n```\n"); let _ = writeln!(out, "### Auth Query Cache Hit Rate\n"); - let _ = writeln!(out, "```\npg_doorman_auth_query_cache{{type=\"hits\"}} / (pg_doorman_auth_query_cache{{type=\"hits\"}} + pg_doorman_auth_query_cache{{type=\"misses\"}})\n```\n"); + let _ = writeln!( + out, + "```\npg_doorman_auth_query_cache{{type=\"hits\"}} / (pg_doorman_auth_query_cache{{type=\"hits\"}} + pg_doorman_auth_query_cache{{type=\"misses\"}})\n```\n" + ); let _ = writeln!(out, "### Auth Query Failure Rate\n"); let _ = writeln!( diff --git a/src/app/logger.rs b/src/app/logger.rs index 153cc559b..2d8795809 100644 --- a/src/app/logger.rs +++ b/src/app/logger.rs @@ -1,6 +1,6 @@ extern crate log; -use log::{info, LevelFilter, Log, Metadata, Record}; +use log::{LevelFilter, Log, Metadata, Record, info}; use std::io::Write; use std::process; use syslog::{BasicLogger, Facility, Formatter3164}; diff --git a/src/app/mod.rs b/src/app/mod.rs index 940060cb9..157c1f9d0 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -13,7 +13,7 @@ pub use logger::init_logging; pub use panic::install_panic_hook; pub use server::run_server; -pub use args::{parse, Args, Commands, GenerateConfig, LogFormat, OutputFormat}; +pub use args::{Args, Commands, GenerateConfig, LogFormat, OutputFormat, parse}; pub fn parse_args() -> Result> { use crate::config::ConfigFormat; @@ -75,7 +75,7 @@ pub fn parse_args() -> Result> { ("prometheus.md", generate::docs::generate_prometheus_doc()), ]; - if let Some(ref dir) = output_dir { + if let Some(dir) = output_dir { std::fs::create_dir_all(dir)?; for (name, content) in &docs { let path = format!("{dir}/{name}"); diff --git a/src/app/server.rs b/src/app/server.rs index 06bc55e10..6343a100b 100644 --- a/src/app/server.rs +++ b/src/app/server.rs @@ -1,7 +1,7 @@ use std::net::ToSocketAddrs; use std::process; -use std::sync::atomic::{AtomicBool, AtomicI64, AtomicUsize, Ordering}; use std::sync::Arc; +use std::sync::atomic::{AtomicBool, AtomicI64, AtomicUsize, Ordering}; use std::time::Duration; use chrono::Utc; @@ -9,18 +9,18 @@ use log::{error, info, warn}; use tokio::io::AsyncWriteExt; use tokio::net::TcpSocket; #[cfg(not(windows))] -use tokio::signal::unix::{signal as unix_signal, SignalKind}; +use tokio::signal::unix::{SignalKind, signal as unix_signal}; #[cfg(windows)] use tokio::signal::windows as win_signal; use tokio::{runtime::Builder, sync::mpsc}; use crate::app::args::Args; -use crate::config::{get_config, reload_config, Config}; +use crate::config::{Config, get_config, reload_config}; use crate::daemon; use crate::messages::{configure_tcp_socket, configure_unix_socket}; -use crate::pool::{retain, ClientServerMap, ConnectionPool}; +use crate::pool::{ClientServerMap, ConnectionPool, retain}; use crate::prometheus::start_prometheus_server; -use crate::stats::{Collector, Reporter, REPORTER, TOTAL_CONNECTION_COUNTER}; +use crate::stats::{Collector, REPORTER, Reporter, TOTAL_CONNECTION_COUNTER}; use crate::utils::core_affinity; use crate::utils::format_duration; use socket2::SockRef; @@ -310,7 +310,8 @@ pub fn run_server(args: Args, config: Config) -> Result<(), Box Result<(), Box { // Pre-auth failures: identity unknown, only connection_id available. // Post-auth failures already logged with [user@pool #cN] inside entrypoint. - warn!("[#c{connection_id}] client {peer_label} disconnected with error: {err}, session={session}"); + warn!( + "[#c{connection_id}] client {peer_label} disconnected with error: {err}, session={session}" + ); } } } diff --git a/src/auth/auth_query.rs b/src/auth/auth_query.rs index 7a3778b0d..ee0f372a9 100644 --- a/src/auth/auth_query.rs +++ b/src/auth/auth_query.rs @@ -7,16 +7,16 @@ //! TTL-based expiration, negative caching, and rate-limited re-fetch. use std::future::Future; -use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; +use std::sync::atomic::{AtomicU64, Ordering}; use std::time::Instant; use dashmap::DashMap; use log::{debug, error, info, warn}; use crate::utils::format_elapsed; -use tokio::sync::mpsc; use tokio::sync::Mutex as TokioMutex; +use tokio::sync::mpsc; use tokio_postgres::{Client, NoTls}; use crate::config::{AuthQueryConfig, Duration}; diff --git a/src/auth/hba.rs b/src/auth/hba.rs index 5916f53bb..9fb060d99 100644 --- a/src/auth/hba.rs +++ b/src/auth/hba.rs @@ -45,7 +45,7 @@ impl NameMatcher { fn matches(&self, value: &str) -> bool { match self { NameMatcher::All => true, - NameMatcher::Name(ref n) => n == value, + NameMatcher::Name(n) => n == value, } } } diff --git a/src/auth/hba_eval_tests.rs b/src/auth/hba_eval_tests.rs index bc2295242..5ae6f02f3 100644 --- a/src/auth/hba_eval_tests.rs +++ b/src/auth/hba_eval_tests.rs @@ -1,5 +1,5 @@ use super::{ - eval_hba_for_pool_password, JWT_PUB_KEY_PASSWORD_PREFIX, MD5_PASSWORD_PREFIX, SCRAM_SHA_256, + JWT_PUB_KEY_PASSWORD_PREFIX, MD5_PASSWORD_PREFIX, SCRAM_SHA_256, eval_hba_for_pool_password, }; use crate::auth::hba::{CheckResult, PgHba}; use crate::errors::ClientIdentifier; diff --git a/src/auth/mod.rs b/src/auth/mod.rs index 0bed5d2f1..07c256fc1 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -24,7 +24,7 @@ use crate::auth::scram::{ prepare_server_final_message, prepare_server_first_response, }; use crate::config::BackendAuthMethod; -use crate::config::{get_config, PoolMode}; +use crate::config::{PoolMode, get_config}; use crate::errors::{ClientIdentifier, Error}; use crate::messages::constants::{ JWT_PUB_KEY_PASSWORD_PREFIX, MD5_PASSWORD_PREFIX, SASL_CONTINUE, SASL_FINAL, SCRAM_SHA_256, @@ -35,8 +35,8 @@ use crate::messages::{ scram_start_challenge, vec_to_string, wrong_password, }; use crate::pool::{ - create_dynamic_pool, get_auth_query_state, get_pool, get_pool_config, is_dynamic_pool, - ConnectionPool, PoolIdentifier, + ConnectionPool, PoolIdentifier, create_dynamic_pool, get_auth_query_state, get_pool, + get_pool_config, is_dynamic_pool, }; use crate::server::ServerParameters; @@ -243,9 +243,9 @@ where ) .await?; return Err(Error::HbaForbiddenError(format!( - "Connection with scram not permitted by HBA configuration for client: {} from address: {:?}", - client_identifier, client_identifier.addr, - ))); + "Connection with scram not permitted by HBA configuration for client: {} from address: {:?}", + client_identifier, client_identifier.addr, + ))); } if client_identifier.is_talos || hba_decision == CheckResult::Trust { @@ -325,7 +325,9 @@ where let server_parameters = match pool.get_server_parameters().await { Ok(params) => params, Err(err) => { - error!("[{username_from_parameters}@{pool_name}] failed to retrieve server parameters: {err}"); + error!( + "[{username_from_parameters}@{pool_name}] failed to retrieve server parameters: {err}" + ); error_response( write, &format!( @@ -360,7 +362,9 @@ where let password_response = match vec_to_string(password_response) { Ok(p) => p, Err(err) => { - error!("[{username_from_parameters}@{pool_name}] PAM: failed to read password from {client_addr}: {err}"); + error!( + "[{username_from_parameters}@{pool_name}] PAM: failed to read password from {client_addr}: {err}" + ); error_response_terminal( write, "Invalid password format. Password must be valid UTF-8 text.", @@ -413,7 +417,9 @@ where let server_secret = match parse_server_secret(pool_password) { Ok(server_secret) => server_secret, Err(err) => { - warn!("[{username_from_parameters}@{pool_name}] SCRAM: failed to parse server secret from {client_addr}: {err}"); + warn!( + "[{username_from_parameters}@{pool_name}] SCRAM: failed to parse server secret from {client_addr}: {err}" + ); error_response_terminal( write, "Server authentication configuration error. Please contact your database administrator.", @@ -432,7 +438,9 @@ where )) { Ok(client_first_message) => client_first_message, Err(err) => { - warn!("[{username_from_parameters}@{pool_name}] SCRAM: client first message parse error from {client_addr}: {err}"); + warn!( + "[{username_from_parameters}@{pool_name}] SCRAM: client first message parse error from {client_addr}: {err}" + ); error_response_terminal( write, "Authentication protocol error. Your client may not support SCRAM authentication properly.", @@ -558,7 +566,9 @@ where let jwt_token = match vec_to_string(jwt_token_response) { Ok(p) => p, Err(err) => { - error!("[{username_from_parameters}@{pool_name}] JWT: failed to parse token from {client_addr}: {err}"); + error!( + "[{username_from_parameters}@{pool_name}] JWT: failed to parse token from {client_addr}: {err}" + ); error_response_terminal( write, "Invalid JWT token format. Token must be valid UTF-8 text.", @@ -573,7 +583,9 @@ where let jwt_user_name = match get_user_name_from_jwt(jwt_pub_key, jwt_token).await { Ok(u) => u, Err(err) => { - error!("[{username_from_parameters}@{pool_name}] JWT: validation failed from {client_addr}: {err}"); + error!( + "[{username_from_parameters}@{pool_name}] JWT: validation failed from {client_addr}: {err}" + ); error_response_terminal( write, "JWT token validation failed. Please provide a valid token.", @@ -586,7 +598,9 @@ where } }; if !jwt_user_name.eq(username_from_parameters) { - error!("[{username_from_parameters}@{pool_name}] JWT: username mismatch from {client_addr} (token={jwt_user_name})"); + error!( + "[{username_from_parameters}@{pool_name}] JWT: username mismatch from {client_addr} (token={jwt_user_name})" + ); error_response_terminal( write, format!("JWT token username mismatch. Token contains username '{jwt_user_name}' but you're trying to connect as '{username_from_parameters}'.").as_str(), @@ -772,7 +786,9 @@ where let client_first = match parse_client_first_message(String::from_utf8_lossy(&first_msg)) { Ok(msg) => msg, Err(err) => { - warn!("[{username}@{pool_name}] auth_query: SCRAM client first message parse error: {err}"); + warn!( + "[{username}@{pool_name}] auth_query: SCRAM client first message parse error: {err}" + ); error_response_terminal( write, "Authentication protocol error. Your client may not support SCRAM authentication properly.", @@ -797,7 +813,9 @@ where let client_final = match parse_client_final_message(String::from_utf8_lossy(&final_msg)) { Ok(msg) => msg, Err(err) => { - warn!("[{username}@{pool_name}] auth_query: SCRAM client final message parse error: {err}"); + warn!( + "[{username}@{pool_name}] auth_query: SCRAM client final message parse error: {err}" + ); error_response_terminal( write, "Authentication protocol error. Your client sent an invalid SCRAM final message.", diff --git a/src/auth/pam.rs b/src/auth/pam.rs index d8b1af6d3..475c497e8 100644 --- a/src/auth/pam.rs +++ b/src/auth/pam.rs @@ -34,10 +34,10 @@ pub fn pam_auth(service: &str, username: &str, password: &str) -> Result<(), Err Err(err) => { return auth_error( &format!( - "Failed to initialize PAM context for service '{service}' and user '{username}'" - ), + "Failed to initialize PAM context for service '{service}' and user '{username}'" + ), err, - ) + ); } }; diff --git a/src/auth/scram.rs b/src/auth/scram.rs index c7d99d087..e159c4b5c 100644 --- a/src/auth/scram.rs +++ b/src/auth/scram.rs @@ -1,7 +1,7 @@ use crate::errors::Error; use crate::messages::constants; -use base64::engine::general_purpose; use base64::Engine; +use base64::engine::general_purpose; use hmac::{Hmac, Mac}; use rand::Rng; use sha1::Digest as Digest1; @@ -191,7 +191,7 @@ pub fn parse_server_secret(data: &str) -> Result { _ => { return Err(Error::ScramServerError( "password secret is not scram".to_string(), - )) + )); } } // $:. @@ -216,7 +216,7 @@ pub fn parse_server_secret(data: &str) -> Result { _ => { return Err(Error::ScramServerError( "password secret is not scram".to_string(), - )) + )); } } match keys.split_once(':') { @@ -232,7 +232,7 @@ pub fn parse_server_secret(data: &str) -> Result { _ => { return Err(Error::ScramServerError( "password secret is not scram".to_string(), - )) + )); } }; match general_purpose::STANDARD.decode(server_key_str) { @@ -240,7 +240,7 @@ pub fn parse_server_secret(data: &str) -> Result { _ => { return Err(Error::ScramServerError( "password secret is not scram".to_string(), - )) + )); } }; Ok(result) @@ -298,7 +298,7 @@ pub fn parse_client_final_message(data: Cow) -> Result { return Err(Error::ScramClientError( "compare channel binding settings".to_string(), - )) + )); } } @@ -353,7 +353,7 @@ pub fn parse_client_final_message(data: Cow) -> Result { return Err(Error::ScramClientError( "decode channel binding".to_string(), - )) + )); } }; match general_purpose::STANDARD.decode(proof) { diff --git a/src/auth/scram_client.rs b/src/auth/scram_client.rs index 5457586d6..e38aa9114 100644 --- a/src/auth/scram_client.rs +++ b/src/auth/scram_client.rs @@ -2,7 +2,7 @@ // https://github.com/sfackler/rust-postgres/ // SASL implementation. -use base64::{engine::general_purpose, Engine as _}; +use base64::{Engine as _, engine::general_purpose}; use bytes::BytesMut; use hmac::{Hmac, Mac}; use rand::{self, Rng}; @@ -339,11 +339,9 @@ mod test { let nonce = "9IZ2O01zb9IgiIZ1WJ/zgpJB"; let client_first = "n,,n=,r=9IZ2O01zb9IgiIZ1WJ/zgpJB"; - let server_first = - "r=9IZ2O01zb9IgiIZ1WJ/zgpJBjx/oIRLs02gGSHcw1KEty3eY,s=fs3IXBy7U7+IvVjZ,i\ + let server_first = "r=9IZ2O01zb9IgiIZ1WJ/zgpJBjx/oIRLs02gGSHcw1KEty3eY,s=fs3IXBy7U7+IvVjZ,i\ =4096"; - let client_final = - "c=biws,r=9IZ2O01zb9IgiIZ1WJ/zgpJBjx/oIRLs02gGSHcw1KEty3eY,p=AmNKosjJzS3\ + let client_final = "c=biws,r=9IZ2O01zb9IgiIZ1WJ/zgpJBjx/oIRLs02gGSHcw1KEty3eY,p=AmNKosjJzS3\ 1NTlQYNs5BTeQjdHdk7lOflDo5re2an8="; let server_final = "v=U+ppxD5XUKtradnv8e2MkeupiA8FU87Sg8CXzXHDAzw="; diff --git a/src/auth/tests.rs b/src/auth/tests.rs index 41fa301fa..8babe19b5 100644 --- a/src/auth/tests.rs +++ b/src/auth/tests.rs @@ -1,6 +1,6 @@ //! Tests for authentication module. -use super::mocks::{run_test, MockReader, MockWriter}; +use super::mocks::{MockReader, MockWriter, run_test}; use super::*; // Mock for get_config and get_pool diff --git a/src/bin/patroni_proxy/config.rs b/src/bin/patroni_proxy/config.rs index 7dc33d987..68aecbe49 100644 --- a/src/bin/patroni_proxy/config.rs +++ b/src/bin/patroni_proxy/config.rs @@ -613,10 +613,11 @@ clusters: let config2 = Config::from_str(yaml2).unwrap(); let diff = ConfigDiff::compute(&config1, &config2); assert!(diff.has_changes()); - assert!(diff - .changes - .iter() - .any(|c| matches!(c, ClusterDiff::Added(name, _) if name == "two"))); + assert!( + diff.changes + .iter() + .any(|c| matches!(c, ClusterDiff::Added(name, _) if name == "two")) + ); } #[test] @@ -655,10 +656,11 @@ clusters: let config2 = Config::from_str(yaml2).unwrap(); let diff = ConfigDiff::compute(&config1, &config2); assert!(diff.has_changes()); - assert!(diff - .changes - .iter() - .any(|c| matches!(c, ClusterDiff::Removed(name) if name == "two"))); + assert!( + diff.changes + .iter() + .any(|c| matches!(c, ClusterDiff::Removed(name) if name == "two")) + ); } #[test] @@ -690,10 +692,11 @@ clusters: let config2 = Config::from_str(yaml2).unwrap(); let diff = ConfigDiff::compute(&config1, &config2); assert!(diff.has_changes()); - assert!(diff - .changes - .iter() - .any(|c| matches!(c, ClusterDiff::HostsChanged(name, _, _) if name == "one"))); + assert!( + diff.changes + .iter() + .any(|c| matches!(c, ClusterDiff::HostsChanged(name, _, _) if name == "one")) + ); } #[test] diff --git a/src/bin/patroni_proxy/main.rs b/src/bin/patroni_proxy/main.rs index 61aed9cd5..7c540ff6f 100644 --- a/src/bin/patroni_proxy/main.rs +++ b/src/bin/patroni_proxy/main.rs @@ -7,12 +7,12 @@ mod stream; use api::start_http_server; use clap::Parser; -use cluster_manager::{handle_config_changes, ClusterManager}; +use cluster_manager::{ClusterManager, handle_config_changes}; use config::{ClusterDiff, ConfigDiff, ConfigRepository}; use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; -use tokio::signal::unix::{signal, SignalKind}; +use tokio::signal::unix::{SignalKind, signal}; use tokio::sync::RwLock; use tracing::{error, info, warn}; diff --git a/src/bin/patroni_proxy/port.rs b/src/bin/patroni_proxy/port.rs index 6cd2d7d4f..3debf1444 100644 --- a/src/bin/patroni_proxy/port.rs +++ b/src/bin/patroni_proxy/port.rs @@ -1,14 +1,14 @@ use std::collections::{HashMap, HashSet}; use std::net::SocketAddr; -use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; +use std::sync::atomic::{AtomicU64, Ordering}; use tokio::net::TcpListener; use tokio::sync::{Mutex, Notify, RwLock}; use tracing::{debug, error, info, warn}; use crate::config::{PortConfig, Role}; use crate::patroni::Member; -use crate::stream::{spawn_proxy, StopHandle}; +use crate::stream::{StopHandle, spawn_proxy}; /// Backend member with connection counter for load balancing #[derive(Debug)] diff --git a/src/bin/patroni_proxy/stream.rs b/src/bin/patroni_proxy/stream.rs index aa2140819..8f621ab4f 100644 --- a/src/bin/patroni_proxy/stream.rs +++ b/src/bin/patroni_proxy/stream.rs @@ -1,7 +1,7 @@ use std::io; use std::net::SocketAddr; -use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; use tokio::net::TcpStream; use tokio::sync::Notify; diff --git a/src/bin/patroni_proxy/tests/bdd/mock_backend_helper.rs b/src/bin/patroni_proxy/tests/bdd/mock_backend_helper.rs index 810334a93..bebbf9b49 100644 --- a/src/bin/patroni_proxy/tests/bdd/mock_backend_helper.rs +++ b/src/bin/patroni_proxy/tests/bdd/mock_backend_helper.rs @@ -3,8 +3,8 @@ use crate::world::PatroniProxyWorld; use cucumber::given; use std::io::{Read, Write}; use std::net::{TcpListener, TcpStream}; -use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; use std::thread; use std::time::Duration; diff --git a/src/bin/patroni_proxy/tests/bdd/proxy_helper.rs b/src/bin/patroni_proxy/tests/bdd/proxy_helper.rs index 611bd9dd5..6af9942e2 100644 --- a/src/bin/patroni_proxy/tests/bdd/proxy_helper.rs +++ b/src/bin/patroni_proxy/tests/bdd/proxy_helper.rs @@ -174,7 +174,7 @@ pub async fn send_sighup_to_proxy(world: &mut PatroniProxyWorld) { let pid = child.id(); // Send SIGHUP signal using nix crate - use nix::sys::signal::{kill, Signal}; + use nix::sys::signal::{Signal, kill}; use nix::unistd::Pid; kill(Pid::from_raw(pid as i32), Signal::SIGHUP).expect("Failed to send SIGHUP"); diff --git a/src/client/core.rs b/src/client/core.rs index c0ea0389a..ae67728b7 100644 --- a/src/client/core.rs +++ b/src/client/core.rs @@ -8,8 +8,8 @@ use std::sync::Arc; use tokio::io::BufReader; use crate::client::buffer_pool::PooledBuffer; -use crate::messages::{error_response, Parse}; -use crate::pool::{get_pool, ClientServerMap, ConnectionPool}; +use crate::messages::{Parse, error_response}; +use crate::pool::{ClientServerMap, ConnectionPool, get_pool}; use crate::server::ServerParameters; use crate::stats::{ClientStats, ServerStats}; diff --git a/src/client/entrypoint.rs b/src/client/entrypoint.rs index 8b38fa18d..bedb76c34 100644 --- a/src/client/entrypoint.rs +++ b/src/client/entrypoint.rs @@ -16,7 +16,7 @@ use crate::utils::rate_limit::RateLimiter; use crate::transport::ClientTransport; use super::core::Client; -use super::startup::{get_startup, startup_tls, ClientConnectionType}; +use super::startup::{ClientConnectionType, get_startup, startup_tls}; /// Identity info returned from client_entrypoint for disconnect logging. pub struct ClientSessionInfo { diff --git a/src/client/migration.rs b/src/client/migration.rs index 7276c795f..d1800ffd7 100644 --- a/src/client/migration.rs +++ b/src/client/migration.rs @@ -2,7 +2,7 @@ use bytes::{Buf, BufMut, BytesMut}; use log::{error, info, warn}; use std::os::unix::io::{AsRawFd, FromRawFd, RawFd}; use std::sync::Arc; -use tokio::io::{split, BufReader}; +use tokio::io::{BufReader, split}; use tokio::net::TcpStream; use tokio::sync::mpsc; @@ -12,11 +12,11 @@ use std::ffi::c_void; use crate::client::buffer_pool::PooledBuffer; use crate::client::core::{CachedStatement, Client, PreparedStatementKey}; use crate::client::util::PREPARED_STATEMENT_COUNTER; -use crate::config::{get_config, BackendAuthMethod}; +use crate::config::{BackendAuthMethod, get_config}; use crate::errors::Error; -use crate::messages::config_socket::configure_tcp_socket; use crate::messages::Parse; -use crate::pool::{get_pool, ClientServerMap, ConnectionPool}; +use crate::messages::config_socket::configure_tcp_socket; +use crate::pool::{ClientServerMap, ConnectionPool, get_pool}; use crate::server::ServerParameters; use crate::stats::ClientStats; @@ -222,7 +222,7 @@ where // Backend auth state (v2): allows new process to skip ScramPending // fallback by receiving the ClientKey from the old process. if let Some(pool) = get_pool(&self.pool_name, &self.username) { - if let Some(ref ba_lock) = pool.address.backend_auth { + if let Some(ba_lock) = &pool.address.backend_auth { match &*ba_lock.read() { BackendAuthMethod::Md5PassTheHash(hash) => { buf.put_u8(1); @@ -1133,7 +1133,7 @@ mod tests { buf.put_u64(42); // connection_id buf.put_i32(1); // secret_key buf.put_u8(1); // transaction_mode - // missing pool_name, username, etc. + // missing pool_name, username, etc. assert!(deserialize_state(buf).is_err()); } diff --git a/src/client/mod.rs b/src/client/mod.rs index 3393db033..3e7efbb36 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -12,8 +12,8 @@ mod util; pub use core::Client; pub use entrypoint::{ - client_entrypoint, client_entrypoint_too_many_clients_already, - client_entrypoint_too_many_clients_already_unix, client_entrypoint_unix, ClientSessionInfo, + ClientSessionInfo, client_entrypoint, client_entrypoint_too_many_clients_already, + client_entrypoint_too_many_clients_already_unix, client_entrypoint_unix, }; pub use startup::startup_tls; pub use util::PREPARED_STATEMENT_COUNTER; diff --git a/src/client/protocol.rs b/src/client/protocol.rs index 57c6e698d..efc573c87 100644 --- a/src/client/protocol.rs +++ b/src/client/protocol.rs @@ -4,15 +4,15 @@ use std::convert::TryInto; use std::sync::Arc; use crate::errors::Error; -use crate::messages::{error_response, Bind, Close, Describe, Parse}; +use crate::messages::{Bind, Close, Describe, Parse, error_response}; use crate::pool::ConnectionPool; use crate::server::Server; +use super::PREPARED_STATEMENT_COUNTER; use super::core::{ BatchOperation, CachedStatement, Client, ParseCompleteTarget, PreparedStatementKey, SkippedParse, }; -use super::PREPARED_STATEMENT_COUNTER; impl Client where @@ -67,7 +67,10 @@ where Ok(_) => (), Err(err) => match err { Error::PreparedStatementError => { - warn!("[{}@{} #c{}] server rejected prepared statement {:?}, evicting from client cache", self.username, self.pool_name, self.connection_id, key); + warn!( + "[{}@{} #c{}] server rejected prepared statement {:?}, evicting from client cache", + self.username, self.pool_name, self.connection_id, key + ); self.prepared.cache.pop(&key); } @@ -81,7 +84,7 @@ where None => { return Err(Error::ClientError(format!( "prepared statement `{key:?}` not found" - ))) + ))); } }; @@ -166,7 +169,7 @@ where None => { return Err(Error::ClientError(format!( "Could not store Prepared statement `{client_given_name}`" - ))) + ))); } }; @@ -247,8 +250,11 @@ where // Track this skipped Parse - ParseComplete will be inserted before BindComplete in response debug!( "[{}@{} #c{}] parse skipped for `{}`: already on server pid={}, synthetic ParseComplete queued", - self.username, self.pool_name, self.connection_id, - server_stmt_name, server.get_process_id() + self.username, + self.pool_name, + self.connection_id, + server_stmt_name, + server.get_process_id() ); // insert_at_beginning starts as false. It will be set to true later // if a new Parse is sent to server AFTER this skipped Parse. diff --git a/src/client/startup.rs b/src/client/startup.rs index f535f93a4..7250a9e4c 100644 --- a/src/client/startup.rs +++ b/src/client/startup.rs @@ -2,9 +2,9 @@ use bytes::{Buf, BufMut, BytesMut}; use log::error; use std::ffi::CStr; use std::str; -use std::sync::atomic::Ordering; use std::sync::Arc; -use tokio::io::{split, AsyncReadExt, BufReader, ReadHalf, WriteHalf}; +use std::sync::atomic::Ordering; +use tokio::io::{AsyncReadExt, BufReader, ReadHalf, WriteHalf, split}; use tokio::net::TcpStream; use crate::auth::authenticate; @@ -19,7 +19,7 @@ use crate::messages::{ }; use crate::pool::ClientServerMap; use crate::server::ServerParameters; -use crate::stats::{ClientStats, CANCEL_CONNECTION_COUNTER}; +use crate::stats::{CANCEL_CONNECTION_COUNTER, ClientStats}; use crate::transport::ClientTransport; use super::buffer_pool::PooledBuffer; diff --git a/src/client/transaction.rs b/src/client/transaction.rs index 1c22e505f..366cafd17 100644 --- a/src/client/transaction.rs +++ b/src/client/transaction.rs @@ -1,6 +1,6 @@ use bytes::{BufMut, BytesMut}; use log::{debug, error, info, warn}; -use std::future::{poll_fn, Future}; +use std::future::{Future, poll_fn}; use std::ops::DerefMut; use std::sync::atomic::Ordering; use std::task::Poll; @@ -14,7 +14,7 @@ use crate::app::server::{ }; use crate::client::batch_handling::PARSE_COMPLETE_MSG; use crate::client::core::{BatchOperation, Client, PreparedStatementKey}; -use crate::client::util::{is_standalone_begin, QUERY_DEALLOCATE}; +use crate::client::util::{QUERY_DEALLOCATE, is_standalone_begin}; use crate::errors::Error; use crate::messages::{ check_query_response, deallocate_response, error_response, error_response_terminal, @@ -790,7 +790,8 @@ where self.connection_id, checkout_us / 1_000, server.get_process_id(), - status.size, status.max_size, + status.size, + status.max_size, status.available, status.waiting, scaling.inflight_creates, @@ -1020,7 +1021,12 @@ where _ => { error!( "[{}@{} #c{}] unexpected message code '{}' (ASCII: {}) from client {}", - self.username, self.pool_name, self.connection_id, code, code as u8, self.addr + self.username, + self.pool_name, + self.connection_id, + code, + code as u8, + self.addr ); TransactionAction::Continue } diff --git a/src/client/util.rs b/src/client/util.rs index 6815948cd..263ed3505 100644 --- a/src/client/util.rs +++ b/src/client/util.rs @@ -1,6 +1,6 @@ use bytes::BytesMut; use once_cell::sync::Lazy; -use std::sync::{atomic::AtomicUsize, Arc}; +use std::sync::{Arc, atomic::AtomicUsize}; /// Incrementally count prepared statements /// to avoid random conflicts in places where the random number generator is weak. diff --git a/src/config/byte_size.rs b/src/config/byte_size.rs index af8b58c47..7d4b01bbc 100644 --- a/src/config/byte_size.rs +++ b/src/config/byte_size.rs @@ -7,7 +7,7 @@ //! This provides backward compatibility with existing numeric configurations //! while allowing more readable string formats. -use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; +use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; use std::fmt; /// Byte size with human-readable parsing support. diff --git a/src/config/duration.rs b/src/config/duration.rs index 32076de90..e717aa3ce 100644 --- a/src/config/duration.rs +++ b/src/config/duration.rs @@ -7,7 +7,7 @@ //! This provides backward compatibility with existing numeric configurations //! while allowing more readable string formats. -use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; +use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; use std::fmt; /// Duration in milliseconds with human-readable parsing support. diff --git a/src/config/mod.rs b/src/config/mod.rs index 931191f11..6a6d70fff 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -13,7 +13,7 @@ use std::sync::Arc; use tokio::fs::File; use tokio::io::AsyncReadExt; -use self::tls::{load_identity, TLSMode}; +use self::tls::{TLSMode, load_identity}; use crate::auth::hba::CheckResult; use crate::errors::Error; use crate::pool::{ClientServerMap, ConnectionPool}; diff --git a/src/config/pool.rs b/src/config/pool.rs index 355b9024d..b8e78bc9f 100644 --- a/src/config/pool.rs +++ b/src/config/pool.rs @@ -4,8 +4,8 @@ use crate::errors::Error; use log::warn; use serde::de::{self, MapAccess, SeqAccess, Visitor}; use serde::{Deserialize, Deserializer, Serialize}; -use std::collections::hash_map::DefaultHasher; use std::collections::HashSet; +use std::collections::hash_map::DefaultHasher; use std::fmt; use std::hash::{Hash, Hasher}; @@ -294,9 +294,7 @@ impl Pool { warn!( "min_guaranteed_pool_size ({}) > pool_size ({}) for user '{}'; \ user is immune to eviction but cannot reach the guarantee", - guaranteed, - user.pool_size, - user.username + guaranteed, user.pool_size, user.username ); } } @@ -374,8 +372,7 @@ impl Pool { // Lifetime longer than the cooldown lets fallback connections outlive // the local-backend recovery, mixing primary and fallback in the pool. - if let (Some(ref lifetime), Some(ref cooldown)) = - (&self.fallback_lifetime, &self.fallback_cooldown) + if let (Some(lifetime), Some(cooldown)) = (&self.fallback_lifetime, &self.fallback_cooldown) { if lifetime.as_millis() > cooldown.as_millis() { log::warn!( diff --git a/src/config/tests.rs b/src/config/tests.rs index 89959fcb5..bea2f94a6 100644 --- a/src/config/tests.rs +++ b/src/config/tests.rs @@ -167,7 +167,9 @@ async fn test_validate_hba_and_pg_hba_both_set() { let result = config.validate().await; assert!(result.is_err()); if let Err(Error::BadConfig(msg)) = result { - assert!(msg.contains("general.hba and general.pg_hba cannot be specified at the same time")); + assert!( + msg.contains("general.hba and general.pg_hba cannot be specified at the same time") + ); } else { panic!("Expected BadConfig error about hba and pg_hba"); } diff --git a/src/daemon/lib.rs b/src/daemon/lib.rs index ba87417a7..542471f6d 100644 --- a/src/daemon/lib.rs +++ b/src/daemon/lib.rs @@ -18,7 +18,7 @@ use std::os::unix::io::AsRawFd; use std::path::{Path, PathBuf}; use std::process::exit; -use super::error::{check_err, errno, ErrorKind}; +use super::error::{ErrorKind, check_err, errno}; pub use super::error::Error; @@ -425,23 +425,25 @@ impl Daemonize { } unsafe fn perform_fork() -> Result, ErrorKind> { - let pid = check_err(libc::fork(), ErrorKind::Fork)?; - if pid == 0 { - Ok(None) - } else { - Ok(Some(pid)) + unsafe { + let pid = check_err(libc::fork(), ErrorKind::Fork)?; + if pid == 0 { Ok(None) } else { Ok(Some(pid)) } } } unsafe fn waitpid(pid: libc::pid_t) -> Result { - let mut child_ret = 0; - check_err(libc::waitpid(pid, &mut child_ret, 0), ErrorKind::Wait)?; - Ok(child_ret) + unsafe { + let mut child_ret = 0; + check_err(libc::waitpid(pid, &mut child_ret, 0), ErrorKind::Wait)?; + Ok(child_ret) + } } unsafe fn set_sid() -> Result<(), ErrorKind> { - check_err(libc::setsid(), ErrorKind::DetachSession)?; - Ok(()) + unsafe { + check_err(libc::setsid(), ErrorKind::DetachSession)?; + Ok(()) + } } unsafe fn redirect_standard_streams( @@ -449,83 +451,95 @@ unsafe fn redirect_standard_streams( stdout: Stdio, stderr: Stdio, ) -> Result<(), ErrorKind> { - let devnull_fd = check_err( - libc::open(b"/dev/null\0" as *const [u8; 10] as _, libc::O_RDWR), - ErrorKind::OpenDevnull, - )?; - - let process_stdio = |fd, stdio: Stdio| { - match stdio.inner { - StdioImpl::Devnull => { - check_err(libc::dup2(devnull_fd, fd), ErrorKind::RedirectStreams)?; - } - StdioImpl::RedirectToFile(file) => { - let raw_fd = file.as_raw_fd(); - check_err(libc::dup2(raw_fd, fd), ErrorKind::RedirectStreams)?; - } - StdioImpl::Keep => (), + unsafe { + let devnull_fd = check_err( + libc::open(b"/dev/null\0" as *const [u8; 10] as _, libc::O_RDWR), + ErrorKind::OpenDevnull, + )?; + + let process_stdio = |fd, stdio: Stdio| { + match stdio.inner { + StdioImpl::Devnull => { + check_err(libc::dup2(devnull_fd, fd), ErrorKind::RedirectStreams)?; + } + StdioImpl::RedirectToFile(file) => { + let raw_fd = file.as_raw_fd(); + check_err(libc::dup2(raw_fd, fd), ErrorKind::RedirectStreams)?; + } + StdioImpl::Keep => (), + }; + Ok(()) }; - Ok(()) - }; - process_stdio(libc::STDIN_FILENO, stdin)?; - process_stdio(libc::STDOUT_FILENO, stdout)?; - process_stdio(libc::STDERR_FILENO, stderr)?; + process_stdio(libc::STDIN_FILENO, stdin)?; + process_stdio(libc::STDOUT_FILENO, stdout)?; + process_stdio(libc::STDERR_FILENO, stderr)?; - check_err(libc::close(devnull_fd), ErrorKind::CloseDevnull)?; + check_err(libc::close(devnull_fd), ErrorKind::CloseDevnull)?; - Ok(()) + Ok(()) + } } unsafe fn get_group(group: Group) -> Result { - match group.inner { - GroupImpl::Id(id) => Ok(id), - GroupImpl::Name(name) => { - let s = CString::new(name).map_err(|_| ErrorKind::GroupContainsNul)?; - match get_gid_by_name(&s) { - Some(id) => get_group(id.into()), - None => Err(ErrorKind::GroupNotFound), + unsafe { + match group.inner { + GroupImpl::Id(id) => Ok(id), + GroupImpl::Name(name) => { + let s = CString::new(name).map_err(|_| ErrorKind::GroupContainsNul)?; + match get_gid_by_name(&s) { + Some(id) => get_group(id.into()), + None => Err(ErrorKind::GroupNotFound), + } } } } } unsafe fn set_group(group: libc::gid_t) -> Result<(), ErrorKind> { - check_err(libc::setgid(group), ErrorKind::SetGroup)?; - Ok(()) + unsafe { + check_err(libc::setgid(group), ErrorKind::SetGroup)?; + Ok(()) + } } unsafe fn get_user(user: User) -> Result { - match user.inner { - UserImpl::Id(id) => Ok(id), - UserImpl::Name(name) => { - let s = CString::new(name).map_err(|_| ErrorKind::UserContainsNul)?; - match get_uid_by_name(&s) { - Some(id) => get_user(id.into()), - None => Err(ErrorKind::UserNotFound), + unsafe { + match user.inner { + UserImpl::Id(id) => Ok(id), + UserImpl::Name(name) => { + let s = CString::new(name).map_err(|_| ErrorKind::UserContainsNul)?; + match get_uid_by_name(&s) { + Some(id) => get_user(id.into()), + None => Err(ErrorKind::UserNotFound), + } } } } } unsafe fn set_user(user: libc::uid_t) -> Result<(), ErrorKind> { - check_err(libc::setuid(user), ErrorKind::SetUser)?; - Ok(()) + unsafe { + check_err(libc::setuid(user), ErrorKind::SetUser)?; + Ok(()) + } } unsafe fn create_pid_file(path: PathBuf) -> Result { - let path_c = pathbuf_into_cstring(path)?; + unsafe { + let path_c = pathbuf_into_cstring(path)?; - let fd = check_err( - libc::open(path_c.as_ptr(), libc::O_WRONLY | libc::O_CREAT, 0o666), - ErrorKind::OpenPidfile, - )?; + let fd = check_err( + libc::open(path_c.as_ptr(), libc::O_WRONLY | libc::O_CREAT, 0o666), + ErrorKind::OpenPidfile, + )?; - //check_err( - // libc::flock(fd, libc::LOCK_EX | libc::LOCK_NB), - // ErrorKind::LockPidfile, - //)?; - Ok(fd) + //check_err( + // libc::flock(fd, libc::LOCK_EX | libc::LOCK_NB), + // ErrorKind::LockPidfile, + //)?; + Ok(fd) + } } unsafe fn chown_pid_file( @@ -533,70 +547,82 @@ unsafe fn chown_pid_file( uid: libc::uid_t, gid: libc::gid_t, ) -> Result<(), ErrorKind> { - let path_c = pathbuf_into_cstring(path)?; - check_err( - libc::chown(path_c.as_ptr(), uid, gid), - ErrorKind::ChownPidfile, - )?; - Ok(()) + unsafe { + let path_c = pathbuf_into_cstring(path)?; + check_err( + libc::chown(path_c.as_ptr(), uid, gid), + ErrorKind::ChownPidfile, + )?; + Ok(()) + } } unsafe fn write_pid_file(fd: libc::c_int) -> Result<(), ErrorKind> { - let pid = libc::getpid(); - let pid_buf = format!("{pid}\n").into_bytes(); - let pid_length = pid_buf.len(); - let pid_c = CString::new(pid_buf).unwrap(); - check_err(libc::ftruncate(fd, 0), ErrorKind::TruncatePidfile)?; + unsafe { + let pid = libc::getpid(); + let pid_buf = format!("{pid}\n").into_bytes(); + let pid_length = pid_buf.len(); + let pid_c = CString::new(pid_buf).unwrap(); + check_err(libc::ftruncate(fd, 0), ErrorKind::TruncatePidfile)?; + + let written = check_err( + libc::write(fd, pid_c.as_ptr() as *const libc::c_void, pid_length), + ErrorKind::WritePid, + )?; - let written = check_err( - libc::write(fd, pid_c.as_ptr() as *const libc::c_void, pid_length), - ErrorKind::WritePid, - )?; + if written < pid_length as isize { + return Err(ErrorKind::WritePidUnspecifiedError); + } - if written < pid_length as isize { - return Err(ErrorKind::WritePidUnspecifiedError); + Ok(()) } - - Ok(()) } unsafe fn set_cloexec_pid_file(fd: libc::c_int) -> Result<(), ErrorKind> { - if cfg!(not(target_os = "redox")) { - let flags = check_err(libc::fcntl(fd, libc::F_GETFD), ErrorKind::GetPidfileFlags)?; - - check_err( - libc::fcntl(fd, libc::F_SETFD, flags | libc::FD_CLOEXEC), - ErrorKind::SetPidfileFlags, - )?; - } else { - check_err(libc::ioctl(fd, libc::FIOCLEX), ErrorKind::SetPidfileFlags)?; + unsafe { + if cfg!(not(target_os = "redox")) { + let flags = check_err(libc::fcntl(fd, libc::F_GETFD), ErrorKind::GetPidfileFlags)?; + + check_err( + libc::fcntl(fd, libc::F_SETFD, flags | libc::FD_CLOEXEC), + ErrorKind::SetPidfileFlags, + )?; + } else { + check_err(libc::ioctl(fd, libc::FIOCLEX), ErrorKind::SetPidfileFlags)?; + } + Ok(()) } - Ok(()) } unsafe fn change_root(path: PathBuf) -> Result<(), ErrorKind> { - let path_c = pathbuf_into_cstring(path)?; - check_err(libc::chroot(path_c.as_ptr()), ErrorKind::Chroot)?; - Ok(()) + unsafe { + let path_c = pathbuf_into_cstring(path)?; + check_err(libc::chroot(path_c.as_ptr()), ErrorKind::Chroot)?; + Ok(()) + } } unsafe fn get_gid_by_name(name: &CString) -> Option { - let ptr = libc::getgrnam(name.as_ptr() as *const libc::c_char); - if ptr.is_null() { - None - } else { - let s = &*ptr; - Some(s.gr_gid) + unsafe { + let ptr = libc::getgrnam(name.as_ptr() as *const libc::c_char); + if ptr.is_null() { + None + } else { + let s = &*ptr; + Some(s.gr_gid) + } } } unsafe fn get_uid_by_name(name: &CString) -> Option { - let ptr = libc::getpwnam(name.as_ptr() as *const libc::c_char); - if ptr.is_null() { - None - } else { - let s = &*ptr; - Some(s.pw_uid) + unsafe { + let ptr = libc::getpwnam(name.as_ptr() as *const libc::c_char); + if ptr.is_null() { + None + } else { + let s = &*ptr; + Some(s.pw_uid) + } } } diff --git a/src/messages/extended.rs b/src/messages/extended.rs index a56605dc0..b8869b43a 100644 --- a/src/messages/extended.rs +++ b/src/messages/extended.rs @@ -3,8 +3,8 @@ use std::collections::hash_map::DefaultHasher; use std::ffi::CString; use std::hash::Hasher; use std::mem; -use std::sync::atomic::Ordering; use std::sync::Arc; +use std::sync::atomic::Ordering; // External crate imports use bytes::{Buf, BufMut, BytesMut}; diff --git a/src/messages/mod.rs b/src/messages/mod.rs index 1afd69a29..c749f9ecd 100644 --- a/src/messages/mod.rs +++ b/src/messages/mod.rs @@ -1,8 +1,8 @@ // Helper functions to send one-off protocol messages and handle TcpStream (TCP socket). // Standard library imports -use std::sync::atomic::AtomicI64; use std::sync::Arc; +use std::sync::atomic::AtomicI64; // External crate imports use once_cell::sync::Lazy; @@ -19,7 +19,7 @@ pub mod types; // Re-export public items pub use config_socket::{configure_tcp_socket, configure_unix_socket}; pub use error::PgErrorMsg; -pub use extended::{close_complete, Bind, Close, Describe, ExtendedProtocolData, Parse}; +pub use extended::{Bind, Close, Describe, ExtendedProtocolData, Parse, close_complete}; pub use protocol::{ check_query_response, command_complete, data_row, data_row_nullable, deallocate_response, error_message, error_response, error_response_terminal, flush, @@ -35,7 +35,7 @@ pub use socket::{ read_message_data, read_message_header, read_message_reuse, write_all, write_all_flush, write_all_half, }; -pub use types::{vec_to_string, BytesMutReader, DataType}; +pub use types::{BytesMutReader, DataType, vec_to_string}; // Re-export protocol constants pub use constants::*; diff --git a/src/messages/protocol.rs b/src/messages/protocol.rs index 403de58d0..babe69088 100644 --- a/src/messages/protocol.rs +++ b/src/messages/protocol.rs @@ -113,7 +113,7 @@ where Err(err) => { return Err(Error::SocketError(format!( "Failed to read password message type identifier: {err}" - ))) + ))); } } @@ -130,7 +130,7 @@ where Err(err) => { return Err(Error::SocketError(format!( "Failed to read password message length: {err}" - ))) + ))); } } @@ -141,7 +141,7 @@ where Err(err) => { return Err(Error::SocketError(format!( "Failed to read password message content: {err}" - ))) + ))); } } diff --git a/src/messages/socket.rs b/src/messages/socket.rs index b14520713..a7ba73342 100644 --- a/src/messages/socket.rs +++ b/src/messages/socket.rs @@ -86,7 +86,7 @@ where Err(err) => { return Err(Error::SocketError(format!( "Error reading message code from socket - Error {err:?}" - ))) + ))); } }; let len = match stream.read_i32().await { @@ -94,7 +94,7 @@ where Err(err) => { return Err(Error::SocketError(format!( "Error reading message len from socket - Code: {code:?}, Error: {err:?}" - ))) + ))); } }; @@ -265,7 +265,7 @@ where W: tokio::io::AsyncWrite + std::marker::Unpin, { const MAX_BUFFER_CHUNK: usize = 4096; // гарантия того что вызовы read из - // буфферизированного stream 8kb будет быстрым. + // буфферизированного stream 8kb будет быстрым. let mut bytes_remained = len; let mut bytes_readed: usize; let mut buffer_size: usize = MAX_BUFFER_CHUNK; @@ -280,7 +280,7 @@ where Err(err) => { return Err(Error::SocketError(format!( "Error reading from socket: {err:?}" - ))) + ))); } }; if bytes_readed == 0 { @@ -295,7 +295,7 @@ where Err(err) => { return Err(Error::SocketError(format!( "Error writing to socket: {err:?}" - ))) + ))); } }; @@ -811,7 +811,9 @@ mod tests { let _bytes = buffer.split(); let cap_after = buffer.capacity(); - assert!(cap_after < cap_before, - "split() leaves remainder capacity ({cap_after}) much less than original ({cap_before})"); + assert!( + cap_after < cap_before, + "split() leaves remainder capacity ({cap_after}) much less than original ({cap_before})" + ); } } diff --git a/src/messages/tests.rs b/src/messages/tests.rs index e8bb41543..ae25b724c 100644 --- a/src/messages/tests.rs +++ b/src/messages/tests.rs @@ -15,8 +15,8 @@ use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; use crate::errors::Error; use crate::messages::protocol::row_description; use crate::messages::{ - data_row, data_row_nullable, error_message, parse_startup, ready_for_query, DataType, - PgErrorMsg, + DataType, PgErrorMsg, data_row, data_row_nullable, error_message, parse_startup, + ready_for_query, }; // Mock implementation for AsyncReadExt diff --git a/src/pool/dynamic.rs b/src/pool/dynamic.rs index e7aa60f6b..dec029e5e 100644 --- a/src/pool/dynamic.rs +++ b/src/pool/dynamic.rs @@ -5,21 +5,21 @@ //! and garbage-collected when idle. On RELOAD, dynamic pools are dropped and recreated //! on the next client connection with fresh settings. -use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::Arc; +use std::sync::atomic::{AtomicU32, Ordering}; use log::{debug, info, warn}; -use crate::config::{get_config, BackendAuthMethod, PoolMode, User}; +use crate::config::{BackendAuthMethod, PoolMode, User, get_config}; use crate::errors::Error; use crate::server::ServerParameters; use crate::stats::AddressStats; use super::types::{PoolConfig, QueueMode, Timeouts}; use super::{ - build_server_tls_for_pool, get_auth_query_state, get_coordinator, get_pool, - register_dynamic_pool, Address, ConnectionPool, Pool, PoolIdentifier, PoolSettings, - PreparedStatementCache, ServerPool, POOLS, + Address, ConnectionPool, POOLS, Pool, PoolIdentifier, PoolSettings, PreparedStatementCache, + ServerPool, build_server_tls_for_pool, get_auth_query_state, get_coordinator, get_pool, + register_dynamic_pool, }; /// Create a dynamic data pool for auth_query passthrough mode. @@ -36,7 +36,7 @@ pub fn create_dynamic_pool( // Fast path: pool already exists if let Some(existing) = get_pool(pool_name, username) { // Update backend_auth (credentials may have changed) - if let (Some(ref ba_lock), Some(new_ba)) = (&existing.address.backend_auth, &backend_auth) { + if let (Some(ba_lock), Some(new_ba)) = (&existing.address.backend_auth, &backend_auth) { debug!( "[{username}@{pool_name}] auth_query: dynamic pool already exists, updating backend_auth" ); @@ -197,7 +197,7 @@ pub fn create_dynamic_pool( // Re-check after clone (another thread may have created it) if let Some(existing) = new_pools.get(&identifier) { - if let (Some(ref ba_lock), Some(ref new_ba)) = ( + if let (Some(ba_lock), Some(new_ba)) = ( &existing.address.backend_auth, &conn_pool.address.backend_auth, ) { diff --git a/src/pool/eviction.rs b/src/pool/eviction.rs index 3be379612..115f4e6f0 100644 --- a/src/pool/eviction.rs +++ b/src/pool/eviction.rs @@ -10,7 +10,7 @@ use log::{debug, info}; use crate::utils::format_duration_ms; use super::pool_coordinator; -use super::{get_pool, ConnectionPool, PoolIdentifier, POOLS}; +use super::{ConnectionPool, POOLS, PoolIdentifier, get_pool}; /// Adapter bridging `PoolCoordinator`'s eviction callbacks to real pool state. /// diff --git a/src/pool/fallback.rs b/src/pool/fallback.rs index 0e76bceb0..d55f0dab7 100644 --- a/src/pool/fallback.rs +++ b/src/pool/fallback.rs @@ -4,8 +4,8 @@ use std::pin::Pin; use std::sync::atomic::{AtomicBool, Ordering}; use std::time::{Duration, Instant}; -use futures::future::Shared; use futures::FutureExt; +use futures::future::Shared; use log::{debug, info}; use parking_lot::Mutex; diff --git a/src/pool/gc.rs b/src/pool/gc.rs index 2d016d2c9..bee6c06d3 100644 --- a/src/pool/gc.rs +++ b/src/pool/gc.rs @@ -1,10 +1,10 @@ -use std::sync::atomic::Ordering; use std::sync::Arc; +use std::sync::atomic::Ordering; use std::time::Duration; use log::{debug, info}; -use super::{PoolIdentifier, AUTH_QUERY_STATE, DYNAMIC_POOLS, POOLS}; +use super::{AUTH_QUERY_STATE, DYNAMIC_POOLS, POOLS, PoolIdentifier}; /// Spawn a background task that periodically removes idle dynamic pools. /// Dynamic pools are created by auth_query passthrough mode — one per user. diff --git a/src/pool/inner.rs b/src/pool/inner.rs index 9012a351d..769bf9377 100644 --- a/src/pool/inner.rs +++ b/src/pool/inner.rs @@ -3,8 +3,8 @@ use std::{ fmt, ops::{Deref, DerefMut}, sync::{ - atomic::{AtomicU64, AtomicUsize, Ordering}, Arc, Weak, + atomic::{AtomicU64, AtomicUsize, Ordering}, }, time::Duration, }; @@ -16,12 +16,12 @@ use crate::utils::clock; use parking_lot::Mutex; -use tokio::sync::{oneshot, Notify, Semaphore, SemaphorePermit, TryAcquireError}; +use tokio::sync::{Notify, Semaphore, SemaphorePermit, TryAcquireError, oneshot}; +use super::ServerPool; use super::errors::{PoolError, RecycleError, TimeoutType}; use super::pool_coordinator; use super::types::{Metrics, PoolConfig, QueueMode, Status, Timeouts}; -use super::ServerPool; use crate::server::Server; const MAX_FAST_RETRY: i32 = 10; @@ -1054,8 +1054,10 @@ impl Pool { let slots = self.inner.slots.lock(); warn!( "[{}@{}] checkout timeout at phase=burst_gate elapsed={}ms size={} inflight={} waiters={}", - self.inner.pool_name, self.inner.username, - start.elapsed().as_millis(), slots.size, + self.inner.pool_name, + self.inner.username, + start.elapsed().as_millis(), + slots.size, self.inner.inflight_creates.load(Ordering::Relaxed), slots.waiters.len(), ); diff --git a/src/pool/mod.rs b/src/pool/mod.rs index e8c4f1f16..3abc43598 100644 --- a/src/pool/mod.rs +++ b/src/pool/mod.rs @@ -5,18 +5,18 @@ use once_cell::sync::{Lazy, OnceCell}; use parking_lot::{Mutex, RwLock}; use std::collections::{HashMap, HashSet}; use std::fmt::{Display, Formatter}; -use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::Arc; +use std::sync::atomic::{AtomicU32, Ordering}; use crate::config::{ - get_config, tls, Address, BackendAuthMethod, General, Pool as ConfigPool, PoolMode, User, + Address, BackendAuthMethod, General, Pool as ConfigPool, PoolMode, User, get_config, tls, }; use crate::errors::Error; use crate::messages::Parse; use crate::server::ServerParameters; -use crate::stats::auth_query::AuthQueryStats; use crate::stats::AddressStats; +use crate::stats::auth_query::AuthQueryStats; mod errors; mod inner; @@ -705,7 +705,9 @@ impl ConnectionPool { Some(new) => *new != old_state.config, // config changed }; if changed { - info!("[pool: {pool_name}] auth_query config changed — collecting dynamic pools for removal"); + info!( + "[pool: {pool_name}] auth_query config changed — collecting dynamic pools for removal" + ); for id in DYNAMIC_POOLS.load().iter() { if id.db == *pool_name { pools_to_remove.push(id.clone()); diff --git a/src/pool/pool_coordinator.rs b/src/pool/pool_coordinator.rs index 5a9e46857..c079a3641 100644 --- a/src/pool/pool_coordinator.rs +++ b/src/pool/pool_coordinator.rs @@ -1,9 +1,9 @@ -use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering}; use std::sync::Arc; +use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering}; use std::time::Duration; use log::{debug, info, warn}; -use tokio::sync::{mpsc, Notify, Semaphore}; +use tokio::sync::{Notify, Semaphore, mpsc}; /// Source of eviction candidates and user state. /// Implemented by the pool layer when wired in; mocked in benchmarks. @@ -2080,7 +2080,7 @@ mod tests { let mut guard = self.permit.lock().unwrap(); if guard.is_some() { *guard = None; // drops permit, frees slot - // Immediately grab the freed slot before the caller + // Immediately grab the freed slot before the caller let stolen = self.coord.try_acquire(); assert!(stolen.is_some(), "should steal the freed permit"); std::mem::forget(stolen); // leak to keep slot occupied for this test diff --git a/src/pool/retain.rs b/src/pool/retain.rs index 3cf77863a..7f86ba1fe 100644 --- a/src/pool/retain.rs +++ b/src/pool/retain.rs @@ -1,5 +1,5 @@ -use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; use log::{info, warn}; use rand::seq::SliceRandom; @@ -7,7 +7,7 @@ use rand::seq::SliceRandom; use crate::config::get_config; use crate::utils::{format_duration_ms, format_elapsed}; -use super::{get_all_pools, ConnectionPool}; +use super::{ConnectionPool, get_all_pools}; impl ConnectionPool { /// Retain pool connections based on idle timeout and lifetime settings. @@ -239,7 +239,8 @@ pub async fn retain_connections() { if prev_failures > 0 { info!( "[{}@{}] replenish recovered after {} failure{}: created {} server{} (min_pool_size={})", - pool.address.username, pool.address.pool_name, + pool.address.username, + pool.address.pool_name, prev_failures, if prev_failures == 1 { "" } else { "s" }, created, @@ -266,8 +267,11 @@ pub async fn retain_connections() { } else if failures % 20 == 0 { warn!( "[{}@{}] replenish still failing: {} consecutive failures (deficit={}, min_pool_size={})", - pool.address.username, pool.address.pool_name, - failures, deficit, min, + pool.address.username, + pool.address.pool_name, + failures, + deficit, + min, ); } } diff --git a/src/pool/server_pool.rs b/src/pool/server_pool.rs index 70f70c66b..733a24116 100644 --- a/src/pool/server_pool.rs +++ b/src/pool/server_pool.rs @@ -4,8 +4,8 @@ //! server connections. It handles connect timeouts, lifetime checks, alive //! checks, pause/resume, and reconnect epoch management. -use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; +use std::sync::atomic::{AtomicU64, Ordering}; use std::time::Duration; use log::{debug, info, warn}; @@ -18,9 +18,9 @@ use crate::server::Server; use crate::stats::ServerStats; use crate::utils::format_duration_ms; +use super::ClientServerMap; use super::errors::{RecycleError, RecycleResult}; use super::types::Metrics; -use super::ClientServerMap; /// Wrapper for the connection pool. pub struct ServerPool { @@ -254,8 +254,7 @@ impl ServerPool { let (result, active_stats) = if should_tls_retry { info!( "plain connection rejected, retrying with tls, user={} pool={} host={} port={} server_tls_mode=allow", - self.address.username, self.address.pool_name, - self.address.host, self.address.port, + self.address.username, self.address.pool_name, self.address.host, self.address.port, ); // Disconnect the plain-attempt stats before registering the TLS-retry stats. // Without this, both entries would remain in SERVER_STATS: the plain one @@ -461,7 +460,7 @@ impl ServerPool { "whitelist round produced no target".into(), )), source, - ) + ); } }; info!( diff --git a/src/prometheus/metrics.rs b/src/prometheus/metrics.rs index daf284d1b..7a08cddf9 100644 --- a/src/prometheus/metrics.rs +++ b/src/prometheus/metrics.rs @@ -3,31 +3,30 @@ #[cfg(target_os = "linux")] use log::error; use once_cell::sync::Lazy; -use std::sync::atomic::Ordering; use std::sync::Arc; +use std::sync::atomic::Ordering; -use crate::pool::{PoolIdentifier, AUTH_QUERY_STATE, COORDINATORS, DYNAMIC_POOLS}; +use crate::pool::{AUTH_QUERY_STATE, COORDINATORS, DYNAMIC_POOLS, PoolIdentifier}; #[cfg(target_os = "linux")] use crate::stats::get_socket_states_count; use crate::stats::pool::PoolStats; use crate::stats::{ - get_server_stats, CANCEL_CONNECTION_COUNTER, PLAIN_CONNECTION_COUNTER, TLS_CONNECTION_COUNTER, - TOTAL_CONNECTION_COUNTER, + CANCEL_CONNECTION_COUNTER, PLAIN_CONNECTION_COUNTER, TLS_CONNECTION_COUNTER, + TOTAL_CONNECTION_COUNTER, get_server_stats, }; -use super::system::get_process_memory_usage; #[cfg(target_os = "linux")] use super::SHOW_SOCKETS; +use super::system::get_process_memory_usage; use super::{ AUTH_QUERY_AUTH, AUTH_QUERY_CACHE, AUTH_QUERY_DYNAMIC_POOLS, AUTH_QUERY_EXECUTOR, COORDINATOR, COORDINATOR_TOTALS, POOL_SCALING_GAUGE, POOL_SCALING_TOTALS, SHOW_ASYNC_CLIENTS_COUNT, - SHOW_CLIENT_CACHE_BYTES, SHOW_CLIENT_CACHE_ENTRIES, SHOW_CONNECTIONS, SHOW_POOLS_BYTES, - SHOW_POOLS_CLIENT, SHOW_POOLS_QUERIES_COUNTER, SHOW_POOLS_QUERIES_PERCENTILE, - SHOW_POOLS_QUERIES_TOTAL_TIME, SHOW_POOLS_SERVER, SHOW_POOLS_TRANSACTIONS_COUNTER, - SHOW_POOLS_TRANSACTIONS_PERCENTILE, SHOW_POOLS_TRANSACTIONS_TOTAL_TIME, - SHOW_POOLS_WAIT_TIME_AVG, SHOW_POOL_CACHE_BYTES, SHOW_POOL_CACHE_ENTRIES, SHOW_POOL_SIZE, - SHOW_SERVERS_PREPARED_HITS, SHOW_SERVERS_PREPARED_MISSES, SHOW_SERVER_TLS_CONNECTIONS, - TOTAL_MEMORY, + SHOW_CLIENT_CACHE_BYTES, SHOW_CLIENT_CACHE_ENTRIES, SHOW_CONNECTIONS, SHOW_POOL_CACHE_BYTES, + SHOW_POOL_CACHE_ENTRIES, SHOW_POOL_SIZE, SHOW_POOLS_BYTES, SHOW_POOLS_CLIENT, + SHOW_POOLS_QUERIES_COUNTER, SHOW_POOLS_QUERIES_PERCENTILE, SHOW_POOLS_QUERIES_TOTAL_TIME, + SHOW_POOLS_SERVER, SHOW_POOLS_TRANSACTIONS_COUNTER, SHOW_POOLS_TRANSACTIONS_PERCENTILE, + SHOW_POOLS_TRANSACTIONS_TOTAL_TIME, SHOW_POOLS_WAIT_TIME_AVG, SHOW_SERVER_TLS_CONNECTIONS, + SHOW_SERVERS_PREPARED_HITS, SHOW_SERVERS_PREPARED_MISSES, TOTAL_MEMORY, }; /// Updates all metrics before they are exposed via the Prometheus endpoint. diff --git a/src/prometheus/server.rs b/src/prometheus/server.rs index 22b1180fe..d40e128d8 100644 --- a/src/prometheus/server.rs +++ b/src/prometheus/server.rs @@ -1,15 +1,15 @@ //! HTTP server for Prometheus metrics endpoint. -use flate2::write::GzEncoder; use flate2::Compression; +use flate2::write::GzEncoder; use log::{error, info}; use prometheus::{Encoder, TextEncoder}; use std::io::Write; use std::net::SocketAddr; use tokio::net::TcpSocket; -use super::metrics::update_metrics; use super::REGISTRY; +use super::metrics::update_metrics; /// Handles HTTP requests for metrics pub async fn handle_metrics_request(stream: tokio::net::TcpStream) { diff --git a/src/server/mod.rs b/src/server/mod.rs index 54546e01d..c2a6edd06 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -13,6 +13,6 @@ mod prepared_statement_cache; mod server_backend; pub use parameters::ServerParameters; -pub use prepared_statement_cache::{intern_query, PreparedStatementCache}; +pub use prepared_statement_cache::{PreparedStatementCache, intern_query}; pub use server_backend::Server; pub use stream::StreamInner; diff --git a/src/server/prepared_statement_cache.rs b/src/server/prepared_statement_cache.rs index 106aa3d4f..cae6e48b2 100644 --- a/src/server/prepared_statement_cache.rs +++ b/src/server/prepared_statement_cache.rs @@ -1,8 +1,8 @@ use dashmap::DashMap; use log::info; use once_cell::sync::Lazy; -use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; +use std::sync::atomic::{AtomicU64, Ordering}; use crate::messages::Parse; use crate::utils::dashmap::new_dashmap_with_capacity; @@ -173,7 +173,10 @@ impl PreparedStatementCache { }; info!( "Pool cache eviction: hash={:#x}, name={}, query=\"{truncated}{ellipsis}\", size={}/{}", - key, entry.parse.name, self.cache.len(), self.max_size, + key, + entry.parse.name, + self.cache.len(), + self.max_size, ); } } diff --git a/src/server/protocol_io.rs b/src/server/protocol_io.rs index f1a38ddc1..53b06529a 100644 --- a/src/server/protocol_io.rs +++ b/src/server/protocol_io.rs @@ -25,11 +25,11 @@ use tokio::time::timeout; use crate::config::get_config; use crate::errors::Error; use crate::errors::Error::MaxMessageSize; -use crate::messages::PgErrorMsg; use crate::messages::MAX_MESSAGE_SIZE; +use crate::messages::PgErrorMsg; use crate::messages::{ - proxy_copy_data, proxy_copy_data_with_timeout, read_message_body_reuse, read_message_header, - write_all_flush, BytesMutReader, + BytesMutReader, proxy_copy_data, proxy_copy_data_with_timeout, read_message_body_reuse, + read_message_header, write_all_flush, }; use super::parameters::ServerParameters; @@ -232,11 +232,14 @@ fn handle_ready_for_query(server: &mut Server, message: &mut BytesMut) -> Result 'E' => { server.in_transaction = true; if let Ok(msg) = PgErrorMsg::parse(message) { - let mut details = - format!( + let mut details = format!( "[{}@{}] transaction rolled back pid={}: severity={}, code={}, message=\"{}\"", - server.address.username, server.address.pool_name, server.get_process_id(), - msg.severity, msg.code, sanitize_for_log(&msg.message), + server.address.username, + server.address.pool_name, + server.get_process_id(), + msg.severity, + msg.code, + sanitize_for_log(&msg.message), ); if let Some(ref hint) = msg.hint { details.push_str(&format!(", hint=\"{}\"", sanitize_for_log(hint))); @@ -282,9 +285,14 @@ fn handle_error_response(server: &mut Server, message: &mut BytesMut) { if let Ok(msg) = PgErrorMsg::parse(message) { let mut details = format!( "[{}@{}] server error pid={}: severity={}, code={}, message=\"{}\", in_transaction={}, in_copy={}", - server.address.username, server.address.pool_name, server.get_process_id(), - msg.severity, msg.code, sanitize_for_log(&msg.message), - server.in_transaction, server.in_copy_mode, + server.address.username, + server.address.pool_name, + server.get_process_id(), + msg.severity, + msg.code, + sanitize_for_log(&msg.message), + server.in_transaction, + server.in_copy_mode, ); if let Some(ref hint) = msg.hint { details.push_str(&format!(", hint=\"{}\"", sanitize_for_log(hint))); @@ -476,11 +484,11 @@ where match code_u8 as char { 'D' => { return handle_large_data_row(server, &mut client_stream, code_u8, message_len) - .await + .await; } 'd' => { return handle_large_copy_data(server, &mut client_stream, code_u8, message_len) - .await + .await; } _ => unreachable!("pending_large_message should only contain 'D' or 'd'"), } @@ -752,7 +760,7 @@ mod tests { //! * `RESET ALL` is reported as `RESET\0`, not `RESET ALL\0`. //! * `CLOSE ALL` is reported as `CLOSE CURSOR ALL\0`, not `CLOSE ALL\0`. - use super::{classify_command_complete, CommandCompleteEffect}; + use super::{CommandCompleteEffect, classify_command_complete}; #[test] fn set_tag_arms_set_cleanup() { diff --git a/src/server/server_backend.rs b/src/server/server_backend.rs index d5f23b3a8..43e69dc69 100644 --- a/src/server/server_backend.rs +++ b/src/server/server_backend.rs @@ -15,20 +15,20 @@ use tokio::io::{AsyncReadExt, BufStream}; // Internal crate imports use crate::auth::scram_client::ScramSha256; -use crate::config::{get_config, tls, Address, BackendAuthMethod, User}; +use crate::config::{Address, BackendAuthMethod, User, get_config, tls}; use crate::errors::{Error, ServerIdentifier}; use crate::messages::PgErrorMsg; use crate::messages::{ - read_message_data, simple_query, startup, sync, BytesMutReader, Close, Parse, + BytesMutReader, Close, Parse, read_message_data, simple_query, startup, sync, }; -use crate::pool::{CancelTarget, ClientServerMap, CANCELED_PIDS}; +use crate::pool::{CANCELED_PIDS, CancelTarget, ClientServerMap}; use crate::stats::ServerStats; use super::authentication::handle_authentication; use super::cleanup::CleanupState; use super::parameters::ServerParameters; use super::startup_error::handle_startup_error; -use super::stream::{create_tcp_stream_inner, create_unix_stream_inner, StreamInner}; +use super::stream::{StreamInner, create_tcp_stream_inner, create_unix_stream_inner}; use super::{prepared_statements, protocol_io, startup_cancel}; /// Buffer flush threshold in bytes (8 KiB). @@ -990,7 +990,15 @@ impl Server { // We have an unexpected message from the server during this exchange. _ => { - error!("[{}@{}] unexpected message code '{}' (ASCII: {}) during server startup to {}:{}", server_identifier.username, server_identifier.pool_name, code, code as u8, address.host, address.port); + error!( + "[{}@{}] unexpected message code '{}' (ASCII: {}) during server startup to {}:{}", + server_identifier.username, + server_identifier.pool_name, + code, + code as u8, + address.host, + address.port + ); return Err(Error::ProtocolSyncError(format!( "Received unexpected message code '{}' (ASCII: {}) during server startup. This may indicate an incompatible PostgreSQL server version or protocol.", code, code as u8 diff --git a/src/server/startup_error.rs b/src/server/startup_error.rs index eaaab0513..90f0a6183 100644 --- a/src/server/startup_error.rs +++ b/src/server/startup_error.rs @@ -4,8 +4,8 @@ use log::error; use tokio::io::AsyncReadExt; use crate::errors::{Error, ServerIdentifier}; -use crate::messages::constants::MESSAGE_TERMINATOR; use crate::messages::PgErrorMsg; +use crate::messages::constants::MESSAGE_TERMINATOR; use super::stream::StreamInner; diff --git a/src/stats/client.rs b/src/stats/client.rs index 7a35f59a2..5f3f4a383 100644 --- a/src/stats/client.rs +++ b/src/stats/client.rs @@ -1,7 +1,7 @@ -use super::{get_reporter, Reporter}; +use super::{Reporter, get_reporter}; use iota::iota; -use std::sync::atomic::*; use std::sync::Arc; +use std::sync::atomic::*; use crate::utils::clock; diff --git a/src/stats/mod.rs b/src/stats/mod.rs index 9049c135f..6d4f44f60 100644 --- a/src/stats/mod.rs +++ b/src/stats/mod.rs @@ -123,7 +123,9 @@ impl Reporter { use std::collections::hash_map::Entry; match CLIENT_STATS.write().entry(client_id) { Entry::Occupied(_) => { - warn!("[#c{client_id}] duplicate stats registration, skipping (likely migrated client id collision)"); + warn!( + "[#c{client_id}] duplicate stats registration, skipping (likely migrated client id collision)" + ); } Entry::Vacant(entry) => { entry.insert(stats); diff --git a/src/stats/pool.rs b/src/stats/pool.rs index 88eeac1c3..591d94a09 100644 --- a/src/stats/pool.rs +++ b/src/stats/pool.rs @@ -17,14 +17,14 @@ use log::{debug, error}; use crate::{config::PoolMode, messages::DataType, pool::PoolIdentifier}; use std::borrow::Cow; use std::collections::HashMap; -use std::sync::atomic::*; use std::sync::Arc; +use std::sync::atomic::*; use crate::pool::get_all_pools; -use crate::stats::client::{CLIENT_STATE_ACTIVE, CLIENT_STATE_IDLE, CLIENT_STATE_WAITING}; -use crate::stats::server::{SERVER_STATE_ACTIVE, SERVER_STATE_IDLE, SERVER_STATE_LOGIN}; use crate::stats::ClientStats; use crate::stats::ServerStats; +use crate::stats::client::{CLIENT_STATE_ACTIVE, CLIENT_STATE_IDLE, CLIENT_STATE_WAITING}; +use crate::stats::server::{SERVER_STATE_ACTIVE, SERVER_STATE_IDLE, SERVER_STATE_LOGIN}; #[derive(Debug, Clone)] /// Comprehensive statistics for a PostgreSQL connection pool. diff --git a/src/stats/server.rs b/src/stats/server.rs index 4df794876..b1c5c26b0 100644 --- a/src/stats/server.rs +++ b/src/stats/server.rs @@ -1,11 +1,11 @@ use super::AddressStats; -use super::{get_reporter, Reporter}; +use super::{Reporter, get_reporter}; use crate::config::Address; use crate::utils::clock; use iota::iota; use parking_lot::Mutex; -use std::sync::atomic::*; use std::sync::Arc; +use std::sync::atomic::*; // Server state constants used to track the current activity state of a server connection. // diff --git a/src/utils/core_affinity.rs b/src/utils/core_affinity.rs index f18839192..c991dea64 100644 --- a/src/utils/core_affinity.rs +++ b/src/utils/core_affinity.rs @@ -64,7 +64,7 @@ fn clear_for_current_helper() -> bool { mod linux { use std::mem; - use libc::{cpu_set_t, sched_getaffinity, sched_setaffinity, CPU_ISSET, CPU_SET, CPU_SETSIZE}; + use libc::{CPU_ISSET, CPU_SET, CPU_SETSIZE, cpu_set_t, sched_getaffinity, sched_setaffinity}; use super::CoreId; @@ -130,11 +130,7 @@ mod linux { ) }; - if result == 0 { - Some(set) - } else { - None - } + if result == 0 { Some(set) } else { None } } fn new_cpu_set() -> cpu_set_t { diff --git a/src/utils/rate_limit.rs b/src/utils/rate_limit.rs index 7555ed345..38500fd6d 100644 --- a/src/utils/rate_limit.rs +++ b/src/utils/rate_limit.rs @@ -1,4 +1,4 @@ -use tokio::sync::mpsc::{channel, Receiver, Sender}; +use tokio::sync::mpsc::{Receiver, Sender, channel}; use tokio::sync::oneshot; use tokio::time::sleep; use tokio::time::{Duration, Instant}; diff --git a/tests/bdd/extended/admin_session.rs b/tests/bdd/extended/admin_session.rs index d38a22550..ac5bd015e 100644 --- a/tests/bdd/extended/admin_session.rs +++ b/tests/bdd/extended/admin_session.rs @@ -220,7 +220,7 @@ pub async fn execute_admin_query_and_store_response( // Format: process_id (4 bytes) + channel (null-terminated) + payload (null-terminated) if data.len() >= 4 { let mut pos = 4; // skip process_id - // Read channel name + // Read channel name if let Some(null_pos) = data[pos..].iter().position(|&b| b == 0) { let channel = String::from_utf8_lossy(&data[pos..pos + null_pos]); response_content.push_str(&channel); diff --git a/tests/bdd/pg_connection.rs b/tests/bdd/pg_connection.rs index 73fab5129..8e719ee20 100644 --- a/tests/bdd/pg_connection.rs +++ b/tests/bdd/pg_connection.rs @@ -1,7 +1,7 @@ use std::pin::Pin; use std::task::{Context, Poll}; -use base64::{engine::general_purpose, Engine as _}; +use base64::{Engine as _, engine::general_purpose}; use hmac::{Hmac, Mac}; use rand::Rng; use sha2::{Digest, Sha256}; diff --git a/tests/bdd/pool_bench_helper.rs b/tests/bdd/pool_bench_helper.rs index a7423eaec..83481f7fd 100644 --- a/tests/bdd/pool_bench_helper.rs +++ b/tests/bdd/pool_bench_helper.rs @@ -6,8 +6,8 @@ use cucumber::{given, then, when}; use pprof::ProfilerGuardBuilder; use std::collections::HashMap; -use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; +use std::sync::atomic::{AtomicU64, Ordering}; use std::time::{Duration, Instant}; /// Check if pprof profiling is enabled via PPROF=1 environment variable diff --git a/tests/bdd/postgres_helper.rs b/tests/bdd/postgres_helper.rs index 226d191a0..119152980 100644 --- a/tests/bdd/postgres_helper.rs +++ b/tests/bdd/postgres_helper.rs @@ -6,8 +6,8 @@ use std::io::{BufRead, BufReader, Write}; use std::os::unix::fs::PermissionsExt; use std::path::PathBuf; use std::process::{Command, Stdio}; -use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; use std::time::Duration; use tempfile::TempDir; use tokio::time::sleep; diff --git a/tests/bdd/shell_helper.rs b/tests/bdd/shell_helper.rs index 9837772a1..0cc877be3 100644 --- a/tests/bdd/shell_helper.rs +++ b/tests/bdd/shell_helper.rs @@ -140,8 +140,10 @@ fn run_command_with_timeout( && !streaming_started.load(std::sync::atomic::Ordering::Relaxed) { streaming_started.store(true, std::sync::atomic::Ordering::Relaxed); - eprintln!("\n=== Command running for more than {} seconds, streaming output ===", - STREAMING_THRESHOLD_SECS); + eprintln!( + "\n=== Command running for more than {} seconds, streaming output ===", + STREAMING_THRESHOLD_SECS + ); } // Check timeout From 3931665724bf4ddedbcd1b07b9a9da31ed17036b Mon Sep 17 00:00:00 2001 From: dmitrivasilyev Date: Thu, 30 Apr 2026 13:37:32 +0300 Subject: [PATCH 04/13] chore(clippy): fix lints surfaced by rust 1.95 + edition 2024 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step 3 of the 3.7.0 upgrade. cargo clippy --fix handled the bulk auto-rewritably (collapsible_if into 2024 if-let-chains, plus collapsible_match, derivable_impls, useless_conversion, and unnecessary_unwrap across admin, app, auth, pool, client, server, util, and several test helpers). Eleven lints required manual rewrites: - if-x-is-some-then-x-unwrap → if-let-Some patterns in scram and server_backend. - result.is_err && unwrap_err → let-Err binding in entrypoint and migration (uses 2024 let-chains). - sort_by(reverse-cmp) → sort_by_key(Reverse) in log_level, pool inner, and a bench helper. - Manual `if x > 0 { y / x } else { default }` → checked_div(x) .unwrap_or(default) in pool inner, stats address, and stats pool. Same observable behavior; the default already matched the original else-branch in each place. cargo fmt and cargo clippy --all-targets --deny warnings now both exit clean on the branch. --- src/admin/commands.rs | 44 +++---- src/app/log_level.rs | 2 +- src/app/server.rs | 10 +- src/auth/auth_query.rs | 59 +++++---- src/auth/hba.rs | 8 +- src/auth/jwt.rs | 8 +- src/auth/mod.rs | 41 +++---- src/auth/scram.rs | 5 +- src/auth/talos.rs | 22 ++-- src/bin/patroni_proxy/port.rs | 49 ++++---- src/client/batch_handling.rs | 5 +- src/client/buffer_pool.rs | 8 +- src/client/core.rs | 8 +- src/client/entrypoint.rs | 10 +- src/client/migration.rs | 26 ++-- src/client/transaction.rs | 12 +- src/config/mod.rs | 18 +-- src/config/pool.rs | 161 ++++++++++++------------- src/config/tls.rs | 24 ++-- src/config/user.rs | 14 +-- src/messages/protocol.rs | 4 +- src/pool/fallback.rs | 16 +-- src/pool/inner.rs | 27 ++--- src/pool/mod.rs | 25 ++-- src/pool/pool_coordinator.rs | 49 ++++---- src/pool/retain.rs | 11 +- src/pool/server_pool.rs | 56 ++++----- src/pool/types.rs | 9 +- src/prometheus/metrics.rs | 8 +- src/prometheus/system.rs | 10 +- src/server/parameters.rs | 11 +- src/server/prepared_statement_cache.rs | 34 +++--- src/server/prepared_statements.rs | 8 +- src/server/protocol_io.rs | 45 +++---- src/server/server_backend.rs | 6 +- src/stats/address.rs | 30 ++--- src/stats/pool.rs | 10 +- src/utils/debug_messages.rs | 30 ++--- tests/bdd/doorman_helper.rs | 19 ++- tests/bdd/extended/admin_session.rs | 10 +- tests/bdd/main.rs | 11 +- tests/bdd/pgbench_helper.rs | 8 +- tests/bdd/pool_bench_helper.rs | 2 +- tests/bdd/postgres_helper.rs | 43 ++++--- 44 files changed, 489 insertions(+), 527 deletions(-) diff --git a/src/admin/commands.rs b/src/admin/commands.rs index 981657d00..0163c24c9 100644 --- a/src/admin/commands.rs +++ b/src/admin/commands.rs @@ -133,16 +133,16 @@ async fn check_db_has_pools( where T: tokio::io::AsyncWrite + std::marker::Unpin, { - if let Some(db_name) = db { - if !pools.keys().any(|id| id.db == *db_name) { - admin_error_response( - stream, - &format!("No pool for database \"{}\"", db_name), - "3D000", - ) - .await?; - return Ok(false); - } + if let Some(db_name) = db + && !pools.keys().any(|id| id.db == *db_name) + { + admin_error_response( + stream, + &format!("No pool for database \"{}\"", db_name), + "3D000", + ) + .await?; + return Ok(false); } Ok(true) } @@ -159,10 +159,10 @@ where return Ok(()); } for (identifier, pool) in pools.iter() { - if let Some(ref db_name) = db { - if identifier.db != *db_name { - continue; - } + if let Some(ref db_name) = db + && identifier.db != *db_name + { + continue; } pool.database.pause(); info!("PAUSE: paused pool {}", identifier); @@ -187,10 +187,10 @@ where return Ok(()); } for (identifier, pool) in pools.iter() { - if let Some(ref db_name) = db { - if identifier.db != *db_name { - continue; - } + if let Some(ref db_name) = db + && identifier.db != *db_name + { + continue; } pool.database.resume(); info!("RESUME: resumed pool {}", identifier); @@ -216,10 +216,10 @@ where return Ok(()); } for (identifier, pool) in pools.iter() { - if let Some(ref db_name) = db { - if identifier.db != *db_name { - continue; - } + if let Some(ref db_name) = db + && identifier.db != *db_name + { + continue; } let new_epoch = pool.database.reconnect(); info!( diff --git a/src/app/log_level.rs b/src/app/log_level.rs index 23301393f..02cd97958 100644 --- a/src/app/log_level.rs +++ b/src/app/log_level.rs @@ -166,7 +166,7 @@ fn parse_filter(s: &str) -> Result<(LevelFilter, Vec<(String, LevelFilter)>), St } // Sort modules by prefix length descending (longest match first) - modules.sort_by(|a, b| b.0.len().cmp(&a.0.len())); + modules.sort_by_key(|m| std::cmp::Reverse(m.0.len())); Ok((base.unwrap_or(LevelFilter::Info), modules)) } diff --git a/src/app/server.rs b/src/app/server.rs index 6343a100b..039de9211 100644 --- a/src/app/server.rs +++ b/src/app/server.rs @@ -301,8 +301,8 @@ pub fn run_server(args: Args, config: Config) -> Result<(), Box() { + if let Ok(ready_fd_str) = std::env::var("PG_DOORMAN_READY_FD") + && let Ok(ready_fd) = ready_fd_str.parse::() { info!("Signaling readiness to parent process (fd={})", ready_fd); let ready_signal: [u8; 1] = [1]; unsafe { @@ -313,7 +313,6 @@ pub fn run_server(args: Args, config: Config) -> Result<(), Box Result<(), Box() { + if let Ok(fd_str) = std::env::var("PG_DOORMAN_MIGRATION_FD") + && let Ok(migration_fd) = fd_str.parse::() { info!( "Migration socket received from parent (fd={})", migration_fd @@ -374,7 +373,6 @@ pub fn run_server(args: Args, config: Config) -> Result<(), Box AuthQueryCache { } // Fast path: check cache without lock - if let Some(entry) = self.entries.get(username) { - if !entry.is_expired(&self.cache_ttl, &self.cache_failure_ttl) { - self.inc(|s| &s.cache_hits); - return if entry.is_negative { - Ok(None) - } else { - Ok(Some(entry.clone())) - }; - } + if let Some(entry) = self.entries.get(username) + && !entry.is_expired(&self.cache_ttl, &self.cache_failure_ttl) + { + self.inc(|s| &s.cache_hits); + return if entry.is_negative { + Ok(None) + } else { + Ok(Some(entry.clone())) + }; } // Slow path: acquire per-username lock @@ -496,15 +496,15 @@ impl AuthQueryCache { let _guard = lock.lock().await; // Double-check after acquiring lock - if let Some(entry) = self.entries.get(username) { - if !entry.is_expired(&self.cache_ttl, &self.cache_failure_ttl) { - self.inc(|s| &s.cache_hits); - return if entry.is_negative { - Ok(None) - } else { - Ok(Some(entry.clone())) - }; - } + if let Some(entry) = self.entries.get(username) + && !entry.is_expired(&self.cache_ttl, &self.cache_failure_ttl) + { + self.inc(|s| &s.cache_hits); + return if entry.is_negative { + Ok(None) + } else { + Ok(Some(entry.clone())) + }; } // Cache miss — fetch from PG @@ -558,18 +558,17 @@ impl AuthQueryCache { let _guard = lock.lock().await; // Check rate limit (under lock to avoid TOCTOU) - if let Some(entry) = self.entries.get(username) { - if let Some(last) = entry.last_refetch_at { - if last.elapsed() < self.min_interval.as_std() { - self.inc(|s| &s.cache_rate_limited); - warn!( - "[{username}@{}] auth_query cache: refetch rate-limited ({} since last)", - self.pool_name, - format_elapsed(last.elapsed()) - ); - return Ok(None); // Rate limited - } - } + if let Some(entry) = self.entries.get(username) + && let Some(last) = entry.last_refetch_at + && last.elapsed() < self.min_interval.as_std() + { + self.inc(|s| &s.cache_rate_limited); + warn!( + "[{username}@{}] auth_query cache: refetch rate-limited ({} since last)", + self.pool_name, + format_elapsed(last.elapsed()) + ); + return Ok(None); // Rate limited } // Fetch fresh from PG diff --git a/src/auth/hba.rs b/src/auth/hba.rs index 9fb060d99..afe849f54 100644 --- a/src/auth/hba.rs +++ b/src/auth/hba.rs @@ -386,10 +386,10 @@ impl PgHba { if !rule.host_type.matches_ssl(transport.is_tls()) { continue; } - if let Some(net) = &rule.address { - if !net.contains(&transport.hba_ip()) { - continue; - } + if let Some(net) = &rule.address + && !net.contains(&transport.hba_ip()) + { + continue; } } } diff --git a/src/auth/jwt.rs b/src/auth/jwt.rs index df633e91d..b215b6eff 100644 --- a/src/auth/jwt.rs +++ b/src/auth/jwt.rs @@ -48,10 +48,10 @@ impl PreferredUsernameClaims { .duration_since(UNIX_EPOCH) .unwrap() .as_secs(); - if let Some(val) = self.default_claims.not_before { - if now < val { - return Err(Error::JWTValidate("not before".to_string())); - } + if let Some(val) = self.default_claims.not_before + && now < val + { + return Err(Error::JWTValidate("not before".to_string())); } if let Some(val) = self.default_claims.expiration { if now > val { diff --git a/src/auth/mod.rs b/src/auth/mod.rs index 07c256fc1..48c90a1e5 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -272,15 +272,15 @@ where .await?; // For static passthrough: promote ScramPending → ScramPassthrough - if let Some(ref client_key) = client_key { - if let Some(ref ba_lock) = pool.address.backend_auth { - let needs_update = matches!(*ba_lock.read(), BackendAuthMethod::ScramPending); - if needs_update { - *ba_lock.write() = BackendAuthMethod::ScramPassthrough(client_key.clone()); - info!( - "[{username_from_parameters}@{pool_name}] static passthrough: ClientKey stored after SCRAM auth" - ); - } + if let Some(ref client_key) = client_key + && let Some(ref ba_lock) = pool.address.backend_auth + { + let needs_update = matches!(*ba_lock.read(), BackendAuthMethod::ScramPending); + if needs_update { + *ba_lock.write() = BackendAuthMethod::ScramPassthrough(client_key.clone()); + info!( + "[{username_from_parameters}@{pool_name}] static passthrough: ClientKey stored after SCRAM auth" + ); } } } else if pool_password.starts_with(MD5_PASSWORD_PREFIX) { @@ -736,18 +736,17 @@ where if expected != password_response { // Password mismatch — try re-fetch (password may have changed in PG) let mut auth_ok = false; - if let Ok(Some(new_entry)) = cache.refetch_on_failure(username).await { - if new_entry.password_hash != *pool_password - && new_entry.password_hash.starts_with(MD5_PASSWORD_PREFIX) - { - let new_expected = md5_hash_second_pass( - new_entry.password_hash.strip_prefix("md5").unwrap(), - &salt, - ); - if new_expected == password_response { - auth_ok = true; - info!("[{username}@{pool_name}] auth_query: re-fetched password matched"); - } + if let Ok(Some(new_entry)) = cache.refetch_on_failure(username).await + && new_entry.password_hash != *pool_password + && new_entry.password_hash.starts_with(MD5_PASSWORD_PREFIX) + { + let new_expected = md5_hash_second_pass( + new_entry.password_hash.strip_prefix("md5").unwrap(), + &salt, + ); + if new_expected == password_response { + auth_ok = true; + info!("[{username}@{pool_name}] auth_query: re-fetched password matched"); } } if !auth_ok { diff --git a/src/auth/scram.rs b/src/auth/scram.rs index e159c4b5c..1ead965a4 100644 --- a/src/auth/scram.rs +++ b/src/auth/scram.rs @@ -372,9 +372,8 @@ pub fn prepare_server_final_message( ) -> Result<(String, Vec), Error> { // checks. let mut gs_2_header = client_first.gs2_flag.to_string() + ",,"; - if client_first.authzid.is_some() { - gs_2_header = - client_first.gs2_flag.to_string() + "," + client_first.authzid.unwrap().as_str() + ","; + if let Some(authzid) = client_first.authzid { + gs_2_header = client_first.gs2_flag.to_string() + "," + authzid.as_str() + ","; } if String::from_utf8_lossy(&client_final.channel_binding) != gs_2_header { return Err(Error::ScramClientError( diff --git a/src/auth/talos.rs b/src/auth/talos.rs index 221dd5592..9b3f4d620 100644 --- a/src/auth/talos.rs +++ b/src/auth/talos.rs @@ -125,12 +125,12 @@ impl TalosClaims { .as_secs(); // Check not_before claim - if let Some(not_before) = self.default_claims.not_before { - if now < not_before { - return Err(Error::JWTValidate(format!( - "Token not yet valid. Current time: {now}, valid from: {not_before}" - ))); - } + if let Some(not_before) = self.default_claims.not_before + && now < not_before + { + return Err(Error::JWTValidate(format!( + "Token not yet valid. Current time: {now}, valid from: {not_before}" + ))); } // Check expiration claim @@ -223,11 +223,11 @@ async fn extract_talos_token_with_key( let mut string_roles = vec![]; for (k, v) in claim.resource_access { // k = postgres.stg:pgstats - if let Some((_, resource_database)) = k.split_once(':') { - if databases.iter().any(|db| resource_database == db) { - string_roles.extend(v.roles); - // No need to continue checking other databases once we've found a match - } + if let Some((_, resource_database)) = k.split_once(':') + && databases.iter().any(|db| resource_database == db) + { + string_roles.extend(v.roles); + // No need to continue checking other databases once we've found a match } } diff --git a/src/bin/patroni_proxy/port.rs b/src/bin/patroni_proxy/port.rs index 3debf1444..9c8a22ca6 100644 --- a/src/bin/patroni_proxy/port.rs +++ b/src/bin/patroni_proxy/port.rs @@ -194,10 +194,10 @@ impl Port { } // Check noloadbalance tag - if let Some(ref tags) = member.tags { - if tags.noloadbalance == Some(true) { - return false; - } + if let Some(ref tags) = member.tags + && tags.noloadbalance == Some(true) + { + return false; } // Check role @@ -218,28 +218,27 @@ impl Port { } // Check lag (only for replicas, leader has no lag) - if let Some(max_lag) = self.max_lag_in_bytes { - if !matches!(member_role, crate::patroni::Role::Leader) { - if let Some(ref lag_value) = member.lag { - // Lag can be a number or string - let lag: Option = if lag_value.is_u64() { - lag_value.as_u64() - } else if lag_value.is_i64() { - lag_value - .as_i64() - .and_then(|v| if v >= 0 { Some(v as u64) } else { None }) - } else if lag_value.is_string() { - lag_value.as_str().and_then(|s| s.parse().ok()) - } else { - None - }; + if let Some(max_lag) = self.max_lag_in_bytes + && !matches!(member_role, crate::patroni::Role::Leader) + && let Some(ref lag_value) = member.lag + { + // Lag can be a number or string + let lag: Option = if lag_value.is_u64() { + lag_value.as_u64() + } else if lag_value.is_i64() { + lag_value + .as_i64() + .and_then(|v| if v >= 0 { Some(v as u64) } else { None }) + } else if lag_value.is_string() { + lag_value.as_str().and_then(|s| s.parse().ok()) + } else { + None + }; - if let Some(lag) = lag { - if lag > max_lag { - return false; - } - } - } + if let Some(lag) = lag + && lag > max_lag + { + return false; } } diff --git a/src/client/batch_handling.rs b/src/client/batch_handling.rs index 747adb767..5f67b336e 100644 --- a/src/client/batch_handling.rs +++ b/src/client/batch_handling.rs @@ -304,10 +304,10 @@ where in_execute = false; execute_count += 1; } - 'D' | 'n' | 'T' => { + 'D' | 'n' | 'T' // DataRow, NoData, or RowDescription can be first message of Execute // Insert ParseComplete before first message of Execute if needed - if !in_execute { + if !in_execute => { if let Some(count) = insertion_map_get(&relevant_execute, execute_count) { for _ in 0..count { new_response.extend_from_slice(&PARSE_COMPLETE_MSG); @@ -316,7 +316,6 @@ where } in_execute = true; } - } 'E' => { // ErrorResponse - if we have an error, the server will skip all subsequent commands in the batch. // We must insert all remaining ParseComplete messages for skipped parses now, diff --git a/src/client/buffer_pool.rs b/src/client/buffer_pool.rs index c322ebbbd..d33d5777d 100644 --- a/src/client/buffer_pool.rs +++ b/src/client/buffer_pool.rs @@ -33,10 +33,10 @@ fn release_buffer(mut buffer: BytesMut) { buffer.clear(); LOCAL_POOL.with(|pool| { - if let Ok(mut pool) = pool.try_borrow_mut() { - if pool.len() < MAX_POOL_SIZE { - pool.push(buffer); - } + if let Ok(mut pool) = pool.try_borrow_mut() + && pool.len() < MAX_POOL_SIZE + { + pool.push(buffer); } // If borrow fails or pool is full, just drop the buffer }) diff --git a/src/client/core.rs b/src/client/core.rs index ae67728b7..2f3734a80 100644 --- a/src/client/core.rs +++ b/src/client/core.rs @@ -475,10 +475,10 @@ impl Drop for Client { .remove(&(self.connection_id as i32, self.secret_key)); // Update server stats if the client was connected to a server - if self.connected_to_server { - if let Some(stats) = self.last_server_stats.as_ref() { - stats.idle(0); - } + if self.connected_to_server + && let Some(stats) = self.last_server_stats.as_ref() + { + stats.idle(0); } // Ensure client is removed from stats tracking when dropped diff --git a/src/client/entrypoint.rs b/src/client/entrypoint.rs index bedb76c34..e0beba774 100644 --- a/src/client/entrypoint.rs +++ b/src/client/entrypoint.rs @@ -238,14 +238,12 @@ pub async fn client_entrypoint( connection_id: client.connection_id, }; let result = client.handle().await; - if !client.is_admin() && result.is_err() { + if !client.is_admin() + && let Err(err) = result.as_ref() + { warn!( "[{}@{} #c{}] client {} disconnected with error: {}", - client.username, - client.pool_name, - client.connection_id, - addr, - result.as_ref().unwrap_err() + client.username, client.pool_name, client.connection_id, addr, err ); client.disconnect_stats(); } diff --git a/src/client/migration.rs b/src/client/migration.rs index d1800ffd7..1c012a078 100644 --- a/src/client/migration.rs +++ b/src/client/migration.rs @@ -34,16 +34,16 @@ fn restore_backend_auth_if_pending( username: &str, pool_name: &str, ) { - if let (Some(p), Some(auth)) = (pool, migrated_auth) { - if let Some(ref ba_lock) = p.address.backend_auth { - let needs_update = matches!(*ba_lock.read(), BackendAuthMethod::ScramPending); - if needs_update { - *ba_lock.write() = auth.clone(); - info!( - "[{}@{}] restored backend auth from migrated client", - username, pool_name - ); - } + if let (Some(p), Some(auth)) = (pool, migrated_auth) + && let Some(ref ba_lock) = p.address.backend_auth + { + let needs_update = matches!(*ba_lock.read(), BackendAuthMethod::ScramPending); + if needs_update { + *ba_lock.write() = auth.clone(); + info!( + "[{}@{}] restored backend auth from migrated client", + username, pool_name + ); } } } @@ -1028,14 +1028,16 @@ pub async fn migration_receiver_task( client.addr ); let result = client.handle().await; - if !client.is_admin() && result.is_err() { + if !client.is_admin() + && let Err(err) = result.as_ref() + { warn!( "[{}@{} #c{}] migrated client {} error: {}", client.username, client.pool_name, client.connection_id, client.addr, - result.as_ref().unwrap_err() + err ); client.disconnect_stats(); } diff --git a/src/client/transaction.rs b/src/client/transaction.rs index 366cafd17..cf7706da7 100644 --- a/src/client/transaction.rs +++ b/src/client/transaction.rs @@ -182,12 +182,12 @@ where .stats .transaction(self.server_parameters.get_application_name()); - if !self.transaction_mode { - if let Some(start) = self.session_xact_start.take() { - server - .stats - .add_xact_time_and_idle(start.elapsed().as_micros() as u64); - } + if !self.transaction_mode + && let Some(start) = self.session_xact_start.take() + { + server + .stats + .add_xact_time_and_idle(start.elapsed().as_micros() as u64); } if self.transaction_mode && !server.in_copy_mode() && (!check_async || !server.is_async()) { diff --git a/src/config/mod.rs b/src/config/mod.rs index 6a6d70fff..7e8d61083 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -446,15 +446,15 @@ impl Config { } } - if let Some(tls_certificate) = self.general.tls_certificate.clone() { - if let Some(tls_private_key) = self.general.tls_private_key.clone() { - match load_identity(Path::new(&tls_certificate), Path::new(&tls_private_key)) { - Ok(_) => (), - Err(err) => { - return Err(Error::BadConfig(format!( - "tls is incorrectly configured: {err:?}" - ))); - } + if let Some(tls_certificate) = self.general.tls_certificate.clone() + && let Some(tls_private_key) = self.general.tls_private_key.clone() + { + match load_identity(Path::new(&tls_certificate), Path::new(&tls_private_key)) { + Ok(_) => (), + Err(err) => { + return Err(Error::BadConfig(format!( + "tls is incorrectly configured: {err:?}" + ))); } } }; diff --git a/src/config/pool.rs b/src/config/pool.rs index b8e78bc9f..c3f8bff5e 100644 --- a/src/config/pool.rs +++ b/src/config/pool.rs @@ -226,78 +226,78 @@ impl Pool { pub async fn validate(&mut self) -> Result<(), Error> { // Validate scaling_warm_pool_ratio - if let Some(ratio) = self.scaling_warm_pool_ratio { - if ratio > 100 { - return Err(Error::BadConfig( - "scaling_warm_pool_ratio must be 0-100".into(), - )); - } + if let Some(ratio) = self.scaling_warm_pool_ratio + && ratio > 100 + { + return Err(Error::BadConfig( + "scaling_warm_pool_ratio must be 0-100".into(), + )); } // Validate pool coordinator settings - if let Some(max) = self.max_db_connections { - if max > 0 { - let total_min: u32 = self.users.iter().filter_map(|u| u.min_pool_size).sum(); - if total_min > max { - return Err(Error::BadConfig(format!( - "sum of min_pool_size ({}) exceeds max_db_connections ({}); \ + if let Some(max) = self.max_db_connections + && max > 0 + { + let total_min: u32 = self.users.iter().filter_map(|u| u.min_pool_size).sum(); + if total_min > max { + return Err(Error::BadConfig(format!( + "sum of min_pool_size ({}) exceeds max_db_connections ({}); \ not all minimums can be satisfied simultaneously", - total_min, max - ))); - } - if let Some(reserve) = self.reserve_pool_size { - if reserve > max { - log::warn!( - "reserve_pool_size ({}) exceeds max_db_connections ({}); \ + total_min, max + ))); + } + if let Some(reserve) = self.reserve_pool_size + && reserve > max + { + log::warn!( + "reserve_pool_size ({}) exceeds max_db_connections ({}); \ PostgreSQL may receive up to {} connections", - reserve, - max, - max + reserve - ); - } - } + reserve, + max, + max + reserve + ); + } - for user in &self.users { - if user.pool_size > max { - log::warn!( - "user '{}' pool_size ({}) exceeds max_db_connections ({}); \ + for user in &self.users { + if user.pool_size > max { + log::warn!( + "user '{}' pool_size ({}) exceeds max_db_connections ({}); \ effectively capped at {}", - user.username, - user.pool_size, - max, - max - ); - } + user.username, + user.pool_size, + max, + max + ); } + } - // min_connection_lifetime > idle_timeout: eviction will never trigger - // because idle connections are closed by idle_timeout first. - if let Some(min_lt) = self.min_connection_lifetime { - if let Some(idle) = self.idle_timeout { - if min_lt > idle && idle > 0 { - log::warn!( - "min_connection_lifetime ({}ms) > idle_timeout ({}ms); \ + // min_connection_lifetime > idle_timeout: eviction will never trigger + // because idle connections are closed by idle_timeout first. + if let Some(min_lt) = self.min_connection_lifetime + && let Some(idle) = self.idle_timeout + && min_lt > idle + && idle > 0 + { + log::warn!( + "min_connection_lifetime ({}ms) > idle_timeout ({}ms); \ idle connections will be closed before becoming evictable", - min_lt, - idle - ); - } - } - } + min_lt, + idle + ); + } - // min_guaranteed_pool_size > any user's pool_size: user becomes - // immune to eviction but cannot reach the guaranteed minimum. - if let Some(guaranteed) = self.min_guaranteed_pool_size { - if guaranteed > 0 { - for user in &self.users { - if guaranteed > user.pool_size { - warn!( - "min_guaranteed_pool_size ({}) > pool_size ({}) for user '{}'; \ + // min_guaranteed_pool_size > any user's pool_size: user becomes + // immune to eviction but cannot reach the guaranteed minimum. + if let Some(guaranteed) = self.min_guaranteed_pool_size + && guaranteed > 0 + { + for user in &self.users { + if guaranteed > user.pool_size { + warn!( + "min_guaranteed_pool_size ({}) > pool_size ({}) for user '{}'; \ user is immune to eviction but cannot reach the guarantee", - guaranteed, user.pool_size, user.username - ); - } - } + guaranteed, user.pool_size, user.username + ); } } } @@ -350,39 +350,38 @@ impl Pool { } } - if let Some(ref dur) = self.patroni_api_timeout { - if dur.as_millis() == 0 { - return Err(Error::BadConfig("patroni_api_timeout must be > 0".into())); - } + if let Some(ref dur) = self.patroni_api_timeout + && dur.as_millis() == 0 + { + return Err(Error::BadConfig("patroni_api_timeout must be > 0".into())); } - if let Some(ref dur) = self.fallback_connect_timeout { - if dur.as_millis() == 0 { - return Err(Error::BadConfig( - "fallback_connect_timeout must be > 0".into(), - )); - } + if let Some(ref dur) = self.fallback_connect_timeout + && dur.as_millis() == 0 + { + return Err(Error::BadConfig( + "fallback_connect_timeout must be > 0".into(), + )); } - if let Some(ref dur) = self.fallback_lifetime { - if dur.as_millis() == 0 { - return Err(Error::BadConfig("fallback_lifetime must be > 0".into())); - } + if let Some(ref dur) = self.fallback_lifetime + && dur.as_millis() == 0 + { + return Err(Error::BadConfig("fallback_lifetime must be > 0".into())); } // Lifetime longer than the cooldown lets fallback connections outlive // the local-backend recovery, mixing primary and fallback in the pool. if let (Some(lifetime), Some(cooldown)) = (&self.fallback_lifetime, &self.fallback_cooldown) + && lifetime.as_millis() > cooldown.as_millis() { - if lifetime.as_millis() > cooldown.as_millis() { - log::warn!( - "fallback_lifetime ({}ms) > fallback_cooldown ({}ms): \ + log::warn!( + "fallback_lifetime ({}ms) > fallback_cooldown ({}ms): \ fallback connections will coexist with local-backend connections \ after the cooldown expires", - lifetime.as_millis(), - cooldown.as_millis() - ); - } + lifetime.as_millis(), + cooldown.as_millis() + ); } // Validate auth_query config diff --git a/src/config/tls.rs b/src/config/tls.rs index 490cd77cb..c7d03a085 100644 --- a/src/config/tls.rs +++ b/src/config/tls.rs @@ -300,20 +300,20 @@ impl ServerTlsConfig { let cert_hash = { let mut hasher = Sha256::new(); - if let Some(ca_path) = ca_cert { - if let Ok(data) = read_file(ca_path) { - hasher.update(&data); - } + if let Some(ca_path) = ca_cert + && let Ok(data) = read_file(ca_path) + { + hasher.update(&data); } - if let Some(cert_path) = client_cert { - if let Ok(data) = read_file(cert_path) { - hasher.update(&data); - } + if let Some(cert_path) = client_cert + && let Ok(data) = read_file(cert_path) + { + hasher.update(&data); } - if let Some(key_path) = client_key { - if let Ok(data) = read_file(key_path) { - hasher.update(&data); - } + if let Some(key_path) = client_key + && let Ok(data) = read_file(key_path) + { + hasher.update(&data); } Some(hasher.finalize().into()) }; diff --git a/src/config/user.rs b/src/config/user.rs index 7e36e3f29..593823ffb 100644 --- a/src/config/user.rs +++ b/src/config/user.rs @@ -63,13 +63,13 @@ impl User { "server_password requires server_username to be set".to_string(), )); } - if let Some(min_pool_size) = self.min_pool_size { - if min_pool_size > self.pool_size { - return Err(Error::BadConfig(format!( - "min_pool_size of {} cannot be larger than pool_size of {}", - min_pool_size, self.pool_size - ))); - } + if let Some(min_pool_size) = self.min_pool_size + && min_pool_size > self.pool_size + { + return Err(Error::BadConfig(format!( + "min_pool_size of {} cannot be larger than pool_size of {}", + min_pool_size, self.pool_size + ))); }; Ok(()) diff --git a/src/messages/protocol.rs b/src/messages/protocol.rs index babe69088..00102e339 100644 --- a/src/messages/protocol.rs +++ b/src/messages/protocol.rs @@ -317,7 +317,7 @@ where { let password = md5_hash_password(user, password, salt); - let mut message = BytesMut::with_capacity(password.len() as usize + 5); + let mut message = BytesMut::with_capacity(password.len() + 5); message.put_u8(b'p'); message.put_i32(password.len() as i32 + 4); @@ -331,7 +331,7 @@ where S: tokio::io::AsyncWrite + std::marker::Unpin, { let password = md5_hash_second_pass(hash, salt); - let mut message = BytesMut::with_capacity(password.len() as usize + 5); + let mut message = BytesMut::with_capacity(password.len() + 5); message.put_u8(b'p'); message.put_i32(password.len() as i32 + 4); diff --git a/src/pool/fallback.rs b/src/pool/fallback.rs index d55f0dab7..e6d0b4e60 100644 --- a/src/pool/fallback.rs +++ b/src/pool/fallback.rs @@ -482,14 +482,14 @@ impl FallbackState { *guard = Some((host.clone(), port, role.clone())); old }; - if let Some((old_host, old_port, _)) = old { - if (old_host.as_str(), old_port) != (host.as_str(), port) { - let _ = crate::prometheus::FALLBACK_HOST.remove_label_values(&[ - &self.pool_name, - &old_host, - &old_port.to_string(), - ]); - } + if let Some((old_host, old_port, _)) = old + && (old_host.as_str(), old_port) != (host.as_str(), port) + { + let _ = crate::prometheus::FALLBACK_HOST.remove_label_values(&[ + &self.pool_name, + &old_host, + &old_port.to_string(), + ]); } info!( "[pool: {}] fallback: whitelisted {}:{} (role: {:?})", diff --git a/src/pool/inner.rs b/src/pool/inner.rs index 769bf9377..77d6b0dc1 100644 --- a/src/pool/inner.rs +++ b/src/pool/inner.rs @@ -56,12 +56,12 @@ pub struct Object { impl Drop for Object { fn drop(&mut self) { - if let Some(mut inner) = self.inner.take() { - if let Some(pool) = self.pool.upgrade() { - inner.metrics.recycled = Some(clock::now()); - inner.metrics.recycle_count += 1; - pool.return_object(inner); - } + if let Some(mut inner) = self.inner.take() + && let Some(pool) = self.pool.upgrade() + { + inner.metrics.recycled = Some(clock::now()); + inner.metrics.recycle_count += 1; + pool.return_object(inner); } } } @@ -699,11 +699,10 @@ impl Pool { tokio::select! { biased; result = &mut rx => { - if let Ok(inner) = result { - if let Ok(inner) = self.recycle_handoff(inner, timeouts).await { + if let Ok(inner) = result + && let Ok(inner) = self.recycle_handoff(inner, timeouts).await { return BurstGateOutcome::Recycled(Box::new(inner)); } - } } _ = on_create => {} _ = tokio::time::sleep(BURST_BACKOFF) => {} @@ -1219,7 +1218,7 @@ impl Pool { } // Sort by age descending (oldest first — highest age value) - candidates.sort_by(|a, b| b.1.cmp(&a.1)); + candidates.sort_by_key(|c| std::cmp::Reverse(c.1)); let to_close: std::collections::HashSet = candidates .into_iter() @@ -1632,11 +1631,9 @@ impl Pool { // Idle ratio: only pre-replace when < 25% of connections are idle. // If the pool has plenty of idle connections it can absorb the // loss of one to lifetime expiry without a spike. - let idle_pct = if slots.size > 0 { - slots.vec.len() * 100 / slots.size - } else { - 100 - }; + let idle_pct = (slots.vec.len() * 100) + .checked_div(slots.size) + .unwrap_or(100); if idle_pct >= 25 { return; } diff --git a/src/pool/mod.rs b/src/pool/mod.rs index 3abc43598..96ed79bae 100644 --- a/src/pool/mod.rs +++ b/src/pool/mod.rs @@ -509,20 +509,19 @@ impl ConnectionPool { for (pool_name, pool_config) in &config.pools { if let Some(ref aq_config) = pool_config.auth_query { // RELOAD: reuse state when config unchanged (preserves cache, executor, stats) - if let Some(old_state) = old_aq_states_for_reuse.get(pool_name) { - if old_state.config == *aq_config { - info!("[pool: {pool_name}] auth_query config unchanged — reusing state"); - auth_query_states.insert(pool_name.clone(), old_state.clone()); - // Still need to ensure shared pool exists in new_pools - if let Some(ref spid) = old_state.shared_pool_id { - if !new_pools.contains_key(spid) { - if let Some(pool) = POOLS.load().get(spid) { - new_pools.insert(spid.clone(), pool.clone()); - } - } - } - continue; + if let Some(old_state) = old_aq_states_for_reuse.get(pool_name) + && old_state.config == *aq_config + { + info!("[pool: {pool_name}] auth_query config unchanged — reusing state"); + auth_query_states.insert(pool_name.clone(), old_state.clone()); + // Still need to ensure shared pool exists in new_pools + if let Some(ref spid) = old_state.shared_pool_id + && !new_pools.contains_key(spid) + && let Some(pool) = POOLS.load().get(spid) + { + new_pools.insert(spid.clone(), pool.clone()); } + continue; } let shared_pool_id = if aq_config.is_dedicated_mode() { diff --git a/src/pool/pool_coordinator.rs b/src/pool/pool_coordinator.rs index c079a3641..9a2cf6769 100644 --- a/src/pool/pool_coordinator.rs +++ b/src/pool/pool_coordinator.rs @@ -301,18 +301,18 @@ impl PoolCoordinator { // check matches what the arbiter will itself try — a lock-free // peek on the semaphore, no extra atomics compared to the Phase D // path below. - if self.config.reserve_pool_size > 0 && self.reserve_semaphore.available_permits() > 0 { - if let Some(permit) = self + if self.config.reserve_pool_size > 0 + && self.reserve_semaphore.available_permits() > 0 + && let Some(permit) = self .try_grant_reserve(database, user, eviction_source) .await - { - return Ok(permit); - } - // Reserve grant failed (arbiter raced another caller or - // oneshot timed out). Fall through to the eviction/wait path — - // the caller still has a budget to spend on peer eviction and - // Phase C wakes before we end up back at the Phase D retry. + { + return Ok(permit); } + // Reserve grant failed (arbiter raced another caller or + // oneshot timed out). Fall through to the eviction/wait path — + // the caller still has a budget to spend on peer eviction and + // Phase C wakes before we end up back at the Phase D retry. // Phase B: try eviction — close an idle connection from another user let evicted = eviction_source.try_evict_one(user); @@ -561,24 +561,23 @@ impl PoolCoordinator { }) .await .is_ok() + && let Ok(Ok(grant)) = tokio::time::timeout(ARBITER_RESPONSE_TIMEOUT, rx).await { - if let Ok(Ok(grant)) = tokio::time::timeout(ARBITER_RESPONSE_TIMEOUT, rx).await { - self.total_connections.fetch_add(1, Ordering::Relaxed); - self.reserve_in_use.fetch_add(1, Ordering::Relaxed); - self.reserve_acquisitions_total - .fetch_add(1, Ordering::Relaxed); - info!( - "[{}@{}] coordinator: reserve permit granted \ + self.total_connections.fetch_add(1, Ordering::Relaxed); + self.reserve_in_use.fetch_add(1, Ordering::Relaxed); + self.reserve_acquisitions_total + .fetch_add(1, Ordering::Relaxed); + info!( + "[{}@{}] coordinator: reserve permit granted \ (active={}/{}, reserve_in_use={}/{})", - user, - database, - self.total_connections.load(Ordering::Relaxed), - max, - self.reserve_in_use.load(Ordering::Relaxed), - self.config.reserve_pool_size, - ); - return Some(grant.into_permit()); - } + user, + database, + self.total_connections.load(Ordering::Relaxed), + max, + self.reserve_in_use.load(Ordering::Relaxed), + self.config.reserve_pool_size, + ); + return Some(grant.into_permit()); } debug!( diff --git a/src/pool/retain.rs b/src/pool/retain.rs index 7f86ba1fe..17c04f98a 100644 --- a/src/pool/retain.rs +++ b/src/pool/retain.rs @@ -28,12 +28,11 @@ impl ConnectionPool { // Uses per-connection timeouts with jitter to prevent mass closures let should_close = |_: &crate::server::Server, metrics: &crate::pool::Metrics| -> bool { // Check idle timeout (per-connection with jitter, 0 = disabled) - if metrics.idle_timeout_ms > 0 { - if let Some(v) = metrics.recycled { - if (v.elapsed().as_millis() as u64) > metrics.idle_timeout_ms { - return true; - } - } + if metrics.idle_timeout_ms > 0 + && let Some(v) = metrics.recycled + && (v.elapsed().as_millis() as u64) > metrics.idle_timeout_ms + { + return true; } // Check server lifetime (per-connection with jitter, 0 = disabled) if metrics.lifetime_ms > 0 && (metrics.age().as_millis() as u64) > metrics.lifetime_ms { diff --git a/src/pool/server_pool.rs b/src/pool/server_pool.rs index 733a24116..a2bc6e717 100644 --- a/src/pool/server_pool.rs +++ b/src/pool/server_pool.rs @@ -303,18 +303,18 @@ impl ServerPool { Err(err) => { active_stats.disconnect(); // Local backend unreachable + Patroni-assisted fallback configured: route via fallback. - if is_backend_unreachable(&err) { - if let Some(ref fallback) = self.fallback_state { - fallback.blacklist(); - crate::prometheus::FALLBACK_ACTIVE - .with_label_values(&[&self.address.pool_name]) - .set(1.0); - info!( - "[{}@{}] fallback: routing through fallback (original error: {err})", - self.address.username, self.address.pool_name, - ); - return self.create_fallback_connection().await; - } + if is_backend_unreachable(&err) + && let Some(ref fallback) = self.fallback_state + { + fallback.blacklist(); + crate::prometheus::FALLBACK_ACTIVE + .with_label_values(&[&self.address.pool_name]) + .set(1.0); + info!( + "[{}@{}] fallback: routing through fallback (original error: {err})", + self.address.username, self.address.pool_name, + ); + return self.create_fallback_connection().await; } // Brief backoff on error to avoid hammering a failing server tokio::time::sleep(Duration::from_millis(10)).await; @@ -837,23 +837,23 @@ impl ServerPool { } // Check if connection was idle too long and needs alive check - if self.idle_check_timeout_ms > 0 { - if let Some(recycled) = metrics.recycled { - let idle_time_ms = recycled.elapsed().as_millis() as u64; - if idle_time_ms > self.idle_check_timeout_ms { - debug!( - "Connection {} idle for {}ms, checking alive...", - conn, idle_time_ms - ); - if conn.check_alive(self.connect_timeout).await.is_err() { - conn.close_reason = Some(format!( - "failed alive check after {} idle", - format_duration_ms(idle_time_ms), - )); - return Err(RecycleError::StaticMessage("Connection failed alive check")); - } - debug!("Connection {} passed alive check", conn); + if self.idle_check_timeout_ms > 0 + && let Some(recycled) = metrics.recycled + { + let idle_time_ms = recycled.elapsed().as_millis() as u64; + if idle_time_ms > self.idle_check_timeout_ms { + debug!( + "Connection {} idle for {}ms, checking alive...", + conn, idle_time_ms + ); + if conn.check_alive(self.connect_timeout).await.is_err() { + conn.close_reason = Some(format!( + "failed alive check after {} idle", + format_duration_ms(idle_time_ms), + )); + return Err(RecycleError::StaticMessage("Connection failed alive check")); } + debug!("Connection {} passed alive check", conn); } } diff --git a/src/pool/types.rs b/src/pool/types.rs index cd9db1cce..73b055787 100644 --- a/src/pool/types.rs +++ b/src/pool/types.rs @@ -96,20 +96,15 @@ impl Timeouts { } /// Mode for dequeuing objects from a pool. -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, Default)] pub enum QueueMode { /// Dequeue the object that was least recently added (first in first out). + #[default] Fifo, /// Dequeue the object that was most recently added (last in first out). Lifo, } -impl Default for QueueMode { - fn default() -> Self { - Self::Fifo - } -} - /// The current pool status. #[derive(Clone, Copy, Debug)] pub struct Status { diff --git a/src/prometheus/metrics.rs b/src/prometheus/metrics.rs index 7a08cddf9..fda883d4f 100644 --- a/src/prometheus/metrics.rs +++ b/src/prometheus/metrics.rs @@ -372,10 +372,10 @@ fn update_coordinator_metrics() { } // Reset counters for databases where coordinator was replaced (new Arc) for (db, new_ptr) in ¤t { - if let Some(old_ptr) = prev.get(db) { - if old_ptr != new_ptr { - reset_coordinator_counters(db); - } + if let Some(old_ptr) = prev.get(db) + && old_ptr != new_ptr + { + reset_coordinator_counters(db); } } *prev = current; diff --git a/src/prometheus/system.rs b/src/prometheus/system.rs index 18c5b7f24..8b9a8abaf 100644 --- a/src/prometheus/system.rs +++ b/src/prometheus/system.rs @@ -8,11 +8,11 @@ pub fn get_process_memory_usage() -> u64 { match std::fs::read_to_string("/proc/self/statm") { Ok(statm) => { let values: Vec<&str> = statm.split_whitespace().collect(); - if !values.is_empty() { - if let Ok(pages) = values[0].parse::() { - // Convert pages to bytes (page size is typically 4KB) - return pages * 4096; - } + if !values.is_empty() + && let Ok(pages) = values[0].parse::() + { + // Convert pages to bytes (page size is typically 4KB) + return pages * 4096; } 0 } diff --git a/src/server/parameters.rs b/src/server/parameters.rs index e6feaf95e..e27c2d531 100644 --- a/src/server/parameters.rs +++ b/src/server/parameters.rs @@ -86,12 +86,11 @@ impl ServerParameters { let mut diff = HashMap::new(); for key in TRACKED_PARAMETERS.iter() { - if let Some(incoming_value) = incoming_parameters.parameters.get(key) { - if let Some(value) = self.parameters.get(key) { - if value != incoming_value { - diff.insert(key.to_string(), incoming_value.to_string()); - } - } + if let Some(incoming_value) = incoming_parameters.parameters.get(key) + && let Some(value) = self.parameters.get(key) + && value != incoming_value + { + diff.insert(key.to_string(), incoming_value.to_string()); } } diff --git a/src/server/prepared_statement_cache.rs b/src/server/prepared_statement_cache.rs index cae6e48b2..d2c342ee4 100644 --- a/src/server/prepared_statement_cache.rs +++ b/src/server/prepared_statement_cache.rs @@ -162,23 +162,23 @@ impl PreparedStatementCache { } // Remove the oldest entry - if let Some(key) = oldest_key { - if let Some((_, entry)) = self.cache.remove(&key) { - let query = entry.parse.query().replace(['\n', '\r'], " "); - let truncated: String = query.chars().take(80).collect(); - let ellipsis = if query.chars().count() > 80 { - "..." - } else { - "" - }; - info!( - "Pool cache eviction: hash={:#x}, name={}, query=\"{truncated}{ellipsis}\", size={}/{}", - key, - entry.parse.name, - self.cache.len(), - self.max_size, - ); - } + if let Some(key) = oldest_key + && let Some((_, entry)) = self.cache.remove(&key) + { + let query = entry.parse.query().replace(['\n', '\r'], " "); + let truncated: String = query.chars().take(80).collect(); + let ellipsis = if query.chars().count() > 80 { + "..." + } else { + "" + }; + info!( + "Pool cache eviction: hash={:#x}, name={}, query=\"{truncated}{ellipsis}\", size={}/{}", + key, + entry.parse.name, + self.cache.len(), + self.max_size, + ); } } } diff --git a/src/server/prepared_statements.rs b/src/server/prepared_statements.rs index 49736a83b..29fd18956 100644 --- a/src/server/prepared_statements.rs +++ b/src/server/prepared_statements.rs @@ -17,10 +17,10 @@ pub(crate) fn add_to_cache( stats.prepared_cache_add(); // If we evict something, we need to close it on the server - if let Some((evicted_name, _)) = cache.push(name.to_string(), ()) { - if evicted_name != name { - return Some(evicted_name); - } + if let Some((evicted_name, _)) = cache.push(name.to_string(), ()) + && evicted_name != name + { + return Some(evicted_name); }; None diff --git a/src/server/protocol_io.rs b/src/server/protocol_io.rs index 53b06529a..f8cb4568a 100644 --- a/src/server/protocol_io.rs +++ b/src/server/protocol_io.rs @@ -645,12 +645,11 @@ where } // CopyData - 'd' => { + 'd' // Don't flush yet, buffer until we reach limit - if server.buffer.len() >= BUFFER_FLUSH_THRESHOLD { + if server.buffer.len() >= BUFFER_FLUSH_THRESHOLD => { break; } - } // CopyDone // Buffer until ReadyForQuery shows up, so don't exit the loop yet. @@ -658,68 +657,60 @@ where // ParseComplete // Response to Parse message in extended query protocol - '1' => { - if server.is_async() { + '1' + if server.is_async() => { server.decrement_expected(); } - } // BindComplete // Response to Bind message in extended query protocol - '2' => { - if server.is_async() { + '2' + if server.is_async() => { server.decrement_expected(); } - } // CloseComplete // Response to Close message in extended query protocol - '3' => { - if server.is_async() { + '3' + if server.is_async() => { server.decrement_expected(); } - } // ParameterDescription // Response to Describe message for a statement - 't' => { - if server.is_async() { + 't' + if server.is_async() => { server.decrement_expected(); } - } // PortalSuspended // Indicates that Execute completed but portal still has rows - 's' => { - if server.is_async() { + 's' + if server.is_async() => { server.decrement_expected(); } - } // NoData // Response to Describe when statement/portal produces no rows // https://www.postgresql.org/docs/current/protocol-flow.html - 'n' => { - if server.is_async() { + 'n' + if server.is_async() => { server.decrement_expected(); } - } // RowDescription // Response to Describe for a portal (or statement if it returns rows) - 'T' => { - if server.is_async() { + 'T' + if server.is_async() => { server.decrement_expected(); } - } // EmptyQueryResponse // Response to Execute with an empty query string - 'I' => { - if server.is_async() { + 'I' + if server.is_async() => { server.decrement_expected(); } - } // Anything else, e.g. notices, etc. // Keep buffering until ReadyForQuery shows up. diff --git a/src/server/server_backend.rs b/src/server/server_backend.rs index 43e69dc69..d03b2e669 100644 --- a/src/server/server_backend.rs +++ b/src/server/server_backend.rs @@ -471,13 +471,13 @@ impl Server { if self.cleanup_state.needs_cleanup_prepare { // flush prepared. self.registering_prepared_statement.clear(); - if self.prepared_statement_cache.is_some() { - let cache_size = self.prepared_statement_cache.as_ref().unwrap().len(); + if let Some(cache) = self.prepared_statement_cache.as_mut() { + let cache_size = cache.len(); info!( "[{}@{}] clearing prepared statement cache pid={}: session state reset ({} entries)", self.address.username, self.address.pool_name, self.process_id, cache_size ); - self.prepared_statement_cache.as_mut().unwrap().clear(); + cache.clear(); } } self.cleanup_state.reset(); diff --git a/src/stats/address.rs b/src/stats/address.rs index 1d324a167..4829e120a 100644 --- a/src/stats/address.rs +++ b/src/stats/address.rs @@ -429,15 +429,12 @@ impl AddressStats { ); // Calculate average time per transaction (or 0 if no transactions) - if current_xact_count == 0 { - self.averages - .xact_time_microseconds - .store(0, Ordering::Relaxed); - } else { - self.averages - .xact_time_microseconds - .store(current_xact_time / current_xact_count, Ordering::Relaxed); - } + self.averages.xact_time_microseconds.store( + current_xact_time + .checked_div(current_xact_count) + .unwrap_or(0), + Ordering::Relaxed, + ); } /// Helper method to update query-related averages @@ -452,15 +449,12 @@ impl AddressStats { ); // Calculate average time per query (or 0 if no queries) - if current_query_count == 0 { - self.averages - .query_time_microseconds - .store(0, Ordering::Relaxed); - } else { - self.averages - .query_time_microseconds - .store(current_query_time / current_query_count, Ordering::Relaxed); - } + self.averages.query_time_microseconds.store( + current_query_time + .checked_div(current_query_count) + .unwrap_or(0), + Ordering::Relaxed, + ); } /// Helper method to update throughput averages diff --git a/src/stats/pool.rs b/src/stats/pool.rs index 591d94a09..d024800cc 100644 --- a/src/stats/pool.rs +++ b/src/stats/pool.rs @@ -543,10 +543,12 @@ impl PoolStats { .load(Ordering::Relaxed); // Calculate average wait time if there are transactions - if current.avg_xact_count > 0 { - current.avg_wait_time = - address.averages.wait_time.load(Ordering::Relaxed) / current.avg_xact_count; - } + current.avg_wait_time = address + .averages + .wait_time + .load(Ordering::Relaxed) + .checked_div(current.avg_xact_count) + .unwrap_or(0); // Add the pool stats to the virtual map map.insert(identifier.clone(), current); diff --git a/src/utils/debug_messages.rs b/src/utils/debug_messages.rs index a5a961e04..debc0eb1f 100644 --- a/src/utils/debug_messages.rs +++ b/src/utils/debug_messages.rs @@ -289,12 +289,12 @@ impl DebugMessageBuffer { /// Add a message to the buffer, grouping with the last message if identical fn add(&mut self, message: BufferedMessage) { - if let Some(last_group) = self.groups.back_mut() { - if last_group.message == message { - last_group.count += 1; - self.total_count += 1; - return; - } + if let Some(last_group) = self.groups.back_mut() + && last_group.message == message + { + last_group.count += 1; + self.total_count += 1; + return; } self.groups.push_back(MessageGroup { message, count: 1 }); @@ -472,15 +472,15 @@ pub fn log_server_to_client(client_addr: &str, server_pid: i32, buffer: &[u8]) { /// connection is closed or when ReadyForQuery resets the state. pub fn cleanup_protocol_state(client_addr: &str, server_pid: i32) { let states = PROTOCOL_STATES.lock(); - if let Some(state) = states.get(&server_pid) { - if state.has_pending() { - warn!( - "Client {} disconnected with pending protocol state: {} (server [{}])", - client_addr, - state.pending_summary(), - server_pid - ); - } + if let Some(state) = states.get(&server_pid) + && state.has_pending() + { + warn!( + "Client {} disconnected with pending protocol state: {} (server [{}])", + client_addr, + state.pending_summary(), + server_pid + ); } } diff --git a/tests/bdd/doorman_helper.rs b/tests/bdd/doorman_helper.rs index b82486f56..c624367c7 100644 --- a/tests/bdd/doorman_helper.rs +++ b/tests/bdd/doorman_helper.rs @@ -450,12 +450,11 @@ fn is_process_running(pid: u32) -> bool { #[given("pg_doorman started in daemon mode with config:")] pub async fn start_doorman_daemon_with_config(world: &mut DoormanWorld, step: &Step) { // Stop any previously running pg_doorman daemon by reading PID from file - if let Some(ref pid_path) = world.doorman_daemon_pid_file { - if let Ok(pid_content) = std::fs::read_to_string(pid_path) { - if let Ok(pid) = pid_content.trim().parse::() { - stop_doorman_daemon(pid); - } - } + if let Some(ref pid_path) = world.doorman_daemon_pid_file + && let Ok(pid_content) = std::fs::read_to_string(pid_path) + && let Ok(pid) = pid_content.trim().parse::() + { + stop_doorman_daemon(pid); } world.doorman_daemon_pid_file = None; @@ -520,10 +519,10 @@ pub async fn start_doorman_daemon_with_config(world: &mut DoormanWorld, step: &S fn extract_daemon_pid_file(config: &str) -> Option { for line in config.lines() { let line = line.trim(); - if line.starts_with("daemon_pid_file") { - if let Some(value) = line.split('=').nth(1) { - return Some(value.trim().trim_matches('"').to_string()); - } + if line.starts_with("daemon_pid_file") + && let Some(value) = line.split('=').nth(1) + { + return Some(value.trim().trim_matches('"').to_string()); } } None diff --git a/tests/bdd/extended/admin_session.rs b/tests/bdd/extended/admin_session.rs index ac5bd015e..3f1b2baa7 100644 --- a/tests/bdd/extended/admin_session.rs +++ b/tests/bdd/extended/admin_session.rs @@ -183,8 +183,8 @@ pub async fn execute_admin_query_and_store_response( let (msg_type, data) = conn.read_message().await.expect("Failed to read message"); match msg_type { - 'T' => { - if data.len() >= 2 { + 'T' + if data.len() >= 2 => { let field_count = i16::from_be_bytes([data[0], data[1]]) as usize; let mut pos = 2; for _ in 0..field_count { @@ -198,7 +198,6 @@ pub async fn execute_admin_query_and_store_response( response_content.push_str(&headers.join("|")); response_content.push('\n'); } - } 'D' => { let row_values = super::helpers::parse_datarow_fields(&data); if !is_first_row { @@ -215,10 +214,10 @@ pub async fn execute_admin_query_and_store_response( response_content.push_str(&tag); } } - 'A' => { + 'A' // NotificationResponse (Async notification) - this is what show help returns // Format: process_id (4 bytes) + channel (null-terminated) + payload (null-terminated) - if data.len() >= 4 { + if data.len() >= 4 => { let mut pos = 4; // skip process_id // Read channel name if let Some(null_pos) = data[pos..].iter().position(|&b| b == 0) { @@ -234,7 +233,6 @@ pub async fn execute_admin_query_and_store_response( } } } - } 'Z' => { // ReadyForQuery - done break; diff --git a/tests/bdd/main.rs b/tests/bdd/main.rs index ea18b18ed..e48bc4d7d 100644 --- a/tests/bdd/main.rs +++ b/tests/bdd/main.rs @@ -126,12 +126,11 @@ fn main() { // Stop daemon process if running (for daemon mode tests) // Read PID from file to handle binary-upgrade where PID changes - if let Some(ref pid_path) = w.doorman_daemon_pid_file { - if let Ok(pid_content) = std::fs::read_to_string(pid_path) { - if let Ok(pid) = pid_content.trim().parse::() { - doorman_helper::stop_doorman_daemon(pid); - } - } + if let Some(ref pid_path) = w.doorman_daemon_pid_file + && let Ok(pid_content) = std::fs::read_to_string(pid_path) + && let Ok(pid) = pid_content.trim().parse::() + { + doorman_helper::stop_doorman_daemon(pid); } w.doorman_daemon_pid_file = None; diff --git a/tests/bdd/pgbench_helper.rs b/tests/bdd/pgbench_helper.rs index a437ff065..2811b7adc 100644 --- a/tests/bdd/pgbench_helper.rs +++ b/tests/bdd/pgbench_helper.rs @@ -308,10 +308,10 @@ fn parse_progress_tps(output: &str) -> Option { // Find the last comma before tps if let Some(comma_pos) = before_tps.rfind(", ") { let num_str = before_tps[comma_pos + 2..].trim(); - if let Ok(tps) = num_str.parse::() { - if tps > 0.0 { - tps_values.push(tps); - } + if let Ok(tps) = num_str.parse::() + && tps > 0.0 + { + tps_values.push(tps); } } } diff --git a/tests/bdd/pool_bench_helper.rs b/tests/bdd/pool_bench_helper.rs index 83481f7fd..3a7be6982 100644 --- a/tests/bdd/pool_bench_helper.rs +++ b/tests/bdd/pool_bench_helper.rs @@ -284,7 +284,7 @@ async fn benchmark_pool_get_impl( // Sort by sample count let mut frame_times: Vec<(String, usize)> = func_samples.into_iter().collect(); - frame_times.sort_by(|a, b| b.1.cmp(&a.1)); + frame_times.sort_by_key(|f| std::cmp::Reverse(f.1)); println!("Total CPU samples: {}", total_samples); println!("| Function | Samples | % |"); diff --git a/tests/bdd/postgres_helper.rs b/tests/bdd/postgres_helper.rs index 119152980..06e2b7b17 100644 --- a/tests/bdd/postgres_helper.rs +++ b/tests/bdd/postgres_helper.rs @@ -68,11 +68,10 @@ fn stream_log_file(log_path: PathBuf) -> Arc { // Check if file size decreased (rotation) if let (Ok(old_meta), Ok(new_meta)) = (reader.get_ref().metadata(), new_file.metadata()) + && new_meta.len() < old_meta.len() { - if new_meta.len() < old_meta.len() { - reader = BufReader::new(new_file); - continue; - } + reader = BufReader::new(new_file); + continue; } } } @@ -187,11 +186,11 @@ pub async fn start_postgres(world: &mut DoormanWorld) { .stdout(Stdio::null()) .stderr(Stdio::null()) .status(); - if let Ok(s) = check { - if s.success() { - success = true; - break; - } + if let Ok(s) = check + && s.success() + { + success = true; + break; } sleep(Duration::from_millis(500)).await; } @@ -216,10 +215,10 @@ pub async fn start_postgres(world: &mut DoormanWorld) { .stdout(Stdio::null()) .stderr(Stdio::null()) .status(); - if let Ok(s) = check_psql { - if s.success() { - success = true; - } + if let Ok(s) = check_psql + && s.success() + { + success = true; } } assert!(success, "Postgres failed to start"); @@ -376,11 +375,11 @@ async fn start_postgres_internal(world: &mut DoormanWorld, hba_content: &str, ex .stdout(Stdio::null()) .stderr(Stdio::null()) .status(); - if let Ok(s) = check { - if s.success() { - success = true; - break; - } + if let Ok(s) = check + && s.success() + { + success = true; + break; } sleep(Duration::from_millis(500)).await; } @@ -405,10 +404,10 @@ async fn start_postgres_internal(world: &mut DoormanWorld, hba_content: &str, ex ], ) .status(); - if let Ok(s) = check_psql { - if s.success() { - success = true; - } + if let Ok(s) = check_psql + && s.success() + { + success = true; } } assert!(success, "Postgres failed to start"); From ba7a29b881e37f9b2f42fe70a991629d92e4ba91 Mon Sep 17 00:00:00 2001 From: dmitrivasilyev Date: Thu, 30 Apr 2026 13:43:26 +0300 Subject: [PATCH 05/13] =?UTF-8?q?release:=203.7.0=20=E2=80=94=20Rust=201.9?= =?UTF-8?q?5,=20tokio=201.52,=20edition=202024?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wraps up the 3.7.0 toolchain refresh: bumps the project version and adds the changelog entry covering the four preceding commits. No user-visible behavior changes since 3.6.4 — this release packages the rust/tokio/edition upgrade landed in the preceding commits. Operators upgrading from 3.6.x do not need any config or deployment changes. --- Cargo.lock | 2 +- Cargo.toml | 2 +- documentation/en/src/changelog.md | 11 +++++++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eaff820a4..302587be5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1875,7 +1875,7 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pg_doorman" -version = "3.6.3" +version = "3.7.0" dependencies = [ "ahash", "arc-swap", diff --git a/Cargo.toml b/Cargo.toml index 1ccdc43da..db4605991 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pg_doorman" -version = "3.6.3" +version = "3.7.0" edition = "2024" rust-version = "1.95.0" license = "MIT" diff --git a/documentation/en/src/changelog.md b/documentation/en/src/changelog.md index 8b2f4f8a2..03a022da6 100644 --- a/documentation/en/src/changelog.md +++ b/documentation/en/src/changelog.md @@ -1,5 +1,16 @@ # Changelog +### 3.7.0 Apr 30, 2026 + +#### Toolchain refresh — Rust 1.95, tokio 1.52, edition 2024 + +Internal release. The project moves to current Rust toolchain and async-runtime defaults; no user-facing behavior changes. Operators upgrading from 3.6.x do not need any config or deployment changes. + +- **Rust toolchain**: 1.87.0 → 1.95.0. `rust-version` in Cargo.toml bumped accordingly. +- **tokio**: 1.49 → 1.52. +- **Edition 2024**. Source-level adjustments where 2024 changed defaults: `std::env::remove_var` is now wrapped in unsafe blocks (it can race with concurrent reads from other threads), `unsafe fn` bodies open explicit `unsafe { ... }` blocks, redundant `ref` bindings dropped from if-let patterns. +- **Clippy lints surfaced under 1.95 + 2024 are resolved**. Bulk auto-rewrites collapsed nested `if`s into 2024 stable let-chains; manual fixes replaced `is_some + unwrap` with `if let`, manual zero-checked division with `checked_div().unwrap_or(default)`, and `sort_by(reverse-cmp)` with `sort_by_key(Reverse)`. + ### 3.6.4 Apr 29, 2026 #### Fallback resilience From a7a7cf9921cb347ce151c2181bdee9cab5904c2e Mon Sep 17 00:00:00 2001 From: dmitrivasilyev Date: Thu, 30 Apr 2026 13:56:29 +0300 Subject: [PATCH 06/13] chore(ci): bump test-runner Rust toolchain to 1.95.0 Why: the test-runner docker image was pinned to rustc 1.87.0, which made make test-bdd (and the corresponding CI job) fail at the MSRV check after Cargo.toml moved to rust-version 1.95.0 in the preceding commits. The image tag in bdd-tests.yml is derived from a hash of tests/nix/flake.nix + flake.lock, so changing the pin triggers a fresh image build on the next workflow run; subsequent BDD jobs pick up the new rustc automatically. --- tests/nix/flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/nix/flake.nix b/tests/nix/flake.nix index f0511af67..8ba375ac0 100644 --- a/tests/nix/flake.nix +++ b/tests/nix/flake.nix @@ -79,7 +79,7 @@ }; # Rust toolchain from rust-overlay - rustToolchain = pkgs.rust-bin.stable."1.87.0".default.override { + rustToolchain = pkgs.rust-bin.stable."1.95.0".default.override { extensions = [ "rust-src" "rust-analyzer" From 0b58d6fe5db3fe61ed662a893111d79893a6b8a7 Mon Sep 17 00:00:00 2001 From: dmitrivasilyev Date: Thu, 30 Apr 2026 14:16:23 +0300 Subject: [PATCH 07/13] chore(ci): update test-runner flake.lock for rustc 1.95.0 The previous test-runner image bump pinned rust-bin.stable.\"1.95.0\" in flake.nix, but the existing flake.lock snapshot of rust-overlay (2025-12-27) didn't have that attribute yet, so nix build failed in CI with: error: attribute '\"1.95.0\"' missing. Refresh nixpkgs, flake-parts, and rust-overlay locks so the new toolchain is available in the next CI image build. --- tests/nix/flake.lock | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/nix/flake.lock b/tests/nix/flake.lock index 1f3bdef5b..0f150a8ce 100644 --- a/tests/nix/flake.lock +++ b/tests/nix/flake.lock @@ -7,11 +7,11 @@ ] }, "locked": { - "lastModified": 1765835352, - "narHash": "sha256-XswHlK/Qtjasvhd1nOa1e8MgZ8GS//jBoTqWtrS1Giw=", + "lastModified": 1775087534, + "narHash": "sha256-91qqW8lhL7TLwgQWijoGBbiD4t7/q75KTi8NxjVmSmA=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "a34fae9c08a15ad73f295041fec82323541400a9", + "rev": "3107b77cd68437b9a76194f0f7f9c55f2329ca5b", "type": "github" }, "original": { @@ -22,11 +22,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1766840161, - "narHash": "sha256-Ss/LHpJJsng8vz1Pe33RSGIWUOcqM1fjrehjUkdrWio=", + "lastModified": 1777395829, + "narHash": "sha256-HposVFZcsBCevgqLR73w/BpSe8J1lMgw5kASnnxO3A4=", "owner": "nixos", "repo": "nixpkgs", - "rev": "3edc4a30ed3903fdf6f90c837f961fa6b49582d1", + "rev": "e75f25705c2934955ee5075e62530d74aca973c6", "type": "github" }, "original": { @@ -50,11 +50,11 @@ ] }, "locked": { - "lastModified": 1766803264, - "narHash": "sha256-eGK6He8BR6L7N73kyyjz/vGxZX1Usnr8Gwfs3D18KgE=", + "lastModified": 1777519017, + "narHash": "sha256-TXilQ8MwFFtZs7HSogSI/LJzAS63nicE8iF63iB93WM=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "6b5c52313aaf3f3e1a0a6757bb89846edfb5195c", + "rev": "09b556f18dacc39e97a46e0a1cba47af7b3af1d8", "type": "github" }, "original": { From 8156f4383fea89a2831a86369c179a5f7118738f Mon Sep 17 00:00:00 2001 From: dmitrivasilyev Date: Thu, 30 Apr 2026 14:26:22 +0300 Subject: [PATCH 08/13] chore(ci): bump packaging, Dockerfile, bench, and nix to 1.95.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 3.7.0 toolchain bump pinned rust-toolchain.toml and Cargo.toml to 1.95.0, but several other places still hard-coded 1.87.0 and broke their CI jobs: - copr-publish, launchpad-publish — RUST_VERSION env. - build-packages — wget URL for rust-1.87.0 in the debian and rpm matrix, plus the rust:1.87.0-alpine container for the static musl build. - pkg/rpm/pg-doorman.spec — %global rust_version. - Dockerfile — base image rust:1.87.0-slim-bookworm. - benches/setup-and-run-bench.sh — rustup --default-toolchain. - documentation/{en,ru}/src/tutorials/contributing.md — the "Rust 1.87.0" line under the prerequisites list. Also fixes the nix test-runner image against the 2026-04-28 nixpkgs snapshot, which removed two attributes used by the flake: - nodePackages was deprecated; npm now ships bundled with nodejs_22, so the standalone nodePackages.npm entry is dropped. - go_1_24 was dropped; switch to go_1_25 (still satisfies tests/go/go.mod's go 1.24.0 minimum). --- .github/workflows/build-packages.yaml | 6 +++--- .github/workflows/copr-publish.yaml | 2 +- .github/workflows/launchpad-publish.yaml | 2 +- Dockerfile | 2 +- benches/setup-and-run-bench.sh | 4 ++-- documentation/en/src/tutorials/contributing.md | 2 +- documentation/ru/src/tutorials/contributing.md | 2 +- pkg/rpm/pg-doorman.spec | 2 +- tests/nix/flake.nix | 9 +++++---- 9 files changed, 16 insertions(+), 15 deletions(-) diff --git a/.github/workflows/build-packages.yaml b/.github/workflows/build-packages.yaml index c199a96e6..b50b573cb 100644 --- a/.github/workflows/build-packages.yaml +++ b/.github/workflows/build-packages.yaml @@ -75,7 +75,7 @@ jobs: - name: Install packages run: | apt-get update && apt-get install git cmake wget build-essential pkg-config libssl-dev clang g++ perl patch -y --option=Dpkg::Options::=--force-confdef && - wget -q https://static.rust-lang.org/dist/rust-1.87.0-x86_64-unknown-linux-gnu.tar.gz -O /tmp/rust.tar.gz && + wget -q https://static.rust-lang.org/dist/rust-1.95.0-x86_64-unknown-linux-gnu.tar.gz -O /tmp/rust.tar.gz && cd /tmp && tar xf rust.tar.gz && ./rust-*-x86_64-unknown-linux-gnu/install.sh - name: Build binaries in release mode run: JEMALLOC_SYS_WITH_MALLOC_CONF="dirty_decay_ms:30000,muzzy_decay_ms:30000,background_thread:true,metadata_thp:auto" cargo build --release @@ -119,7 +119,7 @@ jobs: - name: Install packages run: | yum install -y @"Development Tools" openssl-devel wget perl patch && - wget -q https://static.rust-lang.org/dist/rust-1.87.0-x86_64-unknown-linux-gnu.tar.gz -O /tmp/rust.tar.gz && + wget -q https://static.rust-lang.org/dist/rust-1.95.0-x86_64-unknown-linux-gnu.tar.gz -O /tmp/rust.tar.gz && cd /tmp && tar xf rust.tar.gz && ./rust-*-x86_64-unknown-linux-gnu/install.sh - name: Build binaries in release mode run: JEMALLOC_SYS_WITH_MALLOC_CONF="dirty_decay_ms:30000,muzzy_decay_ms:30000,background_thread:true,metadata_thp:auto" cargo build --release @@ -151,7 +151,7 @@ jobs: needs: [verify-version] runs-on: ubuntu-latest container: - image: rust:1.87.0-alpine + image: rust:1.95.0-alpine env: JEMALLOC_SYS_WITH_MALLOC_CONF: "dirty_decay_ms:30000,muzzy_decay_ms:30000,background_thread:true,metadata_thp:auto" OPENSSL_STATIC: "1" diff --git a/.github/workflows/copr-publish.yaml b/.github/workflows/copr-publish.yaml index bbc9b53d7..68aabcba2 100644 --- a/.github/workflows/copr-publish.yaml +++ b/.github/workflows/copr-publish.yaml @@ -14,7 +14,7 @@ env: # EPEL versions (RHEL/CentOS/Rocky/Alma) EPEL_VERSIONS: "8 9" COPR_PROJECT: "pg-doorman" - RUST_VERSION: "1.87.0" + RUST_VERSION: "1.95.0" jobs: verify-version: diff --git a/.github/workflows/launchpad-publish.yaml b/.github/workflows/launchpad-publish.yaml index b633054f2..9433bbf96 100644 --- a/.github/workflows/launchpad-publish.yaml +++ b/.github/workflows/launchpad-publish.yaml @@ -16,7 +16,7 @@ env: # questing = 25.10 # resolute = 26.04 LTS PPA_NAME: "ppa:vadv/pg-doorman" - RUST_VERSION: "1.87.0" + RUST_VERSION: "1.95.0" # Rebuild number for Launchpad PPA uploads # Increment this when you need to re-upload the same version # (e.g., after fixing packaging issues without changing the actual version) diff --git a/Dockerfile b/Dockerfile index f096aef8b..3f273cfa3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM rust:1.87.0-slim-bookworm AS builder +FROM rust:1.95.0-slim-bookworm AS builder RUN apt-get update && \ apt-get install -y build-essential pkg-config libssl-dev perl diff --git a/benches/setup-and-run-bench.sh b/benches/setup-and-run-bench.sh index fb6575ad7..550641902 100755 --- a/benches/setup-and-run-bench.sh +++ b/benches/setup-and-run-bench.sh @@ -93,9 +93,9 @@ install_rust() { log "Rust already present: $(cargo --version)" return fi - log "Installing rustup 1.87.0" + log "Installing rustup 1.95.0" curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ - | sh -s -- -y --default-toolchain 1.87.0 --profile minimal + | sh -s -- -y --default-toolchain 1.95.0 --profile minimal # shellcheck disable=SC1091 source "$HOME/.cargo/env" } diff --git a/documentation/en/src/tutorials/contributing.md b/documentation/en/src/tutorials/contributing.md index bb7d148fb..63263a16d 100644 --- a/documentation/en/src/tutorials/contributing.md +++ b/documentation/en/src/tutorials/contributing.md @@ -68,7 +68,7 @@ The test Docker image (built with Nix) includes: - Python 3 with asyncpg, psycopg2, aiopg, pytest - Node.js 22 - .NET SDK 8 -- Rust 1.87.0 +- Rust 1.95.0 ### Running Tests diff --git a/documentation/ru/src/tutorials/contributing.md b/documentation/ru/src/tutorials/contributing.md index 1eff2a957..dfe828809 100644 --- a/documentation/ru/src/tutorials/contributing.md +++ b/documentation/ru/src/tutorials/contributing.md @@ -68,7 +68,7 @@ PgDoorman использует BDD-тесты (Behavior-Driven Development) с - Python 3 с asyncpg, psycopg2, aiopg, pytest - Node.js 22 - .NET SDK 8 -- Rust 1.87.0 +- Rust 1.95.0 ### Запуск тестов diff --git a/pkg/rpm/pg-doorman.spec b/pkg/rpm/pg-doorman.spec index f4608e5bd..0defd36a9 100644 --- a/pkg/rpm/pg-doorman.spec +++ b/pkg/rpm/pg-doorman.spec @@ -1,5 +1,5 @@ %global debug_package %{nil} -%global rust_version 1.87.0 +%global rust_version 1.95.0 Name: pg-doorman Version: 3.0.0 diff --git a/tests/nix/flake.nix b/tests/nix/flake.nix index 8ba375ac0..4bd6a291e 100644 --- a/tests/nix/flake.nix +++ b/tests/nix/flake.nix @@ -103,12 +103,13 @@ odyssey pgbouncer - # Node.js + # Node.js (npm is bundled with nodejs_22 since the 2026-04 nixpkgs bump + # which removed the nodePackages attribute set) nodejs_22 - nodePackages.npm - # Go - go_1_24 + # Go (1.24.0 minimum required by tests/go/go.mod; 2026-04 nixpkgs + # dropped go_1_24, so use the next available stable) + go_1_25 # Python environment pythonEnv From ad9516eebe9650e2cfecf499b952414d3e282001 Mon Sep 17 00:00:00 2001 From: dmitrivasilyev Date: Thu, 30 Apr 2026 14:56:12 +0300 Subject: [PATCH 09/13] chore: adopt Rust 1.91 stdlib helpers in a few production paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Now that MSRV is 1.95.0, replace a few verbose stdlib idioms with their newer equivalents where it improves readability: - Duration::from_secs(60) → Duration::from_mins(1) for the JWT TTL in server authentication (was from_secs(120) → from_mins(2)) and the default server idle-check timeout — minutes are the natural unit for these knobs. - Manual is_char_boundary backwards walk in patroni::client:: truncate_str collapses to str::floor_char_boundary, dropping the explicit loop. - Three patroni_proxy port-test backend mocks switched to from_mins(1) for the keep-open sleep, consistent with the config-default change. Other Duration::from_secs sites kept as-is on purpose: their surrounding comments compare values in seconds (e.g. "60s vs loop_wait of 10s"), and converting to minutes would split the comment from the code unit. --- src/bin/patroni_proxy/port.rs | 6 +++--- src/config/general.rs | 2 +- src/patroni/client.rs | 6 +----- src/server/authentication.rs | 2 +- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/bin/patroni_proxy/port.rs b/src/bin/patroni_proxy/port.rs index 9c8a22ca6..3ad896c04 100644 --- a/src/bin/patroni_proxy/port.rs +++ b/src/bin/patroni_proxy/port.rs @@ -685,7 +685,7 @@ mod tests { while let Ok((mut stream, _)) = backend_listener.accept().await { // Keep connection open until it's closed tokio::spawn(async move { - tokio::time::sleep(Duration::from_secs(60)).await; + tokio::time::sleep(Duration::from_mins(1)).await; let _ = stream.shutdown().await; }); } @@ -775,7 +775,7 @@ mod tests { let backend1_task = tokio::spawn(async move { while let Ok((mut stream, _)) = backend1_listener.accept().await { tokio::spawn(async move { - tokio::time::sleep(Duration::from_secs(60)).await; + tokio::time::sleep(Duration::from_mins(1)).await; let _ = stream.shutdown().await; }); } @@ -784,7 +784,7 @@ mod tests { let backend2_task = tokio::spawn(async move { while let Ok((mut stream, _)) = backend2_listener.accept().await { tokio::spawn(async move { - tokio::time::sleep(Duration::from_secs(60)).await; + tokio::time::sleep(Duration::from_mins(1)).await; let _ = stream.shutdown().await; }); } diff --git a/src/config/general.rs b/src/config/general.rs index 204375178..e980faf63 100644 --- a/src/config/general.rs +++ b/src/config/general.rs @@ -262,7 +262,7 @@ impl General { } pub fn default_server_idle_check_timeout() -> Duration { - Duration::from_secs(60) // 60 seconds + Duration::from_mins(1) } pub fn default_connect_timeout() -> Duration { diff --git a/src/patroni/client.rs b/src/patroni/client.rs index 6db19abdb..608b9839a 100644 --- a/src/patroni/client.rs +++ b/src/patroni/client.rs @@ -10,11 +10,7 @@ fn truncate_str(s: &str, max_bytes: usize) -> &str { if s.len() <= max_bytes { return s; } - let mut end = max_bytes; - while end > 0 && !s.is_char_boundary(end) { - end -= 1; - } - &s[..end] + &s[..s.floor_char_boundary(max_bytes)] } /// Errors from the Patroni REST API client. diff --git a/src/server/authentication.rs b/src/server/authentication.rs index 59139118b..3fa71e600 100644 --- a/src/server/authentication.rs +++ b/src/server/authentication.rs @@ -129,7 +129,7 @@ pub(crate) async fn handle_authentication( } // Generate JWT token - let claims = new_claims(server_username, std::time::Duration::from_secs(120)); + let claims = new_claims(server_username, std::time::Duration::from_mins(2)); let token = sign_with_jwt_priv_key( claims, server_password From abc2b8c604735663e58f532953a090ffb15d0e4d Mon Sep 17 00:00:00 2001 From: dmitrivasilyev Date: Thu, 30 Apr 2026 15:13:55 +0300 Subject: [PATCH 10/13] fix(tls-migration): unsafe extern block for edition 2024 Edition 2024 requires extern blocks to be marked unsafe. The TLS migration FFI block in client::migration was guarded by the tls-migration feature, so the cargo check during the edition 2024 migration commit (without the feature) didn't surface it; CI's TLS Migration BDD job rebuilds with the feature on and failed with: error: extern blocks must be unsafe. Mark the block as unsafe extern "C". The function bodies inside were already wrapped in their own unsafe { ... } blocks at the call sites, so no other change is needed. --- src/client/migration.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/migration.rs b/src/client/migration.rs index 1c012a078..937db1fe6 100644 --- a/src/client/migration.rs +++ b/src/client/migration.rs @@ -60,7 +60,7 @@ const MAX_RECV_BUF: usize = 64 * 1024; // Only available with the tls-migration feature (vendored patched OpenSSL). #[cfg(feature = "tls-migration")] #[allow(dead_code)] -extern "C" { +unsafe extern "C" { fn SSL_export_migration_state(ssl: *mut c_void, out: *mut *mut u8, out_len: *mut usize) -> i32; fn SSL_import_migration_state( From d0748075bc6544192d89db36d1d05622798d33fb Mon Sep 17 00:00:00 2001 From: dmitrivasilyev Date: Thu, 30 Apr 2026 15:22:18 +0300 Subject: [PATCH 11/13] fix: remove unwrap from protocol/auth hot paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five production sites called unwrap on values that depend on external input or earlier guards. Each becomes a typed error or a let-Some pattern, so a malformed PG packet or a config mismatch surfaces as a regular Error instead of a panic. - ParameterStatus parsing in server::protocol_io and server::server_backend now propagates the parse error from read_string with ? — the function signature for handle_parameter_status grew a Result so the caller can act on it; a malformed packet from the backend cleanly tears down that connection instead of aborting the worker. - Static-MD5 verification in auth::mod (passthrough_md5_verify) binds the stripped hash via let-Some on the MD5_PASSWORD_PREFIX prefix — the previous unwrap relied on the caller having already done starts_with, which made the call site fragile to refactors. - The auth_query MD5 path is rewritten as let-Some chains so the prefix strip can never panic, even if the cache returns an unexpected entry. - server::authentication's clear-password branch collapses the is_none guard plus two unwrap+clone pairs into a single let-Some pattern, and the JWT key strip is also let-Some'd — no behavior change beyond the panic being a typed error. --- src/auth/mod.rs | 16 ++++++++-------- src/server/authentication.rs | 30 +++++++++++++----------------- src/server/protocol_io.rs | 9 +++++---- src/server/server_backend.rs | 4 ++-- 4 files changed, 28 insertions(+), 31 deletions(-) diff --git a/src/auth/mod.rs b/src/auth/mod.rs index 48c90a1e5..43c0af581 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -527,7 +527,10 @@ where // md5 auth. let salt = md5_challenge(write).await?; let password_response = read_password(read).await?; - let except_md5_hash = md5_hash_second_pass(pool_password.strip_prefix("md5").unwrap(), &salt); + let pool_hash = pool_password + .strip_prefix(MD5_PASSWORD_PREFIX) + .ok_or_else(|| Error::AuthError("static MD5 entry missing 'md5' prefix".into()))?; + let except_md5_hash = md5_hash_second_pass(pool_hash, &salt); if except_md5_hash != password_response { error!( "[{username_from_parameters}@{}] MD5 authentication failed from {client_addr}", @@ -727,23 +730,20 @@ where if hba_decision == CheckResult::Trust { // HBA trust — skip password check - } else if pool_password.starts_with(MD5_PASSWORD_PREFIX) { + } else if let Some(pool_hash) = pool_password.strip_prefix(MD5_PASSWORD_PREFIX) { // MD5 challenge-response let salt = md5_challenge(write).await?; let password_response = read_password(read).await?; - let expected = md5_hash_second_pass(pool_password.strip_prefix("md5").unwrap(), &salt); + let expected = md5_hash_second_pass(pool_hash, &salt); if expected != password_response { // Password mismatch — try re-fetch (password may have changed in PG) let mut auth_ok = false; if let Ok(Some(new_entry)) = cache.refetch_on_failure(username).await && new_entry.password_hash != *pool_password - && new_entry.password_hash.starts_with(MD5_PASSWORD_PREFIX) + && let Some(new_hash) = new_entry.password_hash.strip_prefix(MD5_PASSWORD_PREFIX) { - let new_expected = md5_hash_second_pass( - new_entry.password_hash.strip_prefix("md5").unwrap(), - &salt, - ); + let new_expected = md5_hash_second_pass(new_hash, &salt); if new_expected == password_response { auth_ok = true; info!("[{username}@{pool_name}] auth_query: re-fetched password matched"); diff --git a/src/server/authentication.rs b/src/server/authentication.rs index 3fa71e600..2ecbbd8f8 100644 --- a/src/server/authentication.rs +++ b/src/server/authentication.rs @@ -107,7 +107,9 @@ pub(crate) async fn handle_authentication( // Clear password authentication AUTHENTICATION_CLEAR_PASSWORD => { - if user.server_username.is_none() || user.server_password.is_none() { + let (Some(server_username), Some(server_password)) = + (user.server_username.as_ref(), user.server_password.as_ref()) + else { error!( "[{}@{}] clear password authentication requested by server but not configured", server_identifier.username, server_identifier.pool_name, @@ -116,29 +118,23 @@ pub(crate) async fn handle_authentication( "server wants clear password authentication, but auth for this server is not configured".into(), server_identifier.clone(), )); - } - - let server_password = user.server_password.as_ref().unwrap().clone(); - let server_username = user.server_username.as_ref().unwrap().clone(); + }; - if !server_password.starts_with(JWT_PRIV_KEY_PASSWORD_PREFIX) { + let Some(jwt_priv_key) = server_password.strip_prefix(JWT_PRIV_KEY_PASSWORD_PREFIX) + else { return Err(Error::ServerAuthError( "plain password is not supported".into(), server_identifier.clone(), )); - } + }; // Generate JWT token - let claims = new_claims(server_username, std::time::Duration::from_mins(2)); - let token = sign_with_jwt_priv_key( - claims, - server_password - .strip_prefix(JWT_PRIV_KEY_PASSWORD_PREFIX) - .unwrap() - .to_string(), - ) - .await - .map_err(|err| Error::ServerAuthError(err.to_string(), server_identifier.clone()))?; + let claims = new_claims(server_username.clone(), std::time::Duration::from_mins(2)); + let token = sign_with_jwt_priv_key(claims, jwt_priv_key.to_string()) + .await + .map_err(|err| { + Error::ServerAuthError(err.to_string(), server_identifier.clone()) + })?; let mut password_response = BytesMut::new(); password_response.put_u8(b'p'); diff --git a/src/server/protocol_io.rs b/src/server/protocol_io.rs index f8cb4568a..3d68317d7 100644 --- a/src/server/protocol_io.rs +++ b/src/server/protocol_io.rs @@ -445,9 +445,9 @@ fn handle_parameter_status( server: &mut Server, message: &mut BytesMut, client_server_parameters: &mut Option<&mut ServerParameters>, -) { - let key = message.read_string().unwrap(); - let value = message.read_string().unwrap(); +) -> Result<(), Error> { + let key = message.read_string()?; + let value = message.read_string()?; // Update client parameters if tracking is enabled if let Some(client_server_parameters) = client_server_parameters.as_mut() { @@ -464,6 +464,7 @@ fn handle_parameter_status( // Always update server parameters server.server_parameters.set_param(key, value, false); + Ok(()) } /// Receive data from the server in response to a client request. @@ -617,7 +618,7 @@ where // ParameterStatus - server parameter changed 'S' => { - handle_parameter_status(server, &mut message, &mut client_server_parameters); + handle_parameter_status(server, &mut message, &mut client_server_parameters)?; } // DataRow diff --git a/src/server/server_backend.rs b/src/server/server_backend.rs index d03b2e669..fc630e47d 100644 --- a/src/server/server_backend.rs +++ b/src/server/server_backend.rs @@ -914,8 +914,8 @@ impl Server { let mut bytes = read_message_data(&mut stream, code as u8, len).await?; let _ = bytes.get_u8(); let _ = bytes.get_i32(); - let key = bytes.read_string().unwrap(); - let value = bytes.read_string().unwrap(); + let key = bytes.read_string()?; + let value = bytes.read_string()?; // Save the parameter so we can pass it to the client later. server_parameters.set_param(key, value, true); From 4bcf952c2b77cfdacf5dc07839911abc693f476b Mon Sep 17 00:00:00 2001 From: dmitrivasilyev Date: Thu, 30 Apr 2026 15:23:28 +0300 Subject: [PATCH 12/13] perf(messages): drop intermediate String allocations in builders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five message builders allocated a String only to copy its bytes into a BytesMut/Vec and discard it. Each now writes directly to the destination buffer: - md5_hash_second_pass: write! into a presized Vec instead of format!(...).chars().map(as u8).collect — also drops the UTF-8 decode/re-encode round-trip on what is already ASCII-only hex. - error_message and wrong_password: put_slice the literal/borrowed bytes plus a put_u8(0) for the NUL, instead of format!("{x}\\0") → as_bytes. - row_description: same pattern for the per-column name field. These run on every error reply, every md5 auth handshake, and once per row in administrative SELECTs. Behavior is byte-for-byte identical (verified by visual diff of the bytes pushed). --- src/messages/protocol.rs | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/messages/protocol.rs b/src/messages/protocol.rs index 00102e339..2ef7aeeb9 100644 --- a/src/messages/protocol.rs +++ b/src/messages/protocol.rs @@ -1,5 +1,6 @@ // Standard library imports use std::collections::HashMap; +use std::io::Write; use std::mem; // External crate imports use crate::messages::constants::SCRAM_SHA_256; @@ -295,10 +296,10 @@ pub fn md5_hash_second_pass(hash: &str, salt: &[u8]) -> Vec { md5.update(hash); md5.update(salt); - let mut password = format!("md5{:x}", md5.finalize()) - .chars() - .map(|x| x as u8) - .collect::>(); + // 3 ("md5") + 32 (hex SHA-128 of MD5 output) + 1 (NUL terminator) = 36 bytes, + // exact size — no realloc. + let mut password = Vec::with_capacity(36); + write!(&mut password, "md5{:x}", md5.finalize()).expect("write to Vec is infallible"); password.push(0); password @@ -360,11 +361,13 @@ pub fn error_message(message: &str, code: &str) -> BytesMut { // Error code: not sure how much this matters. error.put_u8(b'C'); - error.put_slice(format!("{code}\0").as_bytes()); + error.put_slice(code.as_bytes()); + error.put_u8(0); // The short error message. error.put_u8(b'M'); - error.put_slice(format!("{message}\0").as_bytes()); + error.put_slice(message.as_bytes()); + error.put_u8(0); // No more fields follow. error.put_u8(0); @@ -410,7 +413,9 @@ where // The short error message. error.put_u8(b'M'); - error.put_slice(format!("password authentication failed for user \"{user}\"\0").as_bytes()); + error.put_slice(b"password authentication failed for user \""); + error.put_slice(user.as_bytes()); + error.put_slice(b"\"\0"); // No more fields follow. error.put_u8(0); @@ -436,7 +441,8 @@ pub fn row_description(columns: &Vec<(&str, DataType)>) -> BytesMut { for (name, data_type) in columns { // Column name - row_desc.put_slice(format!("{name}\0").as_bytes()); + row_desc.put_slice(name.as_bytes()); + row_desc.put_u8(0); // Doesn't belong to any table row_desc.put_i32(0); From 14b1a3c73985fd640a75b6655e0e8e494efbb18d Mon Sep 17 00:00:00 2001 From: dmitrivasilyev Date: Thu, 30 Apr 2026 15:24:47 +0300 Subject: [PATCH 13/13] api(messages): accept slices instead of &Vec in row builders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Public message helpers row_description and data_row_nullable used to take &Vec; the slice form &[T] is the idiomatic Rust API for read-only access and lets callers pass array literals directly, without an intermediate Vec allocation. All call sites in the admin module simplified accordingly: &vec![("name", DataType::Text)] → &[("name", DataType::Text)], which clippy::useless_vec now also enforces. --- src/admin/commands.rs | 4 ++-- src/admin/mod.rs | 8 ++++---- src/admin/show.rs | 6 +++--- src/messages/protocol.rs | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/admin/commands.rs b/src/admin/commands.rs index 0163c24c9..94588b584 100644 --- a/src/admin/commands.rs +++ b/src/admin/commands.rs @@ -42,7 +42,7 @@ where { let mut res = BytesMut::new(); - res.put(row_description(&vec![("success", DataType::Text)])); + res.put(row_description(&[("success", DataType::Text)])); let mut shutdown_success = "t"; @@ -71,7 +71,7 @@ where { let mut res = BytesMut::new(); - res.put(row_description(&vec![("success", DataType::Text)])); + res.put(row_description(&[("success", DataType::Text)])); let mut upgrade_success = "t"; diff --git a/src/admin/mod.rs b/src/admin/mod.rs index b75ac7d15..cdd12446f 100644 --- a/src/admin/mod.rs +++ b/src/admin/mod.rs @@ -174,21 +174,21 @@ where if query_lower.contains("unnest(enumvals)") { // SET log_level = — return enum values for the parameter - res.put(row_description(&vec![("val", DataType::Text)])); + res.put(row_description(&[("val", DataType::Text)])); for val in &["error", "warn", "info", "debug", "trace", "off", "default"] { res.put(data_row(&[val.to_string()])); } } else if query_lower.contains("vartype") { // Type lookup — psql checks if parameter is enum/bool/string - res.put(row_description(&vec![("vartype", DataType::Text)])); + res.put(row_description(&[("vartype", DataType::Text)])); res.put(data_row(&["enum".to_string()])); } else if query_lower.contains("context") { // SET — return settable parameters (filtered by context) - res.put(row_description(&vec![("name", DataType::Text)])); + res.put(row_description(&[("name", DataType::Text)])); res.put(data_row(&["log_level".to_string()])); } else { // SHOW — return all SHOW subcommands from the canonical list - res.put(row_description(&vec![("name", DataType::Text)])); + res.put(row_description(&[("name", DataType::Text)])); for name in SHOW_SUBCOMMANDS { res.put(data_row(&[name.to_string()])); } diff --git a/src/admin/show.rs b/src/admin/show.rs index a81a26e62..39957c354 100644 --- a/src/admin/show.rs +++ b/src/admin/show.rs @@ -91,7 +91,7 @@ where T: tokio::io::AsyncWrite + std::marker::Unpin, { let mut res = BytesMut::new(); - res.put(row_description(&vec![("version", DataType::Text)])); + res.put(row_description(&[("version", DataType::Text)])); res.put(data_row(&[format!("PgDoorman {}", VERSION)])); res.put(command_complete("SHOW")); res.put_u8(b'Z'); @@ -106,7 +106,7 @@ where T: tokio::io::AsyncWrite + std::marker::Unpin, { let mut res = BytesMut::new(); - res.put(row_description(&vec![("log_level", DataType::Text)])); + res.put(row_description(&[("log_level", DataType::Text)])); res.put(data_row(&[log_level::get_log_level()])); res.put(command_complete("SHOW")); res.put_u8(b'Z'); @@ -508,7 +508,7 @@ where T: tokio::io::AsyncWrite + std::marker::Unpin, { let mut res = BytesMut::new(); - res.put(row_description(&vec![ + res.put(row_description(&[ ("name", DataType::Text), ("pool_mode", DataType::Text), ])); diff --git a/src/messages/protocol.rs b/src/messages/protocol.rs index 2ef7aeeb9..78537204b 100644 --- a/src/messages/protocol.rs +++ b/src/messages/protocol.rs @@ -432,7 +432,7 @@ where } /// Create a row description message. -pub fn row_description(columns: &Vec<(&str, DataType)>) -> BytesMut { +pub fn row_description(columns: &[(&str, DataType)]) -> BytesMut { let mut res = BytesMut::new(); let mut row_desc = BytesMut::new(); @@ -503,7 +503,7 @@ pub fn data_row>(row: &[S]) -> BytesMut { } /// Create a data row message with nullable values. -pub fn data_row_nullable(row: &Vec>) -> BytesMut { +pub fn data_row_nullable(row: &[Option]) -> BytesMut { let mut res = BytesMut::new(); let mut data_row = BytesMut::new();