Compare commits

..

1 Commits

Author SHA1 Message Date
Lev Kokotov
28172cc1d5 Fix debug log 2022-08-11 22:47:22 -07:00
78 changed files with 2800 additions and 10855 deletions

View File

@@ -9,51 +9,20 @@ jobs:
# Specify the execution environment. You can specify an image from Dockerhub or use one of our Convenience Images from CircleCI's Developer Hub. # 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 # See: https://circleci.com/docs/2.0/configuration-reference/#docker-machine-macos-windows-executor
docker: docker:
- image: ghcr.io/levkk/pgcat-ci:1.67 - image: levkk/pgcat-ci:latest
environment: environment:
RUST_LOG: info RUST_LOG: info
LLVM_PROFILE_FILE: /tmp/pgcat-%m-%p.profraw RUSTFLAGS: "-C instrument-coverage"
RUSTC_BOOTSTRAP: 1 LLVM_PROFILE_FILE: "pgcat-%m.profraw"
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 - 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"] # auth:
# username: mydockerhub-user
# password: $DOCKERHUB_PASSWORD
environment: environment:
POSTGRES_USER: postgres POSTGRES_USER: postgres
POSTGRES_DB: postgres POSTGRES_DB: postgres
POSTGRES_PASSWORD: postgres POSTGRES_PASSWORD: postgres
POSTGRES_INITDB_ARGS: --auth-local=md5 --auth-host=md5 --auth=md5 POSTGRES_HOST_AUTH_METHOD: scram-sha-256
- 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 # Add steps to the job
# See: https://circleci.com/docs/2.0/configuration-reference/#steps # See: https://circleci.com/docs/2.0/configuration-reference/#steps
steps: steps:
@@ -63,9 +32,18 @@ jobs:
- run: - run:
name: "Lint" name: "Lint"
command: "cargo fmt --check" command: "cargo fmt --check"
- run:
name: "Install dependencies"
command: "sudo apt-get update && sudo apt-get install -y psmisc postgresql-contrib-12 postgresql-client-12 ruby ruby-dev libpq-dev python3 python3-pip lcov llvm-11 && sudo apt-get upgrade curl"
- run:
name: "Install rust tools"
command: "cargo install cargo-binutils rustfilt && rustup component add llvm-tools-preview"
- run:
name: "Build"
command: "cargo build"
- run: - run:
name: "Tests" name: "Tests"
command: "cargo clean && cargo build && cargo test && bash .circleci/run_tests.sh && .circleci/generate_coverage.sh" command: "cargo test && bash .circleci/run_tests.sh && .circleci/generate_coverage.sh"
- store_artifacts: - store_artifacts:
path: /tmp/cov path: /tmp/cov
destination: coverage-data destination: coverage-data

View File

@@ -1,15 +1,7 @@
#!/bin/bash #!/bin/bash
# inspired by https://doc.rust-lang.org/rustc/instrument-coverage.html#tips-for-listing-the-binaries-automatically rust-profdata merge -sparse pgcat-*.profraw -o pgcat.profdata
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 rust-cov export -ignore-filename-regex="rustc|registry" -Xdemangler=rustfilt -instr-profile=pgcat.profdata --object ./target/debug/pgcat --format lcov > ./lcov.info
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 --output-directory /tmp/cov --prefix $(pwd)
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)

View File

