From 7f94e62d060545779b0618137e46c965b17f02b8 Mon Sep 17 00:00:00 2001 From: Mostafa Date: Mon, 11 Nov 2024 08:57:18 -0600 Subject: [PATCH] Initial gh-pages commit (#860) --- .circleci/config.yml | 87 - .circleci/generate_coverage.sh | 15 - .circleci/pgcat.toml | 158 -- .circleci/run_tests.sh | 183 -- .circleci/server.cert | 21 - .circleci/server.key | 28 - .dockerignore | 6 - .editorconfig | 14 - .github/ISSUE_TEMPLATE/bug_report.md | 38 - .github/ISSUE_TEMPLATE/feature_request.md | 20 - .github/dependabot.yml | 16 - .github/workflows/build-and-push.yaml | 68 - .github/workflows/chart-lint-test.yaml | 50 - .github/workflows/chart-release.yaml | 40 - .github/workflows/generate-chart-readme.yaml | 48 - .github/workflows/publish-ci-docker-image.yml | 20 - .github/workflows/publish-deb-package.yml | 59 - .rustfmt.toml | 2 - CONFIG.md | 523 ---- CONTRIBUTING.md | 39 - Cargo.lock | 2168 ----------------- Cargo.toml | 60 - Dockerfile | 22 - Dockerfile.ci | 17 - Dockerfile.dev | 25 - LICENSE | 20 - README.md | 293 +-- charts/pgcat/.helmignore | 23 - charts/pgcat/Chart.yaml | 8 - charts/pgcat/templates/NOTES.txt | 22 - charts/pgcat/templates/_config.tpl | 3 - charts/pgcat/templates/_helpers.tpl | 62 - charts/pgcat/templates/deployment.yaml | 66 - charts/pgcat/templates/ingress.yaml | 61 - charts/pgcat/templates/secret.yaml | 97 - charts/pgcat/templates/service.yaml | 15 - charts/pgcat/templates/serviceaccount.yaml | 12 - charts/pgcat/values.yaml | 374 --- control | 9 - cov-style.css | 158 -- cr.yaml | 1 - ct.yaml | 5 - dev/Dockerfile | 35 - dev/dev_bashrc | 120 - dev/docker-compose.yaml | 94 - dev/script/console | 12 - docker-compose.yml | 17 - examples/docker/pgcat.toml | 127 - grafana_dashboard.json | 2124 ---------------- images/instacart.webp | Bin 3448 -> 0 bytes images/one_signal.webp | Bin 16278 -> 0 bytes images/postgresml.webp | Bin 4862 -> 0 bytes pgcat.minimal.toml | 22 - pgcat.service | 17 - pgcat.toml | 350 --- postinst | 13 - postrm | 4 - prerm | 5 - src/admin.rs | 999 -------- src/auth_passthrough.rs | 138 -- src/client.rs | 2079 ---------------- src/cmd_args.rs | 36 - src/config.rs | 1607 ------------ src/constants.rs | 33 - src/dns_cache.rs | 410 ---- src/errors.rs | 133 - src/lib.rs | 42 - src/logger.rs | 20 - src/main.rs | 340 --- src/messages.rs | 1548 ------------ src/mirrors.rs | 190 -- src/plugins/intercept.rs | 120 - src/plugins/mod.rs | 45 - src/plugins/prewarmer.rs | 28 - src/plugins/query_logger.rs | 38 - src/plugins/table_access.rs | 59 - src/pool.rs | 1223 ---------- src/prometheus.rs | 530 ---- src/query_router.rs | 1968 --------------- src/scram.rs | 325 --- src/server.rs | 1537 ------------ src/sharding.rs | 216 -- src/stats.rs | 130 - src/stats/address.rs | 226 -- src/stats/client.rs | 204 -- src/stats/pool.rs | 154 -- src/stats/server.rs | 229 -- src/tls.rs | 87 - start_test_env.sh | 34 - tests/docker/Dockerfile | 13 - tests/docker/docker-compose.yml | 54 - tests/docker/run.sh | 69 - tests/go/go.mod | 5 - tests/go/go.sum | 2 - tests/go/pgcat.toml | 162 -- tests/go/prepared_test.go | 52 - tests/go/setup.go | 81 - tests/pgbench/simple.sql | 39 - tests/python/.gitignore | 1 - tests/python/requirements.txt | 3 - tests/python/test_auth.py | 71 - tests/python/test_pgcat.py | 242 -- tests/python/utils.py | 110 - tests/ruby/.ruby-version | 2 - tests/ruby/Gemfile | 8 - tests/ruby/Gemfile.lock | 88 - tests/ruby/admin_spec.rb | 138 -- tests/ruby/auth_query_spec.rb | 215 -- tests/ruby/capture | Bin 40638 -> 0 bytes tests/ruby/copy_spec.rb | 102 - tests/ruby/helpers/auth_query_helper.rb | 173 -- tests/ruby/helpers/pg_instance.rb | 108 - tests/ruby/helpers/pg_socket.rb | 259 -- tests/ruby/helpers/pgcat_helper.rb | 178 -- tests/ruby/helpers/pgcat_process.rb | 152 -- tests/ruby/load_balancing_spec.rb | 164 -- tests/ruby/mirrors_spec.rb | 91 - tests/ruby/misc_spec.rb | 424 ---- tests/ruby/plugins_spec.rb | 14 - tests/ruby/prepared_spec.rb | 214 -- tests/ruby/protocol_spec.rb | 155 -- tests/ruby/routing_spec.rb | 81 - tests/ruby/sharding_spec.rb | 196 -- tests/ruby/spec_helper.rb | 28 - tests/ruby/stats_spec.rb | 406 --- tests/ruby/tests.rb | 113 - tests/rust/.gitignore | 1 - tests/rust/Cargo.lock | 1490 ----------- tests/rust/Cargo.toml | 10 - tests/rust/src/main.rs | 34 - tests/sharding/README.md | 35 - tests/sharding/partition_hash_test_setup.sql | 26 - tests/sharding/query_routing.sh | 19 - tests/sharding/query_routing_setup.sql | 104 - tests/sharding/query_routing_test_insert.sql | 55 - .../query_routing_test_primary_replica.sql | 162 -- tests/sharding/query_routing_test_select.sql | 49 - .../sharding/query_routing_test_validate.sql | 11 - utilities/deb.sh | 40 - utilities/generate_config_docs.py | 92 - utilities/requirements.txt | 1 - 141 files changed, 1 insertion(+), 28965 deletions(-) delete mode 100644 .circleci/config.yml delete mode 100755 .circleci/generate_coverage.sh delete mode 100644 .circleci/pgcat.toml delete mode 100644 .circleci/run_tests.sh delete mode 100644 .circleci/server.cert delete mode 100644 .circleci/server.key delete mode 100644 .dockerignore delete mode 100644 .editorconfig delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.md delete mode 100644 .github/dependabot.yml delete mode 100644 .github/workflows/build-and-push.yaml delete mode 100644 .github/workflows/chart-lint-test.yaml delete mode 100644 .github/workflows/chart-release.yaml delete mode 100644 .github/workflows/generate-chart-readme.yaml delete mode 100644 .github/workflows/publish-ci-docker-image.yml delete mode 100644 .github/workflows/publish-deb-package.yml delete mode 100644 .rustfmt.toml delete mode 100644 CONFIG.md delete mode 100644 CONTRIBUTING.md delete mode 100644 Cargo.lock delete mode 100644 Cargo.toml delete mode 100644 Dockerfile delete mode 100644 Dockerfile.ci delete mode 100644 Dockerfile.dev delete mode 100644 LICENSE delete mode 100644 charts/pgcat/.helmignore delete mode 100644 charts/pgcat/Chart.yaml delete mode 100644 charts/pgcat/templates/NOTES.txt delete mode 100644 charts/pgcat/templates/_config.tpl delete mode 100644 charts/pgcat/templates/_helpers.tpl delete mode 100644 charts/pgcat/templates/deployment.yaml delete mode 100644 charts/pgcat/templates/ingress.yaml delete mode 100644 charts/pgcat/templates/secret.yaml delete mode 100644 charts/pgcat/templates/service.yaml delete mode 100644 charts/pgcat/templates/serviceaccount.yaml delete mode 100644 charts/pgcat/values.yaml delete mode 100644 control delete mode 100644 cov-style.css delete mode 100644 cr.yaml delete mode 100644 ct.yaml delete mode 100644 dev/Dockerfile delete mode 100644 dev/dev_bashrc delete mode 100644 dev/docker-compose.yaml delete mode 100755 dev/script/console delete mode 100644 docker-compose.yml delete mode 100644 examples/docker/pgcat.toml delete mode 100644 grafana_dashboard.json delete mode 100644 images/instacart.webp delete mode 100644 images/one_signal.webp delete mode 100644 images/postgresml.webp delete mode 100644 pgcat.minimal.toml delete mode 100644 pgcat.service delete mode 100644 pgcat.toml delete mode 100644 postinst delete mode 100644 postrm delete mode 100644 prerm delete mode 100644 src/admin.rs delete mode 100644 src/auth_passthrough.rs delete mode 100644 src/client.rs delete mode 100644 src/cmd_args.rs delete mode 100644 src/config.rs delete mode 100644 src/constants.rs delete mode 100644 src/dns_cache.rs delete mode 100644 src/errors.rs delete mode 100644 src/lib.rs delete mode 100644 src/logger.rs delete mode 100644 src/main.rs delete mode 100644 src/messages.rs delete mode 100644 src/mirrors.rs delete mode 100644 src/plugins/intercept.rs delete mode 100644 src/plugins/mod.rs delete mode 100644 src/plugins/prewarmer.rs delete mode 100644 src/plugins/query_logger.rs delete mode 100644 src/plugins/table_access.rs delete mode 100644 src/pool.rs delete mode 100644 src/prometheus.rs delete mode 100644 src/query_router.rs delete mode 100644 src/scram.rs delete mode 100644 src/server.rs delete mode 100644 src/sharding.rs delete mode 100644 src/stats.rs delete mode 100644 src/stats/address.rs delete mode 100644 src/stats/client.rs delete mode 100644 src/stats/pool.rs delete mode 100644 src/stats/server.rs delete mode 100644 src/tls.rs delete mode 100755 start_test_env.sh delete mode 100644 tests/docker/Dockerfile delete mode 100644 tests/docker/docker-compose.yml delete mode 100644 tests/docker/run.sh delete mode 100644 tests/go/go.mod delete mode 100644 tests/go/go.sum delete mode 100644 tests/go/pgcat.toml delete mode 100644 tests/go/prepared_test.go delete mode 100644 tests/go/setup.go delete mode 100644 tests/pgbench/simple.sql delete mode 100644 tests/python/.gitignore delete mode 100644 tests/python/requirements.txt delete mode 100644 tests/python/test_auth.py delete mode 100644 tests/python/test_pgcat.py delete mode 100644 tests/python/utils.py delete mode 100644 tests/ruby/.ruby-version delete mode 100644 tests/ruby/Gemfile delete mode 100644 tests/ruby/Gemfile.lock delete mode 100644 tests/ruby/admin_spec.rb delete mode 100644 tests/ruby/auth_query_spec.rb delete mode 100644 tests/ruby/capture delete mode 100644 tests/ruby/copy_spec.rb delete mode 100644 tests/ruby/helpers/auth_query_helper.rb delete mode 100644 tests/ruby/helpers/pg_instance.rb delete mode 100644 tests/ruby/helpers/pg_socket.rb delete mode 100644 tests/ruby/helpers/pgcat_helper.rb delete mode 100644 tests/ruby/helpers/pgcat_process.rb delete mode 100644 tests/ruby/load_balancing_spec.rb delete mode 100644 tests/ruby/mirrors_spec.rb delete mode 100644 tests/ruby/misc_spec.rb delete mode 100644 tests/ruby/plugins_spec.rb delete mode 100644 tests/ruby/prepared_spec.rb delete mode 100644 tests/ruby/protocol_spec.rb delete mode 100644 tests/ruby/routing_spec.rb delete mode 100644 tests/ruby/sharding_spec.rb delete mode 100644 tests/ruby/spec_helper.rb delete mode 100644 tests/ruby/stats_spec.rb delete mode 100644 tests/ruby/tests.rb delete mode 100644 tests/rust/.gitignore delete mode 100644 tests/rust/Cargo.lock delete mode 100644 tests/rust/Cargo.toml delete mode 100644 tests/rust/src/main.rs delete mode 100644 tests/sharding/README.md delete mode 100644 tests/sharding/partition_hash_test_setup.sql delete mode 100644 tests/sharding/query_routing.sh delete mode 100644 tests/sharding/query_routing_setup.sql delete mode 100644 tests/sharding/query_routing_test_insert.sql delete mode 100644 tests/sharding/query_routing_test_primary_replica.sql delete mode 100644 tests/sharding/query_routing_test_select.sql delete mode 100644 tests/sharding/query_routing_test_validate.sql delete mode 100644 utilities/deb.sh delete mode 100644 utilities/generate_config_docs.py delete mode 100644 utilities/requirements.txt diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 0722411..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,87 +0,0 @@ -# Use the latest 2.1 version of CircleCI pipeline process engine. -# See: https://circleci.com/docs/2.0/configuration-reference -version: 2.1 - -# Define a job to be invoked later in a workflow. -# See: https://circleci.com/docs/2.0/configuration-reference/#jobs -jobs: - build: - # Specify the execution environment. You can specify an image from Dockerhub or use one of our Convenience Images from CircleCI's Developer Hub. - # See: https://circleci.com/docs/2.0/configuration-reference/#docker-machine-macos-windows-executor - docker: - - image: ghcr.io/postgresml/pgcat-ci:latest - environment: - RUST_LOG: info - LLVM_PROFILE_FILE: /tmp/pgcat-%m-%p.profraw - RUSTC_BOOTSTRAP: 1 - CARGO_INCREMENTAL: 0 - RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort -Cinstrument-coverage" - RUSTDOCFLAGS: "-Cpanic=abort" - - image: postgres:14 - command: ["postgres", "-p", "5432", "-c", "shared_preload_libraries=pg_stat_statements", "-c", "pg_stat_statements.track=all", "-c", "pg_stat_statements.max=100000"] - environment: - POSTGRES_USER: postgres - POSTGRES_DB: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_INITDB_ARGS: --auth-local=md5 --auth-host=md5 --auth=md5 - - image: postgres:14 - command: ["postgres", "-p", "7432", "-c", "shared_preload_libraries=pg_stat_statements", "-c", "pg_stat_statements.track=all", "-c", "pg_stat_statements.max=100000"] - environment: - POSTGRES_USER: postgres - POSTGRES_DB: postgres - 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", "8432", "-c", "shared_preload_libraries=pg_stat_statements", "-c", "pg_stat_statements.track=all", "-c", "pg_stat_statements.max=100000"] - environment: - POSTGRES_USER: postgres - POSTGRES_DB: postgres - 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", "9432", "-c", "shared_preload_libraries=pg_stat_statements", "-c", "pg_stat_statements.track=all", "-c", "pg_stat_statements.max=100000"] - environment: - POSTGRES_USER: postgres - POSTGRES_DB: postgres - 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: - - checkout - - restore_cache: - key: cargo-lock-2-{{ checksum "Cargo.lock" }} - - run: - name: "Lint" - command: "cargo fmt --check" - - run: - name: "Clippy" - command: "cargo clippy --all --all-targets -- -Dwarnings" - - run: - name: "Tests" - command: "cargo clean && cargo build && cargo test && bash .circleci/run_tests.sh && .circleci/generate_coverage.sh" - - store_artifacts: - path: /tmp/cov - destination: coverage-data - - save_cache: - key: cargo-lock-2-{{ checksum "Cargo.lock" }} - paths: - - target - - ~/.cargo - - -# Invoke jobs via workflows -# See: https://circleci.com/docs/2.0/configuration-reference/#workflows -workflows: - build: - jobs: - - build diff --git a/.circleci/generate_coverage.sh b/.circleci/generate_coverage.sh deleted file mode 100755 index 6244c19..0000000 --- a/.circleci/generate_coverage.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -# inspired by https://doc.rust-lang.org/rustc/instrument-coverage.html#tips-for-listing-the-binaries-automatically -TEST_OBJECTS=$( \ - for file in $(cargo test --no-run 2>&1 | grep "target/debug/deps/pgcat-[[:alnum:]]\+" -o); \ - do \ - printf "%s %s " --object $file; \ - done \ -) - -rust-profdata merge -sparse /tmp/pgcat-*.profraw -o /tmp/pgcat.profdata - -bash -c "rust-cov export -ignore-filename-regex='rustc|registry' -Xdemangler=rustfilt -instr-profile=/tmp/pgcat.profdata $TEST_OBJECTS --object ./target/debug/pgcat --format lcov > ./lcov.info" - -genhtml lcov.info --title "PgCat Code Coverage" --css-file ./cov-style.css --no-function-coverage --highlight --ignore-errors source --legend --output-directory /tmp/cov --prefix $(pwd) diff --git a/.circleci/pgcat.toml b/.circleci/pgcat.toml deleted file mode 100644 index 146d1f2..0000000 --- a/.circleci/pgcat.toml +++ /dev/null @@ -1,158 +0,0 @@ -# -# PgCat config example. -# - -# -# General pooler settings -[general] -# What IP to run on, 0.0.0.0 means accessible from everywhere. -host = "0.0.0.0" - -# Port to run on, same as PgBouncer used in this example. -port = 6432 - -# Whether to enable prometheus exporter or not. -enable_prometheus_exporter = true - -# Port at which prometheus exporter listens on. -prometheus_exporter_port = 9930 - -# How long to wait before aborting a server connection (ms). -connect_timeout = 1000 - -# How much time to give the health check query to return with a result (ms). -healthcheck_timeout = 1000 - -# How long to keep connection available for immediate re-use, without running a healthcheck query on it -healthcheck_delay = 30000 - -# How much time to give clients during shutdown before forcibly killing client connections (ms). -shutdown_timeout = 5000 - -# For how long to ban a server if it fails a health check (seconds). -ban_time = 60 # Seconds - -# If we should log client connections -log_client_connections = false - -# If we should log client disconnections -log_client_disconnections = false - -# Reload config automatically if it changes. -autoreload = 15000 - -# TLS -tls_certificate = ".circleci/server.cert" -tls_private_key = ".circleci/server.key" - -# Credentials to access the virtual administrative database (pgbouncer or pgcat) -# Connecting to that database allows running commands like `SHOW POOLS`, `SHOW DATABASES`, etc.. -admin_username = "admin_user" -admin_password = "admin_pass" - -# pool -# configs are structured as pool. -# the pool_name is what clients use as database name when connecting -# For the example below a client can connect using "postgres://sharding_user:sharding_user@pgcat_host:pgcat_port/sharded_db" -[pools.sharded_db] -# Pool mode (see PgBouncer docs for more). -# session: one server connection per connected client -# transaction: one server connection per client transaction -pool_mode = "transaction" -prepared_statements_cache_size = 500 - -# If the client doesn't specify, route traffic to -# this role by default. -# -# any: round-robin between primary and replicas, -# replica: round-robin between replicas only without touching the primary, -# primary: all queries go to the primary unless otherwise specified. -default_role = "any" - -# Query parser. If enabled, we'll attempt to parse -# every incoming query to determine if it's a read or a write. -# If it's a read query, we'll direct it to a replica. Otherwise, if it's a write, -# we'll direct it to the primary. -query_parser_enabled = true - -# If the query parser is enabled and this setting is enabled, we'll attempt to -# infer the role from the query itself. -query_parser_read_write_splitting = 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. -primary_reads_enabled = true - -# So what if you wanted to implement a different hashing function, -# or you've already built one and you want this pooler to use it? -# -# Current options: -# -# pg_bigint_hash: PARTITION BY HASH (Postgres hashing function) -# sha1: A hashing function based on SHA1 -# -sharding_function = "pg_bigint_hash" - -# Credentials for users that may connect to this cluster -[pools.sharded_db.users.0] -username = "sharding_user" -password = "sharding_user" -# 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 -statement_timeout = 0 - -[pools.sharded_db.users.1] -username = "other_user" -password = "other_user" -pool_size = 21 -statement_timeout = 30000 - -# Shard 0 -[pools.sharded_db.shards.0] -# [ host, port, role ] -servers = [ - [ "127.0.0.1", 5432, "primary" ], - [ "localhost", 5432, "replica" ] -] -# Database name (e.g. "postgres") -database = "shard0" - -[pools.sharded_db.shards.1] -servers = [ - [ "127.0.0.1", 5432, "primary" ], - [ "localhost", 5432, "replica" ], -] -database = "shard1" - -[pools.sharded_db.shards.2] -servers = [ - [ "127.0.0.1", 5432, "primary" ], - [ "localhost", 5432, "replica" ], -] -database = "shard2" - - -[pools.simple_db] -pool_mode = "session" -default_role = "primary" -query_parser_enabled = true -query_parser_read_write_splitting = true -primary_reads_enabled = true -sharding_function = "pg_bigint_hash" -prepared_statements_cache_size = 500 - -[pools.simple_db.users.0] -username = "simple_user" -password = "simple_user" -pool_size = 5 -statement_timeout = 30000 - -[pools.simple_db.shards.0] -servers = [ - [ "127.0.0.1", 5432, "primary" ], - [ "localhost", 5432, "replica" ] -] -database = "some_db" diff --git a/.circleci/run_tests.sh b/.circleci/run_tests.sh deleted file mode 100644 index a93b484..0000000 --- a/.circleci/run_tests.sh +++ /dev/null @@ -1,183 +0,0 @@ -#!/bin/bash - -set -e -set -o xtrace - -# non-zero exit code if we provide bad configs -(! ./target/debug/pgcat "fake_configs" 2>/dev/null) - -# Start PgCat with a particular log level -# for inspection. -function start_pgcat() { - kill -s SIGINT $(pgrep pgcat) || true - RUST_LOG=${1} ./target/debug/pgcat .circleci/pgcat.toml & - sleep 1 -} - -# Setup the database with shards and user -PGPASSWORD=postgres psql -e -h 127.0.0.1 -p 5432 -U postgres -f tests/sharding/query_routing_setup.sql -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 -PGPASSWORD=sharding_user pgbench -h 127.0.0.1 -U sharding_user shard2 -i - -# Start Toxiproxy -kill -9 $(pgrep toxiproxy) || true -LOG_LEVEL=error toxiproxy-server & -sleep 1 - -# Create a database at port 5433, forward it to Postgres -toxiproxy-cli create -l 127.0.0.1:5433 -u 127.0.0.1:5432 postgres_replica - -start_pgcat "info" - -# Check that prometheus is running -curl --fail localhost:9930/metrics - -export PGPASSWORD=sharding_user -export PGDATABASE=sharded_db - -# pgbench test -pgbench -U sharding_user -i -h 127.0.0.1 -p 6432 -pgbench -U sharding_user -h 127.0.0.1 -p 6432 -t 500 -c 2 --protocol simple -f tests/pgbench/simple.sql -pgbench -U sharding_user -h 127.0.0.1 -p 6432 -t 500 -c 2 --protocol extended - -# COPY TO STDOUT test -psql -U sharding_user -h 127.0.0.1 -p 6432 -c 'COPY (SELECT * FROM pgbench_accounts LIMIT 15) TO STDOUT;' > /dev/null - -# Query cancellation test -(psql -U sharding_user -h 127.0.0.1 -p 6432 -c 'SELECT pg_sleep(50)' || true) & -sleep 1 -killall psql -s SIGINT - -# Pause/resume test. -# Running benches before, during, and after pause/resume. -pgbench -U sharding_user -t 500 -c 2 -h 127.0.0.1 -p 6432 --protocol extended & -BENCH_ONE=$! -PGPASSWORD=admin_pass psql -U admin_user -h 127.0.0.1 -p 6432 -d pgbouncer -c 'PAUSE sharded_db,sharding_user' -pgbench -U sharding_user -h 127.0.0.1 -p 6432 -t 500 -c 2 --protocol extended & -BENCH_TWO=$! -PGPASSWORD=admin_pass psql -U admin_user -h 127.0.0.1 -p 6432 -d pgbouncer -c 'RESUME sharded_db,sharding_user' -wait ${BENCH_ONE} -wait ${BENCH_TWO} - -# Reload pool (closing unused server connections) -PGPASSWORD=admin_pass psql -U admin_user -h 127.0.0.1 -p 6432 -d pgbouncer -c 'RELOAD' - -(psql -U sharding_user -h 127.0.0.1 -p 6432 -c 'SELECT pg_sleep(50)' || true) & -sleep 1 -killall psql -s SIGINT - -# Sharding insert -psql -U sharding_user -e -h 127.0.0.1 -p 6432 -f tests/sharding/query_routing_test_insert.sql - -# Sharding select -psql -U sharding_user -e -h 127.0.0.1 -p 6432 -f tests/sharding/query_routing_test_select.sql > /dev/null - -# Replica/primary selection & more sharding tests -psql -U sharding_user -e -h 127.0.0.1 -p 6432 -f tests/sharding/query_routing_test_primary_replica.sql > /dev/null - -# Statement timeout tests -sed -i 's/statement_timeout = 0/statement_timeout = 100/' .circleci/pgcat.toml -kill -SIGHUP $(pgrep pgcat) # Reload config -sleep 0.2 - -# This should timeout -(! psql -U sharding_user -e -h 127.0.0.1 -p 6432 -c 'select pg_sleep(0.5)') - -# Disable statement timeout -sed -i 's/statement_timeout = 100/statement_timeout = 0/' .circleci/pgcat.toml -kill -SIGHUP $(pgrep pgcat) # Reload config again - -# -# Integration tests and ActiveRecord tests -# -cd tests/ruby -sudo bundle install -bundle exec ruby tests.rb --format documentation || exit 1 -bundle exec rspec *_spec.rb --format documentation || exit 1 -cd ../.. - -# -# Python tests -# These tests will start and stop the pgcat server so it will need to be restarted after the tests -# -pip3 install -r tests/python/requirements.txt -pytest || exit 1 - - -# -# Go tests -# Starts its own pgcat server -# -pushd tests/go -/usr/local/go/bin/go test || exit 1 -popd - -start_pgcat "info" - -# -# Rust tests -# -cd tests/rust -cargo run -cd ../../ - -# Admin tests -export PGPASSWORD=admin_pass -psql -U admin_user -e -h 127.0.0.1 -p 6432 -d pgbouncer -c 'SHOW STATS' > /dev/null -psql -U admin_user -h 127.0.0.1 -p 6432 -d pgbouncer -c 'RELOAD' > /dev/null -psql -U admin_user -h 127.0.0.1 -p 6432 -d pgbouncer -c 'SHOW CONFIG' > /dev/null -psql -U admin_user -h 127.0.0.1 -p 6432 -d pgbouncer -c 'SHOW DATABASES' > /dev/null -psql -U admin_user -h 127.0.0.1 -p 6432 -d pgcat -c 'SHOW LISTS' > /dev/null -psql -U admin_user -h 127.0.0.1 -p 6432 -d pgcat -c 'SHOW POOLS' > /dev/null -psql -U admin_user -h 127.0.0.1 -p 6432 -d pgcat -c 'SHOW VERSION' > /dev/null -psql -U admin_user -h 127.0.0.1 -p 6432 -d pgbouncer -c "SET client_encoding TO 'utf8'" > /dev/null # will ignore -(! psql -U admin_user -e -h 127.0.0.1 -p 6432 -d random_db -c 'SHOW STATS' > /dev/null) -export PGPASSWORD=sharding_user - -# Start PgCat in debug to demonstrate failover better -start_pgcat "trace" - -# Add latency to the replica at port 5433 slightly above the healthcheck timeout -toxiproxy-cli toxic add -t latency -a latency=300 postgres_replica -sleep 1 - -# Note the failover in the logs -timeout 5 psql -U sharding_user -e -h 127.0.0.1 -p 6432 <<-EOF -SELECT 1; -SELECT 1; -SELECT 1; -EOF - -# Remove latency -toxiproxy-cli toxic remove --toxicName latency_downstream postgres_replica - -start_pgcat "info" - -# Test session mode (and config reload) -sed -i '0,/simple_db/s/pool_mode = "transaction"/pool_mode = "session"/' .circleci/pgcat.toml - -# Reload config test -kill -SIGHUP $(pgrep pgcat) - -# Revert settings after reload. Makes test runs idempotent -sed -i '0,/simple_db/s/pool_mode = "session"/pool_mode = "transaction"/' .circleci/pgcat.toml - -sleep 1 - -# Prepared statements that will only work in session mode -pgbench -U sharding_user -h 127.0.0.1 -p 6432 -t 500 -c 2 --protocol prepared - -# Attempt clean shut down -killall pgcat -s SIGINT - -# Allow for graceful shutdown -sleep 1 - -kill -9 $(pgrep toxiproxy) -sleep 1 diff --git a/.circleci/server.cert b/.circleci/server.cert deleted file mode 100644 index a24847a..0000000 --- a/.circleci/server.cert +++ /dev/null @@ -1,21 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDazCCAlOgAwIBAgIUChIvUGFJGJe5EDch32rchqoxER0wDQYJKoZIhvcNAQEL -BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM -GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMjA2MjcyMjI2MDZaFw0yMjA3 -MjcyMjI2MDZaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw -HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB -AQUAA4IBDwAwggEKAoIBAQDdTwrBzV1v79faVckFvIn/9V4fypYs4vDi3X+h3wGn -AjEh6mmizlKCwSwAam07D9Q5zKiXFrzNJqzSioOv5zsOAvObwrnzbtKSwfs3aP5g -eEh2clHCZYx9p06WszPcgSB5nTz1NeY4XAwvGn3A+SVCLyPMTNwnem48+ONh2F9u -FHtSuIsEVvTjMlH09O7LjwJlODxy3HNv2JHYM5Hx9tzc+NVYdERPtaVcX8ycw1Eh -9hgGSgfaNM52/JfRMIDhENrsn0S1omRUtcJe72loreiwrECUOLAnAfp9Xqc+rMPP -aLA6ElzmYef1+ZEC0p6isCHPhxY5ESVhKYhE9nQvksjnAgMBAAGjUzBRMB0GA1Ud -DgQWBBQLDtzexqjx7xPtUZuZB/angU9oSDAfBgNVHSMEGDAWgBQLDtzexqjx7xPt -UZuZB/angU9oSDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQC/ -mxY/a/WeLENVj2Gg9EUH0CKzfqeTey1mb6YfPGxzrD7oq1m0Vn2MmTbjZrJgh/Ob -QckO3ElF4kC9+6XP+iDPmabGpjeLgllBboT5l2aqnD1syMrf61WPLzgRzRfplYGy -cjBQDDKPu8Lu0QRMWU28tHYN0bMxJoCuXysGGX5WsuFnKCA6f/V+nycJJXxJH3eB -eLjTueD9/RE3OXhi6m8A29Q1E9AE5EF4uRxYXrr91BmYnk4aFvSmBxhUEzE12eSN -lHB/uSc0+Dp+UVmVr6wW8AQfd16UBA0BUf3kSW3aSvirYPYH0rXiOOpEJgOwOMnR -f5+XAbN1Y+3OsFz/ZmP9 ------END CERTIFICATE----- diff --git a/.circleci/server.key b/.circleci/server.key deleted file mode 100644 index 14e4fd6..0000000 --- a/.circleci/server.key +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDdTwrBzV1v79fa -VckFvIn/9V4fypYs4vDi3X+h3wGnAjEh6mmizlKCwSwAam07D9Q5zKiXFrzNJqzS -ioOv5zsOAvObwrnzbtKSwfs3aP5geEh2clHCZYx9p06WszPcgSB5nTz1NeY4XAwv -Gn3A+SVCLyPMTNwnem48+ONh2F9uFHtSuIsEVvTjMlH09O7LjwJlODxy3HNv2JHY -M5Hx9tzc+NVYdERPtaVcX8ycw1Eh9hgGSgfaNM52/JfRMIDhENrsn0S1omRUtcJe -72loreiwrECUOLAnAfp9Xqc+rMPPaLA6ElzmYef1+ZEC0p6isCHPhxY5ESVhKYhE -9nQvksjnAgMBAAECggEAbnvddO9frFhivJ+DIhgEFQKcIOb0nigV9kx6QYehvYy8 -lp/+aMb0Lk7d9r8rFQdL/icMK5GwZALg2KNKJvEbbF1Q3PwT9VHoUlgBYKJMDEFA -e9GKu7ASuVBjTZzdUUItwkkbe5eS/aQGeSWSjlpTnX0HNCFS72qRymK+scRhsAQf -ZoHyZHDslkvPR3Pos+sndWBYCDHag5/KoPhsMt1+5S9NQcOUHx9Ac0gLHjau3N+P -0FhODHFFGnnpyQvLvj6u3ZOR34ladMgoBglE0O3vPFhckn92EK4teeTWOsUMotiz -qM3QIJTOJjtiY6VDGY93bIa4pFvt7Zi4vIerenKt0QKBgQD/UMFqfevTAMrk10AC -bOa4+cM07ORY4ZwVj5ILhZn+8crDEEtBsUyuEU2FTINtnoEq1yGc/IXpsyS1BHjL -L1xSml5LN3jInbi8z5XQfY5Sj3VOMtwY6yD20jcdeDC44rz3nStXdkcMWxbTMapx -iOPsap5ciUKOMS7LyMidPEG/LQKBgQDd5vHgrLN0FBIIm+vZg6MEm4QyobstVp4l -7V/GZsdL+M8AQv1Rx+5wSUSWKomOIv5lglis7f6g0c9O7Qkr78/wzoyoKC2RRqPp -I90GjY2Iv22N4GIkRrDAgMZbkTitzIB6tbXEVeLAOh3frFJ8IwauRCOiXIjrZdJ4 -FvV86+nU4wKBgQDdWTP2kWkMrBk7QOp7r9Jv+AmnLuHhtOdPQgOJ/bA++X2ik9PL -Bl3GY7XjpSwks1CkxZKcucmXjPp7/X6EGXFfI/owF82dkDADca0e7lufdERtIWb0 -K5WOpz2lTPhgsiLGQfq7fw2lxqsJOnvcpqOD6gOVkmKjSDyb7F0RBJazmQKBgQDD -a8PQTcesjpBjLI3EfX1vbVY7ENu6zfFxDV+vZoxVh8UlQdm90AlYse3JIaUKnB7W -Xrihcucv0hZ0N6RAIW5LcFvHK7sVmdR4WbEpODhRGeTtcZJ8yBSZM898jKQRy2vK -pYRyaADNsWDlvujVkjMr/a40KrIaPQ3h3LZNUaYYaQKBgQD1x8A5S5SiE1cN1vFr -aACkmA2WqEDKKhUsUigJdwW6WB/B9kWlIlz/iV1H9uwBXtSIYG4VqCSTAvh0z4gX -Qu2SrdPm5PYnKzpdynpz78OnGdflD1RKWFGHItR6GN6tj/VmulO6mlFvT4jzBQ7j -+Hf8m2TcD4U3ksz3xw+YOD+cmA== ------END RSA PRIVATE KEY----- diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 1063ad1..0000000 --- a/.dockerignore +++ /dev/null @@ -1,6 +0,0 @@ -target/ -tests/ -tracing/ -.circleci/ -.git/ -dev/ diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index d7a2758..0000000 --- a/.editorconfig +++ /dev/null @@ -1,14 +0,0 @@ -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/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index dd84ea7..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '' -labels: '' -assignees: '' - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Desktop (please complete the following information):** - - OS: [e.g. iOS] - - Browser [e.g. chrome, safari] - - Version [e.g. 22] - -**Smartphone (please complete the following information):** - - Device: [e.g. iPhone6] - - OS: [e.g. iOS8.1] - - Browser [e.g. stock browser, safari] - - Version [e.g. 22] - -**Additional context** -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index bbcbbe7..0000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: '' -labels: '' -assignees: '' - ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 7f3bf9d..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,16 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "cargo" - directory: "/" - schedule: - interval: "daily" - time: "04:00" # UTC - labels: - - "dependencies" - commit-message: - prefix: "chore(deps)" - open-pull-requests-limit: 10 - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "weekly" diff --git a/.github/workflows/build-and-push.yaml b/.github/workflows/build-and-push.yaml deleted file mode 100644 index a2f1c75..0000000 --- a/.github/workflows/build-and-push.yaml +++ /dev/null @@ -1,68 +0,0 @@ -name: Build and Push - -on: - push: - paths: - - '!charts/**.md' - branches: - - main - tags: - - v* - -env: - registry: ghcr.io - image-name: ${{ github.repository }} - -jobs: - build-and-push: - runs-on: ubuntu-latest - - permissions: - contents: read - packages: write - - steps: - - name: Checkout Repository - uses: actions/checkout@v4 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Determine tags - id: metadata - uses: docker/metadata-action@v5 - with: - images: ${{ env.registry }}/${{ env.image-name }} - tags: | - type=sha,prefix=,format=long - type=schedule - type=ref,event=tag - type=ref,event=branch - type=ref,event=pr - type=raw,value=latest,enable={{ is_default_branch }} - - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.registry }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build and push ${{ env.image-name }} - uses: docker/build-push-action@v6 - with: - context: . - platforms: linux/amd64,linux/arm64 - provenance: false - push: true - tags: ${{ steps.metadata.outputs.tags }} - labels: ${{ steps.metadata.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - -concurrency: - group: ${{ github.ref }} - cancel-in-progress: true diff --git a/.github/workflows/chart-lint-test.yaml b/.github/workflows/chart-lint-test.yaml deleted file mode 100644 index c19789f..0000000 --- a/.github/workflows/chart-lint-test.yaml +++ /dev/null @@ -1,50 +0,0 @@ -name: Lint and Test Charts - -on: - pull_request: - paths: - - charts/** - - '!charts/**.md' -jobs: - lint-test: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3.1.0 - with: - fetch-depth: 0 - - - name: Set up Helm - uses: azure/setup-helm@v3 - with: - version: v3.8.1 - - # Python is required because `ct lint` runs Yamale (https://github.com/23andMe/Yamale) and - # yamllint (https://github.com/adrienverge/yamllint) which require Python - - name: Set up Python - uses: actions/setup-python@v5.1.0 - with: - python-version: 3.7 - - - name: Set up chart-testing - uses: helm/chart-testing-action@v2.2.1 - with: - version: v3.5.1 - - - name: Run chart-testing (list-changed) - id: list-changed - run: | - changed=$(ct list-changed --config ct.yaml) - if [[ -n "$changed" ]]; then - echo "changed=true" >> $GITHUB_OUTPUT - fi - - - name: Run chart-testing (lint) - run: ct lint --config ct.yaml - - - name: Create kind cluster - uses: helm/kind-action@v1.10.0 - if: steps.list-changed.outputs.changed == 'true' - - - name: Run chart-testing (install) - run: ct install --config ct.yaml diff --git a/.github/workflows/chart-release.yaml b/.github/workflows/chart-release.yaml deleted file mode 100644 index fa57272..0000000 --- a/.github/workflows/chart-release.yaml +++ /dev/null @@ -1,40 +0,0 @@ -name: Release Charts - -on: - push: - paths: - - charts/** - - '!**.md' - branches: - - main - -jobs: - release: - runs-on: ubuntu-latest - - permissions: - contents: write - - steps: - - name: Checkout - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 - with: - fetch-depth: 0 - - - name: Configure Git - run: | - git config user.name "$GITHUB_ACTOR" - git config user.email "$GITHUB_ACTOR@users.noreply.github.com" - - - name: Install Helm - uses: azure/setup-helm@5119fcb9089d432beecbf79bb2c7915207344b78 # v3.5 - with: - version: v3.13.0 - - - name: Run chart-releaser - uses: helm/chart-releaser-action@a917fd15b20e8b64b94d9158ad54cd6345335584 # v1.6.0 - with: - charts_dir: charts - config: cr.yaml - env: - CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/generate-chart-readme.yaml b/.github/workflows/generate-chart-readme.yaml deleted file mode 100644 index affc40c..0000000 --- a/.github/workflows/generate-chart-readme.yaml +++ /dev/null @@ -1,48 +0,0 @@ -name: '[CI/CD] Update README metadata' - -on: - pull_request_target: - branches: - - main - paths: - - 'charts/*/values.yaml' -# Remove all permissions by default -permissions: {} -jobs: - update-readme-metadata: - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - name: Install readme-generator-for-helm - run: npm install -g @bitnami/readme-generator-for-helm - - name: Checkout - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 - with: - path: charts - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - token: ${{ secrets.GITHUB_TOKEN }} - - name: Execute readme-generator-for-helm - env: - DIFF_URL: "${{github.event.pull_request.diff_url}}" - TEMP_FILE: "${{runner.temp}}/pr-${{github.event.number}}.diff" - run: | - # This request doesn't consume API calls. - curl -Lkso $TEMP_FILE $DIFF_URL - files_changed="$(sed -nr 's/[\-\+]{3} [ab]\/(.*)/\1/p' $TEMP_FILE | sort | uniq)" - # Adding || true to avoid "Process exited with code 1" errors - charts_dirs_changed="$(echo "$files_changed" | xargs dirname | grep -o "pgcat/[^/]*" | sort | uniq || true)" - for chart in ${charts_dirs_changed}; do - echo "Updating README.md for ${chart}" - readme-generator --values "charts/${chart}/values.yaml" --readme "charts/${chart}/README.md" --schema "/tmp/schema.json" - done - - name: Push changes - run: | - # Push all the changes - cd charts - if git status -s | grep pgcat; then - git config user.name "$GITHUB_ACTOR" - git config user.email "$GITHUB_ACTOR@users.noreply.github.com" - git add . && git commit -am "Update README.md with readme-generator-for-helm" --signoff && git push - fi diff --git a/.github/workflows/publish-ci-docker-image.yml b/.github/workflows/publish-ci-docker-image.yml deleted file mode 100644 index 5df4fd4..0000000 --- a/.github/workflows/publish-ci-docker-image.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: publish-ci-docker-image -on: - push: - branches: [ main ] -jobs: - publish-ci-docker-image: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Login to GitHub Container Registry - uses: docker/login-action@v1 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Build CI Docker image - run: | - docker build . -f Dockerfile.ci --tag ghcr.io/postgresml/pgcat-ci:latest - docker run ghcr.io/postgresml/pgcat-ci:latest - docker push ghcr.io/postgresml/pgcat-ci:latest diff --git a/.github/workflows/publish-deb-package.yml b/.github/workflows/publish-deb-package.yml deleted file mode 100644 index dbb7abd..0000000 --- a/.github/workflows/publish-deb-package.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: pgcat package (deb) - -on: - push: - tags: - - v* - workflow_dispatch: - inputs: - packageVersion: - default: "1.1.2-dev1" -jobs: - build: - strategy: - max-parallel: 1 - fail-fast: false # Let the other job finish, or they can lock each other out - matrix: - os: ["buildjet-4vcpu-ubuntu-2204", "buildjet-4vcpu-ubuntu-2204-arm"] - - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v3 - - name: Set package version - if: github.event_name == 'push' # For push event - run: | - TAG=${{ github.ref_name }} - echo "packageVersion=${TAG#v}" >> "$GITHUB_ENV" - - name: Set package version (manual dispatch) - if: github.event_name == 'workflow_dispatch' # For manual dispatch - run: echo "packageVersion=${{ github.event.inputs.packageVersion }}" >> "$GITHUB_ENV" - - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - - name: Install dependencies - env: - DEBIAN_FRONTEND: noninteractive - TZ: Etc/UTC - run: | - curl -sLO https://github.com/deb-s3/deb-s3/releases/download/0.11.4/deb-s3-0.11.4.gem - sudo gem install deb-s3-0.11.4.gem - dpkg-deb --version - - name: Build and release package - env: - AWS_ACCESS_KEY_ID: ${{ vars.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_DEFAULT_REGION: ${{ vars.AWS_DEFAULT_REGION }} - run: | - if [[ $(arch) == "x86_64" ]]; then - export ARCH=amd64 - else - export ARCH=arm64 - fi - - bash utilities/deb.sh ${{ env.packageVersion }} - - deb-s3 upload \ - --lock \ - --bucket apt.postgresml.org \ - pgcat-${{ env.packageVersion }}-ubuntu22.04-${ARCH}.deb \ - --codename $(lsb_release -cs) diff --git a/.rustfmt.toml b/.rustfmt.toml deleted file mode 100644 index 17f3321..0000000 --- a/.rustfmt.toml +++ /dev/null @@ -1,2 +0,0 @@ -edition = "2021" -hard_tabs = false diff --git a/CONFIG.md b/CONFIG.md deleted file mode 100644 index 4e984ed..0000000 --- a/CONFIG.md +++ /dev/null @@ -1,523 +0,0 @@ -# PgCat Configurations -## `general` Section - -### host -``` -path: general.host -default: "0.0.0.0" -``` - -What IP to run on, 0.0.0.0 means accessible from everywhere. - -### port -``` -path: general.port -default: 6432 -``` - -Port to run on, same as PgBouncer used in this example. - -### enable_prometheus_exporter -``` -path: general.enable_prometheus_exporter -default: true -``` - -Whether to enable prometheus exporter or not. - -### prometheus_exporter_port -``` -path: general.prometheus_exporter_port -default: 9930 -``` - -Port at which prometheus exporter listens on. - -### connect_timeout -``` -path: general.connect_timeout -default: 1000 # milliseconds -``` - -How long the client waits to obtain a server connection before aborting (ms). -This is similar to PgBouncer's `query_wait_timeout`. - -### idle_timeout -``` -path: general.idle_timeout -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. - -### server_round_robin -``` -path: general.server_round_robin -default: false -``` - -Whether to use round robin for server selection or not. - -### server_tls -``` -path: general.server_tls -default: false -``` - -Whether to use TLS for server connections or not. - -### verify_server_certificate -``` -path: general.verify_server_certificate -default: false -``` - -Whether to verify server certificate or not. - -### verify_config -``` -path: general.verify_config -default: true -``` - -Whether to verify config or not. - -### idle_client_in_transaction_timeout -``` -path: general.idle_client_in_transaction_timeout -default: 0 # milliseconds -``` - -How long a client is allowed to be idle while in a transaction (ms). - -### healthcheck_timeout -``` -path: general.healthcheck_timeout -default: 1000 # milliseconds -``` - -How much time to give the health check query to return with a result (ms). - -### healthcheck_delay -``` -path: general.healthcheck_delay -default: 30000 # milliseconds -``` - -How long to keep connection available for immediate re-use, without running a healthcheck query on it - -### shutdown_timeout -``` -path: general.shutdown_timeout -default: 60000 # milliseconds -``` - -How much time to give clients during shutdown before forcibly killing client connections (ms). - -### ban_time -``` -path: general.ban_time -default: 60 # seconds -``` - -How long to ban a server if it fails a health check (seconds). - -### log_client_connections -``` -path: general.log_client_connections -default: false -``` - -If we should log client connections - -### log_client_disconnections -``` -path: general.log_client_disconnections -default: false -``` - -If we should log client disconnections - -### autoreload -``` -path: general.autoreload -default: 15000 # milliseconds -``` - -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 -``` -path: general.worker_threads -default: 5 -``` - -Number of worker threads the Runtime will use (4 by default). - -### tcp_keepalives_idle -``` -path: general.tcp_keepalives_idle -default: 5 -``` - -Number of seconds of connection idleness to wait before sending a keepalive packet to the server. - -### tcp_keepalives_count -``` -path: general.tcp_keepalives_count -default: 5 -``` - -Number of unacknowledged keepalive packets allowed before giving up and closing the connection. - -### tcp_keepalives_interval -``` -path: general.tcp_keepalives_interval -default: 5 -``` - -### 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 -``` -path: general.tls_certificate -default: -example: "server.cert" -``` - -Path to TLS Certificate file to use for TLS connections - -### tls_private_key -``` -path: general.tls_private_key -default: -example: "server.key" -``` - -Path to TLS private key file to use for TLS connections - -### admin_username -``` -path: general.admin_username -default: "admin_user" -``` - -User name to access the virtual administrative database (pgbouncer or pgcat) -Connecting to that database allows running commands like `SHOW POOLS`, `SHOW DATABASES`, etc.. - -### admin_password -``` -path: general.admin_password -default: "admin_pass" -``` - -Password to access the virtual administrative database - -### auth_query -``` -path: general.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: general.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: general.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. - -### 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 -``` -path: pools..pool_mode -default: "transaction" -``` - -Pool mode (see PgBouncer docs for more). -`session` one server connection per connected client -`transaction` one server connection per client transaction - -### load_balancing_mode -``` -path: pools..load_balancing_mode -default: "random" -``` - -Load balancing mode -`random` selects the server at random -`loc` selects the server with the least outstanding busy connections - -### default_role -``` -path: pools..default_role -default: "any" -``` - -If the client doesn't specify, PgCat routes traffic to this role by default. -`any` round-robin between primary and replicas, -`replica` round-robin between replicas only without touching the primary, -`primary` all queries go to the primary unless otherwise specified. - -### prepared_statements_cache_size -``` -path: general.prepared_statements_cache_size -default: 0 -``` - -Size of the prepared statements cache. 0 means disabled. -TODO: update documentation - -### query_parser_enabled -``` -path: pools..query_parser_enabled -default: true -``` - -If Query Parser is enabled, we'll attempt to parse -every incoming query to determine if it's a read or a write. -If it's a read query, we'll direct it to a replica. Otherwise, if it's a write, -we'll direct it to the primary. - -### primary_reads_enabled -``` -path: pools..primary_reads_enabled -default: 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 explicitly selected with our custom protocol. - -### sharding_key_regex -``` -path: pools..sharding_key_regex -default: -example: '/\* sharding_key: (\d+) \*/' -``` - -Allow sharding commands to be passed as statement comments instead of -separate commands. If these are unset this functionality is disabled. - -### sharding_function -``` -path: pools..sharding_function -default: "pg_bigint_hash" -``` - -So what if you wanted to implement a different hashing function, -or you've already built one and you want this pooler to use it? -Current options: -`pg_bigint_hash`: PARTITION BY HASH (Postgres hashing function) -`sha1`: A hashing function based on SHA1 - -### auth_query -``` -path: pools..auth_query -default: -example: "SELECT $1" -``` - -Query to be sent to servers to obtain the hash used for md5 authentication. The connection will be -established using the database configured in the pool. This parameter is inherited by every pool -and can be redefined in pool configuration. - -### auth_query_user -``` -path: pools..auth_query_user -default: -example: "sharding_user" -``` - -User to be used for connecting to servers to obtain the hash used for md5 authentication by sending the query -specified in `auth_query_user`. The connection will be established using the database configured in the pool. -This parameter is inherited by every pool and can be redefined in pool configuration. - -### auth_query_password -``` -path: pools..auth_query_password -default: -example: "sharding_user" -``` - -Password to be used for connecting to servers to obtain the hash used for md5 authentication by sending the query -specified in `auth_query_user`. The connection will be established using the database configured in the pool. -This parameter is inherited by every pool and can be redefined in pool configuration. - -### automatic_sharding_key -``` -path: pools..automatic_sharding_key -default: -example: "data.id" -``` - -Automatically parse this from queries and route queries to the right shard! - -### idle_timeout -``` -path: pools..idle_timeout -default: 40000 -``` - -Idle timeout can be overwritten in the pool - -### connect_timeout -``` -path: pools..connect_timeout -default: 3000 -``` - -Connect timeout can be overwritten in the pool - -## `pools..users.` Section - -### username -``` -path: pools..users..username -default: "sharding_user" -``` - -PostgreSQL username used to authenticate the user and connect to the server -if `server_username` is not set. - -### password -``` -path: pools..users..password -default: "sharding_user" -``` - -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 -``` -path: pools..users..pool_size -default: 9 -``` - -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. - -### min_pool_size -``` -path: pools..users..min_pool_size -default: 0 -``` - -Minimum number of idle server connections to retain for this pool. - -### statement_timeout -``` -path: pools..users..statement_timeout -default: 0 -``` - -Maximum query duration. Dangerous, but protects against DBs that died in a non-obvious way. -0 means it is disabled. - -### connect_timeout -``` -path: pools..users..connect_timeout -default: # milliseconds -``` - -How long the client waits to obtain a server connection before aborting (ms). -This is similar to PgBouncer's `query_wait_timeout`. -If unset, uses the `connect_timeout` defined globally. - -## `pools..shards.` Section - -### servers -``` -path: pools..shards..servers -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 -``` -path: pools..shards..mirrors -default: -example: [["1.2.3.4", 5432, 0], ["1.2.3.4", 5432, 1]] -``` - -Array of mirrors for the shard, each mirror entry is an array of `[host, port, index of server in servers array]` -Traffic hitting the server identified by the index will be sent to the mirror. - -### database -``` -path: pools..shards..database -default: "shard0" -``` - -Database name (e.g. "postgres") diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index b946801..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,39 +0,0 @@ -## Introduction - -Thank you for contributing! Just a few tips here: - -1. `cargo fmt` and `cargo clippy` your code before opening up a PR -2. Run the test suite (e.g. `pgbench`) to make sure everything still works. The tests are in `.circleci/run_tests.sh`. -3. Performance is important, make sure there are no regressions in your branch vs. `main`. - -## How to run the integration tests locally and iterate on them -We have integration tests written in Ruby, Python, Go and Rust. -Below are the steps to run them in a developer-friendly way that allows iterating and quick turnaround. -Hear me out, this should be easy, it will involve opening a shell into a container with all the necessary dependancies available for you and you can modify the test code and immediately rerun your test in the interactive shell. - - -Quite simply, make sure you have docker installed and then run -`./start_test_env.sh` - -That is it! - -Within this test environment you can modify the file in your favorite IDE and rerun the tests without having to bootstrap the entire environment again. - -Once the environment is ready, you can run the tests by running -Ruby: `cd /app/tests/ruby && bundle exec ruby .rb --format documentation` -Python: `cd /app/ && pytest` -Rust: `cd /app/tests/rust && cargo run` -Go: `cd /app/tests/go && /usr/local/go/bin/go test` - -You can also rebuild PgCat directly within the environment and the tests will run against the newly built binary -To rebuild PgCat, just run `cargo build` within the container under `/app` - -![Animated gif showing how to run tests](https://github.com/user-attachments/assets/2258fde3-2aed-4efb-bdc5-e4f12dcd4d33) - - - -Happy hacking! - -## TODOs - -See [Issues]([url](https://github.com/levkk/pgcat/issues)). diff --git a/Cargo.lock b/Cargo.lock deleted file mode 100644 index fb58c7b..0000000 --- a/Cargo.lock +++ /dev/null @@ -1,2168 +0,0 @@ -# This file is automatically @generated by Cargo. -# 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 = "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 = "aho-corasick" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" -dependencies = [ - "memchr", -] - -[[package]] -name = "allocator-api2" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" - -[[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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -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.71" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a564d521dd56509c4c47480d00b80ee55f7e385ae48db5744c67ad50c92d2ebf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.26", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "atomic_enum" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6227a8d6fdb862bcb100c4314d0d9579e5cd73fa6df31a2e6f6e1acd3c5f1207" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "autocfg" -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.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" - -[[package]] -name = "bb8" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89aabfae550a5c44b43ab941844ffcd2e993cb6900b342debf59e9ea74acdb8" -dependencies = [ - "async-trait", - "futures-util", - "parking_lot", - "tokio", -] - -[[package]] -name = "bitflags" -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.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 = "chrono" -version = "0.4.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" -dependencies = [ - "android-tzdata", - "iana-time-zone", - "js-sys", - "num-traits", - "time", - "wasm-bindgen", - "winapi", -] - -[[package]] -name = "clap" -version = "4.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f644d0dac522c8b05ddc39aaaccc5b136d5dc4ff216610c5641e3be5becf56c" -dependencies = [ - "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.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" - -[[package]] -name = "cpufeatures" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" -dependencies = [ - "libc", -] - -[[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 = "data-encoding" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" - -[[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 = "either" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" - -[[package]] -name = "enum-as-inner" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9720bba047d567ffc8a3cba48bf19126600e249ab7f128e9233e6376976a116" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "equivalent" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" - -[[package]] -name = "errno" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" -dependencies = [ - "errno-dragonfly", - "libc", - "windows-sys", -] - -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "exitcode" -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.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[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-executor" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" - -[[package]] -name = "futures-macro" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.26", -] - -[[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-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[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 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.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" -dependencies = [ - "ahash", - "allocator-api2", -] - -[[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.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -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 = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" -dependencies = [ - "bytes", - "futures-util", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" - -[[package]] -name = "httpdate" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" - -[[package]] -name = "hyper" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "h2", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-util" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde7055719c54e36e95e8719f95883f22072a48ede39db7fc17a4e1d5281e9b9" -dependencies = [ - "bytes", - "futures-util", - "http", - "http-body", - "hyper", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "windows", -] - -[[package]] -name = "iana-time-zone-haiku" -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 = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" -dependencies = [ - "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 = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" -dependencies = [ - "equivalent", - "hashbrown", -] - -[[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.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" -dependencies = [ - "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.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" - -[[package]] -name = "jemalloc-sys" -version = "0.5.3+5.3.0-patched" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9bd5d616ea7ed58b571b2e209a65759664d7fb021a0819d7a790afc67e47ca1" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "jemallocator" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16c2514137880c52b0b4822b563fadd38257c1f380858addb74a400889696ea6" -dependencies = [ - "jemalloc-sys", - "libc", -] - -[[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 = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - -[[package]] -name = "libc" -version = "0.2.147" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" - -[[package]] -name = "linked-hash-map" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" - -[[package]] -name = "linux-raw-sys" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" - -[[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 = "lru" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efa59af2ddfad1854ae27d75009d538d0998b4b2fd47083e743ac1a10e46c60" -dependencies = [ - "hashbrown", -] - -[[package]] -name = "lru-cache" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" -dependencies = [ - "linked-hash-map", -] - -[[package]] -name = "match_cfg" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" - -[[package]] -name = "matchers" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" -dependencies = [ - "regex-automata 0.1.10", -] - -[[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" -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 = "memoffset" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" -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.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" -dependencies = [ - "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys", -] - -[[package]] -name = "nix" -version = "0.26.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" -dependencies = [ - "bitflags 1.3.2", - "cfg-if", - "libc", - "memoffset", - "pin-utils", - "static_assertions", -] - -[[package]] -name = "nu-ansi-term" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" -dependencies = [ - "overload", - "winapi", -] - -[[package]] -name = "num-traits" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "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.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" - -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - -[[package]] -name = "parking_lot" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[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", - "smallvec", - "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.2.0" -dependencies = [ - "arc-swap", - "async-trait", - "atomic_enum", - "base64", - "bb8", - "bytes", - "chrono", - "clap", - "exitcode", - "fallible-iterator", - "futures", - "hmac", - "http-body-util", - "hyper", - "hyper-util", - "itertools", - "jemallocator", - "log", - "lru", - "md-5", - "nix", - "num_cpus", - "once_cell", - "parking_lot", - "phf", - "pin-project", - "postgres-protocol", - "rand", - "regex", - "rustls", - "rustls-pemfile", - "serde", - "serde_derive", - "serde_json", - "sha-1", - "sha2", - "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.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" -dependencies = [ - "phf_macros", - "phf_shared", -] - -[[package]] -name = "phf_generator" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" -dependencies = [ - "phf_shared", - "rand", -] - -[[package]] -name = "phf_macros" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", - "syn 2.0.26", -] - -[[package]] -name = "phf_shared" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -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.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" - -[[package]] -name = "pin-utils" -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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" - -[[package]] -name = "proc-macro2" -version = "1.0.66" -source = "registry+https://github.com/rust-lang/crates.io-index" -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.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fe8a65d69dd0808184ebb5f836ab526bb259db23c657efa38711b1072ee47f0" -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.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "regex" -version = "1.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata 0.3.3", - "regex-syntax 0.7.4", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", -] - -[[package]] -name = "regex-automata" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax 0.7.4", -] - -[[package]] -name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - -[[package]] -name = "regex-syntax" -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 = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" -dependencies = [ - "hostname", - "quick-error", -] - -[[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 = "rustc-demangle" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" - -[[package]] -name = "rustix" -version = "0.38.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5" -dependencies = [ - "bitflags 2.3.3", - "errno", - "libc", - "linux-raw-sys", - "windows-sys", -] - -[[package]] -name = "rustls" -version = "0.21.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79ea77c539259495ce8ca47f53e66ae0330a8819f67e23ac96ca02f50e7b7d36" -dependencies = [ - "log", - "ring", - "rustls-webpki 0.101.1", - "sct", -] - -[[package]] -name = "rustls-pemfile" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" -dependencies = [ - "base64", -] - -[[package]] -name = "rustls-webpki" -version = "0.100.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e98ff011474fa39949b7e5c0428f9b4937eda7da7848bbb947786b7be0b27dab" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "rustls-webpki" -version = "0.101.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15f36a6828982f422756984e47912a7a51dcbc2a197aa791158f8ca61cd8204e" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "ryu" -version = "1.0.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" -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.171" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.171" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682" -dependencies = [ - "proc-macro2", - "quote", - "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.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" -dependencies = [ - "serde", -] - -[[package]] -name = "sha-1" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" -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 = "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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" -dependencies = [ - "libc", -] - -[[package]] -name = "siphasher" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" - -[[package]] -name = "slab" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" -dependencies = [ - "autocfg", -] - -[[package]] -name = "smallvec" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" - -[[package]] -name = "socket2" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - -[[package]] -name = "sqlparser" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc2c25a6c66789625ef164b4c7d2e548d627902280c13710d33da8222169964" -dependencies = [ - "log", - "sqlparser_derive", -] - -[[package]] -name = "sqlparser_derive" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01b2e185515564f15375f593fb966b5718bc624ba77fe49fa4616ad619690554" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.26", -] - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "stringprep" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -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.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.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45c3457aacde3c65315de5031ec191ce46604304d2446e803d71ade03308d970" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" -dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", -] - -[[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.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" -dependencies = [ - "autocfg", - "backtrace", - "bytes", - "libc", - "mio", - "num_cpus", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2 0.4.9", - "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.26", -] - -[[package]] -name = "tokio-rustls" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" -dependencies = [ - "rustls", - "tokio", -] - -[[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.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", - "tracing", -] - -[[package]] -name = "toml" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17e963a819c331dcacd7ab957d80bc2b9a9c1e71c804826d2f283dd65306542" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - -[[package]] -name = "toml_datetime" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.19.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "winnow", -] - -[[package]] -name = "tracing" -version = "0.1.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -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.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" -dependencies = [ - "lazy_static", - "log", - "tracing-core", -] - -[[package]] -name = "tracing-serde" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" -dependencies = [ - "serde", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex", - "serde", - "serde_json", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "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]] -name = "try-lock" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" - -[[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.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" - -[[package]] -name = "unicode-normalization" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" -dependencies = [ - "tinyvec", -] - -[[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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - -[[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.26", - "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.26", - "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-roots" -version = "0.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b03058f88386e5ff5310d9111d53f48b17d732b401aeb83a8d5190f2ac459338" -dependencies = [ - "rustls-webpki 0.100.2", -] - -[[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" -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" -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.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.48.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" -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" - -[[package]] -name = "winnow" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -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 deleted file mode 100644 index 21cfb0c..0000000 --- a/Cargo.toml +++ /dev/null @@ -1,60 +0,0 @@ -[package] -name = "pgcat" -version = "1.2.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.6" -async-trait = "0.1" -rand = "0.8" -chrono = "0.4" -sha-1 = "0.10" -toml = "0.7" -serde = { version = "1", features = ["derive"] } -serde_derive = "1" -regex = "1" -num_cpus = "1" -once_cell = "1" -sqlparser = { version = "0.41", features = ["visitor"] } -log = "0.4" -arc-swap = "1" -parking_lot = "0.12.1" -hmac = "0.12" -sha2 = "0.10" -base64 = "0.21" -stringprep = "0.1" -tokio-rustls = "0.24" -rustls-pemfile = "1" -http-body-util = "0.1.2" -hyper = { version = "1.4.1", features = ["full"] } -hyper-util = { version = "0.1.7", features = ["tokio"] } -phf = { version = "0.11.1", features = ["macros"] } -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", - "env-filter", - "std", -] } -lru = "0.12.0" - -[target.'cfg(not(target_env = "msvc"))'.dependencies] -jemallocator = "0.5.0" diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 8a7edea..0000000 --- a/Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -FROM rust:1.79.0-slim-bookworm AS builder - -RUN apt-get update && \ - apt-get install -y build-essential - -COPY . /app -WORKDIR /app -RUN cargo build --release - -FROM debian:bookworm-slim -RUN apt-get update && apt-get install -o Dpkg::Options::=--force-confdef -yq --no-install-recommends \ - postgresql-client \ - # Clean up layer - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \ - && truncate -s 0 /var/log/*log -COPY --from=builder /app/target/release/pgcat /usr/bin/pgcat -COPY --from=builder /app/pgcat.toml /etc/pgcat/pgcat.toml -WORKDIR /etc/pgcat -ENV RUST_LOG=info -CMD ["pgcat"] -STOPSIGNAL SIGINT diff --git a/Dockerfile.ci b/Dockerfile.ci deleted file mode 100644 index 565418d..0000000 --- a/Dockerfile.ci +++ /dev/null @@ -1,17 +0,0 @@ -FROM cimg/rust:1.79.0 -COPY --from=sclevine/yj /bin/yj /bin/yj -RUN /bin/yj -h -RUN sudo apt-get update && \ - sudo apt-get install -y \ - psmisc postgresql-contrib-14 postgresql-client-14 libpq-dev \ - ruby ruby-dev python3 python3-pip \ - lcov llvm-11 iproute2 && \ - sudo apt-get upgrade curl && \ - cargo install cargo-binutils rustfilt && \ - rustup component add llvm-tools-preview && \ - pip3 install psycopg2 && sudo gem install bundler && \ - wget -O /tmp/toxiproxy-2.4.0.deb https://github.com/Shopify/toxiproxy/releases/download/v2.4.0/toxiproxy_2.4.0_linux_$(dpkg --print-architecture).deb && \ - sudo dpkg -i /tmp/toxiproxy-2.4.0.deb -RUN wget -O /tmp/go1.21.3.linux-$(dpkg --print-architecture).tar.gz https://go.dev/dl/go1.21.3.linux-$(dpkg --print-architecture).tar.gz && \ - sudo tar -C /usr/local -xzf /tmp/go1.21.3.linux-$(dpkg --print-architecture).tar.gz && \ - rm /tmp/go1.21.3.linux-$(dpkg --print-architecture).tar.gz diff --git a/Dockerfile.dev b/Dockerfile.dev deleted file mode 100644 index a4b8d0e..0000000 --- a/Dockerfile.dev +++ /dev/null @@ -1,25 +0,0 @@ -FROM lukemathwalker/cargo-chef:latest-rust-1 AS chef - -RUN apt-get update && \ - apt-get install -y build-essential - -WORKDIR /app - -FROM chef AS planner -COPY . . -RUN cargo chef prepare --recipe-path recipe.json - -FROM chef AS builder -COPY --from=planner /app/recipe.json recipe.json -# Build dependencies - this is the caching Docker layer! -RUN cargo chef cook --release --recipe-path recipe.json -# Build application -COPY . . -RUN cargo build - -FROM debian:bookworm-slim -COPY --from=builder /app/target/release/pgcat /usr/bin/pgcat -COPY --from=builder /app/pgcat.toml /etc/pgcat/pgcat.toml -WORKDIR /etc/pgcat -ENV RUST_LOG=info -CMD ["pgcat"] diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 57e9e38..0000000 --- a/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -Copyright (c) 2023 PgCat Contributors - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 21e6da7..62b754b 100644 --- a/README.md +++ b/README.md @@ -1,292 +1 @@ -## PgCat: Nextgen PostgreSQL Pooler - -[![CircleCI](https://circleci.com/gh/postgresml/pgcat/tree/main.svg?style=svg)](https://circleci.com/gh/postgresml/pgcat/tree/main) - - Join our Discord! - - -PostgreSQL pooler and proxy (like PgBouncer) with support for sharding, load balancing, failover and mirroring. - -## Features - -| **Feature** | **Status** | **Comments** | -|-------------|------------|--------------| -| Transaction pooling | **Stable** | Identical to PgBouncer with notable improvements for handling bad clients and abandoned transactions. | -| Session pooling | **Stable** | Identical to PgBouncer. | -| Multi-threaded runtime | **Stable** | Using Tokio asynchronous runtime, the pooler takes advantage of multicore machines. | -| Load balancing of read queries | **Stable** | Queries are automatically load balanced between replicas and the primary. | -| 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. | -| 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 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. - - - - - - - - - - - - -
- - - - - - - - - - - -
- - Instacart - - - - PostgresML - - - OneSignal -
- -Some features remain experimental and are being actively developed. They are optional and can be enabled through configuration. - -## Deployment - -See `Dockerfile` for example deployment using Docker. The pooler is configured to spawn 4 workers so 4 CPUs are recommended for optimal performance. That setting can be adjusted to spawn as many (or as little) workers as needed. - -A Docker image is available from `docker pull ghcr.io/postgresml/pgcat:latest`. See our [Github packages repository](https://github.com/postgresml/pgcat/pkgs/container/pgcat). - -For quick local example, use the Docker Compose environment provided: - -```bash -docker-compose up - -# In a new terminal: -PGPASSWORD=postgres psql -h 127.0.0.1 -p 6432 -U postgres -c 'SELECT 1' -``` - -### Config - -See **[Configuration](https://github.com/levkk/pgcat/blob/main/CONFIG.md)**. - -## Contributing - -The project is being actively developed and looking for additional contributors and production deployments. - -### Local development - -1. Install Rust (latest stable will work great). -2. `cargo build --release` (to get better benchmarks). -3. Change the config in `pgcat.toml` to fit your setup (optional given next step). -4. Install Postgres and run `psql -f tests/sharding/query_routing_setup.sql` (user/password may be required depending on your setup) -5. `RUST_LOG=info cargo run --release` You're ready to go! - -### Tests - -When making substantial modifications to the protocol implementation, make sure to test them with pgbench: - -``` -pgbench -i -h 127.0.0.1 -p 6432 && \ -pgbench -t 1000 -p 6432 -h 127.0.0.1 --protocol simple && \ -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 unit and integration tests. - -Run `cargo test` to run Rust unit tests. - -Run the following commands to run Ruby and Python integration tests: - -``` -cd tests/docker/ -docker compose up --exit-code-from main # This will also produce coverage report under ./cov/ -``` - -### Docker-based local development - -You can open a Docker development environment where you can debug tests easier. Run the following command to spin it up: - -``` -./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 container (and bundled gems) will be placed in `dev/cache` so they don't interfere with what you have on your machine. - -## Usage - -### Session mode -In session mode, a client talks to one server for the duration of the connection. Prepared statements, `SET`, and advisory locks are supported. In terms of supported features, there is very little if any difference between session mode and talking directly to the server. - -To use session mode, change `pool_mode = "session"`. - -### Transaction mode -In transaction mode, a client talks to one server for the duration of a single transaction; once it's over, the server is returned to the pool. Prepared statements, `SET`, and advisory locks are not supported; alternatives are to use `SET LOCAL` and `pg_advisory_xact_lock` which are scoped to the transaction. - -This mode is enabled by default. - -### Load balancing of read queries -All queries are load balanced against the configured servers using either the random or least open connections algorithms. The most straightforward configuration example would be to put this pooler in front of several replicas and let it load balance all queries. - -If the configuration includes a primary and replicas, the queries can be separated with the built-in query parser. The query parser, implemented with the `sqlparser` crate, will interpret the query and route all `SELECT` queries to a replica, while all other queries including explicit transactions will be routed to the primary. - -#### Query parser -The query parser will do its best to determine where the query should go, but sometimes that's not possible. In that case, the client can select which server it wants using this custom SQL syntax: - -```sql --- To talk to the primary for the duration of the next transaction: -SET SERVER ROLE TO 'primary'; - --- To talk to the replica for the duration of the next transaction: -SET SERVER ROLE TO 'replica'; - --- Let the query parser decide -SET SERVER ROLE TO 'auto'; - --- Pick any server at random -SET SERVER ROLE TO 'any'; - --- Reset to default configured settings -SET SERVER ROLE TO 'default'; -``` - -The setting will persist until it's changed again or the client disconnects. - -By default, all queries are routed to the first available server; `default_role` setting controls this behavior. - -### Failover -All servers are checked with a `;` (very fast) query before being given to a client. Additionally, the server health is monitored with every client query that it processes. If the server is not reachable, it will be banned and cannot serve any more transactions for the duration of the ban. The queries are routed to the remaining servers. If all servers become banned, the ban list is cleared: this is a safety precaution against false positives. The primary can never be banned. - -The ban time can be changed with `ban_time`. The default is 60 seconds. - -### Sharding -We use the `PARTITION BY HASH` hashing function, the same as used by Postgres for declarative partitioning. This allows to shard the database using Postgres partitions and place the partitions on different servers (shards). Both read and write queries can be routed to the shards using this pooler. - -#### Extended syntax -To route queries to a particular shard, we use this custom SQL syntax: - -```sql --- To talk to a shard explicitly -SET SHARD TO '1'; - --- To let the pooler choose based on a value -SET SHARDING KEY TO '1234'; -``` - -The active shard will last until it's changed again or the client disconnects. By default, the queries are routed to shard 0. - -For hash function implementation, see `src/sharding.rs` and `tests/sharding/partition_hash_test_setup.sql`. - - -##### ActiveRecord/Rails - -```ruby -class User < ActiveRecord::Base -end - -# Metadata will be fetched from shard 0 -ActiveRecord::Base.establish_connection - -# Grab a bunch of users from shard 1 -User.connection.execute "SET SHARD TO '1'" -User.take(10) - -# Using id as the sharding key -User.connection.execute "SET SHARDING KEY TO '1234'" -User.find_by_id(1234) - -# Using geographical sharding -User.connection.execute "SET SERVER ROLE TO 'primary'" -User.connection.execute "SET SHARDING KEY TO '85'" -User.create(name: "test user", email: "test@example.com", zone_id: 85) - -# Let the query parser figure out where the query should go. -# We are still on shard = hash(85) % shards. -User.connection.execute "SET SERVER ROLE TO 'auto'" -User.find_by_email("test@example.com") -``` - -##### Raw SQL - -```sql --- Grab a bunch of users from shard 1 -SET SHARD TO '1'; -SELECT * FROM users LIMT 10; - --- Find by id -SET SHARDING KEY TO '1234'; -SELECT * FROM USERS WHERE id = 1234; - --- Writing in a primary/replicas configuration. -SET SHARDING ROLE TO 'primary'; -SET SHARDING KEY TO '85'; -INSERT INTO users (name, email, zome_id) VALUES ('test user', 'test@example.com', 85); - -SET SERVER ROLE TO 'auto'; -- let the query router figure out where the query should go -SELECT * FROM users WHERE email = 'test@example.com'; -- shard setting lasts until set again; we are reading from the primary -``` - -#### With comments -Issuing queries to the pooler can cause additional latency. To reduce its impact, it's possible to include sharding information inside SQL comments sent via the query. This is reasonably easy to implement with ORMs like [ActiveRecord](https://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-annotate) and [SQLAlchemy](https://docs.sqlalchemy.org/en/20/core/events.html#sql-execution-and-connection-events). - -``` -/* shard_id: 5 */ SELECT * FROM foo WHERE id = 1234; - -/* sharding_key: 1234 */ SELECT * FROM foo WHERE id = 1234; -``` - -#### Automatic query parsing -PgCat can use the `sqlparser` crate to parse SQL queries and extract the sharding key. This is configurable with the `automatic_sharding_key` setting. This feature is still experimental, but it's the ideal implementation for sharding, requiring no client modifications. - -### Statistics reporting - -The stats are very similar to what PgBouncer reports and the names are kept to be comparable. They are accessible by querying the admin database `pgcat`, and `pgbouncer` for compatibility. - -``` -psql -h 127.0.0.1 -p 6432 -d pgbouncer -c 'SHOW DATABASES' -``` - -Additionally, Prometheus statistics are available at `/metrics` via HTTP. - -We also have a [basic Grafana dashboard](https://github.com/postgresml/pgcat/blob/main/grafana_dashboard.json) based on Prometheus metrics that you can import into Grafana and build on it or use it for monitoring. - -### Live configuration reloading - -The config can be reloaded by sending a `kill -s SIGHUP` to the process or by querying `RELOAD` to the admin database. All settings except the `host` and `port` can be reloaded without restarting the pooler, including sharding and replicas configurations. - -### Mirroring - -Mirroring allows to route queries to multiple databases at the same time. This is useful for prewarning replicas before placing them into the active configuration, or for testing different versions of Postgres with live traffic. - -## License - -PgCat is free and open source, released under the MIT license. - -## Contributors - -Many thanks to our amazing contributors! - - - - - +## PgCat: Nextgen PostgreSQL Pooler Helm Charts diff --git a/charts/pgcat/.helmignore b/charts/pgcat/.helmignore deleted file mode 100644 index 0e8a0eb..0000000 --- a/charts/pgcat/.helmignore +++ /dev/null @@ -1,23 +0,0 @@ -# Patterns to ignore when building packages. -# This supports shell glob matching, relative path matching, and -# negation (prefixed with !). Only one pattern per line. -.DS_Store -# Common VCS dirs -.git/ -.gitignore -.bzr/ -.bzrignore -.hg/ -.hgignore -.svn/ -# Common backup files -*.swp -*.bak -*.tmp -*.orig -*~ -# Various IDEs -.project -.idea/ -*.tmproj -.vscode/ diff --git a/charts/pgcat/Chart.yaml b/charts/pgcat/Chart.yaml deleted file mode 100644 index 49df3e1..0000000 --- a/charts/pgcat/Chart.yaml +++ /dev/null @@ -1,8 +0,0 @@ -apiVersion: v2 -name: pgcat -description: A Helm chart for PgCat a PostgreSQL pooler and proxy (like PgBouncer) with support for sharding, load balancing, failover and mirroring. -maintainers: - - name: Wildcard - email: support@w6d.io -appVersion: "1.2.0" -version: 0.2.1 diff --git a/charts/pgcat/templates/NOTES.txt b/charts/pgcat/templates/NOTES.txt deleted file mode 100644 index 552645b..0000000 --- a/charts/pgcat/templates/NOTES.txt +++ /dev/null @@ -1,22 +0,0 @@ -1. Get the application URL by running these commands: -{{- if .Values.ingress.enabled }} -{{- range $host := .Values.ingress.hosts }} - {{- range .paths }} - http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} - {{- end }} -{{- end }} -{{- else if contains "NodePort" .Values.service.type }} - export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "pgcat.fullname" . }}) - export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") - echo http://$NODE_IP:$NODE_PORT -{{- else if contains "LoadBalancer" .Values.service.type }} - NOTE: It may take a few minutes for the LoadBalancer IP to be available. - You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "pgcat.fullname" . }}' - export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "pgcat.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") - echo http://$SERVICE_IP:{{ .Values.service.port }} -{{- else if contains "ClusterIP" .Values.service.type }} - export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "pgcat.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") - export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") - echo "Visit http://127.0.0.1:8080 to use your application" - kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT -{{- end }} diff --git a/charts/pgcat/templates/_config.tpl b/charts/pgcat/templates/_config.tpl deleted file mode 100644 index 09423a1..0000000 --- a/charts/pgcat/templates/_config.tpl +++ /dev/null @@ -1,3 +0,0 @@ -{{/* - Configuration template definition -*/}} diff --git a/charts/pgcat/templates/_helpers.tpl b/charts/pgcat/templates/_helpers.tpl deleted file mode 100644 index 07c2d25..0000000 --- a/charts/pgcat/templates/_helpers.tpl +++ /dev/null @@ -1,62 +0,0 @@ -{{/* -Expand the name of the chart. -*/}} -{{- define "pgcat.name" -}} -{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Create a default fully qualified app name. -We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). -If release name contains chart name it will be used as a full name. -*/}} -{{- define "pgcat.fullname" -}} -{{- if .Values.fullnameOverride }} -{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- $name := default .Chart.Name .Values.nameOverride }} -{{- if contains $name .Release.Name }} -{{- .Release.Name | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} -{{- end }} -{{- end }} -{{- end }} - -{{/* -Create chart name and version as used by the chart label. -*/}} -{{- define "pgcat.chart" -}} -{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Common labels -*/}} -{{- define "pgcat.labels" -}} -helm.sh/chart: {{ include "pgcat.chart" . }} -{{ include "pgcat.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} -{{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} -{{- end }} - -{{/* -Selector labels -*/}} -{{- define "pgcat.selectorLabels" -}} -app.kubernetes.io/name: {{ include "pgcat.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -{{- end }} - -{{/* -Create the name of the service account to use -*/}} -{{- define "pgcat.serviceAccountName" -}} -{{- if .Values.serviceAccount.create }} -{{- default (include "pgcat.fullname" .) .Values.serviceAccount.name }} -{{- else }} -{{- default "default" .Values.serviceAccount.name }} -{{- end }} -{{- end }} diff --git a/charts/pgcat/templates/deployment.yaml b/charts/pgcat/templates/deployment.yaml deleted file mode 100644 index 84c57f1..0000000 --- a/charts/pgcat/templates/deployment.yaml +++ /dev/null @@ -1,66 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ include "pgcat.fullname" . }} - labels: - {{- include "pgcat.labels" . | nindent 4 }} -spec: - replicas: {{ .Values.replicaCount }} - selector: - matchLabels: - {{- include "pgcat.selectorLabels" . | nindent 6 }} - template: - metadata: - annotations: - checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} - {{- with .Values.podAnnotations }} - {{- toYaml . | nindent 8 }} - {{- end }} - labels: - {{- include "pgcat.selectorLabels" . | nindent 8 }} - spec: - {{- with .Values.image.pullSecrets }} - imagePullSecrets: - {{- toYaml . | nindent 8 }} - {{- end }} - serviceAccountName: {{ include "pgcat.serviceAccountName" . }} - securityContext: - {{- toYaml .Values.podSecurityContext | nindent 8 }} - containers: - - name: {{ .Chart.Name }} - securityContext: - {{- toYaml .Values.containerSecurityContext | nindent 12 }} - image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" - imagePullPolicy: {{ .Values.image.pullPolicy }} - ports: - - name: pgcat - containerPort: {{ .Values.configuration.general.port }} - protocol: TCP - livenessProbe: - tcpSocket: - port: pgcat - readinessProbe: - tcpSocket: - port: pgcat - resources: - {{- toYaml .Values.resources | nindent 12 }} - volumeMounts: - - mountPath: /etc/pgcat - name: config - {{- with .Values.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.affinity }} - affinity: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.tolerations }} - tolerations: - {{- toYaml . | nindent 8 }} - {{- end }} - volumes: - - secret: - defaultMode: 420 - secretName: {{ include "pgcat.fullname" . }} - name: config diff --git a/charts/pgcat/templates/ingress.yaml b/charts/pgcat/templates/ingress.yaml deleted file mode 100644 index 1b1a5b3..0000000 --- a/charts/pgcat/templates/ingress.yaml +++ /dev/null @@ -1,61 +0,0 @@ -{{- if .Values.ingress.enabled -}} -{{- $fullName := include "pgcat.fullname" . -}} -{{- $svcPort := .Values.service.port -}} -{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} - {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} - {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} - {{- end }} -{{- end }} -{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} -apiVersion: networking.k8s.io/v1 -{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} -apiVersion: networking.k8s.io/v1beta1 -{{- else -}} -apiVersion: extensions/v1beta1 -{{- end }} -kind: Ingress -metadata: - name: {{ $fullName }} - labels: - {{- include "pgcat.labels" . | nindent 4 }} - {{- with .Values.ingress.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -spec: - {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} - ingressClassName: {{ .Values.ingress.className }} - {{- end }} - {{- if .Values.ingress.tls }} - tls: - {{- range .Values.ingress.tls }} - - hosts: - {{- range .hosts }} - - {{ . | quote }} - {{- end }} - secretName: {{ .secretName }} - {{- end }} - {{- end }} - rules: - {{- range .Values.ingress.hosts }} - - host: {{ .host | quote }} - http: - paths: - {{- range .paths }} - - path: {{ .path }} - {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} - pathType: {{ .pathType }} - {{- end }} - backend: - {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} - service: - name: {{ $fullName }} - port: - number: {{ $svcPort }} - {{- else }} - serviceName: {{ $fullName }} - servicePort: {{ $svcPort }} - {{- end }} - {{- end }} - {{- end }} -{{- end }} diff --git a/charts/pgcat/templates/secret.yaml b/charts/pgcat/templates/secret.yaml deleted file mode 100644 index 358f4e9..0000000 --- a/charts/pgcat/templates/secret.yaml +++ /dev/null @@ -1,97 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: {{ include "pgcat.fullname" . }} - labels: - {{- include "pgcat.labels" . | nindent 4 }} -type: Opaque -stringData: - pgcat.toml: | - [general] - host = {{ .Values.configuration.general.host | quote }} - port = {{ .Values.configuration.general.port }} - enable_prometheus_exporter = {{ .Values.configuration.general.enable_prometheus_exporter }} - prometheus_exporter_port = {{ .Values.configuration.general.prometheus_exporter_port }} - connect_timeout = {{ .Values.configuration.general.connect_timeout }} - idle_timeout = {{ .Values.configuration.general.idle_timeout | int }} - server_lifetime = {{ .Values.configuration.general.server_lifetime | int }} - server_tls = {{ .Values.configuration.general.server_tls }} - idle_client_in_transaction_timeout = {{ .Values.configuration.general.idle_client_in_transaction_timeout | int }} - healthcheck_timeout = {{ .Values.configuration.general.healthcheck_timeout }} - healthcheck_delay = {{ .Values.configuration.general.healthcheck_delay }} - shutdown_timeout = {{ .Values.configuration.general.shutdown_timeout }} - ban_time = {{ .Values.configuration.general.ban_time }} - log_client_connections = {{ .Values.configuration.general.log_client_connections }} - log_client_disconnections = {{ .Values.configuration.general.log_client_disconnections }} - tcp_keepalives_idle = {{ .Values.configuration.general.tcp_keepalives_idle }} - tcp_keepalives_count = {{ .Values.configuration.general.tcp_keepalives_count }} - tcp_keepalives_interval = {{ .Values.configuration.general.tcp_keepalives_interval }} - {{- if and (ne .Values.configuration.general.tls_certificate "-") (ne .Values.configuration.general.tls_private_key "-") }} - tls_certificate = "{{ .Values.configuration.general.tls_certificate }}" - tls_private_key = "{{ .Values.configuration.general.tls_private_key }}" - {{- end }} - admin_username = {{ .Values.configuration.general.admin_username | quote }} - admin_password = {{ .Values.configuration.general.admin_password | quote }} - {{- if and .Values.configuration.general.auth_query_user .Values.configuration.general.auth_query_password .Values.configuration.general.auth_query }} - auth_query = {{ .Values.configuration.general.auth_query | quote }} - auth_query_user = {{ .Values.configuration.general.auth_query_user | quote }} - auth_query_password = {{ .Values.configuration.general.auth_query_password | quote }} - {{- end }} - - {{- range $pool := .Values.configuration.pools }} - - ## - ## pool for {{ $pool.name }} - ## - [pools.{{ $pool.name | quote }}] - pool_mode = {{ default "transaction" $pool.pool_mode | quote }} - load_balancing_mode = {{ default "random" $pool.load_balancing_mode | quote }} - default_role = {{ default "any" $pool.default_role | quote }} - prepared_statements_cache_size = {{ default 500 $pool.prepared_statements_cache_size }} - query_parser_enabled = {{ default true $pool.query_parser_enabled }} - query_parser_read_write_splitting = {{ default true $pool.query_parser_read_write_splitting }} - primary_reads_enabled = {{ default true $pool.primary_reads_enabled }} - sharding_function = {{ default "pg_bigint_hash" $pool.sharding_function | quote }} - - {{- range $index, $user := $pool.users }} - - ## pool {{ $pool.name }} user {{ $user.username | quote }} - ## - [pools.{{ $pool.name | quote }}.users.{{ $index }}] - username = {{ $user.username | quote }} - {{- if $user.password }} - password = {{ $user.password | quote }} - {{- else if and $user.passwordSecret.name $user.passwordSecret.key }} - {{- $secret := (lookup "v1" "Secret" $.Release.Namespace $user.passwordSecret.name) }} - {{- if $secret }} - {{- $password := index $secret.data $user.passwordSecret.key | b64dec }} - password = {{ $password | quote }} - {{- end }} - {{- end }} - pool_size = {{ $user.pool_size }} - statement_timeout = {{ default 0 $user.statement_timeout }} - min_pool_size = {{ default 3 $user.min_pool_size }} - {{- if $user.server_lifetime }} - server_lifetime = {{ $user.server_lifetime }} - {{- end }} - {{- if and $user.server_username $user.server_password }} - server_username = {{ $user.server_username | quote }} - server_password = {{ $user.server_password | quote }} - {{- end }} - {{- end }} - - {{- range $index, $shard := $pool.shards }} - - ## pool {{ $pool.name }} database {{ $shard.database }} - ## - [pools.{{ $pool.name | quote }}.shards.{{ $index }}] - {{- if gt (len $shard.servers) 0}} - servers = [ - {{- range $server := $shard.servers }} - [ {{ $server.host | quote }}, {{ $server.port }}, {{ $server.role | quote }} ], - {{- end }} - ] - {{- end }} - database = {{ $shard.database | quote }} - {{- end }} - {{- end }} diff --git a/charts/pgcat/templates/service.yaml b/charts/pgcat/templates/service.yaml deleted file mode 100644 index 56c4be7..0000000 --- a/charts/pgcat/templates/service.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: {{ include "pgcat.fullname" . }} - labels: - {{- include "pgcat.labels" . | nindent 4 }} -spec: - type: {{ .Values.service.type }} - ports: - - port: {{ .Values.service.port }} - targetPort: pgcat - protocol: TCP - name: pgcat - selector: - {{- include "pgcat.selectorLabels" . | nindent 4 }} diff --git a/charts/pgcat/templates/serviceaccount.yaml b/charts/pgcat/templates/serviceaccount.yaml deleted file mode 100644 index 04ea616..0000000 --- a/charts/pgcat/templates/serviceaccount.yaml +++ /dev/null @@ -1,12 +0,0 @@ -{{- if .Values.serviceAccount.create -}} -apiVersion: v1 -kind: ServiceAccount -metadata: - name: {{ include "pgcat.serviceAccountName" . }} - labels: - {{- include "pgcat.labels" . | nindent 4 }} - {{- with .Values.serviceAccount.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -{{- end }} diff --git a/charts/pgcat/values.yaml b/charts/pgcat/values.yaml deleted file mode 100644 index 20a4e27..0000000 --- a/charts/pgcat/values.yaml +++ /dev/null @@ -1,374 +0,0 @@ -## String to partially override aspnet-core.fullname template (will maintain the release name) -## @param nameOverride String to partially override common.names.fullname -## -nameOverride: "" - -## String to fully override aspnet-core.fullname template -## @param fullnameOverride String to fully override common.names.fullname -## -fullnameOverride: "" - -## Number of PgCat replicas to deploy -## @param replicaCount Number of PgCat replicas to deploy -replicaCount: 1 - -## Bitnami PgCat image version -## ref: https://hub.docker.com/r/bitnami/kubewatch/tags/ -## -## @param image.registry PgCat image registry -## @param image.repository PgCat image name -## @param image.tag PgCat image tag -## @param image.pullPolicy PgCat image tag -## @param image.pullSecrets Specify docker-registry secret names as an array -image: - repository: ghcr.io/postgresml/pgcat - # Overrides the image tag whose default is the chart appVersion. - tag: "main" - ## Specify a imagePullPolicy - ## Defaults to 'Always' if image tag is 'latest', else set to 'IfNotPresent' - ## ref: http://kubernetes.io/docs/user-guide/images/#pre-pulling-images - ## - pullPolicy: IfNotPresent - ## Optionally specify an array of imagePullSecrets. - ## Secrets must be manually created in the namespace. - ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ - ## Example: - ## pullSecrets: - ## - myRegistryKeySecretName - ## - pullSecrets: [] - -## Specifies whether a ServiceAccount should be created -## -## @param serviceAccount.create Enable the creation of a ServiceAccount for PgCat pods -## @param serviceAccount.name Name of the created ServiceAccount -## -serviceAccount: - ## Specifies whether a service account should be created - create: true - ## Annotations to add to the service account - annotations: {} - ## The name of the service account to use. - ## If not set and create is true, a name is generated using the fullname template - name: "" - -## Annotations for server pods. -## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ -## -## @param podAnnotations Annotations for PgCat pods -## -podAnnotations: {} - -## PgCat containers' SecurityContext -## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-pod -## -## @param podSecurityContext.enabled Enabled PgCat pods' Security Context -## @param podSecurityContext.fsGroup Set PgCat pod's Security Context fsGroup -## -podSecurityContext: {} - # fsGroup: 2000 - -## PgCat pods' Security Context -## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-container -## -## @param containerSecurityContext.enabled Enabled PgCat containers' Security Context -## @param containerSecurityContext.runAsUser Set PgCat container's Security Context runAsUser -## @param containerSecurityContext.runAsNonRoot Set PgCat container's Security Context runAsNonRoot -## -containerSecurityContext: {} - # capabilities: - # drop: - # - ALL - # readOnlyRootFilesystem: true - # runAsNonRoot: true - # runAsUser: 1000 - -## PgCat service -## -## @param service.type PgCat service type -## @param service.port PgCat service port -service: - type: ClusterIP - port: 6432 - -ingress: - enabled: false - className: "" - annotations: {} - # kubernetes.io/ingress.class: nginx - # kubernetes.io/tls-acme: "true" - hosts: - - host: chart-example.local - paths: - - path: / - pathType: ImplementationSpecific - tls: [] - # - secretName: chart-example-tls - # hosts: - # - chart-example.local - -## PgCat resource requests and limits -## ref: http://kubernetes.io/docs/user-guide/compute-resources/ -## -## @skip resources Optional description -## @disabled-param resources.limits The resources limits for the PgCat container -## @disabled-param resources.requests The requested resources for the PgCat container -## -resources: - # We usually recommend not to specify default resources and to leave this as a conscious - # choice for the user. This also increases chances charts run on environments with little - # resources, such as Minikube. If you do want to specify resources, uncomment the following - # lines, adjust them as necessary, and remove the curly braces after 'resources:'. - limits: {} - # cpu: 100m - # memory: 128Mi - requests: {} - # cpu: 100m - # memory: 128Mi - -## Node labels for pod assignment. Evaluated as a template. -## ref: https://kubernetes.io/docs/user-guide/node-selection/ -## -## @param nodeSelector Node labels for pod assignment -## -nodeSelector: {} - -## Tolerations for pod assignment. Evaluated as a template. -## ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ -## -## @param tolerations Tolerations for pod assignment -## -tolerations: [] - -## Affinity for pod assignment. Evaluated as a template. -## ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity -## Note: podAffinityPreset, podAntiAffinityPreset, and nodeAffinityPreset will be ignored when it's set -## -## @param affinity Affinity for pod assignment -## -affinity: {} - -## PgCat configuration -## @param configuration [object] -configuration: - ## General pooler settings - ## @param [object] - general: - ## @param configuration.general.host What IP to run on, 0.0.0.0 means accessible from everywhere. - host: "0.0.0.0" - - ## @param configuration.general.port Port to run on, same as PgBouncer used in this example. - port: 6432 - - ## @param configuration.general.enable_prometheus_exporter Whether to enable prometheus exporter or not. - enable_prometheus_exporter: false - - ## @param configuration.general.prometheus_exporter_port Port at which prometheus exporter listens on. - prometheus_exporter_port: 9930 - - # @param configuration.general.connect_timeout How long to wait before aborting a server connection (ms). - connect_timeout: 5000 - - # 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 - - # Whether to use TLS for server connections or not. - server_tls: false - - # How long a client is allowed to be idle while in a transaction (ms). - idle_client_in_transaction_timeout: 0 # milliseconds - - # @param configuration.general.healthcheck_timeout How much time to give `SELECT 1` health check query to return with a result (ms). - healthcheck_timeout: 1000 - - # @param configuration.general.healthcheck_delay How long to keep connection available for immediate re-use, without running a healthcheck query on it - healthcheck_delay: 30000 - - # @param configuration.general.shutdown_timeout How much time to give clients during shutdown before forcibly killing client connections (ms). - shutdown_timeout: 60000 - - # @param configuration.general.ban_time For how long to ban a server if it fails a health check (seconds). - ban_time: 60 # seconds - - # @param configuration.general.log_client_connections If we should log client connections - log_client_connections: false - - # @param configuration.general.log_client_disconnections If we should log client disconnections - log_client_disconnections: false - - # TLS - # tls_certificate: "server.cert" - # tls_private_key: "server.key" - tls_certificate: "-" - tls_private_key: "-" - - # Credentials to access the virtual administrative database (pgbouncer or pgcat) - # Connecting to that database allows running commands like `SHOW POOLS`, `SHOW DATABASES`, etc.. - admin_username: "postgres" - admin_password: "postgres" - - # 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: null - - # 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. - # - # @param configuration.general.auth_query_user - auth_query_user: null - - # 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. - # - # @param configuration.general.auth_query_password - auth_query_password: null - - # Number of seconds of connection idleness to wait before sending a keepalive packet to the server. - tcp_keepalives_idle: 5 - - # Number of unacknowledged keepalive packets allowed before giving up and closing the connection. - tcp_keepalives_count: 5 - - # Number of seconds between keepalive packets. - tcp_keepalives_interval: 5 - - ## pool - ## configs are structured as pool. - ## the pool_name is what clients use as database name when connecting - ## For the example below a client can connect using "postgres://sharding_user:sharding_user@pgcat_host:pgcat_port/sharded" - ## @param [object] - pools: - [{ - name: "simple", pool_mode: "transaction", - users: [{username: "user", password: "pass", pool_size: 5, statement_timeout: 0}], - shards: [{ - servers: [{host: "postgres", port: 5432, role: "primary"}], - database: "postgres" - }] - }] - # - ## default values - # ## - # ## - # ## - # name: "db" - - # ## Pool mode (see PgBouncer docs for more). - # ## session: one server connection per connected client - # ## transaction: one server connection per client transaction - # ## @param configuration.poolsPostgres.pool_mode - # pool_mode: "transaction" - - # ## Load balancing mode - # ## `random` selects the server at random - # ## `loc` selects the server with the least outstanding busy connections - # ## - # ## @param configuration.poolsPostgres.load_balancing_mode - # load_balancing_mode: "random" - - # ## Prepared statements cache size. - # ## TODO: update documentation - # ## - # ## @param configuration.poolsPostgres.prepared_statements_cache_size - # prepared_statements_cache_size: 500 - - # ## If the client doesn't specify, route traffic to - # ## this role by default. - # ## - # ## any: round-robin between primary and replicas, - # ## replica: round-robin between replicas only without touching the primary, - # ## primary: all queries go to the primary unless otherwise specified. - # ## @param configuration.poolsPostgres.default_role - # default_role: "any" - - # ## Query parser. If enabled, we'll attempt to parse - # ## every incoming query to determine if it's a read or a write. - # ## If it's a read query, we'll direct it to a replica. Otherwise, if it's a write, - # ## we'll direct it to the primary. - # ## @param configuration.poolsPostgres.query_parser_enabled - # query_parser_enabled: true - - # ## If the query parser is enabled and this setting is enabled, we'll attempt to - # ## infer the role from the query itself. - # ## @param configuration.poolsPostgres.query_parser_read_write_splitting - # query_parser_read_write_splitting: 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 explicitly selected with our custom protocol. - # ## @param configuration.poolsPostgres.primary_reads_enabled - # primary_reads_enabled: true - - # ## So what if you wanted to implement a different hashing function, - # ## or you've already built one and you want this pooler to use it? - # ## - # ## Current options: - # ## - # ## pg_bigint_hash: PARTITION BY HASH (Postgres hashing function) - # ## sha1: A hashing function based on SHA1 - # ## - # ## @param configuration.poolsPostgres.sharding_function - # sharding_function: "pg_bigint_hash" - - # ## Credentials for users that may connect to this cluster - # ## @param users [array] - # ## @param users[0].username Name of the env var (required) - # ## @param users[0].password Value for the env var (required) leave empty to use existing secret see passwordSecret.name and passwordSecret.key - # ## @param users[0].passwordSecret.name Name of the secret containing the password - # ## @param users[0].passwordSecret.key Key in the secret containing the password - # ## @param users[0].pool_size Maximum number of server connections that can be established for this user - # ## @param users[0].statement_timeout Maximum query duration. Dangerous, but protects against DBs that died in a non-obvious way. - # users: [] - # # - username: "user" - # # password: "pass" - # # - # # # 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. - # # statement_timeout: 0 - # # - # # # PostgreSQL username used to connect to the server. - # # server_username: "postgres - # # - # # # PostgreSQL password used to connect to the server. - # # server_password: "postgres - - # ## @param shards [array] - # ## @param shards[0].server[0].host Host for this shard - # ## @param shards[0].server[0].port Port for this shard - # ## @param shards[0].server[0].role Role for this shard - # shards: [] - # # [ host, port, role ] - # # - servers: - # # - host: "postgres" - # # port: 5432 - # # role: "primary" - # # - host: "postgres" - # # port: 5432 - # # role: "replica" - # # database: "postgres" - # # # [ host, port, role ] - # # - servers: - # # - host: "postgres" - # # port: 5432 - # # role: "primary" - # # - host: "postgres" - # # port: 5432 - # # role: "replica" - # # database: "postgres" - # # # [ host, port, role ] - # # - servers: - # # - host: "postgres" - # # port: 5432 - # # role: "primary" - # # - host: "postgres" - # # port: 5432 - # # role: "replica" - # # database: "postgres" diff --git a/control b/control deleted file mode 100644 index f7e5b50..0000000 --- a/control +++ /dev/null @@ -1,9 +0,0 @@ -Package: pgcat -Version: ${PACKAGE_VERSION} -Section: database -Priority: optional -Architecture: ${ARCH} -Maintainer: PostgresML -Homepage: https://postgresml.org -Description: PgCat - NextGen PostgreSQL Pooler - PostgreSQL pooler and proxy (like PgBouncer) with support for sharding, load balancing, failover and mirroring. diff --git a/cov-style.css b/cov-style.css deleted file mode 100644 index 681cf9e..0000000 --- a/cov-style.css +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Copyright 2021 Collabora, Ltd. - * - * Permission is hereby granted, free of charge, to any person obtaining - * a copy of this software and associated documentation files (the - * "Software"), to deal in the Software without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Software, and to - * permit persons to whom the Software is furnished to do so, subject to - * the following conditions: - * - * The above copyright notice and this permission notice (including the - * next paragraph) shall be included in all copies or substantial - * portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS - * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN - * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -body { - background-color: #f2f2f2; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, - "Noto Sans", Ubuntu, Cantarell, "Helvetica Neue", sans-serif, - "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", - "Noto Color Emoji"; -} - -.sourceHeading, .source, .coverFn, -.testName, .testPer, .testNum, -.coverLegendCovLo, .headerCovTableEntryLo, .coverPerLo, .coverNumLo, -.coverLegendCovMed, .headerCovTableEntryMed, .coverPerMed, .coverNumMed, -.coverLegendCovHi, .headerCovTableEntryHi, .coverPerHi, .coverNumHi, -.coverFile { - font-family: "Menlo", "DejaVu Sans Mono", "Liberation Mono", - "Consolas", "Ubuntu Mono", "Courier New", "andale mono", - "lucida console", monospace; -} - -pre { - font-size: 0.7875rem; -} - -.headerCovTableEntry, .testPer, .testNum, .testName, -.coverLegendCovLo, .headerCovTableEntryLo, .coverPerLo, .coverNumLo, -.coverLegendCovMed, .headerCovTableEntryMed, .coverPerMed, .coverNumMed, -.coverLegendCovHi, .headerCovTableEntryHi, .coverPerHi, .coverNumHi { - text-align: right; - white-space: nowrap; -} - -.coverPerLo, .coverPerMed, .coverPerHi, .testPer { -/* font-weight: bold;*/ -} - -.coverNumLo, .coverNumMed, .coverNumHi, .testNum { - font-style: italic; - font-size: 90%; - padding-left: 1em; -} - -.title { - font-size: 200%; -} - -.tableHead { - text-align: center; - font-weight: bold; - background-color: #bfbfbf; -} - -.coverFile, .coverBar, .coverFn { - background-color: #d9d9d9; -} - -.headerCovTableHead { - font-weight: bold; - text-align: right; -} - -.headerCovTableEntry { - background-color: #d9d9d9; -} - -.coverFnLo, -.coverLegendCovLo, .headerCovTableEntryLo, .coverPerLo, .coverNumLo { - background-color: #f2dada; -} - -.coverFnHi, -.coverLegendCovMed, .headerCovTableEntryMed, .coverPerMed, .coverNumMed { - background-color: #add9ad; -} - -.coverLegendCovHi, .headerCovTableEntryHi, .coverPerHi, .coverNumHi { - background-color: #59b359; -} - -.coverBarOutline { - border-style: solid; - border-width: 1px; - border-color: black; - padding: 0px; -} - -.coverFnLo, .coverFnHi { - text-align: right; -} - -.lineNum { - background-color: #d9d9d9; -} - -.coverLegendCov, .lineCov, .branchCov { - background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAABCAIAAABsYngUAAADAXpUWHRSYXcgcHJvZmlsZSB0eXBlIGV4aWYAAHjazZVbktwgDEX/WUWWgCSExHIwj6rsIMvPxcY9PY9MzVTyEVMNtCwkoYNwGL9+zvADDxHHkNQ8l5wjnlRS4YqJx+upZ08xnf313O/otTw8FBgzwShbP2/5gJyhz1vetp0KuT4ZKmO/OF6/qNsQ+3ZwO9yOhC4HcRsOdRsS3p7T9f+4thVzcXveQtv6sz5t1dfW0CUxzprJEvrE0SwXzJ1jMuStr0CPvhfqdvTmf7hVGTHxEJKI3leEsn4kFWNCT/CGfUnBXDEuyd4yaHGIhnm58/r581nk4Q59Y32N+p69Qc3xPelwJvRWkTeE8mP8UE76Ig/PSE9uT55z3jN+LZ/pJaibXLjxzdl9znHtrqaMLee9qXuL5wx6x8rWuSqjGX4afSV7tYLmKImGc9RxyA60RoUYGCcl6lRp0jjHRg0hJh4MjszcALcFCB0wCjcgJYBGo8kGzF0cB6DhOAik/IiFTrfldNfI4biTB5wegjHCkr9q4StKc66CIlq55CtXiItXwhHFIkeE6ocaiNDcSdUzwXd7+yyuAoJ6ptmxwRqPZQH4D6WXwyUnaIGiYrwKmKxvA0gRIlAEQwICMZMoZYrGHIwIiXQAqgidJfEBLKTKHUFyEsmAgyqAb6wxOlVZ+RLjIgQIlRzEwAaFCFgpKc6PJccZqiqaVDWrqWvRmiWvCsvZ8rpRq4klU8tm5lasBhdPrp7d3L14LVwEN64W1GPxUkqtcFphuWJ1hUKtBx9ypEOPfNjhRzlq49CkpaYtN2veSqudu3TUcc/duvfS66CBozTS0JGHDR9l1ImjNmWmqTNPmz5LmPVBbWN9175BjTY1PkktRXtQg9TsNkHrOtHFDMQ4EYDbIkASmBez6JQSL3KLWSyMqlBGkLrgdFrEQDANYp30YPdCToPkf8MtAAT/C3JhofsCuffcPqLW6/mhk5PQKsOV1CiovpHgnx3LcCvhwlnz9dF8P4Y/vfju+J8aQpZK+A373P3XzDqcKwAAAAZiS0dEAAAAAAAA+UO7fwAAAAlwSFlzAAAOxAAADsQBlSsOGwAAAAd0SU1FB+UEEQYyDQA04tUAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAADklEQVQI12PULVBlwAYAEagAxGHRDdwAAAAASUVORK5CYII='); - background-repeat: repeat-y; - background-position: left top; - background-color: #c6ffb8; -} - -.coverLegendNoCov, .lineNoCov, .branchNoCov, .branchNoExec { - background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAABCAIAAABsYngUAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAB3RJTUUH5QMUCiMidNgp2gAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAAPSURBVAjXY/wZIcWADQAAIa4BbZaExr0AAAAASUVORK5CYII='); - background-repeat: repeat-y; - background-position: left top; - background-color: #ffcfbb; -} - -.coverLegendCov, .coverLegendNoCov { - padding: 0em 1em 0em 1em; -} - -.headerItem, .headerValue, .headerValueLeg { - white-space: nowrap; -} - -.headerItem { - text-align: right; - font-weight: bold; -} - -.ruler { - background-color: #d9d9d9; -} - -.detail { - font-size: 80%; -} - -.versionInfo { - font-size: 80%; - text-align: right; -} - diff --git a/cr.yaml b/cr.yaml deleted file mode 100644 index 0e3201d..0000000 --- a/cr.yaml +++ /dev/null @@ -1 +0,0 @@ -sign: false diff --git a/ct.yaml b/ct.yaml deleted file mode 100644 index fe05590..0000000 --- a/ct.yaml +++ /dev/null @@ -1,5 +0,0 @@ -remote: origin -target-branch: main -chart-dirs: - - charts - diff --git a/dev/Dockerfile b/dev/Dockerfile deleted file mode 100644 index dc03867..0000000 --- a/dev/Dockerfile +++ /dev/null @@ -1,35 +0,0 @@ -FROM rust:bullseye - -# Dependencies -COPY --from=sclevine/yj /bin/yj /bin/yj -RUN /bin/yj -h -RUN apt-get update -y \ - && apt-get install -y \ - llvm-11 psmisc postgresql-contrib postgresql-client \ - ruby ruby-dev libpq-dev python3 python3-pip lcov curl sudo iproute2 \ - strace ngrep iproute2 dnsutils lsof net-tools telnet - -# Rust -RUN cargo install cargo-binutils rustfilt -RUN rustup component add llvm-tools-preview - -# Ruby -RUN sudo gem install bundler - -# Toxyproxy -RUN wget -O toxiproxy-2.4.0.deb https://github.com/Shopify/toxiproxy/releases/download/v2.4.0/toxiproxy_2.4.0_linux_$(dpkg --print-architecture).deb && \ - sudo dpkg -i toxiproxy-2.4.0.deb - -# Config -ENV APP_ROOT=/app -ARG APP_USER=pgcat -COPY dev_bashrc /etc/bash.bashrc - -RUN useradd -m -o -u 999 ${APP_USER} || exit 0 && mkdir ${APP_ROOT} && chown ${APP_USER} ${APP_ROOT} -RUN adduser ${APP_USER} sudo \ - && echo "${APP_USER} ALL=NOPASSWD: ALL" > /etc/sudoers.d/${APP_USER} \ - && chmod ugo+s /usr/sbin/usermod /usr/sbin/groupmod -ENV HOME=${APP_ROOT} -WORKDIR ${APP_ROOT} - -ENTRYPOINT ["/bin/bash"] diff --git a/dev/dev_bashrc b/dev/dev_bashrc deleted file mode 100644 index 25d6a03..0000000 --- a/dev/dev_bashrc +++ /dev/null @@ -1,120 +0,0 @@ -# ~/.bashrc: executed by bash(1) for non-login shells. -# see /usr/share/doc/bash/examples/startup-files (in the package bash-doc) -# for examples - -# FIX USER NEEDED SO WE CAN SHARE UID BETWEEN HOST AND DEV ENV -usermod -o -u $(id -u) pgcat -groupmod -o -g $(id -g) pgcat - -# We fix the setuid in those commands as we now have sudo -sudo chmod ugo-s /usr/sbin/usermod /usr/sbin/groupmod - -# Environment customization -export DEV_ROOT="${APP_ROOT}/dev" -export HISTFILE="${DEV_ROOT}/.bash_history" -export CARGO_TARGET_DIR="${DEV_ROOT}/cache/target" -export CARGO_HOME="${DEV_ROOT}/cache/target/.cargo" -export BUNDLE_PATH="${DEV_ROOT}/cache/bundle" - -# Regular bashrc -# If not running interactively, don't do anything -case $- in - *i*) ;; - *) return;; -esac - -# don't put duplicate lines or lines starting with space in the history. -# See bash(1) for more options -HISTCONTROL=ignoreboth - -# append to the history file, don't overwrite it -shopt -s histappend - -# for setting history length see HISTSIZE and HISTFILESIZE in bash(1) -HISTSIZE=1000 -HISTFILESIZE=2000 - -# check the window size after each command and, if necessary, -# update the values of LINES and COLUMNS. -shopt -s checkwinsize - -# If set, the pattern "**" used in a pathname expansion context will -# match all files and zero or more directories and subdirectories. -#shopt -s globstar - -# make less more friendly for non-text input files, see lesspipe(1) -[ -x /usr/bin/lesspipe ] && eval "$(SHELL=/bin/sh lesspipe)" - -# set variable identifying the chroot you work in (used in the prompt below) -if [ -z "${debian_chroot:-}" ] && [ -r /etc/debian_chroot ]; then - debian_chroot=$(cat /etc/debian_chroot) -fi - -# set a fancy prompt (non-color, unless we know we "want" color) -case "$TERM" in - xterm-color|*-256color) color_prompt=yes;; -esac - -# uncomment for a colored prompt, if the terminal has the capability; turned -# off by default to not distract the user: the focus in a terminal window -# should be on the output of commands, not on the prompt -#force_color_prompt=yes - -if [ -n "$force_color_prompt" ]; then - if [ -x /usr/bin/tput ] && tput setaf 1 >&/dev/null; then - # We have color support; assume it's compliant with Ecma-48 - # (ISO/IEC-6429). (Lack of such support is extremely rare, and such - # a case would tend to support setf rather than setaf.) - color_prompt=yes - else - color_prompt= - fi -fi - -PS1='\[\e]0;pgcat@dev-container\h: \w\a\]${debian_chroot:+($debian_chroot)}\[\033[01;32m\]pgcat\[\033[00m\]@\[\033[01;32m\]dev-container\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\[\033[01;31m\]$(git branch &>/dev/null; if [ $? -eq 0 ]; then echo " ($(git branch | grep ^* |sed s/\*\ //))"; fi)\[\033[00m\]\$ ' - -unset color_prompt force_color_prompt - -# enable color support of ls and also add handy aliases -if [ -x /usr/bin/dircolors ]; then - test -r ~/.dircolors && eval "$(dircolors -b ~/.dircolors)" || eval "$(dircolors -b)" - alias ls='ls --color=auto' - #alias dir='dir --color=auto' - #alias vdir='vdir --color=auto' - - alias grep='grep --color=auto' - alias fgrep='fgrep --color=auto' - alias egrep='egrep --color=auto' -fi - -# colored GCC warnings and errors -#export GCC_COLORS='error=01;31:warning=01;35:note=01;36:caret=01;32:locus=01:quote=01' - -# some more ls aliases -alias ll='ls -alF' -alias la='ls -A' -alias l='ls -CF' - -# Add an "alert" alias for long running commands. Use like so: -# sleep 10; alert -alias alert='notify-send --urgency=low -i "$([ $? = 0 ] && echo terminal || echo error)" "$(history|tail -n1|sed -e '\''s/^\s*[0-9]\+\s*//;s/[;&|]\s*alert$//'\'')"' - -# Alias definitions. -# You may want to put all your additions into a separate file like -# ~/.bash_aliases, instead of adding them here directly. -# See /usr/share/doc/bash-doc/examples in the bash-doc package. - -if [ -f ~/.bash_aliases ]; then - . ~/.bash_aliases -fi - -# enable programmable completion features (you don't need to enable -# this, if it's already enabled in /etc/bash.bashrc and /etc/profile -# sources /etc/bash.bashrc). -if ! shopt -oq posix; then - if [ -f /usr/share/bash-completion/bash_completion ]; then - . /usr/share/bash-completion/bash_completion - elif [ -f /etc/bash_completion ]; then - . /etc/bash_completion - fi -fi diff --git a/dev/docker-compose.yaml b/dev/docker-compose.yaml deleted file mode 100644 index 5fda0c1..0000000 --- a/dev/docker-compose.yaml +++ /dev/null @@ -1,94 +0,0 @@ -version: "3" - -x-common-definition-pg: - &common-definition-pg - image: postgres:14 - network_mode: "service:main" - healthcheck: - test: [ "CMD-SHELL", "pg_isready -U postgres -d postgres" ] - interval: 5s - timeout: 5s - retries: 5 - volumes: - - type: bind - source: ../tests/sharding/query_routing_setup.sql - target: /docker-entrypoint-initdb.d/query_routing_setup.sql - - type: bind - source: ../tests/sharding/partition_hash_test_setup.sql - target: /docker-entrypoint-initdb.d/partition_hash_test_setup.sql - -x-common-env-pg: - &common-env-pg - POSTGRES_USER: postgres - POSTGRES_DB: postgres - POSTGRES_PASSWORD: postgres - -services: - main: - image: gcr.io/google_containers/pause:3.2 - ports: - - 6432 - - pg1: - <<: *common-definition-pg - environment: - <<: *common-env-pg - POSTGRES_INITDB_ARGS: --auth-local=md5 --auth-host=md5 --auth=md5 - PGPORT: 5432 - command: ["postgres", "-p", "5432", "-c", "shared_preload_libraries=pg_stat_statements", "-c", "pg_stat_statements.track=all", "-c", "pg_stat_statements.max=100000"] - - pg2: - <<: *common-definition-pg - environment: - <<: *common-env-pg - POSTGRES_INITDB_ARGS: --auth-local=scram-sha-256 --auth-host=scram-sha-256 --auth=scram-sha-256 - PGPORT: 7432 - command: ["postgres", "-p", "7432", "-c", "shared_preload_libraries=pg_stat_statements", "-c", "pg_stat_statements.track=all", "-c", "pg_stat_statements.max=100000"] - pg3: - <<: *common-definition-pg - environment: - <<: *common-env-pg - POSTGRES_INITDB_ARGS: --auth-local=scram-sha-256 --auth-host=scram-sha-256 --auth=scram-sha-256 - PGPORT: 8432 - command: ["postgres", "-p", "8432", "-c", "shared_preload_libraries=pg_stat_statements", "-c", "pg_stat_statements.track=all", "-c", "pg_stat_statements.max=100000"] - pg4: - <<: *common-definition-pg - environment: - <<: *common-env-pg - 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: . - network_mode: "service:main" - container_name: toxiproxy - environment: - LOG_LEVEL: info - entrypoint: toxiproxy-server - depends_on: - - pg1 - - pg2 - - pg3 - - pg4 - - pg5 - - pgcat-shell: - stdin_open: true - user: "${HOST_UID}:${HOST_GID}" - build: . - network_mode: "service:main" - depends_on: - - toxiproxy - volumes: - - ../:/app/ - entrypoint: - - /bin/bash - - -i diff --git a/dev/script/console b/dev/script/console deleted file mode 100755 index 27715d1..0000000 --- a/dev/script/console +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -export HOST_UID="$(id -u)" -export HOST_GID="$(id -g)" - -if [[ "${1}" == "down" ]]; then - docker-compose -f "${DIR}/../docker-compose.yaml" down - exit 0 -else - docker-compose -f "${DIR}/../docker-compose.yaml" run --rm pgcat-shell -fi diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 96d1f39..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,17 +0,0 @@ -version: "3" -services: - postgres: - image: postgres:14 - environment: - POSTGRES_PASSWORD: postgres - POSTGRES_HOST_AUTH_METHOD: md5 - pgcat: - build: . - command: - - "pgcat" - - "/etc/pgcat/pgcat.toml" - volumes: - - "${PWD}/examples/docker/pgcat.toml:/etc/pgcat/pgcat.toml" - ports: - - "6432:6432" - - "9930:9930" diff --git a/examples/docker/pgcat.toml b/examples/docker/pgcat.toml deleted file mode 100644 index cfd94a1..0000000 --- a/examples/docker/pgcat.toml +++ /dev/null @@ -1,127 +0,0 @@ -# -# PgCat config example. -# - -# -# General pooler settings -[general] -# What IP to run on, 0.0.0.0 means accessible from everywhere. -host = "0.0.0.0" - -# Port to run on, same as PgBouncer used in this example. -port = 6432 - -# Whether to enable prometheus exporter or not. -enable_prometheus_exporter = true - -# Port at which prometheus exporter listens on. -prometheus_exporter_port = 9930 - -# How long to wait before aborting a server connection (ms). -connect_timeout = 5000 - -# How much time to give `SELECT 1` health check query to return with a result (ms). -healthcheck_timeout = 1000 - -# How long to keep connection available for immediate re-use, without running a healthcheck query on it -healthcheck_delay = 30000 - -# How much time to give clients during shutdown before forcibly killing client connections (ms). -shutdown_timeout = 60000 - -# For how long to ban a server if it fails a health check (seconds). -ban_time = 60 # seconds - -# If we should log client connections -log_client_connections = false - -# If we should log client disconnections -log_client_disconnections = false - -# TLS -# tls_certificate = "server.cert" -# tls_private_key = "server.key" - -# Credentials to access the virtual administrative database (pgbouncer or pgcat) -# Connecting to that database allows running commands like `SHOW POOLS`, `SHOW DATABASES`, etc.. -admin_username = "postgres" -admin_password = "postgres" - -# pool -# configs are structured as pool. -# the pool_name is what clients use as database name when connecting -# For the example below a client can connect using "postgres://sharding_user:sharding_user@pgcat_host:pgcat_port/sharded" -[pools.postgres] -# Pool mode (see PgBouncer docs for more). -# session: one server connection per connected client -# transaction: one server connection per client transaction -pool_mode = "transaction" - -# If the client doesn't specify, route traffic to -# this role by default. -# -# any: round-robin between primary and replicas, -# replica: round-robin between replicas only without touching the primary, -# primary: all queries go to the primary unless otherwise specified. -default_role = "any" - -# Query parser. If enabled, we'll attempt to parse -# every incoming query to determine if it's a read or a write. -# If it's a read query, we'll direct it to a replica. Otherwise, if it's a write, -# we'll direct it to the primary. -query_parser_enabled = true - -# If the query parser is enabled and this setting is enabled, we'll attempt to -# infer the role from the query itself. -query_parser_read_write_splitting = 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 explicitly selected with our custom protocol. -primary_reads_enabled = true - -# So what if you wanted to implement a different hashing function, -# or you've already built one and you want this pooler to use it? -# -# Current options: -# -# pg_bigint_hash: PARTITION BY HASH (Postgres hashing function) -# sha1: A hashing function based on SHA1 -# -sharding_function = "pg_bigint_hash" - -# Credentials for users that may connect to this cluster -[pools.postgres.users.0] -username = "postgres" -password = "postgres" -# 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. -statement_timeout = 0 - -# Shard 0 -[pools.postgres.shards.0] -# [ host, port, role ] -servers = [ - [ "postgres", 5432, "primary" ], - [ "postgres", 5432, "replica" ] -] -# Database name (e.g. "postgres") -database = "postgres" - -[pools.postgres.shards.1] -servers = [ - [ "postgres", 5432, "primary" ], - [ "postgres", 5432, "replica" ], -] -database = "postgres" - -[pools.postgres.shards.2] -servers = [ - [ "postgres", 5432, "primary" ], - [ "postgres", 5432, "replica" ], -] -database = "postgres" diff --git a/grafana_dashboard.json b/grafana_dashboard.json deleted file mode 100644 index a5f7f83..0000000 --- a/grafana_dashboard.json +++ /dev/null @@ -1,2124 +0,0 @@ -{ - "annotations": { - "list": [ - { - "builtIn": 1, - "datasource": { - "type": "grafana", - "uid": "-- Grafana --" - }, - "enable": true, - "hide": true, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations & Alerts", - "type": "dashboard" - } - ] - }, - "description": "A dashboard to monitor PgCat deployments. It is based on the metrics exported by the Prometheus exporter that comes bundled with PgCat\n\nPlease visit https://github.com/postgresml/pgcat for more information ", - "editable": true, - "fiscalYearStartMonth": 0, - "graphTooltip": 1, - "id": 4, - "links": [], - "panels": [ - { - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 0 - }, - "id": 39, - "title": "Throughput Metrics", - "type": "row" - }, - { - "datasource": { - "type": "prometheus", - "uid": "fdwe81suzec5ce" - }, - "description": "For this metric, each individual query outside a transaction is counted as one transaction. All queries that run inside a transaction are counted as one transaction.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 100, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "smooth", - "lineWidth": 1, - "pointSize": 3, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "always", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "normal" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 6, - "w": 6, - "x": 0, - "y": 1 - }, - "id": 4, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": false - }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "fdwe81suzec5ce" - }, - "disableTextWrap": false, - "editorMode": "builder", - "exemplar": false, - "expr": "increase(pgcat_servers_transaction_count{pool=~\"$pool\", role=~\"$role\", shard=~\"$shard_id\", index=~\"$instance_index\", host=~\"$host\", user=~\"$user\"}[1m])", - "fullMetaSearch": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "host:{{host}}, identifier:{{role}}{{index}}, shard_id:{{shard}}, pool:{{pool}}", - "range": true, - "refId": "A", - "useBackend": false - } - ], - "title": "Transaction Count", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "fdwe81suzec5ce" - }, - "description": "The number of individual queries run", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 100, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "smooth", - "lineWidth": 1, - "pointSize": 3, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "always", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "normal" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 6, - "w": 6, - "x": 6, - "y": 1 - }, - "id": 41, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": false - }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "fdwe81suzec5ce" - }, - "disableTextWrap": false, - "editorMode": "builder", - "exemplar": false, - "expr": "increase(pgcat_servers_query_count{pool=~\"$pool\", role=~\"$role\", shard=~\"$shard_id\", index=~\"$instance_index\", host=~\"$host\", user=~\"$user\"}[1m])", - "fullMetaSearch": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "host:{{host}}, identifier:{{role}}{{index}}, shard_id:{{shard}}, pool:{{pool}}", - "range": true, - "refId": "A", - "useBackend": false - } - ], - "title": "Query Count", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "fdwe81suzec5ce" - }, - "description": "Average Query Latency", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "smooth", - "lineWidth": 1, - "pointSize": 2, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "always", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "ms" - }, - "overrides": [] - }, - "gridPos": { - "h": 6, - "w": 6, - "x": 12, - "y": 1 - }, - "id": 38, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": false - }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "fdwe81suzec5ce" - }, - "disableTextWrap": false, - "editorMode": "builder", - "exemplar": false, - "expr": "pgcat_stats_avg_query_time{pool=~\"$pool\", role=~\"$role\", shard=~\"$shard_id\", index=~\"$instance_index\", host=~\"$host\", user=~\"$user\"}", - "fullMetaSearch": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "host:{{host}}, identifier:{{role}}{{index}}, shard_id:{{shard}}, pool:{{pool}}", - "range": true, - "refId": "A", - "useBackend": false - } - ], - "title": "Average Query Latency", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "fdwe81suzec5ce" - }, - "description": "Average latency of transactions", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "smooth", - "lineWidth": 1, - "pointSize": 2, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "always", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "ms" - }, - "overrides": [] - }, - "gridPos": { - "h": 6, - "w": 6, - "x": 18, - "y": 1 - }, - "id": 42, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": false - }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "fdwe81suzec5ce" - }, - "disableTextWrap": false, - "editorMode": "builder", - "exemplar": false, - "expr": "pgcat_stats_avg_xact_time{pool=~\"$pool\", role=~\"$role\", shard=~\"$shard_id\", index=~\"$instance_index\", host=~\"$host\", user=~\"$user\"}", - "fullMetaSearch": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "host:{{host}}, identifier:{{role}}{{index}}, shard_id:{{shard}}, pool:{{pool}}", - "range": true, - "refId": "A", - "useBackend": false - } - ], - "title": "Average Transaction Latency", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "fdwe81suzec5ce" - }, - "description": "Data received in bytes", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 100, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "smooth", - "lineWidth": 1, - "pointSize": 3, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "always", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "normal" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "decbytes" - }, - "overrides": [] - }, - "gridPos": { - "h": 6, - "w": 12, - "x": 0, - "y": 7 - }, - "id": 40, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": false - }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "fdwe81suzec5ce" - }, - "disableTextWrap": false, - "editorMode": "builder", - "exemplar": false, - "expr": "increase(pgcat_servers_bytes_received{pool=~\"$pool\", role=~\"$role\", shard=~\"$shard_id\", index=~\"$instance_index\", host=~\"$host\", user=~\"$user\"}[1m])", - "fullMetaSearch": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "host:{{host}}, identifier:{{role}}{{index}}, shard_id:{{shard}}, pool:{{pool}}", - "range": true, - "refId": "A", - "useBackend": false - } - ], - "title": "Data received", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "fdwe81suzec5ce" - }, - "description": "Data sent in bytes", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 100, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "smooth", - "lineWidth": 1, - "pointSize": 3, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "always", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "normal" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "decbytes" - }, - "overrides": [] - }, - "gridPos": { - "h": 6, - "w": 12, - "x": 12, - "y": 7 - }, - "id": 10, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": false - }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "fdwe81suzec5ce" - }, - "disableTextWrap": false, - "editorMode": "builder", - "exemplar": false, - "expr": "increase(pgcat_servers_bytes_sent{pool=~\"$pool\", role=~\"$role\", shard=~\"$shard_id\", index=~\"$instance_index\", host=~\"$host\", user=~\"$user\"}[1m])", - "fullMetaSearch": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "host:{{host}}, identifier:{{role}}{{index}}, shard_id:{{shard}}, pool:{{pool}}", - "range": true, - "refId": "A", - "useBackend": false - } - ], - "title": "Data Sent", - "type": "timeseries" - }, - { - "collapsed": false, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 13 - }, - "id": 22, - "panels": [], - "title": "Capacity metrics", - "type": "row" - }, - { - "datasource": { - "type": "prometheus", - "uid": "fdwe81suzec5ce" - }, - "description": "This is the ratio between the number of active server connections to the pool size. Persistently high ratio (e.g. 80%-100%) may suggest the need for a larger pool or a more performant Database", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "smooth", - "lineWidth": 1, - "pointSize": 3, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "always", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "area" - } - }, - "mappings": [], - "max": 150, - "min": 0, - "thresholds": { - "mode": "percentage", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 60 - } - ] - }, - "unit": "percent" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 13, - "x": 0, - "y": 14 - }, - "id": 30, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": false - }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "fdwe81suzec5ce" - }, - "disableTextWrap": false, - "editorMode": "code", - "exemplar": false, - "expr": "(pgcat_servers_active_count{pool=~\"$pool\", role=~\"$role\", shard=~\"$shard_id\", index=~\"$instance_index\", host=~\"$host\", user=~\"$user\"} / pgcat_databases_pool_size{pool=~\"$pool\", role=~\"$role\", shard=~\"$shard_id\", index=~\"$instance_index\", host=~\"$host\", user=~\"$user\"}) * 100 ", - "fullMetaSearch": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "host:{{host}}, identifier:{{role}}{{index}}, shard_id:{{shard}}, pool:{{pool}}, user:{{user}}", - "range": true, - "refId": "A", - "useBackend": false - } - ], - "title": "Percentage Server Pool Utilization", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "fdwe81suzec5ce" - }, - "description": "The number of Clients currently waiting in the queue waiting for a server connection to be assigned. This should remain close to 0. Any persistent deviation from zero means clients are blocked from querying the database", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "bars", - "fillOpacity": 100, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "smooth", - "lineStyle": { - "fill": "solid" - }, - "lineWidth": 1, - "pointSize": 2, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "always", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "normal" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 11, - "x": 13, - "y": 14 - }, - "id": 31, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "fdwe81suzec5ce" - }, - "disableTextWrap": false, - "editorMode": "builder", - "exemplar": false, - "expr": "pgcat_pools_cl_waiting{pool=~\"$pool\", user=~\"$user\"}", - "fullMetaSearch": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "Pool:{{pool}}, User:{{user}}", - "range": true, - "refId": "A", - "useBackend": false - } - ], - "title": "Waiting Clients", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "fdwe81suzec5ce" - }, - "description": "Banned connections won't get queries routed to them until they are unbanned. Instances are unbanned after the ban timer expires or if all replicas in the pool are banned so they are all unbanned as a failsafe. Primary instances are never banned", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "bars", - "fillOpacity": 50, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "smooth", - "lineWidth": 1, - "pointSize": 2, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "always", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "normal" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 7, - "w": 8, - "x": 0, - "y": 22 - }, - "id": 34, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": false - }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "fdwe81suzec5ce" - }, - "disableTextWrap": false, - "editorMode": "builder", - "exemplar": false, - "expr": "pgcat_servers_is_banned{pool=~\"$pool\", role=~\"$role\", shard=~\"$shard_id\", index=~\"$instance_index\", host=~\"$host\", user=~\"$user\"}", - "fullMetaSearch": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "host:{{host}}, identifier:{{role}}{{index}}, shard_id:{{shard}}, pool:{{pool}}, user:{{user}}", - "range": true, - "refId": "A", - "useBackend": false - } - ], - "title": "Banned Connections", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "fdwe81suzec5ce" - }, - "description": "Paused connection are connections that belong to pool that was paused by the administrator", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "bars", - "fillOpacity": 50, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "smooth", - "lineWidth": 1, - "pointSize": 2, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "always", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "normal" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 7, - "w": 8, - "x": 8, - "y": 22 - }, - "id": 35, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": false - }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "fdwe81suzec5ce" - }, - "disableTextWrap": false, - "editorMode": "builder", - "exemplar": false, - "expr": "pgcat_servers_is_paused{pool=~\"$pool\", role=~\"$role\", shard=~\"$shard_id\", index=~\"$instance_index\", host=~\"$host\", user=~\"$user\"}", - "fullMetaSearch": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "host:{{host}}, identifier:{{role}}{{index}}, shard_id:{{shard}}, pool:{{pool}}, user:{{user}}", - "range": true, - "refId": "A", - "useBackend": false - } - ], - "title": "Paused Connections", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "fdwe81suzec5ce" - }, - "description": "", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "smooth", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "µs" - }, - "overrides": [] - }, - "gridPos": { - "h": 7, - "w": 8, - "x": 16, - "y": 22 - }, - "id": 44, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": false - }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "fdwe81suzec5ce" - }, - "disableTextWrap": false, - "editorMode": "builder", - "exemplar": false, - "expr": "(pgcat_pools_maxwait{pool=~\"$pool\", user=~\"$user\"} * 1000 * 1000) + pgcat_pools_maxwait_us{pool=~\"$pool\", user=~\"$user\"}", - "fullMetaSearch": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "host:{{host}}, identifier:{{role}}{{index}}, shard_id:{{shard}}, pool:{{pool}}", - "range": true, - "refId": "A", - "useBackend": false - } - ], - "title": "Maximum wait time by Pool", - "type": "timeseries" - }, - { - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 29 - }, - "id": 16, - "title": "Server Metrics", - "type": "row" - }, - { - "datasource": { - "type": "prometheus", - "uid": "fdwe81suzec5ce" - }, - "description": "", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 50, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "smooth", - "lineWidth": 1, - "pointSize": 2, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "always", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "normal" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 6, - "w": 6, - "x": 0, - "y": 30 - }, - "id": 26, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": false - }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "fdwe81suzec5ce" - }, - "disableTextWrap": false, - "editorMode": "builder", - "exemplar": false, - "expr": "pgcat_servers_idle_count{pool=~\"$pool\", role=~\"$role\", shard=~\"$shard_id\", index=~\"$instance_index\", host=~\"$host\", user=~\"$user\"}", - "fullMetaSearch": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "host:{{host}}, identifier:{{role}}{{index}}, shard_id:{{shard}}, pool:{{pool}}, user:{{user}}", - "range": true, - "refId": "A", - "useBackend": false - } - ], - "title": "Idle Server Connections", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "fdwe81suzec5ce" - }, - "description": "", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 50, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "smooth", - "lineWidth": 1, - "pointSize": 2, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "always", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "normal" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 6, - "w": 6, - "x": 6, - "y": 30 - }, - "id": 27, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": false - }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "fdwe81suzec5ce" - }, - "disableTextWrap": false, - "editorMode": "builder", - "exemplar": false, - "expr": "pgcat_servers_active_count{pool=~\"$pool\", role=~\"$role\", shard=~\"$shard_id\", index=~\"$instance_index\", host=~\"$host\", user=~\"$user\"}", - "fullMetaSearch": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "host:{{host}}, identifier:{{role}}{{index}}, shard_id:{{shard}}, pool:{{pool}}, user:{{user}}", - "range": true, - "refId": "A", - "useBackend": false - } - ], - "title": "Active Server Connections", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "fdwe81suzec5ce" - }, - "description": "", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 50, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "smooth", - "lineWidth": 1, - "pointSize": 2, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "always", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "normal" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 6, - "w": 6, - "x": 12, - "y": 30 - }, - "id": 28, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": false - }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "fdwe81suzec5ce" - }, - "disableTextWrap": false, - "editorMode": "builder", - "exemplar": false, - "expr": "pgcat_servers_login_count{pool=~\"$pool\", role=~\"$role\", shard=~\"$shard_id\", index=~\"$instance_index\", host=~\"$host\", user=~\"$user\"}", - "fullMetaSearch": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "host:{{host}}, identifier:{{role}}{{index}}, shard_id:{{shard}}, pool:{{pool}}, user:{{user}}", - "range": true, - "refId": "A", - "useBackend": false - } - ], - "title": "Server Connection in Login State", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "fdwe81suzec5ce" - }, - "description": "", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 50, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "smooth", - "lineWidth": 1, - "pointSize": 2, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "always", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "normal" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 6, - "w": 6, - "x": 18, - "y": 30 - }, - "id": 29, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": false - }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "fdwe81suzec5ce" - }, - "disableTextWrap": false, - "editorMode": "builder", - "exemplar": false, - "expr": "pgcat_servers_tested_count{pool=~\"$pool\", role=~\"$role\", shard=~\"$shard_id\", index=~\"$instance_index\", host=~\"$host\", user=~\"$user\"}", - "fullMetaSearch": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "host:{{host}}, identifier:{{role}}{{index}}, shard_id:{{shard}}, pool:{{pool}}, user:{{user}}", - "range": true, - "refId": "A", - "useBackend": false - } - ], - "title": "Server Connection in Tested State", - "type": "timeseries" - }, - { - "collapsed": false, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 36 - }, - "id": 12, - "panels": [], - "title": "Client Metrics", - "type": "row" - }, - { - "datasource": { - "type": "prometheus", - "uid": "fdwe81suzec5ce" - }, - "description": "The number of Clients currently connected but not assigned a server connection nor are they seeking one", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 50, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "smooth", - "lineStyle": { - "fill": "solid" - }, - "lineWidth": 1, - "pointSize": 4, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "always", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "normal" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 6, - "w": 8, - "x": 0, - "y": 37 - }, - "id": 14, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "fdwe81suzec5ce" - }, - "disableTextWrap": false, - "editorMode": "builder", - "exemplar": false, - "expr": "pgcat_pools_cl_idle{pool=~\"$pool\", user=~\"$user\"}", - "fullMetaSearch": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "Pool:{{pool}}, User:{{user}}", - "range": true, - "refId": "A", - "useBackend": false - } - ], - "title": "Idle Clients", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "fdwe81suzec5ce" - }, - "description": "The number of Clients currently assigned a server connection", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 50, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "smooth", - "lineStyle": { - "fill": "solid" - }, - "lineWidth": 1, - "pointSize": 2, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "always", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "normal" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 6, - "w": 8, - "x": 8, - "y": 37 - }, - "id": 1, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "fdwe81suzec5ce" - }, - "disableTextWrap": false, - "editorMode": "builder", - "exemplar": false, - "expr": "pgcat_pools_cl_active{pool=~\"$pool\", user=~\"$user\"}", - "fullMetaSearch": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "Pool:{{pool}}, User:{{user}}", - "range": true, - "refId": "A", - "useBackend": false - } - ], - "title": "Active Clients", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "fdwe81suzec5ce" - }, - "description": "The number of Clients currently waiting in the queue waiting for a server connection to be assigned. This should remain close to 0. Any persistent deviation from zero means clients are blocked from querying the database", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 50, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "smooth", - "lineStyle": { - "fill": "solid" - }, - "lineWidth": 1, - "pointSize": 2, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "always", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "normal" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 6, - "w": 8, - "x": 16, - "y": 37 - }, - "id": 15, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "fdwe81suzec5ce" - }, - "disableTextWrap": false, - "editorMode": "builder", - "exemplar": false, - "expr": "pgcat_pools_cl_waiting{pool=~\"$pool\", user=~\"$user\"}", - "fullMetaSearch": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "Pool:{{pool}}, User:{{user}}", - "range": true, - "refId": "A", - "useBackend": false - } - ], - "title": "Waiting Clients", - "type": "timeseries" - } - ], - "refresh": "5s", - "schemaVersion": 39, - "tags": [ - "Databases", - "PostgreSQL" - ], - "templating": { - "list": [ - { - "allValue": ".+", - "current": { - "selected": true, - "text": "All", - "value": "$__all" - }, - "datasource": { - "type": "prometheus", - "uid": "fdwe81suzec5ce" - }, - "definition": "label_values(pgcat_databases_current_connections,role)", - "description": "Instance Role in the pool", - "hide": 0, - "includeAll": true, - "label": "Role", - "multi": false, - "name": "role", - "options": [], - "query": { - "qryType": 1, - "query": "label_values(pgcat_databases_current_connections,role)", - "refId": "PrometheusVariableQueryEditor-VariableQuery" - }, - "refresh": 1, - "regex": "", - "skipUrlSync": false, - "sort": 0, - "type": "query" - }, - { - "allValue": ".+", - "current": { - "selected": true, - "text": [ - "All" - ], - "value": [ - "$__all" - ] - }, - "datasource": { - "type": "prometheus", - "uid": "fdwe81suzec5ce" - }, - "definition": "label_values(pgcat_databases_current_connections,shard)", - "description": "", - "hide": 0, - "includeAll": true, - "label": "Shard ID", - "multi": true, - "name": "shard_id", - "options": [], - "query": { - "qryType": 1, - "query": "label_values(pgcat_databases_current_connections,shard)", - "refId": "PrometheusVariableQueryEditor-VariableQuery" - }, - "refresh": 1, - "regex": "", - "skipUrlSync": false, - "sort": 0, - "type": "query" - }, - { - "allValue": ".+", - "current": { - "selected": true, - "text": [ - "All" - ], - "value": [ - "$__all" - ] - }, - "datasource": { - "type": "prometheus", - "uid": "fdwe81suzec5ce" - }, - "definition": "label_values(pgcat_databases_current_connections,index)", - "description": "The instance index in the role (e.g. a pool with two replicas will have instances with indices 0 and 1", - "hide": 0, - "includeAll": true, - "label": "Instance Index", - "multi": true, - "name": "instance_index", - "options": [], - "query": { - "qryType": 1, - "query": "label_values(pgcat_databases_current_connections,index)", - "refId": "PrometheusVariableQueryEditor-VariableQuery" - }, - "refresh": 1, - "regex": "", - "skipUrlSync": false, - "sort": 0, - "type": "query" - }, - { - "allValue": ".+", - "current": { - "selected": false, - "text": "All", - "value": "$__all" - }, - "datasource": { - "type": "prometheus", - "uid": "fdwe81suzec5ce" - }, - "definition": "label_values(pgcat_databases_current_connections,pool)", - "description": "The PgCat Connection Pool ", - "hide": 0, - "includeAll": true, - "label": "Pool Name", - "multi": true, - "name": "pool", - "options": [], - "query": { - "qryType": 1, - "query": "label_values(pgcat_databases_current_connections,pool)", - "refId": "PrometheusVariableQueryEditor-VariableQuery" - }, - "refresh": 1, - "regex": "", - "skipUrlSync": false, - "sort": 0, - "type": "query" - }, - { - "allValue": ".+", - "current": { - "selected": true, - "text": "All", - "value": "$__all" - }, - "datasource": { - "type": "prometheus", - "uid": "fdwe81suzec5ce" - }, - "definition": "label_values(pgcat_databases_current_connections,host)", - "description": "The underlying Database host name", - "hide": 0, - "includeAll": true, - "label": "Host", - "multi": false, - "name": "host", - "options": [], - "query": { - "qryType": 1, - "query": "label_values(pgcat_databases_current_connections,host)", - "refId": "PrometheusVariableQueryEditor-VariableQuery" - }, - "refresh": 1, - "regex": "", - "skipUrlSync": false, - "sort": 0, - "type": "query" - }, - { - "allValue": ".+", - "current": { - "selected": true, - "text": [ - "All" - ], - "value": [ - "$__all" - ] - }, - "datasource": { - "type": "prometheus", - "uid": "fdwe81suzec5ce" - }, - "definition": "label_values(usename)", - "description": "PostgreSQL username used by the pool", - "hide": 0, - "includeAll": true, - "label": "Username", - "multi": true, - "name": "user", - "options": [], - "query": { - "qryType": 1, - "query": "label_values(usename)", - "refId": "PrometheusVariableQueryEditor-VariableQuery" - }, - "refresh": 1, - "regex": "", - "skipUrlSync": false, - "sort": 0, - "type": "query" - } - ] - }, - "time": { - "from": "now-30m", - "to": "now" - }, - "timepicker": {}, - "timezone": "browser", - "title": "PgCat Dashboard", - "uid": "ddwejyl5j6jnkb", - "version": 46, - "weekStart": "" -} diff --git a/images/instacart.webp b/images/instacart.webp deleted file mode 100644 index a3aa1fca560ae0f6b6af8ec1aea6b25d02ebc223..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3448 zcmaJ@c{o*DA6^_o8A1qUI5JckMCN18Sm=;h9F8foPGpLM%8+Cx2^mY79YeZxj3q>v`5*?|y&pyY_GW_S*YdCi*%$zN`RPXltU)(F&GK z001Q1cL0b3K>zY(^mAJH7SKNTbo9DScM$+DZ$F%gt|rpb${NYI01$u|@W6so4vx2d zHPC2-1NHxITVDXpL1gg!!Ce1a^N%d6lk;sy0BDe~Lfz39=LchZ7|R9t`5xdWFy?Z+ z>EHz8QW&4Z!3n~6^nl0yiI)$s>o5FkQCv7vlgsehxL_XX`ZtXI8+N>j^MW-putwC$ z%Nw4b*78s6bb#+3U@uR9IJaNzV1+Es-e%X~Q55d{Ko{r(Lx6_MiUt0_4R``Sa2_7L zVdge4f#aJ0;6MJ$kA_`X*yRQsVTTs*0bao2mmeIw16TvMziNBi<&yL-iiSlK0QxBi z`Y8qgV+sJ2L2L&Sln8LL(=m8+}DFEWG@Z2~4 zVcmyd!+-1ce>MN9AG~vsui&j)sR*)ZB*@?qP?h41(>f`f$6Q zXStuvq+0JGZG}3*PRz2L5(uEy^!0A7x#BZa!qtq39eAUCr`4`K;qM5t#b&}TenzEG z&6YB>3-G4U`6d13)SrWZsF7+P@r_|dRI^(<-Hb1J3a8INYLuF7wk)AiwF&547#IF5 zBt(wx@laQr#|%O<(5?N#T@&hch=+Vl8IuM*gnFpD-$cmr8_-tnvuWNb@0`zSm9w<_ zrPUX>)3sZwI~vTbcogCtlX#fAgix-b7tt;JXBaCg#^?C%b(cz|T~DajBVN&$sQ-?0 z-O;J2z$db6*kOA=#LO+|iL)0;FXcqW+>DN-9M4b2?EEJ9QtS$Lq=J_K98JrA$`~By z=#!XZ(ku=#R0uJzSbl6WW``2mr?FG<*jZKaL8M1VXGewKKeG48F8XQyhn=>U=#}i; z0a4-pagh{_%EZFmkYszN_XF<-r8s>uSMA=2%7n_1YeiTUwdV;rnV~8?i!pHi%IfsL z@^|i_>#$-Lb~^VURUJ^FFY}*^*s5~`rt}V9a3z|_dfsc*#7Z?0)M7ou*V^jT1Aezb zlKN6^z#lwHf@@Q6eEUnByoCQN4BxD3$=AoLT{3suYmZdlvEhnYA90iISIg88AO_26@!URB(|kUK8!!s$b!{Ax_yFR6xFJ;Y$t^UK$M3P7#g zXT={bew`ni+{v+kb68fntkkBIQ$B`_#S$cLpkl-b*bGg=cY-hp#32>%mv}P zX6kAf+f3H<$q|wqS4L7r#8S+?Wy+l0rRLVU)$UabYS$wngx*K>!TN_suRIjLWwS>s z{{kb7FmEsJU@X`2xAru-ui1{i$CIP{f(qHw9}&2vY~7bTZPbmo{_yTFqw{HDnnYiO z23bwmxmoopZ+)!bWa(7y4tlo5yRwh`d7)7+GdO+4jhOPi#sX!H9O0N{Rw++Q5*B}> z9jN-8R&kDx_|V~Wc2UL@^{vrcvo>b}X$yhMxG*2WntJHjQRZDJ_s{QT4p-=`6y z?;GbyYyvgYZPA4|ul=1$)6VmqXu6MdCYVH~ zv#LGJ4?Ro#Jc7eZouhd2P+Gn2CqrHZE;e+3w(NYdfxp`C`}$kVVu9A2QXkK?^Xu0f zpbSGf^OcgTDHyRTS6`p+M?INJ*Br?jb+Q)oO!4cPg8akuGYKn7T_b52#^i;_lkJi8 zYDH3R`^;6GuZ0?-7s}v2i2>)%Fb46Fyj>&{KyYu=l{J4_gRb5($5Gi2ec=Y5550U1-8?!`>GB%UHIn|K$dG;lt&!~2s-G%qx=_t= z^Y{A4Sc{&JdNb=6N6ZO{lBsuY_3aN>v^};y#LP{SIWd$h!JKm;xVf(Ljc}T|Ypl0z zYpiDEJKEui6xFHMy>y1NoHOq14Yd^25*^3pFmc{kt0D%`HZE>=O`N^?S#OoLkF4(( zb*nxHJmz=!ZU4u%OE7mj(UKZWxE^k7IwWZR$tRn4W*4ZBZQM67A3x?fd`QQGkf2lXH7x3kS3a#M?p;dsW0u~R z&5d)jPhP*o4Er;w5+;HSAtjqW6m;4_YWyzr?X{fX_MFk2-BNI{H@@|n_# zQJP+<+TMbstx&qUo5JbZQBfuAC5{d6NJJ0L?+%{mK|YjbK*-*sem&~+D@#_xYD z5Q5nRuxmtRXC?O3f*&2Wi{5vRXi1Ldt4?WM3GU$K%M9WQTcKxZ<77Y7yX-i;OnRd9 zNr1%HD)0Do>O`O0$#{bKHU3<&jok1XsDc%$YuHM?hsPa=YR1@NbWTcFR3?BHYA@(m zZF^asPIcp{QY{jEdYJ03`y|OB3`^^=d=ER{ajHU5H`E;Hv znR?po_qfk#{}Ol4x32!-j#%5nRi092Y(qLDRJSAAejwgz%(5*;*+mYKlQZI37dk0v zHqs;}?MX&^dD6bov-ScmCT{m&s+^96*4FA+wXJ*>^tfPrzLq@?Db`wSSt`-+D#X5f zQr6;Pgy6i*v*FJi`vUFDIZ*r3UQOBz!D8_&Ca;eAO548W)9N1{t3r+ZHUv z#<5(ToK#3V^8hsQ#f49IxGbU}^e^-Ccp6xiBoms6xV_Bt`!ul;1#-+iC zEqn&aNL{-41Dj1S%`u$BP2&aGi4(p0Au23hH&rGevZ)Yjjx8;tXc4*Pan2hbe`Iyj zfa)Z_duQ1XFWe)ijH^Q>Y5Ps*JGNfG&kk9mp8vw5%iLu?6LufVM2MD6-5 z61#jXaB;$hW_#F!{m#Y$qe1Lb!YJ{hcHAbH zfl;!~QD{2pc2{xFj!cyJX(C5VoN(>nNXcQ5Ci&jqmhW$MI@eY-G7K;$SXDg8e|t}! zIey%>Nr8Gd!27Aw5nmc^cD*tzj&bH6HG|>j`PflSB(ug)X`>6WkJsT0hw$(1gz?KFy4=84c(g)y6Uk1BQ~b`WgLi z>}Z5cBTQ^~P^v*MLg?l2qBEO%I?L6j7P!D0H zotQ#=b6wEqhk_NdZ}`U?9Noy}-{9}2d$Q%wjGx+%*t~b#Pw^-8CmucD!DqVH>Kmqy z{Ex>U*-g9^vJAeBT2){9+#TO#PXJ%z-}05$Gmg*v^X04KZ{HuX64r{>gx(on=NGO$ z+tc3m56|Vwjom2E@eSVxxXi$qgczWl;ncT$@b|`u!clk9&*mSm}Yl5eu zj8VJ*8?3JUKc6HJau6cU**p<>XuaZG>->m4Hj0S0p9gwAWu|-Q%l?Ms)ia887a?_x0U{O`HxXHB6e9|OrFJ1To5P*>Nt>G_nsiNeaePdZ?< zyXx3cVVKqd1r7~pCo0wr1p@wiAskeW-Zcy%`FgxIrt8s;5d0s%a<`?^B+hjQ;3qP| zUV@i#Qd-o;Z1saw3Kwrjge2{;Qq%cQ8DLs{oX0Q?7+QMx@~ik){zG%3q!FXE6KDrb zDm<)7JN<$BM;zY7&)g8PHFf&{?IJnKj(pPATDV2dH0hn@@VKYHsO?j)S?c)R=Ke!f zAWD_9Ef}GA&y2oSLPpg-vg$B1KQ0Iy(f*4SkmK>09Ffo~|5Bl2g)AGsU(`Q3{k4!h zqRAJh33TfGah&OJeH>i*08qxDuB@KO`+u1CWwoeH4G4?r9p^fg>6iS73<>A3(+#n5_=md)1Ui=P z_K5gC#1lkr^wDu)0G{#*@{YV=5MMs;~v?d-vH_1*fFaL7Jg7 zZyuNAD(wBREhRyj(Hg{d(jwC#{mYBX6Qo3L^5E~j7xSj!c0Id6&_F{v!i$_1a zD(2NtLy#nM=x85LMfvo-I`~WM!F9raPl3W-{!Xh4@~j(w2;(3OZFW9R&at&V|CS^> zrex_{rf52NgILUxY@+DZ(f-;$TeoN#HQ)hGr0~`(EkE;HS_q45{mt5@un*8nNoz1H zPk0$wb3WO641mn_)CW0{kgKS~;7h!lCw6 zLXAU^L{%a~AgEi}w#CXx+i{N`M48258s5~$d?Z+|s>LBIN9`~4P9G{Qc&yGqn37ek zGA8BFu+b>%qpMv3p*<@$s3x9W`ok)8AUI|?23CzrBkl#Yl{Iv7uRG7ax9SndH*CdJ zvrW7lz6U6ma!0@~#u=PAh)6^kLJCGExu?GWsN_Vnw5T4_CyrDRCGIcp;g#|AUQKY- zivVmJJM{`F&x%(dutxf2wy}l?)|$kOOn=z>qV}J*w&HG@8rOaL`ZT4+rtM2}%>r6W zy)e1QMo%C2(r$0MU_7KvQ>ZI#8HG#z8Z$zTOV4=?a_C{F4`v3>X3o4z-1|l6BYyhs zy9{C`S*3_Z#3?G4lG8Rq{~H=cD{Cpj1X{d|Dd-67QJykd99iiSwxm0q%S-pe3>!IN zpi7+H3-pHPjPM-K&f=DVayi;S2NJHHXW#1|7~cm0`dqjtCVZ;|c*N1&QNv+j0T_6L zix|KWRF(MkXFNy>UDGkvb>Oz)c*Os)`(-2qU2;gZtx)>A_or|Q6Fn{E4!NmGK*Ls` z|IpqA=HoXW71phw(>EkL8K|*dn-e3aF2=_~H5!mX5(U*tNJ{AKGj<)lAIr!j|J?6C z1XI)nn35V|YlL5@&JZ5Pzp`)nK^X@0=jMkPzwvLwB^HprFf%xl`%cv4{au;L5Y*dG z;;W!{p@I%!0vai5{ikioI36-_xjxws0`0`~Sb!LeCyVwbHJ_x%T5ZFEX0)LL%VHVS zH8XFAtow&leH+ulCiKd{BKKaQ*|4u(cK2gVf6iJc8B(|^(Oew-WxCJ5OJ4)M7^jV= zyxrd*IUf~H+GJp_B@S)G|bdJTo7JHh++F=)NR#<^ja|;1*M^-Fa=1TYd}TX#Ns* z^F)Fy5nG+9h={05|D``yS6-%0F8VpC8+4Kk-vPRx_g+VAUiFQ7Wt#T#dC#K%w_0Qj zEF*my(gh0cy-Y)-9p6ulV=0kT1sQBUKK$F7b2S%jP&;wb6>kaMD?R7-PjRuXwLJu; zN`e(Q6jR4?zmy_fCp?(96tjr!1b9hwWCQ*Fq55f&(+k`9@^jrBh=&%UsC zM=kJXhx3Mmm@62C9m%G434Uk)RO5|_h$528LFp-|w5R8VRk-Ju}tWDU4t1tS{7VPARO<8ujKC`396*LM0|xLiR2 z+1R?Yz1js~jXR_MoScH*bG73Nc!Olf_PPi4OoK$9?qT!MSUL-8XC=C^=U-~yt;-($ z0arQ-Rtrs!oFj2n7C2>1$XG$R#cb99!3OPZuY}1xrs=E2%(WqG$dD&;<|S|&jj3a+ zzJ(;5CMpn+@$FY|x#gV?Z6MRo-(BJySl--hFz_l572zYY9*u=TJv1HnI_=kTLF(KN ze)i1ts@08YjmR#Gq5l^~$e-Q72jUH`m%h()PO-_RJ&7KUY&nR59b$W@(pCbuD8&b8N}%>8qahGD!eOoAU0S@V z)gU8^dCdeZl9b}skr@P*0?Dxox=DO4F16wGJT`t$x^OZ?@P`O6Uw;2=92gVkOU$%7 zZz+wt1SuJD%Eo3QM)&1V!>eV?H|zr+hu&*0CPd!agqgq-Wjy&D+y6Uk_UziNNHr1% z=m>eKJktDzrS4G6!sTfF%de?P*hJ_0Jc^IaD3TBhhcMHG5w4AL`k15=QjU|E^*};< zD)86swiCjXT!IVLcE-V7fuH?puL2WJs`i$+F!caP;)@DWt(I{zUUzbwMO67c+wktl zLC*A?(&!VPq3syzQg%e2cCE75z=*_n!LkSuLOw~Px^OMd0C z^s-rm;8NAeDC}P>z+7Z60I*dJef*o3;(!Q&URw^a`x(jsD&AdD-z&ZFPBs9|-8H-y z-ypQ9=tg%SoTGQi(BUTq|Lu%jEKXP`OER?ng)&|PF5z8Lt=@n?*x_^4yQrKOVu%XV zM;zl1Sh+p4aTO;-vAJe+q0$aNi&1fRx{~FZINX{QlLEU6=(t7H>sy%CgkOe&t1Iw? zIM0mZ8pHy4R@0{j4<033~3x#vjff}@s)HOiNXzF1}2)e= z<45H&2&UbB+It@>EH+@oQe3W#Ie?qRdVDED|6%CveJerZrvlGz$sF@U)|mbyWG4YR zlBhTSpd34|#!)E{=xPrFI22&Nf&i2krsssU-)ujJlT z2YdcazPFq8niy$%ULI31SaH3sU07RT$kN|0T&SGd8774sI)NkS(QsIz^29Q(MN{JX2ra*ms% z@S_p{Alni9{VxstA;lWSkVf;GVRNf`3SLWiY_a=K5Sa)vxBqk8u0W)!<5&!NxmBxW%^hWZ4qVWC5kok1XzjTwh5$h zint8K0aH;te;h0UTQUb*ISb3HbfkY#cOvas?(ph)IrJ8Se{KidMiO;%{k;U*ydj6! z#ZAkDvzSNt^2}H|TgQgx?&-J_dQ?JCoe;pe9ia;!$?D4LPFE!0&0vVZY`fv+LLLy2 z>o0w1Gqrr8iL~_P{1>~_P3csV{Jc5a>5A-&q)-D10KE>YFxtTw;6epCb_!CY3j=_Z zoo|9=jH5|@3|uOA5yv)7IH7pl=0*ReW2B{y)mxe1sm!TyPpimeDjz4wzSn7K8l*WB z?qwlv-o0yys|Tc)ZDSufS*zAB%iG1)=EP&AeBS-S3O#3bX)D8aqE2}sgV4%B29;hd zfDxK54*QlmH=ekhY5Z>_d_g9Zm+5)X3834y{i+Rwa93*%&Q>klRV<6SzBV6OnSB4+ zfG}m4R~RFszYjX_l32o|mbu?M;U^+Vwu3a49-awT)LItN0TYn1FNQ>7WpSR|OZCa| zrW%6lzUQfPMB)?M{$o^8GJms1!V)Ac5l^Bx!7HjKygA>Q>m6f0nLE8kn7_ScUs4gw zK(4{C5EO#3Owa*p4a!~9!oyPg=OARIRP#b%BnQ{xVQ|69YN6Slp*gfO}6qwCiQ zo8{u@-%7SXQ=*^-Bcl`nfX+q5S4q+(JvlAMI-%APt+RmPV+j* z*hw2fCx1>11tBcw`;=NPgc-{O@5Ycfi@u$zAX3%vtfG#iUSwn-N8@$?|?Nk&2>(X8O*F=i)3MTW1~x zo}@62#a-zP>9tF5P+fz|A~tGKUt1`%h$>z&;Cm-}7Wc4Sb}iDrW%shZ7CMmKO>Vs8 zC7oABhZW#k+uZ0auweI|QzVu9V)tDX+?}?d4Lqc3FHN@`Ns^N=jw3FOnl#WJMV5L@ zklkz<5HB#4?3NTc5qWm1O0`x=4kP7rO=N?Xk)2g^!pINA7JkFHr=VIWS9QJ?m|7Pd zp^_&tW%QiPhtwo;8O6!*`y^P7$6DmEuyF@)KW1*-U0D2bdPH?j3j^QX2K=&}g8qZg zPxX__r}2w~h=iBwaqD1nBgu9oau6Xy=#WXTsh`rfiu6{^0Yx6N=-zspciE;1Tk~V% z7JIVmL@UPtT2*xVTVF5#~Wk>ycf?g3< z^kmTFK6NeoAF0M7QWx2XzQf$fCA?@&q|pg@+d7 zYWu6~HOEYNtpO*@RK?edeWkGRJtGP9=#iek!&N3>HE_^$C@R`l-X=yetWd8OZL z6cs4%pih$4fVUn{@u7GtiKE|@mxBW5tQU}8h&%)tb3TN(__~7dl zgueswvDBESWR(HzD$|v*3XR}2LrNNuZz3n$_6$5`84qQL9yvQFc1x!$skUmF3TMko z$Gse z3a;eJcL68S=8n+ICKwkaI;uH){BEe{DOW~TR!fC_HCs68`B#Hz<#@Ld;@X`xftbh4 zNdnLGc?J6=`r~QGfr9=aM6~6hf$3Q_*)~ztMe5UCMORm(!3df-QMVg0J;VKNGfH#P zH!doK^eL+~W`^Zg%xIFkn~;)^t~^-5c#_P+0{;72FSv0|qJq_U_MMj^_>BNTjv5=N zvwgDjO6fJ8BQdiixG%)ADLNuNrY^=~ZW4ABeL!)ZgIMd3&izOyPZRr>Od_vLI7!Xk z#2R&kahVB)o1c~p`qdDarmW9O`v>&MchzHnE!0U0w8YnB^OM|KDcEE+HlBTIb^n`JBQzxwB6vHZdp9^<+jBRW< zT7Cd^0%cxuD2vl)~T??vC~jB)!U zSAd`-(s7qh9nRrN7ZgnO3k}a(aanLqm}om{)B4f;MD<=nbIw1ft1tagiq~!(C%U z`$E~&Wxt*20MsUbFgR(fiTEm(4mvqgZ335@73xg|>5D{!s4+Oc(b3@N6N_?rJs+>& zvu|s{u=fa8@2?dEn*j{NLN=H$BPwTZJ%cFO`#ofPTWK%0~p zN7}8n=FcDizGY04ZM8bosnP&M)_jEE9z7p)4r;A}ZjbA-4w_WqGej})@59xMf+M_J z-QYFE_dBkYOpWMbjxa@4XPe!;H)}R^^J|@?HTwKYekYy^Q0?N*sph&@9leRG)7o|W zxs2PYga&=6(<-pqg0?4Mj#H^}?!ZE5ou$ZoWfWcOzLf+(NEf*3;R-FcVLzsE&|ibD zC7nPTNNvrLr}@O4-q*mAw!K@93_edUw>fg8mjU}#!k( z{0?UtKLzr_wy&=A4cHfbJ_ZYcF&wqc`Ci`a`tUE>;o!sO<=xJe z=%m9LzaEtEb6#N3D*>N1$H^28Xc9Ngo+W-xS(?!KuWl1FbUOI?0PV8#H|A6}r8uCj zJcX4(#xfxM;x0T{CCu)1HN^L^=6HE7PE4U5;~1VwIK)AWF#+e062*W=AwY;yZa`;i z4bEJ^lW@vQ)loW{5s6&5AeEvzTQ@uxL0FU?QL}vVkaoRrC^M|HKk3-XE`fR$W%U5- z5S?!W!2t3oj2U43);h7EwJ%;+E*qKCb+R|%J)nnsPljDX>d*7QkveEaQT+Yd*0|Ms z7>2a??-glJ-l##?OTZP=nVTS67jn<3@){B!A}c7mzEKq`(44grQG1oKStp-+DoUTH z&EP{Hc6dF2fg3Kf+jOcS-Tt1frs7+`PLoUOW9g(gq!OM^4_-zUty3!YV!>{`R7rjV zVedMP4d5lx!QnSy0agVTBhWX=tbwC^5yF^6sb&(aci(}}I4Yh>jZo$-Jm7W`CJs4B zb<@QFMrFF<(EuDMrb*JKBz*Pl8W!kjDAeQ}LT{_4F=WzNs%@G$p1Z1e4%v0Cc=MVq zP7YVdC?7;T;uPvOJoU&RTdSMoV2-?uDE^iVf}|<&Kwe&6H-1`8spAJ`>J-m8^UWE# zYnDl-5LvJ*DjDJt;K)c0lD79WQeN=MVdUnz$oJAO@Qv>;zvaC>2=y1|+E9%0C< zC~SmN;WWz@KvfCKBcXkzb>e4X-u>vx5dnu0fE+f_+^(6|A$)=R-b7X+M?X!zM}^HZ zFa#-o`z-$+((YTu5Du%D+z6WhuQHI@r)9h&HfInN>#t&W2}CsW5Zh1w4pNU9x!%ID z#!m4!#m+)_#OHkm*4kW8AdcpCq?X`OrCJvo0T*D&x8VJtg%BZ~!%egfTj({aW0R?s zJQ^EEkPRM;q@AP#t}^>b>d6|yC~Z3@R*4E>Wf%2|U^_wbA`oW%so+}p74!uJyMde+ z_~pRlK-@Fl8)Fm-VukhIYWF~GDZ#3#8r7@~LhsD`tEaup=|^N-0pW5Tvq)jgj;+$|y@$fGMgqZq7uW->Qq|2ETQ#Z_ zGI)Zq!5l==EQzMNB-LZK3kuCTX&Ssa5tW+%BNie%<;ezBSNkwyC*Cxlf{r%bRi$qc z7xqc76q}U@31}NR6TzMQFs>B))tQ#Tom93ycrQ}Eb}byu-_|=jsX&L5Z9X^r0%8+e zOLfDOmSU+wn!YqG#wrTkJ|gN;GFmI%9V%SYVti}Ss4kqh$MPuD^2m~RpfG|M3Xc?k z+&rSK-HY*rPbFx`2B~GwStt(L@FH$K_a)A6H~~aZ9BI$BZeaY9HwZFT1TflXFJgSl z*KEw*qB%>jsEk_$Jg{9qf}dlXA2vJ7mZGEWBzb(j{;qnA&s4pao3)!Mqdt)%P$n!O zn25c?@>dgfKu$1VrmgftcN~A3|I|v6AHSTPj`>#07Bq~Fq*U9-Mxi~1uoi|~=^1Ks zzZTbpl7F6mX?^GJq=*Ehr6%eJ=2fXRx!^LWT;kiB1+VPAEq^7);*!O=Xi~DoxONqc z!lSZ8*dXacI=GXsTlK-*1f;@0GP|UL!(I0^S7}QZo7Laf=$kPP-s3b`f^gi{ginRm zCjt(3CVpJ>GVLGxOUy%Qk&%Vg;m4zDm8^n9u0H$-hliDA*cP$*15v`PAlcV@J4Ldp zn><;94FTsCV1d51RLL+g@(ahhJzyNBF(6bW)>(?%7KE33^xKY+K7I7yYbUc8Y2mG@ zyaWu77zcY*yhB3ec1JB?t%X+RAsd9$q!R}=3BZGL;O6n{oxK}$&u>|7u3d@=7vnj8{fv)>Vsnm5~Tp3j}_Q2aYlB0wJMu!i#8wo`JL(T8YDf{0bc08mcFQ?eh>u zI_ay=9-52!gJuB+ZM3bX*Zlk zw!Y=q3)of{6*NuyovC4-Ef6KGVq>_^!`Su>vhL-mO;3k8TmpTc>k}&{iqQ8|kW?6h z9>Ny-tC0D>NqoisE7NmubwXm zwsciI=eZHZKL5-0^@87sNm0}_T7DO`Tqu?Y-nuSEM=QAES3*s4wS(k9m2lY8;&@6I z@fZWsgryK8IPw7~IK!miMKWq0$P+BHl9$?njed?q@YX1WtU1>c(7`#SjAlfAC3L{n zAD}f;xt>yzETkOdHBRU>u*pb6PPn!5rPLXFK@3%gppT z?6>Q0-5ah%GqEafJ)xxZ1&gaYtknqIIa$@5dp4=wxX#*(95r4bOEQCHXb2biyH3b{ zEf)+eU!U_@nQ$Ug7tUU0kz^zxnkl#23Hu^u8QYF?#Jiwo{TriILh z3e`4##5v^!s&Ypj`)jFhM`8XKox}+CFcWSO_QdC2qUj-5*r=E2UT>E;;Hk6=SX%dK zl=g;vU#vycJ>{P3t8~p{?~C8~$PKGUHH}F!;1uJ9t0k7F-0h+6wZ(ei#2(@s7nlgn zYIhc|_^QS;UpEk{_-EhZQ{gfZ4&zJsGa?IO|D1v)0P^Q2g;>W1qV!%OFV@5rToo`p zjMP6^wvu5h>ZT?;paVr#;U$+MY3uPM)&3P~#HQP%RDxj!Km@cHQ8T7^hGH3j+n5tb zd_mlHtJ>i#e$Iv>1ECg-{H`b|gKHRLb#r}*vKWPEGxWE&Po-yEU*-*{bk+&QF-O(o zq>r)Ucd^BOLd`2LSyZrdM}Zm{bFV6Iz9GU_MFh?*&l+I#`bg2`K~QY;w=hoi$s`A2 zxmIKe`fhKlY1eF`Csm;#^mjB&9Rq5*gqOq=lAO}|fSL+A z7&$LSTyEcaZqXE$&XNoGA&N{C+8Wtu!K^Xil5UNTUfKQu$!XERxvpQcGgon2@elwt zLQ>@#n_c3JS`e(pj5_g}a2SFUI1Uq;aMr5K9I9ADOM{Y_F65^^q6f@D_sDN4duNdO z0*@GA@5<#p%w~3YX-=FNjaEj=i8KB(sD9c##8!;Y90B!q1MwpGH^H#lRU~;M@^$M+ z7K_arZB{fY*243^TTAkf?=iDi$u=TdBec^+D`L;4#-0%gVQigw!-~5 zx%yvP$8RafGfmuF7I%S^C72ngb~s-@*%Pt{bgJb{dCI2&xXHu}PY{>w+DaR7`Y+zL zQ+Ps%7@YljEE^=vgzhaARu%~r!sUCau!!KKw6^{S!yTh~iw(W{Gun8-(UgfVo+p^PoO3fx z-kGu!;x+$N#+g%KUtOF=Bcd@0R3`ZoqELX399;a_B*A9W@nX^FCO3inO{?N)^<1~| zmkKF9$^A|(4*8*SrdW5Ef>4M#tf{ebhE75iY8arK?|7Rw&C7c$mHlDo1f8M_tiE!~ zmmvUqc2)GjIAW0E^Kk^Xcvya`nwQa#3D`p^Qq{28Qi$O_CF0C$$bwtYKm)OEmzY=7 z46@y%!ysQp_xZgjd2$>^bjjpo3o2(|;Uir#uJOP6zh z8%M#?KwgsK6&R9EL9@H58ec1;6F7-LKT0QvbcQB zm7G*a!0`Yb$vnKkGc+Gn%Bs3caxUa?s!C*|&z^gEH%N;TH9uWKsO;aE`;o11J#GE! z|Do?UB9d^~oL|K?(%$-(vHW9I6V8HjFfe0FQ+d3hYmrY3?@-~DlS}gVd2QpKR&D_3 z3n^Cqn>2#i)R6;?Ea%9b6Cr~57sJqL8Bo%ICvOo%W8!7P=EsDu_dfVe{D-xF2!;9) zEt$IBov}m;q+eHJr_vUN-?6kbZ;E@Oe(l83sXTE+BJu(%NbjEvu+MFqtX(bS?e|CsUc(>>)Qlx$<489uufIf!MvOj zQS?IUd7~ysX1}96Ja!Ux%_1mVAd=It{I(Xpo0p^s3Bi2lnD!eVZ3P8?>%}l;)0CVu zh@>q221!LtNgILS&H0^pyFn-KCOD4}2r%v(51Q6mUwpfr9?&JKZsoXfLhHt+C%Y?C zo$V)Lr?37Kk2K*!Wm*?|qBK%Vxnf%M%7#R27+JnDNU{j~3#(^U!kykN!KO(&TBI5J z^UJ$OJtJCyY8W6ppt`X>B8>1wc|EBkEPKboY!{41BDIyTu%U^hF7yX-R4V4%4dLZP zd$#Q#vf@dLo=kZTFJkN;gV{fG*77JB$JO5_E5OV-JpQgrl4=$}QgVgf@Cu3C}6`3*GMGxCIq*~W!tk(*&R4_%r-*><|^lVdj-&Cp<}|mzZAb|f$STV^mba0Z~>`; z?Y;XE4;v0py$Fjdny9#|VO+kbWpY?}!bQ*#F8&aJcqbDp+@U9LVS|m(S2eoBvsj&s z`pYSJ<90L$X=6fJdzA^;Jn?bJl3f_=^^!=; zhm!x87Tk6vzW|5qv(&M_pn5qD(}Rc=kmBT8)<^8&i4++|h;+eB&Zbf-S2j2{rM6O~s$z_doF+K3i z=;mO;wiB}Bxff8w2Oi0#cARA9F~>>I^{6$`6?QM;F((xc6=Ew}bYtxNI%zwRPgAlH z&cO+6G~S%^To4{^E})TvZx*I1K{$@wZhALI{{mVghwY>x%j(JDue)o34b@eMmY}Hf zwPXKDOJRgl2k6gM*d-6PoZIlTJ2*X(-z`Gu3}B`|(pfh$jOUPHakjjAd~HM^Cf49d zfi>Zc;$C8;_GoHwx_W~L>KJN3HBlb7h2qWgrKxP1mYdB!5v3L5bTY;^1y#MpeUaEH zTfSp^n_(OI6j(Kg2l*5X7aftxt8>pJWU3_0jY^U9$p!+6LksY!$ z!GiQC<#xn2#-ONeV+@la=Gu@UlBv19NoLP2c2|Vvii?UgtWVj;OK+^Nt*0R>zyOt( z#j#0Fia5+f6Wips7{S<{&wP2as6G0o(%G#Ox%z_P8`>CCu=dLIqq1h09k=3vAo<&*Z^$*$O{vIA)Bz;QOl{r7{Yij*3icWj7D6v8bzQIuXiPasm+nJjlipH9q zv-42Uqc^moGFc}4SAHb+JKc~$PS0akZLo33-$~HXuwnF+Js2!LvR0?!xdAo)(L zpxhP)m7zReffFHyHwW^TT`cbl{APr*gHtrzCc+wwu*b@mA|Oo!WJ|b-$J2~BJ=XrJ zH^WHMvBA46T1=!VWC2EPlTtJ*FlKA_AJ8}@tjeG1k(;zpawWUu2aWg^O+EPOQ{Wbe zIR^)0k!fGo)zuiRE)K$N<-npOr@~OHWDbfzzPQ0sZfSaWPphm3af~IV*BXmXL8jfu z8*K*RNJ?QpN7r&tyYn4`C6L989O~jUjKz*Pk(ZIIlG?AuLhfJ<)K=OFn1dj3$=)7S z*yTofs8)qMvT`-VQFR+J!S3%94SA2x15ggciUibc6a7P86~$f?42Wej{ifDML-C_& z`D0&i-F7*9j-A2dT1(ua8N&75_6hp-)Guz@5o5hA8X$d_NfI0{2K7}JIUTe^&@akN z3gLLZ1C&%G%9QNrGL7;Q?=kERw%W(ydyE67pLSgAJ6HshxQG<{yhE)vz!EARaQYSh zq|-r@Sx0FA0!vL{MN3xSCk1vid{u1KeoJ3SI}`he&_zmmgG2L^NrsBH+M^+tC&JK0 ze@Mcb*w5^#D9@54BU2^@1#4xSF+wj|9TD&Lp-Vl#U+T>yZ3HkE@xr$AB`{-ZKDPQx zTzrQ|>k8J=7NIktf>VfEsL~meaqc2iZvLBV%gheQ@8W6+)NpbMixZXR5$HC)_wu{H zuRbZp=eoX_4YZ}xy zm+1-y20h%Q?lOHI-ea+SaU@K7E2Wy%yZqpeYG@96gCe10D!%AckMhb6uRGK;K{=eX zgqYzYul|H|=vDga&EdI5RzWdo5u72LRjds{T8?nKrDqhqTQ-_LLn?;q@sOE*4D@{^$BFH>x>OCy=#UTlH$&N^SQ{QS0uIH|vn5!1n`p^`@| zG{8#Qs*o}8i0u9~X$Dq_33eu7R8Wepe2CZ{(+ByA{g=B*V{~|wD&WV|Y-%1rnc}4r zxX{vpXq9V>it|+|CJ5F1GSX&;-9@tVO^qsa?~;?>UY%~~QPj;--A!vPe?)T)dvN4i zwmd85Zygjv>~Gc2-|GVQhxg_-U(ki9u+cluBLJRY?QksCL;rbgW0#+QEf@L}^(0r2AEfE`0zcG9ONSDy~*8;E9_ETT58p9a|c$v{G)Bwx?+cS0F+B z{$6Pg)FZY|gePgoqF&=8a4*wBoUy^voDFi(JduK)^m*K8xr_ru0}OAq0IVMz5dE6b z=+w%WeQ7;6L1MKx<#7-cUcPYtl1w5ts$;aJo1M9Z-ZmtG`TL}poLd?;lZW9Z4`_hB zj#1P~jFNu1+886)i`>(XG^&nhHlpLPck=%vxBNf(Y#$SnF zC4K%;mKon;0qY!h?X9r*Z0DZC@WXzPhx*=z0+WS$c&5jr z-m!RjzdUfhY^tE;FozDX`a|l(iNC7KggCOT;bO5@02IH`CqC8`LvAi+oT4EwY-Krl z42YUufnDyhIChO7dWP<;wnAnQB*W$VL@~1=$G;l&Ip81Z)h2k|)&q{9u&J)x&@6ZF z&fFs5gj`*{O>NFca#yxBHGLSL=VSo@kfOE-LtLpw3xq+!q;l%)v}|%6!>gaW^Yz_C zCzRg30s!72tY{REv8}55rU%L(997OuP#EJ<)ANSz`IvYHisp^1T2m6b?qUh>7STe9 z-pw2S>0B8Q&3^aB+4Ge0Y>Ajc0h}>)rx+MFA_oAYVstndV_BwxV~cJw?C?wI8#^`c zAfn>Pv5{-E+qE=n!8iTz7#^wy=!v8!1|}!AdL6as*NO6{E8e#V*rVDr_48L4qt&yY)S`p1X5TgGA+q`QxK&#myAftJ6GZ5U?-3YhkHVJGSK?+*3EE6!v zn$DRK0mZIAIAum#HmJ-~+MA?TkD}cS?)#TI0GRCj%3$GD`llA1d^FWs7g;}tHdGgh z_O9Tl)FeKL(gP|r753=;$d(SJ{&ESq`!UqA+7%&J*oZ^W^Z-!fBy#hFCi6WS8?@)_ zyBM;UP6s)g@fuGBtDm^Sx03Dv=6!YFF-P$IG5#6;(Zgl3;7Q$*q+P?ZP`4bGjy diff --git a/images/postgresml.webp b/images/postgresml.webp deleted file mode 100644 index 09a854289ad2ed2d13a0325a7569385036a892a8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4862 zcmaJ_XES~bTI=5TbKh&P=UG4YzP7%`qesUy0AQ@Dq-&%rX+j17fDo=* z0YE$eXecY|7USb?0sLZD8;BPnE(C#kd+Mtxv74AaWhdDH2ml7)7EZupW#i?psH>}a zrT*{jcmlw?ihLKiTI;`S{*|S)wezw806cb_Lczw}(;J5^aai2X+x-e><1np_qm?ZV z*WfU}CvJl{Jaxre|BZL9u)|;YucT-_4fU0AxzXZyc8CAK*8hQR96cd8jVMk7vV}l# z>*JgJjcu=R&=rQb`r!8VSG&rP!VU^Hz|A0BX9m;&4L}Rf1=s;=zz1*wTmf%D05?N% zj2EDfi!1$$pYbohF3x3*b2$MvIEM=020(DfbL9uF&H$&uwSQ{sWiRsJFA9%B2>^%| zu-GFm03b;N08BC#dtQRYV&DKkFb4pg(0}}H#Q-4r8^^z;T4@deKu#_I za691Ee)cbUU&9&xuWtXZ=6~y_sHv;2j&tD^f5bF8HJeXJZwwd2?v_Ck-t2zXfDj^A z#v34NxbgAsRD)DuvtKYw(xDc4oWn$YQ=wTnyA9$fw|0C%wl5gfEHHPic(c4Mzkv`q zl7M)!gi>N2G7E358~t$6V!r35laojgC%>ImCAURGa?+k_PRgePI!ic&E z(gKhr@a4kO{D`of>i3v0_mbOvzy98MM()+In(If5W~0+PmL#F(X|;}YCrr9zgKRD) zwEVF`y%fcVA038gBz}E6Ii*Oa2n~}^B4DHx0uY_%9iC$Zw^;S%S+@0ASQjV^h)Z;ldVh{$&-*P-rpF zOR2yUK||!5Lj!^OdGOK$YBTr4{f0O>TCR-X-Kx?SB;%A9^RPhy9ztF_<{Sd%;M{mO z@LiOzihHNDDx&-|y*m=ICYfJa0tT{GDQpM%s);(8n{~vfwyPR-wwgvl&i^#;z1iZc zlC(N-tG?8?BPL$Gp6AUb72>P6+yTbC$$h+Ce4ybMf{a#m?Se~HO;?foyhpx}6)T!v z3T}38u4<>IUSrwc_tgF@t@Wb5ysd+U`n67>Nk*hyE_a|`^Q4UFpB z8+h*A%g;ty_etbh#Ml|@nZs!F8@j*9T6hKI=kYOtLMy+W)I|FDIqyvZ0yK(U;Me$E z1d>`JipJR#@SBbGL<9<-I^urvm?xEuDx&zUZ;`~gXY47NTlB08913jy+%*zRsWWPZ zmaXqo8STBR+_qAhf0B6S++xeJ^Fq_Zkrci$HHtJ%LlWg)j()80r+H6xI|`lsP;rKX z7#dCiRwuj zfPG@1nV~F_VCoRE(6Fy3-L!h8B}|BON791yZ$UJb&XBq$9YxA@_73NM9qb-nsMuRj zmyQRs@kG?PKs8~v8v&7zMW684R#>QZte2enS*c}nYr{*jtjfAz&=wKA8A(Z;S6mdA z+crq{kh4TDwa%u0Urs#0R86r+zOGJ2MIi1DMJSqk(2v7zG8dmk&wn`^8E&ej`bz^O z%?UZIQ2!#!A9ELmjYkKc4oN(3-xy~Ep*KwCviT!+@Az7Urv!HmWba1V&eSP^R@;Zt zRFhQVUh&M@Oc6JUSVe&qrcG^wNNXm4XwU~Ijn(&`;Q_AU9HcTQJqG<3oY`)U$^1oK zAAS9WjK}A8tlT!3zopXcSOd??J(c|&r>WOao;_m1{J;}Lg%Hm}Ive?Fi~HG2r05FI z&Iivg6q4H%4BTo3ZKFZ&ehU_r+eO`Wt7J|`{GupM+u>I??WRe8P7m4+0s01^)YA@7 zN*Zq~u22zMn=bAeh2yC3-d_U5p*HYviHLNSEJv!({35COu}7E%=u62MCNv(p|~GX8ecMtrg{L!ueS-?`RP* zJmPoY#5Q#giVwfPV(rF5-53Oh>0||xAB0Nn#@=nJn(7i(IF(T&7PrFYP`HC;%tVJk zzF@OEa5K4HCa{jkDomGFp3HVy)6|HRerg zG4#F@LZ+L$^PGcPFKL-U%y=*N98)$@aoJMGPtGCZJD$nhNo__<6*Kx%Gg7SMRvE}a zapXFH+ExpnyrX1<_-32B0A>Fuq2zOGF!eOXF~7nG$vG~V)^x4x*S^+?Rz6#tE{@De>?AjwO33)L)Y|)UTOP-SyQWLLV}7#sZBTZ#dCLylOR)( z=L5q&LOWOwS!Wv=d4bmNOSaT~!D_=x7AC%2{fiUX9cac?)sDK%Wi;W#WoZdvWsSmx zj8MgQUd%f#rj)9)k6}4Vld9Lz8!2^cgA6s60MpSkWtv0CV-fsy=GYMRJ(5(tiNp`J z;cp`eh|r)FZG9{tVYUkeoeYNKA$BJ5!Cj1DY2cN9bGPWI5C;c+iNVeB92>&uo=mQU zufSN*HMEFw*Y2YMo__b3IB*Ut=t2z7;7Gv2Xp4t99CMG5qT#fj!}QX&3ogr|?oSfr zY3t!tM
bo(a<=FaqB>4pK zKv3S7J?X7UcU)b(`ZEXmhxGILI&$aTK@M&-(KekPH7iam{pdts%62OZ#5ROj52$## z8o-=TA>*|T=lCe)``A;Sz)B}321N`pvuu)D` zjh$rSDN+8UbP`CE)t29+L`h^J8Nfe-aWoifFsFpr_nb#O)$??FrSA7|BLCMP#R0RB zKujFQAbs-y{`F#s@=M+KEZF7gIhDa;;NGhzI*#V?sn{aR)9-50>1pvczC&#rF`{eR zv7fLVvfE9>>4SKZ*^qgwbq&6yx<(wVX$emTD?vi)&jGLRYd1qEPhD(!ghq|l#hZjU~gl* z&XZ6i0c;Z38u?AwhfC~57Z#2Q{ljxMxr28xc?PRlJae;>pIx-1nu38B4`F8?KLwG8 z48Yd0vip2zC~WH#!6@!}>##Y3QXCLMuw(*7TcR$kB%Pmsa6uEr)!Rwfc|VKn>8B(3 zDw3nLornx4hc)Hk0nvh{nfLtD*f3bMnQQ9CqhL)Sg6|~{CVU}hUU8|?; z6V**I0@A#SV*=&!CzQK?z1oV#1f1si?*7XC42i5!v! z!P`#O*_-A2?eOkd<#EV|n|br^k$YwN0qFp6xz#P}n@@=?UY~85*BP}5Kb<}4W!y~M zp}QaNS(sTksou$U+UcLMq#zbv#dP-(#6-p-=sgW~-RkbIy5w8a8$1^*Fml5C{RW+Sj3qI=38f!IT z_I4x%Y#vCdG)Su88})S}b``ACLeC8=`wyXuucil#JSq2g$gQP)S9E0K$5~i>r3aIy zb|Q}-RR`+G$*c5cA8xf#%daC)6~7BvyI2V6%NP83C?paqDlL-3C6t+$u7eANzRhI{ z_h^G?_XrcWR9RBVPl_}T>SM{V(U0$RRoHq@ zdbzM>jEQEyF*%dH{9=(I;pv6GOg_|KpKGw7D2UFx-F%$6gQu9IDxK}d6AR0GE;Lfp zanj9pb#OGX^+@e6h>z&D#}tcO?8K+SLWS^vmQ;qgg-yQe;t|i6`sMB*twoocx?uNi z8vhAr?$US?h}sVsS4462t^mb1w1ezo@B!IKcfD!WIoFg<8m; z2(!wU!|vtnyxfQPp~r=W3<@ZZZ>2>kN7hrBF6Qraup$yQQ?3W=3WHQdYkW_gWxOTN z2|*rv4|zS1$XgeDRwrHxV^vfh1>K0N$VtfYu=_M;BJRg;p`p9x z;ya_)98^lMmjT0KJOhL9r}Z&k=S>hM1lFOqseJGc$_4feGxrioIZ4TbF4hpAKB`aXUXu%CYh1fJcBg4{=jelWi^x1R{}FK$!c)g zjAL)po-jpxc=r$G^^RU;vC9@&6Kz9>Gp0bUo=>&TmW-t?$7?IyWn_i=IX@1xDxd{T zkIHIakYf@!Dcui_N*lZ~NBOJBnLp2vGSI+S>!pwPM|v)fT3$@gW0n8u27Gs)7H6&2`dQs0&o1$^Ysyj?jUDv7j6b({*^E5;Z>s)!_+hwE%U z8A4o3>!?Qf0hq^V2l&a3cbZFL5AOXa&mh7y!t*(Vm{ z;Kc-utR*DSIFpPDTQ%R$AtS~SZc8T`6gqW9J6QyQEcdcrj+HaOd4RzsRunArAsVF+^VO=?u`4MS(%jP(F>VgrQa&R?2|sB@($LJZWvWz;MUu#!G3}0DB#t z%gcQ?Mlmeh^N2sB=Ysi9uig=sZ>eHEY?^QW33^}zZ2#i1`|@o1h@qbaD8HO*1|z4Z N+k2n#`0B63{{UIVm}>w4 diff --git a/pgcat.minimal.toml b/pgcat.minimal.toml deleted file mode 100644 index 4b17a45..0000000 --- a/pgcat.minimal.toml +++ /dev/null @@ -1,22 +0,0 @@ -# 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.service b/pgcat.service deleted file mode 100644 index 3b27cb3..0000000 --- a/pgcat.service +++ /dev/null @@ -1,17 +0,0 @@ -[Unit] -Description=PgCat pooler -After=network.target -StartLimitIntervalSec=0 - -[Service] -User=pgcat -Type=simple -Restart=always -RestartSec=1 -Environment=RUST_LOG=info -LimitNOFILE=65536 -ExecStart=/usr/bin/pgcat /etc/pgcat.toml -ExecReload=/bin/kill -SIGHUP $MAINPID - -[Install] -WantedBy=multi-user.target diff --git a/pgcat.toml b/pgcat.toml deleted file mode 100644 index 9e19c13..0000000 --- a/pgcat.toml +++ /dev/null @@ -1,350 +0,0 @@ -# -# PgCat config example. -# - -# -# General pooler settings -[general] -# What IP to run on, 0.0.0.0 means accessible from everywhere. -host = "0.0.0.0" - -# Port to run on, same as PgBouncer used in this example. -port = 6432 - -# Whether to enable prometheus exporter or not. -enable_prometheus_exporter = true - -# Port at which prometheus exporter listens on. -prometheus_exporter_port = 9930 - -# How long to wait before aborting a server connection (ms). -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 - -# How much time to give the health check query to return with a result (ms). -healthcheck_timeout = 1000 # milliseconds - -# How long to keep connection available for immediate re-use, without running a healthcheck query on it -healthcheck_delay = 30000 # milliseconds - -# How much time to give clients during shutdown before forcibly killing client connections (ms). -shutdown_timeout = 60000 # milliseconds - -# How long to ban a server if it fails a health check (seconds). -ban_time = 60 # seconds - -# If we should log client connections -log_client_connections = false - -# If we should log client disconnections -log_client_disconnections = false - -# When set to true, PgCat reloads configs if it detects a change in the config file. -autoreload = 15000 - -# Number of worker threads the Runtime will use (4 by default). -worker_threads = 5 - -# Number of seconds of connection idleness to wait before sending a keepalive packet to the server. -tcp_keepalives_idle = 5 -# Number of unacknowledged keepalive packets allowed before giving up and closing the connection. -tcp_keepalives_count = 5 -# Number of seconds between keepalive packets. -tcp_keepalives_interval = 5 - -# 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 = ".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.. -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 -# `postgres://sharding_user:sharding_user@pgcat_host:pgcat_port/sharded_db` -[pools.sharded_db] -# Pool mode (see PgBouncer docs for more). -# `session` one server connection per connected client -# `transaction` one server connection per client transaction -pool_mode = "transaction" - -# Load balancing mode -# `random` selects the server at random -# `loc` selects the server with the least outstanding busy conncetions -load_balancing_mode = "random" - -# If the client doesn't specify, PgCat routes traffic to this role by default. -# `any` round-robin between primary and replicas, -# `replica` round-robin between replicas only without touching the primary, -# `primary` all queries go to the primary unless otherwise specified. -default_role = "any" - -# Prepared statements cache size. -# TODO: update documentation -prepared_statements_cache_size = 500 - -# If Query Parser is enabled, we'll attempt to parse -# every incoming query to determine if it's a read or a write. -# If it's a read query, we'll direct it to a replica. Otherwise, if it's a write, -# we'll direct it to the primary. -query_parser_enabled = true - -# If the query parser is enabled and this setting is enabled, we'll attempt to -# infer the role from the query itself. -query_parser_read_write_splitting = 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 explicitly selected with our custom protocol. -primary_reads_enabled = true - -# Allow sharding commands to be passed as statement comments instead of -# separate commands. If these are unset this functionality is disabled. -# sharding_key_regex = '/\* sharding_key: (\d+) \*/' -# shard_id_regex = '/\* shard_id: (\d+) \*/' -# regex_search_limit = 1000 # only look at the first 1000 characters of SQL statements - -# Defines the behavior when no shard is selected in a sharded system. -# `random`: picks a shard at random -# `random_healthy`: picks a shard at random favoring shards with the least number of recent errors -# `shard_`: e.g. shard_0, shard_4, etc. picks a specific shard, everytime -# no_shard_specified_behavior = "shard_0" - -# So what if you wanted to implement a different hashing function, -# or you've already built one and you want this pooler to use it? -# Current options: -# `pg_bigint_hash`: PARTITION BY HASH (Postgres hashing function) -# `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 usename, passwd FROM pg_shadow WHERE usename='$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" - -# Idle timeout can be overwritten in the pool -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 section holds the credentials for users that may connect to this cluster -[pools.sharded_db.users.0] -# PostgreSQL username used to authenticate the user and connect to the server -# if `server_username` is not set. -username = "sharding_user" - -# PostgreSQL password used to authenticate the user and connect to the server -# if `server_password` is not set. -password = "sharding_user" - -pool_mode = "transaction" - -# 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 - -[pools.sharded_db.users.1] -username = "other_user" -password = "other_user" -pool_size = 21 -statement_timeout = 15000 -connect_timeout = 1000 -idle_timeout = 1000 - -# Shard configs are structured as pool..shards. -# Each shard config contains a list of servers that make up the shard -# and the database name to use. -[pools.sharded_db.shards.0] -# Array of servers in the shard, each server entry is an array of `[host, port, role]` -servers = [["127.0.0.1", 5432, "primary"], ["localhost", 5432, "replica"]] - -# Array of mirrors for the shard, each mirror entry is an array of `[host, port, index of server in servers array]` -# Traffic hitting the server identified by the index will be sent to the mirror. -# mirrors = [["1.2.3.4", 5432, 0], ["1.2.3.4", 5432, 1]] - -# Database name (e.g. "postgres") -database = "shard0" - -[pools.sharded_db.shards.1] -servers = [["127.0.0.1", 5432, "primary"], ["localhost", 5432, "replica"]] -database = "shard1" - -[pools.sharded_db.shards.2] -servers = [["127.0.0.1", 5432, "primary" ], ["localhost", 5432, "replica" ]] -database = "shard2" - - -[pools.simple_db] -pool_mode = "session" -default_role = "primary" -query_parser_enabled = true -primary_reads_enabled = true -sharding_function = "pg_bigint_hash" - -[pools.simple_db.users.0] -username = "simple_user" -password = "simple_user" -pool_size = 5 -min_pool_size = 3 -server_lifetime = 60000 -statement_timeout = 0 - -[pools.simple_db.shards.0] -servers = [ - [ "127.0.0.1", 5432, "primary" ], - [ "localhost", 5432, "replica" ] -] -database = "some_db" diff --git a/postinst b/postinst deleted file mode 100644 index 520599a..0000000 --- a/postinst +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -set -e - -systemctl daemon-reload -systemctl enable pgcat - -if ! id pgcat 2> /dev/null; then - useradd -s /usr/bin/false pgcat -fi - -if [ -f /etc/pgcat.toml ]; then - systemctl start pgcat -fi diff --git a/postrm b/postrm deleted file mode 100644 index ec1d0f7..0000000 --- a/postrm +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -set -e - -systemctl daemon-reload diff --git a/prerm b/prerm deleted file mode 100644 index 601b699..0000000 --- a/prerm +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -set -e - -systemctl stop pgcat -systemctl disable pgcat diff --git a/src/admin.rs b/src/admin.rs deleted file mode 100644 index f08ef2e..0000000 --- a/src/admin.rs +++ /dev/null @@ -1,999 +0,0 @@ -use crate::pool::BanReason; -use crate::server::ServerParameters; -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_client_stats, get_server_stats, ClientState, ServerState}; - -pub fn generate_server_parameters_for_admin() -> ServerParameters { - let mut server_parameters = ServerParameters::new(); - - server_parameters.set_param("application_name".to_string(), "".to_string(), true); - server_parameters.set_param("client_encoding".to_string(), "UTF8".to_string(), true); - server_parameters.set_param("server_encoding".to_string(), "UTF8".to_string(), true); - server_parameters.set_param("server_version".to_string(), VERSION.to_string(), true); - server_parameters.set_param("DateStyle".to_string(), "ISO, MDY".to_string(), true); - - server_parameters -} - -/// Handle admin client. -pub async fn handle_admin( - stream: &mut T, - mut query: BytesMut, - client_server_map: ClientServerMap, -) -> Result<(), Error> -where - T: tokio::io::AsyncWrite + std::marker::Unpin, -{ - let code = query.get_u8() as char; - - if code != 'Q' { - return Err(Error::ProtocolSyncError(format!( - "Invalid code, expected 'Q' but got '{}'", - code - ))); - } - - let len = query.get_i32() as usize; - let query = String::from_utf8_lossy(&query[..len - 5]).to_string(); - - trace!("Admin query: {}", query); - - let query_parts: Vec<&str> = query.trim_end_matches(';').split_whitespace().collect(); - - match query_parts - .first() - .unwrap_or(&"") - .to_ascii_uppercase() - .as_str() - { - "BAN" => { - trace!("BAN"); - ban(stream, query_parts).await - } - "UNBAN" => { - trace!("UNBAN"); - unban(stream, query_parts).await - } - "RELOAD" => { - trace!("RELOAD"); - reload(stream, client_server_map).await - } - "SET" => { - trace!("SET"); - ignore_set(stream).await - } - "PAUSE" => { - trace!("PAUSE"); - pause(stream, query_parts).await - } - "RESUME" => { - trace!("RESUME"); - resume(stream, query_parts).await - } - "SHUTDOWN" => { - trace!("SHUTDOWN"); - shutdown(stream).await - } - "SHOW" => match query_parts - .get(1) - .unwrap_or(&"") - .to_ascii_uppercase() - .as_str() - { - "HELP" => { - trace!("SHOW HELP"); - show_help(stream).await - } - "BANS" => { - trace!("SHOW BANS"); - show_bans(stream).await - } - "CONFIG" => { - trace!("SHOW CONFIG"); - show_config(stream).await - } - "DATABASES" => { - trace!("SHOW DATABASES"); - show_databases(stream).await - } - "LISTS" => { - trace!("SHOW LISTS"); - show_lists(stream).await - } - "POOLS" => { - trace!("SHOW POOLS"); - show_pools(stream).await - } - "CLIENTS" => { - trace!("SHOW CLIENTS"); - show_clients(stream).await - } - "SERVERS" => { - trace!("SHOW SERVERS"); - show_servers(stream).await - } - "STATS" => { - trace!("SHOW STATS"); - show_stats(stream).await - } - "VERSION" => { - trace!("SHOW VERSION"); - show_version(stream).await - } - "USERS" => { - trace!("SHOW USERS"); - show_users(stream).await - } - _ => error_response(stream, "Unsupported SHOW query against the admin database").await, - }, - _ => error_response(stream, "Unsupported query against the admin database").await, - } -} - -/// Column-oriented statistics. -async fn show_lists(stream: &mut T) -> Result<(), Error> -where - T: tokio::io::AsyncWrite + std::marker::Unpin, -{ - let client_stats = get_client_stats(); - let server_stats = get_server_stats(); - - let columns = vec![("list", DataType::Text), ("items", DataType::Int4)]; - - let mut users = 1; - let mut databases = 1; - for (_, pool) in get_all_pools() { - databases += pool.databases(); - users += 1; // One user per pool - } - let mut res = BytesMut::new(); - res.put(row_description(&columns)); - res.put(data_row(&vec![ - "databases".to_string(), - databases.to_string(), - ])); - res.put(data_row(&vec!["users".to_string(), users.to_string()])); - res.put(data_row(&vec!["pools".to_string(), databases.to_string()])); - res.put(data_row(&vec![ - "free_clients".to_string(), - client_stats - .keys() - .filter(|client_id| { - client_stats - .get(client_id) - .unwrap() - .state - .load(Ordering::Relaxed) - == ClientState::Idle - }) - .count() - .to_string(), - ])); - res.put(data_row(&vec![ - "used_clients".to_string(), - client_stats - .keys() - .filter(|client_id| { - client_stats - .get(client_id) - .unwrap() - .state - .load(Ordering::Relaxed) - == ClientState::Active - }) - .count() - .to_string(), - ])); - res.put(data_row(&vec![ - "login_clients".to_string(), - "0".to_string(), - ])); - res.put(data_row(&vec![ - "free_servers".to_string(), - server_stats - .keys() - .filter(|server_id| { - server_stats - .get(server_id) - .unwrap() - .state - .load(Ordering::Relaxed) - == ServerState::Idle - }) - .count() - .to_string(), - ])); - res.put(data_row(&vec![ - "used_servers".to_string(), - server_stats - .keys() - .filter(|server_id| { - server_stats - .get(server_id) - .unwrap() - .state - .load(Ordering::Relaxed) - == ServerState::Active - }) - .count() - .to_string(), - ])); - res.put(data_row(&vec!["dns_names".to_string(), "0".to_string()])); - res.put(data_row(&vec!["dns_zones".to_string(), "0".to_string()])); - res.put(data_row(&vec!["dns_queries".to_string(), "0".to_string()])); - res.put(data_row(&vec!["dns_pending".to_string(), "0".to_string()])); - - res.put(command_complete("SHOW")); - - res.put_u8(b'Z'); - res.put_i32(5); - res.put_u8(b'I'); - - write_all_half(stream, &res).await -} - -/// Show PgCat version. -async fn show_version(stream: &mut T) -> Result<(), Error> -where - T: tokio::io::AsyncWrite + std::marker::Unpin, -{ - let mut res = BytesMut::new(); - - res.put(row_description(&vec![("version", DataType::Text)])); - res.put(data_row(&vec![format!("PgCat {}", VERSION)])); - res.put(command_complete("SHOW")); - - res.put_u8(b'Z'); - res.put_i32(5); - res.put_u8(b'I'); - - write_all_half(stream, &res).await -} - -/// Show utilization of connection pools for each shard and replicas. -async fn show_pools(stream: &mut T) -> Result<(), Error> -where - T: tokio::io::AsyncWrite + std::marker::Unpin, -{ - 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")); - - // 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(); - - let detail_msg = [ - "", - "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", - ]; - - res.put(notify("Console usage", detail_msg.join("\n\t"))); - res.put(command_complete("SHOW")); - - // ReadyForQuery - res.put_u8(b'Z'); - res.put_i32(5); - res.put_u8(b'I'); - - write_all_half(stream, &res).await -} - -/// Show shards and replicas. -async fn show_databases(stream: &mut T) -> Result<(), Error> -where - T: tokio::io::AsyncWrite + std::marker::Unpin, -{ - // Columns - let columns = vec![ - ("name", DataType::Text), - ("host", DataType::Text), - ("port", DataType::Text), - ("database", DataType::Text), - ("force_user", DataType::Text), - ("pool_size", DataType::Int4), - ("min_pool_size", DataType::Int4), - ("reserve_pool", DataType::Int4), - ("pool_mode", DataType::Text), - ("max_connections", DataType::Int4), - ("current_connections", DataType::Int4), - ("paused", DataType::Int4), - ("disabled", DataType::Int4), - ]; - - let mut res = BytesMut::new(); - - res.put(row_description(&columns)); - - for (_, pool) in get_all_pools() { - let pool_config = pool.settings.clone(); - for shard in 0..pool.shards() { - let database_name = &pool.address(shard, 0).database; - for server in 0..pool.servers(shard) { - let address = pool.address(shard, server); - let pool_state = pool.pool_state(shard, server); - let banned = pool.is_banned(address); - 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 - 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(), - false => "0".to_string(), - }, - match banned { - // disabled - true => "1".to_string(), - false => "0".to_string(), - }, - ])); - } - } - } - res.put(command_complete("SHOW")); - - // ReadyForQuery - res.put_u8(b'Z'); - res.put_i32(5); - res.put_u8(b'I'); - - write_all_half(stream, &res).await -} - -/// Ignore any SET commands the client sends. -/// This is common initialization done by ORMs. -async fn ignore_set(stream: &mut T) -> Result<(), Error> -where - T: tokio::io::AsyncWrite + std::marker::Unpin, -{ - custom_protocol_response_ok(stream, "SET").await -} - -/// Bans a host from being used -async fn ban(stream: &mut T, tokens: Vec<&str>) -> Result<(), Error> -where - T: tokio::io::AsyncWrite + std::marker::Unpin, -{ - let host = match tokens.get(1) { - Some(host) => host, - None => return error_response(stream, "usage: BAN hostname duration_seconds").await, - }; - - let duration_seconds = match tokens.get(2) { - Some(duration_seconds) => match duration_seconds.parse::() { - Ok(duration_seconds) => duration_seconds, - Err(_) => { - return error_response(stream, "duration_seconds must be an integer").await; - } - }, - None => return error_response(stream, "usage: BAN hostname duration_seconds").await, - }; - - if duration_seconds <= 0 { - return error_response(stream, "duration_seconds must be >= 0").await; - } - - let columns = vec![ - ("db", DataType::Text), - ("user", DataType::Text), - ("role", DataType::Text), - ("host", DataType::Text), - ]; - let mut res = BytesMut::new(); - res.put(row_description(&columns)); - - 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), None); - res.put(data_row(&vec![ - id.db.clone(), - id.user.clone(), - address.role.to_string(), - address.host, - ])); - } - } - } - - res.put(command_complete("BAN")); - - // ReadyForQuery - res.put_u8(b'Z'); - res.put_i32(5); - res.put_u8(b'I'); - - write_all_half(stream, &res).await -} - -/// Clear a host for use -async fn unban(stream: &mut T, tokens: Vec<&str>) -> Result<(), Error> -where - T: tokio::io::AsyncWrite + std::marker::Unpin, -{ - let host = match tokens.get(1) { - Some(host) => host, - None => return error_response(stream, "UNBAN command requires a hostname to unban").await, - }; - - let columns = vec![ - ("db", DataType::Text), - ("user", DataType::Text), - ("role", DataType::Text), - ("host", DataType::Text), - ]; - let mut res = BytesMut::new(); - res.put(row_description(&columns)); - - for (id, pool) in get_all_pools().iter() { - for address in pool.get_addresses_from_host(host) { - if pool.is_banned(&address) { - pool.unban(&address); - res.put(data_row(&vec![ - id.db.clone(), - id.user.clone(), - address.role.to_string(), - address.host, - ])); - } - } - } - - res.put(command_complete("UNBAN")); - - // ReadyForQuery - res.put_u8(b'Z'); - res.put_i32(5); - res.put_u8(b'I'); - - write_all_half(stream, &res).await -} - -/// Shows all the bans -async fn show_bans(stream: &mut T) -> Result<(), Error> -where - T: tokio::io::AsyncWrite + std::marker::Unpin, -{ - let columns = vec![ - ("db", DataType::Text), - ("user", DataType::Text), - ("role", DataType::Text), - ("host", DataType::Text), - ("reason", DataType::Text), - ("ban_time", DataType::Text), - ("ban_duration_seconds", DataType::Text), - ("ban_remaining_seconds", DataType::Text), - ]; - let mut res = BytesMut::new(); - res.put(row_description(&columns)); - - // The block should be pretty quick so we cache the time outside - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs() as i64; - - for (id, pool) in get_all_pools().iter() { - for (address, (ban_reason, ban_time)) in pool.get_bans().iter() { - let ban_duration = match ban_reason { - BanReason::AdminBan(duration) => *duration, - _ => pool.settings.ban_time, - }; - let remaining = ban_duration - (now - ban_time.timestamp()); - if remaining <= 0 { - continue; - } - res.put(data_row(&vec![ - id.db.clone(), - id.user.clone(), - address.role.to_string(), - address.host.clone(), - format!("{:?}", ban_reason), - ban_time.to_string(), - ban_duration.to_string(), - remaining.to_string(), - ])); - } - } - - res.put(command_complete("SHOW BANS")); - - // ReadyForQuery - res.put_u8(b'Z'); - res.put_i32(5); - res.put_u8(b'I'); - - write_all_half(stream, &res).await -} - -/// Reload the configuration file without restarting the process. -async fn reload(stream: &mut T, client_server_map: ClientServerMap) -> Result<(), Error> -where - T: tokio::io::AsyncWrite + std::marker::Unpin, -{ - info!("Reloading config"); - - reload_config(client_server_map).await?; - - get_config().show(); - - let mut res = BytesMut::new(); - - res.put(command_complete("RELOAD")); - - // ReadyForQuery - res.put_u8(b'Z'); - res.put_i32(5); - res.put_u8(b'I'); - - write_all_half(stream, &res).await -} - -/// Shows current configuration. -async fn show_config(stream: &mut T) -> Result<(), Error> -where - T: tokio::io::AsyncWrite + std::marker::Unpin, -{ - let config = &get_config(); - let config: HashMap = config.into(); - - // Configs that cannot be changed without restarting. - let immutables = ["host", "port", "connect_timeout"]; - - // Columns - let columns = vec![ - ("key", DataType::Text), - ("value", DataType::Text), - ("default", DataType::Text), - ("changeable", DataType::Text), - ]; - - // Response data - let mut res = BytesMut::new(); - res.put(row_description(&columns)); - - // DataRow rows - for (key, value) in config { - let changeable = if immutables.iter().filter(|col| *col == &key).count() == 1 { - "no".to_string() - } else { - "yes".to_string() - }; - - let row = vec![key, value, "-".to_string(), changeable]; - - res.put(data_row(&row)); - } - - res.put(command_complete("SHOW")); - - // ReadyForQuery - res.put_u8(b'Z'); - res.put_i32(5); - res.put_u8(b'I'); - - write_all_half(stream, &res).await -} - -/// Show shard and replicas statistics. -async fn show_stats(stream: &mut T) -> Result<(), Error> -where - T: tokio::io::AsyncWrite + std::marker::Unpin, -{ - let columns = vec![ - ("instance", DataType::Text), - ("database", DataType::Text), - ("user", DataType::Text), - ("total_xact_count", DataType::Numeric), - ("total_query_count", DataType::Numeric), - ("total_received", DataType::Numeric), - ("total_sent", DataType::Numeric), - ("total_xact_time", DataType::Numeric), - ("total_query_time", DataType::Numeric), - ("total_wait_time", DataType::Numeric), - ("total_errors", DataType::Numeric), - ("avg_xact_count", DataType::Numeric), - ("avg_query_count", DataType::Numeric), - ("avg_recv", DataType::Numeric), - ("avg_sent", DataType::Numeric), - ("avg_errors", DataType::Numeric), - ("avg_xact_time", DataType::Numeric), - ("avg_query_time", DataType::Numeric), - ("avg_wait_time", DataType::Numeric), - ]; - - let mut res = BytesMut::new(); - res.put(row_description(&columns)); - - for (user_pool, pool) in get_all_pools() { - for shard in 0..pool.shards() { - for server in 0..pool.servers(shard) { - let address = pool.address(shard, server); - - let mut row = vec![address.name(), user_pool.db.clone(), user_pool.user.clone()]; - let stats = address.stats.clone(); - stats.populate_row(&mut row); - - res.put(data_row(&row)); - } - } - } - - res.put(command_complete("SHOW")); - - // ReadyForQuery - res.put_u8(b'Z'); - res.put_i32(5); - res.put_u8(b'I'); - - write_all_half(stream, &res).await -} - -/// Show currently connected clients -async fn show_clients(stream: &mut T) -> Result<(), Error> -where - T: tokio::io::AsyncWrite + std::marker::Unpin, -{ - let columns = vec![ - ("client_id", DataType::Text), - ("database", DataType::Text), - ("user", DataType::Text), - ("application_name", DataType::Text), - ("state", DataType::Text), - ("transaction_count", DataType::Numeric), - ("query_count", DataType::Numeric), - ("error_count", DataType::Numeric), - ("age_seconds", DataType::Numeric), - ("maxwait", DataType::Numeric), - ("maxwait_us", DataType::Numeric), - ]; - - let new_map = get_client_stats(); - let mut res = BytesMut::new(); - res.put(row_description(&columns)); - - for (_, client) in new_map { - let max_wait = client.max_wait_time.load(Ordering::Relaxed); - let row = vec![ - 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()) - .as_secs() - .to_string(), - (max_wait / 1_000_000).to_string(), - (max_wait % 1_000_000).to_string(), - ]; - - res.put(data_row(&row)); - } - - res.put(command_complete("SHOW")); - - // ReadyForQuery - res.put_u8(b'Z'); - res.put_i32(5); - res.put_u8(b'I'); - - write_all_half(stream, &res).await -} - -/// Show currently connected servers -async fn show_servers(stream: &mut T) -> Result<(), Error> -where - T: tokio::io::AsyncWrite + std::marker::Unpin, -{ - let columns = vec![ - ("server_id", DataType::Text), - ("database_name", DataType::Text), - ("user", DataType::Text), - ("address_id", DataType::Text), - ("application_name", DataType::Text), - ("state", DataType::Text), - ("transaction_count", DataType::Numeric), - ("query_count", DataType::Numeric), - ("bytes_sent", DataType::Numeric), - ("bytes_received", DataType::Numeric), - ("age_seconds", DataType::Numeric), - ("prepare_cache_hit", DataType::Numeric), - ("prepare_cache_miss", DataType::Numeric), - ("prepare_cache_eviction", DataType::Numeric), - ("prepare_cache_size", DataType::Numeric), - ]; - - let new_map = get_server_stats(); - let mut res = BytesMut::new(); - 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(), - 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()) - .as_secs() - .to_string(), - server - .prepared_hit_count - .load(Ordering::Relaxed) - .to_string(), - server - .prepared_miss_count - .load(Ordering::Relaxed) - .to_string(), - server - .prepared_eviction_count - .load(Ordering::Relaxed) - .to_string(), - server - .prepared_cache_size - .load(Ordering::Relaxed) - .to_string(), - ]; - - res.put(data_row(&row)); - } - - res.put(command_complete("SHOW")); - - // ReadyForQuery - res.put_u8(b'Z'); - res.put_i32(5); - res.put_u8(b'I'); - - write_all_half(stream, &res).await -} - -/// Pause a pool. It won't pass any more queries to the backends. -async fn pause(stream: &mut T, tokens: Vec<&str>) -> Result<(), Error> -where - T: tokio::io::AsyncWrite + std::marker::Unpin, -{ - let parts: Vec<&str> = match tokens.len() == 2 { - true => tokens[1].split(',').map(|part| part.trim()).collect(), - false => Vec::new(), - }; - - match parts.len() { - 0 => { - for (_, pool) in get_all_pools() { - pool.pause(); - } - - let mut res = BytesMut::new(); - - res.put(command_complete("PAUSE")); - - // ReadyForQuery - res.put_u8(b'Z'); - res.put_i32(5); - res.put_u8(b'I'); - - write_all_half(stream, &res).await - } - 2 => { - let database = parts[0]; - let user = parts[1]; - - match get_pool(database, user) { - Some(pool) => { - pool.pause(); - - let mut res = BytesMut::new(); - - res.put(command_complete(&format!("PAUSE {},{}", database, user))); - - // ReadyForQuery - res.put_u8(b'Z'); - res.put_i32(5); - res.put_u8(b'I'); - - write_all_half(stream, &res).await - } - - None => { - error_response( - stream, - &format!( - "No pool configured for database: {}, user: {}", - database, user - ), - ) - .await - } - } - } - _ => error_response(stream, "usage: PAUSE [db, user]").await, - } -} - -/// Resume a pool. Queries are allowed again. -async fn resume(stream: &mut T, tokens: Vec<&str>) -> Result<(), Error> -where - T: tokio::io::AsyncWrite + std::marker::Unpin, -{ - let parts: Vec<&str> = match tokens.len() == 2 { - true => tokens[1].split(',').map(|part| part.trim()).collect(), - false => Vec::new(), - }; - - match parts.len() { - 0 => { - for (_, pool) in get_all_pools() { - pool.resume(); - } - - let mut res = BytesMut::new(); - - res.put(command_complete("RESUME")); - - // ReadyForQuery - res.put_u8(b'Z'); - res.put_i32(5); - res.put_u8(b'I'); - - write_all_half(stream, &res).await - } - 2 => { - let database = parts[0]; - let user = parts[1]; - - match get_pool(database, user) { - Some(pool) => { - pool.resume(); - - let mut res = BytesMut::new(); - - res.put(command_complete(&format!("RESUME {},{}", database, user))); - - // ReadyForQuery - res.put_u8(b'Z'); - res.put_i32(5); - res.put_u8(b'I'); - - write_all_half(stream, &res).await - } - - None => { - error_response( - stream, - &format!( - "No pool configured for database: {}, user: {}", - database, user - ), - ) - .await - } - } - } - _ => error_response(stream, "usage: RESUME [db, user]").await, - } -} - -/// Send response packets for shutdown. -async fn shutdown(stream: &mut T) -> Result<(), Error> -where - T: tokio::io::AsyncWrite + std::marker::Unpin, -{ - let mut res = BytesMut::new(); - - res.put(row_description(&vec![("success", DataType::Text)])); - - let mut shutdown_success = "t"; - - let pid = std::process::id(); - if signal::kill(Pid::from_raw(pid.try_into().unwrap()), Signal::SIGINT).is_err() { - error!("Unable to send SIGINT to PID: {}", pid); - shutdown_success = "f"; - } - - res.put(data_row(&vec![shutdown_success.to_string()])); - - res.put(command_complete("SHUTDOWN")); - - res.put_u8(b'Z'); - res.put_i32(5); - res.put_u8(b'I'); - - write_all_half(stream, &res).await -} - -/// Show Users. -async fn show_users(stream: &mut T) -> Result<(), Error> -where - T: tokio::io::AsyncWrite + std::marker::Unpin, -{ - let mut res = BytesMut::new(); - - res.put(row_description(&vec![ - ("name", DataType::Text), - ("pool_mode", DataType::Text), - ])); - - for (user_pool, pool) in get_all_pools() { - let pool_config = &pool.settings; - res.put(data_row(&vec![ - user_pool.user.clone(), - pool_config.pool_mode.to_string(), - ])); - } - - res.put(command_complete("SHOW")); - - res.put_u8(b'Z'); - res.put_i32(5); - res.put_u8(b'I'); - - write_all_half(stream, &res).await -} diff --git a/src/auth_passthrough.rs b/src/auth_passthrough.rs deleted file mode 100644 index 53ef93d..0000000 --- a/src/auth_passthrough.rs +++ /dev/null @@ -1,138 +0,0 @@ -use crate::config::AuthType; -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(), - auth_type: AuthType::MD5, - 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, - connect_timeout: None, - idle_timeout: 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 deleted file mode 100644 index 405d72b..0000000 --- a/src/client.rs +++ /dev/null @@ -1,2079 +0,0 @@ -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, VecDeque}; -use std::sync::{atomic::AtomicUsize, Arc}; -use std::time::Instant; -use tokio::io::{split, AsyncReadExt, BufReader, ReadHalf, WriteHalf}; -use tokio::net::TcpStream; -use tokio::sync::broadcast::Receiver; -use tokio::sync::mpsc::Sender; - -use crate::admin::{generate_server_parameters_for_admin, handle_admin}; -use crate::auth_passthrough::refetch_auth_hash; -use crate::config::{ - get_config, get_idle_client_in_transaction_timeout, Address, AuthType, 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, ServerParameters}; -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, - Tls, - CancelQuery, -} - -/// The client state. One of these is created per client. -pub struct Client { - /// The reads are buffered (8K by default). - read: BufReader, - - /// We buffer the writes ourselves because we know the protocol - /// better than a stock buffer. - write: T, - - /// Internal buffer, where we place messages until we have to flush - /// them to the backend. - buffer: BytesMut, - - /// Used to buffer response messages to the client - response_message_queue_buffer: BytesMut, - - /// Address - addr: std::net::SocketAddr, - - /// The client was started with the sole reason to cancel another running query. - cancel_mode: bool, - - /// In transaction mode, the connection is released after each transaction. - /// Session mode has slightly higher throughput per client, but lower capacity. - transaction_mode: bool, - - /// For query cancellation, the client is given a random process ID and secret on startup. - process_id: i32, - secret_key: i32, - - /// Clients are mapped to servers while they use them. This allows a client - /// to connect and cancel a query. - client_server_map: ClientServerMap, - - /// Client parameters, e.g. user, client_encoding, etc. - #[allow(dead_code)] - parameters: HashMap, - - /// Statistics related to this client - stats: Arc, - - /// Clients want to talk to admin database. - admin: bool, - - /// Last address the client talked to. - last_address_id: Option, - - /// Last server process stats we talked to. - last_server_stats: Option>, - - /// Connected to server - connected_to_server: bool, - - /// Name of the server pool for this client (This comes from the database name in the connection string) - pool_name: String, - - /// Postgres user for this client (This comes from the user in the connection string) - username: String, - - /// Server startup and session parameters that we're going to track - server_parameters: ServerParameters, - - /// Used to notify clients about an impending shutdown - shutdown: Receiver<()>, - - /// Whether prepared statements are enabled for this client - prepared_statements_enabled: bool, - - /// Mapping of client named prepared statement to rewritten parse messages - prepared_statements: HashMap, u64)>, - - /// Buffered extended protocol data - extended_protocol_data_buffer: VecDeque, -} - -/// Client entrypoint. -pub async fn client_entrypoint( - mut stream: TcpStream, - client_server_map: ClientServerMap, - shutdown: Receiver<()>, - drain: Sender, - admin_only: bool, - tls_certificate: Option, - log_client_connections: bool, -) -> Result<(), Error> { - // Figure out if the client wants TLS or not. - let addr = match stream.peer_addr() { - Ok(addr) => addr, - Err(err) => { - return Err(Error::SocketError(format!( - "Failed to get peer address: {:?}", - err - ))); - } - }; - - match get_startup::(&mut stream).await { - // Client requested a TLS connection. - Ok((ClientConnectionType::Tls, _)) => { - // TLS settings are configured, will setup TLS now. - if tls_certificate.is_some() { - debug!("Accepting TLS request"); - - let mut yes = BytesMut::new(); - yes.put_u8(b'S'); - write_all(&mut stream, yes).await?; - - // Negotiate TLS. - match startup_tls(stream, client_server_map, shutdown, admin_only).await { - Ok(mut client) => { - if log_client_connections { - info!("Client {:?} connected (TLS)", addr); - } else { - debug!("Client {:?} connected (TLS)", addr); - } - - if !client.is_admin() { - let _ = drain.send(1).await; - } - - let result = client.handle().await; - - if !client.is_admin() { - let _ = drain.send(-1).await; - } - - if result.is_err() { - client.stats.disconnect(); - } - - result - } - Err(err) => Err(err), - } - } - // TLS is not configured, we cannot offer it. - else { - // Rejecting client request for TLS. - let mut no = BytesMut::new(); - no.put_u8(b'N'); - write_all(&mut stream, no).await?; - - // Attempting regular startup. Client can disconnect now - // if they choose. - match get_startup::(&mut stream).await { - // Client accepted unencrypted connection. - Ok((ClientConnectionType::Startup, bytes)) => { - let (read, write) = split(stream); - - // Continue with regular startup. - match Client::startup( - read, - write, - addr, - bytes, - client_server_map, - shutdown, - admin_only, - ) - .await - { - Ok(mut client) => { - if log_client_connections { - info!("Client {:?} connected (plain)", addr); - } else { - debug!("Client {:?} connected (plain)", addr); - } - - if !client.is_admin() { - let _ = drain.send(1).await; - } - - let result = client.handle().await; - - if !client.is_admin() { - let _ = drain.send(-1).await; - } - - if result.is_err() { - client.stats.disconnect(); - } - - result - } - Err(err) => Err(err), - } - } - - // Client probably disconnected rejecting our plain text connection. - Ok((ClientConnectionType::Tls, _)) - | Ok((ClientConnectionType::CancelQuery, _)) => Err(Error::ProtocolSyncError( - "Bad postgres client (plain)".into(), - )), - - Err(err) => Err(err), - } - } - } - - // Client wants to use plain connection without encryption. - Ok((ClientConnectionType::Startup, bytes)) => { - let (read, write) = split(stream); - - // Continue with regular startup. - match Client::startup( - read, - write, - addr, - bytes, - client_server_map, - shutdown, - admin_only, - ) - .await - { - Ok(mut client) => { - if log_client_connections { - info!("Client {:?} connected (plain)", addr); - } else { - debug!("Client {:?} connected (plain)", addr); - } - - if !client.is_admin() { - let _ = drain.send(1).await; - } - - let result = client.handle().await; - - if !client.is_admin() { - let _ = drain.send(-1).await; - } - - if result.is_err() { - client.stats.disconnect(); - } - - result - } - Err(err) => Err(err), - } - } - - // Client wants to cancel a query. - Ok((ClientConnectionType::CancelQuery, bytes)) => { - let (read, write) = split(stream); - - // Continue with cancel query request. - match Client::cancel(read, write, addr, bytes, client_server_map, shutdown).await { - Ok(mut client) => { - info!("Client {:?} issued a cancel query request", addr); - - if !client.is_admin() { - let _ = drain.send(1).await; - } - - let result = client.handle().await; - - if !client.is_admin() { - let _ = drain.send(-1).await; - } - - if result.is_err() { - client.stats.disconnect(); - } - - result - } - - Err(err) => Err(err), - } - } - - // Something failed, probably the socket. - Err(err) => Err(err), - } -} - -/// Handle the first message the client sends. -async fn get_startup(stream: &mut S) -> Result<(ClientConnectionType, BytesMut), Error> -where - S: tokio::io::AsyncRead + std::marker::Unpin + tokio::io::AsyncWrite, -{ - // Get startup message length. - let len = match stream.read_i32().await { - Ok(len) => len, - Err(_) => return Err(Error::ClientBadStartup), - }; - - // Get the rest of the message. - let mut startup = vec![0u8; len as usize - 4]; - match stream.read_exact(&mut startup).await { - Ok(_) => (), - Err(_) => return Err(Error::ClientBadStartup), - }; - - let mut bytes = BytesMut::from(&startup[..]); - let code = bytes.get_i32(); - - match code { - // Client is requesting SSL (TLS). - SSL_REQUEST_CODE => Ok((ClientConnectionType::Tls, bytes)), - - // Client wants to use plain text, requesting regular startup. - PROTOCOL_VERSION_NUMBER => Ok((ClientConnectionType::Startup, bytes)), - - // Client is requesting to cancel a running query (plain text connection). - CANCEL_REQUEST_CODE => Ok((ClientConnectionType::CancelQuery, bytes)), - - // Something else, probably something is wrong and it's not our fault, - // e.g. badly implemented Postgres client. - _ => Err(Error::ProtocolSyncError(format!( - "Unexpected startup code: {}", - code - ))), - } -} - -/// Handle TLS connection negotiation. -pub async fn startup_tls( - stream: TcpStream, - client_server_map: ClientServerMap, - shutdown: Receiver<()>, - admin_only: bool, -) -> Result>, WriteHalf>>, Error> { - // Negotiate TLS. - let tls = Tls::new()?; - let addr = match stream.peer_addr() { - Ok(addr) => addr, - Err(err) => { - return Err(Error::SocketError(format!( - "Failed to get peer address: {:?}", - err - ))); - } - }; - - let mut stream = match tls.acceptor.accept(stream).await { - Ok(stream) => stream, - - // TLS negotiation failed. - Err(err) => { - error!("TLS negotiation failed: {:?}", err); - return Err(Error::TlsError); - } - }; - - // TLS negotiation successful. - // Continue with regular startup using encrypted connection. - match get_startup::>(&mut stream).await { - // Got good startup message, proceeding like normal except we - // are encrypted now. - Ok((ClientConnectionType::Startup, bytes)) => { - let (read, write) = split(stream); - - Client::startup( - read, - write, - addr, - bytes, - client_server_map, - shutdown, - admin_only, - ) - .await - } - - // Bad Postgres client. - Ok((ClientConnectionType::Tls, _)) | Ok((ClientConnectionType::CancelQuery, _)) => { - Err(Error::ProtocolSyncError("Bad postgres client (tls)".into())) - } - - Err(err) => Err(err), - } -} - -impl Client -where - S: tokio::io::AsyncRead + std::marker::Unpin, - T: tokio::io::AsyncWrite + std::marker::Unpin, -{ - pub fn is_admin(&self) -> bool { - self.admin - } - - /// Handle Postgres client startup after TLS negotiation is complete - /// or over plain text. - pub async fn startup( - mut read: S, - mut write: T, - addr: std::net::SocketAddr, - bytes: BytesMut, // The rest of the startup message. - client_server_map: ClientServerMap, - shutdown: Receiver<()>, - admin_only: bool, - ) -> Result, Error> { - let parameters = parse_startup(bytes.clone())?; - - // This parameter is mandatory by the protocol. - let username = match parameters.get("user") { - Some(user) => user, - None => { - return Err(Error::ClientError( - "Missing user parameter on client startup".into(), - )) - } - }; - - let pool_name = match parameters.get("database") { - Some(db) => db, - None => username, - }; - - let application_name = match parameters.get("application_name") { - Some(application_name) => application_name, - None => "pgcat", - }; - - let client_identifier = ClientIdentifier::new(application_name, username, pool_name); - - let admin = ["pgcat", "pgbouncer"] - .iter() - .filter(|db| *db == pool_name) - .count() - == 1; - - if !admin && admin_only { - // Kick any client that's not admin while we're in admin-only mode. - debug!( - "Rejecting non-admin connection to {} when in admin only mode", - pool_name - ); - error_response_terminal( - &mut write, - "terminating connection due to administrator command", - ) - .await?; - return Err(Error::ShuttingDown); - } - - // Generate random backend ID and secret key - let process_id: i32 = rand::random(); - let secret_key: i32 = rand::random(); - - let mut prepared_statements_enabled = false; - - // Authenticate admin user. - let (transaction_mode, mut server_parameters) = if admin { - let config = get_config(); - // TODO: Add SASL support. - // Perform MD5 authentication. - match config.general.admin_auth_type { - AuthType::Trust => (), - AuthType::MD5 => { - let salt = md5_challenge(&mut write).await?; - - let code = match read.read_u8().await { - Ok(p) => p, - Err(_) => { - return Err(Error::ClientSocketError( - "password code".into(), - client_identifier, - )) - } - }; - - // PasswordMessage - if code as char != 'p' { - return Err(Error::ProtocolSyncError(format!( - "Expected p, got {}", - code as char - ))); - } - - let len = match read.read_i32().await { - Ok(len) => len, - 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::ClientSocketError( - "password message".into(), - client_identifier, - )) - } - }; - - // Compare server and client hashes. - let password_hash = md5_hash_password( - &config.general.admin_username, - &config.general.admin_password, - &salt, - ); - - if password_hash != password_response { - let error = - Error::ClientGeneralError("Invalid password".into(), client_identifier); - - warn!("{}", error); - wrong_password(&mut write, username).await?; - - return Err(error); - } - } - } - (false, generate_server_parameters_for_admin()) - } - // Authenticate normal user. - else { - let pool = match get_pool(pool_name, username) { - Some(pool) => pool, - None => { - error_response( - &mut write, - &format!( - "No pool configured for database: {:?}, user: {:?}", - pool_name, username - ), - ) - .await?; - - return Err(Error::ClientGeneralError( - "Invalid pool name".into(), - client_identifier, - )); - } - }; - - // 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. - match pool.settings.user.auth_type { - AuthType::Trust => (), - AuthType::MD5 => { - // Perform MD5 authentication. - // TODO: Add SASL support. - let salt = md5_challenge(&mut write).await?; - - let code = match read.read_u8().await { - Ok(p) => p, - Err(_) => { - return Err(Error::ClientSocketError( - "password code".into(), - client_identifier, - )) - } - }; - - // PasswordMessage - if code as char != 'p' { - return Err(Error::ProtocolSyncError(format!( - "Expected p, got {}", - code as char - ))); - } - - let len = match read.read_i32().await { - Ok(len) => len, - 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::ClientSocketError( - "password message".into(), - client_identifier, - )) - } - }; - - 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())); - } - - let mut hash = (*pool.auth_hash.read()).clone(); - - if hash.is_none() { - warn!( - "Query auth configured \ - but no hash password found \ - for pool {}. Will try to refetch it.", - pool_name - ); - - 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; - prepared_statements_enabled = - transaction_mode && pool.prepared_statement_cache.is_some(); - - // If the pool hasn't been validated yet, - // connect to the servers and figure out what's what. - if !pool.validated() { - match pool.validate().await { - Ok(_) => (), - Err(err) => { - error_response( - &mut write, - &format!( - "Pool down for database: {:?}, user: {:?}", - pool_name, username - ), - ) - .await?; - return Err(Error::ClientError(format!("Pool down: {:?}", err))); - } - } - } - - (transaction_mode, pool.server_parameters()) - }; - - // Update the parameters to merge what the application sent and what's originally on the server - server_parameters.set_from_hashmap(¶meters, false); - - debug!("Password authentication successful"); - - auth_ok(&mut write).await?; - write_all(&mut write, (&server_parameters).into()).await?; - backend_key_data(&mut write, process_id, secret_key).await?; - send_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), - write, - buffer: BytesMut::with_capacity(8196), - response_message_queue_buffer: BytesMut::with_capacity(8196), - addr, - cancel_mode: false, - transaction_mode, - process_id, - secret_key, - client_server_map, - parameters: parameters.clone(), - stats, - admin, - last_address_id: None, - last_server_stats: None, - connected_to_server: false, - pool_name: pool_name.clone(), - username: username.clone(), - server_parameters, - shutdown, - prepared_statements_enabled, - prepared_statements: HashMap::new(), - extended_protocol_data_buffer: VecDeque::new(), - }) - } - - /// Handle cancel request. - pub async fn cancel( - read: S, - write: T, - addr: std::net::SocketAddr, - mut bytes: BytesMut, // The rest of the startup message. - client_server_map: ClientServerMap, - shutdown: Receiver<()>, - ) -> Result, Error> { - let process_id = bytes.get_i32(); - let secret_key = bytes.get_i32(); - Ok(Client { - read: BufReader::new(read), - write, - buffer: BytesMut::with_capacity(8196), - response_message_queue_buffer: BytesMut::with_capacity(8196), - addr, - cancel_mode: true, - transaction_mode: false, - process_id, - secret_key, - client_server_map, - parameters: HashMap::new(), - stats: Arc::new(ClientStats::default()), - admin: false, - last_address_id: None, - last_server_stats: None, - connected_to_server: false, - pool_name: String::from("undefined"), - username: String::from("undefined"), - server_parameters: ServerParameters::new(), - shutdown, - prepared_statements_enabled: false, - prepared_statements: HashMap::new(), - extended_protocol_data_buffer: VecDeque::new(), - }) - } - - /// Handle a connected and authenticated client. - pub async fn handle(&mut self) -> Result<(), Error> { - // The client wants to cancel a query it has issued previously. - if self.cancel_mode { - trace!("Sending CancelRequest"); - - let (process_id, secret_key, address, port) = { - let guard = self.client_server_map.lock(); - - match guard.get(&(self.process_id, self.secret_key)) { - // Drop the mutex as soon as possible. - // We found the server the client is using for its query - // that it wants to cancel. - Some((process_id, secret_key, address, port)) => { - (*process_id, *secret_key, address.clone(), *port) - } - - // The client doesn't know / got the wrong server, - // we're closing the connection for security reasons. - None => return Ok(()), - } - }; - - // Opens a new separate connection to the server, sends the backend_id - // and secret_key and then closes it for security reasons. No other interactions - // take place. - return Server::cancel(&address, port, process_id, secret_key).await; - } - - // 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.register(self.stats.clone()); - - // Result returned by one of the plugins. - let mut plugin_output = None; - - let client_identifier = ClientIdentifier::new( - self.server_parameters.get_application_name(), - &self.username, - &self.pool_name, - ); - - // Get a pool instance referenced by the most up-to-date - // pointer. This ensures we always read the latest config - // when starting a query. - let mut pool = if self.admin { - // Admin clients do not use pools. - ConnectionPool::default() - } else { - self.get_pool().await? - }; - - query_router.update_pool_settings(&pool.settings); - - // Our custom protocol loop. - // We expect the client to either start a transaction with regular queries - // or issue commands for our sharding and server selection protocol. - loop { - trace!( - "Client idle, waiting for message, transaction mode: {}", - self.transaction_mode - ); - - // 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! { - _ = self.shutdown.recv() => { - if !self.admin { - error_response_terminal( - &mut self.write, - "terminating connection due to administrator command" - ).await?; - - self.stats.disconnect(); - return Ok(()); - } - - // Admin clients ignore shutdown. - else { - read_message(&mut self.read).await? - } - }, - message_result = read_message(&mut self.read) => message_result? - }; - - if message[0] as char == 'X' { - debug!("Client disconnecting"); - - self.stats.disconnect(); - - return Ok(()); - } - - // Handle admin database queries. - if self.admin { - debug!("Handling admin command"); - handle_admin(&mut self.write, message, self.client_server_map.clone()).await?; - continue; - } - - // Handle all custom protocol commands, if any. - if self - .handle_custom_protocol(&mut query_router, &message, &pool) - .await? - { - continue; - } - - let mut initial_parsed_ast = None; - - match message[0] as char { - // Query - 'Q' => { - if query_router.query_parser_enabled() { - match query_router.parse(&message) { - Ok(ast) => { - 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); - - initial_parsed_ast = Some(ast); - } - Err(error) => { - warn!( - "Query parsing error: {} (client: {})", - error, client_identifier - ); - } - } - } - } - - // Buffer extended protocol messages even if we do not have - // a server connection yet. Hopefully, when we get the S message - // we'll be able to allocate a connection. Also, clients do not expect - // the server to respond to these messages so even if we were not able to - // 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 - // Parse - 'P' => { - if query_router.query_parser_enabled() { - match query_router.parse(&message) { - Ok(ast) => { - if let Ok(output) = query_router.execute_plugins(&ast).await { - plugin_output = Some(output); - } - - let _ = query_router.infer(&ast); - } - Err(error) => { - warn!( - "Query parsing error: {} (client: {})", - error, client_identifier - ); - } - }; - } - - self.buffer_parse(message, &pool)?; - - continue; - } - - // Bind - 'B' => { - if query_router.query_parser_enabled() { - query_router.infer_shard_from_bind(&message); - } - - self.buffer_bind(message).await?; - - continue; - } - - // Describe - 'D' => { - self.buffer_describe(message).await?; - continue; - } - - 'E' => { - self.extended_protocol_data_buffer - .push_back(ExtendedProtocolData::create_new_execute(message)); - continue; - } - - // Close (F) - 'C' => { - let close: Close = (&message).try_into()?; - - self.extended_protocol_data_buffer - .push_back(ExtendedProtocolData::create_new_close(message, close)); - continue; - } - - _ => (), - } - - // Check on plugin results. - if let Some(PluginOutput::Deny(error)) = plugin_output { - self.reset_buffered_state(); - error_response(&mut self.write, &error).await?; - plugin_output = None; - continue; - }; - - // Check if the pool is paused and wait until it's resumed. - pool.wait_paused().await; - - // Refresh pool information, something might have changed. - pool = self.get_pool().await?; - query_router.update_pool_settings(&pool.settings); - - 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.stats) - .await - { - Ok(conn) => { - debug!("Got connection from pool"); - conn - } - Err(err) => { - // Client is attempting to get results from the server, - // 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.reset_buffered_state(); - } - - error_response( - &mut self.write, - format!("could not get connection from the pool - {}", err).as_str(), - ) - .await?; - - 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; - } - }; - - let mut reference = connection.0; - let address = connection.1; - let server = &mut *reference; - - // Server is assigned to the client in case the client wants to - // cancel a query later. - server.claim(self.process_id, self.secret_key); - self.connected_to_server = true; - - // Update statistics - self.stats.active(); - - self.last_address_id = Some(address.id); - self.last_server_stats = Some(server.stats()); - - debug!( - "Client {:?} talking to server {:?}", - self.addr, - server.address() - ); - - server.sync_parameters(&self.server_parameters).await?; - - let mut initial_message = Some(message); - - let idle_client_timeout_duration = match get_idle_client_in_transaction_timeout() { - 0 => tokio::time::Duration::MAX, - timeout => tokio::time::Duration::from_millis(timeout), - }; - - // Transaction loop. Multiple queries can be issued by the client here. - // The connection belongs to the client until the transaction is over, - // or until the client disconnects if we are in session mode. - // - // If the client is in session mode, no more custom protocol - // commands will be accepted. - loop { - let message = match initial_message { - None => { - trace!("Waiting for message inside transaction or in session mode"); - - // This is not an initial message so discard the initial_parsed_ast - initial_parsed_ast.take(); - - match tokio::time::timeout( - idle_client_timeout_duration, - read_message(&mut self.read), - ) - .await - { - Ok(Ok(message)) => message, - 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); - } - 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, - self.username, - query_router.shard(), - query_router.role() - ); - - break; - } - } - } - - Some(message) => { - initial_message = None; - message - } - }; - - // The message will be forwarded to the server intact. We still would like to - // parse it below to figure out what to do with it. - - // Safe to unwrap because we know this message has a certain length and has the code - // This reads the first byte without advancing the internal pointer and mutating the bytes - let code = *message.first().unwrap() as char; - - trace!("Client message: {}", code); - - match code { - // Query - 'Q' => { - if query_router.query_parser_enabled() { - // We don't want to parse again if we already parsed it as the initial message - let ast = match initial_parsed_ast { - Some(_) => Some(initial_parsed_ast.take().unwrap()), - None => match query_router.parse(&message) { - Ok(ast) => Some(ast), - Err(error) => { - warn!( - "Query parsing error: {} (client: {})", - error, client_identifier - ); - None - } - }, - }; - - if let Some(ast) = ast { - 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; - } - - _ => (), - }; - } - } - - debug!("Sending query to server"); - - 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(); - server - .stats() - .transaction(self.server_parameters.get_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 && !server.in_copy_mode() { - self.stats.idle(); - - break; - } - } - } - - // Terminate - 'X' => { - server.checkin_cleanup().await?; - self.stats.disconnect(); - self.release(); - - return Ok(()); - } - - // Parse - // The query with placeholders is here, e.g. `SELECT * FROM users WHERE email = $1 AND active = $2`. - 'P' => { - if query_router.query_parser_enabled() { - if let Ok(ast) = query_router.parse(&message) { - if let Ok(output) = query_router.execute_plugins(&ast).await { - plugin_output = Some(output); - } - } - } - - self.buffer_parse(message, &pool)?; - } - - // Bind - // The placeholder's replacements are here, e.g. 'user@email.com' and 'true' - 'B' => { - self.buffer_bind(message).await?; - } - - // Describe - // Command a client can issue to describe a previously prepared named statement. - 'D' => { - self.buffer_describe(message).await?; - } - - // Execute - // Execute a prepared statement prepared in `P` and bound in `B`. - 'E' => { - self.extended_protocol_data_buffer - .push_back(ExtendedProtocolData::create_new_execute(message)); - } - - // Close - // Close the prepared statement. - 'C' => { - let close: Close = (&message).try_into()?; - - self.extended_protocol_data_buffer - .push_back(ExtendedProtocolData::create_new_close(message, close)); - } - - // Sync - // Frontend (client) is asking for the query result now. - 'S' => { - debug!("Sending query to server"); - - match plugin_output { - Some(PluginOutput::Deny(error)) => { - error_response(&mut self.write, &error).await?; - plugin_output = None; - self.reset_buffered_state(); - continue; - } - - Some(PluginOutput::Intercept(result)) => { - write_all(&mut self.write, result).await?; - plugin_output = None; - self.reset_buffered_state(); - continue; - } - - _ => (), - }; - - // Prepared statements can arrive like this - // 1. Without named describe - // Client: Parse, with name, query and params - // Sync - // Server: ParseComplete - // ReadyForQuery - // 3. Without named describe - // Client: Parse, with name, query and params - // Describe, with no name - // Sync - // Server: ParseComplete - // ParameterDescription - // RowDescription - // ReadyForQuery - // 2. With named describe - // Client: Parse, with name, query and params - // Describe, with name - // Sync - // Server: ParseComplete - // ParameterDescription - // RowDescription - // ReadyForQuery - - // Iterate over our extended protocol data that we've buffered - while let Some(protocol_data) = - self.extended_protocol_data_buffer.pop_front() - { - match protocol_data { - ExtendedProtocolData::Parse { data, metadata } => { - debug!("Have parse in extended buffer"); - let (parse, hash) = match metadata { - Some(metadata) => metadata, - None => { - let first_char_in_name = *data.get(5).unwrap_or(&0); - if first_char_in_name != 0 { - // This is a named prepared statement while prepared statements are disabled - // Server connection state will need to be cleared at checkin - server.mark_dirty(); - } - // Not a prepared statement - self.buffer.put(&data[..]); - continue; - } - }; - - // This is a prepared statement we already have on the checked out server - if server.has_prepared_statement(&parse.name) { - debug!( - "Prepared statement `{}` found in server cache", - parse.name - ); - - // We don't want to send the parse message to the server - // Instead queue up a parse complete message to send to the client - self.response_message_queue_buffer.put(parse_complete()); - } else { - debug!( - "Prepared statement `{}` not found in server cache", - parse.name - ); - - // TODO: Consider adding the close logic that this function can send for eviction to the client buffer instead - // In this case we don't want to send the parse message to the server since the client is sending it - self.register_parse_to_server_cache( - false, &hash, &parse, &pool, server, &address, - ) - .await?; - - // Add parse message to buffer - self.buffer.put(&data[..]); - } - } - ExtendedProtocolData::Bind { data, metadata } => { - // This is using a prepared statement - if let Some(client_given_name) = metadata { - self.ensure_prepared_statement_is_on_server( - client_given_name, - &pool, - server, - &address, - ) - .await?; - } - - self.buffer.put(&data[..]); - } - ExtendedProtocolData::Describe { data, metadata } => { - // This is using a prepared statement - if let Some(client_given_name) = metadata { - self.ensure_prepared_statement_is_on_server( - client_given_name, - &pool, - server, - &address, - ) - .await?; - } - - self.buffer.put(&data[..]); - } - ExtendedProtocolData::Execute { data } => { - self.buffer.put(&data[..]) - } - ExtendedProtocolData::Close { data, close } => { - // We don't send the close message to the server if prepared statements are enabled - // and it's a close with a prepared statement name provided - if self.prepared_statements_enabled - && close.is_prepared_statement() - && !close.anonymous() - { - self.prepared_statements.remove(&close.name); - - // Queue up a close complete message to send to the client - self.response_message_queue_buffer.put(close_complete()); - } else { - self.buffer.put(&data[..]); - } - } - } - } - - // Add the sync message - self.buffer.put(&message[..]); - - let mut should_send_to_server = true; - - // If we have just a sync message left (maybe after omitting sending some messages to the server) no need to send it to the server - if *self.buffer.first().unwrap() == b'S' { - should_send_to_server = false; - // queue up a ready for query message to send to the client, respecting the transaction state of the server - self.response_message_queue_buffer - .put(ready_for_query(server.in_transaction())); - } - - // Send all queued messages to the client - // NOTE: it's possible we don't perfectly send things back in the same order as postgres would, - // however clients should be able to handle this - if !self.response_message_queue_buffer.is_empty() { - if let Err(err) = write_all_flush( - &mut self.write, - &self.response_message_queue_buffer, - ) - .await - { - // We might be in some kind of error/in between protocol state - server.mark_bad(err.to_string().as_str()); - return Err(err); - } - - self.response_message_queue_buffer.clear(); - } - - if should_send_to_server { - self.send_and_receive_loop( - code, - None, - server, - &address, - &pool, - &self.stats.clone(), - ) - .await?; - } - - self.buffer.clear(); - - if !server.in_transaction() { - self.stats.transaction(); - server - .stats() - .transaction(self.server_parameters.get_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 && !server.in_copy_mode() { - break; - } - } - } - - // CopyData - 'd' => { - self.buffer.put(&message[..]); - - // Want to limit buffer size - if self.buffer.len() > 8196 { - // Forward the data to the server, - self.send_server_message(server, &self.buffer, &address, &pool) - .await?; - self.buffer.clear(); - } - } - - // CopyDone or CopyFail - // Copy is done, successfully or not. - 'c' | 'f' => { - // We may already have some copy data in the buffer, add this message to buffer - self.buffer.put(&message[..]); - - self.send_server_message(server, &self.buffer, &address, &pool) - .await?; - - // Clear the buffer - self.buffer.clear(); - - let response = self - .receive_server_message(server, &address, &pool, &self.stats.clone()) - .await?; - - match write_all_flush(&mut self.write, &response).await { - Ok(_) => (), - Err(err) => { - server.mark_bad(err.to_string().as_str()); - return Err(err); - } - }; - - if !server.in_transaction() { - self.stats.transaction(); - server - .stats() - .transaction(self.server_parameters.get_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 { - break; - } - } - } - - // Some unexpected message. We either did not implement the protocol correctly - // or this is not a Postgres client we're talking to. - _ => { - error!("Unexpected code: {}", code); - } - } - } - - // 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?; - - server.stats().idle(); - self.connected_to_server = false; - - self.release(); - self.stats.idle(); - } - } - - /// Retrieve connection pool, if it exists. - /// Return an error to the client otherwise. - async fn get_pool(&mut self) -> Result { - match get_pool(&self.pool_name, &self.username) { - Some(pool) => Ok(pool), - None => { - error_response( - &mut self.write, - &format!( - "No pool configured for database: {}, user: {}", - self.pool_name, self.username - ), - ) - .await?; - - Err(Error::ClientError(format!( - "Invalid pool name {{ username: {}, pool_name: {}, application_name: {} }}", - self.pool_name, - self.username, - self.server_parameters.get_application_name() - ))) - } - } - } - - /// Handles custom protocol messages - /// Returns true if the message is custom protocol message, false otherwise - /// Does not work with prepared statements, only simple and extended protocol without parameters - async fn handle_custom_protocol( - &mut self, - query_router: &mut QueryRouter, - message: &BytesMut, - pool: &ConnectionPool, - ) -> Result { - let current_shard = query_router.shard(); - - match query_router.try_execute_command(message) { - None => Ok(false), - - Some(custom) => { - match custom { - // SET SHARD TO - (Command::SetShard, _) => { - match query_router.shard() { - None => {} - Some(selected_shard) => { - if selected_shard >= pool.shards() { - // Bad shard number, send error message to client. - query_router.set_shard(current_shard); - - error_response( - &mut self.write, - &format!( - "shard {} is not configured {}, staying on shard {:?} (shard numbers start at 0)", - selected_shard, - pool.shards(), - current_shard, - ), - ) - .await?; - } else { - custom_protocol_response_ok(&mut self.write, "SET SHARD") - .await?; - } - } - } - } - - // SET PRIMARY READS TO - (Command::SetPrimaryReads, _) => { - custom_protocol_response_ok(&mut self.write, "SET PRIMARY READS").await?; - } - - // SET SHARDING KEY TO - (Command::SetShardingKey, _) => { - custom_protocol_response_ok(&mut self.write, "SET SHARDING KEY").await?; - } - - // SET SERVER ROLE TO - (Command::SetServerRole, _) => { - custom_protocol_response_ok(&mut self.write, "SET SERVER ROLE").await?; - } - - // SHOW SERVER ROLE - (Command::ShowServerRole, value) => { - show_response(&mut self.write, "server role", &value).await?; - } - - // SHOW SHARD - (Command::ShowShard, value) => { - show_response(&mut self.write, "shard", &value).await?; - } - - // SHOW PRIMARY READS - (Command::ShowPrimaryReads, value) => { - show_response(&mut self.write, "primary reads", &value).await?; - } - }; - - Ok(true) - } - } - } - - /// Makes sure the the checked out server has the prepared statement and sends it to the server if it doesn't - async fn ensure_prepared_statement_is_on_server( - &mut self, - client_name: String, - pool: &ConnectionPool, - server: &mut Server, - address: &Address, - ) -> Result<(), Error> { - match self.prepared_statements.get(&client_name) { - Some((parse, hash)) => { - debug!("Prepared statement `{}` found in cache", client_name); - // In this case we want to send the parse message to the server - // since pgcat is initiating the prepared statement on this specific server - match self - .register_parse_to_server_cache(true, hash, parse, pool, server, address) - .await - { - Ok(_) => (), - Err(err) => match err { - Error::PreparedStatementError => { - debug!("Removed {} from client cache", client_name); - self.prepared_statements.remove(&client_name); - } - - _ => { - return Err(err); - } - }, - } - } - - None => { - return Err(Error::ClientError(format!( - "prepared statement `{}` not found", - client_name - ))) - } - }; - - Ok(()) - } - - /// Register the parse to the server cache and send it to the server if requested (ie. requested by pgcat) - /// - /// Also updates the pool LRU that this parse was used recently - async fn register_parse_to_server_cache( - &self, - should_send_parse_to_server: bool, - hash: &u64, - parse: &Arc, - pool: &ConnectionPool, - server: &mut Server, - address: &Address, - ) -> Result<(), Error> { - // We want to promote this in the pool's LRU - pool.promote_prepared_statement_hash(hash); - - debug!("Checking for prepared statement {}", parse.name); - - if let Err(err) = server - .register_prepared_statement(parse, should_send_parse_to_server) - .await - { - match err { - // Don't ban for this. - Error::PreparedStatementError => (), - _ => { - pool.ban(address, BanReason::MessageSendFailed, Some(&self.stats)); - } - }; - - return Err(err); - } - - Ok(()) - } - - /// Register and rewrite the parse statement to the clients statement cache - /// and also the pool's statement cache. Add it to extended protocol data. - fn buffer_parse(&mut self, message: BytesMut, pool: &ConnectionPool) -> Result<(), Error> { - // Avoid parsing if prepared statements not enabled - if !self.prepared_statements_enabled { - debug!("Anonymous parse message"); - self.extended_protocol_data_buffer - .push_back(ExtendedProtocolData::create_new_parse(message, None)); - return Ok(()); - } - - let client_given_name = Parse::get_name(&message)?; - let parse: Parse = (&message).try_into()?; - - // Compute the hash of the parse statement - let hash = parse.get_hash(); - - // Add the statement to the cache or check if we already have it - let new_parse = match pool.register_parse_to_cache(hash, &parse) { - Some(parse) => parse, - None => { - return Err(Error::ClientError(format!( - "Could not store Prepared statement `{}`", - client_given_name - ))) - } - }; - - debug!( - "Renamed prepared statement `{}` to `{}` and saved to cache", - client_given_name, new_parse.name - ); - - self.prepared_statements - .insert(client_given_name, (new_parse.clone(), hash)); - - self.extended_protocol_data_buffer - .push_back(ExtendedProtocolData::create_new_parse( - new_parse.as_ref().try_into()?, - Some((new_parse.clone(), hash)), - )); - - Ok(()) - } - - /// Rewrite the Bind (F) message to use the prepared statement name - /// saved in the client cache. - async fn buffer_bind(&mut self, message: BytesMut) -> Result<(), Error> { - // Avoid parsing if prepared statements not enabled - if !self.prepared_statements_enabled { - debug!("Anonymous bind message"); - self.extended_protocol_data_buffer - .push_back(ExtendedProtocolData::create_new_bind(message, None)); - return Ok(()); - } - - let client_given_name = Bind::get_name(&message)?; - - match self.prepared_statements.get(&client_given_name) { - Some((rewritten_parse, _)) => { - let message = Bind::rename(message, &rewritten_parse.name)?; - - debug!( - "Rewrote bind `{}` to `{}`", - client_given_name, rewritten_parse.name - ); - - self.extended_protocol_data_buffer.push_back( - ExtendedProtocolData::create_new_bind(message, Some(client_given_name)), - ); - - Ok(()) - } - None => { - debug!( - "Got bind for unknown prepared statement {:?}", - client_given_name - ); - - error_response( - &mut self.write, - &format!( - "prepared statement \"{}\" does not exist", - client_given_name - ), - ) - .await?; - - Err(Error::ClientError(format!( - "Prepared statement `{}` doesn't exist", - client_given_name - ))) - } - } - } - - /// Rewrite the Describe (F) message to use the prepared statement name - /// saved in the client cache. - async fn buffer_describe(&mut self, message: BytesMut) -> Result<(), Error> { - // Avoid parsing if prepared statements not enabled - if !self.prepared_statements_enabled { - debug!("Anonymous describe message"); - self.extended_protocol_data_buffer - .push_back(ExtendedProtocolData::create_new_describe(message, None)); - - return Ok(()); - } - - let describe: Describe = (&message).try_into()?; - if describe.target == 'P' { - debug!("Portal describe message"); - self.extended_protocol_data_buffer - .push_back(ExtendedProtocolData::create_new_describe(message, None)); - - return Ok(()); - } - - let client_given_name = describe.statement_name.clone(); - - match self.prepared_statements.get(&client_given_name) { - Some((rewritten_parse, _)) => { - let describe = describe.rename(&rewritten_parse.name); - - debug!( - "Rewrote describe `{}` to `{}`", - client_given_name, describe.statement_name - ); - - self.extended_protocol_data_buffer.push_back( - ExtendedProtocolData::create_new_describe( - describe.try_into()?, - Some(client_given_name), - ), - ); - - Ok(()) - } - - None => { - debug!("Got describe for unknown prepared statement {:?}", describe); - - error_response( - &mut self.write, - &format!( - "prepared statement \"{}\" does not exist", - client_given_name - ), - ) - .await?; - - Err(Error::ClientError(format!( - "Prepared statement `{}` doesn't exist", - client_given_name - ))) - } - } - } - - fn reset_buffered_state(&mut self) { - self.buffer.clear(); - self.extended_protocol_data_buffer.clear(); - self.response_message_queue_buffer.clear(); - } - - /// 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(); - guard.remove(&(self.process_id, self.secret_key)); - } - - async fn send_and_receive_loop( - &mut self, - code: char, - message: Option<&BytesMut>, - server: &mut Server, - address: &Address, - pool: &ConnectionPool, - client_stats: &ClientStats, - ) -> Result<(), Error> { - debug!("Sending {} to server", code); - - let message = match message { - Some(message) => message, - None => &self.buffer, - }; - - self.send_server_message(server, message, address, pool) - .await?; - - let query_start = Instant::now(); - // 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, client_stats) - .await?; - - match write_all_flush(&mut self.write, &response).await { - Ok(_) => (), - Err(err) => { - // We might be in some kind of error/in between protocol state, better to just kill this server - server.mark_bad(err.to_string().as_str()); - return Err(err); - } - }; - - if !server.is_data_available() { - break; - } - } - - // Report query executed statistics. - client_stats.query(); - server.stats().query( - Instant::now().duration_since(query_start).as_millis() as u64, - self.server_parameters.get_application_name(), - ); - - Ok(()) - } - - async fn send_server_message( - &self, - server: &mut Server, - message: &BytesMut, - address: &Address, - pool: &ConnectionPool, - ) -> Result<(), Error> { - match server.send(message).await { - Ok(_) => Ok(()), - Err(err) => { - pool.ban(address, BanReason::MessageSendFailed, Some(&self.stats)); - Err(err) - } - } - } - - async fn receive_server_message( - &mut self, - server: &mut Server, - address: &Address, - pool: &ConnectionPool, - client_stats: &ClientStats, - ) -> Result { - let statement_timeout_duration = match pool.settings.user.statement_timeout { - 0 => tokio::time::Duration::MAX, - timeout => tokio::time::Duration::from_millis(timeout), - }; - - match tokio::time::timeout( - statement_timeout_duration, - server.recv(Some(&mut self.server_parameters)), - ) - .await - { - Ok(result) => match result { - Ok(message) => Ok(message), - Err(err) => { - pool.ban(address, BanReason::MessageReceiveFailed, Some(client_stats)); - error_response_terminal( - &mut self.write, - &format!("error receiving data from server: {:?}", err), - ) - .await?; - Err(err) - } - }, - Err(_) => { - server.mark_bad( - format!( - "Statement timeout while talking to {:?} with user {}", - address, pool.settings.user.username - ) - .as_str(), - ); - pool.ban(address, BanReason::StatementTimeout, Some(client_stats)); - error_response_terminal(&mut self.write, "pool statement timeout").await?; - Err(Error::StatementTimeout) - } - } - } -} - -impl Drop for Client { - fn drop(&mut self) { - let mut guard = self.client_server_map.lock(); - guard.remove(&(self.process_id, self.secret_key)); - - // Dirty shutdown - // TODO: refactor, this is not the best way to handle state management. - 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 deleted file mode 100644 index 1abb7ed..0000000 --- a/src/cmd_args.rs +++ /dev/null @@ -1,36 +0,0 @@ -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 { - Args::parse() -} - -#[derive(ValueEnum, Clone, Debug)] -pub enum LogFormat { - Text, - Structured, - Debug, -} diff --git a/src/config.rs b/src/config.rs deleted file mode 100644 index b0d98fb..0000000 --- a/src/config.rs +++ /dev/null @@ -1,1607 +0,0 @@ -/// Parse the configuration file. -use arc_swap::ArcSwap; -use log::{error, info}; -use once_cell::sync::Lazy; -use regex::Regex; -use serde::{Deserializer, Serializer}; -use serde_derive::{Deserialize, Serialize}; - -use std::collections::hash_map::DefaultHasher; -use std::collections::{BTreeMap, HashMap, HashSet}; -use std::hash::{Hash, Hasher}; -use std::path::Path; -use std::sync::atomic::{AtomicU64, Ordering}; -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"); - -/// Globally available configuration. -static CONFIG: Lazy> = Lazy::new(|| ArcSwap::from_pointee(Config::default())); - -/// Server role: primary or replica. -#[derive(Clone, PartialEq, Serialize, Deserialize, Hash, std::cmp::Eq, Debug, Copy)] -pub enum Role { - #[serde(alias = "primary", alias = "Primary")] - Primary, - #[serde(alias = "replica", alias = "Replica")] - Replica, - #[serde(alias = "mirror", alias = "Mirror")] - Mirror, -} - -impl std::fmt::Display for Role { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Role::Primary => write!(f, "primary"), - Role::Replica => write!(f, "replica"), - Role::Mirror => write!(f, "mirror"), - } - } -} - -impl PartialEq> for Role { - fn eq(&self, other: &Option) -> bool { - match other { - None => true, - Some(role) => *self == *role, - } - } -} - -impl PartialEq for Option { - fn eq(&self, other: &Role) -> bool { - match *self { - None => true, - Some(role) => role == *other, - } - } -} - -/// Address identifying a PostgreSQL server uniquely. -#[derive(Clone, Debug)] -pub struct Address { - /// Unique ID per addressable Postgres server. - pub id: usize, - - /// Server host. - pub host: String, - - /// Server port. - pub port: u16, - - /// Shard number of this Postgres server. - pub shard: usize, - - /// The name of the Postgres database. - pub database: String, - - /// Server role: replica, primary. - pub role: Role, - - /// If it's a replica, number it for reference and failover. - pub replica_number: usize, - - /// Position of the server in the pool for failover. - pub address_index: usize, - - /// The name of the user configured to use this pool. - pub username: String, - - /// The name of this pool (i.e. database name visible to the client). - pub pool_name: String, - - /// List of addresses to receive mirrored traffic. - pub mirrors: Vec
, - - /// Address stats - pub stats: Arc, - - /// Number of errors encountered since last successful checkout - pub error_count: Arc, -} - -impl Default for Address { - fn default() -> Address { - Address { - id: 0, - host: String::from("127.0.0.1"), - port: 5432, - shard: 0, - database: String::from("database"), - role: Role::Replica, - replica_number: 0, - address_index: 0, - username: String::from("username"), - pool_name: String::from("pool_name"), - mirrors: Vec::new(), - stats: Arc::new(AddressStats::default()), - error_count: Arc::new(AtomicU64::new(0)), - } - } -} - -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 { - match self.role { - Role::Primary => format!("{}_shard_{}_primary", self.pool_name, self.shard), - Role::Replica => format!( - "{}_shard_{}_replica_{}", - self.pool_name, self.shard, self.replica_number - ), - Role::Mirror => format!( - "{}_shard_{}_mirror_{}", - self.pool_name, self.shard, self.replica_number - ), - } - } - - pub fn error_count(&self) -> u64 { - self.error_count.load(Ordering::Relaxed) - } - - pub fn increment_error_count(&self) { - self.error_count.fetch_add(1, Ordering::Relaxed); - } - - pub fn reset_error_count(&self) { - self.error_count.store(0, Ordering::Relaxed); - } -} - -/// PostgreSQL user. -#[derive(Clone, PartialEq, Hash, Eq, Serialize, Deserialize, Debug)] -pub struct User { - pub username: String, - pub password: Option, - - #[serde(default = "User::default_auth_type")] - pub auth_type: AuthType, - 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, - pub connect_timeout: Option, - pub idle_timeout: Option, -} - -impl Default for User { - fn default() -> User { - User { - username: String::from("postgres"), - password: None, - auth_type: AuthType::MD5, - server_username: None, - server_password: None, - pool_size: 15, - min_pool_size: None, - statement_timeout: 0, - pool_mode: None, - server_lifetime: None, - connect_timeout: None, - idle_timeout: None, - } - } -} - -impl User { - pub fn default_auth_type() -> AuthType { - AuthType::MD5 - } - - fn validate(&self) -> Result<(), Error> { - if let Some(min_pool_size) = self.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); - } - }; - - Ok(()) - } -} - -/// General configuration. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -pub struct General { - #[serde(default = "General::default_host")] - pub host: String, - - #[serde(default = "General::default_port")] - 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")] - pub connect_timeout: u64, - - #[serde(default = "General::default_idle_timeout")] - pub idle_timeout: u64, - - #[serde(default = "General::default_tcp_keepalives_idle")] - pub tcp_keepalives_idle: u64, - #[serde(default = "General::default_tcp_keepalives_count")] - 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, - - #[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, - - #[serde(default = "General::default_healthcheck_timeout")] - pub healthcheck_timeout: u64, - - #[serde(default = "General::default_healthcheck_delay")] - pub healthcheck_delay: u64, - - #[serde(default = "General::default_ban_time")] - pub ban_time: i64, - - #[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)] // 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_admin_auth_type")] - pub admin_auth_type: AuthType, - - #[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, -} - -impl General { - pub fn default_host() -> String { - "0.0.0.0".into() - } - - pub fn default_admin_auth_type() -> AuthType { - AuthType::MD5 - } - - 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 permanent server pool exhaustion - pub fn default_tcp_keepalives_idle() -> u64 { - 5 // 5 seconds - } - - pub fn default_tcp_keepalives_count() -> u32 { - 5 // 5 time - } - - pub fn default_tcp_keepalives_interval() -> u64 { - 5 // 5 seconds - } - - pub fn default_tcp_user_timeout() -> u64 { - 10000 // 10000 milliseconds - } - - pub fn default_idle_timeout() -> u64 { - 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 - } - - pub fn default_healthcheck_delay() -> u64 { - 30000 - } - - pub fn default_ban_time() -> i64 { - 60 - } - - pub fn default_worker_threads() -> usize { - 4 - } - - 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 - } -} - -impl Default for General { - fn default() -> General { - General { - host: Self::default_host(), - port: Self::default_port(), - enable_prometheus_exporter: Some(false), - prometheus_exporter_port: 9930, - connect_timeout: General::default_connect_timeout(), - idle_timeout: General::default_idle_timeout(), - 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, - dns_cache_enabled: false, - dns_max_ttl: Self::default_dns_max_ttl(), - shutdown_timeout: Self::default_shutdown_timeout(), - healthcheck_timeout: Self::default_healthcheck_timeout(), - healthcheck_delay: Self::default_healthcheck_delay(), - ban_time: Self::default_ban_time(), - idle_client_in_transaction_timeout: Self::default_idle_client_in_transaction_timeout(), - server_lifetime: Self::default_server_lifetime(), - server_round_robin: Self::default_server_round_robin(), - worker_threads: Self::default_worker_threads(), - autoreload: None, - tls_certificate: None, - tls_private_key: None, - server_tls: false, - verify_server_certificate: false, - admin_username: String::from("admin"), - admin_password: String::from("admin"), - admin_auth_type: AuthType::MD5, - validate_config: true, - auth_query: None, - auth_query_user: None, - auth_query_password: None, - } - } -} - -/// Pool mode: -/// - transaction: server serves one transaction, -/// - session: server is attached to the client. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Copy, Hash)] -pub enum PoolMode { - #[serde(alias = "transaction", alias = "Transaction")] - Transaction, - - #[serde(alias = "session", alias = "Session")] - Session, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Copy, Hash)] -pub enum AuthType { - #[serde(alias = "trust", alias = "Trust")] - Trust, - - #[serde(alias = "md5", alias = "MD5")] - MD5, -} - -impl std::fmt::Display for PoolMode { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - PoolMode::Transaction => write!(f, "transaction"), - PoolMode::Session => write!(f, "session"), - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Copy, Hash)] -pub enum LoadBalancingMode { - #[serde(alias = "random", alias = "Random")] - Random, - - #[serde(alias = "loc", alias = "LOC", alias = "least_outstanding_connections")] - LeastOutstandingConnections, -} - -impl std::fmt::Display for LoadBalancingMode { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - LoadBalancingMode::Random => write!(f, "random"), - LoadBalancingMode::LeastOutstandingConnections => { - write!(f, "least_outstanding_connections") - } - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] -pub struct Pool { - #[serde(default = "Pool::default_pool_mode")] - pub pool_mode: PoolMode, - - #[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 - pub query_parser_enabled: bool, - - pub query_parser_max_length: Option, - - #[serde(default)] // False - pub query_parser_read_write_splitting: bool, - - #[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")] - pub automatic_sharding_key: Option, - - pub sharding_key_regex: Option, - pub shard_id_regex: Option, - pub regex_search_limit: Option, - - #[serde(default = "Pool::default_default_shard")] - pub default_shard: DefaultShard, - - 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, - - #[serde(default)] // False - pub log_client_parameter_status_changes: bool, - - #[serde(default = "Pool::default_prepared_statements_cache_size")] - pub prepared_statements_cache_size: usize, - - pub plugins: Option, - pub shards: BTreeMap, - pub users: BTreeMap, - // Note, don't put simple fields below these configs. There's a compatibility issue with TOML that makes it - // 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 -} - -impl Pool { - pub fn hash_value(&self) -> u64 { - let mut s = DefaultHasher::new(); - self.hash(&mut s); - 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 - } - - pub fn default_default_shard() -> DefaultShard { - DefaultShard::default() - } - - pub fn default_load_balancing_mode() -> LoadBalancingMode { - LoadBalancingMode::Random - } - - pub fn default_automatic_sharding_key() -> Option { - 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 default_prepared_statements_cache_size() -> usize { - 0 - } - - pub fn validate(&mut self) -> Result<(), Error> { - match self.default_role.as_ref() { - "any" => (), - "primary" => (), - "replica" => (), - other => { - error!( - "Query router default_role must be 'primary', 'replica', or 'any', got: '{}'", - other - ); - return Err(Error::BadConfig); - } - }; - - for (shard_idx, shard) in &self.shards { - match shard_idx.parse::() { - Ok(_) => (), - Err(_) => { - error!( - "Shard '{}' is not a valid number, shards must be numbered starting at 0", - shard_idx - ); - return Err(Error::BadConfig); - } - }; - shard.validate()?; - } - - for (option, name) in [ - (&self.shard_id_regex, "shard_id_regex"), - (&self.sharding_key_regex, "sharding_key_regex"), - ] { - if let Some(regex) = option { - if let Err(parse_err) = Regex::new(regex.as_str()) { - error!("{} is not a valid Regex: {}", name, parse_err); - return Err(Error::BadConfig); - } - } - } - - if self.query_parser_read_write_splitting && !self.query_parser_enabled { - error!( - "query_parser_read_write_splitting is only valid when query_parser_enabled is true" - ); - return Err(Error::BadConfig); - } - - if self.plugins.is_some() && !self.query_parser_enabled { - error!("plugins are only valid when query_parser_enabled is true"); - return Err(Error::BadConfig); - } - - self.automatic_sharding_key = match &self.automatic_sharding_key { - Some(key) => { - // No quotes in the key so we don't have to compare quoted - // to unquoted idents. - let key = key.replace('\"', ""); - - if key.split('.').count() != 2 { - error!( - "automatic_sharding_key '{}' must be fully qualified, e.g. t.{}`", - key, key - ); - return Err(Error::BadConfig); - } - - Some(key) - } - None => None, - }; - - if let DefaultShard::Shard(shard_number) = self.default_shard { - if shard_number >= self.shards.len() { - error!("Invalid shard {:?}", shard_number); - return Err(Error::BadConfig); - } - } - - for user in self.users.values() { - user.validate()?; - } - - Ok(()) - } -} - -impl Default for Pool { - fn default() -> Pool { - Pool { - pool_mode: Self::default_pool_mode(), - load_balancing_mode: Self::default_load_balancing_mode(), - default_role: String::from("any"), - query_parser_enabled: false, - query_parser_max_length: None, - query_parser_read_write_splitting: false, - primary_reads_enabled: false, - connect_timeout: None, - idle_timeout: None, - server_lifetime: None, - sharding_function: ShardingFunction::PgBigintHash, - automatic_sharding_key: None, - sharding_key_regex: None, - shard_id_regex: None, - regex_search_limit: Some(1000), - default_shard: Self::default_default_shard(), - auth_query: None, - auth_query_user: None, - auth_query_password: None, - cleanup_server_connections: true, - log_client_parameter_status_changes: false, - prepared_statements_cache_size: Self::default_prepared_statements_cache_size(), - plugins: None, - shards: BTreeMap::from([(String::from("1"), Shard::default())]), - users: BTreeMap::default(), - } - } -} - -#[derive(Clone, PartialEq, Serialize, Deserialize, Debug, Hash, Eq)] -pub struct ServerConfig { - pub host: String, - pub port: u16, - pub role: Role, -} - -// No Shard Specified handling. -#[derive(Debug, PartialEq, Clone, Eq, Hash, Copy)] -pub enum DefaultShard { - Shard(usize), - Random, - RandomHealthy, -} -impl Default for DefaultShard { - fn default() -> Self { - DefaultShard::Shard(0) - } -} -impl serde::Serialize for DefaultShard { - fn serialize(&self, serializer: S) -> Result { - match self { - DefaultShard::Shard(shard) => { - serializer.serialize_str(&format!("shard_{}", &shard.to_string())) - } - DefaultShard::Random => serializer.serialize_str("random"), - DefaultShard::RandomHealthy => serializer.serialize_str("random_healthy"), - } - } -} -impl<'de> serde::Deserialize<'de> for DefaultShard { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - if let Some(s) = s.strip_prefix("shard_") { - let shard = s.parse::().map_err(serde::de::Error::custom)?; - return Ok(DefaultShard::Shard(shard)); - } - - match s.as_str() { - "random" => Ok(DefaultShard::Random), - "random_healthy" => Ok(DefaultShard::RandomHealthy), - _ => Err(serde::de::Error::custom( - "invalid value for no_shard_specified_behavior", - )), - } - } -} - -#[derive(Clone, PartialEq, Serialize, Deserialize, Debug, Hash, Eq)] -pub struct MirrorServerConfig { - pub host: String, - pub port: u16, - pub mirroring_target_index: usize, -} - -/// Shard configuration. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Hash, Eq)] -pub struct Shard { - pub database: String, - pub mirrors: Option>, - pub servers: Vec, -} - -impl Shard { - pub fn validate(&self) -> Result<(), Error> { - // We use addresses as unique identifiers, - // let's make sure they are unique in the config as well. - let mut dup_check = HashSet::new(); - let mut primary_count = 0; - - if self.servers.is_empty() { - error!("Shard {} has no servers configured", self.database); - return Err(Error::BadConfig); - } - - for server in &self.servers { - dup_check.insert(server); - - // Check that we define only zero or one primary. - if server.role == Role::Primary { - primary_count += 1 - } - } - - if primary_count > 1 { - error!( - "Shard {} has more than one primary configured", - self.database - ); - return Err(Error::BadConfig); - } - - if dup_check.len() != self.servers.len() { - error!("Shard {} contains duplicate server configs", self.database); - return Err(Error::BadConfig); - } - - Ok(()) - } -} - -impl Default for Shard { - fn default() -> Shard { - Shard { - database: String::from("postgres"), - mirrors: None, - servers: vec![ServerConfig { - host: String::from("localhost"), - port: 5432, - role: Role::Primary, - }], - } - } -} - -#[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, -} - -pub trait Plugin { - fn is_enabled(&self) -> bool; -} - -impl std::fmt::Display for Plugins { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - fn is_enabled(arg: Option<&T>) -> bool { - if let Some(arg) = arg { - arg.is_enabled() - } else { - false - } - } - write!( - f, - "interceptor: {}, table_access: {}, query_logger: {}, prewarmer: {}", - is_enabled(self.intercept.as_ref()), - is_enabled(self.table_access.as_ref()), - is_enabled(self.query_logger.as_ref()), - is_enabled(self.prewarmer.as_ref()), - ) - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, Hash, Eq)] -pub struct Intercept { - pub enabled: bool, - pub queries: BTreeMap, -} - -impl Plugin for Intercept { - fn is_enabled(&self) -> bool { - self.enabled - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, Hash, Eq)] -pub struct TableAccess { - pub enabled: bool, - pub tables: Vec, -} - -impl Plugin for TableAccess { - fn is_enabled(&self) -> bool { - self.enabled - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, Hash, Eq)] -pub struct QueryLogger { - pub enabled: bool, -} - -impl Plugin for QueryLogger { - fn is_enabled(&self) -> bool { - self.enabled - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, Hash, Eq)] -pub struct Prewarmer { - pub enabled: bool, - pub queries: Vec, -} - -impl Plugin for Prewarmer { - fn is_enabled(&self) -> bool { - self.enabled - } -} - -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 { - #[allow(clippy::needless_range_loop)] - 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 { - // Serializer maintains the order of fields in the struct - // so we should always put simple fields before nested fields - // in all serializable structs to avoid ValueAfterTable errors - // These errors occur when the toml serializer is about to produce - // ambiguous toml structure like the one below - // [main] - // field1_under_main = 1 - // field2_under_main = 2 - // [main.subconf] - // field1_under_subconf = 1 - // field3_under_main = 3 # This field will be interpreted as being under subconf and not under main - #[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.clone_from(&self.general.auth_query); - } - - if pool.auth_query_user.is_none() { - pool.auth_query_user - .clone_from(&self.general.auth_query_user); - } - - if pool.auth_query_password.is_none() { - pool.auth_query_password - .clone_from(&self.general.auth_query_password); - } - } - } -} - -impl Default for Config { - fn default() -> Config { - Config { - path: Self::default_path(), - general: General::default(), - plugins: None, - pools: HashMap::default(), - } - } -} - -impl From<&Config> for std::collections::HashMap { - fn from(config: &Config) -> HashMap { - let mut r: Vec<(String, String)> = config - .pools - .iter() - .flat_map(|(pool_name, pool)| { - [ - ( - format!("pools.{}.pool_mode", pool_name), - pool.pool_mode.to_string(), - ), - ( - format!("pools.{}.load_balancing_mode", pool_name), - pool.load_balancing_mode.to_string(), - ), - ( - format!("pools.{}.primary_reads_enabled", pool_name), - pool.primary_reads_enabled.to_string(), - ), - ( - format!("pools.{}.query_parser_enabled", pool_name), - pool.query_parser_enabled.to_string(), - ), - ( - format!("pools.{}.query_parser_max_length", pool_name), - match pool.query_parser_max_length { - Some(max_length) => max_length.to_string(), - None => String::from("unlimited"), - }, - ), - ( - format!("pools.{}.query_parser_read_write_splitting", pool_name), - pool.query_parser_read_write_splitting.to_string(), - ), - ( - format!("pools.{}.default_role", pool_name), - pool.default_role.clone(), - ), - ( - format!("pools.{}.sharding_function", pool_name), - pool.sharding_function.to_string(), - ), - ( - format!("pools.{:?}.shard_count", pool_name), - pool.shards.len().to_string(), - ), - ( - format!("pools.{:?}.users", pool_name), - pool.users - .values() - .map(|user| &user.username) - .cloned() - .collect::>() - .join(", "), - ), - ] - }) - .collect(); - - let mut static_settings = vec![ - ("host".to_string(), config.general.host.to_string()), - ("port".to_string(), config.general.port.to_string()), - ( - "prometheus_exporter_port".to_string(), - config.general.prometheus_exporter_port.to_string(), - ), - ( - "connect_timeout".to_string(), - config.general.connect_timeout.to_string(), - ), - ( - "idle_timeout".to_string(), - config.general.idle_timeout.to_string(), - ), - ( - "healthcheck_timeout".to_string(), - config.general.healthcheck_timeout.to_string(), - ), - ( - "shutdown_timeout".to_string(), - config.general.shutdown_timeout.to_string(), - ), - ( - "healthcheck_delay".to_string(), - config.general.healthcheck_delay.to_string(), - ), - ("ban_time".to_string(), config.general.ban_time.to_string()), - ( - "idle_client_in_transaction_timeout".to_string(), - config - .general - .idle_client_in_transaction_timeout - .to_string(), - ), - ]; - - r.append(&mut static_settings); - return r.iter().cloned().collect(); - } -} - -impl Config { - /// Print current configuration. - pub fn show(&self) { - info!("Config path: {}", self.path); - info!("Ban time: {}s", self.general.ban_time); - info!( - "Idle client in transaction timeout: {}ms", - self.general.idle_client_in_transaction_timeout - ); - info!("Worker threads: {}", self.general.worker_threads); - info!( - "Healthcheck timeout: {}ms", - self.general.healthcheck_timeout - ); - info!("Connection timeout: {}ms", self.general.connect_timeout); - info!("Idle timeout: {}ms", self.general.idle_timeout); - info!( - "Log client connections: {}", - self.general.log_client_connections - ); - info!( - "Log client disconnections: {}", - self.general.log_client_disconnections - ); - 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!("Server round robin: {}", self.general.server_round_robin); - match self.general.tls_certificate.clone() { - Some(tls_certificate) => { - info!("TLS certificate: {}", tls_certificate); - - if let Some(tls_private_key) = self.general.tls_private_key.clone() { - info!("TLS private key: {}", tls_private_key); - info!("TLS support is enabled"); - } - } - - None => { - info!("TLS support is disabled"); - } - }; - info!("Server TLS enabled: {}", self.general.server_tls); - info!( - "Server TLS certificate verification: {}", - self.general.verify_server_certificate - ); - info!( - "Plugins: {}", - match self.plugins { - Some(ref plugins) => plugins.to_string(), - None => "not configured".into(), - } - ); - - for (pool_name, pool_config) in &self.pools { - // TODO: Make this output prettier (maybe a table?) - info!( - "[pool: {}] Maximum user connections: {}", - pool_name, - pool_config - .users - .values() - .map(|user_cfg| user_cfg.pool_size) - .sum::() - .to_string() - ); - info!( - "[pool: {}] Default pool mode: {}", - pool_name, - pool_config.pool_mode.to_string() - ); - info!( - "[pool: {}] Load Balancing mode: {:?}", - pool_name, pool_config.load_balancing_mode - ); - let connect_timeout = match pool_config.connect_timeout { - Some(connect_timeout) => connect_timeout, - None => self.general.connect_timeout, - }; - info!( - "[pool: {}] Connection timeout: {}ms", - pool_name, connect_timeout - ); - let idle_timeout = match pool_config.idle_timeout { - Some(idle_timeout) => idle_timeout, - None => self.general.idle_timeout, - }; - info!("[pool: {}] Idle timeout: {}ms", pool_name, idle_timeout); - info!( - "[pool: {}] Sharding function: {}", - pool_name, - pool_config.sharding_function.to_string() - ); - info!( - "[pool: {}] Primary reads: {}", - pool_name, pool_config.primary_reads_enabled - ); - info!( - "[pool: {}] Query router: {}", - pool_name, pool_config.query_parser_enabled - ); - - info!( - "[pool: {}] Query parser max length: {:?}", - pool_name, pool_config.query_parser_max_length - ); - info!( - "[pool: {}] Infer role from query: {}", - pool_name, pool_config.query_parser_read_write_splitting - ); - info!( - "[pool: {}] Number of shards: {}", - pool_name, - pool_config.shards.len() - ); - info!( - "[pool: {}] Number of users: {}", - 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: {}] Log client parameter status changes: {}", - pool_name, pool_config.log_client_parameter_status_changes - ); - info!( - "[pool: {}] Prepared statements server cache size: {}", - pool_name, pool_config.prepared_statements_cache_size - ); - 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(), - } - ); - info!( - "[pool: {}][user: {}] Connection timeout: {}", - pool_name, - user.1.username, - match user.1.connect_timeout { - Some(connect_timeout) => format!("{}ms", connect_timeout), - None => "not set".to_string(), - } - ); - info!( - "[pool: {}][user: {}] Idle timeout: {}", - pool_name, - user.1.username, - match user.1.idle_timeout { - Some(idle_timeout) => format!("{}ms", idle_timeout), - None => "not set".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! - if let Some(tls_certificate) = self.general.tls_certificate.clone() { - match load_certs(Path::new(&tls_certificate)) { - Ok(_) => { - // Cert is okay, but what about the private key? - match self.general.tls_private_key.clone() { - Some(tls_private_key) => match load_keys(Path::new(&tls_private_key)) { - Ok(_) => (), - Err(err) => { - error!("tls_private_key is incorrectly configured: {:?}", err); - return Err(Error::BadConfig); - } - }, - - None => { - error!("tls_certificate is set, but the tls_private_key is not"); - return Err(Error::BadConfig); - } - }; - } - - Err(err) => { - error!("tls_certificate is incorrectly configured: {:?}", err); - return Err(Error::BadConfig); - } - } - }; - - for pool in self.pools.values_mut() { - pool.validate()?; - } - - Ok(()) - } -} - -/// Get a read-only instance of the configuration -/// from anywhere in the app. -/// ArcSwap makes this cheap and quick. -pub fn get_config() -> Config { - (*(*CONFIG.load())).clone() -} - -pub fn get_idle_client_in_transaction_timeout() -> u64 { - CONFIG.load().general.idle_client_in_transaction_timeout -} - -/// Parse the configuration file located at the path. -pub async fn parse(path: &str) -> Result<(), Error> { - let mut contents = String::new(); - let mut file = match File::open(path).await { - Ok(file) => file, - Err(err) => { - error!("Could not open '{}': {}", path, err.to_string()); - return Err(Error::BadConfig); - } - }; - - match file.read_to_string(&mut contents).await { - Ok(_) => (), - Err(err) => { - error!("Could not read config file: {}", err.to_string()); - return Err(Error::BadConfig); - } - }; - - let mut config: Config = match toml::from_str(&contents) { - Ok(config) => config, - Err(err) => { - error!("Could not parse config file: {}", err.to_string()); - return Err(Error::BadConfig); - } - }; - - config.fill_up_auth_query_config(); - config.validate()?; - - config.path = path.to_string(); - - // Update the configuration globally. - CONFIG.store(Arc::new(config.clone())); - - Ok(()) -} - -pub async fn reload_config(client_server_map: ClientServerMap) -> Result { - let old_config = get_config(); - - match parse(&old_config.path).await { - Ok(()) => (), - Err(err) => { - error!("Config reload error: {:?}", err); - return Err(Error::BadConfig); - } - }; - - let new_config = get_config(); - - match CachedResolver::from_config().await { - Ok(_) => (), - 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 { - Ok(false) - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[tokio::test] - async fn test_config() { - parse("pgcat.toml").await.unwrap(); - - assert_eq!(get_config().path, "pgcat.toml".to_string()); - - assert_eq!(get_config().general.ban_time, 60); - assert_eq!(get_config().general.idle_client_in_transaction_timeout, 0); - assert_eq!(get_config().general.idle_timeout, 30000); - assert_eq!(get_config().pools.len(), 2); - assert_eq!(get_config().pools["sharded_db"].shards.len(), 3); - assert_eq!(get_config().pools["sharded_db"].idle_timeout, Some(40000)); - assert_eq!(get_config().pools["simple_db"].shards.len(), 1); - assert_eq!(get_config().pools["sharded_db"].users.len(), 2); - assert_eq!(get_config().pools["simple_db"].users.len(), 1); - - assert_eq!( - get_config().pools["sharded_db"].shards["0"].servers[0].host, - "127.0.0.1" - ); - assert_eq!( - get_config().pools["sharded_db"].shards["1"].servers[0].role, - Role::Primary - ); - assert_eq!( - get_config().pools["sharded_db"].shards["1"].database, - "shard1" - ); - assert_eq!( - get_config().pools["sharded_db"].users["0"].username, - "sharding_user" - ); - assert_eq!( - 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); - assert_eq!(get_config().pools["sharded_db"].default_role, "any"); - - assert_eq!( - get_config().pools["simple_db"].shards["0"].servers[0].host, - "127.0.0.1" - ); - assert_eq!( - get_config().pools["simple_db"].shards["0"].servers[0].port, - 5432 - ); - assert_eq!( - get_config().pools["simple_db"].shards["0"].database, - "some_db" - ); - assert_eq!(get_config().pools["simple_db"].default_role, "primary"); - - assert_eq!( - get_config().pools["simple_db"].users["0"].username, - "simple_user" - ); - assert_eq!( - 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] - async fn test_serialize_configs() { - parse("pgcat.toml").await.unwrap(); - print!("{}", toml::to_string(&get_config()).unwrap()); - } -} diff --git a/src/constants.rs b/src/constants.rs deleted file mode 100644 index 0900d7c..0000000 --- a/src/constants.rs +++ /dev/null @@ -1,33 +0,0 @@ -/// Various protocol constants, as defined in -/// -/// and elsewhere in the source code. - -// Used in the StartupMessage to indicate regular handshake. -pub const PROTOCOL_VERSION_NUMBER: i32 = 196608; - -// SSLRequest: used to indicate we want an SSL connection. -pub const SSL_REQUEST_CODE: i32 = 80877103; - -// CancelRequest: the cancel request code. -pub const CANCEL_REQUEST_CODE: i32 = 80877102; - -// AuthenticationMD5Password -pub const MD5_ENCRYPTED_PASSWORD: i32 = 5; - -// SASL -pub const SASL: i32 = 10; -pub const SASL_CONTINUE: i32 = 11; -pub const SASL_FINAL: i32 = 12; -pub const SCRAM_SHA_256: &str = "SCRAM-SHA-256"; -pub const NONCE_LENGTH: usize = 24; - -// AuthenticationOk -pub const AUTHENTICATION_SUCCESSFUL: i32 = 0; - -// ErrorResponse: A code identifying the field type; if zero, this is the message terminator and no string follows. -pub const MESSAGE_TERMINATOR: u8 = 0; - -// -// Data types -// -pub const _OID_INT8: i32 = 20; // bigint diff --git a/src/dns_cache.rs b/src/dns_cache.rs deleted file mode 100644 index 5c2be5d..0000000 --- a/src/dns_cache.rs +++ /dev/null @@ -1,410 +0,0 @@ -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 deleted file mode 100644 index 13047b4..0000000 --- a/src/errors.rs +++ /dev/null @@ -1,133 +0,0 @@ -//! Errors. - -/// Various errors. -#[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, - ServerMessageParserError(String), - 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), - InvalidShardId(usize), - PreparedStatementError, -} - -#[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 deleted file mode 100644 index 6a8a1e3..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,42 +0,0 @@ -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 plugins; -pub mod pool; -pub mod prometheus; -pub mod query_router; -pub mod scram; -pub mod server; -pub mod sharding; -pub mod stats; -pub mod tls; - -/// Format chrono::Duration to be more human-friendly. -/// -/// # Arguments -/// -/// * `duration` - A duration of time -pub fn format_duration(duration: &chrono::Duration) -> String { - let milliseconds = format!("{:0>3}", duration.num_milliseconds() % 1000); - - let seconds = format!("{:0>2}", duration.num_seconds() % 60); - - let minutes = format!("{:0>2}", duration.num_minutes() % 60); - - let hours = format!("{:0>2}", duration.num_hours() % 24); - - let days = duration.num_days().to_string(); - - format!( - "{}d {}:{}:{}.{}", - days, hours, minutes, seconds, milliseconds - ) -} diff --git a/src/logger.rs b/src/logger.rs deleted file mode 100644 index 867b1a2..0000000 --- a/src/logger.rs +++ /dev/null @@ -1,20 +0,0 @@ -use crate::cmd_args::{Args, LogFormat}; -use tracing_subscriber; -use tracing_subscriber::EnvFilter; - -pub fn init(args: &Args) { - // Iniitalize a default filter, and then override the builtin default "warning" with our - // commandline, (default: "info") - let filter = EnvFilter::from_default_env().add_directive(args.log_level.into()); - - let trace_sub = tracing_subscriber::fmt() - .with_thread_ids(true) - .with_env_filter(filter) - .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 deleted file mode 100644 index 6c8c165..0000000 --- a/src/main.rs +++ /dev/null @@ -1,340 +0,0 @@ -// Copyright (c) 2022 Lev Kokotov - -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: - -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -extern crate arc_swap; -extern crate async_trait; -extern crate bb8; -extern crate bytes; -extern crate exitcode; -extern crate log; -extern crate md5; -extern crate num_cpus; -extern crate once_cell; -extern crate rustls_pemfile; -extern crate serde; -extern crate serde_derive; -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; - -#[cfg(not(target_env = "msvc"))] -#[global_allocator] -static GLOBAL: Jemalloc = Jemalloc; - -use log::{debug, error, info, warn}; -use parking_lot::Mutex; -use pgcat::format_duration; -use tokio::net::TcpListener; -#[cfg(not(windows))] -use tokio::signal::unix::{signal as unix_signal, SignalKind}; -#[cfg(windows)] -use tokio::signal::windows as win_signal; -use tokio::{runtime::Builder, sync::mpsc}; - -use std::collections::HashMap; -use std::net::SocketAddr; -use std::str::FromStr; -use std::sync::Arc; -use tokio::sync::broadcast; - -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> { - let args = cmd_args::parse(); - logger::init(&args); - - info!("Welcome to PgCat! Meow. (Version {})", VERSION); - - if !pgcat::query_router::QueryRouter::setup() { - error!("Could not setup query router"); - std::process::exit(exitcode::CONFIG); - } - - // Create a transient runtime for loading the config for the first time. - { - let runtime = Builder::new_multi_thread().worker_threads(1).build()?; - - runtime.block_on(async { - match pgcat::config::parse(args.config_file.as_str()).await { - Ok(_) => (), - Err(err) => { - error!("Config parse error: {:?}", err); - std::process::exit(exitcode::CONFIG); - } - }; - }); - } - - let config = get_config(); - - // Create the runtime now we know required worker_threads. - let runtime = Builder::new_multi_thread() - .worker_threads(config.general.worker_threads) - .enable_all() - .build()?; - - runtime.block_on(async move { - - if let Some(true) = config.general.enable_prometheus_exporter { - let http_addr_str = format!( - "{}:{}", - config.general.host, config.general.prometheus_exporter_port - ); - - let http_addr = match SocketAddr::from_str(&http_addr_str) { - Ok(addr) => addr, - Err(err) => { - error!("Invalid http address: {}", err); - std::process::exit(exitcode::CONFIG); - } - }; - - tokio::task::spawn(async move { - start_metric_server(http_addr).await; - }); - } - - let addr = format!("{}:{}", config.general.host, config.general.port); - - let listener = match TcpListener::bind(&addr).await { - Ok(sock) => sock, - Err(err) => { - error!("Listener socket error: {:?}", err); - std::process::exit(exitcode::CONFIG); - } - }; - - info!("Running on {}", addr); - - config.show(); - - // Tracks which client is connected to which server for query cancellation. - let client_server_map: ClientServerMap = Arc::new(Mutex::new(HashMap::new())); - - // Statistics reporting. - REPORTER.store(Arc::new(Reporter::default())); - - // Starts (if enabled) dns cache before pools initialization - match dns_cache::CachedResolver::from_config().await { - Ok(_) => (), - Err(err) => error!("DNS cache initialization error: {:?}", err), - }; - - // Connection pool that allows to query all shards and replicas. - match ConnectionPool::from_config(client_server_map.clone()).await { - Ok(_) => (), - Err(err) => { - error!("Pool error: {:?}", err); - std::process::exit(exitcode::CONFIG); - } - }; - - tokio::task::spawn(async move { - let mut stats_collector = Collector::default(); - stats_collector.collect().await; - }); - - info!("Config autoreloader: {}", match config.general.autoreload { - Some(interval) => format!("{} ms", interval), - None => "disabled".into(), - }); - - 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; - debug!("Automatically reloading config"); - - if let Ok(changed) = reload_config(autoreload_client_server_map.clone()).await { - if changed { - get_config().show() - } - }; - } - }); - }; - - - - #[cfg(windows)] - let mut term_signal = win_signal::ctrl_close().unwrap(); - #[cfg(windows)] - let mut interrupt_signal = win_signal::ctrl_c().unwrap(); - #[cfg(windows)] - let mut sighup_signal = win_signal::ctrl_shutdown().unwrap(); - - #[cfg(not(windows))] - let mut term_signal = unix_signal(SignalKind::terminate()).unwrap(); - #[cfg(not(windows))] - let mut interrupt_signal = unix_signal(SignalKind::interrupt()).unwrap(); - #[cfg(not(windows))] - let mut sighup_signal = unix_signal(SignalKind::hangup()).unwrap(); - let (shutdown_tx, _) = broadcast::channel::<()>(1); - let (drain_tx, mut drain_rx) = mpsc::channel::(2048); - let (exit_tx, mut exit_rx) = mpsc::channel::<()>(1); - let mut admin_only = false; - let mut total_clients = 0; - - info!("Waiting for clients"); - - loop { - tokio::select! { - // Reload config: - // kill -SIGHUP $(pgrep pgcat) - _ = sighup_signal.recv() => { - info!("Reloading config"); - - _ = reload_config(client_server_map.clone()).await; - - get_config().show(); - }, - - // Initiate graceful shutdown sequence on sig int - _ = interrupt_signal.recv() => { - info!("Got SIGINT"); - - // Don't want this to happen more than once - if admin_only { - continue; - } - - admin_only = true; - - // Broadcast that client tasks need to finish - let _ = shutdown_tx.send(()); - let exit_tx = exit_tx.clone(); - let _ = drain_tx.send(0).await; - - tokio::task::spawn(async move { - let mut interval = tokio::time::interval(tokio::time::Duration::from_millis(config.general.shutdown_timeout)); - - // First tick fires immediately. - interval.tick().await; - - // Second one in the interval time. - interval.tick().await; - - // We're done waiting. - error!("Graceful shutdown timed out. {} active clients being closed", total_clients); - - let _ = exit_tx.send(()).await; - }); - }, - - _ = term_signal.recv() => { - info!("Got SIGTERM, closing with {} clients active", total_clients); - break; - }, - - new_client = listener.accept() => { - let (socket, addr) = match new_client { - Ok((socket, addr)) => (socket, addr), - Err(err) => { - error!("{:?}", err); - continue; - } - }; - - let shutdown_rx = shutdown_tx.subscribe(); - let drain_tx = drain_tx.clone(); - let client_server_map = client_server_map.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 pgcat::client::client_entrypoint( - socket, - client_server_map, - shutdown_rx, - drain_tx, - admin_only, - tls_certificate, - config.general.log_client_connections, - ) - .await - { - Ok(()) => { - let duration = chrono::offset::Utc::now().naive_utc() - start; - - if get_config().general.log_client_disconnections { - info!( - "Client {:?} disconnected, session duration: {}", - addr, - format_duration(&duration) - ); - } else { - debug!( - "Client {:?} disconnected, session duration: {}", - addr, - format_duration(&duration) - ); - } - } - - Err(err) => { - match err { - pgcat::errors::Error::ClientBadStartup => debug!("Client disconnected with error {:?}", err), - _ => warn!("Client disconnected with error {:?}", err), - } - - } - }; - }); - } - - _ = exit_rx.recv() => { - break; - } - - client_ping = drain_rx.recv() => { - let client_ping = client_ping.unwrap(); - total_clients += client_ping; - - if total_clients == 0 && admin_only { - let _ = exit_tx.send(()).await; - } - } - } - } - - info!("Shutting down..."); - }); - Ok(()) -} diff --git a/src/messages.rs b/src/messages.rs deleted file mode 100644 index 6a114e1..0000000 --- a/src/messages.rs +++ /dev/null @@ -1,1548 +0,0 @@ -/// Helper functions to send one-off protocol messages -/// and handle TcpStream (TCP socket). -use bytes::{Buf, BufMut, BytesMut}; -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 crate::constants::MESSAGE_TERMINATOR; -use std::collections::hash_map::DefaultHasher; -use std::collections::HashMap; -use std::ffi::CString; -use std::fmt::{Display, Formatter}; -use std::hash::{Hash, Hasher}; -use std::io::{BufRead, Cursor}; -use std::mem; -use std::str::FromStr; -use std::sync::atomic::Ordering; -use std::sync::Arc; -use std::time::Duration; - -/// Postgres data type mappings -/// used in RowDescription ('T') message. -pub enum DataType { - Text, - Int4, - Numeric, - Bool, - Oid, - AnyArray, - Any, -} - -impl From<&DataType> for i32 { - fn from(data_type: &DataType) -> i32 { - match data_type { - DataType::Text => 25, - DataType::Int4 => 23, - DataType::Numeric => 1700, - DataType::Bool => 16, - DataType::Oid => 26, - DataType::AnyArray => 2277, - DataType::Any => 2276, - } - } -} - -/// Tell the client that authentication handshake completed successfully. -pub async fn auth_ok(stream: &mut S) -> Result<(), Error> -where - S: tokio::io::AsyncWrite + std::marker::Unpin, -{ - let mut auth_ok = BytesMut::with_capacity(9); - - auth_ok.put_u8(b'R'); - auth_ok.put_i32(8); - auth_ok.put_i32(0); - - write_all(stream, auth_ok).await -} - -/// Generate md5 password challenge. -pub async fn md5_challenge(stream: &mut S) -> Result<[u8; 4], Error> -where - S: tokio::io::AsyncWrite + std::marker::Unpin, -{ - // let mut rng = rand::thread_rng(); - let salt: [u8; 4] = [ - rand::random(), - rand::random(), - rand::random(), - rand::random(), - ]; - - let mut res = BytesMut::new(); - res.put_u8(b'R'); - res.put_i32(12); - res.put_i32(5); // MD5 - res.put_slice(&salt[..]); - - write_all(stream, res).await?; - Ok(salt) -} - -/// Give the client the process_id and secret we generated -/// used in query cancellation. -pub async fn backend_key_data( - stream: &mut S, - backend_id: i32, - secret_key: i32, -) -> Result<(), Error> -where - S: tokio::io::AsyncWrite + std::marker::Unpin, -{ - let mut key_data = BytesMut::from(&b"K"[..]); - key_data.put_i32(12); - key_data.put_i32(backend_id); - key_data.put_i32(secret_key); - - write_all(stream, key_data).await -} - -/// Construct a `Q`: Query message. -pub fn simple_query(query: &str) -> BytesMut { - let mut res = BytesMut::from(&b"Q"[..]); - let query = format!("{}\0", query); - - res.put_i32(query.len() as i32 + 4); - res.put_slice(query.as_bytes()); - - res -} - -/// Tell the client we're ready for another query. -pub async fn send_ready_for_query(stream: &mut S) -> Result<(), Error> -where - S: tokio::io::AsyncWrite + std::marker::Unpin, -{ - write_all(stream, ready_for_query(false)).await -} - -/// 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 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 - - // User - bytes.put(&b"user\0"[..]); - bytes.put_slice(user.as_bytes()); - bytes.put_u8(0); - - // Application name - bytes.put(&b"application_name\0"[..]); - bytes.put_slice(&b"pgcat\0"[..]); - - // Database - bytes.put(&b"database\0"[..]); - bytes.put_slice(database.as_bytes()); - bytes.put_u8(0); - bytes.put_u8(0); // Null terminator - - let len = bytes.len() as i32 + 4i32; - - let mut startup = BytesMut::with_capacity(len as usize); - - startup.put_i32(len); - startup.put(bytes); - - match stream.write_all(&startup).await { - Ok(_) => Ok(()), - Err(err) => Err(Error::SocketError(format!( - "Error writing startup to server socket - Error: {:?}", - err - ))), - } -} - -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(); - let mut buf = Vec::new(); - let mut tmp = String::new(); - - while bytes.has_remaining() { - let mut c = bytes.get_u8(); - - // Null-terminated C-strings. - while c != 0 { - tmp.push(c as char); - c = bytes.get_u8(); - } - - if !tmp.is_empty() { - buf.push(tmp.clone()); - tmp.clear(); - } - } - - // Expect pairs of name and value - // and at least one pair to be present. - if buf.len() % 2 != 0 || buf.len() < 2 { - return Err(Error::ClientBadStartup); - } - - let mut i = 0; - while i < buf.len() { - let name = buf[i].clone(); - let value = buf[i + 1].clone(); - let _ = result.insert(name, value); - i += 2; - } - - Ok(result) -} - -/// Parse StartupMessage parameters. -/// e.g. user, database, application_name, etc. -pub fn parse_startup(bytes: BytesMut) -> Result, Error> { - let result = parse_params(bytes)?; - - // Minimum required parameters - // I want to have the user at the very minimum, according to the protocol spec. - if !result.contains_key("user") { - return Err(Error::ClientBadStartup); - } - - Ok(result) -} - -/// Create md5 password hash given a salt. -pub fn md5_hash_password(user: &str, password: &str, salt: &[u8]) -> Vec { - let mut md5 = Md5::new(); - - // First pass - md5.update(password.as_bytes()); - md5.update(user.as_bytes()); - - let output = md5.finalize_reset(); - - // Second pass - 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()) - .chars() - .map(|x| x as u8) - .collect::>(); - password.push(0); - - password -} - -/// Send password challenge response to the server. -/// This is the MD5 challenge. -pub async fn md5_password( - stream: &mut S, - user: &str, - password: &str, - salt: &[u8], -) -> Result<(), Error> -where - S: tokio::io::AsyncWrite + std::marker::Unpin, -{ - let password = md5_hash_password(user, password, salt); - - let mut message = BytesMut::with_capacity(password.len() + 5); - - message.put_u8(b'p'); - message.put_i32(password.len() as i32 + 4); - message.put_slice(&password[..]); - - 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() + 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. -pub async fn custom_protocol_response_ok(stream: &mut S, message: &str) -> Result<(), Error> -where - S: tokio::io::AsyncWrite + std::marker::Unpin, -{ - let mut res = BytesMut::with_capacity(25); - - let set_complete = BytesMut::from(&format!("{}\0", message)[..]); - let len = (set_complete.len() + 4) as i32; - - // CommandComplete - res.put_u8(b'C'); - res.put_i32(len); - res.put_slice(&set_complete[..]); - - write_all_half(stream, &res).await?; - send_ready_for_query(stream).await -} - -/// Send a custom error message to the client. -/// Tell the client we are ready for the next query and no rollback is necessary. -/// Docs on error codes: . -pub async fn error_response(stream: &mut S, message: &str) -> Result<(), Error> -where - S: tokio::io::AsyncWrite + std::marker::Unpin, -{ - error_response_terminal(stream, message).await?; - send_ready_for_query(stream).await -} - -/// Send a custom error message to the client. -/// Tell the client we are ready for the next query and no rollback is necessary. -/// Docs on error codes: . -pub async fn error_response_terminal(stream: &mut S, message: &str) -> Result<(), Error> -where - S: tokio::io::AsyncWrite + std::marker::Unpin, -{ - let mut error = BytesMut::new(); - - // Error level - error.put_u8(b'S'); - error.put_slice(&b"FATAL\0"[..]); - - // Error level (non-translatable) - error.put_u8(b'V'); - error.put_slice(&b"FATAL\0"[..]); - - // Error code: not sure how much this matters. - error.put_u8(b'C'); - error.put_slice(&b"58000\0"[..]); // system_error, see Appendix A. - - // The short error message. - error.put_u8(b'M'); - error.put_slice(format!("{}\0", message).as_bytes()); - - // No more fields follow. - error.put_u8(0); - - // Compose the two message reply. - let mut res = BytesMut::with_capacity(error.len() + 5); - - res.put_u8(b'E'); - res.put_i32(error.len() as i32 + 4); - res.put(error); - - write_all_half(stream, &res).await -} - -pub async fn wrong_password(stream: &mut S, user: &str) -> Result<(), Error> -where - S: tokio::io::AsyncWrite + std::marker::Unpin, -{ - let mut error = BytesMut::new(); - - // Error level - error.put_u8(b'S'); - error.put_slice(&b"FATAL\0"[..]); - - // Error level (non-translatable) - error.put_u8(b'V'); - error.put_slice(&b"FATAL\0"[..]); - - // Error code: not sure how much this matters. - error.put_u8(b'C'); - error.put_slice(&b"28P01\0"[..]); // system_error, see Appendix A. - - // The short error message. - error.put_u8(b'M'); - error.put_slice(format!("password authentication failed for user \"{}\"\0", user).as_bytes()); - - // No more fields follow. - error.put_u8(0); - - // Compose the two message reply. - let mut res = BytesMut::new(); - - res.put_u8(b'E'); - res.put_i32(error.len() as i32 + 4); - - res.put(error); - - write_all(stream, res).await -} - -/// Respond to a SHOW SHARD command. -pub async fn show_response(stream: &mut S, name: &str, value: &str) -> Result<(), Error> -where - S: tokio::io::AsyncWrite + std::marker::Unpin, -{ - // A SELECT response consists of: - // 1. RowDescription - // 2. One or more DataRow - // 3. CommandComplete - // 4. ReadyForQuery - - // The final messages sent to the client - let mut res = BytesMut::new(); - - // RowDescription - res.put(row_description(&vec![(name, DataType::Text)])); - - // DataRow - res.put(data_row(&vec![value.to_string()])); - - // CommandComplete - res.put(command_complete("SELECT 1")); - - write_all_half(stream, &res).await?; - send_ready_for_query(stream).await -} - -pub fn row_description(columns: &Vec<(&str, DataType)>) -> BytesMut { - let mut res = BytesMut::new(); - let mut row_desc = BytesMut::new(); - - // how many columns we are storing - row_desc.put_i16(columns.len() as i16); - - for (name, data_type) in columns { - // Column name - row_desc.put_slice(format!("{}\0", name).as_bytes()); - - // Doesn't belong to any table - row_desc.put_i32(0); - - // Doesn't belong to any table - row_desc.put_i16(0); - - // Text - row_desc.put_i32(data_type.into()); - - // Text size = variable (-1) - let type_size = match data_type { - 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); - - // Type modifier: none that I know - row_desc.put_i32(-1); - - // Format being used: text (0), binary (1) - row_desc.put_i16(0); - } - - res.put_u8(b'T'); - res.put_i32(row_desc.len() as i32 + 4); - res.put(row_desc); - - res -} - -/// Create a DataRow message. -pub fn data_row(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 { - let column = column.as_bytes(); - data_row.put_i32(column.len() as i32); - data_row.put_slice(column); - } - - res.put_u8(b'D'); - res.put_i32(data_row.len() as i32 + 4); - res.put(data_row); - - 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_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()); - let mut res = BytesMut::new(); - res.put_u8(b'C'); - res.put_i32(cmd.len() as i32 + 4); - res.put(cmd); - 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 -} - -pub fn sync() -> BytesMut { - let mut bytes = BytesMut::with_capacity(mem::size_of::() + mem::size_of::()); - bytes.put_u8(b'S'); - bytes.put_i32(4); - bytes -} - -pub fn parse_complete() -> BytesMut { - let mut bytes = BytesMut::with_capacity(mem::size_of::() + mem::size_of::()); - - bytes.put_u8(b'1'); - bytes.put_i32(4); - bytes -} - -pub fn ready_for_query(in_transaction: bool) -> BytesMut { - let mut bytes = BytesMut::with_capacity( - mem::size_of::() + mem::size_of::() + mem::size_of::(), - ); - - bytes.put_u8(b'Z'); - bytes.put_i32(5); - if in_transaction { - bytes.put_u8(b'T'); - } else { - bytes.put_u8(b'I'); - } - - bytes -} - -/// Write all data in the buffer to the TcpStream. -pub async fn write_all(stream: &mut S, buf: BytesMut) -> Result<(), Error> -where - S: tokio::io::AsyncWrite + std::marker::Unpin, -{ - match stream.write_all(&buf).await { - Ok(_) => Ok(()), - Err(err) => Err(Error::SocketError(format!( - "Error writing to socket - Error: {:?}", - err - ))), - } -} - -/// Write all the data in the buffer to the TcpStream, write owned half (see mpsc). -pub async fn write_all_half(stream: &mut S, buf: &BytesMut) -> Result<(), Error> -where - S: tokio::io::AsyncWrite + std::marker::Unpin, -{ - match stream.write_all(buf).await { - Ok(_) => Ok(()), - Err(err) => Err(Error::SocketError(format!( - "Error writing to socket - Error: {:?}", - err - ))), - } -} - -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) => Err(Error::SocketError(format!( - "Error flushing socket - Error: {:?}", - err - ))), - }, - Err(err) => 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 - S: tokio::io::AsyncRead + std::marker::Unpin, -{ - let code = match stream.read_u8().await { - Ok(code) => code, - Err(err) => { - return Err(Error::SocketError(format!( - "Error reading message code from socket - Error {:?}", - err - ))) - } - }; - - let len = match stream.read_i32().await { - Ok(len) => len, - Err(err) => { - return Err(Error::SocketError(format!( - "Error reading message len from socket - Code: {:?}, Error: {:?}", - code, err - ))) - } - }; - - let mut bytes = BytesMut::with_capacity(len as usize + 1); - - bytes.put_u8(code); - bytes.put_i32(len); - - bytes.resize(bytes.len() + len as usize - mem::size_of::(), b'0'); - - let slice_start = mem::size_of::() + mem::size_of::(); - let slice_end = slice_start + len as usize - mem::size_of::(); - - // Avoids a panic - if slice_end < slice_start { - return Err(Error::SocketError(format!( - "Error reading message from socket - Code: {:?} - Length {:?}, Error: {:?}", - code, len, "Unexpected length value for message" - ))); - } - - match stream.read_exact(&mut bytes[slice_start..slice_end]).await { - Ok(_) => (), - Err(err) => { - return Err(Error::SocketError(format!( - "Error reading message from socket - Code: {:?}, Error: {:?}", - code, err - ))) - } - }; - - Ok(bytes) -} - -pub fn server_parameter_message(key: &str, value: &str) -> BytesMut { - let mut server_info = BytesMut::new(); - - let null_byte_size = 1; - let len: usize = - mem::size_of::() + key.len() + null_byte_size + value.len() + null_byte_size; - - server_info.put_slice("S".as_bytes()); - server_info.put_i32(len.try_into().unwrap()); - server_info.put_slice(key.as_bytes()); - server_info.put_bytes(0, 1); - server_info.put_slice(value.as_bytes()); - server_info.put_bytes(0, 1); - - server_info -} - -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( - &TcpKeepalive::new() - .with_interval(Duration::from_secs(conf.general.tcp_keepalives_interval)) - .with_retries(conf.general.tcp_keepalives_count) - .with_time(Duration::from_secs(conf.general.tcp_keepalives_idle)), - ) { - Ok(_) => (), - Err(err) => error!("Could not configure tcp_keepalive for socket: {}", err), - } - } - Err(err) => error!("Could not configure socket: {}", err), - } - match sock_ref.set_nodelay(true) { - Ok(_) => (), - Err(err) => error!("Could not configure TCP_NODELAY for socket: {}", err), - } -} - -pub trait BytesMutReader { - fn read_string(&mut self) -> Result; -} - -impl BytesMutReader for Cursor<&BytesMut> { - /// Should only be used when reading strings from the message protocol. - /// Can be used to read multiple strings from the same message which are separated by the null byte - fn read_string(&mut self) -> Result { - let mut buf = vec![]; - match self.read_until(b'\0', &mut buf) { - Ok(_) => Ok(String::from_utf8_lossy(&buf[..buf.len() - 1]).to_string()), - Err(err) => Err(Error::ParseBytesError(err.to_string())), - } - } -} - -impl BytesMutReader for BytesMut { - /// Should only be used when reading strings from the message protocol. - /// Can be used to read multiple strings from the same message which are separated by the null byte - fn read_string(&mut self) -> Result { - let null_index = self.iter().position(|&byte| byte == b'\0'); - - match null_index { - Some(index) => { - let string_bytes = self.split_to(index + 1); - Ok(String::from_utf8_lossy(&string_bytes[..string_bytes.len() - 1]).to_string()) - } - None => Err(Error::ParseBytesError("Could not read string".to_string())), - } - } -} - -pub enum ExtendedProtocolData { - Parse { - data: BytesMut, - metadata: Option<(Arc, u64)>, - }, - Bind { - data: BytesMut, - metadata: Option, - }, - Describe { - data: BytesMut, - metadata: Option, - }, - Execute { - data: BytesMut, - }, - Close { - data: BytesMut, - close: Close, - }, -} - -impl ExtendedProtocolData { - pub fn create_new_parse(data: BytesMut, metadata: Option<(Arc, u64)>) -> Self { - Self::Parse { data, metadata } - } - - pub fn create_new_bind(data: BytesMut, metadata: Option) -> Self { - Self::Bind { data, metadata } - } - - pub fn create_new_describe(data: BytesMut, metadata: Option) -> Self { - Self::Describe { data, metadata } - } - - pub fn create_new_execute(data: BytesMut) -> Self { - Self::Execute { data } - } - - pub fn create_new_close(data: BytesMut, close: Close) -> Self { - Self::Close { data, close } - } -} - -/// Parse (F) message. -/// See: -#[derive(Clone, Debug)] -pub struct Parse { - code: char, - #[allow(dead_code)] - len: i32, - pub 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, - 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 { - /// Renames the prepared statement to a new name based on the global counter - pub fn rewrite(mut self) -> Self { - self.name = format!( - "PGCAT_{}", - PREPARED_STATEMENT_COUNTER.fetch_add(1, Ordering::SeqCst) - ); - self - } - - /// Gets the name of the prepared statement from the buffer - pub fn get_name(buf: &BytesMut) -> Result { - let mut cursor = Cursor::new(buf); - // Skip the code and length - cursor.advance(mem::size_of::() + mem::size_of::()); - cursor.read_string() - } - - /// Hashes the parse statement to be used as a key in the global cache - pub fn get_hash(&self) -> u64 { - // TODO_ZAIN: Take a look at which hashing function is being used - let mut hasher = DefaultHasher::new(); - - let concatenated = format!( - "{}{}{}", - self.query, - self.num_params, - self.param_types - .iter() - .map(ToString::to_string) - .collect::>() - .join(",") - ); - - concatenated.hash(&mut hasher); - - hasher.finish() - } - - 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 { - /// Gets the name of the prepared statement from the buffer - pub fn get_name(buf: &BytesMut) -> Result { - let mut cursor = Cursor::new(buf); - // Skip the code and length - cursor.advance(mem::size_of::() + mem::size_of::()); - cursor.read_string()?; - cursor.read_string() - } - - /// Renames the prepared statement to a new name - pub fn rename(buf: BytesMut, new_name: &str) -> Result { - let mut cursor = Cursor::new(&buf); - // Read basic data from the cursor - let code = cursor.get_u8(); - let current_len = cursor.get_i32(); - let portal = cursor.read_string()?; - let prepared_statement = cursor.read_string()?; - - // Calculate new length - let new_len = current_len + new_name.len() as i32 - prepared_statement.len() as i32; - - // Begin building the response buffer - let mut response_buf = BytesMut::with_capacity(new_len as usize + 1); - response_buf.put_u8(code); - response_buf.put_i32(new_len); - - // Put the portal and new name into the buffer - // Note: panic if the provided string contains null byte - response_buf.put_slice(CString::new(portal)?.as_bytes_with_nul()); - response_buf.put_slice(CString::new(new_name)?.as_bytes_with_nul()); - - // Add the remainder of the original buffer into the response - response_buf.put_slice(&buf[cursor.position() as usize..]); - - // Return the buffer - Ok(response_buf) - } - - pub fn anonymous(&self) -> bool { - self.prepared_statement.is_empty() - } -} - -#[derive(Debug, Clone)] -pub struct Describe { - code: char, - - #[allow(dead_code)] - len: i32, - pub 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 empty_new() -> Describe { - Describe { - code: 'D', - len: 4 + 1 + 1, - target: 'S', - statement_name: "".to_string(), - } - } - - 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 -} - -// from https://www.postgresql.org/docs/12/protocol-error-fields.html -#[derive(Debug, Default, PartialEq)] -pub struct PgErrorMsg { - pub severity_localized: String, // S - pub severity: String, // V - pub code: String, // C - pub message: String, // M - pub detail: Option, // D - pub hint: Option, // H - pub position: Option, // P - pub internal_position: Option, // p - pub internal_query: Option, // q - pub where_context: Option, // W - pub schema_name: Option, // s - pub table_name: Option, // t - pub column_name: Option, // c - pub data_type_name: Option, // d - pub constraint_name: Option, // n - pub file_name: Option, // F - pub line: Option, // L - pub routine: Option, // R -} - -// TODO: implement with https://docs.rs/derive_more/latest/derive_more/ -impl Display for PgErrorMsg { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "[severity: {}]", self.severity)?; - write!(f, "[code: {}]", self.code)?; - write!(f, "[message: {}]", self.message)?; - if let Some(val) = &self.detail { - write!(f, "[detail: {val}]")?; - } - if let Some(val) = &self.hint { - write!(f, "[hint: {val}]")?; - } - if let Some(val) = &self.position { - write!(f, "[position: {val}]")?; - } - if let Some(val) = &self.internal_position { - write!(f, "[internal_position: {val}]")?; - } - if let Some(val) = &self.internal_query { - write!(f, "[internal_query: {val}]")?; - } - if let Some(val) = &self.internal_query { - write!(f, "[internal_query: {val}]")?; - } - if let Some(val) = &self.where_context { - write!(f, "[where: {val}]")?; - } - if let Some(val) = &self.schema_name { - write!(f, "[schema_name: {val}]")?; - } - if let Some(val) = &self.table_name { - write!(f, "[table_name: {val}]")?; - } - if let Some(val) = &self.column_name { - write!(f, "[column_name: {val}]")?; - } - if let Some(val) = &self.data_type_name { - write!(f, "[data_type_name: {val}]")?; - } - if let Some(val) = &self.constraint_name { - write!(f, "[constraint_name: {val}]")?; - } - if let Some(val) = &self.file_name { - write!(f, "[file_name: {val}]")?; - } - if let Some(val) = &self.line { - write!(f, "[line: {val}]")?; - } - if let Some(val) = &self.routine { - write!(f, "[routine: {val}]")?; - } - - write!(f, " ")?; - - Ok(()) - } -} - -impl PgErrorMsg { - pub fn parse(error_msg: &[u8]) -> Result { - let mut out = PgErrorMsg { - severity_localized: "".to_string(), - severity: "".to_string(), - code: "".to_string(), - message: "".to_string(), - detail: None, - hint: None, - position: None, - internal_position: None, - internal_query: None, - where_context: None, - schema_name: None, - table_name: None, - column_name: None, - data_type_name: None, - constraint_name: None, - file_name: None, - line: None, - routine: None, - }; - for msg_part in error_msg.split(|v| *v == MESSAGE_TERMINATOR) { - if msg_part.is_empty() { - continue; - } - - let msg_content = match String::from_utf8_lossy(&msg_part[1..]).parse() { - Ok(c) => c, - Err(err) => { - return Err(Error::ServerMessageParserError(format!( - "could not parse server message field. err {:?}", - err - ))) - } - }; - - match &msg_part[0] { - b'S' => { - out.severity_localized = msg_content; - } - b'V' => { - out.severity = msg_content; - } - b'C' => { - out.code = msg_content; - } - b'M' => { - out.message = msg_content; - } - b'D' => { - out.detail = Some(msg_content); - } - b'H' => { - out.hint = Some(msg_content); - } - b'P' => out.position = Some(u32::from_str(msg_content.as_str()).unwrap_or(0)), - b'p' => { - out.internal_position = Some(u32::from_str(msg_content.as_str()).unwrap_or(0)) - } - b'q' => { - out.internal_query = Some(msg_content); - } - b'W' => { - out.where_context = Some(msg_content); - } - b's' => { - out.schema_name = Some(msg_content); - } - b't' => { - out.table_name = Some(msg_content); - } - b'c' => { - out.column_name = Some(msg_content); - } - b'd' => { - out.data_type_name = Some(msg_content); - } - b'n' => { - out.constraint_name = Some(msg_content); - } - b'F' => { - out.file_name = Some(msg_content); - } - b'L' => out.line = Some(u32::from_str(msg_content.as_str()).unwrap_or(0)), - b'R' => { - out.routine = Some(msg_content); - } - _ => {} - } - } - - Ok(out) - } -} - -#[cfg(test)] -mod tests { - use crate::messages::PgErrorMsg; - use log::{error, info}; - - fn field(kind: char, content: &str) -> Vec { - format!("{kind}{content}\0").as_bytes().to_vec() - } - - #[test] - fn parse_fields() { - let mut complete_msg = vec![]; - let severity = "FATAL"; - complete_msg.extend(field('S', severity)); - complete_msg.extend(field('V', severity)); - - let error_code = "29P02"; - complete_msg.extend(field('C', error_code)); - let message = "password authentication failed for user \"wrong_user\""; - complete_msg.extend(field('M', message)); - let detail_msg = "super detailed message"; - complete_msg.extend(field('D', detail_msg)); - let hint_msg = "hint detail here"; - complete_msg.extend(field('H', hint_msg)); - complete_msg.extend(field('P', "123")); - complete_msg.extend(field('p', "234")); - let internal_query = "SELECT * from foo;"; - complete_msg.extend(field('q', internal_query)); - let where_msg = "where goes here"; - complete_msg.extend(field('W', where_msg)); - let schema_msg = "schema_name"; - complete_msg.extend(field('s', schema_msg)); - let table_msg = "table_name"; - complete_msg.extend(field('t', table_msg)); - let column_msg = "column_name"; - complete_msg.extend(field('c', column_msg)); - let data_type_msg = "type_name"; - complete_msg.extend(field('d', data_type_msg)); - let constraint_msg = "constraint_name"; - complete_msg.extend(field('n', constraint_msg)); - let file_msg = "pgcat.c"; - complete_msg.extend(field('F', file_msg)); - complete_msg.extend(field('L', "335")); - let routine_msg = "my_failing_routine"; - complete_msg.extend(field('R', routine_msg)); - - tracing_subscriber::fmt() - .with_max_level(tracing::Level::INFO) - .with_ansi(true) - .init(); - - info!( - "full message: {}", - PgErrorMsg::parse(&complete_msg).unwrap() - ); - assert_eq!( - PgErrorMsg { - severity_localized: severity.to_string(), - severity: severity.to_string(), - code: error_code.to_string(), - message: message.to_string(), - detail: Some(detail_msg.to_string()), - hint: Some(hint_msg.to_string()), - position: Some(123), - internal_position: Some(234), - internal_query: Some(internal_query.to_string()), - where_context: Some(where_msg.to_string()), - schema_name: Some(schema_msg.to_string()), - table_name: Some(table_msg.to_string()), - column_name: Some(column_msg.to_string()), - data_type_name: Some(data_type_msg.to_string()), - constraint_name: Some(constraint_msg.to_string()), - file_name: Some(file_msg.to_string()), - line: Some(335), - routine: Some(routine_msg.to_string()), - }, - PgErrorMsg::parse(&complete_msg).unwrap() - ); - - let mut only_mandatory_msg = vec![]; - only_mandatory_msg.extend(field('S', severity)); - only_mandatory_msg.extend(field('V', severity)); - only_mandatory_msg.extend(field('C', error_code)); - only_mandatory_msg.extend(field('M', message)); - only_mandatory_msg.extend(field('D', detail_msg)); - - let err_fields = PgErrorMsg::parse(&only_mandatory_msg).unwrap(); - info!("only mandatory fields: {}", &err_fields); - error!( - "server error: {}: {}", - err_fields.severity, err_fields.message - ); - assert_eq!( - PgErrorMsg { - severity_localized: severity.to_string(), - severity: severity.to_string(), - code: error_code.to_string(), - message: message.to_string(), - detail: Some(detail_msg.to_string()), - hint: None, - position: None, - internal_position: None, - internal_query: None, - where_context: None, - schema_name: None, - table_name: None, - column_name: None, - data_type_name: None, - constraint_name: None, - file_name: None, - line: None, - routine: None, - }, - PgErrorMsg::parse(&only_mandatory_msg).unwrap() - ); - } -} diff --git a/src/mirrors.rs b/src/mirrors.rs deleted file mode 100644 index 73ab732..0000000 --- a/src/mirrors.rs +++ /dev/null @@ -1,190 +0,0 @@ -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 log::{error, info, trace, warn}; -use tokio::sync::mpsc::{channel, Receiver, Sender}; - -pub struct MirroredClient { - address: Address, - user: User, - database: String, - bytes_rx: Receiver, - disconnect_rx: Receiver<()>, -} - -impl MirroredClient { - async fn create_pool(&self) -> Pool { - let config = get_config(); - let default = std::time::Duration::from_millis(10_000).as_millis() as u64; - let (connection_timeout, idle_timeout, _cfg, prepared_statement_cache_size) = - match config.pools.get(&self.address.pool_name) { - Some(cfg) => ( - cfg.connect_timeout.unwrap_or(default), - cfg.idle_timeout.unwrap_or(default), - cfg.clone(), - cfg.prepared_statements_cache_size, - ), - None => (default, default, crate::config::Pool::default(), 0), - }; - - let manager = ServerPool::new( - self.address.clone(), - self.user.clone(), - self.database.as_str(), - ClientServerMap::default(), - Arc::new(RwLock::new(None)), - None, - true, - false, - prepared_statement_cache_size, - ); - - Pool::builder() - .max_size(1) - .connection_timeout(std::time::Duration::from_millis(connection_timeout)) - .idle_timeout(Some(std::time::Duration::from_millis(idle_timeout))) - .test_on_check_out(false) - .build(manager) - .await - .unwrap() - } - - pub fn start(mut self) { - tokio::spawn(async move { - let pool = self.create_pool().await; - let address = self.address.clone(); - loop { - let mut server = match pool.get().await { - Ok(server) => server, - Err(err) => { - error!( - "Failed to get connection from pool, Discarding message {:?}, {:?}", - err, - address.clone() - ); - continue; - } - }; - - tokio::select! { - // Exit channel events - _ = self.disconnect_rx.recv() => { - info!("Got mirror exit signal, exiting {:?}", address.clone()); - break; - } - - // Incoming data from server (we read to clear the socket buffer and discard the data) - recv_result = server.recv(None) => { - match recv_result { - Ok(message) => trace!("Received from mirror: {} {:?}", String::from_utf8_lossy(&message[..]), address.clone()), - Err(err) => { - server.mark_bad( - format!("Failed to send to mirror, Discarding message {:?}, {:?}", err, address.clone()).as_str() - ); - } - } - } - - // Messages to send to the server - message = self.bytes_rx.recv() => { - match message { - Some(bytes) => { - match server.send(&BytesMut::from(&bytes[..])).await { - Ok(_) => trace!("Sent to mirror: {} {:?}", String::from_utf8_lossy(&bytes[..]), address.clone()), - Err(err) => { - server.mark_bad( - format!("Failed to receive from mirror {:?} {:?}", err, address.clone()).as_str() - ); - } - } - } - None => { - info!("Mirror channel closed, exiting {:?}", address.clone()); - break; - }, - } - } - } - } - }); - } -} -pub struct MirroringManager { - pub byte_senders: Vec>, - pub disconnect_senders: Vec>, -} -impl MirroringManager { - pub fn from_addresses( - user: User, - database: String, - addresses: Vec
, - ) -> MirroringManager { - let mut byte_senders: Vec> = vec![]; - let mut exit_senders: Vec> = vec![]; - - addresses.iter().for_each(|mirror| { - let (bytes_tx, bytes_rx) = channel::(10); - let (exit_tx, exit_rx) = channel::<()>(1); - let mut addr = mirror.clone(); - addr.role = Role::Mirror; - let client = MirroredClient { - user: user.clone(), - database: database.to_owned(), - address: addr, - bytes_rx, - disconnect_rx: exit_rx, - }; - exit_senders.push(exit_tx); - byte_senders.push(bytes_tx); - client.start(); - }); - - Self { - byte_senders, - disconnect_senders: exit_senders, - } - } - - pub fn send(&mut self, bytes: &BytesMut) { - // We want to avoid performing an allocation if we won't be able to send the message - // There is a possibility of a race here where we check the capacity and then the channel is - // closed or the capacity is reduced to 0, but mirroring is best effort anyway - if self - .byte_senders - .iter() - .all(|sender| sender.capacity() == 0 || sender.is_closed()) - { - return; - } - let immutable_bytes = bytes.clone().freeze(); - self.byte_senders.iter_mut().for_each(|sender| { - match sender.try_send(immutable_bytes.clone()) { - Ok(_) => {} - Err(err) => { - warn!("Failed to send bytes to a mirror channel {}", err); - } - } - }); - } - - pub fn disconnect(&mut self) { - self.disconnect_senders - .iter_mut() - .for_each(|sender| match sender.try_send(()) { - Ok(_) => {} - Err(err) => { - warn!( - "Failed to send disconnect signal to a mirror channel {}", - err - ); - } - }); - } -} diff --git a/src/plugins/intercept.rs b/src/plugins/intercept.rs deleted file mode 100644 index d13ab07..0000000 --- a/src/plugins/intercept.rs +++ /dev/null @@ -1,120 +0,0 @@ -//! 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.is_empty() { - 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 deleted file mode 100644 index f1076d0..0000000 --- a/src/plugins/mod.rs +++ /dev/null @@ -1,45 +0,0 @@ -//! 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. - #[allow(clippy::ptr_arg)] - 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 deleted file mode 100644 index cd93db9..0000000 --- a/src/plugins/prewarmer.rs +++ /dev/null @@ -1,28 +0,0 @@ -//! 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 deleted file mode 100644 index 012b040..0000000 --- a/src/plugins/query_logger.rs +++ /dev/null @@ -1,38 +0,0 @@ -//! 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.db, self.user, query); - - Ok(PluginOutput::Allow) - } -} diff --git a/src/plugins/table_access.rs b/src/plugins/table_access.rs deleted file mode 100644 index b8153b5..0000000 --- a/src/plugins/table_access.rs +++ /dev/null @@ -1,59 +0,0 @@ -//! 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 deleted file mode 100644 index 7915a0a..0000000 --- a/src/pool.rs +++ /dev/null @@ -1,1223 +0,0 @@ -use arc_swap::ArcSwap; -use async_trait::async_trait; -use bb8::{ManageConnection, Pool, PooledConnection, QueueStrategy}; -use chrono::naive::NaiveDateTime; -use log::{debug, error, info, warn}; -use lru::LruCache; -use once_cell::sync::Lazy; -use parking_lot::{Mutex, RwLock}; -use rand::seq::SliceRandom; -use rand::thread_rng; -use regex::Regex; -use std::collections::HashMap; -use std::fmt::{Display, Formatter}; -use std::num::NonZeroUsize; -use std::sync::atomic::AtomicU64; -use std::sync::{ - atomic::{AtomicBool, Ordering}, - Arc, -}; -use std::time::Instant; -use tokio::sync::Notify; - -use crate::config::{ - get_config, Address, DefaultShard, General, LoadBalancingMode, Plugins, PoolMode, Role, User, -}; -use crate::errors::Error; - -use crate::auth_passthrough::AuthPassthrough; -use crate::messages::Parse; -use crate::plugins::prewarmer; -use crate::server::{Server, ServerParameters}; -use crate::sharding::ShardingFunction; -use crate::stats::{AddressStats, ClientStats, ServerStats}; - -pub type ProcessId = i32; -pub type SecretKey = i32; -pub type ServerHost = String; -pub type ServerPort = u16; - -pub type BanList = Arc>>>; -pub type ClientServerMap = - Arc>>; -pub type PoolMap = HashMap; -/// The connection pool, globally available. -/// This is atomic and safe and read-optimized. -/// The pool is recreated dynamically when the config is reloaded. -pub static POOLS: Lazy> = Lazy::new(|| ArcSwap::from_pointee(HashMap::default())); - -// Reasons for banning a server. -#[derive(Debug, PartialEq, Clone)] -pub enum BanReason { - FailedHealthCheck, - MessageSendFailed, - MessageReceiveFailed, - FailedCheckout, - StatementTimeout, - AdminBan(i64), -} - -pub type PreparedStatementCacheType = Arc>; - -// TODO: Add stats the this cache -// TODO: Add application name to the cache value to help identify which application is using the cache -// TODO: Create admin command to show which statements are in the cache -#[derive(Debug)] -pub struct PreparedStatementCache { - cache: LruCache>, -} - -impl PreparedStatementCache { - pub fn new(mut size: usize) -> Self { - // Cannot be zeros - if size == 0 { - size = 1; - } - - PreparedStatementCache { - cache: LruCache::new(NonZeroUsize::new(size).unwrap()), - } - } - - /// Adds the prepared statement to the cache if it doesn't exist with a new name - /// if it already exists will give you the existing parse - /// - /// Pass the hash to this so that we can do the compute before acquiring the lock - pub fn get_or_insert(&mut self, parse: &Parse, hash: u64) -> Arc { - match self.cache.get(&hash) { - Some(rewritten_parse) => rewritten_parse.clone(), - None => { - let new_parse = Arc::new(parse.clone().rewrite()); - let evicted = self.cache.push(hash, new_parse.clone()); - - if let Some((_, evicted_parse)) = evicted { - debug!( - "Evicted prepared statement {} from cache", - evicted_parse.name - ); - } - - new_parse - } - } - } - - /// Marks the hash as most recently used if it exists - pub fn promote(&mut self, hash: &u64) { - self.cache.promote(hash); - } -} - -/// An identifier for a PgCat pool, -/// a database visible to clients. -#[derive(Hash, Debug, Clone, PartialEq, Eq, Default)] -pub struct PoolIdentifier { - // The name of the database clients want to connect to. - pub db: String, - - /// The username the client connects with. Each user gets its own pool. - 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 { - PoolIdentifier { - db: db.to_string(), - user: user.to_string(), - } - } -} - -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) - } -} - -/// Pool settings. -#[derive(Clone, Debug)] -pub struct PoolSettings { - /// Transaction or Session. - pub pool_mode: PoolMode, - - /// Random or LeastOutstandingConnections. - pub load_balancing_mode: LoadBalancingMode, - - // Number of shards. - pub shards: usize, - - // Connecting user. - pub user: User, - pub db: String, - - // Default server role to connect to. - pub default_role: Option, - - // Enable/disable query parser. - pub query_parser_enabled: bool, - - // Max length of query the parser will parse. - pub query_parser_max_length: Option, - - // Infer role - pub query_parser_read_write_splitting: bool, - - // Read from the primary as well or not. - pub primary_reads_enabled: bool, - - // Sharding function. - pub sharding_function: ShardingFunction, - - // Sharding key - pub automatic_sharding_key: Option, - - // Health check timeout - pub healthcheck_timeout: u64, - - // Health check delay - pub healthcheck_delay: u64, - - // Ban time - pub ban_time: i64, - - // Regex for searching for the sharding key in SQL statements - pub sharding_key_regex: Option, - - // Regex for searching for the shard id in SQL statements - pub shard_id_regex: Option, - - // What to do when no shard is selected in a sharded system - pub default_shard: DefaultShard, - - // 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 { - fn default() -> PoolSettings { - PoolSettings { - pool_mode: PoolMode::Transaction, - load_balancing_mode: LoadBalancingMode::Random, - shards: 1, - user: User::default(), - db: String::default(), - default_role: None, - query_parser_enabled: false, - query_parser_max_length: None, - query_parser_read_write_splitting: false, - primary_reads_enabled: true, - sharding_function: ShardingFunction::PgBigintHash, - automatic_sharding_key: None, - healthcheck_delay: General::default_healthcheck_delay(), - healthcheck_timeout: General::default_healthcheck_timeout(), - ban_time: General::default_ban_time(), - sharding_key_regex: None, - shard_id_regex: None, - regex_search_limit: 1000, - default_shard: DefaultShard::Shard(0), - auth_query: None, - auth_query_user: None, - auth_query_password: None, - plugins: None, - } - } -} - -/// The globally accessible connection pool. -#[derive(Clone, Debug, Default)] -pub struct ConnectionPool { - /// The pools handled internally by bb8. - databases: Arc>>>, - - /// The addresses (host, port, role) to handle - /// failover and load balancing deterministically. - addresses: Arc>>, - - /// List of banned addresses (see above) - /// that should not be queried. - banlist: BanList, - - /// The server information has to be passed to the - /// clients on startup. We pre-connect to all shards and replicas - /// on pool creation and save the startup parameters here. - original_server_parameters: Arc>, - - /// Pool configuration. - pub settings: Arc, - - /// If not validated, we need to double check the pool is available before allowing a client - /// to use it. - validated: Arc, - - /// Hash value for the pool configs. It is used to compare new configs - /// against current config to decide whether or not we need to recreate - /// the pool after a RELOAD command - pub config_hash: u64, - - /// If the pool has been paused or not. - paused: Arc, - paused_waiter: Arc, - - /// AuthInfo - pub auth_hash: Arc>>, - - /// Cache - pub prepared_statement_cache: Option, -} - -impl ConnectionPool { - /// Construct the connection pool from the configuration. - pub async fn from_config(client_server_map: ClientServerMap) -> Result<(), Error> { - let config = get_config(); - - let mut new_pools = HashMap::new(); - let mut address_id: usize = 0; - - for (pool_name, pool_config) in &config.pools { - let new_pool_hash_value = pool_config.hash_value(); - - // 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); - - if let Some(pool) = old_pool_ref { - // If the pool hasn't changed, get existing reference and insert it into the new_pools. - // We replace all pools at the end, but if the reference is kept, the pool won't get re-created (bb8). - if pool.config_hash == new_pool_hash_value { - info!( - "[pool: {}][user: {}] has not changed", - pool_name, user.username - ); - new_pools.insert(identifier.clone(), pool.clone()); - continue; - } - } - - info!( - "[pool: {}][user: {}] creating new pool", - pool_name, user.username - ); - - let mut shards = Vec::new(); - let mut addresses = Vec::new(); - let mut banlist = Vec::new(); - let mut shard_ids = pool_config - .shards - .clone() - .into_keys() - .collect::>(); - - // 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]; - let mut pools = Vec::new(); - let mut servers = Vec::new(); - let mut replica_number = 0; - - // Load Mirror settings - for (address_index, server) in shard.servers.iter().enumerate() { - let mut mirror_addresses = vec![]; - if let Some(mirror_settings_vec) = &shard.mirrors { - for (mirror_idx, mirror_settings) in - mirror_settings_vec.iter().enumerate() - { - if mirror_settings.mirroring_target_index != address_index { - continue; - } - mirror_addresses.push(Address { - id: address_id, - database: shard.database.clone(), - host: mirror_settings.host.clone(), - port: mirror_settings.port, - role: server.role, - address_index: mirror_idx, - replica_number, - shard: shard_idx.parse::().unwrap(), - username: user.username.clone(), - pool_name: pool_name.clone(), - mirrors: vec![], - stats: Arc::new(AddressStats::default()), - error_count: Arc::new(AtomicU64::new(0)), - }); - address_id += 1; - } - } - - let address = Address { - id: address_id, - database: shard.database.clone(), - host: server.host.clone(), - port: server.port, - role: server.role, - address_index, - replica_number, - shard: shard_idx.parse::().unwrap(), - username: user.username.clone(), - pool_name: pool_name.clone(), - mirrors: mirror_addresses, - stats: Arc::new(AddressStats::default()), - error_count: Arc::new(AtomicU64::new(0)), - }; - - address_id += 1; - - if server.role == Role::Replica { - replica_number += 1; - } - - // We assume every server in the pool share user/passwords - let auth_passthrough = AuthPassthrough::from_pool_config(pool_config); - - if let Some(apt) = &auth_passthrough { - match apt.fetch_hash(&address).await { - Ok(ok) => { - if let Some(ref pool_auth_hash_value) = *(pool_auth_hash.read()) - { - if ok != *pool_auth_hash_value { - warn!( - "Hash is not the same across shards \ - of the same pool, client auth will \ - be done using last obtained hash. \ - Server: {}:{}, Database: {}", - server.host, server.port, shard.database, - ); - } - } - - debug!("Hash obtained for {:?}", address); - - { - let mut pool_auth_hash = pool_auth_hash.write(); - *pool_auth_hash = Some(ok.clone()); - } - } - Err(err) => warn!( - "Could not obtain password hashes \ - using auth_query config, ignoring. \ - Error: {:?}", - err, - ), - } - } - - let manager = ServerPool::new( - address.clone(), - user.clone(), - &shard.database, - client_server_map.clone(), - pool_auth_hash.clone(), - match pool_config.plugins { - Some(ref plugins) => Some(plugins.clone()), - None => config.plugins.clone(), - }, - pool_config.cleanup_server_connections, - pool_config.log_client_parameter_status_changes, - pool_config.prepared_statements_cache_size, - ); - - let connect_timeout = match user.connect_timeout { - Some(connect_timeout) => connect_timeout, - None => match pool_config.connect_timeout { - Some(connect_timeout) => connect_timeout, - None => config.general.connect_timeout, - }, - }; - - let idle_timeout = match user.idle_timeout { - Some(idle_timeout) => idle_timeout, - None => match pool_config.idle_timeout { - Some(idle_timeout) => idle_timeout, - 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 = *[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))) - .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); - } - - shards.push(pools); - addresses.push(servers); - banlist.push(HashMap::new()); - } - - 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: Arc::new(shards), - addresses: Arc::new(addresses), - banlist: Arc::new(RwLock::new(banlist)), - config_hash: new_pool_hash_value, - original_server_parameters: Arc::new(RwLock::new(ServerParameters::new())), - auth_hash: pool_auth_hash, - settings: Arc::new(PoolSettings { - 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), - "primary" => Some(Role::Primary), - _ => unreachable!(), - }, - query_parser_enabled: pool_config.query_parser_enabled, - query_parser_max_length: pool_config.query_parser_max_length, - query_parser_read_write_splitting: pool_config - .query_parser_read_write_splitting, - primary_reads_enabled: pool_config.primary_reads_enabled, - sharding_function: pool_config.sharding_function, - automatic_sharding_key: pool_config.automatic_sharding_key.clone(), - healthcheck_delay: config.general.healthcheck_delay, - healthcheck_timeout: config.general.healthcheck_timeout, - ban_time: config.general.ban_time, - sharding_key_regex: pool_config - .sharding_key_regex - .clone() - .map(|regex| Regex::new(regex.as_str()).unwrap()), - shard_id_regex: pool_config - .shard_id_regex - .clone() - .map(|regex| Regex::new(regex.as_str()).unwrap()), - regex_search_limit: pool_config.regex_search_limit.unwrap_or(1000), - default_shard: pool_config.default_shard, - 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)), - paused_waiter: Arc::new(Notify::new()), - prepared_statement_cache: match pool_config.prepared_statements_cache_size { - 0 => None, - _ => Some(Arc::new(Mutex::new(PreparedStatementCache::new( - pool_config.prepared_statements_cache_size, - )))), - }, - }; - - // 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. - if config.general.validate_config { - let 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); - } - } - - POOLS.store(Arc::new(new_pools.clone())); - Ok(()) - } - - /// 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 - /// the pooler starts up. - pub async fn validate(&self) -> Result<(), Error> { - let mut futures = Vec::new(); - let validated = Arc::clone(&self.validated); - - for shard in 0..self.shards() { - for server in 0..self.servers(shard) { - let databases = self.databases.clone(); - let validated = Arc::clone(&validated); - let pool_server_parameters = Arc::clone(&self.original_server_parameters); - - let task = tokio::task::spawn(async move { - let connection = match databases[shard][server].get().await { - Ok(conn) => conn, - Err(err) => { - error!("Shard {} down or misconfigured: {:?}", shard, err); - return; - } - }; - - let proxy = connection; - let server = &*proxy; - let server_parameters: ServerParameters = server.server_parameters(); - - let mut guard = pool_server_parameters.write(); - *guard = server_parameters; - validated.store(true, Ordering::Relaxed); - }); - - futures.push(task); - } - } - - futures::future::join_all(futures).await; - - // TODO: compare server information to make sure - // all shards are running identical configurations. - if !self.validated() { - error!("Could not validate connection pool"); - return Err(Error::AllServersDown); - } - - Ok(()) - } - - /// The pool can be used by clients. - /// - /// If not, we need to validate it first by connecting to servers. - /// Call `validate()` to do so. - pub fn validated(&self) -> bool { - self.validated.load(Ordering::Relaxed) - } - - /// Pause the pool, allowing no more queries and make clients wait. - pub fn pause(&self) { - self.paused.store(true, Ordering::Relaxed); - } - - /// Resume the pool, allowing queries and resuming any pending queries. - pub fn resume(&self) { - self.paused.store(false, Ordering::Relaxed); - self.paused_waiter.notify_waiters(); - } - - /// Check if the pool is paused. - pub fn paused(&self) -> bool { - self.paused.load(Ordering::Relaxed) - } - - /// Check if the pool is paused and wait until it's resumed. - pub async fn wait_paused(&self) -> bool { - let waiter = self.paused_waiter.notified(); - let paused = self.paused.load(Ordering::Relaxed); - - if paused { - waiter.await; - } - - paused - } - - /// Get a connection from the pool. - pub async fn get( - &self, - shard: Option, // shard number - role: Option, // primary or replica - client_stats: &ClientStats, // client id - ) -> Result<(PooledConnection<'_, ServerPool>, Address), Error> { - let effective_shard_id = if self.shards() == 1 { - // The base, unsharded case - Some(0) - } else { - if !self.valid_shard_id(shard) { - // None is valid shard ID so it is safe to unwrap here - return Err(Error::InvalidShardId(shard.unwrap())); - } - shard - }; - - let mut candidates = self - .addresses - .iter() - .flatten() - .filter(|address| address.role == role) - .collect::>(); - - // We start with a shuffled list of addresses even if we end up resorting - // this is meant to avoid hitting instance 0 everytime if the sorting metric - // ends up being the same for all instances - candidates.shuffle(&mut thread_rng()); - - match effective_shard_id { - Some(shard_id) => candidates.retain(|address| address.shard == shard_id), - None => match self.settings.default_shard { - DefaultShard::Shard(shard_id) => { - candidates.retain(|address| address.shard == shard_id) - } - DefaultShard::Random => (), - DefaultShard::RandomHealthy => { - candidates.sort_by(|a, b| { - b.error_count - .load(Ordering::Relaxed) - .partial_cmp(&a.error_count.load(Ordering::Relaxed)) - .unwrap() - }); - } - }, - }; - - if self.settings.load_balancing_mode == LoadBalancingMode::LeastOutstandingConnections { - candidates.sort_by(|a, b| { - self.busy_connection_count(b) - .partial_cmp(&self.busy_connection_count(a)) - .unwrap() - }); - } - - // 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() { - Some(address) => address, - None => break, - }; - - let mut force_healthcheck = false; - - if self.is_banned(address) { - if self.try_unban(address).await { - force_healthcheck = true; - } else { - debug!("Address {:?} is banned", address); - continue; - } - } - - // Check if we can connect - let mut conn = match self.databases[address.shard][address.address_index] - .get() - .await - { - Ok(conn) => { - address.reset_error_count(); - conn - } - Err(err) => { - error!( - "Connection checkout error for instance {:?}, error: {:?}", - address, err - ); - self.ban(address, BanReason::FailedCheckout, Some(client_stats)); - address.stats.error(); - client_stats.checkout_error(); - continue; - } - }; - - // // Check if this server is alive with a health check. - let server = &mut *conn; - - // Will return error if timestamp is greater than current system time, which it should never be set to - let require_healthcheck = force_healthcheck - || server.last_activity().elapsed().unwrap().as_millis() - > self.settings.healthcheck_delay as u128; - - // Do not issue a health check unless it's been a little while - // since we last checked the server is ok. - // Health checks are pretty expensive. - if !require_healthcheck { - let checkout_time = now.elapsed().as_micros() as u64; - client_stats.checkout_success(); - 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_stats) - .await - { - let checkout_time = now.elapsed().as_micros() as u64; - client_stats.checkout_success(); - 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.checkout_error(); - - Err(Error::AllServersDown) - } - - async fn run_health_check( - &self, - address: &Address, - server: &mut Server, - start: Instant, - client_info: &ClientStats, - ) -> bool { - debug!("Running health check on server {:?}", address); - - server.stats().tested(); - - match tokio::time::timeout( - tokio::time::Duration::from_millis(self.settings.healthcheck_timeout), - server.query(";"), // Cheap query as it skips the query planner - ) - .await - { - // Check if health check succeeded. - Ok(res) => match res { - Ok(_) => { - let checkout_time: u64 = start.elapsed().as_micros() as u64; - client_info.checkout_success(); - 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!( - "Failed health check on instance {:?}, error: {:?}", - address, err - ); - } - }, - - // Health check timed out. - Err(err) => { - error!( - "Health check timeout on instance {:?}, error: {:?}", - address, err - ); - } - } - - // Don't leave a bad connection in the pool. - server.mark_bad("failed health check"); - - self.ban(address, BanReason::FailedHealthCheck, Some(client_info)); - 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_info: Option<&ClientStats>) { - // Count the number of errors since the last successful checkout - // This is used to determine if the shard is down - match reason { - BanReason::FailedHealthCheck - | BanReason::FailedCheckout - | BanReason::MessageSendFailed - | BanReason::MessageReceiveFailed => { - address.increment_error_count(); - } - _ => (), - }; - - // 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(); - - if let Some(client_info) = client_info { - client_info.ban_error(); - address.stats.error(); - } - - guard[address.shard].insert(address.clone(), (reason, now)); - } - - /// Clear the replica to receive traffic again. Takes effect immediately - /// for all new transactions. - pub fn unban(&self, address: &Address) { - let mut guard = self.banlist.write(); - guard[address.shard].remove(address); - } - - /// Check if address is banned - /// true if banned, false otherwise - pub fn is_banned(&self, address: &Address) -> bool { - let guard = self.banlist.read(); - - match guard[address.shard].get(address) { - Some(_) => true, - None => { - debug!("{:?} is ok", address); - false - } - } - } - - /// Determines trying to unban this server was successful - pub async fn try_unban(&self, address: &Address) -> bool { - // If somehow primary ends up being banned we should return true here - if address.role == Role::Primary { - return true; - } - - // Check if all replicas are banned, in that case unban all of them - let replicas_available = self.addresses[address.shard] - .iter() - .filter(|addr| addr.role == Role::Replica) - .count(); - - debug!("Available targets: {}", replicas_available); - - let read_guard = self.banlist.read(); - let all_replicas_banned = read_guard[address.shard].len() == replicas_available; - drop(read_guard); - - if all_replicas_banned { - let mut write_guard = self.banlist.write(); - warn!("Unbanning all replicas."); - write_guard[address.shard].clear(); - - return true; - } - - // Check if ban time is expired - let read_guard = self.banlist.read(); - let exceeded_ban_time = match read_guard[address.shard].get(address) { - Some((ban_reason, timestamp)) => { - let now = chrono::offset::Utc::now().naive_utc(); - match ban_reason { - BanReason::AdminBan(duration) => { - now.timestamp() - timestamp.timestamp() > *duration - } - _ => now.timestamp() - timestamp.timestamp() > self.settings.ban_time, - } - } - None => return true, - }; - drop(read_guard); - - if exceeded_ban_time { - warn!("Unbanning {:?}", address); - let mut write_guard = self.banlist.write(); - write_guard[address.shard].remove(address); - drop(write_guard); - - true - } else { - debug!("{:?} is banned", address); - false - } - } - - /// Get the number of configured shards. - pub fn shards(&self) -> usize { - self.databases.len() - } - - pub fn get_bans(&self) -> Vec<(Address, (BanReason, NaiveDateTime))> { - let mut bans: Vec<(Address, (BanReason, NaiveDateTime))> = Vec::new(); - let guard = self.banlist.read(); - for banlist in guard.iter() { - for (address, (reason, timestamp)) in banlist.iter() { - bans.push((address.clone(), (reason.clone(), *timestamp))); - } - } - bans - } - - /// Get the address from the host url - pub fn get_addresses_from_host(&self, host: &str) -> Vec
{ - let mut addresses = Vec::new(); - for shard in 0..self.shards() { - for server in 0..self.servers(shard) { - let address = self.address(shard, server); - if address.host == host { - addresses.push(address.clone()); - } - } - } - addresses - } - - /// Get the number of servers (primary and replicas) - /// configured for a shard. - pub fn servers(&self, shard: usize) -> usize { - self.addresses[shard].len() - } - - /// Get the total number of servers (databases) we are connected to. - pub fn databases(&self) -> usize { - let mut databases = 0; - for shard in 0..self.shards() { - databases += self.servers(shard); - } - databases - } - - /// Get pool state for a particular shard server as reported by bb8. - pub fn pool_state(&self, shard: usize, server: usize) -> bb8::State { - self.databases[shard][server].state() - } - - /// Get the address information for a shard server. - pub fn address(&self, shard: usize, server: usize) -> &Address { - &self.addresses[shard][server] - } - - pub fn server_parameters(&self) -> ServerParameters { - self.original_server_parameters.read().clone() - } - - /// Get the number of checked out connection for an address - fn busy_connection_count(&self, address: &Address) -> u32 { - let state = self.pool_state(address.shard, address.address_index); - let idle = state.idle_connections; - let provisioned = state.connections; - - if idle > provisioned { - // Unlikely but avoids an overflow panic if this ever happens - return 0; - } - let busy = provisioned - idle; - debug!("{:?} has {:?} busy connections", address, busy); - busy - } - - fn valid_shard_id(&self, shard: Option) -> bool { - match shard { - None => true, - Some(shard) => shard < self.shards(), - } - } - - /// Register a parse statement to the pool's cache and return the rewritten parse - /// - /// Do not pass an anonymous parse statement to this function - pub fn register_parse_to_cache(&self, hash: u64, parse: &Parse) -> Option> { - // We should only be calling this function if the cache is enabled - match self.prepared_statement_cache { - Some(ref prepared_statement_cache) => { - let mut cache = prepared_statement_cache.lock(); - Some(cache.get_or_insert(parse, hash)) - } - None => None, - } - } - - /// Promote a prepared statement hash in the LRU - pub fn promote_prepared_statement_hash(&self, hash: &u64) { - // We should only be calling this function if the cache is enabled - if let Some(ref prepared_statement_cache) = self.prepared_statement_cache { - let mut cache = prepared_statement_cache.lock(); - cache.promote(hash); - } - } -} - -/// Wrapper for the bb8 connection pool. -pub struct ServerPool { - /// Server address. - address: Address, - - /// Server Postgres user. - user: User, - - /// Server database. - database: String, - - /// Client/server mapping. - client_server_map: ClientServerMap, - - /// Server 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, - - /// Log client parameter status changes - log_client_parameter_status_changes: bool, - - /// Prepared statement cache size - prepared_statement_cache_size: usize, -} - -impl ServerPool { - #[allow(clippy::too_many_arguments)] - pub fn new( - address: Address, - user: User, - database: &str, - client_server_map: ClientServerMap, - auth_hash: Arc>>, - plugins: Option, - cleanup_connections: bool, - log_client_parameter_status_changes: bool, - prepared_statement_cache_size: usize, - ) -> ServerPool { - ServerPool { - address, - user, - database: database.to_string(), - client_server_map, - auth_hash, - plugins, - cleanup_connections, - log_client_parameter_status_changes, - prepared_statement_cache_size, - } - } -} - -#[async_trait] -impl ManageConnection for ServerPool { - type Connection = Server; - type Error = Error; - - /// Attempts to create a new connection. - async fn connect(&self) -> Result { - info!("Creating a new server connection {:?}", self.address); - - 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( - &self.address, - &self.user, - &self.database, - self.client_server_map.clone(), - stats.clone(), - self.auth_hash.clone(), - self.cleanup_connections, - self.log_client_parameter_status_changes, - self.prepared_statement_cache_size, - ) - .await - { - 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) => { - stats.disconnect(); - Err(err) - } - } - } - - /// Determines if the connection is still connected to the database. - async fn is_valid(&self, _conn: &mut Self::Connection) -> Result<(), Self::Error> { - Ok(()) - } - - /// Synchronously determine if the connection is no longer usable, if possible. - fn has_broken(&self, conn: &mut Self::Connection) -> bool { - conn.is_bad() - } -} - -/// Get the connection pool -pub fn get_pool(db: &str, user: &str) -> Option { - (*(*POOLS.load())) - .get(&PoolIdentifier::new(db, user)) - .cloned() -} - -/// Get a pointer to all configured pools. -pub fn get_all_pools() -> HashMap { - (*(*POOLS.load())).clone() -} diff --git a/src/prometheus.rs b/src/prometheus.rs deleted file mode 100644 index 6c9a1be..0000000 --- a/src/prometheus.rs +++ /dev/null @@ -1,530 +0,0 @@ -use http_body_util::Full; -use hyper::body; -use hyper::body::Bytes; - -use hyper::server::conn::http1; -use hyper::service::service_fn; -use hyper::{Method, Request, Response, StatusCode}; -use hyper_util::rt::TokioIo; -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 tokio::net::TcpListener; - -use crate::config::Address; -use crate::pool::{get_all_pools, PoolIdentifier}; -use crate::stats::get_server_stats; -use crate::stats::pool::PoolStats; - -struct MetricHelpType { - help: &'static str, - ty: &'static str, -} - -struct ServerPrometheusStats { - bytes_received: u64, - bytes_sent: u64, - transaction_count: u64, - query_count: u64, - error_count: u64, - active_count: u64, - idle_count: u64, - login_count: u64, - tested_count: u64, -} - -// reference for metric types: https://prometheus.io/docs/concepts/metric_types/ -// counters only increase -// gauges can arbitrarily increase or decrease -static METRIC_HELP_AND_TYPES_LOOKUP: phf::Map<&'static str, MetricHelpType> = phf_map! { - "stats_total_query_count" => MetricHelpType { - help: "Number of queries sent by all clients", - ty: "counter", - }, - "stats_total_query_time" => MetricHelpType { - help: "Total amount of time for queries to execute", - ty: "counter", - }, - "stats_total_received" => MetricHelpType { - help: "Number of bytes received from the server", - ty: "counter", - }, - "stats_total_sent" => MetricHelpType { - help: "Number of bytes sent to the server", - ty: "counter", - }, - "stats_total_xact_count" => MetricHelpType { - help: "Total number of transactions started by the client", - ty: "counter", - }, - "stats_total_xact_time" => MetricHelpType { - help: "Total amount of time for all transactions to execute", - ty: "counter", - }, - "stats_total_wait_time" => MetricHelpType { - help: "Total time client waited for a server connection", - ty: "counter", - }, - "stats_avg_query_count" => MetricHelpType { - help: "Average of total_query_count every 15 seconds", - ty: "gauge", - }, - "stats_avg_query_time" => MetricHelpType { - help: "Average time taken for queries to execute every 15 seconds", - ty: "gauge", - }, - "stats_avg_recv" => MetricHelpType { - help: "Average of total_received bytes every 15 seconds", - ty: "gauge", - }, - "stats_avg_sent" => MetricHelpType { - help: "Average of total_sent bytes every 15 seconds", - ty: "gauge", - }, - "stats_avg_errors" => MetricHelpType { - help: "Average number of errors every 15 seconds", - ty: "gauge", - }, - "stats_avg_xact_count" => MetricHelpType { - help: "Average of total_xact_count every 15 seconds", - ty: "gauge", - }, - "stats_avg_xact_time" => MetricHelpType { - help: "Average of total_xact_time every 15 seconds", - ty: "gauge", - }, - "stats_avg_wait_time" => MetricHelpType { - help: "Average of total_wait_time every 15 seconds", - ty: "gauge", - }, - "pools_maxwait_us" => MetricHelpType { - help: "The time a client waited for a server connection in microseconds", - ty: "gauge", - }, - "pools_maxwait" => MetricHelpType { - help: "The time a client waited for a server connection in seconds", - ty: "gauge", - }, - "pools_cl_waiting" => MetricHelpType { - help: "How many clients are waiting for a connection from the pool", - ty: "gauge", - }, - "pools_cl_active" => MetricHelpType { - help: "How many clients are actively communicating with a server", - ty: "gauge", - }, - "pools_cl_idle" => MetricHelpType { - help: "How many clients are idle", - ty: "gauge", - }, - "pools_sv_idle" => MetricHelpType { - help: "How many server connections are idle", - ty: "gauge", - }, - "pools_sv_active" => MetricHelpType { - help: "How many server connections are actively communicating with a client", - ty: "gauge", - }, - "pools_sv_login" => MetricHelpType { - help: "How many server connections are currently being created", - ty: "gauge", - }, - "pools_sv_tested" => MetricHelpType { - help: "How many server connections are currently waiting on a health check to succeed", - ty: "gauge", - }, - "servers_bytes_received" => MetricHelpType { - help: "Volume in bytes of network traffic received by server", - ty: "counter", - }, - "servers_bytes_sent" => MetricHelpType { - help: "Volume in bytes of network traffic sent by server", - ty: "counter", - }, - "servers_transaction_count" => MetricHelpType { - help: "Number of transactions executed by server", - ty: "counter", - }, - "servers_query_count" => MetricHelpType { - help: "Number of queries executed by server", - ty: "counter", - }, - "servers_error_count" => MetricHelpType { - help: "Number of errors", - ty: "counter", - }, - "servers_idle_count" => MetricHelpType { - help: "Number of server connection in idle state", - ty: "gauge", - }, - "servers_active_count" => MetricHelpType { - help: "Number of server connection in active state", - ty: "gauge", - }, - "servers_tested_count" => MetricHelpType { - help: "Number of server connection in tested state", - ty: "gauge", - }, - "servers_login_count" => MetricHelpType { - help: "Number of server connection in login state", - ty: "gauge", - }, - "servers_is_banned" => MetricHelpType { - help: "0 if server is not banned, 1 if server is banned", - ty: "gauge", - }, - "servers_is_paused" => MetricHelpType { - help: "0 if server is not paused, 1 if server is paused", - ty: "gauge", - }, - "databases_pool_size" => MetricHelpType { - help: "Maximum number of server connections", - ty: "gauge", - }, - "databases_current_connections" => MetricHelpType { - help: "Current number of connections for this database", - ty: "gauge", - }, -}; - -struct PrometheusMetric { - name: String, - help: String, - ty: String, - labels: HashMap<&'static str, String>, - value: Value, -} - -impl fmt::Display for PrometheusMetric { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let mut sorted_labels: Vec<_> = self.labels.iter().collect(); - sorted_labels.sort_by_key(|&(key, _)| key); - let formatted_labels = sorted_labels - .iter() - .map(|(key, value)| format!("{}=\"{}\"", key, value)) - .collect::>() - .join(","); - write!( - f, - "{name}{{{formatted_labels}}} {value}", - name = format_args!("pgcat_{}", self.name), - formatted_labels = formatted_labels, - value = self.value - ) - } -} - -impl PrometheusMetric { - fn from_name( - name: &str, - value: V, - labels: HashMap<&'static str, String>, - ) -> Option> { - METRIC_HELP_AND_TYPES_LOOKUP - .get(name) - .map(|metric| PrometheusMetric:: { - name: name.to_owned(), - help: metric.help.to_owned(), - ty: metric.ty.to_owned(), - value, - labels, - }) - } - - fn from_database_info( - address: &Address, - name: &str, - value: u32, - ) -> Option> { - let mut labels = HashMap::new(); - labels.insert("host", address.host.clone()); - labels.insert("shard", address.shard.to_string()); - labels.insert("role", address.role.to_string()); - labels.insert("pool", address.pool_name.clone()); - labels.insert("index", address.address_index.to_string()); - labels.insert("database", address.database.to_string()); - labels.insert("username", address.username.clone()); - - Self::from_name(&format!("databases_{}", name), value, labels) - } - - fn from_server_info( - 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()); - labels.insert("role", address.role.to_string()); - labels.insert("pool", address.pool_name.clone()); - labels.insert("index", address.address_index.to_string()); - labels.insert("database", address.database.to_string()); - labels.insert("username", address.username.clone()); - - Self::from_name(&format!("servers_{}", name), value, labels) - } - - 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()); - labels.insert("pool", address.pool_name.clone()); - labels.insert("role", address.role.to_string()); - labels.insert("index", address.address_index.to_string()); - labels.insert("database", address.database.to_string()); - labels.insert("username", address.username.clone()); - - Self::from_name(&format!("stats_{}", name), value, labels) - } - - fn from_pool(pool_id: PoolIdentifier, name: &str, value: u64) -> Option> { - let mut labels = HashMap::new(); - labels.insert("pool", pool_id.db); - labels.insert("user", pool_id.user); - - Self::from_name(&format!("pools_{}", name), value, labels) - } - - fn get_header(&self) -> String { - format!( - "\n# HELP {name} {help}\n# TYPE {name} {ty}", - name = format_args!("pgcat_{}", self.name), - help = self.help, - ty = self.ty, - ) - } -} - -async fn prometheus_stats( - request: Request, -) -> Result>, hyper::http::Error> { - match (request.method(), request.uri().path()) { - (&Method::GET, "/metrics") => { - let mut lines = Vec::new(); - push_address_stats(&mut lines); - push_pool_stats(&mut lines); - push_server_stats(&mut lines); - push_database_stats(&mut lines); - lines.push("".to_string()); // Ensure to end the stats with a line terminator as required by the specification. - - Response::builder() - .header("content-type", "text/plain; version=0.0.4") - .body(lines.join("\n").into()) - } - _ => Response::builder() - .status(StatusCode::NOT_FOUND) - .body("".into()), - } -} - -// Adds metrics shown in a SHOW STATS admin command. -fn push_address_stats(lines: &mut Vec) { - let mut grouped_metrics: HashMap>> = HashMap::new(); - 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); - let stats = &*address.stats; - for (key, value) in stats.clone() { - if let Some(prometheus_metric) = - PrometheusMetric::::from_address(address, &key, value) - { - grouped_metrics - .entry(key) - .or_default() - .push(prometheus_metric); - } else { - debug!("Metric {} not implemented for {}", key, address.name()); - } - } - } - } - } - for (_key, metrics) in grouped_metrics { - if !metrics.is_empty() { - lines.push(metrics[0].get_header()); - for metric in metrics { - lines.push(metric.to_string()); - } - } - } -} - -// Adds relevant metrics shown in a SHOW POOLS admin command. -fn push_pool_stats(lines: &mut Vec) { - let mut grouped_metrics: HashMap>> = HashMap::new(); - 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) - { - grouped_metrics - .entry(name) - .or_default() - .push(prometheus_metric); - } else { - debug!("Metric {} not implemented for ({})", name, *pool_id); - } - } - } - for (_key, metrics) in grouped_metrics { - if !metrics.is_empty() { - lines.push(metrics[0].get_header()); - for metric in metrics { - lines.push(metric.to_string()); - } - } - } -} - -// Adds relevant metrics shown in a SHOW DATABASES admin command. -fn push_database_stats(lines: &mut Vec) { - let mut grouped_metrics: HashMap>> = HashMap::new(); - for (_, pool) in get_all_pools() { - let pool_config = pool.settings.clone(); - for shard in 0..pool.shards() { - for server in 0..pool.servers(shard) { - let address = pool.address(shard, server); - let pool_state = pool.pool_state(shard, server); - let metrics = vec![ - ("pool_size", pool_config.user.pool_size), - ("current_connections", pool_state.connections), - ]; - for (key, value) in metrics { - if let Some(prometheus_metric) = - PrometheusMetric::::from_database_info(address, key, value) - { - grouped_metrics - .entry(key.to_string()) - .or_default() - .push(prometheus_metric); - } else { - debug!("Metric {} not implemented for {}", key, address.name()); - } - } - } - } - } - for (_key, metrics) in grouped_metrics { - if !metrics.is_empty() { - lines.push(metrics[0].get_header()); - for metric in metrics { - lines.push(metric.to_string()); - } - } - } -} - -// 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 prom_stats = HashMap::::new(); - for (_, stats) in server_stats { - let entry = prom_stats - .entry(stats.address_name()) - .or_insert(ServerPrometheusStats { - bytes_received: 0, - bytes_sent: 0, - transaction_count: 0, - query_count: 0, - error_count: 0, - active_count: 0, - idle_count: 0, - login_count: 0, - tested_count: 0, - }); - entry.bytes_received += stats.bytes_received.load(Ordering::Relaxed); - entry.bytes_sent += stats.bytes_sent.load(Ordering::Relaxed); - entry.transaction_count += stats.transaction_count.load(Ordering::Relaxed); - entry.query_count += stats.query_count.load(Ordering::Relaxed); - entry.error_count += stats.error_count.load(Ordering::Relaxed); - match stats.state.load(Ordering::Relaxed) { - crate::stats::ServerState::Login => entry.login_count += 1, - crate::stats::ServerState::Active => entry.active_count += 1, - crate::stats::ServerState::Tested => entry.tested_count += 1, - crate::stats::ServerState::Idle => entry.idle_count += 1, - } - } - let mut grouped_metrics: HashMap>> = HashMap::new(); - 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(server_info) = prom_stats.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), - ("idle_count", server_info.idle_count), - ("active_count", server_info.active_count), - ("login_count", server_info.login_count), - ("tested_count", server_info.tested_count), - ("is_banned", if pool.is_banned(address) { 1 } else { 0 }), - ("is_paused", if pool.paused() { 1 } else { 0 }), - ]; - for (key, value) in metrics { - if let Some(prometheus_metric) = - PrometheusMetric::::from_server_info(address, key, value) - { - grouped_metrics - .entry(key.to_string()) - .or_default() - .push(prometheus_metric); - } else { - debug!("Metric {} not implemented for {}", key, address.name()); - } - } - } - } - } - } - for (_key, metrics) in grouped_metrics { - if !metrics.is_empty() { - lines.push(metrics[0].get_header()); - for metric in metrics { - lines.push(metric.to_string()); - } - } - } -} - -pub async fn start_metric_server(http_addr: SocketAddr) { - let listener = TcpListener::bind(http_addr); - let listener = match listener.await { - Ok(listener) => listener, - Err(e) => { - error!("Failed to bind prometheus server to HTTP address: {}.", e); - return; - } - }; - info!( - "Exposing prometheus metrics on http://{}/metrics.", - http_addr - ); - loop { - let stream = match listener.accept().await { - Ok((stream, _)) => stream, - Err(e) => { - error!("Error accepting connection: {}", e); - continue; - } - }; - let io = TokioIo::new(stream); - - tokio::task::spawn(async move { - if let Err(err) = http1::Builder::new() - .serve_connection(io, service_fn(prometheus_stats)) - .await - { - eprintln!("Error serving HTTP connection for metrics: {:?}", err); - } - }); - } -} diff --git a/src/query_router.rs b/src/query_router.rs deleted file mode 100644 index c2548bb..0000000 --- a/src/query_router.rs +++ /dev/null @@ -1,1968 +0,0 @@ -/// Route queries automatically based on explicitly requested -/// or implied query characteristics. -use bytes::{Buf, BytesMut}; -use log::{debug, error}; -use once_cell::sync::OnceCell; -use regex::{Regex, RegexSet}; -use sqlparser::ast::Statement::{Delete, Insert, Query, StartTransaction, Update}; -use sqlparser::ast::{ - Assignment, BinaryOperator, Expr, Ident, JoinConstraint, JoinOperator, SetExpr, Statement, - TableFactor, TableWithJoins, 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; - -use std::collections::BTreeSet; -use std::io::Cursor; -use std::{cmp, mem}; - -/// Regexes used to parse custom commands. -const CUSTOM_SQL_REGEXES: [&str; 7] = [ - r"(?i)^ *SET SHARDING KEY TO '?([0-9]+)'? *;? *$", - r"(?i)^ *SET SHARD TO '?([0-9]+|ANY)'? *;? *$", - r"(?i)^ *SHOW SHARD *;? *$", - r"(?i)^ *SET SERVER ROLE TO '(PRIMARY|REPLICA|ANY|AUTO|DEFAULT)' *;? *$", - r"(?i)^ *SHOW SERVER ROLE *;? *$", - r"(?i)^ *SET PRIMARY READS TO '?(on|off|default)'? *;? *$", - r"(?i)^ *SHOW PRIMARY READS *;? *$", -]; - -/// Custom commands. -#[derive(PartialEq, Debug)] -pub enum Command { - SetShardingKey, - SetShard, - ShowShard, - SetServerRole, - ShowServerRole, - SetPrimaryReads, - ShowPrimaryReads, -} - -#[derive(PartialEq, Debug)] -pub enum ShardingKey { - Value(i64), - Placeholder(i16), -} - -#[derive(Clone, Debug)] -enum ParameterFormat { - Text, - Binary, - Uniform(Box), - Specified(Vec), -} - -/// Quickly test for match when a query is received. -static CUSTOM_SQL_REGEX_SET: OnceCell = OnceCell::new(); - -// Get the value inside the custom command. -static CUSTOM_SQL_REGEX_LIST: OnceCell> = OnceCell::new(); - -/// The query router. -pub struct QueryRouter { - /// Which shard we should be talking to right now. - active_shard: Option, - - /// Which server should we be talking to. - active_role: Option, - - /// Should we try to parse queries to route them to replicas or primary automatically - query_parser_enabled: Option, - - /// Include the primary into the replica pool for reads. - primary_reads_enabled: Option, - - /// Pool configuration. - pool_settings: PoolSettings, - - // Placeholders from prepared statement. - placeholders: Vec, -} - -impl QueryRouter { - /// One-time initialization of regexes - /// that parse our custom SQL protocol. - pub fn setup() -> bool { - let set = match RegexSet::new(CUSTOM_SQL_REGEXES) { - Ok(rgx) => rgx, - Err(err) => { - error!("QueryRouter::setup Could not compile regex set: {:?}", err); - return false; - } - }; - - let list: Vec<_> = CUSTOM_SQL_REGEXES - .iter() - .map(|rgx| Regex::new(rgx).unwrap()) - .collect(); - - assert_eq!(list.len(), set.len()); - - match CUSTOM_SQL_REGEX_LIST.set(list) { - Ok(_) => true, - Err(_) => return false, - }; - - CUSTOM_SQL_REGEX_SET.set(set).is_ok() - } - - /// Create a new instance of the query router. - /// Each client gets its own. - pub fn new() -> QueryRouter { - QueryRouter { - active_shard: None, - active_role: None, - query_parser_enabled: None, - primary_reads_enabled: None, - pool_settings: PoolSettings::default(), - placeholders: Vec::new(), - } - } - - /// Pool settings can change because of a config reload. - pub fn update_pool_settings(&mut self, pool_settings: &PoolSettings) { - self.pool_settings = pool_settings.clone(); - } - - pub fn pool_settings(&self) -> &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); - - let code = message_cursor.get_u8() as char; - let len = message_cursor.get_i32() as usize; - - let comment_shard_routing_enabled = self.pool_settings.shard_id_regex.is_some() - || self.pool_settings.sharding_key_regex.is_some(); - - // Check for any sharding regex matches in any queries - if comment_shard_routing_enabled { - match code { - // For Parse and Query messages peek to see if they specify a shard_id as a comment early in the statement - 'P' | 'Q' => { - // Check only the first block of bytes configured by the pool settings - let seg = cmp::min(len - 5, self.pool_settings.regex_search_limit); - - let query_start_index = mem::size_of::() + mem::size_of::(); - - let initial_segment = String::from_utf8_lossy( - &message_buffer[query_start_index..query_start_index + seg], - ); - - // Check for a shard_id included in the query - if let Some(shard_id_regex) = &self.pool_settings.shard_id_regex { - let shard_id = shard_id_regex.captures(&initial_segment).and_then(|cap| { - cap.get(1).and_then(|id| id.as_str().parse::().ok()) - }); - if let Some(shard_id) = shard_id { - debug!("Setting shard to {:?}", shard_id); - self.set_shard(Some(shard_id)); - // Skip other command processing since a sharding command was found - return None; - } - } - - // Check for a sharding_key included in the query - if let Some(sharding_key_regex) = &self.pool_settings.sharding_key_regex { - let sharding_key = - sharding_key_regex - .captures(&initial_segment) - .and_then(|cap| { - cap.get(1).and_then(|id| id.as_str().parse::().ok()) - }); - if let Some(sharding_key) = sharding_key { - debug!("Setting sharding_key to {:?}", sharding_key); - self.set_sharding_key(sharding_key); - // Skip other command processing since a sharding command was found - return None; - } - } - } - _ => {} - } - } - - // Only simple protocol supported for commands processed below - if code != 'Q' { - return None; - } - - let query = message_cursor.read_string().unwrap(); - - let regex_set = match CUSTOM_SQL_REGEX_SET.get() { - Some(regex_set) => regex_set, - None => return None, - }; - - let regex_list = match CUSTOM_SQL_REGEX_LIST.get() { - Some(regex_list) => regex_list, - None => return None, - }; - - let matches: Vec<_> = regex_set.matches(&query).into_iter().collect(); - - // This is not a custom query, try to infer which - // server it'll go to if the query parser is enabled. - if matches.len() != 1 { - debug!("Regular query, not a command"); - return None; - } - - let command = match matches[0] { - 0 => Command::SetShardingKey, - 1 => Command::SetShard, - 2 => Command::ShowShard, - 3 => Command::SetServerRole, - 4 => Command::ShowServerRole, - 5 => Command::SetPrimaryReads, - 6 => Command::ShowPrimaryReads, - _ => unreachable!(), - }; - - let mut value = match command { - Command::SetShardingKey - | Command::SetShard - | Command::SetServerRole - | Command::SetPrimaryReads => { - // Capture value. I know this re-runs the regex engine, but I haven't - // figured out a better way just yet. I think I can write a single Regex - // that matches all 5 custom SQL patterns, but maybe that's not very legible? - // - // I think this is faster than running the Regex engine 5 times. - match regex_list[matches[0]].captures(&query) { - Some(captures) => match captures.get(1) { - Some(value) => value.as_str().to_string(), - None => return None, - }, - None => return None, - } - } - - Command::ShowShard => self - .shard() - .map_or_else(|| "unset".to_string(), |x| x.to_string()), - Command::ShowServerRole => match self.active_role { - Some(Role::Primary) => Role::Primary.to_string(), - Some(Role::Replica) => Role::Replica.to_string(), - Some(Role::Mirror) => Role::Mirror.to_string(), - None => { - if self.query_parser_enabled() { - String::from("auto") - } else { - String::from("any") - } - } - }, - - Command::ShowPrimaryReads => match self.primary_reads_enabled() { - true => String::from("on"), - false => String::from("off"), - }, - }; - - match command { - Command::SetShardingKey => { - // TODO: some error handling here - value = self - .set_sharding_key(value.parse::().unwrap()) - .unwrap() - .to_string(); - } - - Command::SetShard => { - self.active_shard = match value.to_ascii_uppercase().as_ref() { - "ANY" => Some(rand::random::() % self.pool_settings.shards), - _ => Some(value.parse::().unwrap()), - }; - } - - Command::SetServerRole => { - self.active_role = match value.to_ascii_lowercase().as_ref() { - "primary" => { - self.query_parser_enabled = Some(false); - Some(Role::Primary) - } - - "replica" => { - self.query_parser_enabled = Some(false); - Some(Role::Replica) - } - - "any" => { - self.query_parser_enabled = Some(false); - None - } - - "auto" => { - self.query_parser_enabled = Some(true); - None - } - - "default" => { - self.active_role = self.pool_settings.default_role; - self.query_parser_enabled = None; - self.active_role - } - - _ => unreachable!(), - }; - } - - Command::SetPrimaryReads => { - if value == "on" { - debug!("Setting primary reads to on"); - self.primary_reads_enabled = Some(true); - } else if value == "off" { - debug!("Setting primary reads to off"); - self.primary_reads_enabled = Some(false); - } else if value == "default" { - debug!("Setting primary reads to default"); - self.primary_reads_enabled = None; - } - } - - _ => (), - } - - Some((command, value)) - } - - pub fn parse(&self, message: &BytesMut) -> Result, Error> { - let mut message_cursor = Cursor::new(message); - - let code = message_cursor.get_u8() as char; - let len = message_cursor.get_i32() as usize; - - if let Some(max_length) = self.pool_settings.query_parser_max_length { - if len > max_length { - return Err(Error::QueryRouterParserError(format!( - "Query too long for parser: {} > {}", - len, max_length - ))); - } - }; - - let query = match code { - // Query - 'Q' => { - let query = message_cursor.read_string().unwrap(); - debug!("Query: '{}'", query); - query - } - - // Parse (prepared statement) - 'P' => { - // Reads statement name - let _name = message_cursor.read_string().unwrap(); - - // Reads query string - let query = message_cursor.read_string().unwrap(); - - debug!("Prepared statement: '{}'", query); - - query - } - - _ => return Err(Error::UnsupportedStatement), - }; - - match Parser::parse_sql(&PostgreSqlDialect {}, &query) { - Ok(ast) => Ok(ast), - Err(err) => { - debug!("{}: {}", err, query); - Err(Error::QueryRouterParserError(err.to_string())) - } - } - } - - /// Determines if a query is a mutation or not. - fn is_mutation_query(q: &sqlparser::ast::Query) -> bool { - use sqlparser::ast::*; - - match q.body.as_ref() { - SetExpr::Insert(_) => true, - SetExpr::Update(_) => true, - SetExpr::Query(q) => Self::is_mutation_query(q), - _ => false, - } - } - - /// Try to infer which server to connect to based on the contents of the query. - pub fn infer(&mut self, ast: &Vec) -> Result<(), Error> { - if !self.pool_settings.query_parser_read_write_splitting { - return Ok(()); // Nothing to do - } - - debug!("Inferring role"); - - if ast.is_empty() { - // That's weird, no idea, let's go to primary - self.active_role = Some(Role::Primary); - return Err(Error::QueryRouterParserError("empty query".into())); - } - - let mut visited_write_statement = false; - let mut prev_inferred_shard = None; - - for q in ast { - match q { - // All transactions go to the primary, probably a write. - StartTransaction { .. } => { - self.active_role = Some(Role::Primary); - break; - } - - // Likely a read-only query - Query(query) => { - match &self.pool_settings.automatic_sharding_key { - Some(_) => { - // TODO: if we have multiple queries in the same message, - // we can either split them and execute them individually - // or discard shard selection. If they point to the same shard though, - // we can let them through as-is. - // This is basically building a database now :) - let inferred_shard = self.infer_shard(query); - self.handle_inferred_shard(inferred_shard, &mut prev_inferred_shard)?; - } - - None => (), - }; - - let has_locks = !query.locks.is_empty(); - let has_mutation = Self::is_mutation_query(query); - - if has_locks || has_mutation { - self.active_role = Some(Role::Primary); - } else if !visited_write_statement { - // If we already visited a write statement, we should be going to the primary. - self.active_role = match self.primary_reads_enabled() { - false => Some(Role::Replica), // If primary should not be receiving reads, use a replica. - true => None, // Any server role is fine in this case. - } - } - } - - // Likely a write - _ => { - match &self.pool_settings.automatic_sharding_key { - Some(_) => { - // TODO: similar to the above, if we have multiple queries in the - // same message, we can either split them and execute them individually - // or discard shard selection. If they point to the same shard though, - // we can let them through as-is. - let inferred_shard = self.infer_shard_on_write(q)?; - self.handle_inferred_shard(inferred_shard, &mut prev_inferred_shard)?; - } - - None => (), - }; - visited_write_statement = true; - self.active_role = Some(Role::Primary); - } - }; - } - - Ok(()) - } - - fn handle_inferred_shard( - &mut self, - inferred_shard: Option, - prev_inferred_shard: &mut Option, - ) -> Result<(), Error> { - if let Some(shard) = inferred_shard { - if let Some(prev_shard) = *prev_inferred_shard { - if prev_shard != shard { - debug!("Found more than one shard in the query, not supported yet"); - return Err(Error::QueryRouterParserError( - "multiple shards in query".into(), - )); - } - } - *prev_inferred_shard = Some(shard); - self.active_shard = Some(shard); - debug!("Automatically using shard: {:?}", self.active_shard); - }; - Ok(()) - } - - fn infer_shard_on_write(&mut self, q: &Statement) -> Result, Error> { - let mut exprs = Vec::new(); - - // Collect all table names from the query. - let mut table_names = Vec::new(); - - match q { - Insert { - or, - into: _, - table_name, - columns, - overwrite: _, - source, - partitioned, - after_columns, - table: _, - on: _, - returning: _, - ignore: _, - } => { - // Not supported in postgres. - assert!(or.is_none()); - assert!(partitioned.is_none()); - assert!(after_columns.is_empty()); - - Self::process_table(table_name, &mut table_names); - if let Some(source) = source { - Self::process_query(source, &mut exprs, &mut table_names, &Some(columns)); - } - } - Delete { - tables, - from, - using, - selection, - returning: _, - order_by: _, - limit: _, - } => { - if let Some(expr) = selection { - exprs.push(expr.clone()); - } - - // Multi tables delete are not supported in postgres. - assert!(tables.is_empty()); - - Self::process_tables_with_join(from, &mut exprs, &mut table_names); - if let Some(using_tbl_with_join) = using { - Self::process_tables_with_join( - using_tbl_with_join, - &mut exprs, - &mut table_names, - ); - } - Self::process_selection(selection, &mut exprs); - } - Update { - table, - assignments, - from, - selection, - returning: _, - } => { - Self::process_table_with_join(table, &mut exprs, &mut table_names); - if let Some(from_tbl) = from { - Self::process_table_with_join(from_tbl, &mut exprs, &mut table_names); - } - Self::process_selection(selection, &mut exprs); - self.assignment_parser(assignments)?; - } - _ => { - return Ok(None); - } - }; - - Ok(self.infer_shard_from_exprs(exprs, table_names)) - } - - fn process_query( - query: &sqlparser::ast::Query, - exprs: &mut Vec, - table_names: &mut Vec>, - columns: &Option<&Vec>, - ) { - match &*query.body { - SetExpr::Query(query) => { - Self::process_query(query, exprs, table_names, columns); - } - - // SELECT * FROM ... - // We understand that pretty well. - SetExpr::Select(select) => { - Self::process_tables_with_join(&select.from, exprs, table_names); - - // Parse the actual "FROM ..." - Self::process_selection(&select.selection, exprs); - } - - SetExpr::Values(values) => { - if let Some(cols) = columns { - for row in values.rows.iter() { - for (i, expr) in row.iter().enumerate() { - if cols.len() > i { - exprs.push(Expr::BinaryOp { - left: Box::new(Expr::Identifier(cols[i].clone())), - op: BinaryOperator::Eq, - right: Box::new(expr.clone()), - }); - } - } - } - } - } - _ => (), - }; - } - - fn process_selection(selection: &Option, exprs: &mut Vec) { - match selection { - Some(selection) => { - exprs.push(selection.clone()); - } - - None => (), - }; - } - - fn process_tables_with_join( - tables: &[TableWithJoins], - exprs: &mut Vec, - table_names: &mut Vec>, - ) { - for table in tables.iter() { - Self::process_table_with_join(table, exprs, table_names); - } - } - - fn process_table_with_join( - table: &TableWithJoins, - exprs: &mut Vec, - table_names: &mut Vec>, - ) { - if let TableFactor::Table { name, .. } = &table.relation { - Self::process_table(name, table_names); - }; - - // Get table names from all the joins. - for join in table.joins.iter() { - if let TableFactor::Table { name, .. } = &join.relation { - Self::process_table(name, table_names); - }; - - // We can filter results based on join conditions, e.g. - // SELECT * FROM t INNER JOIN B ON B.sharding_key = 5; - if let JoinOperator::Inner(JoinConstraint::On(expr)) = &join.join_operator { - // Parse the selection criteria later. - exprs.push(expr.clone()); - }; - } - } - - fn process_table(name: &sqlparser::ast::ObjectName, table_names: &mut Vec>) { - table_names.push(name.0.clone()) - } - - /// Parse the shard number from the Bind message - /// which contains the arguments for a prepared statement. - /// - /// N.B.: Only supports anonymous prepared statements since we don't - /// keep a cache of them in PgCat. - pub fn infer_shard_from_bind(&mut self, message: &BytesMut) -> bool { - if !self.pool_settings.query_parser_read_write_splitting { - return false; // Nothing to do - } - - debug!("Parsing bind message"); - - let mut message_cursor = Cursor::new(message); - - let code = message_cursor.get_u8() as char; - let len = message_cursor.get_i32(); - - if code != 'B' { - debug!("Not a bind packet"); - return false; - } - - // Check message length - if message.len() != len as usize + 1 { - debug!( - "Message has wrong length, expected {}, but have {}", - len, - message.len() - ); - return false; - } - - // There are no shard keys in the prepared statement. - if self.placeholders.is_empty() { - debug!("There are no placeholders in the prepared statement that matched the automatic sharding key"); - return false; - } - - let sharder = Sharder::new( - self.pool_settings.shards, - self.pool_settings.sharding_function, - ); - - let mut shards = BTreeSet::new(); - - let _portal = message_cursor.read_string(); - let _name = message_cursor.read_string(); - - let num_params = message_cursor.get_i16(); - let parameter_format = match num_params { - 0 => ParameterFormat::Text, // Text - 1 => { - let param_format = message_cursor.get_i16(); - ParameterFormat::Uniform(match param_format { - 0 => Box::new(ParameterFormat::Text), - 1 => Box::new(ParameterFormat::Binary), - _ => unreachable!(), - }) - } - n => { - let mut v = Vec::with_capacity(n as usize); - for _ in 0..n { - let param_format = message_cursor.get_i16(); - v.push(match param_format { - 0 => ParameterFormat::Text, - 1 => ParameterFormat::Binary, - _ => unreachable!(), - }); - } - ParameterFormat::Specified(v) - } - }; - - let num_parameters = message_cursor.get_i16(); - - for i in 0..num_parameters { - let mut len = message_cursor.get_i32() as usize; - let format = match ¶meter_format { - ParameterFormat::Text => ParameterFormat::Text, - ParameterFormat::Uniform(format) => *format.clone(), - ParameterFormat::Specified(formats) => formats[i as usize].clone(), - _ => unreachable!(), - }; - - debug!("Parameter {} (len: {}): {:?}", i, len, format); - - // Postgres counts placeholders starting at 1 - let placeholder = i + 1; - - if self.placeholders.contains(&placeholder) { - let value = match format { - ParameterFormat::Text => { - let mut value = String::new(); - while len > 0 { - value.push(message_cursor.get_u8() as char); - len -= 1; - } - - match value.parse::() { - Ok(value) => value, - Err(_) => { - debug!("Error parsing bind value: {}", value); - continue; - } - } - } - - ParameterFormat::Binary => match len { - 2 => message_cursor.get_i16() as i64, - 4 => message_cursor.get_i32() as i64, - 8 => message_cursor.get_i64(), - _ => { - error!( - "Got wrong length for integer type parameter in bind: {}", - len - ); - continue; - } - }, - - _ => unreachable!(), - }; - - shards.insert(sharder.shard(value)); - } - } - - self.placeholders.clear(); - self.placeholders.shrink_to_fit(); - - // We only support querying one shard at a time. - // TODO: Support multi-shard queries some day. - if shards.len() == 1 { - debug!("Found one sharding key"); - self.set_shard(Some(*shards.first().unwrap())); - true - } else { - debug!("Found no sharding keys"); - false - } - } - - /// An `assignments` exists in the `UPDATE` statements. This parses the assignments and makes - /// sure that we are not updating the sharding key. It's not supported yet. - fn assignment_parser(&self, assignments: &Vec) -> Result<(), Error> { - let sharding_key = self - .pool_settings - .automatic_sharding_key - .as_ref() - .unwrap() - .split('.') - .map(|ident| Ident::new(ident.to_lowercase())) - .collect::>(); - - // Sharding key must be always fully qualified - assert_eq!(sharding_key.len(), 2); - - for a in assignments { - if sharding_key[0].value == "*" - && sharding_key[1].value == a.id.last().unwrap().value.to_lowercase() - { - return Err(Error::QueryRouterParserError( - "Sharding key cannot be updated.".into(), - )); - } - } - Ok(()) - } - - /// A `selection` is the `WHERE` clause. This parses - /// the clause and extracts the sharding key, if present. - fn selection_parser(&self, expr: &Expr, table_names: &Vec>) -> Vec { - let mut result = Vec::new(); - let mut found = false; - - let sharding_key = self - .pool_settings - .automatic_sharding_key - .as_ref() - .unwrap() - .split('.') - .map(|ident| Ident::new(ident.to_lowercase())) - .collect::>(); - - // Sharding key must be always fully qualified - assert_eq!(sharding_key.len(), 2); - - // This parses `sharding_key = 5`. But it's technically - // legal to write `5 = sharding_key`. I don't judge the people - // who do that, but I think ORMs will still use the first variant, - // so we can leave the second as a TODO. - if let Expr::BinaryOp { left, op, right } = expr { - match &**left { - Expr::BinaryOp { .. } => result.extend(self.selection_parser(left, table_names)), - Expr::Identifier(ident) => { - // Only if we're dealing with only one table - // and there is no ambiguity - if ident.value.to_lowercase() == sharding_key[1].value { - // Sharding key is unique enough, don't worry about - // table names. - if &sharding_key[0].value == "*" { - found = true; - } else if table_names.len() == 1 { - let table = &table_names[0]; - - if table.len() == 1 { - // Table is not fully qualified, e.g. - // SELECT * FROM t WHERE sharding_key = 5 - // Make sure the table name from the sharding key matches - // the table name from the query. - found = sharding_key[0].value == table[0].value.to_lowercase(); - } else if table.len() == 2 { - // Table name is fully qualified with the schema: e.g. - // SELECT * FROM public.t WHERE sharding_key = 5 - // Ignore the schema (TODO: at some point, we want schema support) - // and use the table name only. - found = sharding_key[0].value == table[1].value.to_lowercase(); - } else { - debug!("Got table name with more than two idents, which is not possible"); - } - } - } - } - - Expr::CompoundIdentifier(idents) => { - // The key is fully qualified in the query, - // it will exist or Postgres will throw an error. - if idents.len() == 2 { - found = (&sharding_key[0].value == "*" - || sharding_key[0].value == idents[0].value.to_lowercase()) - && sharding_key[1].value == idents[1].value.to_lowercase(); - } - // TODO: key can have schema as well, e.g. public.data.id (len == 3) - } - _ => (), - }; - - match op { - BinaryOperator::Eq => (), - BinaryOperator::Or => (), - BinaryOperator::And => (), - _ => { - // TODO: support other operators than equality. - debug!("Unsupported operation: {:?}", op); - return Vec::new(); - } - }; - - match &**right { - Expr::BinaryOp { .. } => result.extend(self.selection_parser(right, table_names)), - Expr::Value(Value::Number(value, ..)) => { - if found { - match value.parse::() { - Ok(value) => result.push(ShardingKey::Value(value)), - Err(_) => { - debug!("Sharding key was not an integer: {}", value); - } - }; - } - } - - Expr::Value(Value::Placeholder(placeholder)) => { - match placeholder.replace('$', "").parse::() { - Ok(placeholder) => result.push(ShardingKey::Placeholder(placeholder)), - Err(_) => { - debug!( - "Prepared statement didn't have integer placeholders: {}", - placeholder - ); - } - } - } - _ => (), - }; - } - - debug!("Sharding keys found: {:?}", result); - - result - } - - /// Try to figure out which shard the query should go to. - fn infer_shard(&mut self, query: &sqlparser::ast::Query) -> Option { - let mut exprs = Vec::new(); - - // Collect all table names from the query. - let mut table_names = Vec::new(); - - Self::process_query(query, &mut exprs, &mut table_names, &None); - self.infer_shard_from_exprs(exprs, table_names) - } - - fn infer_shard_from_exprs( - &mut self, - exprs: Vec, - table_names: Vec>, - ) -> Option { - let mut shards = BTreeSet::new(); - - let sharder = Sharder::new( - self.pool_settings.shards, - self.pool_settings.sharding_function, - ); - - // Look for sharding keys in either the join condition - // or the selection. - for expr in exprs.iter() { - let sharding_keys = self.selection_parser(expr, &table_names); - - // TODO: Add support for prepared statements here. - // This should just give us the position of the value in the `B` message. - - for value in sharding_keys { - match value { - ShardingKey::Value(value) => { - let shard = sharder.shard(value); - shards.insert(shard); - } - - ShardingKey::Placeholder(position) => { - self.placeholders.push(position); - } - }; - } - } - match shards.len() { - // Didn't find a sharding key, you're on your own. - 0 => { - debug!("No sharding keys found"); - None - } - - 1 => Some(shards.into_iter().last().unwrap()), - - // TODO: support querying multiple shards (some day...) - _ => { - debug!("More than one sharding key found"); - None - } - } - } - - /// 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, - self.pool_settings.sharding_function, - ); - let shard = sharder.shard(sharding_key); - self.set_shard(Some(shard)); - self.active_shard - } - - /// Get the current desired server role we should be talking to. - pub fn role(&self) -> Option { - self.active_role - } - - /// Get desired shard we should be talking to. - pub fn shard(&self) -> Option { - self.active_shard - } - - pub fn set_shard(&mut self, shard: Option) { - self.active_shard = shard; - } - - /// Should we attempt to parse queries? - pub fn query_parser_enabled(&self) -> bool { - match self.query_parser_enabled { - None => { - debug!( - "Using pool settings, query_parser_enabled: {}", - self.pool_settings.query_parser_enabled - ); - self.pool_settings.query_parser_enabled - } - - Some(value) => { - debug!( - "Using query parser override, query_parser_enabled: {}", - value - ); - value - } - } - } - - pub fn primary_reads_enabled(&self) -> bool { - match self.primary_reads_enabled { - None => self.pool_settings.primary_reads_enabled, - Some(value) => value, - } - } -} - -impl Default for QueryRouter { - fn default() -> Self { - Self::new() - } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::config::PoolMode; - use crate::messages::simple_query; - use crate::sharding::ShardingFunction; - use bytes::BufMut; - - #[test] - fn test_defaults() { - QueryRouter::setup(); - let qr = QueryRouter::new(); - - assert_eq!(qr.role(), None); - } - - #[test] - fn test_split_cte_queries() { - QueryRouter::setup(); - let mut qr = QueryRouter::new(); - qr.pool_settings.query_parser_read_write_splitting = true; - qr.pool_settings.query_parser_enabled = true; - - let query = simple_query( - "WITH t AS ( - SELECT id FROM users WHERE name ILIKE '%ja%' - ) - UPDATE user_languages - SET settings = '{}' - FROM t WHERE t.id = user_id;", - ); - let ast = qr.parse(&query).unwrap(); - assert!(qr.infer(&ast).is_ok()); - assert_eq!(qr.role(), Some(Role::Primary)); - } - - #[test] - fn test_infer_replica() { - QueryRouter::setup(); - let mut qr = QueryRouter::new(); - qr.pool_settings.query_parser_read_write_splitting = true; - assert!(qr - .try_execute_command(&simple_query("SET SERVER ROLE TO 'auto'")) - .is_some()); - assert!(qr.query_parser_enabled()); - - assert!(qr - .try_execute_command(&simple_query("SET PRIMARY READS TO off")) - .is_some()); - - let queries = vec![ - simple_query("SELECT * FROM items WHERE id = 5"), - simple_query( - "SELECT id, name, value FROM items INNER JOIN prices ON item.id = prices.item_id", - ), - simple_query("WITH t AS (SELECT * FROM items) SELECT * FROM t"), - ]; - - for query in queries { - // It's a recognized query - assert!(qr.infer(&qr.parse(&query).unwrap()).is_ok()); - assert_eq!(qr.role(), Some(Role::Replica)); - } - } - - #[test] - fn test_infer_primary() { - QueryRouter::setup(); - let mut qr = QueryRouter::new(); - qr.pool_settings.query_parser_read_write_splitting = true; - - let queries = vec![ - simple_query("UPDATE items SET name = 'pumpkin' WHERE id = 5"), - simple_query("INSERT INTO items (id, name) VALUES (5, 'pumpkin')"), - simple_query("DELETE FROM items WHERE id = 5"), - simple_query("BEGIN"), // Transaction start - ]; - - for query in queries { - // It's a recognized query - assert!(qr.infer(&qr.parse(&query).unwrap()).is_ok()); - assert_eq!(qr.role(), Some(Role::Primary)); - } - } - - #[test] - fn test_select_for_update() { - QueryRouter::setup(); - let mut qr = QueryRouter::new(); - qr.pool_settings.query_parser_read_write_splitting = true; - - let queries_in_primary_role = vec![ - simple_query("BEGIN"), // Transaction start - simple_query("SELECT * FROM items WHERE id = 5 FOR UPDATE"), - simple_query("UPDATE items SET name = 'pumpkin' WHERE id = 5"), - ]; - - for query in queries_in_primary_role { - assert!(qr.infer(&qr.parse(&query).unwrap()).is_ok()); - assert_eq!(qr.role(), Some(Role::Primary)); - } - - // query without lock do not change role - let query = simple_query("SELECT * FROM items WHERE id = 5"); - assert!(qr.infer(&qr.parse(&query).unwrap()).is_ok()); - assert_eq!(qr.role(), None); - } - - #[test] - fn test_infer_primary_reads_enabled() { - QueryRouter::setup(); - let mut qr = QueryRouter::new(); - let query = simple_query("SELECT * FROM items WHERE id = 5"); - assert!(qr - .try_execute_command(&simple_query("SET PRIMARY READS TO on")) - .is_some()); - - assert!(qr.infer(&qr.parse(&query).unwrap()).is_ok()); - assert_eq!(qr.role(), None); - } - - #[test] - fn test_infer_parse_prepared() { - QueryRouter::setup(); - let mut qr = QueryRouter::new(); - qr.pool_settings.query_parser_read_write_splitting = true; - - qr.try_execute_command(&simple_query("SET SERVER ROLE TO 'auto'")); - assert!(qr - .try_execute_command(&simple_query("SET PRIMARY READS TO off")) - .is_some()); - - let prepared_stmt = BytesMut::from( - &b"WITH t AS (SELECT * FROM items WHERE name = $1) SELECT * FROM t WHERE id = $2\0"[..], - ); - let mut res = BytesMut::from(&b"P"[..]); - res.put_i32(prepared_stmt.len() as i32 + 4 + 1 + 2); - res.put_u8(0); - res.put(prepared_stmt); - res.put_i16(0); - - assert!(qr.infer(&qr.parse(&res).unwrap()).is_ok()); - assert_eq!(qr.role(), Some(Role::Replica)); - } - - #[test] - fn test_regex_set() { - QueryRouter::setup(); - - let tests = [ - // Upper case - "SET SHARDING KEY TO '1'", - "SET SHARD TO '1'", - "SHOW SHARD", - "SET SERVER ROLE TO 'replica'", - "SET SERVER ROLE TO 'primary'", - "SET SERVER ROLE TO 'any'", - "SET SERVER ROLE TO 'auto'", - "SHOW SERVER ROLE", - "SET PRIMARY READS TO 'on'", - "SET PRIMARY READS TO 'off'", - "SET PRIMARY READS TO 'default'", - "SHOW PRIMARY READS", - // Lower case - "set sharding key to '1'", - "set shard to '1'", - "show shard", - "set server role to 'replica'", - "set server role to 'primary'", - "set server role to 'any'", - "set server role to 'auto'", - "show server role", - "set primary reads to 'on'", - "set primary reads to 'OFF'", - "set primary reads to 'deFaUlt'", - // No quotes - "SET SHARDING KEY TO 11235", - "SET SHARD TO 15", - "SET PRIMARY READS TO off", - // Spaces and semicolon - " SET SHARDING KEY TO 11235 ; ", - " SET SHARD TO 15; ", - " SET SHARDING KEY TO 11235 ;", - " SET SERVER ROLE TO 'primary'; ", - " SET SERVER ROLE TO 'primary' ; ", - " SET SERVER ROLE TO 'primary' ;", - " SET PRIMARY READS TO 'off' ;", - ]; - - // Which regexes it'll match to in the list - let matches = [ - 0, 1, 2, 3, 3, 3, 3, 4, 5, 5, 5, 6, 0, 1, 2, 3, 3, 3, 3, 4, 5, 5, 5, 0, 1, 5, 0, 1, 0, - 3, 3, 3, 5, - ]; - - let list = CUSTOM_SQL_REGEX_LIST.get().unwrap(); - let set = CUSTOM_SQL_REGEX_SET.get().unwrap(); - - for (i, test) in tests.iter().enumerate() { - if !list[matches[i]].is_match(test) { - println!("{} does not match {}", test, list[matches[i]]); - panic!(); - } - assert_eq!(set.matches(test).into_iter().count(), 1); - } - - let bad = [ - "SELECT * FROM table", - "SELECT * FROM table WHERE value = 'set sharding key to 5'", // Don't capture things in the middle of the query - ]; - - for query in &bad { - assert_eq!(set.matches(query).into_iter().count(), 0); - } - } - - #[test] - fn test_try_execute_command() { - QueryRouter::setup(); - let mut qr = QueryRouter::new(); - - // SetShardingKey - let query = simple_query("SET SHARDING KEY TO 13"); - assert_eq!( - qr.try_execute_command(&query), - Some((Command::SetShardingKey, String::from("0"))) - ); - assert_eq!(qr.shard().unwrap(), 0); - - // SetShard - let query = simple_query("SET SHARD TO '1'"); - assert_eq!( - qr.try_execute_command(&query), - Some((Command::SetShard, String::from("1"))) - ); - assert_eq!(qr.shard().unwrap(), 1); - - // ShowShard - let query = simple_query("SHOW SHARD"); - assert_eq!( - qr.try_execute_command(&query), - Some((Command::ShowShard, String::from("1"))) - ); - - // SetServerRole - let roles = ["primary", "replica", "any", "auto", "primary"]; - let verify_roles = [ - Some(Role::Primary), - Some(Role::Replica), - None, - None, - Some(Role::Primary), - ]; - let query_parser_enabled = [false, false, false, true, false]; - - for (idx, role) in roles.iter().enumerate() { - let query = simple_query(&format!("SET SERVER ROLE TO '{}'", role)); - assert_eq!( - qr.try_execute_command(&query), - Some((Command::SetServerRole, String::from(*role))) - ); - assert_eq!(qr.role(), verify_roles[idx],); - assert_eq!(qr.query_parser_enabled(), query_parser_enabled[idx],); - - // ShowServerRole - let query = simple_query("SHOW SERVER ROLE"); - assert_eq!( - qr.try_execute_command(&query), - Some((Command::ShowServerRole, String::from(*role))) - ); - } - - let primary_reads = ["on", "off", "default"]; - let primary_reads_enabled = ["on", "off", "on"]; - - for (idx, primary_reads) in primary_reads.iter().enumerate() { - assert_eq!( - qr.try_execute_command(&simple_query(&format!( - "SET PRIMARY READS TO {}", - primary_reads - ))), - Some((Command::SetPrimaryReads, String::from(*primary_reads))) - ); - assert_eq!( - qr.try_execute_command(&simple_query("SHOW PRIMARY READS")), - Some(( - Command::ShowPrimaryReads, - String::from(primary_reads_enabled[idx]) - )) - ); - } - } - - #[test] - fn test_enable_query_parser() { - QueryRouter::setup(); - let mut qr = QueryRouter::new(); - qr.pool_settings.query_parser_read_write_splitting = true; - - let query = simple_query("SET SERVER ROLE TO 'auto'"); - assert!(qr - .try_execute_command(&simple_query("SET PRIMARY READS TO off")) - .is_some()); - - assert!(qr.try_execute_command(&query).is_some()); - assert!(qr.query_parser_enabled()); - assert_eq!(qr.role(), None); - - let query = simple_query("INSERT INTO test_table VALUES (1)"); - assert!(qr.infer(&qr.parse(&query).unwrap()).is_ok()); - assert_eq!(qr.role(), Some(Role::Primary)); - - let query = simple_query("SELECT * FROM test_table"); - assert!(qr.infer(&qr.parse(&query).unwrap()).is_ok()); - assert_eq!(qr.role(), Some(Role::Replica)); - - assert!(qr.query_parser_enabled()); - let query = simple_query("SET SERVER ROLE TO 'default'"); - assert!(qr.try_execute_command(&query).is_some()); - assert!(!qr.query_parser_enabled()); - } - - #[test] - fn test_query_parser() { - QueryRouter::setup(); - let mut qr = QueryRouter::new(); - qr.pool_settings.query_parser_read_write_splitting = true; - - let query = simple_query("SELECT req_tab_0.* FROM validation req_tab_0 WHERE array['http://www.w3.org/ns/shacl#ValidationResult'] && req_tab_0.type::text[] AND ( ( (req_tab_0.focusnode = 'DataSource_Credilogic_DataSourceAddress_144959227') ) )"); - assert!(qr.infer(&qr.parse(&query).unwrap()).is_ok()); - - let query = simple_query("WITH EmployeeSalaries AS (SELECT Department, Salary FROM Employees) SELECT Department, AVG(Salary) AS AverageSalary FROM EmployeeSalaries GROUP BY Department;"); - assert!(qr.infer(&qr.parse(&query).unwrap()).is_ok()); - } - - #[test] - fn test_update_from_pool_settings() { - QueryRouter::setup(); - - let pool_settings = PoolSettings { - pool_mode: PoolMode::Transaction, - load_balancing_mode: crate::config::LoadBalancingMode::Random, - shards: 2, - user: crate::config::User::default(), - default_role: Some(Role::Replica), - query_parser_enabled: true, - query_parser_max_length: None, - query_parser_read_write_splitting: true, - primary_reads_enabled: false, - sharding_function: ShardingFunction::PgBigintHash, - automatic_sharding_key: Some(String::from("test.id")), - healthcheck_delay: PoolSettings::default().healthcheck_delay, - healthcheck_timeout: PoolSettings::default().healthcheck_timeout, - ban_time: PoolSettings::default().ban_time, - sharding_key_regex: None, - shard_id_regex: None, - default_shard: crate::config::DefaultShard::Shard(0), - 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); - assert_eq!(qr.active_shard, None); - assert_eq!(qr.query_parser_enabled, None); - assert_eq!(qr.primary_reads_enabled, None); - - // Internal state must not be changed due to this, only defaults - qr.update_pool_settings(&pool_settings); - - assert_eq!(qr.active_role, None); - assert_eq!(qr.active_shard, None); - assert!(qr.query_parser_enabled()); - assert!(!qr.primary_reads_enabled()); - - let q1 = simple_query("SET SERVER ROLE TO 'primary'"); - assert!(qr.try_execute_command(&q1).is_some()); - assert_eq!(qr.active_role.unwrap(), Role::Primary); - - let q2 = simple_query("SET SERVER ROLE TO 'default'"); - assert!(qr.try_execute_command(&q2).is_some()); - assert_eq!(qr.active_role.unwrap(), pool_settings.default_role); - } - - #[test] - fn test_parse_multiple_queries() { - QueryRouter::setup(); - - let mut qr = QueryRouter::new(); - assert!(qr - .infer(&qr.parse(&simple_query("BEGIN; SELECT 1; COMMIT;")).unwrap()) - .is_ok()); - assert_eq!(qr.role(), Role::Primary); - - assert!(qr - .infer(&qr.parse(&simple_query("SELECT 1; SELECT 2;")).unwrap()) - .is_ok()); - assert_eq!(qr.role(), Role::Replica); - - assert!(qr - .infer( - &qr.parse(&simple_query( - "SELECT 123; INSERT INTO t VALUES (5); SELECT 1;" - )) - .unwrap() - ) - .is_ok()); - assert_eq!(qr.role(), Role::Primary); - } - - #[test] - fn test_regex_shard_parsing() { - QueryRouter::setup(); - - let pool_settings = PoolSettings { - pool_mode: PoolMode::Transaction, - load_balancing_mode: crate::config::LoadBalancingMode::Random, - shards: 5, - user: crate::config::User::default(), - default_role: Some(Role::Replica), - query_parser_enabled: true, - query_parser_max_length: None, - query_parser_read_write_splitting: true, - primary_reads_enabled: false, - sharding_function: ShardingFunction::PgBigintHash, - automatic_sharding_key: None, - healthcheck_delay: PoolSettings::default().healthcheck_delay, - healthcheck_timeout: PoolSettings::default().healthcheck_timeout, - ban_time: PoolSettings::default().ban_time, - sharding_key_regex: Some(Regex::new(r"/\* sharding_key: (\d+) \*/").unwrap()), - shard_id_regex: Some(Regex::new(r"/\* shard_id: (\d+) \*/").unwrap()), - default_shard: crate::config::DefaultShard::Shard(0), - 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); - - // Shard should start out unset - assert_eq!(qr.active_shard, None); - - // Don't panic when short query eg. ; is sent - let q0 = simple_query(";"); - assert!(qr.try_execute_command(&q0).is_none()); - assert_eq!(qr.active_shard, None); - - // Make sure setting it works - let q1 = simple_query("/* shard_id: 1 */ select 1 from foo;"); - assert!(qr.try_execute_command(&q1).is_none()); - assert_eq!(qr.active_shard, Some(1)); - - // And make sure changing it works - let q2 = simple_query("/* shard_id: 0 */ select 1 from foo;"); - assert!(qr.try_execute_command(&q2).is_none()); - assert_eq!(qr.active_shard, Some(0)); - - // Validate setting by shard with expected shard copied from sharding.rs tests - let q2 = simple_query("/* sharding_key: 6 */ select 1 from foo;"); - assert!(qr.try_execute_command(&q2).is_none()); - assert_eq!(qr.active_shard, Some(2)); - } - - #[test] - fn test_automatic_sharding_key() { - QueryRouter::setup(); - - let mut qr = QueryRouter::new(); - qr.pool_settings.automatic_sharding_key = Some("data.id".to_string()); - qr.pool_settings.shards = 3; - qr.pool_settings.query_parser_read_write_splitting = true; - - assert!(qr - .infer( - &qr.parse(&simple_query("SELECT * FROM data WHERE id = 5")) - .unwrap(), - ) - .is_ok()); - assert_eq!(qr.shard().unwrap(), 2); - - assert!(qr - .infer( - &qr.parse(&simple_query( - "SELECT one, two, three FROM public.data WHERE id = 6" - )) - .unwrap() - ) - .is_ok()); - assert_eq!(qr.shard().unwrap(), 0); - - assert!(qr - .infer( - &qr.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().unwrap(), 2); - - // Shard did not move because we couldn't determine the sharding key since it could be ambiguous - // in the query. - assert!(qr - .infer( - &qr.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().unwrap(), 2); - - assert!(qr - .infer( - &qr.parse(&simple_query( - r#"SELECT * FROM "public"."data" WHERE "id" = 6"# - )) - .unwrap() - ) - .is_ok()); - assert_eq!(qr.shard().unwrap(), 0); - - assert!(qr - .infer( - &qr.parse(&simple_query( - r#"SELECT * FROM "public"."data" WHERE "data"."id" = 5"# - )) - .unwrap() - ) - .is_ok()); - assert_eq!(qr.shard().unwrap(), 2); - - // Super unique sharding key - qr.pool_settings.automatic_sharding_key = Some("*.unique_enough_column_name".to_string()); - assert!(qr - .infer( - &qr.parse(&simple_query( - "SELECT * FROM table_x WHERE unique_enough_column_name = 6" - )) - .unwrap() - ) - .is_ok()); - assert_eq!(qr.shard().unwrap(), 0); - - assert!(qr - .infer( - &qr.parse(&simple_query("SELECT * FROM table_y WHERE another_key = 5")) - .unwrap() - ) - .is_ok()); - assert_eq!(qr.shard().unwrap(), 0); - } - - fn auto_shard_wrapper(qry: &str, should_succeed: bool) -> Option { - let mut qr = QueryRouter::new(); - qr.pool_settings.automatic_sharding_key = Some("*.w_id".to_string()); - qr.pool_settings.shards = 3; - qr.pool_settings.query_parser_read_write_splitting = true; - assert_eq!(qr.shard(), None); - let infer_res = qr.infer(&qr.parse(&simple_query(qry)).unwrap()); - assert_eq!(infer_res.is_ok(), should_succeed); - qr.shard() - } - - fn auto_shard(qry: &str) -> Option { - auto_shard_wrapper(qry, true) - } - - fn auto_shard_fails(qry: &str) -> Option { - auto_shard_wrapper(qry, false) - } - - #[test] - fn test_automatic_sharding_insert_update_delete() { - QueryRouter::setup(); - - assert_eq!( - auto_shard_fails( - "UPDATE ORDERS SET w_id = 3 WHERE O_ID = 3 AND O_D_ID = 3 AND W_ID = 5" - ), - None - ); - - assert_eq!( - auto_shard_fails( - "UPDATE ORDERS o SET o.W_ID = 3 WHERE o.O_ID = 3 AND o.O_D_ID = 3 AND o.W_ID = 5" - ), - None - ); - - assert_eq!( - auto_shard( - "UPDATE ORDERS o SET o.O_CARRIER_ID = 3 WHERE o.O_ID = 3 AND o.O_D_ID = 3 AND o.W_ID = 5" - ), - Some(2) - ); - } - - #[test] - fn test_automatic_sharding_key_tpcc() { - QueryRouter::setup(); - - assert_eq!(auto_shard("SELECT * FROM my_tbl WHERE w_id = 5"), Some(2)); - assert_eq!( - auto_shard("BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ"), - None - ); - assert_eq!(auto_shard("COMMIT"), None); - assert_eq!(auto_shard("ROLLBACK"), None); - - assert_eq!(auto_shard("SELECT NO_O_ID FROM NEW_ORDER WHERE NO_D_ID = 7 AND W_ID = 5 AND NO_O_ID > 3 LIMIT 3"), Some(2)); - assert_eq!(auto_shard("SELECT NO_O_ID FROM NEW_ORDER no WHERE no.NO_D_ID = 7 AND no.W_ID = 5 AND no.NO_O_ID > 3 LIMIT 3"), Some(2)); - - assert_eq!( - auto_shard("DELETE FROM NEW_ORDER WHERE NO_D_ID = 7 AND W_ID = 5 AND NO_O_ID = 3"), - Some(2) - ); - - assert_eq!( - auto_shard("SELECT O_C_ID FROM ORDERS WHERE O_ID = 3 AND O_D_ID = 3 AND W_ID = 5"), - Some(2) - ); - assert_eq!( - auto_shard( - "UPDATE ORDERS SET O_CARRIER_ID = 3 WHERE O_ID = 3 AND O_D_ID = 3 AND W_ID = 5" - ), - Some(2) - ); - - assert_eq!( - auto_shard("UPDATE ORDER_LINE SET OL_DELIVERY_D = 3 WHERE OL_O_ID = 3 AND OL_D_ID = 3 AND W_ID = 5"), - Some(2) - ); - - assert_eq!( - auto_shard("SELECT SUM(OL_AMOUNT) FROM ORDER_LINE WHERE OL_O_ID = 3 AND OL_D_ID = 3 AND W_ID = 5"), - Some(2) - ); - - assert_eq!( - auto_shard("UPDATE CUSTOMER SET C_BALANCE = C_BALANCE + 3 WHERE C_ID = 3 AND C_D_ID = 3 AND W_ID = 5"), - Some(2) - ); - - assert_eq!( - auto_shard("SELECT W_TAX FROM WAREHOUSE WHERE W_ID = 5"), - Some(2) - ); - assert_eq!( - auto_shard("SELECT D_TAX, D_NEXT_O_ID FROM DISTRICT WHERE D_ID = 3 AND W_ID = 5"), - Some(2) - ); - assert_eq!( - auto_shard("UPDATE DISTRICT SET D_NEXT_O_ID = 3 WHERE D_ID = 3 AND W_ID = 5"), - Some(2) - ); - assert_eq!( - auto_shard("SELECT C_DISCOUNT, C_LAST, C_CREDIT FROM CUSTOMER WHERE W_ID = 5 AND C_D_ID = 3 AND C_ID = 3"), - Some(2) - ); - assert_eq!( - auto_shard("INSERT INTO ORDERS (O_ID, O_D_ID, W_ID, O_C_ID, O_ENTRY_D, O_CARRIER_ID, O_OL_CNT, O_ALL_LOCAL) VALUES (3, 3, 5, 3, 3, 3, 3, 3)"), - Some(2) - ); - assert_eq!( - auto_shard("INSERT INTO NEW_ORDER (NO_O_ID, NO_D_ID, W_ID) VALUES (3, 3, 5)"), - Some(2) - ); - assert_eq!( - auto_shard("SELECT I_PRICE, I_NAME, I_DATA FROM ITEM WHERE I_ID = 3"), - None - ); - assert_eq!( - auto_shard("SELECT S_QUANTITY, S_DATA, S_YTD, S_ORDER_CNT, S_REMOTE_CNT, S_DIST_03 FROM STOCK WHERE S_I_ID = 3 AND W_ID = 5"), - Some(2) - ); - assert_eq!( - auto_shard("UPDATE STOCK SET S_QUANTITY = 3, S_YTD = 3, S_ORDER_CNT = 3, S_REMOTE_CNT = 3 WHERE S_I_ID = 3 AND W_ID = 5"), - Some(2) - ); - assert_eq!( - auto_shard("INSERT INTO ORDER_LINE (OL_O_ID, OL_D_ID, W_ID, OL_NUMBER, OL_I_ID, OL_SUPPLY_W_ID, OL_DELIVERY_D, OL_QUANTITY, OL_AMOUNT, OL_DIST_INFO) VALUES (3, 3, 5, 3, 3, 3, 3, 3, 3, 3)"), - Some(2) - ); - assert_eq!( - auto_shard("SELECT C_ID, C_FIRST, C_MIDDLE, C_LAST, C_BALANCE FROM CUSTOMER WHERE W_ID = 5 AND C_D_ID = 3 AND C_ID = 3"), - Some(2) - ); - assert_eq!( - auto_shard("SELECT C_ID, C_FIRST, C_MIDDLE, C_LAST, C_BALANCE FROM CUSTOMER WHERE W_ID = 5 AND C_D_ID = 3 AND C_LAST = '3' ORDER BY C_FIRST"), - Some(2) - ); - assert_eq!( - auto_shard("SELECT O_ID, O_CARRIER_ID, O_ENTRY_D FROM ORDERS WHERE W_ID = 5 AND O_D_ID = 3 AND O_C_ID = 3 ORDER BY O_ID DESC LIMIT 3"), - Some(2) - ); - assert_eq!( - auto_shard("SELECT OL_SUPPLY_W_ID, OL_I_ID, OL_QUANTITY, OL_AMOUNT, OL_DELIVERY_D FROM ORDER_LINE WHERE W_ID = 5 AND OL_D_ID = 3 AND OL_O_ID = 3"), - Some(2) - ); - assert_eq!( - auto_shard("SELECT W_NAME, W_STREET_1, W_STREET_2, W_CITY, W_STATE, W_ZIP FROM WAREHOUSE WHERE W_ID = 5"), - Some(2) - ); - assert_eq!( - auto_shard("UPDATE WAREHOUSE SET W_YTD = W_YTD + 3 WHERE W_ID = 5"), - Some(2) - ); - assert_eq!( - auto_shard("SELECT D_NAME, D_STREET_1, D_STREET_2, D_CITY, D_STATE, D_ZIP FROM DISTRICT WHERE W_ID = 5 AND D_ID = 3"), - Some(2) - ); - assert_eq!( - auto_shard("UPDATE DISTRICT SET D_YTD = D_YTD + 3 WHERE W_ID = 5 AND D_ID = 3"), - Some(2) - ); - assert_eq!( - auto_shard("SELECT C_ID, C_FIRST, C_MIDDLE, C_LAST, C_STREET_1, C_STREET_2, C_CITY, C_STATE, C_ZIP, C_PHONE, C_SINCE, C_CREDIT, C_CREDIT_LIM, C_DISCOUNT, C_BALANCE, C_YTD_PAYMENT, C_PAYMENT_CNT, C_DATA FROM CUSTOMER WHERE W_ID = 5 AND C_D_ID = 3 AND C_ID = 3"), - Some(2) - ); - assert_eq!( - auto_shard("SELECT C_ID, C_FIRST, C_MIDDLE, C_LAST, C_STREET_1, C_STREET_2, C_CITY, C_STATE, C_ZIP, C_PHONE, C_SINCE, C_CREDIT, C_CREDIT_LIM, C_DISCOUNT, C_BALANCE, C_YTD_PAYMENT, C_PAYMENT_CNT, C_DATA FROM CUSTOMER WHERE W_ID = 5 AND C_D_ID = 3 AND C_LAST = '3' ORDER BY C_FIRST"), - Some(2) - ); - assert_eq!( - auto_shard("UPDATE CUSTOMER SET C_BALANCE = 3, C_YTD_PAYMENT = 3, C_PAYMENT_CNT = 3, C_DATA = 3 WHERE W_ID = 5 AND C_D_ID = 3 AND C_ID = 3"), - Some(2) - ); - assert_eq!( - auto_shard("UPDATE CUSTOMER SET C_BALANCE = 3, C_YTD_PAYMENT = 3, C_PAYMENT_CNT = 3 WHERE W_ID = 5 AND C_D_ID = 3 AND C_ID = 3"), - Some(2) - ); - - assert_eq!(auto_shard("INSERT INTO HISTORY (H_C_ID, H_C_D_ID, H_C_W_ID, H_D_ID, W_ID, H_DATE, H_AMOUNT, H_DATA) VALUES (3, 3, 5, 3, 5, 3, 3, 3)"), Some(2)); - assert_eq!( - auto_shard("SELECT D_NEXT_O_ID FROM DISTRICT WHERE W_ID = 5 AND D_ID = 3"), - Some(2) - ); - assert_eq!( - auto_shard( - "SELECT COUNT(DISTINCT(OL_I_ID)) FROM ORDER_LINE, STOCK - WHERE ORDER_LINE.W_ID = 5 - AND OL_D_ID = 3 - AND OL_O_ID < 3 - AND OL_O_ID >= 3 - AND STOCK.W_ID = 5 - AND S_I_ID = OL_I_ID - AND S_QUANTITY < 3" - ), - Some(2) - ); - - // This is a distributed query and contains two shards - assert_eq!( - auto_shard( - "SELECT COUNT(DISTINCT(OL_I_ID)) FROM ORDER_LINE, STOCK - WHERE ORDER_LINE.W_ID = 5 - AND OL_D_ID = 3 - AND OL_O_ID < 3 - AND OL_O_ID >= 3 - AND STOCK.W_ID = 7 - AND S_I_ID = OL_I_ID - AND S_QUANTITY < 3" - ), - None - ); - } - - #[test] - fn test_prepared_statements() { - let stmt = "SELECT * FROM data WHERE id = $1"; - - let mut bind = BytesMut::from(&b"B"[..]); - - let mut payload = BytesMut::from(&b"\0\0"[..]); - payload.put_i16(0); - payload.put_i16(1); - payload.put_i32(1); - payload.put(&b"5"[..]); - payload.put_i16(0); - - bind.put_i32(payload.len() as i32 + 4); - bind.put(payload); - - let mut qr = QueryRouter::new(); - qr.pool_settings.automatic_sharding_key = Some("data.id".to_string()); - qr.pool_settings.shards = 3; - qr.pool_settings.query_parser_read_write_splitting = true; - - assert!(qr.infer(&qr.parse(&simple_query(stmt)).unwrap()).is_ok()); - assert_eq!(qr.placeholders.len(), 1); - - assert!(qr.infer_shard_from_bind(&bind)); - assert_eq!(qr.shard().unwrap(), 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 pool_settings = PoolSettings { - query_parser_enabled: true, - plugins: Some(plugins), - ..Default::default() - }; - let mut qr = QueryRouter::new(); - qr.update_pool_settings(&pool_settings); - - let query = simple_query("SELECT * FROM pg_database"); - let ast = qr.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 = qr.parse(&query).unwrap(); - - let res = qr.execute_plugins(&ast).await; - - assert_eq!(res, Ok(PluginOutput::Allow)); - } -} diff --git a/src/scram.rs b/src/scram.rs deleted file mode 100644 index 111dd5e..0000000 --- a/src/scram.rs +++ /dev/null @@ -1,325 +0,0 @@ -// SCRAM-SHA-256 authentication. Heavily inspired by -// https://github.com/sfackler/rust-postgres/ -// SASL implementation. - -use base64::{engine::general_purpose, Engine as _}; -use bytes::BytesMut; -use hmac::{Hmac, Mac}; -use rand::{self, Rng}; -use sha2::digest::FixedOutput; -use sha2::{Digest, Sha256}; - -use std::fmt::Write; - -use crate::constants::*; -use crate::errors::Error; - -/// Normalize a password string. Postgres -/// passwords don't have to be UTF-8. -fn normalize(pass: &[u8]) -> Vec { - let pass = match std::str::from_utf8(pass) { - Ok(pass) => pass, - Err(_) => return pass.to_vec(), - }; - - match stringprep::saslprep(pass) { - Ok(pass) => pass.into_owned().into_bytes(), - Err(_) => pass.as_bytes().to_vec(), - } -} - -/// Keep the SASL state through the exchange. -/// It takes 3 messages to complete the authentication. -pub struct ScramSha256 { - password: String, - salted_password: [u8; 32], - auth_message: String, - message: BytesMut, - nonce: String, -} - -impl ScramSha256 { - /// Create the Scram state from a password. It'll automatically - /// generate a nonce. - pub fn new(password: &str) -> ScramSha256 { - let mut rng = rand::thread_rng(); - let nonce = (0..NONCE_LENGTH) - .map(|_| { - let mut v = rng.gen_range(0x21u8..0x7e); - if v == 0x2c { - v = 0x7e - } - v as char - }) - .collect::(); - - Self::from_nonce(password, &nonce) - } - - /// Used for testing. - pub fn from_nonce(password: &str, nonce: &str) -> ScramSha256 { - let message = BytesMut::from(format!("{}n=,r={}", "n,,", nonce).as_bytes()); - - ScramSha256 { - password: password.to_string(), - nonce: String::from(nonce), - message, - salted_password: [0u8; 32], - auth_message: String::new(), - } - } - - /// Get the current state of the SASL authentication. - pub fn message(&mut self) -> BytesMut { - self.message.clone() - } - - /// Update the state with message received from server. - pub fn update(&mut self, message: &BytesMut) -> Result { - let server_message = Message::parse(message)?; - - if !server_message.nonce.starts_with(&self.nonce) { - return Err(Error::ProtocolSyncError("SCRAM".to_string())); - } - - let salt = match general_purpose::STANDARD.decode(&server_message.salt) { - Ok(salt) => salt, - Err(_) => return Err(Error::ProtocolSyncError("SCRAM".to_string())), - }; - - let salted_password = Self::hi( - &normalize(self.password.as_bytes()), - &salt, - server_message.iterations, - ); - - // Save for verification of final server message. - self.salted_password = salted_password; - - let mut hmac = match Hmac::::new_from_slice(&salted_password) { - Ok(hmac) => hmac, - Err(_) => return Err(Error::ServerError), - }; - - hmac.update(b"Client Key"); - - let client_key = hmac.finalize().into_bytes(); - - let mut hash = Sha256::default(); - hash.update(client_key.as_slice()); - - let stored_key = hash.finalize_fixed(); - let mut cbind_input = vec![]; - cbind_input.extend("n,,".as_bytes()); - - let cbind_input = general_purpose::STANDARD.encode(&cbind_input); - - self.message.clear(); - - // Start writing the client reply. - match write!( - &mut self.message, - "c={},r={}", - cbind_input, server_message.nonce - ) { - Ok(_) => (), - Err(_) => return Err(Error::ServerError), - }; - - let auth_message = format!( - "n=,r={},{},{}", - self.nonce, - String::from_utf8_lossy(&message[..]), - String::from_utf8_lossy(&self.message[..]) - ); - - let mut hmac = match Hmac::::new_from_slice(&stored_key) { - Ok(hmac) => hmac, - Err(_) => return Err(Error::ServerError), - }; - hmac.update(auth_message.as_bytes()); - - // Save the auth message for server final message verification. - self.auth_message = auth_message; - - let client_signature = hmac.finalize().into_bytes(); - - // Sign the client proof. - let mut client_proof = client_key; - for (proof, signature) in client_proof.iter_mut().zip(client_signature) { - *proof ^= signature; - } - - match write!( - &mut self.message, - ",p={}", - general_purpose::STANDARD.encode(&*client_proof) - ) { - Ok(_) => (), - Err(_) => return Err(Error::ServerError), - }; - - Ok(self.message.clone()) - } - - /// Verify final server message. - pub fn finish(&mut self, message: &BytesMut) -> Result<(), Error> { - let final_message = FinalMessage::parse(message)?; - - let verifier = match general_purpose::STANDARD.decode(final_message.value) { - Ok(verifier) => verifier, - Err(_) => return Err(Error::ProtocolSyncError("SCRAM".to_string())), - }; - - let mut hmac = match Hmac::::new_from_slice(&self.salted_password) { - Ok(hmac) => hmac, - Err(_) => return Err(Error::ServerError), - }; - hmac.update(b"Server Key"); - let server_key = hmac.finalize().into_bytes(); - - let mut hmac = match Hmac::::new_from_slice(&server_key) { - Ok(hmac) => hmac, - Err(_) => return Err(Error::ServerError), - }; - hmac.update(self.auth_message.as_bytes()); - - match hmac.verify_slice(&verifier) { - Ok(_) => Ok(()), - Err(_) => Err(Error::ServerError), - } - } - - /// Hash the password with the salt i-times. - fn hi(str: &[u8], salt: &[u8], i: u32) -> [u8; 32] { - let mut hmac = - Hmac::::new_from_slice(str).expect("HMAC is able to accept all key sizes"); - hmac.update(salt); - hmac.update(&[0, 0, 0, 1]); - let mut prev = hmac.finalize().into_bytes(); - - let mut hi = prev; - - for _ in 1..i { - let mut hmac = Hmac::::new_from_slice(str).expect("already checked above"); - hmac.update(&prev); - prev = hmac.finalize().into_bytes(); - - for (hi, prev) in hi.iter_mut().zip(prev) { - *hi ^= prev; - } - } - - hi.into() - } -} - -/// Parse the server challenge. -struct Message { - nonce: String, - salt: String, - iterations: u32, -} - -impl Message { - /// Parse the server SASL challenge. - fn parse(message: &BytesMut) -> Result { - let parts = String::from_utf8_lossy(&message[..]) - .split(',') - .map(|s| s.to_string()) - .collect::>(); - - if parts.len() != 3 { - return Err(Error::ProtocolSyncError("SCRAM".to_string())); - } - - let nonce = str::replace(&parts[0], "r=", ""); - let salt = str::replace(&parts[1], "s=", ""); - let iterations = match str::replace(&parts[2], "i=", "").parse::() { - Ok(iterations) => iterations, - Err(_) => return Err(Error::ProtocolSyncError("SCRAM".to_string())), - }; - - Ok(Message { - nonce, - salt, - iterations, - }) - } -} - -/// Parse server final validation message. -struct FinalMessage { - value: String, -} - -impl FinalMessage { - /// Parse the server final validation message. - pub fn parse(message: &BytesMut) -> Result { - if !message.starts_with(b"v=") || message.len() < 4 { - return Err(Error::ProtocolSyncError("SCRAM".to_string())); - } - - Ok(FinalMessage { - value: String::from_utf8_lossy(&message[2..]).to_string(), - }) - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn parse_server_first_message() { - let message = BytesMut::from( - "r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=4096".as_bytes(), - ); - let message = Message::parse(&message).unwrap(); - assert_eq!(message.nonce, "fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j"); - assert_eq!(message.salt, "QSXCR+Q6sek8bf92"); - assert_eq!(message.iterations, 4096); - } - - #[test] - fn parse_server_last_message() { - let f = FinalMessage::parse(&BytesMut::from( - "v=U+ppxD5XUKtradnv8e2MkeupiA8FU87Sg8CXzXHDAzw".as_bytes(), - )) - .unwrap(); - assert_eq!( - f.value, - "U+ppxD5XUKtradnv8e2MkeupiA8FU87Sg8CXzXHDAzw".to_string() - ); - } - - // recorded auth exchange from psql - #[test] - fn exchange() { - let password = "foobar"; - let nonce = "9IZ2O01zb9IgiIZ1WJ/zgpJB"; - - let client_first = "n,,n=,r=9IZ2O01zb9IgiIZ1WJ/zgpJB"; - let server_first = - "r=9IZ2O01zb9IgiIZ1WJ/zgpJBjx/oIRLs02gGSHcw1KEty3eY,s=fs3IXBy7U7+IvVjZ,i\ - =4096"; - let client_final = - "c=biws,r=9IZ2O01zb9IgiIZ1WJ/zgpJBjx/oIRLs02gGSHcw1KEty3eY,p=AmNKosjJzS3\ - 1NTlQYNs5BTeQjdHdk7lOflDo5re2an8="; - let server_final = "v=U+ppxD5XUKtradnv8e2MkeupiA8FU87Sg8CXzXHDAzw="; - - let mut scram = ScramSha256::from_nonce(password, nonce); - - let message = scram.message(); - assert_eq!(std::str::from_utf8(&message).unwrap(), client_first); - - let result = scram - .update(&BytesMut::from(server_first.as_bytes())) - .unwrap(); - assert_eq!(std::str::from_utf8(&result).unwrap(), client_final); - - scram - .finish(&BytesMut::from(server_final.as_bytes())) - .unwrap(); - } -} diff --git a/src/server.rs b/src/server.rs deleted file mode 100644 index 882450e..0000000 --- a/src/server.rs +++ /dev/null @@ -1,1537 +0,0 @@ -/// 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 lru::LruCache; -use once_cell::sync::Lazy; -use parking_lot::{Mutex, RwLock}; -use postgres_protocol::message; -use std::collections::{HashMap, HashSet, VecDeque}; -use std::mem; -use std::net::IpAddr; -use std::num::NonZeroUsize; -use std::sync::Arc; -use std::time::SystemTime; -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::{get_config, Address, User}; -use crate::constants::*; -use crate::dns_cache::{AddrSet, CACHED_RESOLVER}; -use crate::errors::{Error, ServerIdentifier}; -use crate::messages::BytesMutReader; -use crate::messages::*; -use crate::mirrors::MirroringManager; -use crate::pool::ClientServerMap; -use crate::scram::ScramSha256; -use crate::stats::ServerStats; -use std::io::Write; - -use pin_project::pin_project; - -#[pin_project(project = SteamInnerProj)] -pub enum StreamInner { - Plain { - #[pin] - stream: TcpStream, - }, - Tls { - #[pin] - stream: TlsStream, - }, -} - -impl AsyncWrite for StreamInner { - fn poll_write( - self: std::pin::Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - buf: &[u8], - ) -> std::task::Poll> { - let this = self.project(); - match this { - SteamInnerProj::Tls { stream } => stream.poll_write(cx, buf), - SteamInnerProj::Plain { stream } => stream.poll_write(cx, buf), - } - } - - fn poll_flush( - self: std::pin::Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> std::task::Poll> { - let this = self.project(); - match this { - SteamInnerProj::Tls { stream } => stream.poll_flush(cx), - SteamInnerProj::Plain { stream } => stream.poll_flush(cx), - } - } - - fn poll_shutdown( - self: std::pin::Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> std::task::Poll> { - let this = self.project(); - match this { - SteamInnerProj::Tls { stream } => stream.poll_shutdown(cx), - SteamInnerProj::Plain { stream } => stream.poll_shutdown(cx), - } - } -} - -impl AsyncRead for StreamInner { - fn poll_read( - self: std::pin::Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - buf: &mut tokio::io::ReadBuf<'_>, - ) -> std::task::Poll> { - let this = self.project(); - match this { - SteamInnerProj::Tls { stream } => stream.poll_read(cx, buf), - SteamInnerProj::Plain { stream } => stream.poll_read(cx, buf), - } - } -} - -impl StreamInner { - pub fn try_write(&mut self, buf: &[u8]) -> std::io::Result { - match self { - StreamInner::Tls { stream } => { - let r = stream.get_mut(); - let mut w = r.1.writer(); - w.write(buf) - } - StreamInner::Plain { stream } => stream.try_write(buf), - } - } -} - -#[derive(Copy, Clone)] -struct CleanupState { - /// If server connection requires RESET ALL before checkin because of set statement - needs_cleanup_set: bool, - - /// If server connection requires DEALLOCATE 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 - ) - } -} - -static TRACKED_PARAMETERS: Lazy> = Lazy::new(|| { - let mut set = HashSet::new(); - set.insert("client_encoding".to_string()); - set.insert("DateStyle".to_string()); - set.insert("TimeZone".to_string()); - set.insert("standard_conforming_strings".to_string()); - set.insert("application_name".to_string()); - set -}); - -#[derive(Debug, Clone)] -pub struct ServerParameters { - parameters: HashMap, -} - -impl Default for ServerParameters { - fn default() -> Self { - Self::new() - } -} - -impl ServerParameters { - pub fn new() -> Self { - let mut server_parameters = ServerParameters { - parameters: HashMap::new(), - }; - - server_parameters.set_param("client_encoding".to_string(), "UTF8".to_string(), false); - server_parameters.set_param("DateStyle".to_string(), "ISO, MDY".to_string(), false); - server_parameters.set_param("TimeZone".to_string(), "Etc/UTC".to_string(), false); - server_parameters.set_param( - "standard_conforming_strings".to_string(), - "on".to_string(), - false, - ); - server_parameters.set_param("application_name".to_string(), "pgcat".to_string(), false); - - server_parameters - } - - /// returns true if a tracked parameter was set, false if it was a non-tracked parameter - /// if startup is false, then then only tracked parameters will be set - pub fn set_param(&mut self, mut key: String, value: String, startup: bool) { - // The startup parameter will send uncapitalized keys but parameter status packets will send capitalized keys - if key == "timezone" { - key = "TimeZone".to_string(); - } else if key == "datestyle" { - key = "DateStyle".to_string(); - }; - - if TRACKED_PARAMETERS.contains(&key) || startup { - self.parameters.insert(key, value); - } - } - - pub fn set_from_hashmap(&mut self, parameters: &HashMap, startup: bool) { - // iterate through each and call set_param - for (key, value) in parameters { - self.set_param(key.to_string(), value.to_string(), startup); - } - } - - // Gets the diff of the parameters - fn compare_params(&self, incoming_parameters: &ServerParameters) -> HashMap { - let mut diff = HashMap::new(); - - // iterate through tracked parameters - for key in TRACKED_PARAMETERS.iter() { - if let Some(incoming_value) = incoming_parameters.parameters.get(key) { - if let Some(value) = self.parameters.get(key) { - if value != incoming_value { - diff.insert(key.to_string(), incoming_value.to_string()); - } - } - } - } - - diff - } - - pub fn get_application_name(&self) -> &String { - // Can unwrap because we set it in the constructor - self.parameters.get("application_name").unwrap() - } - - fn add_parameter_message(key: &str, value: &str, buffer: &mut BytesMut) { - buffer.put_u8(b'S'); - - // 4 is len of i32, the plus for the null terminator - let len = 4 + key.len() + 1 + value.len() + 1; - - buffer.put_i32(len as i32); - - buffer.put_slice(key.as_bytes()); - buffer.put_u8(0); - buffer.put_slice(value.as_bytes()); - buffer.put_u8(0); - } -} - -impl From<&ServerParameters> for BytesMut { - fn from(server_parameters: &ServerParameters) -> Self { - let mut bytes = BytesMut::new(); - - for (key, value) in &server_parameters.parameters { - ServerParameters::add_parameter_message(key, value, &mut bytes); - } - - bytes - } -} - -// pub fn compare - -/// Server state. -pub struct Server { - /// Server host, e.g. localhost, - /// port, e.g. 5432, and role, e.g. primary or replica. - address: Address, - - /// Server TCP connection. - stream: BufStream, - - /// Our server response buffer. We buffer data before we give it to the client. - buffer: BytesMut, - - /// Server information the server sent us over on startup. - server_parameters: ServerParameters, - - /// Backend id and secret key used for query cancellation. - process_id: i32, - secret_key: i32, - - /// Is the server inside a transaction or idle. - in_transaction: bool, - - /// 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 reset statements before checkin - cleanup_state: CleanupState, - - /// Mapping of clients and servers used for query cancellation. - client_server_map: ClientServerMap, - - /// Server connected at. - connected_at: chrono::naive::NaiveDateTime, - - /// Reports various metrics, e.g. data sent & received. - stats: Arc, - - /// Application name using the server at the moment. - application_name: String, - - /// 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, - - /// Log client parameter status changes - log_client_parameter_status_changes: bool, - - /// Prepared statements - prepared_statement_cache: Option>, - - /// Prepared statement being currently registered on the server. - registering_prepared_statement: VecDeque, -} - -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. - #[allow(clippy::too_many_arguments)] - pub async fn startup( - address: &Address, - user: &User, - database: &str, - client_server_map: ClientServerMap, - stats: Arc, - auth_hash: Arc>>, - cleanup_connections: bool, - log_client_parameter_status_changes: bool, - prepared_statement_cache_size: usize, - ) -> 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, - Err(err) => { - error!("Could not connect to server: {}", err); - return Err(Error::SocketError(format!( - "Could not connect to server: {}", - err - ))); - } - }; - - // 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 }))); - } - } - } 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 - let username = match user.server_username { - Some(ref server_username) => server_username, - None => &user.username, - }; - - let password = match user.server_password.as_ref() { - Some(server_password) => Some(server_password), - None => user.password.as_ref(), - }; - - startup(&mut stream, username, database).await?; - - 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: Option = password.map(|password| ScramSha256::new(password)); - - let mut server_parameters = ServerParameters::new(); - - loop { - let code = match stream.read_u8().await { - Ok(code) => code as char, - Err(_) => { - return Err(Error::ServerStartupError( - "message code".into(), - server_identifier, - )) - } - }; - - let len = match stream.read_i32().await { - Ok(len) => len, - Err(_) => { - return Err(Error::ServerStartupError( - "message len".into(), - server_identifier, - )) - } - }; - - trace!("Message: {}", code); - - match code { - // Authentication - 'R' => { - // 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::ServerStartupError( - "auth code".into(), - server_identifier, - )) - } - }; - - trace!("Auth: {}", auth_code); - - match auth_code { - MD5_ENCRYPTED_PASSWORD => { - // The salt is 4 bytes. - // See: https://www.postgresql.org/docs/12/protocol-message-formats.html - let mut salt = vec![0u8; 4]; - - match stream.read_exact(&mut salt).await { - Ok(_) => (), - Err(_) => { - return Err(Error::ServerStartupError( - "salt".into(), - server_identifier, - )) - } - }; - - 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::ServerStartupError( - "sasl message".into(), - server_identifier, - )) - } - }; - - let sasl_type = String::from_utf8_lossy(&sasl_auth[..sasl_len - 2]); - - if sasl_type.contains(SCRAM_SHA_256) { - debug!("Using {}", SCRAM_SHA_256); - - // Generate client message. - let sasl_response = scram.as_mut().unwrap().message(); - - // SASLInitialResponse (F) - let mut res = BytesMut::new(); - res.put_u8(b'p'); - - // length + String length + length + length of sasl response - res.put_i32( - 4 // i32 size - + SCRAM_SHA_256.len() as i32 // length of SASL version string, - + 1 // Null terminator for the SASL version string, - + 4 // i32 size - + sasl_response.len() as i32, // length of SASL response - ); - - res.put_slice(format!("{}\0", SCRAM_SHA_256).as_bytes()); - res.put_i32(sasl_response.len() as i32); - res.put(sasl_response); - - write_all_flush(&mut stream, &res).await?; - } else { - error!("Unsupported SCRAM version: {}", sasl_type); - return Err(Error::ServerError); - } - } - - SASL_CONTINUE => { - trace!("Continuing SASL"); - - let mut sasl_data = vec![0u8; (len - 8) as usize]; - - match stream.read_exact(&mut sasl_data).await { - Ok(_) => (), - Err(_) => { - return Err(Error::ServerStartupError( - "sasl cont message".into(), - server_identifier, - )) - } - }; - - let msg = BytesMut::from(&sasl_data[..]); - let sasl_response = scram.as_mut().unwrap().update(&msg)?; - - // SASLResponse - let mut res = BytesMut::new(); - res.put_u8(b'p'); - res.put_i32(4 + sasl_response.len() as i32); - res.put(sasl_response); - - write_all_flush(&mut stream, &res).await?; - } - - SASL_FINAL => { - trace!("Final SASL"); - - let mut sasl_final = vec![0u8; len as usize - 8]; - match stream.read_exact(&mut sasl_final).await { - Ok(_) => (), - Err(_) => { - return Err(Error::ServerStartupError( - "sasl final message".into(), - server_identifier, - )) - } - }; - - match scram - .as_mut() - .unwrap() - .finish(&BytesMut::from(&sasl_final[..])) - { - Ok(_) => { - debug!("SASL authentication successful"); - } - - Err(err) => { - debug!("SASL authentication failed"); - return Err(err); - } - }; - } - - _ => { - error!("Unsupported authentication mechanism: {}", auth_code); - return Err(Error::ServerError); - } - } - } - - // ErrorResponse - 'E' => { - let error_code = match stream.read_u8().await { - Ok(error_code) => error_code, - Err(_) => { - return Err(Error::ServerStartupError( - "error code message".into(), - server_identifier, - )) - } - }; - - trace!("Error: {}", error_code); - - match error_code { - // No error message is present in the message. - MESSAGE_TERMINATOR => (), - - // An error message will be present. - _ => { - let mut error = vec![0u8; len as usize]; - - match stream.read_exact(&mut error).await { - Ok(_) => (), - Err(_) => { - return Err(Error::ServerStartupError( - "error message".into(), - server_identifier, - )) - } - }; - - let fields = match PgErrorMsg::parse(&error) { - Ok(f) => f, - Err(err) => { - return Err(err); - } - }; - trace!("error fields: {}", &fields); - error!("server error: {}: {}", fields.severity, fields.message); - } - }; - - return Err(Error::ServerError); - } - - // ParameterStatus - 'S' => { - let mut bytes = BytesMut::with_capacity(len as usize - 4); - bytes.resize(len as usize - mem::size_of::(), b'0'); - - match stream.read_exact(&mut bytes[..]).await { - Ok(_) => (), - Err(_) => { - return Err(Error::ServerStartupError( - "parameter status message".into(), - server_identifier, - )) - } - }; - - let key = bytes.read_string().unwrap(); - let value = bytes.read_string().unwrap(); - - // Save the parameter so we can pass it to the client later. - // These can be server_encoding, client_encoding, server timezone, Postgres version, - // and many more interesting things we should know about the Postgres server we are talking to. - server_parameters.set_param(key, value, true); - } - - // BackendKeyData - 'K' => { - // The frontend must save these values if it wishes to be able to issue CancelRequest messages later. - // See: . - process_id = match stream.read_i32().await { - Ok(id) => id, - 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::ServerStartupError( - "secret key message".into(), - server_identifier, - )) - } - }; - } - - // ReadyForQuery - 'Z' => { - let mut idle = vec![0u8; len as usize - 4]; - - match stream.read_exact(&mut idle).await { - Ok(_) => (), - Err(_) => { - return Err(Error::ServerStartupError( - "transaction status message".into(), - server_identifier, - )) - } - }; - - let server = Server { - address: address.clone(), - stream: BufStream::new(stream), - buffer: BytesMut::with_capacity(8196), - server_parameters, - process_id, - secret_key, - in_transaction: false, - in_copy_mode: false, - data_available: false, - bad: false, - cleanup_state: CleanupState::new(), - client_server_map, - addr_set, - connected_at: chrono::offset::Utc::now().naive_utc(), - stats, - application_name: "pgcat".to_string(), - last_activity: SystemTime::now(), - mirror_manager: match address.mirrors.len() { - 0 => None, - _ => Some(MirroringManager::from_addresses( - user.clone(), - database.to_owned(), - address.mirrors.clone(), - )), - }, - cleanup_connections, - log_client_parameter_status_changes, - prepared_statement_cache: match prepared_statement_cache_size { - 0 => None, - _ => Some(LruCache::new( - NonZeroUsize::new(prepared_statement_cache_size).unwrap(), - )), - }, - registering_prepared_statement: VecDeque::new(), - }; - - return Ok(server); - } - - // We have an unexpected message from the server during this exchange. - // Means we implemented the protocol wrong or we're not talking to a Postgres server. - _ => { - error!("Unknown code: {}", code); - return Err(Error::ProtocolSyncError(format!( - "Unknown server code: {}", - code - ))); - } - }; - } - } - - /// Issue a query cancellation request to the server. - /// Uses a separate connection that's not part of the connection pool. - pub async fn cancel( - host: &str, - port: u16, - process_id: i32, - secret_key: i32, - ) -> Result<(), Error> { - let mut stream = match TcpStream::connect(&format!("{}:{}", host, port)).await { - Ok(stream) => stream, - Err(err) => { - error!("Could not connect to server: {}", err); - return Err(Error::SocketError("Error reading cancel message".into())); - } - }; - configure_socket(&stream); - - debug!("Sending CancelRequest"); - - let mut bytes = BytesMut::with_capacity(16); - bytes.put_i32(16); - bytes.put_i32(CANCEL_REQUEST_CODE); - bytes.put_i32(process_id); - bytes.put_i32(secret_key); - - 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()); - - 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: {:?}", - self.address, err - ); - self.bad = true; - Err(err) - } - } - } - - /// Receive data from the server in response to a client request. - /// This method must be called multiple times while `self.is_data_available()` is true - /// in order to receive all data the server has to offer. - pub async fn recv( - &mut self, - mut client_server_parameters: Option<&mut ServerParameters>, - ) -> Result { - loop { - let mut message = match read_message(&mut self.stream).await { - Ok(message) => message, - Err(err) => { - error!( - "Terminating server {:?} because of: {:?}", - self.address, err - ); - self.bad = true; - return Err(err); - } - }; - - // Buffer the message we'll forward to the client later. - self.buffer.put(&message[..]); - - let code = message.get_u8() as char; - let _len = message.get_i32(); - - trace!("Message: {}", code); - - match code { - // ReadyForQuery - 'Z' => { - let transaction_state = message.get_u8() as char; - - match transaction_state { - // In transaction. - 'T' => { - self.in_transaction = true; - } - - // Idle, transaction over. - 'I' => { - self.in_transaction = false; - } - - // Some error occurred, the transaction was rolled back. - 'E' => { - self.in_transaction = true; - } - - // Something totally unexpected, this is not a Postgres server we know. - _ => { - self.bad = true; - return Err(Error::ProtocolSyncError(format!( - "Unknown transaction state: {}", - transaction_state - ))); - } - }; - - // There is no more data available from the server. - self.data_available = false; - break; - } - - // ErrorResponse - 'E' => { - if self.in_copy_mode { - self.in_copy_mode = false; - } - - // Remove the prepared statement from the cache, it has a syntax error or something else bad happened. - if let Some(prepared_stmt_name) = - self.registering_prepared_statement.pop_front() - { - if let Some(ref mut cache) = self.prepared_statement_cache { - if let Some(_removed) = cache.pop(&prepared_stmt_name) { - debug!( - "Removed {} from prepared statement cache", - prepared_stmt_name - ); - } else { - // Shouldn't happen. - debug!("Prepared statement {} was not cached", prepared_stmt_name); - } - } - } - - if self.prepared_statement_cache.is_some() { - let error_message = PgErrorMsg::parse(&message)?; - if error_message.message == "cached plan must not change result type" { - warn!("Server {:?} changed schema, dropping connection to clean up prepared statements", self.address); - // This will still result in an error to the client, but this server connection will drop all cached prepared statements - // so that any new queries will be re-prepared - // TODO: Other ideas to solve errors when there are DDL changes after a statement has been prepared - // - Recreate entire connection pool to force recreation of all server connections - // - Clear the ConnectionPool's statement cache so that new statement names are generated - // - Implement a retry (re-prepare) so the client doesn't see an error - self.cleanup_state.needs_cleanup_prepare = true; - } - } - } - - // CommandComplete - 'C' => { - if self.in_copy_mode { - self.in_copy_mode = false; - } - - match message.read_string() { - Ok(command) => { - // Non-exhaustive list of commands that are likely to change session variables/resources - // which can leak between clients. This is a best effort to block bad clients - // from poisoning a transaction-mode pool by setting inappropriate session variables - match command.as_str() { - "SET" => { - // We don't detect set statements in transactions - // No great way to differentiate between set and set local - // As a result, we will miss cases when set statements are used in transactions - // This will reduce amount of reset statements sent - if !self.in_transaction { - debug!("Server connection marked for clean up"); - self.cleanup_state.needs_cleanup_set = true; - } - } - - "PREPARE" => { - debug!("Server connection marked for clean up"); - self.cleanup_state.needs_cleanup_prepare = true; - } - _ => (), - } - } - - Err(err) => { - warn!("Encountered an error while parsing CommandTag {}", err); - } - } - } - - 'S' => { - let key = message.read_string().unwrap(); - let value = message.read_string().unwrap(); - - if let Some(client_server_parameters) = client_server_parameters.as_mut() { - client_server_parameters.set_param(key.clone(), value.clone(), false); - if self.log_client_parameter_status_changes { - info!("Client parameter status change: {} = {}", key, value) - } - } - - self.server_parameters.set_param(key, value, false); - } - - // DataRow - 'D' => { - // More data is available after this message, this is not the end of the reply. - self.data_available = true; - - // Don't flush yet, the more we buffer, the faster this goes...up to a limit. - if self.buffer.len() >= 8196 { - break; - } - } - - // CopyInResponse: copy is starting from client to server. - '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; - } - - // CopyData - 'd' => { - // Don't flush yet, buffer until we reach limit - if self.buffer.len() >= 8196 { - break; - } - } - - // CopyDone - // Buffer until ReadyForQuery shows up, so don't exit the loop yet. - 'c' => (), - - // Parse complete successfully - '1' => { - self.registering_prepared_statement.pop_front(); - } - - // Anything else, e.g. errors, notices, etc. - // Keep buffering until ReadyForQuery shows up. - _ => (), - }; - } - - let bytes = self.buffer.clone(); - - // Keep track of how much data we got from the server for stats. - self.stats().data_received(bytes.len()); - - // Clear the buffer for next query. - self.buffer.clear(); - - // Successfully received data from server - self.last_activity = SystemTime::now(); - - // Pass the data back to the client. - Ok(bytes) - } - - // Determines if the server already has a prepared statement with the given name - // Increments the prepared statement cache hit counter - pub fn has_prepared_statement(&mut self, name: &str) -> bool { - let cache = match &mut self.prepared_statement_cache { - Some(cache) => cache, - None => return false, - }; - - let has_it = cache.get(name).is_some(); - if has_it { - self.stats.prepared_cache_hit(); - } else { - self.stats.prepared_cache_miss(); - } - - has_it - } - - fn add_prepared_statement_to_cache(&mut self, name: &str) -> Option { - let cache = match &mut self.prepared_statement_cache { - Some(cache) => cache, - None => return None, - }; - - self.stats.prepared_cache_add(); - - // If we evict something, we need to close it on the server - if let Some((evicted_name, _)) = cache.push(name.to_string(), ()) { - if evicted_name != name { - debug!( - "Evicted prepared statement {} from cache, replaced with {}", - evicted_name, name - ); - return Some(evicted_name); - } - }; - - None - } - - fn remove_prepared_statement_from_cache(&mut self, name: &str) { - let cache = match &mut self.prepared_statement_cache { - Some(cache) => cache, - None => return, - }; - - self.stats.prepared_cache_remove(); - cache.pop(name); - } - - pub async fn register_prepared_statement( - &mut self, - parse: &Parse, - should_send_parse_to_server: bool, - ) -> Result<(), Error> { - if !self.has_prepared_statement(&parse.name) { - self.registering_prepared_statement - .push_back(parse.name.clone()); - - let mut bytes = BytesMut::new(); - - if should_send_parse_to_server { - let parse_bytes: BytesMut = parse.try_into()?; - bytes.extend_from_slice(&parse_bytes); - } - - // If we evict something, we need to close it on the server - // We do this by adding it to the messages we're sending to the server before the sync - if let Some(evicted_name) = self.add_prepared_statement_to_cache(&parse.name) { - self.remove_prepared_statement_from_cache(&evicted_name); - let close_bytes: BytesMut = Close::new(&evicted_name).try_into()?; - bytes.extend_from_slice(&close_bytes); - }; - - // If we have a parse or close we need to send to the server, send them and sync - if !bytes.is_empty() { - bytes.extend_from_slice(&sync()); - - self.send(&bytes).await?; - - loop { - self.recv(None).await?; - - if !self.is_data_available() { - break; - } - } - } - }; - - // If it's not there, something went bad, I'm guessing bad syntax or permissions error - // on the server. - if !self.has_prepared_statement(&parse.name) { - Err(Error::PreparedStatementError) - } else { - 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 { - debug!("Server in transaction: {}", self.in_transaction); - self.in_transaction - } - - /// Currently copying data from client to server or vice-versa. - 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 { - self.data_available - } - - /// Server & client are out of sync, we must discard this connection. - /// This happens with clients that misbehave. - pub fn is_bad(&self) -> bool { - 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. - pub fn server_parameters(&self) -> ServerParameters { - self.server_parameters.clone() - } - - pub async fn sync_parameters(&mut self, parameters: &ServerParameters) -> Result<(), Error> { - let parameter_diff = self.server_parameters.compare_params(parameters); - - if parameter_diff.is_empty() { - return Ok(()); - } - - let mut query = String::from(""); - - for (key, value) in parameter_diff { - query.push_str(&format!("SET {} TO '{}';", key, value)); - } - - let res = self.query(&query).await; - - self.cleanup_state.reset(); - - res - } - - /// Indicate that this server connection cannot be re-used and must be discarded. - pub fn mark_bad(&mut self, reason: &str) { - error!("Server {:?} marked bad, reason: {}", self.address, reason); - self.bad = true; - } - - /// Claim this server as mine for the purposes of query cancellation. - pub fn claim(&mut self, process_id: i32, secret_key: i32) { - let mut guard = self.client_server_map.lock(); - guard.insert( - (process_id, secret_key), - ( - self.process_id, - self.secret_key, - self.address.host.clone(), - self.address.port, - ), - ); - } - - /// Execute an arbitrary query against the 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?; - - loop { - let _ = self.recv(None).await?; - - if !self.data_available { - break; - } - } - - Ok(()) - } - - /// Perform any necessary cleanup before putting the server - /// connection back in the pool - pub async fn checkin_cleanup(&mut self) -> Result<(), Error> { - // Client disconnected with an open transaction on the server connection. - // Pgbouncer behavior is to close the server connection but that can cause - // server connection thrashing if clients repeatedly do this. - // Instead, we ROLLBACK that transaction before putting the connection back in the pool - if self.in_transaction() { - warn!(target: "pgcat::server::cleanup", "Server returned while still in transaction, rolling back transaction"); - self.query("ROLLBACK").await?; - } - - // Client disconnected but it performed session-altering operations such as - // SET statement_timeout to 1 or create a prepared statement. We clear that - // to avoid leaking state between clients. For performance reasons we only - // send `RESET ALL` if we think the session is altered instead of just sending - // it before each checkin. - if self.cleanup_state.needs_cleanup() && self.cleanup_connections { - info!(target: "pgcat::server::cleanup", "Server returned with session state altered, discarding state ({}) for application {}", self.cleanup_state, self.application_name); - let mut reset_string = String::from("RESET ROLE;"); - - if self.cleanup_state.needs_cleanup_set { - reset_string.push_str("RESET ALL;"); - }; - - if self.cleanup_state.needs_cleanup_prepare { - reset_string.push_str("DEALLOCATE ALL;"); - // Since we deallocated all prepared statements, we need to clear the cache - if let Some(cache) = &mut self.prepared_statement_cache { - cache.clear(); - } - }; - - self.query(&reset_string).await?; - self.cleanup_state.reset(); - } - - if self.in_copy_mode() { - warn!(target: "pgcat::server::cleanup", "Server returned while still in copy-mode"); - } - - 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 server's latest response timestamp - pub fn last_activity(&self) -> SystemTime { - self.last_activity - } - - // Marks a connection as needing cleanup at checkin - pub fn mark_dirty(&mut self) { - self.cleanup_state.set_true(); - } - - pub fn mirror_send(&mut self, bytes: &BytesMut) { - if let Some(manager) = self.mirror_manager.as_mut() { - manager.send(bytes) - } - } - - pub fn mirror_disconnect(&mut self) { - if let Some(manager) = self.mirror_manager.as_mut() { - manager.disconnect() - } - } - - // 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, - false, - 0, - ) - .await?; - debug!("Connected!, sending query."); - server.send(&simple_query(query)).await?; - let mut message = server.recv(None).await?; - - 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 { - /// Try to do a clean shut down. Best effort because - /// the socket is in non-blocking mode, so it may not be ready - /// for a write. - fn drop(&mut self) { - self.mirror_disconnect(); - - // Update statistics - self.stats.disconnect(); - - let mut bytes = BytesMut::with_capacity(5); - bytes.put_u8(b'X'); - bytes.put_i32(4); - - match self.stream.get_mut().try_write(&bytes) { - Ok(5) => (), - _ => debug!("Dirty shutdown"), - }; - - 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!( - "{} {:?}, session duration: {}", - message, - self.address, - crate::format_duration(&duration) - ); - } -} diff --git a/src/sharding.rs b/src/sharding.rs deleted file mode 100644 index 990f967..0000000 --- a/src/sharding.rs +++ /dev/null @@ -1,216 +0,0 @@ -use serde_derive::{Deserialize, Serialize}; -/// Implements various sharding functions. -use sha1::{Digest, Sha1}; - -/// See: . -const PARTITION_HASH_SEED: u64 = 0x7A5B22367996DCFD; - -/// The sharding functions we support. -#[derive(Debug, PartialEq, Copy, Clone, Serialize, Deserialize, Hash, std::cmp::Eq)] -pub enum ShardingFunction { - #[serde(alias = "pg_bigint_hash", alias = "PgBigintHash")] - PgBigintHash, - #[serde(alias = "sha1", alias = "Sha1")] - Sha1, -} - -impl std::fmt::Display for ShardingFunction { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ShardingFunction::PgBigintHash => write!(f, "pg_bigint_hash"), - ShardingFunction::Sha1 => write!(f, "sha1"), - } - } -} - -/// The sharder. -pub struct Sharder { - /// Number of shards in the cluster. - shards: usize, - - /// The sharding function in use. - sharding_function: ShardingFunction, -} - -impl Sharder { - /// Create new instance of the sharder. - pub fn new(shards: usize, sharding_function: ShardingFunction) -> Sharder { - Sharder { - shards, - sharding_function, - } - } - - /// Compute the shard given sharding key. - pub fn shard(&self, key: i64) -> usize { - match self.sharding_function { - ShardingFunction::PgBigintHash => self.pg_bigint_hash(key), - ShardingFunction::Sha1 => self.sha1(key), - } - } - - /// Hash function used by Postgres to determine which partition - /// to put the row in when using HASH(column) partitioning. - /// Source: . - /// Supports only 1 bigint at the moment, but we can add more later. - fn pg_bigint_hash(&self, key: i64) -> usize { - let mut lohalf = key as u32; - let hihalf = (key >> 32) as u32; - lohalf ^= if key >= 0 { hihalf } else { !hihalf }; - Self::combine(0, Self::pg_u32_hash(lohalf)) as usize % self.shards - } - - /// Example of a hashing function based on SHA1. - fn sha1(&self, key: i64) -> usize { - let mut hasher = Sha1::new(); - - hasher.update(key.to_string().as_bytes()); - - let result = hasher.finalize(); - - // Convert the SHA1 hash into hex so we can parse it as a large integer. - let hex = format!("{:x}", result); - - // Parse the last 8 bytes as an integer (8 bytes = bigint). - let key = i64::from_str_radix(&hex[hex.len() - 8..], 16).unwrap() as usize; - - key % self.shards - } - - #[inline] - fn rot(x: u32, k: u32) -> u32 { - (x << k) | (x >> (32 - k)) - } - - #[inline] - fn mix(mut a: u32, mut b: u32, mut c: u32) -> (u32, u32, u32) { - a = a.wrapping_sub(c); - a ^= Self::rot(c, 4); - c = c.wrapping_add(b); - - b = b.wrapping_sub(a); - b ^= Self::rot(a, 6); - a = a.wrapping_add(c); - - c = c.wrapping_sub(b); - c ^= Self::rot(b, 8); - b = b.wrapping_add(a); - - a = a.wrapping_sub(c); - a ^= Self::rot(c, 16); - c = c.wrapping_add(b); - - b = b.wrapping_sub(a); - b ^= Self::rot(a, 19); - a = a.wrapping_add(c); - - c = c.wrapping_sub(b); - c ^= Self::rot(b, 4); - b = b.wrapping_add(a); - - (a, b, c) - } - - #[inline] - fn _final(mut a: u32, mut b: u32, mut c: u32) -> (u32, u32, u32) { - c ^= b; - c = c.wrapping_sub(Self::rot(b, 14)); - a ^= c; - a = a.wrapping_sub(Self::rot(c, 11)); - b ^= a; - b = b.wrapping_sub(Self::rot(a, 25)); - c ^= b; - c = c.wrapping_sub(Self::rot(b, 16)); - a ^= c; - a = a.wrapping_sub(Self::rot(c, 4)); - b ^= a; - b = b.wrapping_sub(Self::rot(a, 14)); - c ^= b; - c = c.wrapping_sub(Self::rot(b, 24)); - (a, b, c) - } - - #[inline] - fn combine(mut a: u64, b: u64) -> u64 { - a ^= b - .wrapping_add(0x49a0f4dd15e5a8e3_u64) - .wrapping_add(a << 54) - .wrapping_add(a >> 7); - a - } - - #[inline] - fn pg_u32_hash(k: u32) -> u64 { - let mut a: u32 = 0x9e3779b9_u32 + std::mem::size_of::() as u32 + 3923095_u32; - let mut b = a; - let c = a; - - a = a.wrapping_add((PARTITION_HASH_SEED >> 32) as u32); - b = b.wrapping_add(PARTITION_HASH_SEED as u32); - let (mut a, b, c) = Self::mix(a, b, c); - - a = a.wrapping_add(k); - - let (_a, b, c) = Self::_final(a, b, c); - - ((b as u64) << 32) | (c as u64) - } -} - -#[cfg(test)] -mod test { - use super::*; - - // See tests/sharding/partition_hash_test_setup.sql - // The output of those SELECT statements will match this test, - // confirming that we implemented Postgres BIGINT hashing correctly. - #[test] - fn test_pg_bigint_hash() { - let sharder = Sharder::new(5, ShardingFunction::PgBigintHash); - - let shard_0 = vec![1, 4, 5, 14, 19, 39, 40, 46, 47, 53]; - - for v in shard_0 { - assert_eq!(sharder.shard(v), 0); - } - - let shard_1 = vec![2, 3, 11, 17, 21, 23, 30, 49, 51, 54]; - - for v in shard_1 { - assert_eq!(sharder.shard(v), 1); - } - - let shard_2 = vec![6, 7, 15, 16, 18, 20, 25, 28, 34, 35]; - - for v in shard_2 { - assert_eq!(sharder.shard(v), 2); - } - - let shard_3 = vec![8, 12, 13, 22, 29, 31, 33, 36, 41, 43]; - - for v in shard_3 { - assert_eq!(sharder.shard(v), 3); - } - - let shard_4 = vec![9, 10, 24, 26, 27, 32, 37, 38, 42, 45]; - - for v in shard_4 { - assert_eq!(sharder.shard(v), 4); - } - } - - #[test] - fn test_sha1_hash() { - let sharder = Sharder::new(12, ShardingFunction::Sha1); - let ids = [ - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, - ]; - let shards = [ - 4, 7, 8, 3, 6, 0, 0, 10, 3, 11, 1, 7, 4, 4, 11, 2, 5, 0, 8, 3, - ]; - - for (i, id) in ids.iter().enumerate() { - assert_eq!(sharder.shard(*id), shards[i]); - } - } -} diff --git a/src/stats.rs b/src/stats.rs deleted file mode 100644 index 29c00d6..0000000 --- a/src/stats.rs +++ /dev/null @@ -1,130 +0,0 @@ -/// Statistics and reporting. -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; - -// 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>; - -/// Stats for individual client connections -/// Used in SHOW CLIENTS. -static CLIENT_STATS: Lazy>> = - Lazy::new(|| Arc::new(RwLock::new(ClientStatesLookup::default()))); - -/// Stats for individual server connections -/// Used in SHOW SERVERS. -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. client stats, server stats, connection pool stats. -pub static REPORTER: Lazy> = - Lazy::new(|| ArcSwap::from_pointee(Reporter::default())); - -/// Statistics period used for average calculations. -/// 15 seconds. -static STAT_PERIOD: u64 = 15000; - -/// The statistics reporter. An instance is given -/// to each possible source of statistics, -/// e.g. clients, servers, connection pool. -#[derive(Clone, Debug, Default)] -pub struct Reporter {} - -impl Reporter { - /// 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 - 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; - } - - CLIENT_STATS.write().insert(client_id, stats); - } - - /// 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 - fn server_register(&self, server_id: i32, stats: Arc) { - SERVER_STATS.write().insert(server_id, stats); - } - /// 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 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 { - /// 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"); - - tokio::task::spawn(async move { - let mut interval = - tokio::time::interval(tokio::time::Duration::from_millis(STAT_PERIOD)); - loop { - interval.tick().await; - - // Hold read lock for duration of update to retain all server stats - let server_stats = SERVER_STATS.read(); - - for stats in server_stats.values() { - if !stats.check_address_stat_average_is_updated_status() { - stats.address_stats().update_averages(); - stats.address_stats().reset_current_counts(); - stats.set_address_stat_average_is_updated_status(true); - } - } - - // Reset to false for next update - for stats in server_stats.values() { - stats.set_address_stat_average_is_updated_status(false); - } - } - }); - } -} - -/// Get a snapshot of client statistics. -/// by the `Collector`. -pub fn get_client_stats() -> ClientStatesLookup { - CLIENT_STATS.read().clone() -} - -/// Get a snapshot of server statistics. -/// by the `Collector`. -pub fn get_server_stats() -> ServerStatesLookup { - SERVER_STATS.read().clone() -} - -/// Get the statistics reporter used to update stats across the pools/clients. -pub fn get_reporter() -> Reporter { - (*(*REPORTER.load())).clone() -} diff --git a/src/stats/address.rs b/src/stats/address.rs deleted file mode 100644 index a048644..0000000 --- a/src/stats/address.rs +++ /dev/null @@ -1,226 +0,0 @@ -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 deleted file mode 100644 index bd59a2f..0000000 --- a/src/stats/client.rs +++ /dev/null @@ -1,204 +0,0 @@ -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, - - // Time when the client started waiting for a connection from pool, measures in microseconds - // We use connect_time as the reference point for this value - // U64 can represent ~5850 centuries in microseconds, so we should be fine - pub wait_start_us: 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)), - wait_start_us: 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) { - let wait_start = self.connect_time.elapsed().as_micros() as u64; - - self.wait_start_us.store(wait_start, Ordering::Relaxed); - self.state.store(ClientState::Waiting, Ordering::Relaxed); - } - - /// Reports a client is done waiting for a connection and is about to query the server. - pub fn active(&self) { - self.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); - self.update_wait_times(); - } - - /// Reports a client has succeeded in obtaining a connection from a connection pool - pub fn checkout_success(&self) { - self.state.store(ClientState::Active, Ordering::Relaxed); - self.update_wait_times(); - } - - /// 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); - } - - fn update_wait_times(&self) { - if self.wait_start_us.load(Ordering::Relaxed) == 0 { - return; - } - - let wait_time_us = self.get_current_wait_time_us(); - self.total_wait_time - .fetch_add(wait_time_us, Ordering::Relaxed); - self.max_wait_time - .fetch_max(wait_time_us, Ordering::Relaxed); - self.wait_start_us.store(0, Ordering::Relaxed); - } - - pub fn get_current_wait_time_us(&self) -> u64 { - let wait_start_us = self.wait_start_us.load(Ordering::Relaxed); - let microseconds_since_connection_epoch = self.connect_time.elapsed().as_micros() as u64; - if wait_start_us == 0 || microseconds_since_connection_epoch < wait_start_us { - return 0; - } - microseconds_since_connection_epoch - wait_start_us - } - - /// 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 deleted file mode 100644 index b5c6ff5..0000000 --- a/src/stats/pool.rs +++ /dev/null @@ -1,154 +0,0 @@ -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 wait_start_us = client.wait_start_us.load(Ordering::Relaxed); - if wait_start_us > 0 { - let wait_time_us = client.get_current_wait_time_us(); - pool_stats.maxwait = std::cmp::max(pool_stats.maxwait, wait_time_us); - } - } - 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"), - } - } - - map - } - - pub fn generate_header() -> Vec<(&'static str, DataType)> { - 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 { - 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 deleted file mode 100644 index 5d25599..0000000 --- a/src/stats/server.rs +++ /dev/null @@ -1,229 +0,0 @@ -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_eviction_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_eviction_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_eviction_count.fetch_add(1, Ordering::Relaxed); - self.prepared_cache_size.fetch_sub(1, Ordering::Relaxed); - } -} diff --git a/src/tls.rs b/src/tls.rs deleted file mode 100644 index 6c4a7f5..0000000 --- a/src/tls.rs +++ /dev/null @@ -1,87 +0,0 @@ -// Stream wrapper. - -use rustls_pemfile::{certs, read_one, Item}; -use std::iter; -use std::path::Path; -use std::sync::Arc; -use std::time::SystemTime; -use tokio_rustls::rustls::{ - self, - client::{ServerCertVerified, ServerCertVerifier}, - Certificate, PrivateKey, ServerName, -}; -use tokio_rustls::TlsAcceptor; - -use crate::config::get_config; -use crate::errors::Error; - -// TLS -pub fn load_certs(path: &Path) -> std::io::Result> { - certs(&mut std::io::BufReader::new(std::fs::File::open(path)?)) - .map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidInput, "invalid cert")) - .map(|mut certs| certs.drain(..).map(Certificate).collect()) -} - -pub fn load_keys(path: &Path) -> std::io::Result> { - let mut rd = std::io::BufReader::new(std::fs::File::open(path)?); - - iter::from_fn(|| read_one(&mut rd).transpose()) - .filter_map(|item| match item { - Err(err) => Some(Err(err)), - Ok(Item::RSAKey(key)) => Some(Ok(PrivateKey(key))), - Ok(Item::ECKey(key)) => Some(Ok(PrivateKey(key))), - Ok(Item::PKCS8Key(key)) => Some(Ok(PrivateKey(key))), - _ => None, - }) - .collect() -} - -pub struct Tls { - pub acceptor: TlsAcceptor, -} - -impl Tls { - pub fn new() -> Result { - let config = get_config(); - - let certs = match load_certs(Path::new(&config.general.tls_certificate.unwrap())) { - Ok(certs) => certs, - Err(_) => return Err(Error::TlsError), - }; - - let mut keys = match load_keys(Path::new(&config.general.tls_private_key.unwrap())) { - Ok(keys) => keys, - Err(_) => return Err(Error::TlsError), - }; - - let config = match rustls::ServerConfig::builder() - .with_safe_defaults() - .with_no_client_auth() - .with_single_cert(certs, keys.remove(0)) - .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidInput, err)) - { - Ok(c) => c, - Err(_) => return Err(Error::TlsError), - }; - - Ok(Tls { - acceptor: TlsAcceptor::from(Arc::new(config)), - }) - } -} - -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/start_test_env.sh b/start_test_env.sh deleted file mode 100755 index c7ec4b1..0000000 --- a/start_test_env.sh +++ /dev/null @@ -1,34 +0,0 @@ -GREEN="\033[0;32m" -RED="\033[0;31m" -BLUE="\033[0;34m" -RESET="\033[0m" - - -cd tests/docker/ -docker compose kill main || true -docker compose build main -docker compose down -docker compose up -d -# wait for the container to start -while ! docker compose exec main ls; do - echo "Waiting for test environment to start" - sleep 1 -done -echo "===================================" -docker compose exec -e LOG_LEVEL=error -d main toxiproxy-server -docker compose exec --workdir /app main cargo build -docker compose exec -d --workdir /app main ./target/debug/pgcat ./.circleci/pgcat.toml -docker compose exec --workdir /app/tests/ruby main bundle install -docker compose exec --workdir /app/tests/python main pip3 install -r requirements.txt -echo "Interactive test environment ready" -echo "To run integration tests, you can use the following commands:" -echo -e " ${BLUE}Ruby: ${RED}cd /app/tests/ruby && bundle exec ruby tests.rb --format documentation${RESET}" -echo -e " ${BLUE}Python: ${RED}cd /app/ && pytest ${RESET}" -echo -e " ${BLUE}Rust: ${RED}cd /app/tests/rust && cargo run ${RESET}" -echo -e " ${BLUE}Go: ${RED}cd /app/tests/go && /usr/local/go/bin/go test${RESET}" -echo "the source code for tests are directly linked to the source code in the container so you can modify the code and run the tests again" -echo "You can rebuild PgCat from within the container by running" -echo -e " ${GREEN}cargo build${RESET}" -echo "and then run the tests again" -echo "===================================" -docker compose exec --workdir /app/tests main bash diff --git a/tests/docker/Dockerfile b/tests/docker/Dockerfile deleted file mode 100644 index 6eccddd..0000000 --- a/tests/docker/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -FROM rust:bullseye - -COPY --from=sclevine/yj /bin/yj /bin/yj -RUN /bin/yj -h -RUN apt-get update && apt-get install llvm-11 psmisc postgresql-contrib postgresql-client ruby ruby-dev libpq-dev python3 python3-pip lcov curl sudo iproute2 -y -RUN cargo install cargo-binutils rustfilt -RUN rustup component add llvm-tools-preview -RUN sudo gem install bundler -RUN wget -O toxiproxy-2.4.0.deb https://github.com/Shopify/toxiproxy/releases/download/v2.4.0/toxiproxy_2.4.0_linux_$(dpkg --print-architecture).deb && \ - sudo dpkg -i toxiproxy-2.4.0.deb -RUN wget -O go1.21.3.linux-$(dpkg --print-architecture).tar.gz https://go.dev/dl/go1.21.3.linux-$(dpkg --print-architecture).tar.gz && \ - sudo tar -C /usr/local -xzf go1.21.3.linux-$(dpkg --print-architecture).tar.gz && \ - rm go1.21.3.linux-$(dpkg --print-architecture).tar.gz diff --git a/tests/docker/docker-compose.yml b/tests/docker/docker-compose.yml deleted file mode 100644 index 0e174d8..0000000 --- a/tests/docker/docker-compose.yml +++ /dev/null @@ -1,54 +0,0 @@ -services: - pg1: - 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", "-p", "5432", "-c", "shared_preload_libraries=pg_stat_statements", "-c", "pg_stat_statements.track=all", "-c", "pg_stat_statements.max=100000"] - pg2: - image: postgres:14 - network_mode: "service:main" - environment: - POSTGRES_USER: postgres - POSTGRES_DB: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_INITDB_ARGS: --auth-local=scram-sha-256 --auth-host=scram-sha-256 --auth=scram-sha-256 - command: ["postgres", "-p", "7432", "-c", "shared_preload_libraries=pg_stat_statements", "-c", "pg_stat_statements.track=all", "-c", "pg_stat_statements.max=100000"] - pg3: - image: postgres:14 - network_mode: "service:main" - environment: - POSTGRES_USER: postgres - POSTGRES_DB: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_INITDB_ARGS: --auth-local=scram-sha-256 --auth-host=scram-sha-256 --auth=scram-sha-256 - command: ["postgres", "-p", "8432", "-c", "shared_preload_libraries=pg_stat_statements", "-c", "pg_stat_statements.track=all", "-c", "pg_stat_statements.max=100000"] - pg4: - image: postgres:14 - network_mode: "service:main" - environment: - POSTGRES_USER: postgres - POSTGRES_DB: postgres - 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"] - environment: - - INTERACTIVE_TEST_ENVIRONMENT=true - volumes: - - ../../:/app/ - - /app/target/ diff --git a/tests/docker/run.sh b/tests/docker/run.sh deleted file mode 100644 index 07452cb..0000000 --- a/tests/docker/run.sh +++ /dev/null @@ -1,69 +0,0 @@ -#!/bin/bash - -rm -rf /app/target/ || true -rm /app/*.profraw || true -rm /app/pgcat.profdata || true -rm -rf /app/cov || true - -# Prepares the interactive test environment -# -if [ -n "$INTERACTIVE_TEST_ENVIRONMENT" ]; then - ports=(5432 7432 8432 9432 10432) - for port in "${ports[@]}"; do - is_it_up=0 - attempts=0 - while [ $is_it_up -eq 0 ]; do - PGPASSWORD=postgres psql -h 127.0.0.1 -p $port -U postgres -c '\q' > /dev/null 2>&1 - if [ $? -eq 0 ]; then - echo "PostgreSQL on port $port is up." - is_it_up=1 - else - attempts=$((attempts+1)) - if [ $attempts -gt 10 ]; then - echo "PostgreSQL on port $port is down, giving up." - exit 1 - fi - echo "PostgreSQL on port $port is down, waiting for it to start." - sleep 1 - fi - done - done - PGPASSWORD=postgres psql -e -h 127.0.0.1 -p 5432 -U postgres -f /app/tests/sharding/query_routing_setup.sql - PGPASSWORD=postgres psql -e -h 127.0.0.1 -p 7432 -U postgres -f /app/tests/sharding/query_routing_setup.sql - PGPASSWORD=postgres psql -e -h 127.0.0.1 -p 8432 -U postgres -f /app/tests/sharding/query_routing_setup.sql - PGPASSWORD=postgres psql -e -h 127.0.0.1 -p 9432 -U postgres -f /app/tests/sharding/query_routing_setup.sql - PGPASSWORD=postgres psql -e -h 127.0.0.1 -p 10432 -U postgres -f /app/tests/sharding/query_routing_setup.sql - sleep 100000000000000000 - exit 0 -fi - -export LLVM_PROFILE_FILE="/app/pgcat-%m-%p.profraw" -export RUSTC_BOOTSTRAP=1 -export CARGO_INCREMENTAL=0 -export RUSTFLAGS="-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort -Cinstrument-coverage" -export RUSTDOCFLAGS="-Cpanic=abort" - -cd /app/ -cargo clean -cargo build -cargo test --tests - -bash .circleci/run_tests.sh - -TEST_OBJECTS=$( \ - for file in $(cargo test --no-run 2>&1 | grep "target/debug/deps/pgcat-[[:alnum:]]\+" -o); \ - do \ - printf "%s %s " --object $file; \ - done \ -) - -echo "Generating coverage report" - -rust-profdata merge -sparse /app/pgcat-*.profraw -o /app/pgcat.profdata - -bash -c "rust-cov export -ignore-filename-regex='rustc|registry' -Xdemangler=rustfilt -instr-profile=/app/pgcat.profdata $TEST_OBJECTS --object ./target/debug/pgcat --format lcov > ./lcov.info" - -genhtml lcov.info --title "PgCat Code Coverage" --css-file ./cov-style.css --highlight --no-function-coverage --ignore-errors source --legend --output-directory cov --prefix $(pwd) - -rm /app/*.profraw -rm /app/pgcat.profdata diff --git a/tests/go/go.mod b/tests/go/go.mod deleted file mode 100644 index faa2292..0000000 --- a/tests/go/go.mod +++ /dev/null @@ -1,5 +0,0 @@ -module pgcat - -go 1.21 - -require github.com/lib/pq v1.10.9 diff --git a/tests/go/go.sum b/tests/go/go.sum deleted file mode 100644 index aeddeae..0000000 --- a/tests/go/go.sum +++ /dev/null @@ -1,2 +0,0 @@ -github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= -github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= diff --git a/tests/go/pgcat.toml b/tests/go/pgcat.toml deleted file mode 100644 index 72eba8a..0000000 --- a/tests/go/pgcat.toml +++ /dev/null @@ -1,162 +0,0 @@ -# -# PgCat config example. -# - -# -# General pooler settings -[general] -# What IP to run on, 0.0.0.0 means accessible from everywhere. -host = "0.0.0.0" - -# Port to run on, same as PgBouncer used in this example. -port = "${PORT}" - -# Whether to enable prometheus exporter or not. -enable_prometheus_exporter = true - -# Port at which prometheus exporter listens on. -prometheus_exporter_port = 9930 - -# How long to wait before aborting a server connection (ms). -connect_timeout = 1000 - -# How much time to give the health check query to return with a result (ms). -healthcheck_timeout = 1000 - -# How long to keep connection available for immediate re-use, without running a healthcheck query on it -healthcheck_delay = 30000 - -# How much time to give clients during shutdown before forcibly killing client connections (ms). -shutdown_timeout = 5000 - -# For how long to ban a server if it fails a health check (seconds). -ban_time = 60 # Seconds - -# If we should log client connections -log_client_connections = false - -# If we should log client disconnections -log_client_disconnections = false - -# Reload config automatically if it changes. -autoreload = 15000 - -server_round_robin = false - -# TLS -tls_certificate = "../../.circleci/server.cert" -tls_private_key = "../../.circleci/server.key" - -# Credentials to access the virtual administrative database (pgbouncer or pgcat) -# Connecting to that database allows running commands like `SHOW POOLS`, `SHOW DATABASES`, etc.. -admin_username = "admin_user" -admin_password = "admin_pass" - -# pool -# configs are structured as pool. -# the pool_name is what clients use as database name when connecting -# For the example below a client can connect using "postgres://sharding_user:sharding_user@pgcat_host:pgcat_port/sharded_db" -[pools.sharded_db] -# Pool mode (see PgBouncer docs for more). -# session: one server connection per connected client -# transaction: one server connection per client transaction -pool_mode = "transaction" - -# If the client doesn't specify, route traffic to -# this role by default. -# -# any: round-robin between primary and replicas, -# replica: round-robin between replicas only without touching the primary, -# primary: all queries go to the primary unless otherwise specified. -default_role = "any" - -# Query parser. If enabled, we'll attempt to parse -# every incoming query to determine if it's a read or a write. -# If it's a read query, we'll direct it to a replica. Otherwise, if it's a write, -# we'll direct it to the primary. -query_parser_enabled = true - -# If the query parser is enabled and this setting is enabled, we'll attempt to -# infer the role from the query itself. -query_parser_read_write_splitting = 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. -primary_reads_enabled = true - -# So what if you wanted to implement a different hashing function, -# or you've already built one and you want this pooler to use it? -# -# Current options: -# -# pg_bigint_hash: PARTITION BY HASH (Postgres hashing function) -# sha1: A hashing function based on SHA1 -# -sharding_function = "pg_bigint_hash" - -# Prepared statements cache size. -prepared_statements_cache_size = 500 - -# Credentials for users that may connect to this cluster -[pools.sharded_db.users.0] -username = "sharding_user" -password = "sharding_user" -# 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 = 5 -statement_timeout = 0 - - -[pools.sharded_db.users.1] -username = "other_user" -password = "other_user" -pool_size = 21 -statement_timeout = 30000 - -# Shard 0 -[pools.sharded_db.shards.0] -# [ host, port, role ] -servers = [ - [ "127.0.0.1", 5432, "primary" ], - [ "localhost", 5432, "replica" ] -] -# Database name (e.g. "postgres") -database = "shard0" - -[pools.sharded_db.shards.1] -servers = [ - [ "127.0.0.1", 5432, "primary" ], - [ "localhost", 5432, "replica" ], -] -database = "shard1" - -[pools.sharded_db.shards.2] -servers = [ - [ "127.0.0.1", 5432, "primary" ], - [ "localhost", 5432, "replica" ], -] -database = "shard2" - - -[pools.simple_db] -pool_mode = "session" -default_role = "primary" -query_parser_enabled = true -query_parser_read_write_splitting = true -primary_reads_enabled = true -sharding_function = "pg_bigint_hash" - -[pools.simple_db.users.0] -username = "simple_user" -password = "simple_user" -pool_size = 5 -statement_timeout = 30000 - -[pools.simple_db.shards.0] -servers = [ - [ "127.0.0.1", 5432, "primary" ], - [ "localhost", 5432, "replica" ] -] -database = "some_db" diff --git a/tests/go/prepared_test.go b/tests/go/prepared_test.go deleted file mode 100644 index 0a42e72..0000000 --- a/tests/go/prepared_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package pgcat - -import ( - "context" - "database/sql" - "fmt" - _ "github.com/lib/pq" - "testing" -) - -func Test(t *testing.T) { - t.Cleanup(setup(t)) - t.Run("Named parameterized prepared statement works", namedParameterizedPreparedStatement) - t.Run("Unnamed parameterized prepared statement works", unnamedParameterizedPreparedStatement) -} - -func namedParameterizedPreparedStatement(t *testing.T) { - db, err := sql.Open("postgres", fmt.Sprintf("host=localhost port=%d database=sharded_db user=sharding_user password=sharding_user sslmode=disable", port)) - if err != nil { - t.Fatalf("could not open connection: %+v", err) - } - - stmt, err := db.Prepare("SELECT $1") - - if err != nil { - t.Fatalf("could not prepare: %+v", err) - } - - for i := 0; i < 100; i++ { - rows, err := stmt.Query(1) - if err != nil { - t.Fatalf("could not query: %+v", err) - } - _ = rows.Close() - } -} - -func unnamedParameterizedPreparedStatement(t *testing.T) { - db, err := sql.Open("postgres", fmt.Sprintf("host=localhost port=%d database=sharded_db user=sharding_user password=sharding_user sslmode=disable", port)) - if err != nil { - t.Fatalf("could not open connection: %+v", err) - } - - for i := 0; i < 100; i++ { - // Under the hood QueryContext generates an unnamed parameterized prepared statement - rows, err := db.QueryContext(context.Background(), "SELECT $1", 1) - if err != nil { - t.Fatalf("could not query: %+v", err) - } - _ = rows.Close() - } -} diff --git a/tests/go/setup.go b/tests/go/setup.go deleted file mode 100644 index 32ffc4b..0000000 --- a/tests/go/setup.go +++ /dev/null @@ -1,81 +0,0 @@ -package pgcat - -import ( - "context" - "database/sql" - _ "embed" - "fmt" - "math/rand" - "os" - "os/exec" - "strings" - "testing" - "time" -) - -//go:embed pgcat.toml -var pgcatCfg string - -var port = rand.Intn(32760-20000) + 20000 - -func setup(t *testing.T) func() { - cfg, err := os.CreateTemp("/tmp", "pgcat_cfg_*.toml") - if err != nil { - t.Fatalf("could not create temp file: %+v", err) - } - - pgcatCfg = strings.Replace(pgcatCfg, "\"${PORT}\"", fmt.Sprintf("%d", port), 1) - - _, err = cfg.Write([]byte(pgcatCfg)) - if err != nil { - t.Fatalf("could not write temp file: %+v", err) - } - - commandPath := "../../target/debug/pgcat" - if os.Getenv("CARGO_TARGET_DIR") != "" { - commandPath = os.Getenv("CARGO_TARGET_DIR") + "/debug/pgcat" - } - - cmd := exec.Command(commandPath, cfg.Name()) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - go func() { - err = cmd.Run() - if err != nil { - t.Errorf("could not run pgcat: %+v", err) - } - }() - - deadline, cancelFunc := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second)) - defer cancelFunc() - for { - select { - case <-deadline.Done(): - break - case <-time.After(50 * time.Millisecond): - db, err := sql.Open("postgres", fmt.Sprintf("host=localhost port=%d database=pgcat user=admin_user password=admin_pass sslmode=disable", port)) - if err != nil { - continue - } - rows, err := db.QueryContext(deadline, "SHOW STATS") - if err != nil { - continue - } - _ = rows.Close() - _ = db.Close() - break - } - break - } - - return func() { - err := cmd.Process.Signal(os.Interrupt) - if err != nil { - t.Fatalf("could not interrupt pgcat: %+v", err) - } - err = os.Remove(cfg.Name()) - if err != nil { - t.Fatalf("could not remove temp file: %+v", err) - } - } -} diff --git a/tests/pgbench/simple.sql b/tests/pgbench/simple.sql deleted file mode 100644 index a8429d2..0000000 --- a/tests/pgbench/simple.sql +++ /dev/null @@ -1,39 +0,0 @@ - --- \setrandom aid 1 :naccounts -\set aid random(1, 100000) --- \setrandom bid 1 :nbranches -\set bid random(1, 100000) --- \setrandom tid 1 :ntellers -\set tid random(1, 100000) --- \setrandom delta -5000 5000 -\set delta random(-5000,5000) - -\set shard random(0, 2) - -SET SHARD TO :shard; - -SET SERVER ROLE TO 'auto'; - -BEGIN; - -UPDATE pgbench_accounts SET abalance = abalance + :delta WHERE aid = :aid; - -SELECT abalance FROM pgbench_accounts WHERE aid = :aid; - -UPDATE pgbench_tellers SET tbalance = tbalance + :delta WHERE tid = :tid; - -UPDATE pgbench_branches SET bbalance = bbalance + :delta WHERE bid = :bid; - -INSERT INTO pgbench_history (tid, bid, aid, delta, mtime) VALUES (:tid, :bid, :aid, :delta, CURRENT_TIMESTAMP); - -END; - -SET SHARDING KEY TO :aid; - --- Read load balancing -SELECT abalance FROM pgbench_accounts WHERE aid = :aid; - -SET SERVER ROLE TO 'replica'; - --- Read load balancing -SELECT abalance FROM pgbench_accounts WHERE aid = :aid; \ No newline at end of file diff --git a/tests/python/.gitignore b/tests/python/.gitignore deleted file mode 100644 index eba74f4..0000000 --- a/tests/python/.gitignore +++ /dev/null @@ -1 +0,0 @@ -venv/ \ No newline at end of file diff --git a/tests/python/requirements.txt b/tests/python/requirements.txt deleted file mode 100644 index ec8b08f..0000000 --- a/tests/python/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -pytest -psycopg2==2.9.3 -psutil==5.9.1 diff --git a/tests/python/test_auth.py b/tests/python/test_auth.py deleted file mode 100644 index bd94342..0000000 --- a/tests/python/test_auth.py +++ /dev/null @@ -1,71 +0,0 @@ -import utils -import signal - -class TestTrustAuth: - @classmethod - def setup_method(cls): - config= """ - [general] - host = "0.0.0.0" - port = 6432 - admin_username = "admin_user" - admin_password = "" - admin_auth_type = "trust" - - [pools.sharded_db.users.0] - username = "sharding_user" - password = "sharding_user" - auth_type = "trust" - pool_size = 10 - min_pool_size = 1 - pool_mode = "transaction" - - [pools.sharded_db.shards.0] - servers = [ - [ "127.0.0.1", 5432, "primary" ], - ] - database = "shard0" - """ - utils.pgcat_generic_start(config) - - @classmethod - def teardown_method(self): - utils.pg_cat_send_signal(signal.SIGTERM) - - def test_admin_trust_auth(self): - conn, cur = utils.connect_db_trust(admin=True) - cur.execute("SHOW POOLS") - res = cur.fetchall() - print(res) - utils.cleanup_conn(conn, cur) - - def test_normal_trust_auth(self): - conn, cur = utils.connect_db_trust(autocommit=False) - cur.execute("SELECT 1") - res = cur.fetchall() - print(res) - utils.cleanup_conn(conn, cur) - -class TestMD5Auth: - @classmethod - def setup_method(cls): - utils.pgcat_start() - - @classmethod - def teardown_method(self): - utils.pg_cat_send_signal(signal.SIGTERM) - - def test_normal_db_access(self): - conn, cur = utils.connect_db(autocommit=False) - cur.execute("SELECT 1") - res = cur.fetchall() - print(res) - utils.cleanup_conn(conn, cur) - - def test_admin_db_access(self): - conn, cur = utils.connect_db(admin=True) - - cur.execute("SHOW POOLS") - res = cur.fetchall() - print(res) - utils.cleanup_conn(conn, cur) diff --git a/tests/python/test_pgcat.py b/tests/python/test_pgcat.py deleted file mode 100644 index 773715d..0000000 --- a/tests/python/test_pgcat.py +++ /dev/null @@ -1,242 +0,0 @@ - -import signal -import time - -import psycopg2 -import utils - -SHUTDOWN_TIMEOUT = 5 - - -def test_shutdown_logic(): - - # - - - - - - - - - - - - - - - - - - - # NO ACTIVE QUERIES SIGINT HANDLING - - # Start pgcat - utils.pgcat_start() - - # Create client connection and send query (not in transaction) - conn, cur = utils.connect_db() - - cur.execute("BEGIN;") - cur.execute("SELECT 1;") - cur.execute("COMMIT;") - - # Send sigint to pgcat - utils.pg_cat_send_signal(signal.SIGINT) - time.sleep(1) - - # Check that any new queries fail after sigint since server should close with no active transactions - try: - cur.execute("SELECT 1;") - except psycopg2.OperationalError as e: - pass - else: - # Fail if query execution succeeded - raise Exception("Server not closed after sigint") - - utils.cleanup_conn(conn, cur) - utils.pg_cat_send_signal(signal.SIGTERM) - - # - - - - - - - - - - - - - - - - - - - # NO ACTIVE QUERIES ADMIN SHUTDOWN COMMAND - - # Start pgcat - utils.pgcat_start() - - # Create client connection and begin transaction - conn, cur = utils.connect_db() - admin_conn, admin_cur = utils.connect_db(admin=True) - - cur.execute("BEGIN;") - cur.execute("SELECT 1;") - cur.execute("COMMIT;") - - # Send SHUTDOWN command pgcat while not in transaction - admin_cur.execute("SHUTDOWN;") - time.sleep(1) - - # Check that any new queries fail after SHUTDOWN command since server should close with no active transactions - try: - cur.execute("SELECT 1;") - except psycopg2.OperationalError as e: - pass - else: - # Fail if query execution succeeded - raise Exception("Server not closed after sigint") - - utils.cleanup_conn(conn, cur) - utils.cleanup_conn(admin_conn, admin_cur) - utils.pg_cat_send_signal(signal.SIGTERM) - - # - - - - - - - - - - - - - - - - - - - # HANDLE TRANSACTION WITH SIGINT - - # Start pgcat - utils.pgcat_start() - - # Create client connection and begin transaction - conn, cur = utils.connect_db() - - cur.execute("BEGIN;") - cur.execute("SELECT 1;") - - # Send sigint to pgcat while still in transaction - utils.pg_cat_send_signal(signal.SIGINT) - time.sleep(1) - - # Check that any new queries succeed after sigint since server should still allow transaction to complete - try: - cur.execute("SELECT 1;") - except psycopg2.OperationalError as e: - # Fail if query fails since server closed - raise Exception("Server closed while in transaction", e.pgerror) - - utils.cleanup_conn(conn, cur) - utils.pg_cat_send_signal(signal.SIGTERM) - - # - - - - - - - - - - - - - - - - - - - # HANDLE TRANSACTION WITH ADMIN SHUTDOWN COMMAND - - # Start pgcat - utils.pgcat_start() - - # Create client connection and begin transaction - conn, cur = utils.connect_db() - admin_conn, admin_cur = utils.connect_db(admin=True) - - cur.execute("BEGIN;") - cur.execute("SELECT 1;") - - # Send SHUTDOWN command pgcat while still in transaction - admin_cur.execute("SHUTDOWN;") - if admin_cur.fetchall()[0][0] != "t": - raise Exception("PgCat unable to send signal") - time.sleep(1) - - # Check that any new queries succeed after SHUTDOWN command since server should still allow transaction to complete - try: - cur.execute("SELECT 1;") - except psycopg2.OperationalError as e: - # Fail if query fails since server closed - raise Exception("Server closed while in transaction", e.pgerror) - - utils.cleanup_conn(conn, cur) - utils.cleanup_conn(admin_conn, admin_cur) - utils.pg_cat_send_signal(signal.SIGTERM) - - # - - - - - - - - - - - - - - - - - - - # NO NEW NON-ADMIN CONNECTIONS DURING SHUTDOWN - # Start pgcat - utils.pgcat_start() - - # Create client connection and begin transaction - transaction_conn, transaction_cur = utils.connect_db() - - transaction_cur.execute("BEGIN;") - transaction_cur.execute("SELECT 1;") - - # Send sigint to pgcat while still in transaction - utils.pg_cat_send_signal(signal.SIGINT) - time.sleep(1) - - start = time.perf_counter() - try: - conn, cur = utils.connect_db() - cur.execute("SELECT 1;") - utils.cleanup_conn(conn, cur) - except psycopg2.OperationalError as e: - time_taken = time.perf_counter() - start - if time_taken > 0.1: - raise Exception( - "Failed to reject connection within 0.1 seconds, got", time_taken, "seconds") - pass - else: - raise Exception("Able connect to database during shutdown") - - utils.cleanup_conn(transaction_conn, transaction_cur) - utils.pg_cat_send_signal(signal.SIGTERM) - - # - - - - - - - - - - - - - - - - - - - # ALLOW NEW ADMIN CONNECTIONS DURING SHUTDOWN - # Start pgcat - utils.pgcat_start() - - # Create client connection and begin transaction - transaction_conn, transaction_cur = utils.connect_db() - - transaction_cur.execute("BEGIN;") - transaction_cur.execute("SELECT 1;") - - # Send sigint to pgcat while still in transaction - utils.pg_cat_send_signal(signal.SIGINT) - time.sleep(1) - - try: - conn, cur = utils.connect_db(admin=True) - cur.execute("SHOW DATABASES;") - utils.cleanup_conn(conn, cur) - except psycopg2.OperationalError as e: - raise Exception(e) - - utils.cleanup_conn(transaction_conn, transaction_cur) - utils.pg_cat_send_signal(signal.SIGTERM) - - # - - - - - - - - - - - - - - - - - - - # ADMIN CONNECTIONS CONTINUING TO WORK AFTER SHUTDOWN - # Start pgcat - utils.pgcat_start() - - # Create client connection and begin transaction - transaction_conn, transaction_cur = utils.connect_db() - transaction_cur.execute("BEGIN;") - transaction_cur.execute("SELECT 1;") - - admin_conn, admin_cur = utils.connect_db(admin=True) - admin_cur.execute("SHOW DATABASES;") - - # Send sigint to pgcat while still in transaction - utils.pg_cat_send_signal(signal.SIGINT) - time.sleep(1) - - try: - admin_cur.execute("SHOW DATABASES;") - except psycopg2.OperationalError as e: - raise Exception("Could not execute admin command:", e) - - utils.cleanup_conn(transaction_conn, transaction_cur) - utils.cleanup_conn(admin_conn, admin_cur) - utils.pg_cat_send_signal(signal.SIGTERM) - - # - - - - - - - - - - - - - - - - - - - # HANDLE SHUTDOWN TIMEOUT WITH SIGINT - - # Start pgcat - utils.pgcat_start() - - # Create client connection and begin transaction, which should prevent server shutdown unless shutdown timeout is reached - conn, cur = utils.connect_db() - - cur.execute("BEGIN;") - cur.execute("SELECT 1;") - - # Send sigint to pgcat while still in transaction - utils.pg_cat_send_signal(signal.SIGINT) - - # pgcat shutdown timeout is set to SHUTDOWN_TIMEOUT seconds, so we sleep for SHUTDOWN_TIMEOUT + 1 seconds - time.sleep(SHUTDOWN_TIMEOUT + 1) - - # Check that any new queries succeed after sigint since server should still allow transaction to complete - try: - cur.execute("SELECT 1;") - except psycopg2.OperationalError as e: - pass - else: - # Fail if query execution succeeded - raise Exception("Server not closed after sigint and expected timeout") - - utils.cleanup_conn(conn, cur) - utils.pg_cat_send_signal(signal.SIGTERM) - - # - - - - - - - - - - - - - - - - - - diff --git a/tests/python/utils.py b/tests/python/utils.py deleted file mode 100644 index 9a1c6de..0000000 --- a/tests/python/utils.py +++ /dev/null @@ -1,110 +0,0 @@ -import os -import signal -import time -from typing import Tuple -import tempfile - -import psutil -import psycopg2 - -PGCAT_HOST = "127.0.0.1" -PGCAT_PORT = "6432" - - -def _pgcat_start(config_path: str): - pg_cat_send_signal(signal.SIGTERM) - os.system(f"./target/debug/pgcat {config_path} &") - time.sleep(2) - - -def pgcat_start(): - _pgcat_start(config_path='.circleci/pgcat.toml') - - -def pgcat_generic_start(config: str): - tmp = tempfile.NamedTemporaryFile() - with open(tmp.name, 'w') as f: - f.write(config) - _pgcat_start(config_path=tmp.name) - - -def glauth_send_signal(signal: signal.Signals): - try: - for proc in psutil.process_iter(["pid", "name"]): - if proc.name() == "glauth": - os.kill(proc.pid, signal) - except Exception as e: - # The process can be gone when we send this signal - print(e) - - if signal == signal.SIGTERM: - # Returns 0 if pgcat process exists - time.sleep(2) - if not os.system('pgrep glauth'): - raise Exception("glauth not closed after SIGTERM") - - -def pg_cat_send_signal(signal: signal.Signals): - try: - for proc in psutil.process_iter(["pid", "name"]): - if "pgcat" == proc.name(): - os.kill(proc.pid, signal) - except Exception as e: - # The process can be gone when we send this signal - print(e) - - if signal == signal.SIGTERM: - # Returns 0 if pgcat process exists - time.sleep(2) - if not os.system('pgrep pgcat'): - raise Exception("pgcat not closed after SIGTERM") - - -def connect_db( - autocommit: bool = True, - admin: bool = False, -) -> Tuple[psycopg2.extensions.connection, psycopg2.extensions.cursor]: - - if admin: - user = "admin_user" - password = "admin_pass" - db = "pgcat" - else: - user = "sharding_user" - password = "sharding_user" - db = "sharded_db" - - conn = psycopg2.connect( - f"postgres://{user}:{password}@{PGCAT_HOST}:{PGCAT_PORT}/{db}?application_name=testing_pgcat", - connect_timeout=2, - ) - conn.autocommit = autocommit - cur = conn.cursor() - - return (conn, cur) - -def connect_db_trust( - autocommit: bool = True, - admin: bool = False, -) -> Tuple[psycopg2.extensions.connection, psycopg2.extensions.cursor]: - - if admin: - user = "admin_user" - db = "pgcat" - else: - user = "sharding_user" - db = "sharded_db" - - conn = psycopg2.connect( - f"postgres://{user}@{PGCAT_HOST}:{PGCAT_PORT}/{db}?application_name=testing_pgcat", - connect_timeout=2, - ) - conn.autocommit = autocommit - cur = conn.cursor() - - return (conn, cur) - - -def cleanup_conn(conn: psycopg2.extensions.connection, cur: psycopg2.extensions.cursor): - cur.close() - conn.close() diff --git a/tests/ruby/.ruby-version b/tests/ruby/.ruby-version deleted file mode 100644 index cf232b5..0000000 --- a/tests/ruby/.ruby-version +++ /dev/null @@ -1,2 +0,0 @@ -3.0.0 - diff --git a/tests/ruby/Gemfile b/tests/ruby/Gemfile deleted file mode 100644 index ec7cd3a..0000000 --- a/tests/ruby/Gemfile +++ /dev/null @@ -1,8 +0,0 @@ -source "https://rubygems.org" - -gem "pg" -gem "toml" -gem "rspec" -gem "rubocop" -gem "toxiproxy" -gem "activerecord" diff --git a/tests/ruby/Gemfile.lock b/tests/ruby/Gemfile.lock deleted file mode 100644 index ba6999b..0000000 --- a/tests/ruby/Gemfile.lock +++ /dev/null @@ -1,88 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - activemodel (7.1.4) - activesupport (= 7.1.4) - activerecord (7.1.4) - activemodel (= 7.1.4) - activesupport (= 7.1.4) - timeout (>= 0.4.0) - activesupport (7.1.4) - base64 - bigdecimal - concurrent-ruby (~> 1.0, >= 1.0.2) - connection_pool (>= 2.2.5) - drb - i18n (>= 1.6, < 2) - minitest (>= 5.1) - mutex_m - tzinfo (~> 2.0) - ast (2.4.2) - base64 (0.2.0) - bigdecimal (3.1.8) - concurrent-ruby (1.3.4) - connection_pool (2.4.1) - diff-lcs (1.5.0) - drb (2.2.1) - i18n (1.14.5) - concurrent-ruby (~> 1.0) - minitest (5.25.1) - mutex_m (0.2.0) - parallel (1.22.1) - parser (3.1.2.0) - ast (~> 2.4.1) - parslet (2.0.0) - pg (1.3.2) - rainbow (3.1.1) - regexp_parser (2.3.1) - rexml (3.3.6) - strscan - rspec (3.11.0) - rspec-core (~> 3.11.0) - rspec-expectations (~> 3.11.0) - rspec-mocks (~> 3.11.0) - rspec-core (3.11.0) - rspec-support (~> 3.11.0) - rspec-expectations (3.11.0) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.11.0) - rspec-mocks (3.11.1) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.11.0) - rspec-support (3.11.0) - rubocop (1.29.0) - parallel (~> 1.10) - parser (>= 3.1.0.0) - rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 1.8, < 3.0) - rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.17.0, < 2.0) - ruby-progressbar (~> 1.7) - unicode-display_width (>= 1.4.0, < 3.0) - rubocop-ast (1.17.0) - parser (>= 3.1.1.0) - ruby-progressbar (1.11.0) - strscan (3.1.0) - timeout (0.4.1) - toml (0.3.0) - parslet (>= 1.8.0, < 3.0.0) - toxiproxy (2.0.1) - tzinfo (2.0.6) - concurrent-ruby (~> 1.0) - unicode-display_width (2.1.0) - -PLATFORMS - aarch64-linux - arm64-darwin-21 - x86_64-linux - -DEPENDENCIES - activerecord - pg - rspec - rubocop - toml - toxiproxy - -BUNDLED WITH - 2.3.21 diff --git a/tests/ruby/admin_spec.rb b/tests/ruby/admin_spec.rb deleted file mode 100644 index f9613d9..0000000 --- a/tests/ruby/admin_spec.rb +++ /dev/null @@ -1,138 +0,0 @@ -# frozen_string_literal: true -require 'uri' -require_relative 'spec_helper' - -describe "Admin" 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 "Manual Banning" do - let(:processes) { Helpers::Pgcat.single_shard_setup("sharded_db", 10) } - before do - new_configs = processes.pgcat.current_config - # Prevent immediate unbanning when we ban localhost - new_configs["pools"]["sharded_db"]["shards"]["0"]["servers"][0][0] = "127.0.0.1" - new_configs["pools"]["sharded_db"]["shards"]["0"]["servers"][1][0] = "127.0.0.1" - processes.pgcat.update_config(new_configs) - processes.pgcat.reload_config - end - - describe "BAN/UNBAN and SHOW BANS" do - it "bans/unbans hosts" do - admin_conn = PG::connect(processes.pgcat.admin_connection_string) - - # Returns a list of the banned addresses - results = admin_conn.async_exec("BAN localhost 10").to_a - expect(results.count).to eq(2) - expect(results.map{ |r| r["host"] }.uniq).to eq(["localhost"]) - - # Subsequent calls should yield no results - results = admin_conn.async_exec("BAN localhost 10").to_a - expect(results.count).to eq(0) - - results = admin_conn.async_exec("SHOW BANS").to_a - expect(results.count).to eq(2) - expect(results.map{ |r| r["host"] }.uniq).to eq(["localhost"]) - - # Returns a list of the unbanned addresses - results = admin_conn.async_exec("UNBAN localhost").to_a - expect(results.count).to eq(2) - expect(results.map{ |r| r["host"] }.uniq).to eq(["localhost"]) - - # Subsequent calls should yield no results - results = admin_conn.async_exec("UNBAN localhost").to_a - expect(results.count).to eq(0) - - results = admin_conn.async_exec("SHOW BANS").to_a - expect(results.count).to eq(0) - end - - it "honors ban duration" do - admin_conn = PG::connect(processes.pgcat.admin_connection_string) - - # Returns a list of the banned addresses - results = admin_conn.async_exec("BAN localhost 1").to_a - expect(results.count).to eq(2) - expect(results.map{ |r| r["host"] }.uniq).to eq(["localhost"]) - - sleep(2) - - # After 2 seconds the ban should be lifted - results = admin_conn.async_exec("SHOW BANS").to_a - expect(results.count).to eq(0) - end - - it "can handle bad input" do - admin_conn = PG::connect(processes.pgcat.admin_connection_string) - - expect { admin_conn.async_exec("BAN").to_a }.to raise_error(PG::SystemError) - expect { admin_conn.async_exec("BAN a").to_a }.to raise_error(PG::SystemError) - expect { admin_conn.async_exec("BAN a a").to_a }.to raise_error(PG::SystemError) - expect { admin_conn.async_exec("BAN a -5").to_a }.to raise_error(PG::SystemError) - expect { admin_conn.async_exec("BAN a 0").to_a }.to raise_error(PG::SystemError) - expect { admin_conn.async_exec("BAN a a a").to_a }.to raise_error(PG::SystemError) - expect { admin_conn.async_exec("UNBAN").to_a }.to raise_error(PG::SystemError) - end - end - end - - 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] - admin_conn.close - expect(results["name"]).to eq("sharding_user") - expect(results["pool_mode"]).to eq("transaction") - end - end - - [ - "SHOW ME THE MONEY", - "SHOW ME THE WAY", - "SHOW UP", - "SHOWTIME", - "HAMMER TIME", - "SHOWN TO BE TRUE", - "SHOW ", - "SHOW ", - "SHOW 1", - ";;;;;" - ].each do |cmd| - describe "Bad command #{cmd}" do - it "does not panic and responds with PG::SystemError" do - admin_conn = PG::connect(processes.pgcat.admin_connection_string) - expect { admin_conn.async_exec(cmd) }.to raise_error(PG::SystemError).with_message(/Unsupported/) - admin_conn.close - end - end - end - - describe "PAUSE" do - it "pauses all pools" do - admin_conn = PG::connect(processes.pgcat.admin_connection_string) - results = admin_conn.async_exec("SHOW DATABASES").to_a - expect(results.map{ |r| r["paused"] }.uniq).to eq(["0"]) - - admin_conn.async_exec("PAUSE") - - results = admin_conn.async_exec("SHOW DATABASES").to_a - expect(results.map{ |r| r["paused"] }.uniq).to eq(["1"]) - - admin_conn.async_exec("RESUME") - - results = admin_conn.async_exec("SHOW DATABASES").to_a - expect(results.map{ |r| r["paused"] }.uniq).to eq(["0"]) - end - - it "handles errors" do - admin_conn = PG::connect(processes.pgcat.admin_connection_string) - expect { admin_conn.async_exec("PAUSE foo").to_a }.to raise_error(PG::SystemError) - expect { admin_conn.async_exec("PAUSE foo,bar").to_a }.to raise_error(PG::SystemError) - end - end -end diff --git a/tests/ruby/auth_query_spec.rb b/tests/ruby/auth_query_spec.rb deleted file mode 100644 index c1ee744..0000000 --- a/tests/ruby/auth_query_spec.rb +++ /dev/null @@ -1,215 +0,0 @@ -# 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 deleted file mode 100644 index 268680ff5141f12abdc6e19d7056909c5cfd4075..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40638 zcmeHQ33wDm7Vb$%0x zAadw(xCA!B3bKM52rI~aA3}f}C;YSMTWl zUsrW;C~wV@0IiC4<5NYeWzw|OgB$d(*Dgfs1Uqo6$`3FFR?&j#*w&i%gn7F~v(z+w z{i>#!a4X(jyjy#~`Zz#n5wN9RIr-20`2pH<&mU%z&PDv(F(Fzk{D4~-_%Y@$82%*( z{0M8)P&lU!Y|AY02R@Pa=QDi7JwH{4-a8oXSqEG}?@moQo+XS+Y6tO?)`w`{25M|` zuXo@{^G?LKwi59Jx0_A|fUdc4F5=<2_}!Dw1#s1G01ASAuFyX`0g)X0hQwa|GBUO zH~0B($~N!9{MRzX{5Q?DtwekM9nM>3(KNUWc>H-4`iuGRtQURv7|`hiXMa8f?fsq~ z(wk&&Xb#ge!=|DGYl4)RHY?E|#mULcuC0)l!rusNuXWzm#r+$^UA7!0qj}M9yZ4JcF z?j_Z<)^GxD_Whxn$rK#$^p!Oq)MCF!Cmg`|8M7t(cFmN1yUlid?wAGIT|K&Wi0ab1 zW#?A05r2+|?b@}|!_keR!$T3Y-TOK~4>Hir1iEPSn1yA6t^?2(57y9TgO<46&@@2Q zbliq17R^-i-h&8w*=GASH!EbQmb_rPTTNr1>(bR6(WGhfMopSEvu};f(ajq-4iD|n zvZFa7G9#mZnz?^cMndZF#PrDUP&_^>EhWvIo|>B3KXG_cLLwa6!yM5pJTyFXJt%p6 zsk-Nh&tlJC>7c9U2-;+s4$xQz+MYnAKH3PN#Vd4xf;mCcPy&_uNH^$I23n0krJ8O6 zP4{05&|Uh zQ&JKWGOP5?Oh^kFnwrpesHTN!CTz@fJwO`<=ioLmAJ5hxA8tfhpWwnnTCsYo= z)1JO!vqGlW?AKBIuXY9A+QYuvZwR^+xTh=yj%AK$)ko9%+>+hXL4jkLHY@CLufV!Y zj}>_Lo?vz5vICyD;-1^!f4PK>15u8ICMDkoRlQ^iEm-z%qFh@XTgbv=?7i1o;y;*pm6 zydK#MN@lHAv;0AKB54`*iY`KqplkGi)?uKd2~=7-Yyr^gKZR(kAhK{PgXPcLdOx!K zdUumKi`qhY_xggRUMlafl7kACRe9+tQ26y78kL+iG!qKI^u)xJsMPd<=8VL2Ec@6_ zGaMWqipux-G)(&&D303=V(oV?cRhCYw??%;hqiuRqjqUh0k-whT53ybeFP0& zD+LXP#{=9Yb12 z5#Rc#h%Xjqh-VaLnnpKplE#o0kLU9KrJ(?l#*kKdEy>R#JCelwe-rXwS(qKe{MQDG z`7ah`nEzB9c%q?En0aC?*JH`%B+;LX!Jo>)>^S<1JAG4Qxp_kkH>F6a4 zMnEq+{xN&G4|*B>A00JFcf8`g^dyxF(&|eM8kG!b5S`i{K{H|rR9b@|=m+11DF=s& z1|Vq9Sh_Y(7%*XgSc4?@FJZMuhr9xZ8U*oYH4Ik{4iyb#_|G}uyQx8X&ExGL?&+rC zYP|KK4$Ae* zBlfdMR>ritjJR^CuInT+7s;DpaA+3>q1`&pQkT&r2hpGqNgNz<*PaBzqg~GW`+8a( z=Knj8|Ee0KXfWn~_d{a-d#eV){CBnkefP+iZVo0tlB|qrb(sIuAF35dx>4Dqp9qH} z`g0lhQ&oc$4PpLF5dG<`8bs`Gh)2Cq9~^8ygk)t*tMg^N`g&R&#?Oa|YTmm49P-v% zV*Z%CbwBiS@;xRctIC;=Su(8_Wy$< zu|XSv5}WZyHn`ScgX+tC8As4vlq5cpo`^wcKi*lE0i*k8Vhtpb4Z3SjH=yD`XZ?N6 z1~LCPK>n-PV9r9!|0#>b{Pz|c#Qb-*1ASMRPq~}?NU}26Am%^yhgcg)Wd{@o5DrQ7 z=U(uqiVfy0V*Y$s^ryGjpx9r3WP`qpS6{P1jGyG~Rg^p_J{RMsp!8+Yl~-3@6?xLE zG2b%&GA#d`3s1WCQxzpoir>ueOGiu?z8juYYc+2N_1?*LE_$A4k;i(xbV(d1f`wz(qn3A1J z_fwL{liame(RS#3(wk&u@T3gLe-%%P--`KvW|Wx!-r`BVdB*j5_ zCE<|7`^9+hr-~=V|Bv~zn&?k&@g%Xo{>YPj8Lz(PNrxbQW<9E&FnYL$1)iE!4Wg%J zKAkXn7?hl_Q9W3j&L)hel};Fyj;pFc`GnEb4wyL^&(dJ6bOiWI&@^(3TGOmx1Wlen zpi)hBfU=pwSwReRCV@&dJpr1ItzK0^kkb=ktojM_mk4S)OcxI4)mnmHn6?V1ww~QE zfl0csA^xL}SCtUtj5NTHF`r=gCmrwwg1p?q5ep{qb`ZCYMW(lAVtSusxCb3@g$Idy zrg6N_61k*y5I-xas*)g^^AX<~E#iv=8S%U^we?G4dSuuNN*YckSsBC1#%e9)cOnrB zlE&25^4k*6Bs-F1*G0&G6+t$i!u;3ziuo@RWXyjeX`X0kOl|eVTCT^E%}Js^CxSm! z1lfEV{b{=_`qNtk*;uW`)lYhVp~9S`uL<%gh@U}EstNL`JS520d6hztPlJ*Jx2kF9 zaJCS`kgd8Hq8gOXW*p8|Ul@`{pi(Y-1~kpC8?Jn9Zv=a#d+QO~93M9}-Cpu7qjJ&b|gOQ2G7o(IsV*JQ81MTfH*;S7SB_KMeh@!|h# zIccl0w&_-|fmAjzBI;lHsG zrQYapJ&tM`l9>Myb#>G>#hCxv3u69@wGHM!@oU#s%l^fvZOXQdo;_r9lIYJA@Tao2 zIg9?Zm5BZ{Y8zLJ2iboj>F=-YU4rqKbHEp<{89@?ytt6JgSdwm$n>^Td6U6^1E($>w+^uX9Q5Ab$02I+jK(L40dN5nrV8h-XlFV+9V%2X9HDoFt85ii%m5 z>_`&xzdq!@a+u;g=D!vv=D$egG5?89dZM90M>#<~WlIYI{;7=u$zkvR<6^Z^d zsJyFviNcfgcPf7!;%8~Fnjl|=*(|1-=V~DN@;O09B^MURXeWCv8$F3FOBVkbF+?es zRf85}BkP|0B4$qfqXa6YomSAa_fhpQ#T)$)v}c7@7YrEEp#PadnN>I$pJKtbdUVy* z0W~Nm<5OBN&^Q8>nmPnDy##DgRfS#Oh(UIGF@pL$$WY1juVm)rK{oAU?0MX2>bW!? z)SyYLG03L0WajKbpi*;I1x-I1rsHw{MFchFiPwCwwwaq?&cWK|r7caEq^lv~Pkm3v zFvT|ve}@CUP}|VE0~s@UJBZtJi4MIlG2G1#xI%67{97EaZ5WJi*i|8GM6D{Gr?G5@vF6^U>dRIF_<|A}9F zqM=dScw#NrW69XMfM_|xj56ozYYz!eysrP>&# zZ{tFaON0>4#w+2FM1O7of2tT= z5u3Hy{YBBA-ePoOe?uzTL8tSB?cYfnYqdEU$iuJr6b3&e!|{3$5+LClp=^R~&SK;P z)2dVaIIm*%$zR7YzA^WMkq$fw@!x+UK#PSoakIDU{9n<>ZRXiyNE*P+i*?ckoy~UF-|H*&O z(yfy`_>ufbvNCv*xT+WN8Im4!#qFVlLlXVD4*aR&NjWeL1N`}*=udC)B(cB#$dh~- zufBfy5aXxYnji^J;(R0ibBv#Y(*L;Gc{Q+CFHd0nsCP&HT!w-d@z z@6VHrRm><$JeSV*O&lR~x2hN&=Xnc`XVCqWq`$9XhS!&~kpC*46u$$nFK05v{Pz}5 z^7rRS;;LT6XGl6mpXc8Qha~#*0{BzKlj3(We+G*F^cGJN`|FQ9$(Ql!Yo1gS;%8}? zdSY7ORGgT0`~VK-`Fvs;D*4G}^)Tivwgi5spJ~KVI*h3X<-?e>*n0h)ej!llFy04LSn!eoz)3IYa0+nitpey8{?7gtJ+cMC$1S-|^P5|xHMz$Ile~@Utf}o~Nbm4G* zWByS6h>vgz-M?S1!z5kO5dWPqvQ^9&0go|%&+xZ8;0q5Dms>buWM|$E;-*iR>CK6~ z`3HvksROR?AhC8|ju&L+lG;K1(HnHU>3pq2(lZ=o8dvC@ut5R0iOMv3JH>!{~tpBs|d3BN6de%bfSp!6gtd*B59sz zWK5y+#9FS$lFdn?KR1CtRRr1m6Z+G3RJ@*hiy#}Tn7R5%?=N)!BY+{H~+KHgw$w3+IoWov}ZdbbCn3Q(b2hgtz z)vpK5eFoF9ZRtBlLEVjN-<=ugfY3`i&fvS!EvMwXsVGRBDxm0NSRu RI>>4?#30-ASfvEn{{c}CVT}L) diff --git a/tests/ruby/copy_spec.rb b/tests/ruby/copy_spec.rb deleted file mode 100644 index 5d3f2c0..0000000 --- a/tests/ruby/copy_spec.rb +++ /dev/null @@ -1,102 +0,0 @@ -# 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 deleted file mode 100644 index 43d7c78..0000000 --- a/tests/ruby/helpers/auth_query_helper.rb +++ /dev/null @@ -1,173 +0,0 @@ -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_i, "primary"], - ["localhost", replica.port.to_i, "replica"], - ] - }, - }, - "users" => { "0" => user.merge(config_user) } - } - } - pgcat_cfg["general"]["port"] = pgcat.port.to_i - 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_i, "primary"], - ["localhost", replica.port.to_i, "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_instance.rb b/tests/ruby/helpers/pg_instance.rb deleted file mode 100644 index 53617c2..0000000 --- a/tests/ruby/helpers/pg_instance.rb +++ /dev/null @@ -1,108 +0,0 @@ -require 'pg' -require 'toxiproxy' - -class PgInstance - attr_reader :port - attr_reader :username - attr_reader :password - attr_reader :database_name - - def self.mass_takedown(databases) - raise StandardError "block missing" unless block_given? - - databases.each do |database| - database.toxiproxy.toxic(:limit_data, bytes: 1).toxics.each(&:save) - end - sleep 0.1 - yield - ensure - databases.each do |database| - database.toxiproxy.toxics.each(&:destroy) - end - end - - def initialize(port, username, password, database_name) - @original_port = port.to_i - @toxiproxy_port = 10000 + port.to_i - @port = @toxiproxy_port.to_i - - @username = username - @password = password - @database_name = database_name - @toxiproxy_name = "database_#{@original_port}" - Toxiproxy.populate([{ - name: @toxiproxy_name, - listen: "0.0.0.0:#{@toxiproxy_port}", - upstream: "localhost:#{@original_port}", - }]) - - # Toxiproxy server will outlive our PgInstance objects - # so we want to destroy our proxies before exiting - # Ruby finalizer is ideal for doing this - ObjectSpace.define_finalizer(@toxiproxy_name, proc { Toxiproxy[@toxiproxy_name].destroy }) - end - - def with_connection - conn = PG.connect("postgres://#{@username}:#{@password}@localhost:#{port}/#{database_name}") - yield conn - ensure - conn&.close - end - - def reset - reset_toxics - reset_stats - drop_connections - sleep 0.1 - end - - def toxiproxy - Toxiproxy[@toxiproxy_name] - end - - def take_down - if block_given? - Toxiproxy[@toxiproxy_name].toxic(:limit_data, bytes: 1).apply { yield } - else - Toxiproxy[@toxiproxy_name].toxic(:limit_data, bytes: 1).toxics.each(&:save) - end - end - - def add_latency(latency) - if block_given? - Toxiproxy[@toxiproxy_name].toxic(:latency, latency: latency).apply { yield } - else - Toxiproxy[@toxiproxy_name].toxic(:latency, latency: latency).toxics.each(&:save) - end - end - - def delete_proxy - Toxiproxy[@toxiproxy_name].delete - end - - def reset_toxics - Toxiproxy[@toxiproxy_name].toxics.each(&:destroy) - sleep 0.1 - end - - def reset_stats - with_connection { |c| c.async_exec("SELECT pg_stat_statements_reset()") } - end - - def drop_connections - username = with_connection { |c| c.async_exec("SELECT current_user")[0]["current_user"] } - with_connection { |c| c.async_exec("SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE pid <> pg_backend_pid() AND usename='#{username}'") } - end - - def count_connections - with_connection { |c| c.async_exec("SELECT COUNT(*) as count FROM pg_stat_activity")[0]["count"].to_i } - end - - def count_query(query) - with_connection { |c| c.async_exec("SELECT SUM(calls) FROM pg_stat_statements WHERE query = '#{query}'")[0]["sum"].to_i } - end - - def count_select_1_plus_2 - with_connection { |c| c.async_exec("SELECT SUM(calls) FROM pg_stat_statements WHERE query LIKE '%SELECT $1 + $2%'")[0]["sum"].to_i } - end -end diff --git a/tests/ruby/helpers/pg_socket.rb b/tests/ruby/helpers/pg_socket.rb deleted file mode 100644 index 4223449..0000000 --- a/tests/ruby/helpers/pg_socket.rb +++ /dev/null @@ -1,259 +0,0 @@ -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 deleted file mode 100644 index 9b95dbf..0000000 --- a/tests/ruby/helpers/pgcat_helper.rb +++ /dev/null @@ -1,178 +0,0 @@ -require 'json' -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 - def self.three_shard_setup(pool_name, pool_size, pool_mode="transaction", lb_mode="random", log_level="info") - user = { - "password" => "sharding_user", - "pool_size" => pool_size, - "statement_timeout" => 0, - "username" => "sharding_user" - } - - pgcat = PgcatProcess.new(log_level) - primary0 = PgInstance.new(5432, user["username"], user["password"], "shard0") - primary1 = PgInstance.new(7432, user["username"], user["password"], "shard1") - primary2 = PgInstance.new(8432, user["username"], user["password"], "shard2") - - pgcat_cfg = pgcat.current_config - pgcat_cfg["pools"] = { - "#{pool_name}" => { - "default_role" => "any", - "pool_mode" => pool_mode, - "load_balancing_mode" => lb_mode, - "primary_reads_enabled" => true, - "query_parser_enabled" => true, - "query_parser_read_write_splitting" => true, - "automatic_sharding_key" => "data.id", - "sharding_function" => "pg_bigint_hash", - "shards" => { - "0" => { "database" => "shard0", "servers" => [["localhost", primary0.port.to_i, "primary"]] }, - "1" => { "database" => "shard1", "servers" => [["localhost", primary1.port.to_i, "primary"]] }, - "2" => { "database" => "shard2", "servers" => [["localhost", primary2.port.to_i, "primary"]] }, - }, - "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) - - pgcat.start - pgcat.wait_until_ready - - OpenStruct.new.tap do |struct| - struct.pgcat = pgcat - struct.shards = [primary0, primary1, primary2] - struct.all_databases = [primary0, primary1, primary2] - end - end - - def self.single_instance_setup(pool_name, pool_size, pool_mode="transaction", lb_mode="random", log_level="trace") - user = { - "password" => "sharding_user", - "pool_size" => pool_size, - "statement_timeout" => 0, - "username" => "sharding_user" - } - - pgcat = PgcatProcess.new(log_level) - pgcat_cfg = pgcat.current_config - - primary = PgInstance.new(5432, user["username"], user["password"], "shard0") - - # Main proxy configs - pgcat_cfg["pools"] = { - "#{pool_name}" => { - "default_role" => "primary", - "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_i, "primary"] - ] - }, - }, - "users" => { "0" => user } - } - } - pgcat_cfg["general"]["port"] = pgcat.port - pgcat.update_config(pgcat_cfg) - pgcat.start - pgcat.wait_until_ready - - OpenStruct.new.tap do |struct| - struct.pgcat = pgcat - struct.primary = primary - struct.all_databases = [primary] - end - end - - 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, - "statement_timeout" => 0, - "username" => "sharding_user" - } - - pgcat = PgcatProcess.new(log_level) - pgcat_cfg = pgcat.current_config - - primary = PgInstance.new(5432, user["username"], user["password"], "shard0") - replica0 = PgInstance.new(7432, user["username"], user["password"], "shard0") - 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_i, "primary"], - ["localhost", replica0.port.to_i, "replica"], - ["localhost", replica1.port.to_i, "replica"], - ["localhost", replica2.port.to_i, "replica"] - ] - }, - }, - "users" => { "0" => user } - } - - pool_config = pool_config.merge(pool_settings) - - # Main proxy configs - pgcat_cfg["pools"] = { - "#{pool_name}" => pool_config, - } - pgcat_cfg["general"]["port"] = pgcat.port - pgcat.update_config(pgcat_cfg) - pgcat.start - pgcat.wait_until_ready - - OpenStruct.new.tap do |struct| - struct.pgcat = pgcat - struct.primary = primary - struct.replicas = [replica0, replica1, replica2] - struct.all_databases = [primary, replica0, replica1, replica2] - end - end - end -end diff --git a/tests/ruby/helpers/pgcat_process.rb b/tests/ruby/helpers/pgcat_process.rb deleted file mode 100644 index 9328ff6..0000000 --- a/tests/ruby/helpers/pgcat_process.rb +++ /dev/null @@ -1,152 +0,0 @@ -require 'pg' -require 'json' -require 'tempfile' -require 'fileutils' -require 'securerandom' - -class ConfigReloadFailed < StandardError; end -class PgcatProcess - attr_reader :port - attr_reader :pid - - def self.finalize(pid, log_filename, config_filename) - if pid - Process.kill("TERM", pid) - Process.wait(pid) - end - - File.delete(config_filename) if File.exist?(config_filename) - File.delete(log_filename) if File.exist?(log_filename) - end - - def initialize(log_level) - @env = {} - @port = rand(20000..32760) - @log_level = log_level - @log_filename = "/tmp/pgcat_log_#{SecureRandom.urlsafe_base64}.log" - @config_filename = "/tmp/pgcat_cfg_#{SecureRandom.urlsafe_base64}.toml" - - command_path = if ENV['CARGO_TARGET_DIR'] then - "#{ENV['CARGO_TARGET_DIR']}/debug/pgcat" - else - '../../target/debug/pgcat' - end - - @command = "#{command_path} #{@config_filename} --log-level #{@log_level}" - - FileUtils.cp("../../pgcat.toml", @config_filename) - cfg = current_config - cfg["general"]["port"] = @port.to_i - cfg["general"]["enable_prometheus_exporter"] = false - - update_config(cfg) - end - - def logs - File.read(@log_filename) - end - - def update_config(config_hash) - @original_config = current_config - Tempfile.create('json_out', '/tmp') do |f| - f.write(config_hash.to_json) - f.flush - `cat #{f.path} | yj -jt > #{@config_filename}` - end - end - - def current_config - JSON.parse(`cat #{@config_filename} | yj -tj`) - end - - def raw_config_file - File.read(@config_filename) - end - - def reload_config - conn = PG.connect(admin_connection_string) - - conn.async_exec("RELOAD") - rescue PG::ConnectionBad => e - errors = logs.split("Reloading config").last - errors = errors.gsub(/\e\[([;\d]+)?m/, '') # Remove color codes - errors = errors. - split("\n").select{|line| line.include?("ERROR") }. - map { |line| line.split("pgcat::config: ").last } - raise ConfigReloadFailed, errors.join("\n") - ensure - conn&.close - end - - 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(connection_string = nil) - exc = nil - 10.times do - 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) - end - puts exc - raise StandardError, "Process #{@pid} never became ready. Logs #{logs}" - end - - def stop - return unless @pid - - Process.kill("TERM", @pid) - Process.wait(@pid) - @pid = nil - end - - def shutdown - stop - File.delete(@config_filename) if File.exist?(@config_filename) - File.delete(@log_filename) if File.exist?(@log_filename) - end - - def admin_connection_string - cfg = current_config - username = cfg["general"]["admin_username"] - password = cfg["general"]["admin_password"] - - "postgresql://#{username}:#{password}@0.0.0.0:#{@port}/pgcat" - end - - def connection_string(pool_name, username, password = nil, parameters: {}) - cfg = current_config - user_idx, user_obj = cfg["pools"][pool_name]["users"].detect { |k, user| user["username"] == username } - connection_string = "postgresql://#{username}:#{password || user_obj["password"]}@0.0.0.0:#{@port}/#{pool_name}" - - # Add the additional parameters to the connection string - parameter_string = parameters.map { |key, value| "#{key}=#{value}" }.join("&") - connection_string += "?#{parameter_string}" unless parameter_string.empty? - - connection_string - end - - def example_connection_string - cfg = current_config - first_pool_name = cfg["pools"].keys[0] - - db_name = first_pool_name - - username = cfg["pools"][first_pool_name]["users"]["0"]["username"] - password = cfg["pools"][first_pool_name]["users"]["0"]["password"] - - "postgresql://#{username}:#{password}@0.0.0.0:#{@port}/#{db_name}?application_name=example_app" - end -end diff --git a/tests/ruby/load_balancing_spec.rb b/tests/ruby/load_balancing_spec.rb deleted file mode 100644 index 172e606..0000000 --- a/tests/ruby/load_balancing_spec.rb +++ /dev/null @@ -1,164 +0,0 @@ -# frozen_string_literal: true -require_relative 'spec_helper' - -describe "Random Load Balancing" do - let(:processes) { Helpers::Pgcat.single_shard_setup("sharded_db", 5) } - after do - processes.all_databases.map(&:reset) - processes.pgcat.shutdown - end - - context "under regular circumstances" do - it "balances query volume between all instances" do - conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) - - query_count = QUERY_COUNT - expected_share = query_count / processes.all_databases.count - failed_count = 0 - - query_count.times do - conn.async_exec("SELECT 1 + 2") - rescue - failed_count += 1 - end - - expect(failed_count).to eq(0) - processes.all_databases.map(&:count_select_1_plus_2).each do |instance_share| - expect(instance_share).to be_within(expected_share * MARGIN_OF_ERROR).of(expected_share) - end - end - end - - context "when some replicas are down" do - it "balances query volume between working instances" do - conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) - expected_share = QUERY_COUNT / (processes.all_databases.count - 2) - failed_count = 0 - - processes[:replicas][0].take_down do - processes[:replicas][1].take_down do - QUERY_COUNT.times do - conn.async_exec("SELECT 1 + 2") - rescue - conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) - failed_count += 1 - end - end - end - - processes.all_databases.each do |instance| - queries_routed = instance.count_select_1_plus_2 - if processes.replicas[0..1].include?(instance) - expect(queries_routed).to eq(0) - else - expect(queries_routed).to be_within(expected_share * MARGIN_OF_ERROR).of(expected_share) - end - end - end - end -end - -describe "Least Outstanding Queries Load Balancing" do - let(:processes) { Helpers::Pgcat.single_shard_setup("sharded_db", 1, "transaction", "loc") } - after do - processes.all_databases.map(&:reset) - processes.pgcat.shutdown - end - - context "under homogeneous load" do - it "balances query volume between all instances" do - conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) - - query_count = QUERY_COUNT - expected_share = query_count / processes.all_databases.count - failed_count = 0 - - query_count.times do - conn.async_exec("SELECT 1 + 2") - rescue - failed_count += 1 - end - - expect(failed_count).to eq(0) - processes.all_databases.map(&:count_select_1_plus_2).each do |instance_share| - expect(instance_share).to be_within(expected_share * MARGIN_OF_ERROR).of(expected_share) - end - end - end - - context "under heterogeneous load" do - xit "balances query volume between all instances based on how busy they are" do - slow_query_count = 2 - threads = Array.new(slow_query_count) do - Thread.new do - conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) - conn.async_exec("BEGIN") - end - end - - conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) - - query_count = QUERY_COUNT - expected_share = query_count / (processes.all_databases.count - slow_query_count) - failed_count = 0 - - query_count.times do - conn.async_exec("SELECT 1 + 2") - rescue - failed_count += 1 - end - - expect(failed_count).to eq(0) - # Under LOQ, we expect replicas running the slow pg_sleep - # to get no selects - expect( - processes. - all_databases. - map(&:count_select_1_plus_2). - count { |instance_share| instance_share == 0 } - ).to eq(slow_query_count) - - # We also expect the quick queries to be spread across - # the idle servers only - processes. - all_databases. - map(&:count_select_1_plus_2). - reject { |instance_share| instance_share == 0 }. - each do |instance_share| - expect(instance_share).to be_within(expected_share * MARGIN_OF_ERROR).of(expected_share) - end - - threads.map(&:join) - end - end - - context "when some replicas are down" do - it "balances query volume between working instances" do - conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) - expected_share = QUERY_COUNT / (processes.all_databases.count - 2) - failed_count = 0 - - processes[:replicas][0].take_down do - processes[:replicas][1].take_down do - QUERY_COUNT.times do - conn.async_exec("SELECT 1 + 2") - rescue - conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) - failed_count += 1 - end - end - end - - expect(failed_count).to be <= 2 - processes.all_databases.each do |instance| - queries_routed = instance.count_select_1_plus_2 - if processes.replicas[0..1].include?(instance) - expect(queries_routed).to eq(0) - else - expect(queries_routed).to be_within(expected_share * MARGIN_OF_ERROR).of(expected_share) - end - end - end - end -end - diff --git a/tests/ruby/mirrors_spec.rb b/tests/ruby/mirrors_spec.rb deleted file mode 100644 index b6a4514..0000000 --- a/tests/ruby/mirrors_spec.rb +++ /dev/null @@ -1,91 +0,0 @@ -# frozen_string_literal: true -require 'uri' -require_relative 'spec_helper' - -describe "Query Mirroing" do - let(:processes) { Helpers::Pgcat.single_instance_setup("sharded_db", 10) } - let(:mirror_pg) { PgInstance.new(8432, "sharding_user", "sharding_user", "shard2")} - let(:pgcat_conn_str) { processes.pgcat.connection_string("sharded_db", "sharding_user") } - let(:mirror_host) { "localhost" } - - before do - new_configs = processes.pgcat.current_config - new_configs["pools"]["sharded_db"]["shards"]["0"]["mirrors"] = [ - [mirror_host, mirror_pg.port.to_i, 0], - [mirror_host, mirror_pg.port.to_i, 0], - [mirror_host, mirror_pg.port.to_i, 0], - ] - processes.pgcat.update_config(new_configs) - processes.pgcat.reload_config - end - - after do - processes.all_databases.map(&:reset) - mirror_pg.reset - processes.pgcat.shutdown - end - - 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") } - sleep 0.5 - expect(processes.all_databases.first.count_select_1_plus_2).to eq(runs) - # Allow some slack in mirroring successes - expect(mirror_pg.count_select_1_plus_2).to be > ((runs - 5) * 3) - end - - context "when main server connection is closed" do - it "closes the mirror connection" do - baseline_count = processes.all_databases.first.count_connections - 5.times do |i| - # Force pool cycling to detect zombie mirror connections - new_configs = processes.pgcat.current_config - new_configs["pools"]["sharded_db"]["idle_timeout"] = 5000 + i - new_configs["pools"]["sharded_db"]["shards"]["0"]["mirrors"] = [ - [mirror_host, mirror_pg.port.to_i, 0], - [mirror_host, mirror_pg.port.to_i, 0], - [mirror_host, mirror_pg.port.to_i, 0], - ] - processes.pgcat.update_config(new_configs) - processes.pgcat.reload_config - end - conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) - conn.async_exec("SELECT 1 + 2") - sleep 0.5 - # Expect same number of connection even after pool cycling - expect(processes.all_databases.first.count_connections).to be < baseline_count + 2 - end - end - - xcontext "when mirror server goes down temporarily" do - it "continues to transmit queries after recovery" do - conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) - mirror_pg.take_down do - conn.async_exec("SELECT 1 + 2") - sleep 0.1 - end - 10.times { conn.async_exec("SELECT 1 + 2") } - sleep 1 - expect(mirror_pg.count_select_1_plus_2).to be >= 2 - end - end - - context "when a mirror is down" do - let(:mirror_host) { "badhost" } - - it "does not fail to send the main query" do - conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) - # No Errors here - conn.async_exec("SELECT 1 + 2") - expect(processes.all_databases.first.count_select_1_plus_2).to eq(1) - end - - it "does not fail to send the main query (even after thousands of mirror attempts)" do - conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) - # No Errors here - 1000.times { conn.async_exec("SELECT 1 + 2") } - expect(processes.all_databases.first.count_select_1_plus_2).to eq(1000) - end - end -end diff --git a/tests/ruby/misc_spec.rb b/tests/ruby/misc_spec.rb deleted file mode 100644 index aa17e8e..0000000 --- a/tests/ruby/misc_spec.rb +++ /dev/null @@ -1,424 +0,0 @@ -# frozen_string_literal: true -require_relative 'spec_helper' - -describe "Miscellaneous" do - let(:processes) { Helpers::Pgcat.single_shard_setup("sharded_db", 5) } - after do - processes.all_databases.map(&:reset) - processes.pgcat.shutdown - end - - context "when adding then removing instance using RELOAD" do - it "works correctly" do - admin_conn = PG::connect(processes.pgcat.admin_connection_string) - - current_configs = processes.pgcat.current_config - correct_count = current_configs["pools"]["sharded_db"]["shards"]["0"]["servers"].count - expect(admin_conn.async_exec("SHOW DATABASES").count).to eq(correct_count) - - extra_replica = current_configs["pools"]["sharded_db"]["shards"]["0"]["servers"].last.clone - extra_replica[0] = "127.0.0.1" - current_configs["pools"]["sharded_db"]["shards"]["0"]["servers"] << extra_replica - - processes.pgcat.update_config(current_configs) # with replica added - processes.pgcat.reload_config - correct_count = current_configs["pools"]["sharded_db"]["shards"]["0"]["servers"].count - expect(admin_conn.async_exec("SHOW DATABASES").count).to eq(correct_count) - - current_configs["pools"]["sharded_db"]["shards"]["0"]["servers"].pop - - processes.pgcat.update_config(current_configs) # with replica removed again - processes.pgcat.reload_config - correct_count = current_configs["pools"]["sharded_db"]["shards"]["0"]["servers"].count - expect(admin_conn.async_exec("SHOW DATABASES").count).to eq(correct_count) - end - end - - context "when removing then adding instance back using RELOAD" do - it "works correctly" do - admin_conn = PG::connect(processes.pgcat.admin_connection_string) - - current_configs = processes.pgcat.current_config - correct_count = current_configs["pools"]["sharded_db"]["shards"]["0"]["servers"].count - expect(admin_conn.async_exec("SHOW DATABASES").count).to eq(correct_count) - - removed_replica = current_configs["pools"]["sharded_db"]["shards"]["0"]["servers"].pop - processes.pgcat.update_config(current_configs) # with replica removed - processes.pgcat.reload_config - correct_count = current_configs["pools"]["sharded_db"]["shards"]["0"]["servers"].count - expect(admin_conn.async_exec("SHOW DATABASES").count).to eq(correct_count) - - current_configs["pools"]["sharded_db"]["shards"]["0"]["servers"] << removed_replica - - processes.pgcat.update_config(current_configs) # with replica added again - processes.pgcat.reload_config - correct_count = current_configs["pools"]["sharded_db"]["shards"]["0"]["servers"].count - expect(admin_conn.async_exec("SHOW DATABASES").count).to eq(correct_count) - end - end - - describe "TCP Keepalives" do - # Ideally, we should block TCP traffic to the database using - # iptables to mimic passive (connection is dropped without a RST packet) - # but we cannot do this in CircleCI because iptables requires NET_ADMIN - # capability that we cannot enable in CircleCI - # Toxiproxy won't work either because it does not block keepalives - # so our best bet is to query the OS keepalive params set on the socket - - context "default settings" do - it "applies default keepalive settings" do - # We query ss command to verify that we have correct keepalive values set - # we can only verify the keepalives_idle parameter but that's good enough - # example output - #Recv-Q Send-Q Local Address:Port Peer Address:Port Process - #0 0 127.0.0.1:60526 127.0.0.1:18432 timer:(keepalive,1min59sec,0) - #0 0 127.0.0.1:60664 127.0.0.1:19432 timer:(keepalive,4.123ms,0) - - port_search_criteria = processes.all_databases.map { |d| "dport = :#{d.port}"}.join(" or ") - results = `ss -t4 state established -o -at '( #{port_search_criteria} )'`.lines - results.shift - results.each { |line| expect(line).to match(/timer:\(keepalive,.*ms,0\)/) } - end - end - - context "changed settings" do - it "applies keepalive settings from config" do - new_configs = processes.pgcat.current_config - - new_configs["general"]["tcp_keepalives_idle"] = 120 - new_configs["general"]["tcp_keepalives_count"] = 1 - new_configs["general"]["tcp_keepalives_interval"] = 1 - 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 - - port_search_criteria = processes.all_databases.map { |d| "dport = :#{d.port}"}.join(" or ") - results = `ss -t4 state established -o -at '( #{port_search_criteria} )'`.lines - results.shift - results.each { |line| expect(line).to include("timer:(keepalive,1min") } - end - end - end - - describe "Extended Protocol handling" do - it "does not send packets that client does not expect during extended protocol sequence" 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 - - 25.times do - Thread.new do - conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) - conn.async_exec("SELECT pg_sleep(5)") rescue PG::SystemError - ensure - conn&.close - end - end - - sleep(0.5) - conn_under_test = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) - stdout, stderr = with_captured_stdout_stderr do - 15.times do |i| - conn_under_test.async_exec("SELECT 1") rescue PG::SystemError - conn_under_test.exec_params("SELECT #{i} + $1", [i]) rescue PG::SystemError - sleep 1 - end - end - - raise StandardError, "Libpq got unexpected messages while idle" if stderr.include?("arrived from server while idle") - end - end - - describe "Pool recycling after config reload" do - let(:processes) { Helpers::Pgcat.three_shard_setup("sharded_db", 5) } - - it "should update pools for new clients and clients that are no longer in transaction" do - server_conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) - server_conn.async_exec("BEGIN") - - # No config change yet, client should set old configs - current_datebase_from_pg = server_conn.async_exec("SELECT current_database();")[0]["current_database"] - expect(current_datebase_from_pg).to eq('shard0') - - # Swap shards - new_config = processes.pgcat.current_config - shard0 = new_config["pools"]["sharded_db"]["shards"]["0"] - shard1 = new_config["pools"]["sharded_db"]["shards"]["1"] - new_config["pools"]["sharded_db"]["shards"]["0"] = shard1 - new_config["pools"]["sharded_db"]["shards"]["1"] = shard0 - - # Reload config - processes.pgcat.update_config(new_config) - processes.pgcat.reload_config - sleep 0.5 - - # Config changed but transaction is in progress, client should set old configs - current_datebase_from_pg = server_conn.async_exec("SELECT current_database();")[0]["current_database"] - expect(current_datebase_from_pg).to eq('shard0') - server_conn.async_exec("COMMIT") - - # Transaction finished, client should get new configs - current_datebase_from_pg = server_conn.async_exec("SELECT current_database();")[0]["current_database"] - expect(current_datebase_from_pg).to eq('shard1') - - # New connection should get new configs - server_conn.close() - server_conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) - current_datebase_from_pg = server_conn.async_exec("SELECT current_database();")[0]["current_database"] - expect(current_datebase_from_pg).to eq('shard1') - end - end - - describe "Clients closing connection in the middle of transaction" do - it "sends a rollback to the server" do - conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) - conn.async_exec("SET SERVER ROLE to 'primary'") - conn.async_exec("BEGIN") - conn.close - - expect(processes.primary.count_query("ROLLBACK")).to eq(1) - end - end - - describe "Server version reporting" do - it "reports correct version for normal and admin databases" do - server_conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) - expect(server_conn.server_version).not_to eq(0) - server_conn.close - - admin_conn = PG::connect(processes.pgcat.admin_connection_string) - expect(admin_conn.server_version).not_to eq(0) - admin_conn.close - end - end - - describe "State clearance" do - context "session mode" do - let(:processes) { Helpers::Pgcat.single_shard_setup("sharded_db", 5, "session") } - - it "Clears state before connection checkin" do - # Both modes of operation should not raise - # ERROR: prepared statement "prepared_q" already exists - 15.times do - conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) - conn.async_exec("PREPARE prepared_q (int) AS SELECT $1") - conn.close - end - - conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) - initial_value = conn.async_exec("SHOW statement_timeout")[0]["statement_timeout"] - conn.async_exec("SET statement_timeout to 1000") - current_value = conn.async_exec("SHOW statement_timeout")[0]["statement_timeout"] - expect(conn.async_exec("SHOW statement_timeout")[0]["statement_timeout"]).to eq("1s") - conn.close - end - - it "Does not send RESET ALL unless necessary" 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.close - end - - expect(processes.primary.count_query("RESET ALL")).to eq(0) - - 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 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 - let(:processes) { Helpers::Pgcat.single_shard_setup("sharded_db", 5, "transaction") } - it "Clears state before connection checkin" do - # Both modes of operation should not raise - # ERROR: prepared statement "prepared_q" already exists - 15.times do - conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) - conn.async_exec("PREPARE prepared_q (int) AS SELECT $1") - conn.close - end - - 15.times do - conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) - conn.prepare("prepared_q", "SELECT $1") - conn.close - end - end - - it "Does not send RESET ALL unless necessary" 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.exec_params("SELECT $1", [1]) - conn.close - end - - expect(processes.primary.count_query("RESET ALL")).to eq(0) - - 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 ALL")).to eq(10) - end - - it "Respects tracked parameters on startup" do - conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user", parameters: { "application_name" => "my_pgcat_test" })) - - expect(conn.async_exec("SHOW application_name")[0]["application_name"]).to eq("my_pgcat_test") - conn.close - end - - it "Respect tracked parameter on set statemet" do - conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) - - conn.async_exec("SET application_name to 'my_pgcat_test'") - expect(conn.async_exec("SHOW application_name")[0]["application_name"]).to eq("my_pgcat_test") - end - - - it "Ignore untracked parameter on set statemet" do - conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) - orignal_statement_timeout = conn.async_exec("SHOW statement_timeout")[0]["statement_timeout"] - - conn.async_exec("SET statement_timeout to 1500") - expect(conn.async_exec("SHOW statement_timeout")[0]["statement_timeout"]).to eq(orignal_statement_timeout) - end - - end - - context "transaction mode with transactions" do - let(:processes) { Helpers::Pgcat.single_shard_setup("sharded_db", 5, "transaction") } - it "Does not clear set statement state when declared in a transaction" 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("BEGIN") - conn.async_exec("SET statement_timeout to 1000") - conn.async_exec("COMMIT") - conn.close - end - expect(processes.primary.count_query("RESET ALL")).to eq(0) - - 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("BEGIN") - conn.async_exec("SET LOCAL statement_timeout to 1000") - conn.async_exec("COMMIT") - conn.close - end - expect(processes.primary.count_query("RESET 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 - - expect(processes.primary.count_query("RESET 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 - - expect(processes.primary.count_query("RESET ALL")).to eq(0) - end - end - end - - describe "Idle client timeout" do - context "idle transaction timeout set to 0" do - before do - current_configs = processes.pgcat.current_config - correct_idle_client_transaction_timeout = current_configs["general"]["idle_client_in_transaction_timeout"] - - current_configs["general"]["idle_client_in_transaction_timeout"] = 0 - - processes.pgcat.update_config(current_configs) # with timeout 0 - processes.pgcat.reload_config - end - - it "Allow client to be idle in transaction" do - conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) - conn.async_exec("BEGIN") - conn.async_exec("SELECT 1") - sleep(2) - conn.async_exec("COMMIT") - conn.close - end - end - - context "idle transaction timeout set to 500ms" do - before do - current_configs = processes.pgcat.current_config - correct_idle_client_transaction_timeout = current_configs["general"]["idle_client_in_transaction_timeout"] - current_configs["general"]["idle_client_in_transaction_timeout"] = 500 - - processes.pgcat.update_config(current_configs) # with timeout 500 - processes.pgcat.reload_config - end - - it "Allow client to be idle in transaction below timeout" do - conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) - conn.async_exec("BEGIN") - conn.async_exec("SELECT 1") - sleep(0.4) # below 500ms - conn.async_exec("COMMIT") - conn.close - end - - it "Error when client idle in transaction time exceeds timeout" do - conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) - conn.async_exec("BEGIN") - conn.async_exec("SELECT 1") - sleep(1) # above 500ms - expect{ conn.async_exec("COMMIT") }.to raise_error(PG::SystemError, /idle transaction timeout/) - conn.async_exec("SELECT 1") # should be able to send another query - conn.close - end - end - end -end diff --git a/tests/ruby/plugins_spec.rb b/tests/ruby/plugins_spec.rb deleted file mode 100644 index d4e233a..0000000 --- a/tests/ruby/plugins_spec.rb +++ /dev/null @@ -1,14 +0,0 @@ -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 deleted file mode 100644 index 8a31243..0000000 --- a/tests/ruby/prepared_spec.rb +++ /dev/null @@ -1,214 +0,0 @@ -require_relative 'spec_helper' - -describe 'Prepared statements' do - let(:pool_size) { 5 } - let(:processes) { Helpers::Pgcat.single_instance_setup("sharded_db", pool_size) } - let(:prepared_statements_cache_size) { 100 } - let(:server_round_robin) { false } - - before do - new_configs = processes.pgcat.current_config - new_configs["general"]["server_round_robin"] = server_round_robin - new_configs["pools"]["sharded_db"]["prepared_statements_cache_size"] = prepared_statements_cache_size - new_configs["pools"]["sharded_db"]["users"]["0"]["pool_size"] = pool_size - processes.pgcat.update_config(new_configs) - processes.pgcat.reload_config - end - - context 'when trying prepared statements' do - it 'it allows unparameterized statements to succeed' do - conn1 = PG.connect(processes.pgcat.connection_string('sharded_db', 'sharding_user')) - conn2 = PG.connect(processes.pgcat.connection_string('sharded_db', 'sharding_user')) - - prepared_query = "SELECT 1" - - # prepare query on server 1 and client 1 - conn1.prepare('statement1', prepared_query) - conn1.exec_prepared('statement1') - - conn2.transaction do - # Claim server 1 with client 2 - conn2.exec("SELECT 2") - - # Client 1 now runs the prepared query, and it's automatically - # prepared on server 2 - conn1.prepare('statement2', prepared_query) - conn1.exec_prepared('statement2') - - # Client 2 now prepares the same query that was already - # prepared on server 1. And PgBouncer reuses that already - # prepared query for this different client. - conn2.prepare('statement3', prepared_query) - conn2.exec_prepared('statement3') - end - ensure - conn1.close if conn1 - conn2.close if conn2 - end - - it 'it allows parameterized statements to succeed' do - conn1 = PG.connect(processes.pgcat.connection_string('sharded_db', 'sharding_user')) - conn2 = PG.connect(processes.pgcat.connection_string('sharded_db', 'sharding_user')) - - prepared_query = "SELECT $1" - - # prepare query on server 1 and client 1 - conn1.prepare('statement1', prepared_query) - conn1.exec_prepared('statement1', [1]) - - conn2.transaction do - # Claim server 1 with client 2 - conn2.exec("SELECT 2") - - # Client 1 now runs the prepared query, and it's automatically - # prepared on server 2 - conn1.prepare('statement2', prepared_query) - conn1.exec_prepared('statement2', [1]) - - # Client 2 now prepares the same query that was already - # prepared on server 1. And PgBouncer reuses that already - # prepared query for this different client. - conn2.prepare('statement3', prepared_query) - conn2.exec_prepared('statement3', [1]) - end - ensure - conn1.close if conn1 - conn2.close if conn2 - - end - end - - context 'when trying large packets' do - it "works with large parse" do - conn1 = PG.connect(processes.pgcat.connection_string('sharded_db', 'sharding_user')) - - long_string = "1" * 4096 * 10 - prepared_query = "SELECT '#{long_string}'" - - - # prepare query on server 1 and client 1 - conn1.prepare('statement1', prepared_query) - result = conn1.exec_prepared('statement1') - - # assert result matches long_string - expect(result.getvalue(0, 0)).to eq(long_string) - ensure - conn1.close if conn1 - end - - it "works with large bind" do - conn1 = PG.connect(processes.pgcat.connection_string('sharded_db', 'sharding_user')) - - long_string = "1" * 4096 * 10 - prepared_query = "SELECT $1::text" - - # prepare query on server 1 and client 1 - conn1.prepare('statement1', prepared_query) - result = conn1.exec_prepared('statement1', [long_string]) - - # assert result matches long_string - expect(result.getvalue(0, 0)).to eq(long_string) - ensure - conn1.close if conn1 - end - end - - context 'when statement cache is smaller than set of unqiue statements' do - let(:prepared_statements_cache_size) { 1 } - let(:pool_size) { 1 } - - it "evicts all but 1 statement from the server cache" do - conn = PG.connect(processes.pgcat.connection_string('sharded_db', 'sharding_user')) - - 5.times do |i| - prepared_query = "SELECT '#{i}'" - conn.prepare("statement#{i}", prepared_query) - result = conn.exec_prepared("statement#{i}") - expect(result.getvalue(0, 0)).to eq(i.to_s) - end - - # Check number of prepared statements (expected: 1) - n_statements = conn.exec("SELECT count(*) FROM pg_prepared_statements").getvalue(0, 0).to_i - expect(n_statements).to eq(1) - end - end - - context 'when statement cache is larger than set of unqiue statements' do - let(:pool_size) { 1 } - - it "does not evict any of the statements from the cache" do - # cache size 5 - conn = PG.connect(processes.pgcat.connection_string('sharded_db', 'sharding_user')) - - 5.times do |i| - prepared_query = "SELECT '#{i}'" - conn.prepare("statement#{i}", prepared_query) - result = conn.exec_prepared("statement#{i}") - expect(result.getvalue(0, 0)).to eq(i.to_s) - end - - # Check number of prepared statements (expected: 1) - n_statements = conn.exec("SELECT count(*) FROM pg_prepared_statements").getvalue(0, 0).to_i - expect(n_statements).to eq(5) - end - end - - context 'when preparing the same query' do - let(:prepared_statements_cache_size) { 5 } - let(:pool_size) { 5 } - - it "reuses statement cache when there are different statement names on 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]) - end - - # Check number of prepared statements (expected: 1) - n_statements = conn.exec("SELECT count(*) FROM pg_prepared_statements").getvalue(0, 0).to_i - expect(n_statements).to eq(1) - end - - it "reuses statement cache when there are different statement names on different connections" do - 10.times do |i| - conn = PG.connect(processes.pgcat.connection_string('sharded_db', 'sharding_user')) - statement_name = "statement_#{i}" - conn.prepare(statement_name, 'SELECT $1::int') - conn.exec_prepared(statement_name, [1]) - end - - # Check number of prepared statements (expected: 1) - conn = PG.connect(processes.pgcat.connection_string('sharded_db', 'sharding_user')) - n_statements = conn.exec("SELECT count(*) FROM pg_prepared_statements").getvalue(0, 0).to_i - expect(n_statements).to eq(1) - end - end - - context 'when reloading config' do - let(:pool_size) { 1 } - - it "test_reload_config" do - conn = PG.connect(processes.pgcat.connection_string('sharded_db', 'sharding_user')) - - # prepare query - conn.prepare('statement1', 'SELECT 1') - conn.exec_prepared('statement1') - - # Reload config which triggers pool recreation - new_configs = processes.pgcat.current_config - new_configs["pools"]["sharded_db"]["prepared_statements_cache_size"] = prepared_statements_cache_size + 1 - processes.pgcat.update_config(new_configs) - processes.pgcat.reload_config - - # check that we're starting with no prepared statements on the server - conn_check = PG.connect(processes.pgcat.connection_string('sharded_db', 'sharding_user')) - n_statements = conn_check.exec("SELECT count(*) FROM pg_prepared_statements").getvalue(0, 0).to_i - expect(n_statements).to eq(0) - - # still able to run prepared query - conn.exec_prepared('statement1') - end - end -end diff --git a/tests/ruby/protocol_spec.rb b/tests/ruby/protocol_spec.rb deleted file mode 100644 index 9737650..0000000 --- a/tests/ruby/protocol_spec.rb +++ /dev/null @@ -1,155 +0,0 @@ -# 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/routing_spec.rb b/tests/ruby/routing_spec.rb deleted file mode 100644 index 24ea137..0000000 --- a/tests/ruby/routing_spec.rb +++ /dev/null @@ -1,81 +0,0 @@ -# frozen_string_literal: true -require_relative 'spec_helper' - - -describe "Routing" do - let(:processes) { Helpers::Pgcat.single_shard_setup("sharded_db", 5) } - after do - processes.all_databases.map(&:reset) - processes.pgcat.shutdown - end - - describe "SET ROLE" do - context "primary" do - it "routes queries only to primary" do - conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) - conn.async_exec("SET SERVER ROLE to 'primary'") - - query_count = 30 - failed_count = 0 - - query_count.times do - conn.async_exec("SELECT 1 + 2") - rescue - failed_count += 1 - end - - expect(failed_count).to eq(0) - processes.replicas.map(&:count_select_1_plus_2).each do |instance_share| - expect(instance_share).to eq(0) - end - - expect(processes.primary.count_select_1_plus_2).to eq(query_count) - end - end - context "replica" do - it "routes queries only to replicas" do - conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) - conn.async_exec("SET SERVER ROLE to 'replica'") - - expected_share = QUERY_COUNT / processes.replicas.count - failed_count = 0 - - QUERY_COUNT.times do - conn.async_exec("SELECT 1 + 2") - rescue - failed_count += 1 - end - - expect(failed_count).to eq(0) - - processes.replicas.map(&:count_select_1_plus_2).each do |instance_share| - expect(instance_share).to be_within(expected_share * MARGIN_OF_ERROR).of(expected_share) - end - - expect(processes.primary.count_select_1_plus_2).to eq(0) - end - end - - context "any" do - it "routes queries to all instances" do - conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) - conn.async_exec("SET SERVER ROLE to 'any'") - - expected_share = QUERY_COUNT / processes.all_databases.count - failed_count = 0 - - QUERY_COUNT.times do - conn.async_exec("SELECT 1 + 2") - rescue - failed_count += 1 - end - - expect(failed_count).to eq(0) - - processes.all_databases.map(&:count_select_1_plus_2).each do |instance_share| - expect(instance_share).to be_within(expected_share * MARGIN_OF_ERROR).of(expected_share) - end - end - end - end -end diff --git a/tests/ruby/sharding_spec.rb b/tests/ruby/sharding_spec.rb deleted file mode 100644 index 746627d..0000000 --- a/tests/ruby/sharding_spec.rb +++ /dev/null @@ -1,196 +0,0 @@ -# frozen_string_literal: true -require_relative 'spec_helper' - - -describe "Sharding" do - let(:processes) { Helpers::Pgcat.three_shard_setup("sharded_db", 5) } - - before do - conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) - # Setup the sharding data - 3.times do |i| - conn.exec("SET SHARD TO '#{i}'") - - conn.exec("DELETE FROM data WHERE id > 0") rescue nil - end - - 18.times do |i| - i = i + 1 - conn.exec("SET SHARDING KEY TO '#{i}'") - conn.exec("INSERT INTO data (id, value) VALUES (#{i}, 'value_#{i}')") - end - - conn.close - end - - after do - processes.all_databases.map(&:reset) - processes.pgcat.shutdown - end - - 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'") - - 18.times do |i| - result = conn.exec_params("SELECT * FROM data WHERE id = $1", [i + 1]) - expect(result.ntuples).to eq(1) - end - end - - it "can do it with multiple parameters" do - conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) - conn.exec("SET SERVER ROLE TO 'auto'") - - 18.times do |i| - result = conn.exec_params("SELECT * FROM data WHERE id = $1 AND id = $2", [i + 1, i + 1]) - expect(result.ntuples).to eq(1) - end - end - end - - describe "no_shard_specified_behavior config" do - context "when default shard number is invalid" do - it "prevents config reload" do - admin_conn = PG::connect(processes.pgcat.admin_connection_string) - - current_configs = processes.pgcat.current_config - current_configs["pools"]["sharded_db"]["default_shard"] = "shard_99" - - processes.pgcat.update_config(current_configs) - - expect { processes.pgcat.reload_config }.to raise_error(ConfigReloadFailed, /Invalid shard 99/) - end - end - end - - describe "comment-based routing" do - context "when no configs are set" do - it "routes queries with a shard_id comment to the default shard" do - conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) - 10.times { conn.async_exec("/* shard_id: 2 */ SELECT 1 + 2") } - - expect(processes.all_databases.map(&:count_select_1_plus_2)).to eq([10, 0, 0]) - end - - it "does not honor no_shard_specified_behavior directives" do - end - end - - [ - ["shard_id_regex", "/\\* the_shard_id: (\\d+) \\*/", "/* the_shard_id: 1 */"], - ["sharding_key_regex", "/\\* the_sharding_key: (\\d+) \\*/", "/* the_sharding_key: 3 */"], - ].each do |config_name, config_value, comment_to_use| - context "when #{config_name} config is set" do - let(:no_shard_specified_behavior) { nil } - - before do - admin_conn = PG::connect(processes.pgcat.admin_connection_string) - - current_configs = processes.pgcat.current_config - current_configs["pools"]["sharded_db"][config_name] = config_value - if no_shard_specified_behavior - current_configs["pools"]["sharded_db"]["default_shard"] = no_shard_specified_behavior - else - current_configs["pools"]["sharded_db"].delete("default_shard") - end - - processes.pgcat.update_config(current_configs) - processes.pgcat.reload_config - end - - it "routes queries with a shard_id comment to the correct shard" do - conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) - 25.times { conn.async_exec("#{comment_to_use} SELECT 1 + 2") } - - expect(processes.all_databases.map(&:count_select_1_plus_2)).to eq([0, 25, 0]) - end - - context "when no_shard_specified_behavior config is set to random" do - let(:no_shard_specified_behavior) { "random" } - - context "with no shard comment" do - it "sends queries to random shard" do - conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) - 25.times { conn.async_exec("SELECT 1 + 2") } - - expect(processes.all_databases.map(&:count_select_1_plus_2).all?(&:positive?)).to be true - end - end - - context "with a shard comment" do - it "honors the comment" do - conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) - 25.times { conn.async_exec("#{comment_to_use} SELECT 1 + 2") } - - expect(processes.all_databases.map(&:count_select_1_plus_2)).to eq([0, 25, 0]) - end - end - end - - context "when no_shard_specified_behavior config is set to random_healthy" do - let(:no_shard_specified_behavior) { "random_healthy" } - - context "with no shard comment" do - it "sends queries to random healthy shard" do - - good_databases = [processes.all_databases[0], processes.all_databases[2]] - bad_database = processes.all_databases[1] - conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) - 250.times { conn.async_exec("SELECT 99") } - bad_database.take_down do - 250.times do - conn.async_exec("SELECT 99") - rescue PG::ConnectionBad => e - conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) - end - end - - # Routes traffic away from bad shard - 25.times { conn.async_exec("SELECT 1 + 2") } - expect(good_databases.map(&:count_select_1_plus_2).all?(&:positive?)).to be true - expect(bad_database.count_select_1_plus_2).to eq(0) - - # Routes traffic to the bad shard if the shard_id is specified - 25.times { conn.async_exec("#{comment_to_use} SELECT 1 + 2") } - bad_database = processes.all_databases[1] - expect(bad_database.count_select_1_plus_2).to eq(25) - end - end - - context "with a shard comment" do - it "honors the comment" do - conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) - 25.times { conn.async_exec("#{comment_to_use} SELECT 1 + 2") } - - expect(processes.all_databases.map(&:count_select_1_plus_2)).to eq([0, 25, 0]) - end - end - end - - context "when no_shard_specified_behavior config is set to shard_x" do - let(:no_shard_specified_behavior) { "shard_2" } - - context "with no shard comment" do - it "sends queries to the specified shard" do - conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) - 25.times { conn.async_exec("SELECT 1 + 2") } - - expect(processes.all_databases.map(&:count_select_1_plus_2)).to eq([0, 0, 25]) - end - end - - context "with a shard comment" do - it "honors the comment" do - conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user")) - 25.times { conn.async_exec("#{comment_to_use} SELECT 1 + 2") } - - expect(processes.all_databases.map(&:count_select_1_plus_2)).to eq([0, 25, 0]) - end - end - end - end - end - end -end diff --git a/tests/ruby/spec_helper.rb b/tests/ruby/spec_helper.rb deleted file mode 100644 index a95969f..0000000 --- a/tests/ruby/spec_helper.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -require 'pg' -require_relative 'helpers/pgcat_helper' - -QUERY_COUNT = 300 -MARGIN_OF_ERROR = 0.35 - -def with_captured_stdout_stderr - sout = STDOUT.clone - serr = STDERR.clone - STDOUT.reopen("/tmp/out.txt", "w+") - STDERR.reopen("/tmp/err.txt", "w+") - STDOUT.sync = true - STDERR.sync = true - yield - return File.read('/tmp/out.txt'), File.read('/tmp/err.txt') -ensure - 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 deleted file mode 100644 index 8a683a0..0000000 --- a/tests/ruby/stats_spec.rb +++ /dev/null @@ -1,406 +0,0 @@ -# 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].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["maxwait"]).to eq("1") - 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 = [] - admin_conn = PG::connect(processes.pgcat.admin_connection_string) - 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(1.1) - results = admin_conn.async_exec("SHOW POOLS")[0] - # Value is only reported when there are clients waiting - expect(results["maxwait"]).to eq("1") - expect(results["maxwait_us"].to_i).to be_within(20_000).of(100_000) - - sleep(2.5) # Allow time for stats to update - results = admin_conn.async_exec("SHOW POOLS")[0] - # no clients are waiting so value is 0 - expect(results["maxwait"]).to eq("0") - expect(results["maxwait_us"]).to eq("0") - connections.map(&:close) - - 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 - - context "when client has waited for a server" do - let(:processes) { Helpers::Pgcat.single_instance_setup("sharded_db", 2) } - - it "shows correct maxwait" do - threads = [] - connections = Array.new(3) { |i| PG::connect("#{pgcat_conn_str}?application_name=app#{i}") } - 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 CLIENTS") - - normal_client_results = results.reject { |r| r["database"] == "pgcat" } - - non_waiting_clients = normal_client_results.select { |c| c["maxwait"] == "0" } - waiting_clients = normal_client_results.select { |c| c["maxwait"].to_i > 0 } - - expect(non_waiting_clients.count).to eq(2) - non_waiting_clients.each do |client| - expect(client["maxwait_us"].to_i).to be_between(0, 50_000) - end - - expect(waiting_clients.count).to eq(1) - waiting_clients.each do |client| - expect(client["maxwait_us"].to_i).to be_within(200_000).of(500_000) - end - - admin_conn.close - connections.map(&:close) - end - 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/ruby/tests.rb b/tests/ruby/tests.rb deleted file mode 100644 index 1c18e15..0000000 --- a/tests/ruby/tests.rb +++ /dev/null @@ -1,113 +0,0 @@ -# frozen_string_literal: true -require 'pg' -require 'active_record' - -# Uncomment these two to see all queries. -# ActiveRecord.verbose_query_logs = true -# ActiveRecord::Base.logger = Logger.new(STDOUT) - -ActiveRecord::Base.establish_connection( - adapter: 'postgresql', - host: '127.0.0.1', - port: 6432, - username: 'sharding_user', - password: 'sharding_user', - database: 'sharded_db', - application_name: 'testing_pgcat', - prepared_statements: false, # Transaction mode - advisory_locks: false # Same -) - -class TestSafeTable < ActiveRecord::Base - self.table_name = 'test_safe_table' -end - -class ShouldNeverHappenException < RuntimeError -end - -class CreateSafeShardedTable < ActiveRecord::Migration[7.0] - # Disable transasctions or things will fly out of order! - disable_ddl_transaction! - - SHARDS = 3 - - def up - SHARDS.times do |x| - # This will make this migration reversible! - connection.execute "SET SHARD TO '#{x.to_i}'" - connection.execute "SET SERVER ROLE TO 'primary'" - - connection.execute <<-SQL - CREATE TABLE test_safe_table ( - id BIGINT PRIMARY KEY, - name VARCHAR, - description TEXT - ) PARTITION BY HASH (id); - - CREATE TABLE test_safe_table_data PARTITION OF test_safe_table - FOR VALUES WITH (MODULUS #{SHARDS.to_i}, REMAINDER #{x.to_i}); - SQL - end - end - - def down - SHARDS.times do |x| - connection.execute "SET SHARD TO '#{x.to_i}'" - connection.execute "SET SERVER ROLE TO 'primary'" - connection.execute 'DROP TABLE test_safe_table CASCADE' - end - end -end - -SHARDS = 3 - -2.times do - begin - CreateSafeShardedTable.migrate(:down) - rescue Exception - puts "Tables don't exist yet" - end - - CreateSafeShardedTable.migrate(:up) - - SHARDS.times do |x| - TestSafeTable.connection.execute "SET SHARD TO '#{x.to_i}'" - TestSafeTable.connection.execute "SET SERVER ROLE TO 'primary'" - TestSafeTable.connection.execute "TRUNCATE #{TestSafeTable.table_name}" - end - - # Equivalent to Makara's stick_to_master! except it sticks until it's changed. - TestSafeTable.connection.execute "SET SERVER ROLE TO 'primary'" - - 200.times do |x| - x += 1 # Postgres ids start at 1 - TestSafeTable.connection.execute "SET SHARDING KEY TO '#{x.to_i}'" - TestSafeTable.create(id: x, name: "something_special_#{x.to_i}", description: "It's a surprise!") - end - - TestSafeTable.connection.execute "SET SERVER ROLE TO 'replica'" - - 100.times do |x| - x += 1 # 0 confuses our sharding function - TestSafeTable.connection.execute "SET SHARDING KEY TO '#{x.to_i}'" - TestSafeTable.find_by_id(x).id - end - - # Will use the query parser to direct reads to replicas - TestSafeTable.connection.execute "SET SERVER ROLE TO 'auto'" - - 100.times do |x| - x += 101 - TestSafeTable.connection.execute "SET SHARDING KEY TO '#{x.to_i}'" - TestSafeTable.find_by_id(x).id - end -end - -# Test wrong shard -TestSafeTable.connection.execute "SET SHARD TO '1'" -begin - TestSafeTable.create(id: 5, name: 'test', description: 'test description') - raise ShouldNeverHappenException('Uh oh') -rescue ActiveRecord::StatementInvalid - puts 'OK' -end diff --git a/tests/rust/.gitignore b/tests/rust/.gitignore deleted file mode 100644 index 2f7896d..0000000 --- a/tests/rust/.gitignore +++ /dev/null @@ -1 +0,0 @@ -target/ diff --git a/tests/rust/Cargo.lock b/tests/rust/Cargo.lock deleted file mode 100644 index 04dcb00..0000000 --- a/tests/rust/Cargo.lock +++ /dev/null @@ -1,1490 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "addr2line" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - -[[package]] -name = "ahash" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" -dependencies = [ - "getrandom", - "once_cell", - "version_check", -] - -[[package]] -name = "ahash" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - -[[package]] -name = "allocator-api2" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" - -[[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.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" - -[[package]] -name = "backtrace" -version = "0.3.73" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" -dependencies = [ - "addr2line", - "cc", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", -] - -[[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.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" - -[[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.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" - -[[package]] -name = "cc" -version = "1.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6" -dependencies = [ - "shlex", -] - -[[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.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad" -dependencies = [ - "libc", -] - -[[package]] -name = "crc" -version = "3.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" -dependencies = [ - "crc-catalog", -] - -[[package]] -name = "crc-catalog" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" - -[[package]] -name = "crossbeam-queue" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" - -[[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 = "deranged" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" -dependencies = [ - "powerfmt", -] - -[[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.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" - -[[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.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "futures-channel" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" - -[[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.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" - -[[package]] -name = "futures-task" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" - -[[package]] -name = "futures-util" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" -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.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "gimli" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" - -[[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.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash 0.8.11", - "allocator-api2", -] - -[[package]] -name = "hashlink" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" -dependencies = [ - "hashbrown 0.14.5", -] - -[[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.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - -[[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.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -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.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" -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.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "ipnetwork" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f84f1612606f3753f205a4e9a2efd6fe5b4c573a6269b2cc6c3003d44a0d127" - -[[package]] -name = "itoa" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" - -[[package]] -name = "js-sys" -version = "0.3.70" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "libc" -version = "0.2.158" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" - -[[package]] -name = "libredox" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" -dependencies = [ - "bitflags 2.6.0", - "libc", -] - -[[package]] -name = "lock_api" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" - -[[package]] -name = "md-5" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" -dependencies = [ - "cfg-if", - "digest", -] - -[[package]] -name = "memchr" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "miniz_oxide" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" -dependencies = [ - "adler", -] - -[[package]] -name = "mio" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" -dependencies = [ - "hermit-abi", - "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-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "object" -version = "0.36.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" - -[[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.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" -dependencies = [ - "lock_api", - "parking_lot_core 0.9.10", -] - -[[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.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall 0.5.3", - "smallvec", - "windows-targets", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "percent-encoding" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" - -[[package]] -name = "pin-project-lite" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "proc-macro2" -version = "1.0.86" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" -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 1.3.2", -] - -[[package]] -name = "redox_syscall" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_syscall" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" -dependencies = [ - "bitflags 2.6.0", -] - -[[package]] -name = "redox_users" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" -dependencies = [ - "getrandom", - "libredox", - "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 0.5.2", - "untrusted 0.7.1", - "web-sys", - "winapi", -] - -[[package]] -name = "ring" -version = "0.17.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" -dependencies = [ - "cc", - "cfg-if", - "getrandom", - "libc", - "spin 0.9.8", - "untrusted 0.9.0", - "windows-sys", -] - -[[package]] -name = "rust" -version = "0.1.0" -dependencies = [ - "sqlx", - "tokio", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" - -[[package]] -name = "rustls" -version = "0.20.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99" -dependencies = [ - "log", - "ring 0.16.20", - "sct", - "webpki", -] - -[[package]] -name = "rustls-pemfile" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" -dependencies = [ - "base64 0.21.7", -] - -[[package]] -name = "ryu" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring 0.17.8", - "untrusted 0.9.0", -] - -[[package]] -name = "serde" -version = "1.0.209" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.209" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.77", -] - -[[package]] -name = "serde_json" -version = "1.0.127" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha2" -version = "0.10.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "signal-hook-registry" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" -dependencies = [ - "libc", -] - -[[package]] -name = "smallvec" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" - -[[package]] -name = "socket2" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" -dependencies = [ - "libc", - "windows-sys", -] - -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - -[[package]] -name = "sqlformat" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f895e3734318cc55f1fe66258926c9b910c124d47520339efecbb6c59cec7c1f" -dependencies = [ - "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.8", - "atoi", - "base64 0.13.1", - "bitflags 1.3.2", - "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.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" -dependencies = [ - "unicode-bidi", - "unicode-normalization", - "unicode-properties", -] - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[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.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "thiserror" -version = "1.0.63" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.63" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.77", -] - -[[package]] -name = "time" -version = "0.3.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" - -[[package]] -name = "time-macros" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tinyvec" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" -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.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" -dependencies = [ - "backtrace", - "bytes", - "libc", - "mio", - "parking_lot 0.12.3", - "pin-project-lite", - "signal-hook-registry", - "socket2", - "tokio-macros", - "windows-sys", -] - -[[package]] -name = "tokio-macros" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.77", -] - -[[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.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "typenum" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" - -[[package]] -name = "unicode-bidi" -version = "0.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" - -[[package]] -name = "unicode-ident" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" - -[[package]] -name = "unicode-normalization" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "unicode-properties" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ea75f83c0137a9b98608359a5f1af8144876eb67bcb1ce837368e906a9f524" - -[[package]] -name = "unicode-segmentation" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" - -[[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 = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "url" -version = "2.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", -] - -[[package]] -name = "uuid" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "wasite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" - -[[package]] -name = "wasm-bindgen" -version = "0.2.93" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" -dependencies = [ - "cfg-if", - "once_cell", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.93" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn 2.0.77", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.93" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.93" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.77", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.93" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" - -[[package]] -name = "web-sys" -version = "0.3.70" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webpki" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" -dependencies = [ - "ring 0.17.8", - "untrusted 0.9.0", -] - -[[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.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" -dependencies = [ - "redox_syscall 0.4.1", - "wasite", - "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.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "zerocopy" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" -dependencies = [ - "byteorder", - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.77", -] diff --git a/tests/rust/Cargo.toml b/tests/rust/Cargo.toml deleted file mode 100644 index 1600dac..0000000 --- a/tests/rust/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[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 deleted file mode 100644 index 958d75d..0000000 --- a/tests/rust/src/main.rs +++ /dev/null @@ -1,34 +0,0 @@ -#[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 i in 0..1000 { - match sqlx::query(&format!("SELECT {:?}", i % 5)).fetch_all(&pool).await { - Ok(_) => (), - Err(err) => { - panic!("prepared statement error: {}", err); - } - } - } - }); - - handles.push(handle); - } - - for handle in handles { - handle.await.unwrap(); - } -} diff --git a/tests/sharding/README.md b/tests/sharding/README.md deleted file mode 100644 index abb908d..0000000 --- a/tests/sharding/README.md +++ /dev/null @@ -1,35 +0,0 @@ -# Sharding tests - -This helps us test the sharding algorithm we implemented. - - -## Setup - -We setup 3 Postgres DBs, `shard0`, `shard1`, and `shard2`. In each database, we create a partitioned table called `data`. The table is partitioned by hash, and each database will only have _one_ partition, `shard0` will satisfy `modulus 3, remainder 0`, `shard1` will satisfy `modulus 3, remainder 1`, etc. - -To set this up, you can just run: - -```bash -psql -f query_routing_setup.sql -``` - -## Run the tests - -Start up PgCat by running `cargo run --release` in the root of the repo. In a different tab, run this: - -```bash -psql -h 127.0.0.1 -p 6432 -f query_routing_test_insert.sql -psql -h 127.0.0.1 -p 6432 -f query_routing_test_select.sql -``` - -Note that no errors should take place. If our sharding logic was incorrect, we would get some errors -about unsatisfiable partition bounds. We don't because the pooler picked the correct databases -given the sharding keys. - -Finally, you can validate the result again by running - -```bash -psql -f query_routing_test_validate.sql -``` - -## That's it! \ No newline at end of file diff --git a/tests/sharding/partition_hash_test_setup.sql b/tests/sharding/partition_hash_test_setup.sql deleted file mode 100644 index 5e91020..0000000 --- a/tests/sharding/partition_hash_test_setup.sql +++ /dev/null @@ -1,26 +0,0 @@ -DROP TABLE IF EXISTS shards CASCADE; - -CREATE TABLE shards ( - id BIGINT, - value VARCHAR -) PARTITION BY HASH (id); - --- DROP TABLE IF EXISTS shard_0; -CREATE TABLE shard_0 PARTITION OF shards FOR VALUES WITH (MODULUS 5, REMAINDER 0); --- DROP TABLE IF EXISTS shard_1; -CREATE TABLE shard_1 PARTITION OF shards FOR VALUES WITH (MODULUS 5, REMAINDER 1); --- DROP TABLE IF EXISTS shard_2; -CREATE TABLE shard_2 PARTITION OF shards FOR VALUES WITH (MODULUS 5, REMAINDER 2); --- DROP TABLE IF EXISTS shard_3; -CREATE TABLE shard_3 PARTITION OF shards FOR VALUES WITH (MODULUS 5, REMAINDER 3); --- DROP TABLE IF EXISTS shard_4; -CREATE TABLE shard_4 PARTITION OF shards FOR VALUES WITH (MODULUS 5, REMAINDER 4); - - -INSERT INTO shards SELECT generate_series(1, 500), 'value'; - -SELECT * FROM shard_0 ORDER BY id LIMIT 10; -SELECT * FROM shard_1 ORDER BY id LIMIT 10; -SELECT * FROM shard_2 ORDER BY id LIMIT 10; -SELECT * FROM shard_3 ORDER BY id LIMIT 10; -SELECT * FROM shard_4 ORDER BY id LIMIT 10; diff --git a/tests/sharding/query_routing.sh b/tests/sharding/query_routing.sh deleted file mode 100644 index d6098fa..0000000 --- a/tests/sharding/query_routing.sh +++ /dev/null @@ -1,19 +0,0 @@ -#/bin/bash -set -e - -# Setup all the shards. -# sudo service postgresql restart - -echo "Giving Postgres 5 seconds to start up..." - -# sleep 5 - -# psql -f query_routing_setup.sql - -psql -h 127.0.0.1 -p 6432 -f query_routing_test_insert.sql - -psql -h 127.0.0.1 -p 6432 -f query_routing_test_select.sql - -psql -e -h 127.0.0.1 -p 6432 -f query_routing_test_primary_replica.sql - -psql -f query_routing_test_validate.sql diff --git a/tests/sharding/query_routing_setup.sql b/tests/sharding/query_routing_setup.sql deleted file mode 100644 index c25ac18..0000000 --- a/tests/sharding/query_routing_setup.sql +++ /dev/null @@ -1,104 +0,0 @@ -DROP DATABASE IF EXISTS shard0; -DROP DATABASE IF EXISTS shard1; -DROP DATABASE IF EXISTS shard2; -DROP DATABASE IF EXISTS some_db; - -CREATE DATABASE shard0; -CREATE DATABASE shard1; -CREATE DATABASE shard2; -CREATE DATABASE some_db; - -\c shard0 - -DROP TABLE IF EXISTS data CASCADE; - -CREATE TABLE data ( - id BIGINT, - value VARCHAR -) PARTITION BY HASH (id); - -CREATE TABLE data_shard_0 PARTITION OF data FOR VALUES WITH (MODULUS 3, REMAINDER 0); - -\c shard1 - -DROP TABLE IF EXISTS data CASCADE; - -CREATE TABLE data ( - id BIGINT, - value VARCHAR -) PARTITION BY HASH (id); - -CREATE TABLE data_shard_1 PARTITION OF data FOR VALUES WITH (MODULUS 3, REMAINDER 1); - - -\c shard2 - -DROP TABLE IF EXISTS data CASCADE; - -CREATE TABLE data ( - id BIGINT, - value VARCHAR -) PARTITION BY HASH (id); - -CREATE TABLE data_shard_2 PARTITION OF data FOR VALUES WITH (MODULUS 3, REMAINDER 2); - - -\c some_db - -DROP TABLE IF EXISTS data CASCADE; - -CREATE TABLE data ( - id BIGINT, - value VARCHAR -); - -DROP ROLE IF EXISTS sharding_user; -DROP ROLE IF EXISTS other_user; -DROP ROLE IF EXISTS simple_user; -CREATE ROLE sharding_user ENCRYPTED PASSWORD 'sharding_user' LOGIN; -CREATE ROLE other_user ENCRYPTED PASSWORD 'other_user' LOGIN; -CREATE ROLE simple_user ENCRYPTED PASSWORD 'simple_user' LOGIN; - -GRANT CONNECT ON DATABASE shard0 TO sharding_user; -GRANT CONNECT ON DATABASE shard1 TO sharding_user; -GRANT CONNECT ON DATABASE shard2 TO sharding_user; - -GRANT CONNECT ON DATABASE shard0 TO other_user; -GRANT CONNECT ON DATABASE shard1 TO other_user; -GRANT CONNECT ON DATABASE shard2 TO other_user; - -GRANT CONNECT ON DATABASE some_db TO simple_user; - -\c shard0 -CREATE EXTENSION IF NOT EXISTS pg_stat_statements; -GRANT EXECUTE ON FUNCTION pg_stat_statements_reset TO sharding_user; -GRANT ALL ON SCHEMA public TO sharding_user; -GRANT ALL ON TABLE data TO sharding_user; -GRANT ALL ON SCHEMA public TO other_user; -GRANT ALL ON TABLE data TO other_user; -GRANT EXECUTE ON FUNCTION pg_stat_statements_reset TO other_user; - -\c shard1 -CREATE EXTENSION IF NOT EXISTS pg_stat_statements; -GRANT EXECUTE ON FUNCTION pg_stat_statements_reset TO sharding_user; -GRANT ALL ON SCHEMA public TO sharding_user; -GRANT ALL ON TABLE data TO sharding_user; -GRANT ALL ON SCHEMA public TO other_user; -GRANT ALL ON TABLE data TO other_user; -GRANT EXECUTE ON FUNCTION pg_stat_statements_reset TO other_user; - - -\c shard2 -CREATE EXTENSION IF NOT EXISTS pg_stat_statements; -GRANT EXECUTE ON FUNCTION pg_stat_statements_reset TO sharding_user; -GRANT ALL ON SCHEMA public TO sharding_user; -GRANT ALL ON TABLE data TO sharding_user; -GRANT ALL ON SCHEMA public TO other_user; -GRANT ALL ON TABLE data TO other_user; -GRANT EXECUTE ON FUNCTION pg_stat_statements_reset TO other_user; - -\c some_db -CREATE EXTENSION IF NOT EXISTS pg_stat_statements; -GRANT EXECUTE ON FUNCTION pg_stat_statements_reset TO simple_user; -GRANT ALL ON SCHEMA public TO simple_user; -GRANT ALL ON TABLE data TO simple_user; diff --git a/tests/sharding/query_routing_test_insert.sql b/tests/sharding/query_routing_test_insert.sql deleted file mode 100644 index ff8a326..0000000 --- a/tests/sharding/query_routing_test_insert.sql +++ /dev/null @@ -1,55 +0,0 @@ -\set ON_ERROR_STOP on - -SET SHARDING KEY TO '1'; -INSERT INTO data (id, value) VALUES (1, 'value_1'); - -SET SHARDING KEY TO '2'; -INSERT INTO data (id, value) VALUES (2, 'value_1'); - -SET SHARDING KEY TO '3'; -INSERT INTO data (id, value) VALUES (3, 'value_1'); - -SET SHARDING KEY TO '4'; -INSERT INTO data (id, value) VALUES (4, 'value_1'); - -SET SHARDING KEY TO '5'; -INSERT INTO data (id, value) VALUES (5, 'value_1'); - -SET SHARDING KEY TO '6'; -INSERT INTO data (id, value) VALUES (6, 'value_1'); - -SET SHARDING KEY TO '7'; -INSERT INTO data (id, value) VALUES (7, 'value_1'); - -SET SHARDING KEY TO '8'; -INSERT INTO data (id, value) VALUES (8, 'value_1'); - -SET SHARDING KEY TO '9'; -INSERT INTO data (id, value) VALUES (9, 'value_1'); - -SET SHARDING KEY TO '10'; -INSERT INTO data (id, value) VALUES (10, 'value_1'); - -SET SHARDING KEY TO '11'; -INSERT INTO data (id, value) VALUES (11, 'value_1'); - -SET SHARDING KEY TO '12'; -INSERT INTO data (id, value) VALUES (12, 'value_1'); - -SET SHARDING KEY TO '13'; -INSERT INTO data (id, value) VALUES (13, 'value_1'); - -SET SHARDING KEY TO '14'; -INSERT INTO data (id, value) VALUES (14, 'value_1'); - -SET SHARDING KEY TO '15'; -INSERT INTO data (id, value) VALUES (15, 'value_1'); - -SET SHARDING KEY TO '16'; -INSERT INTO data (id, value) VALUES (16, 'value_1'); - -set sharding key to '17'; -INSERT INTO data (id, value) VALUES (17, 'value_1'); - -SeT SHaRDInG KeY to '18'; -INSERT INTO data (id, value) VALUES (18, 'value_1'); diff --git a/tests/sharding/query_routing_test_primary_replica.sql b/tests/sharding/query_routing_test_primary_replica.sql deleted file mode 100644 index 5fe3cbe..0000000 --- a/tests/sharding/query_routing_test_primary_replica.sql +++ /dev/null @@ -1,162 +0,0 @@ -\set ON_ERROR_STOP on - -SET SERVER ROLE TO 'primary'; -SET SHARDING KEY TO '1'; -INSERT INTO data (id, value) VALUES (1, 'value_1'); - -SET SERVER ROLE TO 'replica'; -SET SHARDING KEY TO '1'; -SELECT * FROM data WHERE id = 1; - ---- - -SET SERVER ROLE TO 'primary'; -SET SHARDING KEY TO '2'; -INSERT INTO data (id, value) VALUES (2, 'value_1'); - -SET SERVER ROLE TO 'replica'; -SET SHARDING KEY TO '2'; -SELECT * FROM data WHERE id = 2; - ---- - -SET SERVER ROLE TO 'primary'; -SET SHARDING KEY TO '3'; -INSERT INTO data (id, value) VALUES (3, 'value_1'); - -SET SERVER ROLE TO 'replica'; -SET SHARDING KEY TO '3'; -SELECT * FROM data WHERE id = 3; - ---- - -SET SERVER ROLE TO 'primary'; -SET SHARDING KEY TO '4'; -INSERT INTO data (id, value) VALUES (4, 'value_1'); - -SET SERVER ROLE TO 'replica'; -SET SHARDING KEY TO '4'; -SELECT * FROM data WHERE id = 4; - ---- - -SET SERVER ROLE TO 'primary'; -SET SHARDING KEY TO '5'; -INSERT INTO data (id, value) VALUES (5, 'value_1'); - -SET SERVER ROLE TO 'replica'; -SET SHARDING KEY TO '5'; -SELECT * FROM data WHERE id = 5; - ---- - -SET SERVER ROLE TO 'primary'; -SET SHARDING KEY TO '6'; -INSERT INTO data (id, value) VALUES (6, 'value_1'); - -SET SERVER ROLE TO 'replica'; -SET SHARDING KEY TO '6'; -SELECT * FROM data WHERE id = 6; - ---- - -SET SERVER ROLE TO 'primary'; -SET SHARDING KEY TO '7'; -INSERT INTO data (id, value) VALUES (7, 'value_1'); - -SET SERVER ROLE TO 'replica'; -SET SHARDING KEY TO '7'; -SELECT * FROM data WHERE id = 7; - ---- - -SET SERVER ROLE TO 'primary'; -SET SHARDING KEY TO '8'; -INSERT INTO data (id, value) VALUES (8, 'value_1'); - -SET SERVER ROLE TO 'replica'; -SET SHARDING KEY TO '8'; -SELECT * FROM data WHERE id = 8; - ---- - -SET SERVER ROLE TO 'primary'; -SET SHARDING KEY TO '9'; -INSERT INTO data (id, value) VALUES (9, 'value_1'); - -SET SERVER ROLE TO 'replica'; -SET SHARDING KEY TO '9'; -SELECT * FROM data WHERE id = 9; - ---- - -\set ON_ERROR_STOP on - -SET SERVER ROLE TO 'primary'; -SET SHARDING KEY TO '10'; -INSERT INTO data (id, value) VALUES (10, 'value_1'); - -SET SERVER ROLE TO 'replica'; -SET SHARDING KEY TO '10'; -SELECT * FROM data WHERE id = 10; - ---- - -SET SERVER ROLE TO 'primary'; -SET SHARDING KEY TO '11'; -INSERT INTO data (id, value) VALUES (11, 'value_1'); - -SET SERVER ROLE TO 'replica'; -SET SHARDING KEY TO '11'; -SELECT * FROM data WHERE id = 11; - ---- - -SET SERVER ROLE TO 'primary'; -SET SHARDING KEY TO '12'; -INSERT INTO data (id, value) VALUES (12, 'value_1'); - -SET SERVER ROLE TO 'replica'; -SET SHARDING KEY TO '12'; -SELECT * FROM data WHERE id = 12; - ---- - -SET SERVER ROLE TO 'primary'; -SET SHARDING KEY TO '13'; -INSERT INTO data (id, value) VALUES (13, 'value_1'); - -SET SERVER ROLE TO 'replica'; -SET SHARDING KEY TO '13'; -SELECT * FROM data WHERE id = 13; - ---- - -SET SERVER ROLE TO 'primary'; -SET SHARDING KEY TO '14'; -INSERT INTO data (id, value) VALUES (14, 'value_1'); - -SET SERVER ROLE TO 'replica'; -SET SHARDING KEY TO '14'; -SELECT * FROM data WHERE id = 14; - ---- - -SET SERVER ROLE TO 'primary'; -SELECT 1; - -SET SERVER ROLE TO 'replica'; -SELECT 1; - -set server role to 'replica'; -SeT SeRver Role TO 'PrImARY'; -select 1; - -SET PRIMARY READS TO 'on'; -SELECT 1; - -SET PRIMARY READS TO 'off'; -SELECT 1; - -SET PRIMARY READS TO 'default'; -SELECT 1; diff --git a/tests/sharding/query_routing_test_select.sql b/tests/sharding/query_routing_test_select.sql deleted file mode 100644 index 1b30fdf..0000000 --- a/tests/sharding/query_routing_test_select.sql +++ /dev/null @@ -1,49 +0,0 @@ -\set ON_ERROR_STOP on - -SET SHARDING KEY TO '1'; -SELECT * FROM data WHERE id = 1; - -SET SHARDING KEY TO '2'; -SELECT * FROM data WHERE id = 2; - -SET SHARDING KEY TO '3'; -SELECT * FROM data WHERE id = 3; - -SET SHARDING KEY TO '4'; -SELECT * FROM data WHERE id = 4; - -SET SHARDING KEY TO '5'; -SELECT * FROM data WHERE id = 5; - -SET SHARDING KEY TO '6'; -SELECT * FROM data WHERE id = 6; - -SET SHARDING KEY TO '7'; -SELECT * FROM data WHERE id = 7; - -SET SHARDING KEY TO '8'; -SELECT * FROM data WHERE id = 8; - -SET SHARDING KEY TO '9'; -SELECT * FROM data WHERE id = 9; - -SET SHARDING KEY TO '10'; -SELECT * FROM data WHERE id = 10; - -SET SHARDING KEY TO '11'; -SELECT * FROM data WHERE id = 11; - -SET SHARDING KEY TO '12'; -SELECT * FROM data WHERE id = 12; - -SET SHARDING KEY TO '13'; -SELECT * FROM data WHERE id = 13; - -SET SHARDING KEY TO '14'; -SELECT * FROM data WHERE id = 14; - -SET SHARDING KEY TO '15'; -SELECT * FROM data WHERE id = 15; - -SET SHARDING KEY TO '16'; -SELECT * FROM data WHERE id = 16; diff --git a/tests/sharding/query_routing_test_validate.sql b/tests/sharding/query_routing_test_validate.sql deleted file mode 100644 index 1647087..0000000 --- a/tests/sharding/query_routing_test_validate.sql +++ /dev/null @@ -1,11 +0,0 @@ -\c shard0 - -SELECT * FROM data; - -\c shard1 - -SELECT * FROM data; - -\c shard2 - -SELECT * FROM data; diff --git a/utilities/deb.sh b/utilities/deb.sh deleted file mode 100644 index 4aa8aa7..0000000 --- a/utilities/deb.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/bin/bash -# -# Build an Ubuntu deb. -# -script_dir=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -deb_dir="/tmp/pgcat-build" -export PACKAGE_VERSION=${1:-"1.1.1"} -if [[ $(arch) == "x86_64" ]]; then - export ARCH=amd64 -else - export ARCH=arm64 -fi - -cd "$script_dir/.." -cargo build --release - -rm -rf "$deb_dir" -mkdir -p "$deb_dir/DEBIAN" -mkdir -p "$deb_dir/usr/bin" -mkdir -p "$deb_dir/etc/systemd/system" - -cp target/release/pgcat "$deb_dir/usr/bin/pgcat" -chmod +x "$deb_dir/usr/bin/pgcat" - -cp pgcat.toml "$deb_dir/etc/pgcat.example.toml" -cp pgcat.service "$deb_dir/etc/systemd/system/pgcat.service" - -(cat control | envsubst) > "$deb_dir/DEBIAN/control" -cp postinst "$deb_dir/DEBIAN/postinst" -cp postrm "$deb_dir/DEBIAN/postrm" -cp prerm "$deb_dir/DEBIAN/prerm" - -chmod +x ${deb_dir}/DEBIAN/post* -chmod +x ${deb_dir}/DEBIAN/pre* - -dpkg-deb \ - --root-owner-group \ - -z1 \ - --build "$deb_dir" \ - pgcat-${PACKAGE_VERSION}-ubuntu22.04-${ARCH}.deb diff --git a/utilities/generate_config_docs.py b/utilities/generate_config_docs.py deleted file mode 100644 index d02c889..0000000 --- a/utilities/generate_config_docs.py +++ /dev/null @@ -1,92 +0,0 @@ -import re -import tomli - -class DocGenerator: - def __init__(self, filename): - self.doc = [] - self.current_section = "" - self.current_comment = [] - self.current_field_name = "" - self.current_field_value = [] - self.current_field_unset = False - self.filename = filename - - def write(self): - with open("../CONFIG.md", "w") as text_file: - text_file.write("# PgCat Configurations \n") - for entry in self.doc: - if entry["name"] == "__section__": - text_file.write("## `" + entry["section"] + "` Section" + "\n") - text_file.write("\n") - continue - text_file.write("### " + entry["name"]+ "\n") - text_file.write("```"+ "\n") - text_file.write("path: " + entry["fqdn"]+ "\n") - text_file.write("default: " + entry["defaults"].strip()+ "\n") - if entry["example"] is not None: - text_file.write("example: " + entry["example"].strip()+ "\n") - text_file.write("```"+ "\n") - text_file.write("\n") - text_file.write(entry["comment"]+ "\n") - text_file.write("\n") - - def save_entry(self): - if len(self.current_field_name) == 0: - return - if len(self.current_comment) == 0: - return - self.current_section = self.current_section.replace("sharded_db", "") - self.current_section = self.current_section.replace("simple_db", "") - self.current_section = self.current_section.replace("users.0", "users.") - self.current_section = self.current_section.replace("users.1", "users.") - self.current_section = self.current_section.replace("shards.0", "shards.") - self.current_section = self.current_section.replace("shards.1", "shards.") - self.doc.append( - { - "name": self.current_field_name, - "fqdn": self.current_section + "." + self.current_field_name, - "section": self.current_section, - "comment": "\n".join(self.current_comment), - "defaults": self.current_field_value if not self.current_field_unset else "", - "example": self.current_field_value if self.current_field_unset else None - } - ) - self.current_comment = [] - self.current_field_name = "" - self.current_field_value = [] - def parse(self): - with open("../pgcat.toml", "r") as f: - for line in f.readlines(): - line = line.strip() - if len(line) == 0: - self.save_entry() - - if line.startswith("["): - self.current_section = line[1:-1] - self.current_field_name = "__section__" - self.current_field_unset = False - self.save_entry() - - elif line.startswith("#"): - results = re.search("^#\s*([A-Za-z0-9_]+)\s*=(.+)$", line) - if results is not None: - self.current_field_name = results.group(1) - self.current_field_value = results.group(2) - self.current_field_unset = True - self.save_entry() - else: - self.current_comment.append(line[1:].strip()) - else: - results = re.search("^\s*([A-Za-z0-9_]+)\s*=(.+)$", line) - if results is None: - continue - self.current_field_name = results.group(1) - self.current_field_value = results.group(2) - self.current_field_unset = False - self.save_entry() - self.save_entry() - return self - - -DocGenerator("../pgcat.toml").parse().write() - diff --git a/utilities/requirements.txt b/utilities/requirements.txt deleted file mode 100644 index aab392a..0000000 --- a/utilities/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -tomli