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/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/.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/.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/.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 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..3118a49a 100644 --- a/CONFIG.md +++ b/CONFIG.md @@ -1,4 +1,4 @@ -# PgCat Configurations +# PgCat Configurations ## `general` Section ### host @@ -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 @@ -108,10 +116,10 @@ If we should log client disconnections ### autoreload ``` path: general.autoreload -default: false +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 ``` @@ -143,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 ``` @@ -152,7 +166,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 ``` @@ -180,6 +194,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 @@ -213,7 +243,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 @@ -234,7 +264,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: @@ -256,7 +286,40 @@ Current options: `pg_bigint_hash`: PARTITION BY HASH (Postgres hashing function) `sha1`: A hashing function based on SHA1 -### automatic_sharding_key (experimental) +### 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 default: @@ -289,7 +352,8 @@ path: pools..users..username default: "sharding_user" ``` -Postgresql username +PostgreSQL username used to authenticate the user and connect to the server +if `server_username` is not set. ### password ``` @@ -297,7 +361,26 @@ path: pools..users..password default: "sharding_user" ``` -Postgresql password +PostgreSQL password used to authenticate the user and connect to the server +if `server_password` is not set. + +### server_username +``` +path: pools..users..server_username +default: +example: "another_user" +``` + +PostgreSQL username used to connect to the server. + +### server_password +``` +path: pools..users..server_password +default: +example: "another_password" +``` + +PostgreSQL password used to connect to the server. ### pool_size ``` @@ -328,7 +411,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/Cargo.lock b/Cargo.lock index 9124996f..9bfd4fb6 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 = "0.7.20" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +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,21 +41,103 @@ 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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" +[[package]] +name = "async-stream" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.26", +] + [[package]] name = "async-trait" -version = "0.1.66" +version = "0.1.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a564d521dd56509c4c47480d00b80ee55f7e385ae48db5744c67ad50c92d2ebf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.26", +] + +[[package]] +name = "atomic_enum" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b84f9ebcc6c1f5b8cb160f6990096a5c127f423fcb6e1ccc46c370cbdfb75dfc" +checksum = "6227a8d6fdb862bcb100c4314d0d9579e5cd73fa6df31a2e6f6e1acd3c5f1207" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -43,17 +146,32 @@ 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" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1627eccf3aa91405435ba240be23513eeca466b5dc33866422672264de061582" +checksum = "98b4b0f25f18bcdc3ac72bdb486ed0acf7e185221fd4dc985bc15db5800b0ba2" dependencies = [ "async-trait", "futures-channel", @@ -68,20 +186,32 @@ 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" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" @@ -103,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", @@ -117,26 +247,63 @@ 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 = "clap_builder" +version = "4.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af410122b9778e024f9e0fb35682cc09cc3f85cad5e8d3ba8f47a9702df6e73d" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", ] +[[package]] +name = "clap_derive" +version = "4.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.26", +] + +[[package]] +name = "clap_lex" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "core-foundation-sys" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" [[package]] name = "cpufeatures" -version = "0.2.5" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" dependencies = [ "libc", ] @@ -152,58 +319,38 @@ dependencies = [ ] [[package]] -name = "cxx" -version = "1.0.91" +name = "data-encoding" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86d3488e7665a7a483b57e25bdd90d0aeb2bc7608c8d0346acf2ad3f1caf1d62" -dependencies = [ - "cc", - "cxxbridge-flags", - "cxxbridge-macro", - "link-cplusplus", -] +checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] -name = "cxx-build" -version = "1.0.91" +name = "digest" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48fcaf066a053a41a81dfb14d57d99738b767febb8b735c3016e469fac5da690" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "cc", - "codespan-reporting", - "once_cell", - "proc-macro2", - "quote", - "scratch", - "syn", + "block-buffer", + "crypto-common", + "subtle", ] [[package]] -name = "cxxbridge-flags" -version = "1.0.91" +name = "either" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2ef98b8b717a829ca5603af80e1f9e2e48013ab227b68ef37872ef84ee479bf" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" [[package]] -name = "cxxbridge-macro" -version = "1.0.91" +name = "enum-as-inner" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "086c685979a698443656e5cf7856c95c642295a38599f12fb1ff76fb28d19892" +checksum = "c9720bba047d567ffc8a3cba48bf19126600e249ab7f128e9233e6376976a116" dependencies = [ + "heck", "proc-macro2", "quote", - "syn", -] - -[[package]] -name = "digest" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" -dependencies = [ - "block-buffer", - "crypto-common", - "subtle", + "syn 1.0.109", ] [[package]] @@ -219,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]] @@ -246,17 +399,32 @@ 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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[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" -version = "0.3.26" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13e2792b0ff0340399d58445b88fd9770e3489eff258a4cbc1523418f12abf84" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" dependencies = [ "futures-channel", "futures-core", @@ -269,9 +437,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.26" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e5317663a9089767a1ec00a487df42e0ca174b61b4483213ac24448e4664df5" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" dependencies = [ "futures-core", "futures-sink", @@ -279,15 +447,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.26" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec90ff4d0fe1f57d600049061dc6bb68ed03c7d2fbd697274c41805dcb3f8608" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" [[package]] name = "futures-executor" -version = "0.3.26" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8de0a35a6ab97ec8869e32a2473f4b1324459e14c29275d14b10cb1fd19b50e" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" dependencies = [ "futures-core", "futures-task", @@ -296,38 +464,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.26" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfb8371b6fb2aeb2d280374607aeabfc99d95c72edfe51692e42d3d7f0d08531" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" [[package]] name = "futures-macro" -version = "0.3.26" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a73af87da33b5acf53acfebdc339fe592ecf5357ac7c0a7734ab9d8c876a70" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.26", ] [[package]] name = "futures-sink" -version = "0.3.26" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f310820bb3e8cfd46c80db4d7fb8353e15dfff853a127158425f31e0be6c8364" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" [[package]] name = "futures-task" -version = "0.3.26" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf79a1bf610b10f42aea489289c5a2c478a786509693b80cd39c44ccd936366" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" [[package]] name = "futures-util" -version = "0.3.26" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c1d6de3acfef38d2be4b1f543f553131788603495be83da675e180c8d6b7bd1" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" dependencies = [ "futures-channel", "futures-core", @@ -343,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", @@ -353,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.15" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4" +checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" dependencies = [ "bytes", "fnv", @@ -374,7 +548,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap", + "indexmap 1.9.3", "slab", "tokio", "tokio-util", @@ -388,19 +562,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] -name = "hermit-abi" -version = "0.2.6" +name = "hashbrown" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" -dependencies = [ - "libc", -] +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +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" @@ -411,6 +588,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" @@ -453,9 +641,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.24" +version = "0.14.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e011372fa0b68db8350aa7a248930ecc7839bf46d8485577d69f117a75f164c" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" dependencies = [ "bytes", "futures-channel", @@ -468,7 +656,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2", + "socket2 0.4.9", "tokio", "tower-service", "tracing", @@ -477,65 +665,111 @@ 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 = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "idna" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" dependencies = [ - "cxx", - "cxx-build", + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[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.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", + "equivalent", + "hashbrown 0.14.0", +] + +[[package]] +name = "ipconfig" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" +dependencies = [ + "socket2 0.5.3", + "widestring", "windows-sys", + "winreg", ] +[[package]] +name = "ipnet" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +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 0.3.1", - "io-lifetimes", + "hermit-abi", "rustix", "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" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "jemalloc-sys" @@ -559,39 +793,42 @@ 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", ] +[[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" +version = "0.2.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" [[package]] -name = "link-cplusplus" -version = "1.0.8" +name = "linked-hash-map" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" -dependencies = [ - "cc", -] +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", @@ -599,13 +836,31 @@ dependencies = [ [[package]] name = "log" -version = "0.4.17" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" + +[[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" dependencies = [ - "cfg-if", + "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.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + [[package]] name = "md-5" version = "0.10.5" @@ -630,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", ] @@ -648,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", @@ -657,13 +920,13 @@ dependencies = [ ] [[package]] -name = "num-integer" -version = "0.1.45" +name = "nu-ansi-term" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" dependencies = [ - "autocfg", - "num-traits", + "overload", + "winapi", ] [[package]] @@ -677,19 +940,34 @@ 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", ] +[[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 = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "parking_lot" @@ -703,32 +981,42 @@ 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.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + [[package]] name = "pgcat" -version = "1.0.0" +version = "1.1.0" dependencies = [ "arc-swap", "async-trait", + "atomic_enum", "base64", "bb8", "bytes", "chrono", + "clap", "env_logger", "exitcode", + "fallible-iterator", "futures", "hmac", "hyper", + "itertools", "jemallocator", "log", "md-5", @@ -737,26 +1025,35 @@ dependencies = [ "once_cell", "parking_lot", "phf", + "pin-project", + "postgres-protocol", "rand", "regex", + "rustls", "rustls-pemfile", "serde", "serde_derive", + "serde_json", "sha-1", "sha2", - "socket2", + "socket2 0.4.9", "sqlparser", "stringprep", "tokio", "tokio-rustls", + "tokio-test", "toml", + "tracing", + "tracing-subscriber", + "trust-dns-resolver", + "webpki-roots", ] [[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", @@ -764,9 +1061,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", @@ -774,31 +1071,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", + "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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "030ad2bc4db10a8944cb0d837f158bdfec4d4a4873ab701a95046770d11f8842" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" +dependencies = [ + "proc-macro2", + "quote", + "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" @@ -806,6 +1123,24 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "postgres-protocol" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b7fa9f396f51dffd61546fd8573ee20592287996568e6175ceb0f8699ad75d" +dependencies = [ + "base64", + "byteorder", + "bytes", + "fallible-iterator", + "hmac", + "md-5", + "memchr", + "rand", + "sha2", + "stringprep", +] + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -814,18 +1149,24 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.51" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" 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.23" +version = "1.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +checksum = "5fe8a65d69dd0808184ebb5f836ab526bb259db23c657efa38711b1072ee47f0" dependencies = [ "proc-macro2", ] @@ -862,18 +1203,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.7.1" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" +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 = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310" dependencies = [ "aho-corasick", "memchr", @@ -882,9 +1235,19 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.28" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" + +[[package]] +name = "resolv-conf" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" +checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" +dependencies = [ + "hostname", + "quick-error", +] [[package]] name = "ring" @@ -901,15 +1264,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", @@ -917,36 +1285,56 @@ dependencies = [ [[package]] name = "rustls" -version = "0.20.8" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" +checksum = "79ea77c539259495ce8ca47f53e66ae0330a8819f67e23ac96ca02f50e7b7d36" dependencies = [ "log", "ring", + "rustls-webpki 0.101.1", "sct", - "webpki", ] [[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", ] [[package]] -name = "scopeguard" -version = "1.1.0" +name = "rustls-webpki" +version = "0.100.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +checksum = "d6207cd5ed3d8dca7816f8f3725513a34609c0c765bf652b8c3cb4cfd87db46b" +dependencies = [ + "ring", + "untrusted", +] [[package]] -name = "scratch" -version = "1.0.3" +name = "rustls-webpki" +version = "0.101.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2" +checksum = "15f36a6828982f422756984e47912a7a51dcbc2a197aa791158f8ca61cd8204e" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sct" @@ -960,26 +1348,40 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.154" +version = "1.0.171" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cdd151213925e7f1ab45a9bbfb129316bd00799784b174b7cc7bcd16961c49e" +checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9" +dependencies = [ + "serde_derive", +] [[package]] name = "serde_derive" -version = "1.0.154" +version = "1.0.171" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fc80d722935453bcafdc2c9a73cd6fac4dc1938f0346035d84bf99fa9e33217" +checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.26", +] + +[[package]] +name = "serde_json" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d03b412469450d4404fe8499a268edd7f8b79fecb074b0d812ad64ca21f4031b" +dependencies = [ + "itoa", + "ryu", + "serde", ] [[package]] name = "serde_spanned" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4" +checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" dependencies = [ "serde", ] @@ -997,15 +1399,24 @@ 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", "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" @@ -1032,20 +1443,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" @@ -1054,11 +1475,23 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "sqlparser" -version = "0.32.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0366f270dbabb5cc2e4c88427dc4c08bba144f81e32fbd459a013f26a4d16aa0" +checksum = "37d3706eefb17039056234df6b566b0014f303f867f2656108334a55b8096f59" 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]] @@ -1069,19 +1502,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" @@ -1094,6 +1533,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "2.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45c3457aacde3c65315de5031ec191ce46604304d2446e803d71ade03308d970" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "termcolor" version = "1.2.0" @@ -1103,6 +1553,36 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "thiserror" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a35fc5b8971143ca348fa6df4f024d4d55264f3468c71ad1c2f365b0a4d58c42" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f" +dependencies = [ + "proc-macro2", + "quote", + "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" @@ -1131,51 +1611,74 @@ 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", + "syn 2.0.26", ] [[package]] name = "tokio-rustls" -version = "0.23.4" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" 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 = "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" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" dependencies = [ "bytes", "futures-core", @@ -1187,9 +1690,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.7.2" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7afcae9e3f0fe2c370fd4657108972cbb2fa9db1b9f84849cefd80741b01cb6" +checksum = "c17e963a819c331dcacd7ab957d80bc2b9a9c1e71c804826d2f283dd65306542" dependencies = [ "serde", "serde_spanned", @@ -1199,20 +1702,20 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.19.4" +version = "0.19.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a1eb0622d28f4b9c90adc4ea4b2b46b47663fde9ac5fafcb14a1369d5508825" +checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a" dependencies = [ - "indexmap", + "indexmap 2.0.0", "serde", "serde_spanned", "toml_datetime", @@ -1233,16 +1736,112 @@ checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if", "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" +dependencies = [ + "proc-macro2", + "quote", + "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", + "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]] +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]] @@ -1259,15 +1858,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" @@ -1278,18 +1877,35 @@ 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" 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 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 = "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" @@ -1298,11 +1914,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", ] @@ -1320,9 +1935,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", @@ -1330,24 +1945,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", + "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", @@ -1355,43 +1970,48 @@ 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", + "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", ] [[package]] -name = "webpki" -version = "0.22.0" +name = "webpki-roots" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +checksum = "b03058f88386e5ff5310d9111d53f48b17d732b401aeb83a8d5190f2ac459338" dependencies = [ - "ring", - "untrusted", + "rustls-webpki 0.100.1", ] +[[package]] +name = "widestring" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8" + [[package]] name = "winapi" version = "0.3.9" @@ -1423,20 +2043,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", @@ -1449,51 +2078,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.3.3" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faf09497b8f8b5ac5d3bb4d05c0a99be20f26fd3d5f2db7b0716e946d5103658" +checksum = "81fac9742fd1ad1bd9643b991319f72dd031016d44b77039a26977eb667141e7" dependencies = [ "memchr", ] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys", +] diff --git a/Cargo.toml b/Cargo.toml index e5b1de66..5f4af313 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,26 +1,25 @@ [package] name = "pgcat" -version = "1.0.0" +version = "1.1.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" md-5 = "0.10" -bb8 = "0.8.0" +bb8 = "0.8.1" async-trait = "0.1" 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.32.0" +sqlparser = {version = "0.34", features = ["visitor"] } log = "0.4" arc-swap = "1" env_logger = "0.10" @@ -29,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"] } @@ -37,6 +36,20 @@ exitcode = "1.1.2" futures = "0.3" socket2 = { version = "0.4.7", features = ["all"] } 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"] } +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"] } +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/README.md b/README.md index 28433def..ae310cde 100644 --- a/README.md +++ b/README.md @@ -18,23 +18,56 @@ 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.| | 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. | ## 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 @@ -79,7 +112,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. @@ -98,7 +131,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/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/dev/docker-compose.yaml b/dev/docker-compose.yaml index da759383..5fda0c13 100644 --- a/dev/docker-compose.yaml +++ b/dev/docker-compose.yaml @@ -25,7 +25,9 @@ x-common-env-pg: services: main: - image: kubernetes/pause + image: gcr.io/google_containers/pause:3.2 + ports: + - 6432 pg1: <<: *common-definition-pg @@ -56,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", "10432", "-c", "shared_preload_libraries=pg_stat_statements", "-c", "pg_stat_statements.track=all", "-c", "pg_stat_statements.max=100000"] toxiproxy: build: . @@ -69,6 +78,7 @@ services: - pg2 - pg3 - pg4 + - pg5 pgcat-shell: stdin_open: true diff --git a/examples/docker/pgcat.toml b/examples/docker/pgcat.toml index c41c8cdd..5fd929de 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" @@ -76,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/images/one_signal.webp b/images/one_signal.webp new file mode 100644 index 00000000..79f3d129 Binary files /dev/null and b/images/one_signal.webp differ 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 0d883a33..3e8801b6 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 @@ -45,7 +48,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 @@ -57,10 +60,22 @@ tcp_keepalives_count = 5 # Number of seconds between keepalive packets. tcp_keepalives_interval = 5 -# Path to TLS Certficate file to use for TLS connections -# tls_certificate = "server.cert" +# 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 -# 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.. @@ -68,6 +83,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 @@ -113,6 +180,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" @@ -122,18 +204,86 @@ 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 + +# 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] + +[pools.sharded_db.plugins.prewarmer] +enabled = true +queries = [ + "SELECT pg_prewarm('pgbench_accounts')", +] + +[pools.sharded_db.plugins.query_logger] +enabled = false + +[pools.sharded_db.plugins.table_access] +enabled = false +tables = [ + "pg_user", + "pg_roles", + "pg_database", +] + +[pools.sharded_db.plugins.intercept] +enabled = true + +[pools.sharded_db.plugins.intercept.queries.0] + +query = "select current_database() as a, current_schemas(false) as b" +schema = [ + ["a", "text"], + ["b", "text"], +] +result = [ + ["${DATABASE}", "{public}"], +] + +[pools.sharded_db.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 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 +# 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. 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 @@ -178,6 +328,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/admin.rs b/src/admin.rs index feea3a15..6c83f9b2 100644 --- a/src/admin.rs +++ b/src/admin.rs @@ -1,21 +1,21 @@ use crate::pool::BanReason; -/// Admin database. +use crate::stats::pool::PoolStats; 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; 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_address_stats, get_client_stats, get_pool_stats, get_server_stats, ClientState, ServerState, -}; -use crate::ClientServerMap; +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(); @@ -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 @@ -158,7 +162,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 +177,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 +196,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 +211,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(), ])); @@ -227,52 +259,51 @@ 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 pool_lookup = PoolStats::construct_pool_lookup(); + let mut res = BytesMut::new(); + 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")); - 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), - ]; + // ReadyForQuery + res.put_u8(b'Z'); + res.put_i32(5); + res.put_u8(b'I'); + + 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(); - 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; - let mut row = vec![ - user_pool.db.clone(), - user_pool.user.clone(), - pool_config.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); - } - res.put(data_row(&row)); - } + 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 @@ -320,17 +351,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(), @@ -400,7 +431,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 +648,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 +655,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 +698,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(), ]; @@ -717,6 +742,9 @@ where ("bytes_sent", DataType::Numeric), ("bytes_received", DataType::Numeric), ("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(); @@ -724,21 +752,34 @@ 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(), + server + .prepared_hit_count + .load(Ordering::Relaxed) + .to_string(), + server + .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/auth_passthrough.rs b/src/auth_passthrough.rs new file mode 100644 index 00000000..fc0f6dc6 --- /dev/null +++ b/src/auth_passthrough.rs @@ -0,0 +1,134 @@ +use crate::errors::Error; +use crate::pool::ConnectionPool; +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()), + server_username: None, + server_password: None, + pool_size: 1, + statement_timeout: 0, + pool_mode: None, + server_lifetime: None, + min_pool_size: None, + }; + + 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)) + ) + } + } + } +} + +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 68f6f575..7d5e9798 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,10 +1,11 @@ -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}; use log::{debug, error, info, trace, warn}; - +use once_cell::sync::Lazy; use std::collections::HashMap; +use std::sync::{atomic::AtomicUsize, Arc}; use std::time::Instant; use tokio::io::{split, AsyncReadExt, BufReader, ReadHalf, WriteHalf}; use tokio::net::TcpStream; @@ -12,18 +13,26 @@ use tokio::sync::broadcast::Receiver; use tokio::sync::mpsc::Sender; use crate::admin::{generate_server_info_for_admin, handle_admin}; -use crate::config::{get_config, get_idle_client_in_transaction_timeout, Address, PoolMode}; +use crate::auth_passthrough::refetch_auth_hash; +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; 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, ServerStats}; 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, @@ -66,8 +75,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 +84,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, @@ -92,6 +101,9 @@ pub struct Client { /// Used to notify clients about an impending shutdown shutdown: Receiver<()>, + + /// Prepared statements + prepared_statements: HashMap, } /// Client entrypoint. @@ -135,6 +147,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 +199,10 @@ pub async fn client_entrypoint( if !client.is_admin() { let _ = drain.send(-1).await; + + if result.is_err() { + client.stats.disconnect(); + } } result @@ -194,7 +214,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), @@ -233,6 +253,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 +282,11 @@ pub async fn client_entrypoint( if !client.is_admin() { let _ = drain.send(-1).await; - } + if result.is_err() { + client.stats.disconnect(); + } + } result } @@ -354,9 +381,9 @@ 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), } @@ -382,7 +409,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. @@ -390,7 +416,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(), )) } }; @@ -405,6 +431,8 @@ where None => "pgcat", }; + let client_identifier = ClientIdentifier::new(&application_name, &username, &pool_name); + let admin = ["pgcat", "pgbouncer"] .iter() .filter(|db| *db == pool_name) @@ -435,7 +463,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 @@ -448,19 +481,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, @@ -469,10 +513,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()) @@ -491,18 +537,100 @@ 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, + )); } }; - // 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() { + wrong_password(&mut write, username).await?; + return Err(Error::ClientAuthImpossible(username.into())); + } - 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 {}, obtained. Updating.", client_identifier); + + { + let mut pool_auth_hash = pool.auth_hash.write(); + *pool_auth_hash = Some(fetched_hash.clone()); + } + + hash = Some(fetched_hash); + } + + Err(err) => { + wrong_password(&mut write, username).await?; + + return Err(Error::ClientAuthPassthroughError( + err.to_string(), + client_identifier, + )); + } + } + }; + + 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 {}, will try to refetch it.", + client_identifier + ); + + 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. + if new_password_hash == password_response { + 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::ClientGeneralError( + "Invalid password".into(), + client_identifier, + )); + } } let transaction_mode = pool.settings.pool_mode == PoolMode::Transaction; @@ -537,6 +665,13 @@ where ready_for_query(&mut write).await?; trace!("Startup OK"); + let stats = Arc::new(ClientStats::new( + process_id, + application_name, + username, + pool_name, + tokio::time::Instant::now(), + )); Ok(Client { read: BufReader::new(read), @@ -552,12 +687,13 @@ 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(), shutdown, connected_to_server: false, + prepared_statements: HashMap::new(), }) } @@ -583,15 +719,16 @@ 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"), shutdown, connected_to_server: false, + prepared_statements: HashMap::new(), }) } @@ -627,12 +764,15 @@ 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()); + + // 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 @@ -643,20 +783,25 @@ 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( &mut self.write, "terminating connection due to administrator command" ).await?; - return Ok(()) + + self.stats.disconnect(); + return Ok(()); } // Admin clients ignore shutdown. @@ -675,28 +820,75 @@ 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; } '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); + } } } '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() { - 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; } 'B' => { + if prepared_statements_enabled { + (prepared_statement, message) = self.rewrite_bind(message).await?; + } + self.buffer.put(&message[..]); if query_router.query_parser_enabled() { @@ -708,9 +900,25 @@ where 'X' => { debug!("Client disconnecting"); + + self.stats.disconnect(); + 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; + } + } + } + _ => (), } @@ -721,6 +929,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. @@ -757,7 +977,7 @@ where current_shard, ), ) - .await?; + .await?; } else { custom_protocol_response_ok(&mut self.write, "SET SHARD").await?; } @@ -802,10 +1022,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,15 +1040,32 @@ 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(); } + 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; } }; @@ -840,11 +1080,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 {:?}", @@ -871,7 +1110,58 @@ 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) { + match server.prepare(statement).await { + Ok(_) => (), + Err(err) => { + pool.ban( + &address, + BanReason::MessageSendFailed, + Some(&self.stats), + ); + return Err(err); + } + } + } + } + + // 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"); @@ -885,6 +1175,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); @@ -892,11 +1183,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 @@ -915,18 +1220,49 @@ 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(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 { + if self.transaction_mode && !server.in_copy_mode() { + self.stats.idle(); + break; } } @@ -935,26 +1271,77 @@ where // Terminate 'X' => { server.checkin_cleanup().await?; + self.stats.disconnect(); self.release(); + if prepared_statements_enabled { + server.maintain_cache().await?; + } + return Ok(()); } // 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 { + plugin_output = Some(output); + } + } + } + self.buffer.put(&message[..]); } // 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[..]); + } + + // 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[..]); } @@ -969,12 +1356,30 @@ 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; // 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 @@ -987,17 +1392,25 @@ 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. - if self.transaction_mode { + if self.transaction_mode && !server.in_copy_mode() { break; } } @@ -1028,9 +1441,11 @@ 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 { + match write_all_flush(&mut self.write, &response).await { Ok(_) => (), Err(err) => { server.mark_bad(); @@ -1039,7 +1454,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. @@ -1059,12 +1475,18 @@ 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()); + + if prepared_statements_enabled { + server.maintain_cache().await?; + } + + server.stats().idle(); self.connected_to_server = false; self.release(); - self.stats.client_idle(self.process_id); + self.stats.idle(); } } @@ -1091,6 +1513,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(); @@ -1104,6 +1627,7 @@ where server: &mut Server, address: &Address, pool: &ConnectionPool, + client_stats: &ClientStats, ) -> Result<(), Error> { debug!("Sending {} to server", code); @@ -1119,9 +1643,11 @@ 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 { + match write_all_flush(&mut self.write, &response).await { Ok(_) => (), Err(err) => { server.mark_bad(); @@ -1135,10 +1661,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 +1680,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 +1691,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 +1703,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 +1718,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 +1727,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 +1747,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/cmd_args.rs b/src/cmd_args.rs new file mode 100644 index 00000000..3989d670 --- /dev/null +++ b/src/cmd_args.rs @@ -0,0 +1,36 @@ +use clap::{Parser, ValueEnum}; +use tracing::Level; + +/// PgCat: Nextgen PostgreSQL Pooler +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +pub struct Args { + #[arg(default_value_t = String::from("pgcat.toml"), env)] + pub config_file: String, + + #[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, + + #[arg( + short, + long, + default_value_t = false, + env, + help = "disable colors in the log output" + )] + pub no_color: bool, +} + +pub fn parse() -> Args { + return Args::parse(); +} + +#[derive(ValueEnum, Clone, Debug)] +pub enum LogFormat { + Text, + Structured, + Debug, +} diff --git a/src/config.rs b/src/config.rs index a3182e2b..9228b9bb 100644 --- a/src/config.rs +++ b/src/config.rs @@ -12,9 +12,11 @@ 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; +use crate::stats::AddressStats; use crate::tls::{load_certs, load_keys}; pub const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -62,7 +64,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 +98,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 +117,56 @@ impl Default for Address { username: String::from("username"), pool_name: String::from("pool_name"), mirrors: Vec::new(), + stats: Arc::new(AddressStats::default()), } } } +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 { + 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 { @@ -137,8 +188,13 @@ impl Address { #[derive(Clone, PartialEq, Hash, Eq, Serialize, Deserialize, Debug)] pub struct User { pub username: String, - pub password: String, + pub password: Option, + 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, } @@ -147,13 +203,38 @@ impl Default for User { fn default() -> User { User { username: String::from("postgres"), - password: String::new(), + password: None, + 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 { @@ -161,9 +242,11 @@ pub struct General { pub host: String, #[serde(default = "General::default_port")] - pub port: i16, + 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")] @@ -178,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, @@ -185,6 +270,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, @@ -200,16 +291,43 @@ 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_server_round_robin")] // False + pub server_round_robin: bool, + #[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, + + #[serde(default)] // false + pub server_tls: bool, + + #[serde(default)] // false + pub verify_server_certificate: bool, + 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, + pub auth_query_password: Option, + + #[serde(default)] + pub prepared_statements: bool, + + #[serde(default = "General::default_prepared_statements_cache_size")] + pub prepared_statements_cache_size: usize, } impl General { @@ -217,17 +335,21 @@ impl General { "0.0.0.0".into() } - pub fn default_port() -> i16 { + pub fn default_port() -> u16 { 5432 } + pub fn default_server_lifetime() -> u64 { + 1000 * 60 * 60 // 1 hour + } + pub fn default_connect_timeout() -> u64 { 1000 } // 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 } @@ -240,14 +362,22 @@ impl General { 5 // 5 seconds } + pub fn default_tcp_user_timeout() -> u64 { + 10000 // 10000 milliseconds + } + pub fn default_idle_timeout() -> u64 { - 60000 // 10 minutes + 600000 // 10 minutes } pub fn default_shutdown_timeout() -> u64 { 60000 } + pub fn default_dns_max_ttl() -> u64 { + 30 + } + pub fn default_healthcheck_timeout() -> u64 { 1000 } @@ -267,6 +397,22 @@ impl General { pub fn default_idle_client_in_transaction_timeout() -> u64 { 0 } + + pub fn default_validate_config() -> bool { + true + } + + pub fn default_prometheus_exporter_port() -> i16 { + 9930 + } + + pub fn default_server_round_robin() -> bool { + true + } + + pub fn default_prepared_statements_cache_size() -> usize { + 500 + } } impl Default for General { @@ -287,13 +433,26 @@ 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: 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, + verify_server_certificate: false, admin_username: String::from("admin"), admin_password: String::from("admin"), + auth_query: None, + auth_query_user: None, + auth_query_password: None, + server_lifetime: Self::default_server_lifetime(), + server_round_robin: Self::default_server_round_robin(), + validate_config: true, + prepared_statements: false, + prepared_statements_cache_size: 500, } } } @@ -309,6 +468,7 @@ pub enum PoolMode { #[serde(alias = "session", alias = "Session")] Session, } + impl ToString for PoolMode { fn to_string(&self) -> String { match *self { @@ -345,6 +505,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 @@ -353,10 +514,18 @@ 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")] pub sharding_function: ShardingFunction, #[serde(default = "Pool::default_automatic_sharding_key")] @@ -366,9 +535,17 @@ 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, + + #[serde(default = "Pool::default_cleanup_server_connections")] + pub cleanup_server_connections: bool, + + pub plugins: 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 + // 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 } @@ -380,6 +557,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 } @@ -392,6 +575,18 @@ impl Pool { None } + pub fn default_default_role() -> String { + "any".into() + } + + pub fn default_sharding_function() -> ShardingFunction { + ShardingFunction::PgBigintHash + } + + pub fn default_cleanup_server_connections() -> bool { + true + } + pub fn validate(&mut self) -> Result<(), Error> { match self.default_role.as_ref() { "any" => (), @@ -451,6 +646,10 @@ impl Pool { None => None, }; + for (_, user) in &self.users { + user.validate()?; + } + Ok(()) } } @@ -472,6 +671,12 @@ 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, + server_lifetime: None, + plugins: None, + cleanup_server_connections: true, } } } @@ -521,7 +726,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); @@ -550,6 +755,76 @@ impl Default for Shard { } } +#[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, +} + +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, Hash, Eq)] +pub struct TableAccess { + pub enabled: bool, + pub tables: Vec, +} + +#[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() { + query.substitute(db, user); + query.query = query.query.to_ascii_lowercase(); + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, Hash, Eq)] +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 { @@ -567,14 +842,42 @@ 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, } 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 { @@ -583,6 +886,7 @@ impl Default for Config { path: Self::default_path(), general: General::default(), pools: HashMap::default(), + plugins: None, } } } @@ -702,6 +1006,11 @@ 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 + ); + info!("Sever round robin: {}", self.general.server_round_robin); match self.general.tls_certificate.clone() { Some(tls_certificate) => { info!("TLS certificate: {}", tls_certificate); @@ -720,6 +1029,25 @@ 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 + ); + 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 { + 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?) @@ -734,8 +1062,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: {:?}", @@ -777,21 +1106,113 @@ 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(), + } + ); + info!( + "[pool: {}] Cleanup server connections: {}", + pool_name, pool_config.cleanup_server_connections + ); + 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!( "[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 - ) + ); + 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(), + } + ); + 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(), + } + ); } } } 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) => { @@ -839,9 +1260,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 +} + +pub fn get_prepared_statements_cache_size() -> usize { + CONFIG.load().general.prepared_statements_cache_size } /// Parse the configuration file located at the path. @@ -871,6 +1298,7 @@ pub async fn parse(path: &str) -> Result<(), Error> { } }; + config.fill_up_auth_query_config(); config.validate()?; config.path = path.to_string(); @@ -883,6 +1311,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) => { @@ -890,14 +1319,18 @@ pub async fn reload_config(client_server_map: ClientServerMap) -> Result (), + Err(err) => error!("DNS cache reinitialization error: {:?}", err), + }; + + 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) } @@ -940,7 +1373,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); @@ -965,10 +1401,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/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 310243c0..c076a3f9 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,18 +1,130 @@ -/// Errors. +//! Errors. /// Various errors. -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] 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), TlsError, StatementTimeout, + DNSCachedError(String), ShuttingDown, ParseBytesError(String), + AuthError(String), + AuthPassthroughError(String), + UnsupportedStatement, + QueryRouterParserError(String), + QueryRouterError(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), + } + } +} + +impl From for Error { + fn from(err: std::ffi::NulError) -> Self { + Error::QueryRouterError(err.to_string()) + } } diff --git a/src/lib.rs b/src/lib.rs index 67aa9cba..6a8a1e36 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,18 @@ +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 logger; 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/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 e2ff5d8d..916d1a8e 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; @@ -60,52 +61,32 @@ use std::str::FromStr; use std::sync::Arc; use tokio::sync::broadcast; -mod admin; -mod client; -mod config; -mod constants; -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::pool::{ClientServerMap, ConnectionPool}; -use crate::prometheus::start_metric_server; -use crate::stats::{Collector, Reporter, REPORTER}; +use pgcat::cmd_args; +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}; fn main() -> Result<(), Box> { - multi_logger::MultiLogger::init().unwrap(); + let args = cmd_args::parse(); + logger::init(&args); 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); } - 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 config::parse(&config_file).await { + match pgcat::config::parse(args.config_file.as_str()).await { Ok(_) => (), Err(err) => { error!("Config parse error: {:?}", err); @@ -162,8 +143,13 @@ 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())); + + // 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 { @@ -175,20 +161,23 @@ 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; }); - 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 +185,10 @@ fn main() -> Result<(), Box> { } }; } - } - }); + }); + }; + + #[cfg(windows)] let mut term_signal = win_signal::ctrl_close().unwrap(); @@ -282,18 +273,20 @@ 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(); + + configure_socket(&socket); 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, drain_tx, admin_only, - tls_certificate.clone(), + tls_certificate, config.general.log_client_connections, ) .await @@ -301,7 +294,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, @@ -318,7 +311,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 c9ace4e0..8ebc00a3 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -1,17 +1,21 @@ /// 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}; 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 @@ -20,6 +24,10 @@ pub enum DataType { Text, Int4, Numeric, + Bool, + Oid, + AnyArray, + Any, } impl From<&DataType> for i32 { @@ -28,6 +36,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, } } } @@ -116,7 +128,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 +165,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(); @@ -213,7 +243,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 +283,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. @@ -384,7 +434,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 { @@ -405,6 +455,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); @@ -443,6 +497,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()); @@ -453,6 +530,33 @@ 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'); + 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 @@ -485,6 +589,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 @@ -562,6 +689,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( @@ -571,7 +705,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), @@ -593,3 +727,374 @@ 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(); + // 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(); + 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() + } +} + +/// 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_{}", + PREPARED_STATEMENT_COUNTER.fetch_add(1, Ordering::SeqCst) + ) +} diff --git a/src/mirrors.rs b/src/mirrors.rs index ab2b2dc4..0f2b02c0 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 parking_lot::RwLock; use crate::config::{get_config, Address, Role, User}; use crate::pool::{ClientServerMap, ServerPool}; -use crate::stats::get_reporter; 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 manager = ServerPool::new( self.address.clone(), self.user.clone(), self.database.as_str(), ClientServerMap::default(), - get_reporter(), + Arc::new(RwLock::new(None)), + None, + true, ); Pool::builder() diff --git a/src/multi_logger.rs b/src/multi_logger.rs deleted file mode 100644 index 901db02e..00000000 --- a/src/multi_logger.rs +++ /dev/null @@ -1,80 +0,0 @@ -use log::{Level, 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, erros 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() -> Self { - let stderr_logger = env_logger::builder().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() -> Result<(), SetLoggerError> { - let logger = Self::new(); - - 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().unwrap(); - } -} diff --git a/src/plugins/intercept.rs b/src/plugins/intercept.rs new file mode 100644 index 00000000..166294bc --- /dev/null +++ b/src/plugins/intercept.rs @@ -0,0 +1,120 @@ +//! The intercept plugin. +//! +//! It intercepts queries and returns fake results. + +use async_trait::async_trait; +use bytes::{BufMut, BytesMut}; +use serde::{Deserialize, Serialize}; +use sqlparser::ast::Statement; + +use log::debug; + +use crate::{ + config::Intercept as InterceptConfig, + errors::Error, + messages::{command_complete, data_row_nullable, row_description, DataType}, + plugins::{Plugin, PluginOutput}, + query_router::QueryRouter, +}; + +// 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<'a> { + pub enabled: bool, + pub config: &'a InterceptConfig, +} + +#[async_trait] +impl<'a> Plugin for Intercept<'a> { + async fn run( + &mut self, + query_router: &QueryRouter, + ast: &Vec, + ) -> Result { + if !self.enabled || ast.is_empty() { + return Ok(PluginOutput::Allow); + } + + let mut config = self.config.clone(); + config.substitute( + &query_router.pool_settings().db, + &query_router.pool_settings().user.username, + ); + + let mut result = BytesMut::new(); + + for q in ast { + // Normalization + let q = q.to_string().to_ascii_lowercase(); + + for (_, target) in config.queries.iter() { + if target.query.as_str() == q { + debug!("Intercepting query: {}", q); + + let rd = target + .schema + .iter() + .map(|row| { + let name = &row[0]; + let data_type = &row[1]; + ( + name.as_str(), + match data_type.as_str() { + "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.iter().for_each(|row| { + let row = row + .iter() + .map(|s| { + let s = s.as_str().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) + } + } +} diff --git a/src/plugins/mod.rs b/src/plugins/mod.rs new file mode 100644 index 00000000..5ef6009a --- /dev/null +++ b/src/plugins/mod.rs @@ -0,0 +1,44 @@ +//! 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 prewarmer; +pub mod query_logger; +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 query_logger::QueryLogger; +pub use table_access::TableAccess; + +#[derive(Clone, Debug, PartialEq)] +pub enum PluginOutput { + Allow, + Deny(String), + Overwrite(Vec), + Intercept(BytesMut), +} + +#[async_trait] +pub trait Plugin { + // 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/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 new file mode 100644 index 00000000..debdf39f --- /dev/null +++ b/src/plugins/query_logger.rs @@ -0,0 +1,38 @@ +//! Log all queries to stdout (or somewhere else, why not). + +use crate::{ + errors::Error, + plugins::{Plugin, PluginOutput}, + query_router::QueryRouter, +}; +use async_trait::async_trait; +use log::info; +use sqlparser::ast::Statement; + +pub struct QueryLogger<'a> { + pub enabled: bool, + pub user: &'a str, + pub db: &'a str, +} + +#[async_trait] +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!("[pool: {}][user: {}] {}", self.user, self.db, query); + + Ok(PluginOutput::Allow) + } +} diff --git a/src/plugins/table_access.rs b/src/plugins/table_access.rs new file mode 100644 index 00000000..79c1260e --- /dev/null +++ b/src/plugins/table_access.rs @@ -0,0 +1,59 @@ +//! 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 log::debug; + +use core::ops::ControlFlow; + +pub struct TableAccess<'a> { + pub enabled: bool, + pub tables: &'a Vec, +} + +#[async_trait] +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; + + visit_relations(ast, |relation| { + let relation = relation.to_string(); + let parts = relation.split(".").collect::>(); + let table_name = parts.last().unwrap(); + + if self.tables.contains(&table_name.to_string()) { + found = Some(table_name.to_string()); + ControlFlow::<()>::Break(()) + } else { + ControlFlow::<()>::Continue(()) + } + }); + + if let Some(found) = found { + debug!("Blocking access to table \"{}\"", 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 3a6ec3e6..b9293521 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}; @@ -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, @@ -17,12 +18,16 @@ 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::{get_reporter, Reporter}; +use crate::stats::{AddressStats, ClientStats, ServerStats}; pub type ProcessId = i32; pub type SecretKey = i32; @@ -51,7 +56,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, @@ -60,6 +65,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 { @@ -70,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) @@ -90,6 +103,7 @@ pub struct PoolSettings { // Connecting user. pub user: User, + pub db: String, // Default server role to connect to. pub default_role: Option, @@ -123,6 +137,14 @@ 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, + + /// Plugins + pub plugins: Option, } impl Default for PoolSettings { @@ -132,6 +154,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, @@ -143,6 +166,10 @@ 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, + plugins: None, } } } @@ -161,10 +188,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 +208,9 @@ pub struct ConnectionPool { /// If the pool has been paused or not. paused: Arc, paused_waiter: Arc, + + /// AuthInfo + pub auth_hash: Arc>>, } impl ConnectionPool { @@ -201,6 +227,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 +238,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; } } @@ -237,6 +261,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]; @@ -266,6 +291,7 @@ impl ConnectionPool { username: user.username.clone(), pool_name: pool_name.clone(), mirrors: vec![], + stats: Arc::new(AddressStats::default()), }); address_id += 1; } @@ -283,6 +309,7 @@ impl ConnectionPool { username: user.username.clone(), pool_name: pool_name.clone(), mirrors: mirror_addresses, + stats: Arc::new(AddressStats::default()), }; address_id += 1; @@ -291,12 +318,52 @@ 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(), - get_reporter(), + pool_auth_hash.clone(), + match pool_config.plugins { + Some(ref plugins) => Some(plugins.clone()), + None => config.plugins.clone(), + }, + pool_config.cleanup_server_connections, ); let connect_timeout = match pool_config.connect_timeout { @@ -309,14 +376,44 @@ 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 reaper_rate = *vec![idle_timeout, server_lifetime, POOL_REAPER_RATE] + .iter() + .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 + ); + 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))) - .test_on_check_out(false) - .build(manager) - .await - .unwrap(); + .max_lifetime(Some(std::time::Duration::from_millis(server_lifetime))) + .reaper_rate(std::time::Duration::from_millis(reaper_rate)) + .queue_strategy(queue_strategy) + .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); @@ -328,20 +425,30 @@ 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, addresses, banlist: Arc::new(RwLock::new(banlist)), - stats: get_reporter(), 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, + 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(), user: user.clone(), + db: pool_name.clone(), default_role: match pool_config.default_role.as_str() { "any" => None, "replica" => Some(Role::Replica), @@ -364,6 +471,13 @@ 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(), + 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)), @@ -373,10 +487,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); @@ -387,7 +503,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 @@ -476,9 +593,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() @@ -497,6 +614,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() { @@ -515,10 +636,6 @@ impl ConnectionPool { } } - // Indicate we're waiting on a server connection from a pool. - let now = Instant::now(); - self.stats.client_waiting(client_process_id); - // Check if we can connect let mut conn = match self.databases[address.shard][address.address_index] .get() @@ -526,10 +643,14 @@ 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); + error!( + "Connection checkout error for instance {:?}, error: {:?}", + address, err + ); + self.ban(address, BanReason::FailedCheckout, Some(client_stats)); + address.stats.error(); + client_stats.idle(); + client_stats.checkout_error(); continue; } }; @@ -546,26 +667,33 @@ 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()); + client_stats.active(); 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 { + 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) } @@ -574,11 +702,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,20 +717,20 @@ 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; } // Health check failed. Err(err) => { error!( - "Banning instance {:?} because of failed health check, {:?}", + "Failed health check on instance {:?}, error: {:?}", address, err ); } @@ -611,7 +739,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 ); } @@ -620,23 +748,29 @@ 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; } + error!("Banning instance {:?}, reason: {:?}", address, reason); + 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)); } @@ -793,11 +927,26 @@ 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, - stats: Reporter, + + /// 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 { @@ -806,14 +955,18 @@ impl ServerPool { user: User, database: &str, client_server_map: ClientServerMap, - stats: Reporter, + auth_hash: Arc>>, + plugins: Option, + cleanup_connections: bool, ) -> ServerPool { ServerPool { address, - user, + user: user.clone(), database: database.to_string(), client_server_map, - stats, + auth_hash, + plugins, + cleanup_connections, } } } @@ -826,34 +979,44 @@ 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(), + 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(), + self.auth_hash.clone(), + self.cleanup_connections, ) .await { - Ok(conn) => { - self.stats.server_idle(server_id); + 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) } Err(err) => { - self.stats.server_disconnecting(server_id); + stats.disconnect(); Err(err) } } @@ -881,11 +1044,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..7e264dca 100644 --- a/src/prometheus.rs +++ b/src/prometheus.rs @@ -1,14 +1,17 @@ use hyper::service::{make_service_fn, service_fn}; use hyper::{Body, Method, Request, Response, Server, StatusCode}; -use log::{error, info, warn}; +use log::{debug, error, info}; 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::pool::{get_all_pools, PoolIdentifier}; +use crate::stats::pool::PoolStats; +use crate::stats::{get_server_stats, ServerStats}; struct MetricHelpType { help: &'static str, @@ -220,7 +223,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,10 +234,10 @@ 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_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) } @@ -261,20 +264,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 { + debug!("Metric {} not implemented for {}", key, address.name()); } } } @@ -284,17 +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() { - for (name, value) in stats.iter() { - if let Some(prometheus_metric) = PrometheusMetric::::from_pool(pool, name, *value) + 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_id.clone(), &name, value) { lines.push(prometheus_metric.to_string()); } else { - warn!( - "Metric {} not implemented for ({},{})", - name, pool.0, pool.1 - ); + debug!("Metric {} not implemented for ({})", name, *pool_id); } } } @@ -319,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()); } } } @@ -330,9 +329,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 +340,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) = @@ -353,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()); } } } diff --git a/src/query_router.rs b/src/query_router.rs index 578c7390..126b8138 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}; @@ -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, QueryLogger, 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; @@ -344,37 +348,39 @@ 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 } - _ => 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 +424,7 @@ impl QueryRouter { }; } - true + Ok(()) } /// Parse the shard number from the Bind message @@ -783,6 +789,52 @@ impl QueryRouter { } } + /// Add your plugins here and execute them. + pub async fn execute_plugins(&self, ast: &Vec) -> Result { + 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 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 { + return Ok(PluginOutput::Intercept(output)); + } + } + + 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 { + 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 +862,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 +925,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 +944,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 +956,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 +976,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 +1140,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()); @@ -1110,6 +1173,11 @@ 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, + db: "test".to_string(), + plugins: None, }; let mut qr = QueryRouter::new(); assert_eq!(qr.active_role, None); @@ -1139,15 +1207,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); } @@ -1171,7 +1248,13 @@ 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, + db: "test".to_string(), + plugins: None, }; + let mut qr = QueryRouter::new(); qr.update_pool_settings(pool_settings.clone()); @@ -1202,47 +1285,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); } @@ -1266,11 +1386,61 @@ 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() { + use crate::config::{Plugins, TableAccess}; + let table_access = TableAccess { + enabled: true, + tables: vec![String::from("pg_database")], + }; + 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 mut qr = QueryRouter::new(); + 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() + )) + ); + } + + #[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 b3dbd6f7..55444fba 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,37 +1,158 @@ /// 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::{BTreeSet, HashMap}; use std::io::Read; +use std::net::IpAddr; +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, get_prepared_statements_cache_size, Address, User}; use crate::constants::*; -use crate::errors::Error; +use crate::dns_cache::{AddrSet, CACHED_RESOLVER}; +use crate::errors::{Error, ServerIdentifier}; use crate::messages::*; use crate::mirrors::MirroringManager; use crate::pool::ClientServerMap; use crate::scram::ScramSha256; -use crate::stats::Reporter; +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), + } + } +} + +#[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_id: i32, - /// Server host, e.g. localhost, /// 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, @@ -49,11 +170,14 @@ 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, - /// 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, @@ -62,28 +186,56 @@ 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, - // 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 + addr_set: Option, + + /// Should clean up dirty connections? + cleanup_connections: bool, + + /// Prepared statements + prepared_statements: BTreeSet, } 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, + auth_hash: Arc>>, + cleanup_connections: bool, ) -> 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, @@ -95,30 +247,137 @@ 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 - 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(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 = ScramSha256::new(&user.password); + let mut scram: Option = match password { + Some(password) => Some(ScramSha256::new(password)), + None => None, + }; 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); @@ -129,7 +388,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); @@ -142,32 +406,79 @@ 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, + )) + } }; - md5_password(&mut stream, &user.username, &user.password, &salt[..]) - .await?; + match password { + // Using plaintext password + Some(password) => { + md5_password(&mut stream, 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::ServerAuthError( + "Auth passthrough (auth_query) failed and no user password is set in cleartext".into(), + server_identifier + ) + ), + } + } + } } AUTHENTICATION_SUCCESSFUL => (), SASL => { + if scram.is_none() { + 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]); - if sasl_type == SCRAM_SHA_256 { + if sasl_type.contains(SCRAM_SHA_256) { 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(); @@ -186,7 +497,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); @@ -200,11 +511,16 @@ 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[..]); - let sasl_response = scram.update(&msg)?; + let sasl_response = scram.as_mut().unwrap().update(&msg)?; // SASLResponse let mut res = BytesMut::new(); @@ -212,7 +528,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 => { @@ -221,10 +537,19 @@ 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.finish(&BytesMut::from(&sasl_final[..])) { + match scram + .as_mut() + .unwrap() + .finish(&BytesMut::from(&sasl_final[..])) + { Ok(_) => { debug!("SASL authentication successful"); } @@ -247,7 +572,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); @@ -263,7 +593,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 @@ -282,7 +617,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. @@ -299,12 +639,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, + )) + } }; } @@ -314,25 +664,28 @@ 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(); - let mut server = Server { address: address.clone(), - read: BufReader::new(read), - write, + stream: BufStream::new(stream), buffer: BytesMut::with_capacity(8196), server_info, - server_id, process_id, secret_key, in_transaction: false, + in_copy_mode: 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(), stats, application_name: String::new(), @@ -345,6 +698,8 @@ impl Server { address.mirrors.clone(), )), }, + cleanup_connections, + prepared_statements: BTreeSet::new(), }; server.set_name("pgcat").await?; @@ -377,7 +732,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); @@ -390,22 +745,25 @@ 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. 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 { + match write_all_flush(&mut self.stream, &messages).await { Ok(_) => { // Successfully sent to server self.last_activity = SystemTime::now(); Ok(()) } Err(err) => { - error!("Terminating server because of: {:?}", err); + error!( + "Terminating server {:?} because of: {:?}", + self.address, err + ); self.bad = true; Err(err) } @@ -417,10 +775,13 @@ 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); + error!( + "Terminating server {:?} because of: {:?}", + self.address, err + ); self.bad = true; return Err(err); } @@ -471,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(_) => { @@ -487,12 +859,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; } _ => (), } @@ -516,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; } @@ -545,7 +921,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(); @@ -557,6 +933,115 @@ 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); + + debug!("Should prepare `{}`: {}", name, should_prepare); + + if should_prepare { + self.stats.prepared_cache_miss(); + } else { + self.stats.prepared_cache_hit(); + } + + should_prepare + } + + /// Create a prepared statement on the server. + 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) + 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 { @@ -564,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 { @@ -573,7 +1062,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. @@ -606,6 +1111,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?; @@ -638,10 +1145,15 @@ 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() && 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.needs_cleanup = false; + self.query("RESET ROLE").await?; + self.cleanup_state.reset(); + } + + if self.in_copy_mode() { + warn!("Server returned while still in copy-mode"); } Ok(()) @@ -653,30 +1165,29 @@ 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(()) } } + /// 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 @@ -684,7 +1195,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) { @@ -700,6 +1211,106 @@ 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)), + true, + ) + .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 { @@ -708,25 +1319,31 @@ impl Drop for Server { /// for a write. fn drop(&mut self) { self.mirror_disconnect(); - self.stats.server_disconnecting(self.server_id); - let mut bytes = BytesMut::with_capacity(4); + // Update statistics + self.stats.disconnect(); + + 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. - 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) ); diff --git a/src/stats.rs b/src/stats.rs index 3f7e9d61..29c00d64 100644 --- a/src/stats.rs +++ b/src/stats.rs @@ -1,43 +1,38 @@ -use arc_swap::ArcSwap; /// 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 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>; -/// 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())); - -/// Aggregate stats for each pool (a pool is identified by database name and username) updated every second -/// 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 SERVER_STATS: Lazy>> = + Lazy::new(|| Arc::new(RwLock::new(ServerStatesLookup::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 +40,88 @@ 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) + /// Reports a client is disconnecting from the pooler. + 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) - } - - /// 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) + 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. - pub fn server_disconnecting(&self, server_id: i32) { - let event = Event { - name: EventName::ServerDisconnecting { server_id }, - value: 1, - }; - self.send(event) + /// Reports a server connection is disconnecting from the pooler. + fn server_disconnecting(&self, server_id: i32) { + SERVER_STATS.write().remove(&server_id); } } -/// 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, - }); - } - } - }); - - // 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, - }, - ); - } - }; - } + // Hold read lock for duration of update to retain all server stats + let server_stats = SERVER_STATS.read(); - 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), + 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); } - - // 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), - }; + // Reset to false for next update + for stats in server_stats.values() { + stats.set_address_stat_average_is_updated_status(false); } - - 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() -} - -/// Get a snapshot of pool statistics. Updated once a second -/// 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() + SERVER_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..a0486445 --- /dev/null +++ b/src/stats/address.rs @@ -0,0 +1,226 @@ +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 { + total: AddressStatFields, + + current: AddressStatFields, + + averages: AddressStatFields, + + // Determines if the averages have been updated since the last time they were reported + pub averages_updated: 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.bytes_received.load(Ordering::Relaxed), + ), + ( + "total_sent".to_string(), + self.total.bytes_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.averages.xact_count.load(Ordering::Relaxed), + ), + ( + "avg_query_count".to_string(), + self.averages.query_count.load(Ordering::Relaxed), + ), + ( + "avg_recv".to_string(), + self.averages.bytes_received.load(Ordering::Relaxed), + ), + ( + "avg_sent".to_string(), + self.averages.bytes_sent.load(Ordering::Relaxed), + ), + ( + "avg_errors".to_string(), + self.averages.errors.load(Ordering::Relaxed), + ), + ( + "avg_xact_time".to_string(), + self.averages.xact_time.load(Ordering::Relaxed), + ), + ( + "avg_query_time".to_string(), + self.averages.query_time.load(Ordering::Relaxed), + ), + ( + "avg_wait_time".to_string(), + self.averages.wait_time.load(Ordering::Relaxed), + ), + ] + .into_iter() + } +} + +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.current.errors.fetch_add(1, Ordering::Relaxed); + } + + pub fn update_averages(&self) { + 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) { + for (_key, value) in self.clone() { + row.push(value.to_string()); + } + } +} diff --git a/src/stats/client.rs b/src/stats/client.rs new file mode 100644 index 00000000..6a30ec15 --- /dev/null +++ b/src/stats/client.rs @@ -0,0 +1,174 @@ +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, + + 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, + + /// 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(), + 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)), + 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, + ) -> Self { + Self { + client_id, + connect_time, + application_name: application_name.to_string(), + username: username.to_string(), + pool_name: pool_name.to_string(), + ..Default::default() + } + } + + /// 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); + } + + /// 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); + } + + /// Reports a client is done querying the server and is no longer assigned a server connection + pub fn idle(&self) { + self.state.store(ClientState::Idle, Ordering::Relaxed); + } + + /// Reports a client is waiting for a connection + pub fn waiting(&self) { + 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.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); + } + + /// 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); + self.max_wait_time + .fetch_max(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..d3ac78e9 --- /dev/null +++ b/src/stats/pool.rs @@ -0,0 +1,151 @@ +use log::debug; + +use super::{ClientState, ServerState}; +use crate::{config::PoolMode, messages::DataType, pool::PoolIdentifier}; +use std::collections::HashMap; +use std::sync::atomic::*; + +use crate::pool::get_all_pools; + +#[derive(Debug, Clone)] +/// A struct that holds information about a Pool . +pub struct PoolStats { + 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 PoolStats { + pub fn new(identifier: PoolIdentifier, mode: PoolMode) -> Self { + PoolStats { + identifier, + 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, + } + } + + 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(); + + for (identifier, pool) in get_all_pools() { + map.insert( + identifier.clone(), + PoolStats::new(identifier, pool.settings.pool_mode), + ); + } + + 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"), + } + } + + 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"), + } + } + + 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(), + ]; + } +} + +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), + ("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 new file mode 100644 index 00000000..443c0b6a --- /dev/null +++ b/src/stats/server.rs @@ -0,0 +1,226 @@ +use super::AddressStats; +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, + + 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, + pub prepared_hit_count: Arc, + pub prepared_miss_count: Arc, + pub prepared_cache_size: Arc, +} + +impl Default for ServerStats { + fn default() -> Self { + ServerStats { + server_id: 0, + application_name: Arc::new(RwLock::new(String::new())), + address: Address::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(), + prepared_hit_count: Arc::new(AtomicU64::new(0)), + prepared_miss_count: Arc::new(AtomicU64::new(0)), + prepared_cache_size: Arc::new(AtomicU64::new(0)), + } + } +} + +impl ServerStats { + pub fn new(address: Address, connect_time: Instant) -> Self { + Self { + address, + 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.state.store(ServerState::Idle, Ordering::Relaxed); + } + + /// 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); + } + + /// Reports a server connection is being tested before being given to a client. + pub fn tested(&self) { + self.set_undefined_application(); + self.state.store(ServerState::Tested, Ordering::Relaxed); + } + + /// Reports a server connection is attempting to login. + pub fn login(&self) { + 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.state.store(ServerState::Active, Ordering::Relaxed); + self.set_application(application_name); + } + + pub fn address_stats(&self) -> Arc { + 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.address.pool_name.clone() + } + + pub fn username(&self) -> String { + self.address.username.clone() + } + + 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 aggregation stats + self.set_application(application_name); + self.address.stats.wait_time_add(microseconds); + } + + /// 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()); + 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 + /// 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.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.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.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); + } + + 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/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/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/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() diff --git a/tests/ruby/admin_spec.rb b/tests/ruby/admin_spec.rb index f69c3df6..9a852350 100644 --- a/tests/ruby/admin_spec.rb +++ b/tests/ruby/admin_spec.rb @@ -11,282 +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 - 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 - - # 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_not eq(0) - - 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_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) - - 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 - 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") - - 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("1") - 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 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 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("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 @@ -357,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/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/capture b/tests/ruby/capture new file mode 100644 index 00000000..268680ff Binary files /dev/null and b/tests/ruby/capture differ 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 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/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 c4ebab7f..7a5bd71d 100644 --- a/tests/ruby/helpers/pgcat_helper.rb +++ b/tests/ruby/helpers/pgcat_helper.rb @@ -2,6 +2,14 @@ require 'ostruct' require_relative 'pgcat_process' require_relative 'pg_instance' +require_relative 'pg_socket' + +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 @@ -33,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) @@ -93,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, @@ -109,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/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 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/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") } diff --git a/tests/ruby/misc_spec.rb b/tests/ruby/misc_spec.rb index 1a04c5d6..fe216e5b 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 @@ -308,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 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 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 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 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'") 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 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 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(); + } +} 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