@@ -11,17 +11,14 @@ host = "0.0.0.0"
# Port to run on, same as PgBouncer used in this example. # Port to run on, same as PgBouncer used in this example.
port = 6432 port = 6432
# Whether to enable prometheus exporter or not. # enable prometheus exporter on port 9930
enable_prometheus_exporter = true 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). # How long to wait before aborting a server connection (ms).
connect_timeout = 1000 connect_timeout = 100
# How much time to give the health check query to return with a result (ms). # How much time to give the health check query to return with a result (ms).
healthcheck_timeout = 1000 healthcheck_timeout = 100
# How long to keep connection available for immediate re-use, without running a healthcheck query on it # How long to keep connection available for immediate re-use, without running a healthcheck query on it
healthcheck_delay = 30000 healthcheck_delay = 30000
@@ -32,14 +29,8 @@ shutdown_timeout = 5000
# For how long to ban a server if it fails a health check (seconds). # For how long to ban a server if it fails a health check (seconds).
ban_time = 60 # 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. # Reload config automatically if it changes.
autoreload = 15000 autoreload = true
# TLS # TLS
tls_certificate = ".circleci/server.cert" tls_certificate = ".circleci/server.cert"
@@ -97,13 +88,11 @@ password = "sharding_user"
# The maximum number of connection from a single Pgcat process to any database in the cluster # 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. # is the sum of pool_size across all users.
pool_size = 9 pool_size = 9
statement_timeout = 0
[pools.sharded_db.users.1] [pools.sharded_db.users.1]
username = "other_user" username = "other_user"
password = "other_user" password = "other_user"
pool_size = 21 pool_size = 21
statement_timeout = 30000
# Shard 0 # Shard 0
[pools.sharded_db.shards.0] [pools.sharded_db.shards.0]
@@ -141,7 +130,6 @@ sharding_function = "pg_bigint_hash"
username = "simple_user" username = "simple_user"
password = "simple_user" password = "simple_user"
pool_size = 5 pool_size = 5
statement_timeout = 30000
[pools.simple_db.shards.0] [pools.simple_db.shards.0]
servers = [ servers = [

View File

@@ -3,9 +3,6 @@
set -e set -e
set -o xtrace 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 # Start PgCat with a particular log level
# for inspection. # for inspection.
function start_pgcat() { function start_pgcat() {
@@ -16,17 +13,17 @@ function start_pgcat() {
# Setup the database with shards and user # 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 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 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 shard1 -i
PGPASSWORD=sharding_user pgbench -h 127.0.0.1 -U sharding_user shard2 -i PGPASSWORD=sharding_user pgbench -h 127.0.0.1 -U sharding_user shard2 -i
# Install Toxiproxy to simulate a downed/slow database
wget -O toxiproxy-2.1.4.deb https://github.com/Shopify/toxiproxy/releases/download/v2.1.4/toxiproxy_2.1.4_amd64.deb
sudo dpkg -i toxiproxy-2.1.4.deb
# Start Toxiproxy # Start Toxiproxy
LOG_LEVEL=error toxiproxy-server & toxiproxy-server &
sleep 1 sleep 1
# Create a database at port 5433, forward it to Postgres # Create a database at port 5433, forward it to Postgres
@@ -53,17 +50,6 @@ psql -U sharding_user -h 127.0.0.1 -p 6432 -c 'COPY (SELECT * FROM pgbench_accou
sleep 1 sleep 1
killall psql -s SIGINT 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) # Reload pool (closing unused server connections)
PGPASSWORD=admin_pass psql -U admin_user -h 127.0.0.1 -p 6432 -d pgbouncer -c 'RELOAD' PGPASSWORD=admin_pass psql -U admin_user -h 127.0.0.1 -p 6432 -d pgbouncer -c 'RELOAD'
@@ -80,25 +66,13 @@ psql -U sharding_user -e -h 127.0.0.1 -p 6432 -f tests/sharding/query_routing_te
# Replica/primary selection & more sharding tests # 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 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 # ActiveRecord tests
# #
cd tests/ruby cd tests/ruby
sudo bundle install sudo gem install bundler
bundle exec ruby tests.rb --format documentation || exit 1 bundle install
bundle exec rspec *_spec.rb --format documentation || exit 1 ruby tests.rb
cd ../.. cd ../..
# #
@@ -106,11 +80,7 @@ cd ../..
# These tests will start and stop the pgcat server so it will need to be restarted after the 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 pip3 install -r tests/python/requirements.txt
python3 tests/python/tests.py || exit 1 python3 tests/python/tests.py
start_pgcat "info"
python3 tests/python/async_test.py
start_pgcat "info" start_pgcat "info"
@@ -120,9 +90,9 @@ psql -U admin_user -e -h 127.0.0.1 -p 6432 -d pgbouncer -c 'SHOW STATS' > /dev/n
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 '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 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 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 pgbouncer -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 pgbouncer -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 '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 -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) (! psql -U admin_user -e -h 127.0.0.1 -p 6432 -d random_db -c 'SHOW STATS' > /dev/null)
export PGPASSWORD=sharding_user export PGPASSWORD=sharding_user
@@ -147,14 +117,11 @@ toxiproxy-cli toxic remove --toxicName latency_downstream postgres_replica
start_pgcat "info" start_pgcat "info"
# Test session mode (and config reload) # Test session mode (and config reload)
sed -i '0,/simple_db/s/pool_mode = "transaction"/pool_mode = "session"/' .circleci/pgcat.toml sed -i 's/pool_mode = "transaction"/pool_mode = "session"/' .circleci/pgcat.toml
# Reload config test # Reload config test
kill -SIGHUP $(pgrep pgcat) 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 sleep 1
# Prepared statements that will only work in session mode # Prepared statements that will only work in session mode

View File

@@ -2,5 +2,3 @@ target/
tests/ tests/
tracing/ tracing/
.circleci/ .circleci/
.git/
dev/

View File

@@ -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

View File

@@ -1,12 +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

View File

@@ -1,54 +0,0 @@
name: Build and Push
on: push
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@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Determine tags
id: metadata
uses: docker/metadata-action@v4
with:
images: ${{ env.registry }}/${{ env.image-name }}
tags: |
type=sha,prefix=,format=long
type=schedule
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@v2.1.0
with:
registry: ${{ env.registry }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push ${{ env.image-name }}
uses: docker/build-push-action@v3
with:
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

View File

@@ -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

10
.gitignore vendored
View File

@@ -1,12 +1,4 @@
.idea .idea
/target /target
*.deb *.deb
.vscode .vscode
*.profraw
cov/
lcov.info
# Dev
dev/.bash_history
dev/cache
!dev/cache/.keepme

View File

@@ -1,2 +0,0 @@
edition = "2021"
hard_tabs = false

401
CONFIG.md
View File

@@ -1,401 +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: 5000 # milliseconds
```
How long to wait before aborting a server connection (ms).
### idle_timeout
```
path: general.idle_timeout
default: 30000 # milliseconds
```
How long an idle connection with a server is left open (ms).
### 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
```
When set to true, PgCat reloads configs if it detects a change in the config file.
### 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
```
Number of seconds between keepalive packets.
### tls_certificate
```
path: general.tls_certificate
default: <UNSET>
example: "server.cert"
```
Path to TLS Certificate file to use for TLS connections
### tls_private_key
```
path: general.tls_private_key
default: <UNSET>
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
## `pools.<pool_name>` Section
### pool_mode
```
path: pools.<pool_name>.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.<pool_name>.load_balancing_mode
default: "random"
```
Load balancing mode
`random` selects the server at random
`loc` selects the server with the least outstanding busy conncetions
### default_role
```
path: pools.<pool_name>.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.
### query_parser_enabled
```
path: pools.<pool_name>.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.<pool_name>.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.<pool_name>.sharding_key_regex
default: <UNSET>
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.<pool_name>.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.<pool_name>.auth_query
default: <UNSET>
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.<pool_name>.auth_query_user
default: <UNSET>
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.<pool_name>.auth_query_password
default: <UNSET>
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.<pool_name>.automatic_sharding_key
default: <UNSET>
example: "data.id"
```
Automatically parse this from queries and route queries to the right shard!
### idle_timeout
```
path: pools.<pool_name>.idle_timeout
default: 40000
```
Idle timeout can be overwritten in the pool
### connect_timeout
```
path: pools.<pool_name>.connect_timeout
default: 3000
```
Connect timeout can be overwritten in the pool
## `pools.<pool_name>.users.<user_index>` Section
### username
```
path: pools.<pool_name>.users.<user_index>.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.<pool_name>.users.<user_index>.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.<pool_name>.users.<user_index>.server_username
default: <UNSET>
example: "another_user"
```
PostgreSQL username used to connect to the server.
### server_password
```
path: pools.<pool_name>.users.<user_index>.server_password
default: <UNSET>
example: "another_password"
```
PostgreSQL password used to connect to the server.
### pool_size
```
path: pools.<pool_name>.users.<user_index>.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.
### statement_timeout
```
path: pools.<pool_name>.users.<user_index>.statement_timeout
default: 0
```
Maximum query duration. Dangerous, but protects against DBs that died in a non-obvious way.
0 means it is disabled.
## `pools.<pool_name>.shards.<shard_index>` Section
### servers
```
path: pools.<pool_name>.shards.<shard_index>.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.<pool_name>.shards.<shard_index>.mirrors
default: <UNSET>
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.<pool_name>.shards.<shard_index>.database
default: "shard0"
```
Database name (e.g. "postgres")

934
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,44 +1,35 @@
[package] [package]
name = "pgcat" name = "pgcat"
version = "1.0.1" version = "0.6.0-alpha1"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
bytes = "1" bytes = "1"
md-5 = "0.10" md-5 = "0.10"
bb8 = "0.8.0" bb8 = "0.7"
async-trait = "0.1" async-trait = "0.1"
rand = "0.8" rand = "0.8"
chrono = "0.4" chrono = "0.4"
sha-1 = "0.10" sha-1 = "0.10"
toml = "0.7" toml = "0.5"
serde = "1" serde = "1"
serde_derive = "1" serde_derive = "1"
regex = "1" regex = "1"
num_cpus = "1" num_cpus = "1"
once_cell = "1" once_cell = "1"
sqlparser = "0.33.0" sqlparser = "0.14"
log = "0.4" log = "0.4"
arc-swap = "1" arc-swap = "1"
env_logger = "0.10" env_logger = "0.9"
parking_lot = "0.12.1" parking_lot = "0.11"
hmac = "0.12" hmac = "0.12"
sha2 = "0.10" sha2 = "0.10"
base64 = "0.21" base64 = "0.13"
stringprep = "0.1" stringprep = "0.1"
tokio-rustls = "0.24" tokio-rustls = "0.23"
rustls-pemfile = "1" rustls-pemfile = "1"
hyper = { version = "0.14", features = ["full"] } hyper = { version = "0.14", features = ["full"] }
phf = { version = "0.11.1", features = ["macros"] } phf = { version = "0.10", 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"
[target.'cfg(not(target_env = "msvc"))'.dependencies]
jemallocator = "0.5.0"

View File

@@ -1,12 +1,8 @@
FROM cimg/rust:1.67.1 FROM cimg/rust:1.62.0
RUN sudo apt-get update && \ RUN sudo apt-get update && \
sudo apt-get install -y \ sudo apt-get install -y psmisc postgresql-contrib-12 postgresql-client-12 ruby ruby-dev libpq-dev python3 python3-pip lcov llvm-11 && \
psmisc postgresql-contrib-14 postgresql-client-14 libpq-dev \ sudo apt-get upgrade curl
ruby ruby-dev python3 python3-pip \ RUN cargo install cargo-binutils rustfilt && \
lcov llvm-11 iproute2 && \ rustup component add llvm-tools-preview
sudo apt-get upgrade curl && \ RUN pip3 install psycopg2 && \
cargo install cargo-binutils rustfilt && \ sudo gem install bundler
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

View File

@@ -1,4 +1,4 @@
Copyright (c) 2023 PgCat Contributors Copyright (c) 2022 Lev Kokotov <lev@levthe.dev>
Permission is hereby granted, free of charge, to any person obtaining Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the a copy of this software and associated documentation files (the

468
README.md
View File

@@ -1,98 +1,69 @@
## PgCat: Nextgen PostgreSQL Pooler # PgCat
[![CircleCI](https://circleci.com/gh/postgresml/pgcat/tree/main.svg?style=svg)](https://circleci.com/gh/postgresml/pgcat/tree/main) [![CircleCI](https://circleci.com/gh/levkk/pgcat/tree/main.svg?style=svg)](https://circleci.com/gh/levkk/pgcat/tree/main)
<a href="https://discord.gg/DmyJP3qJ7U" target="_blank">
<img src="https://img.shields.io/discord/1013868243036930099" alt="Join our Discord!" />
</a>
PostgreSQL pooler and proxy (like PgBouncer) with support for sharding, load balancing, failover and mirroring. ![PgCat](./pgcat3.png)
PostgreSQL pooler (like PgBouncer) with sharding, load balancing and failover support.
**Beta**: looking for beta testers, see [#35](https://github.com/levkk/pgcat/issues/35).
## Features ## Features
| **Feature** | **Status** | **Comments** |
| **Feature** | **Status** | **Comments** | |--------------------------------|-----------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
|-------------|------------|--------------| | Transaction pooling | :white_check_mark: | Identical to PgBouncer. |
| Transaction pooling | **Stable** | Identical to PgBouncer with notable improvements for handling bad clients and abandoned transactions. | | Session pooling | :white_check_mark: | Identical to PgBouncer. |
| Session pooling | **Stable** | Identical to PgBouncer. | | `COPY` support | :white_check_mark: | Both `COPY TO` and `COPY FROM` are supported. |
| Multi-threaded runtime | **Stable** | Using Tokio asynchronous runtime, the pooler takes advantage of multicore machines. | | Query cancellation | :white_check_mark: | Supported both in transaction and session pooling modes. |
| Load balancing of read queries | **Stable** | Queries are automatically load balanced between replicas and the primary. | | Load balancing of read queries | :white_check_mark: | Using round-robin between replicas. Primary is included when `primary_reads_enabled` is enabled (default). |
| Failover | **Stable** | Queries are automatically rerouted around broken replicas, validated by regular health checks. | | Sharding | :white_check_mark: | Transactions are sharded using `SET SHARD TO` and `SET SHARDING KEY TO` syntax extensions; see examples below. |
| Admin database statistics | **Stable** | Pooler statistics and administration via the `pgbouncer` and `pgcat` databases. | | Failover | :white_check_mark: | Replicas are tested with a health check. If a health check fails, remaining replicas are attempted; see below for algorithm description and examples. |
| Prometheus statistics | **Stable** | Statistics are reported via a HTTP endpoint for Prometheus. | | Statistics | :white_check_mark: | Statistics available in the admin database (`pgcat` and `pgbouncer`) with `SHOW STATS`, `SHOW POOLS` and others. |
| Client TLS | **Stable** | Clients can connect to the pooler using TLS/SSL. | | Live configuration reloading | :white_check_mark: | Reload supported settings with a `SIGHUP` to the process, e.g. `kill -s SIGHUP $(pgrep pgcat)` or `RELOAD` query issued to the admin database. |
| 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. | | Client authentication | :white_check_mark: :wrench: | MD5 password authentication is supported, SCRAM is on the roadmap; one user is used to connect to Postgres with both SCRAM and MD5 supported. |
| Live configuration reloading | **Stable** | Identical to PgBouncer; all settings can be reloaded dynamically (except `host` and `port`). | | Admin database | :white_check_mark: | The admin database, similar to PgBouncer's, allows to query for statistics and reload the configuration. |
| 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.
<table>
<tr>
<td>
<a href="https://tech.instacart.com/adopting-pgcat-a-nextgen-postgres-proxy-3cf284e68c2f">
<img src="./images/instacart.webp" height="70" width="auto">
</a>
</td>
<td>
<a href="https://postgresml.org/blog/scaling-postgresml-to-one-million-requests-per-second">
<img src="./images/postgresml.webp" height="70" width="auto">
</a>
</td>
<td>
<a href="https://onesignal.com">
<img src="./images/one_signal.webp" height="70" width="auto">
</a>
</td>
</tr>
<tr>
<td>
<a href="https://tech.instacart.com/adopting-pgcat-a-nextgen-postgres-proxy-3cf284e68c2f">
Instacart
</a>
</td>
<td>
<a href="https://postgresml.org/blog/scaling-postgresml-to-one-million-requests-per-second">
PostgresML
</a>
</td>
<td>
OneSignal
</td>
</tr>
</table>
Some features remain experimental and are being actively developed. They are optional and can be enabled through configuration.
## Deployment ## 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. 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: For quick local example, use the Docker Compose environment provided:
```bash ```bash
docker-compose up docker-compose up
# In a new terminal: # In a new terminal:
PGPASSWORD=postgres psql -h 127.0.0.1 -p 6432 -U postgres -c 'SELECT 1' psql -h 127.0.0.1 -p 6432 -c 'SELECT 1'
``` ```
### Config ### Config
See **[Configuration](https://github.com/levkk/pgcat/blob/main/CONFIG.md)**. | **Name** | **Description** | **Examples** |
|-------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------|
| **`general`** | | |
| `host` | The pooler will run on this host, 0.0.0.0 means accessible from everywhere. | `0.0.0.0` |
| `port` | The pooler will run on this port. | `6432` |
| `pool_size` | Maximum allowed server connections per pool. Pools are separated for each user/shard/server role. The connections are allocated as needed. | `15` |
| `pool_mode` | The pool mode to use, i.e. `session` or `transaction`. | `transaction` |
| `connect_timeout` | Maximum time to establish a connection to a server (milliseconds). If reached, the server is banned and the next target is attempted. | `5000` |
| `healthcheck_timeout` | Maximum time to pass a health check (`SELECT 1`, milliseconds). If reached, the server is banned and the next target is attempted. | `1000` |
| `shutdown_timeout` | Maximum time to give clients during shutdown before forcibly killing client connections (ms). | `60000` |
| `healthcheck_delay` | How long to keep connection available for immediate re-use, without running a healthcheck query on it | `30000` |
| `ban_time` | Ban time for a server (seconds). It won't be allowed to serve transactions until the ban expires; failover targets will be used instead. | `60` |
| | | |
| **`user`** | | |
| `name` | The user name. | `sharding_user` |
| `password` | The user password in plaintext. | `hunter2` |
| | | |
| **`shards`** | Shards are numerically numbered starting from 0; the order in the config is preserved by the pooler to route queries accordingly. | `[shards.0]` |
| `servers` | List of servers to connect to and their roles. A server is: `[host, port, role]`, where `role` is either `primary` or `replica`. | `["127.0.0.1", 5432, "primary"]` |
| `database` | The name of the database to connect to. This is the same on all servers that are part of one shard. | |
| **`query_router`** | | |
| `default_role` | Traffic is routed to this role by default (round-robin), unless the client specifies otherwise. Default is `any`, for any role available. | `any`, `primary`, `replica` |
| `query_parser_enabled` | Enable the query parser which will inspect incoming queries and route them to a primary or replicas. | `false` |
| `primary_reads_enabled` | Enable this to allow read queries on the primary; otherwise read queries are routed to the replicas. | `true` |
## Contributing ## Local development
The project is being actively developed and looking for additional contributors and production deployments.
### Local development
1. Install Rust (latest stable will work great). 1. Install Rust (latest stable will work great).
2. `cargo build --release` (to get better benchmarks). 2. `cargo build --release` (to get better benchmarks).
@@ -102,7 +73,7 @@ The project is being actively developed and looking for additional contributors
### Tests ### Tests
When making substantial modifications to the protocol implementation, make sure to test them with pgbench: Quickest way to test your changes is to use pgbench:
``` ```
pgbench -i -h 127.0.0.1 -p 6432 && \ pgbench -i -h 127.0.0.1 -p 6432 && \
@@ -112,26 +83,17 @@ pgbench -t 1000 -p 6432 -h 127.0.0.1 --protocol extended
See [sharding README](./tests/sharding/README.md) for sharding logic testing. 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. | **Feature** | **Tested in CI** | **Tested manually** | **Comments** |
|-----------------------|--------------------|---------------------|--------------------------------------------------------------------------------------------------------------------------|
Run `cargo test` to run Rust unit tests. | Transaction pooling | :white_check_mark: | :white_check_mark: | Used by default for all tests. |
| Session pooling | :white_check_mark: | :white_check_mark: | Tested by running pgbench with `--protocol prepared` which only works in session mode. |
Run the following commands to run Ruby and Python integration tests: | `COPY` | :white_check_mark: | :white_check_mark: | `pgbench -i` uses `COPY`. `COPY FROM` is tested as well. |
| Query cancellation | :white_check_mark: | :white_check_mark: | `psql -c 'SELECT pg_sleep(1000);'` and press `Ctrl-C`. |
``` | Load balancing | :white_check_mark: | :white_check_mark: | We could test this by emitting statistics for each replica and compare them. |
cd tests/docker/ | Failover | :white_check_mark: | :white_check_mark: | Misconfigure a replica in `pgcat.toml` and watch it forward queries to spares. CI testing is using Toxiproxy. |
docker compose up --exit-code-from main # This will also produce coverage report under ./cov/ | Sharding | :white_check_mark: | :white_check_mark: | See `tests/sharding` and `tests/ruby` for an Rails/ActiveRecord example. |
``` | Statistics | :white_check_mark: | :white_check_mark: | Query the admin database with `psql -h 127.0.0.1 -p 6432 -d pgbouncer -c 'SHOW STATS'`. |
| Live config reloading | :white_check_mark: | :white_check_mark: | Run `kill -s SIGHUP $(pgrep pgcat)` and watch the config reload. |
### 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 ## Usage
@@ -146,9 +108,11 @@ In transaction mode, a client talks to one server for the duration of a single t
This mode is enabled by default. This mode is enabled by default.
### Load balancing of read queries ### 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. All queries are load balanced against the configured servers using the round-robin algorithm. The most straight forward 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. If the configuration includes a primary and replicas, the queries can be separated with the built-in query parser. The query parser 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.
The query parser is disabled by default.
#### Query parser #### 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: 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:
@@ -175,18 +139,42 @@ 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. By default, all queries are routed to the first available server; `default_role` setting controls this behavior.
### Failover ### 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. All servers are checked with a `SELECT 1` query before being given to a client. 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. The ban time can be changed with `ban_time`. The default is 60 seconds.
Failover behavior can get pretty interesting (read complex) when multiple configurations and factors are involved. The table below will try to explain what PgCat does in each scenario:
| **Query** | **`SET SERVER ROLE TO`** | **`query_parser_enabled`** | **`primary_reads_enabled`** | **Target state** | **Outcome** |
|---------------------------|--------------------------|----------------------------|-----------------------------|------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Read query, i.e. `SELECT` | unset (any) | false | false | up | Query is routed to the first instance in the round-robin loop. |
| Read query | unset (any) | true | false | up | Query is routed to the first replica instance in the round-robin loop. |
| Read query | unset (any) | true | true | up | Query is routed to the first instance in the round-robin loop. |
| Read query | replica | false | false | up | Query is routed to the first replica instance in the round-robin loop. |
| Read query | primary | false | false | up | Query is routed to the primary. |
| Read query | unset (any) | false | false | down | First instance is banned for reads. Next target in the round-robin loop is attempted. |
| Read query | unset (any) | true | false | down | First replica instance is banned. Next replica instance is attempted in the round-robin loop. |
| Read query | unset (any) | true | true | down | First instance (even if primary) is banned for reads. Next instance is attempted in the round-robin loop. |
| Read query | replica | false | false | down | First replica instance is banned. Next replica instance is attempted in the round-robin loop. |
| Read query | primary | false | false | down | The query is attempted against the primary and fails. The client receives an error. |
| | | | | | |
| Write query e.g. `INSERT` | unset (any) | false | false | up | The query is attempted against the first available instance in the round-robin loop. If the instance is a replica, the query fails and the client receives an error. |
| Write query | unset (any) | true | false | up | The query is routed to the primary. |
| Write query | unset (any) | true | true | up | The query is routed to the primary. |
| Write query | primary | false | false | up | The query is routed to the primary. |
| Write query | replica | false | false | up | The query is routed to the replica and fails. The client receives an error. |
| Write query | unset (any) | true | false | down | The query is routed to the primary and fails. The client receives an error. |
| Write query | unset (any) | true | true | down | The query is routed to the primary and fails. The client receives an error. |
| Write query | primary | false | false | down | The query is routed to the primary and fails. The client receives an error. |
| | | | | | |
### Sharding ### 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. 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: To route queries to a particular shard, we use this custom SQL syntax:
```sql ```sql
-- To talk to a shard explicitly -- To talk to a shard explicitely
SET SHARD TO '1'; SET SHARD TO '1';
-- To let the pooler choose based on a value -- To let the pooler choose based on a value
@@ -197,8 +185,7 @@ The active shard will last until it's changed again or the client disconnects. B
For hash function implementation, see `src/sharding.rs` and `tests/sharding/partition_hash_test_setup.sql`. For hash function implementation, see `src/sharding.rs` and `tests/sharding/partition_hash_test_setup.sql`.
#### ActiveRecord/Rails
##### ActiveRecord/Rails
```ruby ```ruby
class User < ActiveRecord::Base class User < ActiveRecord::Base
@@ -226,7 +213,7 @@ User.connection.execute "SET SERVER ROLE TO 'auto'"
User.find_by_email("test@example.com") User.find_by_email("test@example.com")
``` ```
##### Raw SQL #### Raw SQL
```sql ```sql
-- Grab a bunch of users from shard 1 -- Grab a bunch of users from shard 1
@@ -246,45 +233,268 @@ SET SERVER ROLE TO 'auto'; -- let the query router figure out where the query sh
SELECT * FROM users WHERE email = 'test@example.com'; -- shard setting lasts until set again; we are reading from the primary 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 ### 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. 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' psql -h 127.0.0.1 -p 6432 -d pgbouncer -c 'SHOW DATABASES'
``` ```
Additionally, Prometheus statistics are available at `/metrics` via HTTP.
### Live configuration reloading ### 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. The config can be reloaded by sending a `kill -s SIGHUP` to the process or by querying `RELOAD` to the admin database. Not all settings are currently supported by live reload:
### Mirroring | **Config** | **Requires restart** |
|-------------------------|----------------------|
| `host` | yes |
| `port` | yes |
| `pool_mode` | no |
| `connect_timeout` | yes |
| `healthcheck_timeout` | no |
| `shutdown_timeout` | no |
| `healthcheck_delay` | no |
| `ban_time` | no |
| `user` | yes |
| `shards` | yes |
| `default_role` | no |
| `primary_reads_enabled` | no |
| `query_parser_enabled` | no |
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 ## Benchmarks
PgCat is free and open source, released under the MIT license. You can setup PgBench locally through PgCat:
## Contributors ```
pgbench -h 127.0.0.1 -p 6432 -i
```
Many thanks to our amazing contributors! Coincidenly, this uses `COPY` so you can test if that works. Additionally, we'll be running the following PgBench configurations:
<a href = "https://github.com/postgresml/pgcat/graphs/contributors"> 1. 16 clients, 2 threads
<img src = "https://contrib.rocks/image?repo=postgresml/pgcat"/> 2. 32 clients, 2 threads
</a> 3. 64 clients, 2 threads
4. 128 clients, 2 threads
All queries will be `SELECT` only (`-S`) just so disks don't get in the way, since the dataset will be effectively all in RAM.
My setup:
- 8 cores, 16 hyperthreaded (AMD Ryzen 5800X)
- 32GB RAM (doesn't matter for this benchmark, except to prove that Postgres will fit the whole dataset into RAM)
### PgBouncer
#### Config
```ini
[databases]
shard0 = host=localhost port=5432 user=sharding_user password=sharding_user
[pgbouncer]
pool_mode = transaction
max_client_conn = 1000
```
Everything else stays default.
#### Runs
```
$ pgbench -t 1000 -c 16 -j 2 -p 6432 -h 127.0.0.1 -S --protocol extended shard0
starting vacuum...end.
transaction type: <builtin: select only>
scaling factor: 1
query mode: extended
number of clients: 16
number of threads: 2
number of transactions per client: 1000
number of transactions actually processed: 16000/16000
latency average = 0.155 ms
tps = 103417.377469 (including connections establishing)
tps = 103510.639935 (excluding connections establishing)
$ pgbench -t 1000 -c 32 -j 2 -p 6432 -h 127.0.0.1 -S --protocol extended shard0
starting vacuum...end.
transaction type: <builtin: select only>
scaling factor: 1
query mode: extended
number of clients: 32
number of threads: 2
number of transactions per client: 1000
number of transactions actually processed: 32000/32000
latency average = 0.290 ms
tps = 110325.939785 (including connections establishing)
tps = 110386.513435 (excluding connections establishing)
$ pgbench -t 1000 -c 64 -j 2 -p 6432 -h 127.0.0.1 -S --protocol extended shard0
starting vacuum...end.
transaction type: <builtin: select only>
scaling factor: 1
query mode: extended
number of clients: 64
number of threads: 2
number of transactions per client: 1000
number of transactions actually processed: 64000/64000
latency average = 0.692 ms
tps = 92470.427412 (including connections establishing)
tps = 92618.389350 (excluding connections establishing)
$ pgbench -t 1000 -c 128 -j 2 -p 6432 -h 127.0.0.1 -S --protocol extended shard0
starting vacuum...end.
transaction type: <builtin: select only>
scaling factor: 1
query mode: extended
number of clients: 128
number of threads: 2
number of transactions per client: 1000
number of transactions actually processed: 128000/128000
latency average = 1.406 ms
tps = 91013.429985 (including connections establishing)
tps = 91067.583928 (excluding connections establishing)
```
### PgCat
#### Config
The only thing that matters here is the number of workers in the Tokio pool. Make sure to set it to < than the number of your CPU cores.
Also account for hyper-threading, so if you have that, take the number you got above and divide it by two, that way only "real" cores serving
requests.
My setup is 16 threads, 8 cores (`htop` shows as 16 CPUs), so I set the `max_workers` in Tokio to 4. Too many, and it starts conflicting with PgBench
which is also running on the same system.
#### Runs
```
$ pgbench -t 1000 -c 16 -j 2 -p 6432 -h 127.0.0.1 -S --protocol extended
starting vacuum...end.
transaction type: <builtin: select only>
scaling factor: 1
query mode: extended
number of clients: 16
number of threads: 2
number of transactions per client: 1000
number of transactions actually processed: 16000/16000
latency average = 0.164 ms
tps = 97705.088232 (including connections establishing)
tps = 97872.216045 (excluding connections establishing)
$ pgbench -t 1000 -c 32 -j 2 -p 6432 -h 127.0.0.1 -S --protocol extended
starting vacuum...end.
transaction type: <builtin: select only>
scaling factor: 1
query mode: extended
number of clients: 32
number of threads: 2
number of transactions per client: 1000
number of transactions actually processed: 32000/32000
latency average = 0.288 ms
tps = 111300.488119 (including connections establishing)
tps = 111413.107800 (excluding connections establishing)
$ pgbench -t 1000 -c 64 -j 2 -p 6432 -h 127.0.0.1 -S --protocol extended
starting vacuum...end.
transaction type: <builtin: select only>
scaling factor: 1
query mode: extended
number of clients: 64
number of threads: 2
number of transactions per client: 1000
number of transactions actually processed: 64000/64000
latency average = 0.556 ms
tps = 115190.496139 (including connections establishing)
tps = 115247.521295 (excluding connections establishing)
$ pgbench -t 1000 -c 128 -j 2 -p 6432 -h 127.0.0.1 -S --protocol extended
starting vacuum...end.
transaction type: <builtin: select only>
scaling factor: 1
query mode: extended
number of clients: 128
number of threads: 2
number of transactions per client: 1000
number of transactions actually processed: 128000/128000
latency average = 1.135 ms
tps = 112770.562239 (including connections establishing)
tps = 112796.502381 (excluding connections establishing)
```
### Direct Postgres
Always good to have a base line.
#### Runs
```
$ pgbench -t 1000 -c 16 -j 2 -p 5432 -h 127.0.0.1 -S --protocol extended shard0
Password:
starting vacuum...end.
transaction type: <builtin: select only>
scaling factor: 1
query mode: extended
number of clients: 16
number of threads: 2
number of transactions per client: 1000
number of transactions actually processed: 16000/16000
latency average = 0.115 ms
tps = 139443.955722 (including connections establishing)
tps = 142314.859075 (excluding connections establishing)
$ pgbench -t 1000 -c 32 -j 2 -p 5432 -h 127.0.0.1 -S --protocol extended shard0
Password:
starting vacuum...end.
transaction type: <builtin: select only>
scaling factor: 1
query mode: extended
number of clients: 32
number of threads: 2
number of transactions per client: 1000
number of transactions actually processed: 32000/32000
latency average = 0.212 ms
tps = 150644.840891 (including connections establishing)
tps = 152218.499430 (excluding connections establishing)
$ pgbench -t 1000 -c 64 -j 2 -p 5432 -h 127.0.0.1 -S --protocol extended shard0
Password:
starting vacuum...end.
transaction type: <builtin: select only>
scaling factor: 1
query mode: extended
number of clients: 64
number of threads: 2
number of transactions per client: 1000
number of transactions actually processed: 64000/64000
latency average = 0.420 ms
tps = 152517.663404 (including connections establishing)
tps = 153319.188482 (excluding connections establishing)
$ pgbench -t 1000 -c 128 -j 2 -p 5432 -h 127.0.0.1 -S --protocol extended shard0
Password:
starting vacuum...end.
transaction type: <builtin: select only>
scaling factor: 1
query mode: extended
number of clients: 128
number of threads: 2
number of transactions per client: 1000
number of transactions actually processed: 128000/128000
latency average = 0.854 ms
tps = 149818.594087 (including connections establishing)
tps = 150200.603049 (excluding connections establishing)
```

View File

@@ -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;
}

View File

@@ -1,33 +0,0 @@
FROM rust:bullseye
# Dependencies
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"]

View File

@@ -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

View File

@@ -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: kubernetes/pause
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", "5432", "-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

View File

@@ -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

View File

@@ -1,7 +1,7 @@
version: "3" version: "3"
services: services:
postgres: postgres:
image: postgres:14 image: postgres:13
environment: environment:
POSTGRES_PASSWORD: postgres POSTGRES_PASSWORD: postgres
POSTGRES_HOST_AUTH_METHOD: md5 POSTGRES_HOST_AUTH_METHOD: md5

View File

@@ -11,12 +11,9 @@ host = "0.0.0.0"
# Port to run on, same as PgBouncer used in this example. # Port to run on, same as PgBouncer used in this example.
port = 6432 port = 6432
# Whether to enable prometheus exporter or not. # enable prometheus exporter on port 9930
enable_prometheus_exporter = true 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). # How long to wait before aborting a server connection (ms).
connect_timeout = 5000 connect_timeout = 5000
@@ -32,11 +29,8 @@ shutdown_timeout = 60000
# For how long to ban a server if it fails a health check (seconds). # For how long to ban a server if it fails a health check (seconds).
ban_time = 60 # seconds ban_time = 60 # seconds
# If we should log client connections # Reload config automatically if it changes.
log_client_connections = false autoreload = false
# If we should log client disconnections
log_client_disconnections = false
# TLS # TLS
# tls_certificate = "server.cert" # tls_certificate = "server.cert"
@@ -51,7 +45,7 @@ admin_password = "postgres"
# configs are structured as pool.<pool_name> # configs are structured as pool.<pool_name>
# the pool_name is what clients use as database name when connecting # 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" # For the example below a client can connect using "postgres://sharding_user:sharding_user@pgcat_host:pgcat_port/sharded"
[pools.postgres] [pools.sharded]
# Pool mode (see PgBouncer docs for more). # Pool mode (see PgBouncer docs for more).
# session: one server connection per connected client # session: one server connection per connected client
# transaction: one server connection per client transaction # transaction: one server connection per client transaction
@@ -73,7 +67,7 @@ query_parser_enabled = true
# If the query parser is enabled and this setting is enabled, the primary will be part of the pool of databases used for # 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 # 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. # queries. The primary can always be explicitely selected with our custom protocol.
primary_reads_enabled = true primary_reads_enabled = true
# So what if you wanted to implement a different hashing function, # So what if you wanted to implement a different hashing function,
@@ -87,7 +81,7 @@ primary_reads_enabled = true
sharding_function = "pg_bigint_hash" sharding_function = "pg_bigint_hash"
# Credentials for users that may connect to this cluster # Credentials for users that may connect to this cluster
[pools.postgres.users.0] [pools.sharded.users.0]
username = "postgres" username = "postgres"
password = "postgres" password = "postgres"
# Maximum number of server connections that can be established for this user # Maximum number of server connections that can be established for this user
@@ -95,11 +89,13 @@ password = "postgres"
# is the sum of pool_size across all users. # is the sum of pool_size across all users.
pool_size = 9 pool_size = 9
# Maximum query duration. Dangerous, but protects against DBs that died in a non-obvious way. [pools.sharded.users.1]
statement_timeout = 0 username = "postgres"
password = "postgres"
pool_size = 21
# Shard 0 # Shard 0
[pools.postgres.shards.0] [pools.sharded.shards.0]
# [ host, port, role ] # [ host, port, role ]
servers = [ servers = [
[ "postgres", 5432, "primary" ], [ "postgres", 5432, "primary" ],
@@ -108,16 +104,36 @@ servers = [
# Database name (e.g. "postgres") # Database name (e.g. "postgres")
database = "postgres" database = "postgres"
[pools.postgres.shards.1] [pools.sharded.shards.1]
servers = [ servers = [
[ "postgres", 5432, "primary" ], [ "postgres", 5432, "primary" ],
[ "postgres", 5432, "replica" ], [ "postgres", 5432, "replica" ],
] ]
database = "postgres" database = "postgres"
[pools.postgres.shards.2] [pools.sharded.shards.2]
servers = [ servers = [
[ "postgres", 5432, "primary" ], [ "postgres", 5432, "primary" ],
[ "postgres", 5432, "replica" ], [ "postgres", 5432, "replica" ],
] ]
database = "postgres" database = "postgres"
[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 = "postgres"
password = "postgres"
pool_size = 5
[pools.simple_db.shards.0]
servers = [
[ "postgres", 5432, "primary" ],
[ "postgres", 5432, "replica" ]
]
database = "postgres"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

View File

@@ -11,85 +11,55 @@ host = "0.0.0.0"
# Port to run on, same as PgBouncer used in this example. # Port to run on, same as PgBouncer used in this example.
port = 6432 port = 6432
# Whether to enable prometheus exporter or not. # enable prometheus exporter on port 9930
enable_prometheus_exporter = true 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). # How long to wait before aborting a server connection (ms).
connect_timeout = 5000 # milliseconds connect_timeout = 5000
# How long an idle connection with a server is left open (ms).
idle_timeout = 30000 # milliseconds
# 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). # How much time to give the health check query to return with a result (ms).
healthcheck_timeout = 1000 # milliseconds healthcheck_timeout = 1000
# How long to keep connection available for immediate re-use, without running a healthcheck query on it # How long to keep connection available for immediate re-use, without running a healthcheck query on it
healthcheck_delay = 30000 # milliseconds healthcheck_delay = 30000
# How much time to give clients during shutdown before forcibly killing client connections (ms). # How much time to give clients during shutdown before forcibly killing client connections (ms).
shutdown_timeout = 60000 # milliseconds shutdown_timeout = 60000
# How long to ban a server if it fails a health check (seconds). # For how long to ban a server if it fails a health check (seconds).
ban_time = 60 # seconds ban_time = 60 # seconds
# If we should log client connections # Reload config automatically if it changes.
log_client_connections = false autoreload = false
# If we should log client disconnections # TLS
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 = "server.cert" # tls_certificate = "server.cert"
# Path to TLS private key file to use for TLS connections
# tls_private_key = "server.key" # tls_private_key = "server.key"
# User name to access the virtual administrative database (pgbouncer or pgcat) # Credentials to access the virtual administrative database (pgbouncer or pgcat)
# Connecting to that database allows running commands like `SHOW POOLS`, `SHOW DATABASES`, etc.. # Connecting to that database allows running commands like `SHOW POOLS`, `SHOW DATABASES`, etc..
admin_username = "admin_user" admin_username = "admin_user"
# Password to access the virtual administrative database
admin_password = "admin_pass" admin_password = "admin_pass"
# pool configs are structured as pool.<pool_name> # pool
# the pool_name is what clients use as database name when connecting. # configs are structured as pool.<pool_name>
# For a pool named `sharded_db`, clients access that pool using connection string like # the pool_name is what clients use as database name when connecting
# `postgres://sharding_user:sharding_user@pgcat_host:pgcat_port/sharded_db` # For the example below a client can connect using "postgres://sharding_user:sharding_user@pgcat_host:pgcat_port/sharded_db"
[pools.sharded_db] [pools.sharded_db]
# Pool mode (see PgBouncer docs for more). # Pool mode (see PgBouncer docs for more).
# `session` one server connection per connected client # session: one server connection per connected client
# `transaction` one server connection per client transaction # transaction: one server connection per client transaction
pool_mode = "transaction" pool_mode = "transaction"
# Load balancing mode # If the client doesn't specify, route traffic to
# `random` selects the server at random # this role by default.
# `loc` selects the server with the least outstanding busy conncetions #
load_balancing_mode = "random" # any: round-robin between primary and replicas,
# replica: round-robin between replicas only without touching the primary,
# If the client doesn't specify, PgCat routes traffic to this role by default. # primary: all queries go to the primary unless otherwise specified.
# `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" default_role = "any"
# If Query Parser is enabled, we'll attempt to parse # Query parser. If enabled, we'll attempt to parse
# every incoming query to determine if it's a read or a write. # 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, # 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. # we'll direct it to the primary.
@@ -100,98 +70,52 @@ query_parser_enabled = true
# queries. The primary can always be explicitly selected with our custom protocol. # queries. The primary can always be explicitly selected with our custom protocol.
primary_reads_enabled = true 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
# So what if you wanted to implement a different hashing function, # 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? # or you've already built one and you want this pooler to use it?
#
# Current options: # Current options:
# `pg_bigint_hash`: PARTITION BY HASH (Postgres hashing function) #
# `sha1`: A hashing function based on SHA1 # pg_bigint_hash: PARTITION BY HASH (Postgres hashing function)
# sha1: A hashing function based on SHA1
#
sharding_function = "pg_bigint_hash" sharding_function = "pg_bigint_hash"
# Query to be sent to servers to obtain the hash used for md5 authentication. The connection will be # Credentials for users that may connect to this cluster
# established using the database configured in the pool. This parameter is inherited by every pool
# and can be redefined in pool configuration.
# auth_query = "SELECT $1"
# User to be used for connecting to servers to obtain the hash used for md5 authentication by sending the query
# specified in `auth_query_user`. The connection will be established using the database configured in the pool.
# This parameter is inherited by every pool and can be redefined in pool configuration.
# auth_query_user = "sharding_user"
# Password to be used for connecting to servers to obtain the hash used for md5 authentication by sending the query
# specified in `auth_query_user`. The connection will be established using the database configured in the pool.
# This parameter is inherited by every pool and can be redefined in pool configuration.
# auth_query_password = "sharding_user"
# Automatically parse this from queries and route queries to the right shard!
# automatic_sharding_key = "data.id"
# Idle timeout can be overwritten in the pool
idle_timeout = 40000
# Connect timeout can be overwritten in the pool
connect_timeout = 3000
# User configs are structured as pool.<pool_name>.users.<user_index>
# This section holds the credentials for users that may connect to this cluster
[pools.sharded_db.users.0] [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" username = "sharding_user"
# PostgreSQL password used to authenticate the user and connect to the server
# if `server_password` is not set.
password = "sharding_user" password = "sharding_user"
pool_mode = "session"
# PostgreSQL username used to connect to the server.
# server_username = "another_user"
# PostgreSQL password used to connect to the server.
# server_password = "another_password"
# Maximum number of server connections that can be established for this user # 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 # 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. # is the sum of pool_size across all users.
pool_size = 9 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] [pools.sharded_db.users.1]
username = "other_user" username = "other_user"
password = "other_user" password = "other_user"
pool_size = 21 pool_size = 21
statement_timeout = 15000
# Shard configs are structured as pool.<pool_name>.shards.<shard_id> # Shard 0
# Each shard config contains a list of servers that make up the shard
# and the database name to use.
[pools.sharded_db.shards.0] [pools.sharded_db.shards.0]
# Array of servers in the shard, each server entry is an array of `[host, port, role]` # [ host, port, role ]
servers = [["127.0.0.1", 5432, "primary"], ["localhost", 5432, "replica"]] servers = [
[ "127.0.0.1", 5432, "primary" ],
# Array of mirrors for the shard, each mirror entry is an array of `[host, port, index of server in servers array]` [ "localhost", 5432, "replica" ]
# 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 name (e.g. "postgres")
database = "shard0" database = "shard0"
[pools.sharded_db.shards.1] [pools.sharded_db.shards.1]
servers = [["127.0.0.1", 5432, "primary"], ["localhost", 5432, "replica"]] servers = [
[ "127.0.0.1", 5432, "primary" ],
[ "localhost", 5432, "replica" ],
]
database = "shard1" database = "shard1"
[pools.sharded_db.shards.2] [pools.sharded_db.shards.2]
servers = [["127.0.0.1", 5432, "primary" ], ["localhost", 5432, "replica" ]] servers = [
[ "127.0.0.1", 5432, "primary" ],
[ "localhost", 5432, "replica" ],
]
database = "shard2" database = "shard2"
@@ -206,7 +130,6 @@ sharding_function = "pg_bigint_hash"
username = "simple_user" username = "simple_user"
password = "simple_user" password = "simple_user"
pool_size = 5 pool_size = 5
statement_timeout = 0
[pools.simple_db.shards.0] [pools.simple_db.shards.0]
servers = [ servers = [

BIN
pgcat3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -1,31 +1,25 @@
use crate::pool::BanReason;
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. /// Admin database.
use std::sync::atomic::Ordering; use bytes::{Buf, BufMut, BytesMut};
use std::time::{SystemTime, UNIX_EPOCH}; use log::{info, trace};
use tokio::time::Instant; use std::collections::HashMap;
use crate::config::{get_config, reload_config, VERSION}; use crate::config::{get_config, reload_config, VERSION};
use crate::errors::Error; use crate::errors::Error;
use crate::messages::*; use crate::messages::*;
use crate::pool::{get_all_pools, get_pool}; use crate::pool::get_all_pools;
use crate::stats::{get_client_stats, get_pool_stats, get_server_stats, ClientState, ServerState}; use crate::stats::get_stats;
use crate::ClientServerMap; use crate::ClientServerMap;
pub fn generate_server_info_for_admin() -> BytesMut { pub fn generate_server_info_for_admin() -> BytesMut {
let mut server_info = BytesMut::new(); let mut server_info = BytesMut::new();
server_info.put(server_parameter_message("application_name", "")); server_info.put(server_paramater_message("application_name", ""));
server_info.put(server_parameter_message("client_encoding", "UTF8")); server_info.put(server_paramater_message("client_encoding", "UTF8"));
server_info.put(server_parameter_message("server_encoding", "UTF8")); server_info.put(server_paramater_message("server_encoding", "UTF8"));
server_info.put(server_parameter_message("server_version", VERSION)); server_info.put(server_paramater_message("server_version", VERSION));
server_info.put(server_parameter_message("DateStyle", "ISO, MDY")); server_info.put(server_paramater_message("DateStyle", "ISO, MDY"));
server_info return server_info;
} }
/// Handle admin client. /// Handle admin client.
@@ -40,92 +34,42 @@ where
let code = query.get_u8() as char; let code = query.get_u8() as char;
if code != 'Q' { if code != 'Q' {
return Err(Error::ProtocolSyncError(format!( return Err(Error::ProtocolSyncError);
"Invalid code, expected 'Q' but got '{}'",
code
)));
} }
let len = query.get_i32() as usize; let len = query.get_i32() as usize;
let query = String::from_utf8_lossy(&query[..len - 5]).to_string(); let query = String::from_utf8_lossy(&query[..len - 5])
.to_string()
.to_ascii_uppercase();
trace!("Admin query: {}", query); trace!("Admin query: {}", query);
let query_parts: Vec<&str> = query.trim_end_matches(';').split_whitespace().collect(); if query.starts_with("SHOW STATS") {
trace!("SHOW STATS");
match query_parts[0].to_ascii_uppercase().as_str() { show_stats(stream).await
"BAN" => { } else if query.starts_with("RELOAD") {
trace!("BAN"); trace!("RELOAD");
ban(stream, query_parts).await reload(stream, client_server_map).await
} } else if query.starts_with("SHOW CONFIG") {
"UNBAN" => { trace!("SHOW CONFIG");
trace!("UNBAN"); show_config(stream).await
unban(stream, query_parts).await } else if query.starts_with("SHOW DATABASES") {
} trace!("SHOW DATABASES");
"RELOAD" => { show_databases(stream).await
trace!("RELOAD"); } else if query.starts_with("SHOW POOLS") {
reload(stream, client_server_map).await trace!("SHOW POOLS");
} show_pools(stream).await
"SET" => { } else if query.starts_with("SHOW LISTS") {
trace!("SET"); trace!("SHOW LISTS");
ignore_set(stream).await show_lists(stream).await
} } else if query.starts_with("SHOW VERSION") {
"PAUSE" => { trace!("SHOW VERSION");
trace!("PAUSE"); show_version(stream).await
pause(stream, query_parts[1]).await } else if query.starts_with("SET ") {
} trace!("SET");
"RESUME" => { ignore_set(stream).await
trace!("RESUME"); } else {
resume(stream, query_parts[1]).await error_response(stream, "Unsupported query against the admin database").await
}
"SHUTDOWN" => {
trace!("SHUTDOWN");
shutdown(stream).await
}
"SHOW" => match query_parts[1].to_ascii_uppercase().as_str() {
"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,
} }
} }
@@ -134,8 +78,7 @@ async fn show_lists<T>(stream: &mut T) -> Result<(), Error>
where where
T: tokio::io::AsyncWrite + std::marker::Unpin, T: tokio::io::AsyncWrite + std::marker::Unpin,
{ {
let client_stats = get_client_stats(); let stats = get_stats();
let server_stats = get_server_stats();
let columns = vec![("list", DataType::Text), ("items", DataType::Int4)]; let columns = vec![("list", DataType::Text), ("items", DataType::Int4)];
@@ -155,32 +98,18 @@ where
res.put(data_row(&vec!["pools".to_string(), databases.to_string()])); res.put(data_row(&vec!["pools".to_string(), databases.to_string()]));
res.put(data_row(&vec![ res.put(data_row(&vec![
"free_clients".to_string(), "free_clients".to_string(),
client_stats stats
.keys() .keys()
.filter(|client_id| { .map(|address_id| stats[&address_id]["cl_idle"])
client_stats .sum::<i64>()
.get(client_id)
.unwrap()
.state
.load(Ordering::Relaxed)
== ClientState::Idle
})
.count()
.to_string(), .to_string(),
])); ]));
res.put(data_row(&vec![ res.put(data_row(&vec![
"used_clients".to_string(), "used_clients".to_string(),
client_stats stats
.keys() .keys()
.filter(|client_id| { .map(|address_id| stats[&address_id]["cl_active"])
client_stats .sum::<i64>()
.get(client_id)
.unwrap()
.state
.load(Ordering::Relaxed)
== ClientState::Active
})
.count()
.to_string(), .to_string(),
])); ]));
res.put(data_row(&vec![ res.put(data_row(&vec![
@@ -189,32 +118,18 @@ where
])); ]));
res.put(data_row(&vec![ res.put(data_row(&vec![
"free_servers".to_string(), "free_servers".to_string(),
server_stats stats
.keys() .keys()
.filter(|server_id| { .map(|address_id| stats[&address_id]["sv_idle"])
server_stats .sum::<i64>()
.get(server_id)
.unwrap()
.state
.load(Ordering::Relaxed)
== ServerState::Idle
})
.count()
.to_string(), .to_string(),
])); ]));
res.put(data_row(&vec![ res.put(data_row(&vec![
"used_servers".to_string(), "used_servers".to_string(),
server_stats stats
.keys() .keys()
.filter(|server_id| { .map(|address_id| stats[&address_id]["sv_active"])
server_stats .sum::<i64>()
.get(server_id)
.unwrap()
.state
.load(Ordering::Relaxed)
== ServerState::Active
})
.count()
.to_string(), .to_string(),
])); ]));
res.put(data_row(&vec!["dns_names".to_string(), "0".to_string()])); res.put(data_row(&vec!["dns_names".to_string(), "0".to_string()]));
@@ -228,7 +143,7 @@ where
res.put_i32(5); res.put_i32(5);
res.put_u8(b'I'); res.put_u8(b'I');
write_all_half(stream, &res).await write_all_half(stream, res).await
} }
/// Show PgCat version. /// Show PgCat version.
@@ -239,14 +154,14 @@ where
let mut res = BytesMut::new(); let mut res = BytesMut::new();
res.put(row_description(&vec![("version", DataType::Text)])); res.put(row_description(&vec![("version", DataType::Text)]));
res.put(data_row(&vec![format!("PgCat {}", VERSION)])); res.put(data_row(&vec![format!("PgCat {}", VERSION).to_string()]));
res.put(command_complete("SHOW")); res.put(command_complete("SHOW"));
res.put_u8(b'Z'); res.put_u8(b'Z');
res.put_i32(5); res.put_i32(5);
res.put_u8(b'I'); res.put_u8(b'I');
write_all_half(stream, &res).await write_all_half(stream, res).await
} }
/// Show utilization of connection pools for each shard and replicas. /// Show utilization of connection pools for each shard and replicas.
@@ -254,13 +169,11 @@ async fn show_pools<T>(stream: &mut T) -> Result<(), Error>
where where
T: tokio::io::AsyncWrite + std::marker::Unpin, T: tokio::io::AsyncWrite + std::marker::Unpin,
{ {
let all_pool_stats = get_pool_stats(); let stats = get_stats();
let columns = vec![ let columns = vec![
("database", DataType::Text), ("database", DataType::Text),
("user", DataType::Text), ("user", DataType::Text),
("pool_mode", DataType::Text),
("cl_idle", DataType::Numeric),
("cl_active", DataType::Numeric), ("cl_active", DataType::Numeric),
("cl_waiting", DataType::Numeric), ("cl_waiting", DataType::Numeric),
("cl_cancel_req", DataType::Numeric), ("cl_cancel_req", DataType::Numeric),
@@ -271,20 +184,32 @@ where
("sv_login", DataType::Numeric), ("sv_login", DataType::Numeric),
("maxwait", DataType::Numeric), ("maxwait", DataType::Numeric),
("maxwait_us", DataType::Numeric), ("maxwait_us", DataType::Numeric),
("pool_mode", DataType::Text),
]; ];
let mut res = BytesMut::new(); let mut res = BytesMut::new();
res.put(row_description(&columns)); res.put(row_description(&columns));
for (_, pool) in get_all_pools() {
let pool_config = &pool.settings;
for shard in 0..pool.shards() {
for server in 0..pool.servers(shard) {
let address = pool.address(shard, server);
let stats = match stats.get(&address.id) {
Some(stats) => stats.clone(),
None => HashMap::new(),
};
for ((_user_pool, _pool), pool_stats) in all_pool_stats { let mut row = vec![address.name(), pool_config.user.username.clone()];
let mut row = vec![
pool_stats.database(), for column in &columns[2..columns.len() - 1] {
pool_stats.user(), let value = stats.get(column.0).unwrap_or(&0).to_string();
pool_stats.pool_mode().to_string(), row.push(value);
]; }
pool_stats.populate_row(&mut row);
pool_stats.clear_maxwait(); row.push(pool_config.pool_mode.to_string());
res.put(data_row(&row)); res.put(data_row(&row));
}
}
} }
res.put(command_complete("SHOW")); res.put(command_complete("SHOW"));
@@ -294,7 +219,7 @@ where
res.put_i32(5); res.put_i32(5);
res.put_u8(b'I'); res.put_u8(b'I');
write_all_half(stream, &res).await write_all_half(stream, res).await
} }
/// Show shards and replicas. /// Show shards and replicas.
@@ -326,12 +251,11 @@ where
for (_, pool) in get_all_pools() { for (_, pool) in get_all_pools() {
let pool_config = pool.settings.clone(); let pool_config = pool.settings.clone();
for shard in 0..pool.shards() { for shard in 0..pool.shards() {
let database_name = &pool.address(shard, 0).database; let database_name = &pool_config.shards[&shard.to_string()].database;
for server in 0..pool.servers(shard) { for server in 0..pool.servers(shard) {
let address = pool.address(shard, server); let address = pool.address(shard, server);
let pool_state = pool.pool_state(shard, server); let pool_state = pool.pool_state(shard, server);
let banned = pool.is_banned(address); let banned = pool.is_banned(address, shard, Some(address.role));
let paused = pool.paused();
res.put(data_row(&vec![ res.put(data_row(&vec![
address.name(), // name address.name(), // name
@@ -345,11 +269,7 @@ where
pool_config.pool_mode.to_string(), // pool_mode pool_config.pool_mode.to_string(), // pool_mode
pool_config.user.pool_size.to_string(), // max_connections pool_config.user.pool_size.to_string(), // max_connections
pool_state.connections.to_string(), // current_connections pool_state.connections.to_string(), // current_connections
match paused { "0".to_string(), // paused
// paused
true => "1".to_string(),
false => "0".to_string(),
},
match banned { match banned {
// disabled // disabled
true => "1".to_string(), true => "1".to_string(),
@@ -366,7 +286,7 @@ where
res.put_i32(5); res.put_i32(5);
res.put_u8(b'I'); res.put_u8(b'I');
write_all_half(stream, &res).await write_all_half(stream, res).await
} }
/// Ignore any SET commands the client sends. /// Ignore any SET commands the client sends.
@@ -378,163 +298,6 @@ where
custom_protocol_response_ok(stream, "SET").await custom_protocol_response_ok(stream, "SET").await
} }
/// Bans a host from being used
async fn ban<T>(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::<i64>() {
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<T>(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<T>(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. /// Reload the configuration file without restarting the process.
async fn reload<T>(stream: &mut T, client_server_map: ClientServerMap) -> Result<(), Error> async fn reload<T>(stream: &mut T, client_server_map: ClientServerMap) -> Result<(), Error>
where where
@@ -555,7 +318,7 @@ where
res.put_i32(5); res.put_i32(5);
res.put_u8(b'I'); res.put_u8(b'I');
write_all_half(stream, &res).await write_all_half(stream, res).await
} }
/// Shows current configuration. /// Shows current configuration.
@@ -601,7 +364,7 @@ where
res.put_i32(5); res.put_i32(5);
res.put_u8(b'I'); res.put_u8(b'I');
write_all_half(stream, &res).await write_all_half(stream, res).await
} }
/// Show shard and replicas statistics. /// Show shard and replicas statistics.
@@ -610,7 +373,6 @@ where
T: tokio::io::AsyncWrite + std::marker::Unpin, T: tokio::io::AsyncWrite + std::marker::Unpin,
{ {
let columns = vec![ let columns = vec![
("instance", DataType::Text),
("database", DataType::Text), ("database", DataType::Text),
("user", DataType::Text), ("user", DataType::Text),
("total_xact_count", DataType::Numeric), ("total_xact_count", DataType::Numeric),
@@ -620,28 +382,34 @@ where
("total_xact_time", DataType::Numeric), ("total_xact_time", DataType::Numeric),
("total_query_time", DataType::Numeric), ("total_query_time", DataType::Numeric),
("total_wait_time", DataType::Numeric), ("total_wait_time", DataType::Numeric),
("total_errors", DataType::Numeric),
("avg_xact_count", DataType::Numeric), ("avg_xact_count", DataType::Numeric),
("avg_query_count", DataType::Numeric), ("avg_query_count", DataType::Numeric),
("avg_recv", DataType::Numeric), ("avg_recv", DataType::Numeric),
("avg_sent", DataType::Numeric), ("avg_sent", DataType::Numeric),
("avg_errors", DataType::Numeric),
("avg_xact_time", DataType::Numeric), ("avg_xact_time", DataType::Numeric),
("avg_query_time", DataType::Numeric), ("avg_query_time", DataType::Numeric),
("avg_wait_time", DataType::Numeric), ("avg_wait_time", DataType::Numeric),
]; ];
let stats = get_stats();
let mut res = BytesMut::new(); let mut res = BytesMut::new();
res.put(row_description(&columns)); res.put(row_description(&columns));
for (user_pool, pool) in get_all_pools() { for ((_db_name, username), pool) in get_all_pools() {
for shard in 0..pool.shards() { for shard in 0..pool.shards() {
for server in 0..pool.servers(shard) { for server in 0..pool.servers(shard) {
let address = pool.address(shard, server); let address = pool.address(shard, server);
let stats = match stats.get(&address.id) {
Some(stats) => stats.clone(),
None => HashMap::new(),
};
let mut row = vec![address.name(), user_pool.db.clone(), user_pool.user.clone()]; let mut row = vec![address.name()];
let stats = address.stats.clone(); row.push(username.clone());
stats.populate_row(&mut row);
for column in &columns[2..] {
row.push(stats.get(column.0).unwrap_or(&0).to_string());
}
res.put(data_row(&row)); res.put(data_row(&row));
} }
@@ -655,261 +423,5 @@ where
res.put_i32(5); res.put_i32(5);
res.put_u8(b'I'); res.put_u8(b'I');
write_all_half(stream, &res).await write_all_half(stream, res).await
}
/// Show currently connected clients
async fn show_clients<T>(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),
];
let new_map = get_client_stats();
let mut res = BytesMut::new();
res.put(row_description(&columns));
for (_, client) in new_map {
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(),
];
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<T>(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),
];
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(),
];
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<T>(stream: &mut T, query: &str) -> Result<(), Error>
where
T: tokio::io::AsyncWrite + std::marker::Unpin,
{
let parts: Vec<&str> = query.split(",").map(|part| part.trim()).collect();
if parts.len() != 2 {
error_response(
stream,
"PAUSE requires a database and a user, e.g. PAUSE my_db,my_user",
)
.await
} else {
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
}
}
}
}
/// Resume a pool. Queries are allowed again.
async fn resume<T>(stream: &mut T, query: &str) -> Result<(), Error>
where
T: tokio::io::AsyncWrite + std::marker::Unpin,
{
let parts: Vec<&str> = query.split(",").map(|part| part.trim()).collect();
if parts.len() != 2 {
error_response(
stream,
"RESUME requires a database and a user, e.g. RESUME my_db,my_user",
)
.await
} else {
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
}
}
}
}
/// Send response packets for shutdown.
async fn shutdown<T>(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<T>(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
} }

View File

@@ -1,132 +0,0 @@
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<Self> {
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<Self> {
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<String, Error> {
let auth_user = crate::config::User {
username: self.user.clone(),
password: Some(self.password.clone()),
server_username: None,
server_password: None,
pool_size: 1,
statement_timeout: 0,
pool_mode: None,
};
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<String, Error> {
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
)))
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,120 +1,14 @@
//! Errors. /// Errors.
/// Various errors. /// Various errors.
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub enum Error { pub enum Error {
SocketError(String), SocketError,
ClientSocketError(String, ClientIdentifier),
ClientGeneralError(String, ClientIdentifier),
ClientAuthImpossible(String),
ClientAuthPassthroughError(String, ClientIdentifier),
ClientBadStartup, ClientBadStartup,
ProtocolSyncError(String), ProtocolSyncError,
BadQuery(String),
ServerError, ServerError,
ServerStartupError(String, ServerIdentifier),
ServerAuthError(String, ServerIdentifier),
BadConfig, BadConfig,
AllServersDown, AllServersDown,
ClientError(String), ClientError,
TlsError, TlsError,
StatementTimeout,
ShuttingDown,
ParseBytesError(String),
AuthError(String),
AuthPassthroughError(String),
}
#[derive(Clone, PartialEq, Debug)]
pub struct ClientIdentifier {
pub application_name: String,
pub username: String,
pub pool_name: String,
}
impl ClientIdentifier {
pub fn new(application_name: &str, username: &str, pool_name: &str) -> ClientIdentifier {
ClientIdentifier {
application_name: application_name.into(),
username: username.into(),
pool_name: pool_name.into(),
}
}
}
impl std::fmt::Display for ClientIdentifier {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"{{ application_name: {}, username: {}, pool_name: {} }}",
self.application_name, self.username, self.pool_name
)
}
}
#[derive(Clone, PartialEq, Debug)]
pub struct ServerIdentifier {
pub username: String,
pub database: String,
}
impl ServerIdentifier {
pub fn new(username: &str, database: &str) -> ServerIdentifier {
ServerIdentifier {
username: username.into(),
database: database.into(),
}
}
}
impl std::fmt::Display for ServerIdentifier {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"{{ username: {}, database: {} }}",
self.username, self.database
)
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match &self {
&Error::ClientSocketError(error, client_identifier) => write!(
f,
"Error reading {} from client {}",
error, client_identifier
),
&Error::ClientGeneralError(error, client_identifier) => {
write!(f, "{} {}", error, client_identifier)
}
&Error::ClientAuthImpossible(username) => write!(
f,
"Client auth not possible, \
no cleartext password set for username: {} \
in config and auth passthrough (query_auth) \
is not set up.",
username
),
&Error::ClientAuthPassthroughError(error, client_identifier) => write!(
f,
"No cleartext password set, \
and no auth passthrough could not \
obtain the hash from server for {}, \
the error was: {}",
client_identifier, error
),
&Error::ServerStartupError(error, server_identifier) => write!(
f,
"Error reading {} on server startup {}",
error, server_identifier,
),
&Error::ServerAuthError(error, server_identifier) => {
write!(f, "{} for {}", error, server_identifier,)
}
// The rest can use Debug.
err => write!(f, "{:?}", err),
}
}
} }

View File

@@ -1,35 +0,0 @@
pub mod auth_passthrough;
pub mod config;
pub mod constants;
pub mod errors;
pub mod messages;
pub mod mirrors;
pub mod multi_logger;
pub mod pool;
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
)
}

View File

@@ -24,7 +24,6 @@ extern crate async_trait;
extern crate bb8; extern crate bb8;
extern crate bytes; extern crate bytes;
extern crate env_logger; extern crate env_logger;
extern crate exitcode;
extern crate log; extern crate log;
extern crate md5; extern crate md5;
extern crate num_cpus; extern crate num_cpus;
@@ -37,22 +36,13 @@ extern crate tokio;
extern crate tokio_rustls; extern crate tokio_rustls;
extern crate toml; extern crate toml;
#[cfg(not(target_env = "msvc"))] use log::{debug, error, info};
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 parking_lot::Mutex;
use pgcat::format_duration;
use tokio::net::TcpListener; use tokio::net::TcpListener;
#[cfg(not(windows))] use tokio::{
use tokio::signal::unix::{signal as unix_signal, SignalKind}; signal::unix::{signal as unix_signal, SignalKind},
#[cfg(windows)] sync::mpsc,
use tokio::signal::windows as win_signal; };
use tokio::{runtime::Builder, sync::mpsc};
use std::collections::HashMap; use std::collections::HashMap;
use std::net::SocketAddr; use std::net::SocketAddr;
@@ -61,14 +51,11 @@ use std::sync::Arc;
use tokio::sync::broadcast; use tokio::sync::broadcast;
mod admin; mod admin;
mod auth_passthrough;
mod client; mod client;
mod config; mod config;
mod constants; mod constants;
mod errors; mod errors;
mod messages; mod messages;
mod mirrors;
mod multi_logger;
mod pool; mod pool;
mod prometheus; mod prometheus;
mod query_router; mod query_router;
@@ -79,19 +66,18 @@ mod stats;
mod tls; mod tls;
use crate::config::{get_config, reload_config, VERSION}; use crate::config::{get_config, reload_config, VERSION};
use crate::messages::configure_socket;
use crate::pool::{ClientServerMap, ConnectionPool}; use crate::pool::{ClientServerMap, ConnectionPool};
use crate::prometheus::start_metric_server; use crate::prometheus::start_metric_server;
use crate::stats::{Collector, Reporter, REPORTER}; use crate::stats::{Collector, Reporter, REPORTER};
fn main() -> Result<(), Box<dyn std::error::Error>> { #[tokio::main(worker_threads = 4)]
multi_logger::MultiLogger::init().unwrap(); async fn main() {
env_logger::init();
info!("Welcome to PgCat! Meow. (Version {})", VERSION); info!("Welcome to PgCat! Meow. (Version {})", VERSION);
if !query_router::QueryRouter::setup() { if !query_router::QueryRouter::setup() {
error!("Could not setup query router"); error!("Could not setup query router");
std::process::exit(exitcode::CONFIG); return;
} }
let args = std::env::args().collect::<Vec<String>>(); let args = std::env::args().collect::<Vec<String>>();
@@ -102,255 +88,252 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
String::from("pgcat.toml") String::from("pgcat.toml")
}; };
// Create a transient runtime for loading the config for the first time. match config::parse(&config_file).await {
{ Ok(_) => (),
let runtime = Builder::new_multi_thread().worker_threads(1).build()?; Err(err) => {
error!("Config parse error: {:?}", err);
runtime.block_on(async { return;
match config::parse(&config_file).await { }
Ok(_) => (), };
Err(err) => {
error!("Config parse error: {:?}", err);
std::process::exit(exitcode::CONFIG);
}
};
});
}
let config = get_config(); let config = get_config();
// Create the runtime now we know required worker_threads. if let Some(true) = config.general.enable_prometheus_exporter {
let runtime = Builder::new_multi_thread() let http_addr_str = format!("{}:{}", config.general.host, crate::prometheus::HTTP_PORT);
.worker_threads(config.general.worker_threads) let http_addr = match SocketAddr::from_str(&http_addr_str) {
.enable_all() Ok(addr) => addr,
.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) => { Err(err) => {
error!("Listener socket error: {:?}", err); error!("Invalid http address: {}", err);
std::process::exit(exitcode::CONFIG); return;
} }
}; };
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()));
// 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 { tokio::task::spawn(async move {
let mut stats_collector = Collector::default(); start_metric_server(http_addr).await;
stats_collector.collect().await;
}); });
}
info!("Config autoreloader: {}", match config.general.autoreload { let addr = format!("{}:{}", config.general.host, config.general.port);
Some(interval) => format!("{} ms", interval),
None => "disabled".into(),
});
if let Some(interval) = config.general.autoreload { let listener = match TcpListener::bind(&addr).await {
let mut autoreload_interval = tokio::time::interval(tokio::time::Duration::from_millis(interval)); Ok(sock) => sock,
let autoreload_client_server_map = client_server_map.clone(); Err(err) => {
error!("Listener socket error: {:?}", err);
return;
}
};
tokio::task::spawn(async move { info!("Running on {}", addr);
loop {
autoreload_interval.tick().await;
debug!("Automatically reloading config");
if let Ok(changed) = reload_config(autoreload_client_server_map.clone()).await { config.show();
if changed {
get_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.
let (tx, rx) = mpsc::channel(100);
REPORTER.store(Arc::new(Reporter::new(tx.clone())));
#[cfg(windows)] // Connection pool that allows to query all shards and replicas.
let mut term_signal = win_signal::ctrl_close().unwrap(); match ConnectionPool::from_config(client_server_map.clone()).await {
#[cfg(windows)] Ok(_) => (),
let mut interrupt_signal = win_signal::ctrl_c().unwrap(); Err(err) => {
#[cfg(windows)] error!("Pool error: {:?}", err);
let mut sighup_signal = win_signal::ctrl_shutdown().unwrap(); return;
}
};
#[cfg(not(windows))] // Statistics collector task.
let mut term_signal = unix_signal(SignalKind::terminate()).unwrap(); let collector_tx = tx.clone();
#[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::<i32>(2048);
let (exit_tx, mut exit_rx) = mpsc::channel::<()>(1);
let mut admin_only = false;
let mut total_clients = 0;
info!("Waiting for clients"); // Save these for reloading
let reload_client_server_map = client_server_map.clone();
let autoreload_client_server_map = client_server_map.clone();
tokio::task::spawn(async move {
let mut stats_collector = Collector::new(rx, collector_tx);
stats_collector.collect().await;
});
info!("Waiting for clients");
let (shutdown_event_tx, mut shutdown_event_rx) = broadcast::channel::<()>(1);
let shutdown_event_tx_clone = shutdown_event_tx.clone();
// Client connection loop.
tokio::task::spawn(async move {
// Creates event subscriber for shutdown event, this is dropped when shutdown event is broadcast
let mut listener_shutdown_event_rx = shutdown_event_tx_clone.subscribe();
loop { loop {
tokio::select! { let client_server_map = client_server_map.clone();
// Reload config:
// kill -SIGHUP $(pgrep pgcat)
_ = sighup_signal.recv() => {
info!("Reloading config");
_ = reload_config(client_server_map.clone()).await; // Listen for shutdown event and client connection at the same time
let (socket, addr) = tokio::select! {
get_config().show(); _ = listener_shutdown_event_rx.recv() => {
}, // Exits client connection loop which drops listener, listener_shutdown_event_rx and shutdown_event_tx_clone
// 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; break;
}, }
new_client = listener.accept() => { listener_response = listener.accept() => {
let (socket, addr) = match new_client { match listener_response {
Ok((socket, addr)) => (socket, addr), Ok((socket, addr)) => (socket, addr),
Err(err) => { Err(err) => {
error!("{:?}", err); error!("{:?}", err);
continue; 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 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 {
errors::Error::ClientBadStartup => debug!("Client disconnected with error {:?}", err),
_ => warn!("Client disconnected with error {:?}", err),
}
}
};
});
} }
};
_ = exit_rx.recv() => { // Used to signal shutdown
break; let client_shutdown_handler_rx = shutdown_event_tx_clone.subscribe();
}
client_ping = drain_rx.recv() => { // Used to signal that the task has completed
let client_ping = client_ping.unwrap(); let dummy_tx = shutdown_event_tx_clone.clone();
total_clients += client_ping;
if total_clients == 0 && admin_only { // Handle client.
let _ = exit_tx.send(()).await; tokio::task::spawn(async move {
let start = chrono::offset::Utc::now().naive_utc();
match client::client_entrypoint(
socket,
client_server_map,
client_shutdown_handler_rx,
)
.await
{
Ok(_) => {
let duration = chrono::offset::Utc::now().naive_utc() - start;
info!(
"Client {:?} disconnected, session duration: {}",
addr,
format_duration(&duration)
);
}
Err(err) => {
debug!("Client disconnected with error {:?}", err);
}
};
// Drop this transmitter so receiver knows that the task is completed
drop(dummy_tx);
});
}
});
// Reload config:
// kill -SIGHUP $(pgrep pgcat)
tokio::task::spawn(async move {
let mut stream = unix_signal(SignalKind::hangup()).unwrap();
loop {
stream.recv().await;
info!("Reloading config");
match reload_config(reload_client_server_map.clone()).await {
Ok(_) => (),
Err(_) => continue,
};
get_config().show();
}
});
if config.general.autoreload {
let mut interval = tokio::time::interval(tokio::time::Duration::from_millis(15_000));
tokio::task::spawn(async move {
info!("Config autoreloader started");
loop {
interval.tick().await;
match reload_config(autoreload_client_server_map.clone()).await {
Ok(changed) => {
if changed {
get_config().show()
}
}
Err(_) => (),
};
}
});
}
let mut term_signal = unix_signal(SignalKind::terminate()).unwrap();
let mut interrupt_signal = unix_signal(SignalKind::interrupt()).unwrap();
tokio::select! {
// Initiate graceful shutdown sequence on sig int
_ = interrupt_signal.recv() => {
info!("Got SIGINT, waiting for client connection drain now");
// Broadcast that client tasks need to finish
shutdown_event_tx.send(()).unwrap();
// Closes transmitter
drop(shutdown_event_tx);
// This is in a loop because the first event that the receiver receives will be the shutdown event
// This is not what we are waiting for instead, we want the receiver to send an error once all senders are closed which is reached after the shutdown event is received
loop {
match tokio::time::timeout(
tokio::time::Duration::from_millis(config.general.shutdown_timeout),
shutdown_event_rx.recv(),
)
.await
{
Ok(res) => match res {
Ok(_) => {}
Err(_) => break,
},
Err(_) => {
info!("Timed out while waiting for clients to shutdown");
break;
} }
} }
} }
} },
_ = term_signal.recv() => (),
}
info!("Shutting down..."); info!("Shutting down...");
}); }
Ok(())
/// Format chrono::Duration to be more human-friendly.
///
/// # Arguments
///
/// * `duration` - A duration of time
fn format_duration(duration: &chrono::Duration) -> String {
let seconds = {
let seconds = duration.num_seconds() % 60;
if seconds < 10 {
format!("0{}", seconds)
} else {
format!("{}", seconds)
}
};
let minutes = {
let minutes = duration.num_minutes() % 60;
if minutes < 10 {
format!("0{}", minutes)
} else {
format!("{}", minutes)
}
};
let hours = {
let hours = duration.num_hours() % 24;
if hours < 10 {
format!("0{}", hours)
} else {
format!("{}", hours)
}
};
let days = duration.num_days().to_string();
format!("{}d {}:{}:{}", days, hours, minutes, seconds)
} }

View File

@@ -1,18 +1,13 @@
/// Helper functions to send one-off protocol messages /// Helper functions to send one-off protocol messages
/// and handle TcpStream (TCP socket). /// and handle TcpStream (TCP socket).
use bytes::{Buf, BufMut, BytesMut}; use bytes::{Buf, BufMut, BytesMut};
use log::error;
use md5::{Digest, Md5}; use md5::{Digest, Md5};
use socket2::{SockRef, TcpKeepalive};
use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream; use tokio::net::TcpStream;
use crate::config::get_config;
use crate::errors::Error; use crate::errors::Error;
use std::collections::HashMap; use std::collections::HashMap;
use std::io::{BufRead, Cursor};
use std::mem; use std::mem;
use std::time::Duration;
/// Postgres data type mappings /// Postgres data type mappings
/// used in RowDescription ('T') message. /// used in RowDescription ('T') message.
@@ -43,7 +38,7 @@ where
auth_ok.put_i32(8); auth_ok.put_i32(8);
auth_ok.put_i32(0); auth_ok.put_i32(0);
write_all(stream, auth_ok).await Ok(write_all(stream, auth_ok).await?)
} }
/// Generate md5 password challenge. /// Generate md5 password challenge.
@@ -84,7 +79,7 @@ where
key_data.put_i32(backend_id); key_data.put_i32(backend_id);
key_data.put_i32(secret_key); key_data.put_i32(secret_key);
write_all(stream, key_data).await Ok(write_all(stream, key_data).await?)
} }
/// Construct a `Q`: Query message. /// Construct a `Q`: Query message.
@@ -93,7 +88,7 @@ pub fn simple_query(query: &str) -> BytesMut {
let query = format!("{}\0", query); let query = format!("{}\0", query);
res.put_i32(query.len() as i32 + 4); res.put_i32(query.len() as i32 + 4);
res.put_slice(query.as_bytes()); res.put_slice(&query.as_bytes());
res res
} }
@@ -111,7 +106,7 @@ where
bytes.put_i32(5); bytes.put_i32(5);
bytes.put_u8(b'I'); // Idle bytes.put_u8(b'I'); // Idle
write_all(stream, bytes).await Ok(write_all(stream, bytes).await?)
} }
/// Send the startup packet the server. We're pretending we're a Pg client. /// Send the startup packet the server. We're pretending we're a Pg client.
@@ -123,12 +118,12 @@ pub async fn startup(stream: &mut TcpStream, user: &str, database: &str) -> Resu
// User // User
bytes.put(&b"user\0"[..]); bytes.put(&b"user\0"[..]);
bytes.put_slice(user.as_bytes()); bytes.put_slice(&user.as_bytes());
bytes.put_u8(0); bytes.put_u8(0);
// Database // Database
bytes.put(&b"database\0"[..]); bytes.put(&b"database\0"[..]);
bytes.put_slice(database.as_bytes()); bytes.put_slice(&database.as_bytes());
bytes.put_u8(0); bytes.put_u8(0);
bytes.put_u8(0); // Null terminator bytes.put_u8(0); // Null terminator
@@ -141,12 +136,7 @@ pub async fn startup(stream: &mut TcpStream, user: &str, database: &str) -> Resu
match stream.write_all(&startup).await { match stream.write_all(&startup).await {
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(err) => { Err(_) => return Err(Error::SocketError),
return Err(Error::SocketError(format!(
"Error writing startup to server socket - Error: {:?}",
err
)))
}
} }
} }
@@ -165,7 +155,7 @@ pub fn parse_params(mut bytes: BytesMut) -> Result<HashMap<String, String>, Erro
c = bytes.get_u8(); c = bytes.get_u8();
} }
if !tmp.is_empty() { if tmp.len() > 0 {
buf.push(tmp.clone()); buf.push(tmp.clone());
tmp.clear(); tmp.clear();
} }
@@ -213,13 +203,7 @@ pub fn md5_hash_password(user: &str, password: &str, salt: &[u8]) -> Vec<u8> {
let output = md5.finalize_reset(); let output = md5.finalize_reset();
// Second pass // Second pass
md5_hash_second_pass(&(format!("{:x}", output)), salt) md5.update(format!("{:x}", output));
}
pub fn md5_hash_second_pass(hash: &str, salt: &[u8]) -> Vec<u8> {
let mut md5 = Md5::new();
// Second pass
md5.update(hash);
md5.update(salt); md5.update(salt);
let mut password = format!("md5{:x}", md5.finalize()) let mut password = format!("md5{:x}", md5.finalize())
@@ -250,21 +234,7 @@ where
message.put_i32(password.len() as i32 + 4); message.put_i32(password.len() as i32 + 4);
message.put_slice(&password[..]); message.put_slice(&password[..]);
write_all(stream, message).await Ok(write_all(stream, message).await?)
}
pub async fn md5_password_with_hash<S>(stream: &mut S, hash: &str, salt: &[u8]) -> Result<(), Error>
where
S: tokio::io::AsyncWrite + std::marker::Unpin,
{
let password = md5_hash_second_pass(hash, salt);
let mut message = BytesMut::with_capacity(password.len() as usize + 5);
message.put_u8(b'p');
message.put_i32(password.len() as i32 + 4);
message.put_slice(&password[..]);
write_all(stream, message).await
} }
/// Implements a response to our custom `SET SHARDING KEY` /// Implements a response to our custom `SET SHARDING KEY`
@@ -284,7 +254,7 @@ where
res.put_i32(len); res.put_i32(len);
res.put_slice(&set_complete[..]); res.put_slice(&set_complete[..]);
write_all_half(stream, &res).await?; write_all_half(stream, res).await?;
ready_for_query(stream).await ready_for_query(stream).await
} }
@@ -322,7 +292,7 @@ where
// The short error message. // The short error message.
error.put_u8(b'M'); error.put_u8(b'M');
error.put_slice(format!("{}\0", message).as_bytes()); error.put_slice(&format!("{}\0", message).as_bytes());
// No more fields follow. // No more fields follow.
error.put_u8(0); error.put_u8(0);
@@ -334,7 +304,7 @@ where
res.put_i32(error.len() as i32 + 4); res.put_i32(error.len() as i32 + 4);
res.put(error); res.put(error);
write_all_half(stream, &res).await Ok(write_all_half(stream, res).await?)
} }
pub async fn wrong_password<S>(stream: &mut S, user: &str) -> Result<(), Error> pub async fn wrong_password<S>(stream: &mut S, user: &str) -> Result<(), Error>
@@ -357,7 +327,7 @@ where
// The short error message. // The short error message.
error.put_u8(b'M'); error.put_u8(b'M');
error.put_slice(format!("password authentication failed for user \"{}\"\0", user).as_bytes()); error.put_slice(&format!("password authentication failed for user \"{}\"\0", user).as_bytes());
// No more fields follow. // No more fields follow.
error.put_u8(0); error.put_u8(0);
@@ -396,7 +366,7 @@ where
// CommandComplete // CommandComplete
res.put(command_complete("SELECT 1")); res.put(command_complete("SELECT 1"));
write_all_half(stream, &res).await?; write_all_half(stream, res).await?;
ready_for_query(stream).await ready_for_query(stream).await
} }
@@ -404,12 +374,12 @@ pub fn row_description(columns: &Vec<(&str, DataType)>) -> BytesMut {
let mut res = BytesMut::new(); let mut res = BytesMut::new();
let mut row_desc = BytesMut::new(); let mut row_desc = BytesMut::new();
// how many columns we are storing // how many colums we are storing
row_desc.put_i16(columns.len() as i16); row_desc.put_i16(columns.len() as i16);
for (name, data_type) in columns { for (name, data_type) in columns {
// Column name // Column name
row_desc.put_slice(format!("{}\0", name).as_bytes()); row_desc.put_slice(&format!("{}\0", name).as_bytes());
// Doesn't belong to any table // Doesn't belong to any table
row_desc.put_i32(0); row_desc.put_i32(0);
@@ -453,7 +423,7 @@ pub fn data_row(row: &Vec<String>) -> BytesMut {
for column in row { for column in row {
let column = column.as_bytes(); let column = column.as_bytes();
data_row.put_i32(column.len() as i32); data_row.put_i32(column.len() as i32);
data_row.put_slice(column); data_row.put_slice(&column);
} }
res.put_u8(b'D'); res.put_u8(b'D');
@@ -480,28 +450,18 @@ where
{ {
match stream.write_all(&buf).await { match stream.write_all(&buf).await {
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(err) => { Err(_) => return Err(Error::SocketError),
return 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). /// Write all the data in the buffer to the TcpStream, write owned half (see mpsc).
pub async fn write_all_half<S>(stream: &mut S, buf: &BytesMut) -> Result<(), Error> pub async fn write_all_half<S>(stream: &mut S, buf: BytesMut) -> Result<(), Error>
where where
S: tokio::io::AsyncWrite + std::marker::Unpin, S: tokio::io::AsyncWrite + std::marker::Unpin,
{ {
match stream.write_all(buf).await { match stream.write_all(&buf).await {
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(err) => { Err(_) => return Err(Error::SocketError),
return Err(Error::SocketError(format!(
"Error writing to socket - Error: {:?}",
err
)))
}
} }
} }
@@ -512,56 +472,31 @@ where
{ {
let code = match stream.read_u8().await { let code = match stream.read_u8().await {
Ok(code) => code, Ok(code) => code,
Err(err) => { Err(_) => return Err(Error::SocketError),
return Err(Error::SocketError(format!(
"Error reading message code from socket - Error {:?}",
err
)))
}
}; };
let len = match stream.read_i32().await { let len = match stream.read_i32().await {
Ok(len) => len, Ok(len) => len,
Err(err) => { Err(_) => return Err(Error::SocketError),
return Err(Error::SocketError(format!( };
"Error reading message len from socket - Code: {:?}, Error: {:?}",
code, err let mut buf = vec![0u8; len as usize - 4];
)))
} match stream.read_exact(&mut buf).await {
Ok(_) => (),
Err(_) => return Err(Error::SocketError),
}; };
let mut bytes = BytesMut::with_capacity(len as usize + 1); let mut bytes = BytesMut::with_capacity(len as usize + 1);
bytes.put_u8(code); bytes.put_u8(code);
bytes.put_i32(len); bytes.put_i32(len);
bytes.put_slice(&buf);
bytes.resize(bytes.len() + len as usize - mem::size_of::<i32>(), b'0');
let slice_start = mem::size_of::<u8>() + mem::size_of::<i32>();
let slice_end = slice_start + len as usize - mem::size_of::<i32>();
// 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) Ok(bytes)
} }
pub fn server_parameter_message(key: &str, value: &str) -> BytesMut { pub fn server_paramater_message(key: &str, value: &str) -> BytesMut {
let mut server_info = BytesMut::new(); let mut server_info = BytesMut::new();
let null_byte_size = 1; let null_byte_size = 1;
@@ -575,41 +510,5 @@ pub fn server_parameter_message(key: &str, value: &str) -> BytesMut {
server_info.put_slice(value.as_bytes()); server_info.put_slice(value.as_bytes());
server_info.put_bytes(0, 1); server_info.put_bytes(0, 1);
server_info return server_info;
}
pub fn configure_socket(stream: &TcpStream) {
let sock_ref = SockRef::from(stream);
let conf = get_config();
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 socket: {}", err),
}
}
Err(err) => error!("Could not configure socket: {}", err),
}
}
pub trait BytesMutReader {
fn read_string(&mut self) -> Result<String, Error>;
}
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<String, Error> {
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) => return Err(Error::ParseBytesError(err.to_string())),
}
}
} }

View File

@@ -1,187 +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, PoolIdentifier, ServerPool};
use crate::stats::PoolStats;
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<Bytes>,
disconnect_rx: Receiver<()>,
}
impl MirroredClient {
async fn create_pool(&self) -> Pool<ServerPool> {
let config = get_config();
let default = std::time::Duration::from_millis(10_000).as_millis() as u64;
let (connection_timeout, idle_timeout, cfg) =
match config.pools.get(&self.address.pool_name) {
Some(cfg) => (
cfg.connect_timeout.unwrap_or(default),
cfg.idle_timeout.unwrap_or(default),
cfg.clone(),
),
None => (default, default, crate::config::Pool::default()),
};
let identifier = PoolIdentifier::new(&self.database, &self.user.username);
let manager = ServerPool::new(
self.address.clone(),
self.user.clone(),
self.database.as_str(),
ClientServerMap::default(),
Arc::new(PoolStats::new(identifier, cfg.clone())),
Arc::new(RwLock::new(None)),
);
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() => {
match recv_result {
Ok(message) => trace!("Received from mirror: {} {:?}", String::from_utf8_lossy(&message[..]), address.clone()),
Err(err) => {
server.mark_bad();
error!("Failed to receive from mirror {:?} {:?}", err, address.clone());
}
}
}
// 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();
error!("Failed to send to mirror, Discarding message {:?}, {:?}", err, address.clone())
}
}
}
None => {
info!("Mirror channel closed, exiting {:?}", address.clone());
break;
},
}
}
}
}
});
}
}
pub struct MirroringManager {
pub byte_senders: Vec<Sender<Bytes>>,
pub disconnect_senders: Vec<Sender<()>>,
}
impl MirroringManager {
pub fn from_addresses(
user: User,
database: String,
addresses: Vec<Address>,
) -> MirroringManager {
let mut byte_senders: Vec<Sender<Bytes>> = vec![];
let mut exit_senders: Vec<Sender<()>> = vec![];
addresses.iter().for_each(|mirror| {
let (bytes_tx, bytes_rx) = channel::<Bytes>(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.clone());
byte_senders.push(bytes_tx.clone());
client.start();
});
Self {
byte_senders: byte_senders,
disconnect_senders: exit_senders,
}
}
pub fn send(self: &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(self: &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
);
}
});
}
}

