From 7dd96141e3088615313f26b424dcabe0c411564f Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Sun, 26 Mar 2023 00:33:05 -0700 Subject: [PATCH 01/79] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 28433def..4d6f599d 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ PostgreSQL pooler and proxy (like PgBouncer) with support for sharding, load bal | Live configuration reloading | **Stable** | Identical to PgBouncer; all settings can be reloaded dynamically (except `host` and `port`). | | Sharding using extended SQL syntax | **Experimental** | Clients can dynamically configure the pooler to route queries to specific shards. | | Sharding using comments parsing/Regex | **Experimental** | Clients can include shard information (sharding key, shard ID) in the query comments. | -| Automatic sharding | **Experimental** | PgCat can parse queries detect sharding keys automatically, and route queries to the route shard. | +| Automatic sharding | **Experimental** | PgCat can parse queries, detect sharding keys automatically, and route queries to the correct shard. | | Mirroring | **Experimental** | Mirror queries between multiple databases in order to test servers with realistic production traffic. | @@ -79,7 +79,7 @@ pgbench -t 1000 -p 6432 -h 127.0.0.1 --protocol extended See [sharding README](./tests/sharding/README.md) for sharding logic testing. -Additionally, all features are tested with Ruby, Python, and Rust tests unit and integration tests. +Additionally, all features are tested with Ruby, Python, and Rust unit and integration tests. Run `cargo test` to run Rust unit tests. From 863104aadd03d0b856ca0ede0fdd36f1b1e50676 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Mar 2023 09:12:43 -0700 Subject: [PATCH 02/79] chore(deps): bump regex from 1.7.1 to 1.7.3 (#385) Bumps [regex](https://github.com/rust-lang/regex) from 1.7.1 to 1.7.3. - [Release notes](https://github.com/rust-lang/regex/releases) - [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-lang/regex/compare/1.7.1...1.7.3) --- updated-dependencies: - dependency-name: regex dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9124996f..367aa1ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -871,9 +871,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.7.1" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" +checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d" dependencies = [ "aho-corasick", "memchr", @@ -882,9 +882,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.28" +version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "ring" From 714e043ef0d5b817ecc4f1c2e01b300a925492d5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Mar 2023 09:12:54 -0700 Subject: [PATCH 03/79] chore(deps): bump async-trait from 0.1.66 to 0.1.68 (#382) Bumps [async-trait](https://github.com/dtolnay/async-trait) from 0.1.66 to 0.1.68. - [Release notes](https://github.com/dtolnay/async-trait/releases) - [Commits](https://github.com/dtolnay/async-trait/compare/0.1.66...0.1.68) --- updated-dependencies: - dependency-name: async-trait dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 41 ++++++++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 367aa1ed..0685e156 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,13 +28,13 @@ checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" [[package]] name = "async-trait" -version = "0.1.66" +version = "0.1.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b84f9ebcc6c1f5b8cb160f6990096a5c127f423fcb6e1ccc46c370cbdfb75dfc" +checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.9", ] [[package]] @@ -175,7 +175,7 @@ dependencies = [ "proc-macro2", "quote", "scratch", - "syn", + "syn 1.0.109", ] [[package]] @@ -192,7 +192,7 @@ checksum = "086c685979a698443656e5cf7856c95c642295a38599f12fb1ff76fb28d19892" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -308,7 +308,7 @@ checksum = "95a73af87da33b5acf53acfebdc339fe592ecf5357ac7c0a7734ab9d8c876a70" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -782,7 +782,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -814,18 +814,18 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.51" +version = "1.0.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" +checksum = "ba466839c78239c09faf015484e5cc04860f88242cff4d03eb038f04b4699b73" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.23" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" dependencies = [ "proc-macro2", ] @@ -972,7 +972,7 @@ checksum = "4fc80d722935453bcafdc2c9a73cd6fac4dc1938f0346035d84bf99fa9e33217" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -1094,6 +1094,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "2.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0da4a3c17e109f700685ec577c0f85efd9b19bcf15c913985f14dc1ac01775aa" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "termcolor" version = "1.2.0" @@ -1157,7 +1168,7 @@ checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -1339,7 +1350,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 1.0.109", "wasm-bindgen-shared", ] @@ -1361,7 +1372,7 @@ checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", "wasm-bindgen-backend", "wasm-bindgen-shared", ] From 434b0bb69e3bd11f49a8b888a2d3120ecc9ac879 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Mar 2023 09:13:13 -0700 Subject: [PATCH 04/79] chore(deps): bump serde from 1.0.154 to 1.0.158 (#376) Bumps [serde](https://github.com/serde-rs/serde) from 1.0.154 to 1.0.158. - [Release notes](https://github.com/serde-rs/serde/releases) - [Commits](https://github.com/serde-rs/serde/compare/v1.0.154...v1.0.158) --- updated-dependencies: - dependency-name: serde dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0685e156..df22b520 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -960,9 +960,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.154" +version = "1.0.158" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cdd151213925e7f1ab45a9bbfb129316bd00799784b174b7cc7bcd16961c49e" +checksum = "771d4d9c4163ee138805e12c710dd365e4f44be8be0503cb1bb9eb989425d9c9" [[package]] name = "serde_derive" From 99f790cacfe10f04482d828e1464046ab966a9e9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Mar 2023 09:13:25 -0700 Subject: [PATCH 05/79] chore(deps): bump toml from 0.7.2 to 0.7.3 (#360) Bumps [toml](https://github.com/toml-rs/toml) from 0.7.2 to 0.7.3. - [Release notes](https://github.com/toml-rs/toml/releases) - [Commits](https://github.com/toml-rs/toml/compare/toml-v0.7.2...toml-v0.7.3) --- updated-dependencies: - dependency-name: toml dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index df22b520..59fd21ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1198,9 +1198,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7afcae9e3f0fe2c370fd4657108972cbb2fa9db1b9f84849cefd80741b01cb6" +checksum = "b403acf6f2bb0859c93c7f0d967cb4a75a7ac552100f9322faf64dc047669b21" dependencies = [ "serde", "serde_spanned", @@ -1219,9 +1219,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.19.4" +version = "0.19.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a1eb0622d28f4b9c90adc4ea4b2b46b47663fde9ac5fafcb14a1369d5508825" +checksum = "08de71aa0d6e348f070457f85af8bd566e2bc452156a423ddf22861b3a953fae" dependencies = [ "indexmap", "serde", From e7e71187254a920c47a2f72fd95441c12492dd91 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Mar 2023 09:13:36 -0700 Subject: [PATCH 06/79] chore(deps): bump hyper from 0.14.24 to 0.14.25 (#358) Bumps [hyper](https://github.com/hyperium/hyper) from 0.14.24 to 0.14.25. - [Release notes](https://github.com/hyperium/hyper/releases) - [Changelog](https://github.com/hyperium/hyper/blob/v0.14.25/CHANGELOG.md) - [Commits](https://github.com/hyperium/hyper/compare/v0.14.24...v0.14.25) --- updated-dependencies: - dependency-name: hyper dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 59fd21ca..5af6090e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -453,9 +453,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.24" +version = "0.14.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e011372fa0b68db8350aa7a248930ecc7839bf46d8485577d69f117a75f164c" +checksum = "cc5e554ff619822309ffd57d8734d77cd5ce6238bc956f037ea06c58238c9899" dependencies = [ "bytes", "futures-channel", From 9a2076a9eb5e893ac5431c6436820170c360e847 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Mar 2023 09:13:45 -0700 Subject: [PATCH 07/79] chore(deps): bump futures from 0.3.26 to 0.3.27 (#356) Bumps [futures](https://github.com/rust-lang/futures-rs) from 0.3.26 to 0.3.27. - [Release notes](https://github.com/rust-lang/futures-rs/releases) - [Changelog](https://github.com/rust-lang/futures-rs/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-lang/futures-rs/compare/0.3.26...0.3.27) --- updated-dependencies: - dependency-name: futures dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5af6090e..151b0088 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -254,9 +254,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "futures" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13e2792b0ff0340399d58445b88fd9770e3489eff258a4cbc1523418f12abf84" +checksum = "531ac96c6ff5fd7c62263c5e3c67a603af4fcaee2e1a0ae5565ba3a11e69e549" dependencies = [ "futures-channel", "futures-core", @@ -269,9 +269,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e5317663a9089767a1ec00a487df42e0ca174b61b4483213ac24448e4664df5" +checksum = "164713a5a0dcc3e7b4b1ed7d3b433cabc18025386f9339346e8daf15963cf7ac" dependencies = [ "futures-core", "futures-sink", @@ -279,15 +279,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec90ff4d0fe1f57d600049061dc6bb68ed03c7d2fbd697274c41805dcb3f8608" +checksum = "86d7a0c1aa76363dac491de0ee99faf6941128376f1cf96f07db7603b7de69dd" [[package]] name = "futures-executor" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8de0a35a6ab97ec8869e32a2473f4b1324459e14c29275d14b10cb1fd19b50e" +checksum = "1997dd9df74cdac935c76252744c1ed5794fac083242ea4fe77ef3ed60ba0f83" dependencies = [ "futures-core", "futures-task", @@ -296,15 +296,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfb8371b6fb2aeb2d280374607aeabfc99d95c72edfe51692e42d3d7f0d08531" +checksum = "89d422fa3cbe3b40dca574ab087abb5bc98258ea57eea3fd6f1fa7162c778b91" [[package]] name = "futures-macro" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a73af87da33b5acf53acfebdc339fe592ecf5357ac7c0a7734ab9d8c876a70" +checksum = "3eb14ed937631bd8b8b8977f2c198443447a8355b6e3ca599f38c975e5a963b6" dependencies = [ "proc-macro2", "quote", @@ -313,21 +313,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f310820bb3e8cfd46c80db4d7fb8353e15dfff853a127158425f31e0be6c8364" +checksum = "ec93083a4aecafb2a80a885c9de1f0ccae9dbd32c2bb54b0c3a65690e0b8d2f2" [[package]] name = "futures-task" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf79a1bf610b10f42aea489289c5a2c478a786509693b80cd39c44ccd936366" +checksum = "fd65540d33b37b16542a0438c12e6aeead10d4ac5d05bd3f805b8f35ab592879" [[package]] name = "futures-util" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c1d6de3acfef38d2be4b1f543f553131788603495be83da675e180c8d6b7bd1" +checksum = "3ef6b17e481503ec85211fed8f39d1970f128935ca1f814cd32ac4a6842e84ab" dependencies = [ "futures-channel", "futures-core", From 58ce76d9b9f3706c753c57513c8cd76f459ebcac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Fern=C3=A1ndez?= Date: Tue, 28 Mar 2023 17:19:37 +0200 Subject: [PATCH 08/79] Refactor stats to use atomics (#375) * Refactor stats to use atomics When we are dealing with a high number of connections, generated stats cannot be consumed fast enough by the stats collector loop. This makes the stats subsystem inconsistent and a log of warning messages are thrown due to unregistered server/clients. This change refactors the stats subsystem so it uses atomics: - Now counters are handled using U64 atomics - Event system is dropped and averages are calculated using a loop every 15 seconds. - Now, instead of snapshots being generated ever second we keep track of servers/clients that have registered. Each pool/server/client has its own instance of the counter and makes changes directly, instead of adding an event that gets processed later. * Manually mplement Hash/Eq in `config::Address` ignoring stats * Add tests for client connection counters * Allow connecting to dockerized dev pgcat from the host * stats: Decrease cl_idle when idle socket disconnects --- Cargo.lock | 12 + Cargo.toml | 1 + dev/docker-compose.yaml | 2 + src/admin.rs | 121 +++-- src/client.rs | 155 ++++-- src/config.rs | 42 +- src/main.rs | 5 +- src/mirrors.rs | 26 +- src/pool.rs | 122 +++-- src/prometheus.rs | 59 ++- src/server.rs | 30 +- src/stats.rs | 1021 +++---------------------------------- src/stats/address.rs | 149 ++++++ src/stats/client.rs | 182 +++++++ src/stats/pool.rs | 274 ++++++++++ src/stats/server.rs | 226 ++++++++ tests/ruby/admin_spec.rb | 43 +- tests/ruby/capture | Bin 0 -> 40638 bytes tests/ruby/spec_helper.rb | 7 + 19 files changed, 1299 insertions(+), 1178 deletions(-) create mode 100644 src/stats/address.rs create mode 100644 src/stats/client.rs create mode 100644 src/stats/pool.rs create mode 100644 src/stats/server.rs create mode 100644 tests/ruby/capture diff --git a/Cargo.lock b/Cargo.lock index 151b0088..7e4aa683 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,6 +37,17 @@ dependencies = [ "syn 2.0.9", ] +[[package]] +name = "atomic_enum" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6227a8d6fdb862bcb100c4314d0d9579e5cd73fa6df31a2e6f6e1acd3c5f1207" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -720,6 +731,7 @@ version = "1.0.0" dependencies = [ "arc-swap", "async-trait", + "atomic_enum", "base64", "bb8", "bytes", diff --git a/Cargo.toml b/Cargo.toml index e5b1de66..3f0cbec6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ exitcode = "1.1.2" futures = "0.3" socket2 = { version = "0.4.7", features = ["all"] } nix = "0.26.2" +atomic_enum = "0.2.0" [target.'cfg(not(target_env = "msvc"))'.dependencies] jemallocator = "0.5.0" diff --git a/dev/docker-compose.yaml b/dev/docker-compose.yaml index da759383..15621e87 100644 --- a/dev/docker-compose.yaml +++ b/dev/docker-compose.yaml @@ -26,6 +26,8 @@ x-common-env-pg: services: main: image: kubernetes/pause + ports: + - 6432 pg1: <<: *common-definition-pg diff --git a/src/admin.rs b/src/admin.rs index feea3a15..03af755c 100644 --- a/src/admin.rs +++ b/src/admin.rs @@ -1,10 +1,11 @@ use crate::pool::BanReason; -/// Admin database. use bytes::{Buf, BufMut, BytesMut}; use log::{error, info, trace}; use nix::sys::signal::{self, Signal}; use nix::unistd::Pid; use std::collections::HashMap; +/// Admin database. +use std::sync::atomic::Ordering; use std::time::{SystemTime, UNIX_EPOCH}; use tokio::time::Instant; @@ -12,9 +13,7 @@ use crate::config::{get_config, reload_config, VERSION}; use crate::errors::Error; use crate::messages::*; use crate::pool::{get_all_pools, get_pool}; -use crate::stats::{ - get_address_stats, get_client_stats, get_pool_stats, get_server_stats, ClientState, ServerState, -}; +use crate::stats::{get_client_stats, get_pool_stats, get_server_stats, ClientState, ServerState}; use crate::ClientServerMap; pub fn generate_server_info_for_admin() -> BytesMut { @@ -158,7 +157,14 @@ where "free_clients".to_string(), client_stats .keys() - .filter(|client_id| client_stats.get(client_id).unwrap().state == ClientState::Idle) + .filter(|client_id| { + client_stats + .get(client_id) + .unwrap() + .state + .load(Ordering::Relaxed) + == ClientState::Idle + }) .count() .to_string(), ])); @@ -166,7 +172,14 @@ where "used_clients".to_string(), client_stats .keys() - .filter(|client_id| client_stats.get(client_id).unwrap().state == ClientState::Active) + .filter(|client_id| { + client_stats + .get(client_id) + .unwrap() + .state + .load(Ordering::Relaxed) + == ClientState::Active + }) .count() .to_string(), ])); @@ -178,7 +191,14 @@ where "free_servers".to_string(), server_stats .keys() - .filter(|server_id| server_stats.get(server_id).unwrap().state == ServerState::Idle) + .filter(|server_id| { + server_stats + .get(server_id) + .unwrap() + .state + .load(Ordering::Relaxed) + == ServerState::Idle + }) .count() .to_string(), ])); @@ -186,7 +206,14 @@ where "used_servers".to_string(), server_stats .keys() - .filter(|server_id| server_stats.get(server_id).unwrap().state == ServerState::Active) + .filter(|server_id| { + server_stats + .get(server_id) + .unwrap() + .state + .load(Ordering::Relaxed) + == ServerState::Active + }) .count() .to_string(), ])); @@ -248,28 +275,15 @@ where let mut res = BytesMut::new(); res.put(row_description(&columns)); - for (user_pool, pool) in get_all_pools() { - let def = HashMap::default(); - let pool_stats = all_pool_stats - .get(&(user_pool.db.clone(), user_pool.user.clone())) - .unwrap_or(&def); - let pool_config = &pool.settings; + for ((_user_pool, _pool), pool_stats) in all_pool_stats { let mut row = vec![ - user_pool.db.clone(), - user_pool.user.clone(), - pool_config.pool_mode.to_string(), + pool_stats.database(), + pool_stats.user(), + pool_stats.pool_mode().to_string(), ]; - for column in &columns[3..columns.len()] { - let value = match column.0 { - "maxwait" => (pool_stats.get("maxwait_us").unwrap_or(&0) / 1_000_000).to_string(), - "maxwait_us" => { - (pool_stats.get("maxwait_us").unwrap_or(&0) % 1_000_000).to_string() - } - _other_values => pool_stats.get(column.0).unwrap_or(&0).to_string(), - }; - row.push(value); - } + pool_stats.populate_row(&mut row); + pool_stats.clear_maxwait(); res.put(data_row(&row)); } @@ -400,7 +414,7 @@ where for (id, pool) in get_all_pools().iter() { for address in pool.get_addresses_from_host(host) { if !pool.is_banned(&address) { - pool.ban(&address, BanReason::AdminBan(duration_seconds), -1); + pool.ban(&address, BanReason::AdminBan(duration_seconds), None); res.put(data_row(&vec![ id.db.clone(), id.user.clone(), @@ -617,7 +631,6 @@ where ("avg_wait_time", DataType::Numeric), ]; - let all_stats = get_address_stats(); let mut res = BytesMut::new(); res.put(row_description(&columns)); @@ -625,15 +638,10 @@ where for shard in 0..pool.shards() { for server in 0..pool.servers(shard) { let address = pool.address(shard, server); - let stats = match all_stats.get(&address.id) { - Some(stats) => stats.clone(), - None => HashMap::new(), - }; let mut row = vec![address.name(), user_pool.db.clone(), user_pool.user.clone()]; - for column in &columns[3..] { - row.push(stats.get(column.0).unwrap_or(&0).to_string()); - } + let stats = address.stats.clone(); + stats.populate_row(&mut row); res.put(data_row(&row)); } @@ -673,16 +681,16 @@ where for (_, client) in new_map { let row = vec![ - format!("{:#010X}", client.client_id), - client.pool_name, - client.username, - client.application_name.clone(), - client.state.to_string(), - client.transaction_count.to_string(), - client.query_count.to_string(), - client.error_count.to_string(), + format!("{:#010X}", client.client_id()), + client.pool_name(), + client.username(), + client.application_name(), + client.state.load(Ordering::Relaxed).to_string(), + client.transaction_count.load(Ordering::Relaxed).to_string(), + client.query_count.load(Ordering::Relaxed).to_string(), + client.error_count.load(Ordering::Relaxed).to_string(), Instant::now() - .duration_since(client.connect_time) + .duration_since(client.connect_time()) .as_secs() .to_string(), ]; @@ -724,19 +732,20 @@ where res.put(row_description(&columns)); for (_, server) in new_map { + let application_name = server.application_name.read(); let row = vec![ - format!("{:#010X}", server.server_id), - server.pool_name, - server.username, - server.address_name, - server.application_name, - server.state.to_string(), - server.transaction_count.to_string(), - server.query_count.to_string(), - server.bytes_sent.to_string(), - server.bytes_received.to_string(), + format!("{:#010X}", server.server_id()), + server.pool_name(), + server.username(), + server.address_name(), + application_name.clone(), + server.state.load(Ordering::Relaxed).to_string(), + server.transaction_count.load(Ordering::Relaxed).to_string(), + server.query_count.load(Ordering::Relaxed).to_string(), + server.bytes_sent.load(Ordering::Relaxed).to_string(), + server.bytes_received.load(Ordering::Relaxed).to_string(), Instant::now() - .duration_since(server.connect_time) + .duration_since(server.connect_time()) .as_secs() .to_string(), ]; diff --git a/src/client.rs b/src/client.rs index 68f6f575..f9f4e015 100644 --- a/src/client.rs +++ b/src/client.rs @@ -3,8 +3,8 @@ use crate::pool::BanReason; /// Handle clients by pretending to be a PostgreSQL server. use bytes::{Buf, BufMut, BytesMut}; use log::{debug, error, info, trace, warn}; - use std::collections::HashMap; +use std::sync::Arc; use std::time::Instant; use tokio::io::{split, AsyncReadExt, BufReader, ReadHalf, WriteHalf}; use tokio::net::TcpStream; @@ -19,7 +19,7 @@ use crate::messages::*; use crate::pool::{get_pool, ClientServerMap, ConnectionPool}; use crate::query_router::{Command, QueryRouter}; use crate::server::Server; -use crate::stats::{get_reporter, Reporter}; +use crate::stats::{ClientStats, PoolStats, ServerStats}; use crate::tls::Tls; use tokio_rustls::server::TlsStream; @@ -66,8 +66,8 @@ pub struct Client { #[allow(dead_code)] parameters: HashMap, - /// Statistics - stats: Reporter, + /// Statistics related to this client + stats: Arc, /// Clients want to talk to admin database. admin: bool, @@ -75,8 +75,8 @@ pub struct Client { /// Last address the client talked to. last_address_id: Option, - /// Last server process id we talked to. - last_server_id: Option, + /// Last server process stats we talked to. + last_server_stats: Option>, /// Connected to server connected_to_server: bool, @@ -135,6 +135,10 @@ pub async fn client_entrypoint( if !client.is_admin() { let _ = drain.send(-1).await; + + if result.is_err() { + client.stats.disconnect(); + } } result @@ -183,6 +187,10 @@ pub async fn client_entrypoint( if !client.is_admin() { let _ = drain.send(-1).await; + + if result.is_err() { + client.stats.disconnect(); + } } result @@ -233,6 +241,10 @@ pub async fn client_entrypoint( if !client.is_admin() { let _ = drain.send(-1).await; + + if result.is_err() { + client.stats.disconnect(); + } } result @@ -258,8 +270,11 @@ pub async fn client_entrypoint( if !client.is_admin() { let _ = drain.send(-1).await; - } + if result.is_err() { + client.stats.disconnect(); + } + } result } @@ -382,7 +397,6 @@ where shutdown: Receiver<()>, admin_only: bool, ) -> Result, Error> { - let stats = get_reporter(); let parameters = parse_startup(bytes.clone())?; // This parameter is mandatory by the protocol. @@ -537,6 +551,25 @@ where ready_for_query(&mut write).await?; trace!("Startup OK"); + let pool_stats = match get_pool(pool_name, username) { + Some(pool) => { + if !admin { + pool.stats + } else { + Arc::new(PoolStats::default()) + } + } + None => Arc::new(PoolStats::default()), + }; + + let stats = Arc::new(ClientStats::new( + process_id, + application_name, + username, + pool_name, + tokio::time::Instant::now(), + pool_stats, + )); Ok(Client { read: BufReader::new(read), @@ -552,7 +585,7 @@ where stats, admin, last_address_id: None, - last_server_id: None, + last_server_stats: None, pool_name: pool_name.clone(), username: username.clone(), application_name: application_name.to_string(), @@ -583,10 +616,10 @@ where secret_key, client_server_map, parameters: HashMap::new(), - stats: get_reporter(), + stats: Arc::new(ClientStats::default()), admin: false, last_address_id: None, - last_server_id: None, + last_server_stats: None, pool_name: String::from("undefined"), username: String::from("undefined"), application_name: String::from("undefined"), @@ -627,12 +660,8 @@ where // The query router determines where the query is going to go, // e.g. primary, replica, which shard. let mut query_router = QueryRouter::new(); - self.stats.client_register( - self.process_id, - self.pool_name.clone(), - self.username.clone(), - self.application_name.clone(), - ); + + self.stats.register(self.stats.clone()); // Our custom protocol loop. // We expect the client to either start a transaction with regular queries @@ -656,6 +685,8 @@ where &mut self.write, "terminating connection due to administrator command" ).await?; + self.stats.disconnect(); + return Ok(()) } @@ -708,6 +739,9 @@ where 'X' => { debug!("Client disconnecting"); + + self.stats.disconnect(); + return Ok(()); } @@ -757,7 +791,7 @@ where current_shard, ), ) - .await?; + .await?; } else { custom_protocol_response_ok(&mut self.write, "SET SHARD").await?; } @@ -802,10 +836,13 @@ where }; debug!("Waiting for connection from pool"); + if !self.admin { + self.stats.waiting(); + } // Grab a server from the pool. let connection = match pool - .get(query_router.shard(), query_router.role(), self.process_id) + .get(query_router.shard(), query_router.role(), &self.stats) .await { Ok(conn) => { @@ -817,6 +854,8 @@ where // but we were unable to grab a connection from the pool // We'll send back an error message and clean the extended // protocol buffer + self.stats.idle(); + if message[0] as char == 'S' { error!("Got Sync message but failed to get a connection from the pool"); self.buffer.clear(); @@ -825,7 +864,7 @@ where .await?; error!("Could not get connection from pool: {{ pool_name: {:?}, username: {:?}, shard: {:?}, role: \"{:?}\", error: \"{:?}\" }}", - self.pool_name.clone(), self.username.clone(), query_router.shard(), query_router.role(), err); + self.pool_name.clone(), self.username.clone(), query_router.shard(), query_router.role(), err); continue; } }; @@ -840,11 +879,10 @@ where self.connected_to_server = true; // Update statistics - self.stats - .client_active(self.process_id, server.server_id()); + self.stats.active(); self.last_address_id = Some(address.id); - self.last_server_id = Some(server.server_id()); + self.last_server_stats = Some(server.stats()); debug!( "Client {:?} talking to server {:?}", @@ -885,6 +923,7 @@ where Ok(Err(err)) => { // Client disconnected inside a transaction. // Clean up the server and re-use it. + self.stats.disconnect(); server.checkin_cleanup().await?; return Err(err); @@ -917,16 +956,26 @@ where 'Q' => { debug!("Sending query to server"); - self.send_and_receive_loop(code, Some(&message), server, &address, &pool) - .await?; + self.send_and_receive_loop( + code, + Some(&message), + server, + &address, + &pool, + &self.stats.clone(), + ) + .await?; if !server.in_transaction() { // Report transaction executed statistics. - self.stats.transaction(self.process_id, server.server_id()); + self.stats.transaction(); + server.stats().transaction(&self.application_name); // Release server back to the pool if we are in transaction mode. // If we are in session mode, we keep the server until the client disconnects. if self.transaction_mode { + self.stats.idle(); + break; } } @@ -935,6 +984,7 @@ where // Terminate 'X' => { server.checkin_cleanup().await?; + self.stats.disconnect(); self.release(); return Ok(()); @@ -987,13 +1037,21 @@ where } } - self.send_and_receive_loop(code, None, server, &address, &pool) - .await?; + self.send_and_receive_loop( + code, + None, + server, + &address, + &pool, + &self.stats.clone(), + ) + .await?; self.buffer.clear(); if !server.in_transaction() { - self.stats.transaction(self.process_id, server.server_id()); + self.stats.transaction(); + server.stats().transaction(&self.application_name); // Release server back to the pool if we are in transaction mode. // If we are in session mode, we keep the server until the client disconnects. @@ -1028,7 +1086,9 @@ where // Clear the buffer self.buffer.clear(); - let response = self.receive_server_message(server, &address, &pool).await?; + let response = self + .receive_server_message(server, &address, &pool, &self.stats.clone()) + .await?; match write_all_half(&mut self.write, &response).await { Ok(_) => (), @@ -1039,7 +1099,8 @@ where }; if !server.in_transaction() { - self.stats.transaction(self.process_id, server.server_id()); + self.stats.transaction(); + server.stats().transaction(&self.application_name); // Release server back to the pool if we are in transaction mode. // If we are in session mode, we keep the server until the client disconnects. @@ -1060,11 +1121,11 @@ where // The server is no longer bound to us, we can't cancel it's queries anymore. debug!("Releasing server back into the pool"); server.checkin_cleanup().await?; - self.stats.server_idle(server.server_id()); + server.stats().idle(); self.connected_to_server = false; self.release(); - self.stats.client_idle(self.process_id); + self.stats.idle(); } } @@ -1104,6 +1165,7 @@ where server: &mut Server, address: &Address, pool: &ConnectionPool, + client_stats: &ClientStats, ) -> Result<(), Error> { debug!("Sending {} to server", code); @@ -1119,7 +1181,9 @@ where // Read all data the server has to offer, which can be multiple messages // buffered in 8196 bytes chunks. loop { - let response = self.receive_server_message(server, address, pool).await?; + let response = self + .receive_server_message(server, address, pool, client_stats) + .await?; match write_all_half(&mut self.write, &response).await { Ok(_) => (), @@ -1135,10 +1199,10 @@ where } // Report query executed statistics. - self.stats.query( - self.process_id, - server.server_id(), - Instant::now().duration_since(query_start).as_millis(), + client_stats.query(); + server.stats().query( + Instant::now().duration_since(query_start).as_millis() as u64, + &self.application_name, ); Ok(()) @@ -1154,7 +1218,7 @@ where match server.send(message).await { Ok(_) => Ok(()), Err(err) => { - pool.ban(address, BanReason::MessageSendFailed, self.process_id); + pool.ban(address, BanReason::MessageSendFailed, Some(&self.stats)); Err(err) } } @@ -1165,6 +1229,7 @@ where server: &mut Server, address: &Address, pool: &ConnectionPool, + client_stats: &ClientStats, ) -> Result { if pool.settings.user.statement_timeout > 0 { match tokio::time::timeout( @@ -1176,7 +1241,7 @@ where Ok(result) => match result { Ok(message) => Ok(message), Err(err) => { - pool.ban(address, BanReason::MessageReceiveFailed, self.process_id); + pool.ban(address, BanReason::MessageReceiveFailed, Some(client_stats)); error_response_terminal( &mut self.write, &format!("error receiving data from server: {:?}", err), @@ -1191,7 +1256,7 @@ where address, pool.settings.user.username ); server.mark_bad(); - pool.ban(address, BanReason::StatementTimeout, self.process_id); + pool.ban(address, BanReason::StatementTimeout, Some(client_stats)); error_response_terminal(&mut self.write, "pool statement timeout").await?; Err(Error::StatementTimeout) } @@ -1200,7 +1265,7 @@ where match server.recv().await { Ok(message) => Ok(message), Err(err) => { - pool.ban(address, BanReason::MessageReceiveFailed, self.process_id); + pool.ban(address, BanReason::MessageReceiveFailed, Some(client_stats)); error_response_terminal( &mut self.write, &format!("error receiving data from server: {:?}", err), @@ -1220,9 +1285,9 @@ impl Drop for Client { // Dirty shutdown // TODO: refactor, this is not the best way to handle state management. - self.stats.client_disconnecting(self.process_id); - if self.connected_to_server && self.last_server_id.is_some() { - self.stats.server_idle(self.last_server_id.unwrap()); + + if self.connected_to_server && self.last_server_stats.is_some() { + self.last_server_stats.as_ref().unwrap().idle(); } } } diff --git a/src/config.rs b/src/config.rs index a3182e2b..644532ac 100644 --- a/src/config.rs +++ b/src/config.rs @@ -15,6 +15,7 @@ use tokio::io::AsyncReadExt; use crate::errors::Error; use crate::pool::{ClientServerMap, ConnectionPool}; use crate::sharding::ShardingFunction; +use crate::stats::AddressStats; use crate::tls::{load_certs, load_keys}; pub const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -62,7 +63,7 @@ impl PartialEq for Option { } /// Address identifying a PostgreSQL server uniquely. -#[derive(Clone, PartialEq, Hash, std::cmp::Eq, Debug)] +#[derive(Clone, Debug)] pub struct Address { /// Unique ID per addressable Postgres server. pub id: usize, @@ -96,6 +97,9 @@ pub struct Address { /// List of addresses to receive mirrored traffic. pub mirrors: Vec
, + + /// Address stats + pub stats: Arc, } impl Default for Address { @@ -112,10 +116,46 @@ impl Default for Address { username: String::from("username"), pool_name: String::from("pool_name"), mirrors: Vec::new(), + stats: Arc::new(AddressStats::default()), } } } +// We need to implement PartialEq by ourselves so we skip stats in the comparison +impl PartialEq for Address { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + && self.host == other.host + && self.port == other.port + && self.shard == other.shard + && self.address_index == other.address_index + && self.replica_number == other.replica_number + && self.database == other.database + && self.role == other.role + && self.username == other.username + && self.pool_name == other.pool_name + && self.mirrors == other.mirrors + } +} +impl Eq for Address {} + +// We need to implement Hash by ourselves so we skip stats in the comparison +impl Hash for Address { + fn hash(&self, state: &mut H) { + self.id.hash(state); + self.host.hash(state); + self.port.hash(state); + self.shard.hash(state); + self.address_index.hash(state); + self.replica_number.hash(state); + self.database.hash(state); + self.role.hash(state); + self.username.hash(state); + self.pool_name.hash(state); + self.mirrors.hash(state); + } +} + impl Address { /// Address name (aka database) used in `SHOW STATS`, `SHOW DATABASES`, and `SHOW POOLS`. pub fn name(&self) -> String { diff --git a/src/main.rs b/src/main.rs index e2ff5d8d..a59da210 100644 --- a/src/main.rs +++ b/src/main.rs @@ -162,8 +162,7 @@ fn main() -> Result<(), Box> { let client_server_map: ClientServerMap = Arc::new(Mutex::new(HashMap::new())); // Statistics reporting. - let (stats_tx, stats_rx) = mpsc::channel(500_000); - REPORTER.store(Arc::new(Reporter::new(stats_tx.clone()))); + REPORTER.store(Arc::new(Reporter::default())); // Connection pool that allows to query all shards and replicas. match ConnectionPool::from_config(client_server_map.clone()).await { @@ -175,7 +174,7 @@ fn main() -> Result<(), Box> { }; tokio::task::spawn(async move { - let mut stats_collector = Collector::new(stats_rx, stats_tx.clone()); + let mut stats_collector = Collector::default(); stats_collector.collect().await; }); diff --git a/src/mirrors.rs b/src/mirrors.rs index ab2b2dc4..128fe220 100644 --- a/src/mirrors.rs +++ b/src/mirrors.rs @@ -1,11 +1,13 @@ +use std::sync::Arc; + /// A mirrored PostgreSQL client. /// Packets arrive to us through a channel from the main client and we send them to the server. use bb8::Pool; use bytes::{Bytes, BytesMut}; use crate::config::{get_config, Address, Role, User}; -use crate::pool::{ClientServerMap, ServerPool}; -use crate::stats::get_reporter; +use crate::pool::{ClientServerMap, PoolIdentifier, ServerPool}; +use crate::stats::PoolStats; use log::{error, info, trace, warn}; use tokio::sync::mpsc::{channel, Receiver, Sender}; @@ -21,20 +23,24 @@ impl MirroredClient { async fn create_pool(&self) -> Pool { let config = get_config(); let default = std::time::Duration::from_millis(10_000).as_millis() as u64; - let (connection_timeout, idle_timeout) = match config.pools.get(&self.address.pool_name) { - Some(cfg) => ( - cfg.connect_timeout.unwrap_or(default), - cfg.idle_timeout.unwrap_or(default), - ), - None => (default, default), - }; + let (connection_timeout, idle_timeout, cfg) = + match config.pools.get(&self.address.pool_name) { + Some(cfg) => ( + cfg.connect_timeout.unwrap_or(default), + cfg.idle_timeout.unwrap_or(default), + cfg.clone(), + ), + None => (default, default, crate::config::Pool::default()), + }; + + let identifier = PoolIdentifier::new(&self.database, &self.user.username); let manager = ServerPool::new( self.address.clone(), self.user.clone(), self.database.as_str(), ClientServerMap::default(), - get_reporter(), + Arc::new(PoolStats::new(identifier, cfg.clone())), ); Pool::builder() diff --git a/src/pool.rs b/src/pool.rs index 3a6ec3e6..f6f9118b 100644 --- a/src/pool.rs +++ b/src/pool.rs @@ -22,7 +22,7 @@ use crate::errors::Error; use crate::server::Server; use crate::sharding::ShardingFunction; -use crate::stats::{get_reporter, Reporter}; +use crate::stats::{AddressStats, ClientStats, PoolStats, ServerStats}; pub type ProcessId = i32; pub type SecretKey = i32; @@ -51,7 +51,7 @@ pub enum BanReason { /// An identifier for a PgCat pool, /// a database visible to clients. -#[derive(Hash, Debug, Clone, PartialEq, Eq)] +#[derive(Hash, Debug, Clone, PartialEq, Eq, Default)] pub struct PoolIdentifier { // The name of the database clients want to connect to. pub db: String, @@ -161,10 +161,6 @@ pub struct ConnectionPool { /// that should not be queried. banlist: BanList, - /// The statistics aggregator runs in a separate task - /// and receives stats from clients, servers, and the pool. - stats: Reporter, - /// The server information (K messages) have to be passed to the /// clients on startup. We pre-connect to all shards and replicas /// on pool creation and save the K messages here. @@ -185,6 +181,8 @@ pub struct ConnectionPool { /// If the pool has been paused or not. paused: Arc, paused_waiter: Arc, + + pub stats: Arc, } impl ConnectionPool { @@ -201,6 +199,7 @@ impl ConnectionPool { // There is one pool per database/user pair. for user in pool_config.users.values() { let old_pool_ref = get_pool(pool_name, &user.username); + let identifier = PoolIdentifier::new(pool_name, &user.username); match old_pool_ref { Some(pool) => { @@ -211,10 +210,7 @@ impl ConnectionPool { "[pool: {}][user: {}] has not changed", pool_name, user.username ); - new_pools.insert( - PoolIdentifier::new(pool_name, &user.username), - pool.clone(), - ); + new_pools.insert(identifier.clone(), pool.clone()); continue; } } @@ -234,6 +230,10 @@ impl ConnectionPool { .clone() .into_keys() .collect::>(); + let pool_stats = Arc::new(PoolStats::new(identifier, pool_config.clone())); + + // Allow the pool to be seen in statistics + pool_stats.register(pool_stats.clone()); // Sort by shard number to ensure consistency. shard_ids.sort_by_key(|k| k.parse::().unwrap()); @@ -266,6 +266,7 @@ impl ConnectionPool { username: user.username.clone(), pool_name: pool_name.clone(), mirrors: vec![], + stats: Arc::new(AddressStats::default()), }); address_id += 1; } @@ -283,6 +284,7 @@ impl ConnectionPool { username: user.username.clone(), pool_name: pool_name.clone(), mirrors: mirror_addresses, + stats: Arc::new(AddressStats::default()), }; address_id += 1; @@ -296,7 +298,7 @@ impl ConnectionPool { user.clone(), &shard.database, client_server_map.clone(), - get_reporter(), + pool_stats.clone(), ); let connect_timeout = match pool_config.connect_timeout { @@ -331,9 +333,9 @@ impl ConnectionPool { let pool = ConnectionPool { databases: shards, + stats: pool_stats, addresses, banlist: Arc::new(RwLock::new(banlist)), - stats: get_reporter(), config_hash: new_pool_hash_value, server_info: Arc::new(RwLock::new(BytesMut::new())), settings: PoolSettings { @@ -476,9 +478,9 @@ impl ConnectionPool { /// Get a connection from the pool. pub async fn get( &self, - shard: usize, // shard number - role: Option, // primary or replica - client_process_id: i32, // client id + shard: usize, // shard number + role: Option, // primary or replica + client_stats: &ClientStats, // client id ) -> Result<(PooledConnection<'_, ServerPool>, Address), Error> { let mut candidates: Vec<&Address> = self.addresses[shard] .iter() @@ -517,7 +519,7 @@ impl ConnectionPool { // Indicate we're waiting on a server connection from a pool. let now = Instant::now(); - self.stats.client_waiting(client_process_id); + client_stats.waiting(); // Check if we can connect let mut conn = match self.databases[address.shard][address.address_index] @@ -527,9 +529,10 @@ impl ConnectionPool { Ok(conn) => conn, Err(err) => { error!("Banning instance {:?}, error: {:?}", address, err); - self.ban(address, BanReason::FailedCheckout, client_process_id); - self.stats - .client_checkout_error(client_process_id, address.id); + self.ban(address, BanReason::FailedCheckout, Some(client_stats)); + address.stats.error(); + client_stats.idle(); + client_stats.checkout_error(); continue; } }; @@ -546,18 +549,18 @@ impl ConnectionPool { // since we last checked the server is ok. // Health checks are pretty expensive. if !require_healthcheck { - self.stats.checkout_time( - now.elapsed().as_micros(), - client_process_id, - server.server_id(), - ); - self.stats - .server_active(client_process_id, server.server_id()); + let checkout_time: u64 = now.elapsed().as_micros() as u64; + client_stats.checkout_time(checkout_time); + server + .stats() + .checkout_time(checkout_time, client_stats.application_name()); + server.stats().active(client_stats.application_name()); + return Ok((conn, address.clone())); } if self - .run_health_check(address, server, now, client_process_id) + .run_health_check(address, server, now, client_stats) .await { return Ok((conn, address.clone())); @@ -565,7 +568,6 @@ impl ConnectionPool { continue; } } - Err(Error::AllServersDown) } @@ -574,11 +576,11 @@ impl ConnectionPool { address: &Address, server: &mut Server, start: Instant, - client_process_id: i32, + client_info: &ClientStats, ) -> bool { debug!("Running health check on server {:?}", address); - self.stats.server_tested(server.server_id()); + server.stats().tested(); match tokio::time::timeout( tokio::time::Duration::from_millis(self.settings.healthcheck_timeout), @@ -589,13 +591,13 @@ impl ConnectionPool { // Check if health check succeeded. Ok(res) => match res { Ok(_) => { - self.stats.checkout_time( - start.elapsed().as_micros(), - client_process_id, - server.server_id(), - ); - self.stats - .server_active(client_process_id, server.server_id()); + let checkout_time: u64 = start.elapsed().as_micros() as u64; + client_info.checkout_time(checkout_time); + server + .stats() + .checkout_time(checkout_time, client_info.application_name()); + server.stats().active(client_info.application_name()); + return true; } @@ -620,14 +622,14 @@ impl ConnectionPool { // Don't leave a bad connection in the pool. server.mark_bad(); - self.ban(&address, BanReason::FailedHealthCheck, client_process_id); + self.ban(&address, BanReason::FailedHealthCheck, Some(client_info)); return false; } /// Ban an address (i.e. replica). It no longer will serve /// traffic for any new transactions. Existing transactions on that replica /// will finish successfully or error out to the clients. - pub fn ban(&self, address: &Address, reason: BanReason, client_id: i32) { + pub fn ban(&self, address: &Address, reason: BanReason, client_info: Option<&ClientStats>) { // Primary can never be banned if address.role == Role::Primary { return; @@ -636,7 +638,10 @@ impl ConnectionPool { let now = chrono::offset::Utc::now().naive_utc(); let mut guard = self.banlist.write(); error!("Banning {:?}", address); - self.stats.client_ban_error(client_id, address.id); + if let Some(client_info) = client_info { + client_info.ban_error(); + address.stats.error(); + } guard[address.shard].insert(address.clone(), (reason, now)); } @@ -797,7 +802,7 @@ pub struct ServerPool { user: User, database: String, client_server_map: ClientServerMap, - stats: Reporter, + stats: Arc, } impl ServerPool { @@ -806,11 +811,11 @@ impl ServerPool { user: User, database: &str, client_server_map: ClientServerMap, - stats: Reporter, + stats: Arc, ) -> ServerPool { ServerPool { address, - user, + user: user.clone(), database: database.to_string(), client_server_map, stats, @@ -826,34 +831,31 @@ impl ManageConnection for ServerPool { /// Attempts to create a new connection. async fn connect(&self) -> Result { info!("Creating a new server connection {:?}", self.address); - let server_id = rand::random::(); - self.stats.server_register( - server_id, - self.address.id, - self.address.name(), - self.address.pool_name.clone(), - self.address.username.clone(), - ); - self.stats.server_login(server_id); + let stats = Arc::new(ServerStats::new( + self.address.clone(), + self.stats.clone(), + tokio::time::Instant::now(), + )); + + stats.register(stats.clone()); // Connect to the PostgreSQL server. match Server::startup( - server_id, &self.address, &self.user, &self.database, self.client_server_map.clone(), - self.stats.clone(), + stats.clone(), ) .await { Ok(conn) => { - self.stats.server_idle(server_id); + stats.idle(); Ok(conn) } Err(err) => { - self.stats.server_disconnecting(server_id); + stats.disconnect(); Err(err) } } @@ -881,11 +883,3 @@ pub fn get_pool(db: &str, user: &str) -> Option { pub fn get_all_pools() -> HashMap { (*(*POOLS.load())).clone() } - -/// How many total servers we have in the config. -pub fn get_number_of_addresses() -> usize { - get_all_pools() - .iter() - .map(|(_, pool)| pool.databases()) - .sum() -} diff --git a/src/prometheus.rs b/src/prometheus.rs index e596f9f9..6e578bf0 100644 --- a/src/prometheus.rs +++ b/src/prometheus.rs @@ -5,10 +5,12 @@ use phf::phf_map; use std::collections::HashMap; use std::fmt; use std::net::SocketAddr; +use std::sync::atomic::Ordering; +use std::sync::Arc; use crate::config::Address; use crate::pool::get_all_pools; -use crate::stats::{get_address_stats, get_pool_stats, get_server_stats, ServerInformation}; +use crate::stats::{get_pool_stats, get_server_stats, ServerStats}; struct MetricHelpType { help: &'static str, @@ -220,7 +222,7 @@ impl PrometheusMetric { Self::from_name(&format!("servers_{}", name), value, labels) } - fn from_address(address: &Address, name: &str, value: i64) -> Option> { + fn from_address(address: &Address, name: &str, value: u64) -> Option> { let mut labels = HashMap::new(); labels.insert("host", address.host.clone()); labels.insert("shard", address.shard.to_string()); @@ -231,7 +233,7 @@ impl PrometheusMetric { Self::from_name(&format!("stats_{}", name), value, labels) } - fn from_pool(pool: &(String, String), name: &str, value: i64) -> Option> { + fn from_pool(pool: &(String, String), name: &str, value: u64) -> Option> { let mut labels = HashMap::new(); labels.insert("pool", pool.0.clone()); labels.insert("user", pool.1.clone()); @@ -261,20 +263,18 @@ async fn prometheus_stats(request: Request) -> Result, hype // Adds metrics shown in a SHOW STATS admin command. fn push_address_stats(lines: &mut Vec) { - let address_stats: HashMap> = get_address_stats(); for (_, pool) in get_all_pools() { for shard in 0..pool.shards() { for server in 0..pool.servers(shard) { let address = pool.address(shard, server); - if let Some(address_stats) = address_stats.get(&address.id) { - for (key, value) in address_stats.iter() { - if let Some(prometheus_metric) = - PrometheusMetric::::from_address(address, key, *value) - { - lines.push(prometheus_metric.to_string()); - } else { - warn!("Metric {} not implemented for {}", key, address.name()); - } + let stats = &*address.stats; + for (key, value) in stats.clone() { + if let Some(prometheus_metric) = + PrometheusMetric::::from_address(address, &key, value) + { + lines.push(prometheus_metric.to_string()); + } else { + warn!("Metric {} not implemented for {}", key, address.name()); } } } @@ -286,8 +286,9 @@ fn push_address_stats(lines: &mut Vec) { fn push_pool_stats(lines: &mut Vec) { let pool_stats = get_pool_stats(); for (pool, stats) in pool_stats.iter() { - for (name, value) in stats.iter() { - if let Some(prometheus_metric) = PrometheusMetric::::from_pool(pool, name, *value) + let stats = &**stats; + for (name, value) in stats.clone() { + if let Some(prometheus_metric) = PrometheusMetric::::from_pool(pool, &name, value) { lines.push(prometheus_metric.to_string()); } else { @@ -330,9 +331,9 @@ fn push_database_stats(lines: &mut Vec) { // Adds relevant metrics shown in a SHOW SERVERS admin command. fn push_server_stats(lines: &mut Vec) { let server_stats = get_server_stats(); - let mut server_stats_by_addresses = HashMap::::new(); - for (_, info) in server_stats { - server_stats_by_addresses.insert(info.address_name.clone(), info); + let mut server_stats_by_addresses = HashMap::>::new(); + for (_, stats) in server_stats { + server_stats_by_addresses.insert(stats.address_name(), stats); } for (_, pool) in get_all_pools() { @@ -341,11 +342,23 @@ fn push_server_stats(lines: &mut Vec) { let address = pool.address(shard, server); if let Some(server_info) = server_stats_by_addresses.get(&address.name()) { let metrics = [ - ("bytes_received", server_info.bytes_received), - ("bytes_sent", server_info.bytes_sent), - ("transaction_count", server_info.transaction_count), - ("query_count", server_info.query_count), - ("error_count", server_info.error_count), + ( + "bytes_received", + server_info.bytes_received.load(Ordering::Relaxed), + ), + ("bytes_sent", server_info.bytes_sent.load(Ordering::Relaxed)), + ( + "transaction_count", + server_info.transaction_count.load(Ordering::Relaxed), + ), + ( + "query_count", + server_info.query_count.load(Ordering::Relaxed), + ), + ( + "error_count", + server_info.error_count.load(Ordering::Relaxed), + ), ]; for (key, value) in metrics { if let Some(prometheus_metric) = diff --git a/src/server.rs b/src/server.rs index b3dbd6f7..d09313ec 100644 --- a/src/server.rs +++ b/src/server.rs @@ -3,6 +3,7 @@ use bytes::{Buf, BufMut, BytesMut}; use log::{debug, error, info, trace, warn}; use std::io::Read; +use std::sync::Arc; use std::time::SystemTime; use tokio::io::{AsyncReadExt, BufReader}; use tokio::net::{ @@ -17,12 +18,10 @@ use crate::messages::*; use crate::mirrors::MirroringManager; use crate::pool::ClientServerMap; use crate::scram::ScramSha256; -use crate::stats::Reporter; +use crate::stats::ServerStats; /// Server state. pub struct Server { - server_id: i32, - /// Server host, e.g. localhost, /// port, e.g. 5432, and role, e.g. primary or replica. address: Address, @@ -62,7 +61,7 @@ pub struct Server { connected_at: chrono::naive::NaiveDateTime, /// Reports various metrics, e.g. data sent & received. - stats: Reporter, + stats: Arc, /// Application name using the server at the moment. application_name: String, @@ -77,12 +76,11 @@ impl Server { /// Pretend to be the Postgres client and connect to the server given host, port and credentials. /// Perform the authentication and return the server in a ready for query state. pub async fn startup( - server_id: i32, address: &Address, user: &User, database: &str, client_server_map: ClientServerMap, - stats: Reporter, + stats: Arc, ) -> Result { let mut stream = match TcpStream::connect(&format!("{}:{}", &address.host, address.port)).await { @@ -325,7 +323,6 @@ impl Server { write, buffer: BytesMut::with_capacity(8196), server_info, - server_id, process_id, secret_key, in_transaction: false, @@ -396,7 +393,7 @@ impl Server { /// Send messages to the server from the client. pub async fn send(&mut self, messages: &BytesMut) -> Result<(), Error> { self.mirror_send(messages); - self.stats.data_sent(messages.len(), self.server_id); + self.stats().data_sent(messages.len()); match write_all_half(&mut self.write, messages).await { Ok(_) => { @@ -545,7 +542,7 @@ impl Server { let bytes = self.buffer.clone(); // Keep track of how much data we got from the server for stats. - self.stats.data_received(bytes.len(), self.server_id); + self.stats().data_received(bytes.len()); // Clear the buffer for next query. self.buffer.clear(); @@ -665,18 +662,17 @@ impl Server { } } + /// get Server stats + pub fn stats(&self) -> Arc { + self.stats.clone() + } + /// Get the servers address. #[allow(dead_code)] pub fn address(&self) -> Address { self.address.clone() } - /// Get the server connection identifier - /// Used to uniquely identify connection in statistics - pub fn server_id(&self) -> i32 { - self.server_id - } - // Get server's latest response timestamp pub fn last_activity(&self) -> SystemTime { self.last_activity @@ -708,7 +704,9 @@ impl Drop for Server { /// for a write. fn drop(&mut self) { self.mirror_disconnect(); - self.stats.server_disconnecting(self.server_id); + + // Update statistics + self.stats.disconnect(); let mut bytes = BytesMut::with_capacity(4); bytes.put_u8(b'X'); diff --git a/src/stats.rs b/src/stats.rs index 3f7e9d61..5b7895b4 100644 --- a/src/stats.rs +++ b/src/stats.rs @@ -1,43 +1,46 @@ -use arc_swap::ArcSwap; +use crate::pool::PoolIdentifier; /// Statistics and reporting. -use log::{error, info, trace, warn}; +use arc_swap::ArcSwap; + +use log::{info, warn}; use once_cell::sync::Lazy; +use parking_lot::RwLock; use std::collections::HashMap; + use std::sync::Arc; -use tokio::sync::mpsc::error::TrySendError; -use tokio::sync::mpsc::{channel, Receiver, Sender}; -use tokio::time::Instant; -use crate::pool::{get_all_pools, get_number_of_addresses}; +// Structs that hold stats for different resources +pub mod address; +pub mod client; +pub mod pool; +pub mod server; +pub use address::AddressStats; +pub use client::{ClientState, ClientStats}; +pub use pool::PoolStats; +pub use server::{ServerState, ServerStats}; /// Convenience types for various stats -type ClientStatesLookup = HashMap; -type ServerStatesLookup = HashMap; -type PoolStatsLookup = HashMap<(String, String), HashMap>; -type AddressStatsLookup = HashMap>; +type ClientStatesLookup = HashMap>; +type ServerStatesLookup = HashMap>; +type PoolStatsLookup = HashMap<(String, String), Arc>; -/// Stats for individual client connections updated every second +/// Stats for individual client connections /// Used in SHOW CLIENTS. -static LATEST_CLIENT_STATS: Lazy> = - Lazy::new(|| ArcSwap::from_pointee(ClientStatesLookup::default())); +static CLIENT_STATS: Lazy>> = + Lazy::new(|| Arc::new(RwLock::new(ClientStatesLookup::default()))); -/// Stats for individual server connections updated every second +/// Stats for individual server connections /// Used in SHOW SERVERS. -static LATEST_SERVER_STATS: Lazy> = - Lazy::new(|| ArcSwap::from_pointee(ServerStatesLookup::default())); +static SERVER_STATS: Lazy>> = + Lazy::new(|| Arc::new(RwLock::new(ServerStatesLookup::default()))); -/// Aggregate stats for each pool (a pool is identified by database name and username) updated every second +/// Aggregate stats for each pool (a pool is identified by database name and username) /// Used in SHOW POOLS. -static LATEST_POOL_STATS: Lazy> = - Lazy::new(|| ArcSwap::from_pointee(PoolStatsLookup::default())); - -/// Aggregate stats for individual database instances, updated every second, averages are calculated every 15 -/// Used in SHOW STATS. -static LATEST_ADDRESS_STATS: Lazy> = - Lazy::new(|| ArcSwap::from_pointee(AddressStatsLookup::default())); +static POOL_STATS: Lazy>> = + Lazy::new(|| Arc::new(RwLock::new(PoolStatsLookup::default()))); /// The statistics reporter. An instance is given to each possible source of statistics, -/// e.g. clients, servers, connection pool. +/// e.g. client stats, server stats, connection pool stats. pub static REPORTER: Lazy> = Lazy::new(|| ArcSwap::from_pointee(Reporter::default())); @@ -45,989 +48,89 @@ pub static REPORTER: Lazy> = /// 15 seconds. static STAT_PERIOD: u64 = 15000; -/// The various states that a client can be in -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum ClientState { - Idle, - Waiting, - Active, -} -impl std::fmt::Display for ClientState { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match *self { - ClientState::Idle => write!(f, "idle"), - ClientState::Waiting => write!(f, "waiting"), - ClientState::Active => write!(f, "active"), - } - } -} - -/// The various states that a server can be in -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum ServerState { - Login, - Active, - Tested, - Idle, -} -impl std::fmt::Display for ServerState { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match *self { - ServerState::Login => write!(f, "login"), - ServerState::Active => write!(f, "active"), - ServerState::Tested => write!(f, "tested"), - ServerState::Idle => write!(f, "idle"), - } - } -} - -/// Information we keep track off which can be queried by SHOW CLIENTS -#[derive(Debug, Clone)] -pub struct ClientInformation { - pub state: ClientState, - pub connect_time: Instant, - - /// A random integer assigned to the client and used by stats to track the client - pub client_id: i32, - - pub application_name: String, - pub username: String, - pub pool_name: String, - - /// Total time spent waiting for a connection from pool, measures in microseconds - pub total_wait_time: u64, - - pub transaction_count: u64, - pub query_count: u64, - pub error_count: u64, -} - -/// Information we keep track off which can be queried by SHOW SERVERS -#[derive(Debug, Clone)] -pub struct ServerInformation { - pub state: ServerState, - pub connect_time: Instant, - - /// A random integer assigned to the server and used by stats to track the server - pub server_id: i32, - - pub address_name: String, - pub address_id: usize, - - pub username: String, - pub pool_name: String, - pub application_name: String, - - pub bytes_sent: u64, - pub bytes_received: u64, - - pub transaction_count: u64, - pub query_count: u64, - pub error_count: u64, -} - -/// The names for the events reported -/// to the statistics collector. -#[derive(Debug, Clone)] -enum EventName { - CheckoutTime { - client_id: i32, - server_id: i32, - }, - Query { - client_id: i32, - server_id: i32, - duration_ms: u128, - }, - Transaction { - client_id: i32, - server_id: i32, - }, - - DataSentToServer { - server_id: i32, - }, - DataReceivedFromServer { - server_id: i32, - }, - - ClientRegistered { - client_id: i32, - pool_name: String, - username: String, - application_name: String, - }, - ClientIdle { - client_id: i32, - }, - ClientWaiting { - client_id: i32, - }, - ClientActive { - client_id: i32, - #[allow(dead_code)] - server_id: i32, - }, - ClientDisconnecting { - client_id: i32, - }, - ClientCheckoutError { - client_id: i32, - #[allow(dead_code)] - address_id: usize, - }, - ClientBanError { - client_id: i32, - #[allow(dead_code)] - address_id: usize, - }, - - ServerRegistered { - server_id: i32, - address_id: usize, - address_name: String, - pool_name: String, - username: String, - }, - ServerLogin { - server_id: i32, - }, - ServerIdle { - server_id: i32, - }, - ServerTested { - server_id: i32, - }, - ServerActive { - #[allow(dead_code)] - client_id: i32, - server_id: i32, - }, - ServerDisconnecting { - server_id: i32, - }, - - UpdateStats { - pool_name: String, - username: String, - }, - UpdateAverages { - address_id: usize, - }, -} - -/// Event data sent to the collector -/// from clients and servers. -#[derive(Debug, Clone)] -pub struct Event { - /// The name of the event being reported. - name: EventName, - - /// The value being reported. Meaning differs based on event name. - value: i64, -} - /// The statistics reporter. An instance is given /// to each possible source of statistics, /// e.g. clients, servers, connection pool. -#[derive(Clone, Debug)] -pub struct Reporter { - tx: Sender, -} - -impl Default for Reporter { - fn default() -> Reporter { - let (tx, _rx) = channel(5); - Reporter { tx } - } -} +#[derive(Clone, Debug, Default)] +pub struct Reporter {} impl Reporter { - /// Create a new Reporter instance. - pub fn new(tx: Sender) -> Reporter { - Reporter { tx } - } - - /// Send statistics to the task keeping track of stats. - fn send(&self, event: Event) { - let name = event.name.clone(); - let result = self.tx.try_send(event.clone()); - - match result { - Ok(_) => trace!( - "{:?} event reported successfully, capacity: {} {:?}", - name, - self.tx.capacity(), - event - ), - - Err(err) => match err { - TrySendError::Full { .. } => error!("{:?} event dropped, buffer full", name), - TrySendError::Closed { .. } => error!("{:?} event dropped, channel closed", name), - }, - }; - } - - /// Report a query executed by a client against a server - pub fn query(&self, client_id: i32, server_id: i32, duration_ms: u128) { - let event = Event { - name: EventName::Query { - client_id, - server_id, - duration_ms, - }, - value: 1, - }; - self.send(event); - } - - /// Report a transaction executed by a client a server - /// we report each individual queries outside a transaction as a transaction - /// We only count the initial BEGIN as a transaction, all queries within do not - /// count as transactions - pub fn transaction(&self, client_id: i32, server_id: i32) { - let event = Event { - name: EventName::Transaction { - client_id, - server_id, - }, - value: 1, - }; - self.send(event); - } - - /// Report data sent to a server - pub fn data_sent(&self, amount_bytes: usize, server_id: i32) { - let event = Event { - name: EventName::DataSentToServer { server_id }, - value: amount_bytes as i64, - }; - self.send(event) - } - - /// Report data received from a server - pub fn data_received(&self, amount_bytes: usize, server_id: i32) { - let event = Event { - name: EventName::DataReceivedFromServer { server_id }, - value: amount_bytes as i64, - }; - self.send(event) - } - - /// Reportes the time spent by a client waiting to get a healthy connection from the pool - pub fn checkout_time(&self, microseconds: u128, client_id: i32, server_id: i32) { - let event = Event { - name: EventName::CheckoutTime { - client_id, - server_id, - }, - value: microseconds as i64, - }; - self.send(event) - } - /// Register a client with the stats system. The stats system uses client_id /// to track and aggregate statistics from all source that relate to that client - pub fn client_register( - &self, - client_id: i32, - pool_name: String, - username: String, - app_name: String, - ) { - let event = Event { - name: EventName::ClientRegistered { - client_id, - pool_name, - username, - application_name: app_name, - }, - value: 1, - }; - self.send(event); - } - - /// Reports a client is waiting for a connection - pub fn client_waiting(&self, client_id: i32) { - let event = Event { - name: EventName::ClientWaiting { client_id }, - value: 1, - }; - self.send(event) - } - - /// Reports a client has had the server assigned to it be banned - pub fn client_ban_error(&self, client_id: i32, address_id: usize) { - let event = Event { - name: EventName::ClientBanError { - client_id, - address_id, - }, - value: 1, - }; - self.send(event) - } - - /// Reports a client has failed to obtain a connection from a connection pool - pub fn client_checkout_error(&self, client_id: i32, address_id: usize) { - let event = Event { - name: EventName::ClientCheckoutError { - client_id, - address_id, - }, - value: 1, - }; - self.send(event) - } - - /// Reports a client is done waiting for a connection and is about to query the server. - pub fn client_active(&self, client_id: i32, server_id: i32) { - let event = Event { - name: EventName::ClientActive { - client_id, - server_id, - }, - value: 1, - }; - self.send(event) - } + fn client_register(&self, client_id: i32, stats: Arc) { + if CLIENT_STATS.read().get(&client_id).is_some() { + warn!("Client {:?} was double registered!", client_id); + return; + } - /// Reports a client is done querying the server and is no longer assigned a server connection - pub fn client_idle(&self, client_id: i32) { - let event = Event { - name: EventName::ClientIdle { client_id }, - value: 1, - }; - self.send(event) + CLIENT_STATS.write().insert(client_id, stats); } /// Reports a client is disconecting from the pooler. - pub fn client_disconnecting(&self, client_id: i32) { - let event = Event { - name: EventName::ClientDisconnecting { client_id }, - value: 1, - }; - self.send(event) + fn client_disconnecting(&self, client_id: i32) { + CLIENT_STATS.write().remove(&client_id); } /// Register a server connection with the stats system. The stats system uses server_id /// to track and aggregate statistics from all source that relate to that server - pub fn server_register( - &self, - server_id: i32, - address_id: usize, - address_name: String, - pool_name: String, - username: String, - ) { - let event = Event { - name: EventName::ServerRegistered { - server_id, - address_id, - address_name, - pool_name, - username, - }, - value: 1, - }; - self.send(event); - } - - /// Reports a server connection has been assigned to a client that - /// is about to query the server - pub fn server_active(&self, client_id: i32, server_id: i32) { - let event = Event { - name: EventName::ServerActive { - client_id, - server_id, - }, - value: 1, - }; - self.send(event) - } - - /// Reports a server connection is no longer assigned to a client - /// and is available for the next client to pick it up - pub fn server_idle(&self, server_id: i32) { - let event = Event { - name: EventName::ServerIdle { server_id }, - value: 1, - }; - self.send(event) - } - - /// Reports a server connection is attempting to login. - pub fn server_login(&self, server_id: i32) { - let event = Event { - name: EventName::ServerLogin { server_id }, - value: 1, - }; - self.send(event) + fn server_register(&self, server_id: i32, stats: Arc) { + SERVER_STATS.write().insert(server_id, stats); } - - /// Reports a server connection is being tested before being given to a client. - pub fn server_tested(&self, server_id: i32) { - let event = Event { - name: EventName::ServerTested { server_id }, - value: 1, - }; - - self.send(event) + /// Reports a server connection is disconecting from the pooler. + fn server_disconnecting(&self, server_id: i32) { + SERVER_STATS.write().remove(&server_id); } - /// Reports a server connection is disconecting from the pooler. - pub fn server_disconnecting(&self, server_id: i32) { - let event = Event { - name: EventName::ServerDisconnecting { server_id }, - value: 1, - }; - self.send(event) + /// Register a pool with the stats system. + fn pool_register(&self, identifier: PoolIdentifier, stats: Arc) { + POOL_STATS + .write() + .insert((identifier.db, identifier.user), stats); } } -/// The statistics collector which is receiving statistics -/// from clients, servers, and the connection pool. There is -/// only one collector (kind of like a singleton). -/// The collector can trigger events on its own, e.g. -/// it updates aggregates every second and averages every -/// 15 seconds. -pub struct Collector { - rx: Receiver, - tx: Sender, -} +/// The statistics collector which used for calculating averages +/// There is only one collector (kind of like a singleton) +/// it updates averages every 15 seconds. +#[derive(Default)] +pub struct Collector {} impl Collector { - /// Create a new collector instance. There should only be one instance - /// at a time. This is ensured by mpsc which allows only one receiver. - pub fn new(rx: Receiver, tx: Sender) -> Collector { - Collector { rx, tx } - } - /// The statistics collection handler. It will collect statistics /// for `address_id`s starting at 0 up to `addresses`. pub async fn collect(&mut self) { info!("Events reporter started"); - let mut client_states = ClientStatesLookup::default(); - let mut server_states = ServerStatesLookup::default(); - let mut pool_stat_lookup = PoolStatsLookup::default(); - - let mut address_stat_lookup = AddressStatsLookup::default(); - let mut address_old_stat_lookup = AddressStatsLookup::default(); - - let tx = self.tx.clone(); - tokio::task::spawn(async move { - let mut interval = - tokio::time::interval(tokio::time::Duration::from_millis(STAT_PERIOD / 15)); - loop { - interval.tick().await; - for (user_pool, _) in get_all_pools() { - let _ = tx.try_send(Event { - name: EventName::UpdateStats { - pool_name: user_pool.db, - username: user_pool.user, - }, - value: 0, - }); - } - } - }); - - let tx = self.tx.clone(); tokio::task::spawn(async move { let mut interval = tokio::time::interval(tokio::time::Duration::from_millis(STAT_PERIOD)); loop { interval.tick().await; - for address_id in 0..get_number_of_addresses() { - let _ = tx.try_send(Event { - name: EventName::UpdateAverages { address_id }, - value: 0, - }); + + for stats in SERVER_STATS.read().values() { + stats.address_stats().update_averages(); } } }); - - // The collector loop - loop { - let stat = match self.rx.recv().await { - Some(stat) => stat, - None => { - info!("Events collector is shutting down"); - return; - } - }; - - // Some are counters, some are gauges... - match stat.name { - EventName::Query { - client_id, - server_id, - duration_ms, - } => { - // Update client stats - let app_name = match client_states.get_mut(&client_id) { - Some(client_info) => { - client_info.query_count += stat.value as u64; - client_info.application_name.to_string() - } - None => String::from("Undefined"), - }; - - // Update server stats and pool aggergation stats - match server_states.get_mut(&server_id) { - Some(server_info) => { - server_info.query_count += stat.value as u64; - server_info.application_name = app_name; - - let address_stats = address_stat_lookup - .entry(server_info.address_id) - .or_insert_with(HashMap::default); - let counter = address_stats - .entry("total_query_count".to_string()) - .or_insert(0); - *counter += stat.value; - - let duration = address_stats - .entry("total_query_time".to_string()) - .or_insert(0); - *duration += duration_ms as i64; - } - None => (), - } - } - - EventName::Transaction { - client_id, - server_id, - } => { - // Update client stats - let app_name = match client_states.get_mut(&client_id) { - Some(client_info) => { - client_info.transaction_count += stat.value as u64; - client_info.application_name.to_string() - } - None => String::from("Undefined"), - }; - - // Update server stats and pool aggergation stats - match server_states.get_mut(&server_id) { - Some(server_info) => { - server_info.transaction_count += stat.value as u64; - server_info.application_name = app_name; - - let address_stats = address_stat_lookup - .entry(server_info.address_id) - .or_insert_with(HashMap::default); - let counter = address_stats - .entry("total_xact_count".to_string()) - .or_insert(0); - *counter += stat.value; - } - None => (), - } - } - - EventName::DataSentToServer { server_id } => { - // Update server stats and address aggergation stats - match server_states.get_mut(&server_id) { - Some(server_info) => { - server_info.bytes_sent += stat.value as u64; - - let address_stats = address_stat_lookup - .entry(server_info.address_id) - .or_insert_with(HashMap::default); - let counter = - address_stats.entry("total_sent".to_string()).or_insert(0); - *counter += stat.value; - } - None => (), - } - } - - EventName::DataReceivedFromServer { server_id } => { - // Update server states and address aggergation stats - match server_states.get_mut(&server_id) { - Some(server_info) => { - server_info.bytes_received += stat.value as u64; - - let address_stats = address_stat_lookup - .entry(server_info.address_id) - .or_insert_with(HashMap::default); - let counter = address_stats - .entry("total_received".to_string()) - .or_insert(0); - *counter += stat.value; - } - None => (), - } - } - - EventName::CheckoutTime { - client_id, - server_id, - } => { - // Update client stats - let app_name = match client_states.get_mut(&client_id) { - Some(client_info) => { - client_info.total_wait_time += stat.value as u64; - client_info.application_name.to_string() - } - None => String::from("Undefined"), - }; - - // Update server stats and address aggergation stats - match server_states.get_mut(&server_id) { - Some(server_info) => { - server_info.application_name = app_name; - - let address_stats = address_stat_lookup - .entry(server_info.address_id) - .or_insert_with(HashMap::default); - let counter = address_stats - .entry("total_wait_time".to_string()) - .or_insert(0); - *counter += stat.value; - - let pool_stats = pool_stat_lookup - .entry(( - server_info.pool_name.clone(), - server_info.username.clone(), - )) - .or_insert_with(HashMap::default); - - // We record max wait in microseconds, we do the pgbouncer second/microsecond split on admin - let old_microseconds = - pool_stats.entry("maxwait_us".to_string()).or_insert(0); - if stat.value > *old_microseconds { - *old_microseconds = stat.value; - } - } - None => (), - } - } - - EventName::ClientRegistered { - client_id, - pool_name, - username, - application_name, - } => { - match client_states.get_mut(&client_id) { - Some(_) => warn!("Client {:?} was double registered!", client_id), - None => { - client_states.insert( - client_id, - ClientInformation { - state: ClientState::Idle, - connect_time: Instant::now(), - client_id, - pool_name: pool_name.clone(), - username: username.clone(), - application_name: application_name.clone(), - total_wait_time: 0, - transaction_count: 0, - query_count: 0, - error_count: 0, - }, - ); - } - }; - } - - EventName::ClientBanError { - client_id, - address_id, - } => { - match client_states.get_mut(&client_id) { - Some(client_info) => { - client_info.state = ClientState::Idle; - client_info.error_count += stat.value as u64; - } - None => warn!("Got event {:?} for unregistered client", stat.name), - } - - // Update address aggregation stats - let address_stats = address_stat_lookup - .entry(address_id) - .or_insert_with(HashMap::default); - let counter = address_stats.entry("total_errors".to_string()).or_insert(0); - *counter += stat.value; - } - - EventName::ClientCheckoutError { - client_id, - address_id, - } => { - match client_states.get_mut(&client_id) { - Some(client_info) => { - client_info.state = ClientState::Idle; - client_info.error_count += stat.value as u64; - } - None => warn!("Got event {:?} for unregistered client", stat.name), - } - - // Update address aggregation stats - let address_stats = address_stat_lookup - .entry(address_id) - .or_insert_with(HashMap::default); - let counter = address_stats.entry("total_errors".to_string()).or_insert(0); - *counter += stat.value; - } - - EventName::ClientIdle { client_id } => { - match client_states.get_mut(&client_id) { - Some(client_state) => client_state.state = ClientState::Idle, - None => warn!("Got event {:?} for unregistered client", stat.name), - }; - } - - EventName::ClientWaiting { client_id } => { - match client_states.get_mut(&client_id) { - Some(client_state) => client_state.state = ClientState::Waiting, - None => warn!("Got event {:?} for unregistered client", stat.name), - }; - } - - EventName::ClientActive { - client_id, - server_id: _, - } => { - match client_states.get_mut(&client_id) { - Some(client_state) => client_state.state = ClientState::Active, - None => warn!("Got event {:?} for unregistered client", stat.name), - }; - } - - EventName::ClientDisconnecting { client_id } => { - client_states.remove(&client_id); - } - - EventName::ServerRegistered { - address_name, - server_id, - address_id, - pool_name, - username, - } => { - server_states.insert( - server_id, - ServerInformation { - address_id, - address_name, - server_id, - username, - pool_name, - - state: ServerState::Idle, - application_name: String::from("Undefined"), - connect_time: Instant::now(), - bytes_sent: 0, - bytes_received: 0, - transaction_count: 0, - query_count: 0, - error_count: 0, - }, - ); - } - - EventName::ServerLogin { server_id } => { - match server_states.get_mut(&server_id) { - Some(server_state) => { - server_state.state = ServerState::Login; - server_state.application_name = String::from("Undefined"); - } - None => warn!("Got event {:?} for unregistered server", stat.name), - }; - } - - EventName::ServerTested { server_id } => { - match server_states.get_mut(&server_id) { - Some(server_state) => { - server_state.state = ServerState::Tested; - server_state.application_name = String::from("Undefined"); - } - None => warn!("Got event {:?} for unregistered server", stat.name), - }; - } - - EventName::ServerIdle { server_id } => { - match server_states.get_mut(&server_id) { - Some(server_state) => { - server_state.state = ServerState::Idle; - server_state.application_name = String::from("Undefined"); - } - None => warn!("Got event {:?} for unregistered server", stat.name), - }; - } - - EventName::ServerActive { - client_id, - server_id, - } => { - // Update client stats - let app_name = match client_states.get_mut(&client_id) { - Some(client_info) => client_info.application_name.to_string(), - None => String::from("Undefined"), - }; - - // Update server stats - match server_states.get_mut(&server_id) { - Some(server_state) => { - server_state.state = ServerState::Active; - server_state.application_name = app_name; - } - None => warn!("Got event {:?} for unregistered server", stat.name), - }; - } - - EventName::ServerDisconnecting { server_id } => { - server_states.remove(&server_id); - } - - EventName::UpdateStats { - pool_name, - username, - } => { - let pool_stats = pool_stat_lookup - .entry((pool_name.clone(), username.clone())) - .or_insert_with(HashMap::default); - - // These are re-calculated every iteration of the loop, so we don't want to add values - // from the last iteration. - for stat in &[ - "cl_active", - "cl_waiting", - "cl_idle", - "sv_idle", - "sv_active", - "sv_tested", - "sv_login", - ] { - pool_stats.insert(stat.to_string(), 0); - } - - for (_, client_info) in client_states.iter() { - if client_info.pool_name != pool_name || client_info.username != username { - continue; - } - match client_info.state { - ClientState::Idle => { - let counter = pool_stats.entry("cl_idle".to_string()).or_insert(0); - *counter += 1; - } - ClientState::Waiting => { - let counter = - pool_stats.entry("cl_waiting".to_string()).or_insert(0); - *counter += 1; - } - ClientState::Active => { - let counter = - pool_stats.entry("cl_active".to_string()).or_insert(0); - *counter += 1; - } - }; - } - - for (_, server_info) in server_states.iter() { - if server_info.pool_name != pool_name || server_info.username != username { - continue; - } - match server_info.state { - ServerState::Login => { - let counter = pool_stats.entry("sv_login".to_string()).or_insert(0); - *counter += 1; - } - ServerState::Tested => { - let counter = - pool_stats.entry("sv_tested".to_string()).or_insert(0); - *counter += 1; - } - ServerState::Active => { - let counter = - pool_stats.entry("sv_active".to_string()).or_insert(0); - *counter += 1; - } - ServerState::Idle => { - let counter = pool_stats.entry("sv_idle".to_string()).or_insert(0); - *counter += 1; - } - }; - } - - // The following calls publish the internal stats making it visible - // to clients using admin database to issue queries like `SHOW STATS` - LATEST_CLIENT_STATS.store(Arc::new(client_states.clone())); - LATEST_SERVER_STATS.store(Arc::new(server_states.clone())); - LATEST_POOL_STATS.store(Arc::new(pool_stat_lookup.clone())); - - // Clear maxwait after reporting - pool_stat_lookup - .entry((pool_name.clone(), username.clone())) - .or_insert_with(HashMap::default) - .insert("maxwait_us".to_string(), 0); - } - - EventName::UpdateAverages { address_id } => { - let stats = address_stat_lookup - .entry(address_id) - .or_insert_with(HashMap::default); - let old_stats = address_old_stat_lookup - .entry(address_id) - .or_insert_with(HashMap::default); - - // Calculate averages - for stat in &[ - "avg_query_count", - "avg_query_time", - "avg_recv", - "avg_sent", - "avg_errors", - "avg_xact_time", - "avg_xact_count", - "avg_wait_time", - ] { - let total_name = match stat { - &"avg_recv" => "total_received".to_string(), // Because PgBouncer is saving bytes - _ => stat.replace("avg_", "total_"), - }; - - let old_value = old_stats.entry(total_name.clone()).or_insert(0); - let new_value = stats.get(total_name.as_str()).unwrap_or(&0).to_owned(); - let avg = (new_value - *old_value) / (STAT_PERIOD as i64 / 1_000); // Avg / second - - stats.insert(stat.to_string(), avg); - *old_value = new_value; - } - LATEST_ADDRESS_STATS.store(Arc::new(address_stat_lookup.clone())); - } - }; - } } } -/// Get a snapshot of client statistics. Updated once a second +/// Get a snapshot of client statistics. /// by the `Collector`. pub fn get_client_stats() -> ClientStatesLookup { - (*(*LATEST_CLIENT_STATS.load())).clone() + CLIENT_STATS.read().clone() } -/// Get a snapshot of server statistics. Updated once a second +/// Get a snapshot of server statistics. /// by the `Collector`. pub fn get_server_stats() -> ServerStatesLookup { - (*(*LATEST_SERVER_STATS.load())).clone() + SERVER_STATS.read().clone() } -/// Get a snapshot of pool statistics. Updated once a second +/// Get a snapshot of pool statistics. /// by the `Collector`. pub fn get_pool_stats() -> PoolStatsLookup { - (*(*LATEST_POOL_STATS.load())).clone() -} - -/// Get a snapshot of address statistics. Updated once a second -/// by the `Collector`. -pub fn get_address_stats() -> AddressStatsLookup { - (*(*LATEST_ADDRESS_STATS.load())).clone() + POOL_STATS.read().clone() } /// Get the statistics reporter used to update stats across the pools/clients. diff --git a/src/stats/address.rs b/src/stats/address.rs new file mode 100644 index 00000000..a5759e10 --- /dev/null +++ b/src/stats/address.rs @@ -0,0 +1,149 @@ +use log::warn; +use std::sync::atomic::*; +use std::sync::Arc; + +/// Internal address stats +#[derive(Debug, Clone, Default)] +pub struct AddressStats { + pub total_xact_count: Arc, + pub total_query_count: Arc, + pub total_received: Arc, + pub total_sent: Arc, + pub total_xact_time: Arc, + pub total_query_time: Arc, + pub total_wait_time: Arc, + pub total_errors: Arc, + pub avg_query_count: Arc, + pub avg_query_time: Arc, + pub avg_recv: Arc, + pub avg_sent: Arc, + pub avg_errors: Arc, + pub avg_xact_time: Arc, + pub avg_xact_count: Arc, + pub avg_wait_time: Arc, +} + +impl IntoIterator for AddressStats { + type Item = (String, u64); + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + vec![ + ( + "total_xact_count".to_string(), + self.total_xact_count.load(Ordering::Relaxed), + ), + ( + "total_query_count".to_string(), + self.total_query_count.load(Ordering::Relaxed), + ), + ( + "total_received".to_string(), + self.total_received.load(Ordering::Relaxed), + ), + ( + "total_sent".to_string(), + self.total_sent.load(Ordering::Relaxed), + ), + ( + "total_xact_time".to_string(), + self.total_xact_time.load(Ordering::Relaxed), + ), + ( + "total_query_time".to_string(), + self.total_query_time.load(Ordering::Relaxed), + ), + ( + "total_wait_time".to_string(), + self.total_wait_time.load(Ordering::Relaxed), + ), + ( + "total_errors".to_string(), + self.total_errors.load(Ordering::Relaxed), + ), + ( + "avg_xact_count".to_string(), + self.avg_xact_count.load(Ordering::Relaxed), + ), + ( + "avg_query_count".to_string(), + self.avg_query_count.load(Ordering::Relaxed), + ), + ( + "avg_recv".to_string(), + self.avg_recv.load(Ordering::Relaxed), + ), + ( + "avg_sent".to_string(), + self.avg_sent.load(Ordering::Relaxed), + ), + ( + "avg_errors".to_string(), + self.avg_errors.load(Ordering::Relaxed), + ), + ( + "avg_xact_time".to_string(), + self.avg_xact_time.load(Ordering::Relaxed), + ), + ( + "avg_query_time".to_string(), + self.avg_query_time.load(Ordering::Relaxed), + ), + ( + "avg_wait_time".to_string(), + self.avg_wait_time.load(Ordering::Relaxed), + ), + ] + .into_iter() + } +} + +impl AddressStats { + pub fn error(&self) { + self.total_errors.fetch_add(1, Ordering::Relaxed); + } + + pub fn update_averages(&self) { + let (totals, averages) = self.fields_iterators(); + for data in totals.iter().zip(averages.iter()) { + let (total, average) = data; + if let Err(err) = average.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |avg| { + let total = total.load(Ordering::Relaxed); + let avg = (total - avg) / (crate::stats::STAT_PERIOD / 1_000); // Avg / second + Some(avg) + }) { + warn!("Could not update averages for addresses stats, {:?}", err); + } + } + } + + pub fn populate_row(&self, row: &mut Vec) { + for (_key, value) in self.clone() { + row.push(value.to_string()); + } + } + + fn fields_iterators(&self) -> (Vec>, Vec>) { + let mut totals: Vec> = Vec::new(); + let mut averages: Vec> = Vec::new(); + + totals.push(self.total_xact_count.clone()); + averages.push(self.avg_xact_count.clone()); + totals.push(self.total_query_count.clone()); + averages.push(self.avg_query_count.clone()); + totals.push(self.total_received.clone()); + averages.push(self.avg_recv.clone()); + totals.push(self.total_sent.clone()); + averages.push(self.avg_sent.clone()); + totals.push(self.total_xact_time.clone()); + averages.push(self.avg_xact_time.clone()); + totals.push(self.total_query_time.clone()); + averages.push(self.avg_query_time.clone()); + totals.push(self.total_wait_time.clone()); + averages.push(self.avg_wait_time.clone()); + totals.push(self.total_errors.clone()); + averages.push(self.avg_errors.clone()); + + (totals, averages) + } +} diff --git a/src/stats/client.rs b/src/stats/client.rs new file mode 100644 index 00000000..89235068 --- /dev/null +++ b/src/stats/client.rs @@ -0,0 +1,182 @@ +use super::PoolStats; +use super::{get_reporter, Reporter}; +use atomic_enum::atomic_enum; +use std::sync::atomic::*; +use std::sync::Arc; +use tokio::time::Instant; +/// The various states that a client can be in +#[atomic_enum] +#[derive(PartialEq)] +pub enum ClientState { + Idle = 0, + Waiting, + Active, +} +impl std::fmt::Display for ClientState { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match *self { + ClientState::Idle => write!(f, "idle"), + ClientState::Waiting => write!(f, "waiting"), + ClientState::Active => write!(f, "active"), + } + } +} + +#[derive(Debug, Clone)] +/// Information we keep track of which can be queried by SHOW CLIENTS +pub struct ClientStats { + /// A random integer assigned to the client and used by stats to track the client + client_id: i32, + + /// Data associated with the client, not writable, only set when we construct the ClientStat + application_name: String, + username: String, + pool_name: String, + connect_time: Instant, + + pool_stats: Arc, + reporter: Reporter, + + /// Total time spent waiting for a connection from pool, measures in microseconds + pub total_wait_time: Arc, + + /// Current state of the client + pub state: Arc, + + /// Number of transactions executed by this client + pub transaction_count: Arc, + + /// Number of queries executed by this client + pub query_count: Arc, + + /// Number of errors made by this client + pub error_count: Arc, +} + +impl Default for ClientStats { + fn default() -> Self { + ClientStats { + client_id: 0, + connect_time: Instant::now(), + application_name: String::new(), + username: String::new(), + pool_name: String::new(), + pool_stats: Arc::new(PoolStats::default()), + total_wait_time: Arc::new(AtomicU64::new(0)), + state: Arc::new(AtomicClientState::new(ClientState::Idle)), + transaction_count: Arc::new(AtomicU64::new(0)), + query_count: Arc::new(AtomicU64::new(0)), + error_count: Arc::new(AtomicU64::new(0)), + reporter: get_reporter(), + } + } +} + +impl ClientStats { + pub fn new( + client_id: i32, + application_name: &str, + username: &str, + pool_name: &str, + connect_time: Instant, + pool_stats: Arc, + ) -> Self { + Self { + client_id, + pool_stats, + connect_time, + application_name: application_name.to_string(), + username: username.to_string(), + pool_name: pool_name.to_string(), + ..Default::default() + } + } + + /// Reports a client is disconecting from the pooler and + /// update metrics on the corresponding pool. + pub fn disconnect(&self) { + self.reporter.client_disconnecting(self.client_id); + self.pool_stats + .client_disconnect(self.state.load(Ordering::Relaxed)) + } + + /// Register a client with the stats system. The stats system uses client_id + /// to track and aggregate statistics from all source that relate to that client + pub fn register(&self, stats: Arc) { + self.reporter.client_register(self.client_id, stats); + self.state.store(ClientState::Idle, Ordering::Relaxed); + self.pool_stats.cl_idle.fetch_add(1, Ordering::Relaxed); + } + + /// Reports a client is done querying the server and is no longer assigned a server connection + pub fn idle(&self) { + self.pool_stats + .client_idle(self.state.load(Ordering::Relaxed)); + self.state.store(ClientState::Idle, Ordering::Relaxed); + } + + /// Reports a client is waiting for a connection + pub fn waiting(&self) { + self.pool_stats + .client_waiting(self.state.load(Ordering::Relaxed)); + self.state.store(ClientState::Waiting, Ordering::Relaxed); + } + + /// Reports a client is done waiting for a connection and is about to query the server. + pub fn active(&self) { + self.pool_stats + .client_active(self.state.load(Ordering::Relaxed)); + self.state.store(ClientState::Active, Ordering::Relaxed); + } + + /// Reports a client has failed to obtain a connection from a connection pool + pub fn checkout_error(&self) { + self.state.store(ClientState::Idle, Ordering::Relaxed); + } + + /// Reports a client has had the server assigned to it be banned + pub fn ban_error(&self) { + self.state.store(ClientState::Idle, Ordering::Relaxed); + self.error_count.fetch_add(1, Ordering::Relaxed); + } + + /// Reportes the time spent by a client waiting to get a healthy connection from the pool + pub fn checkout_time(&self, microseconds: u64) { + self.total_wait_time + .fetch_add(microseconds, Ordering::Relaxed); + } + + /// Report a query executed by a client against a server + pub fn query(&self) { + self.query_count.fetch_add(1, Ordering::Relaxed); + } + + /// Report a transaction executed by a client a server + /// we report each individual queries outside a transaction as a transaction + /// We only count the initial BEGIN as a transaction, all queries within do not + /// count as transactions + pub fn transaction(&self) { + self.transaction_count.fetch_add(1, Ordering::Relaxed); + } + + // Helper methods for show clients + pub fn connect_time(&self) -> Instant { + self.connect_time + } + + pub fn client_id(&self) -> i32 { + self.client_id + } + + pub fn application_name(&self) -> String { + self.application_name.clone() + } + + pub fn username(&self) -> String { + self.username.clone() + } + + pub fn pool_name(&self) -> String { + self.pool_name.clone() + } +} diff --git a/src/stats/pool.rs b/src/stats/pool.rs new file mode 100644 index 00000000..1b01ef2e --- /dev/null +++ b/src/stats/pool.rs @@ -0,0 +1,274 @@ +use crate::config::Pool; +use crate::config::PoolMode; +use crate::pool::PoolIdentifier; +use std::sync::atomic::*; +use std::sync::Arc; + +use super::get_reporter; +use super::Reporter; +use super::{ClientState, ServerState}; + +#[derive(Debug, Clone, Default)] +/// A struct that holds information about a Pool . +pub struct PoolStats { + // Pool identifier, cannot be changed after creating the instance + identifier: PoolIdentifier, + + // Pool Config, cannot be changed after creating the instance + config: Pool, + + // A reference to the global reporter. + reporter: Reporter, + + /// Counters (atomics) + pub cl_idle: Arc, + pub cl_active: Arc, + pub cl_waiting: Arc, + pub cl_cancel_req: Arc, + pub sv_active: Arc, + pub sv_idle: Arc, + pub sv_used: Arc, + pub sv_tested: Arc, + pub sv_login: Arc, + pub maxwait: Arc, +} + +impl IntoIterator for PoolStats { + type Item = (String, u64); + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + vec![ + ("cl_idle".to_string(), self.cl_idle.load(Ordering::Relaxed)), + ( + "cl_active".to_string(), + self.cl_active.load(Ordering::Relaxed), + ), + ( + "cl_waiting".to_string(), + self.cl_waiting.load(Ordering::Relaxed), + ), + ( + "cl_cancel_req".to_string(), + self.cl_cancel_req.load(Ordering::Relaxed), + ), + ( + "sv_active".to_string(), + self.sv_active.load(Ordering::Relaxed), + ), + ("sv_idle".to_string(), self.sv_idle.load(Ordering::Relaxed)), + ("sv_used".to_string(), self.sv_used.load(Ordering::Relaxed)), + ( + "sv_tested".to_string(), + self.sv_tested.load(Ordering::Relaxed), + ), + ( + "sv_login".to_string(), + self.sv_login.load(Ordering::Relaxed), + ), + ( + "maxwait".to_string(), + self.maxwait.load(Ordering::Relaxed) / 1_000_000, + ), + ( + "maxwait_us".to_string(), + self.maxwait.load(Ordering::Relaxed) % 1_000_000, + ), + ] + .into_iter() + } +} + +impl PoolStats { + pub fn new(identifier: PoolIdentifier, config: Pool) -> Self { + Self { + identifier, + config, + reporter: get_reporter(), + ..Default::default() + } + } + + // Getters + pub fn register(&self, stats: Arc) { + self.reporter.pool_register(self.identifier.clone(), stats); + } + + pub fn database(&self) -> String { + self.identifier.db.clone() + } + + pub fn user(&self) -> String { + self.identifier.user.clone() + } + + pub fn pool_mode(&self) -> PoolMode { + self.config.pool_mode + } + + /// Populates an array of strings with counters (used by admin in show pools) + pub fn populate_row(&self, row: &mut Vec) { + for (_key, value) in self.clone() { + row.push(value.to_string()); + } + } + + /// Deletes the maxwait counter, this is done everytime we obtain metrics + pub fn clear_maxwait(&self) { + self.maxwait.store(0, Ordering::Relaxed); + } + + /// Notified when a server of the pool enters login state. + /// + /// Arguments: + /// + /// `from`: The state of the server that notifies. + pub fn server_login(&self, from: ServerState) { + self.sv_login.fetch_add(1, Ordering::Relaxed); + if from != ServerState::Login { + self.decrease_from_server_state(from); + } + } + + /// Notified when a server of the pool become 'active' + /// + /// Arguments: + /// + /// `from`: The state of the server that notifies. + pub fn server_active(&self, from: ServerState) { + self.sv_active.fetch_add(1, Ordering::Relaxed); + if from != ServerState::Active { + self.decrease_from_server_state(from); + } + } + + /// Notified when a server of the pool become 'tested' + /// + /// Arguments: + /// + /// `from`: The state of the server that notifies. + pub fn server_tested(&self, from: ServerState) { + self.sv_tested.fetch_add(1, Ordering::Relaxed); + if from != ServerState::Tested { + self.decrease_from_server_state(from); + } + } + + /// Notified when a server of the pool become 'idle' + /// + /// Arguments: + /// + /// `from`: The state of the server that notifies. + pub fn server_idle(&self, from: ServerState) { + self.sv_idle.fetch_add(1, Ordering::Relaxed); + if from != ServerState::Idle { + self.decrease_from_server_state(from); + } + } + + /// Notified when a client of the pool become 'waiting' + /// + /// Arguments: + /// + /// `from`: The state of the client that notifies. + pub fn client_waiting(&self, from: ClientState) { + if from != ClientState::Waiting { + self.cl_waiting.fetch_add(1, Ordering::Relaxed); + self.decrease_from_client_state(from); + } + } + + /// Notified when a client of the pool become 'active' + /// + /// Arguments: + /// + /// `from`: The state of the client that notifies. + pub fn client_active(&self, from: ClientState) { + if from != ClientState::Active { + self.cl_active.fetch_add(1, Ordering::Relaxed); + self.decrease_from_client_state(from); + } + } + + /// Notified when a client of the pool become 'idle' + /// + /// Arguments: + /// + /// `from`: The state of the client that notifies. + pub fn client_idle(&self, from: ClientState) { + if from != ClientState::Idle { + self.cl_idle.fetch_add(1, Ordering::Relaxed); + self.decrease_from_client_state(from); + } + } + + /// Notified when a client disconnects. + /// + /// Arguments: + /// + /// `from`: The state of the client that notifies. + pub fn client_disconnect(&self, from: ClientState) { + let counter = match from { + ClientState::Idle => &self.cl_idle, + ClientState::Waiting => &self.cl_waiting, + ClientState::Active => &self.cl_active, + }; + + Self::decrease_counter(counter.clone()); + } + + /// Notified when a server disconnects. + /// + /// Arguments: + /// + /// `from`: The state of the client that notifies. + pub fn server_disconnect(&self, from: ServerState) { + let counter = match from { + ServerState::Active => &self.sv_active, + ServerState::Idle => &self.sv_idle, + ServerState::Login => &self.sv_login, + ServerState::Tested => &self.sv_tested, + }; + Self::decrease_counter(counter.clone()); + } + + // helpers for counter decrease + fn decrease_from_server_state(&self, from: ServerState) { + let counter = match from { + ServerState::Tested => &self.sv_tested, + ServerState::Active => &self.sv_active, + ServerState::Idle => &self.sv_idle, + ServerState::Login => &self.sv_login, + }; + Self::decrease_counter(counter.clone()); + } + + fn decrease_from_client_state(&self, from: ClientState) { + let counter = match from { + ClientState::Active => &self.cl_active, + ClientState::Idle => &self.cl_idle, + ClientState::Waiting => &self.cl_waiting, + }; + Self::decrease_counter(counter.clone()); + } + + fn decrease_counter(value: Arc) { + if value.load(Ordering::Relaxed) > 0 { + value.fetch_sub(1, Ordering::Relaxed); + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_decrease() { + let stat: PoolStats = PoolStats::default(); + stat.server_login(ServerState::Login); + stat.server_idle(ServerState::Login); + assert_eq!(stat.sv_login.load(Ordering::Relaxed), 0); + assert_eq!(stat.sv_idle.load(Ordering::Relaxed), 1); + } +} diff --git a/src/stats/server.rs b/src/stats/server.rs new file mode 100644 index 00000000..009e9b57 --- /dev/null +++ b/src/stats/server.rs @@ -0,0 +1,226 @@ +use super::AddressStats; +use super::PoolStats; +use super::{get_reporter, Reporter}; +use crate::config::Address; +use atomic_enum::atomic_enum; +use parking_lot::RwLock; +use std::sync::atomic::*; +use std::sync::Arc; +use tokio::time::Instant; + +/// The various states that a server can be in +#[atomic_enum] +#[derive(PartialEq)] +pub enum ServerState { + Login = 0, + Active, + Tested, + Idle, +} +impl std::fmt::Display for ServerState { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match *self { + ServerState::Login => write!(f, "login"), + ServerState::Active => write!(f, "active"), + ServerState::Tested => write!(f, "tested"), + ServerState::Idle => write!(f, "idle"), + } + } +} + +/// Information we keep track of which can be queried by SHOW SERVERS +#[derive(Debug, Clone)] +pub struct ServerStats { + /// A random integer assigned to the server and used by stats to track the server + server_id: i32, + + /// Context information, only to be read + address: Address, + connect_time: Instant, + + pool_stats: Arc, + reporter: Reporter, + + /// Data + pub application_name: Arc>, + pub state: Arc, + pub bytes_sent: Arc, + pub bytes_received: Arc, + pub transaction_count: Arc, + pub query_count: Arc, + pub error_count: Arc, +} + +impl Default for ServerStats { + fn default() -> Self { + ServerStats { + server_id: 0, + application_name: Arc::new(RwLock::new(String::new())), + address: Address::default(), + pool_stats: Arc::new(PoolStats::default()), + connect_time: Instant::now(), + state: Arc::new(AtomicServerState::new(ServerState::Login)), + bytes_sent: Arc::new(AtomicU64::new(0)), + bytes_received: Arc::new(AtomicU64::new(0)), + transaction_count: Arc::new(AtomicU64::new(0)), + query_count: Arc::new(AtomicU64::new(0)), + error_count: Arc::new(AtomicU64::new(0)), + reporter: get_reporter(), + } + } +} + +impl ServerStats { + pub fn new(address: Address, pool_stats: Arc, connect_time: Instant) -> Self { + Self { + address, + pool_stats, + connect_time, + server_id: rand::random::(), + ..Default::default() + } + } + + pub fn server_id(&self) -> i32 { + self.server_id + } + + /// Register a server connection with the stats system. The stats system uses server_id + /// to track and aggregate statistics from all source that relate to that server + // Delegates to reporter + pub fn register(&self, stats: Arc) { + self.reporter.server_register(self.server_id, stats); + self.login(); + } + + /// Reports a server connection is no longer assigned to a client + /// and is available for the next client to pick it up + pub fn idle(&self) { + self.pool_stats + .server_idle(self.state.load(Ordering::Relaxed)); + + self.state.store(ServerState::Idle, Ordering::Relaxed); + self.set_undefined_application(); + } + + /// Reports a server connection is disconecting from the pooler. + /// Also updates metrics on the pool regarding server usage. + pub fn disconnect(&self) { + self.reporter.server_disconnecting(self.server_id); + self.pool_stats + .server_disconnect(self.state.load(Ordering::Relaxed)) + } + + /// Reports a server connection is being tested before being given to a client. + pub fn tested(&self) { + self.set_undefined_application(); + self.pool_stats + .server_tested(self.state.load(Ordering::Relaxed)); + self.state.store(ServerState::Tested, Ordering::Relaxed); + } + + /// Reports a server connection is attempting to login. + pub fn login(&self) { + self.pool_stats + .server_login(self.state.load(Ordering::Relaxed)); + self.state.store(ServerState::Login, Ordering::Relaxed); + self.set_undefined_application(); + } + + /// Reports a server connection has been assigned to a client that + /// is about to query the server + pub fn active(&self, application_name: String) { + self.pool_stats + .server_active(self.state.load(Ordering::Relaxed)); + self.state.store(ServerState::Active, Ordering::Relaxed); + self.set_application(application_name); + } + + pub fn address_stats(&self) -> Arc { + self.address.stats.clone() + } + + // Helper methods for show_servers + pub fn pool_name(&self) -> String { + self.pool_stats.database() + } + + pub fn username(&self) -> String { + self.pool_stats.user() + } + + pub fn address_name(&self) -> String { + self.address.name() + } + + pub fn connect_time(&self) -> Instant { + self.connect_time + } + + fn set_application(&self, name: String) { + let mut application_name = self.application_name.write(); + *application_name = name; + } + + fn set_undefined_application(&self) { + self.set_application(String::from("Undefined")) + } + + pub fn checkout_time(&self, microseconds: u64, application_name: String) { + // Update server stats and address aggergation stats + self.set_application(application_name); + self.address + .stats + .total_wait_time + .fetch_add(microseconds, Ordering::Relaxed); + self.pool_stats + .maxwait + .fetch_max(microseconds, Ordering::Relaxed); + } + + /// Report a query executed by a client against a server + pub fn query(&self, milliseconds: u64, application_name: &str) { + self.set_application(application_name.to_string()); + let address_stats = self.address_stats(); + address_stats + .total_query_count + .fetch_add(1, Ordering::Relaxed); + address_stats + .total_query_time + .fetch_add(milliseconds, Ordering::Relaxed); + } + + /// Report a transaction executed by a client a server + /// we report each individual queries outside a transaction as a transaction + /// We only count the initial BEGIN as a transaction, all queries within do not + /// count as transactions + pub fn transaction(&self, application_name: &str) { + self.set_application(application_name.to_string()); + + self.transaction_count.fetch_add(1, Ordering::Relaxed); + self.address + .stats + .total_xact_count + .fetch_add(1, Ordering::Relaxed); + } + + /// Report data sent to a server + pub fn data_sent(&self, amount_bytes: usize) { + self.bytes_sent + .fetch_add(amount_bytes as u64, Ordering::Relaxed); + self.address + .stats + .total_sent + .fetch_add(amount_bytes as u64, Ordering::Relaxed); + } + + /// Report data received from a server + pub fn data_received(&self, amount_bytes: usize) { + self.bytes_received + .fetch_add(amount_bytes as u64, Ordering::Relaxed); + self.address + .stats + .total_received + .fetch_add(amount_bytes as u64, Ordering::Relaxed); + } +} diff --git a/tests/ruby/admin_spec.rb b/tests/ruby/admin_spec.rb index f69c3df6..b611e8cb 100644 --- a/tests/ruby/admin_spec.rb +++ b/tests/ruby/admin_spec.rb @@ -176,6 +176,47 @@ end end + context "clients connects and disconnect normally" do + let(:processes) { Helpers::Pgcat.single_instance_setup("sharded_db", 2) } + + it 'shows the same number of clients before and after' do + clients_before = clients_connected_to_pool(processes: processes) + threads = [] + connections = Array.new(4) { PG::connect("#{pgcat_conn_str}?application_name=one_query") } + connections.each do |c| + threads << Thread.new { c.async_exec("SELECT 1") } + end + clients_between = clients_connected_to_pool(processes: processes) + expect(clients_before).not_to eq(clients_between) + connections.each(&:close) + clients_after = clients_connected_to_pool(processes: processes) + expect(clients_before).to eq(clients_after) + end + end + + context "clients connects and disconnect abruptly" do + let(:processes) { Helpers::Pgcat.single_instance_setup("sharded_db", 10) } + + it 'shows the same number of clients before and after' do + threads = [] + connections = Array.new(2) { PG::connect("#{pgcat_conn_str}?application_name=one_query") } + connections.each do |c| + threads << Thread.new { c.async_exec("SELECT 1") } + end + clients_before = clients_connected_to_pool(processes: processes) + random_string = (0...8).map { (65 + rand(26)).chr }.join + connection_string = "#{pgcat_conn_str}?application_name=#{random_string}" + faulty_client = Process.spawn("psql -Atx #{connection_string} >/dev/null") + sleep(1) + # psql starts two processes, we only know the pid of the parent, this + # ensure both are killed + `pkill -9 -f '#{random_string}'` + Process.wait(faulty_client) + clients_after = clients_connected_to_pool(processes: processes) + expect(clients_before).to eq(clients_after) + end + end + context "clients overwhelm server pools" do let(:processes) { Helpers::Pgcat.single_instance_setup("sharded_db", 2) } @@ -199,7 +240,7 @@ sleep(2.5) # Allow time for stats to update results = admin_conn.async_exec("SHOW POOLS")[0] - %w[cl_active cl_waiting cl_cancel_req sv_active sv_used sv_tested sv_login maxwait].each do |s| + %w[cl_active cl_waiting cl_cancel_req sv_active sv_used sv_tested sv_login].each do |s| raise StandardError, "Field #{s} was expected to be 0 but found to be #{results[s]}" if results[s] != "0" end expect(results["cl_idle"]).to eq("4") diff --git a/tests/ruby/capture b/tests/ruby/capture new file mode 100644 index 0000000000000000000000000000000000000000..268680ff5141f12abdc6e19d7056909c5cfd4075 GIT binary patch literal 40638 zcmeHQ33wDm7Vb$%0x zAadw(xCA!B3bKM52rI~aA3}f}C;YSMTWl zUsrW;C~wV@0IiC4<5NYeWzw|OgB$d(*Dgfs1Uqo6$`3FFR?&j#*w&i%gn7F~v(z+w z{i>#!a4X(jyjy#~`Zz#n5wN9RIr-20`2pH<&mU%z&PDv(F(Fzk{D4~-_%Y@$82%*( z{0M8)P&lU!Y|AY02R@Pa=QDi7JwH{4-a8oXSqEG}?@moQo+XS+Y6tO?)`w`{25M|` zuXo@{^G?LKwi59Jx0_A|fUdc4F5=<2_}!Dw1#s1G01ASAuFyX`0g)X0hQwa|GBUO zH~0B($~N!9{MRzX{5Q?DtwekM9nM>3(KNUWc>H-4`iuGRtQURv7|`hiXMa8f?fsq~ z(wk&&Xb#ge!=|DGYl4)RHY?E|#mULcuC0)l!rusNuXWzm#r+$^UA7!0qj}M9yZ4JcF z?j_Z<)^GxD_Whxn$rK#$^p!Oq)MCF!Cmg`|8M7t(cFmN1yUlid?wAGIT|K&Wi0ab1 zW#?A05r2+|?b@}|!_keR!$T3Y-TOK~4>Hir1iEPSn1yA6t^?2(57y9TgO<46&@@2Q zbliq17R^-i-h&8w*=GASH!EbQmb_rPTTNr1>(bR6(WGhfMopSEvu};f(ajq-4iD|n zvZFa7G9#mZnz?^cMndZF#PrDUP&_^>EhWvIo|>B3KXG_cLLwa6!yM5pJTyFXJt%p6 zsk-Nh&tlJC>7c9U2-;+s4$xQz+MYnAKH3PN#Vd4xf;mCcPy&_uNH^$I23n0krJ8O6 zP4{05&|Uh zQ&JKWGOP5?Oh^kFnwrpesHTN!CTz@fJwO`<=ioLmAJ5hxA8tfhpWwnnTCsYo= z)1JO!vqGlW?AKBIuXY9A+QYuvZwR^+xTh=yj%AK$)ko9%+>+hXL4jkLHY@CLufV!Y zj}>_Lo?vz5vICyD;-1^!f4PK>15u8ICMDkoRlQ^iEm-z%qFh@XTgbv=?7i1o;y;*pm6 zydK#MN@lHAv;0AKB54`*iY`KqplkGi)?uKd2~=7-Yyr^gKZR(kAhK{PgXPcLdOx!K zdUumKi`qhY_xggRUMlafl7kACRe9+tQ26y78kL+iG!qKI^u)xJsMPd<=8VL2Ec@6_ zGaMWqipux-G)(&&D303=V(oV?cRhCYw??%;hqiuRqjqUh0k-whT53ybeFP0& zD+LXP#{=9Yb12 z5#Rc#h%Xjqh-VaLnnpKplE#o0kLU9KrJ(?l#*kKdEy>R#JCelwe-rXwS(qKe{MQDG z`7ah`nEzB9c%q?En0aC?*JH`%B+;LX!Jo>)>^S<1JAG4Qxp_kkH>F6a4 zMnEq+{xN&G4|*B>A00JFcf8`g^dyxF(&|eM8kG!b5S`i{K{H|rR9b@|=m+11DF=s& z1|Vq9Sh_Y(7%*XgSc4?@FJZMuhr9xZ8U*oYH4Ik{4iyb#_|G}uyQx8X&ExGL?&+rC zYP|KK4$Ae* zBlfdMR>ritjJR^CuInT+7s;DpaA+3>q1`&pQkT&r2hpGqNgNz<*PaBzqg~GW`+8a( z=Knj8|Ee0KXfWn~_d{a-d#eV){CBnkefP+iZVo0tlB|qrb(sIuAF35dx>4Dqp9qH} z`g0lhQ&oc$4PpLF5dG<`8bs`Gh)2Cq9~^8ygk)t*tMg^N`g&R&#?Oa|YTmm49P-v% zV*Z%CbwBiS@;xRctIC;=Su(8_Wy$< zu|XSv5}WZyHn`ScgX+tC8As4vlq5cpo`^wcKi*lE0i*k8Vhtpb4Z3SjH=yD`XZ?N6 z1~LCPK>n-PV9r9!|0#>b{Pz|c#Qb-*1ASMRPq~}?NU}26Am%^yhgcg)Wd{@o5DrQ7 z=U(uqiVfy0V*Y$s^ryGjpx9r3WP`qpS6{P1jGyG~Rg^p_J{RMsp!8+Yl~-3@6?xLE zG2b%&GA#d`3s1WCQxzpoir>ueOGiu?z8juYYc+2N_1?*LE_$A4k;i(xbV(d1f`wz(qn3A1J z_fwL{liame(RS#3(wk&u@T3gLe-%%P--`KvW|Wx!-r`BVdB*j5_ zCE<|7`^9+hr-~=V|Bv~zn&?k&@g%Xo{>YPj8Lz(PNrxbQW<9E&FnYL$1)iE!4Wg%J zKAkXn7?hl_Q9W3j&L)hel};Fyj;pFc`GnEb4wyL^&(dJ6bOiWI&@^(3TGOmx1Wlen zpi)hBfU=pwSwReRCV@&dJpr1ItzK0^kkb=ktojM_mk4S)OcxI4)mnmHn6?V1ww~QE zfl0csA^xL}SCtUtj5NTHF`r=gCmrwwg1p?q5ep{qb`ZCYMW(lAVtSusxCb3@g$Idy zrg6N_61k*y5I-xas*)g^^AX<~E#iv=8S%U^we?G4dSuuNN*YckSsBC1#%e9)cOnrB zlE&25^4k*6Bs-F1*G0&G6+t$i!u;3ziuo@RWXyjeX`X0kOl|eVTCT^E%}Js^CxSm! z1lfEV{b{=_`qNtk*;uW`)lYhVp~9S`uL<%gh@U}EstNL`JS520d6hztPlJ*Jx2kF9 zaJCS`kgd8Hq8gOXW*p8|Ul@`{pi(Y-1~kpC8?Jn9Zv=a#d+QO~93M9}-Cpu7qjJ&b|gOQ2G7o(IsV*JQ81MTfH*;S7SB_KMeh@!|h# zIccl0w&_-|fmAjzBI;lHsG zrQYapJ&tM`l9>Myb#>G>#hCxv3u69@wGHM!@oU#s%l^fvZOXQdo;_r9lIYJA@Tao2 zIg9?Zm5BZ{Y8zLJ2iboj>F=-YU4rqKbHEp<{89@?ytt6JgSdwm$n>^Td6U6^1E($>w+^uX9Q5Ab$02I+jK(L40dN5nrV8h-XlFV+9V%2X9HDoFt85ii%m5 z>_`&xzdq!@a+u;g=D!vv=D$egG5?89dZM90M>#<~WlIYI{;7=u$zkvR<6^Z^d zsJyFviNcfgcPf7!;%8~Fnjl|=*(|1-=V~DN@;O09B^MURXeWCv8$F3FOBVkbF+?es zRf85}BkP|0B4$qfqXa6YomSAa_fhpQ#T)$)v}c7@7YrEEp#PadnN>I$pJKtbdUVy* z0W~Nm<5OBN&^Q8>nmPnDy##DgRfS#Oh(UIGF@pL$$WY1juVm)rK{oAU?0MX2>bW!? z)SyYLG03L0WajKbpi*;I1x-I1rsHw{MFchFiPwCwwwaq?&cWK|r7caEq^lv~Pkm3v zFvT|ve}@CUP}|VE0~s@UJBZtJi4MIlG2G1#xI%67{97EaZ5WJi*i|8GM6D{Gr?G5@vF6^U>dRIF_<|A}9F zqM=dScw#NrW69XMfM_|xj56ozYYz!eysrP>&# zZ{tFaON0>4#w+2FM1O7of2tT= z5u3Hy{YBBA-ePoOe?uzTL8tSB?cYfnYqdEU$iuJr6b3&e!|{3$5+LClp=^R~&SK;P z)2dVaIIm*%$zR7YzA^WMkq$fw@!x+UK#PSoakIDU{9n<>ZRXiyNE*P+i*?ckoy~UF-|H*&O z(yfy`_>ufbvNCv*xT+WN8Im4!#qFVlLlXVD4*aR&NjWeL1N`}*=udC)B(cB#$dh~- zufBfy5aXxYnji^J;(R0ibBv#Y(*L;Gc{Q+CFHd0nsCP&HT!w-d@z z@6VHrRm><$JeSV*O&lR~x2hN&=Xnc`XVCqWq`$9XhS!&~kpC*46u$$nFK05v{Pz}5 z^7rRS;;LT6XGl6mpXc8Qha~#*0{BzKlj3(We+G*F^cGJN`|FQ9$(Ql!Yo1gS;%8}? zdSY7ORGgT0`~VK-`Fvs;D*4G}^)Tivwgi5spJ~KVI*h3X<-?e>*n0h)ej!llFy04LSn!eoz)3IYa0+nitpey8{?7gtJ+cMC$1S-|^P5|xHMz$Ile~@Utf}o~Nbm4G* zWByS6h>vgz-M?S1!z5kO5dWPqvQ^9&0go|%&+xZ8;0q5Dms>buWM|$E;-*iR>CK6~ z`3HvksROR?AhC8|ju&L+lG;K1(HnHU>3pq2(lZ=o8dvC@ut5R0iOMv3JH>!{~tpBs|d3BN6de%bfSp!6gtd*B59sz zWK5y+#9FS$lFdn?KR1CtRRr1m6Z+G3RJ@*hiy#}Tn7R5%?=N)!BY+{H~+KHgw$w3+IoWov}ZdbbCn3Q(b2hgtz z)vpK5eFoF9ZRtBlLEVjN-<=ugfY3`i&fvS!EvMwXsVGRBDxm0NSRu RI>>4?#30-ASfvEn{{c}CVT}L) literal 0 HcmV?d00001 diff --git a/tests/ruby/spec_helper.rb b/tests/ruby/spec_helper.rb index d6796401..a95969fd 100644 --- a/tests/ruby/spec_helper.rb +++ b/tests/ruby/spec_helper.rb @@ -19,3 +19,10 @@ def with_captured_stdout_stderr STDOUT.reopen(sout) STDERR.reopen(serr) end + +def clients_connected_to_pool(pool_index: 0, processes:) + admin_conn = PG::connect(processes.pgcat.admin_connection_string) + results = admin_conn.async_exec("SHOW POOLS")[pool_index] + admin_conn.close + results['cl_idle'].to_i + results['cl_active'].to_i + results['cl_waiting'].to_i +end From 568f04feeed774a3cbbaebfb19d5bdf0b33da3c8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Mar 2023 09:54:31 -0700 Subject: [PATCH 09/79] chore(deps): bump serde_derive from 1.0.154 to 1.0.159 (#387) Bumps [serde_derive](https://github.com/serde-rs/serde) from 1.0.154 to 1.0.159. - [Release notes](https://github.com/serde-rs/serde/releases) - [Commits](https://github.com/serde-rs/serde/compare/v1.0.154...v1.0.159) --- updated-dependencies: - dependency-name: serde_derive dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7e4aa683..5374d413 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -978,13 +978,13 @@ checksum = "771d4d9c4163ee138805e12c710dd365e4f44be8be0503cb1bb9eb989425d9c9" [[package]] name = "serde_derive" -version = "1.0.154" +version = "1.0.159" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fc80d722935453bcafdc2c9a73cd6fac4dc1938f0346035d84bf99fa9e33217" +checksum = "4c614d17805b093df4b147b51339e7e44bf05ef59fba1e45d83500bcfb4d8585" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.9", ] [[package]] From 0757d7f3a0b195dda87eb12fb5fa25a5c28b8b28 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Mar 2023 09:54:39 -0700 Subject: [PATCH 10/79] chore(deps): bump serde from 1.0.158 to 1.0.159 (#386) Bumps [serde](https://github.com/serde-rs/serde) from 1.0.158 to 1.0.159. - [Release notes](https://github.com/serde-rs/serde/releases) - [Commits](https://github.com/serde-rs/serde/compare/v1.0.158...v1.0.159) --- updated-dependencies: - dependency-name: serde dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5374d413..a4e53465 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -972,9 +972,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.158" +version = "1.0.159" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "771d4d9c4163ee138805e12c710dd365e4f44be8be0503cb1bb9eb989425d9c9" +checksum = "3c04e8343c3daeec41f58990b9d77068df31209f2af111e059e9fe9646693065" [[package]] name = "serde_derive" From 6f768a84ce8acfb3a7e861fa0dd92d31b3c6afce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Fern=C3=A1ndez?= Date: Thu, 30 Mar 2023 22:29:23 +0200 Subject: [PATCH 11/79] Auth passthrough (auth_query) (#266) * Add a new exec_simple_query method This adds a new `exec_simple_query` method so we can make 'out of band' queries to servers that don't interfere with pools at all. In order to reuse startup code for making these simple queries, we need to set the stats (`Reporter`) optional, so using these simple queries wont interfere with stats. * Add auth passthough (auth_query) Adds a feature that allows setting auth passthrough for md5 auth. It adds 3 new (general and pool) config parameters: - `auth_query`: An string containing a query that will be executed on boot to obtain the hash of a given user. This query have to use a placeholder `$1`, so pgcat can replace it with the user its trying to fetch the hash from. - `auth_query_user`: The user to use for connecting to the server and executing the auth_query. - `auth_query_password`: The password to use for connecting to the server and executing the auth_query. The configuration can be done either on the general config (so pools share them) or in a per-pool basis. The behavior is, at boot time, when validating server connections, a hash is fetched per server and stored in the pool. When new server connections are created, and no cleartext password is specified, the obtained hash is used for creating them, if the hash could not be obtained for whatever reason, it retries it. When client authentication is tried, it uses cleartext passwords if specified, it not, it checks whether we have query_auth set up, if so, it tries to use the obtained hash for making client auth. If there is no hash (we could not obtain one when validating the connection), a new fetch is tried. Once we have a hash, we authenticate using it against whathever the client has sent us, if there is a failure we refetch the hash and retry auth (so password changes can be done). The idea with this 'retrial' mechanism is to make it fault tolerant, so if for whatever reason hash could not be obtained during connection validation, or the password has change, we can still connect later. * Add documentation for Auth passthrough --- .circleci/config.yml | 8 + .circleci/run_tests.sh | 1 + .rustfmt.toml | 2 + CONFIG.md | 56 +++++- Cargo.lock | 44 ++++- Cargo.toml | 3 +- README.md | 1 + dev/docker-compose.yaml | 8 + src/auth_passthrough.rs | 107 ++++++++++++ src/client.rs | 82 ++++++++- src/config.rs | 89 +++++++++- src/errors.rs | 2 + src/lib.rs | 1 + src/main.rs | 1 + src/messages.rs | 22 ++- src/mirrors.rs | 2 + src/pool.rs | 53 +++++- src/query_router.rs | 6 + src/server.rs | 150 ++++++++++++++++- tests/docker/docker-compose.yml | 9 + tests/ruby/auth_query_spec.rb | 215 ++++++++++++++++++++++++ tests/ruby/helpers/auth_query_helper.rb | 173 +++++++++++++++++++ tests/ruby/helpers/pgcat_helper.rb | 7 + tests/ruby/helpers/pgcat_process.rb | 15 +- 24 files changed, 1026 insertions(+), 31 deletions(-) create mode 100644 .rustfmt.toml create mode 100644 src/auth_passthrough.rs create mode 100644 tests/ruby/auth_query_spec.rb create mode 100644 tests/ruby/helpers/auth_query_helper.rb diff --git a/.circleci/config.yml b/.circleci/config.yml index 5e2d114a..c7f5c9fa 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -46,6 +46,14 @@ jobs: POSTGRES_PASSWORD: postgres POSTGRES_INITDB_ARGS: --auth-local=scram-sha-256 --auth-host=scram-sha-256 --auth=scram-sha-256 + - image: postgres:14 + command: ["postgres", "-p", "10432", "-c", "shared_preload_libraries=pg_stat_statements"] + environment: + POSTGRES_USER: postgres + POSTGRES_DB: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_INITDB_ARGS: --auth-local=md5 --auth-host=md5 --auth=md5 + # Add steps to the job # See: https://circleci.com/docs/2.0/configuration-reference/#steps steps: diff --git a/.circleci/run_tests.sh b/.circleci/run_tests.sh index a5cfab0b..4ba497c3 100644 --- a/.circleci/run_tests.sh +++ b/.circleci/run_tests.sh @@ -19,6 +19,7 @@ PGPASSWORD=postgres psql -e -h 127.0.0.1 -p 5432 -U postgres -f tests/sharding/q PGPASSWORD=postgres psql -e -h 127.0.0.1 -p 7432 -U postgres -f tests/sharding/query_routing_setup.sql PGPASSWORD=postgres psql -e -h 127.0.0.1 -p 8432 -U postgres -f tests/sharding/query_routing_setup.sql PGPASSWORD=postgres psql -e -h 127.0.0.1 -p 9432 -U postgres -f tests/sharding/query_routing_setup.sql +PGPASSWORD=postgres psql -e -h 127.0.0.1 -p 10432 -U postgres -f tests/sharding/query_routing_setup.sql PGPASSWORD=sharding_user pgbench -h 127.0.0.1 -U sharding_user shard0 -i PGPASSWORD=sharding_user pgbench -h 127.0.0.1 -U sharding_user shard1 -i diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 00000000..17f33211 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,2 @@ +edition = "2021" +hard_tabs = false diff --git a/CONFIG.md b/CONFIG.md index bcd6f09f..3cec2530 100644 --- a/CONFIG.md +++ b/CONFIG.md @@ -175,11 +175,41 @@ Connecting to that database allows running commands like `SHOW POOLS`, `SHOW DAT ### admin_password ``` path: general.admin_password -default: "admin_pass" +default: ``` Password to access the virtual administrative database +### auth_query (experimental) +``` +path: general.auth_query +default: +``` + +Query to be sent to servers to obtain the hash used for md5 authentication. The connection will be +established using the database configured in the pool. This parameter is inherited by every pool +and can be redefined in pool configuration. + +### auth_query_user (experimental) +``` +path: general.auth_query_user +default: +``` + +User to be used for connecting to servers to obtain the hash used for md5 authentication by sending the query +specified in `auth_query_user`. The connection will be established using the database configured in the pool. +This parameter is inherited by every pool and can be redefined in pool configuration. + +### auth_query_password (experimental) +``` +path: general.auth_query_password +default: +``` + +Password to be used for connecting to servers to obtain the hash used for md5 authentication by sending the query +specified in `auth_query_user`. The connection will be established using the database configured in the pool. +This parameter is inherited by every pool and can be redefined in pool configuration. + ## `pools.` Section ### pool_mode @@ -281,6 +311,30 @@ default: 3000 Connect timeout can be overwritten in the pool +### auth_query (experimental) +``` +path: general.auth_query +default: +``` + +Auth query can be overwritten in the pool + +### auth_query_user (experimental) +``` +path: general.auth_query_user +default: +``` + +Auth query user can be overwritten in the pool + +### auth_query_password (experimental) +``` +path: general.auth_query_password +default: +``` + +Auth query password can be overwritten in the pool + ## `pools..users.` Section ### username diff --git a/Cargo.lock b/Cargo.lock index a4e53465..ad7bb8ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -45,7 +45,7 @@ checksum = "6227a8d6fdb862bcb100c4314d0d9579e5cd73fa6df31a2e6f6e1acd3c5f1207" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -54,6 +54,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.21.0" @@ -94,6 +100,12 @@ version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + [[package]] name = "bytes" version = "1.4.0" @@ -257,6 +269,12 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de853764b47027c2e862a995c34978ffa63c1501f2e15f987ba11bd4f9bba193" +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + [[package]] name = "fnv" version = "1.0.7" @@ -732,12 +750,13 @@ dependencies = [ "arc-swap", "async-trait", "atomic_enum", - "base64", + "base64 0.21.0", "bb8", "bytes", "chrono", "env_logger", "exitcode", + "fallible-iterator", "futures", "hmac", "hyper", @@ -749,6 +768,7 @@ dependencies = [ "once_cell", "parking_lot", "phf", + "postgres-protocol", "rand", "regex", "rustls-pemfile", @@ -818,6 +838,24 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "postgres-protocol" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "878c6cbf956e03af9aa8204b407b9cbf47c072164800aa918c516cd4b056c50c" +dependencies = [ + "base64 0.13.1", + "byteorder", + "bytes", + "fallible-iterator", + "hmac", + "md-5", + "memchr", + "rand", + "sha2", + "stringprep", +] + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -945,7 +983,7 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" dependencies = [ - "base64", + "base64 0.21.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 3f0cbec6..89cfe643 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,6 @@ version = "1.0.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] tokio = { version = "1", features = ["full"] } bytes = "1" @@ -38,6 +37,8 @@ futures = "0.3" socket2 = { version = "0.4.7", features = ["all"] } nix = "0.26.2" atomic_enum = "0.2.0" +postgres-protocol = "0.6.4" +fallible-iterator = "0.2" [target.'cfg(not(target_env = "msvc"))'.dependencies] jemallocator = "0.5.0" diff --git a/README.md b/README.md index 4d6f599d..63b5ab1a 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ PostgreSQL pooler and proxy (like PgBouncer) with support for sharding, load bal | Sharding using comments parsing/Regex | **Experimental** | Clients can include shard information (sharding key, shard ID) in the query comments. | | Automatic sharding | **Experimental** | PgCat can parse queries, detect sharding keys automatically, and route queries to the correct shard. | | Mirroring | **Experimental** | Mirror queries between multiple databases in order to test servers with realistic production traffic. | +| Auth passthrough | **Experimental** | MD5 password authentication can be configured to use an `auth_query` so no cleartext passwords are needed in the config file. | ## Status diff --git a/dev/docker-compose.yaml b/dev/docker-compose.yaml index 15621e87..71704bcb 100644 --- a/dev/docker-compose.yaml +++ b/dev/docker-compose.yaml @@ -58,6 +58,13 @@ services: POSTGRES_INITDB_ARGS: --auth-local=scram-sha-256 --auth-host=scram-sha-256 --auth=scram-sha-256 PGPORT: 9432 command: ["postgres", "-p", "9432", "-c", "shared_preload_libraries=pg_stat_statements", "-c", "pg_stat_statements.track=all", "-c", "pg_stat_statements.max=100000"] + pg5: + <<: *common-definition-pg + environment: + <<: *common-env-pg + POSTGRES_INITDB_ARGS: --auth-local=md5 --auth-host=md5 --auth=md5 + PGPORT: 10432 + command: ["postgres", "-p", "5432", "-c", "shared_preload_libraries=pg_stat_statements", "-c", "pg_stat_statements.track=all", "-c", "pg_stat_statements.max=100000"] toxiproxy: build: . @@ -71,6 +78,7 @@ services: - pg2 - pg3 - pg4 + - pg5 pgcat-shell: stdin_open: true diff --git a/src/auth_passthrough.rs b/src/auth_passthrough.rs new file mode 100644 index 00000000..b9f0e97f --- /dev/null +++ b/src/auth_passthrough.rs @@ -0,0 +1,107 @@ +use crate::errors::Error; +use crate::server::Server; +use log::debug; + +#[derive(Clone, Debug)] +pub struct AuthPassthrough { + password: String, + query: String, + user: String, +} + +impl AuthPassthrough { + /// Initializes an AuthPassthrough. + pub fn new(query: &str, user: &str, password: &str) -> Self { + AuthPassthrough { + password: password.to_string(), + query: query.to_string(), + user: user.to_string(), + } + } + + /// Returns an AuthPassthrough given the pool configuration. + /// If any of required values is not set, None is returned. + pub fn from_pool_config(pool_config: &crate::config::Pool) -> Option { + if pool_config.is_auth_query_configured() { + return Some(AuthPassthrough::new( + pool_config.auth_query.as_ref().unwrap(), + pool_config.auth_query_user.as_ref().unwrap(), + pool_config.auth_query_password.as_ref().unwrap(), + )); + } + + None + } + + /// Returns an AuthPassthrough given the pool settings. + /// If any of required values is not set, None is returned. + pub fn from_pool_settings(pool_settings: &crate::pool::PoolSettings) -> Option { + let pool_config = crate::config::Pool { + auth_query: pool_settings.auth_query.clone(), + auth_query_password: pool_settings.auth_query_password.clone(), + auth_query_user: pool_settings.auth_query_user.clone(), + ..Default::default() + }; + + AuthPassthrough::from_pool_config(&pool_config) + } + + /// Connects to server and executes auth_query for the specified address. + /// If the response is a row with two columns containing the username set in the address. + /// and its MD5 hash, the MD5 hash returned. + /// + /// Note that the query is executed, changing $1 with the name of the user + /// this is so we only hold in memory (and transfer) the least amount of 'sensitive' data. + /// Also, it is compatible with pgbouncer. + /// + /// # Arguments + /// + /// * `address` - An Address of the server we want to connect to. The username for the hash will be obtained from this value. + /// + /// # Examples + /// + /// ``` + /// use pgcat::auth_passthrough::AuthPassthrough; + /// use pgcat::config::Address; + /// let auth_passthrough = AuthPassthrough::new("SELECT * FROM public.user_lookup('$1');", "postgres", "postgres"); + /// auth_passthrough.fetch_hash(&Address::default()); + /// ``` + /// + pub async fn fetch_hash(&self, address: &crate::config::Address) -> Result { + let auth_user = crate::config::User { + username: self.user.clone(), + password: Some(self.password.clone()), + pool_size: 1, + statement_timeout: 0, + }; + + let user = &address.username; + + debug!("Connecting to server to obtain auth hashes."); + let auth_query = self.query.replace("$1", user); + match Server::exec_simple_query(address, &auth_user, &auth_query).await { + Ok(password_data) => { + if password_data.len() == 2 && password_data.first().unwrap() == user { + if let Some(stripped_hash) = password_data.last().unwrap().to_string().strip_prefix("md5") { + Ok(stripped_hash.to_string()) + } + else { + Err(Error::AuthPassthroughError( + "Obtained hash from auth_query does not seem to be in md5 format.".to_string(), + )) + } + } else { + Err(Error::AuthPassthroughError( + "Data obtained from query does not follow the scheme 'user','hash'." + .to_string(), + )) + } + } + Err(err) => { + Err(Error::AuthPassthroughError( + format!("Error trying to obtain password from auth_query, ignoring hash for user '{}'. Error: {:?}", + user, err))) + } + } + } +} diff --git a/src/client.rs b/src/client.rs index f9f4e015..d75c069d 100644 --- a/src/client.rs +++ b/src/client.rs @@ -12,9 +12,9 @@ use tokio::sync::broadcast::Receiver; use tokio::sync::mpsc::Sender; use crate::admin::{generate_server_info_for_admin, handle_admin}; +use crate::auth_passthrough::AuthPassthrough; use crate::config::{get_config, get_idle_client_in_transaction_timeout, Address, PoolMode}; use crate::constants::*; - use crate::messages::*; use crate::pool::{get_pool, ClientServerMap, ConnectionPool}; use crate::query_router::{Command, QueryRouter}; @@ -377,6 +377,20 @@ pub async fn startup_tls( } } +async fn refetch_auth_hash(pool: &ConnectionPool) -> Result { + let address = pool.address(0, 0); + if let Some(apt) = AuthPassthrough::from_pool_settings(&pool.settings) { + let hash = apt.fetch_hash(address).await?; + + return Ok(hash); + } + + Err(Error::ClientError(format!( + "Could not obtain hash for {{ username: {:?}, database: {:?} }}. Auth passthrough not enabled.", + address.username, address.database + ))) +} + impl Client where S: tokio::io::AsyncRead + std::marker::Unpin, @@ -509,14 +523,68 @@ where } }; - // Compare server and client hashes. - let password_hash = md5_hash_password(username, &pool.settings.user.password, &salt); + // Obtain the hash to compare, we give preference to that written in cleartext in config + // if there is nothing set in cleartext and auth passthrough (auth_query) is configured, we use the hash obtained + // when the pool was created. If there is no hash there, we try to fetch it one more time. + let password_hash = if let Some(password) = &pool.settings.user.password { + Some(md5_hash_password(username, password, &salt)) + } else { + if !get_config().is_auth_query_configured() { + return Err(Error::ClientError(format!("Client auth not possible, no cleartext password set for username: {:?} in config and auth passthrough (query_auth) is not set up.", username))); + } - if password_hash != password_response { - warn!("Invalid password {{ username: {:?}, pool_name: {:?}, application_name: {:?} }}", username, pool_name, application_name); - wrong_password(&mut write, username).await?; + let mut hash = (*pool.auth_hash.read()).clone(); - return Err(Error::ClientError(format!("Invalid password {{ username: {:?}, pool_name: {:?}, application_name: {:?} }}", username, pool_name, application_name))); + if hash.is_none() { + warn!("Query auth configured but no hash password found for pool {}. Will try to refetch it.", pool_name); + match refetch_auth_hash(&pool).await { + Ok(fetched_hash) => { + warn!("Password for {{ username: {:?}, pool_name: {:?}, application_name: {:?} }}, obtained. Updating.", username, pool_name, application_name); + { + let mut pool_auth_hash = pool.auth_hash.write(); + *pool_auth_hash = Some(fetched_hash.clone()); + } + + hash = Some(fetched_hash); + } + Err(err) => { + return Err( + Error::ClientError( + format!("No cleartext password set, and no auth passthrough could not obtain the hash from server for {{ username: {:?}, pool_name: {:?}, application_name: {:?} }}, the error was: {:?}", + username, + pool_name, + application_name, + err) + ) + ); + } + } + }; + + Some(md5_hash_second_pass(&hash.unwrap(), &salt)) + }; + + // Once we have the resulting hash, we compare with what the client gave us. + // If they do not match and auth query is set up, we try to refetch the hash one more time + // to see if the password has changed since the pool was created. + // + // @TODO: we could end up fetching again the same password twice (see above). + if password_hash.unwrap() != password_response { + warn!("Invalid password {{ username: {:?}, pool_name: {:?}, application_name: {:?} }}, will try to refetch it.", username, pool_name, application_name); + let fetched_hash = refetch_auth_hash(&pool).await?; + let new_password_hash = md5_hash_second_pass(&fetched_hash, &salt); + + // Ok password changed in server an auth is possible. + if new_password_hash == password_response { + warn!("Password for {{ username: {:?}, pool_name: {:?}, application_name: {:?} }}, changed in server. Updating.", username, pool_name, application_name); + { + let mut pool_auth_hash = pool.auth_hash.write(); + *pool_auth_hash = Some(fetched_hash); + } + } else { + wrong_password(&mut write, username).await?; + return Err(Error::ClientError(format!("Invalid password {{ username: {:?}, pool_name: {:?}, application_name: {:?} }}", username, pool_name, application_name))); + } } let transaction_mode = pool.settings.pool_mode == PoolMode::Transaction; diff --git a/src/config.rs b/src/config.rs index 644532ac..6545457c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -177,7 +177,7 @@ impl Address { #[derive(Clone, PartialEq, Hash, Eq, Serialize, Deserialize, Debug)] pub struct User { pub username: String, - pub password: String, + pub password: Option, pub pool_size: u32, #[serde(default)] // 0 pub statement_timeout: u64, @@ -187,7 +187,7 @@ impl Default for User { fn default() -> User { User { username: String::from("postgres"), - password: String::new(), + password: None, pool_size: 15, statement_timeout: 0, } @@ -250,6 +250,10 @@ pub struct General { pub tls_private_key: Option, pub admin_username: String, pub admin_password: String, + + pub auth_query: Option, + pub auth_query_user: Option, + pub auth_query_password: Option, } impl General { @@ -334,6 +338,9 @@ impl Default for General { tls_private_key: None, admin_username: String::from("admin"), admin_password: String::from("admin"), + auth_query: None, + auth_query_user: None, + auth_query_password: None, } } } @@ -406,6 +413,10 @@ pub struct Pool { pub shard_id_regex: Option, pub regex_search_limit: Option, + pub auth_query: Option, + pub auth_query_user: Option, + pub auth_query_password: Option, + pub shards: BTreeMap, pub users: BTreeMap, // Note, don't put simple fields below these configs. There's a compatability issue with TOML that makes it @@ -420,6 +431,12 @@ impl Pool { s.finish() } + pub fn is_auth_query_configured(&self) -> bool { + self.auth_query_password.is_some() + && self.auth_query_user.is_some() + && self.auth_query_password.is_some() + } + pub fn default_pool_mode() -> PoolMode { PoolMode::Transaction } @@ -512,6 +529,9 @@ impl Default for Pool { sharding_key_regex: None, shard_id_regex: None, regex_search_limit: Some(1000), + auth_query: None, + auth_query_user: None, + auth_query_password: None, } } } @@ -612,9 +632,31 @@ pub struct Config { } impl Config { + pub fn is_auth_query_configured(&self) -> bool { + self.pools + .iter() + .any(|(_name, pool)| pool.is_auth_query_configured()) + } + pub fn default_path() -> String { String::from("pgcat.toml") } + + pub fn fill_up_auth_query_config(&mut self) { + for (_name, pool) in self.pools.iter_mut() { + if pool.auth_query.is_none() { + pool.auth_query = self.general.auth_query.clone(); + } + + if pool.auth_query_user.is_none() { + pool.auth_query_user = self.general.auth_query_user.clone(); + } + + if pool.auth_query_password.is_none() { + pool.auth_query_password = self.general.auth_query_password.clone(); + } + } + } } impl Default for Config { @@ -832,6 +874,35 @@ impl Config { } pub fn validate(&mut self) -> Result<(), Error> { + // Validation for auth_query feature + if self.general.auth_query.is_some() + && (self.general.auth_query_user.is_none() + || self.general.auth_query_password.is_none()) + { + error!("If auth_query is specified, you need to provide a value for `auth_query_user`, `auth_query_password`"); + return Err(Error::BadConfig); + } + + for (name, pool) in self.pools.iter() { + if pool.auth_query.is_some() + && (pool.auth_query_user.is_none() || pool.auth_query_password.is_none()) + { + error!("Error in pool {{ {} }}. If auth_query is specified, you need to provide a value for `auth_query_user`, `auth_query_password`", name); + return Err(Error::BadConfig); + } + + for (_name, user_data) in pool.users.iter() { + if (pool.auth_query.is_none() + || pool.auth_query_password.is_none() + || pool.auth_query_user.is_none()) + && user_data.password.is_none() + { + error!("Error in pool {{ {} }}. You have to specify a user password for every pool if auth_query is not specified", name); + return Err(Error::BadConfig); + } + } + } + // Validate TLS! match self.general.tls_certificate.clone() { Some(tls_certificate) => { @@ -911,6 +982,7 @@ pub async fn parse(path: &str) -> Result<(), Error> { } }; + config.fill_up_auth_query_config(); config.validate()?; config.path = path.to_string(); @@ -980,7 +1052,10 @@ mod test { "sharding_user" ); assert_eq!( - get_config().pools["sharded_db"].users["1"].password, + get_config().pools["sharded_db"].users["1"] + .password + .as_ref() + .unwrap(), "other_user" ); assert_eq!(get_config().pools["sharded_db"].users["1"].pool_size, 21); @@ -1005,10 +1080,16 @@ mod test { "simple_user" ); assert_eq!( - get_config().pools["simple_db"].users["0"].password, + get_config().pools["simple_db"].users["0"] + .password + .as_ref() + .unwrap(), "simple_user" ); assert_eq!(get_config().pools["simple_db"].users["0"].pool_size, 5); + assert_eq!(get_config().general.auth_query, None); + assert_eq!(get_config().general.auth_query_user, None); + assert_eq!(get_config().general.auth_query_password, None); } #[tokio::test] diff --git a/src/errors.rs b/src/errors.rs index 310243c0..58fc088b 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -15,4 +15,6 @@ pub enum Error { StatementTimeout, ShuttingDown, ParseBytesError(String), + AuthError(String), + AuthPassthroughError(String), } diff --git a/src/lib.rs b/src/lib.rs index 67aa9cba..2645cd42 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +pub mod auth_passthrough; pub mod config; pub mod constants; pub mod errors; diff --git a/src/main.rs b/src/main.rs index a59da210..4c8987f1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -61,6 +61,7 @@ use std::sync::Arc; use tokio::sync::broadcast; mod admin; +mod auth_passthrough; mod client; mod config; mod constants; diff --git a/src/messages.rs b/src/messages.rs index c9ace4e0..61c36c6d 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -213,7 +213,13 @@ pub fn md5_hash_password(user: &str, password: &str, salt: &[u8]) -> Vec { let output = md5.finalize_reset(); // Second pass - md5.update(format!("{:x}", output)); + md5_hash_second_pass(&(format!("{:x}", output)), salt) +} + +pub fn md5_hash_second_pass(hash: &str, salt: &[u8]) -> Vec { + let mut md5 = Md5::new(); + // Second pass + md5.update(hash); md5.update(salt); let mut password = format!("md5{:x}", md5.finalize()) @@ -247,6 +253,20 @@ where write_all(stream, message).await } +pub async fn md5_password_with_hash(stream: &mut S, hash: &str, salt: &[u8]) -> Result<(), Error> +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); + + message.put_u8(b'p'); + message.put_i32(password.len() as i32 + 4); + message.put_slice(&password[..]); + + write_all(stream, message).await +} + /// Implements a response to our custom `SET SHARDING KEY` /// and `SET SERVER ROLE` commands. /// This tells the client we're ready for the next query. diff --git a/src/mirrors.rs b/src/mirrors.rs index 128fe220..17f91d4d 100644 --- a/src/mirrors.rs +++ b/src/mirrors.rs @@ -4,6 +4,7 @@ use std::sync::Arc; /// Packets arrive to us through a channel from the main client and we send them to the server. use bb8::Pool; use bytes::{Bytes, BytesMut}; +use parking_lot::RwLock; use crate::config::{get_config, Address, Role, User}; use crate::pool::{ClientServerMap, PoolIdentifier, ServerPool}; @@ -41,6 +42,7 @@ impl MirroredClient { self.database.as_str(), ClientServerMap::default(), Arc::new(PoolStats::new(identifier, cfg.clone())), + Arc::new(RwLock::new(None)), ); Pool::builder() diff --git a/src/pool.rs b/src/pool.rs index f6f9118b..e1ab7cb4 100644 --- a/src/pool.rs +++ b/src/pool.rs @@ -20,6 +20,7 @@ use tokio::sync::Notify; use crate::config::{get_config, Address, General, LoadBalancingMode, PoolMode, Role, User}; use crate::errors::Error; +use crate::auth_passthrough::AuthPassthrough; use crate::server::Server; use crate::sharding::ShardingFunction; use crate::stats::{AddressStats, ClientStats, PoolStats, ServerStats}; @@ -123,6 +124,11 @@ pub struct PoolSettings { // Limit how much of each query is searched for a potential shard regex match pub regex_search_limit: usize, + + // Auth query parameters + pub auth_query: Option, + pub auth_query_user: Option, + pub auth_query_password: Option, } impl Default for PoolSettings { @@ -143,6 +149,9 @@ impl Default for PoolSettings { sharding_key_regex: None, shard_id_regex: None, regex_search_limit: 1000, + auth_query: None, + auth_query_user: None, + auth_query_password: None, } } } @@ -183,6 +192,9 @@ pub struct ConnectionPool { paused_waiter: Arc, pub stats: Arc, + + /// AuthInfo + pub auth_hash: Arc>>, } impl ConnectionPool { @@ -237,6 +249,7 @@ impl ConnectionPool { // Sort by shard number to ensure consistency. shard_ids.sort_by_key(|k| k.parse::().unwrap()); + let pool_auth_hash: Arc>> = Arc::new(RwLock::new(None)); for shard_idx in &shard_ids { let shard = &pool_config.shards[shard_idx]; @@ -293,12 +306,35 @@ impl ConnectionPool { replica_number += 1; } + // We assume every server in the pool share user/passwords + let auth_passthrough = AuthPassthrough::from_pool_config(pool_config); + + if let Some(apt) = &auth_passthrough { + match apt.fetch_hash(&address).await { + Ok(ok) => { + if let Some(ref pool_auth_hash_value) = *(pool_auth_hash.read()) { + if ok != *pool_auth_hash_value { + warn!("Hash is not the same across shards of the same pool, client auth will \ + be done using last obtained hash. Server: {}:{}, Database: {}", server.host, server.port, shard.database); + } + } + debug!("Hash obtained for {:?}", address); + { + let mut pool_auth_hash = pool_auth_hash.write(); + *pool_auth_hash = Some(ok.clone()); + } + }, + Err(err) => warn!("Could not obtain password hashes using auth_query config, ignoring. Error: {:?}", err), + } + } + let manager = ServerPool::new( address.clone(), user.clone(), &shard.database, client_server_map.clone(), pool_stats.clone(), + pool_auth_hash.clone(), ); let connect_timeout = match pool_config.connect_timeout { @@ -330,6 +366,12 @@ impl ConnectionPool { } assert_eq!(shards.len(), addresses.len()); + if let Some(ref _auth_hash) = *(pool_auth_hash.clone().read()) { + info!( + "Auth hash obtained from query_auth for pool {{ name: {}, user: {} }}", + pool_name, user.username + ); + } let pool = ConnectionPool { databases: shards, @@ -338,6 +380,7 @@ impl ConnectionPool { banlist: Arc::new(RwLock::new(banlist)), config_hash: new_pool_hash_value, server_info: Arc::new(RwLock::new(BytesMut::new())), + auth_hash: pool_auth_hash, settings: PoolSettings { pool_mode: pool_config.pool_mode, load_balancing_mode: pool_config.load_balancing_mode, @@ -366,6 +409,9 @@ impl ConnectionPool { .clone() .map(|regex| Regex::new(regex.as_str()).unwrap()), regex_search_limit: pool_config.regex_search_limit.unwrap_or(1000), + auth_query: pool_config.auth_query.clone(), + auth_query_user: pool_config.auth_query_user.clone(), + auth_query_password: pool_config.auth_query_password.clone(), }, validated: Arc::new(AtomicBool::new(false)), paused: Arc::new(AtomicBool::new(false)), @@ -389,7 +435,8 @@ impl ConnectionPool { Ok(()) } - /// Connect to all shards and grab server information. + /// Connect to all shards, grab server information, and possibly + /// passwords to use in client auth. /// Return server information we will pass to the clients /// when they connect. /// This also warms up the pool for clients that connect when @@ -803,6 +850,7 @@ pub struct ServerPool { database: String, client_server_map: ClientServerMap, stats: Arc, + auth_hash: Arc>>, } impl ServerPool { @@ -812,6 +860,7 @@ impl ServerPool { database: &str, client_server_map: ClientServerMap, stats: Arc, + auth_hash: Arc>>, ) -> ServerPool { ServerPool { address, @@ -819,6 +868,7 @@ impl ServerPool { database: database.to_string(), client_server_map, stats, + auth_hash, } } } @@ -847,6 +897,7 @@ impl ManageConnection for ServerPool { &self.database, self.client_server_map.clone(), stats.clone(), + self.auth_hash.clone(), ) .await { diff --git a/src/query_router.rs b/src/query_router.rs index 578c7390..0ea907b5 100644 --- a/src/query_router.rs +++ b/src/query_router.rs @@ -1110,6 +1110,9 @@ mod test { sharding_key_regex: None, shard_id_regex: None, regex_search_limit: 1000, + auth_query: None, + auth_query_password: None, + auth_query_user: None, }; let mut qr = QueryRouter::new(); assert_eq!(qr.active_role, None); @@ -1171,6 +1174,9 @@ mod test { sharding_key_regex: Some(Regex::new(r"/\* sharding_key: (\d+) \*/").unwrap()), shard_id_regex: Some(Regex::new(r"/\* shard_id: (\d+) \*/").unwrap()), regex_search_limit: 1000, + auth_query: None, + auth_query_password: None, + auth_query_user: None, }; let mut qr = QueryRouter::new(); qr.update_pool_settings(pool_settings.clone()); diff --git a/src/server.rs b/src/server.rs index d09313ec..37f0e0c7 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,7 +1,11 @@ /// Implementation of the PostgreSQL server (database) protocol. /// Here we are pretending to the a Postgres client. use bytes::{Buf, BufMut, BytesMut}; +use fallible_iterator::FallibleIterator; use log::{debug, error, info, trace, warn}; +use parking_lot::{Mutex, RwLock}; +use postgres_protocol::message; +use std::collections::HashMap; use std::io::Read; use std::sync::Arc; use std::time::SystemTime; @@ -81,6 +85,7 @@ impl Server { database: &str, client_server_map: ClientServerMap, stats: Arc, + auth_hash: Arc>>, ) -> Result { let mut stream = match TcpStream::connect(&format!("{}:{}", &address.host, address.port)).await { @@ -106,7 +111,10 @@ impl Server { // We'll be handling multiple packets, but they will all be structured the same. // We'll loop here until this exchange is complete. - let mut scram = ScramSha256::new(&user.password); + let mut scram: Option = None; + if let Some(password) = &user.password.clone() { + scram = Some(ScramSha256::new(password)); + } loop { let code = match stream.read_u8().await { @@ -143,13 +151,40 @@ impl Server { Err(_) => return Err(Error::SocketError(format!("Error reading salt on server startup {{ username: {:?}, database: {:?} }}", user.username, database))), }; - md5_password(&mut stream, &user.username, &user.password, &salt[..]) - .await?; + match &user.password { + // Using plaintext password + Some(password) => { + md5_password(&mut stream, &user.username, password, &salt[..]) + .await? + } + + // Using auth passthrough, in this case we should already have a + // hash obtained when the pool was validated. If we reach this point + // and don't have a hash, we return an error. + None => { + let option_hash = (*auth_hash.read()).clone(); + match option_hash { + Some(hash) => + md5_password_with_hash( + &mut stream, + &hash, + &salt[..], + ) + .await?, + None => + return Err(Error::AuthError(format!("Auth passthrough (auth_query) failed and no user password is set in cleartext for {{ username: {:?}, database: {:?} }}", user.username, database))) + } + } + } } AUTHENTICATION_SUCCESSFUL => (), SASL => { + if scram.is_none() { + return Err(Error::AuthError(format!("SASL auth required and not password specified, auth passthrough (auth_query) method is currently unsupported for SASL auth {{ username: {:?}, database: {:?} }}", user.username, database))); + } + debug!("Starting SASL authentication"); let sasl_len = (len - 8) as usize; let mut sasl_auth = vec![0u8; sasl_len]; @@ -165,7 +200,7 @@ impl Server { debug!("Using {}", SCRAM_SHA_256); // Generate client message. - let sasl_response = scram.message(); + let sasl_response = scram.as_mut().unwrap().message(); // SASLInitialResponse (F) let mut res = BytesMut::new(); @@ -202,7 +237,7 @@ impl Server { }; let msg = BytesMut::from(&sasl_data[..]); - let sasl_response = scram.update(&msg)?; + let sasl_response = scram.as_mut().unwrap().update(&msg)?; // SASLResponse let mut res = BytesMut::new(); @@ -222,7 +257,11 @@ impl Server { Err(_) => return Err(Error::SocketError(format!("Error reading sasl final message on server startup {{ username: {:?}, database: {:?} }}", user.username, database))), }; - match scram.finish(&BytesMut::from(&sasl_final[..])) { + match scram + .as_mut() + .unwrap() + .finish(&BytesMut::from(&sasl_final[..])) + { Ok(_) => { debug!("SASL authentication successful"); } @@ -696,6 +735,105 @@ impl Server { None => (), } } + + // This is so we can execute out of band queries to the server. + // The connection will be opened, the query executed and closed. + pub async fn exec_simple_query( + address: &Address, + user: &User, + query: &str, + ) -> Result, Error> { + let client_server_map: ClientServerMap = Arc::new(Mutex::new(HashMap::new())); + + debug!("Connecting to server to obtain auth hashes."); + let mut server = Server::startup( + address, + user, + &address.database, + client_server_map, + Arc::new(ServerStats::default()), + Arc::new(RwLock::new(None)), + ) + .await?; + debug!("Connected!, sending query."); + server.send(&simple_query(query)).await?; + let mut message = server.recv().await?; + + Ok(parse_query_message(&mut message).await?) + } +} + +async fn parse_query_message(message: &mut BytesMut) -> Result, Error> { + let mut pair = Vec::::new(); + match message::backend::Message::parse(message) { + Ok(Some(message::backend::Message::RowDescription(_description))) => {} + Ok(Some(message::backend::Message::ErrorResponse(err))) => { + return Err(Error::ProtocolSyncError(format!( + "Protocol error parsing response. Err: {:?}", + err.fields() + .iterator() + .fold(String::default(), |acc, element| acc + + element.unwrap().value()) + ))) + } + Ok(_) => { + return Err(Error::ProtocolSyncError( + "Protocol error, expected Row Description.".to_string(), + )) + } + Err(err) => { + return Err(Error::ProtocolSyncError(format!( + "Protocol error parsing response. Err: {:?}", + err + ))) + } + } + + while !message.is_empty() { + match message::backend::Message::parse(message) { + Ok(postgres_message) => { + match postgres_message { + Some(message::backend::Message::DataRow(data)) => { + let buf = data.buffer(); + trace!("Data: {:?}", buf); + + for item in data.ranges().iterator() { + match item.as_ref() { + Ok(range) => match range { + Some(range) => { + pair.push(String::from_utf8_lossy(&buf[range.clone()]).to_string()); + } + None => return Err(Error::ProtocolSyncError(String::from( + "Data expected while receiving query auth data, found nothing.", + ))), + }, + Err(err) => { + return Err(Error::ProtocolSyncError(format!( + "Data error, err: {:?}", + err + ))) + } + } + } + } + Some(message::backend::Message::CommandComplete(_)) => {} + Some(message::backend::Message::ReadyForQuery(_)) => {} + _ => { + return Err(Error::ProtocolSyncError( + "Unexpected message while receiving auth query data.".to_string(), + )) + } + } + } + Err(err) => { + return Err(Error::ProtocolSyncError(format!( + "Parse error, err: {:?}", + err + ))) + } + }; + } + Ok(pair) } impl Drop for Server { diff --git a/tests/docker/docker-compose.yml b/tests/docker/docker-compose.yml index e57d8529..93e94550 100644 --- a/tests/docker/docker-compose.yml +++ b/tests/docker/docker-compose.yml @@ -36,6 +36,15 @@ services: POSTGRES_PASSWORD: postgres POSTGRES_INITDB_ARGS: --auth-local=scram-sha-256 --auth-host=scram-sha-256 --auth=scram-sha-256 command: ["postgres", "-p", "9432", "-c", "shared_preload_libraries=pg_stat_statements", "-c", "pg_stat_statements.track=all", "-c", "pg_stat_statements.max=100000"] + pg5: + image: postgres:14 + network_mode: "service:main" + environment: + POSTGRES_USER: postgres + POSTGRES_DB: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_INITDB_ARGS: --auth-local=md5 --auth-host=md5 --auth=md5 + command: ["postgres", "-c", "shared_preload_libraries=pg_stat_statements", "-c", "pg_stat_statements.track=all", "-p", "10432"] main: build: . command: ["bash", "/app/tests/docker/run.sh"] diff --git a/tests/ruby/auth_query_spec.rb b/tests/ruby/auth_query_spec.rb new file mode 100644 index 00000000..1ac62164 --- /dev/null +++ b/tests/ruby/auth_query_spec.rb @@ -0,0 +1,215 @@ +# frozen_string_literal: true + +require_relative 'spec_helper' +require_relative 'helpers/auth_query_helper' + +describe "Auth Query" do + let(:configured_instances) {[5432, 10432]} + let(:config_user) { { 'username' => 'sharding_user', 'password' => 'sharding_user' } } + let(:pg_user) { { 'username' => 'sharding_user', 'password' => 'sharding_user' } } + let(:processes) { Helpers::AuthQuery.single_shard_auth_query(pool_name: "sharded_db", pg_user: pg_user, config_user: config_user, extra_conf: config, wait_until_ready: wait_until_ready ) } + let(:config) { {} } + let(:wait_until_ready) { true } + + after do + unless @failing_process + processes.all_databases.map(&:reset) + processes.pgcat.shutdown + end + @failing_process = false + end + + context "when auth_query is not configured" do + context 'and cleartext passwords are set' do + it "uses local passwords" do + conn = PG.connect(processes.pgcat.connection_string("sharded_db", config_user['username'], config_user['password'])) + + expect(conn.async_exec("SELECT 1 + 2")).not_to be_nil + end + end + + context 'and cleartext passwords are not set' do + let(:config_user) { { 'username' => 'sharding_user' } } + + it "does not start because it is not possible to authenticate" do + @failing_process = true + expect { processes.pgcat }.to raise_error(StandardError, /You have to specify a user password for every pool if auth_query is not specified/) + end + end + end + + context 'when auth_query is configured' do + context 'with global configuration' do + around(:example) do |example| + + # Set up auth query + Helpers::AuthQuery.set_up_auth_query_for_user( + user: 'md5_auth_user', + password: 'secret' + ); + + example.run + + # Drop auth query support + Helpers::AuthQuery.tear_down_auth_query_for_user( + user: 'md5_auth_user', + password: 'secret' + ); + end + + context 'with correct global parameters' do + let(:config) { { 'general' => { 'auth_query' => "SELECT * FROM public.user_lookup('$1');", 'auth_query_user' => 'md5_auth_user', 'auth_query_password' => 'secret' } } } + context 'and with cleartext passwords set' do + it 'it uses local passwords' do + conn = PG.connect(processes.pgcat.connection_string("sharded_db", pg_user['username'], pg_user['password'])) + expect(conn.exec("SELECT 1 + 2")).not_to be_nil + end + end + + context 'and with cleartext passwords not set' do + let(:config_user) { { 'username' => 'sharding_user', 'password' => 'sharding_user' } } + + it 'it uses obtained passwords' do + connection_string = processes.pgcat.connection_string("sharded_db", pg_user['username'], pg_user['password']) + conn = PG.connect(connection_string) + expect(conn.async_exec("SELECT 1 + 2")).not_to be_nil + end + + it 'allows passwords to be changed without closing existing connections' do + pgconn = PG.connect(processes.pgcat.connection_string("sharded_db", pg_user['username'])) + expect(pgconn.exec("SELECT 1 + 2")).not_to be_nil + Helpers::AuthQuery.exec_in_instances(query: "ALTER USER #{pg_user['username']} WITH ENCRYPTED PASSWORD 'secret2';") + expect(pgconn.exec("SELECT 1 + 4")).not_to be_nil + Helpers::AuthQuery.exec_in_instances(query: "ALTER USER #{pg_user['username']} WITH ENCRYPTED PASSWORD '#{pg_user['password']}';") + end + + it 'allows passwords to be changed and that new password is needed when reconnecting' do + pgconn = PG.connect(processes.pgcat.connection_string("sharded_db", pg_user['username'])) + expect(pgconn.exec("SELECT 1 + 2")).not_to be_nil + Helpers::AuthQuery.exec_in_instances(query: "ALTER USER #{pg_user['username']} WITH ENCRYPTED PASSWORD 'secret2';") + newconn = PG.connect(processes.pgcat.connection_string("sharded_db", pg_user['username'], 'secret2')) + expect(newconn.exec("SELECT 1 + 2")).not_to be_nil + Helpers::AuthQuery.exec_in_instances(query: "ALTER USER #{pg_user['username']} WITH ENCRYPTED PASSWORD '#{pg_user['password']}';") + end + end + end + + context 'with wrong parameters' do + let(:config) { { 'general' => { 'auth_query' => 'SELECT 1', 'auth_query_user' => 'wrong_user', 'auth_query_password' => 'wrong' } } } + + context 'and with clear text passwords set' do + it "it uses local passwords" do + conn = PG.connect(processes.pgcat.connection_string("sharded_db", pg_user['username'], pg_user['password'])) + + expect(conn.async_exec("SELECT 1 + 2")).not_to be_nil + end + end + + context 'and with cleartext passwords not set' do + let(:config_user) { { 'username' => 'sharding_user' } } + it "it fails to start as it cannot authenticate against servers" do + @failing_process = true + expect { PG.connect(processes.pgcat.connection_string("sharded_db", pg_user['username'], pg_user['password'])) }.to raise_error(StandardError, /Error trying to obtain password from auth_query/ ) + end + + context 'and we fix the issue and reload' do + let(:wait_until_ready) { false } + + it 'fails in the beginning but starts working after reloading config' do + connection_string = processes.pgcat.connection_string("sharded_db", pg_user['username'], pg_user['password']) + while !(processes.pgcat.logs =~ /Waiting for clients/) do + sleep 0.5 + end + + expect { PG.connect(connection_string)}.to raise_error(PG::ConnectionBad) + expect(processes.pgcat.logs).to match(/Error trying to obtain password from auth_query/) + + current_config = processes.pgcat.current_config + config = { 'general' => { 'auth_query' => "SELECT * FROM public.user_lookup('$1');", 'auth_query_user' => 'md5_auth_user', 'auth_query_password' => 'secret' } } + processes.pgcat.update_config(current_config.deep_merge(config)) + processes.pgcat.reload_config + + conn = nil + expect { conn = PG.connect(connection_string)}.not_to raise_error + expect(conn.async_exec("SELECT 1 + 2")).not_to be_nil + end + end + end + end + end + + context 'with per pool configuration' do + around(:example) do |example| + + # Set up auth query + Helpers::AuthQuery.set_up_auth_query_for_user( + user: 'md5_auth_user', + password: 'secret' + ); + + Helpers::AuthQuery.set_up_auth_query_for_user( + user: 'md5_auth_user1', + password: 'secret', + database: 'shard1' + ); + + example.run + + # Tear down auth query + Helpers::AuthQuery.tear_down_auth_query_for_user( + user: 'md5_auth_user', + password: 'secret' + ); + + Helpers::AuthQuery.tear_down_auth_query_for_user( + user: 'md5_auth_user1', + password: 'secret', + database: 'shard1' + ); + end + + context 'with correct parameters' do + let(:processes) { Helpers::AuthQuery.two_pools_auth_query(pool_names: ["sharded_db0", "sharded_db1"], pg_user: pg_user, config_user: config_user, extra_conf: config ) } + let(:config) { + { 'pools' => + { + 'sharded_db0' => { + 'auth_query' => "SELECT * FROM public.user_lookup('$1');", + 'auth_query_user' => 'md5_auth_user', + 'auth_query_password' => 'secret' + }, + 'sharded_db1' => { + 'auth_query' => "SELECT * FROM public.user_lookup('$1');", + 'auth_query_user' => 'md5_auth_user1', + 'auth_query_password' => 'secret' + }, + } + } + } + + context 'and with cleartext passwords set' do + it 'it uses local passwords' do + conn = PG.connect(processes.pgcat.connection_string("sharded_db0", pg_user['username'], pg_user['password'])) + expect(conn.exec("SELECT 1 + 2")).not_to be_nil + conn = PG.connect(processes.pgcat.connection_string("sharded_db1", pg_user['username'], pg_user['password'])) + expect(conn.exec("SELECT 1 + 2")).not_to be_nil + end + end + + context 'and with cleartext passwords not set' do + let(:config_user) { { 'username' => 'sharding_user' } } + + it 'it uses obtained passwords' do + connection_string = processes.pgcat.connection_string("sharded_db0", pg_user['username'], pg_user['password']) + conn = PG.connect(connection_string) + expect(conn.async_exec("SELECT 1 + 2")).not_to be_nil + connection_string = processes.pgcat.connection_string("sharded_db1", pg_user['username'], pg_user['password']) + conn = PG.connect(connection_string) + expect(conn.async_exec("SELECT 1 + 2")).not_to be_nil + end + end + + end + end + end +end diff --git a/tests/ruby/helpers/auth_query_helper.rb b/tests/ruby/helpers/auth_query_helper.rb new file mode 100644 index 00000000..60e85713 --- /dev/null +++ b/tests/ruby/helpers/auth_query_helper.rb @@ -0,0 +1,173 @@ +module Helpers + module AuthQuery + def self.single_shard_auth_query( + pg_user:, + config_user:, + pool_name:, + extra_conf: {}, + log_level: 'debug', + wait_until_ready: true + ) + + user = { + "pool_size" => 10, + "statement_timeout" => 0, + } + + pgcat = PgcatProcess.new(log_level) + pgcat_cfg = pgcat.current_config.deep_merge(extra_conf) + + primary = PgInstance.new(5432, pg_user["username"], pg_user["password"], "shard0") + replica = PgInstance.new(10432, pg_user["username"], pg_user["password"], "shard0") + + # Main proxy configs + pgcat_cfg["pools"] = { + "#{pool_name}" => { + "default_role" => "any", + "pool_mode" => "transaction", + "load_balancing_mode" => "random", + "primary_reads_enabled" => false, + "query_parser_enabled" => false, + "sharding_function" => "pg_bigint_hash", + "shards" => { + "0" => { + "database" => "shard0", + "servers" => [ + ["localhost", primary.port.to_s, "primary"], + ["localhost", replica.port.to_s, "replica"], + ] + }, + }, + "users" => { "0" => user.merge(config_user) } + } + } + pgcat_cfg["general"]["port"] = pgcat.port + pgcat.update_config(pgcat_cfg) + pgcat.start + + pgcat.wait_until_ready( + pgcat.connection_string( + "sharded_db", + pg_user['username'], + pg_user['password'] + ) + ) if wait_until_ready + + OpenStruct.new.tap do |struct| + struct.pgcat = pgcat + struct.primary = primary + struct.replicas = [replica] + struct.all_databases = [primary] + end + end + + def self.two_pools_auth_query( + pg_user:, + config_user:, + pool_names:, + extra_conf: {}, + log_level: 'debug' + ) + + user = { + "pool_size" => 10, + "statement_timeout" => 0, + } + + pgcat = PgcatProcess.new(log_level) + pgcat_cfg = pgcat.current_config + + primary = PgInstance.new(5432, pg_user["username"], pg_user["password"], "shard0") + replica = PgInstance.new(10432, pg_user["username"], pg_user["password"], "shard0") + + pool_template = Proc.new do |database| + { + "default_role" => "any", + "pool_mode" => "transaction", + "load_balancing_mode" => "random", + "primary_reads_enabled" => false, + "query_parser_enabled" => false, + "sharding_function" => "pg_bigint_hash", + "shards" => { + "0" => { + "database" => database, + "servers" => [ + ["localhost", primary.port.to_s, "primary"], + ["localhost", replica.port.to_s, "replica"], + ] + }, + }, + "users" => { "0" => user.merge(config_user) } + } + end + # Main proxy configs + pgcat_cfg["pools"] = { + "#{pool_names[0]}" => pool_template.call("shard0"), + "#{pool_names[1]}" => pool_template.call("shard1") + } + + pgcat_cfg["general"]["port"] = pgcat.port + pgcat.update_config(pgcat_cfg.deep_merge(extra_conf)) + pgcat.start + + pgcat.wait_until_ready(pgcat.connection_string("sharded_db0", pg_user['username'], pg_user['password'])) + + OpenStruct.new.tap do |struct| + struct.pgcat = pgcat + struct.primary = primary + struct.replicas = [replica] + struct.all_databases = [primary] + end + end + + def self.create_query_auth_function(user) + return <<-SQL +CREATE OR REPLACE FUNCTION public.user_lookup(in i_username text, out uname text, out phash text) +RETURNS record AS $$ +BEGIN + SELECT usename, passwd FROM pg_catalog.pg_shadow + WHERE usename = i_username INTO uname, phash; + RETURN; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +GRANT EXECUTE ON FUNCTION public.user_lookup(text) TO #{user}; +SQL + end + + def self.exec_in_instances(query:, instance_ports: [ 5432, 10432 ], database: 'postgres', user: 'postgres', password: 'postgres') + instance_ports.each do |port| + c = PG.connect("postgres://#{user}:#{password}@localhost:#{port}/#{database}") + c.exec(query) + c.close + end + end + + def self.set_up_auth_query_for_user(user:, password:, instance_ports: [ 5432, 10432 ], database: 'shard0' ) + instance_ports.each do |port| + connection = PG.connect("postgres://postgres:postgres@localhost:#{port}/#{database}") + connection.exec(self.drop_query_auth_function(user)) rescue PG::UndefinedFunction + connection.exec("DROP ROLE #{user}") rescue PG::UndefinedObject + connection.exec("CREATE ROLE #{user} ENCRYPTED PASSWORD '#{password}' LOGIN;") + connection.exec(self.create_query_auth_function(user)) + connection.close + end + end + + def self.tear_down_auth_query_for_user(user:, password:, instance_ports: [ 5432, 10432 ], database: 'shard0' ) + instance_ports.each do |port| + connection = PG.connect("postgres://postgres:postgres@localhost:#{port}/#{database}") + connection.exec(self.drop_query_auth_function(user)) rescue PG::UndefinedFunction + connection.exec("DROP ROLE #{user}") + connection.close + end + end + + def self.drop_query_auth_function(user) + return <<-SQL +REVOKE ALL ON FUNCTION public.user_lookup(text) FROM public, #{user}; +DROP FUNCTION public.user_lookup(in i_username text, out uname text, out phash text); +SQL + end + end +end diff --git a/tests/ruby/helpers/pgcat_helper.rb b/tests/ruby/helpers/pgcat_helper.rb index c4ebab7f..13dc6686 100644 --- a/tests/ruby/helpers/pgcat_helper.rb +++ b/tests/ruby/helpers/pgcat_helper.rb @@ -3,6 +3,13 @@ require_relative 'pgcat_process' require_relative 'pg_instance' +class ::Hash + def deep_merge(second) + merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 } + self.merge(second, &merger) + end +end + module Helpers module Pgcat def self.three_shard_setup(pool_name, pool_size, pool_mode="transaction", lb_mode="random", log_level="info") diff --git a/tests/ruby/helpers/pgcat_process.rb b/tests/ruby/helpers/pgcat_process.rb index 6120c99f..e1dbea8b 100644 --- a/tests/ruby/helpers/pgcat_process.rb +++ b/tests/ruby/helpers/pgcat_process.rb @@ -67,17 +67,21 @@ def reload_config def start raise StandardError, "Process is already started" unless @pid.nil? @pid = Process.spawn(@env, @command, err: @log_filename, out: @log_filename) + Process.detach(@pid) ObjectSpace.define_finalizer(@log_filename, proc { PgcatProcess.finalize(@pid, @log_filename, @config_filename) }) return self end - def wait_until_ready + def wait_until_ready(connection_string = nil) exc = nil 10.times do - PG::connect(example_connection_string).close + Process.kill 0, @pid + PG::connect(connection_string || example_connection_string).close return self + rescue Errno::ESRCH + raise StandardError, "Process #{@pid} died. #{logs}" rescue => e exc = e sleep(0.5) @@ -108,13 +112,10 @@ def admin_connection_string "postgresql://#{username}:#{password}@0.0.0.0:#{@port}/pgcat" end - def connection_string(pool_name, username) + def connection_string(pool_name, username, password = nil) cfg = current_config - user_idx, user_obj = cfg["pools"][pool_name]["users"].detect { |k, user| user["username"] == username } - password = user_obj["password"] - - "postgresql://#{username}:#{password}@0.0.0.0:#{@port}/#{pool_name}" + "postgresql://#{username}:#{password || user_obj["password"]}@0.0.0.0:#{@port}/#{pool_name}" end def example_connection_string From 9094988491aa71ae97ace787f63c41d4fa471d45 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 31 Mar 2023 09:34:51 -0700 Subject: [PATCH 12/79] chore(deps): bump postgres-protocol from 0.6.4 to 0.6.5 (#391) Bumps [postgres-protocol](https://github.com/sfackler/rust-postgres) from 0.6.4 to 0.6.5. - [Release notes](https://github.com/sfackler/rust-postgres/releases) - [Commits](https://github.com/sfackler/rust-postgres/compare/postgres-protocol-v0.6.4...postgres-protocol-v0.6.5) --- updated-dependencies: - dependency-name: postgres-protocol dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 16 +++++----------- Cargo.toml | 2 +- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ad7bb8ea..9074e2e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -54,12 +54,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - [[package]] name = "base64" version = "0.21.0" @@ -750,7 +744,7 @@ dependencies = [ "arc-swap", "async-trait", "atomic_enum", - "base64 0.21.0", + "base64", "bb8", "bytes", "chrono", @@ -840,11 +834,11 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "postgres-protocol" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "878c6cbf956e03af9aa8204b407b9cbf47c072164800aa918c516cd4b056c50c" +checksum = "78b7fa9f396f51dffd61546fd8573ee20592287996568e6175ceb0f8699ad75d" dependencies = [ - "base64 0.13.1", + "base64", "byteorder", "bytes", "fallible-iterator", @@ -983,7 +977,7 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" dependencies = [ - "base64 0.21.0", + "base64", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 89cfe643..462c859f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ futures = "0.3" socket2 = { version = "0.4.7", features = ["all"] } nix = "0.26.2" atomic_enum = "0.2.0" -postgres-protocol = "0.6.4" +postgres-protocol = "0.6.5" fallible-iterator = "0.2" [target.'cfg(not(target_env = "msvc"))'.dependencies] From faa9c1f64a94f8b6bbb073fd886a3ff175075251 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 31 Mar 2023 09:35:01 -0700 Subject: [PATCH 13/79] chore(deps): bump futures from 0.3.27 to 0.3.28 (#392) Bumps [futures](https://github.com/rust-lang/futures-rs) from 0.3.27 to 0.3.28. - [Release notes](https://github.com/rust-lang/futures-rs/releases) - [Changelog](https://github.com/rust-lang/futures-rs/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-lang/futures-rs/compare/0.3.27...0.3.28) --- updated-dependencies: - dependency-name: futures dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9074e2e9..3b5e3b80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -277,9 +277,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "futures" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "531ac96c6ff5fd7c62263c5e3c67a603af4fcaee2e1a0ae5565ba3a11e69e549" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" dependencies = [ "futures-channel", "futures-core", @@ -292,9 +292,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "164713a5a0dcc3e7b4b1ed7d3b433cabc18025386f9339346e8daf15963cf7ac" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" dependencies = [ "futures-core", "futures-sink", @@ -302,15 +302,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86d7a0c1aa76363dac491de0ee99faf6941128376f1cf96f07db7603b7de69dd" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" [[package]] name = "futures-executor" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1997dd9df74cdac935c76252744c1ed5794fac083242ea4fe77ef3ed60ba0f83" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" dependencies = [ "futures-core", "futures-task", @@ -319,38 +319,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d422fa3cbe3b40dca574ab087abb5bc98258ea57eea3fd6f1fa7162c778b91" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" [[package]] name = "futures-macro" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eb14ed937631bd8b8b8977f2c198443447a8355b6e3ca599f38c975e5a963b6" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.9", ] [[package]] name = "futures-sink" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec93083a4aecafb2a80a885c9de1f0ccae9dbd32c2bb54b0c3a65690e0b8d2f2" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" [[package]] name = "futures-task" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd65540d33b37b16542a0438c12e6aeead10d4ac5d05bd3f805b8f35ab592879" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" [[package]] name = "futures-util" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ef6b17e481503ec85211fed8f39d1970f128935ca1f814cd32ac4a6842e84ab" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" dependencies = [ "futures-channel", "futures-core", From 7ddd23b514ca7898dc24c7d7a040a5b2c524a4c3 Mon Sep 17 00:00:00 2001 From: Mostafa Abdelraouf Date: Sat, 1 Apr 2023 15:27:57 -0500 Subject: [PATCH 14/79] Protocol-level test helpers (#393) I needed to have granular control over protocol message testing. For example, being able to send protocol messages one-by-one and then be able to inspect the results. In order to do that, I created this low-level ruby client that can be used to send protocol messages in any order without blocking and also allows inspection of response messages. --- tests/ruby/helpers/pg_socket.rb | 259 +++++++++++++++++++++++++++++ tests/ruby/helpers/pgcat_helper.rb | 1 + tests/ruby/protocol_spec.rb | 155 +++++++++++++++++ 3 files changed, 415 insertions(+) create mode 100644 tests/ruby/helpers/pg_socket.rb create mode 100644 tests/ruby/protocol_spec.rb diff --git a/tests/ruby/helpers/pg_socket.rb b/tests/ruby/helpers/pg_socket.rb new file mode 100644 index 00000000..42234491 --- /dev/null +++ b/tests/ruby/helpers/pg_socket.rb @@ -0,0 +1,259 @@ +require 'socket' +require 'digest/md5' + +BACKEND_MESSAGE_CODES = { + 'Z' => "ReadyForQuery", + 'C' => "CommandComplete", + 'T' => "RowDescription", + 'D' => "DataRow", + '1' => "ParseComplete", + '2' => "BindComplete", + 'E' => "ErrorResponse", + 's' => "PortalSuspended", +} + +class PostgresSocket + def initialize(host, port) + @port = port + @host = host + @socket = TCPSocket.new @host, @port + @parameters = {} + @verbose = true + end + + def send_md5_password_message(username, password, salt) + m = Digest::MD5.hexdigest(password + username) + m = Digest::MD5.hexdigest(m + salt.map(&:chr).join("")) + m = 'md5' + m + bytes = (m.split("").map(&:ord) + [0]).flatten + message_size = bytes.count + 4 + + message = [] + + message << 'p'.ord + message << [message_size].pack('l>').unpack('CCCC') # 4 + message << bytes + message.flatten! + + + @socket.write(message.pack('C*')) + end + + def send_startup_message(username, database, password) + message = [] + + message << [196608].pack('l>').unpack('CCCC') # 4 + message << "user".split('').map(&:ord) # 4, 8 + message << 0 # 1, 9 + message << username.split('').map(&:ord) # 2, 11 + message << 0 # 1, 12 + message << "database".split('').map(&:ord) # 8, 20 + message << 0 # 1, 21 + message << database.split('').map(&:ord) # 2, 23 + message << 0 # 1, 24 + message << 0 # 1, 25 + message.flatten! + + total_message_size = message.size + 4 + + message_len = [total_message_size].pack('l>').unpack('CCCC') + + @socket.write([message_len + message].flatten.pack('C*')) + + sleep 0.1 + + read_startup_response(username, password) + end + + def read_startup_response(username, password) + message_code, message_len = @socket.recv(5).unpack("al>") + while message_code == 'R' + auth_code = @socket.recv(4).unpack('l>').pop + case auth_code + when 5 # md5 + salt = @socket.recv(4).unpack('CCCC') + send_md5_password_message(username, password, salt) + message_code, message_len = @socket.recv(5).unpack("al>") + when 0 # trust + break + end + end + loop do + message_code, message_len = @socket.recv(5).unpack("al>") + if message_code == 'Z' + @socket.recv(1).unpack("a") # most likely I + break # We are good to go + end + if message_code == 'S' + actual_message = @socket.recv(message_len - 4).unpack("C*") + k,v = actual_message.pack('U*').split(/\x00/) + @parameters[k] = v + end + if message_code == 'K' + process_id, secret_key = @socket.recv(message_len - 4).unpack("l>l>") + @parameters["process_id"] = process_id + @parameters["secret_key"] = secret_key + end + end + return @parameters + end + + def cancel_query + socket = TCPSocket.new @host, @port + process_key = @parameters["process_id"] + secret_key = @parameters["secret_key"] + message = [] + message << [16].pack('l>').unpack('CCCC') # 4 + message << [80877102].pack('l>').unpack('CCCC') # 4 + message << [process_key.to_i].pack('l>').unpack('CCCC') # 4 + message << [secret_key.to_i].pack('l>').unpack('CCCC') # 4 + message.flatten! + socket.write(message.flatten.pack('C*')) + socket.close + log "[F] Sent CancelRequest message" + end + + def send_query_message(query) + query_size = query.length + message_size = 1 + 4 + query_size + message = [] + message << "Q".ord + message << [message_size].pack('l>').unpack('CCCC') # 4 + message << query.split('').map(&:ord) # 2, 11 + message << 0 # 1, 12 + message.flatten! + @socket.write(message.flatten.pack('C*')) + log "[F] Sent Q message (#{query})" + end + + def send_parse_message(query) + query_size = query.length + message_size = 2 + 2 + 4 + query_size + message = [] + message << "P".ord + message << [message_size].pack('l>').unpack('CCCC') # 4 + message << 0 # unnamed statement + message << query.split('').map(&:ord) # 2, 11 + message << 0 # 1, 12 + message << [0, 0] + message.flatten! + @socket.write(message.flatten.pack('C*')) + log "[F] Sent P message (#{query})" + end + + def send_bind_message + message = [] + message << "B".ord + message << [12].pack('l>').unpack('CCCC') # 4 + message << 0 # unnamed statement + message << 0 # unnamed statement + message << [0, 0] # 2 + message << [0, 0] # 2 + message << [0, 0] # 2 + message.flatten! + @socket.write(message.flatten.pack('C*')) + log "[F] Sent B message" + end + + def send_describe_message(mode) + message = [] + message << "D".ord + message << [6].pack('l>').unpack('CCCC') # 4 + message << mode.ord + message << 0 # unnamed statement + message.flatten! + @socket.write(message.flatten.pack('C*')) + log "[F] Sent D message" + end + + def send_execute_message(limit=0) + message = [] + message << "E".ord + message << [9].pack('l>').unpack('CCCC') # 4 + message << 0 # unnamed statement + message << [limit].pack('l>').unpack('CCCC') # 4 + message.flatten! + @socket.write(message.flatten.pack('C*')) + log "[F] Sent E message" + end + + def send_sync_message + message = [] + message << "S".ord + message << [4].pack('l>').unpack('CCCC') # 4 + message.flatten! + @socket.write(message.flatten.pack('C*')) + log "[F] Sent S message" + end + + def send_copydone_message + message = [] + message << "c".ord + message << [4].pack('l>').unpack('CCCC') # 4 + message.flatten! + @socket.write(message.flatten.pack('C*')) + log "[F] Sent c message" + end + + def send_copyfail_message + message = [] + message << "f".ord + message << [5].pack('l>').unpack('CCCC') # 4 + message << 0 + message.flatten! + @socket.write(message.flatten.pack('C*')) + log "[F] Sent f message" + end + + def send_flush_message + message = [] + message << "H".ord + message << [4].pack('l>').unpack('CCCC') # 4 + message.flatten! + @socket.write(message.flatten.pack('C*')) + log "[F] Sent H message" + end + + def read_from_server() + output_messages = [] + retry_count = 0 + message_code = nil + message_len = 0 + loop do + begin + message_code, message_len = @socket.recv_nonblock(5).unpack("al>") + rescue IO::WaitReadable + return output_messages if retry_count > 50 + + retry_count += 1 + sleep(0.01) + next + end + message = { + code: message_code, + len: message_len, + bytes: [] + } + log "[B] #{BACKEND_MESSAGE_CODES[message_code] || ('UnknownMessage(' + message_code + ')')}" + + actual_message_length = message_len - 4 + if actual_message_length > 0 + message[:bytes] = @socket.recv(message_len - 4).unpack("C*") + log "\t#{message[:bytes].join(",")}" + log "\t#{message[:bytes].map(&:chr).join(" ")}" + end + output_messages << message + return output_messages if message_code == 'Z' + end + end + + def log(msg) + return unless @verbose + + puts msg + end + + def close + @socket.close + end +end diff --git a/tests/ruby/helpers/pgcat_helper.rb b/tests/ruby/helpers/pgcat_helper.rb index 13dc6686..ad4c32a4 100644 --- a/tests/ruby/helpers/pgcat_helper.rb +++ b/tests/ruby/helpers/pgcat_helper.rb @@ -2,6 +2,7 @@ require 'ostruct' require_relative 'pgcat_process' require_relative 'pg_instance' +require_relative 'pg_socket' class ::Hash def deep_merge(second) diff --git a/tests/ruby/protocol_spec.rb b/tests/ruby/protocol_spec.rb new file mode 100644 index 00000000..9737650b --- /dev/null +++ b/tests/ruby/protocol_spec.rb @@ -0,0 +1,155 @@ +# frozen_string_literal: true +require_relative 'spec_helper' + + +describe "Portocol handling" do + let(:processes) { Helpers::Pgcat.single_instance_setup("sharded_db", 1, "session") } + let(:sequence) { [] } + let(:pgcat_socket) { PostgresSocket.new('localhost', processes.pgcat.port) } + let(:pgdb_socket) { PostgresSocket.new('localhost', processes.all_databases.first.port) } + + after do + pgdb_socket.close + pgcat_socket.close + processes.all_databases.map(&:reset) + processes.pgcat.shutdown + end + + def run_comparison(sequence, socket_a, socket_b) + sequence.each do |msg, *args| + socket_a.send(msg, *args) + socket_b.send(msg, *args) + + compare_messages( + socket_a.read_from_server, + socket_b.read_from_server + ) + end + end + + def compare_messages(msg_arr0, msg_arr1) + if msg_arr0.count != msg_arr1.count + error_output = [] + + error_output << "#{msg_arr0.count} : #{msg_arr1.count}" + error_output << "PgCat Messages" + error_output += msg_arr0.map { |message| "\t#{message[:code]} - #{message[:bytes].map(&:chr).join(" ")}" } + error_output << "PgServer Messages" + error_output += msg_arr1.map { |message| "\t#{message[:code]} - #{message[:bytes].map(&:chr).join(" ")}" } + error_desc = error_output.join("\n") + raise StandardError, "Message count mismatch #{error_desc}" + end + + (0..msg_arr0.count - 1).all? do |i| + msg0 = msg_arr0[i] + msg1 = msg_arr1[i] + + result = [ + msg0[:code] == msg1[:code], + msg0[:len] == msg1[:len], + msg0[:bytes] == msg1[:bytes], + ].all? + + next result if result + + if result == false + error_string = [] + if msg0[:code] != msg1[:code] + error_string << "code #{msg0[:code]} != #{msg1[:code]}" + end + if msg0[:len] != msg1[:len] + error_string << "len #{msg0[:len]} != #{msg1[:len]}" + end + if msg0[:bytes] != msg1[:bytes] + error_string << "bytes #{msg0[:bytes]} != #{msg1[:bytes]}" + end + err = error_string.join("\n") + + raise StandardError, "Message mismatch #{err}" + end + end + end + + RSpec.shared_examples "at parity with database" do + before do + pgcat_socket.send_startup_message("sharding_user", "sharded_db", "sharding_user") + pgdb_socket.send_startup_message("sharding_user", "shard0", "sharding_user") + end + + it "works" do + run_comparison(sequence, pgcat_socket, pgdb_socket) + end + end + + context "Cancel Query" do + let(:sequence) { + [ + [:send_query_message, "SELECT pg_sleep(5)"], + [:cancel_query] + ] + } + + it_behaves_like "at parity with database" + end + + xcontext "Simple query after parse" do + let(:sequence) { + [ + [:send_parse_message, "SELECT 5"], + [:send_query_message, "SELECT 1"], + [:send_bind_message], + [:send_describe_message, "P"], + [:send_execute_message], + [:send_sync_message], + ] + } + + # Known to fail due to PgCat not supporting flush + it_behaves_like "at parity with database" + end + + xcontext "Flush message" do + let(:sequence) { + [ + [:send_parse_message, "SELECT 1"], + [:send_flush_message] + ] + } + + # Known to fail due to PgCat not supporting flush + it_behaves_like "at parity with database" + end + + xcontext "Bind without parse" do + let(:sequence) { + [ + [:send_bind_message] + ] + } + # This is known to fail. + # Server responds immediately, Proxy buffers the message + it_behaves_like "at parity with database" + end + + context "Simple message" do + let(:sequence) { + [[:send_query_message, "SELECT 1"]] + } + + it_behaves_like "at parity with database" + end + + context "Extended protocol" do + let(:sequence) { + [ + [:send_parse_message, "SELECT 1"], + [:send_bind_message], + [:send_describe_message, "P"], + [:send_execute_message], + [:send_sync_message], + ] + } + + it_behaves_like "at parity with database" + end +end From 89e15f09b5d85388541d04982cac5abef77f426a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 2 Apr 2023 23:00:09 -0700 Subject: [PATCH 15/79] chore(deps): bump tokio-rustls from 0.23.4 to 0.24.0 (#394) Bumps [tokio-rustls](https://github.com/tokio-rs/tls) from 0.23.4 to 0.24.0. - [Release notes](https://github.com/tokio-rs/tls/releases) - [Commits](https://github.com/tokio-rs/tls/commits) --- updated-dependencies: - dependency-name: tokio-rustls dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 31 +++++++++++++++---------------- Cargo.toml | 2 +- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3b5e3b80..be4cb3ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -961,14 +961,14 @@ dependencies = [ [[package]] name = "rustls" -version = "0.20.8" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" +checksum = "07180898a28ed6a7f7ba2311594308f595e3dd2e3c3812fa0a80a47b45f17e5d" dependencies = [ "log", "ring", + "rustls-webpki", "sct", - "webpki", ] [[package]] @@ -980,6 +980,16 @@ dependencies = [ "base64", ] +[[package]] +name = "rustls-webpki" +version = "0.100.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6207cd5ed3d8dca7816f8f3725513a34609c0c765bf652b8c3cb4cfd87db46b" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "scopeguard" version = "1.1.0" @@ -1217,13 +1227,12 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.23.4" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +checksum = "e0d409377ff5b1e3ca6437aa86c1eb7d40c134bfec254e44c830defa92669db5" dependencies = [ "rustls", "tokio", - "webpki", ] [[package]] @@ -1437,16 +1446,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webpki" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 462c859f..02c80881 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ hmac = "0.12" sha2 = "0.10" base64 = "0.21" stringprep = "0.1" -tokio-rustls = "0.23" +tokio-rustls = "0.24" rustls-pemfile = "1" hyper = { version = "0.14", features = ["full"] } phf = { version = "0.11.1", features = ["macros"] } From a62f6b0eea230b130fc39d285251ba24e3117516 Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Wed, 5 Apr 2023 15:06:19 -0700 Subject: [PATCH 16/79] Fix port; add user pool mode (#395) * Fix port; add user pool mode * will probably break our session/transaction mode tests --- pgcat.toml | 1 + src/auth_passthrough.rs | 1 + src/config.rs | 23 ++++++++++++++++++----- src/pool.rs | 5 ++++- 4 files changed, 24 insertions(+), 6 deletions(-) diff --git a/pgcat.toml b/pgcat.toml index 0d883a33..a6a4af50 100644 --- a/pgcat.toml +++ b/pgcat.toml @@ -134,6 +134,7 @@ password = "sharding_user" # is the sum of pool_size across all users. pool_size = 9 + # Maximum query duration. Dangerous, but protects against DBs that died in a non-obvious way. # 0 means it is disabled. statement_timeout = 0 diff --git a/src/auth_passthrough.rs b/src/auth_passthrough.rs index b9f0e97f..76483e59 100644 --- a/src/auth_passthrough.rs +++ b/src/auth_passthrough.rs @@ -73,6 +73,7 @@ impl AuthPassthrough { password: Some(self.password.clone()), pool_size: 1, statement_timeout: 0, + pool_mode: None, }; let user = &address.username; diff --git a/src/config.rs b/src/config.rs index 6545457c..37494f67 100644 --- a/src/config.rs +++ b/src/config.rs @@ -179,6 +179,7 @@ pub struct User { pub username: String, pub password: Option, pub pool_size: u32, + pub pool_mode: Option, #[serde(default)] // 0 pub statement_timeout: u64, } @@ -190,6 +191,7 @@ impl Default for User { password: None, pool_size: 15, statement_timeout: 0, + pool_mode: None, } } } @@ -201,7 +203,7 @@ pub struct General { pub host: String, #[serde(default = "General::default_port")] - pub port: i16, + pub port: u16, pub enable_prometheus_exporter: Option, pub prometheus_exporter_port: i16, @@ -261,7 +263,7 @@ impl General { "0.0.0.0".into() } - pub fn default_port() -> i16 { + pub fn default_port() -> u16 { 5432 } @@ -356,6 +358,7 @@ pub enum PoolMode { #[serde(alias = "session", alias = "Session")] Session, } + impl ToString for PoolMode { fn to_string(&self) -> String { match *self { @@ -816,8 +819,9 @@ impl Config { .to_string() ); info!( - "[pool: {}] Pool mode: {:?}", - pool_name, pool_config.pool_mode + "[pool: {}] Default pool mode: {}", + pool_name, + pool_config.pool_mode.to_string() ); info!( "[pool: {}] Load Balancing mode: {:?}", @@ -868,7 +872,16 @@ impl Config { info!( "[pool: {}][user: {}] Statement timeout: {}", pool_name, user.1.username, user.1.statement_timeout - ) + ); + info!( + "[pool: {}][user: {}] Pool mode: {}", + pool_name, + user.1.username, + match user.1.pool_mode { + Some(pool_mode) => pool_mode.to_string(), + None => pool_config.pool_mode.to_string(), + } + ); } } } diff --git a/src/pool.rs b/src/pool.rs index e1ab7cb4..7f8e41c0 100644 --- a/src/pool.rs +++ b/src/pool.rs @@ -382,7 +382,10 @@ impl ConnectionPool { server_info: Arc::new(RwLock::new(BytesMut::new())), auth_hash: pool_auth_hash, settings: PoolSettings { - pool_mode: pool_config.pool_mode, + pool_mode: match user.pool_mode { + Some(pool_mode) => pool_mode, + None => pool_config.pool_mode, + }, load_balancing_mode: pool_config.load_balancing_mode, // shards: pool_config.shards.clone(), shards: shard_ids.len(), From 692353c83916a1ed9da01e126f24e4c5baaea23c Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Mon, 10 Apr 2023 14:51:01 -0700 Subject: [PATCH 17/79] A couple things (#397) * Format cleanup * fmt * finally --- .circleci/pgcat.toml | 2 +- .editorconfig | 14 ++++ Cargo.lock | 2 +- Cargo.toml | 2 +- examples/docker/pgcat.toml | 3 - pgcat.toml | 2 +- src/auth_passthrough.rs | 48 ++++++++---- src/client.rs | 146 +++++++++++++++++++++++++------------ src/config.rs | 6 +- src/errors.rs | 102 +++++++++++++++++++++++++- src/main.rs | 25 ++++--- src/server.rs | 113 +++++++++++++++++++++++----- src/stats/server.rs | 1 - 13 files changed, 366 insertions(+), 100 deletions(-) create mode 100644 .editorconfig diff --git a/.circleci/pgcat.toml b/.circleci/pgcat.toml index 0d47ed72..377680a0 100644 --- a/.circleci/pgcat.toml +++ b/.circleci/pgcat.toml @@ -39,7 +39,7 @@ log_client_connections = false log_client_disconnections = false # Reload config automatically if it changes. -autoreload = true +autoreload = 15000 # TLS tls_certificate = ".circleci/server.cert" diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..d7a2758d --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +root = true + +[*] +trim_trailing_whitespace = true +insert_final_newline = true + +[*.rs] +indent_style = space +indent_size = 4 +max_line_length = 120 + +[*.toml] +indent_style = space +indent_size = 2 diff --git a/Cargo.lock b/Cargo.lock index be4cb3ec..5d6c56f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -739,7 +739,7 @@ dependencies = [ [[package]] name = "pgcat" -version = "1.0.0" +version = "1.0.1" dependencies = [ "arc-swap", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 02c80881..4e33c645 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pgcat" -version = "1.0.0" +version = "1.0.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/examples/docker/pgcat.toml b/examples/docker/pgcat.toml index c41c8cdd..bfc4c2e2 100644 --- a/examples/docker/pgcat.toml +++ b/examples/docker/pgcat.toml @@ -38,9 +38,6 @@ log_client_connections = false # If we should log client disconnections log_client_disconnections = false -# Reload config automatically if it changes. -autoreload = false - # TLS # tls_certificate = "server.cert" # tls_private_key = "server.key" diff --git a/pgcat.toml b/pgcat.toml index a6a4af50..18339197 100644 --- a/pgcat.toml +++ b/pgcat.toml @@ -45,7 +45,7 @@ log_client_connections = false log_client_disconnections = false # When set to true, PgCat reloads configs if it detects a change in the config file. -autoreload = false +autoreload = 15000 # Number of worker threads the Runtime will use (4 by default). worker_threads = 5 diff --git a/src/auth_passthrough.rs b/src/auth_passthrough.rs index 76483e59..f313dead 100644 --- a/src/auth_passthrough.rs +++ b/src/auth_passthrough.rs @@ -1,4 +1,5 @@ use crate::errors::Error; +use crate::pool::ConnectionPool; use crate::server::Server; use log::debug; @@ -78,19 +79,25 @@ impl AuthPassthrough { let user = &address.username; - debug!("Connecting to server to obtain auth hashes."); + debug!("Connecting to server to obtain auth hashes"); + let auth_query = self.query.replace("$1", user); + match Server::exec_simple_query(address, &auth_user, &auth_query).await { Ok(password_data) => { if password_data.len() == 2 && password_data.first().unwrap() == user { - if let Some(stripped_hash) = password_data.last().unwrap().to_string().strip_prefix("md5") { - Ok(stripped_hash.to_string()) - } - else { - Err(Error::AuthPassthroughError( - "Obtained hash from auth_query does not seem to be in md5 format.".to_string(), - )) - } + if let Some(stripped_hash) = password_data + .last() + .unwrap() + .to_string() + .strip_prefix("md5") { + Ok(stripped_hash.to_string()) + } + else { + Err(Error::AuthPassthroughError( + "Obtained hash from auth_query does not seem to be in md5 format.".to_string(), + )) + } } else { Err(Error::AuthPassthroughError( "Data obtained from query does not follow the scheme 'user','hash'." @@ -99,10 +106,25 @@ impl AuthPassthrough { } } Err(err) => { - Err(Error::AuthPassthroughError( - format!("Error trying to obtain password from auth_query, ignoring hash for user '{}'. Error: {:?}", - user, err))) + Err(Error::AuthPassthroughError( + format!("Error trying to obtain password from auth_query, ignoring hash for user '{}'. Error: {:?}", + user, err)) + ) } - } + } + } +} + +pub async fn refetch_auth_hash(pool: &ConnectionPool) -> Result { + let address = pool.address(0, 0); + if let Some(apt) = AuthPassthrough::from_pool_settings(&pool.settings) { + let hash = apt.fetch_hash(address).await?; + + return Ok(hash); } + + Err(Error::ClientError(format!( + "Could not obtain hash for {{ username: {:?}, database: {:?} }}. Auth passthrough not enabled.", + address.username, address.database + ))) } diff --git a/src/client.rs b/src/client.rs index d75c069d..4114e4bb 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,4 +1,4 @@ -use crate::errors::Error; +use crate::errors::{ClientIdentifier, Error}; use crate::pool::BanReason; /// Handle clients by pretending to be a PostgreSQL server. use bytes::{Buf, BufMut, BytesMut}; @@ -12,7 +12,7 @@ use tokio::sync::broadcast::Receiver; use tokio::sync::mpsc::Sender; use crate::admin::{generate_server_info_for_admin, handle_admin}; -use crate::auth_passthrough::AuthPassthrough; +use crate::auth_passthrough::refetch_auth_hash; use crate::config::{get_config, get_idle_client_in_transaction_timeout, Address, PoolMode}; use crate::constants::*; use crate::messages::*; @@ -202,7 +202,7 @@ pub async fn client_entrypoint( // Client probably disconnected rejecting our plain text connection. Ok((ClientConnectionType::Tls, _)) | Ok((ClientConnectionType::CancelQuery, _)) => Err(Error::ProtocolSyncError( - format!("Bad postgres client (plain)"), + "Bad postgres client (plain)".into(), )), Err(err) => Err(err), @@ -369,28 +369,14 @@ pub async fn startup_tls( } // Bad Postgres client. - Ok((ClientConnectionType::Tls, _)) | Ok((ClientConnectionType::CancelQuery, _)) => Err( - Error::ProtocolSyncError(format!("Bad postgres client (tls)")), - ), + Ok((ClientConnectionType::Tls, _)) | Ok((ClientConnectionType::CancelQuery, _)) => { + Err(Error::ProtocolSyncError("Bad postgres client (tls)".into())) + } Err(err) => Err(err), } } -async fn refetch_auth_hash(pool: &ConnectionPool) -> Result { - let address = pool.address(0, 0); - if let Some(apt) = AuthPassthrough::from_pool_settings(&pool.settings) { - let hash = apt.fetch_hash(address).await?; - - return Ok(hash); - } - - Err(Error::ClientError(format!( - "Could not obtain hash for {{ username: {:?}, database: {:?} }}. Auth passthrough not enabled.", - address.username, address.database - ))) -} - impl Client where S: tokio::io::AsyncRead + std::marker::Unpin, @@ -418,7 +404,7 @@ where Some(user) => user, None => { return Err(Error::ClientError( - "Missing user parameter on client startup".to_string(), + "Missing user parameter on client startup".into(), )) } }; @@ -433,6 +419,8 @@ where None => "pgcat", }; + let client_identifier = ClientIdentifier::new(&application_name, &username, &pool_name); + let admin = ["pgcat", "pgbouncer"] .iter() .filter(|db| *db == pool_name) @@ -463,7 +451,12 @@ where let code = match read.read_u8().await { Ok(p) => p, - Err(_) => return Err(Error::SocketError(format!("Error reading password code from client {{ username: {:?}, pool_name: {:?}, application_name: {:?} }}", username, pool_name, application_name))), + Err(_) => { + return Err(Error::ClientSocketError( + "password code".into(), + client_identifier, + )) + } }; // PasswordMessage @@ -476,19 +469,30 @@ where let len = match read.read_i32().await { Ok(len) => len, - Err(_) => return Err(Error::SocketError(format!("Error reading password message length from client {{ username: {:?}, pool_name: {:?}, application_name: {:?} }}", username, pool_name, application_name))), + Err(_) => { + return Err(Error::ClientSocketError( + "password message length".into(), + client_identifier, + )) + } }; let mut password_response = vec![0u8; (len - 4) as usize]; match read.read_exact(&mut password_response).await { Ok(_) => (), - Err(_) => return Err(Error::SocketError(format!("Error reading password message from client {{ username: {:?}, pool_name: {:?}, application_name: {:?} }}", username, pool_name, application_name))), + Err(_) => { + return Err(Error::ClientSocketError( + "password message".into(), + client_identifier, + )) + } }; // Authenticate admin user. let (transaction_mode, server_info) = if admin { let config = get_config(); + // Compare server and client hashes. let password_hash = md5_hash_password( &config.general.admin_username, @@ -497,10 +501,12 @@ where ); if password_hash != password_response { - warn!("Invalid password {{ username: {:?}, pool_name: {:?}, application_name: {:?} }}", username, pool_name, application_name); + let error = Error::ClientGeneralError("Invalid password".into(), client_identifier); + + warn!("{}", error); wrong_password(&mut write, username).await?; - return Err(Error::ClientError(format!("Invalid password {{ username: {:?}, pool_name: {:?}, application_name: {:?} }}", username, pool_name, application_name))); + return Err(error); } (false, generate_server_info_for_admin()) @@ -519,7 +525,10 @@ where ) .await?; - return Err(Error::ClientError(format!("Invalid pool name {{ username: {:?}, pool_name: {:?}, application_name: {:?} }}", username, pool_name, application_name))); + return Err(Error::ClientGeneralError( + "Invalid pool name".into(), + client_identifier, + )); } }; @@ -530,16 +539,23 @@ where Some(md5_hash_password(username, password, &salt)) } else { if !get_config().is_auth_query_configured() { - return Err(Error::ClientError(format!("Client auth not possible, no cleartext password set for username: {:?} in config and auth passthrough (query_auth) is not set up.", username))); + return Err(Error::ClientAuthImpossible(username.into())); } let mut hash = (*pool.auth_hash.read()).clone(); if hash.is_none() { - warn!("Query auth configured but no hash password found for pool {}. Will try to refetch it.", pool_name); + warn!( + "Query auth configured \ + but no hash password found \ + for pool {}. Will try to refetch it.", + pool_name + ); + match refetch_auth_hash(&pool).await { Ok(fetched_hash) => { - warn!("Password for {{ username: {:?}, pool_name: {:?}, application_name: {:?} }}, obtained. Updating.", username, pool_name, application_name); + warn!("Password for {}, obtained. Updating.", client_identifier); + { let mut pool_auth_hash = pool.auth_hash.write(); *pool_auth_hash = Some(fetched_hash.clone()); @@ -547,16 +563,12 @@ where hash = Some(fetched_hash); } + Err(err) => { - return Err( - Error::ClientError( - format!("No cleartext password set, and no auth passthrough could not obtain the hash from server for {{ username: {:?}, pool_name: {:?}, application_name: {:?} }}, the error was: {:?}", - username, - pool_name, - application_name, - err) - ) - ); + return Err(Error::ClientAuthPassthroughError( + err.to_string(), + client_identifier, + )); } } }; @@ -570,20 +582,31 @@ where // // @TODO: we could end up fetching again the same password twice (see above). if password_hash.unwrap() != password_response { - warn!("Invalid password {{ username: {:?}, pool_name: {:?}, application_name: {:?} }}, will try to refetch it.", username, pool_name, application_name); + warn!( + "Invalid password {}, will try to refetch it.", + client_identifier + ); + let fetched_hash = refetch_auth_hash(&pool).await?; let new_password_hash = md5_hash_second_pass(&fetched_hash, &salt); // Ok password changed in server an auth is possible. if new_password_hash == password_response { - warn!("Password for {{ username: {:?}, pool_name: {:?}, application_name: {:?} }}, changed in server. Updating.", username, pool_name, application_name); + warn!( + "Password for {}, changed in server. Updating.", + client_identifier + ); + { let mut pool_auth_hash = pool.auth_hash.write(); *pool_auth_hash = Some(fetched_hash); } } else { wrong_password(&mut write, username).await?; - return Err(Error::ClientError(format!("Invalid password {{ username: {:?}, pool_name: {:?}, application_name: {:?} }}", username, pool_name, application_name))); + return Err(Error::ClientGeneralError( + "Invalid password".into(), + client_identifier, + )); } } @@ -753,9 +776,9 @@ where &mut self.write, "terminating connection due to administrator command" ).await?; - self.stats.disconnect(); - return Ok(()) + self.stats.disconnect(); + return Ok(()); } // Admin clients ignore shutdown. @@ -928,11 +951,26 @@ where error!("Got Sync message but failed to get a connection from the pool"); self.buffer.clear(); } + error_response(&mut self.write, "could not get connection from the pool") .await?; - error!("Could not get connection from pool: {{ pool_name: {:?}, username: {:?}, shard: {:?}, role: \"{:?}\", error: \"{:?}\" }}", - self.pool_name.clone(), self.username.clone(), query_router.shard(), query_router.role(), err); + error!( + "Could not get connection from pool: \ + {{ \ + pool_name: {:?}, \ + username: {:?}, \ + shard: {:?}, \ + role: \"{:?}\", \ + error: \"{:?}\" \ + }}", + self.pool_name, + self.username, + query_router.shard(), + query_router.role(), + err + ); + continue; } }; @@ -999,11 +1037,25 @@ where Err(_) => { // Client idle in transaction timeout error_response(&mut self.write, "idle transaction timeout").await?; - error!("Client idle in transaction timeout: {{ pool_name: {:?}, username: {:?}, shard: {:?}, role: \"{:?}\"}}", self.pool_name.clone(), self.username.clone(), query_router.shard(), query_router.role()); + error!( + "Client idle in transaction timeout: \ + {{ \ + pool_name: {}, \ + username: {}, \ + shard: {}, \ + role: \"{:?}\" \ + }}", + self.pool_name, + self.username, + query_router.shard(), + query_router.role() + ); + break; } } } + Some(message) => { initial_message = None; message diff --git a/src/config.rs b/src/config.rs index 37494f67..13528af2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -245,8 +245,8 @@ pub struct General { #[serde(default = "General::default_worker_threads")] pub worker_threads: usize, - #[serde(default)] // False - pub autoreload: bool, + #[serde(default)] // None + pub autoreload: Option, pub tls_certificate: Option, pub tls_private_key: Option, @@ -335,7 +335,7 @@ impl Default for General { tcp_keepalives_interval: Self::default_tcp_keepalives_interval(), log_client_connections: false, log_client_disconnections: false, - autoreload: false, + autoreload: None, tls_certificate: None, tls_private_key: None, admin_username: String::from("admin"), diff --git a/src/errors.rs b/src/errors.rs index 58fc088b..0930ab8b 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,13 +1,19 @@ -/// Errors. +//! Errors. /// Various errors. #[derive(Debug, PartialEq)] pub enum Error { SocketError(String), + ClientSocketError(String, ClientIdentifier), + ClientGeneralError(String, ClientIdentifier), + ClientAuthImpossible(String), + ClientAuthPassthroughError(String, ClientIdentifier), ClientBadStartup, ProtocolSyncError(String), BadQuery(String), ServerError, + ServerStartupError(String, ServerIdentifier), + ServerAuthError(String, ServerIdentifier), BadConfig, AllServersDown, ClientError(String), @@ -18,3 +24,97 @@ pub enum Error { AuthError(String), AuthPassthroughError(String), } + +#[derive(Clone, PartialEq, Debug)] +pub struct ClientIdentifier { + pub application_name: String, + pub username: String, + pub pool_name: String, +} + +impl ClientIdentifier { + pub fn new(application_name: &str, username: &str, pool_name: &str) -> ClientIdentifier { + ClientIdentifier { + application_name: application_name.into(), + username: username.into(), + pool_name: pool_name.into(), + } + } +} + +impl std::fmt::Display for ClientIdentifier { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "{{ application_name: {}, username: {}, pool_name: {} }}", + self.application_name, self.username, self.pool_name + ) + } +} + +#[derive(Clone, PartialEq, Debug)] +pub struct ServerIdentifier { + pub username: String, + pub database: String, +} + +impl ServerIdentifier { + pub fn new(username: &str, database: &str) -> ServerIdentifier { + ServerIdentifier { + username: username.into(), + database: database.into(), + } + } +} + +impl std::fmt::Display for ServerIdentifier { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "{{ username: {}, database: {} }}", + self.username, self.database + ) + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match &self { + &Error::ClientSocketError(error, client_identifier) => write!( + f, + "Error reading {} from client {}", + error, client_identifier + ), + &Error::ClientGeneralError(error, client_identifier) => { + write!(f, "{} {}", error, client_identifier) + } + &Error::ClientAuthImpossible(username) => write!( + f, + "Client auth not possible, \ + no cleartext password set for username: {} \ + in config and auth passthrough (query_auth) \ + is not set up.", + username + ), + &Error::ClientAuthPassthroughError(error, client_identifier) => write!( + f, + "No cleartext password set, \ + and no auth passthrough could not \ + obtain the hash from server for {}, \ + the error was: {}", + client_identifier, error + ), + &Error::ServerStartupError(error, server_identifier) => write!( + f, + "Error reading {} on server startup {}", + error, server_identifier, + ), + &Error::ServerAuthError(error, server_identifier) => { + write!(f, "{} for {}", error, server_identifier,) + } + + // The rest can use Debug. + err => write!(f, "{:?}", err), + } + } +} diff --git a/src/main.rs b/src/main.rs index 4c8987f1..39c67c2e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -179,16 +179,19 @@ fn main() -> Result<(), Box> { stats_collector.collect().await; }); - info!("Config autoreloader: {}", config.general.autoreload); + info!("Config autoreloader: {}", match config.general.autoreload { + Some(interval) => format!("{} ms", interval), + None => "disabled".into(), + }); - let mut autoreload_interval = tokio::time::interval(tokio::time::Duration::from_millis(15_000)); - let autoreload_client_server_map = client_server_map.clone(); + if let Some(interval) = config.general.autoreload { + let mut autoreload_interval = tokio::time::interval(tokio::time::Duration::from_millis(interval)); + let autoreload_client_server_map = client_server_map.clone(); - tokio::task::spawn(async move { - loop { - autoreload_interval.tick().await; - if config.general.autoreload { - info!("Automatically reloading config"); + tokio::task::spawn(async move { + loop { + autoreload_interval.tick().await; + debug!("Automatically reloading config"); if let Ok(changed) = reload_config(autoreload_client_server_map.clone()).await { if changed { @@ -196,8 +199,10 @@ fn main() -> Result<(), Box> { } }; } - } - }); + }); + }; + + #[cfg(windows)] let mut term_signal = win_signal::ctrl_close().unwrap(); diff --git a/src/server.rs b/src/server.rs index 37f0e0c7..14862bd0 100644 --- a/src/server.rs +++ b/src/server.rs @@ -17,7 +17,7 @@ use tokio::net::{ use crate::config::{Address, User}; use crate::constants::*; -use crate::errors::Error; +use crate::errors::{Error, ServerIdentifier}; use crate::messages::*; use crate::mirrors::MirroringManager; use crate::pool::ClientServerMap; @@ -108,6 +108,7 @@ impl Server { let mut server_info = BytesMut::new(); let mut process_id: i32 = 0; let mut secret_key: i32 = 0; + let server_identifier = ServerIdentifier::new(&user.username, &database); // We'll be handling multiple packets, but they will all be structured the same. // We'll loop here until this exchange is complete. @@ -119,12 +120,22 @@ impl Server { loop { let code = match stream.read_u8().await { Ok(code) => code as char, - Err(_) => return Err(Error::SocketError(format!("Error reading message code on server startup {{ username: {:?}, database: {:?} }}", user.username, database))), + Err(_) => { + return Err(Error::ServerStartupError( + "message code".into(), + server_identifier, + )) + } }; let len = match stream.read_i32().await { Ok(len) => len, - Err(_) => return Err(Error::SocketError(format!("Error reading message len on server startup {{ username: {:?}, database: {:?} }}", user.username, database))), + Err(_) => { + return Err(Error::ServerStartupError( + "message len".into(), + server_identifier, + )) + } }; trace!("Message: {}", code); @@ -135,7 +146,12 @@ impl Server { // Determine which kind of authentication is required, if any. let auth_code = match stream.read_i32().await { Ok(auth_code) => auth_code, - Err(_) => return Err(Error::SocketError(format!("Error reading auth code on server startup {{ username: {:?}, database: {:?} }}", user.username, database))), + Err(_) => { + return Err(Error::ServerStartupError( + "auth code".into(), + server_identifier, + )) + } }; trace!("Auth: {}", auth_code); @@ -148,7 +164,12 @@ impl Server { match stream.read_exact(&mut salt).await { Ok(_) => (), - Err(_) => return Err(Error::SocketError(format!("Error reading salt on server startup {{ username: {:?}, database: {:?} }}", user.username, database))), + Err(_) => { + return Err(Error::ServerStartupError( + "salt".into(), + server_identifier, + )) + } }; match &user.password { @@ -171,8 +192,12 @@ impl Server { &salt[..], ) .await?, - None => - return Err(Error::AuthError(format!("Auth passthrough (auth_query) failed and no user password is set in cleartext for {{ username: {:?}, database: {:?} }}", user.username, database))) + None => return Err( + Error::ServerAuthError( + "Auth passthrough (auth_query) failed and no user password is set in cleartext".into(), + server_identifier + ) + ), } } } @@ -182,16 +207,28 @@ impl Server { SASL => { if scram.is_none() { - return Err(Error::AuthError(format!("SASL auth required and not password specified, auth passthrough (auth_query) method is currently unsupported for SASL auth {{ username: {:?}, database: {:?} }}", user.username, database))); + return Err(Error::ServerAuthError( + "SASL auth required and no password specified. \ + Auth passthrough (auth_query) method is currently \ + unsupported for SASL auth" + .into(), + server_identifier, + )); } debug!("Starting SASL authentication"); + let sasl_len = (len - 8) as usize; let mut sasl_auth = vec![0u8; sasl_len]; match stream.read_exact(&mut sasl_auth).await { Ok(_) => (), - Err(_) => return Err(Error::SocketError(format!("Error reading sasl message on server startup {{ username: {:?}, database: {:?} }}", user.username, database))), + Err(_) => { + return Err(Error::ServerStartupError( + "sasl message".into(), + server_identifier, + )) + } }; let sasl_type = String::from_utf8_lossy(&sasl_auth[..sasl_len - 2]); @@ -233,7 +270,12 @@ impl Server { match stream.read_exact(&mut sasl_data).await { Ok(_) => (), - Err(_) => return Err(Error::SocketError(format!("Error reading sasl cont message on server startup {{ username: {:?}, database: {:?} }}", user.username, database))), + Err(_) => { + return Err(Error::ServerStartupError( + "sasl cont message".into(), + server_identifier, + )) + } }; let msg = BytesMut::from(&sasl_data[..]); @@ -254,7 +296,12 @@ impl Server { let mut sasl_final = vec![0u8; len as usize - 8]; match stream.read_exact(&mut sasl_final).await { Ok(_) => (), - Err(_) => return Err(Error::SocketError(format!("Error reading sasl final message on server startup {{ username: {:?}, database: {:?} }}", user.username, database))), + Err(_) => { + return Err(Error::ServerStartupError( + "sasl final message".into(), + server_identifier, + )) + } }; match scram @@ -284,7 +331,12 @@ impl Server { 'E' => { let error_code = match stream.read_u8().await { Ok(error_code) => error_code, - Err(_) => return Err(Error::SocketError(format!("Error reading error code message on server startup {{ username: {:?}, database: {:?} }}", user.username, database))), + Err(_) => { + return Err(Error::ServerStartupError( + "error code message".into(), + server_identifier, + )) + } }; trace!("Error: {}", error_code); @@ -300,7 +352,12 @@ impl Server { match stream.read_exact(&mut error).await { Ok(_) => (), - Err(_) => return Err(Error::SocketError(format!("Error reading error message on server startup {{ username: {:?}, database: {:?} }}", user.username, database))), + Err(_) => { + return Err(Error::ServerStartupError( + "error message".into(), + server_identifier, + )) + } }; // TODO: the error message contains multiple fields; we can decode them and @@ -319,7 +376,12 @@ impl Server { match stream.read_exact(&mut param).await { Ok(_) => (), - Err(_) => return Err(Error::SocketError(format!("Error reading parameter status message on server startup {{ username: {:?}, database: {:?} }}", user.username, database))), + Err(_) => { + return Err(Error::ServerStartupError( + "parameter status message".into(), + server_identifier, + )) + } }; // Save the parameter so we can pass it to the client later. @@ -336,12 +398,22 @@ impl Server { // See: . process_id = match stream.read_i32().await { Ok(id) => id, - Err(_) => return Err(Error::SocketError(format!("Error reading process id message on server startup {{ username: {:?}, database: {:?} }}", user.username, database))), + Err(_) => { + return Err(Error::ServerStartupError( + "process id message".into(), + server_identifier, + )) + } }; secret_key = match stream.read_i32().await { Ok(id) => id, - Err(_) => return Err(Error::SocketError(format!("Error reading secret key message on server startup {{ username: {:?}, database: {:?} }}", user.username, database))), + Err(_) => { + return Err(Error::ServerStartupError( + "secret key message".into(), + server_identifier, + )) + } }; } @@ -351,7 +423,12 @@ impl Server { match stream.read_exact(&mut idle).await { Ok(_) => (), - Err(_) => return Err(Error::SocketError(format!("Error reading transaction status message on server startup {{ username: {:?}, database: {:?} }}", user.username, database))), + Err(_) => { + return Err(Error::ServerStartupError( + "transaction status message".into(), + server_identifier, + )) + } }; let (read, write) = stream.into_split(); @@ -413,7 +490,7 @@ impl Server { Ok(stream) => stream, Err(err) => { error!("Could not connect to server: {}", err); - return Err(Error::SocketError(format!("Error reading cancel message"))); + return Err(Error::SocketError("Error reading cancel message".into())); } }; configure_socket(&stream); diff --git a/src/stats/server.rs b/src/stats/server.rs index 009e9b57..08968a12 100644 --- a/src/stats/server.rs +++ b/src/stats/server.rs @@ -100,7 +100,6 @@ impl ServerStats { .server_idle(self.state.load(Ordering::Relaxed)); self.state.store(ServerState::Idle, Ordering::Relaxed); - self.set_undefined_application(); } /// Reports a server connection is disconecting from the pooler. From d568739db94f2333d833ea69fe2a5616837a2635 Mon Sep 17 00:00:00 2001 From: Kian-Meng Ang Date: Tue, 11 Apr 2023 09:37:16 +0800 Subject: [PATCH 18/79] Fix typos (#398) Found via `typos --format brief` --- CONFIG.md | 2 +- README.md | 2 +- examples/docker/pgcat.toml | 2 +- pgcat.toml | 4 ++-- src/config.rs | 4 ++-- src/messages.rs | 2 +- src/multi_logger.rs | 2 +- src/query_router.rs | 2 +- src/stats.rs | 4 ++-- src/stats/client.rs | 4 ++-- src/stats/server.rs | 2 +- tests/ruby/admin_spec.rb | 6 +++--- tests/ruby/load_balancing_spec.rb | 2 +- tests/ruby/sharding_spec.rb | 2 +- 14 files changed, 20 insertions(+), 20 deletions(-) diff --git a/CONFIG.md b/CONFIG.md index 3cec2530..667daf12 100644 --- a/CONFIG.md +++ b/CONFIG.md @@ -152,7 +152,7 @@ default: example: "server.cert" ``` -Path to TLS Certficate file to use for TLS connections +Path to TLS Certificate file to use for TLS connections ### tls_private_key ``` diff --git a/README.md b/README.md index 63b5ab1a..56a86057 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ You can open a Docker development environment where you can debug tests easier. ./dev/script/console ``` -This will open a terminal in an environment similar to that used in tests. In there, you can compile the pooler, run tests, do some debugging with the test environment, etc. Objects compiled inside the contaner (and bundled gems) will be placed in `dev/cache` so they don't interfere with what you have on your machine. +This will open a terminal in an environment similar to that used in tests. In there, you can compile the pooler, run tests, do some debugging with the test environment, etc. Objects compiled inside the container (and bundled gems) will be placed in `dev/cache` so they don't interfere with what you have on your machine. ## Usage diff --git a/examples/docker/pgcat.toml b/examples/docker/pgcat.toml index bfc4c2e2..5fd929de 100644 --- a/examples/docker/pgcat.toml +++ b/examples/docker/pgcat.toml @@ -73,7 +73,7 @@ query_parser_enabled = true # If the query parser is enabled and this setting is enabled, the primary will be part of the pool of databases used for # load balancing of read queries. Otherwise, the primary will only be used for write -# queries. The primary can always be explicitely selected with our custom protocol. +# queries. The primary can always be explicitly selected with our custom protocol. primary_reads_enabled = true # So what if you wanted to implement a different hashing function, diff --git a/pgcat.toml b/pgcat.toml index 18339197..7db6abe3 100644 --- a/pgcat.toml +++ b/pgcat.toml @@ -57,7 +57,7 @@ tcp_keepalives_count = 5 # Number of seconds between keepalive packets. tcp_keepalives_interval = 5 -# Path to TLS Certficate file to use for TLS connections +# Path to TLS Certificate file to use for TLS connections # tls_certificate = "server.cert" # Path to TLS private key file to use for TLS connections # tls_private_key = "server.key" @@ -123,7 +123,7 @@ idle_timeout = 40000 connect_timeout = 3000 # User configs are structured as pool..users. -# This secion holds the credentials for users that may connect to this cluster +# This section holds the credentials for users that may connect to this cluster [pools.sharded_db.users.0] # Postgresql username username = "sharding_user" diff --git a/src/config.rs b/src/config.rs index 13528af2..4e0ff8ae 100644 --- a/src/config.rs +++ b/src/config.rs @@ -273,7 +273,7 @@ impl General { // These keepalive defaults should detect a dead connection within 30 seconds. // Tokio defaults to disabling keepalives which keeps dead connections around indefinitely. - // This can lead to permenant server pool exhaustion + // This can lead to permanent server pool exhaustion pub fn default_tcp_keepalives_idle() -> u64 { 5 // 5 seconds } @@ -422,7 +422,7 @@ pub struct Pool { pub shards: BTreeMap, pub users: BTreeMap, - // Note, don't put simple fields below these configs. There's a compatability issue with TOML that makes it + // Note, don't put simple fields below these configs. There's a compatibility issue with TOML that makes it // incompatible to have simple fields in TOML after complex objects. See // https://users.rust-lang.org/t/why-toml-to-string-get-error-valueaftertable/85903 } diff --git a/src/messages.rs b/src/messages.rs index 61c36c6d..ba4818ce 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -404,7 +404,7 @@ pub fn row_description(columns: &Vec<(&str, DataType)>) -> BytesMut { let mut res = BytesMut::new(); let mut row_desc = BytesMut::new(); - // how many colums we are storing + // how many columns we are storing row_desc.put_i16(columns.len() as i16); for (name, data_type) in columns { diff --git a/src/multi_logger.rs b/src/multi_logger.rs index 901db02e..2a61794e 100644 --- a/src/multi_logger.rs +++ b/src/multi_logger.rs @@ -17,7 +17,7 @@ use log::{Level, Log, Metadata, Record, SetLoggerError}; // // So to summarize, if no `STDOUT_LOG` env var is present, the logger is the default logger. If `STDOUT_LOG` is set, everything // but errors, that matches the log level set in the `STDOUT_LOG` env var is sent to stdout. You can have also some esoteric configuration -// where you set `RUST_LOG=debug` and `STDOUT_LOG=info`, in here, erros will go to stderr, warns and infos to stdout and debugs to stderr. +// where you set `RUST_LOG=debug` and `STDOUT_LOG=info`, in here, errors will go to stderr, warns and infos to stdout and debugs to stderr. // pub struct MultiLogger { stderr_logger: env_logger::Logger, diff --git a/src/query_router.rs b/src/query_router.rs index 0ea907b5..5b2ba0c4 100644 --- a/src/query_router.rs +++ b/src/query_router.rs @@ -1,4 +1,4 @@ -/// Route queries automatically based on explicitely requested +/// Route queries automatically based on explicitly requested /// or implied query characteristics. use bytes::{Buf, BytesMut}; use log::{debug, error}; diff --git a/src/stats.rs b/src/stats.rs index 5b7895b4..b95a143a 100644 --- a/src/stats.rs +++ b/src/stats.rs @@ -66,7 +66,7 @@ impl Reporter { CLIENT_STATS.write().insert(client_id, stats); } - /// Reports a client is disconecting from the pooler. + /// Reports a client is disconnecting from the pooler. fn client_disconnecting(&self, client_id: i32) { CLIENT_STATS.write().remove(&client_id); } @@ -76,7 +76,7 @@ impl Reporter { fn server_register(&self, server_id: i32, stats: Arc) { SERVER_STATS.write().insert(server_id, stats); } - /// Reports a server connection is disconecting from the pooler. + /// Reports a server connection is disconnecting from the pooler. fn server_disconnecting(&self, server_id: i32) { SERVER_STATS.write().remove(&server_id); } diff --git a/src/stats/client.rs b/src/stats/client.rs index 89235068..4cbcab2f 100644 --- a/src/stats/client.rs +++ b/src/stats/client.rs @@ -92,7 +92,7 @@ impl ClientStats { } } - /// Reports a client is disconecting from the pooler and + /// Reports a client is disconnecting from the pooler and /// update metrics on the corresponding pool. pub fn disconnect(&self) { self.reporter.client_disconnecting(self.client_id); @@ -140,7 +140,7 @@ impl ClientStats { self.error_count.fetch_add(1, Ordering::Relaxed); } - /// Reportes the time spent by a client waiting to get a healthy connection from the pool + /// Reporters the time spent by a client waiting to get a healthy connection from the pool pub fn checkout_time(&self, microseconds: u64) { self.total_wait_time .fetch_add(microseconds, Ordering::Relaxed); diff --git a/src/stats/server.rs b/src/stats/server.rs index 08968a12..d25f3b4e 100644 --- a/src/stats/server.rs +++ b/src/stats/server.rs @@ -102,7 +102,7 @@ impl ServerStats { self.state.store(ServerState::Idle, Ordering::Relaxed); } - /// Reports a server connection is disconecting from the pooler. + /// Reports a server connection is disconnecting from the pooler. /// Also updates metrics on the pool regarding server usage. pub fn disconnect(&self) { self.reporter.server_disconnecting(self.server_id); diff --git a/tests/ruby/admin_spec.rb b/tests/ruby/admin_spec.rb index b611e8cb..fceb95bf 100644 --- a/tests/ruby/admin_spec.rb +++ b/tests/ruby/admin_spec.rb @@ -37,9 +37,9 @@ describe "SHOW POOLS" do context "bad credentials" do it "does not change any stats" do - bad_passsword_url = URI(pgcat_conn_str) - bad_passsword_url.password = "wrong" - expect { PG::connect("#{bad_passsword_url.to_s}?application_name=bad_password") }.to raise_error(PG::ConnectionBad) + bad_password_url = URI(pgcat_conn_str) + bad_password_url.password = "wrong" + expect { PG::connect("#{bad_password_url.to_s}?application_name=bad_password") }.to raise_error(PG::ConnectionBad) sleep(1) admin_conn = PG::connect(processes.pgcat.admin_connection_string) diff --git a/tests/ruby/load_balancing_spec.rb b/tests/ruby/load_balancing_spec.rb index cd647406..172e6069 100644 --- a/tests/ruby/load_balancing_spec.rb +++ b/tests/ruby/load_balancing_spec.rb @@ -65,7 +65,7 @@ processes.pgcat.shutdown end - context "under homogenous load" do + context "under homogeneous load" do it "balances query volume between all instances" do conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) diff --git a/tests/ruby/sharding_spec.rb b/tests/ruby/sharding_spec.rb index 4c4053de..123c10dc 100644 --- a/tests/ruby/sharding_spec.rb +++ b/tests/ruby/sharding_spec.rb @@ -27,7 +27,7 @@ processes.pgcat.shutdown end - describe "automatic routing of extended procotol" do + describe "automatic routing of extended protocol" do it "can do it" do conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) conn.exec("SET SERVER ROLE TO 'auto'") From 421c5d4b64429f8f9d04e0ea8e6ee64fced9e6ce Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Tue, 11 Apr 2023 10:32:48 -0700 Subject: [PATCH 19/79] Load config on client connect (#401) --- src/main.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index 39c67c2e..acd19352 100644 --- a/src/main.rs +++ b/src/main.rs @@ -287,7 +287,7 @@ fn main() -> Result<(), Box> { let drain_tx = drain_tx.clone(); let client_server_map = client_server_map.clone(); - let tls_certificate = config.general.tls_certificate.clone(); + let tls_certificate = get_config().general.tls_certificate.clone(); tokio::task::spawn(async move { let start = chrono::offset::Utc::now().naive_utc(); @@ -298,7 +298,7 @@ fn main() -> Result<(), Box> { shutdown_rx, drain_tx, admin_only, - tls_certificate.clone(), + tls_certificate, config.general.log_client_connections, ) .await @@ -306,7 +306,7 @@ fn main() -> Result<(), Box> { Ok(()) => { let duration = chrono::offset::Utc::now().naive_utc() - start; - if config.general.log_client_disconnections { + if get_config().general.log_client_disconnections { info!( "Client {:?} disconnected, session duration: {}", addr, From bae12fca9945c0b75b5f494505b61a2b382f4d7b Mon Sep 17 00:00:00 2001 From: Cluas Date: Thu, 13 Apr 2023 00:29:43 +0800 Subject: [PATCH 20/79] feat: set keepalive for pgcat server itself (#402) * feat: set keepalive for pgcat server self * docs: note also set for client --- CONFIG.md | 4 ++-- src/main.rs | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CONFIG.md b/CONFIG.md index 667daf12..613da752 100644 --- a/CONFIG.md +++ b/CONFIG.md @@ -1,4 +1,4 @@ -# PgCat Configurations +# PgCat Configurations ## `general` Section ### host @@ -127,7 +127,7 @@ path: general.tcp_keepalives_idle default: 5 ``` -Number of seconds of connection idleness to wait before sending a keepalive packet to the server. +Number of seconds of connection idleness to wait before sending a keepalive packet to the server and client. ### tcp_keepalives_count ``` diff --git a/src/main.rs b/src/main.rs index acd19352..b3265ed8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -79,6 +79,7 @@ mod stats; mod tls; use crate::config::{get_config, reload_config, VERSION}; +use crate::messages::configure_socket; use crate::pool::{ClientServerMap, ConnectionPool}; use crate::prometheus::start_metric_server; use crate::stats::{Collector, Reporter, REPORTER}; @@ -289,6 +290,8 @@ fn main() -> Result<(), Box> { let tls_certificate = get_config().general.tls_certificate.clone(); + configure_socket(&socket); + tokio::task::spawn(async move { let start = chrono::offset::Utc::now().naive_utc(); From 5dfbc102a9538be048162abd1c3d8f3c3f051d19 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Apr 2023 10:23:52 -0700 Subject: [PATCH 21/79] chore(deps): bump hyper from 0.14.25 to 0.14.26 (#406) Bumps [hyper](https://github.com/hyperium/hyper) from 0.14.25 to 0.14.26. - [Release notes](https://github.com/hyperium/hyper/releases) - [Changelog](https://github.com/hyperium/hyper/blob/v0.14.26/CHANGELOG.md) - [Commits](https://github.com/hyperium/hyper/compare/v0.14.25...v0.14.26) --- updated-dependencies: - dependency-name: hyper dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5d6c56f0..09916dae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -387,9 +387,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.15" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4" +checksum = "66b91535aa35fea1523ad1b86cb6b53c28e0ae566ba4a460f4457e936cad7c6f" dependencies = [ "bytes", "fnv", @@ -476,9 +476,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.25" +version = "0.14.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc5e554ff619822309ffd57d8734d77cd5ce6238bc956f037ea06c58238c9899" +checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4" dependencies = [ "bytes", "futures-channel", From 8e99e652151e9947df0d9fa9ca8d93018317df9d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Apr 2023 10:24:44 -0700 Subject: [PATCH 22/79] chore(deps): bump sqlparser from 0.32.0 to 0.33.0 (#399) Bumps [sqlparser](https://github.com/sqlparser-rs/sqlparser-rs) from 0.32.0 to 0.33.0. - [Release notes](https://github.com/sqlparser-rs/sqlparser-rs/releases) - [Changelog](https://github.com/sqlparser-rs/sqlparser-rs/blob/main/CHANGELOG.md) - [Commits](https://github.com/sqlparser-rs/sqlparser-rs/compare/v0.32.0...v0.33.0) --- updated-dependencies: - dependency-name: sqlparser dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 09916dae..cc5f4094 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1108,9 +1108,9 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "sqlparser" -version = "0.32.0" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0366f270dbabb5cc2e4c88427dc4c08bba144f81e32fbd459a013f26a4d16aa0" +checksum = "355dc4d4b6207ca8a3434fc587db0a8016130a574dbcdbfb93d7f7b5bc5b211a" dependencies = [ "log", ] diff --git a/Cargo.toml b/Cargo.toml index 4e33c645..a5573518 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ serde_derive = "1" regex = "1" num_cpus = "1" once_cell = "1" -sqlparser = "0.32.0" +sqlparser = "0.33.0" log = "0.4" arc-swap = "1" env_logger = "0.10" From 6aacf1fa197fa6ff987a2b94d82aababfe23acd5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Apr 2023 10:24:52 -0700 Subject: [PATCH 23/79] chore(deps): bump serde_derive from 1.0.159 to 1.0.160 (#403) Bumps [serde_derive](https://github.com/serde-rs/serde) from 1.0.159 to 1.0.160. - [Release notes](https://github.com/serde-rs/serde/releases) - [Commits](https://github.com/serde-rs/serde/compare/v1.0.159...v1.0.160) --- updated-dependencies: - dependency-name: serde_derive dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cc5f4094..f47951e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1020,9 +1020,9 @@ checksum = "3c04e8343c3daeec41f58990b9d77068df31209f2af111e059e9fe9646693065" [[package]] name = "serde_derive" -version = "1.0.159" +version = "1.0.160" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c614d17805b093df4b147b51339e7e44bf05ef59fba1e45d83500bcfb4d8585" +checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df" dependencies = [ "proc-macro2", "quote", From a18eb42df5c038dfa15b1e4cd8751aae86505b27 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Apr 2023 10:25:00 -0700 Subject: [PATCH 24/79] chore(deps): bump serde from 1.0.159 to 1.0.160 (#404) Bumps [serde](https://github.com/serde-rs/serde) from 1.0.159 to 1.0.160. - [Release notes](https://github.com/serde-rs/serde/releases) - [Commits](https://github.com/serde-rs/serde/compare/v1.0.159...v1.0.160) --- updated-dependencies: - dependency-name: serde dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f47951e2..e0027296 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1014,9 +1014,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.159" +version = "1.0.160" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c04e8343c3daeec41f58990b9d77068df31209f2af111e059e9fe9646693065" +checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" [[package]] name = "serde_derive" From 3dae3d07776d6448b2137fd40d62d836fb309eee Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Tue, 18 Apr 2023 09:57:17 -0700 Subject: [PATCH 25/79] Separate server and client passwords optionally (#407) * Separate server and user passwords * config --- CONFIG.md | 92 ++++++++++++-------------------------- pgcat.toml | 16 ++++++- src/auth_passthrough.rs | 2 + src/client.rs | 5 +++ src/config.rs | 4 ++ src/server.rs | 30 +++++++++---- utilities/requirements.txt | 1 + 7 files changed, 76 insertions(+), 74 deletions(-) create mode 100644 utilities/requirements.txt diff --git a/CONFIG.md b/CONFIG.md index 613da752..fbee178f 100644 --- a/CONFIG.md +++ b/CONFIG.md @@ -1,4 +1,4 @@ -# PgCat Configurations +# PgCat Configurations ## `general` Section ### host @@ -108,7 +108,7 @@ If we should log client disconnections ### autoreload ``` path: general.autoreload -default: false +default: 15000 ``` When set to true, PgCat reloads configs if it detects a change in the config file. @@ -127,7 +127,7 @@ path: general.tcp_keepalives_idle default: 5 ``` -Number of seconds of connection idleness to wait before sending a keepalive packet to the server and client. +Number of seconds of connection idleness to wait before sending a keepalive packet to the server. ### tcp_keepalives_count ``` @@ -175,41 +175,11 @@ Connecting to that database allows running commands like `SHOW POOLS`, `SHOW DAT ### admin_password ``` path: general.admin_password -default: +default: "admin_pass" ``` Password to access the virtual administrative database -### auth_query (experimental) -``` -path: general.auth_query -default: -``` - -Query to be sent to servers to obtain the hash used for md5 authentication. The connection will be -established using the database configured in the pool. This parameter is inherited by every pool -and can be redefined in pool configuration. - -### auth_query_user (experimental) -``` -path: general.auth_query_user -default: -``` - -User to be used for connecting to servers to obtain the hash used for md5 authentication by sending the query -specified in `auth_query_user`. The connection will be established using the database configured in the pool. -This parameter is inherited by every pool and can be redefined in pool configuration. - -### auth_query_password (experimental) -``` -path: general.auth_query_password -default: -``` - -Password to be used for connecting to servers to obtain the hash used for md5 authentication by sending the query -specified in `auth_query_user`. The connection will be established using the database configured in the pool. -This parameter is inherited by every pool and can be redefined in pool configuration. - ## `pools.` Section ### pool_mode @@ -243,7 +213,7 @@ If the client doesn't specify, PgCat routes traffic to this role by default. `replica` round-robin between replicas only without touching the primary, `primary` all queries go to the primary unless otherwise specified. -### query_parser_enabled (experimental) +### query_parser_enabled ``` path: pools..query_parser_enabled default: true @@ -264,7 +234,7 @@ If the query parser is enabled and this setting is enabled, the primary will be load balancing of read queries. Otherwise, the primary will only be used for write queries. The primary can always be explicitly selected with our custom protocol. -### sharding_key_regex (experimental) +### sharding_key_regex ``` path: pools..sharding_key_regex default: @@ -286,7 +256,7 @@ Current options: `pg_bigint_hash`: PARTITION BY HASH (Postgres hashing function) `sha1`: A hashing function based on SHA1 -### automatic_sharding_key (experimental) +### automatic_sharding_key ``` path: pools..automatic_sharding_key default: @@ -311,47 +281,43 @@ default: 3000 Connect timeout can be overwritten in the pool -### auth_query (experimental) -``` -path: general.auth_query -default: -``` - -Auth query can be overwritten in the pool +## `pools..users.` Section -### auth_query_user (experimental) +### username ``` -path: general.auth_query_user -default: +path: pools..users..username +default: "sharding_user" ``` -Auth query user can be overwritten in the pool +PostgreSQL username used to authenticate the user and connect to the server +if `server_username` is not set. -### auth_query_password (experimental) +### password ``` -path: general.auth_query_password -default: +path: pools..users..password +default: "sharding_user" ``` -Auth query password can be overwritten in the pool +PostgreSQL password used to authenticate the user and connect to the server +if `server_password` is not set. -## `pools..users.` Section - -### username +### server_username ``` -path: pools..users..username -default: "sharding_user" +path: pools..users..server_username +default: +example: "another_user" ``` -Postgresql username +PostgreSQL username used to connect to the server. -### password +### server_password ``` -path: pools..users..password -default: "sharding_user" +path: pools..users..server_password +default: +example: "another_password" ``` -Postgresql password +PostgreSQL password used to connect to the server. ### pool_size ``` @@ -382,7 +348,7 @@ default: [["127.0.0.1", 5432, "primary"], ["localhost", 5432, "replica"]] Array of servers in the shard, each server entry is an array of `[host, port, role]` -### mirrors (experimental) +### mirrors ``` path: pools..shards..mirrors default: diff --git a/pgcat.toml b/pgcat.toml index 7db6abe3..c9faef8f 100644 --- a/pgcat.toml +++ b/pgcat.toml @@ -125,10 +125,22 @@ connect_timeout = 3000 # User configs are structured as pool..users. # This section holds the credentials for users that may connect to this cluster [pools.sharded_db.users.0] -# Postgresql username +# PostgreSQL username used to authenticate the user and connect to the server +# if `server_username` is not set. username = "sharding_user" -# Postgresql password + +# PostgreSQL password used to authenticate the user and connect to the server +# if `server_password` is not set. password = "sharding_user" + +pool_mode = "session" + +# PostgreSQL username used to connect to the server. +# server_username = "another_user" + +# PostgreSQL password used to connect to the server. +# server_password = "another_password" + # Maximum number of server connections that can be established for this user # The maximum number of connection from a single Pgcat process to any database in the cluster # is the sum of pool_size across all users. diff --git a/src/auth_passthrough.rs b/src/auth_passthrough.rs index f313dead..4e936262 100644 --- a/src/auth_passthrough.rs +++ b/src/auth_passthrough.rs @@ -72,6 +72,8 @@ impl AuthPassthrough { let auth_user = crate::config::User { username: self.user.clone(), password: Some(self.password.clone()), + server_username: None, + server_password: None, pool_size: 1, statement_timeout: 0, pool_mode: None, diff --git a/src/client.rs b/src/client.rs index 4114e4bb..5098ec6f 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1128,6 +1128,11 @@ where self.buffer.put(&message[..]); } + // Close the prepared statement. + 'C' => { + self.buffer.put(&message[..]); + } + // Execute // Execute a prepared statement prepared in `P` and bound in `B`. 'E' => { diff --git a/src/config.rs b/src/config.rs index 4e0ff8ae..00c0f068 100644 --- a/src/config.rs +++ b/src/config.rs @@ -178,6 +178,8 @@ impl Address { pub struct User { pub username: String, pub password: Option, + pub server_username: Option, + pub server_password: Option, pub pool_size: u32, pub pool_mode: Option, #[serde(default)] // 0 @@ -189,6 +191,8 @@ impl Default for User { User { username: String::from("postgres"), password: None, + server_username: None, + server_password: None, pool_size: 15, statement_timeout: 0, pool_mode: None, diff --git a/src/server.rs b/src/server.rs index 14862bd0..84bed6cc 100644 --- a/src/server.rs +++ b/src/server.rs @@ -103,19 +103,32 @@ impl Server { trace!("Sending StartupMessage"); // StartupMessage - startup(&mut stream, &user.username, database).await?; + let username = match user.server_username { + Some(ref server_username) => server_username, + None => &user.username, + }; + + let password = match user.server_password { + Some(ref server_password) => Some(server_password), + None => match user.password { + Some(ref password) => Some(password), + None => None, + }, + }; + + startup(&mut stream, username, database).await?; let mut server_info = BytesMut::new(); let mut process_id: i32 = 0; let mut secret_key: i32 = 0; - let server_identifier = ServerIdentifier::new(&user.username, &database); + let server_identifier = ServerIdentifier::new(username, &database); // We'll be handling multiple packets, but they will all be structured the same. // We'll loop here until this exchange is complete. - let mut scram: Option = None; - if let Some(password) = &user.password.clone() { - scram = Some(ScramSha256::new(password)); - } + let mut scram: Option = match password { + Some(password) => Some(ScramSha256::new(password)), + None => None, + }; loop { let code = match stream.read_u8().await { @@ -172,11 +185,10 @@ impl Server { } }; - match &user.password { + match password { // Using plaintext password Some(password) => { - md5_password(&mut stream, &user.username, password, &salt[..]) - .await? + md5_password(&mut stream, username, password, &salt[..]).await? } // Using auth passthrough, in this case we should already have a diff --git a/utilities/requirements.txt b/utilities/requirements.txt new file mode 100644 index 00000000..aab392a3 --- /dev/null +++ b/utilities/requirements.txt @@ -0,0 +1 @@ +tomli From 4ccc1e7fa3fe90be385af5cbf108d033a3e338eb Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Wed, 19 Apr 2023 07:45:26 -0700 Subject: [PATCH 26/79] Fix CONFIG (#408) Fix readme --- CONFIG.md | 33 +++++++++++++++++++++++++++++++++ pgcat.toml | 15 +++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/CONFIG.md b/CONFIG.md index fbee178f..72d5683a 100644 --- a/CONFIG.md +++ b/CONFIG.md @@ -256,6 +256,39 @@ Current options: `pg_bigint_hash`: PARTITION BY HASH (Postgres hashing function) `sha1`: A hashing function based on SHA1 +### auth_query +``` +path: pools..auth_query +default: +example: "SELECT $1" +``` + +Query to be sent to servers to obtain the hash used for md5 authentication. The connection will be +established using the database configured in the pool. This parameter is inherited by every pool +and can be redefined in pool configuration. + +### auth_query_user +``` +path: pools..auth_query_user +default: +example: "sharding_user" +``` + +User to be used for connecting to servers to obtain the hash used for md5 authentication by sending the query +specified in `auth_query_user`. The connection will be established using the database configured in the pool. +This parameter is inherited by every pool and can be redefined in pool configuration. + +### auth_query_password +``` +path: pools..auth_query_password +default: +example: "sharding_user" +``` + +Password to be used for connecting to servers to obtain the hash used for md5 authentication by sending the query +specified in `auth_query_user`. The connection will be established using the database configured in the pool. +This parameter is inherited by every pool and can be redefined in pool configuration. + ### automatic_sharding_key ``` path: pools..automatic_sharding_key diff --git a/pgcat.toml b/pgcat.toml index c9faef8f..6cb9a299 100644 --- a/pgcat.toml +++ b/pgcat.toml @@ -113,6 +113,21 @@ primary_reads_enabled = true # `sha1`: A hashing function based on SHA1 sharding_function = "pg_bigint_hash" +# Query to be sent to servers to obtain the hash used for md5 authentication. The connection will be +# established using the database configured in the pool. This parameter is inherited by every pool +# and can be redefined in pool configuration. +# auth_query = "SELECT $1" + +# User to be used for connecting to servers to obtain the hash used for md5 authentication by sending the query +# specified in `auth_query_user`. The connection will be established using the database configured in the pool. +# This parameter is inherited by every pool and can be redefined in pool configuration. +# auth_query_user = "sharding_user" + +# Password to be used for connecting to servers to obtain the hash used for md5 authentication by sending the query +# specified in `auth_query_user`. The connection will be established using the database configured in the pool. +# This parameter is inherited by every pool and can be redefined in pool configuration. +# auth_query_password = "sharding_user" + # Automatically parse this from queries and route queries to the right shard! # automatic_sharding_key = "data.id" From 66805d7e77b695f47d272720dbc4ab2f587001ee Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Thu, 20 Apr 2023 07:53:55 -0700 Subject: [PATCH 27/79] README updates (#409) * Better table * add image * promote auth passthrough to stable * fmt --- README.md | 46 ++++++++++++++++++++++++++++++++++------- images/one_signal.webp | Bin 0 -> 16278 bytes 2 files changed, 39 insertions(+), 7 deletions(-) create mode 100644 images/one_signal.webp diff --git a/README.md b/README.md index 56a86057..4dd75246 100644 --- a/README.md +++ b/README.md @@ -21,21 +21,53 @@ PostgreSQL pooler and proxy (like PgBouncer) with support for sharding, load bal | Client TLS | **Stable** | Clients can connect to the pooler using TLS/SSL. | | Client/Server authentication | **Stable** | Clients can connect using MD5 authentication, supported by `libpq` and all Postgres client drivers. PgCat can connect to Postgres using MD5 and SCRAM-SHA-256. | | Live configuration reloading | **Stable** | Identical to PgBouncer; all settings can be reloaded dynamically (except `host` and `port`). | +| Auth passthrough | **Stable** | MD5 password authentication can be configured to use an `auth_query` so no cleartext passwords are needed in the config file.| | Sharding using extended SQL syntax | **Experimental** | Clients can dynamically configure the pooler to route queries to specific shards. | | Sharding using comments parsing/Regex | **Experimental** | Clients can include shard information (sharding key, shard ID) in the query comments. | | Automatic sharding | **Experimental** | PgCat can parse queries, detect sharding keys automatically, and route queries to the correct shard. | | Mirroring | **Experimental** | Mirror queries between multiple databases in order to test servers with realistic production traffic. | -| Auth passthrough | **Experimental** | MD5 password authentication can be configured to use an `auth_query` so no cleartext passwords are needed in the config file. | ## Status -PgCat is stable and used in production to serve hundreds of thousands of queries per second. Some features remain experimental and are being actively developed. They are optional and can be enabled through configuration. - -| | | -|-|-| -||| -| [Instacart](https://tech.instacart.com/adopting-pgcat-a-nextgen-postgres-proxy-3cf284e68c2f) | [PostgresML](https://postgresml.org/blog/scaling-postgresml-to-one-million-requests-per-second) | +PgCat is stable and used in production to serve hundreds of thousands of queries per second. + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ + Instacart + + + + PostgresML + + + OneSignal +
+ +Some features remain experimental and are being actively developed. They are optional and can be enabled through configuration. ## Deployment diff --git a/images/one_signal.webp b/images/one_signal.webp new file mode 100644 index 0000000000000000000000000000000000000000..79f3d1293df27aff07988ec9fed98941a10f7c19 GIT binary patch literal 16278 zcmeIZQ?PJN(0H zotQ#=b6wEqhk_NdZ}`U?9Noy}-{9}2d$Q%wjGx+%*t~b#Pw^-8CmucD!DqVH>Kmqy z{Ex>U*-g9^vJAeBT2){9+#TO#PXJ%z-}05$Gmg*v^X04KZ{HuX64r{>gx(on=NGO$ z+tc3m56|Vwjom2E@eSVxxXi$qgczWl;ncT$@b|`u!clk9&*mSm}Yl5eu zj8VJ*8?3JUKc6HJau6cU**p<>XuaZG>->m4Hj0S0p9gwAWu|-Q%l?Ms)ia887a?_x0U{O`HxXHB6e9|OrFJ1To5P*>Nt>G_nsiNeaePdZ?< zyXx3cVVKqd1r7~pCo0wr1p@wiAskeW-Zcy%`FgxIrt8s;5d0s%a<`?^B+hjQ;3qP| zUV@i#Qd-o;Z1saw3Kwrjge2{;Qq%cQ8DLs{oX0Q?7+QMx@~ik){zG%3q!FXE6KDrb zDm<)7JN<$BM;zY7&)g8PHFf&{?IJnKj(pPATDV2dH0hn@@VKYHsO?j)S?c)R=Ke!f zAWD_9Ef}GA&y2oSLPpg-vg$B1KQ0Iy(f*4SkmK>09Ffo~|5Bl2g)AGsU(`Q3{k4!h zqRAJh33TfGah&OJeH>i*08qxDuB@KO`+u1CWwoeH4G4?r9p^fg>6iS73<>A3(+#n5_=md)1Ui=P z_K5gC#1lkr^wDu)0G{#*@{YV=5MMs;~v?d-vH_1*fFaL7Jg7 zZyuNAD(wBREhRyj(Hg{d(jwC#{mYBX6Qo3L^5E~j7xSj!c0Id6&_F{v!i$_1a zD(2NtLy#nM=x85LMfvo-I`~WM!F9raPl3W-{!Xh4@~j(w2;(3OZFW9R&at&V|CS^> zrex_{rf52NgILUxY@+DZ(f-;$TeoN#HQ)hGr0~`(EkE;HS_q45{mt5@un*8nNoz1H zPk0$wb3WO641mn_)CW0{kgKS~;7h!lCw6 zLXAU^L{%a~AgEi}w#CXx+i{N`M48258s5~$d?Z+|s>LBIN9`~4P9G{Qc&yGqn37ek zGA8BFu+b>%qpMv3p*<@$s3x9W`ok)8AUI|?23CzrBkl#Yl{Iv7uRG7ax9SndH*CdJ zvrW7lz6U6ma!0@~#u=PAh)6^kLJCGExu?GWsN_Vnw5T4_CyrDRCGIcp;g#|AUQKY- zivVmJJM{`F&x%(dutxf2wy}l?)|$kOOn=z>qV}J*w&HG@8rOaL`ZT4+rtM2}%>r6W zy)e1QMo%C2(r$0MU_7KvQ>ZI#8HG#z8Z$zTOV4=?a_C{F4`v3>X3o4z-1|l6BYyhs zy9{C`S*3_Z#3?G4lG8Rq{~H=cD{Cpj1X{d|Dd-67QJykd99iiSwxm0q%S-pe3>!IN zpi7+H3-pHPjPM-K&f=DVayi;S2NJHHXW#1|7~cm0`dqjtCVZ;|c*N1&QNv+j0T_6L zix|KWRF(MkXFNy>UDGkvb>Oz)c*Os)`(-2qU2;gZtx)>A_or|Q6Fn{E4!NmGK*Ls` z|IpqA=HoXW71phw(>EkL8K|*dn-e3aF2=_~H5!mX5(U*tNJ{AKGj<)lAIr!j|J?6C z1XI)nn35V|YlL5@&JZ5Pzp`)nK^X@0=jMkPzwvLwB^HprFf%xl`%cv4{au;L5Y*dG z;;W!{p@I%!0vai5{ikioI36-_xjxws0`0`~Sb!LeCyVwbHJ_x%T5ZFEX0)LL%VHVS zH8XFAtow&leH+ulCiKd{BKKaQ*|4u(cK2gVf6iJc8B(|^(Oew-WxCJ5OJ4)M7^jV= zyxrd*IUf~H+GJp_B@S)G|bdJTo7JHh++F=)NR#<^ja|;1*M^-Fa=1TYd}TX#Ns* z^F)Fy5nG+9h={05|D``yS6-%0F8VpC8+4Kk-vPRx_g+VAUiFQ7Wt#T#dC#K%w_0Qj zEF*my(gh0cy-Y)-9p6ulV=0kT1sQBUKK$F7b2S%jP&;wb6>kaMD?R7-PjRuXwLJu; zN`e(Q6jR4?zmy_fCp?(96tjr!1b9hwWCQ*Fq55f&(+k`9@^jrBh=&%UsC zM=kJXhx3Mmm@62C9m%G434Uk)RO5|_h$528LFp-|w5R8VRk-Ju}tWDU4t1tS{7VPARO<8ujKC`396*LM0|xLiR2 z+1R?Yz1js~jXR_MoScH*bG73Nc!Olf_PPi4OoK$9?qT!MSUL-8XC=C^=U-~yt;-($ z0arQ-Rtrs!oFj2n7C2>1$XG$R#cb99!3OPZuY}1xrs=E2%(WqG$dD&;<|S|&jj3a+ zzJ(;5CMpn+@$FY|x#gV?Z6MRo-(BJySl--hFz_l572zYY9*u=TJv1HnI_=kTLF(KN ze)i1ts@08YjmR#Gq5l^~$e-Q72jUH`m%h()PO-_RJ&7KUY&nR59b$W@(pCbuD8&b8N}%>8qahGD!eOoAU0S@V z)gU8^dCdeZl9b}skr@P*0?Dxox=DO4F16wGJT`t$x^OZ?@P`O6Uw;2=92gVkOU$%7 zZz+wt1SuJD%Eo3QM)&1V!>eV?H|zr+hu&*0CPd!agqgq-Wjy&D+y6Uk_UziNNHr1% z=m>eKJktDzrS4G6!sTfF%de?P*hJ_0Jc^IaD3TBhhcMHG5w4AL`k15=QjU|E^*};< zD)86swiCjXT!IVLcE-V7fuH?puL2WJs`i$+F!caP;)@DWt(I{zUUzbwMO67c+wktl zLC*A?(&!VPq3syzQg%e2cCE75z=*_n!LkSuLOw~Px^OMd0C z^s-rm;8NAeDC}P>z+7Z60I*dJef*o3;(!Q&URw^a`x(jsD&AdD-z&ZFPBs9|-8H-y z-ypQ9=tg%SoTGQi(BUTq|Lu%jEKXP`OER?ng)&|PF5z8Lt=@n?*x_^4yQrKOVu%XV zM;zl1Sh+p4aTO;-vAJe+q0$aNi&1fRx{~FZINX{QlLEU6=(t7H>sy%CgkOe&t1Iw? zIM0mZ8pHy4R@0{j4<033~3x#vjff}@s)HOiNXzF1}2)e= z<45H&2&UbB+It@>EH+@oQe3W#Ie?qRdVDED|6%CveJerZrvlGz$sF@U)|mbyWG4YR zlBhTSpd34|#!)E{=xPrFI22&Nf&i2krsssU-)ujJlT z2YdcazPFq8niy$%ULI31SaH3sU07RT$kN|0T&SGd8774sI)NkS(QsIz^29Q(MN{JX2ra*ms% z@S_p{Alni9{VxstA;lWSkVf;GVRNf`3SLWiY_a=K5Sa)vxBqk8u0W)!<5&!NxmBxW%^hWZ4qVWC5kok1XzjTwh5$h zint8K0aH;te;h0UTQUb*ISb3HbfkY#cOvas?(ph)IrJ8Se{KidMiO;%{k;U*ydj6! z#ZAkDvzSNt^2}H|TgQgx?&-J_dQ?JCoe;pe9ia;!$?D4LPFE!0&0vVZY`fv+LLLy2 z>o0w1Gqrr8iL~_P{1>~_P3csV{Jc5a>5A-&q)-D10KE>YFxtTw;6epCb_!CY3j=_Z zoo|9=jH5|@3|uOA5yv)7IH7pl=0*ReW2B{y)mxe1sm!TyPpimeDjz4wzSn7K8l*WB z?qwlv-o0yys|Tc)ZDSufS*zAB%iG1)=EP&AeBS-S3O#3bX)D8aqE2}sgV4%B29;hd zfDxK54*QlmH=ekhY5Z>_d_g9Zm+5)X3834y{i+Rwa93*%&Q>klRV<6SzBV6OnSB4+ zfG}m4R~RFszYjX_l32o|mbu?M;U^+Vwu3a49-awT)LItN0TYn1FNQ>7WpSR|OZCa| zrW%6lzUQfPMB)?M{$o^8GJms1!V)Ac5l^Bx!7HjKygA>Q>m6f0nLE8kn7_ScUs4gw zK(4{C5EO#3Owa*p4a!~9!oyPg=OARIRP#b%BnQ{xVQ|69YN6Slp*gfO}6qwCiQ zo8{u@-%7SXQ=*^-Bcl`nfX+q5S4q+(JvlAMI-%APt+RmPV+j* z*hw2fCx1>11tBcw`;=NPgc-{O@5Ycfi@u$zAX3%vtfG#iUSwn-N8@$?|?Nk&2>(X8O*F=i)3MTW1~x zo}@62#a-zP>9tF5P+fz|A~tGKUt1`%h$>z&;Cm-}7Wc4Sb}iDrW%shZ7CMmKO>Vs8 zC7oABhZW#k+uZ0auweI|QzVu9V)tDX+?}?d4Lqc3FHN@`Ns^N=jw3FOnl#WJMV5L@ zklkz<5HB#4?3NTc5qWm1O0`x=4kP7rO=N?Xk)2g^!pINA7JkFHr=VIWS9QJ?m|7Pd zp^_&tW%QiPhtwo;8O6!*`y^P7$6DmEuyF@)KW1*-U0D2bdPH?j3j^QX2K=&}g8qZg zPxX__r}2w~h=iBwaqD1nBgu9oau6Xy=#WXTsh`rfiu6{^0Yx6N=-zspciE;1Tk~V% z7JIVmL@UPtT2*xVTVF5#~Wk>ycf?g3< z^kmTFK6NeoAF0M7QWx2XzQf$fCA?@&q|pg@+d7 zYWu6~HOEYNtpO*@RK?edeWkGRJtGP9=#iek!&N3>HE_^$C@R`l-X=yetWd8OZL z6cs4%pih$4fVUn{@u7GtiKE|@mxBW5tQU}8h&%)tb3TN(__~7dl zgueswvDBESWR(HzD$|v*3XR}2LrNNuZz3n$_6$5`84qQL9yvQFc1x!$skUmF3TMko z$Gse z3a;eJcL68S=8n+ICKwkaI;uH){BEe{DOW~TR!fC_HCs68`B#Hz<#@Ld;@X`xftbh4 zNdnLGc?J6=`r~QGfr9=aM6~6hf$3Q_*)~ztMe5UCMORm(!3df-QMVg0J;VKNGfH#P zH!doK^eL+~W`^Zg%xIFkn~;)^t~^-5c#_P+0{;72FSv0|qJq_U_MMj^_>BNTjv5=N zvwgDjO6fJ8BQdiixG%)ADLNuNrY^=~ZW4ABeL!)ZgIMd3&izOyPZRr>Od_vLI7!Xk z#2R&kahVB)o1c~p`qdDarmW9O`v>&MchzHnE!0U0w8YnB^OM|KDcEE+HlBTIb^n`JBQzxwB6vHZdp9^<+jBRW< zT7Cd^0%cxuD2vl)~T??vC~jB)!U zSAd`-(s7qh9nRrN7ZgnO3k}a(aanLqm}om{)B4f;MD<=nbIw1ft1tagiq~!(C%U z`$E~&Wxt*20MsUbFgR(fiTEm(4mvqgZ335@73xg|>5D{!s4+Oc(b3@N6N_?rJs+>& zvu|s{u=fa8@2?dEn*j{NLN=H$BPwTZJ%cFO`#ofPTWK%0~p zN7}8n=FcDizGY04ZM8bosnP&M)_jEE9z7p)4r;A}ZjbA-4w_WqGej})@59xMf+M_J z-QYFE_dBkYOpWMbjxa@4XPe!;H)}R^^J|@?HTwKYekYy^Q0?N*sph&@9leRG)7o|W zxs2PYga&=6(<-pqg0?4Mj#H^}?!ZE5ou$ZoWfWcOzLf+(NEf*3;R-FcVLzsE&|ibD zC7nPTNNvrLr}@O4-q*mAw!K@93_edUw>fg8mjU}#!k( z{0?UtKLzr_wy&=A4cHfbJ_ZYcF&wqc`Ci`a`tUE>;o!sO<=xJe z=%m9LzaEtEb6#N3D*>N1$H^28Xc9Ngo+W-xS(?!KuWl1FbUOI?0PV8#H|A6}r8uCj zJcX4(#xfxM;x0T{CCu)1HN^L^=6HE7PE4U5;~1VwIK)AWF#+e062*W=AwY;yZa`;i z4bEJ^lW@vQ)loW{5s6&5AeEvzTQ@uxL0FU?QL}vVkaoRrC^M|HKk3-XE`fR$W%U5- z5S?!W!2t3oj2U43);h7EwJ%;+E*qKCb+R|%J)nnsPljDX>d*7QkveEaQT+Yd*0|Ms z7>2a??-glJ-l##?OTZP=nVTS67jn<3@){B!A}c7mzEKq`(44grQG1oKStp-+DoUTH z&EP{Hc6dF2fg3Kf+jOcS-Tt1frs7+`PLoUOW9g(gq!OM^4_-zUty3!YV!>{`R7rjV zVedMP4d5lx!QnSy0agVTBhWX=tbwC^5yF^6sb&(aci(}}I4Yh>jZo$-Jm7W`CJs4B zb<@QFMrFF<(EuDMrb*JKBz*Pl8W!kjDAeQ}LT{_4F=WzNs%@G$p1Z1e4%v0Cc=MVq zP7YVdC?7;T;uPvOJoU&RTdSMoV2-?uDE^iVf}|<&Kwe&6H-1`8spAJ`>J-m8^UWE# zYnDl-5LvJ*DjDJt;K)c0lD79WQeN=MVdUnz$oJAO@Qv>;zvaC>2=y1|+E9%0C< zC~SmN;WWz@KvfCKBcXkzb>e4X-u>vx5dnu0fE+f_+^(6|A$)=R-b7X+M?X!zM}^HZ zFa#-o`z-$+((YTu5Du%D+z6WhuQHI@r)9h&HfInN>#t&W2}CsW5Zh1w4pNU9x!%ID z#!m4!#m+)_#OHkm*4kW8AdcpCq?X`OrCJvo0T*D&x8VJtg%BZ~!%egfTj({aW0R?s zJQ^EEkPRM;q@AP#t}^>b>d6|yC~Z3@R*4E>Wf%2|U^_wbA`oW%so+}p74!uJyMde+ z_~pRlK-@Fl8)Fm-VukhIYWF~GDZ#3#8r7@~LhsD`tEaup=|^N-0pW5Tvq)jgj;+$|y@$fGMgqZq7uW->Qq|2ETQ#Z_ zGI)Zq!5l==EQzMNB-LZK3kuCTX&Ssa5tW+%BNie%<;ezBSNkwyC*Cxlf{r%bRi$qc z7xqc76q}U@31}NR6TzMQFs>B))tQ#Tom93ycrQ}Eb}byu-_|=jsX&L5Z9X^r0%8+e zOLfDOmSU+wn!YqG#wrTkJ|gN;GFmI%9V%SYVti}Ss4kqh$MPuD^2m~RpfG|M3Xc?k z+&rSK-HY*rPbFx`2B~GwStt(L@FH$K_a)A6H~~aZ9BI$BZeaY9HwZFT1TflXFJgSl z*KEw*qB%>jsEk_$Jg{9qf}dlXA2vJ7mZGEWBzb(j{;qnA&s4pao3)!Mqdt)%P$n!O zn25c?@>dgfKu$1VrmgftcN~A3|I|v6AHSTPj`>#07Bq~Fq*U9-Mxi~1uoi|~=^1Ks zzZTbpl7F6mX?^GJq=*Ehr6%eJ=2fXRx!^LWT;kiB1+VPAEq^7);*!O=Xi~DoxONqc z!lSZ8*dXacI=GXsTlK-*1f;@0GP|UL!(I0^S7}QZo7Laf=$kPP-s3b`f^gi{ginRm zCjt(3CVpJ>GVLGxOUy%Qk&%Vg;m4zDm8^n9u0H$-hliDA*cP$*15v`PAlcV@J4Ldp zn><;94FTsCV1d51RLL+g@(ahhJzyNBF(6bW)>(?%7KE33^xKY+K7I7yYbUc8Y2mG@ zyaWu77zcY*yhB3ec1JB?t%X+RAsd9$q!R}=3BZGL;O6n{oxK}$&u>|7u3d@=7vnj8{fv)>Vsnm5~Tp3j}_Q2aYlB0wJMu!i#8wo`JL(T8YDf{0bc08mcFQ?eh>u zI_ay=9-52!gJuB+ZM3bX*Zlk zw!Y=q3)of{6*NuyovC4-Ef6KGVq>_^!`Su>vhL-mO;3k8TmpTc>k}&{iqQ8|kW?6h z9>Ny-tC0D>NqoisE7NmubwXm zwsciI=eZHZKL5-0^@87sNm0}_T7DO`Tqu?Y-nuSEM=QAES3*s4wS(k9m2lY8;&@6I z@fZWsgryK8IPw7~IK!miMKWq0$P+BHl9$?njed?q@YX1WtU1>c(7`#SjAlfAC3L{n zAD}f;xt>yzETkOdHBRU>u*pb6PPn!5rPLXFK@3%gppT z?6>Q0-5ah%GqEafJ)xxZ1&gaYtknqIIa$@5dp4=wxX#*(95r4bOEQCHXb2biyH3b{ zEf)+eU!U_@nQ$Ug7tUU0kz^zxnkl#23Hu^u8QYF?#Jiwo{TriILh z3e`4##5v^!s&Ypj`)jFhM`8XKox}+CFcWSO_QdC2qUj-5*r=E2UT>E;;Hk6=SX%dK zl=g;vU#vycJ>{P3t8~p{?~C8~$PKGUHH}F!;1uJ9t0k7F-0h+6wZ(ei#2(@s7nlgn zYIhc|_^QS;UpEk{_-EhZQ{gfZ4&zJsGa?IO|D1v)0P^Q2g;>W1qV!%OFV@5rToo`p zjMP6^wvu5h>ZT?;paVr#;U$+MY3uPM)&3P~#HQP%RDxj!Km@cHQ8T7^hGH3j+n5tb zd_mlHtJ>i#e$Iv>1ECg-{H`b|gKHRLb#r}*vKWPEGxWE&Po-yEU*-*{bk+&QF-O(o zq>r)Ucd^BOLd`2LSyZrdM}Zm{bFV6Iz9GU_MFh?*&l+I#`bg2`K~QY;w=hoi$s`A2 zxmIKe`fhKlY1eF`Csm;#^mjB&9Rq5*gqOq=lAO}|fSL+A z7&$LSTyEcaZqXE$&XNoGA&N{C+8Wtu!K^Xil5UNTUfKQu$!XERxvpQcGgon2@elwt zLQ>@#n_c3JS`e(pj5_g}a2SFUI1Uq;aMr5K9I9ADOM{Y_F65^^q6f@D_sDN4duNdO z0*@GA@5<#p%w~3YX-=FNjaEj=i8KB(sD9c##8!;Y90B!q1MwpGH^H#lRU~;M@^$M+ z7K_arZB{fY*243^TTAkf?=iDi$u=TdBec^+D`L;4#-0%gVQigw!-~5 zx%yvP$8RafGfmuF7I%S^C72ngb~s-@*%Pt{bgJb{dCI2&xXHu}PY{>w+DaR7`Y+zL zQ+Ps%7@YljEE^=vgzhaARu%~r!sUCau!!KKw6^{S!yTh~iw(W{Gun8-(UgfVo+p^PoO3fx z-kGu!;x+$N#+g%KUtOF=Bcd@0R3`ZoqELX399;a_B*A9W@nX^FCO3inO{?N)^<1~| zmkKF9$^A|(4*8*SrdW5Ef>4M#tf{ebhE75iY8arK?|7Rw&C7c$mHlDo1f8M_tiE!~ zmmvUqc2)GjIAW0E^Kk^Xcvya`nwQa#3D`p^Qq{28Qi$O_CF0C$$bwtYKm)OEmzY=7 z46@y%!ysQp_xZgjd2$>^bjjpo3o2(|;Uir#uJOP6zh z8%M#?KwgsK6&R9EL9@H58ec1;6F7-LKT0QvbcQB zm7G*a!0`Yb$vnKkGc+Gn%Bs3caxUa?s!C*|&z^gEH%N;TH9uWKsO;aE`;o11J#GE! z|Do?UB9d^~oL|K?(%$-(vHW9I6V8HjFfe0FQ+d3hYmrY3?@-~DlS}gVd2QpKR&D_3 z3n^Cqn>2#i)R6;?Ea%9b6Cr~57sJqL8Bo%ICvOo%W8!7P=EsDu_dfVe{D-xF2!;9) zEt$IBov}m;q+eHJr_vUN-?6kbZ;E@Oe(l83sXTE+BJu(%NbjEvu+MFqtX(bS?e|CsUc(>>)Qlx$<489uufIf!MvOj zQS?IUd7~ysX1}96Ja!Ux%_1mVAd=It{I(Xpo0p^s3Bi2lnD!eVZ3P8?>%}l;)0CVu zh@>q221!LtNgILS&H0^pyFn-KCOD4}2r%v(51Q6mUwpfr9?&JKZsoXfLhHt+C%Y?C zo$V)Lr?37Kk2K*!Wm*?|qBK%Vxnf%M%7#R27+JnDNU{j~3#(^U!kykN!KO(&TBI5J z^UJ$OJtJCyY8W6ppt`X>B8>1wc|EBkEPKboY!{41BDIyTu%U^hF7yX-R4V4%4dLZP zd$#Q#vf@dLo=kZTFJkN;gV{fG*77JB$JO5_E5OV-JpQgrl4=$}QgVgf@Cu3C}6`3*GMGxCIq*~W!tk(*&R4_%r-*><|^lVdj-&Cp<}|mzZAb|f$STV^mba0Z~>`; z?Y;XE4;v0py$Fjdny9#|VO+kbWpY?}!bQ*#F8&aJcqbDp+@U9LVS|m(S2eoBvsj&s z`pYSJ<90L$X=6fJdzA^;Jn?bJl3f_=^^!=; zhm!x87Tk6vzW|5qv(&M_pn5qD(}Rc=kmBT8)<^8&i4++|h;+eB&Zbf-S2j2{rM6O~s$z_doF+K3i z=;mO;wiB}Bxff8w2Oi0#cARA9F~>>I^{6$`6?QM;F((xc6=Ew}bYtxNI%zwRPgAlH z&cO+6G~S%^To4{^E})TvZx*I1K{$@wZhALI{{mVghwY>x%j(JDue)o34b@eMmY}Hf zwPXKDOJRgl2k6gM*d-6PoZIlTJ2*X(-z`Gu3}B`|(pfh$jOUPHakjjAd~HM^Cf49d zfi>Zc;$C8;_GoHwx_W~L>KJN3HBlb7h2qWgrKxP1mYdB!5v3L5bTY;^1y#MpeUaEH zTfSp^n_(OI6j(Kg2l*5X7aftxt8>pJWU3_0jY^U9$p!+6LksY!$ z!GiQC<#xn2#-ONeV+@la=Gu@UlBv19NoLP2c2|Vvii?UgtWVj;OK+^Nt*0R>zyOt( z#j#0Fia5+f6Wips7{S<{&wP2as6G0o(%G#Ox%z_P8`>CCu=dLIqq1h09k=3vAo<&*Z^$*$O{vIA)Bz;QOl{r7{Yij*3icWj7D6v8bzQIuXiPasm+nJjlipH9q zv-42Uqc^moGFc}4SAHb+JKc~$PS0akZLo33-$~HXuwnF+Js2!LvR0?!xdAo)(L zpxhP)m7zReffFHyHwW^TT`cbl{APr*gHtrzCc+wwu*b@mA|Oo!WJ|b-$J2~BJ=XrJ zH^WHMvBA46T1=!VWC2EPlTtJ*FlKA_AJ8}@tjeG1k(;zpawWUu2aWg^O+EPOQ{Wbe zIR^)0k!fGo)zuiRE)K$N<-npOr@~OHWDbfzzPQ0sZfSaWPphm3af~IV*BXmXL8jfu z8*K*RNJ?QpN7r&tyYn4`C6L989O~jUjKz*Pk(ZIIlG?AuLhfJ<)K=OFn1dj3$=)7S z*yTofs8)qMvT`-VQFR+J!S3%94SA2x15ggciUibc6a7P86~$f?42Wej{ifDML-C_& z`D0&i-F7*9j-A2dT1(ua8N&75_6hp-)Guz@5o5hA8X$d_NfI0{2K7}JIUTe^&@akN z3gLLZ1C&%G%9QNrGL7;Q?=kERw%W(ydyE67pLSgAJ6HshxQG<{yhE)vz!EARaQYSh zq|-r@Sx0FA0!vL{MN3xSCk1vid{u1KeoJ3SI}`he&_zmmgG2L^NrsBH+M^+tC&JK0 ze@Mcb*w5^#D9@54BU2^@1#4xSF+wj|9TD&Lp-Vl#U+T>yZ3HkE@xr$AB`{-ZKDPQx zTzrQ|>k8J=7NIktf>VfEsL~meaqc2iZvLBV%gheQ@8W6+)NpbMixZXR5$HC)_wu{H zuRbZp=eoX_4YZ}xy zm+1-y20h%Q?lOHI-ea+SaU@K7E2Wy%yZqpeYG@96gCe10D!%AckMhb6uRGK;K{=eX zgqYzYul|H|=vDga&EdI5RzWdo5u72LRjds{T8?nKrDqhqTQ-_LLn?;q@sOE*4D@{^$BFH>x>OCy=#UTlH$&N^SQ{QS0uIH|vn5!1n`p^`@| zG{8#Qs*o}8i0u9~X$Dq_33eu7R8Wepe2CZ{(+ByA{g=B*V{~|wD&WV|Y-%1rnc}4r zxX{vpXq9V>it|+|CJ5F1GSX&;-9@tVO^qsa?~;?>UY%~~QPj;--A!vPe?)T)dvN4i zwmd85Zygjv>~Gc2-|GVQhxg_-U(ki9u+cluBLJRY?QksCL;rbgW0#+QEf@L}^(0r2AEfE`0zcG9ONSDy~*8;E9_ETT58p9a|c$v{G)Bwx?+cS0F+B z{$6Pg)FZY|gePgoqF&=8a4*wBoUy^voDFi(JduK)^m*K8xr_ru0}OAq0IVMz5dE6b z=+w%WeQ7;6L1MKx<#7-cUcPYtl1w5ts$;aJo1M9Z-ZmtG`TL}poLd?;lZW9Z4`_hB zj#1P~jFNu1+886)i`>(XG^&nhHlpLPck=%vxBNf(Y#$SnF zC4K%;mKon;0qY!h?X9r*Z0DZC@WXzPhx*=z0+WS$c&5jr z-m!RjzdUfhY^tE;FozDX`a|l(iNC7KggCOT;bO5@02IH`CqC8`LvAi+oT4EwY-Krl z42YUufnDyhIChO7dWP<;wnAnQB*W$VL@~1=$G;l&Ip81Z)h2k|)&q{9u&J)x&@6ZF z&fFs5gj`*{O>NFca#yxBHGLSL=VSo@kfOE-LtLpw3xq+!q;l%)v}|%6!>gaW^Yz_C zCzRg30s!72tY{REv8}55rU%L(997OuP#EJ<)ANSz`IvYHisp^1T2m6b?qUh>7STe9 z-pw2S>0B8Q&3^aB+4Ge0Y>Ajc0h}>)rx+MFA_oAYVstndV_BwxV~cJw?C?wI8#^`c zAfn>Pv5{-E+qE=n!8iTz7#^wy=!v8!1|}!AdL6as*NO6{E8e#V*rVDr_48L4qt&yY)S`p1X5TgGA+q`QxK&#myAftJ6GZ5U?-3YhkHVJGSK?+*3EE6!v zn$DRK0mZIAIAum#HmJ-~+MA?TkD}cS?)#TI0GRCj%3$GD`llA1d^FWs7g;}tHdGgh z_O9Tl)FeKL(gP|r753=;$d(SJ{&ESq`!UqA+7%&J*oZ^W^Z-!fBy#hFCi6WS8?@)_ zyBM;UP6s)g@fuGBtDm^Sx03Dv=6!YFF-P$IG5#6;(Zgl3;7Q$*q+P?ZP`4bGjy literal 0 HcmV?d00001 From 62b2d994c16faf14f5ae6a66dd863c60960b74a1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Apr 2023 06:33:52 -0700 Subject: [PATCH 28/79] chore(deps): bump regex from 1.7.3 to 1.8.0 (#411) Bumps [regex](https://github.com/rust-lang/regex) from 1.7.3 to 1.8.0. - [Release notes](https://github.com/rust-lang/regex/releases) - [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-lang/regex/commits) --- updated-dependencies: - dependency-name: regex dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e0027296..bf961a61 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "aho-corasick" -version = "0.7.20" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" dependencies = [ "memchr", ] @@ -915,9 +915,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.7.3" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d" +checksum = "ac6cf59af1067a3fb53fbe5c88c053764e930f932be1d71d3ffe032cbe147f59" dependencies = [ "aho-corasick", "memchr", @@ -926,9 +926,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.29" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" +checksum = "b6868896879ba532248f33598de5181522d8b3d9d724dfd230911e1a7d4822f5" [[package]] name = "ring" From cb5ff40a59b95c0bffdb67961a16fe78e85f0192 Mon Sep 17 00:00:00 2001 From: Shawn <18649508+NOBLES5E@users.noreply.github.com> Date: Wed, 26 Apr 2023 11:28:54 -0400 Subject: [PATCH 29/79] fix typo (#415) chore: typo --- src/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.rs b/src/config.rs index 00c0f068..5ff09ee1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -588,7 +588,7 @@ impl Shard { if primary_count > 1 { error!( - "Shard {} has more than on primary configured", + "Shard {} has more than one primary configured", self.database ); return Err(Error::BadConfig); From 4a87b4807d5b9601612623890f091cadbc0b109e Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Wed, 26 Apr 2023 16:33:26 -0700 Subject: [PATCH 30/79] Add more pool settings (#416) * Add some pool settings * fmt --- CONFIG.md | 8 ++++ pgcat.toml | 5 +++ src/auth_passthrough.rs | 2 + src/config.rs | 91 +++++++++++++++++++++++++++++++++++++++-- src/pool.rs | 53 +++++++++++++++++------- 5 files changed, 141 insertions(+), 18 deletions(-) diff --git a/CONFIG.md b/CONFIG.md index 72d5683a..1a05a776 100644 --- a/CONFIG.md +++ b/CONFIG.md @@ -49,6 +49,14 @@ default: 30000 # milliseconds How long an idle connection with a server is left open (ms). +### server_lifetime +``` +path: general.server_lifetime +default: 86400000 # 24 hours +``` + +Max connection lifetime before it's closed, even if actively used. + ### idle_client_in_transaction_timeout ``` path: general.idle_client_in_transaction_timeout diff --git a/pgcat.toml b/pgcat.toml index 6cb9a299..9203cb60 100644 --- a/pgcat.toml +++ b/pgcat.toml @@ -23,6 +23,9 @@ connect_timeout = 5000 # milliseconds # How long an idle connection with a server is left open (ms). idle_timeout = 30000 # milliseconds +# Max connection lifetime before it's closed, even if actively used. +server_lifetime = 86400000 # 24 hours + # How long a client is allowed to be idle while in a transaction (ms). idle_client_in_transaction_timeout = 0 # milliseconds @@ -206,6 +209,8 @@ sharding_function = "pg_bigint_hash" username = "simple_user" password = "simple_user" pool_size = 5 +min_pool_size = 3 +server_lifetime = 60000 statement_timeout = 0 [pools.simple_db.shards.0] diff --git a/src/auth_passthrough.rs b/src/auth_passthrough.rs index 4e936262..fc0f6dc6 100644 --- a/src/auth_passthrough.rs +++ b/src/auth_passthrough.rs @@ -77,6 +77,8 @@ impl AuthPassthrough { pool_size: 1, statement_timeout: 0, pool_mode: None, + server_lifetime: None, + min_pool_size: None, }; let user = &address.username; diff --git a/src/config.rs b/src/config.rs index 5ff09ee1..d822486d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -181,7 +181,9 @@ pub struct User { pub server_username: Option, pub server_password: Option, pub pool_size: u32, + pub min_pool_size: Option, pub pool_mode: Option, + pub server_lifetime: Option, #[serde(default)] // 0 pub statement_timeout: u64, } @@ -194,12 +196,34 @@ impl Default for User { server_username: None, server_password: None, pool_size: 15, + min_pool_size: None, statement_timeout: 0, pool_mode: None, + server_lifetime: None, } } } +impl User { + fn validate(&self) -> Result<(), Error> { + match self.min_pool_size { + Some(min_pool_size) => { + if min_pool_size > self.pool_size { + error!( + "min_pool_size of {} cannot be larger than pool_size of {}", + min_pool_size, self.pool_size + ); + return Err(Error::BadConfig); + } + } + + None => (), + }; + + Ok(()) + } +} + /// General configuration. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct General { @@ -246,6 +270,9 @@ pub struct General { #[serde(default = "General::default_idle_client_in_transaction_timeout")] pub idle_client_in_transaction_timeout: u64, + #[serde(default = "General::default_server_lifetime")] + pub server_lifetime: u64, + #[serde(default = "General::default_worker_threads")] pub worker_threads: usize, @@ -271,6 +298,10 @@ impl General { 5432 } + pub fn default_server_lifetime() -> u64 { + 1000 * 60 * 60 * 24 // 24 hours + } + pub fn default_connect_timeout() -> u64 { 1000 } @@ -347,6 +378,7 @@ impl Default for General { auth_query: None, auth_query_user: None, auth_query_password: None, + server_lifetime: 1000 * 3600 * 24, // 24 hours, } } } @@ -411,6 +443,8 @@ pub struct Pool { pub idle_timeout: Option, + pub server_lifetime: Option, + pub sharding_function: ShardingFunction, #[serde(default = "Pool::default_automatic_sharding_key")] @@ -515,6 +549,10 @@ impl Pool { None => None, }; + for (_, user) in &self.users { + user.validate()?; + } + Ok(()) } } @@ -539,6 +577,7 @@ impl Default for Pool { auth_query: None, auth_query_user: None, auth_query_password: None, + server_lifetime: None, } } } @@ -791,6 +830,10 @@ impl Config { ); info!("Shutdown timeout: {}ms", self.general.shutdown_timeout); info!("Healthcheck delay: {}ms", self.general.healthcheck_delay); + info!( + "Default max server lifetime: {}ms", + self.general.server_lifetime + ); match self.general.tls_certificate.clone() { Some(tls_certificate) => { info!("TLS certificate: {}", tls_certificate); @@ -867,12 +910,26 @@ impl Config { pool_name, pool_config.users.len() ); + info!( + "[pool: {}] Max server lifetime: {}", + pool_name, + match pool_config.server_lifetime { + Some(server_lifetime) => format!("{}ms", server_lifetime), + None => "default".to_string(), + } + ); for user in &pool_config.users { info!( "[pool: {}][user: {}] Pool size: {}", pool_name, user.1.username, user.1.pool_size, ); + info!( + "[pool: {}][user: {}] Minimum pool size: {}", + pool_name, + user.1.username, + user.1.min_pool_size.unwrap_or(0) + ); info!( "[pool: {}][user: {}] Statement timeout: {}", pool_name, user.1.username, user.1.statement_timeout @@ -886,6 +943,15 @@ impl Config { None => pool_config.pool_mode.to_string(), } ); + info!( + "[pool: {}][user: {}] Max server lifetime: {}", + pool_name, + user.1.username, + match user.1.server_lifetime { + Some(server_lifetime) => format!("{}ms", server_lifetime), + None => "default".to_string(), + } + ); } } } @@ -896,7 +962,13 @@ impl Config { && (self.general.auth_query_user.is_none() || self.general.auth_query_password.is_none()) { - error!("If auth_query is specified, you need to provide a value for `auth_query_user`, `auth_query_password`"); + error!( + "If auth_query is specified, \ + you need to provide a value \ + for `auth_query_user`, \ + `auth_query_password`" + ); + return Err(Error::BadConfig); } @@ -904,7 +976,14 @@ impl Config { if pool.auth_query.is_some() && (pool.auth_query_user.is_none() || pool.auth_query_password.is_none()) { - error!("Error in pool {{ {} }}. If auth_query is specified, you need to provide a value for `auth_query_user`, `auth_query_password`", name); + error!( + "Error in pool {{ {} }}. \ + If auth_query is specified, you need \ + to provide a value for `auth_query_user`, \ + `auth_query_password`", + name + ); + return Err(Error::BadConfig); } @@ -914,7 +993,13 @@ impl Config { || pool.auth_query_user.is_none()) && user_data.password.is_none() { - error!("Error in pool {{ {} }}. You have to specify a user password for every pool if auth_query is not specified", name); + error!( + "Error in pool {{ {} }}. \ + You have to specify a user password \ + for every pool if auth_query is not specified", + name + ); + return Err(Error::BadConfig); } } diff --git a/src/pool.rs b/src/pool.rs index 7f8e41c0..8ec88604 100644 --- a/src/pool.rs +++ b/src/pool.rs @@ -311,21 +311,34 @@ impl ConnectionPool { if let Some(apt) = &auth_passthrough { match apt.fetch_hash(&address).await { - Ok(ok) => { - if let Some(ref pool_auth_hash_value) = *(pool_auth_hash.read()) { - if ok != *pool_auth_hash_value { - warn!("Hash is not the same across shards of the same pool, client auth will \ - be done using last obtained hash. Server: {}:{}, Database: {}", server.host, server.port, shard.database); - } - } - debug!("Hash obtained for {:?}", address); - { - let mut pool_auth_hash = pool_auth_hash.write(); - *pool_auth_hash = Some(ok.clone()); - } - }, - Err(err) => warn!("Could not obtain password hashes using auth_query config, ignoring. Error: {:?}", err), - } + Ok(ok) => { + if let Some(ref pool_auth_hash_value) = *(pool_auth_hash.read()) + { + if ok != *pool_auth_hash_value { + warn!( + "Hash is not the same across shards \ + of the same pool, client auth will \ + be done using last obtained hash. \ + Server: {}:{}, Database: {}", + server.host, server.port, shard.database, + ); + } + } + + debug!("Hash obtained for {:?}", address); + + { + let mut pool_auth_hash = pool_auth_hash.write(); + *pool_auth_hash = Some(ok.clone()); + } + } + Err(err) => warn!( + "Could not obtain password hashes \ + using auth_query config, ignoring. \ + Error: {:?}", + err, + ), + } } let manager = ServerPool::new( @@ -347,10 +360,20 @@ impl ConnectionPool { None => config.general.idle_timeout, }; + let server_lifetime = match user.server_lifetime { + Some(server_lifetime) => server_lifetime, + None => match pool_config.server_lifetime { + Some(server_lifetime) => server_lifetime, + None => config.general.server_lifetime, + }, + }; + let pool = Pool::builder() .max_size(user.pool_size) + .min_idle(user.min_pool_size) .connection_timeout(std::time::Duration::from_millis(connect_timeout)) .idle_timeout(Some(std::time::Duration::from_millis(idle_timeout))) + .max_lifetime(Some(std::time::Duration::from_millis(server_lifetime))) .test_on_check_out(false) .build(manager) .await From 0d504032b260c0250b2459d33719379b2ef261bd Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Sun, 30 Apr 2023 09:41:46 -0700 Subject: [PATCH 31/79] Server TLS (#417) * Server TLS * Finish up TLS * thats it * diff * remove dead code * maybe? * dirty shutdown * skip flakey test * remove unused error * fetch config once --- Cargo.lock | 32 ++++++ Cargo.toml | 3 + pgcat.toml | 10 +- src/client.rs | 13 ++- src/config.rs | 14 +++ src/messages.rs | 43 +++++++- src/pool.rs | 3 +- src/server.rs | 201 ++++++++++++++++++++++++++++++++----- src/tls.rs | 23 ++++- tests/ruby/mirrors_spec.rb | 2 +- 10 files changed, 311 insertions(+), 33 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bf961a61..7991667e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -762,9 +762,11 @@ dependencies = [ "once_cell", "parking_lot", "phf", + "pin-project", "postgres-protocol", "rand", "regex", + "rustls", "rustls-pemfile", "serde", "serde_derive", @@ -776,6 +778,7 @@ dependencies = [ "tokio", "tokio-rustls", "toml", + "webpki-roots", ] [[package]] @@ -820,6 +823,26 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pin-project" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "pin-project-lite" version = "0.2.9" @@ -1446,6 +1469,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa54963694b65584e170cf5dc46aeb4dcaa5584e652ff5f3952e56d66aff0125" +dependencies = [ + "rustls-webpki", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index a5573518..28e94a6d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,9 @@ nix = "0.26.2" atomic_enum = "0.2.0" postgres-protocol = "0.6.5" fallible-iterator = "0.2" +pin-project = "1" +webpki-roots = "0.23" +rustls = { version = "0.21", features = ["dangerous_configuration"] } [target.'cfg(not(target_env = "msvc"))'.dependencies] jemallocator = "0.5.0" diff --git a/pgcat.toml b/pgcat.toml index 9203cb60..df2ba715 100644 --- a/pgcat.toml +++ b/pgcat.toml @@ -61,9 +61,15 @@ tcp_keepalives_count = 5 tcp_keepalives_interval = 5 # Path to TLS Certificate file to use for TLS connections -# tls_certificate = "server.cert" +# tls_certificate = ".circleci/server.cert" # Path to TLS private key file to use for TLS connections -# tls_private_key = "server.key" +# tls_private_key = ".circleci/server.key" + +# Enable/disable server TLS +server_tls = false + +# Verify server certificate is completely authentic. +verify_server_certificate = false # User name to access the virtual administrative database (pgbouncer or pgcat) # Connecting to that database allows running commands like `SHOW POOLS`, `SHOW DATABASES`, etc.. diff --git a/src/client.rs b/src/client.rs index 5098ec6f..efde7554 100644 --- a/src/client.rs +++ b/src/client.rs @@ -539,6 +539,7 @@ where Some(md5_hash_password(username, password, &salt)) } else { if !get_config().is_auth_query_configured() { + wrong_password(&mut write, username).await?; return Err(Error::ClientAuthImpossible(username.into())); } @@ -565,6 +566,8 @@ where } Err(err) => { + wrong_password(&mut write, username).await?; + return Err(Error::ClientAuthPassthroughError( err.to_string(), client_identifier, @@ -587,7 +590,15 @@ where client_identifier ); - let fetched_hash = refetch_auth_hash(&pool).await?; + let fetched_hash = match refetch_auth_hash(&pool).await { + Ok(fetched_hash) => fetched_hash, + Err(err) => { + wrong_password(&mut write, username).await?; + + return Err(err); + } + }; + let new_password_hash = md5_hash_second_pass(&fetched_hash, &salt); // Ok password changed in server an auth is possible. diff --git a/src/config.rs b/src/config.rs index d822486d..4af7beda 100644 --- a/src/config.rs +++ b/src/config.rs @@ -281,6 +281,13 @@ pub struct General { pub tls_certificate: Option, pub tls_private_key: Option, + + #[serde(default)] // false + pub server_tls: bool, + + #[serde(default)] // false + pub verify_server_certificate: bool, + pub admin_username: String, pub admin_password: String, @@ -373,6 +380,8 @@ impl Default for General { autoreload: None, tls_certificate: None, tls_private_key: None, + server_tls: false, + verify_server_certificate: false, admin_username: String::from("admin"), admin_password: String::from("admin"), auth_query: None, @@ -852,6 +861,11 @@ impl Config { info!("TLS support is disabled"); } }; + info!("Server TLS enabled: {}", self.general.server_tls); + info!( + "Server TLS certificate verification: {}", + self.general.verify_server_certificate + ); for (pool_name, pool_config) in &self.pools { // TODO: Make this output prettier (maybe a table?) diff --git a/src/messages.rs b/src/messages.rs index ba4818ce..0e980fe6 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -116,7 +116,10 @@ where /// Send the startup packet the server. We're pretending we're a Pg client. /// This tells the server which user we are and what database we want. -pub async fn startup(stream: &mut TcpStream, user: &str, database: &str) -> Result<(), Error> { +pub async fn startup(stream: &mut S, user: &str, database: &str) -> Result<(), Error> +where + S: tokio::io::AsyncWrite + std::marker::Unpin, +{ let mut bytes = BytesMut::with_capacity(25); bytes.put_i32(196608); // Protocol number @@ -150,6 +153,21 @@ pub async fn startup(stream: &mut TcpStream, user: &str, database: &str) -> Resu } } +pub async fn ssl_request(stream: &mut TcpStream) -> Result<(), Error> { + let mut bytes = BytesMut::with_capacity(12); + + bytes.put_i32(8); + bytes.put_i32(80877103); + + match stream.write_all(&bytes).await { + Ok(_) => Ok(()), + Err(err) => Err(Error::SocketError(format!( + "Error writing SSLRequest to server socket - Error: {:?}", + err + ))), + } +} + /// Parse the params the server sends as a key/value format. pub fn parse_params(mut bytes: BytesMut) -> Result, Error> { let mut result = HashMap::new(); @@ -505,6 +523,29 @@ where } } +pub async fn write_all_flush(stream: &mut S, buf: &[u8]) -> Result<(), Error> +where + S: tokio::io::AsyncWrite + std::marker::Unpin, +{ + match stream.write_all(buf).await { + Ok(_) => match stream.flush().await { + Ok(_) => Ok(()), + Err(err) => { + return Err(Error::SocketError(format!( + "Error flushing socket - Error: {:?}", + err + ))) + } + }, + Err(err) => { + return Err(Error::SocketError(format!( + "Error writing to socket - Error: {:?}", + err + ))) + } + } +} + /// Read a complete message from the socket. pub async fn read_message(stream: &mut S) -> Result where diff --git a/src/pool.rs b/src/pool.rs index 8ec88604..ee8de446 100644 --- a/src/pool.rs +++ b/src/pool.rs @@ -376,8 +376,7 @@ impl ConnectionPool { .max_lifetime(Some(std::time::Duration::from_millis(server_lifetime))) .test_on_check_out(false) .build(manager) - .await - .unwrap(); + .await?; pools.push(pool); servers.push(address); diff --git a/src/server.rs b/src/server.rs index 84bed6cc..5bcd5fb9 100644 --- a/src/server.rs +++ b/src/server.rs @@ -9,13 +9,12 @@ use std::collections::HashMap; use std::io::Read; use std::sync::Arc; use std::time::SystemTime; -use tokio::io::{AsyncReadExt, BufReader}; -use tokio::net::{ - tcp::{OwnedReadHalf, OwnedWriteHalf}, - TcpStream, -}; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, BufStream}; +use tokio::net::TcpStream; +use tokio_rustls::rustls::{OwnedTrustAnchor, RootCertStore}; +use tokio_rustls::{client::TlsStream, TlsConnector}; -use crate::config::{Address, User}; +use crate::config::{get_config, Address, User}; use crate::constants::*; use crate::errors::{Error, ServerIdentifier}; use crate::messages::*; @@ -23,6 +22,84 @@ use crate::mirrors::MirroringManager; use crate::pool::ClientServerMap; use crate::scram::ScramSha256; use crate::stats::ServerStats; +use std::io::Write; + +use pin_project::pin_project; + +#[pin_project(project = SteamInnerProj)] +pub enum StreamInner { + Plain { + #[pin] + stream: TcpStream, + }, + Tls { + #[pin] + stream: TlsStream, + }, +} + +impl AsyncWrite for StreamInner { + fn poll_write( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> std::task::Poll> { + let this = self.project(); + match this { + SteamInnerProj::Tls { stream } => stream.poll_write(cx, buf), + SteamInnerProj::Plain { stream } => stream.poll_write(cx, buf), + } + } + + fn poll_flush( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + let this = self.project(); + match this { + SteamInnerProj::Tls { stream } => stream.poll_flush(cx), + SteamInnerProj::Plain { stream } => stream.poll_flush(cx), + } + } + + fn poll_shutdown( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + let this = self.project(); + match this { + SteamInnerProj::Tls { stream } => stream.poll_shutdown(cx), + SteamInnerProj::Plain { stream } => stream.poll_shutdown(cx), + } + } +} + +impl AsyncRead for StreamInner { + fn poll_read( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut tokio::io::ReadBuf<'_>, + ) -> std::task::Poll> { + let this = self.project(); + match this { + SteamInnerProj::Tls { stream } => stream.poll_read(cx, buf), + SteamInnerProj::Plain { stream } => stream.poll_read(cx, buf), + } + } +} + +impl StreamInner { + pub fn try_write(&mut self, buf: &[u8]) -> std::io::Result { + match self { + StreamInner::Tls { stream } => { + let r = stream.get_mut(); + let mut w = r.1.writer(); + w.write(buf) + } + StreamInner::Plain { stream } => stream.try_write(buf), + } + } +} /// Server state. pub struct Server { @@ -30,11 +107,8 @@ pub struct Server { /// port, e.g. 5432, and role, e.g. primary or replica. address: Address, - /// Buffered read socket. - read: BufReader, - - /// Unbuffered write socket (our client code buffers). - write: OwnedWriteHalf, + /// Server TCP connection. + stream: BufStream, /// Our server response buffer. We buffer data before we give it to the client. buffer: BytesMut, @@ -98,8 +172,88 @@ impl Server { ))); } }; + + // TCP timeouts. configure_socket(&stream); + let config = get_config(); + + let mut stream = if config.general.server_tls { + // Request a TLS connection + ssl_request(&mut stream).await?; + + let response = match stream.read_u8().await { + Ok(response) => response as char, + Err(err) => { + return Err(Error::SocketError(format!( + "Server socket error: {:?}", + err + ))) + } + }; + + match response { + // Server supports TLS + 'S' => { + debug!("Connecting to server using TLS"); + + let mut root_store = RootCertStore::empty(); + root_store.add_server_trust_anchors( + webpki_roots::TLS_SERVER_ROOTS.0.iter().map(|ta| { + OwnedTrustAnchor::from_subject_spki_name_constraints( + ta.subject, + ta.spki, + ta.name_constraints, + ) + }), + ); + + let mut tls_config = rustls::ClientConfig::builder() + .with_safe_defaults() + .with_root_certificates(root_store) + .with_no_client_auth(); + + // Equivalent to sslmode=prefer which is fine most places. + // If you want verify-full, change `verify_server_certificate` to true. + if !config.general.verify_server_certificate { + let mut dangerous = tls_config.dangerous(); + dangerous.set_certificate_verifier(Arc::new( + crate::tls::NoCertificateVerification {}, + )); + } + + let connector = TlsConnector::from(Arc::new(tls_config)); + let stream = match connector + .connect(address.host.as_str().try_into().unwrap(), stream) + .await + { + Ok(stream) => stream, + Err(err) => { + return Err(Error::SocketError(format!("Server TLS error: {:?}", err))) + } + }; + + StreamInner::Tls { stream } + } + + // Server does not support TLS + 'N' => StreamInner::Plain { stream }, + + // Something else? + m => { + return Err(Error::SocketError(format!( + "Unknown message: {}", + m as char + ))); + } + } + } else { + StreamInner::Plain { stream } + }; + + // let (read, write) = split(stream); + // let (mut read, mut write) = (ReadInner::Plain { stream: read }, WriteInner::Plain { stream: write }); + trace!("Sending StartupMessage"); // StartupMessage @@ -245,7 +399,7 @@ impl Server { let sasl_type = String::from_utf8_lossy(&sasl_auth[..sasl_len - 2]); - if sasl_type == SCRAM_SHA_256 { + if sasl_type.contains(SCRAM_SHA_256) { debug!("Using {}", SCRAM_SHA_256); // Generate client message. @@ -268,7 +422,7 @@ impl Server { res.put_i32(sasl_response.len() as i32); res.put(sasl_response); - write_all(&mut stream, res).await?; + write_all_flush(&mut stream, &res).await?; } else { error!("Unsupported SCRAM version: {}", sasl_type); return Err(Error::ServerError); @@ -299,7 +453,7 @@ impl Server { res.put_i32(4 + sasl_response.len() as i32); res.put(sasl_response); - write_all(&mut stream, res).await?; + write_all_flush(&mut stream, &res).await?; } SASL_FINAL => { @@ -443,12 +597,9 @@ impl Server { } }; - let (read, write) = stream.into_split(); - let mut server = Server { address: address.clone(), - read: BufReader::new(read), - write, + stream: BufStream::new(stream), buffer: BytesMut::with_capacity(8196), server_info, process_id, @@ -515,7 +666,7 @@ impl Server { bytes.put_i32(process_id); bytes.put_i32(secret_key); - write_all(&mut stream, bytes).await + write_all_flush(&mut stream, &bytes).await } /// Send messages to the server from the client. @@ -523,7 +674,7 @@ impl Server { self.mirror_send(messages); self.stats().data_sent(messages.len()); - match write_all_half(&mut self.write, messages).await { + match write_all_flush(&mut self.stream, &messages).await { Ok(_) => { // Successfully sent to server self.last_activity = SystemTime::now(); @@ -542,7 +693,7 @@ impl Server { /// in order to receive all data the server has to offer. pub async fn recv(&mut self) -> Result { loop { - let mut message = match read_message(&mut self.read).await { + let mut message = match read_message(&mut self.stream).await { Ok(message) => message, Err(err) => { error!("Terminating server because of: {:?}", err); @@ -935,13 +1086,13 @@ impl Drop for Server { // Update statistics self.stats.disconnect(); - let mut bytes = BytesMut::with_capacity(4); + let mut bytes = BytesMut::with_capacity(5); bytes.put_u8(b'X'); bytes.put_i32(4); - match self.write.try_write(&bytes) { - Ok(_) => (), - Err(_) => debug!("Dirty shutdown"), + match self.stream.get_mut().try_write(&bytes) { + Ok(5) => (), + _ => debug!("Dirty shutdown"), }; // Should not matter. diff --git a/src/tls.rs b/src/tls.rs index fbfbae75..6c4a7f5b 100644 --- a/src/tls.rs +++ b/src/tls.rs @@ -4,7 +4,12 @@ use rustls_pemfile::{certs, read_one, Item}; use std::iter; use std::path::Path; use std::sync::Arc; -use tokio_rustls::rustls::{self, Certificate, PrivateKey}; +use std::time::SystemTime; +use tokio_rustls::rustls::{ + self, + client::{ServerCertVerified, ServerCertVerifier}, + Certificate, PrivateKey, ServerName, +}; use tokio_rustls::TlsAcceptor; use crate::config::get_config; @@ -64,3 +69,19 @@ impl Tls { }) } } + +pub struct NoCertificateVerification; + +impl ServerCertVerifier for NoCertificateVerification { + fn verify_server_cert( + &self, + _end_entity: &Certificate, + _intermediates: &[Certificate], + _server_name: &ServerName, + _scts: &mut dyn Iterator, + _ocsp_response: &[u8], + _now: SystemTime, + ) -> Result { + Ok(ServerCertVerified::assertion()) + } +} diff --git a/tests/ruby/mirrors_spec.rb b/tests/ruby/mirrors_spec.rb index 801df28c..898d0d71 100644 --- a/tests/ruby/mirrors_spec.rb +++ b/tests/ruby/mirrors_spec.rb @@ -25,7 +25,7 @@ processes.pgcat.shutdown end - it "can mirror a query" do + xit "can mirror a query" do conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) runs = 15 runs.times { conn.async_exec("SELECT 1 + 2") } From 3601130ba1e18bf4c5eb38e426a8f78ca7faa410 Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Sun, 30 Apr 2023 09:44:25 -0700 Subject: [PATCH 32/79] Readme update (#418) * Readme update * m * wording --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4dd75246..ae310cde 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ PostgreSQL pooler and proxy (like PgBouncer) with support for sharding, load bal | Failover | **Stable** | Queries are automatically rerouted around broken replicas, validated by regular health checks. | | Admin database statistics | **Stable** | Pooler statistics and administration via the `pgbouncer` and `pgcat` databases. | | Prometheus statistics | **Stable** | Statistics are reported via a HTTP endpoint for Prometheus. | -| Client TLS | **Stable** | Clients can connect to the pooler using TLS/SSL. | +| SSL/TLS | **Stable** | Clients can connect to the pooler using TLS. Pooler can connect to Postgres servers using TLS. | | Client/Server authentication | **Stable** | Clients can connect using MD5 authentication, supported by `libpq` and all Postgres client drivers. PgCat can connect to Postgres using MD5 and SCRAM-SHA-256. | | Live configuration reloading | **Stable** | Identical to PgBouncer; all settings can be reloaded dynamically (except `host` and `port`). | | Auth passthrough | **Stable** | MD5 password authentication can be configured to use an `auth_query` so no cleartext passwords are needed in the config file.| From 7dfbd993f2e51dfe5545a1dd941c565f15b1c3fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Fern=C3=A1ndez?= Date: Tue, 2 May 2023 10:26:40 +0200 Subject: [PATCH 33/79] Add dns_cache for server addresses as in pgbouncer (#249) * Add dns_cache so server addresses are cached and invalidated when DNS changes. Adds a module to deal with dns_cache feature. It's main struct is CachedResolver, which is a simple thread safe hostname <-> Ips cache with the ability to refresh resolutions every `dns_max_ttl` seconds. This way, a client can check whether its ip address has changed. * Allow reloading dns cached * Add documentation for dns_cached --- CONFIG.md | 16 ++ Cargo.lock | 288 +++++++++++++++++++++++++++++++++ Cargo.toml | 2 + pgcat.toml | 8 + src/config.rs | 17 ++ src/dns_cache.rs | 410 +++++++++++++++++++++++++++++++++++++++++++++++ src/errors.rs | 1 + src/lib.rs | 1 + src/main.rs | 12 +- src/server.rs | 42 ++++- 10 files changed, 794 insertions(+), 3 deletions(-) create mode 100644 src/dns_cache.rs diff --git a/CONFIG.md b/CONFIG.md index 1a05a776..b36a190d 100644 --- a/CONFIG.md +++ b/CONFIG.md @@ -188,6 +188,22 @@ default: "admin_pass" Password to access the virtual administrative database +### dns_cache_enabled +``` +path: general.dns_cache_enabled +default: false +``` +When enabled, ip resolutions for server connections specified using hostnames will be cached +and checked for changes every `dns_max_ttl` seconds. If a change in the host resolution is found +old ip connections are closed (gracefully) and new connections will start using new ip. + +### dns_max_ttl +``` +path: general.dns_max_ttl +default: 30 +``` +Specifies how often (in seconds) cached ip addresses for servers are rechecked (see `dns_cache_enabled`). + ## `pools.` Section ### pool_mode diff --git a/Cargo.lock b/Cargo.lock index 7991667e..7641e9c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,27 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" +[[package]] +name = "async-stream" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad5c83079eae9969be7fadefe640a1c566901f05ff91ab221de4b6f68d9507e" +dependencies = [ + "async-stream-impl", + "futures-core", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10f203db73a71dfa2fb6dd22763990fa26f3d2625a6da2da900d23b87d26be27" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "async-trait" version = "0.1.68" @@ -212,6 +233,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "data-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57" + [[package]] name = "digest" version = "0.10.6" @@ -223,6 +250,18 @@ dependencies = [ "subtle", ] +[[package]] +name = "enum-as-inner" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9720bba047d567ffc8a3cba48bf19126600e249ab7f128e9233e6376976a116" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "env_logger" version = "0.10.0" @@ -275,6 +314,15 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "form_urlencoded" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +dependencies = [ + "percent-encoding", +] + [[package]] name = "futures" version = "0.3.28" @@ -410,6 +458,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + [[package]] name = "hermit-abi" version = "0.2.6" @@ -434,6 +488,17 @@ dependencies = [ "digest", ] +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi", +] + [[package]] name = "http" version = "0.2.9" @@ -522,6 +587,27 @@ dependencies = [ "cxx-build", ] +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "indexmap" version = "1.9.2" @@ -542,6 +628,24 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "ipconfig" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd302af1b90f2463a98fa5ad469fc212c8e3175a41c3068601bfa2727591c5be" +dependencies = [ + "socket2", + "widestring", + "winapi", + "winreg", +] + +[[package]] +name = "ipnet" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f88c5561171189e69df9d98bcf18fd5f9558300f7ea7b801eb8a0fd748bd8745" + [[package]] name = "is-terminal" version = "0.4.4" @@ -589,6 +693,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "libc" version = "0.2.139" @@ -604,6 +714,12 @@ dependencies = [ "cc", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.1.4" @@ -629,6 +745,27 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + [[package]] name = "md-5" version = "0.10.5" @@ -737,6 +874,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "percent-encoding" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" + [[package]] name = "pgcat" version = "1.0.1" @@ -777,7 +920,9 @@ dependencies = [ "stringprep", "tokio", "tokio-rustls", + "tokio-test", "toml", + "trust-dns-resolver", "webpki-roots", ] @@ -888,6 +1033,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quote" version = "1.0.26" @@ -953,6 +1104,16 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6868896879ba532248f33598de5181522d8b3d9d724dfd230911e1a7d4822f5" +[[package]] +name = "resolv-conf" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" +dependencies = [ + "hostname", + "quick-error", +] + [[package]] name = "ring" version = "0.16.20" @@ -1191,6 +1352,26 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "thiserror" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "time" version = "0.1.45" @@ -1258,6 +1439,30 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d660770404473ccd7bc9f8b28494a811bc18542b915c0855c51e8f419d5223ce" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-test" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53474327ae5e166530d17f2d956afcb4f8a004de581b3cae10f12006bc8163e3" +dependencies = [ + "async-stream", + "bytes", + "futures-core", + "tokio", + "tokio-stream", +] + [[package]] name = "tokio-util" version = "0.7.7" @@ -1320,9 +1525,21 @@ checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if", "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "tracing-core" version = "0.1.30" @@ -1332,6 +1549,51 @@ dependencies = [ "once_cell", ] +[[package]] +name = "trust-dns-proto" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f7f83d1e4a0e4358ac54c5c3681e5d7da5efc5a7a632c90bb6d6669ddd9bc26" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna 0.2.3", + "ipnet", + "lazy_static", + "rand", + "smallvec", + "thiserror", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "trust-dns-resolver" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aff21aa4dcefb0a1afbfac26deb0adc93888c7d295fb63ab273ef276ba2b7cfe" +dependencies = [ + "cfg-if", + "futures-util", + "ipconfig", + "lazy_static", + "lru-cache", + "parking_lot", + "resolv-conf", + "smallvec", + "thiserror", + "tokio", + "tracing", + "trust-dns-proto", +] + [[package]] name = "try-lock" version = "0.2.4" @@ -1377,6 +1639,17 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +[[package]] +name = "url" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +dependencies = [ + "form_urlencoded", + "idna 0.3.0", + "percent-encoding", +] + [[package]] name = "version_check" version = "0.9.4" @@ -1478,6 +1751,12 @@ dependencies = [ "rustls-webpki", ] +[[package]] +name = "widestring" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17882f045410753661207383517a6f62ec3dbeb6a4ed2acce01f0728238d1983" + [[package]] name = "winapi" version = "0.3.9" @@ -1583,3 +1862,12 @@ checksum = "faf09497b8f8b5ac5d3bb4d05c0a99be20f26fd3d5f2db7b0716e946d5103658" dependencies = [ "memchr", ] + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] diff --git a/Cargo.toml b/Cargo.toml index 28e94a6d..436c3dd5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,8 @@ fallible-iterator = "0.2" pin-project = "1" webpki-roots = "0.23" rustls = { version = "0.21", features = ["dangerous_configuration"] } +trust-dns-resolver = "0.22.0" +tokio-test = "0.4.2" [target.'cfg(not(target_env = "msvc"))'.dependencies] jemallocator = "0.5.0" diff --git a/pgcat.toml b/pgcat.toml index df2ba715..c844ce1f 100644 --- a/pgcat.toml +++ b/pgcat.toml @@ -146,6 +146,14 @@ idle_timeout = 40000 # Connect timeout can be overwritten in the pool connect_timeout = 3000 +# When enabled, ip resolutions for server connections specified using hostnames will be cached +# and checked for changes every `dns_max_ttl` seconds. If a change in the host resolution is found +# old ip connections are closed (gracefully) and new connections will start using new ip. +# dns_cache_enabled = false + +# Specifies how often (in seconds) cached ip addresses for servers are rechecked (see `dns_cache_enabled`). +# dns_max_ttl = 30 + # User configs are structured as pool..users. # This section holds the credentials for users that may connect to this cluster [pools.sharded_db.users.0] diff --git a/src/config.rs b/src/config.rs index 4af7beda..fd7d3912 100644 --- a/src/config.rs +++ b/src/config.rs @@ -12,6 +12,7 @@ use std::sync::Arc; use tokio::fs::File; use tokio::io::AsyncReadExt; +use crate::dns_cache::CachedResolver; use crate::errors::Error; use crate::pool::{ClientServerMap, ConnectionPool}; use crate::sharding::ShardingFunction; @@ -255,6 +256,12 @@ pub struct General { #[serde(default)] // False pub log_client_disconnections: bool, + #[serde(default)] // False + pub dns_cache_enabled: bool, + + #[serde(default = "General::default_dns_max_ttl")] + pub dns_max_ttl: u64, + #[serde(default = "General::default_shutdown_timeout")] pub shutdown_timeout: u64, @@ -336,6 +343,10 @@ impl General { 60000 } + pub fn default_dns_max_ttl() -> u64 { + 30 + } + pub fn default_healthcheck_timeout() -> u64 { 1000 } @@ -378,6 +389,8 @@ impl Default for General { log_client_connections: false, log_client_disconnections: false, autoreload: None, + dns_cache_enabled: false, + dns_max_ttl: Self::default_dns_max_ttl(), tls_certificate: None, tls_private_key: None, server_tls: false, @@ -1119,6 +1132,10 @@ pub async fn reload_config(client_server_map: ClientServerMap) -> Result (), + Err(err) => error!("DNS cache reinitialization error: {:?}", err), + }; if old_config.pools != new_config.pools { info!("Pool configuration changed"); diff --git a/src/dns_cache.rs b/src/dns_cache.rs new file mode 100644 index 00000000..5c2be5dc --- /dev/null +++ b/src/dns_cache.rs @@ -0,0 +1,410 @@ +use crate::config::get_config; +use crate::errors::Error; +use arc_swap::ArcSwap; +use log::{debug, error, info, warn}; +use once_cell::sync::Lazy; +use std::collections::{HashMap, HashSet}; +use std::io; +use std::net::IpAddr; +use std::sync::Arc; +use std::sync::RwLock; +use tokio::time::{sleep, Duration}; +use trust_dns_resolver::error::{ResolveError, ResolveResult}; +use trust_dns_resolver::lookup_ip::LookupIp; +use trust_dns_resolver::TokioAsyncResolver; + +/// Cached Resolver Globally available +pub static CACHED_RESOLVER: Lazy> = + Lazy::new(|| ArcSwap::from_pointee(CachedResolver::default())); + +// Ip addressed are returned as a set of addresses +// so we can compare. +#[derive(Clone, PartialEq, Debug)] +pub struct AddrSet { + set: HashSet, +} + +impl AddrSet { + fn new() -> AddrSet { + AddrSet { + set: HashSet::new(), + } + } +} + +impl From for AddrSet { + fn from(lookup_ip: LookupIp) -> Self { + let mut addr_set = AddrSet::new(); + for address in lookup_ip.iter() { + addr_set.set.insert(address); + } + addr_set + } +} + +/// +/// A CachedResolver is a DNS resolution cache mechanism with customizable expiration time. +/// +/// The system works as follows: +/// +/// When a host is to be resolved, if we have not resolved it before, a new resolution is +/// executed and stored in the internal cache. Concurrently, every `dns_max_ttl` time, the +/// cache is refreshed. +/// +/// # Example: +/// +/// ``` +/// use pgcat::dns_cache::{CachedResolverConfig, CachedResolver}; +/// +/// # tokio_test::block_on(async { +/// let config = CachedResolverConfig::default(); +/// let resolver = CachedResolver::new(config, None).await.unwrap(); +/// let addrset = resolver.lookup_ip("www.example.com.").await.unwrap(); +/// # }) +/// ``` +/// +/// // Now the ip resolution is stored in local cache and subsequent +/// // calls will be returned from cache. Also, the cache is refreshed +/// // and updated every 10 seconds. +/// +/// // You can now check if an 'old' lookup differs from what it's currently +/// // store in cache by using `has_changed`. +/// resolver.has_changed("www.example.com.", addrset) +#[derive(Default)] +pub struct CachedResolver { + // The configuration of the cached_resolver. + config: CachedResolverConfig, + + // This is the hash that contains the hash. + data: Option>>, + + // The resolver to be used for DNS queries. + resolver: Option, + + // The RefreshLoop + refresh_loop: RwLock>>, +} + +/// +/// Configuration +#[derive(Clone, Debug, Default, PartialEq)] +pub struct CachedResolverConfig { + /// Amount of time in secods that a resolved dns address is considered stale. + dns_max_ttl: u64, + + /// Enabled or disabled? (this is so we can reload config) + enabled: bool, +} + +impl CachedResolverConfig { + fn new(dns_max_ttl: u64, enabled: bool) -> Self { + CachedResolverConfig { + dns_max_ttl, + enabled, + } + } +} + +impl From for CachedResolverConfig { + fn from(config: crate::config::Config) -> Self { + CachedResolverConfig::new(config.general.dns_max_ttl, config.general.dns_cache_enabled) + } +} + +impl CachedResolver { + /// + /// Returns a new Arc based on passed configuration. + /// It also starts the loop that will refresh cache entries. + /// + /// # Arguments: + /// + /// * `config` - The `CachedResolverConfig` to be used to create the resolver. + /// + /// # Example: + /// + /// ``` + /// use pgcat::dns_cache::{CachedResolverConfig, CachedResolver}; + /// + /// # tokio_test::block_on(async { + /// let config = CachedResolverConfig::default(); + /// let resolver = CachedResolver::new(config, None).await.unwrap(); + /// # }) + /// ``` + /// + pub async fn new( + config: CachedResolverConfig, + data: Option>, + ) -> Result, io::Error> { + // Construct a new Resolver with default configuration options + let resolver = Some(TokioAsyncResolver::tokio_from_system_conf()?); + + let data = if let Some(hash) = data { + Some(RwLock::new(hash)) + } else { + Some(RwLock::new(HashMap::new())) + }; + + let instance = Arc::new(Self { + config, + resolver, + data, + refresh_loop: RwLock::new(None), + }); + + if instance.enabled() { + info!("Scheduling DNS refresh loop"); + let refresh_loop = tokio::task::spawn({ + let instance = instance.clone(); + async move { + instance.refresh_dns_entries_loop().await; + } + }); + *(instance.refresh_loop.write().unwrap()) = Some(refresh_loop); + } + + Ok(instance) + } + + pub fn enabled(&self) -> bool { + self.config.enabled + } + + // Schedules the refresher + async fn refresh_dns_entries_loop(&self) { + let resolver = TokioAsyncResolver::tokio_from_system_conf().unwrap(); + let interval = Duration::from_secs(self.config.dns_max_ttl); + loop { + debug!("Begin refreshing cached DNS addresses."); + // To minimize the time we hold the lock, we first create + // an array with keys. + let mut hostnames: Vec = Vec::new(); + { + if let Some(ref data) = self.data { + for hostname in data.read().unwrap().keys() { + hostnames.push(hostname.clone()); + } + } + } + + for hostname in hostnames.iter() { + let addrset = self + .fetch_from_cache(hostname.as_str()) + .expect("Could not obtain expected address from cache, this should not happen"); + + match resolver.lookup_ip(hostname).await { + Ok(lookup_ip) => { + let new_addrset = AddrSet::from(lookup_ip); + debug!( + "Obtained address for host ({}) -> ({:?})", + hostname, new_addrset + ); + + if addrset != new_addrset { + debug!( + "Addr changed from {:?} to {:?} updating cache.", + addrset, new_addrset + ); + self.store_in_cache(hostname, new_addrset); + } + } + Err(err) => { + error!( + "There was an error trying to resolv {}: ({}).", + hostname, err + ); + } + } + } + debug!("Finished refreshing cached DNS addresses."); + sleep(interval).await; + } + } + + /// Returns a `AddrSet` given the specified hostname. + /// + /// This method first tries to fetch the value from the cache, if it misses + /// then it is resolved and stored in the cache. TTL from records is ignored. + /// + /// # Arguments + /// + /// * `host` - A string slice referencing the hostname to be resolved. + /// + /// # Example: + /// + /// ``` + /// use pgcat::dns_cache::{CachedResolverConfig, CachedResolver}; + /// + /// # tokio_test::block_on(async { + /// let config = CachedResolverConfig::default(); + /// let resolver = CachedResolver::new(config, None).await.unwrap(); + /// let response = resolver.lookup_ip("www.google.com."); + /// # }) + /// ``` + /// + pub async fn lookup_ip(&self, host: &str) -> ResolveResult { + debug!("Lookup up {} in cache", host); + match self.fetch_from_cache(host) { + Some(addr_set) => { + debug!("Cache hit!"); + Ok(addr_set) + } + None => { + debug!("Not found, executing a dns query!"); + if let Some(ref resolver) = self.resolver { + let addr_set = AddrSet::from(resolver.lookup_ip(host).await?); + debug!("Obtained: {:?}", addr_set); + self.store_in_cache(host, addr_set.clone()); + Ok(addr_set) + } else { + Err(ResolveError::from("No resolver available")) + } + } + } + } + + // + // Returns true if the stored host resolution differs from the AddrSet passed. + pub fn has_changed(&self, host: &str, addr_set: &AddrSet) -> bool { + if let Some(fetched_addr_set) = self.fetch_from_cache(host) { + return fetched_addr_set != *addr_set; + } + false + } + + // Fetches an AddrSet from the inner cache adquiring the read lock. + fn fetch_from_cache(&self, key: &str) -> Option { + if let Some(ref hash) = self.data { + if let Some(addr_set) = hash.read().unwrap().get(key) { + return Some(addr_set.clone()); + } + } + None + } + + // Sets up the global CACHED_RESOLVER static variable so we can globally use DNS + // cache. + pub async fn from_config() -> Result<(), Error> { + let cached_resolver = CACHED_RESOLVER.load(); + let desired_config = CachedResolverConfig::from(get_config()); + + if cached_resolver.config != desired_config { + if let Some(ref refresh_loop) = *(cached_resolver.refresh_loop.write().unwrap()) { + warn!("Killing Dnscache refresh loop as its configuration is being reloaded"); + refresh_loop.abort() + } + let new_resolver = if let Some(ref data) = cached_resolver.data { + let data = Some(data.read().unwrap().clone()); + CachedResolver::new(desired_config, data).await + } else { + CachedResolver::new(desired_config, None).await + }; + + match new_resolver { + Ok(ok) => { + CACHED_RESOLVER.store(ok); + Ok(()) + } + Err(err) => { + let message = format!("Error setting up cached_resolver. Error: {:?}, will continue without this feature.", err); + Err(Error::DNSCachedError(message)) + } + } + } else { + Ok(()) + } + } + + // Stores the AddrSet in cache adquiring the write lock. + fn store_in_cache(&self, host: &str, addr_set: AddrSet) { + if let Some(ref data) = self.data { + data.write().unwrap().insert(host.to_string(), addr_set); + } else { + error!("Could not insert, Hash not initialized"); + } + } +} +#[cfg(test)] +mod tests { + use super::*; + use trust_dns_resolver::error::ResolveError; + + #[tokio::test] + async fn new() { + let config = CachedResolverConfig { + dns_max_ttl: 10, + enabled: true, + }; + let resolver = CachedResolver::new(config, None).await; + assert!(resolver.is_ok()); + } + + #[tokio::test] + async fn lookup_ip() { + let config = CachedResolverConfig { + dns_max_ttl: 10, + enabled: true, + }; + let resolver = CachedResolver::new(config, None).await.unwrap(); + let response = resolver.lookup_ip("www.google.com.").await; + assert!(response.is_ok()); + } + + #[tokio::test] + async fn has_changed() { + let config = CachedResolverConfig { + dns_max_ttl: 10, + enabled: true, + }; + let resolver = CachedResolver::new(config, None).await.unwrap(); + let hostname = "www.google.com."; + let response = resolver.lookup_ip(hostname).await; + let addr_set = response.unwrap(); + assert!(!resolver.has_changed(hostname, &addr_set)); + } + + #[tokio::test] + async fn unknown_host() { + let config = CachedResolverConfig { + dns_max_ttl: 10, + enabled: true, + }; + let resolver = CachedResolver::new(config, None).await.unwrap(); + let hostname = "www.idontexists."; + let response = resolver.lookup_ip(hostname).await; + assert!(matches!(response, Err(ResolveError { .. }))); + } + + #[tokio::test] + async fn incorrect_address() { + let config = CachedResolverConfig { + dns_max_ttl: 10, + enabled: true, + }; + let resolver = CachedResolver::new(config, None).await.unwrap(); + let hostname = "w ww.idontexists."; + let response = resolver.lookup_ip(hostname).await; + assert!(matches!(response, Err(ResolveError { .. }))); + assert!(!resolver.has_changed(hostname, &AddrSet::new())); + } + + #[tokio::test] + // Ok, this test is based on the fact that google does DNS RR + // and does not responds with every available ip everytime, so + // if I cache here, it will miss after one cache iteration or two. + async fn thread() { + let config = CachedResolverConfig { + dns_max_ttl: 10, + enabled: true, + }; + let resolver = CachedResolver::new(config, None).await.unwrap(); + let hostname = "www.google.com."; + let response = resolver.lookup_ip(hostname).await; + let addr_set = response.unwrap(); + assert!(!resolver.has_changed(hostname, &addr_set)); + let resolver_for_refresher = resolver.clone(); + let _thread_handle = tokio::task::spawn(async move { + resolver_for_refresher.refresh_dns_entries_loop().await; + }); + assert!(!resolver.has_changed(hostname, &addr_set)); + } +} diff --git a/src/errors.rs b/src/errors.rs index 0930ab8b..fb70c042 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -19,6 +19,7 @@ pub enum Error { ClientError(String), TlsError, StatementTimeout, + DNSCachedError(String), ShuttingDown, ParseBytesError(String), AuthError(String), diff --git a/src/lib.rs b/src/lib.rs index 2645cd42..3a58bb38 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ pub mod auth_passthrough; pub mod config; pub mod constants; +pub mod dns_cache; pub mod errors; pub mod messages; pub mod mirrors; diff --git a/src/main.rs b/src/main.rs index b3265ed8..dc48dd58 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,6 +36,7 @@ extern crate sqlparser; extern crate tokio; extern crate tokio_rustls; extern crate toml; +extern crate trust_dns_resolver; #[cfg(not(target_env = "msvc"))] use jemallocator::Jemalloc; @@ -65,6 +66,7 @@ mod auth_passthrough; mod client; mod config; mod constants; +mod dns_cache; mod errors; mod messages; mod mirrors; @@ -166,8 +168,14 @@ fn main() -> Result<(), Box> { // Statistics reporting. REPORTER.store(Arc::new(Reporter::default())); - // Connection pool that allows to query all shards and replicas. - match ConnectionPool::from_config(client_server_map.clone()).await { + // Starts (if enabled) dns cache before pools initialization + match dns_cache::CachedResolver::from_config().await { + Ok(_) => (), + Err(err) => error!("DNS cache initialization error: {:?}", err), + }; + + // Connection pool that allows to query all shards and replicas. + match ConnectionPool::from_config(client_server_map.clone()).await { Ok(_) => (), Err(err) => { error!("Pool error: {:?}", err); diff --git a/src/server.rs b/src/server.rs index 5bcd5fb9..ff5ab20b 100644 --- a/src/server.rs +++ b/src/server.rs @@ -7,6 +7,7 @@ use parking_lot::{Mutex, RwLock}; use postgres_protocol::message; use std::collections::HashMap; use std::io::Read; +use std::net::IpAddr; use std::sync::Arc; use std::time::SystemTime; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, BufStream}; @@ -16,6 +17,7 @@ use tokio_rustls::{client::TlsStream, TlsConnector}; use crate::config::{get_config, Address, User}; use crate::constants::*; +use crate::dns_cache::{AddrSet, CACHED_RESOLVER}; use crate::errors::{Error, ServerIdentifier}; use crate::messages::*; use crate::mirrors::MirroringManager; @@ -148,6 +150,9 @@ pub struct Server { last_activity: SystemTime, mirror_manager: Option, + + // Associated addresses used + addr_set: Option, } impl Server { @@ -161,6 +166,24 @@ impl Server { stats: Arc, auth_hash: Arc>>, ) -> Result { + let cached_resolver = CACHED_RESOLVER.load(); + let mut addr_set: Option = None; + + // If we are caching addresses and hostname is not an IP + if cached_resolver.enabled() && address.host.parse::().is_err() { + debug!("Resolving {}", &address.host); + addr_set = match cached_resolver.lookup_ip(&address.host).await { + Ok(ok) => { + debug!("Obtained: {:?}", ok); + Some(ok) + } + Err(err) => { + warn!("Error trying to resolve {}, ({:?})", &address.host, err); + None + } + } + }; + let mut stream = match TcpStream::connect(&format!("{}:{}", &address.host, address.port)).await { Ok(stream) => stream, @@ -609,6 +632,7 @@ impl Server { bad: false, needs_cleanup: false, client_server_map, + addr_set, connected_at: chrono::offset::Utc::now().naive_utc(), stats, application_name: String::new(), @@ -849,7 +873,23 @@ impl Server { /// Server & client are out of sync, we must discard this connection. /// This happens with clients that misbehave. pub fn is_bad(&self) -> bool { - self.bad + if self.bad { + return self.bad; + }; + let cached_resolver = CACHED_RESOLVER.load(); + if cached_resolver.enabled() { + if let Some(addr_set) = &self.addr_set { + if cached_resolver.has_changed(self.address.host.as_str(), addr_set) { + warn!( + "DNS changed for {}, it was {:?}. Dropping server connection.", + self.address.host.as_str(), + addr_set + ); + return true; + } + } + } + false } /// Get server startup information to forward it to the client. From 23819c85492f3d41238aa6152e9f27bebd65ac5a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 May 2023 07:32:44 -0700 Subject: [PATCH 34/79] chore(deps): bump rustls from 0.21.0 to 0.21.1 (#419) Bumps [rustls](https://github.com/rustls/rustls) from 0.21.0 to 0.21.1. - [Release notes](https://github.com/rustls/rustls/releases) - [Changelog](https://github.com/rustls/rustls/blob/main/RELEASE_NOTES.md) - [Commits](https://github.com/rustls/rustls/compare/v/0.21.0...v/0.21.1) --- updated-dependencies: - dependency-name: rustls dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7641e9c2..dad2e6c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1145,9 +1145,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07180898a28ed6a7f7ba2311594308f595e3dd2e3c3812fa0a80a47b45f17e5d" +checksum = "c911ba11bc8433e811ce56fde130ccf32f5127cab0e0194e9c68c5a5b671791e" dependencies = [ "log", "ring", From 09e54e1175abb20c5d6549bd4fe6da69701e6420 Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Wed, 3 May 2023 09:13:05 -0700 Subject: [PATCH 35/79] Plugins! (#420) * Some queries * Plugins!! * cleanup * actual names * the actual plugins * comment * fix tests * Tests * unused errors * Increase reaper rate to actually enforce settings * ok --- Cargo.lock | 35 +++- Cargo.toml | 7 +- pgcat.toml | 3 + src/admin.rs | 2 +- src/client.rs | 91 +++++++++- src/config.rs | 4 + src/errors.rs | 4 +- src/lib.rs | 5 + src/main.rs | 54 ++---- src/messages.rs | 35 ++++ src/plugins/intercept.rs | 278 +++++++++++++++++++++++++++++ src/plugins/mod.rs | 40 +++++ src/plugins/table_access.rs | 50 ++++++ src/pool.rs | 24 +++ src/query_router.rs | 221 +++++++++++++++++------ tests/ruby/helpers/pgcat_helper.rb | 1 + tests/ruby/plugins_spec.rb | 14 ++ 17 files changed, 772 insertions(+), 96 deletions(-) create mode 100644 src/plugins/intercept.rs create mode 100644 src/plugins/mod.rs create mode 100644 src/plugins/table_access.rs create mode 100644 tests/ruby/plugins_spec.rb diff --git a/Cargo.lock b/Cargo.lock index dad2e6c5..6724a92c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -882,7 +882,7 @@ checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" [[package]] name = "pgcat" -version = "1.0.1" +version = "1.0.2-alpha1" dependencies = [ "arc-swap", "async-trait", @@ -913,6 +913,7 @@ dependencies = [ "rustls-pemfile", "serde", "serde_derive", + "serde_json", "sha-1", "sha2", "socket2", @@ -1174,6 +1175,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "ryu" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" + [[package]] name = "scopeguard" version = "1.1.0" @@ -1201,6 +1208,9 @@ name = "serde" version = "1.0.160" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" +dependencies = [ + "serde_derive", +] [[package]] name = "serde_derive" @@ -1213,6 +1223,17 @@ dependencies = [ "syn 2.0.9", ] +[[package]] +name = "serde_json" +version = "1.0.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +dependencies = [ + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_spanned" version = "0.6.1" @@ -1297,6 +1318,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "355dc4d4b6207ca8a3434fc587db0a8016130a574dbcdbfb93d7f7b5bc5b211a" dependencies = [ "log", + "sqlparser_derive", +] + +[[package]] +name = "sqlparser_derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55fe75cb4a364c7f7ae06c7dbbc8d84bddd85d6cdf9975963c3935bc1991761e" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 436c3dd5..80549821 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pgcat" -version = "1.0.1" +version = "1.0.2-alpha1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -14,12 +14,12 @@ rand = "0.8" chrono = "0.4" sha-1 = "0.10" toml = "0.7" -serde = "1" +serde = { version = "1", features = ["derive"] } serde_derive = "1" regex = "1" num_cpus = "1" once_cell = "1" -sqlparser = "0.33.0" +sqlparser = {version = "0.33", features = ["visitor"] } log = "0.4" arc-swap = "1" env_logger = "0.10" @@ -44,6 +44,7 @@ webpki-roots = "0.23" rustls = { version = "0.21", features = ["dangerous_configuration"] } trust-dns-resolver = "0.22.0" tokio-test = "0.4.2" +serde_json = "1" [target.'cfg(not(target_env = "msvc"))'.dependencies] jemallocator = "0.5.0" diff --git a/pgcat.toml b/pgcat.toml index c844ce1f..dfb57822 100644 --- a/pgcat.toml +++ b/pgcat.toml @@ -77,6 +77,9 @@ admin_username = "admin_user" # Password to access the virtual administrative database admin_password = "admin_pass" +# Plugins!! +# query_router_plugins = ["pg_table_access", "intercept"] + # pool configs are structured as pool. # the pool_name is what clients use as database name when connecting. # For a pool named `sharded_db`, clients access that pool using connection string like diff --git a/src/admin.rs b/src/admin.rs index 03af755c..ceba20c8 100644 --- a/src/admin.rs +++ b/src/admin.rs @@ -12,9 +12,9 @@ use tokio::time::Instant; use crate::config::{get_config, reload_config, VERSION}; use crate::errors::Error; use crate::messages::*; +use crate::pool::ClientServerMap; use crate::pool::{get_all_pools, get_pool}; use crate::stats::{get_client_stats, get_pool_stats, get_server_stats, ClientState, ServerState}; -use crate::ClientServerMap; pub fn generate_server_info_for_admin() -> BytesMut { let mut server_info = BytesMut::new(); diff --git a/src/client.rs b/src/client.rs index efde7554..331a0da4 100644 --- a/src/client.rs +++ b/src/client.rs @@ -16,6 +16,7 @@ use crate::auth_passthrough::refetch_auth_hash; use crate::config::{get_config, get_idle_client_in_transaction_timeout, Address, PoolMode}; use crate::constants::*; use crate::messages::*; +use crate::plugins::PluginOutput; use crate::pool::{get_pool, ClientServerMap, ConnectionPool}; use crate::query_router::{Command, QueryRouter}; use crate::server::Server; @@ -765,6 +766,9 @@ where self.stats.register(self.stats.clone()); + // Result returned by one of the plugins. + let mut plugin_output = None; + // Our custom protocol loop. // We expect the client to either start a transaction with regular queries // or issue commands for our sharding and server selection protocol. @@ -815,7 +819,25 @@ where 'Q' => { if query_router.query_parser_enabled() { - query_router.infer(&message); + if let Ok(ast) = QueryRouter::parse(&message) { + let plugin_result = query_router.execute_plugins(&ast).await; + + match plugin_result { + Ok(PluginOutput::Deny(error)) => { + error_response(&mut self.write, &error).await?; + continue; + } + + Ok(PluginOutput::Intercept(result)) => { + write_all(&mut self.write, result).await?; + continue; + } + + _ => (), + }; + + let _ = query_router.infer(&ast); + } } } @@ -823,7 +845,13 @@ where self.buffer.put(&message[..]); if query_router.query_parser_enabled() { - query_router.infer(&message); + if let Ok(ast) = QueryRouter::parse(&message) { + if let Ok(output) = query_router.execute_plugins(&ast).await { + plugin_output = Some(output); + } + + let _ = query_router.infer(&ast); + } } continue; @@ -857,6 +885,18 @@ where continue; } + // Check on plugin results. + match plugin_output { + Some(PluginOutput::Deny(error)) => { + self.buffer.clear(); + error_response(&mut self.write, &error).await?; + plugin_output = None; + continue; + } + + _ => (), + }; + // Get a pool instance referenced by the most up-to-date // pointer. This ensures we always read the latest config // when starting a query. @@ -1085,6 +1125,27 @@ where match code { // Query 'Q' => { + if query_router.query_parser_enabled() { + if let Ok(ast) = QueryRouter::parse(&message) { + let plugin_result = query_router.execute_plugins(&ast).await; + + match plugin_result { + Ok(PluginOutput::Deny(error)) => { + error_response(&mut self.write, &error).await?; + continue; + } + + Ok(PluginOutput::Intercept(result)) => { + write_all(&mut self.write, result).await?; + continue; + } + + _ => (), + }; + + let _ = query_router.infer(&ast); + } + } debug!("Sending query to server"); self.send_and_receive_loop( @@ -1124,6 +1185,14 @@ where // Parse // The query with placeholders is here, e.g. `SELECT * FROM users WHERE email = $1 AND active = $2`. 'P' => { + if query_router.query_parser_enabled() { + if let Ok(ast) = QueryRouter::parse(&message) { + if let Ok(output) = query_router.execute_plugins(&ast).await { + plugin_output = Some(output); + } + } + } + self.buffer.put(&message[..]); } @@ -1155,6 +1224,24 @@ where 'S' => { debug!("Sending query to server"); + match plugin_output { + Some(PluginOutput::Deny(error)) => { + error_response(&mut self.write, &error).await?; + plugin_output = None; + self.buffer.clear(); + continue; + } + + Some(PluginOutput::Intercept(result)) => { + write_all(&mut self.write, result).await?; + plugin_output = None; + self.buffer.clear(); + continue; + } + + _ => (), + }; + self.buffer.put(&message[..]); let first_message_code = (*self.buffer.get(0).unwrap_or(&0)) as char; diff --git a/src/config.rs b/src/config.rs index fd7d3912..aa98421c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -298,9 +298,12 @@ pub struct General { pub admin_username: String, pub admin_password: String, + // Support for auth query pub auth_query: Option, pub auth_query_user: Option, pub auth_query_password: Option, + + pub query_router_plugins: Option>, } impl General { @@ -401,6 +404,7 @@ impl Default for General { auth_query_user: None, auth_query_password: None, server_lifetime: 1000 * 3600 * 24, // 24 hours, + query_router_plugins: None, } } } diff --git a/src/errors.rs b/src/errors.rs index fb70c042..b1796eee 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,7 +1,7 @@ //! Errors. /// Various errors. -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] pub enum Error { SocketError(String), ClientSocketError(String, ClientIdentifier), @@ -24,6 +24,8 @@ pub enum Error { ParseBytesError(String), AuthError(String), AuthPassthroughError(String), + UnsupportedStatement, + QueryRouterParserError(String), } #[derive(Clone, PartialEq, Debug)] diff --git a/src/lib.rs b/src/lib.rs index 3a58bb38..db6167db 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,6 @@ +pub mod admin; pub mod auth_passthrough; +pub mod client; pub mod config; pub mod constants; pub mod dns_cache; @@ -6,7 +8,10 @@ pub mod errors; pub mod messages; pub mod mirrors; pub mod multi_logger; +pub mod plugins; pub mod pool; +pub mod prometheus; +pub mod query_router; pub mod scram; pub mod server; pub mod sharding; diff --git a/src/main.rs b/src/main.rs index dc48dd58..6af4db95 100644 --- a/src/main.rs +++ b/src/main.rs @@ -61,37 +61,19 @@ use std::str::FromStr; use std::sync::Arc; use tokio::sync::broadcast; -mod admin; -mod auth_passthrough; -mod client; -mod config; -mod constants; -mod dns_cache; -mod errors; -mod messages; -mod mirrors; -mod multi_logger; -mod pool; -mod prometheus; -mod query_router; -mod scram; -mod server; -mod sharding; -mod stats; -mod tls; - -use crate::config::{get_config, reload_config, VERSION}; -use crate::messages::configure_socket; -use crate::pool::{ClientServerMap, ConnectionPool}; -use crate::prometheus::start_metric_server; -use crate::stats::{Collector, Reporter, REPORTER}; +use pgcat::config::{get_config, reload_config, VERSION}; +use pgcat::dns_cache; +use pgcat::messages::configure_socket; +use pgcat::pool::{ClientServerMap, ConnectionPool}; +use pgcat::prometheus::start_metric_server; +use pgcat::stats::{Collector, Reporter, REPORTER}; fn main() -> Result<(), Box> { - multi_logger::MultiLogger::init().unwrap(); + pgcat::multi_logger::MultiLogger::init().unwrap(); info!("Welcome to PgCat! Meow. (Version {})", VERSION); - if !query_router::QueryRouter::setup() { + if !pgcat::query_router::QueryRouter::setup() { error!("Could not setup query router"); std::process::exit(exitcode::CONFIG); } @@ -109,7 +91,7 @@ fn main() -> Result<(), Box> { let runtime = Builder::new_multi_thread().worker_threads(1).build()?; runtime.block_on(async { - match config::parse(&config_file).await { + match pgcat::config::parse(&config_file).await { Ok(_) => (), Err(err) => { error!("Config parse error: {:?}", err); @@ -168,14 +150,14 @@ fn main() -> Result<(), Box> { // Statistics reporting. REPORTER.store(Arc::new(Reporter::default())); - // Starts (if enabled) dns cache before pools initialization - match dns_cache::CachedResolver::from_config().await { - Ok(_) => (), - Err(err) => error!("DNS cache initialization error: {:?}", err), - }; + // Starts (if enabled) dns cache before pools initialization + match dns_cache::CachedResolver::from_config().await { + Ok(_) => (), + Err(err) => error!("DNS cache initialization error: {:?}", err), + }; - // Connection pool that allows to query all shards and replicas. - match ConnectionPool::from_config(client_server_map.clone()).await { + // Connection pool that allows to query all shards and replicas. + match ConnectionPool::from_config(client_server_map.clone()).await { Ok(_) => (), Err(err) => { error!("Pool error: {:?}", err); @@ -303,7 +285,7 @@ fn main() -> Result<(), Box> { tokio::task::spawn(async move { let start = chrono::offset::Utc::now().naive_utc(); - match client::client_entrypoint( + match pgcat::client::client_entrypoint( socket, client_server_map, shutdown_rx, @@ -334,7 +316,7 @@ fn main() -> Result<(), Box> { Err(err) => { match err { - errors::Error::ClientBadStartup => debug!("Client disconnected with error {:?}", err), + pgcat::errors::Error::ClientBadStartup => debug!("Client disconnected with error {:?}", err), _ => warn!("Client disconnected with error {:?}", err), } diff --git a/src/messages.rs b/src/messages.rs index 0e980fe6..ee4886df 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -20,6 +20,10 @@ pub enum DataType { Text, Int4, Numeric, + Bool, + Oid, + AnyArray, + Any, } impl From<&DataType> for i32 { @@ -28,6 +32,10 @@ impl From<&DataType> for i32 { DataType::Text => 25, DataType::Int4 => 23, DataType::Numeric => 1700, + DataType::Bool => 16, + DataType::Oid => 26, + DataType::AnyArray => 2277, + DataType::Any => 2276, } } } @@ -443,6 +451,10 @@ pub fn row_description(columns: &Vec<(&str, DataType)>) -> BytesMut { DataType::Text => -1, DataType::Int4 => 4, DataType::Numeric => -1, + DataType::Bool => 1, + DataType::Oid => 4, + DataType::AnyArray => -1, + DataType::Any => -1, }; row_desc.put_i16(type_size); @@ -481,6 +493,29 @@ pub fn data_row(row: &Vec) -> BytesMut { res } +pub fn data_row_nullable(row: &Vec>) -> BytesMut { + let mut res = BytesMut::new(); + let mut data_row = BytesMut::new(); + + data_row.put_i16(row.len() as i16); + + for column in row { + if let Some(column) = column { + let column = column.as_bytes(); + data_row.put_i32(column.len() as i32); + data_row.put_slice(column); + } else { + data_row.put_i32(-1 as i32); + } + } + + res.put_u8(b'D'); + res.put_i32(data_row.len() as i32 + 4); + res.put(data_row); + + res +} + /// Create a CommandComplete message. pub fn command_complete(command: &str) -> BytesMut { let cmd = BytesMut::from(format!("{}\0", command).as_bytes()); diff --git a/src/plugins/intercept.rs b/src/plugins/intercept.rs new file mode 100644 index 00000000..6e250dca --- /dev/null +++ b/src/plugins/intercept.rs @@ -0,0 +1,278 @@ +//! The intercept plugin. +//! +//! It intercepts queries and returns fake results. + +use arc_swap::ArcSwap; +use async_trait::async_trait; +use bytes::{BufMut, BytesMut}; +use once_cell::sync::Lazy; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use sqlparser::ast::Statement; +use std::collections::HashMap; + +use log::debug; +use std::sync::Arc; + +use crate::{ + errors::Error, + messages::{command_complete, data_row_nullable, row_description, DataType}, + plugins::{Plugin, PluginOutput}, + pool::{PoolIdentifier, PoolMap}, + query_router::QueryRouter, +}; + +pub static CONFIG: Lazy>> = + Lazy::new(|| ArcSwap::from_pointee(HashMap::new())); + +/// Configure the intercept plugin. +pub fn configure(pools: &PoolMap) { + let mut config = HashMap::new(); + for (identifier, _) in pools.iter() { + // TODO: make this configurable from a text config. + let value = fool_datagrip(&identifier.db, &identifier.user); + config.insert(identifier.clone(), value); + } + + CONFIG.store(Arc::new(config)); +} + +// TODO: use these structs for deserialization +#[derive(Serialize, Deserialize)] +pub struct Rule { + query: String, + schema: Vec, + result: Vec>, +} + +#[derive(Serialize, Deserialize)] +pub struct Column { + name: String, + data_type: String, +} + +/// The intercept plugin. +pub struct Intercept; + +#[async_trait] +impl Plugin for Intercept { + async fn run( + &mut self, + query_router: &QueryRouter, + ast: &Vec, + ) -> Result { + if ast.is_empty() { + return Ok(PluginOutput::Allow); + } + + let mut result = BytesMut::new(); + let query_map = match CONFIG.load().get(&PoolIdentifier::new( + &query_router.pool_settings().db, + &query_router.pool_settings().user.username, + )) { + Some(query_map) => query_map.clone(), + None => return Ok(PluginOutput::Allow), + }; + + for q in ast { + // Normalization + let q = q.to_string().to_ascii_lowercase(); + + for target in query_map.as_array().unwrap().iter() { + if target["query"].as_str().unwrap() == q { + debug!("Query matched: {}", q); + + let rd = target["schema"] + .as_array() + .unwrap() + .iter() + .map(|row| { + let row = row.as_object().unwrap(); + ( + row["name"].as_str().unwrap(), + match row["data_type"].as_str().unwrap() { + "text" => DataType::Text, + "anyarray" => DataType::AnyArray, + "oid" => DataType::Oid, + "bool" => DataType::Bool, + "int4" => DataType::Int4, + _ => DataType::Any, + }, + ) + }) + .collect::>(); + + result.put(row_description(&rd)); + + target["result"].as_array().unwrap().iter().for_each(|row| { + let row = row + .as_array() + .unwrap() + .iter() + .map(|s| { + let s = s.as_str().unwrap().to_string(); + + if s == "" { + None + } else { + Some(s) + } + }) + .collect::>>(); + result.put(data_row_nullable(&row)); + }); + + result.put(command_complete("SELECT")); + } + } + } + + if !result.is_empty() { + result.put_u8(b'Z'); + result.put_i32(5); + result.put_u8(b'I'); + + return Ok(PluginOutput::Intercept(result)); + } else { + Ok(PluginOutput::Allow) + } + } +} + +/// Make IntelliJ SQL plugin believe it's talking to an actual database +/// instead of PgCat. +fn fool_datagrip(database: &str, user: &str) -> Value { + json!([ + { + "query": "select current_database() as a, current_schemas(false) as b", + "schema": [ + { + "name": "a", + "data_type": "text", + }, + { + "name": "b", + "data_type": "anyarray", + }, + ], + + "result": [ + [database, "{public}"], + ], + }, + { + "query": "select current_database(), current_schema(), current_user", + "schema": [ + { + "name": "current_database", + "data_type": "text", + }, + { + "name": "current_schema", + "data_type": "text", + }, + { + "name": "current_user", + "data_type": "text", + } + ], + + "result": [ + ["sharded_db", "public", "sharding_user"], + ], + }, + { + "query": "select cast(n.oid as bigint) as id, datname as name, d.description, datistemplate as is_template, datallowconn as allow_connections, pg_catalog.pg_get_userbyid(n.datdba) as \"owner\" from pg_catalog.pg_database as n left join pg_catalog.pg_shdescription as d on n.oid = d.objoid order by case when datname = pg_catalog.current_database() then -cast(1 as bigint) else cast(n.oid as bigint) end", + "schema": [ + { + "name": "id", + "data_type": "oid", + }, + { + "name": "name", + "data_type": "text", + }, + { + "name": "description", + "data_type": "text", + }, + { + "name": "is_template", + "data_type": "bool", + }, + { + "name": "allow_connections", + "data_type": "bool", + }, + { + "name": "owner", + "data_type": "text", + } + ], + "result": [ + ["16387", database, "", "f", "t", user], + ] + }, + { + "query": "select cast(r.oid as bigint) as role_id, rolname as role_name, rolsuper as is_super, rolinherit as is_inherit, rolcreaterole as can_createrole, rolcreatedb as can_createdb, rolcanlogin as can_login, rolreplication as is_replication, rolconnlimit as conn_limit, rolvaliduntil as valid_until, rolbypassrls as bypass_rls, rolconfig as config, d.description from pg_catalog.pg_roles as r left join pg_catalog.pg_shdescription as d on d.objoid = r.oid", + "schema": [ + { + "name": "role_id", + "data_type": "oid", + }, + { + "name": "role_name", + "data_type": "text", + }, + { + "name": "is_super", + "data_type": "bool", + }, + { + "name": "is_inherit", + "data_type": "bool", + }, + { + "name": "can_createrole", + "data_type": "bool", + }, + { + "name": "can_createdb", + "data_type": "bool", + }, + { + "name": "can_login", + "data_type": "bool", + }, + { + "name": "is_replication", + "data_type": "bool", + }, + { + "name": "conn_limit", + "data_type": "int4", + }, + { + "name": "valid_until", + "data_type": "text", + }, + { + "name": "bypass_rls", + "data_type": "bool", + }, + { + "name": "config", + "data_type": "text", + }, + { + "name": "description", + "data_type": "text", + }, + ], + "result": [ + ["10", "postgres", "f", "t", "f", "f", "t", "f", "-1", "", "f", "", ""], + ["16419", user, "f", "t", "f", "f", "t", "f", "-1", "", "f", "", ""], + ] + } + ]) +} diff --git a/src/plugins/mod.rs b/src/plugins/mod.rs new file mode 100644 index 00000000..92fa70b7 --- /dev/null +++ b/src/plugins/mod.rs @@ -0,0 +1,40 @@ +//! The plugin ecosystem. +//! +//! Currently plugins only grant access or deny access to the database for a particual query. +//! Example use cases: +//! - block known bad queries +//! - block access to system catalogs +//! - block dangerous modifications like `DROP TABLE` +//! - etc +//! + +pub mod intercept; +pub mod table_access; + +use crate::{errors::Error, query_router::QueryRouter}; +use async_trait::async_trait; +use bytes::BytesMut; +use sqlparser::ast::Statement; + +pub use intercept::Intercept; +pub use table_access::TableAccess; + +#[derive(Clone, Debug, PartialEq)] +pub enum PluginOutput { + Allow, + Deny(String), + Overwrite(Vec), + Intercept(BytesMut), +} + +#[async_trait] +pub trait Plugin { + // Custom output is allowed because we want to extend this system + // to rewriting queries some day. So an output of a plugin could be + // a rewritten AST. + async fn run( + &mut self, + query_router: &QueryRouter, + ast: &Vec, + ) -> Result; +} diff --git a/src/plugins/table_access.rs b/src/plugins/table_access.rs new file mode 100644 index 00000000..2e23278a --- /dev/null +++ b/src/plugins/table_access.rs @@ -0,0 +1,50 @@ +//! This query router plugin will check if the user can access a particular +//! table as part of their query. If they can't, the query will not be routed. + +use async_trait::async_trait; +use sqlparser::ast::{visit_relations, Statement}; + +use crate::{ + errors::Error, + plugins::{Plugin, PluginOutput}, + query_router::QueryRouter, +}; + +use core::ops::ControlFlow; + +pub struct TableAccess { + pub forbidden_tables: Vec, +} + +#[async_trait] +impl Plugin for TableAccess { + async fn run( + &mut self, + _query_router: &QueryRouter, + ast: &Vec, + ) -> Result { + let mut found = None; + + visit_relations(ast, |relation| { + let relation = relation.to_string(); + let parts = relation.split(".").collect::>(); + let table_name = parts.last().unwrap(); + + if self.forbidden_tables.contains(&table_name.to_string()) { + found = Some(table_name.to_string()); + ControlFlow::<()>::Break(()) + } else { + ControlFlow::<()>::Continue(()) + } + }); + + if let Some(found) = found { + Ok(PluginOutput::Deny(format!( + "permission for table \"{}\" denied", + found + ))) + } else { + Ok(PluginOutput::Allow) + } + } +} diff --git a/src/pool.rs b/src/pool.rs index ee8de446..b986548a 100644 --- a/src/pool.rs +++ b/src/pool.rs @@ -61,6 +61,8 @@ pub struct PoolIdentifier { pub user: String, } +static POOL_REAPER_RATE: u64 = 30_000; // 30 seconds by default + impl PoolIdentifier { /// Create a new user/pool identifier. pub fn new(db: &str, user: &str) -> PoolIdentifier { @@ -91,6 +93,7 @@ pub struct PoolSettings { // Connecting user. pub user: User, + pub db: String, // Default server role to connect to. pub default_role: Option, @@ -129,6 +132,8 @@ pub struct PoolSettings { pub auth_query: Option, pub auth_query_user: Option, pub auth_query_password: Option, + + pub plugins: Option>, } impl Default for PoolSettings { @@ -138,6 +143,7 @@ impl Default for PoolSettings { load_balancing_mode: LoadBalancingMode::Random, shards: 1, user: User::default(), + db: String::default(), default_role: None, query_parser_enabled: false, primary_reads_enabled: true, @@ -152,6 +158,7 @@ impl Default for PoolSettings { auth_query: None, auth_query_user: None, auth_query_password: None, + plugins: None, } } } @@ -368,12 +375,20 @@ impl ConnectionPool { }, }; + let reaper_rate = *vec![idle_timeout, server_lifetime, POOL_REAPER_RATE] + .iter() + .min() + .unwrap(); + + debug!("Pool reaper rate: {}ms", reaper_rate); + let pool = Pool::builder() .max_size(user.pool_size) .min_idle(user.min_pool_size) .connection_timeout(std::time::Duration::from_millis(connect_timeout)) .idle_timeout(Some(std::time::Duration::from_millis(idle_timeout))) .max_lifetime(Some(std::time::Duration::from_millis(server_lifetime))) + .reaper_rate(std::time::Duration::from_millis(reaper_rate)) .test_on_check_out(false) .build(manager) .await?; @@ -412,6 +427,7 @@ impl ConnectionPool { // shards: pool_config.shards.clone(), shards: shard_ids.len(), user: user.clone(), + db: pool_name.clone(), default_role: match pool_config.default_role.as_str() { "any" => None, "replica" => Some(Role::Replica), @@ -437,6 +453,7 @@ impl ConnectionPool { auth_query: pool_config.auth_query.clone(), auth_query_user: pool_config.auth_query_user.clone(), auth_query_password: pool_config.auth_query_password.clone(), + plugins: config.general.query_router_plugins.clone(), }, validated: Arc::new(AtomicBool::new(false)), paused: Arc::new(AtomicBool::new(false)), @@ -456,6 +473,13 @@ impl ConnectionPool { } } + // Initialize plugins here if required. + if let Some(plugins) = config.general.query_router_plugins { + if plugins.contains(&String::from("intercept")) { + crate::plugins::intercept::configure(&new_pools); + } + } + POOLS.store(Arc::new(new_pools.clone())); Ok(()) } diff --git a/src/query_router.rs b/src/query_router.rs index 5b2ba0c4..93bcd4f2 100644 --- a/src/query_router.rs +++ b/src/query_router.rs @@ -6,13 +6,16 @@ use once_cell::sync::OnceCell; use regex::{Regex, RegexSet}; use sqlparser::ast::Statement::{Query, StartTransaction}; use sqlparser::ast::{ - BinaryOperator, Expr, Ident, JoinConstraint, JoinOperator, SetExpr, TableFactor, Value, + BinaryOperator, Expr, Ident, JoinConstraint, JoinOperator, SetExpr, Statement, TableFactor, + Value, }; use sqlparser::dialect::PostgreSqlDialect; use sqlparser::parser::Parser; use crate::config::Role; +use crate::errors::Error; use crate::messages::BytesMutReader; +use crate::plugins::{Intercept, Plugin, PluginOutput, TableAccess}; use crate::pool::PoolSettings; use crate::sharding::Sharder; @@ -129,6 +132,10 @@ impl QueryRouter { self.pool_settings = pool_settings; } + pub fn pool_settings<'a>(&'a self) -> &'a PoolSettings { + &self.pool_settings + } + /// Try to parse a command and execute it. pub fn try_execute_command(&mut self, message_buffer: &BytesMut) -> Option<(Command, String)> { let mut message_cursor = Cursor::new(message_buffer); @@ -324,10 +331,7 @@ impl QueryRouter { Some((command, value)) } - /// Try to infer which server to connect to based on the contents of the query. - pub fn infer(&mut self, message: &BytesMut) -> bool { - debug!("Inferring role"); - + pub fn parse(message: &BytesMut) -> Result, Error> { let mut message_cursor = Cursor::new(message); let code = message_cursor.get_u8() as char; @@ -353,28 +357,29 @@ impl QueryRouter { query } - _ => return false, + _ => return Err(Error::UnsupportedStatement), }; - let ast = match Parser::parse_sql(&PostgreSqlDialect {}, &query) { - Ok(ast) => ast, + match Parser::parse_sql(&PostgreSqlDialect {}, &query) { + Ok(ast) => Ok(ast), Err(err) => { - // SELECT ... FOR UPDATE won't get parsed correctly. debug!("{}: {}", err, query); - self.active_role = Some(Role::Primary); - return false; + Err(Error::QueryRouterParserError(err.to_string())) } - }; + } + } - debug!("AST: {:?}", ast); + /// Try to infer which server to connect to based on the contents of the query. + pub fn infer(&mut self, ast: &Vec) -> Result<(), Error> { + debug!("Inferring role"); if ast.is_empty() { // That's weird, no idea, let's go to primary self.active_role = Some(Role::Primary); - return false; + return Err(Error::QueryRouterParserError("empty query".into())); } - for q in &ast { + for q in ast { match q { // All transactions go to the primary, probably a write. StartTransaction { .. } => { @@ -418,7 +423,7 @@ impl QueryRouter { }; } - true + Ok(()) } /// Parse the shard number from the Bind message @@ -783,6 +788,32 @@ impl QueryRouter { } } + /// Add your plugins here and execute them. + pub async fn execute_plugins(&self, ast: &Vec) -> Result { + if let Some(plugins) = &self.pool_settings.plugins { + if plugins.contains(&String::from("intercept")) { + let mut intercept = Intercept {}; + let result = intercept.run(&self, ast).await; + + if let Ok(PluginOutput::Intercept(output)) = result { + return Ok(PluginOutput::Intercept(output)); + } + } + + if plugins.contains(&String::from("pg_table_access")) { + let mut table_access = TableAccess { + forbidden_tables: vec![String::from("pg_database"), String::from("pg_roles")], + }; + + if let Ok(PluginOutput::Deny(error)) = table_access.run(&self, ast).await { + return Ok(PluginOutput::Deny(error)); + } + } + } + + Ok(PluginOutput::Allow) + } + fn set_sharding_key(&mut self, sharding_key: i64) -> Option { let sharder = Sharder::new( self.pool_settings.shards, @@ -810,11 +841,22 @@ impl QueryRouter { /// Should we attempt to parse queries? pub fn query_parser_enabled(&self) -> bool { let enabled = match self.query_parser_enabled { - None => self.pool_settings.query_parser_enabled, - Some(value) => value, - }; + None => { + debug!( + "Using pool settings, query_parser_enabled: {}", + self.pool_settings.query_parser_enabled + ); + self.pool_settings.query_parser_enabled + } - debug!("Query parser enabled: {}", enabled); + Some(value) => { + debug!( + "Using query parser override, query_parser_enabled: {}", + value + ); + value + } + }; enabled } @@ -862,7 +904,7 @@ mod test { for query in queries { // It's a recognized query - assert!(qr.infer(&query)); + assert!(qr.infer(&QueryRouter::parse(&query).unwrap()).is_ok()); assert_eq!(qr.role(), Some(Role::Replica)); } } @@ -881,7 +923,7 @@ mod test { for query in queries { // It's a recognized query - assert!(qr.infer(&query)); + assert!(qr.infer(&QueryRouter::parse(&query).unwrap()).is_ok()); assert_eq!(qr.role(), Some(Role::Primary)); } } @@ -893,7 +935,7 @@ mod test { let query = simple_query("SELECT * FROM items WHERE id = 5"); assert!(qr.try_execute_command(&simple_query("SET PRIMARY READS TO on")) != None); - assert!(qr.infer(&query)); + assert!(qr.infer(&QueryRouter::parse(&query).unwrap()).is_ok()); assert_eq!(qr.role(), None); } @@ -913,7 +955,7 @@ mod test { res.put(prepared_stmt); res.put_i16(0); - assert!(qr.infer(&res)); + assert!(qr.infer(&QueryRouter::parse(&res).unwrap()).is_ok()); assert_eq!(qr.role(), Some(Role::Replica)); } @@ -1077,11 +1119,11 @@ mod test { assert_eq!(qr.role(), None); let query = simple_query("INSERT INTO test_table VALUES (1)"); - assert!(qr.infer(&query)); + assert!(qr.infer(&QueryRouter::parse(&query).unwrap()).is_ok()); assert_eq!(qr.role(), Some(Role::Primary)); let query = simple_query("SELECT * FROM test_table"); - assert!(qr.infer(&query)); + assert!(qr.infer(&QueryRouter::parse(&query).unwrap()).is_ok()); assert_eq!(qr.role(), Some(Role::Replica)); assert!(qr.query_parser_enabled()); @@ -1113,6 +1155,8 @@ mod test { auth_query: None, auth_query_password: None, auth_query_user: None, + db: "test".to_string(), + plugins: None, }; let mut qr = QueryRouter::new(); assert_eq!(qr.active_role, None); @@ -1142,15 +1186,24 @@ mod test { QueryRouter::setup(); let mut qr = QueryRouter::new(); - assert!(qr.infer(&simple_query("BEGIN; SELECT 1; COMMIT;"))); + assert!(qr + .infer(&QueryRouter::parse(&simple_query("BEGIN; SELECT 1; COMMIT;")).unwrap()) + .is_ok()); assert_eq!(qr.role(), Role::Primary); - assert!(qr.infer(&simple_query("SELECT 1; SELECT 2;"))); + assert!(qr + .infer(&QueryRouter::parse(&simple_query("SELECT 1; SELECT 2;")).unwrap()) + .is_ok()); assert_eq!(qr.role(), Role::Replica); - assert!(qr.infer(&simple_query( - "SELECT 123; INSERT INTO t VALUES (5); SELECT 1;" - ))); + assert!(qr + .infer( + &QueryRouter::parse(&simple_query( + "SELECT 123; INSERT INTO t VALUES (5); SELECT 1;" + )) + .unwrap() + ) + .is_ok()); assert_eq!(qr.role(), Role::Primary); } @@ -1177,6 +1230,8 @@ mod test { auth_query: None, auth_query_password: None, auth_query_user: None, + db: "test".to_string(), + plugins: None, }; let mut qr = QueryRouter::new(); qr.update_pool_settings(pool_settings.clone()); @@ -1208,47 +1263,84 @@ mod test { qr.pool_settings.automatic_sharding_key = Some("data.id".to_string()); qr.pool_settings.shards = 3; - assert!(qr.infer(&simple_query("SELECT * FROM data WHERE id = 5"))); + assert!(qr + .infer(&QueryRouter::parse(&simple_query("SELECT * FROM data WHERE id = 5")).unwrap()) + .is_ok()); assert_eq!(qr.shard(), 2); - assert!(qr.infer(&simple_query( - "SELECT one, two, three FROM public.data WHERE id = 6" - ))); + assert!(qr + .infer( + &QueryRouter::parse(&simple_query( + "SELECT one, two, three FROM public.data WHERE id = 6" + )) + .unwrap() + ) + .is_ok()); assert_eq!(qr.shard(), 0); - assert!(qr.infer(&simple_query( - "SELECT * FROM data + assert!(qr + .infer( + &QueryRouter::parse(&simple_query( + "SELECT * FROM data INNER JOIN t2 ON data.id = 5 AND t2.data_id = data.id WHERE data.id = 5" - ))); + )) + .unwrap() + ) + .is_ok()); assert_eq!(qr.shard(), 2); // Shard did not move because we couldn't determine the sharding key since it could be ambiguous // in the query. - assert!(qr.infer(&simple_query( - "SELECT * FROM t2 INNER JOIN data ON id = 6 AND data.id = t2.data_id" - ))); + assert!(qr + .infer( + &QueryRouter::parse(&simple_query( + "SELECT * FROM t2 INNER JOIN data ON id = 6 AND data.id = t2.data_id" + )) + .unwrap() + ) + .is_ok()); assert_eq!(qr.shard(), 2); - assert!(qr.infer(&simple_query( - r#"SELECT * FROM "public"."data" WHERE "id" = 6"# - ))); + assert!(qr + .infer( + &QueryRouter::parse(&simple_query( + r#"SELECT * FROM "public"."data" WHERE "id" = 6"# + )) + .unwrap() + ) + .is_ok()); assert_eq!(qr.shard(), 0); - assert!(qr.infer(&simple_query( - r#"SELECT * FROM "public"."data" WHERE "data"."id" = 5"# - ))); + assert!(qr + .infer( + &QueryRouter::parse(&simple_query( + r#"SELECT * FROM "public"."data" WHERE "data"."id" = 5"# + )) + .unwrap() + ) + .is_ok()); assert_eq!(qr.shard(), 2); // Super unique sharding key qr.pool_settings.automatic_sharding_key = Some("*.unique_enough_column_name".to_string()); - assert!(qr.infer(&simple_query( - "SELECT * FROM table_x WHERE unique_enough_column_name = 6" - ))); + assert!(qr + .infer( + &QueryRouter::parse(&simple_query( + "SELECT * FROM table_x WHERE unique_enough_column_name = 6" + )) + .unwrap() + ) + .is_ok()); assert_eq!(qr.shard(), 0); - assert!(qr.infer(&simple_query("SELECT * FROM table_y WHERE another_key = 5"))); + assert!(qr + .infer( + &QueryRouter::parse(&simple_query("SELECT * FROM table_y WHERE another_key = 5")) + .unwrap() + ) + .is_ok()); assert_eq!(qr.shard(), 0); } @@ -1272,11 +1364,36 @@ mod test { qr.pool_settings.automatic_sharding_key = Some("data.id".to_string()); qr.pool_settings.shards = 3; - assert!(qr.infer(&simple_query(stmt))); + assert!(qr + .infer(&QueryRouter::parse(&simple_query(stmt)).unwrap()) + .is_ok()); assert_eq!(qr.placeholders.len(), 1); assert!(qr.infer_shard_from_bind(&bind)); assert_eq!(qr.shard(), 2); assert!(qr.placeholders.is_empty()); } + + #[tokio::test] + async fn test_table_access_plugin() { + QueryRouter::setup(); + + let mut qr = QueryRouter::new(); + + let mut pool_settings = PoolSettings::default(); + pool_settings.plugins = Some(vec![String::from("pg_table_access")]); + qr.update_pool_settings(pool_settings); + + let query = simple_query("SELECT * FROM pg_database"); + let ast = QueryRouter::parse(&query).unwrap(); + + let res = qr.execute_plugins(&ast).await; + + assert_eq!( + res, + Ok(PluginOutput::Deny( + "permission for table \"pg_database\" denied".to_string() + )) + ); + } } diff --git a/tests/ruby/helpers/pgcat_helper.rb b/tests/ruby/helpers/pgcat_helper.rb index ad4c32a4..eb0cdaa9 100644 --- a/tests/ruby/helpers/pgcat_helper.rb +++ b/tests/ruby/helpers/pgcat_helper.rb @@ -27,6 +27,7 @@ def self.three_shard_setup(pool_name, pool_size, pool_mode="transaction", lb_mod primary2 = PgInstance.new(8432, user["username"], user["password"], "shard2") pgcat_cfg = pgcat.current_config + pgcat_cfg["general"]["query_router_plugins"] = ["intercept"] pgcat_cfg["pools"] = { "#{pool_name}" => { "default_role" => "any", diff --git a/tests/ruby/plugins_spec.rb b/tests/ruby/plugins_spec.rb new file mode 100644 index 00000000..d4e233ab --- /dev/null +++ b/tests/ruby/plugins_spec.rb @@ -0,0 +1,14 @@ +require_relative 'spec_helper' + + +describe "Plugins" do + let(:processes) { Helpers::Pgcat.three_shard_setup("sharded_db", 5) } + + context "intercept" do + it "will intercept an intellij query" do + conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) + res = conn.exec("select current_database() as a, current_schemas(false) as b") + expect(res.values).to eq([["sharded_db", "{public}"]]) + end + end +end From d5e329fec54c1b9fef1fcc5b75c773ee5a7ec994 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 May 2023 10:00:05 -0700 Subject: [PATCH 36/79] chore(deps): bump regex from 1.8.0 to 1.8.1 (#413) Bumps [regex](https://github.com/rust-lang/regex) from 1.8.0 to 1.8.1. - [Release notes](https://github.com/rust-lang/regex/releases) - [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-lang/regex/commits/1.8.1) --- updated-dependencies: - dependency-name: regex dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6724a92c..6b2d76f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1090,9 +1090,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac6cf59af1067a3fb53fbe5c88c053764e930f932be1d71d3ffe032cbe147f59" +checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370" dependencies = [ "aho-corasick", "memchr", @@ -1101,9 +1101,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6868896879ba532248f33598de5181522d8b3d9d724dfd230911e1a7d4822f5" +checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" [[package]] name = "resolv-conf" From 811885f46480d563f9b23080b8b032692351cfc3 Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Wed, 3 May 2023 16:13:45 -0700 Subject: [PATCH 37/79] Actually plugins (#421) * more plugins * clean up * fix tests * fix flakey test --- Cargo.toml | 1 + pgcat.toml | 42 ++++++++++++++++++-- src/config.rs | 63 ++++++++++++++++++++++++++---- src/plugins/intercept.rs | 50 ++++++++++++++---------- src/plugins/mod.rs | 9 +++-- src/plugins/query_logger.rs | 49 +++++++++++++++++++++++ src/plugins/table_access.rs | 29 ++++++++++++-- src/pool.rs | 31 +++++++++++---- src/query_router.rs | 51 +++++++++++++----------- tests/ruby/admin_spec.rb | 8 ++-- tests/ruby/helpers/pgcat_helper.rb | 1 - 11 files changed, 264 insertions(+), 70 deletions(-) create mode 100644 src/plugins/query_logger.rs diff --git a/Cargo.toml b/Cargo.toml index 80549821..1b3c8b1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,3 +48,4 @@ serde_json = "1" [target.'cfg(not(target_env = "msvc"))'.dependencies] jemallocator = "0.5.0" + diff --git a/pgcat.toml b/pgcat.toml index dfb57822..ce366329 100644 --- a/pgcat.toml +++ b/pgcat.toml @@ -77,9 +77,6 @@ admin_username = "admin_user" # Password to access the virtual administrative database admin_password = "admin_pass" -# Plugins!! -# query_router_plugins = ["pg_table_access", "intercept"] - # pool configs are structured as pool. # the pool_name is what clients use as database name when connecting. # For a pool named `sharded_db`, clients access that pool using connection string like @@ -157,6 +154,45 @@ connect_timeout = 3000 # Specifies how often (in seconds) cached ip addresses for servers are rechecked (see `dns_cache_enabled`). # dns_max_ttl = 30 +[plugins] + +[plugins.query_logger] +enabled = false + +[plugins.table_access] +enabled = false +tables = [ + "pg_user", + "pg_roles", + "pg_database", +] + +[plugins.intercept] +enabled = true + +[plugins.intercept.queries.0] + +query = "select current_database() as a, current_schemas(false) as b" +schema = [ + ["a", "text"], + ["b", "text"], +] +result = [ + ["${DATABASE}", "{public}"], +] + +[plugins.intercept.queries.1] + +query = "select current_database(), current_schema(), current_user" +schema = [ + ["current_database", "text"], + ["current_schema", "text"], + ["current_user", "text"], +] +result = [ + ["${DATABASE}", "public", "${USER}"], +] + # User configs are structured as pool..users. # This section holds the credentials for users that may connect to this cluster [pools.sharded_db.users.0] diff --git a/src/config.rs b/src/config.rs index aa98421c..cdf891ee 100644 --- a/src/config.rs +++ b/src/config.rs @@ -302,8 +302,6 @@ pub struct General { pub auth_query: Option, pub auth_query_user: Option, pub auth_query_password: Option, - - pub query_router_plugins: Option>, } impl General { @@ -404,7 +402,6 @@ impl Default for General { auth_query_user: None, auth_query_password: None, server_lifetime: 1000 * 3600 * 24, // 24 hours, - query_router_plugins: None, } } } @@ -682,6 +679,55 @@ impl Default for Shard { } } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] +pub struct Plugins { + pub intercept: Option, + pub table_access: Option, + pub query_logger: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] +pub struct Intercept { + pub enabled: bool, + pub queries: BTreeMap, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] +pub struct TableAccess { + pub enabled: bool, + pub tables: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] +pub struct QueryLogger { + pub enabled: bool, +} + +impl Intercept { + pub fn substitute(&mut self, db: &str, user: &str) { + for (_, query) in self.queries.iter_mut() { + query.substitute(db, user); + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] +pub struct Query { + pub query: String, + pub schema: Vec>, + pub result: Vec>, +} + +impl Query { + pub fn substitute(&mut self, db: &str, user: &str) { + for col in self.result.iter_mut() { + for i in 0..col.len() { + col[i] = col[i].replace("${USER}", user).replace("${DATABASE}", db); + } + } + } +} + /// Configuration wrapper. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct Config { @@ -700,6 +746,7 @@ pub struct Config { pub path: String, pub general: General, + pub plugins: Option, pub pools: HashMap, } @@ -737,6 +784,7 @@ impl Default for Config { path: Self::default_path(), general: General::default(), pools: HashMap::default(), + plugins: None, } } } @@ -1128,6 +1176,7 @@ pub async fn parse(path: &str) -> Result<(), Error> { pub async fn reload_config(client_server_map: ClientServerMap) -> Result { let old_config = get_config(); + match parse(&old_config.path).await { Ok(()) => (), Err(err) => { @@ -1135,18 +1184,18 @@ pub async fn reload_config(client_server_map: ClientServerMap) -> Result (), Err(err) => error!("DNS cache reinitialization error: {:?}", err), }; - if old_config.pools != new_config.pools { - info!("Pool configuration changed"); + if old_config != new_config { + info!("Config changed, reloading"); ConnectionPool::from_config(client_server_map).await?; Ok(true) - } else if old_config != new_config { - Ok(true) } else { Ok(false) } diff --git a/src/plugins/intercept.rs b/src/plugins/intercept.rs index 6e250dca..88d24d0e 100644 --- a/src/plugins/intercept.rs +++ b/src/plugins/intercept.rs @@ -11,10 +11,11 @@ use serde_json::{json, Value}; use sqlparser::ast::Statement; use std::collections::HashMap; -use log::debug; +use log::{debug, info}; use std::sync::Arc; use crate::{ + config::Intercept as InterceptConfig, errors::Error, messages::{command_complete, data_row_nullable, row_description, DataType}, plugins::{Plugin, PluginOutput}, @@ -22,19 +23,29 @@ use crate::{ query_router::QueryRouter, }; -pub static CONFIG: Lazy>> = +pub static CONFIG: Lazy>> = Lazy::new(|| ArcSwap::from_pointee(HashMap::new())); -/// Configure the intercept plugin. -pub fn configure(pools: &PoolMap) { +/// Check if the interceptor plugin has been enabled. +pub fn enabled() -> bool { + !CONFIG.load().is_empty() +} + +pub fn setup(intercept_config: &InterceptConfig, pools: &PoolMap) { let mut config = HashMap::new(); for (identifier, _) in pools.iter() { - // TODO: make this configurable from a text config. - let value = fool_datagrip(&identifier.db, &identifier.user); - config.insert(identifier.clone(), value); + let mut intercept_config = intercept_config.clone(); + intercept_config.substitute(&identifier.db, &identifier.user); + config.insert(identifier.clone(), intercept_config); } CONFIG.store(Arc::new(config)); + + info!("Intercepting {} queries", intercept_config.queries.len()); +} + +pub fn disable() { + CONFIG.store(Arc::new(HashMap::new())); } // TODO: use these structs for deserialization @@ -78,19 +89,19 @@ impl Plugin for Intercept { // Normalization let q = q.to_string().to_ascii_lowercase(); - for target in query_map.as_array().unwrap().iter() { - if target["query"].as_str().unwrap() == q { - debug!("Query matched: {}", q); + for (_, target) in query_map.queries.iter() { + if target.query.as_str() == q { + debug!("Intercepting query: {}", q); - let rd = target["schema"] - .as_array() - .unwrap() + let rd = target + .schema .iter() .map(|row| { - let row = row.as_object().unwrap(); + let name = &row[0]; + let data_type = &row[1]; ( - row["name"].as_str().unwrap(), - match row["data_type"].as_str().unwrap() { + name.as_str(), + match data_type.as_str() { "text" => DataType::Text, "anyarray" => DataType::AnyArray, "oid" => DataType::Oid, @@ -104,13 +115,11 @@ impl Plugin for Intercept { result.put(row_description(&rd)); - target["result"].as_array().unwrap().iter().for_each(|row| { + target.result.iter().for_each(|row| { let row = row - .as_array() - .unwrap() .iter() .map(|s| { - let s = s.as_str().unwrap().to_string(); + let s = s.as_str().to_string(); if s == "" { None @@ -141,6 +150,7 @@ impl Plugin for Intercept { /// Make IntelliJ SQL plugin believe it's talking to an actual database /// instead of PgCat. +#[allow(dead_code)] fn fool_datagrip(database: &str, user: &str) -> Value { json!([ { diff --git a/src/plugins/mod.rs b/src/plugins/mod.rs index 92fa70b7..6661ece6 100644 --- a/src/plugins/mod.rs +++ b/src/plugins/mod.rs @@ -9,6 +9,7 @@ //! pub mod intercept; +pub mod query_logger; pub mod table_access; use crate::{errors::Error, query_router::QueryRouter}; @@ -17,6 +18,7 @@ use bytes::BytesMut; use sqlparser::ast::Statement; pub use intercept::Intercept; +pub use query_logger::QueryLogger; pub use table_access::TableAccess; #[derive(Clone, Debug, PartialEq)] @@ -29,12 +31,13 @@ pub enum PluginOutput { #[async_trait] pub trait Plugin { - // Custom output is allowed because we want to extend this system - // to rewriting queries some day. So an output of a plugin could be - // a rewritten AST. + // Run before the query is sent to the server. async fn run( &mut self, query_router: &QueryRouter, ast: &Vec, ) -> Result; + + // TODO: run after the result is returned + // async fn callback(&mut self, query_router: &QueryRouter); } diff --git a/src/plugins/query_logger.rs b/src/plugins/query_logger.rs new file mode 100644 index 00000000..2dfda8bc --- /dev/null +++ b/src/plugins/query_logger.rs @@ -0,0 +1,49 @@ +//! Log all queries to stdout (or somewhere else, why not). + +use crate::{ + errors::Error, + plugins::{Plugin, PluginOutput}, + query_router::QueryRouter, +}; +use arc_swap::ArcSwap; +use async_trait::async_trait; +use log::info; +use once_cell::sync::Lazy; +use sqlparser::ast::Statement; +use std::sync::Arc; + +static ENABLED: Lazy> = Lazy::new(|| ArcSwap::from_pointee(false)); + +pub struct QueryLogger; + +pub fn setup() { + ENABLED.store(Arc::new(true)); + + info!("Logging queries to stdout"); +} + +pub fn disable() { + ENABLED.store(Arc::new(false)); +} + +pub fn enabled() -> bool { + **ENABLED.load() +} + +#[async_trait] +impl Plugin for QueryLogger { + async fn run( + &mut self, + _query_router: &QueryRouter, + ast: &Vec, + ) -> Result { + let query = ast + .iter() + .map(|q| q.to_string()) + .collect::>() + .join("; "); + info!("{}", query); + + Ok(PluginOutput::Allow) + } +} diff --git a/src/plugins/table_access.rs b/src/plugins/table_access.rs index 2e23278a..4613a4fb 100644 --- a/src/plugins/table_access.rs +++ b/src/plugins/table_access.rs @@ -5,17 +5,37 @@ use async_trait::async_trait; use sqlparser::ast::{visit_relations, Statement}; use crate::{ + config::TableAccess as TableAccessConfig, errors::Error, plugins::{Plugin, PluginOutput}, query_router::QueryRouter, }; +use log::{debug, info}; + +use arc_swap::ArcSwap; use core::ops::ControlFlow; +use once_cell::sync::Lazy; +use std::sync::Arc; + +static CONFIG: Lazy>> = Lazy::new(|| ArcSwap::from_pointee(vec![])); + +pub fn setup(config: &TableAccessConfig) { + CONFIG.store(Arc::new(config.tables.clone())); + + info!("Blocking access to {} tables", config.tables.len()); +} -pub struct TableAccess { - pub forbidden_tables: Vec, +pub fn enabled() -> bool { + !CONFIG.load().is_empty() } +pub fn disable() { + CONFIG.store(Arc::new(vec![])); +} + +pub struct TableAccess; + #[async_trait] impl Plugin for TableAccess { async fn run( @@ -24,13 +44,14 @@ impl Plugin for TableAccess { ast: &Vec, ) -> Result { let mut found = None; + let forbidden_tables = CONFIG.load(); visit_relations(ast, |relation| { let relation = relation.to_string(); let parts = relation.split(".").collect::>(); let table_name = parts.last().unwrap(); - if self.forbidden_tables.contains(&table_name.to_string()) { + if forbidden_tables.contains(&table_name.to_string()) { found = Some(table_name.to_string()); ControlFlow::<()>::Break(()) } else { @@ -39,6 +60,8 @@ impl Plugin for TableAccess { }); if let Some(found) = found { + debug!("Blocking access to table \"{}\"", found); + Ok(PluginOutput::Deny(format!( "permission for table \"{}\" denied", found diff --git a/src/pool.rs b/src/pool.rs index b986548a..2fd380ce 100644 --- a/src/pool.rs +++ b/src/pool.rs @@ -132,8 +132,6 @@ pub struct PoolSettings { pub auth_query: Option, pub auth_query_user: Option, pub auth_query_password: Option, - - pub plugins: Option>, } impl Default for PoolSettings { @@ -158,7 +156,6 @@ impl Default for PoolSettings { auth_query: None, auth_query_user: None, auth_query_password: None, - plugins: None, } } } @@ -453,7 +450,6 @@ impl ConnectionPool { auth_query: pool_config.auth_query.clone(), auth_query_user: pool_config.auth_query_user.clone(), auth_query_password: pool_config.auth_query_password.clone(), - plugins: config.general.query_router_plugins.clone(), }, validated: Arc::new(AtomicBool::new(false)), paused: Arc::new(AtomicBool::new(false)), @@ -473,10 +469,29 @@ impl ConnectionPool { } } - // Initialize plugins here if required. - if let Some(plugins) = config.general.query_router_plugins { - if plugins.contains(&String::from("intercept")) { - crate::plugins::intercept::configure(&new_pools); + if let Some(ref plugins) = config.plugins { + if let Some(ref intercept) = plugins.intercept { + if intercept.enabled { + crate::plugins::intercept::setup(intercept, &new_pools); + } else { + crate::plugins::intercept::disable(); + } + } + + if let Some(ref table_access) = plugins.table_access { + if table_access.enabled { + crate::plugins::table_access::setup(table_access); + } else { + crate::plugins::table_access::disable(); + } + } + + if let Some(ref query_logger) = plugins.query_logger { + if query_logger.enabled { + crate::plugins::query_logger::setup(); + } else { + crate::plugins::query_logger::disable(); + } } } diff --git a/src/query_router.rs b/src/query_router.rs index 93bcd4f2..d995b804 100644 --- a/src/query_router.rs +++ b/src/query_router.rs @@ -15,7 +15,10 @@ use sqlparser::parser::Parser; use crate::config::Role; use crate::errors::Error; use crate::messages::BytesMutReader; -use crate::plugins::{Intercept, Plugin, PluginOutput, TableAccess}; +use crate::plugins::{ + intercept, query_logger, table_access, Intercept, Plugin, PluginOutput, QueryLogger, + TableAccess, +}; use crate::pool::PoolSettings; use crate::sharding::Sharder; @@ -790,24 +793,26 @@ impl QueryRouter { /// Add your plugins here and execute them. pub async fn execute_plugins(&self, ast: &Vec) -> Result { - if let Some(plugins) = &self.pool_settings.plugins { - if plugins.contains(&String::from("intercept")) { - let mut intercept = Intercept {}; - let result = intercept.run(&self, ast).await; + if query_logger::enabled() { + let mut query_logger = QueryLogger {}; + let _ = query_logger.run(&self, ast).await; + } - if let Ok(PluginOutput::Intercept(output)) = result { - return Ok(PluginOutput::Intercept(output)); - } + if intercept::enabled() { + let mut intercept = Intercept {}; + let result = intercept.run(&self, ast).await; + + if let Ok(PluginOutput::Intercept(output)) = result { + return Ok(PluginOutput::Intercept(output)); } + } - if plugins.contains(&String::from("pg_table_access")) { - let mut table_access = TableAccess { - forbidden_tables: vec![String::from("pg_database"), String::from("pg_roles")], - }; + if table_access::enabled() { + let mut table_access = TableAccess {}; + let result = table_access.run(&self, ast).await; - if let Ok(PluginOutput::Deny(error)) = table_access.run(&self, ast).await { - return Ok(PluginOutput::Deny(error)); - } + if let Ok(PluginOutput::Deny(error)) = result { + return Ok(PluginOutput::Deny(error)); } } @@ -1156,7 +1161,6 @@ mod test { auth_query_password: None, auth_query_user: None, db: "test".to_string(), - plugins: None, }; let mut qr = QueryRouter::new(); assert_eq!(qr.active_role, None); @@ -1231,7 +1235,6 @@ mod test { auth_query_password: None, auth_query_user: None, db: "test".to_string(), - plugins: None, }; let mut qr = QueryRouter::new(); qr.update_pool_settings(pool_settings.clone()); @@ -1376,13 +1379,17 @@ mod test { #[tokio::test] async fn test_table_access_plugin() { - QueryRouter::setup(); + use crate::config::TableAccess; + let ta = TableAccess { + enabled: true, + tables: vec![String::from("pg_database")], + }; - let mut qr = QueryRouter::new(); + crate::plugins::table_access::setup(&ta); - let mut pool_settings = PoolSettings::default(); - pool_settings.plugins = Some(vec![String::from("pg_table_access")]); - qr.update_pool_settings(pool_settings); + QueryRouter::setup(); + + let qr = QueryRouter::new(); let query = simple_query("SELECT * FROM pg_database"); let ast = QueryRouter::parse(&query).unwrap(); diff --git a/tests/ruby/admin_spec.rb b/tests/ruby/admin_spec.rb index fceb95bf..ea21630f 100644 --- a/tests/ruby/admin_spec.rb +++ b/tests/ruby/admin_spec.rb @@ -71,15 +71,17 @@ context "client connects but issues no queries" do it "only affects cl_idle stats" do + admin_conn = PG::connect(processes.pgcat.admin_connection_string) + + before_test = admin_conn.async_exec("SHOW POOLS")[0]["sv_idle"] connections = Array.new(20) { PG::connect(pgcat_conn_str) } sleep(1) - admin_conn = PG::connect(processes.pgcat.admin_connection_string) results = admin_conn.async_exec("SHOW POOLS")[0] %w[cl_active cl_waiting cl_cancel_req sv_active sv_used sv_tested sv_login maxwait].each do |s| raise StandardError, "Field #{s} was expected to be 0 but found to be #{results[s]}" if results[s] != "0" end expect(results["cl_idle"]).to eq("20") - expect(results["sv_idle"]).to eq("1") + expect(results["sv_idle"]).to eq(before_test) connections.map(&:close) sleep(1.1) @@ -87,7 +89,7 @@ %w[cl_active cl_idle cl_waiting cl_cancel_req sv_active sv_used sv_tested sv_login maxwait].each do |s| raise StandardError, "Field #{s} was expected to be 0 but found to be #{results[s]}" if results[s] != "0" end - expect(results["sv_idle"]).to eq("1") + expect(results["sv_idle"]).to eq(before_test) end end diff --git a/tests/ruby/helpers/pgcat_helper.rb b/tests/ruby/helpers/pgcat_helper.rb index eb0cdaa9..ad4c32a4 100644 --- a/tests/ruby/helpers/pgcat_helper.rb +++ b/tests/ruby/helpers/pgcat_helper.rb @@ -27,7 +27,6 @@ def self.three_shard_setup(pool_name, pool_size, pool_mode="transaction", lb_mod primary2 = PgInstance.new(8432, user["username"], user["password"], "shard2") pgcat_cfg = pgcat.current_config - pgcat_cfg["general"]["query_router_plugins"] = ["intercept"] pgcat_cfg["pools"] = { "#{pool_name}" => { "default_role" => "any", From 128ef72911cf4d3c35e22ad4d0cc9e7a0749c801 Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Wed, 3 May 2023 16:47:20 -0700 Subject: [PATCH 38/79] lowercase config query (#422) * lowercase config query * remove debug --- src/config.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/config.rs b/src/config.rs index cdf891ee..56129407 100644 --- a/src/config.rs +++ b/src/config.rs @@ -707,6 +707,7 @@ impl Intercept { pub fn substitute(&mut self, db: &str, user: &str) { for (_, query) in self.queries.iter_mut() { query.substitute(db, user); + query.query = query.query.to_ascii_lowercase(); } } } From ba5243b6dd607cb3f36c7395b70e8e3df783d06c Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Wed, 3 May 2023 17:07:23 -0700 Subject: [PATCH 39/79] Optionally validate config on boot (#423) --- src/config.rs | 8 ++++++++ src/pool.rs | 10 ++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/config.rs b/src/config.rs index 56129407..c7fce635 100644 --- a/src/config.rs +++ b/src/config.rs @@ -298,6 +298,9 @@ pub struct General { pub admin_username: String, pub admin_password: String, + #[serde(default = "General::default_validate_config")] + pub validate_config: bool, + // Support for auth query pub auth_query: Option, pub auth_query_user: Option, @@ -367,6 +370,10 @@ impl General { pub fn default_idle_client_in_transaction_timeout() -> u64 { 0 } + + pub fn default_validate_config() -> bool { + true + } } impl Default for General { @@ -402,6 +409,7 @@ impl Default for General { auth_query_user: None, auth_query_password: None, server_lifetime: 1000 * 3600 * 24, // 24 hours, + validate_config: true, } } } diff --git a/src/pool.rs b/src/pool.rs index 2fd380ce..27ff8616 100644 --- a/src/pool.rs +++ b/src/pool.rs @@ -459,10 +459,12 @@ impl ConnectionPool { // Connect to the servers to make sure pool configuration is valid // before setting it globally. // Do this async and somewhere else, we don't have to wait here. - let mut validate_pool = pool.clone(); - tokio::task::spawn(async move { - let _ = validate_pool.validate().await; - }); + if config.general.validate_config { + let mut validate_pool = pool.clone(); + tokio::task::spawn(async move { + let _ = validate_pool.validate().await; + }); + } // There is one pool per database/user pair. new_pools.insert(PoolIdentifier::new(pool_name, &user.username), pool); From 389993bf3edca36af79fa742f8fb03c36f38c445 Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Fri, 5 May 2023 08:27:19 -0700 Subject: [PATCH 40/79] Accurate log messages (#425) --- src/pool.rs | 14 ++++++++++---- src/server.rs | 22 ++++++++++++++++------ 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/pool.rs b/src/pool.rs index 27ff8616..4664193c 100644 --- a/src/pool.rs +++ b/src/pool.rs @@ -641,7 +641,10 @@ impl ConnectionPool { { Ok(conn) => conn, Err(err) => { - error!("Banning instance {:?}, error: {:?}", address, err); + error!( + "Connection checkout error for instance {:?}, error: {:?}", + address, err + ); self.ban(address, BanReason::FailedCheckout, Some(client_stats)); address.stats.error(); client_stats.idle(); @@ -717,7 +720,7 @@ impl ConnectionPool { // Health check failed. Err(err) => { error!( - "Banning instance {:?} because of failed health check, {:?}", + "Failed health check on instance {:?}, error: {:?}", address, err ); } @@ -726,7 +729,7 @@ impl ConnectionPool { // Health check timed out. Err(err) => { error!( - "Banning instance {:?} because of health check timeout, {:?}", + "Health check timeout on instance {:?}, error: {:?}", address, err ); } @@ -748,13 +751,16 @@ impl ConnectionPool { return; } + error!("Banning instance {:?}, reason: {:?}", address, reason); + let now = chrono::offset::Utc::now().naive_utc(); let mut guard = self.banlist.write(); - error!("Banning {:?}", address); + if let Some(client_info) = client_info { client_info.ban_error(); address.stats.error(); } + guard[address.shard].insert(address.clone(), (reason, now)); } diff --git a/src/server.rs b/src/server.rs index ff5ab20b..152f0524 100644 --- a/src/server.rs +++ b/src/server.rs @@ -705,7 +705,10 @@ impl Server { Ok(()) } Err(err) => { - error!("Terminating server because of: {:?}", err); + error!( + "Terminating server {:?} because of: {:?}", + self.address, err + ); self.bad = true; Err(err) } @@ -720,7 +723,10 @@ impl Server { let mut message = match read_message(&mut self.stream).await { Ok(message) => message, Err(err) => { - error!("Terminating server because of: {:?}", err); + error!( + "Terminating server {:?} because of: {:?}", + self.address, err + ); self.bad = true; return Err(err); } @@ -1135,14 +1141,18 @@ impl Drop for Server { _ => debug!("Dirty shutdown"), }; - // Should not matter. - self.bad = true; - let now = chrono::offset::Utc::now().naive_utc(); let duration = now - self.connected_at; + let message = if self.bad { + "Server connection terminated" + } else { + "Server connection closed" + }; + info!( - "Server connection closed {:?}, session duration: {}", + "{} {:?}, session duration: {}", + message, self.address, crate::format_duration(&duration) ); From 159eb89bf0baccdf3475bbc81cab373d87019191 Mon Sep 17 00:00:00 2001 From: Andrew Tanner Date: Fri, 5 May 2023 18:31:27 -0400 Subject: [PATCH 41/79] First try with role reset (#427) * First try with role rest * update * extra line * Update src/server.rs Co-authored-by: Lev Kokotov * Update tests/ruby/misc_spec.rb Co-authored-by: Lev Kokotov --------- Co-authored-by: Lev Kokotov --- src/server.rs | 1 + tests/ruby/misc_spec.rb | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/src/server.rs b/src/server.rs index 152f0524..8f0ece99 100644 --- a/src/server.rs +++ b/src/server.rs @@ -963,6 +963,7 @@ impl Server { if self.needs_cleanup { warn!("Server returned with session state altered, discarding state"); self.query("DISCARD ALL").await?; + self.query("RESET ROLE").await?; self.needs_cleanup = false; } diff --git a/tests/ruby/misc_spec.rb b/tests/ruby/misc_spec.rb index 1a04c5d6..e4d6f6fb 100644 --- a/tests/ruby/misc_spec.rb +++ b/tests/ruby/misc_spec.rb @@ -241,6 +241,18 @@ expect(processes.primary.count_query("DISCARD ALL")).to eq(10) end + + it "Resets server roles correctly" do + 10.times do + conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) + conn.async_exec("SET SERVER ROLE to 'primary'") + conn.async_exec("SELECT 1") + conn.async_exec("SET statement_timeout to 5000") + conn.close + end + + expect(processes.primary.count_query("RESET ROLE")).to eq(10) + end end context "transaction mode" do From 571b02e178ab2fc98dd8cd89f65284f153fe6288 Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Mon, 8 May 2023 10:06:16 -0700 Subject: [PATCH 42/79] Calculate averages correctly and preserve totals like before (#429) * Reset totals after avg calculation * like it used to be --- Cargo.lock | 16 ++++++++++++++ Cargo.toml | 1 + src/stats/address.rs | 50 ++++++++++++++++++++++++++++++++------------ 3 files changed, 54 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6b2d76f2..9c553d45 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -250,6 +250,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" + [[package]] name = "enum-as-inner" version = "0.5.1" @@ -658,6 +664,15 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.5" @@ -897,6 +912,7 @@ dependencies = [ "futures", "hmac", "hyper", + "itertools", "jemallocator", "log", "md-5", diff --git a/Cargo.toml b/Cargo.toml index 1b3c8b1e..5428ab31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,7 @@ rustls = { version = "0.21", features = ["dangerous_configuration"] } trust-dns-resolver = "0.22.0" tokio-test = "0.4.2" serde_json = "1" +itertools = "0.10" [target.'cfg(not(target_env = "msvc"))'.dependencies] jemallocator = "0.5.0" diff --git a/src/stats/address.rs b/src/stats/address.rs index a5759e10..51d6a688 100644 --- a/src/stats/address.rs +++ b/src/stats/address.rs @@ -1,4 +1,3 @@ -use log::warn; use std::sync::atomic::*; use std::sync::Arc; @@ -13,6 +12,16 @@ pub struct AddressStats { pub total_query_time: Arc, pub total_wait_time: Arc, pub total_errors: Arc, + + pub old_total_xact_count: Arc, + pub old_total_query_count: Arc, + pub old_total_received: Arc, + pub old_total_sent: Arc, + pub old_total_xact_time: Arc, + pub old_total_query_time: Arc, + pub old_total_wait_time: Arc, + pub old_total_errors: Arc, + pub avg_query_count: Arc, pub avg_query_time: Arc, pub avg_recv: Arc, @@ -104,16 +113,16 @@ impl AddressStats { } pub fn update_averages(&self) { - let (totals, averages) = self.fields_iterators(); - for data in totals.iter().zip(averages.iter()) { - let (total, average) = data; - if let Err(err) = average.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |avg| { - let total = total.load(Ordering::Relaxed); - let avg = (total - avg) / (crate::stats::STAT_PERIOD / 1_000); // Avg / second - Some(avg) - }) { - warn!("Could not update averages for addresses stats, {:?}", err); - } + let (totals, averages, old_totals) = self.fields_iterators(); + for data in itertools::izip!(totals, averages, old_totals) { + let (total, average, old_total) = data; + let total = total.load(Ordering::Relaxed); + let old = old_total.load(Ordering::Relaxed); + average.store( + (total - old) / (crate::stats::STAT_PERIOD / 1_000), + Ordering::Relaxed, + ); // Avg / second + old_total.store(total, Ordering::Relaxed); } } @@ -123,27 +132,42 @@ impl AddressStats { } } - fn fields_iterators(&self) -> (Vec>, Vec>) { + fn fields_iterators( + &self, + ) -> ( + Vec>, + Vec>, + Vec>, + ) { let mut totals: Vec> = Vec::new(); let mut averages: Vec> = Vec::new(); + let mut old_totals: Vec> = Vec::new(); totals.push(self.total_xact_count.clone()); + old_totals.push(self.old_total_xact_count.clone()); averages.push(self.avg_xact_count.clone()); totals.push(self.total_query_count.clone()); + old_totals.push(self.old_total_query_count.clone()); averages.push(self.avg_query_count.clone()); totals.push(self.total_received.clone()); + old_totals.push(self.old_total_received.clone()); averages.push(self.avg_recv.clone()); totals.push(self.total_sent.clone()); + old_totals.push(self.old_total_sent.clone()); averages.push(self.avg_sent.clone()); totals.push(self.total_xact_time.clone()); + old_totals.push(self.old_total_xact_time.clone()); averages.push(self.avg_xact_time.clone()); totals.push(self.total_query_time.clone()); + old_totals.push(self.old_total_query_time.clone()); averages.push(self.avg_query_time.clone()); totals.push(self.total_wait_time.clone()); + old_totals.push(self.old_total_wait_time.clone()); averages.push(self.avg_wait_time.clone()); totals.push(self.total_errors.clone()); + old_totals.push(self.old_total_errors.clone()); averages.push(self.avg_errors.clone()); - (totals, averages) + (totals, averages, old_totals) } } From 5056cbe8ed5b19f9b0d7fc3f333a44c04810ab70 Mon Sep 17 00:00:00 2001 From: Mostafa Abdelraouf Date: Wed, 10 May 2023 10:24:35 -0500 Subject: [PATCH 43/79] Fix docker-compose dev stack for Apple silicon (#432) The docker-compose dev setup is broken under Apple silicon, starting the stack fails with the following error. Switching to a different docker image fixes the issue. --- dev/docker-compose.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dev/docker-compose.yaml b/dev/docker-compose.yaml index 71704bcb..5fda0c13 100644 --- a/dev/docker-compose.yaml +++ b/dev/docker-compose.yaml @@ -25,7 +25,7 @@ x-common-env-pg: services: main: - image: kubernetes/pause + image: gcr.io/google_containers/pause:3.2 ports: - 6432 @@ -64,7 +64,7 @@ services: <<: *common-env-pg POSTGRES_INITDB_ARGS: --auth-local=md5 --auth-host=md5 --auth=md5 PGPORT: 10432 - command: ["postgres", "-p", "5432", "-c", "shared_preload_libraries=pg_stat_statements", "-c", "pg_stat_statements.track=all", "-c", "pg_stat_statements.max=100000"] + command: ["postgres", "-p", "10432", "-c", "shared_preload_libraries=pg_stat_statements", "-c", "pg_stat_statements.track=all", "-c", "pg_stat_statements.max=100000"] toxiproxy: build: . From 73260690b0011b48912b6a194f33d936ce4cacd6 Mon Sep 17 00:00:00 2001 From: Zain Kabani <77307340+zainkabani@users.noreply.github.com> Date: Thu, 11 May 2023 20:37:58 -0400 Subject: [PATCH 44/79] Fixes average stats bug (#436) * Add test * Fix test * Add fix --- src/stats.rs | 15 +++++++++++++-- src/stats/address.rs | 14 ++++++++------ src/stats/server.rs | 11 +++++++++++ tests/ruby/admin_spec.rb | 13 +++++++------ 4 files changed, 39 insertions(+), 14 deletions(-) diff --git a/src/stats.rs b/src/stats.rs index b95a143a..6de784f5 100644 --- a/src/stats.rs +++ b/src/stats.rs @@ -107,8 +107,19 @@ impl Collector { loop { interval.tick().await; - for stats in SERVER_STATS.read().values() { - stats.address_stats().update_averages(); + // Hold read lock for duration of update to retain all server stats + let server_stats = SERVER_STATS.read(); + + for stats in server_stats.values() { + if !stats.check_address_stat_average_is_updated_status() { + stats.address_stats().update_averages(); + stats.set_address_stat_average_is_updated_status(true); + } + } + + // Reset to false for next update + for stats in server_stats.values() { + stats.set_address_stat_average_is_updated_status(false); } } }); diff --git a/src/stats/address.rs b/src/stats/address.rs index 51d6a688..89e4ebe7 100644 --- a/src/stats/address.rs +++ b/src/stats/address.rs @@ -30,6 +30,9 @@ pub struct AddressStats { pub avg_xact_time: Arc, pub avg_xact_count: Arc, pub avg_wait_time: Arc, + + // Determines if the averages have been updated since the last time they were reported + pub averages_updated: Arc, } impl IntoIterator for AddressStats { @@ -114,15 +117,14 @@ impl AddressStats { pub fn update_averages(&self) { let (totals, averages, old_totals) = self.fields_iterators(); - for data in itertools::izip!(totals, averages, old_totals) { - let (total, average, old_total) = data; - let total = total.load(Ordering::Relaxed); - let old = old_total.load(Ordering::Relaxed); + for (total, average, old_total) in itertools::izip!(totals, averages, old_totals) { + let total_value = total.load(Ordering::Relaxed); + let old_total_value = old_total.load(Ordering::Relaxed); average.store( - (total - old) / (crate::stats::STAT_PERIOD / 1_000), + (total_value - old_total_value) / (crate::stats::STAT_PERIOD / 1_000), Ordering::Relaxed, ); // Avg / second - old_total.store(total, Ordering::Relaxed); + old_total.store(total_value, Ordering::Relaxed); } } diff --git a/src/stats/server.rs b/src/stats/server.rs index d25f3b4e..399e585f 100644 --- a/src/stats/server.rs +++ b/src/stats/server.rs @@ -139,6 +139,17 @@ impl ServerStats { self.address.stats.clone() } + pub fn check_address_stat_average_is_updated_status(&self) -> bool { + self.address.stats.averages_updated.load(Ordering::Relaxed) + } + + pub fn set_address_stat_average_is_updated_status(&self, is_checked: bool) { + self.address + .stats + .averages_updated + .store(is_checked, Ordering::Relaxed); + } + // Helper methods for show_servers pub fn pool_name(&self) -> String { self.pool_stats.database() diff --git a/tests/ruby/admin_spec.rb b/tests/ruby/admin_spec.rb index ea21630f..e054b45e 100644 --- a/tests/ruby/admin_spec.rb +++ b/tests/ruby/admin_spec.rb @@ -14,11 +14,12 @@ describe "SHOW STATS" do context "clients connect and make one query" do it "updates *_query_time and *_wait_time" do - connection = PG::connect("#{pgcat_conn_str}?application_name=one_query") - connection.async_exec("SELECT pg_sleep(0.25)") - connection.async_exec("SELECT pg_sleep(0.25)") - connection.async_exec("SELECT pg_sleep(0.25)") - connection.close + connections = Array.new(3) { PG::connect("#{pgcat_conn_str}?application_name=one_query") } + connections.each do |c| + Thread.new { c.async_exec("SELECT pg_sleep(0.25)") } + end + sleep(1) + connections.map(&:close) # wait for averages to be calculated, we shouldn't do this too often sleep(15.5) @@ -26,7 +27,7 @@ results = admin_conn.async_exec("SHOW STATS")[0] admin_conn.close expect(results["total_query_time"].to_i).to be_within(200).of(750) - expect(results["avg_query_time"].to_i).to_not eq(0) + expect(results["avg_query_time"].to_i).to be_within(20).of(50) expect(results["total_wait_time"].to_i).to_not eq(0) expect(results["avg_wait_time"].to_i).to_not eq(0) From 0907f1b77f5a6a224fa4c902fb6dfb42c42911d8 Mon Sep 17 00:00:00 2001 From: Zain Kabani <77307340+zainkabani@users.noreply.github.com> Date: Thu, 11 May 2023 20:40:10 -0400 Subject: [PATCH 45/79] Improve logging for connection cleanup (#428) * initial commit * fix * fmt --- src/server.rs | 64 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 53 insertions(+), 11 deletions(-) diff --git a/src/server.rs b/src/server.rs index 8f0ece99..dceab49d 100644 --- a/src/server.rs +++ b/src/server.rs @@ -103,6 +103,48 @@ impl StreamInner { } } +#[derive(Copy, Clone)] +struct CleanupState { + /// If server connection requires DISCARD ALL before checkin because of set statement + needs_cleanup_set: bool, + + /// If server connection requires DISCARD ALL before checkin because of prepare statement + needs_cleanup_prepare: bool, +} + +impl CleanupState { + fn new() -> Self { + CleanupState { + needs_cleanup_set: false, + needs_cleanup_prepare: false, + } + } + + fn needs_cleanup(&self) -> bool { + self.needs_cleanup_set || self.needs_cleanup_prepare + } + + fn set_true(&mut self) { + self.needs_cleanup_set = true; + self.needs_cleanup_prepare = true; + } + + fn reset(&mut self) { + self.needs_cleanup_set = false; + self.needs_cleanup_prepare = false; + } +} + +impl std::fmt::Display for CleanupState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "SET: {}, PREPARE: {}", + self.needs_cleanup_set, self.needs_cleanup_prepare + ) + } +} + /// Server state. pub struct Server { /// Server host, e.g. localhost, @@ -131,8 +173,8 @@ pub struct Server { /// Is the server broken? We'll remote it from the pool if so. bad: bool, - /// If server connection requires a DISCARD ALL before checkin - needs_cleanup: bool, + /// If server connection requires DISCARD ALL before checkin + cleanup_state: CleanupState, /// Mapping of clients and servers used for query cancellation. client_server_map: ClientServerMap, @@ -630,7 +672,7 @@ impl Server { in_transaction: false, data_available: false, bad: false, - needs_cleanup: false, + cleanup_state: CleanupState::new(), client_server_map, addr_set, connected_at: chrono::offset::Utc::now().naive_utc(), @@ -793,12 +835,12 @@ impl Server { // This will reduce amount of discard statements sent if !self.in_transaction { debug!("Server connection marked for clean up"); - self.needs_cleanup = true; + self.cleanup_state.needs_cleanup_set = true; } } "PREPARE\0" => { debug!("Server connection marked for clean up"); - self.needs_cleanup = true; + self.cleanup_state.needs_cleanup_prepare = true; } _ => (), } @@ -960,11 +1002,11 @@ impl Server { // to avoid leaking state between clients. For performance reasons we only // send `DISCARD ALL` if we think the session is altered instead of just sending // it before each checkin. - if self.needs_cleanup { - warn!("Server returned with session state altered, discarding state"); + if self.cleanup_state.needs_cleanup() { + warn!("Server returned with session state altered, discarding state ({}) for application {}", self.cleanup_state, self.application_name); self.query("DISCARD ALL").await?; self.query("RESET ROLE").await?; - self.needs_cleanup = false; + self.cleanup_state.reset(); } Ok(()) @@ -976,12 +1018,12 @@ impl Server { self.application_name = name.to_string(); // We don't want `SET application_name` to mark the server connection // as needing cleanup - let needs_cleanup_before = self.needs_cleanup; + let needs_cleanup_before = self.cleanup_state; let result = Ok(self .query(&format!("SET application_name = '{}'", name)) .await?); - self.needs_cleanup = needs_cleanup_before; + self.cleanup_state = needs_cleanup_before; result } else { Ok(()) @@ -1006,7 +1048,7 @@ impl Server { // Marks a connection as needing DISCARD ALL at checkin pub fn mark_dirty(&mut self) { - self.needs_cleanup = true; + self.cleanup_state.set_true(); } pub fn mirror_send(&mut self, bytes: &BytesMut) { From 52b1b438505198f84e1d56fa3f4767dddb77f67d Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Fri, 12 May 2023 09:50:52 -0700 Subject: [PATCH 46/79] Prewarmer (#435) * Prewarmer * hmm * Tests * default * fix test * Correct configuration * Added minimal config example * remove connect_timeout --- Cargo.lock | 2 +- Cargo.toml | 2 +- pgcat.minimal.toml | 22 ++++ pgcat.toml | 72 ++++++++++- src/config.rs | 78 +++++++++++- src/mirrors.rs | 1 + src/plugins/intercept.rs | 194 ++--------------------------- src/plugins/mod.rs | 1 + src/plugins/prewarmer.rs | 28 +++++ src/plugins/query_logger.rs | 31 ++--- src/plugins/table_access.rs | 34 ++--- src/pool.rs | 66 +++++----- src/query_router.rs | 69 +++++++--- src/server.rs | 2 + tests/ruby/helpers/pgcat_helper.rb | 19 ++- 15 files changed, 337 insertions(+), 284 deletions(-) create mode 100644 pgcat.minimal.toml create mode 100644 src/plugins/prewarmer.rs diff --git a/Cargo.lock b/Cargo.lock index 9c553d45..4703a0b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -897,7 +897,7 @@ checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" [[package]] name = "pgcat" -version = "1.0.2-alpha1" +version = "1.0.2-alpha2" dependencies = [ "arc-swap", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 5428ab31..af1b9ae0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pgcat" -version = "1.0.2-alpha1" +version = "1.0.2-alpha2" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/pgcat.minimal.toml b/pgcat.minimal.toml new file mode 100644 index 00000000..4b17a454 --- /dev/null +++ b/pgcat.minimal.toml @@ -0,0 +1,22 @@ +# This is an example of the most basic config +# that will mimic what PgBouncer does in transaction mode with one server. + +[general] + +host = "0.0.0.0" +port = 6433 +admin_username = "pgcat" +admin_password = "pgcat" + +[pools.pgml.users.0] +username = "postgres" +password = "postgres" +pool_size = 10 +min_pool_size = 1 +pool_mode = "transaction" + +[pools.pgml.shards.0] +servers = [ + ["127.0.0.1", 28815, "primary"] +] +database = "postgres" diff --git a/pgcat.toml b/pgcat.toml index ce366329..e6b54b25 100644 --- a/pgcat.toml +++ b/pgcat.toml @@ -77,6 +77,58 @@ admin_username = "admin_user" # Password to access the virtual administrative database admin_password = "admin_pass" +# Default plugins that are configured on all pools. +[plugins] + +# Prewarmer plugin that runs queries on server startup, before giving the connection +# to the client. +[plugins.prewarmer] +enabled = false +queries = [ + "SELECT pg_prewarm('pgbench_accounts')", +] + +# Log all queries to stdout. +[plugins.query_logger] +enabled = false + +# Block access to tables that Postgres does not allow us to control. +[plugins.table_access] +enabled = false +tables = [ + "pg_user", + "pg_roles", + "pg_database", +] + +# Intercept user queries and give a fake reply. +[plugins.intercept] +enabled = true + +[plugins.intercept.queries.0] + +query = "select current_database() as a, current_schemas(false) as b" +schema = [ + ["a", "text"], + ["b", "text"], +] +result = [ + ["${DATABASE}", "{public}"], +] + +[plugins.intercept.queries.1] + +query = "select current_database(), current_schema(), current_user" +schema = [ + ["current_database", "text"], + ["current_schema", "text"], + ["current_user", "text"], +] +result = [ + ["${DATABASE}", "public", "${USER}"], +] + + # pool configs are structured as pool. # the pool_name is what clients use as database name when connecting. # For a pool named `sharded_db`, clients access that pool using connection string like @@ -154,12 +206,20 @@ connect_timeout = 3000 # Specifies how often (in seconds) cached ip addresses for servers are rechecked (see `dns_cache_enabled`). # dns_max_ttl = 30 -[plugins] +# Plugins can be configured on a pool-per-pool basis. This overrides the global plugins setting, +# so all plugins have to be configured here again. +[pool.sharded_db.plugins] -[plugins.query_logger] +[pools.sharded_db.plugins.prewarmer] +enabled = true +queries = [ + "SELECT pg_prewarm('pgbench_accounts')", +] + +[pools.sharded_db.plugins.query_logger] enabled = false -[plugins.table_access] +[pools.sharded_db.plugins.table_access] enabled = false tables = [ "pg_user", @@ -167,10 +227,10 @@ tables = [ "pg_database", ] -[plugins.intercept] +[pools.sharded_db.plugins.intercept] enabled = true -[plugins.intercept.queries.0] +[pools.sharded_db.plugins.intercept.queries.0] query = "select current_database() as a, current_schemas(false) as b" schema = [ @@ -181,7 +241,7 @@ result = [ ["${DATABASE}", "{public}"], ] -[plugins.intercept.queries.1] +[pools.sharded_db.plugins.intercept.queries.1] query = "select current_database(), current_schema(), current_user" schema = [ diff --git a/src/config.rs b/src/config.rs index c7fce635..f4177730 100644 --- a/src/config.rs +++ b/src/config.rs @@ -122,6 +122,16 @@ impl Default for Address { } } +impl std::fmt::Display for Address { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "[address: {}:{}][database: {}][user: {}]", + self.host, self.port, self.database, self.username + ) + } +} + // We need to implement PartialEq by ourselves so we skip stats in the comparison impl PartialEq for Address { fn eq(&self, other: &Self) -> bool { @@ -235,6 +245,8 @@ pub struct General { pub port: u16, pub enable_prometheus_exporter: Option, + + #[serde(default = "General::default_prometheus_exporter_port")] pub prometheus_exporter_port: i16, #[serde(default = "General::default_connect_timeout")] @@ -374,6 +386,10 @@ impl General { pub fn default_validate_config() -> bool { true } + + pub fn default_prometheus_exporter_port() -> i16 { + 9930 + } } impl Default for General { @@ -462,6 +478,7 @@ pub struct Pool { #[serde(default = "Pool::default_load_balancing_mode")] pub load_balancing_mode: LoadBalancingMode, + #[serde(default = "Pool::default_default_role")] pub default_role: String, #[serde(default)] // False @@ -476,6 +493,7 @@ pub struct Pool { pub server_lifetime: Option, + #[serde(default = "Pool::default_sharding_function")] pub sharding_function: ShardingFunction, #[serde(default = "Pool::default_automatic_sharding_key")] @@ -489,6 +507,7 @@ pub struct Pool { pub auth_query_user: Option, pub auth_query_password: Option, + pub plugins: Option, pub shards: BTreeMap, pub users: BTreeMap, // Note, don't put simple fields below these configs. There's a compatibility issue with TOML that makes it @@ -521,6 +540,14 @@ impl Pool { None } + pub fn default_default_role() -> String { + "any".into() + } + + pub fn default_sharding_function() -> ShardingFunction { + ShardingFunction::PgBigintHash + } + pub fn validate(&mut self) -> Result<(), Error> { match self.default_role.as_ref() { "any" => (), @@ -609,6 +636,7 @@ impl Default for Pool { auth_query_user: None, auth_query_password: None, server_lifetime: None, + plugins: None, } } } @@ -687,30 +715,50 @@ impl Default for Shard { } } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, Hash, Eq)] pub struct Plugins { pub intercept: Option, pub table_access: Option, pub query_logger: Option, + pub prewarmer: Option, } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] +impl std::fmt::Display for Plugins { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "interceptor: {}, table_access: {}, query_logger: {}, prewarmer: {}", + self.intercept.is_some(), + self.table_access.is_some(), + self.query_logger.is_some(), + self.prewarmer.is_some(), + ) + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, Hash, Eq)] pub struct Intercept { pub enabled: bool, pub queries: BTreeMap, } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, Hash, Eq)] pub struct TableAccess { pub enabled: bool, pub tables: Vec, } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, Hash, Eq)] pub struct QueryLogger { pub enabled: bool, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, Hash, Eq)] +pub struct Prewarmer { + pub enabled: bool, + pub queries: Vec, +} + impl Intercept { pub fn substitute(&mut self, db: &str, user: &str) { for (_, query) in self.queries.iter_mut() { @@ -720,7 +768,7 @@ impl Intercept { } } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, Hash, Eq)] pub struct Query { pub query: String, pub schema: Vec>, @@ -754,8 +802,13 @@ pub struct Config { #[serde(default = "Config::default_path")] pub path: String, + // General and global settings. pub general: General, + + // Plugins that should run in all pools. pub plugins: Option, + + // Connection pools. pub pools: HashMap, } @@ -940,6 +993,13 @@ impl Config { "Server TLS certificate verification: {}", self.general.verify_server_certificate ); + info!( + "Plugins: {}", + match self.plugins { + Some(ref plugins) => plugins.to_string(), + None => "not configured".into(), + } + ); for (pool_name, pool_config) in &self.pools { // TODO: Make this output prettier (maybe a table?) @@ -1006,6 +1066,14 @@ impl Config { None => "default".to_string(), } ); + info!( + "[pool: {}] Plugins: {}", + pool_name, + match pool_config.plugins { + Some(ref plugins) => plugins.to_string(), + None => "not configured".into(), + } + ); for user in &pool_config.users { info!( diff --git a/src/mirrors.rs b/src/mirrors.rs index 17f91d4d..d6d691ff 100644 --- a/src/mirrors.rs +++ b/src/mirrors.rs @@ -43,6 +43,7 @@ impl MirroredClient { ClientServerMap::default(), Arc::new(PoolStats::new(identifier, cfg.clone())), Arc::new(RwLock::new(None)), + None, ); Pool::builder() diff --git a/src/plugins/intercept.rs b/src/plugins/intercept.rs index 88d24d0e..166294bc 100644 --- a/src/plugins/intercept.rs +++ b/src/plugins/intercept.rs @@ -2,52 +2,21 @@ //! //! It intercepts queries and returns fake results. -use arc_swap::ArcSwap; use async_trait::async_trait; use bytes::{BufMut, BytesMut}; -use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; -use serde_json::{json, Value}; use sqlparser::ast::Statement; -use std::collections::HashMap; -use log::{debug, info}; -use std::sync::Arc; +use log::debug; use crate::{ config::Intercept as InterceptConfig, errors::Error, messages::{command_complete, data_row_nullable, row_description, DataType}, plugins::{Plugin, PluginOutput}, - pool::{PoolIdentifier, PoolMap}, query_router::QueryRouter, }; -pub static CONFIG: Lazy>> = - Lazy::new(|| ArcSwap::from_pointee(HashMap::new())); - -/// Check if the interceptor plugin has been enabled. -pub fn enabled() -> bool { - !CONFIG.load().is_empty() -} - -pub fn setup(intercept_config: &InterceptConfig, pools: &PoolMap) { - let mut config = HashMap::new(); - for (identifier, _) in pools.iter() { - let mut intercept_config = intercept_config.clone(); - intercept_config.substitute(&identifier.db, &identifier.user); - config.insert(identifier.clone(), intercept_config); - } - - CONFIG.store(Arc::new(config)); - - info!("Intercepting {} queries", intercept_config.queries.len()); -} - -pub fn disable() { - CONFIG.store(Arc::new(HashMap::new())); -} - // TODO: use these structs for deserialization #[derive(Serialize, Deserialize)] pub struct Rule { @@ -63,33 +32,35 @@ pub struct Column { } /// The intercept plugin. -pub struct Intercept; +pub struct Intercept<'a> { + pub enabled: bool, + pub config: &'a InterceptConfig, +} #[async_trait] -impl Plugin for Intercept { +impl<'a> Plugin for Intercept<'a> { async fn run( &mut self, query_router: &QueryRouter, ast: &Vec, ) -> Result { - if ast.is_empty() { + if !self.enabled || ast.is_empty() { return Ok(PluginOutput::Allow); } - let mut result = BytesMut::new(); - let query_map = match CONFIG.load().get(&PoolIdentifier::new( + let mut config = self.config.clone(); + config.substitute( &query_router.pool_settings().db, &query_router.pool_settings().user.username, - )) { - Some(query_map) => query_map.clone(), - None => return Ok(PluginOutput::Allow), - }; + ); + + let mut result = BytesMut::new(); for q in ast { // Normalization let q = q.to_string().to_ascii_lowercase(); - for (_, target) in query_map.queries.iter() { + for (_, target) in config.queries.iter() { if target.query.as_str() == q { debug!("Intercepting query: {}", q); @@ -147,142 +118,3 @@ impl Plugin for Intercept { } } } - -/// Make IntelliJ SQL plugin believe it's talking to an actual database -/// instead of PgCat. -#[allow(dead_code)] -fn fool_datagrip(database: &str, user: &str) -> Value { - json!([ - { - "query": "select current_database() as a, current_schemas(false) as b", - "schema": [ - { - "name": "a", - "data_type": "text", - }, - { - "name": "b", - "data_type": "anyarray", - }, - ], - - "result": [ - [database, "{public}"], - ], - }, - { - "query": "select current_database(), current_schema(), current_user", - "schema": [ - { - "name": "current_database", - "data_type": "text", - }, - { - "name": "current_schema", - "data_type": "text", - }, - { - "name": "current_user", - "data_type": "text", - } - ], - - "result": [ - ["sharded_db", "public", "sharding_user"], - ], - }, - { - "query": "select cast(n.oid as bigint) as id, datname as name, d.description, datistemplate as is_template, datallowconn as allow_connections, pg_catalog.pg_get_userbyid(n.datdba) as \"owner\" from pg_catalog.pg_database as n left join pg_catalog.pg_shdescription as d on n.oid = d.objoid order by case when datname = pg_catalog.current_database() then -cast(1 as bigint) else cast(n.oid as bigint) end", - "schema": [ - { - "name": "id", - "data_type": "oid", - }, - { - "name": "name", - "data_type": "text", - }, - { - "name": "description", - "data_type": "text", - }, - { - "name": "is_template", - "data_type": "bool", - }, - { - "name": "allow_connections", - "data_type": "bool", - }, - { - "name": "owner", - "data_type": "text", - } - ], - "result": [ - ["16387", database, "", "f", "t", user], - ] - }, - { - "query": "select cast(r.oid as bigint) as role_id, rolname as role_name, rolsuper as is_super, rolinherit as is_inherit, rolcreaterole as can_createrole, rolcreatedb as can_createdb, rolcanlogin as can_login, rolreplication as is_replication, rolconnlimit as conn_limit, rolvaliduntil as valid_until, rolbypassrls as bypass_rls, rolconfig as config, d.description from pg_catalog.pg_roles as r left join pg_catalog.pg_shdescription as d on d.objoid = r.oid", - "schema": [ - { - "name": "role_id", - "data_type": "oid", - }, - { - "name": "role_name", - "data_type": "text", - }, - { - "name": "is_super", - "data_type": "bool", - }, - { - "name": "is_inherit", - "data_type": "bool", - }, - { - "name": "can_createrole", - "data_type": "bool", - }, - { - "name": "can_createdb", - "data_type": "bool", - }, - { - "name": "can_login", - "data_type": "bool", - }, - { - "name": "is_replication", - "data_type": "bool", - }, - { - "name": "conn_limit", - "data_type": "int4", - }, - { - "name": "valid_until", - "data_type": "text", - }, - { - "name": "bypass_rls", - "data_type": "bool", - }, - { - "name": "config", - "data_type": "text", - }, - { - "name": "description", - "data_type": "text", - }, - ], - "result": [ - ["10", "postgres", "f", "t", "f", "f", "t", "f", "-1", "", "f", "", ""], - ["16419", user, "f", "t", "f", "f", "t", "f", "-1", "", "f", "", ""], - ] - } - ]) -} diff --git a/src/plugins/mod.rs b/src/plugins/mod.rs index 6661ece6..5ef6009a 100644 --- a/src/plugins/mod.rs +++ b/src/plugins/mod.rs @@ -9,6 +9,7 @@ //! pub mod intercept; +pub mod prewarmer; pub mod query_logger; pub mod table_access; diff --git a/src/plugins/prewarmer.rs b/src/plugins/prewarmer.rs new file mode 100644 index 00000000..a09bbe9d --- /dev/null +++ b/src/plugins/prewarmer.rs @@ -0,0 +1,28 @@ +//! Prewarm new connections before giving them to the client. +use crate::{errors::Error, server::Server}; +use log::info; + +pub struct Prewarmer<'a> { + pub enabled: bool, + pub server: &'a mut Server, + pub queries: &'a Vec, +} + +impl<'a> Prewarmer<'a> { + pub async fn run(&mut self) -> Result<(), Error> { + if !self.enabled { + return Ok(()); + } + + for query in self.queries { + info!( + "{} Prewarning with query: `{}`", + self.server.address(), + query + ); + self.server.query(&query).await?; + } + + Ok(()) + } +} diff --git a/src/plugins/query_logger.rs b/src/plugins/query_logger.rs index 2dfda8bc..debdf39f 100644 --- a/src/plugins/query_logger.rs +++ b/src/plugins/query_logger.rs @@ -5,44 +5,33 @@ use crate::{ plugins::{Plugin, PluginOutput}, query_router::QueryRouter, }; -use arc_swap::ArcSwap; use async_trait::async_trait; use log::info; -use once_cell::sync::Lazy; use sqlparser::ast::Statement; -use std::sync::Arc; -static ENABLED: Lazy> = Lazy::new(|| ArcSwap::from_pointee(false)); - -pub struct QueryLogger; - -pub fn setup() { - ENABLED.store(Arc::new(true)); - - info!("Logging queries to stdout"); -} - -pub fn disable() { - ENABLED.store(Arc::new(false)); -} - -pub fn enabled() -> bool { - **ENABLED.load() +pub struct QueryLogger<'a> { + pub enabled: bool, + pub user: &'a str, + pub db: &'a str, } #[async_trait] -impl Plugin for QueryLogger { +impl<'a> Plugin for QueryLogger<'a> { async fn run( &mut self, _query_router: &QueryRouter, ast: &Vec, ) -> Result { + if !self.enabled { + return Ok(PluginOutput::Allow); + } + let query = ast .iter() .map(|q| q.to_string()) .collect::>() .join("; "); - info!("{}", query); + info!("[pool: {}][user: {}] {}", self.user, self.db, query); Ok(PluginOutput::Allow) } diff --git a/src/plugins/table_access.rs b/src/plugins/table_access.rs index 4613a4fb..79c1260e 100644 --- a/src/plugins/table_access.rs +++ b/src/plugins/table_access.rs @@ -5,53 +5,39 @@ use async_trait::async_trait; use sqlparser::ast::{visit_relations, Statement}; use crate::{ - config::TableAccess as TableAccessConfig, errors::Error, plugins::{Plugin, PluginOutput}, query_router::QueryRouter, }; -use log::{debug, info}; +use log::debug; -use arc_swap::ArcSwap; use core::ops::ControlFlow; -use once_cell::sync::Lazy; -use std::sync::Arc; -static CONFIG: Lazy>> = Lazy::new(|| ArcSwap::from_pointee(vec![])); - -pub fn setup(config: &TableAccessConfig) { - CONFIG.store(Arc::new(config.tables.clone())); - - info!("Blocking access to {} tables", config.tables.len()); +pub struct TableAccess<'a> { + pub enabled: bool, + pub tables: &'a Vec, } -pub fn enabled() -> bool { - !CONFIG.load().is_empty() -} - -pub fn disable() { - CONFIG.store(Arc::new(vec![])); -} - -pub struct TableAccess; - #[async_trait] -impl Plugin for TableAccess { +impl<'a> Plugin for TableAccess<'a> { async fn run( &mut self, _query_router: &QueryRouter, ast: &Vec, ) -> Result { + if !self.enabled { + return Ok(PluginOutput::Allow); + } + let mut found = None; - let forbidden_tables = CONFIG.load(); visit_relations(ast, |relation| { let relation = relation.to_string(); let parts = relation.split(".").collect::>(); let table_name = parts.last().unwrap(); - if forbidden_tables.contains(&table_name.to_string()) { + if self.tables.contains(&table_name.to_string()) { found = Some(table_name.to_string()); ControlFlow::<()>::Break(()) } else { diff --git a/src/pool.rs b/src/pool.rs index 4664193c..8e03ae44 100644 --- a/src/pool.rs +++ b/src/pool.rs @@ -17,10 +17,13 @@ use std::sync::{ use std::time::Instant; use tokio::sync::Notify; -use crate::config::{get_config, Address, General, LoadBalancingMode, PoolMode, Role, User}; +use crate::config::{ + get_config, Address, General, LoadBalancingMode, Plugins, PoolMode, Role, User, +}; use crate::errors::Error; use crate::auth_passthrough::AuthPassthrough; +use crate::plugins::prewarmer; use crate::server::Server; use crate::sharding::ShardingFunction; use crate::stats::{AddressStats, ClientStats, PoolStats, ServerStats}; @@ -132,6 +135,9 @@ pub struct PoolSettings { pub auth_query: Option, pub auth_query_user: Option, pub auth_query_password: Option, + + /// Plugins + pub plugins: Option, } impl Default for PoolSettings { @@ -156,6 +162,7 @@ impl Default for PoolSettings { auth_query: None, auth_query_user: None, auth_query_password: None, + plugins: None, } } } @@ -195,6 +202,7 @@ pub struct ConnectionPool { paused: Arc, paused_waiter: Arc, + /// Statistics. pub stats: Arc, /// AuthInfo @@ -352,6 +360,10 @@ impl ConnectionPool { client_server_map.clone(), pool_stats.clone(), pool_auth_hash.clone(), + match pool_config.plugins { + Some(ref plugins) => Some(plugins.clone()), + None => config.plugins.clone(), + }, ); let connect_timeout = match pool_config.connect_timeout { @@ -377,7 +389,10 @@ impl ConnectionPool { .min() .unwrap(); - debug!("Pool reaper rate: {}ms", reaper_rate); + debug!( + "[pool: {}][user: {}] Pool reaper rate: {}ms", + pool_name, user.username, reaper_rate + ); let pool = Pool::builder() .max_size(user.pool_size) @@ -450,6 +465,10 @@ impl ConnectionPool { auth_query: pool_config.auth_query.clone(), auth_query_user: pool_config.auth_query_user.clone(), auth_query_password: pool_config.auth_query_password.clone(), + plugins: match pool_config.plugins { + Some(ref plugins) => Some(plugins.clone()), + None => config.plugins.clone(), + }, }, validated: Arc::new(AtomicBool::new(false)), paused: Arc::new(AtomicBool::new(false)), @@ -471,32 +490,6 @@ impl ConnectionPool { } } - if let Some(ref plugins) = config.plugins { - if let Some(ref intercept) = plugins.intercept { - if intercept.enabled { - crate::plugins::intercept::setup(intercept, &new_pools); - } else { - crate::plugins::intercept::disable(); - } - } - - if let Some(ref table_access) = plugins.table_access { - if table_access.enabled { - crate::plugins::table_access::setup(table_access); - } else { - crate::plugins::table_access::disable(); - } - } - - if let Some(ref query_logger) = plugins.query_logger { - if query_logger.enabled { - crate::plugins::query_logger::setup(); - } else { - crate::plugins::query_logger::disable(); - } - } - } - POOLS.store(Arc::new(new_pools.clone())); Ok(()) } @@ -923,6 +916,7 @@ pub struct ServerPool { client_server_map: ClientServerMap, stats: Arc, auth_hash: Arc>>, + plugins: Option, } impl ServerPool { @@ -933,6 +927,7 @@ impl ServerPool { client_server_map: ClientServerMap, stats: Arc, auth_hash: Arc>>, + plugins: Option, ) -> ServerPool { ServerPool { address, @@ -941,6 +936,7 @@ impl ServerPool { client_server_map, stats, auth_hash, + plugins, } } } @@ -973,7 +969,19 @@ impl ManageConnection for ServerPool { ) .await { - Ok(conn) => { + Ok(mut conn) => { + if let Some(ref plugins) = self.plugins { + if let Some(ref prewarmer) = plugins.prewarmer { + let mut prewarmer = prewarmer::Prewarmer { + enabled: prewarmer.enabled, + server: &mut conn, + queries: &prewarmer.queries, + }; + + prewarmer.run().await?; + } + } + stats.idle(); Ok(conn) } diff --git a/src/query_router.rs b/src/query_router.rs index d995b804..3e3a23a8 100644 --- a/src/query_router.rs +++ b/src/query_router.rs @@ -15,10 +15,7 @@ use sqlparser::parser::Parser; use crate::config::Role; use crate::errors::Error; use crate::messages::BytesMutReader; -use crate::plugins::{ - intercept, query_logger, table_access, Intercept, Plugin, PluginOutput, QueryLogger, - TableAccess, -}; +use crate::plugins::{Intercept, Plugin, PluginOutput, QueryLogger, TableAccess}; use crate::pool::PoolSettings; use crate::sharding::Sharder; @@ -793,13 +790,27 @@ impl QueryRouter { /// Add your plugins here and execute them. pub async fn execute_plugins(&self, ast: &Vec) -> Result { - if query_logger::enabled() { - let mut query_logger = QueryLogger {}; + let plugins = match self.pool_settings.plugins { + Some(ref plugins) => plugins, + None => return Ok(PluginOutput::Allow), + }; + + if let Some(ref query_logger) = plugins.query_logger { + let mut query_logger = QueryLogger { + enabled: query_logger.enabled, + user: &self.pool_settings.user.username, + db: &self.pool_settings.db, + }; + let _ = query_logger.run(&self, ast).await; } - if intercept::enabled() { - let mut intercept = Intercept {}; + if let Some(ref intercept) = plugins.intercept { + let mut intercept = Intercept { + enabled: intercept.enabled, + config: &intercept, + }; + let result = intercept.run(&self, ast).await; if let Ok(PluginOutput::Intercept(output)) = result { @@ -807,8 +818,12 @@ impl QueryRouter { } } - if table_access::enabled() { - let mut table_access = TableAccess {}; + if let Some(ref table_access) = plugins.table_access { + let mut table_access = TableAccess { + enabled: table_access.enabled, + tables: &table_access.tables, + }; + let result = table_access.run(&self, ast).await; if let Ok(PluginOutput::Deny(error)) = result { @@ -1161,6 +1176,7 @@ mod test { auth_query_password: None, auth_query_user: None, db: "test".to_string(), + plugins: None, }; let mut qr = QueryRouter::new(); assert_eq!(qr.active_role, None); @@ -1235,7 +1251,9 @@ mod test { auth_query_password: None, auth_query_user: None, db: "test".to_string(), + plugins: None, }; + let mut qr = QueryRouter::new(); qr.update_pool_settings(pool_settings.clone()); @@ -1379,17 +1397,25 @@ mod test { #[tokio::test] async fn test_table_access_plugin() { - use crate::config::TableAccess; - let ta = TableAccess { + use crate::config::{Plugins, TableAccess}; + let table_access = TableAccess { enabled: true, tables: vec![String::from("pg_database")], }; - - crate::plugins::table_access::setup(&ta); + let plugins = Plugins { + table_access: Some(table_access), + intercept: None, + query_logger: None, + prewarmer: None, + }; QueryRouter::setup(); + let mut pool_settings = PoolSettings::default(); + pool_settings.query_parser_enabled = true; + pool_settings.plugins = Some(plugins); - let qr = QueryRouter::new(); + let mut qr = QueryRouter::new(); + qr.update_pool_settings(pool_settings); let query = simple_query("SELECT * FROM pg_database"); let ast = QueryRouter::parse(&query).unwrap(); @@ -1403,4 +1429,17 @@ mod test { )) ); } + + #[tokio::test] + async fn test_plugins_disabled_by_defaault() { + QueryRouter::setup(); + let qr = QueryRouter::new(); + + let query = simple_query("SELECT * FROM pg_database"); + let ast = QueryRouter::parse(&query).unwrap(); + + let res = qr.execute_plugins(&ast).await; + + assert_eq!(res, Ok(PluginOutput::Allow)); + } } diff --git a/src/server.rs b/src/server.rs index dceab49d..244c06e7 100644 --- a/src/server.rs +++ b/src/server.rs @@ -970,6 +970,8 @@ impl Server { /// It will use the simple query protocol. /// Result will not be returned, so this is useful for things like `SET` or `ROLLBACK`. pub async fn query(&mut self, query: &str) -> Result<(), Error> { + debug!("Running `{}` on server {:?}", query, self.address); + let query = simple_query(query); self.send(&query).await?; diff --git a/tests/ruby/helpers/pgcat_helper.rb b/tests/ruby/helpers/pgcat_helper.rb index ad4c32a4..e36801eb 100644 --- a/tests/ruby/helpers/pgcat_helper.rb +++ b/tests/ruby/helpers/pgcat_helper.rb @@ -41,7 +41,24 @@ def self.three_shard_setup(pool_name, pool_size, pool_mode="transaction", lb_mod "1" => { "database" => "shard1", "servers" => [["localhost", primary1.port.to_s, "primary"]] }, "2" => { "database" => "shard2", "servers" => [["localhost", primary2.port.to_s, "primary"]] }, }, - "users" => { "0" => user } + "users" => { "0" => user }, + "plugins" => { + "intercept" => { + "enabled" => true, + "queries" => { + "0" => { + "query" => "select current_database() as a, current_schemas(false) as b", + "schema" => [ + ["a", "text"], + ["b", "text"], + ], + "result" => [ + ["${DATABASE}", "{public}"], + ] + } + } + } + } } } pgcat.update_config(pgcat_cfg) From 0898461c01e3d489a0b83d6dae7eb4c484e88509 Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Fri, 12 May 2023 12:48:37 -0700 Subject: [PATCH 47/79] Allow to deploy pools without checking (#438) --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/pool.rs | 10 +++++++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4703a0b2..d89fa79f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -897,7 +897,7 @@ checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" [[package]] name = "pgcat" -version = "1.0.2-alpha2" +version = "1.0.2-alpha3" dependencies = [ "arc-swap", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index af1b9ae0..17f7e0e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pgcat" -version = "1.0.2-alpha2" +version = "1.0.2-alpha3" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/src/pool.rs b/src/pool.rs index 8e03ae44..6235e220 100644 --- a/src/pool.rs +++ b/src/pool.rs @@ -401,9 +401,13 @@ impl ConnectionPool { .idle_timeout(Some(std::time::Duration::from_millis(idle_timeout))) .max_lifetime(Some(std::time::Duration::from_millis(server_lifetime))) .reaper_rate(std::time::Duration::from_millis(reaper_rate)) - .test_on_check_out(false) - .build(manager) - .await?; + .test_on_check_out(false); + + let pool = if config.general.validate_config { + pool.build(manager).await? + } else { + pool.build_unchecked(manager) + }; pools.push(pool); servers.push(address); From 7f57a89d75e793a210608e288e11d80db08c912e Mon Sep 17 00:00:00 2001 From: Zain Kabani <77307340+zainkabani@users.noreply.github.com> Date: Thu, 18 May 2023 00:38:10 -0400 Subject: [PATCH 48/79] Fix time based average stats (#442) * keep track of current stats and zero them after updating averages * Try tests * typo * remove commented test stuff * Avoid dividing by zero * Fix test * refactor, get rid of iterator. do it manually * trigger build * Fix --- src/stats.rs | 1 + src/stats/address.rs | 233 ++++++++++++++++++++++++--------------- src/stats/server.rs | 31 ++---- tests/ruby/admin_spec.rb | 2 +- 4 files changed, 151 insertions(+), 116 deletions(-) diff --git a/src/stats.rs b/src/stats.rs index 6de784f5..ce076d2d 100644 --- a/src/stats.rs +++ b/src/stats.rs @@ -113,6 +113,7 @@ impl Collector { for stats in server_stats.values() { if !stats.check_address_stat_average_is_updated_status() { stats.address_stats().update_averages(); + stats.address_stats().reset_current_counts(); stats.set_address_stat_average_is_updated_status(true); } } diff --git a/src/stats/address.rs b/src/stats/address.rs index 89e4ebe7..a0486445 100644 --- a/src/stats/address.rs +++ b/src/stats/address.rs @@ -1,35 +1,26 @@ use std::sync::atomic::*; use std::sync::Arc; +#[derive(Debug, Clone, Default)] +struct AddressStatFields { + xact_count: Arc, + query_count: Arc, + bytes_received: Arc, + bytes_sent: Arc, + xact_time: Arc, + query_time: Arc, + wait_time: Arc, + errors: Arc, +} + /// Internal address stats #[derive(Debug, Clone, Default)] pub struct AddressStats { - pub total_xact_count: Arc, - pub total_query_count: Arc, - pub total_received: Arc, - pub total_sent: Arc, - pub total_xact_time: Arc, - pub total_query_time: Arc, - pub total_wait_time: Arc, - pub total_errors: Arc, - - pub old_total_xact_count: Arc, - pub old_total_query_count: Arc, - pub old_total_received: Arc, - pub old_total_sent: Arc, - pub old_total_xact_time: Arc, - pub old_total_query_time: Arc, - pub old_total_wait_time: Arc, - pub old_total_errors: Arc, - - pub avg_query_count: Arc, - pub avg_query_time: Arc, - pub avg_recv: Arc, - pub avg_sent: Arc, - pub avg_errors: Arc, - pub avg_xact_time: Arc, - pub avg_xact_count: Arc, - pub avg_wait_time: Arc, + total: AddressStatFields, + + current: AddressStatFields, + + averages: AddressStatFields, // Determines if the averages have been updated since the last time they were reported pub averages_updated: Arc, @@ -43,67 +34,67 @@ impl IntoIterator for AddressStats { vec![ ( "total_xact_count".to_string(), - self.total_xact_count.load(Ordering::Relaxed), + self.total.xact_count.load(Ordering::Relaxed), ), ( "total_query_count".to_string(), - self.total_query_count.load(Ordering::Relaxed), + self.total.query_count.load(Ordering::Relaxed), ), ( "total_received".to_string(), - self.total_received.load(Ordering::Relaxed), + self.total.bytes_received.load(Ordering::Relaxed), ), ( "total_sent".to_string(), - self.total_sent.load(Ordering::Relaxed), + self.total.bytes_sent.load(Ordering::Relaxed), ), ( "total_xact_time".to_string(), - self.total_xact_time.load(Ordering::Relaxed), + self.total.xact_time.load(Ordering::Relaxed), ), ( "total_query_time".to_string(), - self.total_query_time.load(Ordering::Relaxed), + self.total.query_time.load(Ordering::Relaxed), ), ( "total_wait_time".to_string(), - self.total_wait_time.load(Ordering::Relaxed), + self.total.wait_time.load(Ordering::Relaxed), ), ( "total_errors".to_string(), - self.total_errors.load(Ordering::Relaxed), + self.total.errors.load(Ordering::Relaxed), ), ( "avg_xact_count".to_string(), - self.avg_xact_count.load(Ordering::Relaxed), + self.averages.xact_count.load(Ordering::Relaxed), ), ( "avg_query_count".to_string(), - self.avg_query_count.load(Ordering::Relaxed), + self.averages.query_count.load(Ordering::Relaxed), ), ( "avg_recv".to_string(), - self.avg_recv.load(Ordering::Relaxed), + self.averages.bytes_received.load(Ordering::Relaxed), ), ( "avg_sent".to_string(), - self.avg_sent.load(Ordering::Relaxed), + self.averages.bytes_sent.load(Ordering::Relaxed), ), ( "avg_errors".to_string(), - self.avg_errors.load(Ordering::Relaxed), + self.averages.errors.load(Ordering::Relaxed), ), ( "avg_xact_time".to_string(), - self.avg_xact_time.load(Ordering::Relaxed), + self.averages.xact_time.load(Ordering::Relaxed), ), ( "avg_query_time".to_string(), - self.avg_query_time.load(Ordering::Relaxed), + self.averages.query_time.load(Ordering::Relaxed), ), ( "avg_wait_time".to_string(), - self.avg_wait_time.load(Ordering::Relaxed), + self.averages.wait_time.load(Ordering::Relaxed), ), ] .into_iter() @@ -111,21 +102,120 @@ impl IntoIterator for AddressStats { } impl AddressStats { + pub fn xact_count_add(&self) { + self.total.xact_count.fetch_add(1, Ordering::Relaxed); + self.current.xact_count.fetch_add(1, Ordering::Relaxed); + } + + pub fn query_count_add(&self) { + self.total.query_count.fetch_add(1, Ordering::Relaxed); + self.current.query_count.fetch_add(1, Ordering::Relaxed); + } + + pub fn bytes_received_add(&self, bytes: u64) { + self.total + .bytes_received + .fetch_add(bytes, Ordering::Relaxed); + self.current + .bytes_received + .fetch_add(bytes, Ordering::Relaxed); + } + + pub fn bytes_sent_add(&self, bytes: u64) { + self.total.bytes_sent.fetch_add(bytes, Ordering::Relaxed); + self.current.bytes_sent.fetch_add(bytes, Ordering::Relaxed); + } + + pub fn xact_time_add(&self, time: u64) { + self.total.xact_time.fetch_add(time, Ordering::Relaxed); + self.current.xact_time.fetch_add(time, Ordering::Relaxed); + } + + pub fn query_time_add(&self, time: u64) { + self.total.query_time.fetch_add(time, Ordering::Relaxed); + self.current.query_time.fetch_add(time, Ordering::Relaxed); + } + + pub fn wait_time_add(&self, time: u64) { + self.total.wait_time.fetch_add(time, Ordering::Relaxed); + self.current.wait_time.fetch_add(time, Ordering::Relaxed); + } + pub fn error(&self) { - self.total_errors.fetch_add(1, Ordering::Relaxed); + self.total.errors.fetch_add(1, Ordering::Relaxed); + self.current.errors.fetch_add(1, Ordering::Relaxed); } pub fn update_averages(&self) { - let (totals, averages, old_totals) = self.fields_iterators(); - for (total, average, old_total) in itertools::izip!(totals, averages, old_totals) { - let total_value = total.load(Ordering::Relaxed); - let old_total_value = old_total.load(Ordering::Relaxed); - average.store( - (total_value - old_total_value) / (crate::stats::STAT_PERIOD / 1_000), - Ordering::Relaxed, - ); // Avg / second - old_total.store(total_value, Ordering::Relaxed); + let stat_period_per_second = crate::stats::STAT_PERIOD / 1_000; + + // xact_count + let current_xact_count = self.current.xact_count.load(Ordering::Relaxed); + let current_xact_time = self.current.xact_time.load(Ordering::Relaxed); + self.averages.xact_count.store( + current_xact_count / stat_period_per_second, + Ordering::Relaxed, + ); + if current_xact_count == 0 { + self.averages.xact_time.store(0, Ordering::Relaxed); + } else { + self.averages + .xact_time + .store(current_xact_time / current_xact_count, Ordering::Relaxed); + } + + // query_count + let current_query_count = self.current.query_count.load(Ordering::Relaxed); + let current_query_time = self.current.query_time.load(Ordering::Relaxed); + self.averages.query_count.store( + current_query_count / stat_period_per_second, + Ordering::Relaxed, + ); + if current_query_count == 0 { + self.averages.query_time.store(0, Ordering::Relaxed); + } else { + self.averages + .query_time + .store(current_query_time / current_query_count, Ordering::Relaxed); } + + // bytes_received + let current_bytes_received = self.current.bytes_received.load(Ordering::Relaxed); + self.averages.bytes_received.store( + current_bytes_received / stat_period_per_second, + Ordering::Relaxed, + ); + + // bytes_sent + let current_bytes_sent = self.current.bytes_sent.load(Ordering::Relaxed); + self.averages.bytes_sent.store( + current_bytes_sent / stat_period_per_second, + Ordering::Relaxed, + ); + + // wait_time + let current_wait_time = self.current.wait_time.load(Ordering::Relaxed); + self.averages.wait_time.store( + current_wait_time / stat_period_per_second, + Ordering::Relaxed, + ); + + // errors + let current_errors = self.current.errors.load(Ordering::Relaxed); + self.averages + .errors + .store(current_errors / stat_period_per_second, Ordering::Relaxed); + } + + pub fn reset_current_counts(&self) { + self.current.xact_count.store(0, Ordering::Relaxed); + self.current.xact_time.store(0, Ordering::Relaxed); + self.current.query_count.store(0, Ordering::Relaxed); + self.current.query_time.store(0, Ordering::Relaxed); + self.current.bytes_received.store(0, Ordering::Relaxed); + self.current.bytes_sent.store(0, Ordering::Relaxed); + self.current.wait_time.store(0, Ordering::Relaxed); + self.current.errors.store(0, Ordering::Relaxed); } pub fn populate_row(&self, row: &mut Vec) { @@ -133,43 +223,4 @@ impl AddressStats { row.push(value.to_string()); } } - - fn fields_iterators( - &self, - ) -> ( - Vec>, - Vec>, - Vec>, - ) { - let mut totals: Vec> = Vec::new(); - let mut averages: Vec> = Vec::new(); - let mut old_totals: Vec> = Vec::new(); - - totals.push(self.total_xact_count.clone()); - old_totals.push(self.old_total_xact_count.clone()); - averages.push(self.avg_xact_count.clone()); - totals.push(self.total_query_count.clone()); - old_totals.push(self.old_total_query_count.clone()); - averages.push(self.avg_query_count.clone()); - totals.push(self.total_received.clone()); - old_totals.push(self.old_total_received.clone()); - averages.push(self.avg_recv.clone()); - totals.push(self.total_sent.clone()); - old_totals.push(self.old_total_sent.clone()); - averages.push(self.avg_sent.clone()); - totals.push(self.total_xact_time.clone()); - old_totals.push(self.old_total_xact_time.clone()); - averages.push(self.avg_xact_time.clone()); - totals.push(self.total_query_time.clone()); - old_totals.push(self.old_total_query_time.clone()); - averages.push(self.avg_query_time.clone()); - totals.push(self.total_wait_time.clone()); - old_totals.push(self.old_total_wait_time.clone()); - averages.push(self.avg_wait_time.clone()); - totals.push(self.total_errors.clone()); - old_totals.push(self.old_total_errors.clone()); - averages.push(self.avg_errors.clone()); - - (totals, averages, old_totals) - } } diff --git a/src/stats/server.rs b/src/stats/server.rs index 399e585f..a327fa34 100644 --- a/src/stats/server.rs +++ b/src/stats/server.rs @@ -177,12 +177,9 @@ impl ServerStats { } pub fn checkout_time(&self, microseconds: u64, application_name: String) { - // Update server stats and address aggergation stats + // Update server stats and address aggregation stats self.set_application(application_name); - self.address - .stats - .total_wait_time - .fetch_add(microseconds, Ordering::Relaxed); + self.address.stats.wait_time_add(microseconds); self.pool_stats .maxwait .fetch_max(microseconds, Ordering::Relaxed); @@ -191,13 +188,8 @@ impl ServerStats { /// Report a query executed by a client against a server pub fn query(&self, milliseconds: u64, application_name: &str) { self.set_application(application_name.to_string()); - let address_stats = self.address_stats(); - address_stats - .total_query_count - .fetch_add(1, Ordering::Relaxed); - address_stats - .total_query_time - .fetch_add(milliseconds, Ordering::Relaxed); + self.address.stats.query_count_add(); + self.address.stats.query_time_add(milliseconds); } /// Report a transaction executed by a client a server @@ -208,29 +200,20 @@ impl ServerStats { self.set_application(application_name.to_string()); self.transaction_count.fetch_add(1, Ordering::Relaxed); - self.address - .stats - .total_xact_count - .fetch_add(1, Ordering::Relaxed); + self.address.stats.xact_count_add(); } /// Report data sent to a server pub fn data_sent(&self, amount_bytes: usize) { self.bytes_sent .fetch_add(amount_bytes as u64, Ordering::Relaxed); - self.address - .stats - .total_sent - .fetch_add(amount_bytes as u64, Ordering::Relaxed); + self.address.stats.bytes_sent_add(amount_bytes as u64); } /// Report data received from a server pub fn data_received(&self, amount_bytes: usize) { self.bytes_received .fetch_add(amount_bytes as u64, Ordering::Relaxed); - self.address - .stats - .total_received - .fetch_add(amount_bytes as u64, Ordering::Relaxed); + self.address.stats.bytes_received_add(amount_bytes as u64); } } diff --git a/tests/ruby/admin_spec.rb b/tests/ruby/admin_spec.rb index e054b45e..f93b1a6c 100644 --- a/tests/ruby/admin_spec.rb +++ b/tests/ruby/admin_spec.rb @@ -27,7 +27,7 @@ results = admin_conn.async_exec("SHOW STATS")[0] admin_conn.close expect(results["total_query_time"].to_i).to be_within(200).of(750) - expect(results["avg_query_time"].to_i).to be_within(20).of(50) + expect(results["avg_query_time"].to_i).to be_within(50).of(250) expect(results["total_wait_time"].to_i).to_not eq(0) expect(results["avg_wait_time"].to_i).to_not eq(0) From 37e3349c2428de0749fe6c71ec9f157194873a27 Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Thu, 18 May 2023 10:46:55 -0700 Subject: [PATCH 49/79] Optionally clean up server connections (#444) * Optionally clean up server connections * move setting to pool * fix test * Print setting to screen * fmt * Fix pool_settings override in tests --- src/config.rs | 17 +++++++++++ src/mirrors.rs | 1 + src/pool.rs | 20 +++++++++++++ src/server.rs | 12 ++++++-- tests/ruby/helpers/pgcat_helper.rb | 46 ++++++++++++++++-------------- tests/ruby/misc_spec.rb | 25 ++++++++++++++++ 6 files changed, 97 insertions(+), 24 deletions(-) diff --git a/src/config.rs b/src/config.rs index f4177730..2f5596c6 100644 --- a/src/config.rs +++ b/src/config.rs @@ -487,10 +487,15 @@ pub struct Pool { #[serde(default)] // False pub primary_reads_enabled: bool, + /// Maximum time to allow for establishing a new server connection. pub connect_timeout: Option, + /// Close idle connections that have been opened for longer than this. pub idle_timeout: Option, + /// Close server connections that have been opened for longer than this. + /// Only applied to idle connections. If the connection is actively used for + /// longer than this period, the pool will not interrupt it. pub server_lifetime: Option, #[serde(default = "Pool::default_sharding_function")] @@ -507,6 +512,9 @@ pub struct Pool { pub auth_query_user: Option, pub auth_query_password: Option, + #[serde(default = "Pool::default_cleanup_server_connections")] + pub cleanup_server_connections: bool, + pub plugins: Option, pub shards: BTreeMap, pub users: BTreeMap, @@ -548,6 +556,10 @@ impl Pool { ShardingFunction::PgBigintHash } + pub fn default_cleanup_server_connections() -> bool { + true + } + pub fn validate(&mut self) -> Result<(), Error> { match self.default_role.as_ref() { "any" => (), @@ -637,6 +649,7 @@ impl Default for Pool { auth_query_password: None, server_lifetime: None, plugins: None, + cleanup_server_connections: true, } } } @@ -1066,6 +1079,10 @@ impl Config { None => "default".to_string(), } ); + info!( + "[pool: {}] Cleanup server connections: {}", + pool_name, pool_config.cleanup_server_connections + ); info!( "[pool: {}] Plugins: {}", pool_name, diff --git a/src/mirrors.rs b/src/mirrors.rs index d6d691ff..7e2c9a09 100644 --- a/src/mirrors.rs +++ b/src/mirrors.rs @@ -44,6 +44,7 @@ impl MirroredClient { Arc::new(PoolStats::new(identifier, cfg.clone())), Arc::new(RwLock::new(None)), None, + true, ); Pool::builder() diff --git a/src/pool.rs b/src/pool.rs index 6235e220..a0b0c4d1 100644 --- a/src/pool.rs +++ b/src/pool.rs @@ -364,6 +364,7 @@ impl ConnectionPool { Some(ref plugins) => Some(plugins.clone()), None => config.plugins.clone(), }, + pool_config.cleanup_server_connections, ); let connect_timeout = match pool_config.connect_timeout { @@ -914,13 +915,29 @@ impl ConnectionPool { /// Wrapper for the bb8 connection pool. pub struct ServerPool { + /// Server address. address: Address, + + /// Server Postgres user. user: User, + + /// Server database. database: String, + + /// Client/server mapping. client_server_map: ClientServerMap, + + /// Server statistics. stats: Arc, + + /// Server auth hash (for auth passthrough). auth_hash: Arc>>, + + /// Server plugins. plugins: Option, + + /// Should we clean up dirty connections before putting them into the pool? + cleanup_connections: bool, } impl ServerPool { @@ -932,6 +949,7 @@ impl ServerPool { stats: Arc, auth_hash: Arc>>, plugins: Option, + cleanup_connections: bool, ) -> ServerPool { ServerPool { address, @@ -941,6 +959,7 @@ impl ServerPool { stats, auth_hash, plugins, + cleanup_connections, } } } @@ -970,6 +989,7 @@ impl ManageConnection for ServerPool { self.client_server_map.clone(), stats.clone(), self.auth_hash.clone(), + self.cleanup_connections, ) .await { diff --git a/src/server.rs b/src/server.rs index 244c06e7..32dd91f8 100644 --- a/src/server.rs +++ b/src/server.rs @@ -188,13 +188,16 @@ pub struct Server { /// Application name using the server at the moment. application_name: String, - // Last time that a successful server send or response happened + /// Last time that a successful server send or response happened last_activity: SystemTime, mirror_manager: Option, - // Associated addresses used + /// Associated addresses used addr_set: Option, + + /// Should clean up dirty connections? + cleanup_connections: bool, } impl Server { @@ -207,6 +210,7 @@ impl Server { client_server_map: ClientServerMap, stats: Arc, auth_hash: Arc>>, + cleanup_connections: bool, ) -> Result { let cached_resolver = CACHED_RESOLVER.load(); let mut addr_set: Option = None; @@ -687,6 +691,7 @@ impl Server { address.mirrors.clone(), )), }, + cleanup_connections, }; server.set_name("pgcat").await?; @@ -1004,7 +1009,7 @@ impl Server { // to avoid leaking state between clients. For performance reasons we only // send `DISCARD ALL` if we think the session is altered instead of just sending // it before each checkin. - if self.cleanup_state.needs_cleanup() { + if self.cleanup_state.needs_cleanup() && self.cleanup_connections { warn!("Server returned with session state altered, discarding state ({}) for application {}", self.cleanup_state, self.application_name); self.query("DISCARD ALL").await?; self.query("RESET ROLE").await?; @@ -1084,6 +1089,7 @@ impl Server { client_server_map, Arc::new(ServerStats::default()), Arc::new(RwLock::new(None)), + true, ) .await?; debug!("Connected!, sending query."); diff --git a/tests/ruby/helpers/pgcat_helper.rb b/tests/ruby/helpers/pgcat_helper.rb index e36801eb..7a5bd71d 100644 --- a/tests/ruby/helpers/pgcat_helper.rb +++ b/tests/ruby/helpers/pgcat_helper.rb @@ -118,7 +118,7 @@ def self.single_instance_setup(pool_name, pool_size, pool_mode="transaction", lb end end - def self.single_shard_setup(pool_name, pool_size, pool_mode="transaction", lb_mode="random", log_level="info") + def self.single_shard_setup(pool_name, pool_size, pool_mode="transaction", lb_mode="random", log_level="info", pool_settings={}) user = { "password" => "sharding_user", "pool_size" => pool_size, @@ -134,28 +134,32 @@ def self.single_shard_setup(pool_name, pool_size, pool_mode="transaction", lb_mo replica1 = PgInstance.new(8432, user["username"], user["password"], "shard0") replica2 = PgInstance.new(9432, user["username"], user["password"], "shard0") + pool_config = { + "default_role" => "any", + "pool_mode" => pool_mode, + "load_balancing_mode" => lb_mode, + "primary_reads_enabled" => false, + "query_parser_enabled" => false, + "sharding_function" => "pg_bigint_hash", + "shards" => { + "0" => { + "database" => "shard0", + "servers" => [ + ["localhost", primary.port.to_s, "primary"], + ["localhost", replica0.port.to_s, "replica"], + ["localhost", replica1.port.to_s, "replica"], + ["localhost", replica2.port.to_s, "replica"] + ] + }, + }, + "users" => { "0" => user } + } + + pool_config = pool_config.merge(pool_settings) + # Main proxy configs pgcat_cfg["pools"] = { - "#{pool_name}" => { - "default_role" => "any", - "pool_mode" => pool_mode, - "load_balancing_mode" => lb_mode, - "primary_reads_enabled" => false, - "query_parser_enabled" => false, - "sharding_function" => "pg_bigint_hash", - "shards" => { - "0" => { - "database" => "shard0", - "servers" => [ - ["localhost", primary.port.to_s, "primary"], - ["localhost", replica0.port.to_s, "replica"], - ["localhost", replica1.port.to_s, "replica"], - ["localhost", replica2.port.to_s, "replica"] - ] - }, - }, - "users" => { "0" => user } - } + "#{pool_name}" => pool_config, } pgcat_cfg["general"]["port"] = pgcat.port pgcat.update_config(pgcat_cfg) diff --git a/tests/ruby/misc_spec.rb b/tests/ruby/misc_spec.rb index e4d6f6fb..fe216e5b 100644 --- a/tests/ruby/misc_spec.rb +++ b/tests/ruby/misc_spec.rb @@ -320,6 +320,31 @@ expect(processes.primary.count_query("DISCARD ALL")).to eq(0) end end + + context "server cleanup disabled" do + let(:processes) { Helpers::Pgcat.single_shard_setup("sharded_db", 1, "transaction", "random", "info", { "cleanup_server_connections" => false }) } + + it "will not clean up connection state" do + conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) + processes.primary.reset_stats + conn.async_exec("SET statement_timeout TO 1000") + conn.close + + puts processes.pgcat.logs + expect(processes.primary.count_query("DISCARD ALL")).to eq(0) + end + + it "will not clean up prepared statements" do + conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) + processes.primary.reset_stats + conn.async_exec("PREPARE prepared_q (int) AS SELECT $1") + + conn.close + + puts processes.pgcat.logs + expect(processes.primary.count_query("DISCARD ALL")).to eq(0) + end + end end describe "Idle client timeout" do From 100778670ce58ac4afd57b5cf2bba35e4d806293 Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Thu, 18 May 2023 16:41:22 -0700 Subject: [PATCH 50/79] Ensure data makes it to the client (#446) * Ensure data makes it to the client * flush all buffers --- src/client.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client.rs b/src/client.rs index 331a0da4..24d76bd5 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1313,7 +1313,7 @@ where .receive_server_message(server, &address, &pool, &self.stats.clone()) .await?; - match write_all_half(&mut self.write, &response).await { + match write_all_flush(&mut self.write, &response).await { Ok(_) => (), Err(err) => { server.mark_bad(); @@ -1408,7 +1408,7 @@ where .receive_server_message(server, address, pool, client_stats) .await?; - match write_all_half(&mut self.write, &response).await { + match write_all_flush(&mut self.write, &response).await { Ok(_) => (), Err(err) => { server.mark_bad(); From d63be9b93a94bce9755864ea57909170c3f0294a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 May 2023 08:58:13 -0700 Subject: [PATCH 51/79] chore(deps): bump toml from 0.7.3 to 0.7.4 (#447) Bumps [toml](https://github.com/toml-rs/toml) from 0.7.3 to 0.7.4. - [Commits](https://github.com/toml-rs/toml/compare/toml-v0.7.3...toml-v0.7.4) --- updated-dependencies: - dependency-name: toml dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d89fa79f..d26c4d52 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1252,9 +1252,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4" +checksum = "93107647184f6027e3b7dcb2e11034cf95ffa1e3a682c67951963ac69c1c007d" dependencies = [ "serde", ] @@ -1528,9 +1528,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b403acf6f2bb0859c93c7f0d967cb4a75a7ac552100f9322faf64dc047669b21" +checksum = "d6135d499e69981f9ff0ef2167955a5333c35e36f6937d382974566b3d5b94ec" dependencies = [ "serde", "serde_spanned", @@ -1540,18 +1540,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" +checksum = "5a76a9312f5ba4c2dec6b9161fdf25d87ad8a09256ccea5a556fef03c706a10f" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.19.6" +version = "0.19.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08de71aa0d6e348f070457f85af8bd566e2bc452156a423ddf22861b3a953fae" +checksum = "92d964908cec0d030b812013af25a0e57fddfadb1e066ecc6681d86253129d4f" dependencies = [ "indexmap", "serde", @@ -1905,9 +1905,9 @@ checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" [[package]] name = "winnow" -version = "0.3.3" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faf09497b8f8b5ac5d3bb4d05c0a99be20f26fd3d5f2db7b0716e946d5103658" +checksum = "61de7bac303dc551fe038e2b3cef0f571087a47571ea6e79a87692ac99b99699" dependencies = [ "memchr", ] From a8a30ad43b16cd30a5c66357a88b2758307feb54 Mon Sep 17 00:00:00 2001 From: Mostafa Abdelraouf Date: Tue, 23 May 2023 08:44:49 -0500 Subject: [PATCH 52/79] Refactor Pool Stats to be based off of Server/Client stats (#445) What is wrong Stats reported by SHOW POOLS seem to be leaking. We see lingering cl_idle , cl_waiting, and similarly for sv_idle , sv_active. We confirmed that these are reporting issues not actual lingering clients. This behavior is readily reproducible by running while true; do psql "postgres://sharding_user:sharding_user@localhost:6432/sharded_db" -c "SELECT 1" > /dev/null 2>&1 & done Why it happens I wasn't able to get to figure our the reason for the bug but my best guess is that we have race conditions when updating pool-level stats. So even though individual update operations are atomic, we perform a check then update sequence which is not protected by a guard. https://github.com/postgresml/pgcat/blob/main/src/stats/pool.rs#L174-L179 I am also suspecting that using Relaxed ordering might allow this behavior (I changed all operations to use Ordering::SeqCst but still got lingering clients) How to fix Since SHOW POOLS/SHOW SERVER/SHOW CLIENTS all show the current state of the proxy (as opposed to SHOW STATS which show aggregate values), this PR refactors SHOW POOLS to have it construct the results directly from SHOW SERVER and SHOW CLIENT datasets. This reduces the complexity of stat updates and eliminates the need for having locks when updating pool stats as we only care about updating individual client/server states. This will change the semantics of maxwait, so instead of it holding the maxwait time ever encountered by a client (connected or disconnected), it will only consider connected clients which should be okay given PgCat tends to hold on to client connections more than Pgbouncer. --- src/admin.rs | 40 +---- src/client.rs | 14 +- src/mirrors.rs | 8 +- src/pool.rs | 42 ++--- src/prometheus.rs | 24 ++- src/stats.rs | 21 --- src/stats/client.rs | 20 +-- src/stats/pool.rs | 377 +++++++++++++-------------------------- src/stats/server.rs | 24 +-- tests/ruby/admin_spec.rb | 322 +-------------------------------- tests/ruby/stats_spec.rb | 369 ++++++++++++++++++++++++++++++++++++++ 11 files changed, 548 insertions(+), 713 deletions(-) create mode 100644 tests/ruby/stats_spec.rb diff --git a/src/admin.rs b/src/admin.rs index ceba20c8..804476c9 100644 --- a/src/admin.rs +++ b/src/admin.rs @@ -1,4 +1,5 @@ use crate::pool::BanReason; +use crate::stats::pool::PoolStats; use bytes::{Buf, BufMut, BytesMut}; use log::{error, info, trace}; use nix::sys::signal::{self, Signal}; @@ -14,7 +15,7 @@ use crate::errors::Error; use crate::messages::*; use crate::pool::ClientServerMap; use crate::pool::{get_all_pools, get_pool}; -use crate::stats::{get_client_stats, get_pool_stats, get_server_stats, ClientState, ServerState}; +use crate::stats::{get_client_stats, get_server_stats, ClientState, ServerState}; pub fn generate_server_info_for_admin() -> BytesMut { let mut server_info = BytesMut::new(); @@ -254,39 +255,12 @@ async fn show_pools(stream: &mut T) -> Result<(), Error> where T: tokio::io::AsyncWrite + std::marker::Unpin, { - let all_pool_stats = get_pool_stats(); - - let columns = vec![ - ("database", DataType::Text), - ("user", DataType::Text), - ("pool_mode", DataType::Text), - ("cl_idle", DataType::Numeric), - ("cl_active", DataType::Numeric), - ("cl_waiting", DataType::Numeric), - ("cl_cancel_req", DataType::Numeric), - ("sv_active", DataType::Numeric), - ("sv_idle", DataType::Numeric), - ("sv_used", DataType::Numeric), - ("sv_tested", DataType::Numeric), - ("sv_login", DataType::Numeric), - ("maxwait", DataType::Numeric), - ("maxwait_us", DataType::Numeric), - ]; - + let pool_lookup = PoolStats::construct_pool_lookup(); let mut res = BytesMut::new(); - res.put(row_description(&columns)); - - for ((_user_pool, _pool), pool_stats) in all_pool_stats { - let mut row = vec![ - pool_stats.database(), - pool_stats.user(), - pool_stats.pool_mode().to_string(), - ]; - pool_stats.populate_row(&mut row); - pool_stats.clear_maxwait(); - res.put(data_row(&row)); - } - + res.put(row_description(&PoolStats::generate_header())); + pool_lookup.iter().for_each(|(_identifier, pool_stats)| { + res.put(data_row(&pool_stats.generate_row())); + }); res.put(command_complete("SHOW")); // ReadyForQuery diff --git a/src/client.rs b/src/client.rs index 24d76bd5..1ff558b5 100644 --- a/src/client.rs +++ b/src/client.rs @@ -20,7 +20,7 @@ use crate::plugins::PluginOutput; use crate::pool::{get_pool, ClientServerMap, ConnectionPool}; use crate::query_router::{Command, QueryRouter}; use crate::server::Server; -use crate::stats::{ClientStats, PoolStats, ServerStats}; +use crate::stats::{ClientStats, ServerStats}; use crate::tls::Tls; use tokio_rustls::server::TlsStream; @@ -654,24 +654,12 @@ where ready_for_query(&mut write).await?; trace!("Startup OK"); - let pool_stats = match get_pool(pool_name, username) { - Some(pool) => { - if !admin { - pool.stats - } else { - Arc::new(PoolStats::default()) - } - } - None => Arc::new(PoolStats::default()), - }; - let stats = Arc::new(ClientStats::new( process_id, application_name, username, pool_name, tokio::time::Instant::now(), - pool_stats, )); Ok(Client { diff --git a/src/mirrors.rs b/src/mirrors.rs index 7e2c9a09..0f2b02c0 100644 --- a/src/mirrors.rs +++ b/src/mirrors.rs @@ -7,8 +7,7 @@ use bytes::{Bytes, BytesMut}; use parking_lot::RwLock; use crate::config::{get_config, Address, Role, User}; -use crate::pool::{ClientServerMap, PoolIdentifier, ServerPool}; -use crate::stats::PoolStats; +use crate::pool::{ClientServerMap, ServerPool}; use log::{error, info, trace, warn}; use tokio::sync::mpsc::{channel, Receiver, Sender}; @@ -24,7 +23,7 @@ impl MirroredClient { async fn create_pool(&self) -> Pool { let config = get_config(); let default = std::time::Duration::from_millis(10_000).as_millis() as u64; - let (connection_timeout, idle_timeout, cfg) = + let (connection_timeout, idle_timeout, _cfg) = match config.pools.get(&self.address.pool_name) { Some(cfg) => ( cfg.connect_timeout.unwrap_or(default), @@ -34,14 +33,11 @@ impl MirroredClient { None => (default, default, crate::config::Pool::default()), }; - let identifier = PoolIdentifier::new(&self.database, &self.user.username); - let manager = ServerPool::new( self.address.clone(), self.user.clone(), self.database.as_str(), ClientServerMap::default(), - Arc::new(PoolStats::new(identifier, cfg.clone())), Arc::new(RwLock::new(None)), None, true, diff --git a/src/pool.rs b/src/pool.rs index a0b0c4d1..4fa27753 100644 --- a/src/pool.rs +++ b/src/pool.rs @@ -10,6 +10,7 @@ use rand::seq::SliceRandom; use rand::thread_rng; use regex::Regex; use std::collections::HashMap; +use std::fmt::{Display, Formatter}; use std::sync::{ atomic::{AtomicBool, Ordering}, Arc, @@ -26,7 +27,7 @@ use crate::auth_passthrough::AuthPassthrough; use crate::plugins::prewarmer; use crate::server::Server; use crate::sharding::ShardingFunction; -use crate::stats::{AddressStats, ClientStats, PoolStats, ServerStats}; +use crate::stats::{AddressStats, ClientStats, ServerStats}; pub type ProcessId = i32; pub type SecretKey = i32; @@ -76,6 +77,12 @@ impl PoolIdentifier { } } +impl Display for PoolIdentifier { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}@{}", self.user, self.db) + } +} + impl From<&Address> for PoolIdentifier { fn from(address: &Address) -> PoolIdentifier { PoolIdentifier::new(&address.database, &address.username) @@ -202,9 +209,6 @@ pub struct ConnectionPool { paused: Arc, paused_waiter: Arc, - /// Statistics. - pub stats: Arc, - /// AuthInfo pub auth_hash: Arc>>, } @@ -254,10 +258,6 @@ impl ConnectionPool { .clone() .into_keys() .collect::>(); - let pool_stats = Arc::new(PoolStats::new(identifier, pool_config.clone())); - - // Allow the pool to be seen in statistics - pool_stats.register(pool_stats.clone()); // Sort by shard number to ensure consistency. shard_ids.sort_by_key(|k| k.parse::().unwrap()); @@ -358,7 +358,6 @@ impl ConnectionPool { user.clone(), &shard.database, client_server_map.clone(), - pool_stats.clone(), pool_auth_hash.clone(), match pool_config.plugins { Some(ref plugins) => Some(plugins.clone()), @@ -429,7 +428,6 @@ impl ConnectionPool { let pool = ConnectionPool { databases: shards, - stats: pool_stats, addresses, banlist: Arc::new(RwLock::new(banlist)), config_hash: new_pool_hash_value, @@ -610,6 +608,10 @@ impl ConnectionPool { }); } + // Indicate we're waiting on a server connection from a pool. + let now = Instant::now(); + client_stats.waiting(); + while !candidates.is_empty() { // Get the next candidate let address = match candidates.pop() { @@ -628,10 +630,6 @@ impl ConnectionPool { } } - // Indicate we're waiting on a server connection from a pool. - let now = Instant::now(); - client_stats.waiting(); - // Check if we can connect let mut conn = match self.databases[address.shard][address.address_index] .get() @@ -669,7 +667,7 @@ impl ConnectionPool { .stats() .checkout_time(checkout_time, client_stats.application_name()); server.stats().active(client_stats.application_name()); - + client_stats.active(); return Ok((conn, address.clone())); } @@ -677,11 +675,19 @@ impl ConnectionPool { .run_health_check(address, server, now, client_stats) .await { + let checkout_time: u64 = now.elapsed().as_micros() as u64; + client_stats.checkout_time(checkout_time); + server + .stats() + .checkout_time(checkout_time, client_stats.application_name()); + server.stats().active(client_stats.application_name()); + client_stats.active(); return Ok((conn, address.clone())); } else { continue; } } + client_stats.idle(); Err(Error::AllServersDown) } @@ -927,9 +933,6 @@ pub struct ServerPool { /// Client/server mapping. client_server_map: ClientServerMap, - /// Server statistics. - stats: Arc, - /// Server auth hash (for auth passthrough). auth_hash: Arc>>, @@ -946,7 +949,6 @@ impl ServerPool { user: User, database: &str, client_server_map: ClientServerMap, - stats: Arc, auth_hash: Arc>>, plugins: Option, cleanup_connections: bool, @@ -956,7 +958,6 @@ impl ServerPool { user: user.clone(), database: database.to_string(), client_server_map, - stats, auth_hash, plugins, cleanup_connections, @@ -975,7 +976,6 @@ impl ManageConnection for ServerPool { let stats = Arc::new(ServerStats::new( self.address.clone(), - self.stats.clone(), tokio::time::Instant::now(), )); diff --git a/src/prometheus.rs b/src/prometheus.rs index 6e578bf0..b4884604 100644 --- a/src/prometheus.rs +++ b/src/prometheus.rs @@ -9,8 +9,9 @@ use std::sync::atomic::Ordering; use std::sync::Arc; use crate::config::Address; -use crate::pool::get_all_pools; -use crate::stats::{get_pool_stats, get_server_stats, ServerStats}; +use crate::pool::{get_all_pools, PoolIdentifier}; +use crate::stats::pool::PoolStats; +use crate::stats::{get_server_stats, ServerStats}; struct MetricHelpType { help: &'static str, @@ -233,10 +234,10 @@ impl PrometheusMetric { Self::from_name(&format!("stats_{}", name), value, labels) } - fn from_pool(pool: &(String, String), name: &str, value: u64) -> Option> { + fn from_pool(pool_id: PoolIdentifier, name: &str, value: u64) -> Option> { let mut labels = HashMap::new(); - labels.insert("pool", pool.0.clone()); - labels.insert("user", pool.1.clone()); + labels.insert("pool", pool_id.db); + labels.insert("user", pool_id.user); Self::from_name(&format!("pools_{}", name), value, labels) } @@ -284,18 +285,15 @@ fn push_address_stats(lines: &mut Vec) { // Adds relevant metrics shown in a SHOW POOLS admin command. fn push_pool_stats(lines: &mut Vec) { - let pool_stats = get_pool_stats(); - for (pool, stats) in pool_stats.iter() { - let stats = &**stats; + let pool_stats = PoolStats::construct_pool_lookup(); + for (pool_id, stats) in pool_stats.iter() { for (name, value) in stats.clone() { - if let Some(prometheus_metric) = PrometheusMetric::::from_pool(pool, &name, value) + if let Some(prometheus_metric) = + PrometheusMetric::::from_pool(pool_id.clone(), &name, value) { lines.push(prometheus_metric.to_string()); } else { - warn!( - "Metric {} not implemented for ({},{})", - name, pool.0, pool.1 - ); + warn!("Metric {} not implemented for ({})", name, *pool_id); } } } diff --git a/src/stats.rs b/src/stats.rs index ce076d2d..29c00d64 100644 --- a/src/stats.rs +++ b/src/stats.rs @@ -1,4 +1,3 @@ -use crate::pool::PoolIdentifier; /// Statistics and reporting. use arc_swap::ArcSwap; @@ -16,13 +15,11 @@ pub mod pool; pub mod server; pub use address::AddressStats; pub use client::{ClientState, ClientStats}; -pub use pool::PoolStats; pub use server::{ServerState, ServerStats}; /// Convenience types for various stats type ClientStatesLookup = HashMap>; type ServerStatesLookup = HashMap>; -type PoolStatsLookup = HashMap<(String, String), Arc>; /// Stats for individual client connections /// Used in SHOW CLIENTS. @@ -34,11 +31,6 @@ static CLIENT_STATS: Lazy>> = static SERVER_STATS: Lazy>> = Lazy::new(|| Arc::new(RwLock::new(ServerStatesLookup::default()))); -/// Aggregate stats for each pool (a pool is identified by database name and username) -/// Used in SHOW POOLS. -static POOL_STATS: Lazy>> = - Lazy::new(|| Arc::new(RwLock::new(PoolStatsLookup::default()))); - /// The statistics reporter. An instance is given to each possible source of statistics, /// e.g. client stats, server stats, connection pool stats. pub static REPORTER: Lazy> = @@ -80,13 +72,6 @@ impl Reporter { fn server_disconnecting(&self, server_id: i32) { SERVER_STATS.write().remove(&server_id); } - - /// Register a pool with the stats system. - fn pool_register(&self, identifier: PoolIdentifier, stats: Arc) { - POOL_STATS - .write() - .insert((identifier.db, identifier.user), stats); - } } /// The statistics collector which used for calculating averages @@ -139,12 +124,6 @@ pub fn get_server_stats() -> ServerStatesLookup { SERVER_STATS.read().clone() } -/// Get a snapshot of pool statistics. -/// by the `Collector`. -pub fn get_pool_stats() -> PoolStatsLookup { - POOL_STATS.read().clone() -} - /// Get the statistics reporter used to update stats across the pools/clients. pub fn get_reporter() -> Reporter { (*(*REPORTER.load())).clone() diff --git a/src/stats/client.rs b/src/stats/client.rs index 4cbcab2f..6a30ec15 100644 --- a/src/stats/client.rs +++ b/src/stats/client.rs @@ -1,4 +1,3 @@ -use super::PoolStats; use super::{get_reporter, Reporter}; use atomic_enum::atomic_enum; use std::sync::atomic::*; @@ -34,12 +33,14 @@ pub struct ClientStats { pool_name: String, connect_time: Instant, - pool_stats: Arc, reporter: Reporter, /// Total time spent waiting for a connection from pool, measures in microseconds pub total_wait_time: Arc, + /// Maximum time spent waiting for a connection from pool, measures in microseconds + pub max_wait_time: Arc, + /// Current state of the client pub state: Arc, @@ -61,8 +62,8 @@ impl Default for ClientStats { application_name: String::new(), username: String::new(), pool_name: String::new(), - pool_stats: Arc::new(PoolStats::default()), total_wait_time: Arc::new(AtomicU64::new(0)), + max_wait_time: Arc::new(AtomicU64::new(0)), state: Arc::new(AtomicClientState::new(ClientState::Idle)), transaction_count: Arc::new(AtomicU64::new(0)), query_count: Arc::new(AtomicU64::new(0)), @@ -79,11 +80,9 @@ impl ClientStats { username: &str, pool_name: &str, connect_time: Instant, - pool_stats: Arc, ) -> Self { Self { client_id, - pool_stats, connect_time, application_name: application_name.to_string(), username: username.to_string(), @@ -96,8 +95,6 @@ impl ClientStats { /// update metrics on the corresponding pool. pub fn disconnect(&self) { self.reporter.client_disconnecting(self.client_id); - self.pool_stats - .client_disconnect(self.state.load(Ordering::Relaxed)) } /// Register a client with the stats system. The stats system uses client_id @@ -105,27 +102,20 @@ impl ClientStats { pub fn register(&self, stats: Arc) { self.reporter.client_register(self.client_id, stats); self.state.store(ClientState::Idle, Ordering::Relaxed); - self.pool_stats.cl_idle.fetch_add(1, Ordering::Relaxed); } /// Reports a client is done querying the server and is no longer assigned a server connection pub fn idle(&self) { - self.pool_stats - .client_idle(self.state.load(Ordering::Relaxed)); self.state.store(ClientState::Idle, Ordering::Relaxed); } /// Reports a client is waiting for a connection pub fn waiting(&self) { - self.pool_stats - .client_waiting(self.state.load(Ordering::Relaxed)); self.state.store(ClientState::Waiting, Ordering::Relaxed); } /// Reports a client is done waiting for a connection and is about to query the server. pub fn active(&self) { - self.pool_stats - .client_active(self.state.load(Ordering::Relaxed)); self.state.store(ClientState::Active, Ordering::Relaxed); } @@ -144,6 +134,8 @@ impl ClientStats { pub fn checkout_time(&self, microseconds: u64) { self.total_wait_time .fetch_add(microseconds, Ordering::Relaxed); + self.max_wait_time + .fetch_max(microseconds, Ordering::Relaxed); } /// Report a query executed by a client against a server diff --git a/src/stats/pool.rs b/src/stats/pool.rs index 1b01ef2e..d3ac78e9 100644 --- a/src/stats/pool.rs +++ b/src/stats/pool.rs @@ -1,274 +1,151 @@ -use crate::config::Pool; -use crate::config::PoolMode; -use crate::pool::PoolIdentifier; -use std::sync::atomic::*; -use std::sync::Arc; +use log::debug; -use super::get_reporter; -use super::Reporter; use super::{ClientState, ServerState}; +use crate::{config::PoolMode, messages::DataType, pool::PoolIdentifier}; +use std::collections::HashMap; +use std::sync::atomic::*; -#[derive(Debug, Clone, Default)] +use crate::pool::get_all_pools; + +#[derive(Debug, Clone)] /// A struct that holds information about a Pool . pub struct PoolStats { - // Pool identifier, cannot be changed after creating the instance - identifier: PoolIdentifier, - - // Pool Config, cannot be changed after creating the instance - config: Pool, - - // A reference to the global reporter. - reporter: Reporter, - - /// Counters (atomics) - pub cl_idle: Arc, - pub cl_active: Arc, - pub cl_waiting: Arc, - pub cl_cancel_req: Arc, - pub sv_active: Arc, - pub sv_idle: Arc, - pub sv_used: Arc, - pub sv_tested: Arc, - pub sv_login: Arc, - pub maxwait: Arc, + pub identifier: PoolIdentifier, + pub mode: PoolMode, + pub cl_idle: u64, + pub cl_active: u64, + pub cl_waiting: u64, + pub cl_cancel_req: u64, + pub sv_active: u64, + pub sv_idle: u64, + pub sv_used: u64, + pub sv_tested: u64, + pub sv_login: u64, + pub maxwait: u64, } - -impl IntoIterator for PoolStats { - type Item = (String, u64); - type IntoIter = std::vec::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - vec![ - ("cl_idle".to_string(), self.cl_idle.load(Ordering::Relaxed)), - ( - "cl_active".to_string(), - self.cl_active.load(Ordering::Relaxed), - ), - ( - "cl_waiting".to_string(), - self.cl_waiting.load(Ordering::Relaxed), - ), - ( - "cl_cancel_req".to_string(), - self.cl_cancel_req.load(Ordering::Relaxed), - ), - ( - "sv_active".to_string(), - self.sv_active.load(Ordering::Relaxed), - ), - ("sv_idle".to_string(), self.sv_idle.load(Ordering::Relaxed)), - ("sv_used".to_string(), self.sv_used.load(Ordering::Relaxed)), - ( - "sv_tested".to_string(), - self.sv_tested.load(Ordering::Relaxed), - ), - ( - "sv_login".to_string(), - self.sv_login.load(Ordering::Relaxed), - ), - ( - "maxwait".to_string(), - self.maxwait.load(Ordering::Relaxed) / 1_000_000, - ), - ( - "maxwait_us".to_string(), - self.maxwait.load(Ordering::Relaxed) % 1_000_000, - ), - ] - .into_iter() - } -} - impl PoolStats { - pub fn new(identifier: PoolIdentifier, config: Pool) -> Self { - Self { + pub fn new(identifier: PoolIdentifier, mode: PoolMode) -> Self { + PoolStats { identifier, - config, - reporter: get_reporter(), - ..Default::default() - } - } - - // Getters - pub fn register(&self, stats: Arc) { - self.reporter.pool_register(self.identifier.clone(), stats); - } - - pub fn database(&self) -> String { - self.identifier.db.clone() - } - - pub fn user(&self) -> String { - self.identifier.user.clone() - } - - pub fn pool_mode(&self) -> PoolMode { - self.config.pool_mode - } - - /// Populates an array of strings with counters (used by admin in show pools) - pub fn populate_row(&self, row: &mut Vec) { - for (_key, value) in self.clone() { - row.push(value.to_string()); - } - } - - /// Deletes the maxwait counter, this is done everytime we obtain metrics - pub fn clear_maxwait(&self) { - self.maxwait.store(0, Ordering::Relaxed); - } - - /// Notified when a server of the pool enters login state. - /// - /// Arguments: - /// - /// `from`: The state of the server that notifies. - pub fn server_login(&self, from: ServerState) { - self.sv_login.fetch_add(1, Ordering::Relaxed); - if from != ServerState::Login { - self.decrease_from_server_state(from); - } - } - - /// Notified when a server of the pool become 'active' - /// - /// Arguments: - /// - /// `from`: The state of the server that notifies. - pub fn server_active(&self, from: ServerState) { - self.sv_active.fetch_add(1, Ordering::Relaxed); - if from != ServerState::Active { - self.decrease_from_server_state(from); - } - } - - /// Notified when a server of the pool become 'tested' - /// - /// Arguments: - /// - /// `from`: The state of the server that notifies. - pub fn server_tested(&self, from: ServerState) { - self.sv_tested.fetch_add(1, Ordering::Relaxed); - if from != ServerState::Tested { - self.decrease_from_server_state(from); + mode, + cl_idle: 0, + cl_active: 0, + cl_waiting: 0, + cl_cancel_req: 0, + sv_active: 0, + sv_idle: 0, + sv_used: 0, + sv_tested: 0, + sv_login: 0, + maxwait: 0, } } - /// Notified when a server of the pool become 'idle' - /// - /// Arguments: - /// - /// `from`: The state of the server that notifies. - pub fn server_idle(&self, from: ServerState) { - self.sv_idle.fetch_add(1, Ordering::Relaxed); - if from != ServerState::Idle { - self.decrease_from_server_state(from); - } - } + pub fn construct_pool_lookup() -> HashMap { + let mut map: HashMap = HashMap::new(); + let client_map = super::get_client_stats(); + let server_map = super::get_server_stats(); - /// Notified when a client of the pool become 'waiting' - /// - /// Arguments: - /// - /// `from`: The state of the client that notifies. - pub fn client_waiting(&self, from: ClientState) { - if from != ClientState::Waiting { - self.cl_waiting.fetch_add(1, Ordering::Relaxed); - self.decrease_from_client_state(from); + for (identifier, pool) in get_all_pools() { + map.insert( + identifier.clone(), + PoolStats::new(identifier, pool.settings.pool_mode), + ); } - } - /// Notified when a client of the pool become 'active' - /// - /// Arguments: - /// - /// `from`: The state of the client that notifies. - pub fn client_active(&self, from: ClientState) { - if from != ClientState::Active { - self.cl_active.fetch_add(1, Ordering::Relaxed); - self.decrease_from_client_state(from); + for client in client_map.values() { + match map.get_mut(&PoolIdentifier { + db: client.pool_name(), + user: client.username(), + }) { + Some(pool_stats) => { + match client.state.load(Ordering::Relaxed) { + ClientState::Active => pool_stats.cl_active += 1, + ClientState::Idle => pool_stats.cl_idle += 1, + ClientState::Waiting => pool_stats.cl_waiting += 1, + } + let max_wait = client.max_wait_time.load(Ordering::Relaxed); + pool_stats.maxwait = std::cmp::max(pool_stats.maxwait, max_wait); + } + None => debug!("Client from an obselete pool"), + } } - } - /// Notified when a client of the pool become 'idle' - /// - /// Arguments: - /// - /// `from`: The state of the client that notifies. - pub fn client_idle(&self, from: ClientState) { - if from != ClientState::Idle { - self.cl_idle.fetch_add(1, Ordering::Relaxed); - self.decrease_from_client_state(from); + for server in server_map.values() { + match map.get_mut(&PoolIdentifier { + db: server.pool_name(), + user: server.username(), + }) { + Some(pool_stats) => match server.state.load(Ordering::Relaxed) { + ServerState::Active => pool_stats.sv_active += 1, + ServerState::Idle => pool_stats.sv_idle += 1, + ServerState::Login => pool_stats.sv_login += 1, + ServerState::Tested => pool_stats.sv_tested += 1, + }, + None => debug!("Server from an obselete pool"), + } } - } - /// Notified when a client disconnects. - /// - /// Arguments: - /// - /// `from`: The state of the client that notifies. - pub fn client_disconnect(&self, from: ClientState) { - let counter = match from { - ClientState::Idle => &self.cl_idle, - ClientState::Waiting => &self.cl_waiting, - ClientState::Active => &self.cl_active, - }; - - Self::decrease_counter(counter.clone()); - } - - /// Notified when a server disconnects. - /// - /// Arguments: - /// - /// `from`: The state of the client that notifies. - pub fn server_disconnect(&self, from: ServerState) { - let counter = match from { - ServerState::Active => &self.sv_active, - ServerState::Idle => &self.sv_idle, - ServerState::Login => &self.sv_login, - ServerState::Tested => &self.sv_tested, - }; - Self::decrease_counter(counter.clone()); - } - - // helpers for counter decrease - fn decrease_from_server_state(&self, from: ServerState) { - let counter = match from { - ServerState::Tested => &self.sv_tested, - ServerState::Active => &self.sv_active, - ServerState::Idle => &self.sv_idle, - ServerState::Login => &self.sv_login, - }; - Self::decrease_counter(counter.clone()); - } - - fn decrease_from_client_state(&self, from: ClientState) { - let counter = match from { - ClientState::Active => &self.cl_active, - ClientState::Idle => &self.cl_idle, - ClientState::Waiting => &self.cl_waiting, - }; - Self::decrease_counter(counter.clone()); - } - - fn decrease_counter(value: Arc) { - if value.load(Ordering::Relaxed) > 0 { - value.fetch_sub(1, Ordering::Relaxed); - } + return map; + } + + pub fn generate_header() -> Vec<(&'static str, DataType)> { + return vec![ + ("database", DataType::Text), + ("user", DataType::Text), + ("pool_mode", DataType::Text), + ("cl_idle", DataType::Numeric), + ("cl_active", DataType::Numeric), + ("cl_waiting", DataType::Numeric), + ("cl_cancel_req", DataType::Numeric), + ("sv_active", DataType::Numeric), + ("sv_idle", DataType::Numeric), + ("sv_used", DataType::Numeric), + ("sv_tested", DataType::Numeric), + ("sv_login", DataType::Numeric), + ("maxwait", DataType::Numeric), + ("maxwait_us", DataType::Numeric), + ]; + } + + pub fn generate_row(&self) -> Vec { + return vec![ + self.identifier.db.clone(), + self.identifier.user.clone(), + self.mode.to_string(), + self.cl_idle.to_string(), + self.cl_active.to_string(), + self.cl_waiting.to_string(), + self.cl_cancel_req.to_string(), + self.sv_active.to_string(), + self.sv_idle.to_string(), + self.sv_used.to_string(), + self.sv_tested.to_string(), + self.sv_login.to_string(), + (self.maxwait / 1_000_000).to_string(), + (self.maxwait % 1_000_000).to_string(), + ]; } } -#[cfg(test)] -mod test { - use super::*; +impl IntoIterator for PoolStats { + type Item = (String, u64); + type IntoIter = std::vec::IntoIter; - #[test] - fn test_decrease() { - let stat: PoolStats = PoolStats::default(); - stat.server_login(ServerState::Login); - stat.server_idle(ServerState::Login); - assert_eq!(stat.sv_login.load(Ordering::Relaxed), 0); - assert_eq!(stat.sv_idle.load(Ordering::Relaxed), 1); + fn into_iter(self) -> Self::IntoIter { + vec![ + ("cl_idle".to_string(), self.cl_idle), + ("cl_active".to_string(), self.cl_active), + ("cl_waiting".to_string(), self.cl_waiting), + ("cl_cancel_req".to_string(), self.cl_cancel_req), + ("sv_active".to_string(), self.sv_active), + ("sv_idle".to_string(), self.sv_idle), + ("sv_used".to_string(), self.sv_used), + ("sv_tested".to_string(), self.sv_tested), + ("sv_login".to_string(), self.sv_login), + ("maxwait".to_string(), self.maxwait / 1_000_000), + ("maxwait_us".to_string(), self.maxwait % 1_000_000), + ] + .into_iter() } } diff --git a/src/stats/server.rs b/src/stats/server.rs index a327fa34..e156ee09 100644 --- a/src/stats/server.rs +++ b/src/stats/server.rs @@ -1,5 +1,4 @@ use super::AddressStats; -use super::PoolStats; use super::{get_reporter, Reporter}; use crate::config::Address; use atomic_enum::atomic_enum; @@ -38,7 +37,6 @@ pub struct ServerStats { address: Address, connect_time: Instant, - pool_stats: Arc, reporter: Reporter, /// Data @@ -57,7 +55,6 @@ impl Default for ServerStats { server_id: 0, application_name: Arc::new(RwLock::new(String::new())), address: Address::default(), - pool_stats: Arc::new(PoolStats::default()), connect_time: Instant::now(), state: Arc::new(AtomicServerState::new(ServerState::Login)), bytes_sent: Arc::new(AtomicU64::new(0)), @@ -71,10 +68,9 @@ impl Default for ServerStats { } impl ServerStats { - pub fn new(address: Address, pool_stats: Arc, connect_time: Instant) -> Self { + pub fn new(address: Address, connect_time: Instant) -> Self { Self { address, - pool_stats, connect_time, server_id: rand::random::(), ..Default::default() @@ -96,9 +92,6 @@ impl ServerStats { /// Reports a server connection is no longer assigned to a client /// and is available for the next client to pick it up pub fn idle(&self) { - self.pool_stats - .server_idle(self.state.load(Ordering::Relaxed)); - self.state.store(ServerState::Idle, Ordering::Relaxed); } @@ -106,22 +99,16 @@ impl ServerStats { /// Also updates metrics on the pool regarding server usage. pub fn disconnect(&self) { self.reporter.server_disconnecting(self.server_id); - self.pool_stats - .server_disconnect(self.state.load(Ordering::Relaxed)) } /// Reports a server connection is being tested before being given to a client. pub fn tested(&self) { self.set_undefined_application(); - self.pool_stats - .server_tested(self.state.load(Ordering::Relaxed)); self.state.store(ServerState::Tested, Ordering::Relaxed); } /// Reports a server connection is attempting to login. pub fn login(&self) { - self.pool_stats - .server_login(self.state.load(Ordering::Relaxed)); self.state.store(ServerState::Login, Ordering::Relaxed); self.set_undefined_application(); } @@ -129,8 +116,6 @@ impl ServerStats { /// Reports a server connection has been assigned to a client that /// is about to query the server pub fn active(&self, application_name: String) { - self.pool_stats - .server_active(self.state.load(Ordering::Relaxed)); self.state.store(ServerState::Active, Ordering::Relaxed); self.set_application(application_name); } @@ -152,11 +137,11 @@ impl ServerStats { // Helper methods for show_servers pub fn pool_name(&self) -> String { - self.pool_stats.database() + self.address.pool_name.clone() } pub fn username(&self) -> String { - self.pool_stats.user() + self.address.username.clone() } pub fn address_name(&self) -> String { @@ -180,9 +165,6 @@ impl ServerStats { // Update server stats and address aggregation stats self.set_application(application_name); self.address.stats.wait_time_add(microseconds); - self.pool_stats - .maxwait - .fetch_max(microseconds, Ordering::Relaxed); } /// Report a query executed by a client against a server diff --git a/tests/ruby/admin_spec.rb b/tests/ruby/admin_spec.rb index f93b1a6c..9a852350 100644 --- a/tests/ruby/admin_spec.rb +++ b/tests/ruby/admin_spec.rb @@ -11,326 +11,6 @@ processes.pgcat.shutdown end - describe "SHOW STATS" do - context "clients connect and make one query" do - it "updates *_query_time and *_wait_time" do - connections = Array.new(3) { PG::connect("#{pgcat_conn_str}?application_name=one_query") } - connections.each do |c| - Thread.new { c.async_exec("SELECT pg_sleep(0.25)") } - end - sleep(1) - connections.map(&:close) - - # wait for averages to be calculated, we shouldn't do this too often - sleep(15.5) - admin_conn = PG::connect(processes.pgcat.admin_connection_string) - results = admin_conn.async_exec("SHOW STATS")[0] - admin_conn.close - expect(results["total_query_time"].to_i).to be_within(200).of(750) - expect(results["avg_query_time"].to_i).to be_within(50).of(250) - - expect(results["total_wait_time"].to_i).to_not eq(0) - expect(results["avg_wait_time"].to_i).to_not eq(0) - end - end - end - - describe "SHOW POOLS" do - context "bad credentials" do - it "does not change any stats" do - bad_password_url = URI(pgcat_conn_str) - bad_password_url.password = "wrong" - expect { PG::connect("#{bad_password_url.to_s}?application_name=bad_password") }.to raise_error(PG::ConnectionBad) - - sleep(1) - admin_conn = PG::connect(processes.pgcat.admin_connection_string) - results = admin_conn.async_exec("SHOW POOLS")[0] - %w[cl_idle cl_active cl_waiting cl_cancel_req sv_active sv_used sv_tested sv_login maxwait].each do |s| - raise StandardError, "Field #{s} was expected to be 0 but found to be #{results[s]}" if results[s] != "0" - end - - expect(results["sv_idle"]).to eq("1") - end - end - - context "bad database name" do - it "does not change any stats" do - bad_db_url = URI(pgcat_conn_str) - bad_db_url.path = "/wrong_db" - expect { PG::connect("#{bad_db_url.to_s}?application_name=bad_db") }.to raise_error(PG::ConnectionBad) - - sleep(1) - admin_conn = PG::connect(processes.pgcat.admin_connection_string) - results = admin_conn.async_exec("SHOW POOLS")[0] - %w[cl_idle cl_active cl_waiting cl_cancel_req sv_active sv_used sv_tested sv_login maxwait].each do |s| - raise StandardError, "Field #{s} was expected to be 0 but found to be #{results[s]}" if results[s] != "0" - end - - expect(results["sv_idle"]).to eq("1") - end - end - - context "client connects but issues no queries" do - it "only affects cl_idle stats" do - admin_conn = PG::connect(processes.pgcat.admin_connection_string) - - before_test = admin_conn.async_exec("SHOW POOLS")[0]["sv_idle"] - connections = Array.new(20) { PG::connect(pgcat_conn_str) } - sleep(1) - results = admin_conn.async_exec("SHOW POOLS")[0] - %w[cl_active cl_waiting cl_cancel_req sv_active sv_used sv_tested sv_login maxwait].each do |s| - raise StandardError, "Field #{s} was expected to be 0 but found to be #{results[s]}" if results[s] != "0" - end - expect(results["cl_idle"]).to eq("20") - expect(results["sv_idle"]).to eq(before_test) - - connections.map(&:close) - sleep(1.1) - results = admin_conn.async_exec("SHOW POOLS")[0] - %w[cl_active cl_idle cl_waiting cl_cancel_req sv_active sv_used sv_tested sv_login maxwait].each do |s| - raise StandardError, "Field #{s} was expected to be 0 but found to be #{results[s]}" if results[s] != "0" - end - expect(results["sv_idle"]).to eq(before_test) - end - end - - context "clients connect and make one query" do - it "only affects cl_idle, sv_idle stats" do - connections = Array.new(5) { PG::connect("#{pgcat_conn_str}?application_name=one_query") } - connections.each do |c| - Thread.new { c.async_exec("SELECT pg_sleep(2.5)") } - end - - sleep(1.1) - admin_conn = PG::connect(processes.pgcat.admin_connection_string) - results = admin_conn.async_exec("SHOW POOLS")[0] - %w[cl_idle cl_waiting cl_cancel_req sv_idle sv_used sv_tested sv_login maxwait].each do |s| - raise StandardError, "Field #{s} was expected to be 0 but found to be #{results[s]}" if results[s] != "0" - end - expect(results["cl_active"]).to eq("5") - expect(results["sv_active"]).to eq("5") - - sleep(3) - results = admin_conn.async_exec("SHOW POOLS")[0] - %w[cl_active cl_waiting cl_cancel_req sv_active sv_used sv_tested sv_login maxwait].each do |s| - raise StandardError, "Field #{s} was expected to be 0 but found to be #{results[s]}" if results[s] != "0" - end - expect(results["cl_idle"]).to eq("5") - expect(results["sv_idle"]).to eq("5") - - connections.map(&:close) - sleep(1) - results = admin_conn.async_exec("SHOW POOLS")[0] - %w[cl_idle cl_active cl_waiting cl_cancel_req sv_active sv_used sv_tested sv_login maxwait].each do |s| - raise StandardError, "Field #{s} was expected to be 0 but found to be #{results[s]}" if results[s] != "0" - end - expect(results["sv_idle"]).to eq("5") - end - end - - context "client connects and opens a transaction and closes connection uncleanly" do - it "produces correct statistics" do - connections = Array.new(5) { PG::connect("#{pgcat_conn_str}?application_name=one_query") } - connections.each do |c| - Thread.new do - c.async_exec("BEGIN") - c.async_exec("SELECT pg_sleep(0.01)") - c.close - end - end - - sleep(1.1) - admin_conn = PG::connect(processes.pgcat.admin_connection_string) - results = admin_conn.async_exec("SHOW POOLS")[0] - %w[cl_idle cl_active cl_waiting cl_cancel_req sv_active sv_used sv_tested sv_login maxwait].each do |s| - raise StandardError, "Field #{s} was expected to be 0 but found to be #{results[s]}" if results[s] != "0" - end - expect(results["sv_idle"]).to eq("5") - end - end - - context "client fail to checkout connection from the pool" do - it "counts clients as idle" do - new_configs = processes.pgcat.current_config - new_configs["general"]["connect_timeout"] = 500 - new_configs["general"]["ban_time"] = 1 - new_configs["general"]["shutdown_timeout"] = 1 - new_configs["pools"]["sharded_db"]["users"]["0"]["pool_size"] = 1 - processes.pgcat.update_config(new_configs) - processes.pgcat.reload_config - - threads = [] - connections = Array.new(5) { PG::connect("#{pgcat_conn_str}?application_name=one_query") } - connections.each do |c| - threads << Thread.new { c.async_exec("SELECT pg_sleep(1)") rescue PG::SystemError } - end - - sleep(2) - admin_conn = PG::connect(processes.pgcat.admin_connection_string) - results = admin_conn.async_exec("SHOW POOLS")[0] - %w[cl_active cl_waiting cl_cancel_req sv_active sv_used sv_tested sv_login maxwait].each do |s| - raise StandardError, "Field #{s} was expected to be 0 but found to be #{results[s]}" if results[s] != "0" - end - expect(results["cl_idle"]).to eq("5") - expect(results["sv_idle"]).to eq("1") - - threads.map(&:join) - connections.map(&:close) - end - end - - context "clients connects and disconnect normally" do - let(:processes) { Helpers::Pgcat.single_instance_setup("sharded_db", 2) } - - it 'shows the same number of clients before and after' do - clients_before = clients_connected_to_pool(processes: processes) - threads = [] - connections = Array.new(4) { PG::connect("#{pgcat_conn_str}?application_name=one_query") } - connections.each do |c| - threads << Thread.new { c.async_exec("SELECT 1") } - end - clients_between = clients_connected_to_pool(processes: processes) - expect(clients_before).not_to eq(clients_between) - connections.each(&:close) - clients_after = clients_connected_to_pool(processes: processes) - expect(clients_before).to eq(clients_after) - end - end - - context "clients connects and disconnect abruptly" do - let(:processes) { Helpers::Pgcat.single_instance_setup("sharded_db", 10) } - - it 'shows the same number of clients before and after' do - threads = [] - connections = Array.new(2) { PG::connect("#{pgcat_conn_str}?application_name=one_query") } - connections.each do |c| - threads << Thread.new { c.async_exec("SELECT 1") } - end - clients_before = clients_connected_to_pool(processes: processes) - random_string = (0...8).map { (65 + rand(26)).chr }.join - connection_string = "#{pgcat_conn_str}?application_name=#{random_string}" - faulty_client = Process.spawn("psql -Atx #{connection_string} >/dev/null") - sleep(1) - # psql starts two processes, we only know the pid of the parent, this - # ensure both are killed - `pkill -9 -f '#{random_string}'` - Process.wait(faulty_client) - clients_after = clients_connected_to_pool(processes: processes) - expect(clients_before).to eq(clients_after) - end - end - - context "clients overwhelm server pools" do - let(:processes) { Helpers::Pgcat.single_instance_setup("sharded_db", 2) } - - it "cl_waiting is updated to show it" do - threads = [] - connections = Array.new(4) { PG::connect("#{pgcat_conn_str}?application_name=one_query") } - connections.each do |c| - threads << Thread.new { c.async_exec("SELECT pg_sleep(1.5)") } - end - - sleep(1.1) # Allow time for stats to update - admin_conn = PG::connect(processes.pgcat.admin_connection_string) - results = admin_conn.async_exec("SHOW POOLS")[0] - %w[cl_idle cl_cancel_req sv_idle sv_used sv_tested sv_login maxwait].each do |s| - raise StandardError, "Field #{s} was expected to be 0 but found to be #{results[s]}" if results[s] != "0" - end - - expect(results["cl_waiting"]).to eq("2") - expect(results["cl_active"]).to eq("2") - expect(results["sv_active"]).to eq("2") - - sleep(2.5) # Allow time for stats to update - results = admin_conn.async_exec("SHOW POOLS")[0] - %w[cl_active cl_waiting cl_cancel_req sv_active sv_used sv_tested sv_login].each do |s| - raise StandardError, "Field #{s} was expected to be 0 but found to be #{results[s]}" if results[s] != "0" - end - expect(results["cl_idle"]).to eq("4") - expect(results["sv_idle"]).to eq("2") - - threads.map(&:join) - connections.map(&:close) - end - - it "show correct max_wait" do - threads = [] - connections = Array.new(4) { PG::connect("#{pgcat_conn_str}?application_name=one_query") } - connections.each do |c| - threads << Thread.new { c.async_exec("SELECT pg_sleep(1.5)") } - end - - sleep(2.5) # Allow time for stats to update - admin_conn = PG::connect(processes.pgcat.admin_connection_string) - results = admin_conn.async_exec("SHOW POOLS")[0] - - expect(results["maxwait"]).to eq("1") - expect(results["maxwait_us"].to_i).to be_within(200_000).of(500_000) - - sleep(4.5) # Allow time for stats to update - results = admin_conn.async_exec("SHOW POOLS")[0] - expect(results["maxwait"]).to eq("0") - - threads.map(&:join) - connections.map(&:close) - end - end - end - - describe "SHOW CLIENTS" do - it "reports correct number and application names" do - conn_str = processes.pgcat.connection_string("sharded_db", "sharding_user") - connections = Array.new(20) { |i| PG::connect("#{conn_str}?application_name=app#{i % 5}") } - - admin_conn = PG::connect(processes.pgcat.admin_connection_string) - sleep(1) # Wait for stats to be updated - - results = admin_conn.async_exec("SHOW CLIENTS") - expect(results.count).to eq(21) # count admin clients - expect(results.select { |c| c["application_name"] == "app3" || c["application_name"] == "app4" }.count).to eq(8) - expect(results.select { |c| c["database"] == "pgcat" }.count).to eq(1) - - connections[0..5].map(&:close) - sleep(1) # Wait for stats to be updated - results = admin_conn.async_exec("SHOW CLIENTS") - expect(results.count).to eq(15) - - connections[6..].map(&:close) - sleep(1) # Wait for stats to be updated - expect(admin_conn.async_exec("SHOW CLIENTS").count).to eq(1) - admin_conn.close - end - - it "reports correct number of queries and transactions" do - conn_str = processes.pgcat.connection_string("sharded_db", "sharding_user") - - connections = Array.new(2) { |i| PG::connect("#{conn_str}?application_name=app#{i}") } - connections.each do |c| - c.async_exec("SELECT 1") - c.async_exec("SELECT 2") - c.async_exec("SELECT 3") - c.async_exec("BEGIN") - c.async_exec("SELECT 4") - c.async_exec("SELECT 5") - c.async_exec("COMMIT") - end - - admin_conn = PG::connect(processes.pgcat.admin_connection_string) - sleep(1) # Wait for stats to be updated - - results = admin_conn.async_exec("SHOW CLIENTS") - expect(results.count).to eq(3) - normal_client_results = results.reject { |r| r["database"] == "pgcat" } - expect(normal_client_results[0]["transaction_count"]).to eq("4") - expect(normal_client_results[1]["transaction_count"]).to eq("4") - expect(normal_client_results[0]["query_count"]).to eq("7") - expect(normal_client_results[1]["query_count"]).to eq("7") - - admin_conn.close - connections.map(&:close) - end - end - describe "Manual Banning" do let(:processes) { Helpers::Pgcat.single_shard_setup("sharded_db", 10) } before do @@ -401,7 +81,7 @@ end end - describe "SHOW users" do + describe "SHOW USERS" do it "returns the right users" do admin_conn = PG::connect(processes.pgcat.admin_connection_string) results = admin_conn.async_exec("SHOW USERS")[0] diff --git a/tests/ruby/stats_spec.rb b/tests/ruby/stats_spec.rb new file mode 100644 index 00000000..132b80a9 --- /dev/null +++ b/tests/ruby/stats_spec.rb @@ -0,0 +1,369 @@ +# frozen_string_literal: true +require 'open3' +require_relative 'spec_helper' + +describe "Stats" do + let(:processes) { Helpers::Pgcat.single_instance_setup("sharded_db", 10) } + let(:pgcat_conn_str) { processes.pgcat.connection_string("sharded_db", "sharding_user") } + + after do + processes.all_databases.map(&:reset) + processes.pgcat.shutdown + end + + describe "SHOW STATS" do + context "clients connect and make one query" do + it "updates *_query_time and *_wait_time" do + connections = Array.new(3) { PG::connect("#{pgcat_conn_str}?application_name=one_query") } + connections.each do |c| + Thread.new { c.async_exec("SELECT pg_sleep(0.25)") } + end + sleep(1) + connections.map(&:close) + + # wait for averages to be calculated, we shouldn't do this too often + sleep(15.5) + admin_conn = PG::connect(processes.pgcat.admin_connection_string) + results = admin_conn.async_exec("SHOW STATS")[0] + admin_conn.close + expect(results["total_query_time"].to_i).to be_within(200).of(750) + expect(results["avg_query_time"].to_i).to be_within(50).of(250) + + expect(results["total_wait_time"].to_i).to_not eq(0) + expect(results["avg_wait_time"].to_i).to_not eq(0) + end + end + end + + describe "SHOW POOLS" do + context "bad credentials" do + it "does not change any stats" do + bad_password_url = URI(pgcat_conn_str) + bad_password_url.password = "wrong" + expect { PG::connect("#{bad_password_url.to_s}?application_name=bad_password") }.to raise_error(PG::ConnectionBad) + + sleep(1) + admin_conn = PG::connect(processes.pgcat.admin_connection_string) + results = admin_conn.async_exec("SHOW POOLS")[0] + %w[cl_idle cl_active cl_waiting cl_cancel_req sv_active sv_used sv_tested sv_login maxwait].each do |s| + raise StandardError, "Field #{s} was expected to be 0 but found to be #{results[s]}" if results[s] != "0" + end + + expect(results["sv_idle"]).to eq("1") + end + end + + context "bad database name" do + it "does not change any stats" do + bad_db_url = URI(pgcat_conn_str) + bad_db_url.path = "/wrong_db" + expect { PG::connect("#{bad_db_url.to_s}?application_name=bad_db") }.to raise_error(PG::ConnectionBad) + + sleep(1) + admin_conn = PG::connect(processes.pgcat.admin_connection_string) + results = admin_conn.async_exec("SHOW POOLS")[0] + %w[cl_idle cl_active cl_waiting cl_cancel_req sv_active sv_used sv_tested sv_login maxwait].each do |s| + raise StandardError, "Field #{s} was expected to be 0 but found to be #{results[s]}" if results[s] != "0" + end + + expect(results["sv_idle"]).to eq("1") + end + end + + context "client connects but issues no queries" do + it "only affects cl_idle stats" do + admin_conn = PG::connect(processes.pgcat.admin_connection_string) + + before_test = admin_conn.async_exec("SHOW POOLS")[0]["sv_idle"] + connections = Array.new(20) { PG::connect(pgcat_conn_str) } + sleep(1) + results = admin_conn.async_exec("SHOW POOLS")[0] + %w[cl_active cl_waiting cl_cancel_req sv_active sv_used sv_tested sv_login maxwait].each do |s| + raise StandardError, "Field #{s} was expected to be 0 but found to be #{results[s]}" if results[s] != "0" + end + expect(results["cl_idle"]).to eq("20") + expect(results["sv_idle"]).to eq(before_test) + + connections.map(&:close) + sleep(1.1) + results = admin_conn.async_exec("SHOW POOLS")[0] + %w[cl_active cl_idle cl_waiting cl_cancel_req sv_active sv_used sv_tested sv_login maxwait].each do |s| + raise StandardError, "Field #{s} was expected to be 0 but found to be #{results[s]}" if results[s] != "0" + end + expect(results["sv_idle"]).to eq(before_test) + end + end + + context "clients connect and make one query" do + it "only affects cl_idle, sv_idle stats" do + connections = Array.new(5) { PG::connect("#{pgcat_conn_str}?application_name=one_query") } + connections.each do |c| + Thread.new { c.async_exec("SELECT pg_sleep(2.5)") } + end + + sleep(1.1) + admin_conn = PG::connect(processes.pgcat.admin_connection_string) + results = admin_conn.async_exec("SHOW POOLS")[0] + %w[cl_idle cl_waiting cl_cancel_req sv_idle sv_used sv_tested sv_login maxwait].each do |s| + raise StandardError, "Field #{s} was expected to be 0 but found to be #{results[s]}" if results[s] != "0" + end + expect(results["cl_active"]).to eq("5") + expect(results["sv_active"]).to eq("5") + + sleep(3) + results = admin_conn.async_exec("SHOW POOLS")[0] + %w[cl_active cl_waiting cl_cancel_req sv_active sv_used sv_tested sv_login maxwait].each do |s| + raise StandardError, "Field #{s} was expected to be 0 but found to be #{results[s]}" if results[s] != "0" + end + expect(results["cl_idle"]).to eq("5") + expect(results["sv_idle"]).to eq("5") + + connections.map(&:close) + sleep(1) + results = admin_conn.async_exec("SHOW POOLS")[0] + %w[cl_idle cl_active cl_waiting cl_cancel_req sv_active sv_used sv_tested sv_login maxwait].each do |s| + raise StandardError, "Field #{s} was expected to be 0 but found to be #{results[s]}" if results[s] != "0" + end + expect(results["sv_idle"]).to eq("5") + end + end + + context "client connects and opens a transaction and closes connection uncleanly" do + it "produces correct statistics" do + connections = Array.new(5) { PG::connect("#{pgcat_conn_str}?application_name=one_query") } + connections.each do |c| + Thread.new do + c.async_exec("BEGIN") + c.async_exec("SELECT pg_sleep(0.01)") + c.close + end + end + + sleep(1.1) + admin_conn = PG::connect(processes.pgcat.admin_connection_string) + results = admin_conn.async_exec("SHOW POOLS")[0] + %w[cl_idle cl_active cl_waiting cl_cancel_req sv_active sv_used sv_tested sv_login maxwait].each do |s| + raise StandardError, "Field #{s} was expected to be 0 but found to be #{results[s]}" if results[s] != "0" + end + expect(results["sv_idle"]).to eq("5") + end + end + + context "client fail to checkout connection from the pool" do + it "counts clients as idle" do + new_configs = processes.pgcat.current_config + new_configs["general"]["connect_timeout"] = 500 + new_configs["general"]["ban_time"] = 1 + new_configs["general"]["shutdown_timeout"] = 1 + new_configs["pools"]["sharded_db"]["users"]["0"]["pool_size"] = 1 + processes.pgcat.update_config(new_configs) + processes.pgcat.reload_config + + threads = [] + connections = Array.new(5) { PG::connect("#{pgcat_conn_str}?application_name=one_query") } + connections.each do |c| + threads << Thread.new { c.async_exec("SELECT pg_sleep(1)") rescue PG::SystemError } + end + + sleep(2) + admin_conn = PG::connect(processes.pgcat.admin_connection_string) + results = admin_conn.async_exec("SHOW POOLS")[0] + %w[cl_active cl_waiting cl_cancel_req sv_active sv_used sv_tested sv_login maxwait].each do |s| + raise StandardError, "Field #{s} was expected to be 0 but found to be #{results[s]}" if results[s] != "0" + end + expect(results["cl_idle"]).to eq("5") + expect(results["sv_idle"]).to eq("1") + + threads.map(&:join) + connections.map(&:close) + end + end + + context "clients connects and disconnect normally" do + let(:processes) { Helpers::Pgcat.single_instance_setup("sharded_db", 2) } + + it 'shows the same number of clients before and after' do + clients_before = clients_connected_to_pool(processes: processes) + threads = [] + connections = Array.new(4) { PG::connect("#{pgcat_conn_str}?application_name=one_query") } + connections.each do |c| + threads << Thread.new { c.async_exec("SELECT 1") rescue nil } + end + clients_between = clients_connected_to_pool(processes: processes) + expect(clients_before).not_to eq(clients_between) + connections.each(&:close) + clients_after = clients_connected_to_pool(processes: processes) + expect(clients_before).to eq(clients_after) + end + end + + context "clients connects and disconnect abruptly" do + let(:processes) { Helpers::Pgcat.single_instance_setup("sharded_db", 10) } + + it 'shows the same number of clients before and after' do + threads = [] + connections = Array.new(2) { PG::connect("#{pgcat_conn_str}?application_name=one_query") } + connections.each do |c| + threads << Thread.new { c.async_exec("SELECT 1") } + end + clients_before = clients_connected_to_pool(processes: processes) + random_string = (0...8).map { (65 + rand(26)).chr }.join + connection_string = "#{pgcat_conn_str}?application_name=#{random_string}" + faulty_client = Process.spawn("psql -Atx #{connection_string} >/dev/null") + sleep(1) + # psql starts two processes, we only know the pid of the parent, this + # ensure both are killed + `pkill -9 -f '#{random_string}'` + Process.wait(faulty_client) + clients_after = clients_connected_to_pool(processes: processes) + expect(clients_before).to eq(clients_after) + end + end + + context "clients overwhelm server pools" do + let(:processes) { Helpers::Pgcat.single_instance_setup("sharded_db", 2) } + + it "cl_waiting is updated to show it" do + threads = [] + connections = Array.new(4) { PG::connect("#{pgcat_conn_str}?application_name=one_query") } + connections.each do |c| + threads << Thread.new { c.async_exec("SELECT pg_sleep(1.5)") } + end + + sleep(1.1) # Allow time for stats to update + admin_conn = PG::connect(processes.pgcat.admin_connection_string) + results = admin_conn.async_exec("SHOW POOLS")[0] + %w[cl_idle cl_cancel_req sv_idle sv_used sv_tested sv_login maxwait].each do |s| + raise StandardError, "Field #{s} was expected to be 0 but found to be #{results[s]}" if results[s] != "0" + end + + expect(results["cl_waiting"]).to eq("2") + expect(results["cl_active"]).to eq("2") + expect(results["sv_active"]).to eq("2") + + sleep(2.5) # Allow time for stats to update + results = admin_conn.async_exec("SHOW POOLS")[0] + %w[cl_active cl_waiting cl_cancel_req sv_active sv_used sv_tested sv_login].each do |s| + raise StandardError, "Field #{s} was expected to be 0 but found to be #{results[s]}" if results[s] != "0" + end + expect(results["cl_idle"]).to eq("4") + expect(results["sv_idle"]).to eq("2") + + threads.map(&:join) + connections.map(&:close) + end + + it "show correct max_wait" do + threads = [] + connections = Array.new(4) { PG::connect("#{pgcat_conn_str}?application_name=one_query") } + connections.each do |c| + threads << Thread.new { c.async_exec("SELECT pg_sleep(1.5)") rescue nil } + end + + sleep(2.5) # Allow time for stats to update + admin_conn = PG::connect(processes.pgcat.admin_connection_string) + results = admin_conn.async_exec("SHOW POOLS")[0] + + expect(results["maxwait"]).to eq("1") + expect(results["maxwait_us"].to_i).to be_within(200_000).of(500_000) + connections.map(&:close) + + sleep(4.5) # Allow time for stats to update + results = admin_conn.async_exec("SHOW POOLS")[0] + expect(results["maxwait"]).to eq("0") + + threads.map(&:join) + end + end + end + + describe "SHOW CLIENTS" do + it "reports correct number and application names" do + conn_str = processes.pgcat.connection_string("sharded_db", "sharding_user") + connections = Array.new(20) { |i| PG::connect("#{conn_str}?application_name=app#{i % 5}") } + + admin_conn = PG::connect(processes.pgcat.admin_connection_string) + sleep(1) # Wait for stats to be updated + + results = admin_conn.async_exec("SHOW CLIENTS") + expect(results.count).to eq(21) # count admin clients + expect(results.select { |c| c["application_name"] == "app3" || c["application_name"] == "app4" }.count).to eq(8) + expect(results.select { |c| c["database"] == "pgcat" }.count).to eq(1) + + connections[0..5].map(&:close) + sleep(1) # Wait for stats to be updated + results = admin_conn.async_exec("SHOW CLIENTS") + expect(results.count).to eq(15) + + connections[6..].map(&:close) + sleep(1) # Wait for stats to be updated + expect(admin_conn.async_exec("SHOW CLIENTS").count).to eq(1) + admin_conn.close + end + + it "reports correct number of queries and transactions" do + conn_str = processes.pgcat.connection_string("sharded_db", "sharding_user") + + connections = Array.new(2) { |i| PG::connect("#{conn_str}?application_name=app#{i}") } + connections.each do |c| + c.async_exec("SELECT 1") + c.async_exec("SELECT 2") + c.async_exec("SELECT 3") + c.async_exec("BEGIN") + c.async_exec("SELECT 4") + c.async_exec("SELECT 5") + c.async_exec("COMMIT") + end + + admin_conn = PG::connect(processes.pgcat.admin_connection_string) + sleep(1) # Wait for stats to be updated + + results = admin_conn.async_exec("SHOW CLIENTS") + expect(results.count).to eq(3) + normal_client_results = results.reject { |r| r["database"] == "pgcat" } + expect(normal_client_results[0]["transaction_count"]).to eq("4") + expect(normal_client_results[1]["transaction_count"]).to eq("4") + expect(normal_client_results[0]["query_count"]).to eq("7") + expect(normal_client_results[1]["query_count"]).to eq("7") + + admin_conn.close + connections.map(&:close) + end + end + + + describe "Query Storm" do + context "when the proxy receives overwhelmingly large number of short quick queries" do + it "should not have lingering clients or active servers" do + new_configs = processes.pgcat.current_config + + new_configs["general"]["connect_timeout"] = 500 + new_configs["general"]["ban_time"] = 1 + new_configs["general"]["shutdown_timeout"] = 1 + new_configs["pools"]["sharded_db"]["users"]["0"]["pool_size"] = 1 + processes.pgcat.update_config(new_configs) + processes.pgcat.reload_config + + Array.new(40) do + Thread.new do + conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) + conn.async_exec("SELECT pg_sleep(0.1)") + rescue PG::SystemError + ensure + conn.close + end + end.each(&:join) + + sleep 1 + + admin_conn = PG::connect(processes.pgcat.admin_connection_string) + results = admin_conn.async_exec("SHOW POOLS")[0] + %w[cl_idle cl_waiting cl_cancel_req sv_used sv_tested sv_login].each do |s| + raise StandardError, "Field #{s} was expected to be 0 but found to be #{results[s]}" if results[s] != "0" + end + + admin_conn.close + end + end + end +end From b67c33b6d044fb5e243d3d9681f5499958ce5066 Mon Sep 17 00:00:00 2001 From: Zain Kabani <77307340+zainkabani@users.noreply.github.com> Date: Sun, 28 May 2023 22:46:13 -0400 Subject: [PATCH 53/79] Use latest bb8 and use Lifo as the queue strategy in the pool (#455) * Use git bb8 * Use latest bb8 and change pool is use stack --- Cargo.lock | 3 +-- Cargo.toml | 2 +- src/pool.rs | 3 ++- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d26c4d52..c99453a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -84,8 +84,7 @@ checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" [[package]] name = "bb8" version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1627eccf3aa91405435ba240be23513eeca466b5dc33866422672264de061582" +source = "git+https://github.com/djc/bb8.git?rev=ad653e0#ad653e021607eb1f90ed6ce554d1766920308ffa" dependencies = [ "async-trait", "futures-channel", diff --git a/Cargo.toml b/Cargo.toml index 17f7e0e7..cee5683a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ edition = "2021" tokio = { version = "1", features = ["full"] } bytes = "1" md-5 = "0.10" -bb8 = "0.8.0" +bb8 = { git = "https://github.com/djc/bb8.git", rev = "ad653e0" } # https://github.com/djc/bb8/commit/ad653e021607eb1f90ed6ce554d1766920308ffa async-trait = "0.1" rand = "0.8" chrono = "0.4" diff --git a/src/pool.rs b/src/pool.rs index 4fa27753..dff7b8e6 100644 --- a/src/pool.rs +++ b/src/pool.rs @@ -1,6 +1,6 @@ use arc_swap::ArcSwap; use async_trait::async_trait; -use bb8::{ManageConnection, Pool, PooledConnection}; +use bb8::{ManageConnection, Pool, PooledConnection, QueueStrategy}; use bytes::{BufMut, BytesMut}; use chrono::naive::NaiveDateTime; use log::{debug, error, info, warn}; @@ -401,6 +401,7 @@ impl ConnectionPool { .idle_timeout(Some(std::time::Duration::from_millis(idle_timeout))) .max_lifetime(Some(std::time::Duration::from_millis(server_lifetime))) .reaper_rate(std::time::Duration::from_millis(reaper_rate)) + .queue_strategy(QueueStrategy::Lifo) .test_on_check_out(false); let pool = if config.general.validate_config { From 0bc453a771907078cc29f81bafbbb082b80e331e Mon Sep 17 00:00:00 2001 From: Zain Kabani <77307340+zainkabani@users.noreply.github.com> Date: Wed, 31 May 2023 11:25:42 -0400 Subject: [PATCH 54/79] Change default server lifetime and bump bb8 version to use LIFO correctly (#462) Change default server lifetime and idle timeouts and bump bb8 version to use LIFO correctly --- Cargo.lock | 5 +++-- Cargo.toml | 2 +- src/config.rs | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c99453a9..95fd682d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -83,8 +83,9 @@ checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" [[package]] name = "bb8" -version = "0.8.0" -source = "git+https://github.com/djc/bb8.git?rev=ad653e0#ad653e021607eb1f90ed6ce554d1766920308ffa" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98b4b0f25f18bcdc3ac72bdb486ed0acf7e185221fd4dc985bc15db5800b0ba2" dependencies = [ "async-trait", "futures-channel", diff --git a/Cargo.toml b/Cargo.toml index cee5683a..e2632359 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ edition = "2021" tokio = { version = "1", features = ["full"] } bytes = "1" md-5 = "0.10" -bb8 = { git = "https://github.com/djc/bb8.git", rev = "ad653e0" } # https://github.com/djc/bb8/commit/ad653e021607eb1f90ed6ce554d1766920308ffa +bb8 = "0.8.1" async-trait = "0.1" rand = "0.8" chrono = "0.4" diff --git a/src/config.rs b/src/config.rs index 2f5596c6..4468f739 100644 --- a/src/config.rs +++ b/src/config.rs @@ -329,7 +329,7 @@ impl General { } pub fn default_server_lifetime() -> u64 { - 1000 * 60 * 60 * 24 // 24 hours + 1000 * 60 * 60 // 1 hour } pub fn default_connect_timeout() -> u64 { @@ -352,7 +352,7 @@ impl General { } pub fn default_idle_timeout() -> u64 { - 60000 // 10 minutes + 60000 // 1 minute } pub fn default_shutdown_timeout() -> u64 { From aca97388212baf5954958890ee798977f8a94d44 Mon Sep 17 00:00:00 2001 From: Zain Kabani <77307340+zainkabani@users.noreply.github.com> Date: Fri, 9 Jun 2023 14:35:20 -0400 Subject: [PATCH 55/79] Make queue strategy configurable and default to Fifo (#463) * Change idle timeout default to 10 minutes * Revert lifo for now while we investigate connection thrashing issues * Make queue strategy configurable * test revert idle time out * Add pgcat start to python test --- src/config.rs | 13 +++++++++++-- src/pool.rs | 7 ++++++- tests/python/tests.py | 1 + 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/config.rs b/src/config.rs index 4468f739..7573d39b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -292,6 +292,9 @@ pub struct General { #[serde(default = "General::default_server_lifetime")] pub server_lifetime: u64, + #[serde(default = "General::default_server_round_robin")] // False + pub server_round_robin: bool, + #[serde(default = "General::default_worker_threads")] pub worker_threads: usize, @@ -352,7 +355,7 @@ impl General { } pub fn default_idle_timeout() -> u64 { - 60000 // 1 minute + 600000 // 10 minutes } pub fn default_shutdown_timeout() -> u64 { @@ -390,6 +393,10 @@ impl General { pub fn default_prometheus_exporter_port() -> i16 { 9930 } + + pub fn default_server_round_robin() -> bool { + true + } } impl Default for General { @@ -424,7 +431,8 @@ impl Default for General { auth_query: None, auth_query_user: None, auth_query_password: None, - server_lifetime: 1000 * 3600 * 24, // 24 hours, + server_lifetime: Self::default_server_lifetime(), + server_round_robin: false, validate_config: true, } } @@ -983,6 +991,7 @@ impl Config { "Default max server lifetime: {}ms", self.general.server_lifetime ); + info!("Sever round robin: {}", self.general.server_round_robin); match self.general.tls_certificate.clone() { Some(tls_certificate) => { info!("TLS certificate: {}", tls_certificate); diff --git a/src/pool.rs b/src/pool.rs index dff7b8e6..b9293521 100644 --- a/src/pool.rs +++ b/src/pool.rs @@ -389,6 +389,11 @@ impl ConnectionPool { .min() .unwrap(); + let queue_strategy = match config.general.server_round_robin { + true => QueueStrategy::Fifo, + false => QueueStrategy::Lifo, + }; + debug!( "[pool: {}][user: {}] Pool reaper rate: {}ms", pool_name, user.username, reaper_rate @@ -401,7 +406,7 @@ impl ConnectionPool { .idle_timeout(Some(std::time::Duration::from_millis(idle_timeout))) .max_lifetime(Some(std::time::Duration::from_millis(server_lifetime))) .reaper_rate(std::time::Duration::from_millis(reaper_rate)) - .queue_strategy(QueueStrategy::Lifo) + .queue_strategy(queue_strategy) .test_on_check_out(false); let pool = if config.general.validate_config { diff --git a/tests/python/tests.py b/tests/python/tests.py index 6108ff21..cd54081f 100644 --- a/tests/python/tests.py +++ b/tests/python/tests.py @@ -63,6 +63,7 @@ def cleanup_conn(conn: psycopg2.extensions.connection, cur: psycopg2.extensions. def test_normal_db_access(): + pgcat_start() conn, cur = connect_db(autocommit=False) cur.execute("SELECT 1") res = cur.fetchall() From 557b425fb1be6e464f6a5d2f1bd6363f5076e686 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Jun 2023 00:50:26 -0700 Subject: [PATCH 56/79] chore(deps): bump log from 0.4.17 to 0.4.19 (#470) Bumps [log](https://github.com/rust-lang/log) from 0.4.17 to 0.4.19. - [Release notes](https://github.com/rust-lang/log/releases) - [Changelog](https://github.com/rust-lang/log/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-lang/log/compare/0.4.17...0.4.19) --- updated-dependencies: - dependency-name: log dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 95fd682d..f4329dfc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -753,12 +753,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.17" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" [[package]] name = "lru-cache" From b5489dc1e647119a83158195b9bfb9201a6d5097 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Jun 2023 00:50:46 -0700 Subject: [PATCH 57/79] chore(deps): bump regex from 1.8.1 to 1.8.4 (#466) Bumps [regex](https://github.com/rust-lang/regex) from 1.8.1 to 1.8.4. - [Release notes](https://github.com/rust-lang/regex/releases) - [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-lang/regex/compare/1.8.1...1.8.4) --- updated-dependencies: - dependency-name: regex dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f4329dfc..f2b9a440 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1103,9 +1103,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.8.1" +version = "1.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370" +checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" dependencies = [ "aho-corasick", "memchr", @@ -1114,9 +1114,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" +checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" [[package]] name = "resolv-conf" From 1d3746ec9e53b4e584eb36914fa4bd76085c4e49 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Jun 2023 00:51:05 -0700 Subject: [PATCH 58/79] chore(deps): bump sqlparser from 0.33.0 to 0.34.0 (#448) Bumps [sqlparser](https://github.com/sqlparser-rs/sqlparser-rs) from 0.33.0 to 0.34.0. - [Changelog](https://github.com/sqlparser-rs/sqlparser-rs/blob/main/CHANGELOG.md) - [Commits](https://github.com/sqlparser-rs/sqlparser-rs/compare/v0.33.0...v0.34.0) --- updated-dependencies: - dependency-name: sqlparser dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f2b9a440..51ba9652 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1326,9 +1326,9 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "sqlparser" -version = "0.33.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "355dc4d4b6207ca8a3434fc587db0a8016130a574dbcdbfb93d7f7b5bc5b211a" +checksum = "37d3706eefb17039056234df6b566b0014f303f867f2656108334a55b8096f59" dependencies = [ "log", "sqlparser_derive", diff --git a/Cargo.toml b/Cargo.toml index e2632359..a70c7e93 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ serde_derive = "1" regex = "1" num_cpus = "1" once_cell = "1" -sqlparser = {version = "0.33", features = ["visitor"] } +sqlparser = {version = "0.34", features = ["visitor"] } log = "0.4" arc-swap = "1" env_logger = "0.10" From a8c81e5df67522f9683f034c233a2cdb9a004f82 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Jun 2023 00:51:24 -0700 Subject: [PATCH 59/79] chore(deps): bump pin-project from 1.0.12 to 1.1.0 (#440) Bumps [pin-project](https://github.com/taiki-e/pin-project) from 1.0.12 to 1.1.0. - [Release notes](https://github.com/taiki-e/pin-project/releases) - [Changelog](https://github.com/taiki-e/pin-project/blob/main/CHANGELOG.md) - [Commits](https://github.com/taiki-e/pin-project/compare/v1.0.12...v1.1.0) --- updated-dependencies: - dependency-name: pin-project dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 51ba9652..0baef1ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -984,22 +984,22 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.0.12" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" +checksum = "c95a7476719eab1e366eaf73d0260af3021184f18177925b07f54b30089ceead" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.0.12" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" +checksum = "39407670928234ebc5e6e580247dd567ad73a3578460c5990f9503df207e8f07" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.9", ] [[package]] From 94c781881f166a1dffac27ecdbe7f876ddf113ba Mon Sep 17 00:00:00 2001 From: Jeff Chen Date: Mon, 12 Jun 2023 10:23:56 -0600 Subject: [PATCH 60/79] Report min_pool_size correctly (#471) --- src/admin.rs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/admin.rs b/src/admin.rs index 804476c9..9ae5e9d5 100644 --- a/src/admin.rs +++ b/src/admin.rs @@ -308,17 +308,17 @@ where let paused = pool.paused(); res.put(data_row(&vec![ - address.name(), // name - address.host.to_string(), // host - address.port.to_string(), // port - database_name.to_string(), // database - pool_config.user.username.to_string(), // force_user - pool_config.user.pool_size.to_string(), // pool_size - "0".to_string(), // min_pool_size - "0".to_string(), // reserve_pool - pool_config.pool_mode.to_string(), // pool_mode - pool_config.user.pool_size.to_string(), // max_connections - pool_state.connections.to_string(), // current_connections + address.name(), // name + address.host.to_string(), // host + address.port.to_string(), // port + database_name.to_string(), // database + pool_config.user.username.to_string(), // force_user + pool_config.user.pool_size.to_string(), // pool_size + pool_config.user.min_pool_size.unwrap_or(0).to_string(), // min_pool_size + "0".to_string(), // reserve_pool + pool_config.pool_mode.to_string(), // pool_mode + pool_config.user.pool_size.to_string(), // max_connections + pool_state.connections.to_string(), // current_connections match paused { // paused true => "1".to_string(), From c7d6273037b8eb33c8b102c0d124c65a80bc5321 Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Fri, 16 Jun 2023 12:57:44 -0700 Subject: [PATCH 61/79] Support for prepared statements (#474) * Start prepared statements * parse * Ok * optional * dont rewrite anonymous prepared stmts * Dont rewrite anonymous prep statements * hm? * prep statements * I see! * comment * Print config value * Rewrite bind and add sqlx test * fmt * ok * Fix * Fix stats * its late * clean up PREPARE --- dev/Dockerfile | 2 +- pgcat.toml | 3 + src/admin.rs | 10 + src/client.rs | 215 ++++++- src/config.rs | 9 + src/errors.rs | 7 + src/messages.rs | 293 +++++++++ src/query_router.rs | 5 +- src/server.rs | 43 +- src/stats/server.rs | 15 + tests/rust/.gitignore | 1 + tests/rust/Cargo.lock | 1322 ++++++++++++++++++++++++++++++++++++++++ tests/rust/Cargo.toml | 10 + tests/rust/src/main.rs | 29 + 14 files changed, 1954 insertions(+), 10 deletions(-) create mode 100644 tests/rust/.gitignore create mode 100644 tests/rust/Cargo.lock create mode 100644 tests/rust/Cargo.toml create mode 100644 tests/rust/src/main.rs diff --git a/dev/Dockerfile b/dev/Dockerfile index f2054ef2..bd24174f 100644 --- a/dev/Dockerfile +++ b/dev/Dockerfile @@ -1,4 +1,4 @@ -FROM rust:bullseye +FROM rust:1.70-bullseye # Dependencies RUN apt-get update -y \ diff --git a/pgcat.toml b/pgcat.toml index e6b54b25..41d0210a 100644 --- a/pgcat.toml +++ b/pgcat.toml @@ -60,6 +60,9 @@ tcp_keepalives_count = 5 # Number of seconds between keepalive packets. tcp_keepalives_interval = 5 +# Handle prepared statements. +prepared_statements = true + # Path to TLS Certificate file to use for TLS connections # tls_certificate = ".circleci/server.cert" # Path to TLS private key file to use for TLS connections diff --git a/src/admin.rs b/src/admin.rs index 9ae5e9d5..bbca956f 100644 --- a/src/admin.rs +++ b/src/admin.rs @@ -699,6 +699,8 @@ where ("bytes_sent", DataType::Numeric), ("bytes_received", DataType::Numeric), ("age_seconds", DataType::Numeric), + ("prepare_cache_hit", DataType::Numeric), + ("prepare_cache_miss", DataType::Numeric), ]; let new_map = get_server_stats(); @@ -722,6 +724,14 @@ where .duration_since(server.connect_time()) .as_secs() .to_string(), + server + .prepared_hit_count + .load(Ordering::Relaxed) + .to_string(), + server + .prepared_miss_count + .load(Ordering::Relaxed) + .to_string(), ]; res.put(data_row(&row)); diff --git a/src/client.rs b/src/client.rs index 1ff558b5..608d838d 100644 --- a/src/client.rs +++ b/src/client.rs @@ -3,8 +3,9 @@ use crate::pool::BanReason; /// Handle clients by pretending to be a PostgreSQL server. use bytes::{Buf, BufMut, BytesMut}; use log::{debug, error, info, trace, warn}; +use once_cell::sync::Lazy; use std::collections::HashMap; -use std::sync::Arc; +use std::sync::{atomic::AtomicUsize, Arc}; use std::time::Instant; use tokio::io::{split, AsyncReadExt, BufReader, ReadHalf, WriteHalf}; use tokio::net::TcpStream; @@ -13,7 +14,9 @@ use tokio::sync::mpsc::Sender; use crate::admin::{generate_server_info_for_admin, handle_admin}; use crate::auth_passthrough::refetch_auth_hash; -use crate::config::{get_config, get_idle_client_in_transaction_timeout, Address, PoolMode}; +use crate::config::{ + get_config, get_idle_client_in_transaction_timeout, get_prepared_statements, Address, PoolMode, +}; use crate::constants::*; use crate::messages::*; use crate::plugins::PluginOutput; @@ -25,6 +28,11 @@ use crate::tls::Tls; use tokio_rustls::server::TlsStream; +/// Incrementally count prepared statements +/// to avoid random conflicts in places where the random number generator is weak. +pub static PREPARED_STATEMENT_COUNTER: Lazy> = + Lazy::new(|| Arc::new(AtomicUsize::new(0))); + /// Type of connection received from client. enum ClientConnectionType { Startup, @@ -93,6 +101,9 @@ pub struct Client { /// Used to notify clients about an impending shutdown shutdown: Receiver<()>, + + /// Prepared statements + prepared_statements: HashMap, } /// Client entrypoint. @@ -682,6 +693,7 @@ where application_name: application_name.to_string(), shutdown, connected_to_server: false, + prepared_statements: HashMap::new(), }) } @@ -716,6 +728,7 @@ where application_name: String::from("undefined"), shutdown, connected_to_server: false, + prepared_statements: HashMap::new(), }) } @@ -757,6 +770,10 @@ where // Result returned by one of the plugins. let mut plugin_output = None; + // Prepared statement being executed + let mut prepared_statement = None; + let mut will_prepare = false; + // Our custom protocol loop. // We expect the client to either start a transaction with regular queries // or issue commands for our sharding and server selection protocol. @@ -766,13 +783,16 @@ where self.transaction_mode ); + // Should we rewrite prepared statements and bind messages? + let mut prepared_statements_enabled = get_prepared_statements(); + // Read a complete message from the client, which normally would be // either a `Q` (query) or `P` (prepare, extended protocol). // We can parse it here before grabbing a server from the pool, // in case the client is sending some custom protocol messages, e.g. // SET SHARDING KEY TO 'bigint'; - let message = tokio::select! { + let mut message = tokio::select! { _ = self.shutdown.recv() => { if !self.admin { error_response_terminal( @@ -800,7 +820,21 @@ where // allocate a connection, we wouldn't be able to send back an error message // to the client so we buffer them and defer the decision to error out or not // to when we get the S message - 'D' | 'E' => { + 'D' => { + if prepared_statements_enabled { + let name; + (name, message) = self.rewrite_describe(message).await?; + + if let Some(name) = name { + prepared_statement = Some(name); + } + } + + self.buffer.put(&message[..]); + continue; + } + + 'E' => { self.buffer.put(&message[..]); continue; } @@ -830,6 +864,11 @@ where } 'P' => { + if prepared_statements_enabled { + (prepared_statement, message) = self.rewrite_parse(message)?; + will_prepare = true; + } + self.buffer.put(&message[..]); if query_router.query_parser_enabled() { @@ -846,6 +885,10 @@ where } 'B' => { + if prepared_statements_enabled { + (prepared_statement, message) = self.rewrite_bind(message).await?; + } + self.buffer.put(&message[..]); if query_router.query_parser_enabled() { @@ -1054,7 +1097,48 @@ where // If the client is in session mode, no more custom protocol // commands will be accepted. loop { - let message = match initial_message { + // Only check if we should rewrite prepared statements + // in session mode. In transaction mode, we check at the beginning of + // each transaction. + if !self.transaction_mode { + prepared_statements_enabled = get_prepared_statements(); + } + + debug!("Prepared statement active: {:?}", prepared_statement); + + // We are processing a prepared statement. + if let Some(ref name) = prepared_statement { + debug!("Checking prepared statement is on server"); + // Get the prepared statement the server expects to see. + let statement = match self.prepared_statements.get(name) { + Some(statement) => { + debug!("Prepared statement `{}` found in cache", name); + statement + } + None => { + return Err(Error::ClientError(format!( + "prepared statement `{}` not found", + name + ))) + } + }; + + // Since it's already in the buffer, we don't need to prepare it on this server. + if will_prepare { + server.will_prepare(&statement.name); + will_prepare = false; + } else { + // The statement is not prepared on the server, so we need to prepare it. + if server.should_prepare(&statement.name) { + server.prepare(statement).await?; + } + } + + // Done processing the prepared statement. + prepared_statement = None; + } + + let mut message = match initial_message { None => { trace!("Waiting for message inside transaction or in session mode"); @@ -1173,6 +1257,11 @@ where // Parse // The query with placeholders is here, e.g. `SELECT * FROM users WHERE email = $1 AND active = $2`. 'P' => { + if prepared_statements_enabled { + (prepared_statement, message) = self.rewrite_parse(message)?; + will_prepare = true; + } + if query_router.query_parser_enabled() { if let Ok(ast) = QueryRouter::parse(&message) { if let Ok(output) = query_router.execute_plugins(&ast).await { @@ -1187,12 +1276,25 @@ where // Bind // The placeholder's replacements are here, e.g. 'user@email.com' and 'true' 'B' => { + if prepared_statements_enabled { + (prepared_statement, message) = self.rewrite_bind(message).await?; + } + self.buffer.put(&message[..]); } // Describe // Command a client can issue to describe a previously prepared named statement. 'D' => { + if prepared_statements_enabled { + let name; + (name, message) = self.rewrite_describe(message).await?; + + if let Some(name) = name { + prepared_statement = Some(name); + } + } + self.buffer.put(&message[..]); } @@ -1235,7 +1337,7 @@ where let first_message_code = (*self.buffer.get(0).unwrap_or(&0)) as char; // Almost certainly true - if first_message_code == 'P' { + if first_message_code == 'P' && !prepared_statements_enabled { // Message layout // P followed by 32 int followed by null-terminated statement name // So message code should be in offset 0 of the buffer, first character @@ -1363,6 +1465,107 @@ where } } + /// Rewrite Parse (F) message to set the prepared statement name to one we control. + /// Save it into the client cache. + fn rewrite_parse(&mut self, message: BytesMut) -> Result<(Option, BytesMut), Error> { + let parse: Parse = (&message).try_into()?; + + let name = parse.name.clone(); + + // Don't rewrite anonymous prepared statements + if parse.anonymous() { + debug!("Anonymous prepared statement"); + return Ok((None, message)); + } + + let parse = parse.rename(); + + debug!( + "Renamed prepared statement `{}` to `{}` and saved to cache", + name, parse.name + ); + + self.prepared_statements.insert(name.clone(), parse.clone()); + + Ok((Some(name), parse.try_into()?)) + } + + /// Rewrite the Bind (F) message to use the prepared statement name + /// saved in the client cache. + async fn rewrite_bind( + &mut self, + message: BytesMut, + ) -> Result<(Option, BytesMut), Error> { + let bind: Bind = (&message).try_into()?; + let name = bind.prepared_statement.clone(); + + if bind.anonymous() { + debug!("Anonymous bind message"); + return Ok((None, message)); + } + + match self.prepared_statements.get(&name) { + Some(prepared_stmt) => { + let bind = bind.reassign(prepared_stmt); + + debug!("Rewrote bind `{}` to `{}`", name, bind.prepared_statement); + + Ok((Some(name), bind.try_into()?)) + } + None => { + debug!("Got bind for unknown prepared statement {:?}", bind); + + error_response( + &mut self.write, + &format!( + "prepared statement \"{}\" does not exist", + bind.prepared_statement + ), + ) + .await?; + + Err(Error::ClientError(format!( + "Prepared statement `{}` doesn't exist", + name + ))) + } + } + } + + /// Rewrite the Describe (F) message to use the prepared statement name + /// saved in the client cache. + async fn rewrite_describe( + &mut self, + message: BytesMut, + ) -> Result<(Option, BytesMut), Error> { + let describe: Describe = (&message).try_into()?; + let name = describe.statement_name.clone(); + + if describe.anonymous() { + debug!("Anonymous describe"); + return Ok((None, message)); + } + + match self.prepared_statements.get(&name) { + Some(prepared_stmt) => { + let describe = describe.rename(&prepared_stmt.name); + + debug!( + "Rewrote describe `{}` to `{}`", + name, describe.statement_name + ); + + Ok((Some(name), describe.try_into()?)) + } + + None => { + debug!("Got describe for unknown prepared statement {:?}", describe); + + Ok((None, message)) + } + } + } + /// Release the server from the client: it can't cancel its queries anymore. pub fn release(&self) { let mut guard = self.client_server_map.lock(); diff --git a/src/config.rs b/src/config.rs index 7573d39b..66c20758 100644 --- a/src/config.rs +++ b/src/config.rs @@ -320,6 +320,9 @@ pub struct General { pub auth_query: Option, pub auth_query_user: Option, pub auth_query_password: Option, + + #[serde(default)] + pub prepared_statements: bool, } impl General { @@ -434,6 +437,7 @@ impl Default for General { server_lifetime: Self::default_server_lifetime(), server_round_robin: false, validate_config: true, + prepared_statements: false, } } } @@ -1015,6 +1019,7 @@ impl Config { "Server TLS certificate verification: {}", self.general.verify_server_certificate ); + info!("Prepared statements: {}", self.general.prepared_statements); info!( "Plugins: {}", match self.plugins { @@ -1239,6 +1244,10 @@ pub fn get_idle_client_in_transaction_timeout() -> u64 { .idle_client_in_transaction_timeout } +pub fn get_prepared_statements() -> bool { + (*(*CONFIG.load())).general.prepared_statements +} + /// Parse the configuration file located at the path. pub async fn parse(path: &str) -> Result<(), Error> { let mut contents = String::new(); diff --git a/src/errors.rs b/src/errors.rs index b1796eee..c076a3f9 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -26,6 +26,7 @@ pub enum Error { AuthPassthroughError(String), UnsupportedStatement, QueryRouterParserError(String), + QueryRouterError(String), } #[derive(Clone, PartialEq, Debug)] @@ -121,3 +122,9 @@ impl std::fmt::Display for Error { } } } + +impl From for Error { + fn from(err: std::ffi::NulError) -> Self { + Error::QueryRouterError(err.to_string()) + } +} diff --git a/src/messages.rs b/src/messages.rs index ee4886df..552497f1 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -7,11 +7,15 @@ use socket2::{SockRef, TcpKeepalive}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::TcpStream; +use crate::client::PREPARED_STATEMENT_COUNTER; use crate::config::get_config; use crate::errors::Error; + use std::collections::HashMap; +use std::ffi::CString; use std::io::{BufRead, Cursor}; use std::mem; +use std::sync::atomic::Ordering; use std::time::Duration; /// Postgres data type mappings @@ -526,6 +530,13 @@ pub fn command_complete(command: &str) -> BytesMut { res } +pub fn flush() -> BytesMut { + let mut bytes = BytesMut::new(); + bytes.put_u8(b'H'); + bytes.put_i32(4); + bytes +} + /// Write all data in the buffer to the TcpStream. pub async fn write_all(stream: &mut S, buf: BytesMut) -> Result<(), Error> where @@ -689,3 +700,285 @@ impl BytesMutReader for Cursor<&BytesMut> { } } } + +/// Parse (F) message. +/// See: +#[derive(Clone, Debug)] +pub struct Parse { + code: char, + #[allow(dead_code)] + len: i32, + pub name: String, + pub generated_name: String, + query: String, + num_params: i16, + param_types: Vec, +} + +impl TryFrom<&BytesMut> for Parse { + type Error = Error; + + fn try_from(buf: &BytesMut) -> Result { + let mut cursor = Cursor::new(buf); + let code = cursor.get_u8() as char; + let len = cursor.get_i32(); + let name = cursor.read_string()?; + let query = cursor.read_string()?; + let num_params = cursor.get_i16(); + let mut param_types = Vec::new(); + + for _ in 0..num_params { + param_types.push(cursor.get_i32()); + } + + Ok(Parse { + code, + len, + name, + generated_name: prepared_statement_name(), + query, + num_params, + param_types, + }) + } +} + +impl TryFrom for BytesMut { + type Error = Error; + + fn try_from(parse: Parse) -> Result { + let mut bytes = BytesMut::new(); + + let name_binding = CString::new(parse.name)?; + let name = name_binding.as_bytes_with_nul(); + + let query_binding = CString::new(parse.query)?; + let query = query_binding.as_bytes_with_nul(); + + // Recompute length of the message. + let len = 4 // self + + name.len() + + query.len() + + 2 + + 4 * parse.num_params as usize; + + bytes.put_u8(parse.code as u8); + bytes.put_i32(len as i32); + bytes.put_slice(name); + bytes.put_slice(query); + bytes.put_i16(parse.num_params); + for param in parse.param_types { + bytes.put_i32(param); + } + + Ok(bytes) + } +} + +impl TryFrom<&Parse> for BytesMut { + type Error = Error; + + fn try_from(parse: &Parse) -> Result { + parse.clone().try_into() + } +} + +impl Parse { + pub fn rename(mut self) -> Self { + self.name = self.generated_name.to_string(); + self + } + + pub fn anonymous(&self) -> bool { + self.name.is_empty() + } +} + +/// Bind (B) message. +/// See: +#[derive(Clone, Debug)] +pub struct Bind { + code: char, + #[allow(dead_code)] + len: i64, + portal: String, + pub prepared_statement: String, + num_param_format_codes: i16, + param_format_codes: Vec, + num_param_values: i16, + param_values: Vec<(i32, BytesMut)>, + num_result_column_format_codes: i16, + result_columns_format_codes: Vec, +} + +impl TryFrom<&BytesMut> for Bind { + type Error = Error; + + fn try_from(buf: &BytesMut) -> Result { + let mut cursor = Cursor::new(buf); + let code = cursor.get_u8() as char; + let len = cursor.get_i32(); + let portal = cursor.read_string()?; + let prepared_statement = cursor.read_string()?; + let num_param_format_codes = cursor.get_i16(); + let mut param_format_codes = Vec::new(); + + for _ in 0..num_param_format_codes { + param_format_codes.push(cursor.get_i16()); + } + + let num_param_values = cursor.get_i16(); + let mut param_values = Vec::new(); + + for _ in 0..num_param_values { + let param_len = cursor.get_i32(); + let mut param = BytesMut::with_capacity(param_len as usize); + param.resize(param_len as usize, b'0'); + cursor.copy_to_slice(&mut param); + param_values.push((param_len, param)); + } + + let num_result_column_format_codes = cursor.get_i16(); + let mut result_columns_format_codes = Vec::new(); + + for _ in 0..num_result_column_format_codes { + result_columns_format_codes.push(cursor.get_i16()); + } + + Ok(Bind { + code, + len: len as i64, + portal, + prepared_statement, + num_param_format_codes, + param_format_codes, + num_param_values, + param_values, + num_result_column_format_codes, + result_columns_format_codes, + }) + } +} + +impl TryFrom for BytesMut { + type Error = Error; + + fn try_from(bind: Bind) -> Result { + let mut bytes = BytesMut::new(); + + let portal_binding = CString::new(bind.portal)?; + let portal = portal_binding.as_bytes_with_nul(); + + let prepared_statement_binding = CString::new(bind.prepared_statement)?; + let prepared_statement = prepared_statement_binding.as_bytes_with_nul(); + + let mut len = 4 // self + + portal.len() + + prepared_statement.len() + + 2 // num_param_format_codes + + 2 * bind.num_param_format_codes as usize // num_param_format_codes + + 2; // num_param_values + + for (param_len, _) in &bind.param_values { + len += 4 + *param_len as usize; + } + len += 2; // num_result_column_format_codes + len += 2 * bind.num_result_column_format_codes as usize; + + bytes.put_u8(bind.code as u8); + bytes.put_i32(len as i32); + bytes.put_slice(portal); + bytes.put_slice(prepared_statement); + bytes.put_i16(bind.num_param_format_codes); + for param_format_code in bind.param_format_codes { + bytes.put_i16(param_format_code); + } + bytes.put_i16(bind.num_param_values); + for (param_len, param) in bind.param_values { + bytes.put_i32(param_len); + bytes.put_slice(¶m); + } + bytes.put_i16(bind.num_result_column_format_codes); + for result_column_format_code in bind.result_columns_format_codes { + bytes.put_i16(result_column_format_code); + } + + Ok(bytes) + } +} + +impl Bind { + pub fn reassign(mut self, parse: &Parse) -> Self { + self.prepared_statement = parse.name.clone(); + self + } + + pub fn anonymous(&self) -> bool { + self.prepared_statement.is_empty() + } +} + +#[derive(Debug, Clone)] +pub struct Describe { + code: char, + + #[allow(dead_code)] + len: i32, + target: char, + pub statement_name: String, +} + +impl TryFrom<&BytesMut> for Describe { + type Error = Error; + + fn try_from(bytes: &BytesMut) -> Result { + let mut cursor = Cursor::new(bytes); + let code = cursor.get_u8() as char; + let len = cursor.get_i32(); + let target = cursor.get_u8() as char; + let statement_name = cursor.read_string()?; + + Ok(Describe { + code, + len, + target, + statement_name, + }) + } +} + +impl TryFrom for BytesMut { + type Error = Error; + + fn try_from(describe: Describe) -> Result { + let mut bytes = BytesMut::new(); + let statement_name_binding = CString::new(describe.statement_name)?; + let statement_name = statement_name_binding.as_bytes_with_nul(); + let len = 4 + 1 + statement_name.len(); + + bytes.put_u8(describe.code as u8); + bytes.put_i32(len as i32); + bytes.put_u8(describe.target as u8); + bytes.put_slice(statement_name); + + Ok(bytes) + } +} + +impl Describe { + pub fn rename(mut self, name: &str) -> Self { + self.statement_name = name.to_string(); + self + } + + pub fn anonymous(&self) -> bool { + self.statement_name.is_empty() + } +} + +pub fn prepared_statement_name() -> String { + format!( + "P_{}", + PREPARED_STATEMENT_COUNTER.fetch_add(1, Ordering::SeqCst) + ) +} diff --git a/src/query_router.rs b/src/query_router.rs index 3e3a23a8..126b8138 100644 --- a/src/query_router.rs +++ b/src/query_router.rs @@ -331,7 +331,7 @@ impl QueryRouter { Some((command, value)) } - pub fn parse(message: &BytesMut) -> Result, Error> { + pub fn parse(message: &BytesMut) -> Result, Error> { let mut message_cursor = Cursor::new(message); let code = message_cursor.get_u8() as char; @@ -348,12 +348,13 @@ impl QueryRouter { // Parse (prepared statement) 'P' => { // Reads statement name - message_cursor.read_string().unwrap(); + let _name = message_cursor.read_string().unwrap(); // Reads query string let query = message_cursor.read_string().unwrap(); debug!("Prepared statement: '{}'", query); + query } diff --git a/src/server.rs b/src/server.rs index 32dd91f8..ab29db09 100644 --- a/src/server.rs +++ b/src/server.rs @@ -5,7 +5,7 @@ use fallible_iterator::FallibleIterator; use log::{debug, error, info, trace, warn}; use parking_lot::{Mutex, RwLock}; use postgres_protocol::message; -use std::collections::HashMap; +use std::collections::{BTreeSet, HashMap}; use std::io::Read; use std::net::IpAddr; use std::sync::Arc; @@ -198,6 +198,9 @@ pub struct Server { /// Should clean up dirty connections? cleanup_connections: bool, + + /// Prepared statements + prepared_statements: BTreeSet, } impl Server { @@ -692,6 +695,7 @@ impl Server { )), }, cleanup_connections, + prepared_statements: BTreeSet::new(), }; server.set_name("pgcat").await?; @@ -910,6 +914,43 @@ impl Server { Ok(bytes) } + pub fn will_prepare(&mut self, name: &str) { + debug!("Will prepare `{}`", name); + + self.prepared_statements.insert(name.to_string()); + } + + pub fn should_prepare(&self, name: &str) -> bool { + let should_prepare = !self.prepared_statements.contains(name); + + debug!("Should prepare `{}`: {}", name, should_prepare); + + if should_prepare { + self.stats.prepared_cache_miss(); + } else { + self.stats.prepared_cache_hit(); + } + + should_prepare + } + + pub async fn prepare(&mut self, parse: &Parse) -> Result<(), Error> { + debug!("Preparing `{}`", parse.name); + + let bytes: BytesMut = parse.try_into()?; + self.send(&bytes).await?; + self.send(&flush()).await?; + + // Read and discard ParseComplete (B) + let _ = read_message(&mut self.stream).await?; + + self.prepared_statements.insert(parse.name.to_string()); + + debug!("Prepared `{}`", parse.name); + + Ok(()) + } + /// If the server is still inside a transaction. /// If the client disconnects while the server is in a transaction, we will clean it up. pub fn in_transaction(&self) -> bool { diff --git a/src/stats/server.rs b/src/stats/server.rs index e156ee09..6fb2dc97 100644 --- a/src/stats/server.rs +++ b/src/stats/server.rs @@ -47,6 +47,8 @@ pub struct ServerStats { pub transaction_count: Arc, pub query_count: Arc, pub error_count: Arc, + pub prepared_hit_count: Arc, + pub prepared_miss_count: Arc, } impl Default for ServerStats { @@ -63,6 +65,8 @@ impl Default for ServerStats { query_count: Arc::new(AtomicU64::new(0)), error_count: Arc::new(AtomicU64::new(0)), reporter: get_reporter(), + prepared_hit_count: Arc::new(AtomicU64::new(0)), + prepared_miss_count: Arc::new(AtomicU64::new(0)), } } } @@ -172,6 +176,7 @@ impl ServerStats { self.set_application(application_name.to_string()); self.address.stats.query_count_add(); self.address.stats.query_time_add(milliseconds); + self.query_count.fetch_add(1, Ordering::Relaxed); } /// Report a transaction executed by a client a server @@ -198,4 +203,14 @@ impl ServerStats { .fetch_add(amount_bytes as u64, Ordering::Relaxed); self.address.stats.bytes_received_add(amount_bytes as u64); } + + /// Report a prepared statement that already exists on the server. + pub fn prepared_cache_hit(&self) { + self.prepared_hit_count.fetch_add(1, Ordering::Relaxed); + } + + /// Report a prepared statement that does not exist on the server yet. + pub fn prepared_cache_miss(&self) { + self.prepared_miss_count.fetch_add(1, Ordering::Relaxed); + } } diff --git a/tests/rust/.gitignore b/tests/rust/.gitignore new file mode 100644 index 00000000..2f7896d1 --- /dev/null +++ b/tests/rust/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/tests/rust/Cargo.lock b/tests/rust/Cargo.lock new file mode 100644 index 00000000..f24df81e --- /dev/null +++ b/tests/rust/Cargo.lock @@ -0,0 +1,1322 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", +] + +[[package]] +name = "allocator-api2" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56fc6cf8dc8c4158eed8649f9b8b0ea1518eb62b544fe9490d66fa0b349eafe9" + +[[package]] +name = "atoi" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c57d12312ff59c811c0643f4d80830505833c9ffaebd193d819392b265be8e" +dependencies = [ + "num-traits", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cpufeatures" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e69e28e9f7f77debdedbaafa2866e1de9ba56df55a8bd7cfc724c25a09987c" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cace84e55f07e7301bae1c519df89cdad8cc3cd868413d3fdbdeca9ff3db484" + +[[package]] +name = "crossbeam-queue" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "form_urlencoded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" + +[[package]] +name = "futures-intrusive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a604f7a68fbf8103337523b1fadc8ade7361ee3f112f7c680ad179651616aed5" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot 0.11.2", +] + +[[package]] +name = "futures-sink" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" + +[[package]] +name = "futures-task" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" + +[[package]] +name = "futures-util" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +dependencies = [ + "futures-core", + "futures-sink", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +dependencies = [ + "ahash 0.8.3", + "allocator-api2", +] + +[[package]] +name = "hashlink" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312f66718a2d7789ffef4f4b7b213138ed9f1eb3aa1d0d82fc99f88fb3ffd26f" +dependencies = [ + "hashbrown 0.14.0", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "ipnetwork" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f84f1612606f3753f205a4e9a2efd6fe5b4c573a6269b2cc6c3003d44a0d127" + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" + +[[package]] +name = "js-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.146" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b" + +[[package]] +name = "lock_api" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" + +[[package]] +name = "md-5" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" +dependencies = [ + "digest", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mio" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.8", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.3.5", + "smallvec", + "windows-targets", +] + +[[package]] +name = "paste" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" + +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec2b086b7a862cf4de201096214fa870344cf922b2b30c167badb3af3195406" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall 0.2.16", + "thiserror", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + +[[package]] +name = "rust" +version = "0.1.0" +dependencies = [ + "sqlx", + "tokio", +] + +[[package]] +name = "rustls" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" +dependencies = [ + "log", + "ring", + "sct", + "webpki", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" +dependencies = [ + "base64 0.21.2", +] + +[[package]] +name = "ryu" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "serde" +version = "1.0.164" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.164" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "serde_json" +version = "1.0.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "socket2" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "sqlformat" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c12bc9199d1db8234678b7051747c07f517cdcf019262d1847b94ec8b1aee3e" +dependencies = [ + "itertools", + "nom", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8de3b03a925878ed54a954f621e64bf55a3c1bd29652d0d1a17830405350188" +dependencies = [ + "sqlx-core", + "sqlx-macros", +] + +[[package]] +name = "sqlx-core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa8241483a83a3f33aa5fff7e7d9def398ff9990b2752b6c6112b83c6d246029" +dependencies = [ + "ahash 0.7.6", + "atoi", + "base64 0.13.1", + "bitflags", + "byteorder", + "bytes", + "crc", + "crossbeam-queue", + "dirs", + "dotenvy", + "either", + "event-listener", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-util", + "hashlink", + "hex", + "hkdf", + "hmac", + "indexmap", + "ipnetwork", + "itoa", + "libc", + "log", + "md-5", + "memchr", + "once_cell", + "paste", + "percent-encoding", + "rand", + "rustls", + "rustls-pemfile", + "serde", + "serde_json", + "sha1", + "sha2", + "smallvec", + "sqlformat", + "sqlx-rt", + "stringprep", + "thiserror", + "time", + "tokio-stream", + "url", + "uuid", + "webpki-roots", + "whoami", +] + +[[package]] +name = "sqlx-macros" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9966e64ae989e7e575b19d7265cb79d7fc3cbbdf179835cb0d716f294c2049c9" +dependencies = [ + "dotenvy", + "either", + "heck", + "once_cell", + "proc-macro2", + "quote", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-rt", + "syn 1.0.109", + "url", +] + +[[package]] +name = "sqlx-rt" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804d3f245f894e61b1e6263c84b23ca675d96753b5abfd5cc8597d86806e8024" +dependencies = [ + "once_cell", + "tokio", + "tokio-rustls", +] + +[[package]] +name = "stringprep" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee348cb74b87454fff4b551cbf727025810a004f88aeacae7f85b87f4e9a1c1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "time" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd" +dependencies = [ + "itoa", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" + +[[package]] +name = "time-macros" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" +dependencies = [ + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94d7b1cfd2aa4011f2de74c2c4c63665e27a71006b0a192dcd2710272e73dfa2" +dependencies = [ + "autocfg", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot 0.12.1", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "tokio-rustls" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +dependencies = [ + "rustls", + "tokio", + "webpki", +] + +[[package]] +name = "tokio-stream" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-ident" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "url" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "uuid" +version = "1.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa2982af2eec27de306107c027578ff7f423d65f7250e40ce0fea8f45248b81" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.18", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" + +[[package]] +name = "web-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" +dependencies = [ + "webpki", +] + +[[package]] +name = "whoami" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c70234412ca409cc04e864e89523cb0fc37f5e1344ebed5a3ebf4192b6b9f68" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" diff --git a/tests/rust/Cargo.toml b/tests/rust/Cargo.toml new file mode 100644 index 00000000..1600dacf --- /dev/null +++ b/tests/rust/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "rust" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +sqlx = { version = "0.6.2", features = [ "runtime-tokio-rustls", "postgres", "json", "tls", "migrate", "time", "uuid", "ipnetwork"] } +tokio = { version = "1", features = ["full"] } diff --git a/tests/rust/src/main.rs b/tests/rust/src/main.rs new file mode 100644 index 00000000..79667bcc --- /dev/null +++ b/tests/rust/src/main.rs @@ -0,0 +1,29 @@ +#[tokio::main] +async fn main() { + test_prepared_statements().await; +} + +async fn test_prepared_statements() { + let pool = sqlx::postgres::PgPoolOptions::new() + .max_connections(5) + .connect("postgres://sharding_user:sharding_user@127.0.0.1:6432/sharded_db") + .await + .unwrap(); + + let mut handles = Vec::new(); + + for _ in 0..5 { + let pool = pool.clone(); + let handle = tokio::task::spawn(async move { + for _ in 0..1000 { + sqlx::query("SELECT 1").fetch_all(&pool).await.unwrap(); + } + }); + + handles.push(handle); + } + + for handle in handles { + handle.await.unwrap(); + } +} From 473bb3d17d42b002f05f34c29ac8e6bc5cb864b0 Mon Sep 17 00:00:00 2001 From: Juraj Bubniak Date: Sat, 17 Jun 2023 03:48:38 +0200 Subject: [PATCH 62/79] Log not implemented messages as debug in prometheus metrics. (#477) --- src/prometheus.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/prometheus.rs b/src/prometheus.rs index b4884604..7774b5a5 100644 --- a/src/prometheus.rs +++ b/src/prometheus.rs @@ -1,6 +1,6 @@ use hyper::service::{make_service_fn, service_fn}; use hyper::{Body, Method, Request, Response, Server, StatusCode}; -use log::{error, info, warn}; +use log::{error, info, debug}; use phf::phf_map; use std::collections::HashMap; use std::fmt; @@ -275,7 +275,7 @@ fn push_address_stats(lines: &mut Vec) { { lines.push(prometheus_metric.to_string()); } else { - warn!("Metric {} not implemented for {}", key, address.name()); + debug!("Metric {} not implemented for {}", key, address.name()); } } } @@ -293,7 +293,7 @@ fn push_pool_stats(lines: &mut Vec) { { lines.push(prometheus_metric.to_string()); } else { - warn!("Metric {} not implemented for ({})", name, *pool_id); + debug!("Metric {} not implemented for ({})", name, *pool_id); } } } @@ -318,7 +318,7 @@ fn push_database_stats(lines: &mut Vec) { { lines.push(prometheus_metric.to_string()); } else { - warn!("Metric {} not implemented for {}", key, address.name()); + debug!("Metric {} not implemented for {}", key, address.name()); } } } From b167de5aa3d3b173081cce48e0a4ca2ccda89b28 Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Sat, 17 Jun 2023 08:57:33 -0700 Subject: [PATCH 63/79] fmt (#480) --- src/prometheus.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/prometheus.rs b/src/prometheus.rs index 7774b5a5..1f070a32 100644 --- a/src/prometheus.rs +++ b/src/prometheus.rs @@ -1,6 +1,6 @@ use hyper::service::{make_service_fn, service_fn}; use hyper::{Body, Method, Request, Response, Server, StatusCode}; -use log::{error, info, debug}; +use log::{debug, error, info}; use phf::phf_map; use std::collections::HashMap; use std::fmt; From 73500c0c961a57ae3bac0618046775a43ea78129 Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Sat, 17 Jun 2023 09:09:54 -0700 Subject: [PATCH 64/79] Fix build (#481) --- src/prometheus.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/prometheus.rs b/src/prometheus.rs index 1f070a32..7e264dca 100644 --- a/src/prometheus.rs +++ b/src/prometheus.rs @@ -364,7 +364,7 @@ fn push_server_stats(lines: &mut Vec) { { lines.push(prometheus_metric.to_string()); } else { - warn!("Metric {} not implemented for {}", key, address.name()); + debug!("Metric {} not implemented for {}", key, address.name()); } } } From 4b78af9676596135bfcf16b049f4b1649a9064ed Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Sun, 18 Jun 2023 23:02:34 -0700 Subject: [PATCH 65/79] Implement Close for prepared statements (#482) * Partial support for Close * Close * respect config value * prepared spec * Hmm * Print cache size --- pgcat.toml | 3 ++ src/admin.rs | 5 +++ src/client.rs | 50 ++++++++++++++++++++++- src/config.rs | 24 +++++++++-- src/messages.rs | 80 ++++++++++++++++++++++++++++++++++++- src/server.rs | 76 ++++++++++++++++++++++++++++++++++- src/stats/server.rs | 10 +++++ tests/ruby/prepared_spec.rb | 29 ++++++++++++++ 8 files changed, 269 insertions(+), 8 deletions(-) create mode 100644 tests/ruby/prepared_spec.rb diff --git a/pgcat.toml b/pgcat.toml index 41d0210a..3e8801b6 100644 --- a/pgcat.toml +++ b/pgcat.toml @@ -63,6 +63,9 @@ tcp_keepalives_interval = 5 # Handle prepared statements. prepared_statements = true +# Prepared statements server cache size. +prepared_statements_cache_size = 500 + # Path to TLS Certificate file to use for TLS connections # tls_certificate = ".circleci/server.cert" # Path to TLS private key file to use for TLS connections diff --git a/src/admin.rs b/src/admin.rs index bbca956f..03b984ae 100644 --- a/src/admin.rs +++ b/src/admin.rs @@ -701,6 +701,7 @@ where ("age_seconds", DataType::Numeric), ("prepare_cache_hit", DataType::Numeric), ("prepare_cache_miss", DataType::Numeric), + ("prepare_cache_size", DataType::Numeric), ]; let new_map = get_server_stats(); @@ -732,6 +733,10 @@ where .prepared_miss_count .load(Ordering::Relaxed) .to_string(), + server + .prepared_cache_size + .load(Ordering::Relaxed) + .to_string(), ]; res.put(data_row(&row)); diff --git a/src/client.rs b/src/client.rs index 608d838d..6c0d06fc 100644 --- a/src/client.rs +++ b/src/client.rs @@ -906,6 +906,19 @@ where return Ok(()); } + // Close (F) + 'C' => { + if prepared_statements_enabled { + let close: Close = (&message).try_into()?; + + if close.is_prepared_statement() && !close.anonymous() { + self.prepared_statements.remove(&close.name); + write_all_flush(&mut self.write, &close_complete()).await?; + continue; + } + } + } + _ => (), } @@ -1130,7 +1143,17 @@ where } else { // The statement is not prepared on the server, so we need to prepare it. if server.should_prepare(&statement.name) { - server.prepare(statement).await?; + match server.prepare(statement).await { + Ok(_) => (), + Err(err) => { + pool.ban( + &address, + BanReason::MessageSendFailed, + Some(&self.stats), + ); + return Err(err); + } + } } } @@ -1251,6 +1274,10 @@ where self.stats.disconnect(); self.release(); + if prepared_statements_enabled { + server.maintain_cache().await?; + } + return Ok(()); } @@ -1300,6 +1327,21 @@ where // Close the prepared statement. 'C' => { + if prepared_statements_enabled { + let close: Close = (&message).try_into()?; + + if close.is_prepared_statement() && !close.anonymous() { + match self.prepared_statements.get(&close.name) { + Some(parse) => { + server.will_close(&parse.generated_name); + } + + // A prepared statement slipped through? Not impossible, since we don't support PREPARE yet. + None => (), + }; + } + } + self.buffer.put(&message[..]); } @@ -1433,7 +1475,13 @@ where // The server is no longer bound to us, we can't cancel it's queries anymore. debug!("Releasing server back into the pool"); + server.checkin_cleanup().await?; + + if prepared_statements_enabled { + server.maintain_cache().await?; + } + server.stats().idle(); self.connected_to_server = false; diff --git a/src/config.rs b/src/config.rs index 66c20758..a2314fc1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -323,6 +323,9 @@ pub struct General { #[serde(default)] pub prepared_statements: bool, + + #[serde(default = "General::default_prepared_statements_cache_size")] + pub prepared_statements_cache_size: usize, } impl General { @@ -400,6 +403,10 @@ impl General { pub fn default_server_round_robin() -> bool { true } + + pub fn default_prepared_statements_cache_size() -> usize { + 500 + } } impl Default for General { @@ -438,6 +445,7 @@ impl Default for General { server_round_robin: false, validate_config: true, prepared_statements: false, + prepared_statements_cache_size: 500, } } } @@ -1020,6 +1028,12 @@ impl Config { self.general.verify_server_certificate ); info!("Prepared statements: {}", self.general.prepared_statements); + if self.general.prepared_statements { + info!( + "Prepared statements server cache size: {}", + self.general.prepared_statements_cache_size + ); + } info!( "Plugins: {}", match self.plugins { @@ -1239,13 +1253,15 @@ pub fn get_config() -> Config { } pub fn get_idle_client_in_transaction_timeout() -> u64 { - (*(*CONFIG.load())) - .general - .idle_client_in_transaction_timeout + CONFIG.load().general.idle_client_in_transaction_timeout } pub fn get_prepared_statements() -> bool { - (*(*CONFIG.load())).general.prepared_statements + CONFIG.load().general.prepared_statements +} + +pub fn get_prepared_statements_cache_size() -> usize { + CONFIG.load().general.prepared_statements_cache_size } /// Parse the configuration file located at the path. diff --git a/src/messages.rs b/src/messages.rs index 552497f1..196abe83 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -1,7 +1,7 @@ /// Helper functions to send one-off protocol messages /// and handle TcpStream (TCP socket). use bytes::{Buf, BufMut, BytesMut}; -use log::error; +use log::{debug, error}; use md5::{Digest, Md5}; use socket2::{SockRef, TcpKeepalive}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; @@ -976,6 +976,84 @@ impl Describe { } } +/// Close (F) message. +/// See: +#[derive(Clone, Debug)] +pub struct Close { + code: char, + #[allow(dead_code)] + len: i32, + close_type: char, + pub name: String, +} + +impl TryFrom<&BytesMut> for Close { + type Error = Error; + + fn try_from(bytes: &BytesMut) -> Result { + let mut cursor = Cursor::new(bytes); + let code = cursor.get_u8() as char; + let len = cursor.get_i32(); + let close_type = cursor.get_u8() as char; + let name = cursor.read_string()?; + + Ok(Close { + code, + len, + close_type, + name, + }) + } +} + +impl TryFrom for BytesMut { + type Error = Error; + + fn try_from(close: Close) -> Result { + debug!("Close: {:?}", close); + + let mut bytes = BytesMut::new(); + let name_binding = CString::new(close.name)?; + let name = name_binding.as_bytes_with_nul(); + let len = 4 + 1 + name.len(); + + bytes.put_u8(close.code as u8); + bytes.put_i32(len as i32); + bytes.put_u8(close.close_type as u8); + bytes.put_slice(name); + + Ok(bytes) + } +} + +impl Close { + pub fn new(name: &str) -> Close { + let name = name.to_string(); + + Close { + code: 'C', + len: 4 + 1 + name.len() as i32 + 1, // will be recalculated + close_type: 'S', + name, + } + } + + pub fn is_prepared_statement(&self) -> bool { + self.close_type == 'S' + } + + pub fn anonymous(&self) -> bool { + self.name.is_empty() + } +} + +pub fn close_complete() -> BytesMut { + let mut bytes = BytesMut::new(); + bytes.put_u8(b'3'); + bytes.put_i32(4); + bytes +} + pub fn prepared_statement_name() -> String { format!( "P_{}", diff --git a/src/server.rs b/src/server.rs index ab29db09..fa68b678 100644 --- a/src/server.rs +++ b/src/server.rs @@ -15,7 +15,7 @@ use tokio::net::TcpStream; use tokio_rustls::rustls::{OwnedTrustAnchor, RootCertStore}; use tokio_rustls::{client::TlsStream, TlsConnector}; -use crate::config::{get_config, Address, User}; +use crate::config::{get_config, get_prepared_statements_cache_size, Address, User}; use crate::constants::*; use crate::dns_cache::{AddrSet, CACHED_RESOLVER}; use crate::errors::{Error, ServerIdentifier}; @@ -914,12 +914,16 @@ impl Server { Ok(bytes) } + /// Add the prepared statement to being tracked by this server. + /// The client is processing data that will create a prepared statement on this server. pub fn will_prepare(&mut self, name: &str) { debug!("Will prepare `{}`", name); self.prepared_statements.insert(name.to_string()); + self.stats.prepared_cache_add(); } + /// Check if we should prepare a statement on the server. pub fn should_prepare(&self, name: &str) -> bool { let should_prepare = !self.prepared_statements.contains(name); @@ -934,6 +938,7 @@ impl Server { should_prepare } + /// Create a prepared statement on the server. pub async fn prepare(&mut self, parse: &Parse) -> Result<(), Error> { debug!("Preparing `{}`", parse.name); @@ -942,15 +947,82 @@ impl Server { self.send(&flush()).await?; // Read and discard ParseComplete (B) - let _ = read_message(&mut self.stream).await?; + match read_message(&mut self.stream).await { + Ok(_) => (), + Err(err) => { + self.bad = true; + return Err(err); + } + } self.prepared_statements.insert(parse.name.to_string()); + self.stats.prepared_cache_add(); debug!("Prepared `{}`", parse.name); Ok(()) } + /// Maintain adequate cache size on the server. + pub async fn maintain_cache(&mut self) -> Result<(), Error> { + debug!("Cache maintenance run"); + + let max_cache_size = get_prepared_statements_cache_size(); + let mut names = Vec::new(); + + while self.prepared_statements.len() >= max_cache_size { + // The prepared statmeents are alphanumerically sorted by the BTree. + // FIFO. + if let Some(name) = self.prepared_statements.pop_last() { + names.push(name); + } + } + + self.deallocate(names).await?; + + Ok(()) + } + + /// Remove the prepared statement from being tracked by this server. + /// The client is processing data that will cause the server to close the prepared statement. + pub fn will_close(&mut self, name: &str) { + debug!("Will close `{}`", name); + + self.prepared_statements.remove(name); + } + + /// Close a prepared statement on the server. + pub async fn deallocate(&mut self, names: Vec) -> Result<(), Error> { + for name in &names { + debug!("Deallocating prepared statement `{}`", name); + + let close = Close::new(name); + let bytes: BytesMut = close.try_into()?; + + self.send(&bytes).await?; + } + + self.send(&flush()).await?; + + // Read and discard CloseComplete (3) + for name in &names { + match read_message(&mut self.stream).await { + Ok(_) => { + self.prepared_statements.remove(name); + self.stats.prepared_cache_remove(); + debug!("Closed `{}`", name); + } + + Err(err) => { + self.bad = true; + return Err(err); + } + }; + } + + Ok(()) + } + /// If the server is still inside a transaction. /// If the client disconnects while the server is in a transaction, we will clean it up. pub fn in_transaction(&self) -> bool { diff --git a/src/stats/server.rs b/src/stats/server.rs index 6fb2dc97..443c0b6a 100644 --- a/src/stats/server.rs +++ b/src/stats/server.rs @@ -49,6 +49,7 @@ pub struct ServerStats { pub error_count: Arc, pub prepared_hit_count: Arc, pub prepared_miss_count: Arc, + pub prepared_cache_size: Arc, } impl Default for ServerStats { @@ -67,6 +68,7 @@ impl Default for ServerStats { reporter: get_reporter(), prepared_hit_count: Arc::new(AtomicU64::new(0)), prepared_miss_count: Arc::new(AtomicU64::new(0)), + prepared_cache_size: Arc::new(AtomicU64::new(0)), } } } @@ -213,4 +215,12 @@ impl ServerStats { pub fn prepared_cache_miss(&self) { self.prepared_miss_count.fetch_add(1, Ordering::Relaxed); } + + pub fn prepared_cache_add(&self) { + self.prepared_cache_size.fetch_add(1, Ordering::Relaxed); + } + + pub fn prepared_cache_remove(&self) { + self.prepared_cache_size.fetch_sub(1, Ordering::Relaxed); + } } diff --git a/tests/ruby/prepared_spec.rb b/tests/ruby/prepared_spec.rb new file mode 100644 index 00000000..58a30006 --- /dev/null +++ b/tests/ruby/prepared_spec.rb @@ -0,0 +1,29 @@ +require_relative 'spec_helper' + +describe 'Prepared statements' do + let(:processes) { Helpers::Pgcat.three_shard_setup('sharded_db', 5) } + + context 'enabled' do + it 'will work over the same connection' do + conn = PG.connect(processes.pgcat.connection_string('sharded_db', 'sharding_user')) + + 10.times do |i| + statement_name = "statement_#{i}" + conn.prepare(statement_name, 'SELECT $1::int') + conn.exec_prepared(statement_name, [1]) + conn.describe_prepared(statement_name) + end + end + + it 'will work with new connections' do + 10.times do + conn = PG.connect(processes.pgcat.connection_string('sharded_db', 'sharding_user')) + + statement_name = 'statement1' + conn.prepare('statement1', 'SELECT $1::int') + conn.exec_prepared('statement1', [1]) + conn.describe_prepared('statement1') + end + end + end +end From 1ed6e925ed712303dc9cbde78c0d5409a96d5cd6 Mon Sep 17 00:00:00 2001 From: Zain Kabani <77307340+zainkabani@users.noreply.github.com> Date: Fri, 23 Jun 2023 12:15:44 -0400 Subject: [PATCH 66/79] Fixes the default for round robing in General (#488) --- src/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.rs b/src/config.rs index a2314fc1..ea5a8bfe 100644 --- a/src/config.rs +++ b/src/config.rs @@ -442,7 +442,7 @@ impl Default for General { auth_query_user: None, auth_query_password: None, server_lifetime: Self::default_server_lifetime(), - server_round_robin: false, + server_round_robin: Self::default_server_round_robin(), validate_config: true, prepared_statements: false, prepared_statements_cache_size: 500, From 7205537b490f5c205772d3833d273c9b8a74179e Mon Sep 17 00:00:00 2001 From: Voldemarich Date: Mon, 10 Jul 2023 11:35:43 +0300 Subject: [PATCH 67/79] [BUG] Fix binding of NULL value parameters in prepared statements (#496) Fix binding of NULL value parameters in prepared statements Co-authored-by: anon --- src/messages.rs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/messages.rs b/src/messages.rs index 196abe83..1f494658 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -832,10 +832,21 @@ impl TryFrom<&BytesMut> for Bind { for _ in 0..num_param_values { let param_len = cursor.get_i32(); - let mut param = BytesMut::with_capacity(param_len as usize); - param.resize(param_len as usize, b'0'); - cursor.copy_to_slice(&mut param); - param_values.push((param_len, param)); + // There is special occasion when the parameter is NULL + // In that case, param length is defined as -1 + // So if the passed parameter len is over 0 + if param_len > 0 { + let mut param = BytesMut::with_capacity(param_len as usize); + param.resize(param_len as usize, b'0'); + cursor.copy_to_slice(&mut param); + // we push and the length and the parameter into vector + param_values.push((param_len, param)); + } else { + // otherwise we push a tuple with -1 and 0-len BytesMut + // which means that after encountering -1 postgres proceeds + // to processing another parameter + param_values.push((param_len, BytesMut::new())); + } } let num_result_column_format_codes = cursor.get_i16(); From cf7f6f35ab315bc0a6bac81640888c8e90ac36c1 Mon Sep 17 00:00:00 2001 From: Cluas Date: Wed, 12 Jul 2023 20:42:44 +0800 Subject: [PATCH 68/79] docs: fix general.autoreload description (#491) * docs: fix autoreload description Signed-off-by: Cluas * docs: add blank line Signed-off-by: Cluas --------- Signed-off-by: Cluas --- CONFIG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CONFIG.md b/CONFIG.md index b36a190d..7a1a8674 100644 --- a/CONFIG.md +++ b/CONFIG.md @@ -1,4 +1,4 @@ -# PgCat Configurations +# PgCat Configurations ## `general` Section ### host @@ -116,10 +116,10 @@ If we should log client disconnections ### autoreload ``` path: general.autoreload -default: 15000 +default: 15000 # milliseconds ``` -When set to true, PgCat reloads configs if it detects a change in the config file. +When set, PgCat automatically reloads its configurations at the specified interval (in milliseconds) if it detects changes in the configuration file. The default interval is 15000 milliseconds or 15 seconds. ### worker_threads ``` From 67579c9af44bff10850bf8a63e415e1476ff4e03 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 Jul 2023 05:46:31 -0700 Subject: [PATCH 69/79] chore(deps): bump rustls from 0.21.1 to 0.21.5 (#501) Bumps [rustls](https://github.com/rustls/rustls) from 0.21.1 to 0.21.5. - [Release notes](https://github.com/rustls/rustls/releases) - [Commits](https://github.com/rustls/rustls/compare/v/0.21.1...v/0.21.5) --- updated-dependencies: - dependency-name: rustls dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0baef1ce..b5a43b8f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1159,13 +1159,13 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.1" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c911ba11bc8433e811ce56fde130ccf32f5127cab0e0194e9c68c5a5b671791e" +checksum = "79ea77c539259495ce8ca47f53e66ae0330a8819f67e23ac96ca02f50e7b7d36" dependencies = [ "log", "ring", - "rustls-webpki", + "rustls-webpki 0.101.1", "sct", ] @@ -1188,6 +1188,16 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rustls-webpki" +version = "0.101.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15f36a6828982f422756984e47912a7a51dcbc2a197aa791158f8ca61cd8204e" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "ryu" version = "1.0.13" @@ -1794,7 +1804,7 @@ version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa54963694b65584e170cf5dc46aeb4dcaa5584e652ff5f3952e56d66aff0125" dependencies = [ - "rustls-webpki", + "rustls-webpki 0.100.1", ] [[package]] From 3c9565d351e2938a777b992b1057c58758c0f8ff Mon Sep 17 00:00:00 2001 From: Mostafa Abdelraouf Date: Wed, 12 Jul 2023 13:24:30 -0500 Subject: [PATCH 70/79] Add support for tcp_user_timeout (#503) * Add support for tcp_user_timeout * option * duration * Some() * docs * fmt, compile --- CONFIG.md | 8 +++++++- src/config.rs | 7 +++++++ src/messages.rs | 9 ++++++++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/CONFIG.md b/CONFIG.md index 7a1a8674..3118a49a 100644 --- a/CONFIG.md +++ b/CONFIG.md @@ -151,7 +151,13 @@ path: general.tcp_keepalives_interval default: 5 ``` -Number of seconds between keepalive packets. +### tcp_user_timeout +``` +path: general.tcp_user_timeout +default: 10000 +``` +A linux-only parameters that defines the amount of time in milliseconds that transmitted data may remain unacknowledged or buffered data may remain untransmitted (due to zero window size) before TCP will forcibly disconnect + ### tls_certificate ``` diff --git a/src/config.rs b/src/config.rs index ea5a8bfe..9228b9bb 100644 --- a/src/config.rs +++ b/src/config.rs @@ -261,6 +261,8 @@ pub struct General { pub tcp_keepalives_count: u32, #[serde(default = "General::default_tcp_keepalives_interval")] pub tcp_keepalives_interval: u64, + #[serde(default = "General::default_tcp_user_timeout")] + pub tcp_user_timeout: u64, #[serde(default)] // False pub log_client_connections: bool, @@ -360,6 +362,10 @@ impl General { 5 // 5 seconds } + pub fn default_tcp_user_timeout() -> u64 { + 10000 // 10000 milliseconds + } + pub fn default_idle_timeout() -> u64 { 600000 // 10 minutes } @@ -427,6 +433,7 @@ impl Default for General { tcp_keepalives_idle: Self::default_tcp_keepalives_idle(), tcp_keepalives_count: Self::default_tcp_keepalives_count(), tcp_keepalives_interval: Self::default_tcp_keepalives_interval(), + tcp_user_timeout: Self::default_tcp_user_timeout(), log_client_connections: false, log_client_disconnections: false, autoreload: None, diff --git a/src/messages.rs b/src/messages.rs index 1f494658..eb0ea73c 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -669,6 +669,13 @@ pub fn configure_socket(stream: &TcpStream) { let sock_ref = SockRef::from(stream); let conf = get_config(); + #[cfg(target_os = "linux")] + match sock_ref.set_tcp_user_timeout(Some(Duration::from_millis(conf.general.tcp_user_timeout))) + { + Ok(_) => (), + Err(err) => error!("Could not configure tcp_user_timeout for socket: {}", err), + } + match sock_ref.set_keepalive(true) { Ok(_) => { match sock_ref.set_tcp_keepalive( @@ -678,7 +685,7 @@ pub fn configure_socket(stream: &TcpStream) { .with_time(Duration::from_secs(conf.general.tcp_keepalives_idle)), ) { Ok(_) => (), - Err(err) => error!("Could not configure socket: {}", err), + Err(err) => error!("Could not configure tcp_keepalive for socket: {}", err), } } Err(err) => error!("Could not configure socket: {}", err), From b2e6dfd9bb380d2745d53d708b8cfc518da6870d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 Jul 2023 21:41:48 -0700 Subject: [PATCH 71/79] chore(deps): bump rustls-pemfile from 1.0.2 to 1.0.3 (#504) Bumps [rustls-pemfile](https://github.com/rustls/pemfile) from 1.0.2 to 1.0.3. - [Commits](https://github.com/rustls/pemfile/commits) --- updated-dependencies: - dependency-name: rustls-pemfile dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b5a43b8f..05597050 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1171,9 +1171,9 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" +checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" dependencies = [ "base64", ] From 15b6db8e4eb7c17d0843c936c769063475f11376 Mon Sep 17 00:00:00 2001 From: Sebastian Webber Date: Fri, 14 Jul 2023 02:40:04 -0300 Subject: [PATCH 72/79] add "show help" command (#505) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds a new function to handle notify and use it in the SHOW HELP command, which displays the available options in the admin console. Also, adding Fabrízio as a co-author for all the help with the protocol and the help to structure this PR. Signed-off-by: Sebastian Webber Co-authored-by: Fabrízio de Royes Mello --- src/admin.rs | 45 ++++++++++++++++++++++++++++++++++++++++++++- src/messages.rs | 20 ++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/src/admin.rs b/src/admin.rs index 03b984ae..d17659e8 100644 --- a/src/admin.rs +++ b/src/admin.rs @@ -1,7 +1,7 @@ use crate::pool::BanReason; use crate::stats::pool::PoolStats; use bytes::{Buf, BufMut, BytesMut}; -use log::{error, info, trace}; +use log::{debug, error, info, trace}; use nix::sys::signal::{self, Signal}; use nix::unistd::Pid; use std::collections::HashMap; @@ -84,6 +84,10 @@ where shutdown(stream).await } "SHOW" => match query_parts[1].to_ascii_uppercase().as_str() { + "HELP" => { + trace!("SHOW HELP"); + show_help(stream).await + } "BANS" => { trace!("SHOW BANS"); show_bans(stream).await @@ -271,6 +275,45 @@ where write_all_half(stream, &res).await } +/// Show all available options. +async fn show_help(stream: &mut T) -> Result<(), Error> +where + T: tokio::io::AsyncWrite + std::marker::Unpin, +{ + let mut res = BytesMut::new(); + + let detail_msg = vec![ + "", + "SHOW HELP|CONFIG|DATABASES|POOLS|CLIENTS|SERVERS|USERS|VERSION", + // "SHOW PEERS|PEER_POOLS", // missing PEERS|PEER_POOLS + // "SHOW FDS|SOCKETS|ACTIVE_SOCKETS|LISTS|MEM|STATE", // missing FDS|SOCKETS|ACTIVE_SOCKETS|MEM|STATE + "SHOW LISTS", + // "SHOW DNS_HOSTS|DNS_ZONES", // missing DNS_HOSTS|DNS_ZONES + "SHOW STATS", // missing STATS_TOTALS|STATS_AVERAGES|TOTALS + "SET key = arg", + "RELOAD", + "PAUSE [, ]", + "RESUME [, ]", + // "DISABLE ", // missing + // "ENABLE ", // missing + // "RECONNECT []", missing + // "KILL ", + // "SUSPEND", + "SHUTDOWN", + // "WAIT_CLOSE []", // missing + ]; + + res.put(notify("Console usage", detail_msg.join("\n\t"))); + res.put(command_complete("SHOW")); + + // ReadyForQuery + res.put_u8(b'Z'); + res.put_i32(5); + res.put_u8(b'I'); + + write_all_half(stream, &res).await +} + /// Show shards and replicas. async fn show_databases(stream: &mut T) -> Result<(), Error> where diff --git a/src/messages.rs b/src/messages.rs index eb0ea73c..8ebc00a3 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -530,6 +530,26 @@ pub fn command_complete(command: &str) -> BytesMut { res } +/// Create a notify message. +pub fn notify(message: &str, details: String) -> BytesMut { + let mut notify_cmd = BytesMut::new(); + + notify_cmd.put_slice("SNOTICE\0".as_bytes()); + notify_cmd.put_slice("C00000\0".as_bytes()); + notify_cmd.put_slice(format!("M{}\0", message).as_bytes()); + notify_cmd.put_slice(format!("D{}\0", details).as_bytes()); + + // this extra byte says that is the end of the package + notify_cmd.put_u8(0); + + let mut res = BytesMut::new(); + res.put_u8(b'N'); + res.put_i32(notify_cmd.len() as i32 + 4); + res.put(notify_cmd); + + res +} + pub fn flush() -> BytesMut { let mut bytes = BytesMut::new(); bytes.put_u8(b'H'); From 3e08c6bd8df85112aa089394bba4ed5be209e483 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Jul 2023 07:58:11 -0700 Subject: [PATCH 73/79] chore(deps): bump num_cpus from 1.15.0 to 1.16.0 (#507) Bumps [num_cpus](https://github.com/seanmonstar/num_cpus) from 1.15.0 to 1.16.0. - [Release notes](https://github.com/seanmonstar/num_cpus/releases) - [Changelog](https://github.com/seanmonstar/num_cpus/blob/master/CHANGELOG.md) - [Commits](https://github.com/seanmonstar/num_cpus/compare/v1.15.0...v1.16.0) --- updated-dependencies: - dependency-name: num_cpus dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 05597050..80d0ba55 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -470,15 +470,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" -[[package]] -name = "hermit-abi" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" -dependencies = [ - "libc", -] - [[package]] name = "hermit-abi" version = "0.3.1" @@ -658,7 +649,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21b6b32576413a8e69b90e952e4a026476040d81017b80445deda5f2d3921857" dependencies = [ - "hermit-abi 0.3.1", + "hermit-abi", "io-lifetimes", "rustix", "windows-sys", @@ -849,11 +840,11 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.2.6", + "hermit-abi", "libc", ] From 5d87e3781e6c7e8cdfb32a66238a0540d79402f5 Mon Sep 17 00:00:00 2001 From: Sebastian Webber Date: Fri, 14 Jul 2023 14:30:49 -0300 Subject: [PATCH 74/79] push and build only in main and tags (#508) this commit changes the CI behavior to only build and push when something is committed to main or is a new tag. --- .github/workflows/build-and-push.yaml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-and-push.yaml b/.github/workflows/build-and-push.yaml index 1baa93be..953bee9e 100644 --- a/.github/workflows/build-and-push.yaml +++ b/.github/workflows/build-and-push.yaml @@ -1,6 +1,11 @@ name: Build and Push -on: push +on: + push: + branches: + - main + tags: + - v* env: registry: ghcr.io From 7bdb4e5cd9c8e2578fd127b7b89c926a1c411033 Mon Sep 17 00:00:00 2001 From: Sebastian Webber Date: Tue, 18 Jul 2023 17:52:40 -0300 Subject: [PATCH 75/79] Add cmd line parser (#512) This commit adds the clap library and configures the necessary args to parse from the command line, expanding the current option of a single file and adding support for environment variables. Signed-off-by: Sebastian Webber --- Cargo.lock | 756 ++++++++++++++++++++++++++------------------ Cargo.toml | 1 + src/admin.rs | 2 +- src/cmd_args.rs | 17 + src/main.rs | 15 +- src/multi_logger.rs | 15 +- 6 files changed, 479 insertions(+), 327 deletions(-) create mode 100644 src/cmd_args.rs diff --git a/Cargo.lock b/Cargo.lock index 80d0ba55..b0d455ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,15 +2,36 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "aho-corasick" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -20,6 +41,55 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is-terminal", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" + +[[package]] +name = "anstyle-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +dependencies = [ + "anstyle", + "windows-sys", +] + [[package]] name = "arc-swap" version = "1.6.0" @@ -28,34 +98,35 @@ checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" [[package]] name = "async-stream" -version = "0.3.3" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dad5c83079eae9969be7fadefe640a1c566901f05ff91ab221de4b6f68d9507e" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" dependencies = [ "async-stream-impl", "futures-core", + "pin-project-lite", ] [[package]] name = "async-stream-impl" -version = "0.3.3" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10f203db73a71dfa2fb6dd22763990fa26f3d2625a6da2da900d23b87d26be27" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.26", ] [[package]] name = "async-trait" -version = "0.1.68" +version = "0.1.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" +checksum = "a564d521dd56509c4c47480d00b80ee55f7e385ae48db5744c67ad50c92d2ebf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.9", + "syn 2.0.26", ] [[package]] @@ -75,11 +146,26 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "backtrace" +version = "0.3.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + [[package]] name = "base64" -version = "0.21.0" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" [[package]] name = "bb8" @@ -100,20 +186,26 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" + [[package]] name = "block-buffer" -version = "0.10.3" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "bumpalo" -version = "3.12.0" +version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" [[package]] name = "byteorder" @@ -141,13 +233,13 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.23" +version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" +checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" dependencies = [ + "android-tzdata", "iana-time-zone", "js-sys", - "num-integer", "num-traits", "time", "wasm-bindgen", @@ -155,95 +247,88 @@ dependencies = [ ] [[package]] -name = "codespan-reporting" -version = "0.11.1" +name = "clap" +version = "4.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +checksum = "8f644d0dac522c8b05ddc39aaaccc5b136d5dc4ff216610c5641e3be5becf56c" dependencies = [ - "termcolor", - "unicode-width", + "clap_builder", + "clap_derive", + "once_cell", ] [[package]] -name = "core-foundation-sys" -version = "0.8.3" +name = "clap_builder" +version = "4.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +checksum = "af410122b9778e024f9e0fb35682cc09cc3f85cad5e8d3ba8f47a9702df6e73d" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] [[package]] -name = "cpufeatures" -version = "0.2.5" +name = "clap_derive" +version = "4.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050" dependencies = [ - "libc", + "heck", + "proc-macro2", + "quote", + "syn 2.0.26", ] [[package]] -name = "crypto-common" -version = "0.1.6" +name = "clap_lex" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] +checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" [[package]] -name = "cxx" -version = "1.0.91" +name = "colorchoice" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86d3488e7665a7a483b57e25bdd90d0aeb2bc7608c8d0346acf2ad3f1caf1d62" -dependencies = [ - "cc", - "cxxbridge-flags", - "cxxbridge-macro", - "link-cplusplus", -] +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] -name = "cxx-build" -version = "1.0.91" +name = "core-foundation-sys" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48fcaf066a053a41a81dfb14d57d99738b767febb8b735c3016e469fac5da690" -dependencies = [ - "cc", - "codespan-reporting", - "once_cell", - "proc-macro2", - "quote", - "scratch", - "syn 1.0.109", -] +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" [[package]] -name = "cxxbridge-flags" -version = "1.0.91" +name = "cpufeatures" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2ef98b8b717a829ca5603af80e1f9e2e48013ab227b68ef37872ef84ee479bf" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +dependencies = [ + "libc", +] [[package]] -name = "cxxbridge-macro" -version = "1.0.91" +name = "crypto-common" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "086c685979a698443656e5cf7856c95c642295a38599f12fb1ff76fb28d19892" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", + "generic-array", + "typenum", ] [[package]] name = "data-encoding" -version = "2.3.2" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57" +checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "digest" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", @@ -281,15 +366,21 @@ dependencies = [ "termcolor", ] +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "errno" -version = "0.2.8" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" dependencies = [ "errno-dragonfly", "libc", - "winapi", + "windows-sys", ] [[package]] @@ -322,9 +413,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" dependencies = [ "percent-encoding", ] @@ -385,7 +476,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.9", + "syn 2.0.26", ] [[package]] @@ -420,9 +511,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.6" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -430,20 +521,26 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.8" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if", "libc", "wasi 0.11.0+wasi-snapshot-preview1", ] +[[package]] +name = "gimli" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" + [[package]] name = "h2" -version = "0.3.17" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66b91535aa35fea1523ad1b86cb6b53c28e0ae566ba4a460f4457e936cad7c6f" +checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" dependencies = [ "bytes", "fnv", @@ -451,7 +548,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap", + "indexmap 1.9.3", "slab", "tokio", "tokio-util", @@ -464,17 +561,23 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" + [[package]] name = "heck" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" [[package]] name = "hmac" @@ -538,9 +641,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.26" +version = "0.14.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" dependencies = [ "bytes", "futures-channel", @@ -553,7 +656,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2", + "socket2 0.4.9", "tokio", "tower-service", "tracing", @@ -562,26 +665,25 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.53" +version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" +checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "winapi", + "windows", ] [[package]] name = "iana-time-zone-haiku" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ - "cxx", - "cxx-build", + "cc", ] [[package]] @@ -597,9 +699,9 @@ dependencies = [ [[package]] name = "idna" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -607,50 +709,49 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.9.2" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", ] [[package]] -name = "io-lifetimes" -version = "1.0.5" +name = "indexmap" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1abeb7a0dd0f8181267ff8adc397075586500b81b28a73e8a0208b00fc170fb3" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" dependencies = [ - "libc", - "windows-sys", + "equivalent", + "hashbrown 0.14.0", ] [[package]] name = "ipconfig" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd302af1b90f2463a98fa5ad469fc212c8e3175a41c3068601bfa2727591c5be" +checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" dependencies = [ - "socket2", + "socket2 0.5.3", "widestring", - "winapi", + "windows-sys", "winreg", ] [[package]] name = "ipnet" -version = "2.5.1" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f88c5561171189e69df9d98bcf18fd5f9558300f7ea7b801eb8a0fd748bd8745" +checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" [[package]] name = "is-terminal" -version = "0.4.4" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b6b32576413a8e69b90e952e4a026476040d81017b80445deda5f2d3921857" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ "hermit-abi", - "io-lifetimes", "rustix", "windows-sys", ] @@ -666,9 +767,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.5" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "jemalloc-sys" @@ -692,9 +793,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.61" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" dependencies = [ "wasm-bindgen", ] @@ -707,18 +808,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.139" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" - -[[package]] -name = "link-cplusplus" -version = "1.0.8" +version = "0.2.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" -dependencies = [ - "cc", -] +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" [[package]] name = "linked-hash-map" @@ -728,15 +820,15 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.1.4" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" +checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" [[package]] name = "lock_api" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" dependencies = [ "autocfg", "scopeguard", @@ -765,9 +857,9 @@ checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" [[package]] name = "matches" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" [[package]] name = "md-5" @@ -793,14 +885,22 @@ dependencies = [ "autocfg", ] +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + [[package]] name = "mio" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" dependencies = [ "libc", - "log", "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys", ] @@ -811,7 +911,7 @@ version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cfg-if", "libc", "memoffset", @@ -819,16 +919,6 @@ dependencies = [ "static_assertions", ] -[[package]] -name = "num-integer" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" -dependencies = [ - "autocfg", - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.15" @@ -848,11 +938,20 @@ dependencies = [ "libc", ] +[[package]] +name = "object" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" -version = "1.17.1" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "parking_lot" @@ -866,22 +965,22 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-sys", + "windows-targets", ] [[package]] name = "percent-encoding" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "pgcat" @@ -894,6 +993,7 @@ dependencies = [ "bb8", "bytes", "chrono", + "clap", "env_logger", "exitcode", "fallible-iterator", @@ -920,7 +1020,7 @@ dependencies = [ "serde_json", "sha-1", "sha2", - "socket2", + "socket2 0.4.9", "sqlparser", "stringprep", "tokio", @@ -933,9 +1033,9 @@ dependencies = [ [[package]] name = "phf" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928c6535de93548188ef63bb7c4036bd415cd8f36ad25af44b9789b2ee72a48c" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ "phf_macros", "phf_shared", @@ -943,9 +1043,9 @@ dependencies = [ [[package]] name = "phf_generator" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1181c94580fa345f50f19d738aaa39c0ed30a600d95cb2d3e23f94266f14fbf" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" dependencies = [ "phf_shared", "rand", @@ -953,51 +1053,51 @@ dependencies = [ [[package]] name = "phf_macros" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92aacdc5f16768709a569e913f7451034034178b05bdc8acda226659a3dccc66" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" dependencies = [ "phf_generator", "phf_shared", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.26", ] [[package]] name = "phf_shared" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1fb5f6f826b772a8d4c0394209441e7d37cbbb967ae9c7e0e8134365c9ee676" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" dependencies = [ "siphasher", ] [[package]] name = "pin-project" -version = "1.1.0" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c95a7476719eab1e366eaf73d0260af3021184f18177925b07f54b30089ceead" +checksum = "030ad2bc4db10a8944cb0d837f158bdfec4d4a4873ab701a95046770d11f8842" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.0" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39407670928234ebc5e6e580247dd567ad73a3578460c5990f9503df207e8f07" +checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.9", + "syn 2.0.26", ] [[package]] name = "pin-project-lite" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" [[package]] name = "pin-utils" @@ -1031,9 +1131,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.53" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba466839c78239c09faf015484e5cc04860f88242cff4d03eb038f04b4699b73" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" dependencies = [ "unicode-ident", ] @@ -1046,9 +1146,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.26" +version = "1.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +checksum = "5fe8a65d69dd0808184ebb5f836ab526bb259db23c657efa38711b1072ee47f0" dependencies = [ "proc-macro2", ] @@ -1085,18 +1185,30 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.2.16" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] name = "regex" -version = "1.8.4" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" +checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310" dependencies = [ "aho-corasick", "memchr", @@ -1105,9 +1217,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" +checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" [[package]] name = "resolv-conf" @@ -1134,15 +1246,20 @@ dependencies = [ "winapi", ] +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + [[package]] name = "rustix" -version = "0.36.8" +version = "0.38.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43abb88211988493c1abb44a70efa56ff0ce98f233b7b276146f1f3f7ba9644" +checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5" dependencies = [ - "bitflags", + "bitflags 2.3.3", "errno", - "io-lifetimes", "libc", "linux-raw-sys", "windows-sys", @@ -1191,21 +1308,15 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.13" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" [[package]] name = "scopeguard" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" - -[[package]] -name = "scratch" -version = "1.0.3" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sct" @@ -1219,29 +1330,29 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.160" +version = "1.0.171" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" +checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.160" +version = "1.0.171" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df" +checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682" dependencies = [ "proc-macro2", "quote", - "syn 2.0.9", + "syn 2.0.26", ] [[package]] name = "serde_json" -version = "1.0.96" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +checksum = "d03b412469450d4404fe8499a268edd7f8b79fecb074b0d812ad64ca21f4031b" dependencies = [ "itoa", "ryu", @@ -1250,9 +1361,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93107647184f6027e3b7dcb2e11034cf95ffa1e3a682c67951963ac69c1c007d" +checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" dependencies = [ "serde", ] @@ -1270,9 +1381,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" dependencies = [ "cfg-if", "cpufeatures", @@ -1305,20 +1416,30 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" [[package]] name = "socket2" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" dependencies = [ "libc", "winapi", ] +[[package]] +name = "socket2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +dependencies = [ + "libc", + "windows-sys", +] + [[package]] name = "spin" version = "0.5.2" @@ -1354,19 +1475,25 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "stringprep" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ee348cb74b87454fff4b551cbf727025810a004f88aeacae7f85b87f4e9a1c1" +checksum = "db3737bde7edce97102e0e2b15365bf7a20bfdb5f60f4f9e8d7004258a51a8da" dependencies = [ "unicode-bidi", "unicode-normalization", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "subtle" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "syn" @@ -1381,9 +1508,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.9" +version = "2.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0da4a3c17e109f700685ec577c0f85efd9b19bcf15c913985f14dc1ac01775aa" +checksum = "45c3457aacde3c65315de5031ec191ce46604304d2446e803d71ade03308d970" dependencies = [ "proc-macro2", "quote", @@ -1401,22 +1528,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.37" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" +checksum = "a35fc5b8971143ca348fa6df4f024d4d55264f3468c71ad1c2f365b0a4d58c42" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.37" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" +checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.26", ] [[package]] @@ -1447,40 +1574,40 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.26.0" +version = "1.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03201d01c3c27a29c8a5cee5b55a93ddae1ccf6f08f65365c2c918f8c1b76f64" +checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" dependencies = [ "autocfg", + "backtrace", "bytes", "libc", - "memchr", "mio", "num_cpus", "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.4.9", "tokio-macros", "windows-sys", ] [[package]] name = "tokio-macros" -version = "1.8.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.26", ] [[package]] name = "tokio-rustls" -version = "0.24.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0d409377ff5b1e3ca6437aa86c1eb7d40c134bfec254e44c830defa92669db5" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ "rustls", "tokio", @@ -1488,9 +1615,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.11" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d660770404473ccd7bc9f8b28494a811bc18542b915c0855c51e8f419d5223ce" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" dependencies = [ "futures-core", "pin-project-lite", @@ -1512,9 +1639,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.7" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" dependencies = [ "bytes", "futures-core", @@ -1526,9 +1653,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6135d499e69981f9ff0ef2167955a5333c35e36f6937d382974566b3d5b94ec" +checksum = "c17e963a819c331dcacd7ab957d80bc2b9a9c1e71c804826d2f283dd65306542" dependencies = [ "serde", "serde_spanned", @@ -1538,20 +1665,20 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a76a9312f5ba4c2dec6b9161fdf25d87ad8a09256ccea5a556fef03c706a10f" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.19.9" +version = "0.19.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d964908cec0d030b812013af25a0e57fddfadb1e066ecc6681d86253129d4f" +checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a" dependencies = [ - "indexmap", + "indexmap 2.0.0", "serde", "serde_spanned", "toml_datetime", @@ -1578,20 +1705,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.23" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.26", ] [[package]] name = "tracing-core" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" dependencies = [ "once_cell", ] @@ -1655,15 +1782,15 @@ checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" [[package]] name = "unicode-bidi" -version = "0.3.10" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54675592c1dbefd78cbd98db9bacd89886e1ca50692a0692baefffdeb92dd58" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.6" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" [[package]] name = "unicode-normalization" @@ -1674,12 +1801,6 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-width" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" - [[package]] name = "untrusted" version = "0.7.1" @@ -1688,15 +1809,21 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "url" -version = "2.3.1" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" dependencies = [ "form_urlencoded", - "idna 0.3.0", + "idna 0.4.0", "percent-encoding", ] +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "version_check" version = "0.9.4" @@ -1705,11 +1832,10 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "want" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ - "log", "try-lock", ] @@ -1727,9 +1853,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -1737,24 +1863,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.26", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1762,28 +1888,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.26", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" [[package]] name = "web-sys" -version = "0.3.61" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" dependencies = [ "js-sys", "wasm-bindgen", @@ -1791,18 +1917,18 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.23.0" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa54963694b65584e170cf5dc46aeb4dcaa5584e652ff5f3952e56d66aff0125" +checksum = "b03058f88386e5ff5310d9111d53f48b17d732b401aeb83a8d5190f2ac459338" dependencies = [ "rustls-webpki 0.100.1", ] [[package]] name = "widestring" -version = "0.5.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17882f045410753661207383517a6f62ec3dbeb6a4ed2acce01f0728238d1983" +checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8" [[package]] name = "winapi" @@ -1835,20 +1961,29 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" -version = "0.45.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" -version = "0.42.1" +version = "0.48.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" +checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", @@ -1861,60 +1996,61 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.1" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" [[package]] name = "windows_aarch64_msvc" -version = "0.42.1" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" [[package]] name = "windows_i686_gnu" -version = "0.42.1" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" [[package]] name = "windows_i686_msvc" -version = "0.42.1" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" [[package]] name = "windows_x86_64_gnu" -version = "0.42.1" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.1" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" [[package]] name = "windows_x86_64_msvc" -version = "0.42.1" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" [[package]] name = "winnow" -version = "0.4.6" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61de7bac303dc551fe038e2b3cef0f571087a47571ea6e79a87692ac99b99699" +checksum = "81fac9742fd1ad1bd9643b991319f72dd031016d44b77039a26977eb667141e7" dependencies = [ "memchr", ] [[package]] name = "winreg" -version = "0.10.1" +version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ - "winapi", + "cfg-if", + "windows-sys", ] diff --git a/Cargo.toml b/Cargo.toml index a70c7e93..e778d830 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ trust-dns-resolver = "0.22.0" tokio-test = "0.4.2" serde_json = "1" itertools = "0.10" +clap = { version = "4.3.1", features = ["derive", "env"] } [target.'cfg(not(target_env = "msvc"))'.dependencies] jemallocator = "0.5.0" diff --git a/src/admin.rs b/src/admin.rs index d17659e8..6c83f9b2 100644 --- a/src/admin.rs +++ b/src/admin.rs @@ -1,7 +1,7 @@ use crate::pool::BanReason; use crate::stats::pool::PoolStats; use bytes::{Buf, BufMut, BytesMut}; -use log::{debug, error, info, trace}; +use log::{error, info, trace}; use nix::sys::signal::{self, Signal}; use nix::unistd::Pid; use std::collections::HashMap; diff --git a/src/cmd_args.rs b/src/cmd_args.rs new file mode 100644 index 00000000..159eefe9 --- /dev/null +++ b/src/cmd_args.rs @@ -0,0 +1,17 @@ +use clap::Parser; +use log::LevelFilter; + +/// PgCat: Nextgen PostgreSQL Pooler +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +pub(crate) struct Args { + #[arg(default_value_t = String::from("pgcat.toml"), env)] + pub config_file: String, + + #[arg(short, long, default_value_t = LevelFilter::Info, env)] + pub log_level: log::LevelFilter, +} + +pub(crate) fn parse() -> Args { + return Args::parse(); +} diff --git a/src/main.rs b/src/main.rs index 6af4db95..2b3294ee 100644 --- a/src/main.rs +++ b/src/main.rs @@ -68,8 +68,11 @@ use pgcat::pool::{ClientServerMap, ConnectionPool}; use pgcat::prometheus::start_metric_server; use pgcat::stats::{Collector, Reporter, REPORTER}; +mod cmd_args; + fn main() -> Result<(), Box> { - pgcat::multi_logger::MultiLogger::init().unwrap(); + let args = cmd_args::parse(); + pgcat::multi_logger::MultiLogger::init(args.log_level).unwrap(); info!("Welcome to PgCat! Meow. (Version {})", VERSION); @@ -78,20 +81,12 @@ fn main() -> Result<(), Box> { std::process::exit(exitcode::CONFIG); } - let args = std::env::args().collect::>(); - - let config_file = if args.len() == 2 { - args[1].to_string() - } else { - String::from("pgcat.toml") - }; - // Create a transient runtime for loading the config for the first time. { let runtime = Builder::new_multi_thread().worker_threads(1).build()?; runtime.block_on(async { - match pgcat::config::parse(&config_file).await { + match pgcat::config::parse(args.config_file.as_str()).await { Ok(_) => (), Err(err) => { error!("Config parse error: {:?}", err); diff --git a/src/multi_logger.rs b/src/multi_logger.rs index 2a61794e..8c620698 100644 --- a/src/multi_logger.rs +++ b/src/multi_logger.rs @@ -1,4 +1,4 @@ -use log::{Level, Log, Metadata, Record, SetLoggerError}; +use log::{Level, LevelFilter, Log, Metadata, Record, SetLoggerError}; // This is a special kind of logger that allows sending logs to different // targets depending on the log level. @@ -25,8 +25,11 @@ pub struct MultiLogger { } impl MultiLogger { - fn new() -> Self { - let stderr_logger = env_logger::builder().format_timestamp_micros().build(); + fn new(filter: LevelFilter) -> Self { + let stderr_logger = env_logger::builder() + .filter(None, filter) + .format_timestamp_micros() + .build(); let stdout_logger = env_logger::Builder::from_env("STDOUT_LOG") .format_timestamp_micros() .target(env_logger::Target::Stdout) @@ -38,8 +41,8 @@ impl MultiLogger { } } - pub fn init() -> Result<(), SetLoggerError> { - let logger = Self::new(); + pub fn init(filter: LevelFilter) -> Result<(), SetLoggerError> { + let logger = Self::new(filter); log::set_max_level(logger.stderr_logger.filter()); log::set_boxed_logger(Box::new(logger)) @@ -75,6 +78,6 @@ mod test { #[test] fn test_init() { - MultiLogger::init().unwrap(); + MultiLogger::init(LevelFilter::Error).unwrap(); } } From f85e5bd9e8546c0f0670db212a23278035256ab4 Mon Sep 17 00:00:00 2001 From: Sebastian Webber Date: Wed, 19 Jul 2023 03:07:13 -0300 Subject: [PATCH 76/79] add support for multiple log formats (#517) this commit adds the tracing-subscriber crate and use its formatters to support multiple log formats. More details in https://github.com/postgresml/pgcat/issues/464#issuecomment-1641430299 Signed-off-by: Sebastian Webber --- Cargo.lock | 82 ++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 ++ src/cmd_args.rs | 22 ++++++++---- src/lib.rs | 2 +- src/main.rs | 19 +++++++++-- src/multi_logger.rs | 83 --------------------------------------------- 6 files changed, 117 insertions(+), 93 deletions(-) delete mode 100644 src/multi_logger.rs diff --git a/Cargo.lock b/Cargo.lock index b0d455ed..dc9869b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -919,6 +919,16 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-traits" version = "0.2.15" @@ -953,6 +963,12 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "parking_lot" version = "0.12.1" @@ -1027,6 +1043,8 @@ dependencies = [ "tokio-rustls", "tokio-test", "toml", + "tracing", + "tracing-subscriber", "trust-dns-resolver", "webpki-roots", ] @@ -1390,6 +1408,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -1546,6 +1573,16 @@ dependencies = [ "syn 2.0.26", ] +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "time" version = "0.1.45" @@ -1721,6 +1758,45 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +dependencies = [ + "nu-ansi-term", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", + "tracing-serde", ] [[package]] @@ -1824,6 +1900,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "version_check" version = "0.9.4" diff --git a/Cargo.toml b/Cargo.toml index e778d830..a1c09ab2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,8 @@ tokio-test = "0.4.2" serde_json = "1" itertools = "0.10" clap = { version = "4.3.1", features = ["derive", "env"] } +tracing = "0.1.37" +tracing-subscriber = { version = "0.3.17", features = ["json"]} [target.'cfg(not(target_env = "msvc"))'.dependencies] jemallocator = "0.5.0" diff --git a/src/cmd_args.rs b/src/cmd_args.rs index 159eefe9..08782f47 100644 --- a/src/cmd_args.rs +++ b/src/cmd_args.rs @@ -1,17 +1,27 @@ -use clap::Parser; -use log::LevelFilter; +use clap::{Parser, ValueEnum}; +use tracing::Level; /// PgCat: Nextgen PostgreSQL Pooler #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] -pub(crate) struct Args { +pub struct Args { #[arg(default_value_t = String::from("pgcat.toml"), env)] pub config_file: String, - #[arg(short, long, default_value_t = LevelFilter::Info, env)] - pub log_level: log::LevelFilter, + #[arg(short, long, default_value_t = tracing::Level::INFO, env)] + pub log_level: Level, + + #[clap(short='F', long, value_enum, default_value_t=LogFormat::Text, env)] + pub log_format: LogFormat, } -pub(crate) fn parse() -> Args { +pub fn parse() -> Args { return Args::parse(); } + +#[derive(ValueEnum, Clone, Debug)] +pub enum LogFormat { + Text, + Structured, + Debug, +} diff --git a/src/lib.rs b/src/lib.rs index db6167db..a0a71ebb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,13 +1,13 @@ pub mod admin; pub mod auth_passthrough; pub mod client; +pub mod cmd_args; pub mod config; pub mod constants; pub mod dns_cache; pub mod errors; pub mod messages; pub mod mirrors; -pub mod multi_logger; pub mod plugins; pub mod pool; pub mod prometheus; diff --git a/src/main.rs b/src/main.rs index 2b3294ee..a6b3aab2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -61,18 +61,31 @@ use std::str::FromStr; use std::sync::Arc; use tokio::sync::broadcast; +use pgcat::cmd_args; +use pgcat::cmd_args::LogFormat; use pgcat::config::{get_config, reload_config, VERSION}; use pgcat::dns_cache; use pgcat::messages::configure_socket; use pgcat::pool::{ClientServerMap, ConnectionPool}; use pgcat::prometheus::start_metric_server; use pgcat::stats::{Collector, Reporter, REPORTER}; - -mod cmd_args; +use tracing_subscriber; fn main() -> Result<(), Box> { let args = cmd_args::parse(); - pgcat::multi_logger::MultiLogger::init(args.log_level).unwrap(); + match args.log_format { + LogFormat::Structured => tracing_subscriber::fmt() + .json() + .with_max_level(args.log_level) + .init(), + LogFormat::Debug => tracing_subscriber::fmt() + .pretty() + .with_max_level(args.log_level) + .init(), + _ => tracing_subscriber::fmt() + .with_max_level(args.log_level) + .init(), + }; info!("Welcome to PgCat! Meow. (Version {})", VERSION); diff --git a/src/multi_logger.rs b/src/multi_logger.rs deleted file mode 100644 index 8c620698..00000000 --- a/src/multi_logger.rs +++ /dev/null @@ -1,83 +0,0 @@ -use log::{Level, LevelFilter, Log, Metadata, Record, SetLoggerError}; - -// This is a special kind of logger that allows sending logs to different -// targets depending on the log level. -// -// By default, if nothing is set, it acts as a regular env_log logger, -// it sends everything to standard error. -// -// If the Env variable `STDOUT_LOG` is defined, it will be used for -// configuring the standard out logger. -// -// The behavior is: -// - If it is an error, the message is written to standard error. -// - If it is not, and it matches the log level of the standard output logger (`STDOUT_LOG` env var), it will be send to standard output. -// - If the above is not true, it is sent to the stderr logger that will log it or not depending on the value -// of the RUST_LOG env var. -// -// So to summarize, if no `STDOUT_LOG` env var is present, the logger is the default logger. If `STDOUT_LOG` is set, everything -// but errors, that matches the log level set in the `STDOUT_LOG` env var is sent to stdout. You can have also some esoteric configuration -// where you set `RUST_LOG=debug` and `STDOUT_LOG=info`, in here, errors will go to stderr, warns and infos to stdout and debugs to stderr. -// -pub struct MultiLogger { - stderr_logger: env_logger::Logger, - stdout_logger: env_logger::Logger, -} - -impl MultiLogger { - fn new(filter: LevelFilter) -> Self { - let stderr_logger = env_logger::builder() - .filter(None, filter) - .format_timestamp_micros() - .build(); - let stdout_logger = env_logger::Builder::from_env("STDOUT_LOG") - .format_timestamp_micros() - .target(env_logger::Target::Stdout) - .build(); - - Self { - stderr_logger, - stdout_logger, - } - } - - pub fn init(filter: LevelFilter) -> Result<(), SetLoggerError> { - let logger = Self::new(filter); - - log::set_max_level(logger.stderr_logger.filter()); - log::set_boxed_logger(Box::new(logger)) - } -} - -impl Log for MultiLogger { - fn enabled(&self, metadata: &Metadata) -> bool { - self.stderr_logger.enabled(metadata) && self.stdout_logger.enabled(metadata) - } - - fn log(&self, record: &Record) { - if record.level() == Level::Error { - self.stderr_logger.log(record); - } else { - if self.stdout_logger.matches(record) { - self.stdout_logger.log(record); - } else { - self.stderr_logger.log(record); - } - } - } - - fn flush(&self) { - self.stderr_logger.flush(); - self.stdout_logger.flush(); - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_init() { - MultiLogger::init(LevelFilter::Error).unwrap(); - } -} From 19cb8a3022b298c39741d6c654f6193953a77337 Mon Sep 17 00:00:00 2001 From: Sebastian Webber Date: Thu, 20 Jul 2023 01:15:55 -0300 Subject: [PATCH 77/79] add `--no-color` option to disable colors in the terminal (#518) add --no-color option to disable colors this commit adds a new option to disable colors in the terminal and also moves the logger configuration to a different crate. Signed-off-by: Sebastian Webber --- src/cmd_args.rs | 9 +++++++++ src/lib.rs | 1 + src/logger.rs | 14 ++++++++++++++ src/main.rs | 17 ++--------------- 4 files changed, 26 insertions(+), 15 deletions(-) create mode 100644 src/logger.rs diff --git a/src/cmd_args.rs b/src/cmd_args.rs index 08782f47..3989d670 100644 --- a/src/cmd_args.rs +++ b/src/cmd_args.rs @@ -13,6 +13,15 @@ pub struct Args { #[clap(short='F', long, value_enum, default_value_t=LogFormat::Text, env)] pub log_format: LogFormat, + + #[arg( + short, + long, + default_value_t = false, + env, + help = "disable colors in the log output" + )] + pub no_color: bool, } pub fn parse() -> Args { diff --git a/src/lib.rs b/src/lib.rs index a0a71ebb..6a8a1e36 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ pub mod config; pub mod constants; pub mod dns_cache; pub mod errors; +pub mod logger; pub mod messages; pub mod mirrors; pub mod plugins; diff --git a/src/logger.rs b/src/logger.rs new file mode 100644 index 00000000..c3536a0c --- /dev/null +++ b/src/logger.rs @@ -0,0 +1,14 @@ +use crate::cmd_args::{Args, LogFormat}; +use tracing_subscriber; + +pub fn init(args: &Args) { + let trace_sub = tracing_subscriber::fmt() + .with_max_level(args.log_level) + .with_ansi(!args.no_color); + + match args.log_format { + LogFormat::Structured => trace_sub.json().init(), + LogFormat::Debug => trace_sub.pretty().init(), + _ => trace_sub.init(), + }; +} diff --git a/src/main.rs b/src/main.rs index a6b3aab2..916d1a8e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -62,30 +62,17 @@ use std::sync::Arc; use tokio::sync::broadcast; use pgcat::cmd_args; -use pgcat::cmd_args::LogFormat; use pgcat::config::{get_config, reload_config, VERSION}; use pgcat::dns_cache; +use pgcat::logger; use pgcat::messages::configure_socket; use pgcat::pool::{ClientServerMap, ConnectionPool}; use pgcat::prometheus::start_metric_server; use pgcat::stats::{Collector, Reporter, REPORTER}; -use tracing_subscriber; fn main() -> Result<(), Box> { let args = cmd_args::parse(); - match args.log_format { - LogFormat::Structured => tracing_subscriber::fmt() - .json() - .with_max_level(args.log_level) - .init(), - LogFormat::Debug => tracing_subscriber::fmt() - .pretty() - .with_max_level(args.log_level) - .init(), - _ => tracing_subscriber::fmt() - .with_max_level(args.log_level) - .init(), - }; + logger::init(&args); info!("Welcome to PgCat! Meow. (Version {})", VERSION); From 2a8f3653a6230b8f2c639c80ac716030f8e39973 Mon Sep 17 00:00:00 2001 From: Mostafa Abdelraouf Date: Fri, 21 Jul 2023 01:06:01 -0500 Subject: [PATCH 78/79] Fix COPY FROM and add tests (#522) * Fix COPY FROM and add tests * E * fmt --- src/client.rs | 4 +- src/server.rs | 29 +++++++++++- tests/ruby/copy_spec.rb | 102 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 132 insertions(+), 3 deletions(-) create mode 100644 tests/ruby/copy_spec.rb diff --git a/src/client.rs b/src/client.rs index 6c0d06fc..7d5e9798 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1260,7 +1260,7 @@ where // Release server back to the pool if we are in transaction mode. // If we are in session mode, we keep the server until the client disconnects. - if self.transaction_mode { + if self.transaction_mode && !server.in_copy_mode() { self.stats.idle(); break; @@ -1410,7 +1410,7 @@ where // Release server back to the pool if we are in transaction mode. // If we are in session mode, we keep the server until the client disconnects. - if self.transaction_mode { + if self.transaction_mode && !server.in_copy_mode() { break; } } diff --git a/src/server.rs b/src/server.rs index fa68b678..55444fba 100644 --- a/src/server.rs +++ b/src/server.rs @@ -170,6 +170,9 @@ pub struct Server { /// Is there more data for the client to read. data_available: bool, + /// Is the server in copy-in or copy-out modes + in_copy_mode: bool, + /// Is the server broken? We'll remote it from the pool if so. bad: bool, @@ -677,6 +680,7 @@ impl Server { process_id, secret_key, in_transaction: false, + in_copy_mode: false, data_available: false, bad: false, cleanup_state: CleanupState::new(), @@ -828,8 +832,19 @@ impl Server { break; } + // ErrorResponse + 'E' => { + if self.in_copy_mode { + self.in_copy_mode = false; + } + } + // CommandComplete 'C' => { + if self.in_copy_mode { + self.in_copy_mode = false; + } + let mut command_tag = String::new(); match message.reader().read_to_string(&mut command_tag) { Ok(_) => { @@ -873,10 +888,14 @@ impl Server { } // CopyInResponse: copy is starting from client to server. - 'G' => break, + 'G' => { + self.in_copy_mode = true; + break; + } // CopyOutResponse: copy is starting from the server to the client. 'H' => { + self.in_copy_mode = true; self.data_available = true; break; } @@ -1030,6 +1049,10 @@ impl Server { self.in_transaction } + pub fn in_copy_mode(&self) -> bool { + self.in_copy_mode + } + /// We don't buffer all of server responses, e.g. COPY OUT produces too much data. /// The client is responsible to call `self.recv()` while this method returns true. pub fn is_data_available(&self) -> bool { @@ -1129,6 +1152,10 @@ impl Server { self.cleanup_state.reset(); } + if self.in_copy_mode() { + warn!("Server returned while still in copy-mode"); + } + Ok(()) } diff --git a/tests/ruby/copy_spec.rb b/tests/ruby/copy_spec.rb new file mode 100644 index 00000000..5d3f2c02 --- /dev/null +++ b/tests/ruby/copy_spec.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true +require_relative 'spec_helper' + + +describe "COPY Handling" do + let(:processes) { Helpers::Pgcat.single_instance_setup("sharded_db", 5) } + before do + new_configs = processes.pgcat.current_config + + # Allow connections in the pool to expire faster + new_configs["general"]["idle_timeout"] = 5 + processes.pgcat.update_config(new_configs) + # We need to kill the old process that was using the default configs + processes.pgcat.stop + processes.pgcat.start + processes.pgcat.wait_until_ready + end + + before do + processes.all_databases.first.with_connection do |conn| + conn.async_exec "CREATE TABLE copy_test_table (a TEXT,b TEXT,c TEXT,d TEXT)" + end + end + + after do + processes.all_databases.first.with_connection do |conn| + conn.async_exec "DROP TABLE copy_test_table;" + end + end + + after do + processes.all_databases.map(&:reset) + processes.pgcat.shutdown + end + + describe "COPY FROM" do + context "within transaction" do + it "finishes within alloted time" do + conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) + Timeout.timeout(3) do + conn.async_exec("BEGIN") + conn.copy_data "COPY copy_test_table FROM STDIN CSV" do + sleep 0.5 + conn.put_copy_data "some,data,to,copy\n" + conn.put_copy_data "more,data,to,copy\n" + end + conn.async_exec("COMMIT") + end + + res = conn.async_exec("SELECT * FROM copy_test_table").to_a + expect(res).to eq([ + {"a"=>"some", "b"=>"data", "c"=>"to", "d"=>"copy"}, + {"a"=>"more", "b"=>"data", "c"=>"to", "d"=>"copy"} + ]) + end + end + + context "outside transaction" do + it "finishes within alloted time" do + conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) + Timeout.timeout(3) do + conn.copy_data "COPY copy_test_table FROM STDIN CSV" do + sleep 0.5 + conn.put_copy_data "some,data,to,copy\n" + conn.put_copy_data "more,data,to,copy\n" + end + end + + res = conn.async_exec("SELECT * FROM copy_test_table").to_a + expect(res).to eq([ + {"a"=>"some", "b"=>"data", "c"=>"to", "d"=>"copy"}, + {"a"=>"more", "b"=>"data", "c"=>"to", "d"=>"copy"} + ]) + end + end + end + + describe "COPY TO" do + before do + conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) + conn.async_exec("BEGIN") + conn.copy_data "COPY copy_test_table FROM STDIN CSV" do + conn.put_copy_data "some,data,to,copy\n" + conn.put_copy_data "more,data,to,copy\n" + end + conn.async_exec("COMMIT") + conn.close + end + + it "works" do + res = [] + conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) + conn.copy_data "COPY copy_test_table TO STDOUT CSV" do + while row=conn.get_copy_data + res << row + end + end + expect(res).to eq(["some,data,to,copy\n", "more,data,to,copy\n"]) + end + end + +end From 4cf54a612251fd01c6565212a74776575dd188e8 Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Tue, 25 Jul 2023 10:27:04 -0700 Subject: [PATCH 79/79] Release 1.1 (#526) --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dc9869b6..9bfd4fb6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1000,7 +1000,7 @@ checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "pgcat" -version = "1.0.2-alpha3" +version = "1.1.0" dependencies = [ "arc-swap", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index a1c09ab2..5f4af313 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pgcat" -version = "1.0.2-alpha3" +version = "1.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html