View File

@@ -1,80 +0,0 @@
use log::{Level, Log, Metadata, Record, SetLoggerError};
// This is a special kind of logger that allows sending logs to different
// targets depending on the log level.
//
// By default, if nothing is set, it acts as a regular env_log logger,
// it sends everything to standard error.
//
// If the Env variable `STDOUT_LOG` is defined, it will be used for
// configuring the standard out logger.
//
// The behavior is:
// - If it is an error, the message is written to standard error.
// - If it is not, and it matches the log level of the standard output logger (`STDOUT_LOG` env var), it will be send to standard output.
// - If the above is not true, it is sent to the stderr logger that will log it or not depending on the value
// of the RUST_LOG env var.
//
// So to summarize, if no `STDOUT_LOG` env var is present, the logger is the default logger. If `STDOUT_LOG` is set, everything
// but errors, that matches the log level set in the `STDOUT_LOG` env var is sent to stdout. You can have also some esoteric configuration
// where you set `RUST_LOG=debug` and `STDOUT_LOG=info`, in here, errors will go to stderr, warns and infos to stdout and debugs to stderr.
//
pub struct MultiLogger {
stderr_logger: env_logger::Logger,
stdout_logger: env_logger::Logger,
}
impl MultiLogger {
fn new() -> Self {
let stderr_logger = env_logger::builder().format_timestamp_micros().build();
let stdout_logger = env_logger::Builder::from_env("STDOUT_LOG")
.format_timestamp_micros()
.target(env_logger::Target::Stdout)
.build();
Self {
stderr_logger,
stdout_logger,
}
}
pub fn init() -> Result<(), SetLoggerError> {
let logger = Self::new();
log::set_max_level(logger.stderr_logger.filter());
log::set_boxed_logger(Box::new(logger))
}
}
impl Log for MultiLogger {
fn enabled(&self, metadata: &Metadata) -> bool {
self.stderr_logger.enabled(metadata) && self.stdout_logger.enabled(metadata)
}
fn log(&self, record: &Record) {
if record.level() == Level::Error {
self.stderr_logger.log(record);
} else {
if self.stdout_logger.matches(record) {
self.stdout_logger.log(record);
} else {
self.stderr_logger.log(record);
}
}
}
fn flush(&self) {
self.stderr_logger.flush();
self.stdout_logger.flush();
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_init() {
MultiLogger::init().unwrap();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -5,12 +5,12 @@ use phf::phf_map;
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt; use std::fmt;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use crate::config::Address; use crate::config::Address;
use crate::pool::get_all_pools; use crate::pool::get_all_pools;
use crate::stats::{get_pool_stats, get_server_stats, ServerStats}; use crate::stats::get_stats;
pub const HTTP_PORT: usize = 9930;
struct MetricHelpType { struct MetricHelpType {
help: &'static str, help: &'static str,
@@ -21,141 +21,109 @@ struct MetricHelpType {
// counters only increase // counters only increase
// gauges can arbitrarily increase or decrease // gauges can arbitrarily increase or decrease
static METRIC_HELP_AND_TYPES_LOOKUP: phf::Map<&'static str, MetricHelpType> = phf_map! { static METRIC_HELP_AND_TYPES_LOOKUP: phf::Map<&'static str, MetricHelpType> = phf_map! {
"stats_total_query_count" => MetricHelpType { "total_query_count" => MetricHelpType {
help: "Number of queries sent by all clients", help: "Number of queries sent by all clients",
ty: "counter", ty: "counter",
}, },
"stats_total_query_time" => MetricHelpType { "total_query_time" => MetricHelpType {
help: "Total amount of time for queries to execute", help: "Total amount of time for queries to execute",
ty: "counter", ty: "counter",
}, },
"stats_total_received" => MetricHelpType { "total_received" => MetricHelpType {
help: "Number of bytes received from the server", help: "Number of bytes received from the server",
ty: "counter", ty: "counter",
}, },
"stats_total_sent" => MetricHelpType { "total_sent" => MetricHelpType {
help: "Number of bytes sent to the server", help: "Number of bytes sent to the server",
ty: "counter", ty: "counter",
}, },
"stats_total_xact_count" => MetricHelpType { "total_xact_count" => MetricHelpType {
help: "Total number of transactions started by the client", help: "Total number of transactions started by the client",
ty: "counter", ty: "counter",
}, },
"stats_total_xact_time" => MetricHelpType { "total_xact_time" => MetricHelpType {
help: "Total amount of time for all transactions to execute", help: "Total amount of time for all transactions to execute",
ty: "counter", ty: "counter",
}, },
"stats_total_wait_time" => MetricHelpType { "total_wait_time" => MetricHelpType {
help: "Total time client waited for a server connection", help: "Total time client waited for a server connection",
ty: "counter", ty: "counter",
}, },
"stats_avg_query_count" => MetricHelpType { "avg_query_count" => MetricHelpType {
help: "Average of total_query_count every 15 seconds", help: "Average of total_query_count every 15 seconds",
ty: "gauge", ty: "gauge",
}, },
"stats_avg_query_time" => MetricHelpType { "avg_query_time" => MetricHelpType {
help: "Average time taken for queries to execute every 15 seconds", help: "Average time taken for queries to execute every 15 seconds",
ty: "gauge", ty: "gauge",
}, },
"stats_avg_recv" => MetricHelpType { "avg_recv" => MetricHelpType {
help: "Average of total_received bytes every 15 seconds", help: "Average of total_received bytes every 15 seconds",
ty: "gauge", ty: "gauge",
}, },
"stats_avg_sent" => MetricHelpType { "avg_sent" => MetricHelpType {
help: "Average of total_sent bytes every 15 seconds", help: "Average of total_sent bytes every 15 seconds",
ty: "gauge", ty: "gauge",
}, },
"stats_avg_errors" => MetricHelpType { "avg_xact_count" => 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", help: "Average of total_xact_count every 15 seconds",
ty: "gauge", ty: "gauge",
}, },
"stats_avg_xact_time" => MetricHelpType { "avg_xact_time" => MetricHelpType {
help: "Average of total_xact_time every 15 seconds", help: "Average of total_xact_time every 15 seconds",
ty: "gauge", ty: "gauge",
}, },
"stats_avg_wait_time" => MetricHelpType { "avg_wait_time" => MetricHelpType {
help: "Average of total_wait_time every 15 seconds", help: "Average of total_wait_time every 15 seconds",
ty: "gauge", ty: "gauge",
}, },
"pools_maxwait_us" => MetricHelpType { "maxwait_us" => MetricHelpType {
help: "The time a client waited for a server connection in microseconds", help: "The time a client waited for a server connection in microseconds",
ty: "gauge", ty: "gauge",
}, },
"pools_maxwait" => MetricHelpType { "maxwait" => MetricHelpType {
help: "The time a client waited for a server connection in seconds", help: "The time a client waited for a server connection in seconds",
ty: "gauge", ty: "gauge",
}, },
"pools_cl_waiting" => MetricHelpType { "cl_waiting" => MetricHelpType {
help: "How many clients are waiting for a connection from the pool", help: "How many clients are waiting for a connection from the pool",
ty: "gauge", ty: "gauge",
}, },
"pools_cl_active" => MetricHelpType { "cl_active" => MetricHelpType {
help: "How many clients are actively communicating with a server", help: "How many clients are actively communicating with a server",
ty: "gauge", ty: "gauge",
}, },
"pools_cl_idle" => MetricHelpType { "cl_idle" => MetricHelpType {
help: "How many clients are idle", help: "How many clients are idle",
ty: "gauge", ty: "gauge",
}, },
"pools_sv_idle" => MetricHelpType { "sv_idle" => MetricHelpType {
help: "How many server connections are idle", help: "How many server connections are idle",
ty: "gauge", ty: "gauge",
}, },
"pools_sv_active" => MetricHelpType { "sv_active" => MetricHelpType {
help: "How many server connections are actively communicating with a client", help: "How many server connections are actively communicating with a client",
ty: "gauge", ty: "gauge",
}, },
"pools_sv_login" => MetricHelpType { "sv_login" => MetricHelpType {
help: "How many server connections are currently being created", help: "How many server connections are currently being created",
ty: "gauge", ty: "gauge",
}, },
"pools_sv_tested" => MetricHelpType { "sv_tested" => MetricHelpType {
help: "How many server connections are currently waiting on a health check to succeed", help: "How many server connections are currently waiting on a health check to succeed",
ty: "gauge", ty: "gauge",
}, },
"servers_bytes_received" => MetricHelpType {
help: "Volume in bytes of network traffic received by server",
ty: "gauge",
},
"servers_bytes_sent" => MetricHelpType {
help: "Volume in bytes of network traffic sent by server",
ty: "gauge",
},
"servers_transaction_count" => MetricHelpType {
help: "Number of transactions executed by server",
ty: "gauge",
},
"servers_query_count" => MetricHelpType {
help: "Number of queries executed by server",
ty: "gauge",
},
"servers_error_count" => MetricHelpType {
help: "Number of errors",
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<Value: fmt::Display> { struct PrometheusMetric {
name: String, name: String,
help: String, help: String,
ty: String, ty: String,
labels: HashMap<&'static str, String>, labels: HashMap<&'static str, String>,
value: Value, value: i64,
} }
impl<Value: fmt::Display> fmt::Display for PrometheusMetric<Value> { impl fmt::Display for PrometheusMetric {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let formatted_labels = self let formatted_labels = self
.labels .labels
@@ -175,81 +143,50 @@ impl<Value: fmt::Display> fmt::Display for PrometheusMetric<Value> {
} }
} }
impl<Value: fmt::Display> PrometheusMetric<Value> { impl PrometheusMetric {
fn from_name<V: fmt::Display>( fn new(address: &Address, name: &str, value: i64) -> Option<PrometheusMetric> {
name: &str, let mut labels = HashMap::new();
value: V, labels.insert("host", address.host.clone());
labels: HashMap<&'static str, String>, labels.insert("shard", address.shard.to_string());
) -> Option<PrometheusMetric<V>> { labels.insert("role", address.role.to_string());
labels.insert("database", address.database.to_string());
METRIC_HELP_AND_TYPES_LOOKUP METRIC_HELP_AND_TYPES_LOOKUP
.get(name) .get(name)
.map(|metric| PrometheusMetric::<V> { .map(|metric| PrometheusMetric {
name: name.to_owned(), name: name.to_owned(),
help: metric.help.to_owned(), help: metric.help.to_owned(),
ty: metric.ty.to_owned(), ty: metric.ty.to_owned(),
value,
labels, labels,
value,
}) })
} }
fn from_database_info(
address: &Address,
name: &str,
value: u32,
) -> Option<PrometheusMetric<u32>> {
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("database", address.database.to_string());
Self::from_name(&format!("databases_{}", name), value, labels)
}
fn from_server_info(
address: &Address,
name: &str,
value: u64,
) -> Option<PrometheusMetric<u64>> {
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("database", address.database.to_string());
Self::from_name(&format!("servers_{}", name), value, labels)
}
fn from_address(address: &Address, name: &str, value: u64) -> Option<PrometheusMetric<u64>> {
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("database", address.database.to_string());
Self::from_name(&format!("stats_{}", name), value, labels)
}
fn from_pool(pool: &(String, String), name: &str, value: u64) -> Option<PrometheusMetric<u64>> {
let mut labels = HashMap::new();
labels.insert("pool", pool.0.clone());
labels.insert("user", pool.1.clone());
Self::from_name(&format!("pools_{}", name), value, labels)
}
} }
async fn prometheus_stats(request: Request<Body>) -> Result<Response<Body>, hyper::http::Error> { async fn prometheus_stats(request: Request<Body>) -> Result<Response<Body>, hyper::http::Error> {
match (request.method(), request.uri().path()) { match (request.method(), request.uri().path()) {
(&Method::GET, "/metrics") => { (&Method::GET, "/metrics") => {
let stats = get_stats();
let mut lines = Vec::new(); let mut lines = Vec::new();
push_address_stats(&mut lines); for (_, pool) in get_all_pools() {
push_pool_stats(&mut lines); for shard in 0..pool.shards() {
push_server_stats(&mut lines); for server in 0..pool.servers(shard) {
push_database_stats(&mut lines); let address = pool.address(shard, server);
if let Some(address_stats) = stats.get(&address.id) {
for (key, value) in address_stats.iter() {
if let Some(prometheus_metric) =
PrometheusMetric::new(address, key, *value)
{
lines.push(prometheus_metric.to_string());
} else {
warn!("Metric {} not implemented for {}", key, address.name());
}
}
}
}
}
}
Response::builder() Response::builder()
.header("content-type", "text/plain; version=0.0.4") .header("content-type", "text/plain; version=0.0.4")
@@ -261,124 +198,10 @@ async fn prometheus_stats(request: Request<Body>) -> Result<Response<Body>, hype
} }
} }
// Adds metrics shown in a SHOW STATS admin command.
fn push_address_stats(lines: &mut Vec<String>) {
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::<u64>::from_address(address, &key, value)
{
lines.push(prometheus_metric.to_string());
} else {
warn!("Metric {} not implemented for {}", key, address.name());
}
}
}
}
}
}
// Adds relevant metrics shown in a SHOW POOLS admin command.
fn push_pool_stats(lines: &mut Vec<String>) {
let pool_stats = get_pool_stats();
for (pool, stats) in pool_stats.iter() {
let stats = &**stats;
for (name, value) in stats.clone() {
if let Some(prometheus_metric) = PrometheusMetric::<u64>::from_pool(pool, &name, value)
{
lines.push(prometheus_metric.to_string());
} else {
warn!(
"Metric {} not implemented for ({},{})",
name, pool.0, pool.1
);
}
}
}
}
// Adds relevant metrics shown in a SHOW DATABASES admin command.
fn push_database_stats(lines: &mut Vec<String>) {
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::<u32>::from_database_info(address, key, value)
{
lines.push(prometheus_metric.to_string());
} else {
warn!("Metric {} not implemented for {}", key, address.name());
}
}
}
}
}
}
// Adds relevant metrics shown in a SHOW SERVERS admin command.
fn push_server_stats(lines: &mut Vec<String>) {
let server_stats = get_server_stats();
let mut server_stats_by_addresses = HashMap::<String, Arc<ServerStats>>::new();
for (_, stats) in server_stats {
server_stats_by_addresses.insert(stats.address_name(), stats);
}
for (_, pool) in get_all_pools() {
for shard in 0..pool.shards() {
for server in 0..pool.servers(shard) {
let address = pool.address(shard, server);
if let Some(server_info) = server_stats_by_addresses.get(&address.name()) {
let metrics = [
(
"bytes_received",
server_info.bytes_received.load(Ordering::Relaxed),
),
("bytes_sent", server_info.bytes_sent.load(Ordering::Relaxed)),
(
"transaction_count",
server_info.transaction_count.load(Ordering::Relaxed),
),
(
"query_count",
server_info.query_count.load(Ordering::Relaxed),
),
(
"error_count",
server_info.error_count.load(Ordering::Relaxed),
),
];
for (key, value) in metrics {
if let Some(prometheus_metric) =
PrometheusMetric::<u64>::from_server_info(address, key, value)
{
lines.push(prometheus_metric.to_string());
} else {
warn!("Metric {} not implemented for {}", key, address.name());
}
}
}
}
}
}
}
pub async fn start_metric_server(http_addr: SocketAddr) { pub async fn start_metric_server(http_addr: SocketAddr) {
let http_service_factory = let http_service_factory =
make_service_fn(|_conn| async { Ok::<_, hyper::Error>(service_fn(prometheus_stats)) }); make_service_fn(|_conn| async { Ok::<_, hyper::Error>(service_fn(prometheus_stats)) });
let server = Server::bind(&http_addr).serve(http_service_factory); let server = Server::bind(&http_addr.into()).serve(http_service_factory);
info!( info!(
"Exposing prometheus metrics on http://{}/metrics.", "Exposing prometheus metrics on http://{}/metrics.",
http_addr http_addr

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,6 @@
// https://github.com/sfackler/rust-postgres/ // https://github.com/sfackler/rust-postgres/
// SASL implementation. // SASL implementation.
use base64::{engine::general_purpose, Engine as _};
use bytes::BytesMut; use bytes::BytesMut;
use hmac::{Hmac, Mac}; use hmac::{Hmac, Mac};
use rand::{self, Rng}; use rand::{self, Rng};
@@ -58,7 +57,7 @@ impl ScramSha256 {
/// Used for testing. /// Used for testing.
pub fn from_nonce(password: &str, nonce: &str) -> ScramSha256 { pub fn from_nonce(password: &str, nonce: &str) -> ScramSha256 {
let message = BytesMut::from(format!("{}n=,r={}", "n,,", nonce).as_bytes()); let message = BytesMut::from(&format!("{}n=,r={}", "n,,", nonce).as_bytes()[..]);
ScramSha256 { ScramSha256 {
password: password.to_string(), password: password.to_string(),
@@ -79,16 +78,16 @@ impl ScramSha256 {
let server_message = Message::parse(message)?; let server_message = Message::parse(message)?;
if !server_message.nonce.starts_with(&self.nonce) { if !server_message.nonce.starts_with(&self.nonce) {
return Err(Error::ProtocolSyncError(format!("SCRAM"))); return Err(Error::ProtocolSyncError);
} }
let salt = match general_purpose::STANDARD.decode(&server_message.salt) { let salt = match base64::decode(&server_message.salt) {
Ok(salt) => salt, Ok(salt) => salt,
Err(_) => return Err(Error::ProtocolSyncError(format!("SCRAM"))), Err(_) => return Err(Error::ProtocolSyncError),
}; };
let salted_password = Self::hi( let salted_password = Self::hi(
&normalize(self.password.as_bytes()), &normalize(&self.password.as_bytes()[..]),
&salt, &salt,
server_message.iterations, server_message.iterations,
); );
@@ -112,7 +111,7 @@ impl ScramSha256 {
let mut cbind_input = vec![]; let mut cbind_input = vec![];
cbind_input.extend("n,,".as_bytes()); cbind_input.extend("n,,".as_bytes());
let cbind_input = general_purpose::STANDARD.encode(&cbind_input); let cbind_input = base64::encode(&cbind_input);
self.message.clear(); self.message.clear();
@@ -150,11 +149,7 @@ impl ScramSha256 {
*proof ^= signature; *proof ^= signature;
} }
match write!( match write!(&mut self.message, ",p={}", base64::encode(&*client_proof)) {
&mut self.message,
",p={}",
general_purpose::STANDARD.encode(&*client_proof)
) {
Ok(_) => (), Ok(_) => (),
Err(_) => return Err(Error::ServerError), Err(_) => return Err(Error::ServerError),
}; };
@@ -166,9 +161,9 @@ impl ScramSha256 {
pub fn finish(&mut self, message: &BytesMut) -> Result<(), Error> { pub fn finish(&mut self, message: &BytesMut) -> Result<(), Error> {
let final_message = FinalMessage::parse(message)?; let final_message = FinalMessage::parse(message)?;
let verifier = match general_purpose::STANDARD.decode(&final_message.value) { let verifier = match base64::decode(&final_message.value) {
Ok(verifier) => verifier, Ok(verifier) => verifier,
Err(_) => return Err(Error::ProtocolSyncError(format!("SCRAM"))), Err(_) => return Err(Error::ProtocolSyncError),
}; };
let mut hmac = match Hmac::<Sha256>::new_from_slice(&self.salted_password) { let mut hmac = match Hmac::<Sha256>::new_from_slice(&self.salted_password) {
@@ -186,7 +181,7 @@ impl ScramSha256 {
match hmac.verify_slice(&verifier) { match hmac.verify_slice(&verifier) {
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(_) => Err(Error::ServerError), Err(_) => return Err(Error::ServerError),
} }
} }
@@ -225,19 +220,19 @@ impl Message {
/// Parse the server SASL challenge. /// Parse the server SASL challenge.
fn parse(message: &BytesMut) -> Result<Message, Error> { fn parse(message: &BytesMut) -> Result<Message, Error> {
let parts = String::from_utf8_lossy(&message[..]) let parts = String::from_utf8_lossy(&message[..])
.split(',') .split(",")
.map(|s| s.to_string()) .map(|s| s.to_string())
.collect::<Vec<String>>(); .collect::<Vec<String>>();
if parts.len() != 3 { if parts.len() != 3 {
return Err(Error::ProtocolSyncError(format!("SCRAM"))); return Err(Error::ProtocolSyncError);
} }
let nonce = str::replace(&parts[0], "r=", ""); let nonce = str::replace(&parts[0], "r=", "");
let salt = str::replace(&parts[1], "s=", ""); let salt = str::replace(&parts[1], "s=", "");
let iterations = match str::replace(&parts[2], "i=", "").parse::<u32>() { let iterations = match str::replace(&parts[2], "i=", "").parse::<u32>() {
Ok(iterations) => iterations, Ok(iterations) => iterations,
Err(_) => return Err(Error::ProtocolSyncError(format!("SCRAM"))), Err(_) => return Err(Error::ProtocolSyncError),
}; };
Ok(Message { Ok(Message {
@@ -257,7 +252,7 @@ impl FinalMessage {
/// Parse the server final validation message. /// Parse the server final validation message.
pub fn parse(message: &BytesMut) -> Result<FinalMessage, Error> { pub fn parse(message: &BytesMut) -> Result<FinalMessage, Error> {
if !message.starts_with(b"v=") || message.len() < 4 { if !message.starts_with(b"v=") || message.len() < 4 {
return Err(Error::ProtocolSyncError(format!("SCRAM"))); return Err(Error::ProtocolSyncError);
} }
Ok(FinalMessage { Ok(FinalMessage {
@@ -273,7 +268,7 @@ mod test {
#[test] #[test]
fn parse_server_first_message() { fn parse_server_first_message() {
let message = BytesMut::from( let message = BytesMut::from(
"r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=4096".as_bytes(), &"r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=4096".as_bytes()[..],
); );
let message = Message::parse(&message).unwrap(); let message = Message::parse(&message).unwrap();
assert_eq!(message.nonce, "fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j"); assert_eq!(message.nonce, "fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j");
@@ -284,7 +279,7 @@ mod test {
#[test] #[test]
fn parse_server_last_message() { fn parse_server_last_message() {
let f = FinalMessage::parse(&BytesMut::from( let f = FinalMessage::parse(&BytesMut::from(
"v=U+ppxD5XUKtradnv8e2MkeupiA8FU87Sg8CXzXHDAzw".as_bytes(), &"v=U+ppxD5XUKtradnv8e2MkeupiA8FU87Sg8CXzXHDAzw".as_bytes()[..],
)) ))
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
@@ -314,12 +309,12 @@ mod test {
assert_eq!(std::str::from_utf8(&message).unwrap(), client_first); assert_eq!(std::str::from_utf8(&message).unwrap(), client_first);
let result = scram let result = scram
.update(&BytesMut::from(server_first.as_bytes())) .update(&BytesMut::from(&server_first.as_bytes()[..]))
.unwrap(); .unwrap();
assert_eq!(std::str::from_utf8(&result).unwrap(), client_final); assert_eq!(std::str::from_utf8(&result).unwrap(), client_final);
scram scram
.finish(&BytesMut::from(server_final.as_bytes())) .finish(&BytesMut::from(&server_final.as_bytes()[..]))
.unwrap(); .unwrap();
} }
} }

View File

@@ -1,13 +1,7 @@
/// Implementation of the PostgreSQL server (database) protocol. /// Implementation of the PostgreSQL server (database) protocol.
/// Here we are pretending to the a Postgres client. /// Here we are pretending to the a Postgres client.
use bytes::{Buf, BufMut, BytesMut}; use bytes::{Buf, BufMut, BytesMut};
use fallible_iterator::FallibleIterator; use log::{debug, error, info, trace};
use log::{debug, error, info, trace, warn};
use parking_lot::{Mutex, RwLock};
use postgres_protocol::message;
use std::collections::HashMap;
use std::io::Read;
use std::sync::Arc;
use std::time::SystemTime; use std::time::SystemTime;
use tokio::io::{AsyncReadExt, BufReader}; use tokio::io::{AsyncReadExt, BufReader};
use tokio::net::{ use tokio::net::{
@@ -17,12 +11,11 @@ use tokio::net::{
use crate::config::{Address, User}; use crate::config::{Address, User};
use crate::constants::*; use crate::constants::*;
use crate::errors::{Error, ServerIdentifier}; use crate::errors::Error;
use crate::messages::*; use crate::messages::*;
use crate::mirrors::MirroringManager;
use crate::pool::ClientServerMap;
use crate::scram::ScramSha256; use crate::scram::ScramSha256;
use crate::stats::ServerStats; use crate::stats::Reporter;
use crate::ClientServerMap;
/// Server state. /// Server state.
pub struct Server { pub struct Server {
@@ -38,7 +31,6 @@ pub struct Server {
/// Our server response buffer. We buffer data before we give it to the client. /// Our server response buffer. We buffer data before we give it to the client.
buffer: BytesMut, buffer: BytesMut,
is_async: bool,
/// Server information the server sent us over on startup. /// Server information the server sent us over on startup.
server_info: BytesMut, server_info: BytesMut,
@@ -56,9 +48,6 @@ pub struct Server {
/// Is the server broken? We'll remote it from the pool if so. /// Is the server broken? We'll remote it from the pool if so.
bad: bool, bad: bool,
/// If server connection requires a DISCARD ALL before checkin
needs_cleanup: bool,
/// Mapping of clients and servers used for query cancellation. /// Mapping of clients and servers used for query cancellation.
client_server_map: ClientServerMap, client_server_map: ClientServerMap,
@@ -66,15 +55,13 @@ pub struct Server {
connected_at: chrono::naive::NaiveDateTime, connected_at: chrono::naive::NaiveDateTime,
/// Reports various metrics, e.g. data sent & received. /// Reports various metrics, e.g. data sent & received.
stats: Arc<ServerStats>, stats: Reporter,
/// Application name using the server at the moment. /// Application name using the server at the moment.
application_name: String, application_name: String,
// Last time that a successful server send or response happened // Last time that a successful server send or response happened
last_activity: SystemTime, last_activity: SystemTime,
mirror_manager: Option<MirroringManager>,
} }
impl Server { impl Server {
@@ -85,71 +72,39 @@ impl Server {
user: &User, user: &User,
database: &str, database: &str,
client_server_map: ClientServerMap, client_server_map: ClientServerMap,
stats: Arc<ServerStats>, stats: Reporter,
auth_hash: Arc<RwLock<Option<String>>>,
) -> Result<Server, Error> { ) -> Result<Server, Error> {
let mut stream = let mut stream =
match TcpStream::connect(&format!("{}:{}", &address.host, address.port)).await { match TcpStream::connect(&format!("{}:{}", &address.host, &address.port)).await {
Ok(stream) => stream, Ok(stream) => stream,
Err(err) => { Err(err) => {
error!("Could not connect to server: {}", err); error!("Could not connect to server: {}", err);
return Err(Error::SocketError(format!( return Err(Error::SocketError);
"Could not connect to server: {}",
err
)));
} }
}; };
configure_socket(&stream);
trace!("Sending StartupMessage"); trace!("Sending StartupMessage");
// StartupMessage // StartupMessage
let username = match user.server_username { startup(&mut stream, &user.username, database).await?;
Some(ref server_username) => server_username,
None => &user.username,
};
let password = match user.server_password {
Some(ref server_password) => Some(server_password),
None => match user.password {
Some(ref password) => Some(password),
None => None,
},
};
startup(&mut stream, username, database).await?;
let mut server_info = BytesMut::new(); let mut server_info = BytesMut::new();
let mut process_id: i32 = 0; let mut process_id: i32 = 0;
let mut secret_key: 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 be handling multiple packets, but they will all be structured the same.
// We'll loop here until this exchange is complete. // We'll loop here until this exchange is complete.
let mut scram: Option<ScramSha256> = match password { let mut scram = ScramSha256::new(&user.password);
Some(password) => Some(ScramSha256::new(password)),
None => None,
};
loop { loop {
let code = match stream.read_u8().await { let code = match stream.read_u8().await {
Ok(code) => code as char, Ok(code) => code as char,
Err(_) => { Err(_) => return Err(Error::SocketError),
return Err(Error::ServerStartupError(
"message code".into(),
server_identifier,
))
}
}; };
let len = match stream.read_i32().await { let len = match stream.read_i32().await {
Ok(len) => len, Ok(len) => len,
Err(_) => { Err(_) => return Err(Error::SocketError),
return Err(Error::ServerStartupError(
"message len".into(),
server_identifier,
))
}
}; };
trace!("Message: {}", code); trace!("Message: {}", code);
@@ -160,12 +115,7 @@ impl Server {
// Determine which kind of authentication is required, if any. // Determine which kind of authentication is required, if any.
let auth_code = match stream.read_i32().await { let auth_code = match stream.read_i32().await {
Ok(auth_code) => auth_code, Ok(auth_code) => auth_code,
Err(_) => { Err(_) => return Err(Error::SocketError),
return Err(Error::ServerStartupError(
"auth code".into(),
server_identifier,
))
}
}; };
trace!("Auth: {}", auth_code); trace!("Auth: {}", auth_code);
@@ -178,70 +128,23 @@ impl Server {
match stream.read_exact(&mut salt).await { match stream.read_exact(&mut salt).await {
Ok(_) => (), Ok(_) => (),
Err(_) => { Err(_) => return Err(Error::SocketError),
return Err(Error::ServerStartupError(
"salt".into(),
server_identifier,
))
}
}; };
match password { md5_password(&mut stream, &user.username, &user.password, &salt[..])
// Using plaintext password .await?;
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 => (), AUTHENTICATION_SUCCESSFUL => (),
SASL => { 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"); debug!("Starting SASL authentication");
let sasl_len = (len - 8) as usize; let sasl_len = (len - 8) as usize;
let mut sasl_auth = vec![0u8; sasl_len]; let mut sasl_auth = vec![0u8; sasl_len];
match stream.read_exact(&mut sasl_auth).await { match stream.read_exact(&mut sasl_auth).await {
Ok(_) => (), Ok(_) => (),
Err(_) => { Err(_) => return Err(Error::SocketError),
return Err(Error::ServerStartupError(
"sasl message".into(),
server_identifier,
))
}
}; };
let sasl_type = String::from_utf8_lossy(&sasl_auth[..sasl_len - 2]); let sasl_type = String::from_utf8_lossy(&sasl_auth[..sasl_len - 2]);
@@ -250,7 +153,7 @@ impl Server {
debug!("Using {}", SCRAM_SHA_256); debug!("Using {}", SCRAM_SHA_256);
// Generate client message. // Generate client message.
let sasl_response = scram.as_mut().unwrap().message(); let sasl_response = scram.message();
// SASLInitialResponse (F) // SASLInitialResponse (F)
let mut res = BytesMut::new(); let mut res = BytesMut::new();
@@ -265,7 +168,7 @@ impl Server {
+ sasl_response.len() as i32, // length of SASL response + sasl_response.len() as i32, // length of SASL response
); );
res.put_slice(format!("{}\0", SCRAM_SHA_256).as_bytes()); res.put_slice(&format!("{}\0", SCRAM_SHA_256).as_bytes()[..]);
res.put_i32(sasl_response.len() as i32); res.put_i32(sasl_response.len() as i32);
res.put(sasl_response); res.put(sasl_response);
@@ -283,16 +186,11 @@ impl Server {
match stream.read_exact(&mut sasl_data).await { match stream.read_exact(&mut sasl_data).await {
Ok(_) => (), Ok(_) => (),
Err(_) => { Err(_) => return Err(Error::SocketError),
return Err(Error::ServerStartupError(
"sasl cont message".into(),
server_identifier,
))
}
}; };
let msg = BytesMut::from(&sasl_data[..]); let msg = BytesMut::from(&sasl_data[..]);
let sasl_response = scram.as_mut().unwrap().update(&msg)?; let sasl_response = scram.update(&msg)?;
// SASLResponse // SASLResponse
let mut res = BytesMut::new(); let mut res = BytesMut::new();
@@ -309,19 +207,10 @@ impl Server {
let mut sasl_final = vec![0u8; len as usize - 8]; let mut sasl_final = vec![0u8; len as usize - 8];
match stream.read_exact(&mut sasl_final).await { match stream.read_exact(&mut sasl_final).await {
Ok(_) => (), Ok(_) => (),
Err(_) => { Err(_) => return Err(Error::SocketError),
return Err(Error::ServerStartupError(
"sasl final message".into(),
server_identifier,
))
}
}; };
match scram match scram.finish(&BytesMut::from(&sasl_final[..])) {
.as_mut()
.unwrap()
.finish(&BytesMut::from(&sasl_final[..]))
{
Ok(_) => { Ok(_) => {
debug!("SASL authentication successful"); debug!("SASL authentication successful");
} }
@@ -344,12 +233,7 @@ impl Server {
'E' => { 'E' => {
let error_code = match stream.read_u8().await { let error_code = match stream.read_u8().await {
Ok(error_code) => error_code, Ok(error_code) => error_code,
Err(_) => { Err(_) => return Err(Error::SocketError),
return Err(Error::ServerStartupError(
"error code message".into(),
server_identifier,
))
}
}; };
trace!("Error: {}", error_code); trace!("Error: {}", error_code);
@@ -365,12 +249,7 @@ impl Server {
match stream.read_exact(&mut error).await { match stream.read_exact(&mut error).await {
Ok(_) => (), Ok(_) => (),
Err(_) => { Err(_) => return Err(Error::SocketError),
return Err(Error::ServerStartupError(
"error message".into(),
server_identifier,
))
}
}; };
// TODO: the error message contains multiple fields; we can decode them and // TODO: the error message contains multiple fields; we can decode them and
@@ -389,12 +268,7 @@ impl Server {
match stream.read_exact(&mut param).await { match stream.read_exact(&mut param).await {
Ok(_) => (), Ok(_) => (),
Err(_) => { Err(_) => return Err(Error::SocketError),
return Err(Error::ServerStartupError(
"parameter status message".into(),
server_identifier,
))
}
}; };
// Save the parameter so we can pass it to the client later. // Save the parameter so we can pass it to the client later.
@@ -411,22 +285,12 @@ impl Server {
// See: <https://www.postgresql.org/docs/12/protocol-message-formats.html>. // See: <https://www.postgresql.org/docs/12/protocol-message-formats.html>.
process_id = match stream.read_i32().await { process_id = match stream.read_i32().await {
Ok(id) => id, Ok(id) => id,
Err(_) => { Err(_) => return Err(Error::SocketError),
return Err(Error::ServerStartupError(
"process id message".into(),
server_identifier,
))
}
}; };
secret_key = match stream.read_i32().await { secret_key = match stream.read_i32().await {
Ok(id) => id, Ok(id) => id,
Err(_) => { Err(_) => return Err(Error::SocketError),
return Err(Error::ServerStartupError(
"secret key message".into(),
server_identifier,
))
}
}; };
} }
@@ -436,12 +300,7 @@ impl Server {
match stream.read_exact(&mut idle).await { match stream.read_exact(&mut idle).await {
Ok(_) => (), Ok(_) => (),
Err(_) => { Err(_) => return Err(Error::SocketError),
return Err(Error::ServerStartupError(
"transaction status message".into(),
server_identifier,
))
}
}; };
let (read, write) = stream.into_split(); let (read, write) = stream.into_split();
@@ -449,29 +308,19 @@ impl Server {
let mut server = Server { let mut server = Server {
address: address.clone(), address: address.clone(),
read: BufReader::new(read), read: BufReader::new(read),
write, write: write,
buffer: BytesMut::with_capacity(8196), buffer: BytesMut::with_capacity(8196),
is_async: false, server_info: server_info,
server_info, process_id: process_id,
process_id, secret_key: secret_key,
secret_key,
in_transaction: false, in_transaction: false,
data_available: false, data_available: false,
bad: false, bad: false,
needs_cleanup: false, client_server_map: client_server_map,
client_server_map,
connected_at: chrono::offset::Utc::now().naive_utc(), connected_at: chrono::offset::Utc::now().naive_utc(),
stats, stats: stats,
application_name: String::new(), application_name: String::new(),
last_activity: SystemTime::now(), last_activity: SystemTime::now(),
mirror_manager: match address.mirrors.len() {
0 => None,
_ => Some(MirroringManager::from_addresses(
user.clone(),
database.to_owned(),
address.mirrors.clone(),
)),
},
}; };
server.set_name("pgcat").await?; server.set_name("pgcat").await?;
@@ -483,10 +332,7 @@ impl Server {
// Means we implemented the protocol wrong or we're not talking to a Postgres server. // Means we implemented the protocol wrong or we're not talking to a Postgres server.
_ => { _ => {
error!("Unknown code: {}", code); error!("Unknown code: {}", code);
return Err(Error::ProtocolSyncError(format!( return Err(Error::ProtocolSyncError);
"Unknown server code: {}",
code
)));
} }
}; };
} }
@@ -496,7 +342,7 @@ impl Server {
/// Uses a separate connection that's not part of the connection pool. /// Uses a separate connection that's not part of the connection pool.
pub async fn cancel( pub async fn cancel(
host: &str, host: &str,
port: u16, port: &str,
process_id: i32, process_id: i32,
secret_key: i32, secret_key: i32,
) -> Result<(), Error> { ) -> Result<(), Error> {
@@ -504,10 +350,9 @@ impl Server {
Ok(stream) => stream, Ok(stream) => stream,
Err(err) => { Err(err) => {
error!("Could not connect to server: {}", err); error!("Could not connect to server: {}", err);
return Err(Error::SocketError("Error reading cancel message".into())); return Err(Error::SocketError);
} }
}; };
configure_socket(&stream);
debug!("Sending CancelRequest"); debug!("Sending CancelRequest");
@@ -517,13 +362,13 @@ impl Server {
bytes.put_i32(process_id); bytes.put_i32(process_id);
bytes.put_i32(secret_key); bytes.put_i32(secret_key);
write_all(&mut stream, bytes).await Ok(write_all(&mut stream, bytes).await?)
} }
/// Send messages to the server from the client. /// Send messages to the server from the client.
pub async fn send(&mut self, messages: &BytesMut) -> Result<(), Error> { pub async fn send(&mut self, messages: BytesMut) -> Result<(), Error> {
self.mirror_send(messages); self.stats
self.stats().data_sent(messages.len()); .data_sent(messages.len(), self.process_id, self.address.id);
match write_all_half(&mut self.write, messages).await { match write_all_half(&mut self.write, messages).await {
Ok(_) => { Ok(_) => {
@@ -539,16 +384,6 @@ impl Server {
} }
} }
/// Switch to async mode, flushing messages as soon
/// as we receive them without buffering or waiting for "ReadyForQuery".
pub fn switch_async(&mut self, on: bool) {
if on {
self.is_async = true;
} else {
self.is_async = false;
}
}
/// Receive data from the server in response to a client request. /// 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 /// 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. /// in order to receive all data the server has to offer.
@@ -595,10 +430,7 @@ impl Server {
// Something totally unexpected, this is not a Postgres server we know. // Something totally unexpected, this is not a Postgres server we know.
_ => { _ => {
self.bad = true; self.bad = true;
return Err(Error::ProtocolSyncError(format!( return Err(Error::ProtocolSyncError);
"Unknown transaction state: {}",
transaction_state
)));
} }
}; };
@@ -608,46 +440,10 @@ impl Server {
break; break;
} }
// CommandComplete
'C' => {
let mut command_tag = String::new();
match message.reader().read_to_string(&mut command_tag) {
Ok(_) => {
// 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_tag.as_str() {
"SET\0" => {
// 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 discard statements sent
if !self.in_transaction {
debug!("Server connection marked for clean up");
self.needs_cleanup = true;
}
}
"PREPARE\0" => {
debug!("Server connection marked for clean up");
self.needs_cleanup = true;
}
_ => (),
}
}
Err(err) => {
warn!("Encountered an error while parsing CommandTag {}", err);
}
}
}
// DataRow // DataRow
'D' => { 'D' => {
// More data is available after this message, this is not the end of the reply. // More data is available after this message, this is not the end of the reply.
// If we're async, flush to client now. self.data_available = true;
if !self.is_async {
self.data_available = true;
}
// Don't flush yet, the more we buffer, the faster this goes...up to a limit. // Don't flush yet, the more we buffer, the faster this goes...up to a limit.
if self.buffer.len() >= 8196 { if self.buffer.len() >= 8196 {
@@ -660,20 +456,13 @@ impl Server {
// CopyOutResponse: copy is starting from the server to the client. // CopyOutResponse: copy is starting from the server to the client.
'H' => { 'H' => {
// If we're in async mode, flush now. self.data_available = true;
if !self.is_async {
self.data_available = true;
}
break; break;
} }
// CopyData // CopyData: we are not buffering this one because there will be many more
'd' => { // and we don't know how big this packet could be, best not to take a risk.
// Don't flush yet, buffer until we reach limit 'd' => break,
if self.buffer.len() >= 8196 {
break;
}
}
// CopyDone // CopyDone
// Buffer until ReadyForQuery shows up, so don't exit the loop yet. // Buffer until ReadyForQuery shows up, so don't exit the loop yet.
@@ -683,16 +472,13 @@ impl Server {
// Keep buffering until ReadyForQuery shows up. // Keep buffering until ReadyForQuery shows up.
_ => (), _ => (),
}; };
if self.is_async {
break;
}
} }
let bytes = self.buffer.clone(); let bytes = self.buffer.clone();
// Keep track of how much data we got from the server for stats. // Keep track of how much data we got from the server for stats.
self.stats().data_received(bytes.len()); self.stats
.data_received(bytes.len(), self.process_id, self.address.id);
// Clear the buffer for next query. // Clear the buffer for next query.
self.buffer.clear(); self.buffer.clear();
@@ -707,7 +493,6 @@ impl Server {
/// If the server is still inside a transaction. /// If the server is still inside a transaction.
/// If the client disconnects while the server is in a transaction, we will clean it up. /// If the client disconnects while the server is in a transaction, we will clean it up.
pub fn in_transaction(&self) -> bool { pub fn in_transaction(&self) -> bool {
debug!("Server in transaction: {}", self.in_transaction);
self.in_transaction self.in_transaction
} }
@@ -744,7 +529,7 @@ impl Server {
self.process_id, self.process_id,
self.secret_key, self.secret_key,
self.address.host.clone(), self.address.host.clone(),
self.address.port, self.address.port.clone(),
), ),
); );
} }
@@ -755,7 +540,7 @@ impl Server {
pub async fn query(&mut self, query: &str) -> Result<(), Error> { pub async fn query(&mut self, query: &str) -> Result<(), Error> {
let query = simple_query(query); let query = simple_query(query);
self.send(&query).await?; self.send(query).await?;
loop { loop {
let _ = self.recv().await?; let _ = self.recv().await?;
@@ -768,183 +553,34 @@ impl Server {
Ok(()) 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!("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 `DISCARD ALL` if we think the session is altered instead of just sending
// it before each checkin.
if self.needs_cleanup {
warn!("Server returned with session state altered, discarding state");
self.query("DISCARD ALL").await?;
self.needs_cleanup = false;
}
Ok(())
}
/// A shorthand for `SET application_name = $1`. /// A shorthand for `SET application_name = $1`.
#[allow(dead_code)]
pub async fn set_name(&mut self, name: &str) -> Result<(), Error> { pub async fn set_name(&mut self, name: &str) -> Result<(), Error> {
if self.application_name != name { if self.application_name != name {
self.application_name = name.to_string(); self.application_name = name.to_string();
// We don't want `SET application_name` to mark the server connection Ok(self
// as needing cleanup
let needs_cleanup_before = self.needs_cleanup;
let result = Ok(self
.query(&format!("SET application_name = '{}'", name)) .query(&format!("SET application_name = '{}'", name))
.await?); .await?)
self.needs_cleanup = needs_cleanup_before;
result
} else { } else {
Ok(()) Ok(())
} }
} }
/// get Server stats
pub fn stats(&self) -> Arc<ServerStats> {
self.stats.clone()
}
/// Get the servers address. /// Get the servers address.
#[allow(dead_code)] #[allow(dead_code)]
pub fn address(&self) -> Address { pub fn address(&self) -> Address {
self.address.clone() self.address.clone()
} }
/// Get the server's unique identifier.
pub fn process_id(&self) -> i32 {
self.process_id
}
// Get server's latest response timestamp // Get server's latest response timestamp
pub fn last_activity(&self) -> SystemTime { pub fn last_activity(&self) -> SystemTime {
self.last_activity self.last_activity
} }
// Marks a connection as needing DISCARD ALL at checkin
pub fn mark_dirty(&mut self) {
self.needs_cleanup = true;
}
pub fn mirror_send(&mut self, bytes: &BytesMut) {
match self.mirror_manager.as_mut() {
Some(manager) => manager.send(bytes),
None => (),
}
}
pub fn mirror_disconnect(&mut self) {
match self.mirror_manager.as_mut() {
Some(manager) => manager.disconnect(),
None => (),
}
}
// This is so we can execute out of band queries to the server.
// The connection will be opened, the query executed and closed.
pub async fn exec_simple_query(
address: &Address,
user: &User,
query: &str,
) -> Result<Vec<String>, Error> {
let client_server_map: ClientServerMap = Arc::new(Mutex::new(HashMap::new()));
debug!("Connecting to server to obtain auth hashes.");
let mut server = Server::startup(
address,
user,
&address.database,
client_server_map,
Arc::new(ServerStats::default()),
Arc::new(RwLock::new(None)),
)
.await?;
debug!("Connected!, sending query.");
server.send(&simple_query(query)).await?;
let mut message = server.recv().await?;
Ok(parse_query_message(&mut message).await?)
}
}
async fn parse_query_message(message: &mut BytesMut) -> Result<Vec<String>, Error> {
let mut pair = Vec::<String>::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 { impl Drop for Server {
@@ -952,10 +588,8 @@ impl Drop for Server {
/// the socket is in non-blocking mode, so it may not be ready /// the socket is in non-blocking mode, so it may not be ready
/// for a write. /// for a write.
fn drop(&mut self) { fn drop(&mut self) {
self.mirror_disconnect(); self.stats
.server_disconnecting(self.process_id(), self.address.id);
// Update statistics
self.stats.disconnect();
let mut bytes = BytesMut::with_capacity(4); let mut bytes = BytesMut::with_capacity(4);
bytes.put_u8(b'X'); bytes.put_u8(b'X');
@@ -973,8 +607,7 @@ impl Drop for Server {
let duration = now - self.connected_at; let duration = now - self.connected_at;
info!( info!(
"Server connection closed {:?}, session duration: {}", "Server connection closed, session duration: {}",
self.address,
crate::format_duration(&duration) crate::format_duration(&duration)
); );
} }

View File

@@ -1,4 +1,3 @@
use serde_derive::{Deserialize, Serialize};
/// Implements various sharding functions. /// Implements various sharding functions.
use sha1::{Digest, Sha1}; use sha1::{Digest, Sha1};
@@ -6,23 +5,12 @@ use sha1::{Digest, Sha1};
const PARTITION_HASH_SEED: u64 = 0x7A5B22367996DCFD; const PARTITION_HASH_SEED: u64 = 0x7A5B22367996DCFD;
/// The sharding functions we support. /// The sharding functions we support.
#[derive(Debug, PartialEq, Copy, Clone, Serialize, Deserialize, Hash, std::cmp::Eq)] #[derive(Debug, PartialEq, Copy, Clone)]
pub enum ShardingFunction { pub enum ShardingFunction {
#[serde(alias = "pg_bigint_hash", alias = "PgBigintHash")]
PgBigintHash, PgBigintHash,
#[serde(alias = "sha1", alias = "Sha1")]
Sha1, Sha1,
} }
impl ToString for ShardingFunction {
fn to_string(&self) -> String {
match *self {
ShardingFunction::PgBigintHash => "pg_bigint_hash".to_string(),
ShardingFunction::Sha1 => "sha1".to_string(),
}
}
}
/// The sharder. /// The sharder.
pub struct Sharder { pub struct Sharder {
/// Number of shards in the cluster. /// Number of shards in the cluster.
@@ -133,7 +121,7 @@ impl Sharder {
#[inline] #[inline]
fn combine(mut a: u64, b: u64) -> u64 { fn combine(mut a: u64, b: u64) -> u64 {
a ^= b a ^= b
.wrapping_add(0x49a0f4dd15e5a8e3_u64) .wrapping_add(0x49a0f4dd15e5a8e3 as u64)
.wrapping_add(a << 54) .wrapping_add(a << 54)
.wrapping_add(a >> 7); .wrapping_add(a >> 7);
a a
@@ -141,7 +129,7 @@ impl Sharder {
#[inline] #[inline]
fn pg_u32_hash(k: u32) -> u64 { fn pg_u32_hash(k: u32) -> u64 {
let mut a: u32 = 0x9e3779b9_u32 + std::mem::size_of::<u32>() as u32 + 3923095_u32; let mut a: u32 = 0x9e3779b9 as u32 + std::mem::size_of::<u32>() as u32 + 3923095 as u32;
let mut b = a; let mut b = a;
let c = a; let c = a;

View File

@@ -1,136 +1,541 @@
use crate::pool::PoolIdentifier;
/// Statistics and reporting.
use arc_swap::ArcSwap; use arc_swap::ArcSwap;
/// Statistics and reporting.
use log::{info, warn}; use log::info;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use parking_lot::RwLock; use parking_lot::Mutex;
use std::collections::HashMap; use std::collections::HashMap;
use tokio::sync::mpsc::{channel, Receiver, Sender};
use std::sync::Arc; use crate::pool::get_number_of_addresses;
// Structs that hold stats for different resources
pub mod address;
pub mod client;
pub mod pool;
pub mod server;
pub use address::AddressStats;
pub use client::{ClientState, ClientStats};
pub use pool::PoolStats;
pub use server::{ServerState, ServerStats};
/// Convenience types for various stats
type ClientStatesLookup = HashMap<i32, Arc<ClientStats>>;
type ServerStatesLookup = HashMap<i32, Arc<ServerStats>>;
type PoolStatsLookup = HashMap<(String, String), Arc<PoolStats>>;
/// Stats for individual client connections
/// Used in SHOW CLIENTS.
static CLIENT_STATS: Lazy<Arc<RwLock<ClientStatesLookup>>> =
Lazy::new(|| Arc::new(RwLock::new(ClientStatesLookup::default())));
/// Stats for individual server connections
/// Used in SHOW SERVERS.
static SERVER_STATS: Lazy<Arc<RwLock<ServerStatesLookup>>> =
Lazy::new(|| Arc::new(RwLock::new(ServerStatesLookup::default())));
/// Aggregate stats for each pool (a pool is identified by database name and username)
/// Used in SHOW POOLS.
static POOL_STATS: Lazy<Arc<RwLock<PoolStatsLookup>>> =
Lazy::new(|| Arc::new(RwLock::new(PoolStatsLookup::default())));
/// The statistics reporter. An instance is given to each possible source of statistics,
/// e.g. client stats, server stats, connection pool stats.
pub static REPORTER: Lazy<ArcSwap<Reporter>> = pub static REPORTER: Lazy<ArcSwap<Reporter>> =
Lazy::new(|| ArcSwap::from_pointee(Reporter::default())); Lazy::new(|| ArcSwap::from_pointee(Reporter::default()));
/// Latest stats updated every second; used in SHOW STATS and other admin commands.
static LATEST_STATS: Lazy<Mutex<HashMap<usize, HashMap<String, i64>>>> =
Lazy::new(|| Mutex::new(HashMap::new()));
/// Statistics period used for average calculations. /// Statistics period used for average calculations.
/// 15 seconds. /// 15 seconds.
static STAT_PERIOD: u64 = 15000; static STAT_PERIOD: u64 = 15000;
/// The names for the events reported
/// to the statistics collector.
#[derive(Debug, Clone, Copy)]
enum EventName {
CheckoutTime,
Query,
Transaction,
DataSent,
DataReceived,
ClientWaiting,
ClientActive,
ClientIdle,
ClientDisconnecting,
ServerActive,
ServerIdle,
ServerTested,
ServerLogin,
ServerDisconnecting,
UpdateStats,
UpdateAverages,
}
/// Event data sent to the collector
/// from clients and servers.
#[derive(Debug)]
pub struct Event {
/// The name of the event being reported.
name: EventName,
/// The value being reported. Meaning differs based on event name.
value: i64,
/// The client or server connection reporting the event.
process_id: i32,
/// The server the client is connected to.
address_id: usize,
}
/// The statistics reporter. An instance is given /// The statistics reporter. An instance is given
/// to each possible source of statistics, /// to each possible source of statistics,
/// e.g. clients, servers, connection pool. /// e.g. clients, servers, connection pool.
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug)]
pub struct Reporter {} pub struct Reporter {
tx: Sender<Event>,
}
impl Reporter { impl Default for Reporter {
/// Register a client with the stats system. The stats system uses client_id fn default() -> Reporter {
/// to track and aggregate statistics from all source that relate to that client let (tx, _rx) = channel(5);
fn client_register(&self, client_id: i32, stats: Arc<ClientStats>) { Reporter { tx }
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<ServerStats>) {
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);
}
/// Register a pool with the stats system.
fn pool_register(&self, identifier: PoolIdentifier, stats: Arc<PoolStats>) {
POOL_STATS
.write()
.insert((identifier.db, identifier.user), stats);
} }
} }
/// The statistics collector which used for calculating averages impl Reporter {
/// There is only one collector (kind of like a singleton) /// Create a new Reporter instance.
/// it updates averages every 15 seconds. pub fn new(tx: Sender<Event>) -> Reporter {
#[derive(Default)] Reporter { tx: tx }
pub struct Collector {} }
/// Report a query executed by a client against
/// a server identified by the `address_id`.
pub fn query(&self, process_id: i32, address_id: usize) {
let event = Event {
name: EventName::Query,
value: 1,
process_id: process_id,
address_id: address_id,
};
let _ = self.tx.try_send(event);
}
/// Report a transaction executed by a client against
/// a server identified by the `address_id`.
pub fn transaction(&self, process_id: i32, address_id: usize) {
let event = Event {
name: EventName::Transaction,
value: 1,
process_id: process_id,
address_id: address_id,
};
let _ = self.tx.try_send(event);
}
/// Report data sent to a server identified by `address_id`.
/// The `amount` is measured in bytes.
pub fn data_sent(&self, amount: usize, process_id: i32, address_id: usize) {
let event = Event {
name: EventName::DataSent,
value: amount as i64,
process_id: process_id,
address_id: address_id,
};
let _ = self.tx.try_send(event);
}
/// Report data received from a server identified by `address_id`.
/// The `amount` is measured in bytes.
pub fn data_received(&self, amount: usize, process_id: i32, address_id: usize) {
let event = Event {
name: EventName::DataReceived,
value: amount as i64,
process_id: process_id,
address_id: address_id,
};
let _ = self.tx.try_send(event);
}
/// Time spent waiting to get a healthy connection from the pool
/// for a server identified by `address_id`.
/// Measured in milliseconds.
pub fn checkout_time(&self, ms: u128, process_id: i32, address_id: usize) {
let event = Event {
name: EventName::CheckoutTime,
value: ms as i64,
process_id: process_id,
address_id: address_id,
};
let _ = self.tx.try_send(event);
}
/// Reports a client identified by `process_id` waiting for a connection
/// to a server identified by `address_id`.
pub fn client_waiting(&self, process_id: i32, address_id: usize) {
let event = Event {
name: EventName::ClientWaiting,
value: 1,
process_id: process_id,
address_id: address_id,
};
let _ = self.tx.try_send(event);
}
/// Reports a client identified by `process_id` is done waiting for a connection
/// to a server identified by `address_id` and is about to query the server.
pub fn client_active(&self, process_id: i32, address_id: usize) {
let event = Event {
name: EventName::ClientActive,
value: 1,
process_id: process_id,
address_id: address_id,
};
let _ = self.tx.try_send(event);
}
/// Reports a client identified by `process_id` is done querying the server
/// identified by `address_id` and is no longer active.
pub fn client_idle(&self, process_id: i32, address_id: usize) {
let event = Event {
name: EventName::ClientIdle,
value: 1,
process_id: process_id,
address_id: address_id,
};
let _ = self.tx.try_send(event);
}
/// Reports a client identified by `process_id` is disconecting from the pooler.
/// The last server it was connected to is identified by `address_id`.
pub fn client_disconnecting(&self, process_id: i32, address_id: usize) {
let event = Event {
name: EventName::ClientDisconnecting,
value: 1,
process_id: process_id,
address_id: address_id,
};
let _ = self.tx.try_send(event);
}
/// Reports a server connection identified by `process_id` for
/// a configured server identified by `address_id` is actively used
/// by a client.
pub fn server_active(&self, process_id: i32, address_id: usize) {
let event = Event {
name: EventName::ServerActive,
value: 1,
process_id: process_id,
address_id: address_id,
};
let _ = self.tx.try_send(event);
}
/// Reports a server connection identified by `process_id` for
/// a configured server identified by `address_id` is no longer
/// actively used by a client and is now idle.
pub fn server_idle(&self, process_id: i32, address_id: usize) {
let event = Event {
name: EventName::ServerIdle,
value: 1,
process_id: process_id,
address_id: address_id,
};
let _ = self.tx.try_send(event);
}
/// Reports a server connection identified by `process_id` for
/// a configured server identified by `address_id` is attempting
/// to login.
pub fn server_login(&self, process_id: i32, address_id: usize) {
let event = Event {
name: EventName::ServerLogin,
value: 1,
process_id: process_id,
address_id: address_id,
};
let _ = self.tx.try_send(event);
}
/// Reports a server connection identified by `process_id` for
/// a configured server identified by `address_id` is being
/// tested before being given to a client.
pub fn server_tested(&self, process_id: i32, address_id: usize) {
let event = Event {
name: EventName::ServerTested,
value: 1,
process_id: process_id,
address_id: address_id,
};
let _ = self.tx.try_send(event);
}
/// Reports a server connection identified by `process_id` is disconecting from the pooler.
/// The configured server it was connected to is identified by `address_id`.
pub fn server_disconnecting(&self, process_id: i32, address_id: usize) {
let event = Event {
name: EventName::ServerDisconnecting,
value: 1,
process_id: process_id,
address_id: address_id,
};
let _ = self.tx.try_send(event);
}
}
/// The statistics collector which is receiving statistics
/// from clients, servers, and the connection pool. There is
/// only one collector (kind of like a singleton).
/// The collector can trigger events on its own, e.g.
/// it updates aggregates every second and averages every
/// 15 seconds.
pub struct Collector {
rx: Receiver<Event>,
tx: Sender<Event>,
}
impl Collector { impl Collector {
/// Create a new collector instance. There should only be one instance
/// at a time. This is ensured by mpsc which allows only one receiver.
pub fn new(rx: Receiver<Event>, tx: Sender<Event>) -> Collector {
Collector { rx, tx }
}
/// The statistics collection handler. It will collect statistics /// The statistics collection handler. It will collect statistics
/// for `address_id`s starting at 0 up to `addresses`. /// for `address_id`s starting at 0 up to `addresses`.
pub async fn collect(&mut self) { pub async fn collect(&mut self) {
info!("Events reporter started"); info!("Events reporter started");
let stats_template = HashMap::from([
("total_query_count", 0),
("total_query_time", 0),
("total_received", 0),
("total_sent", 0),
("total_xact_count", 0),
("total_xact_time", 0),
("total_wait_time", 0),
("avg_query_count", 0),
("avg_query_time", 0),
("avg_recv", 0),
("avg_sent", 0),
("avg_xact_count", 0),
("avg_xact_time", 0),
("avg_wait_time", 0),
("maxwait_us", 0),
("maxwait", 0),
("cl_waiting", 0),
("cl_active", 0),
("cl_idle", 0),
("sv_idle", 0),
("sv_active", 0),
("sv_login", 0),
("sv_tested", 0),
]);
let mut stats = HashMap::new();
// Stats saved after each iteration of the flush event. Used in calculation
// of averages in the last flush period.
let mut old_stats: HashMap<usize, HashMap<String, i64>> = HashMap::new();
// Track which state the client and server are at any given time.
let mut client_server_states: HashMap<usize, HashMap<i32, EventName>> = HashMap::new();
// Flush stats to StatsD and calculate averages every 15 seconds.
let tx = self.tx.clone();
tokio::task::spawn(async move {
let mut interval =
tokio::time::interval(tokio::time::Duration::from_millis(STAT_PERIOD / 15));
loop {
interval.tick().await;
let address_count = get_number_of_addresses();
for address_id in 0..address_count {
let _ = tx.try_send(Event {
name: EventName::UpdateStats,
value: 0,
process_id: -1,
address_id: address_id,
});
}
}
});
let tx = self.tx.clone();
tokio::task::spawn(async move { tokio::task::spawn(async move {
let mut interval = let mut interval =
tokio::time::interval(tokio::time::Duration::from_millis(STAT_PERIOD)); tokio::time::interval(tokio::time::Duration::from_millis(STAT_PERIOD));
loop { loop {
interval.tick().await; interval.tick().await;
let address_count = get_number_of_addresses();
for stats in SERVER_STATS.read().values() { for address_id in 0..address_count {
stats.address_stats().update_averages(); let _ = tx.try_send(Event {
name: EventName::UpdateAverages,
value: 0,
process_id: -1,
address_id: address_id,
});
} }
} }
}); });
// The collector loop
loop {
let stat = match self.rx.recv().await {
Some(stat) => stat,
None => {
info!("Events collector is shutting down");
return;
}
};
let stats = stats
.entry(stat.address_id)
.or_insert(stats_template.clone());
let client_server_states = client_server_states
.entry(stat.address_id)
.or_insert(HashMap::new());
let old_stats = old_stats.entry(stat.address_id).or_insert(HashMap::new());
// Some are counters, some are gauges...
match stat.name {
EventName::Query => {
let counter = stats.entry("total_query_count").or_insert(0);
*counter += stat.value;
}
EventName::Transaction => {
let counter = stats.entry("total_xact_count").or_insert(0);
*counter += stat.value;
}
EventName::DataSent => {
let counter = stats.entry("total_sent").or_insert(0);
*counter += stat.value;
}
EventName::DataReceived => {
let counter = stats.entry("total_received").or_insert(0);
*counter += stat.value;
}
EventName::CheckoutTime => {
let counter = stats.entry("total_wait_time").or_insert(0);
*counter += stat.value;
let counter = stats.entry("maxwait_us").or_insert(0);
let mic_part = stat.value % 1_000_000;
// Report max time here
if mic_part > *counter {
*counter = mic_part;
}
let counter = stats.entry("maxwait").or_insert(0);
let seconds = *counter / 1_000_000;
if seconds > *counter {
*counter = seconds;
}
}
EventName::ClientActive
| EventName::ClientWaiting
| EventName::ClientIdle
| EventName::ServerActive
| EventName::ServerIdle
| EventName::ServerTested
| EventName::ServerLogin => {
client_server_states.insert(stat.process_id, stat.name);
}
EventName::ClientDisconnecting | EventName::ServerDisconnecting => {
client_server_states.remove(&stat.process_id);
}
EventName::UpdateStats => {
// Calculate connection states
for (_, state) in client_server_states.iter() {
match state {
EventName::ClientActive => {
let counter = stats.entry("cl_active").or_insert(0);
*counter += 1;
}
EventName::ClientWaiting => {
let counter = stats.entry("cl_waiting").or_insert(0);
*counter += 1;
}
EventName::ServerIdle => {
let counter = stats.entry("sv_idle").or_insert(0);
*counter += 1;
}
EventName::ServerActive => {
let counter = stats.entry("sv_active").or_insert(0);
*counter += 1;
}
EventName::ServerTested => {
let counter = stats.entry("sv_tested").or_insert(0);
*counter += 1;
}
EventName::ServerLogin => {
let counter = stats.entry("sv_login").or_insert(0);
*counter += 1;
}
EventName::ClientIdle => {
let counter = stats.entry("cl_idle").or_insert(0);
*counter += 1;
}
_ => unreachable!(),
};
}
// Update latest stats used in SHOW STATS
let mut guard = LATEST_STATS.lock();
for (key, value) in stats.iter() {
let entry = guard.entry(stat.address_id).or_insert(HashMap::new());
entry.insert(key.to_string(), value.clone());
}
// These are re-calculated every iteration of the loop, so we don't want to add values
// from the last iteration.
for stat in &[
"cl_active",
"cl_waiting",
"cl_idle",
"sv_idle",
"sv_active",
"sv_tested",
"sv_login",
"maxwait",
"maxwait_us",
] {
stats.insert(stat, 0);
}
}
EventName::UpdateAverages => {
// Calculate averages
for stat in &[
"avg_query_count",
"avg_query_time",
"avg_recv",
"avg_sent",
"avg_xact_time",
"avg_xact_count",
"avg_wait_time",
] {
let total_name = match stat {
&"avg_recv" => "total_received".to_string(), // Because PgBouncer is saving bytes
_ => stat.replace("avg_", "total_"),
};
let old_value = old_stats.entry(total_name.clone()).or_insert(0);
let new_value = stats.get(total_name.as_str()).unwrap_or(&0).to_owned();
let avg = (new_value - *old_value) / (STAT_PERIOD as i64 / 1_000); // Avg / second
stats.insert(stat, avg);
*old_value = new_value;
}
}
};
}
} }
} }
/// Get a snapshot of client statistics. /// Get a snapshot of statistics. Updated once a second
/// by the `Collector`. /// by the `Collector`.
pub fn get_client_stats() -> ClientStatesLookup { pub fn get_stats() -> HashMap<usize, HashMap<String, i64>> {
CLIENT_STATS.read().clone() LATEST_STATS.lock().clone()
}
/// Get a snapshot of server statistics.
/// by the `Collector`.
pub fn get_server_stats() -> ServerStatesLookup {
SERVER_STATS.read().clone()
}
/// Get a snapshot of pool statistics.
/// by the `Collector`.
pub fn get_pool_stats() -> PoolStatsLookup {
POOL_STATS.read().clone()
} }
/// Get the statistics reporter used to update stats across the pools/clients. /// Get the statistics reporter used to update stats across the pools/clients.

View File

@@ -1,149 +0,0 @@
use log::warn;
use std::sync::atomic::*;
use std::sync::Arc;
/// Internal address stats
#[derive(Debug, Clone, Default)]
pub struct AddressStats {
pub total_xact_count: Arc<AtomicU64>,
pub total_query_count: Arc<AtomicU64>,
pub total_received: Arc<AtomicU64>,
pub total_sent: Arc<AtomicU64>,
pub total_xact_time: Arc<AtomicU64>,
pub total_query_time: Arc<AtomicU64>,
pub total_wait_time: Arc<AtomicU64>,
pub total_errors: Arc<AtomicU64>,
pub avg_query_count: Arc<AtomicU64>,
pub avg_query_time: Arc<AtomicU64>,
pub avg_recv: Arc<AtomicU64>,
pub avg_sent: Arc<AtomicU64>,
pub avg_errors: Arc<AtomicU64>,
pub avg_xact_time: Arc<AtomicU64>,
pub avg_xact_count: Arc<AtomicU64>,
pub avg_wait_time: Arc<AtomicU64>,
}
impl IntoIterator for AddressStats {
type Item = (String, u64);
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
vec![
(
"total_xact_count".to_string(),
self.total_xact_count.load(Ordering::Relaxed),
),
(
"total_query_count".to_string(),
self.total_query_count.load(Ordering::Relaxed),
),
(
"total_received".to_string(),
self.total_received.load(Ordering::Relaxed),
),
(
"total_sent".to_string(),
self.total_sent.load(Ordering::Relaxed),
),
(
"total_xact_time".to_string(),
self.total_xact_time.load(Ordering::Relaxed),
),
(
"total_query_time".to_string(),
self.total_query_time.load(Ordering::Relaxed),
),
(
"total_wait_time".to_string(),
self.total_wait_time.load(Ordering::Relaxed),
),
(
"total_errors".to_string(),
self.total_errors.load(Ordering::Relaxed),
),
(
"avg_xact_count".to_string(),
self.avg_xact_count.load(Ordering::Relaxed),
),
(
"avg_query_count".to_string(),
self.avg_query_count.load(Ordering::Relaxed),
),
(
"avg_recv".to_string(),
self.avg_recv.load(Ordering::Relaxed),
),
(
"avg_sent".to_string(),
self.avg_sent.load(Ordering::Relaxed),
),
(
"avg_errors".to_string(),
self.avg_errors.load(Ordering::Relaxed),
),
(
"avg_xact_time".to_string(),
self.avg_xact_time.load(Ordering::Relaxed),
),
(
"avg_query_time".to_string(),
self.avg_query_time.load(Ordering::Relaxed),
),
(
"avg_wait_time".to_string(),
self.avg_wait_time.load(Ordering::Relaxed),
),
]
.into_iter()
}
}
impl AddressStats {
pub fn error(&self) {
self.total_errors.fetch_add(1, Ordering::Relaxed);
}
pub fn update_averages(&self) {
let (totals, averages) = self.fields_iterators();
for data in totals.iter().zip(averages.iter()) {
let (total, average) = data;
if let Err(err) = average.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |avg| {
let total = total.load(Ordering::Relaxed);
let avg = (total - avg) / (crate::stats::STAT_PERIOD / 1_000); // Avg / second
Some(avg)
}) {
warn!("Could not update averages for addresses stats, {:?}", err);
}
}
}
pub fn populate_row(&self, row: &mut Vec<String>) {
for (_key, value) in self.clone() {
row.push(value.to_string());
}
}
fn fields_iterators(&self) -> (Vec<Arc<AtomicU64>>, Vec<Arc<AtomicU64>>) {
let mut totals: Vec<Arc<AtomicU64>> = Vec::new();
let mut averages: Vec<Arc<AtomicU64>> = Vec::new();
totals.push(self.total_xact_count.clone());
averages.push(self.avg_xact_count.clone());
totals.push(self.total_query_count.clone());
averages.push(self.avg_query_count.clone());
totals.push(self.total_received.clone());
averages.push(self.avg_recv.clone());
totals.push(self.total_sent.clone());
averages.push(self.avg_sent.clone());
totals.push(self.total_xact_time.clone());
averages.push(self.avg_xact_time.clone());
totals.push(self.total_query_time.clone());
averages.push(self.avg_query_time.clone());
totals.push(self.total_wait_time.clone());
averages.push(self.avg_wait_time.clone());
totals.push(self.total_errors.clone());
averages.push(self.avg_errors.clone());
(totals, averages)
}
}

View File

@@ -1,182 +0,0 @@
use super::PoolStats;
use super::{get_reporter, Reporter};
use atomic_enum::atomic_enum;
use std::sync::atomic::*;
use std::sync::Arc;
use tokio::time::Instant;
/// The various states that a client can be in
#[atomic_enum]
#[derive(PartialEq)]
pub enum ClientState {
Idle = 0,
Waiting,
Active,
}
impl std::fmt::Display for ClientState {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match *self {
ClientState::Idle => write!(f, "idle"),
ClientState::Waiting => write!(f, "waiting"),
ClientState::Active => write!(f, "active"),
}
}
}
#[derive(Debug, Clone)]
/// Information we keep track of which can be queried by SHOW CLIENTS
pub struct ClientStats {
/// A random integer assigned to the client and used by stats to track the client
client_id: i32,
/// Data associated with the client, not writable, only set when we construct the ClientStat
application_name: String,
username: String,
pool_name: String,
connect_time: Instant,
pool_stats: Arc<PoolStats>,
reporter: Reporter,
/// Total time spent waiting for a connection from pool, measures in microseconds
pub total_wait_time: Arc<AtomicU64>,
/// Current state of the client
pub state: Arc<AtomicClientState>,
/// Number of transactions executed by this client
pub transaction_count: Arc<AtomicU64>,
/// Number of queries executed by this client
pub query_count: Arc<AtomicU64>,
/// Number of errors made by this client
pub error_count: Arc<AtomicU64>,
}
impl Default for ClientStats {
fn default() -> Self {
ClientStats {
client_id: 0,
connect_time: Instant::now(),
application_name: String::new(),
username: String::new(),
pool_name: String::new(),
pool_stats: Arc::new(PoolStats::default()),
total_wait_time: Arc::new(AtomicU64::new(0)),
state: Arc::new(AtomicClientState::new(ClientState::Idle)),
transaction_count: Arc::new(AtomicU64::new(0)),
query_count: Arc::new(AtomicU64::new(0)),
error_count: Arc::new(AtomicU64::new(0)),
reporter: get_reporter(),
}
}
}
impl ClientStats {
pub fn new(
client_id: i32,
application_name: &str,
username: &str,
pool_name: &str,
connect_time: Instant,
pool_stats: Arc<PoolStats>,
) -> Self {
Self {
client_id,
pool_stats,
connect_time,
application_name: application_name.to_string(),
username: username.to_string(),
pool_name: pool_name.to_string(),
..Default::default()
}
}
/// Reports a client is disconnecting from the pooler and
/// update metrics on the corresponding pool.
pub fn disconnect(&self) {
self.reporter.client_disconnecting(self.client_id);
self.pool_stats
.client_disconnect(self.state.load(Ordering::Relaxed))
}
/// Register a client with the stats system. The stats system uses client_id
/// to track and aggregate statistics from all source that relate to that client
pub fn register(&self, stats: Arc<ClientStats>) {
self.reporter.client_register(self.client_id, stats);
self.state.store(ClientState::Idle, Ordering::Relaxed);
self.pool_stats.cl_idle.fetch_add(1, Ordering::Relaxed);
}
/// Reports a client is done querying the server and is no longer assigned a server connection
pub fn idle(&self) {
self.pool_stats
.client_idle(self.state.load(Ordering::Relaxed));
self.state.store(ClientState::Idle, Ordering::Relaxed);
}
/// Reports a client is waiting for a connection
pub fn waiting(&self) {
self.pool_stats
.client_waiting(self.state.load(Ordering::Relaxed));
self.state.store(ClientState::Waiting, Ordering::Relaxed);
}
/// Reports a client is done waiting for a connection and is about to query the server.
pub fn active(&self) {
self.pool_stats
.client_active(self.state.load(Ordering::Relaxed));
self.state.store(ClientState::Active, Ordering::Relaxed);
}
/// Reports a client has failed to obtain a connection from a connection pool
pub fn checkout_error(&self) {
self.state.store(ClientState::Idle, Ordering::Relaxed);
}
/// Reports a client has had the server assigned to it be banned
pub fn ban_error(&self) {
self.state.store(ClientState::Idle, Ordering::Relaxed);
self.error_count.fetch_add(1, Ordering::Relaxed);
}
/// Reporters the time spent by a client waiting to get a healthy connection from the pool
pub fn checkout_time(&self, microseconds: u64) {
self.total_wait_time
.fetch_add(microseconds, Ordering::Relaxed);
}
/// 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()
}
}

View File

@@ -1,274 +0,0 @@
use crate::config::Pool;
use crate::config::PoolMode;
use crate::pool::PoolIdentifier;
use std::sync::atomic::*;
use std::sync::Arc;
use super::get_reporter;
use super::Reporter;
use super::{ClientState, ServerState};
#[derive(Debug, Clone, Default)]
/// A struct that holds information about a Pool .
pub struct PoolStats {
// Pool identifier, cannot be changed after creating the instance
identifier: PoolIdentifier,
// Pool Config, cannot be changed after creating the instance
config: Pool,
// A reference to the global reporter.
reporter: Reporter,
/// Counters (atomics)
pub cl_idle: Arc<AtomicU64>,
pub cl_active: Arc<AtomicU64>,
pub cl_waiting: Arc<AtomicU64>,
pub cl_cancel_req: Arc<AtomicU64>,
pub sv_active: Arc<AtomicU64>,
pub sv_idle: Arc<AtomicU64>,
pub sv_used: Arc<AtomicU64>,
pub sv_tested: Arc<AtomicU64>,
pub sv_login: Arc<AtomicU64>,
pub maxwait: Arc<AtomicU64>,
}
impl IntoIterator for PoolStats {
type Item = (String, u64);
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
vec![
("cl_idle".to_string(), self.cl_idle.load(Ordering::Relaxed)),
(
"cl_active".to_string(),
self.cl_active.load(Ordering::Relaxed),
),
(
"cl_waiting".to_string(),
self.cl_waiting.load(Ordering::Relaxed),
),
(
"cl_cancel_req".to_string(),
self.cl_cancel_req.load(Ordering::Relaxed),
),
(
"sv_active".to_string(),
self.sv_active.load(Ordering::Relaxed),
),
("sv_idle".to_string(), self.sv_idle.load(Ordering::Relaxed)),
("sv_used".to_string(), self.sv_used.load(Ordering::Relaxed)),
(
"sv_tested".to_string(),
self.sv_tested.load(Ordering::Relaxed),
),
(
"sv_login".to_string(),
self.sv_login.load(Ordering::Relaxed),
),
(
"maxwait".to_string(),
self.maxwait.load(Ordering::Relaxed) / 1_000_000,
),
(
"maxwait_us".to_string(),
self.maxwait.load(Ordering::Relaxed) % 1_000_000,
),
]
.into_iter()
}
}
impl PoolStats {
pub fn new(identifier: PoolIdentifier, config: Pool) -> Self {
Self {
identifier,
config,
reporter: get_reporter(),
..Default::default()
}
}
// Getters
pub fn register(&self, stats: Arc<PoolStats>) {
self.reporter.pool_register(self.identifier.clone(), stats);
}
pub fn database(&self) -> String {
self.identifier.db.clone()
}
pub fn user(&self) -> String {
self.identifier.user.clone()
}
pub fn pool_mode(&self) -> PoolMode {
self.config.pool_mode
}
/// Populates an array of strings with counters (used by admin in show pools)
pub fn populate_row(&self, row: &mut Vec<String>) {
for (_key, value) in self.clone() {
row.push(value.to_string());
}
}
/// Deletes the maxwait counter, this is done everytime we obtain metrics
pub fn clear_maxwait(&self) {
self.maxwait.store(0, Ordering::Relaxed);
}
/// Notified when a server of the pool enters login state.
///
/// Arguments:
///
/// `from`: The state of the server that notifies.
pub fn server_login(&self, from: ServerState) {
self.sv_login.fetch_add(1, Ordering::Relaxed);
if from != ServerState::Login {
self.decrease_from_server_state(from);
}
}
/// Notified when a server of the pool become 'active'
///
/// Arguments:
///
/// `from`: The state of the server that notifies.
pub fn server_active(&self, from: ServerState) {
self.sv_active.fetch_add(1, Ordering::Relaxed);
if from != ServerState::Active {
self.decrease_from_server_state(from);
}
}
/// Notified when a server of the pool become 'tested'
///
/// Arguments:
///
/// `from`: The state of the server that notifies.
pub fn server_tested(&self, from: ServerState) {
self.sv_tested.fetch_add(1, Ordering::Relaxed);
if from != ServerState::Tested {
self.decrease_from_server_state(from);
}
}
/// Notified when a server of the pool become 'idle'
///
/// Arguments:
///
/// `from`: The state of the server that notifies.
pub fn server_idle(&self, from: ServerState) {
self.sv_idle.fetch_add(1, Ordering::Relaxed);
if from != ServerState::Idle {
self.decrease_from_server_state(from);
}
}
/// Notified when a client of the pool become 'waiting'
///
/// Arguments:
///
/// `from`: The state of the client that notifies.
pub fn client_waiting(&self, from: ClientState) {
if from != ClientState::Waiting {
self.cl_waiting.fetch_add(1, Ordering::Relaxed);
self.decrease_from_client_state(from);
}
}
/// Notified when a client of the pool become 'active'
///
/// Arguments:
///
/// `from`: The state of the client that notifies.
pub fn client_active(&self, from: ClientState) {
if from != ClientState::Active {
self.cl_active.fetch_add(1, Ordering::Relaxed);
self.decrease_from_client_state(from);
}
}
/// Notified when a client of the pool become 'idle'
///
/// Arguments:
///
/// `from`: The state of the client that notifies.
pub fn client_idle(&self, from: ClientState) {
if from != ClientState::Idle {
self.cl_idle.fetch_add(1, Ordering::Relaxed);
self.decrease_from_client_state(from);
}
}
/// Notified when a client disconnects.
///
/// Arguments:
///
/// `from`: The state of the client that notifies.
pub fn client_disconnect(&self, from: ClientState) {
let counter = match from {
ClientState::Idle => &self.cl_idle,
ClientState::Waiting => &self.cl_waiting,
ClientState::Active => &self.cl_active,
};
Self::decrease_counter(counter.clone());
}
/// Notified when a server disconnects.
///
/// Arguments:
///
/// `from`: The state of the client that notifies.
pub fn server_disconnect(&self, from: ServerState) {
let counter = match from {
ServerState::Active => &self.sv_active,
ServerState::Idle => &self.sv_idle,
ServerState::Login => &self.sv_login,
ServerState::Tested => &self.sv_tested,
};
Self::decrease_counter(counter.clone());
}
// helpers for counter decrease
fn decrease_from_server_state(&self, from: ServerState) {
let counter = match from {
ServerState::Tested => &self.sv_tested,
ServerState::Active => &self.sv_active,
ServerState::Idle => &self.sv_idle,
ServerState::Login => &self.sv_login,
};
Self::decrease_counter(counter.clone());
}
fn decrease_from_client_state(&self, from: ClientState) {
let counter = match from {
ClientState::Active => &self.cl_active,
ClientState::Idle => &self.cl_idle,
ClientState::Waiting => &self.cl_waiting,
};
Self::decrease_counter(counter.clone());
}
fn decrease_counter(value: Arc<AtomicU64>) {
if value.load(Ordering::Relaxed) > 0 {
value.fetch_sub(1, Ordering::Relaxed);
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_decrease() {
let stat: PoolStats = PoolStats::default();
stat.server_login(ServerState::Login);
stat.server_idle(ServerState::Login);
assert_eq!(stat.sv_login.load(Ordering::Relaxed), 0);
assert_eq!(stat.sv_idle.load(Ordering::Relaxed), 1);
}
}

View File

@@ -1,225 +0,0 @@
use super::AddressStats;
use super::PoolStats;
use super::{get_reporter, Reporter};
use crate::config::Address;
use atomic_enum::atomic_enum;
use parking_lot::RwLock;
use std::sync::atomic::*;
use std::sync::Arc;
use tokio::time::Instant;
/// The various states that a server can be in
#[atomic_enum]
#[derive(PartialEq)]
pub enum ServerState {
Login = 0,
Active,
Tested,
Idle,
}
impl std::fmt::Display for ServerState {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match *self {
ServerState::Login => write!(f, "login"),
ServerState::Active => write!(f, "active"),
ServerState::Tested => write!(f, "tested"),
ServerState::Idle => write!(f, "idle"),
}
}
}
/// Information we keep track of which can be queried by SHOW SERVERS
#[derive(Debug, Clone)]
pub struct ServerStats {
/// A random integer assigned to the server and used by stats to track the server
server_id: i32,
/// Context information, only to be read
address: Address,
connect_time: Instant,
pool_stats: Arc<PoolStats>,
reporter: Reporter,
/// Data
pub application_name: Arc<RwLock<String>>,
pub state: Arc<AtomicServerState>,
pub bytes_sent: Arc<AtomicU64>,
pub bytes_received: Arc<AtomicU64>,
pub transaction_count: Arc<AtomicU64>,
pub query_count: Arc<AtomicU64>,
pub error_count: Arc<AtomicU64>,
}
impl Default for ServerStats {
fn default() -> Self {
ServerStats {
server_id: 0,
application_name: Arc::new(RwLock::new(String::new())),
address: Address::default(),
pool_stats: Arc::new(PoolStats::default()),
connect_time: Instant::now(),
state: Arc::new(AtomicServerState::new(ServerState::Login)),
bytes_sent: Arc::new(AtomicU64::new(0)),
bytes_received: Arc::new(AtomicU64::new(0)),
transaction_count: Arc::new(AtomicU64::new(0)),
query_count: Arc::new(AtomicU64::new(0)),
error_count: Arc::new(AtomicU64::new(0)),
reporter: get_reporter(),
}
}
}
impl ServerStats {
pub fn new(address: Address, pool_stats: Arc<PoolStats>, connect_time: Instant) -> Self {
Self {
address,
pool_stats,
connect_time,
server_id: rand::random::<i32>(),
..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<ServerStats>) {
self.reporter.server_register(self.server_id, stats);
self.login();
}
/// Reports a server connection is no longer assigned to a client
/// and is available for the next client to pick it up
pub fn idle(&self) {
self.pool_stats
.server_idle(self.state.load(Ordering::Relaxed));
self.state.store(ServerState::Idle, Ordering::Relaxed);
}
/// 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);
self.pool_stats
.server_disconnect(self.state.load(Ordering::Relaxed))
}
/// Reports a server connection is being tested before being given to a client.
pub fn tested(&self) {
self.set_undefined_application();
self.pool_stats
.server_tested(self.state.load(Ordering::Relaxed));
self.state.store(ServerState::Tested, Ordering::Relaxed);
}
/// Reports a server connection is attempting to login.
pub fn login(&self) {
self.pool_stats
.server_login(self.state.load(Ordering::Relaxed));
self.state.store(ServerState::Login, Ordering::Relaxed);
self.set_undefined_application();
}
/// Reports a server connection has been assigned to a client that
/// is about to query the server
pub fn active(&self, application_name: String) {
self.pool_stats
.server_active(self.state.load(Ordering::Relaxed));
self.state.store(ServerState::Active, Ordering::Relaxed);
self.set_application(application_name);
}
pub fn address_stats(&self) -> Arc<AddressStats> {
self.address.stats.clone()
}
// Helper methods for show_servers
pub fn pool_name(&self) -> String {
self.pool_stats.database()
}
pub fn username(&self) -> String {
self.pool_stats.user()
}
pub fn address_name(&self) -> String {
self.address.name()
}
pub fn connect_time(&self) -> Instant {
self.connect_time
}
fn set_application(&self, name: String) {
let mut application_name = self.application_name.write();
*application_name = name;
}
fn set_undefined_application(&self) {
self.set_application(String::from("Undefined"))
}
pub fn checkout_time(&self, microseconds: u64, application_name: String) {
// Update server stats and address aggergation stats
self.set_application(application_name);
self.address
.stats
.total_wait_time
.fetch_add(microseconds, Ordering::Relaxed);
self.pool_stats
.maxwait
.fetch_max(microseconds, Ordering::Relaxed);
}
/// Report a query executed by a client against a server
pub fn query(&self, milliseconds: u64, application_name: &str) {
self.set_application(application_name.to_string());
let address_stats = self.address_stats();
address_stats
.total_query_count
.fetch_add(1, Ordering::Relaxed);
address_stats
.total_query_time
.fetch_add(milliseconds, Ordering::Relaxed);
}
/// Report a transaction executed by a client a server
/// we report each individual queries outside a transaction as a transaction
/// We only count the initial BEGIN as a transaction, all queries within do not
/// count as transactions
pub fn transaction(&self, application_name: &str) {
self.set_application(application_name.to_string());
self.transaction_count.fetch_add(1, Ordering::Relaxed);
self.address
.stats
.total_xact_count
.fetch_add(1, Ordering::Relaxed);
}
/// Report data sent to a server
pub fn data_sent(&self, amount_bytes: usize) {
self.bytes_sent
.fetch_add(amount_bytes as u64, Ordering::Relaxed);
self.address
.stats
.total_sent
.fetch_add(amount_bytes as u64, Ordering::Relaxed);
}
/// Report data received from a server
pub fn data_received(&self, amount_bytes: usize) {
self.bytes_received
.fetch_add(amount_bytes as u64, Ordering::Relaxed);
self.address
.stats
.total_received
.fetch_add(amount_bytes as u64, Ordering::Relaxed);
}
}

View File

@@ -1,7 +1,6 @@
// Stream wrapper. // Stream wrapper.
use rustls_pemfile::{certs, read_one, Item}; use rustls_pemfile::{certs, rsa_private_keys};
use std::iter;
use std::path::Path; use std::path::Path;
use std::sync::Arc; use std::sync::Arc;
use tokio_rustls::rustls::{self, Certificate, PrivateKey}; use tokio_rustls::rustls::{self, Certificate, PrivateKey};
@@ -18,17 +17,9 @@ pub fn load_certs(path: &Path) -> std::io::Result<Vec<Certificate>> {
} }
pub fn load_keys(path: &Path) -> std::io::Result<Vec<PrivateKey>> { pub fn load_keys(path: &Path) -> std::io::Result<Vec<PrivateKey>> {
let mut rd = std::io::BufReader::new(std::fs::File::open(path)?); rsa_private_keys(&mut std::io::BufReader::new(std::fs::File::open(path)?))
.map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidInput, "invalid key"))
iter::from_fn(|| read_one(&mut rd).transpose()) .map(|mut keys| keys.drain(..).map(PrivateKey).collect())
.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 struct Tls {
@@ -39,12 +30,12 @@ impl Tls {
pub fn new() -> Result<Self, Error> { pub fn new() -> Result<Self, Error> {
let config = get_config(); let config = get_config();
let certs = match load_certs(Path::new(&config.general.tls_certificate.unwrap())) { let certs = match load_certs(&Path::new(&config.general.tls_certificate.unwrap())) {
Ok(certs) => certs, Ok(certs) => certs,
Err(_) => return Err(Error::TlsError), Err(_) => return Err(Error::TlsError),
}; };
let mut keys = match load_keys(Path::new(&config.general.tls_private_key.unwrap())) { let mut keys = match load_keys(&Path::new(&config.general.tls_private_key.unwrap())) {
Ok(keys) => keys, Ok(keys) => keys,
Err(_) => return Err(Error::TlsError), Err(_) => return Err(Error::TlsError),
}; };

View File

@@ -1,8 +0,0 @@
FROM rust:bullseye
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

View File

@@ -1,53 +0,0 @@
version: "3"
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"]
volumes:
- ../../:/app/
- /app/target/

View File

@@ -1,37 +0,0 @@
#!/bin/bash
rm -rf /app/target/ || true
rm /app/*.profraw || true
rm /app/pgcat.profdata || true
rm -rf /app/cov || true
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

View File

@@ -1,60 +0,0 @@
import psycopg2
import asyncio
import asyncpg
PGCAT_HOST = "127.0.0.1"
PGCAT_PORT = "6432"
def regular_main():
# Connect to the PostgreSQL database
conn = psycopg2.connect(
host=PGCAT_HOST,
database="sharded_db",
user="sharding_user",
password="sharding_user",
port=PGCAT_PORT,
)
# Open a cursor to perform database operations
cur = conn.cursor()
# Execute a SQL query
cur.execute("SELECT 1")
# Fetch the results
rows = cur.fetchall()
# Print the results
for row in rows:
print(row[0])
# Close the cursor and the database connection
cur.close()
conn.close()
async def main():
# Connect to the PostgreSQL database
conn = await asyncpg.connect(
host=PGCAT_HOST,
database="sharded_db",
user="sharding_user",
password="sharding_user",
port=PGCAT_PORT,
)
# Execute a SQL query
for _ in range(25):
rows = await conn.fetch("SELECT 1")
# Print the results
for row in rows:
print(row[0])
# Close the database connection
await conn.close()
regular_main()
asyncio.run(main())

View File

@@ -1,11 +1,2 @@
asyncio==3.4.3
asyncpg==0.27.0
black==23.3.0
click==8.1.3
mypy-extensions==1.0.0
packaging==23.1
pathspec==0.11.1
platformdirs==3.2.0
psutil==5.9.1
psycopg2==2.9.3 psycopg2==2.9.3
tomli==2.0.1 psutil==5.9.1

View File

@@ -14,18 +14,12 @@ PGCAT_PORT = "6432"
def pgcat_start(): def pgcat_start():
pg_cat_send_signal(signal.SIGTERM) pg_cat_send_signal(signal.SIGTERM)
os.system("./target/debug/pgcat .circleci/pgcat.toml &") os.system("./target/debug/pgcat .circleci/pgcat.toml &")
time.sleep(2)
def pg_cat_send_signal(signal: signal.Signals): def pg_cat_send_signal(signal: signal.Signals):
try: for proc in psutil.process_iter(["pid", "name"]):
for proc in psutil.process_iter(["pid", "name"]): if "pgcat" == proc.name():
if "pgcat" == proc.name(): os.kill(proc.pid, signal)
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: if signal == signal.SIGTERM:
# Returns 0 if pgcat process exists # Returns 0 if pgcat process exists
time.sleep(2) time.sleep(2)
@@ -33,23 +27,11 @@ def pg_cat_send_signal(signal: signal.Signals):
raise Exception("pgcat not closed after SIGTERM") raise Exception("pgcat not closed after SIGTERM")
def connect_db( def connect_normal_db(
autocommit: bool = True, autocommit: bool = False,
admin: bool = False,
) -> Tuple[psycopg2.extensions.connection, psycopg2.extensions.cursor]: ) -> 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( conn = psycopg2.connect(
f"postgres://{user}:{password}@{PGCAT_HOST}:{PGCAT_PORT}/{db}?application_name=testing_pgcat", f"postgres://sharding_user:sharding_user@{PGCAT_HOST}:{PGCAT_PORT}/sharded_db?application_name=testing_pgcat"
connect_timeout=2,
) )
conn.autocommit = autocommit conn.autocommit = autocommit
cur = conn.cursor() cur = conn.cursor()
@@ -63,7 +45,7 @@ def cleanup_conn(conn: psycopg2.extensions.connection, cur: psycopg2.extensions.
def test_normal_db_access(): def test_normal_db_access():
conn, cur = connect_db(autocommit=False) conn, cur = connect_normal_db()
cur.execute("SELECT 1") cur.execute("SELECT 1")
res = cur.fetchall() res = cur.fetchall()
print(res) print(res)
@@ -71,7 +53,11 @@ def test_normal_db_access():
def test_admin_db_access(): def test_admin_db_access():
conn, cur = connect_db(admin=True) conn = psycopg2.connect(
f"postgres://admin_user:admin_pass@{PGCAT_HOST}:{PGCAT_PORT}/pgcat"
)
conn.autocommit = True # BEGIN/COMMIT is not supported by admin db
cur = conn.cursor()
cur.execute("SHOW POOLS") cur.execute("SHOW POOLS")
res = cur.fetchall() res = cur.fetchall()
@@ -81,14 +67,15 @@ def test_admin_db_access():
def test_shutdown_logic(): def test_shutdown_logic():
# - - - - - - - - - - - - - - - - - - ##### NO ACTIVE QUERIES SIGINT HANDLING #####
# NO ACTIVE QUERIES SIGINT HANDLING
# Start pgcat # Start pgcat
pgcat_start() pgcat_start()
# Wait for server to fully start up
time.sleep(2)
# Create client connection and send query (not in transaction) # Create client connection and send query (not in transaction)
conn, cur = connect_db() conn, cur = connect_normal_db(True)
cur.execute("BEGIN;") cur.execute("BEGIN;")
cur.execute("SELECT 1;") cur.execute("SELECT 1;")
@@ -110,45 +97,17 @@ def test_shutdown_logic():
cleanup_conn(conn, cur) cleanup_conn(conn, cur)
pg_cat_send_signal(signal.SIGTERM) pg_cat_send_signal(signal.SIGTERM)
# - - - - - - - - - - - - - - - - - - ##### END #####
# NO ACTIVE QUERIES ADMIN SHUTDOWN COMMAND
##### HANDLE TRANSACTION WITH SIGINT #####
# Start pgcat # Start pgcat
pgcat_start() pgcat_start()
# Create client connection and begin transaction # Wait for server to fully start up
conn, cur = connect_db() time.sleep(2)
admin_conn, admin_cur = 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")
cleanup_conn(conn, cur)
cleanup_conn(admin_conn, admin_cur)
pg_cat_send_signal(signal.SIGTERM)
# - - - - - - - - - - - - - - - - - -
# HANDLE TRANSACTION WITH SIGINT
# Start pgcat
pgcat_start()
# Create client connection and begin transaction # Create client connection and begin transaction
conn, cur = connect_db() conn, cur = connect_normal_db(True)
cur.execute("BEGIN;") cur.execute("BEGIN;")
cur.execute("SELECT 1;") cur.execute("SELECT 1;")
@@ -167,127 +126,17 @@ def test_shutdown_logic():
cleanup_conn(conn, cur) cleanup_conn(conn, cur)
pg_cat_send_signal(signal.SIGTERM) pg_cat_send_signal(signal.SIGTERM)
# - - - - - - - - - - - - - - - - - - ##### END #####
# HANDLE TRANSACTION WITH ADMIN SHUTDOWN COMMAND
##### HANDLE SHUTDOWN TIMEOUT WITH SIGINT #####
# Start pgcat # Start pgcat
pgcat_start() pgcat_start()
# Create client connection and begin transaction # Wait for server to fully start up
conn, cur = connect_db() time.sleep(3)
admin_conn, admin_cur = 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)
cleanup_conn(conn, cur)
cleanup_conn(admin_conn, admin_cur)
pg_cat_send_signal(signal.SIGTERM)
# - - - - - - - - - - - - - - - - - -
# NO NEW NON-ADMIN CONNECTIONS DURING SHUTDOWN
# Start pgcat
pgcat_start()
# Create client connection and begin transaction
transaction_conn, transaction_cur = connect_db()
transaction_cur.execute("BEGIN;")
transaction_cur.execute("SELECT 1;")
# Send sigint to pgcat while still in transaction
pg_cat_send_signal(signal.SIGINT)
time.sleep(1)
start = time.perf_counter()
try:
conn, cur = connect_db()
cur.execute("SELECT 1;")
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")
cleanup_conn(transaction_conn, transaction_cur)
pg_cat_send_signal(signal.SIGTERM)
# - - - - - - - - - - - - - - - - - -
# ALLOW NEW ADMIN CONNECTIONS DURING SHUTDOWN
# Start pgcat
pgcat_start()
# Create client connection and begin transaction
transaction_conn, transaction_cur = connect_db()
transaction_cur.execute("BEGIN;")
transaction_cur.execute("SELECT 1;")
# Send sigint to pgcat while still in transaction
pg_cat_send_signal(signal.SIGINT)
time.sleep(1)
try:
conn, cur = connect_db(admin=True)
cur.execute("SHOW DATABASES;")
cleanup_conn(conn, cur)
except psycopg2.OperationalError as e:
raise Exception(e)
cleanup_conn(transaction_conn, transaction_cur)
pg_cat_send_signal(signal.SIGTERM)
# - - - - - - - - - - - - - - - - - -
# ADMIN CONNECTIONS CONTINUING TO WORK AFTER SHUTDOWN
# Start pgcat
pgcat_start()
# Create client connection and begin transaction
transaction_conn, transaction_cur = connect_db()
transaction_cur.execute("BEGIN;")
transaction_cur.execute("SELECT 1;")
admin_conn, admin_cur = connect_db(admin=True)
admin_cur.execute("SHOW DATABASES;")
# Send sigint to pgcat while still in transaction
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)
cleanup_conn(transaction_conn, transaction_cur)
cleanup_conn(admin_conn, admin_cur)
pg_cat_send_signal(signal.SIGTERM)
# - - - - - - - - - - - - - - - - - -
# HANDLE SHUTDOWN TIMEOUT WITH SIGINT
# Start pgcat
pgcat_start()
# Create client connection and begin transaction, which should prevent server shutdown unless shutdown timeout is reached # Create client connection and begin transaction, which should prevent server shutdown unless shutdown timeout is reached
conn, cur = connect_db() conn, cur = connect_normal_db(True)
cur.execute("BEGIN;") cur.execute("BEGIN;")
cur.execute("SELECT 1;") cur.execute("SELECT 1;")
@@ -310,7 +159,7 @@ def test_shutdown_logic():
cleanup_conn(conn, cur) cleanup_conn(conn, cur)
pg_cat_send_signal(signal.SIGTERM) pg_cat_send_signal(signal.SIGTERM)
# - - - - - - - - - - - - - - - - - - ##### END #####
test_normal_db_access() test_normal_db_access()

View File

@@ -1,8 +1,6 @@
source "https://rubygems.org" source "https://rubygems.org"
gem "pg" gem "pg"
gem "toml"
gem "rspec"
gem "rubocop"
gem "toxiproxy"
gem "activerecord" gem "activerecord"
gem "rubocop"
gem "toml", "~> 0.3.0"

View File

@@ -1,22 +1,21 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
activemodel (7.0.4.1) activemodel (7.0.3.1)
activesupport (= 7.0.4.1) activesupport (= 7.0.3.1)
activerecord (7.0.4.1) activerecord (7.0.3.1)
activemodel (= 7.0.4.1) activemodel (= 7.0.3.1)
activesupport (= 7.0.4.1) activesupport (= 7.0.3.1)
activesupport (7.0.4.1) activesupport (7.0.3.1)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2) i18n (>= 1.6, < 2)
minitest (>= 5.1) minitest (>= 5.1)
tzinfo (~> 2.0) tzinfo (~> 2.0)
ast (2.4.2) ast (2.4.2)
concurrent-ruby (1.1.10) concurrent-ruby (1.1.10)
diff-lcs (1.5.0) i18n (1.11.0)
i18n (1.12.0)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
minitest (5.17.0) minitest (5.16.2)
parallel (1.22.1) parallel (1.22.1)
parser (3.1.2.0) parser (3.1.2.0)
ast (~> 2.4.1) ast (~> 2.4.1)
@@ -25,19 +24,6 @@ GEM
rainbow (3.1.1) rainbow (3.1.1)
regexp_parser (2.3.1) regexp_parser (2.3.1)
rexml (3.2.5) rexml (3.2.5)
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) rubocop (1.29.0)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 3.1.0.0) parser (>= 3.1.0.0)
@@ -52,23 +38,19 @@ GEM
ruby-progressbar (1.11.0) ruby-progressbar (1.11.0)
toml (0.3.0) toml (0.3.0)
parslet (>= 1.8.0, < 3.0.0) parslet (>= 1.8.0, < 3.0.0)
toxiproxy (2.0.1) tzinfo (2.0.4)
tzinfo (2.0.5)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
unicode-display_width (2.1.0) unicode-display_width (2.1.0)
PLATFORMS PLATFORMS
aarch64-linux
arm64-darwin-21 arm64-darwin-21
x86_64-linux x86_64-linux
DEPENDENCIES DEPENDENCIES
activerecord activerecord
pg pg
rspec
rubocop rubocop
toml toml (~> 0.3.0)
toxiproxy
BUNDLED WITH BUNDLED WITH
2.3.21 2.3.7

View File

@@ -1,410 +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 "SHOW STATS" do
context "clients connect and make one query" do
it "updates *_query_time and *_wait_time" do
connection = PG::connect("#{pgcat_conn_str}?application_name=one_query")
connection.async_exec("SELECT pg_sleep(0.25)")
connection.async_exec("SELECT pg_sleep(0.25)")
connection.async_exec("SELECT pg_sleep(0.25)")
connection.close
# wait for averages to be calculated, we shouldn't do this too often
sleep(15.5)
admin_conn = PG::connect(processes.pgcat.admin_connection_string)
results = admin_conn.async_exec("SHOW STATS")[0]
admin_conn.close
expect(results["total_query_time"].to_i).to be_within(200).of(750)
expect(results["avg_query_time"].to_i).to_not eq(0)
expect(results["total_wait_time"].to_i).to_not eq(0)
expect(results["avg_wait_time"].to_i).to_not eq(0)
end
end
end
describe "SHOW POOLS" do
context "bad credentials" do
it "does not change any stats" do
bad_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
connections = Array.new(20) { PG::connect(pgcat_conn_str) }
sleep(1)
admin_conn = PG::connect(processes.pgcat.admin_connection_string)
results = admin_conn.async_exec("SHOW POOLS")[0]
%w[cl_active cl_waiting cl_cancel_req sv_active sv_used sv_tested sv_login maxwait].each do |s|
raise StandardError, "Field #{s} was expected to be 0 but found to be #{results[s]}" if results[s] != "0"
end
expect(results["cl_idle"]).to eq("20")
expect(results["sv_idle"]).to eq("1")
connections.map(&:close)
sleep(1.1)
results = admin_conn.async_exec("SHOW POOLS")[0]
%w[cl_active cl_idle cl_waiting cl_cancel_req sv_active sv_used sv_tested sv_login maxwait].each do |s|
raise StandardError, "Field #{s} was expected to be 0 but found to be #{results[s]}" if results[s] != "0"
end
expect(results["sv_idle"]).to eq("1")
end
end
context "clients connect and make one query" do
it "only affects cl_idle, sv_idle stats" do
connections = Array.new(5) { PG::connect("#{pgcat_conn_str}?application_name=one_query") }
connections.each do |c|
Thread.new { c.async_exec("SELECT pg_sleep(2.5)") }
end
sleep(1.1)
admin_conn = PG::connect(processes.pgcat.admin_connection_string)
results = admin_conn.async_exec("SHOW POOLS")[0]
%w[cl_idle cl_waiting cl_cancel_req sv_idle sv_used sv_tested sv_login maxwait].each do |s|
raise StandardError, "Field #{s} was expected to be 0 but found to be #{results[s]}" if results[s] != "0"
end
expect(results["cl_active"]).to eq("5")
expect(results["sv_active"]).to eq("5")
sleep(3)
results = admin_conn.async_exec("SHOW POOLS")[0]
%w[cl_active cl_waiting cl_cancel_req sv_active sv_used sv_tested sv_login maxwait].each do |s|
raise StandardError, "Field #{s} was expected to be 0 but found to be #{results[s]}" if results[s] != "0"
end
expect(results["cl_idle"]).to eq("5")
expect(results["sv_idle"]).to eq("5")
connections.map(&:close)
sleep(1)
results = admin_conn.async_exec("SHOW POOLS")[0]
%w[cl_idle cl_active cl_waiting cl_cancel_req sv_active sv_used sv_tested sv_login maxwait].each do |s|
raise StandardError, "Field #{s} was expected to be 0 but found to be #{results[s]}" if results[s] != "0"
end
expect(results["sv_idle"]).to eq("5")
end
end
context "client connects and opens a transaction and closes connection uncleanly" do
it "produces correct statistics" do
connections = Array.new(5) { PG::connect("#{pgcat_conn_str}?application_name=one_query") }
connections.each do |c|
Thread.new do
c.async_exec("BEGIN")
c.async_exec("SELECT pg_sleep(0.01)")
c.close
end
end
sleep(1.1)
admin_conn = PG::connect(processes.pgcat.admin_connection_string)
results = admin_conn.async_exec("SHOW POOLS")[0]
%w[cl_idle cl_active cl_waiting cl_cancel_req sv_active sv_used sv_tested sv_login maxwait].each do |s|
raise StandardError, "Field #{s} was expected to be 0 but found to be #{results[s]}" if results[s] != "0"
end
expect(results["sv_idle"]).to eq("5")
end
end
context "client fail to checkout connection from the pool" do
it "counts clients as idle" do
new_configs = processes.pgcat.current_config
new_configs["general"]["connect_timeout"] = 500
new_configs["general"]["ban_time"] = 1
new_configs["general"]["shutdown_timeout"] = 1
new_configs["pools"]["sharded_db"]["users"]["0"]["pool_size"] = 1
processes.pgcat.update_config(new_configs)
processes.pgcat.reload_config
threads = []
connections = Array.new(5) { PG::connect("#{pgcat_conn_str}?application_name=one_query") }
connections.each do |c|
threads << Thread.new { c.async_exec("SELECT pg_sleep(1)") rescue PG::SystemError }
end
sleep(2)
admin_conn = PG::connect(processes.pgcat.admin_connection_string)
results = admin_conn.async_exec("SHOW POOLS")[0]
%w[cl_active cl_waiting cl_cancel_req sv_active sv_used sv_tested sv_login maxwait].each do |s|
raise StandardError, "Field #{s} was expected to be 0 but found to be #{results[s]}" if results[s] != "0"
end
expect(results["cl_idle"]).to eq("5")
expect(results["sv_idle"]).to eq("1")
threads.map(&:join)
connections.map(&:close)
end
end
context "clients connects and disconnect normally" do
let(:processes) { Helpers::Pgcat.single_instance_setup("sharded_db", 2) }
it 'shows the same number of clients before and after' do
clients_before = clients_connected_to_pool(processes: processes)
threads = []
connections = Array.new(4) { PG::connect("#{pgcat_conn_str}?application_name=one_query") }
connections.each do |c|
threads << Thread.new { c.async_exec("SELECT 1") }
end
clients_between = clients_connected_to_pool(processes: processes)
expect(clients_before).not_to eq(clients_between)
connections.each(&:close)
clients_after = clients_connected_to_pool(processes: processes)
expect(clients_before).to eq(clients_after)
end
end
context "clients connects and disconnect abruptly" do
let(:processes) { Helpers::Pgcat.single_instance_setup("sharded_db", 10) }
it 'shows the same number of clients before and after' do
threads = []
connections = Array.new(2) { PG::connect("#{pgcat_conn_str}?application_name=one_query") }
connections.each do |c|
threads << Thread.new { c.async_exec("SELECT 1") }
end
clients_before = clients_connected_to_pool(processes: processes)
random_string = (0...8).map { (65 + rand(26)).chr }.join
connection_string = "#{pgcat_conn_str}?application_name=#{random_string}"
faulty_client = Process.spawn("psql -Atx #{connection_string} >/dev/null")
sleep(1)
# psql starts two processes, we only know the pid of the parent, this
# ensure both are killed
`pkill -9 -f '#{random_string}'`
Process.wait(faulty_client)
clients_after = clients_connected_to_pool(processes: processes)
expect(clients_before).to eq(clients_after)
end
end
context "clients overwhelm server pools" do
let(:processes) { Helpers::Pgcat.single_instance_setup("sharded_db", 2) }
it "cl_waiting is updated to show it" do
threads = []
connections = Array.new(4) { PG::connect("#{pgcat_conn_str}?application_name=one_query") }
connections.each do |c|
threads << Thread.new { c.async_exec("SELECT pg_sleep(1.5)") }
end
sleep(1.1) # Allow time for stats to update
admin_conn = PG::connect(processes.pgcat.admin_connection_string)
results = admin_conn.async_exec("SHOW POOLS")[0]
%w[cl_idle cl_cancel_req sv_idle sv_used sv_tested sv_login maxwait].each do |s|
raise StandardError, "Field #{s} was expected to be 0 but found to be #{results[s]}" if results[s] != "0"
end
expect(results["cl_waiting"]).to eq("2")
expect(results["cl_active"]).to eq("2")
expect(results["sv_active"]).to eq("2")
sleep(2.5) # Allow time for stats to update
results = admin_conn.async_exec("SHOW POOLS")[0]
%w[cl_active cl_waiting cl_cancel_req sv_active sv_used sv_tested sv_login].each do |s|
raise StandardError, "Field #{s} was expected to be 0 but found to be #{results[s]}" if results[s] != "0"
end
expect(results["cl_idle"]).to eq("4")
expect(results["sv_idle"]).to eq("2")
threads.map(&:join)
connections.map(&:close)
end
it "show correct max_wait" do
threads = []
connections = Array.new(4) { PG::connect("#{pgcat_conn_str}?application_name=one_query") }
connections.each do |c|
threads << Thread.new { c.async_exec("SELECT pg_sleep(1.5)") }
end
sleep(2.5) # Allow time for stats to update
admin_conn = PG::connect(processes.pgcat.admin_connection_string)
results = admin_conn.async_exec("SHOW POOLS")[0]
expect(results["maxwait"]).to eq("1")
expect(results["maxwait_us"].to_i).to be_within(200_000).of(500_000)
sleep(4.5) # Allow time for stats to update
results = admin_conn.async_exec("SHOW POOLS")[0]
expect(results["maxwait"]).to eq("0")
threads.map(&:join)
connections.map(&:close)
end
end
end
describe "SHOW CLIENTS" do
it "reports correct number and application names" do
conn_str = processes.pgcat.connection_string("sharded_db", "sharding_user")
connections = Array.new(20) { |i| PG::connect("#{conn_str}?application_name=app#{i % 5}") }
admin_conn = PG::connect(processes.pgcat.admin_connection_string)
sleep(1) # Wait for stats to be updated
results = admin_conn.async_exec("SHOW CLIENTS")
expect(results.count).to eq(21) # count admin clients
expect(results.select { |c| c["application_name"] == "app3" || c["application_name"] == "app4" }.count).to eq(8)
expect(results.select { |c| c["database"] == "pgcat" }.count).to eq(1)
connections[0..5].map(&:close)
sleep(1) # Wait for stats to be updated
results = admin_conn.async_exec("SHOW CLIENTS")
expect(results.count).to eq(15)
connections[6..].map(&:close)
sleep(1) # Wait for stats to be updated
expect(admin_conn.async_exec("SHOW CLIENTS").count).to eq(1)
admin_conn.close
end
it "reports correct number of queries and transactions" do
conn_str = processes.pgcat.connection_string("sharded_db", "sharding_user")
connections = Array.new(2) { |i| PG::connect("#{conn_str}?application_name=app#{i}") }
connections.each do |c|
c.async_exec("SELECT 1")
c.async_exec("SELECT 2")
c.async_exec("SELECT 3")
c.async_exec("BEGIN")
c.async_exec("SELECT 4")
c.async_exec("SELECT 5")
c.async_exec("COMMIT")
end
admin_conn = PG::connect(processes.pgcat.admin_connection_string)
sleep(1) # Wait for stats to be updated
results = admin_conn.async_exec("SHOW CLIENTS")
expect(results.count).to eq(3)
normal_client_results = results.reject { |r| r["database"] == "pgcat" }
expect(normal_client_results[0]["transaction_count"]).to eq("4")
expect(normal_client_results[1]["transaction_count"]).to eq("4")
expect(normal_client_results[0]["query_count"]).to eq("7")
expect(normal_client_results[1]["query_count"]).to eq("7")
admin_conn.close
connections.map(&:close)
end
end
describe "Manual Banning" do
let(:processes) { Helpers::Pgcat.single_shard_setup("sharded_db", 10) }
before do
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
end

View File

@@ -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

Binary file not shown.

View File

@@ -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_s, "primary"],
["localhost", replica.port.to_s, "replica"],
]
},
},
"users" => { "0" => user.merge(config_user) }
}
}
pgcat_cfg["general"]["port"] = pgcat.port
pgcat.update_config(pgcat_cfg)
pgcat.start
pgcat.wait_until_ready(
pgcat.connection_string(
"sharded_db",
pg_user['username'],
pg_user['password']
)
) if wait_until_ready
OpenStruct.new.tap do |struct|
struct.pgcat = pgcat
struct.primary = primary
struct.replicas = [replica]
struct.all_databases = [primary]
end
end
def self.two_pools_auth_query(
pg_user:,
config_user:,
pool_names:,
extra_conf: {},
log_level: 'debug'
)
user = {
"pool_size" => 10,
"statement_timeout" => 0,
}
pgcat = PgcatProcess.new(log_level)
pgcat_cfg = pgcat.current_config
primary = PgInstance.new(5432, pg_user["username"], pg_user["password"], "shard0")
replica = PgInstance.new(10432, pg_user["username"], pg_user["password"], "shard0")
pool_template = Proc.new do |database|
{
"default_role" => "any",
"pool_mode" => "transaction",
"load_balancing_mode" => "random",
"primary_reads_enabled" => false,
"query_parser_enabled" => false,
"sharding_function" => "pg_bigint_hash",
"shards" => {
"0" => {
"database" => database,
"servers" => [
["localhost", primary.port.to_s, "primary"],
["localhost", replica.port.to_s, "replica"],
]
},
},
"users" => { "0" => user.merge(config_user) }
}
end
# Main proxy configs
pgcat_cfg["pools"] = {
"#{pool_names[0]}" => pool_template.call("shard0"),
"#{pool_names[1]}" => pool_template.call("shard1")
}
pgcat_cfg["general"]["port"] = pgcat.port
pgcat.update_config(pgcat_cfg.deep_merge(extra_conf))
pgcat.start
pgcat.wait_until_ready(pgcat.connection_string("sharded_db0", pg_user['username'], pg_user['password']))
OpenStruct.new.tap do |struct|
struct.pgcat = pgcat
struct.primary = primary
struct.replicas = [replica]
struct.all_databases = [primary]
end
end
def self.create_query_auth_function(user)
return <<-SQL
CREATE OR REPLACE FUNCTION public.user_lookup(in i_username text, out uname text, out phash text)
RETURNS record AS $$
BEGIN
SELECT usename, passwd FROM pg_catalog.pg_shadow
WHERE usename = i_username INTO uname, phash;
RETURN;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
GRANT EXECUTE ON FUNCTION public.user_lookup(text) TO #{user};
SQL
end
def self.exec_in_instances(query:, instance_ports: [ 5432, 10432 ], database: 'postgres', user: 'postgres', password: 'postgres')
instance_ports.each do |port|
c = PG.connect("postgres://#{user}:#{password}@localhost:#{port}/#{database}")
c.exec(query)
c.close
end
end
def self.set_up_auth_query_for_user(user:, password:, instance_ports: [ 5432, 10432 ], database: 'shard0' )
instance_ports.each do |port|
connection = PG.connect("postgres://postgres:postgres@localhost:#{port}/#{database}")
connection.exec(self.drop_query_auth_function(user)) rescue PG::UndefinedFunction
connection.exec("DROP ROLE #{user}") rescue PG::UndefinedObject
connection.exec("CREATE ROLE #{user} ENCRYPTED PASSWORD '#{password}' LOGIN;")
connection.exec(self.create_query_auth_function(user))
connection.close
end
end
def self.tear_down_auth_query_for_user(user:, password:, instance_ports: [ 5432, 10432 ], database: 'shard0' )
instance_ports.each do |port|
connection = PG.connect("postgres://postgres:postgres@localhost:#{port}/#{database}")
connection.exec(self.drop_query_auth_function(user)) rescue PG::UndefinedFunction
connection.exec("DROP ROLE #{user}")
connection.close
end
end
def self.drop_query_auth_function(user)
return <<-SQL
REVOKE ALL ON FUNCTION public.user_lookup(text) FROM public, #{user};
DROP FUNCTION public.user_lookup(in i_username text, out uname text, out phash text);
SQL
end
end
end

View File

@@ -1,94 +0,0 @@
require 'pg'
require 'toxiproxy'
class PgInstance
attr_reader :port
attr_reader :username
attr_reader :password
attr_reader :database_name
def initialize(port, username, password, database_name)
@original_port = port
@toxiproxy_port = 10000 + port.to_i
@port = @toxiproxy_port
@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: 5).apply { yield }
else
Toxiproxy[@toxiproxy_name].toxic(:limit_data, bytes: 5).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 = 'SELECT $1 + $2'")[0]["sum"].to_i }
end
end

View File

@@ -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

View File

@@ -1,156 +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,
"automatic_sharding_key" => "data.id",
"sharding_function" => "pg_bigint_hash",
"shards" => {
"0" => { "database" => "shard0", "servers" => [["localhost", primary0.port.to_s, "primary"]] },
"1" => { "database" => "shard1", "servers" => [["localhost", primary1.port.to_s, "primary"]] },
"2" => { "database" => "shard2", "servers" => [["localhost", primary2.port.to_s, "primary"]] },
},
"users" => { "0" => user }
}
}
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_s, "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")
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")
# Main proxy configs
pgcat_cfg["pools"] = {
"#{pool_name}" => {
"default_role" => "any",
"pool_mode" => pool_mode,
"load_balancing_mode" => lb_mode,
"primary_reads_enabled" => false,
"query_parser_enabled" => false,
"sharding_function" => "pg_bigint_hash",
"shards" => {
"0" => {
"database" => "shard0",
"servers" => [
["localhost", primary.port.to_s, "primary"],
["localhost", replica0.port.to_s, "replica"],
["localhost", replica1.port.to_s, "replica"],
["localhost", replica2.port.to_s, "replica"]
]
},
},
"users" => { "0" => user }
}
}
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

View File

@@ -1,132 +0,0 @@
require 'pg'
require 'toml'
require 'fileutils'
require 'securerandom'
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 = {"RUST_LOG" => log_level}
@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}"
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
output_to_write = TOML::Generator.new(config_hash).body
output_to_write = output_to_write.gsub(/,\s*["|'](\d+)["|']\s*,/, ',\1,')
output_to_write = output_to_write.gsub(/,\s*["|'](\d+)["|']\s*\]/, ',\1]')
File.write(@config_filename, output_to_write)
end
def current_config
loadable_string = File.read(@config_filename)
loadable_string = loadable_string.gsub(/,\s*(\d+)\s*,/, ', "\1",')
loadable_string = loadable_string.gsub(/,\s*(\d+)\s*\]/, ', "\1"]')
TOML.load(loadable_string)
end
def reload_config
`kill -s HUP #{@pid}`
sleep 0.5
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)
cfg = current_config
user_idx, user_obj = cfg["pools"][pool_name]["users"].detect { |k, user| user["username"] == username }
"postgresql://#{username}:#{password || user_obj["password"]}@0.0.0.0:#{@port}/#{pool_name}"
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

View File

@@ -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

View File

@@ -1,90 +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_s, "0"],
[mirror_host, mirror_pg.port.to_s, "0"],
[mirror_host, mirror_pg.port.to_s, "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
it "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)
expect(mirror_pg.count_select_1_plus_2).to eq(runs * 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_s, "0"],
[mirror_host, mirror_pg.port.to_s, "0"],
[mirror_host, mirror_pg.port.to_s, "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

View File

@@ -1,366 +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 DISCARD 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("DISCARD 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("DISCARD ALL")).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 DISCARD 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("DISCARD 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("DISCARD ALL")).to eq(10)
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("DISCARD 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("DISCARD 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"]
puts(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

View File

@@ -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

View File

@@ -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

View File

@@ -1,51 +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")
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
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
end

View File

@@ -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

View File

@@ -1,6 +1,10 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'pg'
require 'active_record' require 'active_record'
require 'pg'
require 'toml'
$stdout.sync = true
# Uncomment these two to see all queries. # Uncomment these two to see all queries.
# ActiveRecord.verbose_query_logs = true # ActiveRecord.verbose_query_logs = true
@@ -111,3 +115,89 @@ begin
rescue ActiveRecord::StatementInvalid rescue ActiveRecord::StatementInvalid
puts 'OK' puts 'OK'
end end
# Test evil clients
def poorly_behaved_client
conn = PG::connect("postgres://sharding_user:sharding_user@127.0.0.1:6432/sharded_db?application_name=testing_pgcat")
conn.async_exec 'BEGIN'
conn.async_exec 'SELECT 1'
conn.close
puts 'Bad client ok'
end
25.times do
poorly_behaved_client
end
def test_server_parameters
server_conn = PG::connect("postgres://sharding_user:sharding_user@127.0.0.1:6432/sharded_db?application_name=testing_pgcat")
raise StandardError, "Bad server version" if server_conn.server_version == 0
server_conn.close
admin_conn = PG::connect("postgres://admin_user:admin_pass@127.0.0.1:6432/pgcat")
raise StandardError, "Bad server version" if admin_conn.server_version == 0
admin_conn.close
puts 'Server parameters ok'
end
class ConfigEditor
def initialize
@original_config_text = File.read('../../.circleci/pgcat.toml')
text_to_load = @original_config_text.gsub("5432", "\"5432\"")
@original_configs = TOML.load(text_to_load)
end
def original_configs
TOML.load(TOML::Generator.new(@original_configs).body)
end
def with_modified_configs(new_configs)
text_to_write = TOML::Generator.new(new_configs).body
text_to_write = text_to_write.gsub("\"5432\"", "5432")
File.write('../../.circleci/pgcat.toml', text_to_write)
yield
ensure
File.write('../../.circleci/pgcat.toml', @original_config_text)
end
end
def test_reload_pool_recycling
admin_conn = PG::connect("postgres://admin_user:admin_pass@127.0.0.1:6432/pgcat")
server_conn = PG::connect("postgres://sharding_user:sharding_user@127.0.0.1:6432/sharded_db?application_name=testing_pgcat")
server_conn.async_exec("BEGIN")
conf_editor = ConfigEditor.new
new_configs = conf_editor.original_configs
# swap shards
new_configs["pools"]["sharded_db"]["shards"]["0"]["database"] = "shard1"
new_configs["pools"]["sharded_db"]["shards"]["1"]["database"] = "shard0"
raise StandardError if server_conn.async_exec("SELECT current_database();")[0]["current_database"] != 'shard0'
conf_editor.with_modified_configs(new_configs) { admin_conn.async_exec("RELOAD") }
raise StandardError if server_conn.async_exec("SELECT current_database();")[0]["current_database"] != 'shard0'
server_conn.async_exec("COMMIT;")
# Transaction finished, client should get new configs
raise StandardError if server_conn.async_exec("SELECT current_database();")[0]["current_database"] != 'shard1'
server_conn.close()
# New connection should get new configs
server_conn = PG::connect("postgres://sharding_user:sharding_user@127.0.0.1:6432/sharded_db?application_name=testing_pgcat")
raise StandardError if server_conn.async_exec("SELECT current_database();")[0]["current_database"] != 'shard1'
ensure
admin_conn.async_exec("RELOAD") # Go back to old state
admin_conn.close
server_conn.close
puts "Pool Recycling okay!"
end
test_reload_pool_recycling

View File

@@ -70,35 +70,23 @@ GRANT CONNECT ON DATABASE shard2 TO other_user;
GRANT CONNECT ON DATABASE some_db TO simple_user; GRANT CONNECT ON DATABASE some_db TO simple_user;
\c shard0 \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 SCHEMA public TO sharding_user;
GRANT ALL ON TABLE data TO sharding_user; GRANT ALL ON TABLE data TO sharding_user;
GRANT ALL ON SCHEMA public TO other_user; GRANT ALL ON SCHEMA public TO other_user;
GRANT ALL ON TABLE data TO other_user; GRANT ALL ON TABLE data TO other_user;
GRANT EXECUTE ON FUNCTION pg_stat_statements_reset TO other_user;
\c shard1 \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 SCHEMA public TO sharding_user;
GRANT ALL ON TABLE data TO sharding_user; GRANT ALL ON TABLE data TO sharding_user;
GRANT ALL ON SCHEMA public TO other_user; GRANT ALL ON SCHEMA public TO other_user;
GRANT ALL ON TABLE data TO other_user; GRANT ALL ON TABLE data TO other_user;
GRANT EXECUTE ON FUNCTION pg_stat_statements_reset TO other_user;
\c shard2 \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 SCHEMA public TO sharding_user;
GRANT ALL ON TABLE data TO sharding_user; GRANT ALL ON TABLE data TO sharding_user;
GRANT ALL ON SCHEMA public TO other_user; GRANT ALL ON SCHEMA public TO other_user;
GRANT ALL ON TABLE data 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 \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 SCHEMA public TO simple_user;
GRANT ALL ON TABLE data TO simple_user; GRANT ALL ON TABLE data TO simple_user;

View File

@@ -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", "<pool_name>")
self.current_section = self.current_section.replace("simple_db", "<pool_name>")
self.current_section = self.current_section.replace("users.0", "users.<user_index>")
self.current_section = self.current_section.replace("users.1", "users.<user_index>")
self.current_section = self.current_section.replace("shards.0", "shards.<shard_index>")
self.current_section = self.current_section.replace("shards.1", "shards.<shard_index>")
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 "<UNSET>",
"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()

View File

@@ -1 +0,0 @@
tomli