mirror of
https://github.com/postgresml/pgcat.git
synced 2026-03-23 09:26:30 +00:00
Compare commits
80 Commits
levkk-deb-
...
mostafa_sq
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6e11e11da | ||
|
|
4aaa4378cf | ||
|
|
670311daf9 | ||
|
|
b9ec7f8036 | ||
|
|
d91d23848b | ||
|
|
bbbc01a467 | ||
|
|
9bb71ede9d | ||
|
|
88b2afb19b | ||
|
|
f0865ca616 | ||
|
|
7d047c6c19 | ||
|
|
f73d15f82c | ||
|
|
69af6cc5e5 | ||
|
|
ca34597002 | ||
|
|
2def40ea6a | ||
|
|
c05129018d | ||
|
|
4a7a6a8e7a | ||
|
|
29a476e190 | ||
|
|
81933b918d | ||
|
|
7cbc9178d8 | ||
|
|
2c8b2f0776 | ||
|
|
8f9a2b8e6f | ||
|
|
cbf4d58144 | ||
|
|
731aa047ba | ||
|
|
88dbcc21d1 | ||
|
|
c34b15bddc | ||
|
|
0b034a6831 | ||
|
|
966b8e093c | ||
|
|
c9270a47d4 | ||
|
|
0d94d0b90a | ||
|
|
358724f7a9 | ||
|
|
e1e4929d43 | ||
|
|
dc4d6edf17 | ||
|
|
ec3920d60f | ||
|
|
4c5498b915 | ||
|
|
0e8064b049 | ||
|
|
4dbef49ec9 | ||
|
|
bc07dc9c81 | ||
|
|
9b8166b313 | ||
|
|
e58d69f3de | ||
|
|
e76d720ffb | ||
|
|
998cc16a3c | ||
|
|
7c37da2fad | ||
|
|
b45c6b1d23 | ||
|
|
dae240d30c | ||
|
|
b52ea8e7f1 | ||
|
|
7d3003a16a | ||
|
|
d37df43a90 | ||
|
|
2c7bf52c17 | ||
|
|
de8df29ca4 | ||
|
|
c4fb72b9fc | ||
|
|
3371c01e0e | ||
|
|
c2a483f36a | ||
|
|
51cd13b8b5 | ||
|
|
a054b454d2 | ||
|
|
04e9814770 | ||
|
|
037d232fcd | ||
|
|
b2933762e7 | ||
|
|
df8aa888f9 | ||
|
|
7f5639c94a | ||
|
|
c0112f6f12 | ||
|
|
b7ceee2ddf | ||
|
|
0b01d70b55 | ||
|
|
33db0dffa8 | ||
|
|
7994a661d9 | ||
|
|
9937193332 | ||
|
|
baa00ff546 | ||
|
|
ffe820497f | ||
|
|
be549f3faa | ||
|
|
4301ab0606 | ||
|
|
5143500c9a | ||
|
|
3255323bff | ||
|
|
bb27586758 | ||
|
|
4f0f45b576 | ||
|
|
f94ce97ebc | ||
|
|
9ab128579d | ||
|
|
1cde74f05e | ||
|
|
a4de6c1eb6 | ||
|
|
e14b283f0c | ||
|
|
7c3c90c38e | ||
|
|
2ca21b2bec |
@@ -9,7 +9,7 @@ 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: ghcr.io/postgresml/pgcat-ci:latest
|
||||||
environment:
|
environment:
|
||||||
RUST_LOG: info
|
RUST_LOG: info
|
||||||
LLVM_PROFILE_FILE: /tmp/pgcat-%m-%p.profraw
|
LLVM_PROFILE_FILE: /tmp/pgcat-%m-%p.profraw
|
||||||
@@ -63,6 +63,9 @@ jobs:
|
|||||||
- run:
|
- run:
|
||||||
name: "Lint"
|
name: "Lint"
|
||||||
command: "cargo fmt --check"
|
command: "cargo fmt --check"
|
||||||
|
- run:
|
||||||
|
name: "Clippy"
|
||||||
|
command: "cargo clippy --all --all-targets -- -Dwarnings"
|
||||||
- run:
|
- run:
|
||||||
name: "Tests"
|
name: "Tests"
|
||||||
command: "cargo clean && cargo build && cargo test && bash .circleci/run_tests.sh && .circleci/generate_coverage.sh"
|
command: "cargo clean && cargo build && cargo test && bash .circleci/run_tests.sh && .circleci/generate_coverage.sh"
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ admin_password = "admin_pass"
|
|||||||
# 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"
|
||||||
|
prepared_statements_cache_size = 500
|
||||||
|
|
||||||
# If the client doesn't specify, route traffic to
|
# If the client doesn't specify, route traffic to
|
||||||
# this role by default.
|
# this role by default.
|
||||||
@@ -74,6 +75,10 @@ default_role = "any"
|
|||||||
# we'll direct it to the primary.
|
# we'll direct it to the primary.
|
||||||
query_parser_enabled = true
|
query_parser_enabled = true
|
||||||
|
|
||||||
|
# If the query parser is enabled and this setting is enabled, we'll attempt to
|
||||||
|
# infer the role from the query itself.
|
||||||
|
query_parser_read_write_splitting = true
|
||||||
|
|
||||||
# If the query parser is enabled and this setting is enabled, the primary will be part of the pool of databases used for
|
# 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 explicitely selected with our custom protocol.
|
# queries. The primary can always be explicitely selected with our custom protocol.
|
||||||
@@ -134,8 +139,10 @@ database = "shard2"
|
|||||||
pool_mode = "session"
|
pool_mode = "session"
|
||||||
default_role = "primary"
|
default_role = "primary"
|
||||||
query_parser_enabled = true
|
query_parser_enabled = true
|
||||||
|
query_parser_read_write_splitting = true
|
||||||
primary_reads_enabled = true
|
primary_reads_enabled = true
|
||||||
sharding_function = "pg_bigint_hash"
|
sharding_function = "pg_bigint_hash"
|
||||||
|
prepared_statements_cache_size = 500
|
||||||
|
|
||||||
[pools.simple_db.users.0]
|
[pools.simple_db.users.0]
|
||||||
username = "simple_user"
|
username = "simple_user"
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ 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
|
||||||
|
|
||||||
# Start Toxiproxy
|
# Start Toxiproxy
|
||||||
|
kill -9 $(pgrep toxiproxy) || true
|
||||||
LOG_LEVEL=error toxiproxy-server &
|
LOG_LEVEL=error toxiproxy-server &
|
||||||
sleep 1
|
sleep 1
|
||||||
|
|
||||||
@@ -106,10 +107,26 @@ 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
|
pytest || exit 1
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Go tests
|
||||||
|
# Starts its own pgcat server
|
||||||
|
#
|
||||||
|
pushd tests/go
|
||||||
|
/usr/local/go/bin/go test || exit 1
|
||||||
|
popd
|
||||||
|
|
||||||
start_pgcat "info"
|
start_pgcat "info"
|
||||||
|
|
||||||
|
#
|
||||||
|
# Rust tests
|
||||||
|
#
|
||||||
|
cd tests/rust
|
||||||
|
cargo run
|
||||||
|
cd ../../
|
||||||
|
|
||||||
# Admin tests
|
# Admin tests
|
||||||
export PGPASSWORD=admin_pass
|
export PGPASSWORD=admin_pass
|
||||||
psql -U admin_user -e -h 127.0.0.1 -p 6432 -d pgbouncer -c 'SHOW STATS' > /dev/null
|
psql -U admin_user -e -h 127.0.0.1 -p 6432 -d pgbouncer -c 'SHOW STATS' > /dev/null
|
||||||
@@ -161,3 +178,6 @@ killall pgcat -s SIGINT
|
|||||||
|
|
||||||
# Allow for graceful shutdown
|
# Allow for graceful shutdown
|
||||||
sleep 1
|
sleep 1
|
||||||
|
|
||||||
|
kill -9 $(pgrep toxiproxy)
|
||||||
|
sleep 1
|
||||||
|
|||||||
4
.github/dependabot.yml
vendored
4
.github/dependabot.yml
vendored
@@ -10,3 +10,7 @@ updates:
|
|||||||
commit-message:
|
commit-message:
|
||||||
prefix: "chore(deps)"
|
prefix: "chore(deps)"
|
||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 10
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
|||||||
20
.github/workflows/build-and-push.yaml
vendored
20
.github/workflows/build-and-push.yaml
vendored
@@ -2,7 +2,9 @@ name: Build and Push
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
paths:
|
||||||
|
- '!charts/**.md'
|
||||||
|
branches:
|
||||||
- main
|
- main
|
||||||
tags:
|
tags:
|
||||||
- v*
|
- v*
|
||||||
@@ -21,14 +23,17 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Determine tags
|
- name: Determine tags
|
||||||
id: metadata
|
id: metadata
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: ${{ env.registry }}/${{ env.image-name }}
|
images: ${{ env.registry }}/${{ env.image-name }}
|
||||||
tags: |
|
tags: |
|
||||||
@@ -40,15 +45,18 @@ jobs:
|
|||||||
type=raw,value=latest,enable={{ is_default_branch }}
|
type=raw,value=latest,enable={{ is_default_branch }}
|
||||||
|
|
||||||
- name: Log in to the Container registry
|
- name: Log in to the Container registry
|
||||||
uses: docker/login-action@v2.1.0
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.registry }}
|
registry: ${{ env.registry }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build and push ${{ env.image-name }}
|
- name: Build and push ${{ env.image-name }}
|
||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
provenance: false
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.metadata.outputs.tags }}
|
tags: ${{ steps.metadata.outputs.tags }}
|
||||||
labels: ${{ steps.metadata.outputs.labels }}
|
labels: ${{ steps.metadata.outputs.labels }}
|
||||||
|
|||||||
50
.github/workflows/chart-lint-test.yaml
vendored
Normal file
50
.github/workflows/chart-lint-test.yaml
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
name: Lint and Test Charts
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- charts/**
|
||||||
|
- '!charts/**.md'
|
||||||
|
jobs:
|
||||||
|
lint-test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3.1.0
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Helm
|
||||||
|
uses: azure/setup-helm@v3
|
||||||
|
with:
|
||||||
|
version: v3.8.1
|
||||||
|
|
||||||
|
# Python is required because `ct lint` runs Yamale (https://github.com/23andMe/Yamale) and
|
||||||
|
# yamllint (https://github.com/adrienverge/yamllint) which require Python
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5.1.0
|
||||||
|
with:
|
||||||
|
python-version: 3.7
|
||||||
|
|
||||||
|
- name: Set up chart-testing
|
||||||
|
uses: helm/chart-testing-action@v2.2.1
|
||||||
|
with:
|
||||||
|
version: v3.5.1
|
||||||
|
|
||||||
|
- name: Run chart-testing (list-changed)
|
||||||
|
id: list-changed
|
||||||
|
run: |
|
||||||
|
changed=$(ct list-changed --config ct.yaml)
|
||||||
|
if [[ -n "$changed" ]]; then
|
||||||
|
echo "changed=true" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Run chart-testing (lint)
|
||||||
|
run: ct lint --config ct.yaml
|
||||||
|
|
||||||
|
- name: Create kind cluster
|
||||||
|
uses: helm/kind-action@v1.10.0
|
||||||
|
if: steps.list-changed.outputs.changed == 'true'
|
||||||
|
|
||||||
|
- name: Run chart-testing (install)
|
||||||
|
run: ct install --config ct.yaml
|
||||||
40
.github/workflows/chart-release.yaml
vendored
Normal file
40
.github/workflows/chart-release.yaml
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
name: Release Charts
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- charts/**
|
||||||
|
- '!**.md'
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Configure Git
|
||||||
|
run: |
|
||||||
|
git config user.name "$GITHUB_ACTOR"
|
||||||
|
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
|
||||||
|
|
||||||
|
- name: Install Helm
|
||||||
|
uses: azure/setup-helm@5119fcb9089d432beecbf79bb2c7915207344b78 # v3.5
|
||||||
|
with:
|
||||||
|
version: v3.13.0
|
||||||
|
|
||||||
|
- name: Run chart-releaser
|
||||||
|
uses: helm/chart-releaser-action@be16258da8010256c6e82849661221415f031968 # v1.5.0
|
||||||
|
with:
|
||||||
|
charts_dir: charts
|
||||||
|
config: cr.yaml
|
||||||
|
env:
|
||||||
|
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
48
.github/workflows/generate-chart-readme.yaml
vendored
Normal file
48
.github/workflows/generate-chart-readme.yaml
vendored
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
name: '[CI/CD] Update README metadata'
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- 'charts/*/values.yaml'
|
||||||
|
# Remove all permissions by default
|
||||||
|
permissions: {}
|
||||||
|
jobs:
|
||||||
|
update-readme-metadata:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- name: Install readme-generator-for-helm
|
||||||
|
run: npm install -g @bitnami/readme-generator-for-helm
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608
|
||||||
|
with:
|
||||||
|
path: charts
|
||||||
|
ref: ${{github.event.pull_request.head.ref}}
|
||||||
|
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Execute readme-generator-for-helm
|
||||||
|
env:
|
||||||
|
DIFF_URL: "${{github.event.pull_request.diff_url}}"
|
||||||
|
TEMP_FILE: "${{runner.temp}}/pr-${{github.event.number}}.diff"
|
||||||
|
run: |
|
||||||
|
# This request doesn't consume API calls.
|
||||||
|
curl -Lkso $TEMP_FILE $DIFF_URL
|
||||||
|
files_changed="$(sed -nr 's/[\-\+]{3} [ab]\/(.*)/\1/p' $TEMP_FILE | sort | uniq)"
|
||||||
|
# Adding || true to avoid "Process exited with code 1" errors
|
||||||
|
charts_dirs_changed="$(echo "$files_changed" | xargs dirname | grep -o "pgcat/[^/]*" | sort | uniq || true)"
|
||||||
|
for chart in ${charts_dirs_changed}; do
|
||||||
|
echo "Updating README.md for ${chart}"
|
||||||
|
readme-generator --values "charts/${chart}/values.yaml" --readme "charts/${chart}/README.md" --schema "/tmp/schema.json"
|
||||||
|
done
|
||||||
|
- name: Push changes
|
||||||
|
run: |
|
||||||
|
# Push all the changes
|
||||||
|
cd charts
|
||||||
|
if git status -s | grep pgcat; then
|
||||||
|
git config user.name "$GITHUB_ACTOR"
|
||||||
|
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
|
||||||
|
git add . && git commit -am "Update README.md with readme-generator-for-helm" --signoff && git push
|
||||||
|
fi
|
||||||
59
.github/workflows/publish-deb-package.yml
vendored
Normal file
59
.github/workflows/publish-deb-package.yml
vendored
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
name: pgcat package (deb)
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- v*
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
packageVersion:
|
||||||
|
default: "1.1.2-dev1"
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
strategy:
|
||||||
|
max-parallel: 1
|
||||||
|
fail-fast: false # Let the other job finish, or they can lock each other out
|
||||||
|
matrix:
|
||||||
|
os: ["buildjet-4vcpu-ubuntu-2204", "buildjet-4vcpu-ubuntu-2204-arm"]
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Set package version
|
||||||
|
if: github.event_name == 'push' # For push event
|
||||||
|
run: |
|
||||||
|
TAG=${{ github.ref_name }}
|
||||||
|
echo "packageVersion=${TAG#v}" >> "$GITHUB_ENV"
|
||||||
|
- name: Set package version (manual dispatch)
|
||||||
|
if: github.event_name == 'workflow_dispatch' # For manual dispatch
|
||||||
|
run: echo "packageVersion=${{ github.event.inputs.packageVersion }}" >> "$GITHUB_ENV"
|
||||||
|
- uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
- name: Install dependencies
|
||||||
|
env:
|
||||||
|
DEBIAN_FRONTEND: noninteractive
|
||||||
|
TZ: Etc/UTC
|
||||||
|
run: |
|
||||||
|
curl -sLO https://github.com/deb-s3/deb-s3/releases/download/0.11.4/deb-s3-0.11.4.gem
|
||||||
|
sudo gem install deb-s3-0.11.4.gem
|
||||||
|
dpkg-deb --version
|
||||||
|
- name: Build and release package
|
||||||
|
env:
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ vars.AWS_ACCESS_KEY_ID }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||||
|
AWS_DEFAULT_REGION: ${{ vars.AWS_DEFAULT_REGION }}
|
||||||
|
run: |
|
||||||
|
if [[ $(arch) == "x86_64" ]]; then
|
||||||
|
export ARCH=amd64
|
||||||
|
else
|
||||||
|
export ARCH=arm64
|
||||||
|
fi
|
||||||
|
|
||||||
|
bash utilities/deb.sh ${{ env.packageVersion }}
|
||||||
|
|
||||||
|
deb-s3 upload \
|
||||||
|
--lock \
|
||||||
|
--bucket apt.postgresml.org \
|
||||||
|
pgcat-${{ env.packageVersion }}-ubuntu22.04-${ARCH}.deb \
|
||||||
|
--codename $(lsb_release -cs)
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -10,3 +10,5 @@ lcov.info
|
|||||||
dev/.bash_history
|
dev/.bash_history
|
||||||
dev/cache
|
dev/cache
|
||||||
!dev/cache/.keepme
|
!dev/cache/.keepme
|
||||||
|
.venv
|
||||||
|
**/__pycache__
|
||||||
|
|||||||
74
CONFIG.md
74
CONFIG.md
@@ -57,6 +57,38 @@ default: 86400000 # 24 hours
|
|||||||
|
|
||||||
Max connection lifetime before it's closed, even if actively used.
|
Max connection lifetime before it's closed, even if actively used.
|
||||||
|
|
||||||
|
### server_round_robin
|
||||||
|
```
|
||||||
|
path: general.server_round_robin
|
||||||
|
default: false
|
||||||
|
```
|
||||||
|
|
||||||
|
Whether to use round robin for server selection or not.
|
||||||
|
|
||||||
|
### server_tls
|
||||||
|
```
|
||||||
|
path: general.server_tls
|
||||||
|
default: false
|
||||||
|
```
|
||||||
|
|
||||||
|
Whether to use TLS for server connections or not.
|
||||||
|
|
||||||
|
### verify_server_certificate
|
||||||
|
```
|
||||||
|
path: general.verify_server_certificate
|
||||||
|
default: false
|
||||||
|
```
|
||||||
|
|
||||||
|
Whether to verify server certificate or not.
|
||||||
|
|
||||||
|
### verify_config
|
||||||
|
```
|
||||||
|
path: general.verify_config
|
||||||
|
default: true
|
||||||
|
```
|
||||||
|
|
||||||
|
Whether to verify config or not.
|
||||||
|
|
||||||
### idle_client_in_transaction_timeout
|
### idle_client_in_transaction_timeout
|
||||||
```
|
```
|
||||||
path: general.idle_client_in_transaction_timeout
|
path: general.idle_client_in_transaction_timeout
|
||||||
@@ -194,6 +226,39 @@ default: "admin_pass"
|
|||||||
|
|
||||||
Password to access the virtual administrative database
|
Password to access the virtual administrative database
|
||||||
|
|
||||||
|
### auth_query
|
||||||
|
```
|
||||||
|
path: general.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: general.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: general.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.
|
||||||
|
|
||||||
### dns_cache_enabled
|
### dns_cache_enabled
|
||||||
```
|
```
|
||||||
path: general.dns_cache_enabled
|
path: general.dns_cache_enabled
|
||||||
@@ -243,6 +308,15 @@ If the client doesn't specify, PgCat routes traffic to this role by default.
|
|||||||
`replica` round-robin between replicas only without touching the primary,
|
`replica` round-robin between replicas only without touching the primary,
|
||||||
`primary` all queries go to the primary unless otherwise specified.
|
`primary` all queries go to the primary unless otherwise specified.
|
||||||
|
|
||||||
|
### prepared_statements_cache_size
|
||||||
|
```
|
||||||
|
path: general.prepared_statements_cache_size
|
||||||
|
default: 0
|
||||||
|
```
|
||||||
|
|
||||||
|
Size of the prepared statements cache. 0 means disabled.
|
||||||
|
TODO: update documentation
|
||||||
|
|
||||||
### query_parser_enabled
|
### query_parser_enabled
|
||||||
```
|
```
|
||||||
path: pools.<pool_name>.query_parser_enabled
|
path: pools.<pool_name>.query_parser_enabled
|
||||||
|
|||||||
@@ -2,10 +2,36 @@
|
|||||||
|
|
||||||
Thank you for contributing! Just a few tips here:
|
Thank you for contributing! Just a few tips here:
|
||||||
|
|
||||||
1. `cargo fmt` your code before opening up a PR
|
1. `cargo fmt` and `cargo clippy` your code before opening up a PR
|
||||||
2. Run the test suite (e.g. `pgbench`) to make sure everything still works. The tests are in `.circleci/run_tests.sh`.
|
2. Run the test suite (e.g. `pgbench`) to make sure everything still works. The tests are in `.circleci/run_tests.sh`.
|
||||||
3. Performance is important, make sure there are no regressions in your branch vs. `main`.
|
3. Performance is important, make sure there are no regressions in your branch vs. `main`.
|
||||||
|
|
||||||
|
## How to run the integration tests locally and iterate on them
|
||||||
|
We have integration tests written in Ruby, Python, Go and Rust.
|
||||||
|
Below are the steps to run them in a developer-friendly way that allows iterating and quick turnaround.
|
||||||
|
Hear me out, this should be easy, it will involve opening a shell into a container with all the necessary dependancies available for you and you can modify the test code and immediately rerun your test in the interactive shell.
|
||||||
|
|
||||||
|
|
||||||
|
Quite simply, make sure you have docker installed and then run
|
||||||
|
`./start_test_env.sh`
|
||||||
|
|
||||||
|
That is it!
|
||||||
|
|
||||||
|
Within this test environment you can modify the file in your favorite IDE and rerun the tests without having to bootstrap the entire environment again.
|
||||||
|
|
||||||
|
Once the environment is ready, you can run the tests by running
|
||||||
|
Ruby: `cd /app/tests/ruby && bundle exec ruby <test_name>.rb --format documentation`
|
||||||
|
Python: `cd /app/ && pytest`
|
||||||
|
Rust: `cd /app/tests/rust && cargo run`
|
||||||
|
Go: `cd /app/tests/go && /usr/local/go/bin/go test`
|
||||||
|
|
||||||
|
You can also rebuild PgCat directly within the environment and the tests will run against the newly built binary
|
||||||
|
To rebuild PgCat, just run `cargo build` within the container under `/app`
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Happy hacking!
|
Happy hacking!
|
||||||
|
|
||||||
## TODOs
|
## TODOs
|
||||||
|
|||||||
139
Cargo.lock
generated
139
Cargo.lock
generated
@@ -17,6 +17,17 @@ version = "1.0.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ahash"
|
||||||
|
version = "0.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"once_cell",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
@@ -26,6 +37,12 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "allocator-api2"
|
||||||
|
version = "0.2.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "android-tzdata"
|
name = "android-tzdata"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@@ -129,6 +146,12 @@ dependencies = [
|
|||||||
"syn 2.0.26",
|
"syn 2.0.26",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "atomic-waker"
|
||||||
|
version = "1.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atomic_enum"
|
name = "atomic_enum"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -525,34 +548,32 @@ checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h2"
|
name = "h2"
|
||||||
version = "0.3.20"
|
version = "0.4.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049"
|
checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"atomic-waker",
|
||||||
"bytes",
|
"bytes",
|
||||||
"fnv",
|
"fnv",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"futures-util",
|
|
||||||
"http",
|
"http",
|
||||||
"indexmap 1.9.3",
|
"indexmap",
|
||||||
"slab",
|
"slab",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hashbrown"
|
|
||||||
version = "0.12.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.14.0"
|
version = "0.14.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
|
checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
|
||||||
|
dependencies = [
|
||||||
|
"ahash",
|
||||||
|
"allocator-api2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
@@ -588,9 +609,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "0.2.9"
|
version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482"
|
checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"fnv",
|
"fnv",
|
||||||
@@ -599,12 +620,24 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http-body"
|
name = "http-body"
|
||||||
version = "0.4.5"
|
version = "1.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
|
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"http",
|
"http",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "http-body-util"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-util",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -622,13 +655,12 @@ checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper"
|
name = "hyper"
|
||||||
version = "0.14.27"
|
version = "1.4.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468"
|
checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"h2",
|
"h2",
|
||||||
"http",
|
"http",
|
||||||
@@ -637,13 +669,26 @@ dependencies = [
|
|||||||
"httpdate",
|
"httpdate",
|
||||||
"itoa",
|
"itoa",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"socket2 0.4.9",
|
"smallvec",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-service",
|
|
||||||
"tracing",
|
|
||||||
"want",
|
"want",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hyper-util"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cde7055719c54e36e95e8719f95883f22072a48ede39db7fc17a4e1d5281e9b9"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-util",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"hyper",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iana-time-zone"
|
name = "iana-time-zone"
|
||||||
version = "0.1.57"
|
version = "0.1.57"
|
||||||
@@ -688,16 +733,6 @@ dependencies = [
|
|||||||
"unicode-normalization",
|
"unicode-normalization",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "indexmap"
|
|
||||||
version = "1.9.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
|
|
||||||
dependencies = [
|
|
||||||
"autocfg",
|
|
||||||
"hashbrown 0.12.3",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
@@ -705,7 +740,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
|
checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown 0.14.0",
|
"hashbrown",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -821,6 +856,15 @@ version = "0.4.19"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4"
|
checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lru"
|
||||||
|
version = "0.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1efa59af2ddfad1854ae27d75009d538d0998b4b2fd47083e743ac1a10e46c60"
|
||||||
|
dependencies = [
|
||||||
|
"hashbrown",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lru-cache"
|
name = "lru-cache"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
@@ -990,7 +1034,7 @@ checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pgcat"
|
name = "pgcat"
|
||||||
version = "1.1.1"
|
version = "1.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -1004,10 +1048,13 @@ dependencies = [
|
|||||||
"fallible-iterator",
|
"fallible-iterator",
|
||||||
"futures",
|
"futures",
|
||||||
"hmac",
|
"hmac",
|
||||||
|
"http-body-util",
|
||||||
"hyper",
|
"hyper",
|
||||||
|
"hyper-util",
|
||||||
"itertools",
|
"itertools",
|
||||||
"jemallocator",
|
"jemallocator",
|
||||||
"log",
|
"log",
|
||||||
|
"lru",
|
||||||
"md-5",
|
"md-5",
|
||||||
"nix",
|
"nix",
|
||||||
"num_cpus",
|
"num_cpus",
|
||||||
@@ -1310,9 +1357,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-webpki"
|
name = "rustls-webpki"
|
||||||
version = "0.100.1"
|
version = "0.100.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d6207cd5ed3d8dca7816f8f3725513a34609c0c765bf652b8c3cb4cfd87db46b"
|
checksum = "e98ff011474fa39949b7e5c0428f9b4937eda7da7848bbb947786b7be0b27dab"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ring",
|
"ring",
|
||||||
"untrusted",
|
"untrusted",
|
||||||
@@ -1447,9 +1494,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
version = "1.11.0"
|
version = "1.13.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
|
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
@@ -1479,9 +1526,9 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlparser"
|
name = "sqlparser"
|
||||||
version = "0.34.0"
|
version = "0.51.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "37d3706eefb17039056234df6b566b0014f303f867f2656108334a55b8096f59"
|
checksum = "5fe11944a61da0da3f592e19a45ebe5ab92dc14a779907ff1f08fbb797bfefc7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"sqlparser_derive",
|
"sqlparser_derive",
|
||||||
@@ -1489,13 +1536,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlparser_derive"
|
name = "sqlparser_derive"
|
||||||
version = "0.1.1"
|
version = "0.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "55fe75cb4a364c7f7ae06c7dbbc8d84bddd85d6cdf9975963c3935bc1991761e"
|
checksum = "01b2e185515564f15375f593fb966b5718bc624ba77fe49fa4616ad619690554"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 1.0.109",
|
"syn 2.0.26",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1710,19 +1757,13 @@ version = "0.19.14"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a"
|
checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap 2.0.0",
|
"indexmap",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_spanned",
|
"serde_spanned",
|
||||||
"toml_datetime",
|
"toml_datetime",
|
||||||
"winnow",
|
"winnow",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tower-service"
|
|
||||||
version = "0.3.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing"
|
name = "tracing"
|
||||||
version = "0.1.37"
|
version = "0.1.37"
|
||||||
@@ -2002,7 +2043,7 @@ version = "0.23.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b03058f88386e5ff5310d9111d53f48b17d732b401aeb83a8d5190f2ac459338"
|
checksum = "b03058f88386e5ff5310d9111d53f48b17d732b401aeb83a8d5190f2ac459338"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustls-webpki 0.100.1",
|
"rustls-webpki 0.100.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
16
Cargo.toml
16
Cargo.toml
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "pgcat"
|
name = "pgcat"
|
||||||
version = "1.1.1"
|
version = "1.2.0"
|
||||||
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
|
||||||
@@ -19,7 +19,7 @@ serde_derive = "1"
|
|||||||
regex = "1"
|
regex = "1"
|
||||||
num_cpus = "1"
|
num_cpus = "1"
|
||||||
once_cell = "1"
|
once_cell = "1"
|
||||||
sqlparser = {version = "0.34", features = ["visitor"] }
|
sqlparser = { version = "0.51", features = ["visitor"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
arc-swap = "1"
|
arc-swap = "1"
|
||||||
parking_lot = "0.12.1"
|
parking_lot = "0.12.1"
|
||||||
@@ -29,7 +29,9 @@ base64 = "0.21"
|
|||||||
stringprep = "0.1"
|
stringprep = "0.1"
|
||||||
tokio-rustls = "0.24"
|
tokio-rustls = "0.24"
|
||||||
rustls-pemfile = "1"
|
rustls-pemfile = "1"
|
||||||
hyper = { version = "0.14", features = ["full"] }
|
http-body-util = "0.1.2"
|
||||||
|
hyper = { version = "1.4.1", features = ["full"] }
|
||||||
|
hyper-util = { version = "0.1.7", features = ["tokio"] }
|
||||||
phf = { version = "0.11.1", features = ["macros"] }
|
phf = { version = "0.11.1", features = ["macros"] }
|
||||||
exitcode = "1.1.2"
|
exitcode = "1.1.2"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
@@ -47,8 +49,12 @@ serde_json = "1"
|
|||||||
itertools = "0.10"
|
itertools = "0.10"
|
||||||
clap = { version = "4.3.1", features = ["derive", "env"] }
|
clap = { version = "4.3.1", features = ["derive", "env"] }
|
||||||
tracing = "0.1.37"
|
tracing = "0.1.37"
|
||||||
tracing-subscriber = { version = "0.3.17", features = ["json", "env-filter", "std"]}
|
tracing-subscriber = { version = "0.3.17", features = [
|
||||||
|
"json",
|
||||||
|
"env-filter",
|
||||||
|
"std",
|
||||||
|
] }
|
||||||
|
lru = "0.12.0"
|
||||||
|
|
||||||
[target.'cfg(not(target_env = "msvc"))'.dependencies]
|
[target.'cfg(not(target_env = "msvc"))'.dependencies]
|
||||||
jemallocator = "0.5.0"
|
jemallocator = "0.5.0"
|
||||||
|
|
||||||
|
|||||||
15
Dockerfile
15
Dockerfile
@@ -1,11 +1,22 @@
|
|||||||
FROM rust:1 AS builder
|
FROM rust:1.79.0-slim-bookworm AS builder
|
||||||
|
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y build-essential
|
||||||
|
|
||||||
COPY . /app
|
COPY . /app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN cargo build --release
|
RUN cargo build --release
|
||||||
|
|
||||||
FROM debian:bullseye-slim
|
FROM debian:bookworm-slim
|
||||||
|
RUN apt-get update && apt-get install -o Dpkg::Options::=--force-confdef -yq --no-install-recommends \
|
||||||
|
postgresql-client \
|
||||||
|
# Clean up layer
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
|
||||||
|
&& truncate -s 0 /var/log/*log
|
||||||
COPY --from=builder /app/target/release/pgcat /usr/bin/pgcat
|
COPY --from=builder /app/target/release/pgcat /usr/bin/pgcat
|
||||||
COPY --from=builder /app/pgcat.toml /etc/pgcat/pgcat.toml
|
COPY --from=builder /app/pgcat.toml /etc/pgcat/pgcat.toml
|
||||||
WORKDIR /etc/pgcat
|
WORKDIR /etc/pgcat
|
||||||
ENV RUST_LOG=info
|
ENV RUST_LOG=info
|
||||||
CMD ["pgcat"]
|
CMD ["pgcat"]
|
||||||
|
STOPSIGNAL SIGINT
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
FROM cimg/rust:1.67.1
|
FROM cimg/rust:1.79.0
|
||||||
|
COPY --from=sclevine/yj /bin/yj /bin/yj
|
||||||
|
RUN /bin/yj -h
|
||||||
RUN sudo apt-get update && \
|
RUN sudo apt-get update && \
|
||||||
sudo apt-get install -y \
|
sudo apt-get install -y \
|
||||||
psmisc postgresql-contrib-14 postgresql-client-14 libpq-dev \
|
psmisc postgresql-contrib-14 postgresql-client-14 libpq-dev \
|
||||||
@@ -7,6 +9,9 @@ RUN sudo apt-get update && \
|
|||||||
sudo apt-get upgrade curl && \
|
sudo apt-get upgrade curl && \
|
||||||
cargo install cargo-binutils rustfilt && \
|
cargo install cargo-binutils rustfilt && \
|
||||||
rustup component add llvm-tools-preview && \
|
rustup component add llvm-tools-preview && \
|
||||||
pip3 install psycopg2 && sudo gem install bundler && \
|
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 && \
|
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
|
sudo dpkg -i /tmp/toxiproxy-2.4.0.deb
|
||||||
|
RUN wget -O /tmp/go1.21.3.linux-$(dpkg --print-architecture).tar.gz https://go.dev/dl/go1.21.3.linux-$(dpkg --print-architecture).tar.gz && \
|
||||||
|
sudo tar -C /usr/local -xzf /tmp/go1.21.3.linux-$(dpkg --print-architecture).tar.gz && \
|
||||||
|
rm /tmp/go1.21.3.linux-$(dpkg --print-architecture).tar.gz
|
||||||
|
|||||||
25
Dockerfile.dev
Normal file
25
Dockerfile.dev
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
FROM lukemathwalker/cargo-chef:latest-rust-1 AS chef
|
||||||
|
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y build-essential
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
FROM chef AS planner
|
||||||
|
COPY . .
|
||||||
|
RUN cargo chef prepare --recipe-path recipe.json
|
||||||
|
|
||||||
|
FROM chef AS builder
|
||||||
|
COPY --from=planner /app/recipe.json recipe.json
|
||||||
|
# Build dependencies - this is the caching Docker layer!
|
||||||
|
RUN cargo chef cook --release --recipe-path recipe.json
|
||||||
|
# Build application
|
||||||
|
COPY . .
|
||||||
|
RUN cargo build
|
||||||
|
|
||||||
|
FROM debian:bookworm-slim
|
||||||
|
COPY --from=builder /app/target/release/pgcat /usr/bin/pgcat
|
||||||
|
COPY --from=builder /app/pgcat.toml /etc/pgcat/pgcat.toml
|
||||||
|
WORKDIR /etc/pgcat
|
||||||
|
ENV RUST_LOG=info
|
||||||
|
CMD ["pgcat"]
|
||||||
@@ -40,7 +40,7 @@ PgCat is stable and used in production to serve hundreds of thousands of queries
|
|||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://postgresml.org/blog/scaling-postgresml-to-one-million-requests-per-second">
|
<a href="https://postgresml.org/blog/scaling-postgresml-to-1-million-requests-per-second">
|
||||||
<img src="./images/postgresml.webp" height="70" width="auto">
|
<img src="./images/postgresml.webp" height="70" width="auto">
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
@@ -57,7 +57,7 @@ PgCat is stable and used in production to serve hundreds of thousands of queries
|
|||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://postgresml.org/blog/scaling-postgresml-to-one-million-requests-per-second">
|
<a href="https://postgresml.org/blog/scaling-postgresml-to-1-million-requests-per-second">
|
||||||
PostgresML
|
PostgresML
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
@@ -268,6 +268,8 @@ psql -h 127.0.0.1 -p 6432 -d pgbouncer -c 'SHOW DATABASES'
|
|||||||
|
|
||||||
Additionally, Prometheus statistics are available at `/metrics` via HTTP.
|
Additionally, Prometheus statistics are available at `/metrics` via HTTP.
|
||||||
|
|
||||||
|
We also have a [basic Grafana dashboard](https://github.com/postgresml/pgcat/blob/main/grafana_dashboard.json) based on Prometheus metrics that you can import into Grafana and build on it or use it for monitoring.
|
||||||
|
|
||||||
### Live configuration reloading
|
### 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. All settings except the `host` and `port` can be reloaded without restarting the pooler, including sharding and replicas configurations.
|
||||||
|
|||||||
23
charts/pgcat/.helmignore
Normal file
23
charts/pgcat/.helmignore
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Patterns to ignore when building packages.
|
||||||
|
# This supports shell glob matching, relative path matching, and
|
||||||
|
# negation (prefixed with !). Only one pattern per line.
|
||||||
|
.DS_Store
|
||||||
|
# Common VCS dirs
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
.bzr/
|
||||||
|
.bzrignore
|
||||||
|
.hg/
|
||||||
|
.hgignore
|
||||||
|
.svn/
|
||||||
|
# Common backup files
|
||||||
|
*.swp
|
||||||
|
*.bak
|
||||||
|
*.tmp
|
||||||
|
*.orig
|
||||||
|
*~
|
||||||
|
# Various IDEs
|
||||||
|
.project
|
||||||
|
.idea/
|
||||||
|
*.tmproj
|
||||||
|
.vscode/
|
||||||
8
charts/pgcat/Chart.yaml
Normal file
8
charts/pgcat/Chart.yaml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
apiVersion: v2
|
||||||
|
name: pgcat
|
||||||
|
description: A Helm chart for PgCat a PostgreSQL pooler and proxy (like PgBouncer) with support for sharding, load balancing, failover and mirroring.
|
||||||
|
maintainers:
|
||||||
|
- name: Wildcard
|
||||||
|
email: support@w6d.io
|
||||||
|
appVersion: "1.2.0"
|
||||||
|
version: 0.2.0
|
||||||
22
charts/pgcat/templates/NOTES.txt
Normal file
22
charts/pgcat/templates/NOTES.txt
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
1. Get the application URL by running these commands:
|
||||||
|
{{- if .Values.ingress.enabled }}
|
||||||
|
{{- range $host := .Values.ingress.hosts }}
|
||||||
|
{{- range .paths }}
|
||||||
|
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- else if contains "NodePort" .Values.service.type }}
|
||||||
|
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "pgcat.fullname" . }})
|
||||||
|
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
|
||||||
|
echo http://$NODE_IP:$NODE_PORT
|
||||||
|
{{- else if contains "LoadBalancer" .Values.service.type }}
|
||||||
|
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
|
||||||
|
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "pgcat.fullname" . }}'
|
||||||
|
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "pgcat.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
|
||||||
|
echo http://$SERVICE_IP:{{ .Values.service.port }}
|
||||||
|
{{- else if contains "ClusterIP" .Values.service.type }}
|
||||||
|
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "pgcat.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
||||||
|
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
|
||||||
|
echo "Visit http://127.0.0.1:8080 to use your application"
|
||||||
|
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
|
||||||
|
{{- end }}
|
||||||
3
charts/pgcat/templates/_config.tpl
Normal file
3
charts/pgcat/templates/_config.tpl
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{{/*
|
||||||
|
Configuration template definition
|
||||||
|
*/}}
|
||||||
62
charts/pgcat/templates/_helpers.tpl
Normal file
62
charts/pgcat/templates/_helpers.tpl
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
{{/*
|
||||||
|
Expand the name of the chart.
|
||||||
|
*/}}
|
||||||
|
{{- define "pgcat.name" -}}
|
||||||
|
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Create a default fully qualified app name.
|
||||||
|
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||||
|
If release name contains chart name it will be used as a full name.
|
||||||
|
*/}}
|
||||||
|
{{- define "pgcat.fullname" -}}
|
||||||
|
{{- if .Values.fullnameOverride }}
|
||||||
|
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- else }}
|
||||||
|
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||||
|
{{- if contains $name .Release.Name }}
|
||||||
|
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- else }}
|
||||||
|
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Create chart name and version as used by the chart label.
|
||||||
|
*/}}
|
||||||
|
{{- define "pgcat.chart" -}}
|
||||||
|
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Common labels
|
||||||
|
*/}}
|
||||||
|
{{- define "pgcat.labels" -}}
|
||||||
|
helm.sh/chart: {{ include "pgcat.chart" . }}
|
||||||
|
{{ include "pgcat.selectorLabels" . }}
|
||||||
|
{{- if .Chart.AppVersion }}
|
||||||
|
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||||
|
{{- end }}
|
||||||
|
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Selector labels
|
||||||
|
*/}}
|
||||||
|
{{- define "pgcat.selectorLabels" -}}
|
||||||
|
app.kubernetes.io/name: {{ include "pgcat.name" . }}
|
||||||
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Create the name of the service account to use
|
||||||
|
*/}}
|
||||||
|
{{- define "pgcat.serviceAccountName" -}}
|
||||||
|
{{- if .Values.serviceAccount.create }}
|
||||||
|
{{- default (include "pgcat.fullname" .) .Values.serviceAccount.name }}
|
||||||
|
{{- else }}
|
||||||
|
{{- default "default" .Values.serviceAccount.name }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
66
charts/pgcat/templates/deployment.yaml
Normal file
66
charts/pgcat/templates/deployment.yaml
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: {{ include "pgcat.fullname" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "pgcat.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
replicas: {{ .Values.replicaCount }}
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
{{- include "pgcat.selectorLabels" . | nindent 6 }}
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
|
||||||
|
{{- with .Values.podAnnotations }}
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
labels:
|
||||||
|
{{- include "pgcat.selectorLabels" . | nindent 8 }}
|
||||||
|
spec:
|
||||||
|
{{- with .Values.image.pullSecrets }}
|
||||||
|
imagePullSecrets:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
serviceAccountName: {{ include "pgcat.serviceAccountName" . }}
|
||||||
|
securityContext:
|
||||||
|
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||||
|
containers:
|
||||||
|
- name: {{ .Chart.Name }}
|
||||||
|
securityContext:
|
||||||
|
{{- toYaml .Values.containerSecurityContext | nindent 12 }}
|
||||||
|
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||||
|
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||||
|
ports:
|
||||||
|
- name: pgcat
|
||||||
|
containerPort: {{ .Values.configuration.general.port }}
|
||||||
|
protocol: TCP
|
||||||
|
livenessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: pgcat
|
||||||
|
readinessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: pgcat
|
||||||
|
resources:
|
||||||
|
{{- toYaml .Values.resources | nindent 12 }}
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /etc/pgcat
|
||||||
|
name: config
|
||||||
|
{{- with .Values.nodeSelector }}
|
||||||
|
nodeSelector:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.affinity }}
|
||||||
|
affinity:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.tolerations }}
|
||||||
|
tolerations:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
volumes:
|
||||||
|
- secret:
|
||||||
|
defaultMode: 420
|
||||||
|
secretName: {{ include "pgcat.fullname" . }}
|
||||||
|
name: config
|
||||||
61
charts/pgcat/templates/ingress.yaml
Normal file
61
charts/pgcat/templates/ingress.yaml
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
{{- if .Values.ingress.enabled -}}
|
||||||
|
{{- $fullName := include "pgcat.fullname" . -}}
|
||||||
|
{{- $svcPort := .Values.service.port -}}
|
||||||
|
{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
|
||||||
|
{{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }}
|
||||||
|
{{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||||
|
apiVersion: networking.k8s.io/v1beta1
|
||||||
|
{{- else -}}
|
||||||
|
apiVersion: extensions/v1beta1
|
||||||
|
{{- end }}
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: {{ $fullName }}
|
||||||
|
labels:
|
||||||
|
{{- include "pgcat.labels" . | nindent 4 }}
|
||||||
|
{{- with .Values.ingress.annotations }}
|
||||||
|
annotations:
|
||||||
|
{{- toYaml . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
spec:
|
||||||
|
{{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
|
||||||
|
ingressClassName: {{ .Values.ingress.className }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.ingress.tls }}
|
||||||
|
tls:
|
||||||
|
{{- range .Values.ingress.tls }}
|
||||||
|
- hosts:
|
||||||
|
{{- range .hosts }}
|
||||||
|
- {{ . | quote }}
|
||||||
|
{{- end }}
|
||||||
|
secretName: {{ .secretName }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
rules:
|
||||||
|
{{- range .Values.ingress.hosts }}
|
||||||
|
- host: {{ .host | quote }}
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
{{- range .paths }}
|
||||||
|
- path: {{ .path }}
|
||||||
|
{{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
|
||||||
|
pathType: {{ .pathType }}
|
||||||
|
{{- end }}
|
||||||
|
backend:
|
||||||
|
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||||
|
service:
|
||||||
|
name: {{ $fullName }}
|
||||||
|
port:
|
||||||
|
number: {{ $svcPort }}
|
||||||
|
{{- else }}
|
||||||
|
serviceName: {{ $fullName }}
|
||||||
|
servicePort: {{ $svcPort }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
86
charts/pgcat/templates/secret.yaml
Normal file
86
charts/pgcat/templates/secret.yaml
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: {{ include "pgcat.fullname" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "pgcat.labels" . | nindent 4 }}
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
pgcat.toml: |
|
||||||
|
[general]
|
||||||
|
host = {{ .Values.configuration.general.host | quote }}
|
||||||
|
port = {{ .Values.configuration.general.port }}
|
||||||
|
enable_prometheus_exporter = {{ .Values.configuration.general.enable_prometheus_exporter }}
|
||||||
|
prometheus_exporter_port = {{ .Values.configuration.general.prometheus_exporter_port }}
|
||||||
|
connect_timeout = {{ .Values.configuration.general.connect_timeout }}
|
||||||
|
idle_timeout = {{ .Values.configuration.general.idle_timeout | int }}
|
||||||
|
server_lifetime = {{ .Values.configuration.general.server_lifetime | int }}
|
||||||
|
idle_client_in_transaction_timeout = {{ .Values.configuration.general.idle_client_in_transaction_timeout | int }}
|
||||||
|
healthcheck_timeout = {{ .Values.configuration.general.healthcheck_timeout }}
|
||||||
|
healthcheck_delay = {{ .Values.configuration.general.healthcheck_delay }}
|
||||||
|
shutdown_timeout = {{ .Values.configuration.general.shutdown_timeout }}
|
||||||
|
ban_time = {{ .Values.configuration.general.ban_time }}
|
||||||
|
log_client_connections = {{ .Values.configuration.general.log_client_connections }}
|
||||||
|
log_client_disconnections = {{ .Values.configuration.general.log_client_disconnections }}
|
||||||
|
tcp_keepalives_idle = {{ .Values.configuration.general.tcp_keepalives_idle }}
|
||||||
|
tcp_keepalives_count = {{ .Values.configuration.general.tcp_keepalives_count }}
|
||||||
|
tcp_keepalives_interval = {{ .Values.configuration.general.tcp_keepalives_interval }}
|
||||||
|
{{- if and (ne .Values.configuration.general.tls_certificate "-") (ne .Values.configuration.general.tls_private_key "-") }}
|
||||||
|
tls_certificate = "{{ .Values.configuration.general.tls_certificate }}"
|
||||||
|
tls_private_key = "{{ .Values.configuration.general.tls_private_key }}"
|
||||||
|
{{- end }}
|
||||||
|
admin_username = {{ .Values.configuration.general.admin_username | quote }}
|
||||||
|
admin_password = {{ .Values.configuration.general.admin_password | quote }}
|
||||||
|
{{- if and .Values.configuration.general.auth_query_user .Values.configuration.general.auth_query_password .Values.configuration.general.auth_query }}
|
||||||
|
auth_query = {{ .Values.configuration.general.auth_query | quote }}
|
||||||
|
auth_query_user = {{ .Values.configuration.general.auth_query_user | quote }}
|
||||||
|
auth_query_password = {{ .Values.configuration.general.auth_query_password | quote }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- range $pool := .Values.configuration.pools }}
|
||||||
|
|
||||||
|
##
|
||||||
|
## pool for {{ $pool.name }}
|
||||||
|
##
|
||||||
|
[pools.{{ $pool.name | quote }}]
|
||||||
|
pool_mode = {{ default "transaction" $pool.pool_mode | quote }}
|
||||||
|
load_balancing_mode = {{ default "random" $pool.load_balancing_mode | quote }}
|
||||||
|
default_role = {{ default "any" $pool.default_role | quote }}
|
||||||
|
prepared_statements_cache_size = {{ default 500 $pool.prepared_statements_cache_size }}
|
||||||
|
query_parser_enabled = {{ default true $pool.query_parser_enabled }}
|
||||||
|
query_parser_read_write_splitting = {{ default true $pool.query_parser_read_write_splitting }}
|
||||||
|
primary_reads_enabled = {{ default true $pool.primary_reads_enabled }}
|
||||||
|
sharding_function = {{ default "pg_bigint_hash" $pool.sharding_function | quote }}
|
||||||
|
|
||||||
|
{{- range $index, $user := $pool.users }}
|
||||||
|
|
||||||
|
## pool {{ $pool.name }} user {{ $user.username | quote }}
|
||||||
|
##
|
||||||
|
[pools.{{ $pool.name | quote }}.users.{{ $index }}]
|
||||||
|
username = {{ $user.username | quote }}
|
||||||
|
password = {{ $user.password | quote }}
|
||||||
|
pool_size = {{ $user.pool_size }}
|
||||||
|
statement_timeout = {{ $user.statement_timeout }}
|
||||||
|
min_pool_size = 3
|
||||||
|
server_lifetime = 60000
|
||||||
|
{{- if and $user.server_username $user.server_password }}
|
||||||
|
server_username = {{ $user.server_username | quote }}
|
||||||
|
server_password = {{ $user.server_password | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- range $index, $shard := $pool.shards }}
|
||||||
|
|
||||||
|
## pool {{ $pool.name }} database {{ $shard.database }}
|
||||||
|
##
|
||||||
|
[pools.{{ $pool.name | quote }}.shards.{{ $index }}]
|
||||||
|
{{- if gt (len $shard.servers) 0}}
|
||||||
|
servers = [
|
||||||
|
{{- range $server := $shard.servers }}
|
||||||
|
[ {{ $server.host | quote }}, {{ $server.port }}, {{ $server.role | quote }} ],
|
||||||
|
{{- end }}
|
||||||
|
]
|
||||||
|
{{- end }}
|
||||||
|
database = {{ $shard.database | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
15
charts/pgcat/templates/service.yaml
Normal file
15
charts/pgcat/templates/service.yaml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: {{ include "pgcat.fullname" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "pgcat.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
type: {{ .Values.service.type }}
|
||||||
|
ports:
|
||||||
|
- port: {{ .Values.service.port }}
|
||||||
|
targetPort: pgcat
|
||||||
|
protocol: TCP
|
||||||
|
name: pgcat
|
||||||
|
selector:
|
||||||
|
{{- include "pgcat.selectorLabels" . | nindent 4 }}
|
||||||
12
charts/pgcat/templates/serviceaccount.yaml
Normal file
12
charts/pgcat/templates/serviceaccount.yaml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{{- if .Values.serviceAccount.create -}}
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: {{ include "pgcat.serviceAccountName" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "pgcat.labels" . | nindent 4 }}
|
||||||
|
{{- with .Values.serviceAccount.annotations }}
|
||||||
|
annotations:
|
||||||
|
{{- toYaml . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
369
charts/pgcat/values.yaml
Normal file
369
charts/pgcat/values.yaml
Normal file
@@ -0,0 +1,369 @@
|
|||||||
|
## String to partially override aspnet-core.fullname template (will maintain the release name)
|
||||||
|
## @param nameOverride String to partially override common.names.fullname
|
||||||
|
##
|
||||||
|
nameOverride: ""
|
||||||
|
|
||||||
|
## String to fully override aspnet-core.fullname template
|
||||||
|
## @param fullnameOverride String to fully override common.names.fullname
|
||||||
|
##
|
||||||
|
fullnameOverride: ""
|
||||||
|
|
||||||
|
## Number of PgCat replicas to deploy
|
||||||
|
## @param replicaCount Number of PgCat replicas to deploy
|
||||||
|
replicaCount: 1
|
||||||
|
|
||||||
|
## Bitnami PgCat image version
|
||||||
|
## ref: https://hub.docker.com/r/bitnami/kubewatch/tags/
|
||||||
|
##
|
||||||
|
## @param image.registry PgCat image registry
|
||||||
|
## @param image.repository PgCat image name
|
||||||
|
## @param image.tag PgCat image tag
|
||||||
|
## @param image.pullPolicy PgCat image tag
|
||||||
|
## @param image.pullSecrets Specify docker-registry secret names as an array
|
||||||
|
image:
|
||||||
|
repository: ghcr.io/postgresml/pgcat
|
||||||
|
# Overrides the image tag whose default is the chart appVersion.
|
||||||
|
tag: "main"
|
||||||
|
## Specify a imagePullPolicy
|
||||||
|
## Defaults to 'Always' if image tag is 'latest', else set to 'IfNotPresent'
|
||||||
|
## ref: http://kubernetes.io/docs/user-guide/images/#pre-pulling-images
|
||||||
|
##
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
## Optionally specify an array of imagePullSecrets.
|
||||||
|
## Secrets must be manually created in the namespace.
|
||||||
|
## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
|
||||||
|
## Example:
|
||||||
|
## pullSecrets:
|
||||||
|
## - myRegistryKeySecretName
|
||||||
|
##
|
||||||
|
pullSecrets: []
|
||||||
|
|
||||||
|
## Specifies whether a ServiceAccount should be created
|
||||||
|
##
|
||||||
|
## @param serviceAccount.create Enable the creation of a ServiceAccount for PgCat pods
|
||||||
|
## @param serviceAccount.name Name of the created ServiceAccount
|
||||||
|
##
|
||||||
|
serviceAccount:
|
||||||
|
## Specifies whether a service account should be created
|
||||||
|
create: true
|
||||||
|
## Annotations to add to the service account
|
||||||
|
annotations: {}
|
||||||
|
## The name of the service account to use.
|
||||||
|
## If not set and create is true, a name is generated using the fullname template
|
||||||
|
name: ""
|
||||||
|
|
||||||
|
## Annotations for server pods.
|
||||||
|
## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/
|
||||||
|
##
|
||||||
|
## @param podAnnotations Annotations for PgCat pods
|
||||||
|
##
|
||||||
|
podAnnotations: {}
|
||||||
|
|
||||||
|
## PgCat containers' SecurityContext
|
||||||
|
## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-pod
|
||||||
|
##
|
||||||
|
## @param podSecurityContext.enabled Enabled PgCat pods' Security Context
|
||||||
|
## @param podSecurityContext.fsGroup Set PgCat pod's Security Context fsGroup
|
||||||
|
##
|
||||||
|
podSecurityContext: {}
|
||||||
|
# fsGroup: 2000
|
||||||
|
|
||||||
|
## PgCat pods' Security Context
|
||||||
|
## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-container
|
||||||
|
##
|
||||||
|
## @param containerSecurityContext.enabled Enabled PgCat containers' Security Context
|
||||||
|
## @param containerSecurityContext.runAsUser Set PgCat container's Security Context runAsUser
|
||||||
|
## @param containerSecurityContext.runAsNonRoot Set PgCat container's Security Context runAsNonRoot
|
||||||
|
##
|
||||||
|
containerSecurityContext: {}
|
||||||
|
# capabilities:
|
||||||
|
# drop:
|
||||||
|
# - ALL
|
||||||
|
# readOnlyRootFilesystem: true
|
||||||
|
# runAsNonRoot: true
|
||||||
|
# runAsUser: 1000
|
||||||
|
|
||||||
|
## PgCat service
|
||||||
|
##
|
||||||
|
## @param service.type PgCat service type
|
||||||
|
## @param service.port PgCat service port
|
||||||
|
service:
|
||||||
|
type: ClusterIP
|
||||||
|
port: 6432
|
||||||
|
|
||||||
|
ingress:
|
||||||
|
enabled: false
|
||||||
|
className: ""
|
||||||
|
annotations: {}
|
||||||
|
# kubernetes.io/ingress.class: nginx
|
||||||
|
# kubernetes.io/tls-acme: "true"
|
||||||
|
hosts:
|
||||||
|
- host: chart-example.local
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: ImplementationSpecific
|
||||||
|
tls: []
|
||||||
|
# - secretName: chart-example-tls
|
||||||
|
# hosts:
|
||||||
|
# - chart-example.local
|
||||||
|
|
||||||
|
## PgCat resource requests and limits
|
||||||
|
## ref: http://kubernetes.io/docs/user-guide/compute-resources/
|
||||||
|
##
|
||||||
|
## @skip resources Optional description
|
||||||
|
## @disabled-param resources.limits The resources limits for the PgCat container
|
||||||
|
## @disabled-param resources.requests The requested resources for the PgCat container
|
||||||
|
##
|
||||||
|
resources:
|
||||||
|
# We usually recommend not to specify default resources and to leave this as a conscious
|
||||||
|
# choice for the user. This also increases chances charts run on environments with little
|
||||||
|
# resources, such as Minikube. If you do want to specify resources, uncomment the following
|
||||||
|
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
|
||||||
|
limits: {}
|
||||||
|
# cpu: 100m
|
||||||
|
# memory: 128Mi
|
||||||
|
requests: {}
|
||||||
|
# cpu: 100m
|
||||||
|
# memory: 128Mi
|
||||||
|
|
||||||
|
## Node labels for pod assignment. Evaluated as a template.
|
||||||
|
## ref: https://kubernetes.io/docs/user-guide/node-selection/
|
||||||
|
##
|
||||||
|
## @param nodeSelector Node labels for pod assignment
|
||||||
|
##
|
||||||
|
nodeSelector: {}
|
||||||
|
|
||||||
|
## Tolerations for pod assignment. Evaluated as a template.
|
||||||
|
## ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/
|
||||||
|
##
|
||||||
|
## @param tolerations Tolerations for pod assignment
|
||||||
|
##
|
||||||
|
tolerations: []
|
||||||
|
|
||||||
|
## Affinity for pod assignment. Evaluated as a template.
|
||||||
|
## ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity
|
||||||
|
## Note: podAffinityPreset, podAntiAffinityPreset, and nodeAffinityPreset will be ignored when it's set
|
||||||
|
##
|
||||||
|
## @param affinity Affinity for pod assignment
|
||||||
|
##
|
||||||
|
affinity: {}
|
||||||
|
|
||||||
|
## PgCat configuration
|
||||||
|
## @param configuration [object]
|
||||||
|
configuration:
|
||||||
|
## General pooler settings
|
||||||
|
## @param [object]
|
||||||
|
general:
|
||||||
|
## @param configuration.general.host What IP to run on, 0.0.0.0 means accessible from everywhere.
|
||||||
|
host: "0.0.0.0"
|
||||||
|
|
||||||
|
## @param configuration.general.port Port to run on, same as PgBouncer used in this example.
|
||||||
|
port: 6432
|
||||||
|
|
||||||
|
## @param configuration.general.enable_prometheus_exporter Whether to enable prometheus exporter or not.
|
||||||
|
enable_prometheus_exporter: false
|
||||||
|
|
||||||
|
## @param configuration.general.prometheus_exporter_port Port at which prometheus exporter listens on.
|
||||||
|
prometheus_exporter_port: 9930
|
||||||
|
|
||||||
|
# @param configuration.general.connect_timeout How long to wait before aborting a server connection (ms).
|
||||||
|
connect_timeout: 5000
|
||||||
|
|
||||||
|
# How long an idle connection with a server is left open (ms).
|
||||||
|
idle_timeout: 30000 # milliseconds
|
||||||
|
|
||||||
|
# Max connection lifetime before it's closed, even if actively used.
|
||||||
|
server_lifetime: 86400000 # 24 hours
|
||||||
|
|
||||||
|
# How long a client is allowed to be idle while in a transaction (ms).
|
||||||
|
idle_client_in_transaction_timeout: 0 # milliseconds
|
||||||
|
|
||||||
|
# @param configuration.general.healthcheck_timeout How much time to give `SELECT 1` health check query to return with a result (ms).
|
||||||
|
healthcheck_timeout: 1000
|
||||||
|
|
||||||
|
# @param configuration.general.healthcheck_delay How long to keep connection available for immediate re-use, without running a healthcheck query on it
|
||||||
|
healthcheck_delay: 30000
|
||||||
|
|
||||||
|
# @param configuration.general.shutdown_timeout How much time to give clients during shutdown before forcibly killing client connections (ms).
|
||||||
|
shutdown_timeout: 60000
|
||||||
|
|
||||||
|
# @param configuration.general.ban_time For how long to ban a server if it fails a health check (seconds).
|
||||||
|
ban_time: 60 # seconds
|
||||||
|
|
||||||
|
# @param configuration.general.log_client_connections If we should log client connections
|
||||||
|
log_client_connections: false
|
||||||
|
|
||||||
|
# @param configuration.general.log_client_disconnections If we should log client disconnections
|
||||||
|
log_client_disconnections: false
|
||||||
|
|
||||||
|
# TLS
|
||||||
|
# tls_certificate: "server.cert"
|
||||||
|
# tls_private_key: "server.key"
|
||||||
|
tls_certificate: "-"
|
||||||
|
tls_private_key: "-"
|
||||||
|
|
||||||
|
# Credentials to access the virtual administrative database (pgbouncer or pgcat)
|
||||||
|
# Connecting to that database allows running commands like `SHOW POOLS`, `SHOW DATABASES`, etc..
|
||||||
|
admin_username: "postgres"
|
||||||
|
admin_password: "postgres"
|
||||||
|
|
||||||
|
# Query to be sent to servers to obtain the hash used for md5 authentication. The connection will be
|
||||||
|
# established using the database configured in the pool. This parameter is inherited by every pool and
|
||||||
|
# can be redefined in pool configuration.
|
||||||
|
auth_query: null
|
||||||
|
|
||||||
|
# User to be used for connecting to servers to obtain the hash used for md5 authentication by sending
|
||||||
|
# the query specified in auth_query_user. The connection will be established using the database configured
|
||||||
|
# in the pool. This parameter is inherited by every pool and can be redefined in pool configuration.
|
||||||
|
#
|
||||||
|
# @param configuration.general.auth_query_user
|
||||||
|
auth_query_user: null
|
||||||
|
|
||||||
|
# Password to be used for connecting to servers to obtain the hash used for md5 authentication by sending
|
||||||
|
# the query specified in auth_query_user. The connection will be established using the database configured
|
||||||
|
# in the pool. This parameter is inherited by every pool and can be redefined in pool configuration.
|
||||||
|
#
|
||||||
|
# @param configuration.general.auth_query_password
|
||||||
|
auth_query_password: null
|
||||||
|
|
||||||
|
# Number of seconds of connection idleness to wait before sending a keepalive packet to the server.
|
||||||
|
tcp_keepalives_idle: 5
|
||||||
|
|
||||||
|
# Number of unacknowledged keepalive packets allowed before giving up and closing the connection.
|
||||||
|
tcp_keepalives_count: 5
|
||||||
|
|
||||||
|
# Number of seconds between keepalive packets.
|
||||||
|
tcp_keepalives_interval: 5
|
||||||
|
|
||||||
|
## pool
|
||||||
|
## configs are structured as pool.<pool_name>
|
||||||
|
## the pool_name is what clients use as database name when connecting
|
||||||
|
## For the example below a client can connect using "postgres://sharding_user:sharding_user@pgcat_host:pgcat_port/sharded"
|
||||||
|
## @param [object]
|
||||||
|
pools:
|
||||||
|
[{
|
||||||
|
name: "simple", pool_mode: "transaction",
|
||||||
|
users: [{username: "user", password: "pass", pool_size: 5, statement_timeout: 0}],
|
||||||
|
shards: [{
|
||||||
|
servers: [{host: "postgres", port: 5432, role: "primary"}],
|
||||||
|
database: "postgres"
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
# - ## default values
|
||||||
|
# ##
|
||||||
|
# ##
|
||||||
|
# ##
|
||||||
|
# name: "db"
|
||||||
|
|
||||||
|
# ## Pool mode (see PgBouncer docs for more).
|
||||||
|
# ## session: one server connection per connected client
|
||||||
|
# ## transaction: one server connection per client transaction
|
||||||
|
# ## @param configuration.poolsPostgres.pool_mode
|
||||||
|
# pool_mode: "transaction"
|
||||||
|
|
||||||
|
# ## Load balancing mode
|
||||||
|
# ## `random` selects the server at random
|
||||||
|
# ## `loc` selects the server with the least outstanding busy connections
|
||||||
|
# ##
|
||||||
|
# ## @param configuration.poolsPostgres.load_balancing_mode
|
||||||
|
# load_balancing_mode: "random"
|
||||||
|
|
||||||
|
# ## Prepared statements cache size.
|
||||||
|
# ## TODO: update documentation
|
||||||
|
# ##
|
||||||
|
# ## @param configuration.poolsPostgres.prepared_statements_cache_size
|
||||||
|
# prepared_statements_cache_size: 500
|
||||||
|
|
||||||
|
# ## If the client doesn't specify, route traffic to
|
||||||
|
# ## this role by default.
|
||||||
|
# ##
|
||||||
|
# ## any: round-robin between primary and replicas,
|
||||||
|
# ## replica: round-robin between replicas only without touching the primary,
|
||||||
|
# ## primary: all queries go to the primary unless otherwise specified.
|
||||||
|
# ## @param configuration.poolsPostgres.default_role
|
||||||
|
# default_role: "any"
|
||||||
|
|
||||||
|
# ## Query parser. If enabled, we'll attempt to parse
|
||||||
|
# ## every incoming query to determine if it's a read or a write.
|
||||||
|
# ## If it's a read query, we'll direct it to a replica. Otherwise, if it's a write,
|
||||||
|
# ## we'll direct it to the primary.
|
||||||
|
# ## @param configuration.poolsPostgres.query_parser_enabled
|
||||||
|
# query_parser_enabled: true
|
||||||
|
|
||||||
|
# ## If the query parser is enabled and this setting is enabled, we'll attempt to
|
||||||
|
# ## infer the role from the query itself.
|
||||||
|
# ## @param configuration.poolsPostgres.query_parser_read_write_splitting
|
||||||
|
# query_parser_read_write_splitting: true
|
||||||
|
|
||||||
|
# ## If the query parser is enabled and this setting is enabled, the primary will be part of the pool of databases used for
|
||||||
|
# ## load balancing of read queries. Otherwise, the primary will only be used for write
|
||||||
|
# ## queries. The primary can always be explicitly selected with our custom protocol.
|
||||||
|
# ## @param configuration.poolsPostgres.primary_reads_enabled
|
||||||
|
# primary_reads_enabled: true
|
||||||
|
|
||||||
|
# ## So what if you wanted to implement a different hashing function,
|
||||||
|
# ## or you've already built one and you want this pooler to use it?
|
||||||
|
# ##
|
||||||
|
# ## Current options:
|
||||||
|
# ##
|
||||||
|
# ## pg_bigint_hash: PARTITION BY HASH (Postgres hashing function)
|
||||||
|
# ## sha1: A hashing function based on SHA1
|
||||||
|
# ##
|
||||||
|
# ## @param configuration.poolsPostgres.sharding_function
|
||||||
|
# sharding_function: "pg_bigint_hash"
|
||||||
|
|
||||||
|
# ## Credentials for users that may connect to this cluster
|
||||||
|
# ## @param users [array]
|
||||||
|
# ## @param users[0].username Name of the env var (required)
|
||||||
|
# ## @param users[0].password Value for the env var (required)
|
||||||
|
# ## @param users[0].pool_size Maximum number of server connections that can be established for this user
|
||||||
|
# ## @param users[0].statement_timeout Maximum query duration. Dangerous, but protects against DBs that died in a non-obvious way.
|
||||||
|
# users: []
|
||||||
|
# # - username: "user"
|
||||||
|
# # password: "pass"
|
||||||
|
# #
|
||||||
|
# # # The maximum number of connection from a single Pgcat process to any database in the cluster
|
||||||
|
# # # is the sum of pool_size across all users.
|
||||||
|
# # pool_size: 9
|
||||||
|
# #
|
||||||
|
# # # Maximum query duration. Dangerous, but protects against DBs that died in a non-obvious way.
|
||||||
|
# # statement_timeout: 0
|
||||||
|
# #
|
||||||
|
# # # PostgreSQL username used to connect to the server.
|
||||||
|
# # server_username: "postgres
|
||||||
|
# #
|
||||||
|
# # # PostgreSQL password used to connect to the server.
|
||||||
|
# # server_password: "postgres
|
||||||
|
|
||||||
|
# ## @param shards [array]
|
||||||
|
# ## @param shards[0].server[0].host Host for this shard
|
||||||
|
# ## @param shards[0].server[0].port Port for this shard
|
||||||
|
# ## @param shards[0].server[0].role Role for this shard
|
||||||
|
# shards: []
|
||||||
|
# # [ host, port, role ]
|
||||||
|
# # - servers:
|
||||||
|
# # - host: "postgres"
|
||||||
|
# # port: 5432
|
||||||
|
# # role: "primary"
|
||||||
|
# # - host: "postgres"
|
||||||
|
# # port: 5432
|
||||||
|
# # role: "replica"
|
||||||
|
# # database: "postgres"
|
||||||
|
# # # [ host, port, role ]
|
||||||
|
# # - servers:
|
||||||
|
# # - host: "postgres"
|
||||||
|
# # port: 5432
|
||||||
|
# # role: "primary"
|
||||||
|
# # - host: "postgres"
|
||||||
|
# # port: 5432
|
||||||
|
# # role: "replica"
|
||||||
|
# # database: "postgres"
|
||||||
|
# # # [ host, port, role ]
|
||||||
|
# # - servers:
|
||||||
|
# # - host: "postgres"
|
||||||
|
# # port: 5432
|
||||||
|
# # role: "primary"
|
||||||
|
# # - host: "postgres"
|
||||||
|
# # port: 5432
|
||||||
|
# # role: "replica"
|
||||||
|
# # database: "postgres"
|
||||||
9
control
Normal file
9
control
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
Package: pgcat
|
||||||
|
Version: ${PACKAGE_VERSION}
|
||||||
|
Section: database
|
||||||
|
Priority: optional
|
||||||
|
Architecture: ${ARCH}
|
||||||
|
Maintainer: PostgresML <team@postgresml.org>
|
||||||
|
Homepage: https://postgresml.org
|
||||||
|
Description: PgCat - NextGen PostgreSQL Pooler
|
||||||
|
PostgreSQL pooler and proxy (like PgBouncer) with support for sharding, load balancing, failover and mirroring.
|
||||||
5
ct.yaml
Normal file
5
ct.yaml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
remote: origin
|
||||||
|
target-branch: main
|
||||||
|
chart-dirs:
|
||||||
|
- charts
|
||||||
|
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
FROM rust:1.70-bullseye
|
FROM rust:bullseye
|
||||||
|
|
||||||
# Dependencies
|
# Dependencies
|
||||||
|
COPY --from=sclevine/yj /bin/yj /bin/yj
|
||||||
|
RUN /bin/yj -h
|
||||||
RUN apt-get update -y \
|
RUN apt-get update -y \
|
||||||
&& apt-get install -y \
|
&& apt-get install -y \
|
||||||
llvm-11 psmisc postgresql-contrib postgresql-client \
|
llvm-11 psmisc postgresql-contrib postgresql-client \
|
||||||
|
|||||||
@@ -71,6 +71,10 @@ default_role = "any"
|
|||||||
# we'll direct it to the primary.
|
# we'll direct it to the primary.
|
||||||
query_parser_enabled = true
|
query_parser_enabled = true
|
||||||
|
|
||||||
|
# If the query parser is enabled and this setting is enabled, we'll attempt to
|
||||||
|
# infer the role from the query itself.
|
||||||
|
query_parser_read_write_splitting = true
|
||||||
|
|
||||||
# If the query parser is enabled and this setting is enabled, the primary will be part of the pool of databases used for
|
# 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 explicitly selected with our custom protocol.
|
||||||
|
|||||||
2124
grafana_dashboard.json
Normal file
2124
grafana_dashboard.json
Normal file
File diff suppressed because it is too large
Load Diff
17
pgcat.service
Normal file
17
pgcat.service
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=PgCat pooler
|
||||||
|
After=network.target
|
||||||
|
StartLimitIntervalSec=0
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=pgcat
|
||||||
|
Type=simple
|
||||||
|
Restart=always
|
||||||
|
RestartSec=1
|
||||||
|
Environment=RUST_LOG=info
|
||||||
|
LimitNOFILE=65536
|
||||||
|
ExecStart=/usr/bin/pgcat /etc/pgcat.toml
|
||||||
|
ExecReload=/bin/kill -SIGHUP $MAINPID
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
24
pgcat.toml
24
pgcat.toml
@@ -60,12 +60,6 @@ tcp_keepalives_count = 5
|
|||||||
# Number of seconds between keepalive packets.
|
# Number of seconds between keepalive packets.
|
||||||
tcp_keepalives_interval = 5
|
tcp_keepalives_interval = 5
|
||||||
|
|
||||||
# Handle prepared statements.
|
|
||||||
prepared_statements = true
|
|
||||||
|
|
||||||
# Prepared statements server cache size.
|
|
||||||
prepared_statements_cache_size = 500
|
|
||||||
|
|
||||||
# Path to TLS Certificate file to use for TLS connections
|
# Path to TLS Certificate file to use for TLS connections
|
||||||
# tls_certificate = ".circleci/server.cert"
|
# tls_certificate = ".circleci/server.cert"
|
||||||
# Path to TLS private key file to use for TLS connections
|
# Path to TLS private key file to use for TLS connections
|
||||||
@@ -156,12 +150,20 @@ load_balancing_mode = "random"
|
|||||||
# `primary` all queries go to the primary unless otherwise specified.
|
# `primary` all queries go to the primary unless otherwise specified.
|
||||||
default_role = "any"
|
default_role = "any"
|
||||||
|
|
||||||
|
# Prepared statements cache size.
|
||||||
|
# TODO: update documentation
|
||||||
|
prepared_statements_cache_size = 500
|
||||||
|
|
||||||
# If Query Parser is enabled, we'll attempt to parse
|
# If Query Parser is 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.
|
||||||
query_parser_enabled = true
|
query_parser_enabled = true
|
||||||
|
|
||||||
|
# If the query parser is enabled and this setting is enabled, we'll attempt to
|
||||||
|
# infer the role from the query itself.
|
||||||
|
query_parser_read_write_splitting = true
|
||||||
|
|
||||||
# If the query parser is enabled and this setting is enabled, the primary will be part of the pool of databases used for
|
# 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 explicitly selected with our custom protocol.
|
||||||
@@ -173,6 +175,12 @@ primary_reads_enabled = true
|
|||||||
# shard_id_regex = '/\* shard_id: (\d+) \*/'
|
# shard_id_regex = '/\* shard_id: (\d+) \*/'
|
||||||
# regex_search_limit = 1000 # only look at the first 1000 characters of SQL statements
|
# regex_search_limit = 1000 # only look at the first 1000 characters of SQL statements
|
||||||
|
|
||||||
|
# Defines the behavior when no shard is selected in a sharded system.
|
||||||
|
# `random`: picks a shard at random
|
||||||
|
# `random_healthy`: picks a shard at random favoring shards with the least number of recent errors
|
||||||
|
# `shard_<number>`: e.g. shard_0, shard_4, etc. picks a specific shard, everytime
|
||||||
|
# no_shard_specified_behavior = "shard_0"
|
||||||
|
|
||||||
# So what if you wanted to implement a different hashing function,
|
# 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:
|
||||||
@@ -183,7 +191,7 @@ sharding_function = "pg_bigint_hash"
|
|||||||
# Query to be sent to servers to obtain the hash used for md5 authentication. The connection will be
|
# 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
|
# established using the database configured in the pool. This parameter is inherited by every pool
|
||||||
# and can be redefined in pool configuration.
|
# and can be redefined in pool configuration.
|
||||||
# auth_query = "SELECT $1"
|
# auth_query="SELECT usename, passwd FROM pg_shadow WHERE usename='$1'"
|
||||||
|
|
||||||
# User to be used for connecting to servers to obtain the hash used for md5 authentication by sending the query
|
# 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.
|
# specified in `auth_query_user`. The connection will be established using the database configured in the pool.
|
||||||
@@ -293,6 +301,8 @@ username = "other_user"
|
|||||||
password = "other_user"
|
password = "other_user"
|
||||||
pool_size = 21
|
pool_size = 21
|
||||||
statement_timeout = 15000
|
statement_timeout = 15000
|
||||||
|
connect_timeout = 1000
|
||||||
|
idle_timeout = 1000
|
||||||
|
|
||||||
# Shard configs are structured as pool.<pool_name>.shards.<shard_id>
|
# Shard configs are structured as pool.<pool_name>.shards.<shard_id>
|
||||||
# Each shard config contains a list of servers that make up the shard
|
# Each shard config contains a list of servers that make up the shard
|
||||||
|
|||||||
13
postinst
Normal file
13
postinst
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable pgcat
|
||||||
|
|
||||||
|
if ! id pgcat 2> /dev/null; then
|
||||||
|
useradd -s /usr/bin/false pgcat
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f /etc/pgcat.toml ]; then
|
||||||
|
systemctl start pgcat
|
||||||
|
fi
|
||||||
5
prerm
Normal file
5
prerm
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
systemctl stop pgcat
|
||||||
|
systemctl disable pgcat
|
||||||
216
src/admin.rs
216
src/admin.rs
@@ -1,4 +1,5 @@
|
|||||||
use crate::pool::BanReason;
|
use crate::pool::BanReason;
|
||||||
|
use crate::server::ServerParameters;
|
||||||
use crate::stats::pool::PoolStats;
|
use crate::stats::pool::PoolStats;
|
||||||
use bytes::{Buf, BufMut, BytesMut};
|
use bytes::{Buf, BufMut, BytesMut};
|
||||||
use log::{error, info, trace};
|
use log::{error, info, trace};
|
||||||
@@ -17,16 +18,16 @@ use crate::pool::ClientServerMap;
|
|||||||
use crate::pool::{get_all_pools, get_pool};
|
use crate::pool::{get_all_pools, get_pool};
|
||||||
use crate::stats::{get_client_stats, get_server_stats, ClientState, ServerState};
|
use crate::stats::{get_client_stats, get_server_stats, ClientState, ServerState};
|
||||||
|
|
||||||
pub fn generate_server_info_for_admin() -> BytesMut {
|
pub fn generate_server_parameters_for_admin() -> ServerParameters {
|
||||||
let mut server_info = BytesMut::new();
|
let mut server_parameters = ServerParameters::new();
|
||||||
|
|
||||||
server_info.put(server_parameter_message("application_name", ""));
|
server_parameters.set_param("application_name".to_string(), "".to_string(), true);
|
||||||
server_info.put(server_parameter_message("client_encoding", "UTF8"));
|
server_parameters.set_param("client_encoding".to_string(), "UTF8".to_string(), true);
|
||||||
server_info.put(server_parameter_message("server_encoding", "UTF8"));
|
server_parameters.set_param("server_encoding".to_string(), "UTF8".to_string(), true);
|
||||||
server_info.put(server_parameter_message("server_version", VERSION));
|
server_parameters.set_param("server_version".to_string(), VERSION.to_string(), true);
|
||||||
server_info.put(server_parameter_message("DateStyle", "ISO, MDY"));
|
server_parameters.set_param("DateStyle".to_string(), "ISO, MDY".to_string(), true);
|
||||||
|
|
||||||
server_info
|
server_parameters
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle admin client.
|
/// Handle admin client.
|
||||||
@@ -54,7 +55,12 @@ where
|
|||||||
|
|
||||||
let query_parts: Vec<&str> = query.trim_end_matches(';').split_whitespace().collect();
|
let query_parts: Vec<&str> = query.trim_end_matches(';').split_whitespace().collect();
|
||||||
|
|
||||||
match query_parts[0].to_ascii_uppercase().as_str() {
|
match query_parts
|
||||||
|
.first()
|
||||||
|
.unwrap_or(&"")
|
||||||
|
.to_ascii_uppercase()
|
||||||
|
.as_str()
|
||||||
|
{
|
||||||
"BAN" => {
|
"BAN" => {
|
||||||
trace!("BAN");
|
trace!("BAN");
|
||||||
ban(stream, query_parts).await
|
ban(stream, query_parts).await
|
||||||
@@ -73,17 +79,22 @@ where
|
|||||||
}
|
}
|
||||||
"PAUSE" => {
|
"PAUSE" => {
|
||||||
trace!("PAUSE");
|
trace!("PAUSE");
|
||||||
pause(stream, query_parts[1]).await
|
pause(stream, query_parts).await
|
||||||
}
|
}
|
||||||
"RESUME" => {
|
"RESUME" => {
|
||||||
trace!("RESUME");
|
trace!("RESUME");
|
||||||
resume(stream, query_parts[1]).await
|
resume(stream, query_parts).await
|
||||||
}
|
}
|
||||||
"SHUTDOWN" => {
|
"SHUTDOWN" => {
|
||||||
trace!("SHUTDOWN");
|
trace!("SHUTDOWN");
|
||||||
shutdown(stream).await
|
shutdown(stream).await
|
||||||
}
|
}
|
||||||
"SHOW" => match query_parts[1].to_ascii_uppercase().as_str() {
|
"SHOW" => match query_parts
|
||||||
|
.get(1)
|
||||||
|
.unwrap_or(&"")
|
||||||
|
.to_ascii_uppercase()
|
||||||
|
.as_str()
|
||||||
|
{
|
||||||
"HELP" => {
|
"HELP" => {
|
||||||
trace!("SHOW HELP");
|
trace!("SHOW HELP");
|
||||||
show_help(stream).await
|
show_help(stream).await
|
||||||
@@ -282,7 +293,7 @@ where
|
|||||||
{
|
{
|
||||||
let mut res = BytesMut::new();
|
let mut res = BytesMut::new();
|
||||||
|
|
||||||
let detail_msg = vec![
|
let detail_msg = [
|
||||||
"",
|
"",
|
||||||
"SHOW HELP|CONFIG|DATABASES|POOLS|CLIENTS|SERVERS|USERS|VERSION",
|
"SHOW HELP|CONFIG|DATABASES|POOLS|CLIENTS|SERVERS|USERS|VERSION",
|
||||||
// "SHOW PEERS|PEER_POOLS", // missing PEERS|PEER_POOLS
|
// "SHOW PEERS|PEER_POOLS", // missing PEERS|PEER_POOLS
|
||||||
@@ -300,7 +311,6 @@ where
|
|||||||
// "KILL <db>",
|
// "KILL <db>",
|
||||||
// "SUSPEND",
|
// "SUSPEND",
|
||||||
"SHUTDOWN",
|
"SHUTDOWN",
|
||||||
// "WAIT_CLOSE [<db>]", // missing
|
|
||||||
];
|
];
|
||||||
|
|
||||||
res.put(notify("Console usage", detail_msg.join("\n\t")));
|
res.put(notify("Console usage", detail_msg.join("\n\t")));
|
||||||
@@ -690,6 +700,8 @@ where
|
|||||||
("query_count", DataType::Numeric),
|
("query_count", DataType::Numeric),
|
||||||
("error_count", DataType::Numeric),
|
("error_count", DataType::Numeric),
|
||||||
("age_seconds", DataType::Numeric),
|
("age_seconds", DataType::Numeric),
|
||||||
|
("maxwait", DataType::Numeric),
|
||||||
|
("maxwait_us", DataType::Numeric),
|
||||||
];
|
];
|
||||||
|
|
||||||
let new_map = get_client_stats();
|
let new_map = get_client_stats();
|
||||||
@@ -697,6 +709,7 @@ where
|
|||||||
res.put(row_description(&columns));
|
res.put(row_description(&columns));
|
||||||
|
|
||||||
for (_, client) in new_map {
|
for (_, client) in new_map {
|
||||||
|
let max_wait = client.max_wait_time.load(Ordering::Relaxed);
|
||||||
let row = vec![
|
let row = vec![
|
||||||
format!("{:#010X}", client.client_id()),
|
format!("{:#010X}", client.client_id()),
|
||||||
client.pool_name(),
|
client.pool_name(),
|
||||||
@@ -710,6 +723,8 @@ where
|
|||||||
.duration_since(client.connect_time())
|
.duration_since(client.connect_time())
|
||||||
.as_secs()
|
.as_secs()
|
||||||
.to_string(),
|
.to_string(),
|
||||||
|
(max_wait / 1_000_000).to_string(),
|
||||||
|
(max_wait % 1_000_000).to_string(),
|
||||||
];
|
];
|
||||||
|
|
||||||
res.put(data_row(&row));
|
res.put(data_row(&row));
|
||||||
@@ -744,6 +759,7 @@ where
|
|||||||
("age_seconds", DataType::Numeric),
|
("age_seconds", DataType::Numeric),
|
||||||
("prepare_cache_hit", DataType::Numeric),
|
("prepare_cache_hit", DataType::Numeric),
|
||||||
("prepare_cache_miss", DataType::Numeric),
|
("prepare_cache_miss", DataType::Numeric),
|
||||||
|
("prepare_cache_eviction", DataType::Numeric),
|
||||||
("prepare_cache_size", DataType::Numeric),
|
("prepare_cache_size", DataType::Numeric),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -776,6 +792,10 @@ where
|
|||||||
.prepared_miss_count
|
.prepared_miss_count
|
||||||
.load(Ordering::Relaxed)
|
.load(Ordering::Relaxed)
|
||||||
.to_string(),
|
.to_string(),
|
||||||
|
server
|
||||||
|
.prepared_eviction_count
|
||||||
|
.load(Ordering::Relaxed)
|
||||||
|
.to_string(),
|
||||||
server
|
server
|
||||||
.prepared_cache_size
|
.prepared_cache_size
|
||||||
.load(Ordering::Relaxed)
|
.load(Ordering::Relaxed)
|
||||||
@@ -796,96 +816,128 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Pause a pool. It won't pass any more queries to the backends.
|
/// Pause a pool. It won't pass any more queries to the backends.
|
||||||
async fn pause<T>(stream: &mut T, query: &str) -> Result<(), Error>
|
async fn pause<T>(stream: &mut T, tokens: Vec<&str>) -> Result<(), Error>
|
||||||
where
|
where
|
||||||
T: tokio::io::AsyncWrite + std::marker::Unpin,
|
T: tokio::io::AsyncWrite + std::marker::Unpin,
|
||||||
{
|
{
|
||||||
let parts: Vec<&str> = query.split(",").map(|part| part.trim()).collect();
|
let parts: Vec<&str> = match tokens.len() == 2 {
|
||||||
|
true => tokens[1].split(',').map(|part| part.trim()).collect(),
|
||||||
|
false => Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
if parts.len() != 2 {
|
match parts.len() {
|
||||||
error_response(
|
0 => {
|
||||||
stream,
|
for (_, pool) in get_all_pools() {
|
||||||
"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();
|
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 => {
|
let mut res = BytesMut::new();
|
||||||
error_response(
|
|
||||||
stream,
|
res.put(command_complete("PAUSE"));
|
||||||
&format!(
|
|
||||||
"No pool configured for database: {}, user: {}",
|
// ReadyForQuery
|
||||||
database, user
|
res.put_u8(b'Z');
|
||||||
),
|
res.put_i32(5);
|
||||||
)
|
res.put_u8(b'I');
|
||||||
.await
|
|
||||||
|
write_all_half(stream, &res).await
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
let database = parts[0];
|
||||||
|
let user = parts[1];
|
||||||
|
|
||||||
|
match get_pool(database, user) {
|
||||||
|
Some(pool) => {
|
||||||
|
pool.pause();
|
||||||
|
|
||||||
|
let mut res = BytesMut::new();
|
||||||
|
|
||||||
|
res.put(command_complete(&format!("PAUSE {},{}", database, user)));
|
||||||
|
|
||||||
|
// ReadyForQuery
|
||||||
|
res.put_u8(b'Z');
|
||||||
|
res.put_i32(5);
|
||||||
|
res.put_u8(b'I');
|
||||||
|
|
||||||
|
write_all_half(stream, &res).await
|
||||||
|
}
|
||||||
|
|
||||||
|
None => {
|
||||||
|
error_response(
|
||||||
|
stream,
|
||||||
|
&format!(
|
||||||
|
"No pool configured for database: {}, user: {}",
|
||||||
|
database, user
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
_ => error_response(stream, "usage: PAUSE [db, user]").await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resume a pool. Queries are allowed again.
|
/// Resume a pool. Queries are allowed again.
|
||||||
async fn resume<T>(stream: &mut T, query: &str) -> Result<(), Error>
|
async fn resume<T>(stream: &mut T, tokens: Vec<&str>) -> Result<(), Error>
|
||||||
where
|
where
|
||||||
T: tokio::io::AsyncWrite + std::marker::Unpin,
|
T: tokio::io::AsyncWrite + std::marker::Unpin,
|
||||||
{
|
{
|
||||||
let parts: Vec<&str> = query.split(",").map(|part| part.trim()).collect();
|
let parts: Vec<&str> = match tokens.len() == 2 {
|
||||||
|
true => tokens[1].split(',').map(|part| part.trim()).collect(),
|
||||||
|
false => Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
if parts.len() != 2 {
|
match parts.len() {
|
||||||
error_response(
|
0 => {
|
||||||
stream,
|
for (_, pool) in get_all_pools() {
|
||||||
"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();
|
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 => {
|
let mut res = BytesMut::new();
|
||||||
error_response(
|
|
||||||
stream,
|
res.put(command_complete("RESUME"));
|
||||||
&format!(
|
|
||||||
"No pool configured for database: {}, user: {}",
|
// ReadyForQuery
|
||||||
database, user
|
res.put_u8(b'Z');
|
||||||
),
|
res.put_i32(5);
|
||||||
)
|
res.put_u8(b'I');
|
||||||
.await
|
|
||||||
|
write_all_half(stream, &res).await
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
let database = parts[0];
|
||||||
|
let user = parts[1];
|
||||||
|
|
||||||
|
match get_pool(database, user) {
|
||||||
|
Some(pool) => {
|
||||||
|
pool.resume();
|
||||||
|
|
||||||
|
let mut res = BytesMut::new();
|
||||||
|
|
||||||
|
res.put(command_complete(&format!("RESUME {},{}", database, user)));
|
||||||
|
|
||||||
|
// ReadyForQuery
|
||||||
|
res.put_u8(b'Z');
|
||||||
|
res.put_i32(5);
|
||||||
|
res.put_u8(b'I');
|
||||||
|
|
||||||
|
write_all_half(stream, &res).await
|
||||||
|
}
|
||||||
|
|
||||||
|
None => {
|
||||||
|
error_response(
|
||||||
|
stream,
|
||||||
|
&format!(
|
||||||
|
"No pool configured for database: {}, user: {}",
|
||||||
|
database, user
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
_ => error_response(stream, "usage: RESUME [db, user]").await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::config::AuthType;
|
||||||
use crate::errors::Error;
|
use crate::errors::Error;
|
||||||
use crate::pool::ConnectionPool;
|
use crate::pool::ConnectionPool;
|
||||||
use crate::server::Server;
|
use crate::server::Server;
|
||||||
@@ -71,6 +72,7 @@ impl AuthPassthrough {
|
|||||||
pub async fn fetch_hash(&self, address: &crate::config::Address) -> Result<String, Error> {
|
pub async fn fetch_hash(&self, address: &crate::config::Address) -> Result<String, Error> {
|
||||||
let auth_user = crate::config::User {
|
let auth_user = crate::config::User {
|
||||||
username: self.user.clone(),
|
username: self.user.clone(),
|
||||||
|
auth_type: AuthType::MD5,
|
||||||
password: Some(self.password.clone()),
|
password: Some(self.password.clone()),
|
||||||
server_username: None,
|
server_username: None,
|
||||||
server_password: None,
|
server_password: None,
|
||||||
@@ -79,6 +81,8 @@ impl AuthPassthrough {
|
|||||||
pool_mode: None,
|
pool_mode: None,
|
||||||
server_lifetime: None,
|
server_lifetime: None,
|
||||||
min_pool_size: None,
|
min_pool_size: None,
|
||||||
|
connect_timeout: None,
|
||||||
|
idle_timeout: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let user = &address.username;
|
let user = &address.username;
|
||||||
|
|||||||
1498
src/client.rs
1498
src/client.rs
File diff suppressed because it is too large
Load Diff
@@ -25,7 +25,7 @@ pub struct Args {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse() -> Args {
|
pub fn parse() -> Args {
|
||||||
return Args::parse();
|
Args::parse()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(ValueEnum, Clone, Debug)]
|
#[derive(ValueEnum, Clone, Debug)]
|
||||||
|
|||||||
428
src/config.rs
428
src/config.rs
@@ -3,11 +3,14 @@ use arc_swap::ArcSwap;
|
|||||||
use log::{error, info};
|
use log::{error, info};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
use serde::{Deserializer, Serializer};
|
||||||
use serde_derive::{Deserialize, Serialize};
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
|
||||||
use std::collections::hash_map::DefaultHasher;
|
use std::collections::hash_map::DefaultHasher;
|
||||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::fs::File;
|
use tokio::fs::File;
|
||||||
use tokio::io::AsyncReadExt;
|
use tokio::io::AsyncReadExt;
|
||||||
@@ -35,12 +38,12 @@ pub enum Role {
|
|||||||
Mirror,
|
Mirror,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToString for Role {
|
impl std::fmt::Display for Role {
|
||||||
fn to_string(&self) -> String {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match *self {
|
match self {
|
||||||
Role::Primary => "primary".to_string(),
|
Role::Primary => write!(f, "primary"),
|
||||||
Role::Replica => "replica".to_string(),
|
Role::Replica => write!(f, "replica"),
|
||||||
Role::Mirror => "mirror".to_string(),
|
Role::Mirror => write!(f, "mirror"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -101,6 +104,9 @@ pub struct Address {
|
|||||||
|
|
||||||
/// Address stats
|
/// Address stats
|
||||||
pub stats: Arc<AddressStats>,
|
pub stats: Arc<AddressStats>,
|
||||||
|
|
||||||
|
/// Number of errors encountered since last successful checkout
|
||||||
|
pub error_count: Arc<AtomicU64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Address {
|
impl Default for Address {
|
||||||
@@ -110,14 +116,15 @@ impl Default for Address {
|
|||||||
host: String::from("127.0.0.1"),
|
host: String::from("127.0.0.1"),
|
||||||
port: 5432,
|
port: 5432,
|
||||||
shard: 0,
|
shard: 0,
|
||||||
address_index: 0,
|
|
||||||
replica_number: 0,
|
|
||||||
database: String::from("database"),
|
database: String::from("database"),
|
||||||
role: Role::Replica,
|
role: Role::Replica,
|
||||||
|
replica_number: 0,
|
||||||
|
address_index: 0,
|
||||||
username: String::from("username"),
|
username: String::from("username"),
|
||||||
pool_name: String::from("pool_name"),
|
pool_name: String::from("pool_name"),
|
||||||
mirrors: Vec::new(),
|
mirrors: Vec::new(),
|
||||||
stats: Arc::new(AddressStats::default()),
|
stats: Arc::new(AddressStats::default()),
|
||||||
|
error_count: Arc::new(AtomicU64::new(0)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -182,6 +189,18 @@ impl Address {
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn error_count(&self) -> u64 {
|
||||||
|
self.error_count.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn increment_error_count(&self) {
|
||||||
|
self.error_count.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset_error_count(&self) {
|
||||||
|
self.error_count.store(0, Ordering::Relaxed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// PostgreSQL user.
|
/// PostgreSQL user.
|
||||||
@@ -189,6 +208,9 @@ impl Address {
|
|||||||
pub struct User {
|
pub struct User {
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub password: Option<String>,
|
pub password: Option<String>,
|
||||||
|
|
||||||
|
#[serde(default = "User::default_auth_type")]
|
||||||
|
pub auth_type: AuthType,
|
||||||
pub server_username: Option<String>,
|
pub server_username: Option<String>,
|
||||||
pub server_password: Option<String>,
|
pub server_password: Option<String>,
|
||||||
pub pool_size: u32,
|
pub pool_size: u32,
|
||||||
@@ -197,6 +219,8 @@ pub struct User {
|
|||||||
pub server_lifetime: Option<u64>,
|
pub server_lifetime: Option<u64>,
|
||||||
#[serde(default)] // 0
|
#[serde(default)] // 0
|
||||||
pub statement_timeout: u64,
|
pub statement_timeout: u64,
|
||||||
|
pub connect_timeout: Option<u64>,
|
||||||
|
pub idle_timeout: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for User {
|
impl Default for User {
|
||||||
@@ -204,6 +228,7 @@ impl Default for User {
|
|||||||
User {
|
User {
|
||||||
username: String::from("postgres"),
|
username: String::from("postgres"),
|
||||||
password: None,
|
password: None,
|
||||||
|
auth_type: AuthType::MD5,
|
||||||
server_username: None,
|
server_username: None,
|
||||||
server_password: None,
|
server_password: None,
|
||||||
pool_size: 15,
|
pool_size: 15,
|
||||||
@@ -211,24 +236,26 @@ impl Default for User {
|
|||||||
statement_timeout: 0,
|
statement_timeout: 0,
|
||||||
pool_mode: None,
|
pool_mode: None,
|
||||||
server_lifetime: None,
|
server_lifetime: None,
|
||||||
|
connect_timeout: None,
|
||||||
|
idle_timeout: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl User {
|
impl User {
|
||||||
fn validate(&self) -> Result<(), Error> {
|
pub fn default_auth_type() -> AuthType {
|
||||||
match self.min_pool_size {
|
AuthType::MD5
|
||||||
Some(min_pool_size) => {
|
}
|
||||||
if min_pool_size > self.pool_size {
|
|
||||||
error!(
|
|
||||||
"min_pool_size of {} cannot be larger than pool_size of {}",
|
|
||||||
min_pool_size, self.pool_size
|
|
||||||
);
|
|
||||||
return Err(Error::BadConfig);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
None => (),
|
fn validate(&self) -> Result<(), Error> {
|
||||||
|
if let Some(min_pool_size) = self.min_pool_size {
|
||||||
|
if min_pool_size > self.pool_size {
|
||||||
|
error!(
|
||||||
|
"min_pool_size of {} cannot be larger than pool_size of {}",
|
||||||
|
min_pool_size, self.pool_size
|
||||||
|
);
|
||||||
|
return Err(Error::BadConfig);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -315,6 +342,9 @@ pub struct General {
|
|||||||
pub admin_username: String,
|
pub admin_username: String,
|
||||||
pub admin_password: String,
|
pub admin_password: String,
|
||||||
|
|
||||||
|
#[serde(default = "General::default_admin_auth_type")]
|
||||||
|
pub admin_auth_type: AuthType,
|
||||||
|
|
||||||
#[serde(default = "General::default_validate_config")]
|
#[serde(default = "General::default_validate_config")]
|
||||||
pub validate_config: bool,
|
pub validate_config: bool,
|
||||||
|
|
||||||
@@ -322,12 +352,6 @@ pub struct General {
|
|||||||
pub auth_query: Option<String>,
|
pub auth_query: Option<String>,
|
||||||
pub auth_query_user: Option<String>,
|
pub auth_query_user: Option<String>,
|
||||||
pub auth_query_password: Option<String>,
|
pub auth_query_password: Option<String>,
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub prepared_statements: bool,
|
|
||||||
|
|
||||||
#[serde(default = "General::default_prepared_statements_cache_size")]
|
|
||||||
pub prepared_statements_cache_size: usize,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl General {
|
impl General {
|
||||||
@@ -335,6 +359,10 @@ impl General {
|
|||||||
"0.0.0.0".into()
|
"0.0.0.0".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn default_admin_auth_type() -> AuthType {
|
||||||
|
AuthType::MD5
|
||||||
|
}
|
||||||
|
|
||||||
pub fn default_port() -> u16 {
|
pub fn default_port() -> u16 {
|
||||||
5432
|
5432
|
||||||
}
|
}
|
||||||
@@ -409,10 +437,6 @@ impl General {
|
|||||||
pub fn default_server_round_robin() -> bool {
|
pub fn default_server_round_robin() -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn default_prepared_statements_cache_size() -> usize {
|
|
||||||
500
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for General {
|
impl Default for General {
|
||||||
@@ -424,35 +448,34 @@ impl Default for General {
|
|||||||
prometheus_exporter_port: 9930,
|
prometheus_exporter_port: 9930,
|
||||||
connect_timeout: General::default_connect_timeout(),
|
connect_timeout: General::default_connect_timeout(),
|
||||||
idle_timeout: General::default_idle_timeout(),
|
idle_timeout: General::default_idle_timeout(),
|
||||||
shutdown_timeout: Self::default_shutdown_timeout(),
|
|
||||||
healthcheck_timeout: Self::default_healthcheck_timeout(),
|
|
||||||
healthcheck_delay: Self::default_healthcheck_delay(),
|
|
||||||
ban_time: Self::default_ban_time(),
|
|
||||||
worker_threads: Self::default_worker_threads(),
|
|
||||||
idle_client_in_transaction_timeout: Self::default_idle_client_in_transaction_timeout(),
|
|
||||||
tcp_keepalives_idle: Self::default_tcp_keepalives_idle(),
|
tcp_keepalives_idle: Self::default_tcp_keepalives_idle(),
|
||||||
tcp_keepalives_count: Self::default_tcp_keepalives_count(),
|
tcp_keepalives_count: Self::default_tcp_keepalives_count(),
|
||||||
tcp_keepalives_interval: Self::default_tcp_keepalives_interval(),
|
tcp_keepalives_interval: Self::default_tcp_keepalives_interval(),
|
||||||
tcp_user_timeout: Self::default_tcp_user_timeout(),
|
tcp_user_timeout: Self::default_tcp_user_timeout(),
|
||||||
log_client_connections: false,
|
log_client_connections: false,
|
||||||
log_client_disconnections: false,
|
log_client_disconnections: false,
|
||||||
autoreload: None,
|
|
||||||
dns_cache_enabled: false,
|
dns_cache_enabled: false,
|
||||||
dns_max_ttl: Self::default_dns_max_ttl(),
|
dns_max_ttl: Self::default_dns_max_ttl(),
|
||||||
|
shutdown_timeout: Self::default_shutdown_timeout(),
|
||||||
|
healthcheck_timeout: Self::default_healthcheck_timeout(),
|
||||||
|
healthcheck_delay: Self::default_healthcheck_delay(),
|
||||||
|
ban_time: Self::default_ban_time(),
|
||||||
|
idle_client_in_transaction_timeout: Self::default_idle_client_in_transaction_timeout(),
|
||||||
|
server_lifetime: Self::default_server_lifetime(),
|
||||||
|
server_round_robin: Self::default_server_round_robin(),
|
||||||
|
worker_threads: Self::default_worker_threads(),
|
||||||
|
autoreload: None,
|
||||||
tls_certificate: None,
|
tls_certificate: None,
|
||||||
tls_private_key: None,
|
tls_private_key: None,
|
||||||
server_tls: false,
|
server_tls: false,
|
||||||
verify_server_certificate: false,
|
verify_server_certificate: false,
|
||||||
admin_username: String::from("admin"),
|
admin_username: String::from("admin"),
|
||||||
admin_password: String::from("admin"),
|
admin_password: String::from("admin"),
|
||||||
|
admin_auth_type: AuthType::MD5,
|
||||||
|
validate_config: true,
|
||||||
auth_query: None,
|
auth_query: None,
|
||||||
auth_query_user: None,
|
auth_query_user: None,
|
||||||
auth_query_password: None,
|
auth_query_password: None,
|
||||||
server_lifetime: Self::default_server_lifetime(),
|
|
||||||
server_round_robin: Self::default_server_round_robin(),
|
|
||||||
validate_config: true,
|
|
||||||
prepared_statements: false,
|
|
||||||
prepared_statements_cache_size: 500,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -469,11 +492,20 @@ pub enum PoolMode {
|
|||||||
Session,
|
Session,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToString for PoolMode {
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Copy, Hash)]
|
||||||
fn to_string(&self) -> String {
|
pub enum AuthType {
|
||||||
match *self {
|
#[serde(alias = "trust", alias = "Trust")]
|
||||||
PoolMode::Transaction => "transaction".to_string(),
|
Trust,
|
||||||
PoolMode::Session => "session".to_string(),
|
|
||||||
|
#[serde(alias = "md5", alias = "MD5")]
|
||||||
|
MD5,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for PoolMode {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
PoolMode::Transaction => write!(f, "transaction"),
|
||||||
|
PoolMode::Session => write!(f, "session"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -486,12 +518,13 @@ pub enum LoadBalancingMode {
|
|||||||
#[serde(alias = "loc", alias = "LOC", alias = "least_outstanding_connections")]
|
#[serde(alias = "loc", alias = "LOC", alias = "least_outstanding_connections")]
|
||||||
LeastOutstandingConnections,
|
LeastOutstandingConnections,
|
||||||
}
|
}
|
||||||
impl ToString for LoadBalancingMode {
|
|
||||||
fn to_string(&self) -> String {
|
impl std::fmt::Display for LoadBalancingMode {
|
||||||
match *self {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
LoadBalancingMode::Random => "random".to_string(),
|
match self {
|
||||||
|
LoadBalancingMode::Random => write!(f, "random"),
|
||||||
LoadBalancingMode::LeastOutstandingConnections => {
|
LoadBalancingMode::LeastOutstandingConnections => {
|
||||||
"least_outstanding_connections".to_string()
|
write!(f, "least_outstanding_connections")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -511,6 +544,11 @@ pub struct Pool {
|
|||||||
#[serde(default)] // False
|
#[serde(default)] // False
|
||||||
pub query_parser_enabled: bool,
|
pub query_parser_enabled: bool,
|
||||||
|
|
||||||
|
pub query_parser_max_length: Option<usize>,
|
||||||
|
|
||||||
|
#[serde(default)] // False
|
||||||
|
pub query_parser_read_write_splitting: bool,
|
||||||
|
|
||||||
#[serde(default)] // False
|
#[serde(default)] // False
|
||||||
pub primary_reads_enabled: bool,
|
pub primary_reads_enabled: bool,
|
||||||
|
|
||||||
@@ -535,6 +573,9 @@ pub struct Pool {
|
|||||||
pub shard_id_regex: Option<String>,
|
pub shard_id_regex: Option<String>,
|
||||||
pub regex_search_limit: Option<usize>,
|
pub regex_search_limit: Option<usize>,
|
||||||
|
|
||||||
|
#[serde(default = "Pool::default_default_shard")]
|
||||||
|
pub default_shard: DefaultShard,
|
||||||
|
|
||||||
pub auth_query: Option<String>,
|
pub auth_query: Option<String>,
|
||||||
pub auth_query_user: Option<String>,
|
pub auth_query_user: Option<String>,
|
||||||
pub auth_query_password: Option<String>,
|
pub auth_query_password: Option<String>,
|
||||||
@@ -542,6 +583,12 @@ pub struct Pool {
|
|||||||
#[serde(default = "Pool::default_cleanup_server_connections")]
|
#[serde(default = "Pool::default_cleanup_server_connections")]
|
||||||
pub cleanup_server_connections: bool,
|
pub cleanup_server_connections: bool,
|
||||||
|
|
||||||
|
#[serde(default)] // False
|
||||||
|
pub log_client_parameter_status_changes: bool,
|
||||||
|
|
||||||
|
#[serde(default = "Pool::default_prepared_statements_cache_size")]
|
||||||
|
pub prepared_statements_cache_size: usize,
|
||||||
|
|
||||||
pub plugins: Option<Plugins>,
|
pub plugins: Option<Plugins>,
|
||||||
pub shards: BTreeMap<String, Shard>,
|
pub shards: BTreeMap<String, Shard>,
|
||||||
pub users: BTreeMap<String, User>,
|
pub users: BTreeMap<String, User>,
|
||||||
@@ -567,6 +614,10 @@ impl Pool {
|
|||||||
PoolMode::Transaction
|
PoolMode::Transaction
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn default_default_shard() -> DefaultShard {
|
||||||
|
DefaultShard::default()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn default_load_balancing_mode() -> LoadBalancingMode {
|
pub fn default_load_balancing_mode() -> LoadBalancingMode {
|
||||||
LoadBalancingMode::Random
|
LoadBalancingMode::Random
|
||||||
}
|
}
|
||||||
@@ -587,6 +638,10 @@ impl Pool {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn default_prepared_statements_cache_size() -> usize {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
pub fn validate(&mut self) -> Result<(), Error> {
|
pub fn validate(&mut self) -> Result<(), Error> {
|
||||||
match self.default_role.as_ref() {
|
match self.default_role.as_ref() {
|
||||||
"any" => (),
|
"any" => (),
|
||||||
@@ -627,13 +682,25 @@ impl Pool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.query_parser_read_write_splitting && !self.query_parser_enabled {
|
||||||
|
error!(
|
||||||
|
"query_parser_read_write_splitting is only valid when query_parser_enabled is true"
|
||||||
|
);
|
||||||
|
return Err(Error::BadConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.plugins.is_some() && !self.query_parser_enabled {
|
||||||
|
error!("plugins are only valid when query_parser_enabled is true");
|
||||||
|
return Err(Error::BadConfig);
|
||||||
|
}
|
||||||
|
|
||||||
self.automatic_sharding_key = match &self.automatic_sharding_key {
|
self.automatic_sharding_key = match &self.automatic_sharding_key {
|
||||||
Some(key) => {
|
Some(key) => {
|
||||||
// No quotes in the key so we don't have to compare quoted
|
// No quotes in the key so we don't have to compare quoted
|
||||||
// to unquoted idents.
|
// to unquoted idents.
|
||||||
let key = key.replace("\"", "");
|
let key = key.replace('\"', "");
|
||||||
|
|
||||||
if key.split(".").count() != 2 {
|
if key.split('.').count() != 2 {
|
||||||
error!(
|
error!(
|
||||||
"automatic_sharding_key '{}' must be fully qualified, e.g. t.{}`",
|
"automatic_sharding_key '{}' must be fully qualified, e.g. t.{}`",
|
||||||
key, key
|
key, key
|
||||||
@@ -646,7 +713,14 @@ impl Pool {
|
|||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
for (_, user) in &self.users {
|
if let DefaultShard::Shard(shard_number) = self.default_shard {
|
||||||
|
if shard_number >= self.shards.len() {
|
||||||
|
error!("Invalid shard {:?}", shard_number);
|
||||||
|
return Err(Error::BadConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for user in self.users.values() {
|
||||||
user.validate()?;
|
user.validate()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -659,24 +733,29 @@ impl Default for Pool {
|
|||||||
Pool {
|
Pool {
|
||||||
pool_mode: Self::default_pool_mode(),
|
pool_mode: Self::default_pool_mode(),
|
||||||
load_balancing_mode: Self::default_load_balancing_mode(),
|
load_balancing_mode: Self::default_load_balancing_mode(),
|
||||||
shards: BTreeMap::from([(String::from("1"), Shard::default())]),
|
|
||||||
users: BTreeMap::default(),
|
|
||||||
default_role: String::from("any"),
|
default_role: String::from("any"),
|
||||||
query_parser_enabled: false,
|
query_parser_enabled: false,
|
||||||
|
query_parser_max_length: None,
|
||||||
|
query_parser_read_write_splitting: false,
|
||||||
primary_reads_enabled: false,
|
primary_reads_enabled: false,
|
||||||
sharding_function: ShardingFunction::PgBigintHash,
|
|
||||||
automatic_sharding_key: None,
|
|
||||||
connect_timeout: None,
|
connect_timeout: None,
|
||||||
idle_timeout: None,
|
idle_timeout: None,
|
||||||
|
server_lifetime: None,
|
||||||
|
sharding_function: ShardingFunction::PgBigintHash,
|
||||||
|
automatic_sharding_key: None,
|
||||||
sharding_key_regex: None,
|
sharding_key_regex: None,
|
||||||
shard_id_regex: None,
|
shard_id_regex: None,
|
||||||
regex_search_limit: Some(1000),
|
regex_search_limit: Some(1000),
|
||||||
|
default_shard: Self::default_default_shard(),
|
||||||
auth_query: None,
|
auth_query: None,
|
||||||
auth_query_user: None,
|
auth_query_user: None,
|
||||||
auth_query_password: None,
|
auth_query_password: None,
|
||||||
server_lifetime: None,
|
|
||||||
plugins: None,
|
|
||||||
cleanup_server_connections: true,
|
cleanup_server_connections: true,
|
||||||
|
log_client_parameter_status_changes: false,
|
||||||
|
prepared_statements_cache_size: Self::default_prepared_statements_cache_size(),
|
||||||
|
plugins: None,
|
||||||
|
shards: BTreeMap::from([(String::from("1"), Shard::default())]),
|
||||||
|
users: BTreeMap::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -688,6 +767,50 @@ pub struct ServerConfig {
|
|||||||
pub role: Role,
|
pub role: Role,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No Shard Specified handling.
|
||||||
|
#[derive(Debug, PartialEq, Clone, Eq, Hash, Copy)]
|
||||||
|
pub enum DefaultShard {
|
||||||
|
Shard(usize),
|
||||||
|
Random,
|
||||||
|
RandomHealthy,
|
||||||
|
}
|
||||||
|
impl Default for DefaultShard {
|
||||||
|
fn default() -> Self {
|
||||||
|
DefaultShard::Shard(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl serde::Serialize for DefaultShard {
|
||||||
|
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||||
|
match self {
|
||||||
|
DefaultShard::Shard(shard) => {
|
||||||
|
serializer.serialize_str(&format!("shard_{}", &shard.to_string()))
|
||||||
|
}
|
||||||
|
DefaultShard::Random => serializer.serialize_str("random"),
|
||||||
|
DefaultShard::RandomHealthy => serializer.serialize_str("random_healthy"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<'de> serde::Deserialize<'de> for DefaultShard {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let s = String::deserialize(deserializer)?;
|
||||||
|
if let Some(s) = s.strip_prefix("shard_") {
|
||||||
|
let shard = s.parse::<usize>().map_err(serde::de::Error::custom)?;
|
||||||
|
return Ok(DefaultShard::Shard(shard));
|
||||||
|
}
|
||||||
|
|
||||||
|
match s.as_str() {
|
||||||
|
"random" => Ok(DefaultShard::Random),
|
||||||
|
"random_healthy" => Ok(DefaultShard::RandomHealthy),
|
||||||
|
_ => Err(serde::de::Error::custom(
|
||||||
|
"invalid value for no_shard_specified_behavior",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug, Hash, Eq)]
|
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug, Hash, Eq)]
|
||||||
pub struct MirrorServerConfig {
|
pub struct MirrorServerConfig {
|
||||||
pub host: String,
|
pub host: String,
|
||||||
@@ -744,13 +867,13 @@ impl Shard {
|
|||||||
impl Default for Shard {
|
impl Default for Shard {
|
||||||
fn default() -> Shard {
|
fn default() -> Shard {
|
||||||
Shard {
|
Shard {
|
||||||
|
database: String::from("postgres"),
|
||||||
|
mirrors: None,
|
||||||
servers: vec![ServerConfig {
|
servers: vec![ServerConfig {
|
||||||
host: String::from("localhost"),
|
host: String::from("localhost"),
|
||||||
port: 5432,
|
port: 5432,
|
||||||
role: Role::Primary,
|
role: Role::Primary,
|
||||||
}],
|
}],
|
||||||
mirrors: None,
|
|
||||||
database: String::from("postgres"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -763,15 +886,26 @@ pub struct Plugins {
|
|||||||
pub prewarmer: Option<Prewarmer>,
|
pub prewarmer: Option<Prewarmer>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait Plugin {
|
||||||
|
fn is_enabled(&self) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for Plugins {
|
impl std::fmt::Display for Plugins {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
fn is_enabled<T: Plugin>(arg: Option<&T>) -> bool {
|
||||||
|
if let Some(arg) = arg {
|
||||||
|
arg.is_enabled()
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"interceptor: {}, table_access: {}, query_logger: {}, prewarmer: {}",
|
"interceptor: {}, table_access: {}, query_logger: {}, prewarmer: {}",
|
||||||
self.intercept.is_some(),
|
is_enabled(self.intercept.as_ref()),
|
||||||
self.table_access.is_some(),
|
is_enabled(self.table_access.as_ref()),
|
||||||
self.query_logger.is_some(),
|
is_enabled(self.query_logger.as_ref()),
|
||||||
self.prewarmer.is_some(),
|
is_enabled(self.prewarmer.as_ref()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -782,23 +916,47 @@ pub struct Intercept {
|
|||||||
pub queries: BTreeMap<String, Query>,
|
pub queries: BTreeMap<String, Query>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Plugin for Intercept {
|
||||||
|
fn is_enabled(&self) -> bool {
|
||||||
|
self.enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, Hash, Eq)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, Hash, Eq)]
|
||||||
pub struct TableAccess {
|
pub struct TableAccess {
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
pub tables: Vec<String>,
|
pub tables: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Plugin for TableAccess {
|
||||||
|
fn is_enabled(&self) -> bool {
|
||||||
|
self.enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, Hash, Eq)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, Hash, Eq)]
|
||||||
pub struct QueryLogger {
|
pub struct QueryLogger {
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Plugin for QueryLogger {
|
||||||
|
fn is_enabled(&self) -> bool {
|
||||||
|
self.enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, Hash, Eq)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, Hash, Eq)]
|
||||||
pub struct Prewarmer {
|
pub struct Prewarmer {
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
pub queries: Vec<String>,
|
pub queries: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Plugin for Prewarmer {
|
||||||
|
fn is_enabled(&self) -> bool {
|
||||||
|
self.enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Intercept {
|
impl Intercept {
|
||||||
pub fn substitute(&mut self, db: &str, user: &str) {
|
pub fn substitute(&mut self, db: &str, user: &str) {
|
||||||
for (_, query) in self.queries.iter_mut() {
|
for (_, query) in self.queries.iter_mut() {
|
||||||
@@ -816,6 +974,7 @@ pub struct Query {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Query {
|
impl Query {
|
||||||
|
#[allow(clippy::needless_range_loop)]
|
||||||
pub fn substitute(&mut self, db: &str, user: &str) {
|
pub fn substitute(&mut self, db: &str, user: &str) {
|
||||||
for col in self.result.iter_mut() {
|
for col in self.result.iter_mut() {
|
||||||
for i in 0..col.len() {
|
for i in 0..col.len() {
|
||||||
@@ -866,15 +1025,17 @@ impl Config {
|
|||||||
pub fn fill_up_auth_query_config(&mut self) {
|
pub fn fill_up_auth_query_config(&mut self) {
|
||||||
for (_name, pool) in self.pools.iter_mut() {
|
for (_name, pool) in self.pools.iter_mut() {
|
||||||
if pool.auth_query.is_none() {
|
if pool.auth_query.is_none() {
|
||||||
pool.auth_query = self.general.auth_query.clone();
|
pool.auth_query.clone_from(&self.general.auth_query);
|
||||||
}
|
}
|
||||||
|
|
||||||
if pool.auth_query_user.is_none() {
|
if pool.auth_query_user.is_none() {
|
||||||
pool.auth_query_user = self.general.auth_query_user.clone();
|
pool.auth_query_user
|
||||||
|
.clone_from(&self.general.auth_query_user);
|
||||||
}
|
}
|
||||||
|
|
||||||
if pool.auth_query_password.is_none() {
|
if pool.auth_query_password.is_none() {
|
||||||
pool.auth_query_password = self.general.auth_query_password.clone();
|
pool.auth_query_password
|
||||||
|
.clone_from(&self.general.auth_query_password);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -885,8 +1046,8 @@ impl Default for Config {
|
|||||||
Config {
|
Config {
|
||||||
path: Self::default_path(),
|
path: Self::default_path(),
|
||||||
general: General::default(),
|
general: General::default(),
|
||||||
pools: HashMap::default(),
|
|
||||||
plugins: None,
|
plugins: None,
|
||||||
|
pools: HashMap::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -914,6 +1075,17 @@ impl From<&Config> for std::collections::HashMap<String, String> {
|
|||||||
format!("pools.{}.query_parser_enabled", pool_name),
|
format!("pools.{}.query_parser_enabled", pool_name),
|
||||||
pool.query_parser_enabled.to_string(),
|
pool.query_parser_enabled.to_string(),
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
format!("pools.{}.query_parser_max_length", pool_name),
|
||||||
|
match pool.query_parser_max_length {
|
||||||
|
Some(max_length) => max_length.to_string(),
|
||||||
|
None => String::from("unlimited"),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
format!("pools.{}.query_parser_read_write_splitting", pool_name),
|
||||||
|
pool.query_parser_read_write_splitting.to_string(),
|
||||||
|
),
|
||||||
(
|
(
|
||||||
format!("pools.{}.default_role", pool_name),
|
format!("pools.{}.default_role", pool_name),
|
||||||
pool.default_role.clone(),
|
pool.default_role.clone(),
|
||||||
@@ -929,8 +1101,8 @@ impl From<&Config> for std::collections::HashMap<String, String> {
|
|||||||
(
|
(
|
||||||
format!("pools.{:?}.users", pool_name),
|
format!("pools.{:?}.users", pool_name),
|
||||||
pool.users
|
pool.users
|
||||||
.iter()
|
.values()
|
||||||
.map(|(_username, user)| &user.username)
|
.map(|user| &user.username)
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
.join(", "),
|
.join(", "),
|
||||||
@@ -984,6 +1156,7 @@ impl From<&Config> for std::collections::HashMap<String, String> {
|
|||||||
impl Config {
|
impl Config {
|
||||||
/// Print current configuration.
|
/// Print current configuration.
|
||||||
pub fn show(&self) {
|
pub fn show(&self) {
|
||||||
|
info!("Config path: {}", self.path);
|
||||||
info!("Ban time: {}s", self.general.ban_time);
|
info!("Ban time: {}s", self.general.ban_time);
|
||||||
info!(
|
info!(
|
||||||
"Idle client in transaction timeout: {}ms",
|
"Idle client in transaction timeout: {}ms",
|
||||||
@@ -1010,18 +1183,14 @@ impl Config {
|
|||||||
"Default max server lifetime: {}ms",
|
"Default max server lifetime: {}ms",
|
||||||
self.general.server_lifetime
|
self.general.server_lifetime
|
||||||
);
|
);
|
||||||
info!("Sever round robin: {}", self.general.server_round_robin);
|
info!("Server round robin: {}", self.general.server_round_robin);
|
||||||
match self.general.tls_certificate.clone() {
|
match self.general.tls_certificate.clone() {
|
||||||
Some(tls_certificate) => {
|
Some(tls_certificate) => {
|
||||||
info!("TLS certificate: {}", tls_certificate);
|
info!("TLS certificate: {}", tls_certificate);
|
||||||
|
|
||||||
match self.general.tls_private_key.clone() {
|
if let Some(tls_private_key) = self.general.tls_private_key.clone() {
|
||||||
Some(tls_private_key) => {
|
info!("TLS private key: {}", tls_private_key);
|
||||||
info!("TLS private key: {}", tls_private_key);
|
info!("TLS support is enabled");
|
||||||
info!("TLS support is enabled");
|
|
||||||
}
|
|
||||||
|
|
||||||
None => (),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1034,13 +1203,6 @@ impl Config {
|
|||||||
"Server TLS certificate verification: {}",
|
"Server TLS certificate verification: {}",
|
||||||
self.general.verify_server_certificate
|
self.general.verify_server_certificate
|
||||||
);
|
);
|
||||||
info!("Prepared statements: {}", self.general.prepared_statements);
|
|
||||||
if self.general.prepared_statements {
|
|
||||||
info!(
|
|
||||||
"Prepared statements server cache size: {}",
|
|
||||||
self.general.prepared_statements_cache_size
|
|
||||||
);
|
|
||||||
}
|
|
||||||
info!(
|
info!(
|
||||||
"Plugins: {}",
|
"Plugins: {}",
|
||||||
match self.plugins {
|
match self.plugins {
|
||||||
@@ -1056,8 +1218,8 @@ impl Config {
|
|||||||
pool_name,
|
pool_name,
|
||||||
pool_config
|
pool_config
|
||||||
.users
|
.users
|
||||||
.iter()
|
.values()
|
||||||
.map(|(_, user_cfg)| user_cfg.pool_size)
|
.map(|user_cfg| user_cfg.pool_size)
|
||||||
.sum::<u32>()
|
.sum::<u32>()
|
||||||
.to_string()
|
.to_string()
|
||||||
);
|
);
|
||||||
@@ -1096,6 +1258,15 @@ impl Config {
|
|||||||
"[pool: {}] Query router: {}",
|
"[pool: {}] Query router: {}",
|
||||||
pool_name, pool_config.query_parser_enabled
|
pool_name, pool_config.query_parser_enabled
|
||||||
);
|
);
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"[pool: {}] Query parser max length: {:?}",
|
||||||
|
pool_name, pool_config.query_parser_max_length
|
||||||
|
);
|
||||||
|
info!(
|
||||||
|
"[pool: {}] Infer role from query: {}",
|
||||||
|
pool_name, pool_config.query_parser_read_write_splitting
|
||||||
|
);
|
||||||
info!(
|
info!(
|
||||||
"[pool: {}] Number of shards: {}",
|
"[pool: {}] Number of shards: {}",
|
||||||
pool_name,
|
pool_name,
|
||||||
@@ -1118,6 +1289,14 @@ impl Config {
|
|||||||
"[pool: {}] Cleanup server connections: {}",
|
"[pool: {}] Cleanup server connections: {}",
|
||||||
pool_name, pool_config.cleanup_server_connections
|
pool_name, pool_config.cleanup_server_connections
|
||||||
);
|
);
|
||||||
|
info!(
|
||||||
|
"[pool: {}] Log client parameter status changes: {}",
|
||||||
|
pool_name, pool_config.log_client_parameter_status_changes
|
||||||
|
);
|
||||||
|
info!(
|
||||||
|
"[pool: {}] Prepared statements server cache size: {}",
|
||||||
|
pool_name, pool_config.prepared_statements_cache_size
|
||||||
|
);
|
||||||
info!(
|
info!(
|
||||||
"[pool: {}] Plugins: {}",
|
"[pool: {}] Plugins: {}",
|
||||||
pool_name,
|
pool_name,
|
||||||
@@ -1160,6 +1339,24 @@ impl Config {
|
|||||||
None => "default".to_string(),
|
None => "default".to_string(),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
info!(
|
||||||
|
"[pool: {}][user: {}] Connection timeout: {}",
|
||||||
|
pool_name,
|
||||||
|
user.1.username,
|
||||||
|
match user.1.connect_timeout {
|
||||||
|
Some(connect_timeout) => format!("{}ms", connect_timeout),
|
||||||
|
None => "not set".to_string(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
info!(
|
||||||
|
"[pool: {}][user: {}] Idle timeout: {}",
|
||||||
|
pool_name,
|
||||||
|
user.1.username,
|
||||||
|
match user.1.idle_timeout {
|
||||||
|
Some(idle_timeout) => format!("{}ms", idle_timeout),
|
||||||
|
None => "not set".to_string(),
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1214,34 +1411,31 @@ impl Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate TLS!
|
// Validate TLS!
|
||||||
match self.general.tls_certificate.clone() {
|
if let Some(tls_certificate) = self.general.tls_certificate.clone() {
|
||||||
Some(tls_certificate) => {
|
match load_certs(Path::new(&tls_certificate)) {
|
||||||
match load_certs(Path::new(&tls_certificate)) {
|
Ok(_) => {
|
||||||
Ok(_) => {
|
// Cert is okay, but what about the private key?
|
||||||
// Cert is okay, but what about the private key?
|
match self.general.tls_private_key.clone() {
|
||||||
match self.general.tls_private_key.clone() {
|
Some(tls_private_key) => match load_keys(Path::new(&tls_private_key)) {
|
||||||
Some(tls_private_key) => match load_keys(Path::new(&tls_private_key)) {
|
Ok(_) => (),
|
||||||
Ok(_) => (),
|
Err(err) => {
|
||||||
Err(err) => {
|
error!("tls_private_key is incorrectly configured: {:?}", err);
|
||||||
error!("tls_private_key is incorrectly configured: {:?}", err);
|
|
||||||
return Err(Error::BadConfig);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
None => {
|
|
||||||
error!("tls_certificate is set, but the tls_private_key is not");
|
|
||||||
return Err(Error::BadConfig);
|
return Err(Error::BadConfig);
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
}
|
|
||||||
|
|
||||||
Err(err) => {
|
None => {
|
||||||
error!("tls_certificate is incorrectly configured: {:?}", err);
|
error!("tls_certificate is set, but the tls_private_key is not");
|
||||||
return Err(Error::BadConfig);
|
return Err(Error::BadConfig);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(err) => {
|
||||||
|
error!("tls_certificate is incorrectly configured: {:?}", err);
|
||||||
|
return Err(Error::BadConfig);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => (),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
for pool in self.pools.values_mut() {
|
for pool in self.pools.values_mut() {
|
||||||
@@ -1263,14 +1457,6 @@ pub fn get_idle_client_in_transaction_timeout() -> u64 {
|
|||||||
CONFIG.load().general.idle_client_in_transaction_timeout
|
CONFIG.load().general.idle_client_in_transaction_timeout
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_prepared_statements() -> bool {
|
|
||||||
CONFIG.load().general.prepared_statements
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_prepared_statements_cache_size() -> usize {
|
|
||||||
CONFIG.load().general.prepared_statements_cache_size
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse the configuration file located at the path.
|
/// Parse the configuration file located at the path.
|
||||||
pub async fn parse(path: &str) -> Result<(), Error> {
|
pub async fn parse(path: &str) -> Result<(), Error> {
|
||||||
let mut contents = String::new();
|
let mut contents = String::new();
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ pub enum Error {
|
|||||||
ProtocolSyncError(String),
|
ProtocolSyncError(String),
|
||||||
BadQuery(String),
|
BadQuery(String),
|
||||||
ServerError,
|
ServerError,
|
||||||
|
ServerMessageParserError(String),
|
||||||
ServerStartupError(String, ServerIdentifier),
|
ServerStartupError(String, ServerIdentifier),
|
||||||
ServerAuthError(String, ServerIdentifier),
|
ServerAuthError(String, ServerIdentifier),
|
||||||
BadConfig,
|
BadConfig,
|
||||||
@@ -27,6 +28,8 @@ pub enum Error {
|
|||||||
UnsupportedStatement,
|
UnsupportedStatement,
|
||||||
QueryRouterParserError(String),
|
QueryRouterParserError(String),
|
||||||
QueryRouterError(String),
|
QueryRouterError(String),
|
||||||
|
InvalidShardId(usize),
|
||||||
|
PreparedStatementError,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Debug)]
|
#[derive(Clone, PartialEq, Debug)]
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ pub fn init(args: &Args) {
|
|||||||
let filter = EnvFilter::from_default_env().add_directive(args.log_level.into());
|
let filter = EnvFilter::from_default_env().add_directive(args.log_level.into());
|
||||||
|
|
||||||
let trace_sub = tracing_subscriber::fmt()
|
let trace_sub = tracing_subscriber::fmt()
|
||||||
|
.with_thread_ids(true)
|
||||||
.with_env_filter(filter)
|
.with_env_filter(filter)
|
||||||
.with_ansi(!args.no_color);
|
.with_ansi(!args.no_color);
|
||||||
|
|
||||||
|
|||||||
572
src/messages.rs
572
src/messages.rs
@@ -11,11 +11,17 @@ use crate::client::PREPARED_STATEMENT_COUNTER;
|
|||||||
use crate::config::get_config;
|
use crate::config::get_config;
|
||||||
use crate::errors::Error;
|
use crate::errors::Error;
|
||||||
|
|
||||||
|
use crate::constants::MESSAGE_TERMINATOR;
|
||||||
|
use std::collections::hash_map::DefaultHasher;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::ffi::CString;
|
use std::ffi::CString;
|
||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
use std::io::{BufRead, Cursor};
|
use std::io::{BufRead, Cursor};
|
||||||
use std::mem;
|
use std::mem;
|
||||||
|
use std::str::FromStr;
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
/// Postgres data type mappings
|
/// Postgres data type mappings
|
||||||
@@ -111,19 +117,11 @@ pub fn simple_query(query: &str) -> BytesMut {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Tell the client we're ready for another query.
|
/// Tell the client we're ready for another query.
|
||||||
pub async fn ready_for_query<S>(stream: &mut S) -> Result<(), Error>
|
pub async fn send_ready_for_query<S>(stream: &mut S) -> Result<(), Error>
|
||||||
where
|
where
|
||||||
S: tokio::io::AsyncWrite + std::marker::Unpin,
|
S: tokio::io::AsyncWrite + std::marker::Unpin,
|
||||||
{
|
{
|
||||||
let mut bytes = BytesMut::with_capacity(
|
write_all(stream, ready_for_query(false)).await
|
||||||
mem::size_of::<u8>() + mem::size_of::<i32>() + mem::size_of::<u8>(),
|
|
||||||
);
|
|
||||||
|
|
||||||
bytes.put_u8(b'Z');
|
|
||||||
bytes.put_i32(5);
|
|
||||||
bytes.put_u8(b'I'); // Idle
|
|
||||||
|
|
||||||
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.
|
||||||
@@ -141,6 +139,10 @@ where
|
|||||||
bytes.put_slice(user.as_bytes());
|
bytes.put_slice(user.as_bytes());
|
||||||
bytes.put_u8(0);
|
bytes.put_u8(0);
|
||||||
|
|
||||||
|
// Application name
|
||||||
|
bytes.put(&b"application_name\0"[..]);
|
||||||
|
bytes.put_slice(&b"pgcat\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());
|
||||||
@@ -156,12 +158,10 @@ where
|
|||||||
|
|
||||||
match stream.write_all(&startup).await {
|
match stream.write_all(&startup).await {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(err) => {
|
Err(err) => Err(Error::SocketError(format!(
|
||||||
return Err(Error::SocketError(format!(
|
"Error writing startup to server socket - Error: {:?}",
|
||||||
"Error writing startup to server socket - Error: {:?}",
|
err
|
||||||
err
|
))),
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,8 +237,8 @@ pub fn md5_hash_password(user: &str, password: &str, salt: &[u8]) -> Vec<u8> {
|
|||||||
let mut md5 = Md5::new();
|
let mut md5 = Md5::new();
|
||||||
|
|
||||||
// First pass
|
// First pass
|
||||||
md5.update(&password.as_bytes());
|
md5.update(password.as_bytes());
|
||||||
md5.update(&user.as_bytes());
|
md5.update(user.as_bytes());
|
||||||
|
|
||||||
let output = md5.finalize_reset();
|
let output = md5.finalize_reset();
|
||||||
|
|
||||||
@@ -274,7 +274,7 @@ where
|
|||||||
{
|
{
|
||||||
let password = md5_hash_password(user, password, salt);
|
let password = md5_hash_password(user, password, salt);
|
||||||
|
|
||||||
let mut message = BytesMut::with_capacity(password.len() as usize + 5);
|
let mut message = BytesMut::with_capacity(password.len() + 5);
|
||||||
|
|
||||||
message.put_u8(b'p');
|
message.put_u8(b'p');
|
||||||
message.put_i32(password.len() as i32 + 4);
|
message.put_i32(password.len() as i32 + 4);
|
||||||
@@ -288,7 +288,7 @@ where
|
|||||||
S: tokio::io::AsyncWrite + std::marker::Unpin,
|
S: tokio::io::AsyncWrite + std::marker::Unpin,
|
||||||
{
|
{
|
||||||
let password = md5_hash_second_pass(hash, salt);
|
let password = md5_hash_second_pass(hash, salt);
|
||||||
let mut message = BytesMut::with_capacity(password.len() as usize + 5);
|
let mut message = BytesMut::with_capacity(password.len() + 5);
|
||||||
|
|
||||||
message.put_u8(b'p');
|
message.put_u8(b'p');
|
||||||
message.put_i32(password.len() as i32 + 4);
|
message.put_i32(password.len() as i32 + 4);
|
||||||
@@ -315,7 +315,7 @@ where
|
|||||||
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
|
send_ready_for_query(stream).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send a custom error message to the client.
|
/// Send a custom error message to the client.
|
||||||
@@ -326,7 +326,7 @@ where
|
|||||||
S: tokio::io::AsyncWrite + std::marker::Unpin,
|
S: tokio::io::AsyncWrite + std::marker::Unpin,
|
||||||
{
|
{
|
||||||
error_response_terminal(stream, message).await?;
|
error_response_terminal(stream, message).await?;
|
||||||
ready_for_query(stream).await
|
send_ready_for_query(stream).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send a custom error message to the client.
|
/// Send a custom error message to the client.
|
||||||
@@ -427,7 +427,7 @@ where
|
|||||||
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
|
send_ready_for_query(stream).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn row_description(columns: &Vec<(&str, DataType)>) -> BytesMut {
|
pub fn row_description(columns: &Vec<(&str, DataType)>) -> BytesMut {
|
||||||
@@ -509,7 +509,7 @@ pub fn data_row_nullable(row: &Vec<Option<String>>) -> BytesMut {
|
|||||||
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);
|
||||||
} else {
|
} else {
|
||||||
data_row.put_i32(-1 as i32);
|
data_row.put_i32(-1_i32);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -557,6 +557,37 @@ pub fn flush() -> BytesMut {
|
|||||||
bytes
|
bytes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn sync() -> BytesMut {
|
||||||
|
let mut bytes = BytesMut::with_capacity(mem::size_of::<u8>() + mem::size_of::<i32>());
|
||||||
|
bytes.put_u8(b'S');
|
||||||
|
bytes.put_i32(4);
|
||||||
|
bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_complete() -> BytesMut {
|
||||||
|
let mut bytes = BytesMut::with_capacity(mem::size_of::<u8>() + mem::size_of::<i32>());
|
||||||
|
|
||||||
|
bytes.put_u8(b'1');
|
||||||
|
bytes.put_i32(4);
|
||||||
|
bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ready_for_query(in_transaction: bool) -> BytesMut {
|
||||||
|
let mut bytes = BytesMut::with_capacity(
|
||||||
|
mem::size_of::<u8>() + mem::size_of::<i32>() + mem::size_of::<u8>(),
|
||||||
|
);
|
||||||
|
|
||||||
|
bytes.put_u8(b'Z');
|
||||||
|
bytes.put_i32(5);
|
||||||
|
if in_transaction {
|
||||||
|
bytes.put_u8(b'T');
|
||||||
|
} else {
|
||||||
|
bytes.put_u8(b'I');
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes
|
||||||
|
}
|
||||||
|
|
||||||
/// Write all data in the buffer to the TcpStream.
|
/// Write all data in the buffer to the TcpStream.
|
||||||
pub async fn write_all<S>(stream: &mut S, buf: BytesMut) -> Result<(), Error>
|
pub async fn write_all<S>(stream: &mut S, buf: BytesMut) -> Result<(), Error>
|
||||||
where
|
where
|
||||||
@@ -564,12 +595,10 @@ where
|
|||||||
{
|
{
|
||||||
match stream.write_all(&buf).await {
|
match stream.write_all(&buf).await {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(err) => {
|
Err(err) => Err(Error::SocketError(format!(
|
||||||
return Err(Error::SocketError(format!(
|
"Error writing to socket - Error: {:?}",
|
||||||
"Error writing to socket - Error: {:?}",
|
err
|
||||||
err
|
))),
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -580,12 +609,10 @@ where
|
|||||||
{
|
{
|
||||||
match stream.write_all(buf).await {
|
match stream.write_all(buf).await {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(err) => {
|
Err(err) => Err(Error::SocketError(format!(
|
||||||
return Err(Error::SocketError(format!(
|
"Error writing to socket - Error: {:?}",
|
||||||
"Error writing to socket - Error: {:?}",
|
err
|
||||||
err
|
))),
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -596,19 +623,15 @@ where
|
|||||||
match stream.write_all(buf).await {
|
match stream.write_all(buf).await {
|
||||||
Ok(_) => match stream.flush().await {
|
Ok(_) => match stream.flush().await {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(err) => {
|
Err(err) => Err(Error::SocketError(format!(
|
||||||
return Err(Error::SocketError(format!(
|
"Error flushing socket - Error: {:?}",
|
||||||
"Error flushing socket - Error: {:?}",
|
|
||||||
err
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(err) => {
|
|
||||||
return Err(Error::SocketError(format!(
|
|
||||||
"Error writing to socket - Error: {:?}",
|
|
||||||
err
|
err
|
||||||
)))
|
))),
|
||||||
}
|
},
|
||||||
|
Err(err) => Err(Error::SocketError(format!(
|
||||||
|
"Error writing to socket - Error: {:?}",
|
||||||
|
err
|
||||||
|
))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -710,6 +733,10 @@ pub fn configure_socket(stream: &TcpStream) {
|
|||||||
}
|
}
|
||||||
Err(err) => error!("Could not configure socket: {}", err),
|
Err(err) => error!("Could not configure socket: {}", err),
|
||||||
}
|
}
|
||||||
|
match sock_ref.set_nodelay(true) {
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(err) => error!("Could not configure TCP_NODELAY for socket: {}", err),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait BytesMutReader {
|
pub trait BytesMutReader {
|
||||||
@@ -723,11 +750,71 @@ impl BytesMutReader for Cursor<&BytesMut> {
|
|||||||
let mut buf = vec![];
|
let mut buf = vec![];
|
||||||
match self.read_until(b'\0', &mut buf) {
|
match self.read_until(b'\0', &mut buf) {
|
||||||
Ok(_) => Ok(String::from_utf8_lossy(&buf[..buf.len() - 1]).to_string()),
|
Ok(_) => Ok(String::from_utf8_lossy(&buf[..buf.len() - 1]).to_string()),
|
||||||
Err(err) => return Err(Error::ParseBytesError(err.to_string())),
|
Err(err) => Err(Error::ParseBytesError(err.to_string())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl BytesMutReader for BytesMut {
|
||||||
|
/// Should only be used when reading strings from the message protocol.
|
||||||
|
/// Can be used to read multiple strings from the same message which are separated by the null byte
|
||||||
|
fn read_string(&mut self) -> Result<String, Error> {
|
||||||
|
let null_index = self.iter().position(|&byte| byte == b'\0');
|
||||||
|
|
||||||
|
match null_index {
|
||||||
|
Some(index) => {
|
||||||
|
let string_bytes = self.split_to(index + 1);
|
||||||
|
Ok(String::from_utf8_lossy(&string_bytes[..string_bytes.len() - 1]).to_string())
|
||||||
|
}
|
||||||
|
None => Err(Error::ParseBytesError("Could not read string".to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum ExtendedProtocolData {
|
||||||
|
Parse {
|
||||||
|
data: BytesMut,
|
||||||
|
metadata: Option<(Arc<Parse>, u64)>,
|
||||||
|
},
|
||||||
|
Bind {
|
||||||
|
data: BytesMut,
|
||||||
|
metadata: Option<String>,
|
||||||
|
},
|
||||||
|
Describe {
|
||||||
|
data: BytesMut,
|
||||||
|
metadata: Option<String>,
|
||||||
|
},
|
||||||
|
Execute {
|
||||||
|
data: BytesMut,
|
||||||
|
},
|
||||||
|
Close {
|
||||||
|
data: BytesMut,
|
||||||
|
close: Close,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExtendedProtocolData {
|
||||||
|
pub fn create_new_parse(data: BytesMut, metadata: Option<(Arc<Parse>, u64)>) -> Self {
|
||||||
|
Self::Parse { data, metadata }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_new_bind(data: BytesMut, metadata: Option<String>) -> Self {
|
||||||
|
Self::Bind { data, metadata }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_new_describe(data: BytesMut, metadata: Option<String>) -> Self {
|
||||||
|
Self::Describe { data, metadata }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_new_execute(data: BytesMut) -> Self {
|
||||||
|
Self::Execute { data }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_new_close(data: BytesMut, close: Close) -> Self {
|
||||||
|
Self::Close { data, close }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Parse (F) message.
|
/// Parse (F) message.
|
||||||
/// See: <https://www.postgresql.org/docs/current/protocol-message-formats.html>
|
/// See: <https://www.postgresql.org/docs/current/protocol-message-formats.html>
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -736,7 +823,6 @@ pub struct Parse {
|
|||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
len: i32,
|
len: i32,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub generated_name: String,
|
|
||||||
query: String,
|
query: String,
|
||||||
num_params: i16,
|
num_params: i16,
|
||||||
param_types: Vec<i32>,
|
param_types: Vec<i32>,
|
||||||
@@ -762,7 +848,6 @@ impl TryFrom<&BytesMut> for Parse {
|
|||||||
code,
|
code,
|
||||||
len,
|
len,
|
||||||
name,
|
name,
|
||||||
generated_name: prepared_statement_name(),
|
|
||||||
query,
|
query,
|
||||||
num_params,
|
num_params,
|
||||||
param_types,
|
param_types,
|
||||||
@@ -811,11 +896,44 @@ impl TryFrom<&Parse> for BytesMut {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Parse {
|
impl Parse {
|
||||||
pub fn rename(mut self) -> Self {
|
/// Renames the prepared statement to a new name based on the global counter
|
||||||
self.name = self.generated_name.to_string();
|
pub fn rewrite(mut self) -> Self {
|
||||||
|
self.name = format!(
|
||||||
|
"PGCAT_{}",
|
||||||
|
PREPARED_STATEMENT_COUNTER.fetch_add(1, Ordering::SeqCst)
|
||||||
|
);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Gets the name of the prepared statement from the buffer
|
||||||
|
pub fn get_name(buf: &BytesMut) -> Result<String, Error> {
|
||||||
|
let mut cursor = Cursor::new(buf);
|
||||||
|
// Skip the code and length
|
||||||
|
cursor.advance(mem::size_of::<u8>() + mem::size_of::<i32>());
|
||||||
|
cursor.read_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hashes the parse statement to be used as a key in the global cache
|
||||||
|
pub fn get_hash(&self) -> u64 {
|
||||||
|
// TODO_ZAIN: Take a look at which hashing function is being used
|
||||||
|
let mut hasher = DefaultHasher::new();
|
||||||
|
|
||||||
|
let concatenated = format!(
|
||||||
|
"{}{}{}",
|
||||||
|
self.query,
|
||||||
|
self.num_params,
|
||||||
|
self.param_types
|
||||||
|
.iter()
|
||||||
|
.map(ToString::to_string)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(",")
|
||||||
|
);
|
||||||
|
|
||||||
|
concatenated.hash(&mut hasher);
|
||||||
|
|
||||||
|
hasher.finish()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn anonymous(&self) -> bool {
|
pub fn anonymous(&self) -> bool {
|
||||||
self.name.is_empty()
|
self.name.is_empty()
|
||||||
}
|
}
|
||||||
@@ -946,9 +1064,42 @@ impl TryFrom<Bind> for BytesMut {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Bind {
|
impl Bind {
|
||||||
pub fn reassign(mut self, parse: &Parse) -> Self {
|
/// Gets the name of the prepared statement from the buffer
|
||||||
self.prepared_statement = parse.name.clone();
|
pub fn get_name(buf: &BytesMut) -> Result<String, Error> {
|
||||||
self
|
let mut cursor = Cursor::new(buf);
|
||||||
|
// Skip the code and length
|
||||||
|
cursor.advance(mem::size_of::<u8>() + mem::size_of::<i32>());
|
||||||
|
cursor.read_string()?;
|
||||||
|
cursor.read_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renames the prepared statement to a new name
|
||||||
|
pub fn rename(buf: BytesMut, new_name: &str) -> Result<BytesMut, Error> {
|
||||||
|
let mut cursor = Cursor::new(&buf);
|
||||||
|
// Read basic data from the cursor
|
||||||
|
let code = cursor.get_u8();
|
||||||
|
let current_len = cursor.get_i32();
|
||||||
|
let portal = cursor.read_string()?;
|
||||||
|
let prepared_statement = cursor.read_string()?;
|
||||||
|
|
||||||
|
// Calculate new length
|
||||||
|
let new_len = current_len + new_name.len() as i32 - prepared_statement.len() as i32;
|
||||||
|
|
||||||
|
// Begin building the response buffer
|
||||||
|
let mut response_buf = BytesMut::with_capacity(new_len as usize + 1);
|
||||||
|
response_buf.put_u8(code);
|
||||||
|
response_buf.put_i32(new_len);
|
||||||
|
|
||||||
|
// Put the portal and new name into the buffer
|
||||||
|
// Note: panic if the provided string contains null byte
|
||||||
|
response_buf.put_slice(CString::new(portal)?.as_bytes_with_nul());
|
||||||
|
response_buf.put_slice(CString::new(new_name)?.as_bytes_with_nul());
|
||||||
|
|
||||||
|
// Add the remainder of the original buffer into the response
|
||||||
|
response_buf.put_slice(&buf[cursor.position() as usize..]);
|
||||||
|
|
||||||
|
// Return the buffer
|
||||||
|
Ok(response_buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn anonymous(&self) -> bool {
|
pub fn anonymous(&self) -> bool {
|
||||||
@@ -962,7 +1113,7 @@ pub struct Describe {
|
|||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
len: i32,
|
len: i32,
|
||||||
target: char,
|
pub target: char,
|
||||||
pub statement_name: String,
|
pub statement_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1004,6 +1155,15 @@ impl TryFrom<Describe> for BytesMut {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Describe {
|
impl Describe {
|
||||||
|
pub fn empty_new() -> Describe {
|
||||||
|
Describe {
|
||||||
|
code: 'D',
|
||||||
|
len: 4 + 1 + 1,
|
||||||
|
target: 'S',
|
||||||
|
statement_name: "".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn rename(mut self, name: &str) -> Self {
|
pub fn rename(mut self, name: &str) -> Self {
|
||||||
self.statement_name = name.to_string();
|
self.statement_name = name.to_string();
|
||||||
self
|
self
|
||||||
@@ -1092,9 +1252,297 @@ pub fn close_complete() -> BytesMut {
|
|||||||
bytes
|
bytes
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn prepared_statement_name() -> String {
|
// from https://www.postgresql.org/docs/12/protocol-error-fields.html
|
||||||
format!(
|
#[derive(Debug, Default, PartialEq)]
|
||||||
"P_{}",
|
pub struct PgErrorMsg {
|
||||||
PREPARED_STATEMENT_COUNTER.fetch_add(1, Ordering::SeqCst)
|
pub severity_localized: String, // S
|
||||||
)
|
pub severity: String, // V
|
||||||
|
pub code: String, // C
|
||||||
|
pub message: String, // M
|
||||||
|
pub detail: Option<String>, // D
|
||||||
|
pub hint: Option<String>, // H
|
||||||
|
pub position: Option<u32>, // P
|
||||||
|
pub internal_position: Option<u32>, // p
|
||||||
|
pub internal_query: Option<String>, // q
|
||||||
|
pub where_context: Option<String>, // W
|
||||||
|
pub schema_name: Option<String>, // s
|
||||||
|
pub table_name: Option<String>, // t
|
||||||
|
pub column_name: Option<String>, // c
|
||||||
|
pub data_type_name: Option<String>, // d
|
||||||
|
pub constraint_name: Option<String>, // n
|
||||||
|
pub file_name: Option<String>, // F
|
||||||
|
pub line: Option<u32>, // L
|
||||||
|
pub routine: Option<String>, // R
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: implement with https://docs.rs/derive_more/latest/derive_more/
|
||||||
|
impl Display for PgErrorMsg {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "[severity: {}]", self.severity)?;
|
||||||
|
write!(f, "[code: {}]", self.code)?;
|
||||||
|
write!(f, "[message: {}]", self.message)?;
|
||||||
|
if let Some(val) = &self.detail {
|
||||||
|
write!(f, "[detail: {val}]")?;
|
||||||
|
}
|
||||||
|
if let Some(val) = &self.hint {
|
||||||
|
write!(f, "[hint: {val}]")?;
|
||||||
|
}
|
||||||
|
if let Some(val) = &self.position {
|
||||||
|
write!(f, "[position: {val}]")?;
|
||||||
|
}
|
||||||
|
if let Some(val) = &self.internal_position {
|
||||||
|
write!(f, "[internal_position: {val}]")?;
|
||||||
|
}
|
||||||
|
if let Some(val) = &self.internal_query {
|
||||||
|
write!(f, "[internal_query: {val}]")?;
|
||||||
|
}
|
||||||
|
if let Some(val) = &self.internal_query {
|
||||||
|
write!(f, "[internal_query: {val}]")?;
|
||||||
|
}
|
||||||
|
if let Some(val) = &self.where_context {
|
||||||
|
write!(f, "[where: {val}]")?;
|
||||||
|
}
|
||||||
|
if let Some(val) = &self.schema_name {
|
||||||
|
write!(f, "[schema_name: {val}]")?;
|
||||||
|
}
|
||||||
|
if let Some(val) = &self.table_name {
|
||||||
|
write!(f, "[table_name: {val}]")?;
|
||||||
|
}
|
||||||
|
if let Some(val) = &self.column_name {
|
||||||
|
write!(f, "[column_name: {val}]")?;
|
||||||
|
}
|
||||||
|
if let Some(val) = &self.data_type_name {
|
||||||
|
write!(f, "[data_type_name: {val}]")?;
|
||||||
|
}
|
||||||
|
if let Some(val) = &self.constraint_name {
|
||||||
|
write!(f, "[constraint_name: {val}]")?;
|
||||||
|
}
|
||||||
|
if let Some(val) = &self.file_name {
|
||||||
|
write!(f, "[file_name: {val}]")?;
|
||||||
|
}
|
||||||
|
if let Some(val) = &self.line {
|
||||||
|
write!(f, "[line: {val}]")?;
|
||||||
|
}
|
||||||
|
if let Some(val) = &self.routine {
|
||||||
|
write!(f, "[routine: {val}]")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
write!(f, " ")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PgErrorMsg {
|
||||||
|
pub fn parse(error_msg: &[u8]) -> Result<PgErrorMsg, Error> {
|
||||||
|
let mut out = PgErrorMsg {
|
||||||
|
severity_localized: "".to_string(),
|
||||||
|
severity: "".to_string(),
|
||||||
|
code: "".to_string(),
|
||||||
|
message: "".to_string(),
|
||||||
|
detail: None,
|
||||||
|
hint: None,
|
||||||
|
position: None,
|
||||||
|
internal_position: None,
|
||||||
|
internal_query: None,
|
||||||
|
where_context: None,
|
||||||
|
schema_name: None,
|
||||||
|
table_name: None,
|
||||||
|
column_name: None,
|
||||||
|
data_type_name: None,
|
||||||
|
constraint_name: None,
|
||||||
|
file_name: None,
|
||||||
|
line: None,
|
||||||
|
routine: None,
|
||||||
|
};
|
||||||
|
for msg_part in error_msg.split(|v| *v == MESSAGE_TERMINATOR) {
|
||||||
|
if msg_part.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let msg_content = match String::from_utf8_lossy(&msg_part[1..]).parse() {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(err) => {
|
||||||
|
return Err(Error::ServerMessageParserError(format!(
|
||||||
|
"could not parse server message field. err {:?}",
|
||||||
|
err
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match &msg_part[0] {
|
||||||
|
b'S' => {
|
||||||
|
out.severity_localized = msg_content;
|
||||||
|
}
|
||||||
|
b'V' => {
|
||||||
|
out.severity = msg_content;
|
||||||
|
}
|
||||||
|
b'C' => {
|
||||||
|
out.code = msg_content;
|
||||||
|
}
|
||||||
|
b'M' => {
|
||||||
|
out.message = msg_content;
|
||||||
|
}
|
||||||
|
b'D' => {
|
||||||
|
out.detail = Some(msg_content);
|
||||||
|
}
|
||||||
|
b'H' => {
|
||||||
|
out.hint = Some(msg_content);
|
||||||
|
}
|
||||||
|
b'P' => out.position = Some(u32::from_str(msg_content.as_str()).unwrap_or(0)),
|
||||||
|
b'p' => {
|
||||||
|
out.internal_position = Some(u32::from_str(msg_content.as_str()).unwrap_or(0))
|
||||||
|
}
|
||||||
|
b'q' => {
|
||||||
|
out.internal_query = Some(msg_content);
|
||||||
|
}
|
||||||
|
b'W' => {
|
||||||
|
out.where_context = Some(msg_content);
|
||||||
|
}
|
||||||
|
b's' => {
|
||||||
|
out.schema_name = Some(msg_content);
|
||||||
|
}
|
||||||
|
b't' => {
|
||||||
|
out.table_name = Some(msg_content);
|
||||||
|
}
|
||||||
|
b'c' => {
|
||||||
|
out.column_name = Some(msg_content);
|
||||||
|
}
|
||||||
|
b'd' => {
|
||||||
|
out.data_type_name = Some(msg_content);
|
||||||
|
}
|
||||||
|
b'n' => {
|
||||||
|
out.constraint_name = Some(msg_content);
|
||||||
|
}
|
||||||
|
b'F' => {
|
||||||
|
out.file_name = Some(msg_content);
|
||||||
|
}
|
||||||
|
b'L' => out.line = Some(u32::from_str(msg_content.as_str()).unwrap_or(0)),
|
||||||
|
b'R' => {
|
||||||
|
out.routine = Some(msg_content);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::messages::PgErrorMsg;
|
||||||
|
use log::{error, info};
|
||||||
|
|
||||||
|
fn field(kind: char, content: &str) -> Vec<u8> {
|
||||||
|
format!("{kind}{content}\0").as_bytes().to_vec()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_fields() {
|
||||||
|
let mut complete_msg = vec![];
|
||||||
|
let severity = "FATAL";
|
||||||
|
complete_msg.extend(field('S', severity));
|
||||||
|
complete_msg.extend(field('V', severity));
|
||||||
|
|
||||||
|
let error_code = "29P02";
|
||||||
|
complete_msg.extend(field('C', error_code));
|
||||||
|
let message = "password authentication failed for user \"wrong_user\"";
|
||||||
|
complete_msg.extend(field('M', message));
|
||||||
|
let detail_msg = "super detailed message";
|
||||||
|
complete_msg.extend(field('D', detail_msg));
|
||||||
|
let hint_msg = "hint detail here";
|
||||||
|
complete_msg.extend(field('H', hint_msg));
|
||||||
|
complete_msg.extend(field('P', "123"));
|
||||||
|
complete_msg.extend(field('p', "234"));
|
||||||
|
let internal_query = "SELECT * from foo;";
|
||||||
|
complete_msg.extend(field('q', internal_query));
|
||||||
|
let where_msg = "where goes here";
|
||||||
|
complete_msg.extend(field('W', where_msg));
|
||||||
|
let schema_msg = "schema_name";
|
||||||
|
complete_msg.extend(field('s', schema_msg));
|
||||||
|
let table_msg = "table_name";
|
||||||
|
complete_msg.extend(field('t', table_msg));
|
||||||
|
let column_msg = "column_name";
|
||||||
|
complete_msg.extend(field('c', column_msg));
|
||||||
|
let data_type_msg = "type_name";
|
||||||
|
complete_msg.extend(field('d', data_type_msg));
|
||||||
|
let constraint_msg = "constraint_name";
|
||||||
|
complete_msg.extend(field('n', constraint_msg));
|
||||||
|
let file_msg = "pgcat.c";
|
||||||
|
complete_msg.extend(field('F', file_msg));
|
||||||
|
complete_msg.extend(field('L', "335"));
|
||||||
|
let routine_msg = "my_failing_routine";
|
||||||
|
complete_msg.extend(field('R', routine_msg));
|
||||||
|
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_max_level(tracing::Level::INFO)
|
||||||
|
.with_ansi(true)
|
||||||
|
.init();
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"full message: {}",
|
||||||
|
PgErrorMsg::parse(&complete_msg).unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
PgErrorMsg {
|
||||||
|
severity_localized: severity.to_string(),
|
||||||
|
severity: severity.to_string(),
|
||||||
|
code: error_code.to_string(),
|
||||||
|
message: message.to_string(),
|
||||||
|
detail: Some(detail_msg.to_string()),
|
||||||
|
hint: Some(hint_msg.to_string()),
|
||||||
|
position: Some(123),
|
||||||
|
internal_position: Some(234),
|
||||||
|
internal_query: Some(internal_query.to_string()),
|
||||||
|
where_context: Some(where_msg.to_string()),
|
||||||
|
schema_name: Some(schema_msg.to_string()),
|
||||||
|
table_name: Some(table_msg.to_string()),
|
||||||
|
column_name: Some(column_msg.to_string()),
|
||||||
|
data_type_name: Some(data_type_msg.to_string()),
|
||||||
|
constraint_name: Some(constraint_msg.to_string()),
|
||||||
|
file_name: Some(file_msg.to_string()),
|
||||||
|
line: Some(335),
|
||||||
|
routine: Some(routine_msg.to_string()),
|
||||||
|
},
|
||||||
|
PgErrorMsg::parse(&complete_msg).unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut only_mandatory_msg = vec![];
|
||||||
|
only_mandatory_msg.extend(field('S', severity));
|
||||||
|
only_mandatory_msg.extend(field('V', severity));
|
||||||
|
only_mandatory_msg.extend(field('C', error_code));
|
||||||
|
only_mandatory_msg.extend(field('M', message));
|
||||||
|
only_mandatory_msg.extend(field('D', detail_msg));
|
||||||
|
|
||||||
|
let err_fields = PgErrorMsg::parse(&only_mandatory_msg).unwrap();
|
||||||
|
info!("only mandatory fields: {}", &err_fields);
|
||||||
|
error!(
|
||||||
|
"server error: {}: {}",
|
||||||
|
err_fields.severity, err_fields.message
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
PgErrorMsg {
|
||||||
|
severity_localized: severity.to_string(),
|
||||||
|
severity: severity.to_string(),
|
||||||
|
code: error_code.to_string(),
|
||||||
|
message: message.to_string(),
|
||||||
|
detail: Some(detail_msg.to_string()),
|
||||||
|
hint: None,
|
||||||
|
position: None,
|
||||||
|
internal_position: None,
|
||||||
|
internal_query: None,
|
||||||
|
where_context: None,
|
||||||
|
schema_name: None,
|
||||||
|
table_name: None,
|
||||||
|
column_name: None,
|
||||||
|
data_type_name: None,
|
||||||
|
constraint_name: None,
|
||||||
|
file_name: None,
|
||||||
|
line: None,
|
||||||
|
routine: None,
|
||||||
|
},
|
||||||
|
PgErrorMsg::parse(&only_mandatory_msg).unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,14 +23,15 @@ impl MirroredClient {
|
|||||||
async fn create_pool(&self) -> Pool<ServerPool> {
|
async fn create_pool(&self) -> Pool<ServerPool> {
|
||||||
let config = get_config();
|
let config = get_config();
|
||||||
let default = std::time::Duration::from_millis(10_000).as_millis() as u64;
|
let default = std::time::Duration::from_millis(10_000).as_millis() as u64;
|
||||||
let (connection_timeout, idle_timeout, _cfg) =
|
let (connection_timeout, idle_timeout, _cfg, prepared_statement_cache_size) =
|
||||||
match config.pools.get(&self.address.pool_name) {
|
match config.pools.get(&self.address.pool_name) {
|
||||||
Some(cfg) => (
|
Some(cfg) => (
|
||||||
cfg.connect_timeout.unwrap_or(default),
|
cfg.connect_timeout.unwrap_or(default),
|
||||||
cfg.idle_timeout.unwrap_or(default),
|
cfg.idle_timeout.unwrap_or(default),
|
||||||
cfg.clone(),
|
cfg.clone(),
|
||||||
|
cfg.prepared_statements_cache_size,
|
||||||
),
|
),
|
||||||
None => (default, default, crate::config::Pool::default()),
|
None => (default, default, crate::config::Pool::default(), 0),
|
||||||
};
|
};
|
||||||
|
|
||||||
let manager = ServerPool::new(
|
let manager = ServerPool::new(
|
||||||
@@ -41,6 +42,8 @@ impl MirroredClient {
|
|||||||
Arc::new(RwLock::new(None)),
|
Arc::new(RwLock::new(None)),
|
||||||
None,
|
None,
|
||||||
true,
|
true,
|
||||||
|
false,
|
||||||
|
prepared_statement_cache_size,
|
||||||
);
|
);
|
||||||
|
|
||||||
Pool::builder()
|
Pool::builder()
|
||||||
@@ -78,12 +81,13 @@ impl MirroredClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Incoming data from server (we read to clear the socket buffer and discard the data)
|
// Incoming data from server (we read to clear the socket buffer and discard the data)
|
||||||
recv_result = server.recv() => {
|
recv_result = server.recv(None) => {
|
||||||
match recv_result {
|
match recv_result {
|
||||||
Ok(message) => trace!("Received from mirror: {} {:?}", String::from_utf8_lossy(&message[..]), address.clone()),
|
Ok(message) => trace!("Received from mirror: {} {:?}", String::from_utf8_lossy(&message[..]), address.clone()),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
server.mark_bad();
|
server.mark_bad(
|
||||||
error!("Failed to receive from mirror {:?} {:?}", err, address.clone());
|
format!("Failed to send to mirror, Discarding message {:?}, {:?}", err, address.clone()).as_str()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,8 +99,9 @@ impl MirroredClient {
|
|||||||
match server.send(&BytesMut::from(&bytes[..])).await {
|
match server.send(&BytesMut::from(&bytes[..])).await {
|
||||||
Ok(_) => trace!("Sent to mirror: {} {:?}", String::from_utf8_lossy(&bytes[..]), address.clone()),
|
Ok(_) => trace!("Sent to mirror: {} {:?}", String::from_utf8_lossy(&bytes[..]), address.clone()),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
server.mark_bad();
|
server.mark_bad(
|
||||||
error!("Failed to send to mirror, Discarding message {:?}, {:?}", err, address.clone())
|
format!("Failed to receive from mirror {:?} {:?}", err, address.clone()).as_str()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -136,18 +141,18 @@ impl MirroringManager {
|
|||||||
bytes_rx,
|
bytes_rx,
|
||||||
disconnect_rx: exit_rx,
|
disconnect_rx: exit_rx,
|
||||||
};
|
};
|
||||||
exit_senders.push(exit_tx.clone());
|
exit_senders.push(exit_tx);
|
||||||
byte_senders.push(bytes_tx.clone());
|
byte_senders.push(bytes_tx);
|
||||||
client.start();
|
client.start();
|
||||||
});
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
byte_senders: byte_senders,
|
byte_senders,
|
||||||
disconnect_senders: exit_senders,
|
disconnect_senders: exit_senders,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send(self: &mut Self, bytes: &BytesMut) {
|
pub fn send(&mut self, bytes: &BytesMut) {
|
||||||
// We want to avoid performing an allocation if we won't be able to send the message
|
// 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
|
// 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
|
// closed or the capacity is reduced to 0, but mirroring is best effort anyway
|
||||||
@@ -169,7 +174,7 @@ impl MirroringManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn disconnect(self: &mut Self) {
|
pub fn disconnect(&mut self) {
|
||||||
self.disconnect_senders
|
self.disconnect_senders
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.for_each(|sender| match sender.try_send(()) {
|
.for_each(|sender| match sender.try_send(()) {
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ impl<'a> Plugin for Intercept<'a> {
|
|||||||
.map(|s| {
|
.map(|s| {
|
||||||
let s = s.as_str().to_string();
|
let s = s.as_str().to_string();
|
||||||
|
|
||||||
if s == "" {
|
if s.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(s)
|
Some(s)
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ pub enum PluginOutput {
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait Plugin {
|
pub trait Plugin {
|
||||||
// Run before the query is sent to the server.
|
// Run before the query is sent to the server.
|
||||||
|
#[allow(clippy::ptr_arg)]
|
||||||
async fn run(
|
async fn run(
|
||||||
&mut self,
|
&mut self,
|
||||||
query_router: &QueryRouter,
|
query_router: &QueryRouter,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ impl<'a> Prewarmer<'a> {
|
|||||||
self.server.address(),
|
self.server.address(),
|
||||||
query
|
query
|
||||||
);
|
);
|
||||||
self.server.query(&query).await?;
|
self.server.query(query).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ impl<'a> Plugin for QueryLogger<'a> {
|
|||||||
.map(|q| q.to_string())
|
.map(|q| q.to_string())
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
.join("; ");
|
.join("; ");
|
||||||
info!("[pool: {}][user: {}] {}", self.user, self.db, query);
|
info!("[pool: {}][user: {}] {}", self.db, self.user, query);
|
||||||
|
|
||||||
Ok(PluginOutput::Allow)
|
Ok(PluginOutput::Allow)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ impl<'a> Plugin for TableAccess<'a> {
|
|||||||
|
|
||||||
visit_relations(ast, |relation| {
|
visit_relations(ast, |relation| {
|
||||||
let relation = relation.to_string();
|
let relation = relation.to_string();
|
||||||
let parts = relation.split(".").collect::<Vec<&str>>();
|
let parts = relation.split('.').collect::<Vec<&str>>();
|
||||||
let table_name = parts.last().unwrap();
|
let table_name = parts.last().unwrap();
|
||||||
|
|
||||||
if self.tables.contains(&table_name.to_string()) {
|
if self.tables.contains(&table_name.to_string()) {
|
||||||
|
|||||||
309
src/pool.rs
309
src/pool.rs
@@ -1,9 +1,9 @@
|
|||||||
use arc_swap::ArcSwap;
|
use arc_swap::ArcSwap;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use bb8::{ManageConnection, Pool, PooledConnection, QueueStrategy};
|
use bb8::{ManageConnection, Pool, PooledConnection, QueueStrategy};
|
||||||
use bytes::{BufMut, BytesMut};
|
|
||||||
use chrono::naive::NaiveDateTime;
|
use chrono::naive::NaiveDateTime;
|
||||||
use log::{debug, error, info, warn};
|
use log::{debug, error, info, warn};
|
||||||
|
use lru::LruCache;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use parking_lot::{Mutex, RwLock};
|
use parking_lot::{Mutex, RwLock};
|
||||||
use rand::seq::SliceRandom;
|
use rand::seq::SliceRandom;
|
||||||
@@ -11,6 +11,8 @@ use rand::thread_rng;
|
|||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fmt::{Display, Formatter};
|
use std::fmt::{Display, Formatter};
|
||||||
|
use std::num::NonZeroUsize;
|
||||||
|
use std::sync::atomic::AtomicU64;
|
||||||
use std::sync::{
|
use std::sync::{
|
||||||
atomic::{AtomicBool, Ordering},
|
atomic::{AtomicBool, Ordering},
|
||||||
Arc,
|
Arc,
|
||||||
@@ -19,13 +21,14 @@ use std::time::Instant;
|
|||||||
use tokio::sync::Notify;
|
use tokio::sync::Notify;
|
||||||
|
|
||||||
use crate::config::{
|
use crate::config::{
|
||||||
get_config, Address, General, LoadBalancingMode, Plugins, PoolMode, Role, User,
|
get_config, Address, DefaultShard, General, LoadBalancingMode, Plugins, PoolMode, Role, User,
|
||||||
};
|
};
|
||||||
use crate::errors::Error;
|
use crate::errors::Error;
|
||||||
|
|
||||||
use crate::auth_passthrough::AuthPassthrough;
|
use crate::auth_passthrough::AuthPassthrough;
|
||||||
|
use crate::messages::Parse;
|
||||||
use crate::plugins::prewarmer;
|
use crate::plugins::prewarmer;
|
||||||
use crate::server::Server;
|
use crate::server::{Server, ServerParameters};
|
||||||
use crate::sharding::ShardingFunction;
|
use crate::sharding::ShardingFunction;
|
||||||
use crate::stats::{AddressStats, ClientStats, ServerStats};
|
use crate::stats::{AddressStats, ClientStats, ServerStats};
|
||||||
|
|
||||||
@@ -54,6 +57,57 @@ pub enum BanReason {
|
|||||||
AdminBan(i64),
|
AdminBan(i64),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub type PreparedStatementCacheType = Arc<Mutex<PreparedStatementCache>>;
|
||||||
|
|
||||||
|
// TODO: Add stats the this cache
|
||||||
|
// TODO: Add application name to the cache value to help identify which application is using the cache
|
||||||
|
// TODO: Create admin command to show which statements are in the cache
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct PreparedStatementCache {
|
||||||
|
cache: LruCache<u64, Arc<Parse>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PreparedStatementCache {
|
||||||
|
pub fn new(mut size: usize) -> Self {
|
||||||
|
// Cannot be zeros
|
||||||
|
if size == 0 {
|
||||||
|
size = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
PreparedStatementCache {
|
||||||
|
cache: LruCache::new(NonZeroUsize::new(size).unwrap()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds the prepared statement to the cache if it doesn't exist with a new name
|
||||||
|
/// if it already exists will give you the existing parse
|
||||||
|
///
|
||||||
|
/// Pass the hash to this so that we can do the compute before acquiring the lock
|
||||||
|
pub fn get_or_insert(&mut self, parse: &Parse, hash: u64) -> Arc<Parse> {
|
||||||
|
match self.cache.get(&hash) {
|
||||||
|
Some(rewritten_parse) => rewritten_parse.clone(),
|
||||||
|
None => {
|
||||||
|
let new_parse = Arc::new(parse.clone().rewrite());
|
||||||
|
let evicted = self.cache.push(hash, new_parse.clone());
|
||||||
|
|
||||||
|
if let Some((_, evicted_parse)) = evicted {
|
||||||
|
debug!(
|
||||||
|
"Evicted prepared statement {} from cache",
|
||||||
|
evicted_parse.name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
new_parse
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marks the hash as most recently used if it exists
|
||||||
|
pub fn promote(&mut self, hash: &u64) {
|
||||||
|
self.cache.promote(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// An identifier for a PgCat pool,
|
/// An identifier for a PgCat pool,
|
||||||
/// a database visible to clients.
|
/// a database visible to clients.
|
||||||
#[derive(Hash, Debug, Clone, PartialEq, Eq, Default)]
|
#[derive(Hash, Debug, Clone, PartialEq, Eq, Default)]
|
||||||
@@ -111,6 +165,12 @@ pub struct PoolSettings {
|
|||||||
// Enable/disable query parser.
|
// Enable/disable query parser.
|
||||||
pub query_parser_enabled: bool,
|
pub query_parser_enabled: bool,
|
||||||
|
|
||||||
|
// Max length of query the parser will parse.
|
||||||
|
pub query_parser_max_length: Option<usize>,
|
||||||
|
|
||||||
|
// Infer role
|
||||||
|
pub query_parser_read_write_splitting: bool,
|
||||||
|
|
||||||
// Read from the primary as well or not.
|
// Read from the primary as well or not.
|
||||||
pub primary_reads_enabled: bool,
|
pub primary_reads_enabled: bool,
|
||||||
|
|
||||||
@@ -135,6 +195,9 @@ pub struct PoolSettings {
|
|||||||
// Regex for searching for the shard id in SQL statements
|
// Regex for searching for the shard id in SQL statements
|
||||||
pub shard_id_regex: Option<Regex>,
|
pub shard_id_regex: Option<Regex>,
|
||||||
|
|
||||||
|
// What to do when no shard is selected in a sharded system
|
||||||
|
pub default_shard: DefaultShard,
|
||||||
|
|
||||||
// Limit how much of each query is searched for a potential shard regex match
|
// Limit how much of each query is searched for a potential shard regex match
|
||||||
pub regex_search_limit: usize,
|
pub regex_search_limit: usize,
|
||||||
|
|
||||||
@@ -157,6 +220,8 @@ impl Default for PoolSettings {
|
|||||||
db: String::default(),
|
db: String::default(),
|
||||||
default_role: None,
|
default_role: None,
|
||||||
query_parser_enabled: false,
|
query_parser_enabled: false,
|
||||||
|
query_parser_max_length: None,
|
||||||
|
query_parser_read_write_splitting: false,
|
||||||
primary_reads_enabled: true,
|
primary_reads_enabled: true,
|
||||||
sharding_function: ShardingFunction::PgBigintHash,
|
sharding_function: ShardingFunction::PgBigintHash,
|
||||||
automatic_sharding_key: None,
|
automatic_sharding_key: None,
|
||||||
@@ -166,6 +231,7 @@ impl Default for PoolSettings {
|
|||||||
sharding_key_regex: None,
|
sharding_key_regex: None,
|
||||||
shard_id_regex: None,
|
shard_id_regex: None,
|
||||||
regex_search_limit: 1000,
|
regex_search_limit: 1000,
|
||||||
|
default_shard: DefaultShard::Shard(0),
|
||||||
auth_query: None,
|
auth_query: None,
|
||||||
auth_query_user: None,
|
auth_query_user: None,
|
||||||
auth_query_password: None,
|
auth_query_password: None,
|
||||||
@@ -178,23 +244,23 @@ impl Default for PoolSettings {
|
|||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct ConnectionPool {
|
pub struct ConnectionPool {
|
||||||
/// The pools handled internally by bb8.
|
/// The pools handled internally by bb8.
|
||||||
databases: Vec<Vec<Pool<ServerPool>>>,
|
databases: Arc<Vec<Vec<Pool<ServerPool>>>>,
|
||||||
|
|
||||||
/// The addresses (host, port, role) to handle
|
/// The addresses (host, port, role) to handle
|
||||||
/// failover and load balancing deterministically.
|
/// failover and load balancing deterministically.
|
||||||
addresses: Vec<Vec<Address>>,
|
addresses: Arc<Vec<Vec<Address>>>,
|
||||||
|
|
||||||
/// List of banned addresses (see above)
|
/// List of banned addresses (see above)
|
||||||
/// that should not be queried.
|
/// that should not be queried.
|
||||||
banlist: BanList,
|
banlist: BanList,
|
||||||
|
|
||||||
/// The server information (K messages) have to be passed to the
|
/// The server information has to be passed to the
|
||||||
/// clients on startup. We pre-connect to all shards and replicas
|
/// clients on startup. We pre-connect to all shards and replicas
|
||||||
/// on pool creation and save the K messages here.
|
/// on pool creation and save the startup parameters here.
|
||||||
server_info: Arc<RwLock<BytesMut>>,
|
original_server_parameters: Arc<RwLock<ServerParameters>>,
|
||||||
|
|
||||||
/// Pool configuration.
|
/// Pool configuration.
|
||||||
pub settings: PoolSettings,
|
pub settings: Arc<PoolSettings>,
|
||||||
|
|
||||||
/// If not validated, we need to double check the pool is available before allowing a client
|
/// If not validated, we need to double check the pool is available before allowing a client
|
||||||
/// to use it.
|
/// to use it.
|
||||||
@@ -211,6 +277,9 @@ pub struct ConnectionPool {
|
|||||||
|
|
||||||
/// AuthInfo
|
/// AuthInfo
|
||||||
pub auth_hash: Arc<RwLock<Option<String>>>,
|
pub auth_hash: Arc<RwLock<Option<String>>>,
|
||||||
|
|
||||||
|
/// Cache
|
||||||
|
pub prepared_statement_cache: Option<PreparedStatementCacheType>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConnectionPool {
|
impl ConnectionPool {
|
||||||
@@ -229,20 +298,17 @@ impl ConnectionPool {
|
|||||||
let old_pool_ref = get_pool(pool_name, &user.username);
|
let old_pool_ref = get_pool(pool_name, &user.username);
|
||||||
let identifier = PoolIdentifier::new(pool_name, &user.username);
|
let identifier = PoolIdentifier::new(pool_name, &user.username);
|
||||||
|
|
||||||
match old_pool_ref {
|
if let Some(pool) = old_pool_ref {
|
||||||
Some(pool) => {
|
// If the pool hasn't changed, get existing reference and insert it into the new_pools.
|
||||||
// If the pool hasn't changed, get existing reference and insert it into the new_pools.
|
// We replace all pools at the end, but if the reference is kept, the pool won't get re-created (bb8).
|
||||||
// We replace all pools at the end, but if the reference is kept, the pool won't get re-created (bb8).
|
if pool.config_hash == new_pool_hash_value {
|
||||||
if pool.config_hash == new_pool_hash_value {
|
info!(
|
||||||
info!(
|
"[pool: {}][user: {}] has not changed",
|
||||||
"[pool: {}][user: {}] has not changed",
|
pool_name, user.username
|
||||||
pool_name, user.username
|
);
|
||||||
);
|
new_pools.insert(identifier.clone(), pool.clone());
|
||||||
new_pools.insert(identifier.clone(), pool.clone());
|
continue;
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
None => (),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
@@ -292,6 +358,7 @@ impl ConnectionPool {
|
|||||||
pool_name: pool_name.clone(),
|
pool_name: pool_name.clone(),
|
||||||
mirrors: vec![],
|
mirrors: vec![],
|
||||||
stats: Arc::new(AddressStats::default()),
|
stats: Arc::new(AddressStats::default()),
|
||||||
|
error_count: Arc::new(AtomicU64::new(0)),
|
||||||
});
|
});
|
||||||
address_id += 1;
|
address_id += 1;
|
||||||
}
|
}
|
||||||
@@ -310,6 +377,7 @@ impl ConnectionPool {
|
|||||||
pool_name: pool_name.clone(),
|
pool_name: pool_name.clone(),
|
||||||
mirrors: mirror_addresses,
|
mirrors: mirror_addresses,
|
||||||
stats: Arc::new(AddressStats::default()),
|
stats: Arc::new(AddressStats::default()),
|
||||||
|
error_count: Arc::new(AtomicU64::new(0)),
|
||||||
};
|
};
|
||||||
|
|
||||||
address_id += 1;
|
address_id += 1;
|
||||||
@@ -364,16 +432,24 @@ impl ConnectionPool {
|
|||||||
None => config.plugins.clone(),
|
None => config.plugins.clone(),
|
||||||
},
|
},
|
||||||
pool_config.cleanup_server_connections,
|
pool_config.cleanup_server_connections,
|
||||||
|
pool_config.log_client_parameter_status_changes,
|
||||||
|
pool_config.prepared_statements_cache_size,
|
||||||
);
|
);
|
||||||
|
|
||||||
let connect_timeout = match pool_config.connect_timeout {
|
let connect_timeout = match user.connect_timeout {
|
||||||
Some(connect_timeout) => connect_timeout,
|
Some(connect_timeout) => connect_timeout,
|
||||||
None => config.general.connect_timeout,
|
None => match pool_config.connect_timeout {
|
||||||
|
Some(connect_timeout) => connect_timeout,
|
||||||
|
None => config.general.connect_timeout,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let idle_timeout = match pool_config.idle_timeout {
|
let idle_timeout = match user.idle_timeout {
|
||||||
Some(idle_timeout) => idle_timeout,
|
Some(idle_timeout) => idle_timeout,
|
||||||
None => config.general.idle_timeout,
|
None => match pool_config.idle_timeout {
|
||||||
|
Some(idle_timeout) => idle_timeout,
|
||||||
|
None => config.general.idle_timeout,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let server_lifetime = match user.server_lifetime {
|
let server_lifetime = match user.server_lifetime {
|
||||||
@@ -384,7 +460,7 @@ impl ConnectionPool {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let reaper_rate = *vec![idle_timeout, server_lifetime, POOL_REAPER_RATE]
|
let reaper_rate = *[idle_timeout, server_lifetime, POOL_REAPER_RATE]
|
||||||
.iter()
|
.iter()
|
||||||
.min()
|
.min()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -433,13 +509,13 @@ impl ConnectionPool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let pool = ConnectionPool {
|
let pool = ConnectionPool {
|
||||||
databases: shards,
|
databases: Arc::new(shards),
|
||||||
addresses,
|
addresses: Arc::new(addresses),
|
||||||
banlist: Arc::new(RwLock::new(banlist)),
|
banlist: Arc::new(RwLock::new(banlist)),
|
||||||
config_hash: new_pool_hash_value,
|
config_hash: new_pool_hash_value,
|
||||||
server_info: Arc::new(RwLock::new(BytesMut::new())),
|
original_server_parameters: Arc::new(RwLock::new(ServerParameters::new())),
|
||||||
auth_hash: pool_auth_hash,
|
auth_hash: pool_auth_hash,
|
||||||
settings: PoolSettings {
|
settings: Arc::new(PoolSettings {
|
||||||
pool_mode: match user.pool_mode {
|
pool_mode: match user.pool_mode {
|
||||||
Some(pool_mode) => pool_mode,
|
Some(pool_mode) => pool_mode,
|
||||||
None => pool_config.pool_mode,
|
None => pool_config.pool_mode,
|
||||||
@@ -456,6 +532,9 @@ impl ConnectionPool {
|
|||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
},
|
},
|
||||||
query_parser_enabled: pool_config.query_parser_enabled,
|
query_parser_enabled: pool_config.query_parser_enabled,
|
||||||
|
query_parser_max_length: pool_config.query_parser_max_length,
|
||||||
|
query_parser_read_write_splitting: pool_config
|
||||||
|
.query_parser_read_write_splitting,
|
||||||
primary_reads_enabled: pool_config.primary_reads_enabled,
|
primary_reads_enabled: pool_config.primary_reads_enabled,
|
||||||
sharding_function: pool_config.sharding_function,
|
sharding_function: pool_config.sharding_function,
|
||||||
automatic_sharding_key: pool_config.automatic_sharding_key.clone(),
|
automatic_sharding_key: pool_config.automatic_sharding_key.clone(),
|
||||||
@@ -471,6 +550,7 @@ impl ConnectionPool {
|
|||||||
.clone()
|
.clone()
|
||||||
.map(|regex| Regex::new(regex.as_str()).unwrap()),
|
.map(|regex| Regex::new(regex.as_str()).unwrap()),
|
||||||
regex_search_limit: pool_config.regex_search_limit.unwrap_or(1000),
|
regex_search_limit: pool_config.regex_search_limit.unwrap_or(1000),
|
||||||
|
default_shard: pool_config.default_shard,
|
||||||
auth_query: pool_config.auth_query.clone(),
|
auth_query: pool_config.auth_query.clone(),
|
||||||
auth_query_user: pool_config.auth_query_user.clone(),
|
auth_query_user: pool_config.auth_query_user.clone(),
|
||||||
auth_query_password: pool_config.auth_query_password.clone(),
|
auth_query_password: pool_config.auth_query_password.clone(),
|
||||||
@@ -478,17 +558,23 @@ impl ConnectionPool {
|
|||||||
Some(ref plugins) => Some(plugins.clone()),
|
Some(ref plugins) => Some(plugins.clone()),
|
||||||
None => config.plugins.clone(),
|
None => config.plugins.clone(),
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
validated: Arc::new(AtomicBool::new(false)),
|
validated: Arc::new(AtomicBool::new(false)),
|
||||||
paused: Arc::new(AtomicBool::new(false)),
|
paused: Arc::new(AtomicBool::new(false)),
|
||||||
paused_waiter: Arc::new(Notify::new()),
|
paused_waiter: Arc::new(Notify::new()),
|
||||||
|
prepared_statement_cache: match pool_config.prepared_statements_cache_size {
|
||||||
|
0 => None,
|
||||||
|
_ => Some(Arc::new(Mutex::new(PreparedStatementCache::new(
|
||||||
|
pool_config.prepared_statements_cache_size,
|
||||||
|
)))),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Connect to the servers to make sure pool configuration is valid
|
// Connect to the servers to make sure pool configuration is valid
|
||||||
// before setting it globally.
|
// before setting it globally.
|
||||||
// Do this async and somewhere else, we don't have to wait here.
|
// Do this async and somewhere else, we don't have to wait here.
|
||||||
if config.general.validate_config {
|
if config.general.validate_config {
|
||||||
let mut validate_pool = pool.clone();
|
let validate_pool = pool.clone();
|
||||||
tokio::task::spawn(async move {
|
tokio::task::spawn(async move {
|
||||||
let _ = validate_pool.validate().await;
|
let _ = validate_pool.validate().await;
|
||||||
});
|
});
|
||||||
@@ -509,7 +595,7 @@ impl ConnectionPool {
|
|||||||
/// when they connect.
|
/// when they connect.
|
||||||
/// This also warms up the pool for clients that connect when
|
/// This also warms up the pool for clients that connect when
|
||||||
/// the pooler starts up.
|
/// the pooler starts up.
|
||||||
pub async fn validate(&mut self) -> Result<(), Error> {
|
pub async fn validate(&self) -> Result<(), Error> {
|
||||||
let mut futures = Vec::new();
|
let mut futures = Vec::new();
|
||||||
let validated = Arc::clone(&self.validated);
|
let validated = Arc::clone(&self.validated);
|
||||||
|
|
||||||
@@ -517,7 +603,7 @@ impl ConnectionPool {
|
|||||||
for server in 0..self.servers(shard) {
|
for server in 0..self.servers(shard) {
|
||||||
let databases = self.databases.clone();
|
let databases = self.databases.clone();
|
||||||
let validated = Arc::clone(&validated);
|
let validated = Arc::clone(&validated);
|
||||||
let pool_server_info = Arc::clone(&self.server_info);
|
let pool_server_parameters = Arc::clone(&self.original_server_parameters);
|
||||||
|
|
||||||
let task = tokio::task::spawn(async move {
|
let task = tokio::task::spawn(async move {
|
||||||
let connection = match databases[shard][server].get().await {
|
let connection = match databases[shard][server].get().await {
|
||||||
@@ -530,11 +616,10 @@ impl ConnectionPool {
|
|||||||
|
|
||||||
let proxy = connection;
|
let proxy = connection;
|
||||||
let server = &*proxy;
|
let server = &*proxy;
|
||||||
let server_info = server.server_info();
|
let server_parameters: ServerParameters = server.server_parameters();
|
||||||
|
|
||||||
let mut guard = pool_server_info.write();
|
let mut guard = pool_server_parameters.write();
|
||||||
guard.clear();
|
*guard = server_parameters;
|
||||||
guard.put(server_info.clone());
|
|
||||||
validated.store(true, Ordering::Relaxed);
|
validated.store(true, Ordering::Relaxed);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -546,7 +631,7 @@ impl ConnectionPool {
|
|||||||
|
|
||||||
// TODO: compare server information to make sure
|
// TODO: compare server information to make sure
|
||||||
// all shards are running identical configurations.
|
// all shards are running identical configurations.
|
||||||
if self.server_info.read().is_empty() {
|
if !self.validated() {
|
||||||
error!("Could not validate connection pool");
|
error!("Could not validate connection pool");
|
||||||
return Err(Error::AllServersDown);
|
return Err(Error::AllServersDown);
|
||||||
}
|
}
|
||||||
@@ -593,19 +678,51 @@ impl ConnectionPool {
|
|||||||
/// Get a connection from the pool.
|
/// Get a connection from the pool.
|
||||||
pub async fn get(
|
pub async fn get(
|
||||||
&self,
|
&self,
|
||||||
shard: usize, // shard number
|
shard: Option<usize>, // shard number
|
||||||
role: Option<Role>, // primary or replica
|
role: Option<Role>, // primary or replica
|
||||||
client_stats: &ClientStats, // client id
|
client_stats: &ClientStats, // client id
|
||||||
) -> Result<(PooledConnection<'_, ServerPool>, Address), Error> {
|
) -> Result<(PooledConnection<'_, ServerPool>, Address), Error> {
|
||||||
let mut candidates: Vec<&Address> = self.addresses[shard]
|
let effective_shard_id = if self.shards() == 1 {
|
||||||
.iter()
|
// The base, unsharded case
|
||||||
.filter(|address| address.role == role)
|
Some(0)
|
||||||
.collect();
|
} else {
|
||||||
|
if !self.valid_shard_id(shard) {
|
||||||
|
// None is valid shard ID so it is safe to unwrap here
|
||||||
|
return Err(Error::InvalidShardId(shard.unwrap()));
|
||||||
|
}
|
||||||
|
shard
|
||||||
|
};
|
||||||
|
|
||||||
// We shuffle even if least_outstanding_queries is used to avoid imbalance
|
let mut candidates = self
|
||||||
// in cases where all candidates have more or less the same number of outstanding
|
.addresses
|
||||||
// queries
|
.iter()
|
||||||
|
.flatten()
|
||||||
|
.filter(|address| address.role == role)
|
||||||
|
.collect::<Vec<&Address>>();
|
||||||
|
|
||||||
|
// We start with a shuffled list of addresses even if we end up resorting
|
||||||
|
// this is meant to avoid hitting instance 0 everytime if the sorting metric
|
||||||
|
// ends up being the same for all instances
|
||||||
candidates.shuffle(&mut thread_rng());
|
candidates.shuffle(&mut thread_rng());
|
||||||
|
|
||||||
|
match effective_shard_id {
|
||||||
|
Some(shard_id) => candidates.retain(|address| address.shard == shard_id),
|
||||||
|
None => match self.settings.default_shard {
|
||||||
|
DefaultShard::Shard(shard_id) => {
|
||||||
|
candidates.retain(|address| address.shard == shard_id)
|
||||||
|
}
|
||||||
|
DefaultShard::Random => (),
|
||||||
|
DefaultShard::RandomHealthy => {
|
||||||
|
candidates.sort_by(|a, b| {
|
||||||
|
b.error_count
|
||||||
|
.load(Ordering::Relaxed)
|
||||||
|
.partial_cmp(&a.error_count.load(Ordering::Relaxed))
|
||||||
|
.unwrap()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
if self.settings.load_balancing_mode == LoadBalancingMode::LeastOutstandingConnections {
|
if self.settings.load_balancing_mode == LoadBalancingMode::LeastOutstandingConnections {
|
||||||
candidates.sort_by(|a, b| {
|
candidates.sort_by(|a, b| {
|
||||||
self.busy_connection_count(b)
|
self.busy_connection_count(b)
|
||||||
@@ -628,7 +745,7 @@ impl ConnectionPool {
|
|||||||
let mut force_healthcheck = false;
|
let mut force_healthcheck = false;
|
||||||
|
|
||||||
if self.is_banned(address) {
|
if self.is_banned(address) {
|
||||||
if self.try_unban(&address).await {
|
if self.try_unban(address).await {
|
||||||
force_healthcheck = true;
|
force_healthcheck = true;
|
||||||
} else {
|
} else {
|
||||||
debug!("Address {:?} is banned", address);
|
debug!("Address {:?} is banned", address);
|
||||||
@@ -641,7 +758,10 @@ impl ConnectionPool {
|
|||||||
.get()
|
.get()
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(conn) => conn,
|
Ok(conn) => {
|
||||||
|
address.reset_error_count();
|
||||||
|
conn
|
||||||
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(
|
error!(
|
||||||
"Connection checkout error for instance {:?}, error: {:?}",
|
"Connection checkout error for instance {:?}, error: {:?}",
|
||||||
@@ -649,7 +769,6 @@ impl ConnectionPool {
|
|||||||
);
|
);
|
||||||
self.ban(address, BanReason::FailedCheckout, Some(client_stats));
|
self.ban(address, BanReason::FailedCheckout, Some(client_stats));
|
||||||
address.stats.error();
|
address.stats.error();
|
||||||
client_stats.idle();
|
|
||||||
client_stats.checkout_error();
|
client_stats.checkout_error();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -667,8 +786,8 @@ impl ConnectionPool {
|
|||||||
// since we last checked the server is ok.
|
// since we last checked the server is ok.
|
||||||
// Health checks are pretty expensive.
|
// Health checks are pretty expensive.
|
||||||
if !require_healthcheck {
|
if !require_healthcheck {
|
||||||
let checkout_time: u64 = now.elapsed().as_micros() as u64;
|
let checkout_time = now.elapsed().as_micros() as u64;
|
||||||
client_stats.checkout_time(checkout_time);
|
client_stats.checkout_success();
|
||||||
server
|
server
|
||||||
.stats()
|
.stats()
|
||||||
.checkout_time(checkout_time, client_stats.application_name());
|
.checkout_time(checkout_time, client_stats.application_name());
|
||||||
@@ -681,8 +800,8 @@ impl ConnectionPool {
|
|||||||
.run_health_check(address, server, now, client_stats)
|
.run_health_check(address, server, now, client_stats)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
let checkout_time: u64 = now.elapsed().as_micros() as u64;
|
let checkout_time = now.elapsed().as_micros() as u64;
|
||||||
client_stats.checkout_time(checkout_time);
|
client_stats.checkout_success();
|
||||||
server
|
server
|
||||||
.stats()
|
.stats()
|
||||||
.checkout_time(checkout_time, client_stats.application_name());
|
.checkout_time(checkout_time, client_stats.application_name());
|
||||||
@@ -693,7 +812,9 @@ impl ConnectionPool {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
client_stats.idle();
|
|
||||||
|
client_stats.checkout_error();
|
||||||
|
|
||||||
Err(Error::AllServersDown)
|
Err(Error::AllServersDown)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -718,7 +839,7 @@ impl ConnectionPool {
|
|||||||
Ok(res) => match res {
|
Ok(res) => match res {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
let checkout_time: u64 = start.elapsed().as_micros() as u64;
|
let checkout_time: u64 = start.elapsed().as_micros() as u64;
|
||||||
client_info.checkout_time(checkout_time);
|
client_info.checkout_success();
|
||||||
server
|
server
|
||||||
.stats()
|
.stats()
|
||||||
.checkout_time(checkout_time, client_info.application_name());
|
.checkout_time(checkout_time, client_info.application_name());
|
||||||
@@ -746,16 +867,28 @@ impl ConnectionPool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Don't leave a bad connection in the pool.
|
// Don't leave a bad connection in the pool.
|
||||||
server.mark_bad();
|
server.mark_bad("failed health check");
|
||||||
|
|
||||||
self.ban(&address, BanReason::FailedHealthCheck, Some(client_info));
|
self.ban(address, BanReason::FailedHealthCheck, Some(client_info));
|
||||||
return false;
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ban an address (i.e. replica). It no longer will serve
|
/// Ban an address (i.e. replica). It no longer will serve
|
||||||
/// traffic for any new transactions. Existing transactions on that replica
|
/// traffic for any new transactions. Existing transactions on that replica
|
||||||
/// will finish successfully or error out to the clients.
|
/// will finish successfully or error out to the clients.
|
||||||
pub fn ban(&self, address: &Address, reason: BanReason, client_info: Option<&ClientStats>) {
|
pub fn ban(&self, address: &Address, reason: BanReason, client_info: Option<&ClientStats>) {
|
||||||
|
// Count the number of errors since the last successful checkout
|
||||||
|
// This is used to determine if the shard is down
|
||||||
|
match reason {
|
||||||
|
BanReason::FailedHealthCheck
|
||||||
|
| BanReason::FailedCheckout
|
||||||
|
| BanReason::MessageSendFailed
|
||||||
|
| BanReason::MessageReceiveFailed => {
|
||||||
|
address.increment_error_count();
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
};
|
||||||
|
|
||||||
// Primary can never be banned
|
// Primary can never be banned
|
||||||
if address.role == Role::Primary {
|
if address.role == Role::Primary {
|
||||||
return;
|
return;
|
||||||
@@ -861,10 +994,10 @@ impl ConnectionPool {
|
|||||||
let guard = self.banlist.read();
|
let guard = self.banlist.read();
|
||||||
for banlist in guard.iter() {
|
for banlist in guard.iter() {
|
||||||
for (address, (reason, timestamp)) in banlist.iter() {
|
for (address, (reason, timestamp)) in banlist.iter() {
|
||||||
bans.push((address.clone(), (reason.clone(), timestamp.clone())));
|
bans.push((address.clone(), (reason.clone(), *timestamp)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return bans;
|
bans
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the address from the host url
|
/// Get the address from the host url
|
||||||
@@ -906,10 +1039,11 @@ impl ConnectionPool {
|
|||||||
&self.addresses[shard][server]
|
&self.addresses[shard][server]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn server_info(&self) -> BytesMut {
|
pub fn server_parameters(&self) -> ServerParameters {
|
||||||
self.server_info.read().clone()
|
self.original_server_parameters.read().clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the number of checked out connection for an address
|
||||||
fn busy_connection_count(&self, address: &Address) -> u32 {
|
fn busy_connection_count(&self, address: &Address) -> u32 {
|
||||||
let state = self.pool_state(address.shard, address.address_index);
|
let state = self.pool_state(address.shard, address.address_index);
|
||||||
let idle = state.idle_connections;
|
let idle = state.idle_connections;
|
||||||
@@ -921,7 +1055,37 @@ impl ConnectionPool {
|
|||||||
}
|
}
|
||||||
let busy = provisioned - idle;
|
let busy = provisioned - idle;
|
||||||
debug!("{:?} has {:?} busy connections", address, busy);
|
debug!("{:?} has {:?} busy connections", address, busy);
|
||||||
return busy;
|
busy
|
||||||
|
}
|
||||||
|
|
||||||
|
fn valid_shard_id(&self, shard: Option<usize>) -> bool {
|
||||||
|
match shard {
|
||||||
|
None => true,
|
||||||
|
Some(shard) => shard < self.shards(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Register a parse statement to the pool's cache and return the rewritten parse
|
||||||
|
///
|
||||||
|
/// Do not pass an anonymous parse statement to this function
|
||||||
|
pub fn register_parse_to_cache(&self, hash: u64, parse: &Parse) -> Option<Arc<Parse>> {
|
||||||
|
// We should only be calling this function if the cache is enabled
|
||||||
|
match self.prepared_statement_cache {
|
||||||
|
Some(ref prepared_statement_cache) => {
|
||||||
|
let mut cache = prepared_statement_cache.lock();
|
||||||
|
Some(cache.get_or_insert(parse, hash))
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Promote a prepared statement hash in the LRU
|
||||||
|
pub fn promote_prepared_statement_hash(&self, hash: &u64) {
|
||||||
|
// We should only be calling this function if the cache is enabled
|
||||||
|
if let Some(ref prepared_statement_cache) = self.prepared_statement_cache {
|
||||||
|
let mut cache = prepared_statement_cache.lock();
|
||||||
|
cache.promote(hash);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -947,9 +1111,16 @@ pub struct ServerPool {
|
|||||||
|
|
||||||
/// Should we clean up dirty connections before putting them into the pool?
|
/// Should we clean up dirty connections before putting them into the pool?
|
||||||
cleanup_connections: bool,
|
cleanup_connections: bool,
|
||||||
|
|
||||||
|
/// Log client parameter status changes
|
||||||
|
log_client_parameter_status_changes: bool,
|
||||||
|
|
||||||
|
/// Prepared statement cache size
|
||||||
|
prepared_statement_cache_size: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ServerPool {
|
impl ServerPool {
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn new(
|
pub fn new(
|
||||||
address: Address,
|
address: Address,
|
||||||
user: User,
|
user: User,
|
||||||
@@ -958,15 +1129,19 @@ impl ServerPool {
|
|||||||
auth_hash: Arc<RwLock<Option<String>>>,
|
auth_hash: Arc<RwLock<Option<String>>>,
|
||||||
plugins: Option<Plugins>,
|
plugins: Option<Plugins>,
|
||||||
cleanup_connections: bool,
|
cleanup_connections: bool,
|
||||||
|
log_client_parameter_status_changes: bool,
|
||||||
|
prepared_statement_cache_size: usize,
|
||||||
) -> ServerPool {
|
) -> ServerPool {
|
||||||
ServerPool {
|
ServerPool {
|
||||||
address,
|
address,
|
||||||
user: user.clone(),
|
user,
|
||||||
database: database.to_string(),
|
database: database.to_string(),
|
||||||
client_server_map,
|
client_server_map,
|
||||||
auth_hash,
|
auth_hash,
|
||||||
plugins,
|
plugins,
|
||||||
cleanup_connections,
|
cleanup_connections,
|
||||||
|
log_client_parameter_status_changes,
|
||||||
|
prepared_statement_cache_size,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -996,6 +1171,8 @@ impl ManageConnection for ServerPool {
|
|||||||
stats.clone(),
|
stats.clone(),
|
||||||
self.auth_hash.clone(),
|
self.auth_hash.clone(),
|
||||||
self.cleanup_connections,
|
self.cleanup_connections,
|
||||||
|
self.log_client_parameter_status_changes,
|
||||||
|
self.prepared_statement_cache_size,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,23 +1,41 @@
|
|||||||
use hyper::service::{make_service_fn, service_fn};
|
use http_body_util::Full;
|
||||||
use hyper::{Body, Method, Request, Response, Server, StatusCode};
|
use hyper::body;
|
||||||
|
use hyper::body::Bytes;
|
||||||
|
|
||||||
|
use hyper::server::conn::http1;
|
||||||
|
use hyper::service::service_fn;
|
||||||
|
use hyper::{Method, Request, Response, StatusCode};
|
||||||
|
use hyper_util::rt::TokioIo;
|
||||||
use log::{debug, error, info};
|
use log::{debug, error, info};
|
||||||
use phf::phf_map;
|
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::atomic::Ordering;
|
||||||
use std::sync::Arc;
|
use tokio::net::TcpListener;
|
||||||
|
|
||||||
use crate::config::Address;
|
use crate::config::Address;
|
||||||
use crate::pool::{get_all_pools, PoolIdentifier};
|
use crate::pool::{get_all_pools, PoolIdentifier};
|
||||||
|
use crate::stats::get_server_stats;
|
||||||
use crate::stats::pool::PoolStats;
|
use crate::stats::pool::PoolStats;
|
||||||
use crate::stats::{get_server_stats, ServerStats};
|
|
||||||
|
|
||||||
struct MetricHelpType {
|
struct MetricHelpType {
|
||||||
help: &'static str,
|
help: &'static str,
|
||||||
ty: &'static str,
|
ty: &'static str,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ServerPrometheusStats {
|
||||||
|
bytes_received: u64,
|
||||||
|
bytes_sent: u64,
|
||||||
|
transaction_count: u64,
|
||||||
|
query_count: u64,
|
||||||
|
error_count: u64,
|
||||||
|
active_count: u64,
|
||||||
|
idle_count: u64,
|
||||||
|
login_count: u64,
|
||||||
|
tested_count: u64,
|
||||||
|
}
|
||||||
|
|
||||||
// reference for metric types: https://prometheus.io/docs/concepts/metric_types/
|
// reference for metric types: https://prometheus.io/docs/concepts/metric_types/
|
||||||
// counters only increase
|
// counters only increase
|
||||||
// gauges can arbitrarily increase or decrease
|
// gauges can arbitrarily increase or decrease
|
||||||
@@ -120,22 +138,46 @@ static METRIC_HELP_AND_TYPES_LOOKUP: phf::Map<&'static str, MetricHelpType> = ph
|
|||||||
},
|
},
|
||||||
"servers_bytes_received" => MetricHelpType {
|
"servers_bytes_received" => MetricHelpType {
|
||||||
help: "Volume in bytes of network traffic received by server",
|
help: "Volume in bytes of network traffic received by server",
|
||||||
ty: "gauge",
|
ty: "counter",
|
||||||
},
|
},
|
||||||
"servers_bytes_sent" => MetricHelpType {
|
"servers_bytes_sent" => MetricHelpType {
|
||||||
help: "Volume in bytes of network traffic sent by server",
|
help: "Volume in bytes of network traffic sent by server",
|
||||||
ty: "gauge",
|
ty: "counter",
|
||||||
},
|
},
|
||||||
"servers_transaction_count" => MetricHelpType {
|
"servers_transaction_count" => MetricHelpType {
|
||||||
help: "Number of transactions executed by server",
|
help: "Number of transactions executed by server",
|
||||||
ty: "gauge",
|
ty: "counter",
|
||||||
},
|
},
|
||||||
"servers_query_count" => MetricHelpType {
|
"servers_query_count" => MetricHelpType {
|
||||||
help: "Number of queries executed by server",
|
help: "Number of queries executed by server",
|
||||||
ty: "gauge",
|
ty: "counter",
|
||||||
},
|
},
|
||||||
"servers_error_count" => MetricHelpType {
|
"servers_error_count" => MetricHelpType {
|
||||||
help: "Number of errors",
|
help: "Number of errors",
|
||||||
|
ty: "counter",
|
||||||
|
},
|
||||||
|
"servers_idle_count" => MetricHelpType {
|
||||||
|
help: "Number of server connection in idle state",
|
||||||
|
ty: "gauge",
|
||||||
|
},
|
||||||
|
"servers_active_count" => MetricHelpType {
|
||||||
|
help: "Number of server connection in active state",
|
||||||
|
ty: "gauge",
|
||||||
|
},
|
||||||
|
"servers_tested_count" => MetricHelpType {
|
||||||
|
help: "Number of server connection in tested state",
|
||||||
|
ty: "gauge",
|
||||||
|
},
|
||||||
|
"servers_login_count" => MetricHelpType {
|
||||||
|
help: "Number of server connection in login state",
|
||||||
|
ty: "gauge",
|
||||||
|
},
|
||||||
|
"servers_is_banned" => MetricHelpType {
|
||||||
|
help: "0 if server is not banned, 1 if server is banned",
|
||||||
|
ty: "gauge",
|
||||||
|
},
|
||||||
|
"servers_is_paused" => MetricHelpType {
|
||||||
|
help: "0 if server is not paused, 1 if server is paused",
|
||||||
ty: "gauge",
|
ty: "gauge",
|
||||||
},
|
},
|
||||||
"databases_pool_size" => MetricHelpType {
|
"databases_pool_size" => MetricHelpType {
|
||||||
@@ -158,18 +200,17 @@ struct PrometheusMetric<Value: fmt::Display> {
|
|||||||
|
|
||||||
impl<Value: fmt::Display> fmt::Display for PrometheusMetric<Value> {
|
impl<Value: fmt::Display> fmt::Display for PrometheusMetric<Value> {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
let formatted_labels = self
|
let mut sorted_labels: Vec<_> = self.labels.iter().collect();
|
||||||
.labels
|
sorted_labels.sort_by_key(|&(key, _)| key);
|
||||||
|
let formatted_labels = sorted_labels
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(key, value)| format!("{}=\"{}\"", key, value))
|
.map(|(key, value)| format!("{}=\"{}\"", key, value))
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(",");
|
.join(",");
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"# HELP {name} {help}\n# TYPE {name} {ty}\n{name}{{{formatted_labels}}} {value}\n",
|
"{name}{{{formatted_labels}}} {value}",
|
||||||
name = format_args!("pgcat_{}", self.name),
|
name = format_args!("pgcat_{}", self.name),
|
||||||
help = self.help,
|
|
||||||
ty = self.ty,
|
|
||||||
formatted_labels = formatted_labels,
|
formatted_labels = formatted_labels,
|
||||||
value = self.value
|
value = self.value
|
||||||
)
|
)
|
||||||
@@ -203,7 +244,9 @@ impl<Value: fmt::Display> PrometheusMetric<Value> {
|
|||||||
labels.insert("shard", address.shard.to_string());
|
labels.insert("shard", address.shard.to_string());
|
||||||
labels.insert("role", address.role.to_string());
|
labels.insert("role", address.role.to_string());
|
||||||
labels.insert("pool", address.pool_name.clone());
|
labels.insert("pool", address.pool_name.clone());
|
||||||
|
labels.insert("index", address.address_index.to_string());
|
||||||
labels.insert("database", address.database.to_string());
|
labels.insert("database", address.database.to_string());
|
||||||
|
labels.insert("username", address.username.clone());
|
||||||
|
|
||||||
Self::from_name(&format!("databases_{}", name), value, labels)
|
Self::from_name(&format!("databases_{}", name), value, labels)
|
||||||
}
|
}
|
||||||
@@ -218,7 +261,9 @@ impl<Value: fmt::Display> PrometheusMetric<Value> {
|
|||||||
labels.insert("shard", address.shard.to_string());
|
labels.insert("shard", address.shard.to_string());
|
||||||
labels.insert("role", address.role.to_string());
|
labels.insert("role", address.role.to_string());
|
||||||
labels.insert("pool", address.pool_name.clone());
|
labels.insert("pool", address.pool_name.clone());
|
||||||
|
labels.insert("index", address.address_index.to_string());
|
||||||
labels.insert("database", address.database.to_string());
|
labels.insert("database", address.database.to_string());
|
||||||
|
labels.insert("username", address.username.clone());
|
||||||
|
|
||||||
Self::from_name(&format!("servers_{}", name), value, labels)
|
Self::from_name(&format!("servers_{}", name), value, labels)
|
||||||
}
|
}
|
||||||
@@ -229,7 +274,9 @@ impl<Value: fmt::Display> PrometheusMetric<Value> {
|
|||||||
labels.insert("shard", address.shard.to_string());
|
labels.insert("shard", address.shard.to_string());
|
||||||
labels.insert("pool", address.pool_name.clone());
|
labels.insert("pool", address.pool_name.clone());
|
||||||
labels.insert("role", address.role.to_string());
|
labels.insert("role", address.role.to_string());
|
||||||
|
labels.insert("index", address.address_index.to_string());
|
||||||
labels.insert("database", address.database.to_string());
|
labels.insert("database", address.database.to_string());
|
||||||
|
labels.insert("username", address.username.clone());
|
||||||
|
|
||||||
Self::from_name(&format!("stats_{}", name), value, labels)
|
Self::from_name(&format!("stats_{}", name), value, labels)
|
||||||
}
|
}
|
||||||
@@ -241,9 +288,20 @@ impl<Value: fmt::Display> PrometheusMetric<Value> {
|
|||||||
|
|
||||||
Self::from_name(&format!("pools_{}", name), value, labels)
|
Self::from_name(&format!("pools_{}", name), value, labels)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_header(&self) -> String {
|
||||||
|
format!(
|
||||||
|
"\n# HELP {name} {help}\n# TYPE {name} {ty}",
|
||||||
|
name = format_args!("pgcat_{}", self.name),
|
||||||
|
help = self.help,
|
||||||
|
ty = self.ty,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn prometheus_stats(request: Request<Body>) -> Result<Response<Body>, hyper::http::Error> {
|
async fn prometheus_stats(
|
||||||
|
request: Request<body::Incoming>,
|
||||||
|
) -> Result<Response<Full<Bytes>>, hyper::http::Error> {
|
||||||
match (request.method(), request.uri().path()) {
|
match (request.method(), request.uri().path()) {
|
||||||
(&Method::GET, "/metrics") => {
|
(&Method::GET, "/metrics") => {
|
||||||
let mut lines = Vec::new();
|
let mut lines = Vec::new();
|
||||||
@@ -264,6 +322,7 @@ async fn prometheus_stats(request: Request<Body>) -> Result<Response<Body>, hype
|
|||||||
|
|
||||||
// Adds metrics shown in a SHOW STATS admin command.
|
// Adds metrics shown in a SHOW STATS admin command.
|
||||||
fn push_address_stats(lines: &mut Vec<String>) {
|
fn push_address_stats(lines: &mut Vec<String>) {
|
||||||
|
let mut grouped_metrics: HashMap<String, Vec<PrometheusMetric<u64>>> = HashMap::new();
|
||||||
for (_, pool) in get_all_pools() {
|
for (_, 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) {
|
||||||
@@ -273,7 +332,10 @@ fn push_address_stats(lines: &mut Vec<String>) {
|
|||||||
if let Some(prometheus_metric) =
|
if let Some(prometheus_metric) =
|
||||||
PrometheusMetric::<u64>::from_address(address, &key, value)
|
PrometheusMetric::<u64>::from_address(address, &key, value)
|
||||||
{
|
{
|
||||||
lines.push(prometheus_metric.to_string());
|
grouped_metrics
|
||||||
|
.entry(key)
|
||||||
|
.or_default()
|
||||||
|
.push(prometheus_metric);
|
||||||
} else {
|
} else {
|
||||||
debug!("Metric {} not implemented for {}", key, address.name());
|
debug!("Metric {} not implemented for {}", key, address.name());
|
||||||
}
|
}
|
||||||
@@ -281,33 +343,53 @@ fn push_address_stats(lines: &mut Vec<String>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (_key, metrics) in grouped_metrics {
|
||||||
|
if !metrics.is_empty() {
|
||||||
|
lines.push(metrics[0].get_header());
|
||||||
|
for metric in metrics {
|
||||||
|
lines.push(metric.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adds relevant metrics shown in a SHOW POOLS admin command.
|
// Adds relevant metrics shown in a SHOW POOLS admin command.
|
||||||
fn push_pool_stats(lines: &mut Vec<String>) {
|
fn push_pool_stats(lines: &mut Vec<String>) {
|
||||||
|
let mut grouped_metrics: HashMap<String, Vec<PrometheusMetric<u64>>> = HashMap::new();
|
||||||
let pool_stats = PoolStats::construct_pool_lookup();
|
let pool_stats = PoolStats::construct_pool_lookup();
|
||||||
for (pool_id, stats) in pool_stats.iter() {
|
for (pool_id, stats) in pool_stats.iter() {
|
||||||
for (name, value) in stats.clone() {
|
for (name, value) in stats.clone() {
|
||||||
if let Some(prometheus_metric) =
|
if let Some(prometheus_metric) =
|
||||||
PrometheusMetric::<u64>::from_pool(pool_id.clone(), &name, value)
|
PrometheusMetric::<u64>::from_pool(pool_id.clone(), &name, value)
|
||||||
{
|
{
|
||||||
lines.push(prometheus_metric.to_string());
|
grouped_metrics
|
||||||
|
.entry(name)
|
||||||
|
.or_default()
|
||||||
|
.push(prometheus_metric);
|
||||||
} else {
|
} else {
|
||||||
debug!("Metric {} not implemented for ({})", name, *pool_id);
|
debug!("Metric {} not implemented for ({})", name, *pool_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (_key, metrics) in grouped_metrics {
|
||||||
|
if !metrics.is_empty() {
|
||||||
|
lines.push(metrics[0].get_header());
|
||||||
|
for metric in metrics {
|
||||||
|
lines.push(metric.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adds relevant metrics shown in a SHOW DATABASES admin command.
|
// Adds relevant metrics shown in a SHOW DATABASES admin command.
|
||||||
fn push_database_stats(lines: &mut Vec<String>) {
|
fn push_database_stats(lines: &mut Vec<String>) {
|
||||||
|
let mut grouped_metrics: HashMap<String, Vec<PrometheusMetric<u32>>> = HashMap::new();
|
||||||
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() {
|
||||||
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 metrics = vec![
|
let metrics = vec![
|
||||||
("pool_size", pool_config.user.pool_size),
|
("pool_size", pool_config.user.pool_size),
|
||||||
("current_connections", pool_state.connections),
|
("current_connections", pool_state.connections),
|
||||||
@@ -316,7 +398,10 @@ fn push_database_stats(lines: &mut Vec<String>) {
|
|||||||
if let Some(prometheus_metric) =
|
if let Some(prometheus_metric) =
|
||||||
PrometheusMetric::<u32>::from_database_info(address, key, value)
|
PrometheusMetric::<u32>::from_database_info(address, key, value)
|
||||||
{
|
{
|
||||||
lines.push(prometheus_metric.to_string());
|
grouped_metrics
|
||||||
|
.entry(key.to_string())
|
||||||
|
.or_default()
|
||||||
|
.push(prometheus_metric);
|
||||||
} else {
|
} else {
|
||||||
debug!("Metric {} not implemented for {}", key, address.name());
|
debug!("Metric {} not implemented for {}", key, address.name());
|
||||||
}
|
}
|
||||||
@@ -324,45 +409,73 @@ fn push_database_stats(lines: &mut Vec<String>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (_key, metrics) in grouped_metrics {
|
||||||
|
if !metrics.is_empty() {
|
||||||
|
lines.push(metrics[0].get_header());
|
||||||
|
for metric in metrics {
|
||||||
|
lines.push(metric.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adds relevant metrics shown in a SHOW SERVERS admin command.
|
// Adds relevant metrics shown in a SHOW SERVERS admin command.
|
||||||
fn push_server_stats(lines: &mut Vec<String>) {
|
fn push_server_stats(lines: &mut Vec<String>) {
|
||||||
let server_stats = get_server_stats();
|
let server_stats = get_server_stats();
|
||||||
let mut server_stats_by_addresses = HashMap::<String, Arc<ServerStats>>::new();
|
let mut prom_stats = HashMap::<String, ServerPrometheusStats>::new();
|
||||||
for (_, stats) in server_stats {
|
for (_, stats) in server_stats {
|
||||||
server_stats_by_addresses.insert(stats.address_name(), stats);
|
let entry = prom_stats
|
||||||
|
.entry(stats.address_name())
|
||||||
|
.or_insert(ServerPrometheusStats {
|
||||||
|
bytes_received: 0,
|
||||||
|
bytes_sent: 0,
|
||||||
|
transaction_count: 0,
|
||||||
|
query_count: 0,
|
||||||
|
error_count: 0,
|
||||||
|
active_count: 0,
|
||||||
|
idle_count: 0,
|
||||||
|
login_count: 0,
|
||||||
|
tested_count: 0,
|
||||||
|
});
|
||||||
|
entry.bytes_received += stats.bytes_received.load(Ordering::Relaxed);
|
||||||
|
entry.bytes_sent += stats.bytes_sent.load(Ordering::Relaxed);
|
||||||
|
entry.transaction_count += stats.transaction_count.load(Ordering::Relaxed);
|
||||||
|
entry.query_count += stats.query_count.load(Ordering::Relaxed);
|
||||||
|
entry.error_count += stats.error_count.load(Ordering::Relaxed);
|
||||||
|
match stats.state.load(Ordering::Relaxed) {
|
||||||
|
crate::stats::ServerState::Login => entry.login_count += 1,
|
||||||
|
crate::stats::ServerState::Active => entry.active_count += 1,
|
||||||
|
crate::stats::ServerState::Tested => entry.tested_count += 1,
|
||||||
|
crate::stats::ServerState::Idle => entry.idle_count += 1,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
let mut grouped_metrics: HashMap<String, Vec<PrometheusMetric<u64>>> = HashMap::new();
|
||||||
for (_, pool) in get_all_pools() {
|
for (_, 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);
|
||||||
if let Some(server_info) = server_stats_by_addresses.get(&address.name()) {
|
if let Some(server_info) = prom_stats.get(&address.name()) {
|
||||||
let metrics = [
|
let metrics = [
|
||||||
(
|
("bytes_received", server_info.bytes_received),
|
||||||
"bytes_received",
|
("bytes_sent", server_info.bytes_sent),
|
||||||
server_info.bytes_received.load(Ordering::Relaxed),
|
("transaction_count", server_info.transaction_count),
|
||||||
),
|
("query_count", server_info.query_count),
|
||||||
("bytes_sent", server_info.bytes_sent.load(Ordering::Relaxed)),
|
("error_count", server_info.error_count),
|
||||||
(
|
("idle_count", server_info.idle_count),
|
||||||
"transaction_count",
|
("active_count", server_info.active_count),
|
||||||
server_info.transaction_count.load(Ordering::Relaxed),
|
("login_count", server_info.login_count),
|
||||||
),
|
("tested_count", server_info.tested_count),
|
||||||
(
|
("is_banned", if pool.is_banned(address) { 1 } else { 0 }),
|
||||||
"query_count",
|
("is_paused", if pool.paused() { 1 } else { 0 }),
|
||||||
server_info.query_count.load(Ordering::Relaxed),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"error_count",
|
|
||||||
server_info.error_count.load(Ordering::Relaxed),
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
for (key, value) in metrics {
|
for (key, value) in metrics {
|
||||||
if let Some(prometheus_metric) =
|
if let Some(prometheus_metric) =
|
||||||
PrometheusMetric::<u64>::from_server_info(address, key, value)
|
PrometheusMetric::<u64>::from_server_info(address, key, value)
|
||||||
{
|
{
|
||||||
lines.push(prometheus_metric.to_string());
|
grouped_metrics
|
||||||
|
.entry(key.to_string())
|
||||||
|
.or_default()
|
||||||
|
.push(prometheus_metric);
|
||||||
} else {
|
} else {
|
||||||
debug!("Metric {} not implemented for {}", key, address.name());
|
debug!("Metric {} not implemented for {}", key, address.name());
|
||||||
}
|
}
|
||||||
@@ -371,17 +484,46 @@ fn push_server_stats(lines: &mut Vec<String>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (_key, metrics) in grouped_metrics {
|
||||||
|
if !metrics.is_empty() {
|
||||||
|
lines.push(metrics[0].get_header());
|
||||||
|
for metric in metrics {
|
||||||
|
lines.push(metric.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn start_metric_server(http_addr: SocketAddr) {
|
pub async fn start_metric_server(http_addr: SocketAddr) {
|
||||||
let http_service_factory =
|
let listener = TcpListener::bind(http_addr);
|
||||||
make_service_fn(|_conn| async { Ok::<_, hyper::Error>(service_fn(prometheus_stats)) });
|
let listener = match listener.await {
|
||||||
let server = Server::bind(&http_addr).serve(http_service_factory);
|
Ok(listener) => listener,
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to bind prometheus server to HTTP address: {}.", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
info!(
|
info!(
|
||||||
"Exposing prometheus metrics on http://{}/metrics.",
|
"Exposing prometheus metrics on http://{}/metrics.",
|
||||||
http_addr
|
http_addr
|
||||||
);
|
);
|
||||||
if let Err(e) = server.await {
|
loop {
|
||||||
error!("Failed to run HTTP server: {}.", e);
|
let stream = match listener.accept().await {
|
||||||
|
Ok((stream, _)) => stream,
|
||||||
|
Err(e) => {
|
||||||
|
error!("Error accepting connection: {}", e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let io = TokioIo::new(stream);
|
||||||
|
|
||||||
|
tokio::task::spawn(async move {
|
||||||
|
if let Err(err) = http1::Builder::new()
|
||||||
|
.serve_connection(io, service_fn(prometheus_stats))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
eprintln!("Error serving HTTP connection for metrics: {:?}", err);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
14
src/scram.rs
14
src/scram.rs
@@ -79,12 +79,12 @@ 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("SCRAM".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let salt = match general_purpose::STANDARD.decode(&server_message.salt) {
|
let salt = match general_purpose::STANDARD.decode(&server_message.salt) {
|
||||||
Ok(salt) => salt,
|
Ok(salt) => salt,
|
||||||
Err(_) => return Err(Error::ProtocolSyncError(format!("SCRAM"))),
|
Err(_) => return Err(Error::ProtocolSyncError("SCRAM".to_string())),
|
||||||
};
|
};
|
||||||
|
|
||||||
let salted_password = Self::hi(
|
let salted_password = Self::hi(
|
||||||
@@ -166,9 +166,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 general_purpose::STANDARD.decode(final_message.value) {
|
||||||
Ok(verifier) => verifier,
|
Ok(verifier) => verifier,
|
||||||
Err(_) => return Err(Error::ProtocolSyncError(format!("SCRAM"))),
|
Err(_) => return Err(Error::ProtocolSyncError("SCRAM".to_string())),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut hmac = match Hmac::<Sha256>::new_from_slice(&self.salted_password) {
|
let mut hmac = match Hmac::<Sha256>::new_from_slice(&self.salted_password) {
|
||||||
@@ -230,14 +230,14 @@ impl Message {
|
|||||||
.collect::<Vec<String>>();
|
.collect::<Vec<String>>();
|
||||||
|
|
||||||
if parts.len() != 3 {
|
if parts.len() != 3 {
|
||||||
return Err(Error::ProtocolSyncError(format!("SCRAM")));
|
return Err(Error::ProtocolSyncError("SCRAM".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
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("SCRAM".to_string())),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Message {
|
Ok(Message {
|
||||||
@@ -257,7 +257,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("SCRAM".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(FinalMessage {
|
Ok(FinalMessage {
|
||||||
|
|||||||
536
src/server.rs
536
src/server.rs
@@ -3,11 +3,14 @@
|
|||||||
use bytes::{Buf, BufMut, BytesMut};
|
use bytes::{Buf, BufMut, BytesMut};
|
||||||
use fallible_iterator::FallibleIterator;
|
use fallible_iterator::FallibleIterator;
|
||||||
use log::{debug, error, info, trace, warn};
|
use log::{debug, error, info, trace, warn};
|
||||||
|
use lru::LruCache;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
use parking_lot::{Mutex, RwLock};
|
use parking_lot::{Mutex, RwLock};
|
||||||
use postgres_protocol::message;
|
use postgres_protocol::message;
|
||||||
use std::collections::{BTreeSet, HashMap};
|
use std::collections::{HashMap, HashSet, VecDeque};
|
||||||
use std::io::Read;
|
use std::mem;
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
|
use std::num::NonZeroUsize;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, BufStream};
|
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, BufStream};
|
||||||
@@ -15,10 +18,11 @@ use tokio::net::TcpStream;
|
|||||||
use tokio_rustls::rustls::{OwnedTrustAnchor, RootCertStore};
|
use tokio_rustls::rustls::{OwnedTrustAnchor, RootCertStore};
|
||||||
use tokio_rustls::{client::TlsStream, TlsConnector};
|
use tokio_rustls::{client::TlsStream, TlsConnector};
|
||||||
|
|
||||||
use crate::config::{get_config, get_prepared_statements_cache_size, Address, User};
|
use crate::config::{get_config, Address, User};
|
||||||
use crate::constants::*;
|
use crate::constants::*;
|
||||||
use crate::dns_cache::{AddrSet, CACHED_RESOLVER};
|
use crate::dns_cache::{AddrSet, CACHED_RESOLVER};
|
||||||
use crate::errors::{Error, ServerIdentifier};
|
use crate::errors::{Error, ServerIdentifier};
|
||||||
|
use crate::messages::BytesMutReader;
|
||||||
use crate::messages::*;
|
use crate::messages::*;
|
||||||
use crate::mirrors::MirroringManager;
|
use crate::mirrors::MirroringManager;
|
||||||
use crate::pool::ClientServerMap;
|
use crate::pool::ClientServerMap;
|
||||||
@@ -105,10 +109,10 @@ impl StreamInner {
|
|||||||
|
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone)]
|
||||||
struct CleanupState {
|
struct CleanupState {
|
||||||
/// If server connection requires DISCARD ALL before checkin because of set statement
|
/// If server connection requires RESET ALL before checkin because of set statement
|
||||||
needs_cleanup_set: bool,
|
needs_cleanup_set: bool,
|
||||||
|
|
||||||
/// If server connection requires DISCARD ALL before checkin because of prepare statement
|
/// If server connection requires DEALLOCATE ALL before checkin because of prepare statement
|
||||||
needs_cleanup_prepare: bool,
|
needs_cleanup_prepare: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,6 +149,120 @@ impl std::fmt::Display for CleanupState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static TRACKED_PARAMETERS: Lazy<HashSet<String>> = Lazy::new(|| {
|
||||||
|
let mut set = HashSet::new();
|
||||||
|
set.insert("client_encoding".to_string());
|
||||||
|
set.insert("DateStyle".to_string());
|
||||||
|
set.insert("TimeZone".to_string());
|
||||||
|
set.insert("standard_conforming_strings".to_string());
|
||||||
|
set.insert("application_name".to_string());
|
||||||
|
set
|
||||||
|
});
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ServerParameters {
|
||||||
|
parameters: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ServerParameters {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServerParameters {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let mut server_parameters = ServerParameters {
|
||||||
|
parameters: HashMap::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
server_parameters.set_param("client_encoding".to_string(), "UTF8".to_string(), false);
|
||||||
|
server_parameters.set_param("DateStyle".to_string(), "ISO, MDY".to_string(), false);
|
||||||
|
server_parameters.set_param("TimeZone".to_string(), "Etc/UTC".to_string(), false);
|
||||||
|
server_parameters.set_param(
|
||||||
|
"standard_conforming_strings".to_string(),
|
||||||
|
"on".to_string(),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
server_parameters.set_param("application_name".to_string(), "pgcat".to_string(), false);
|
||||||
|
|
||||||
|
server_parameters
|
||||||
|
}
|
||||||
|
|
||||||
|
/// returns true if a tracked parameter was set, false if it was a non-tracked parameter
|
||||||
|
/// if startup is false, then then only tracked parameters will be set
|
||||||
|
pub fn set_param(&mut self, mut key: String, value: String, startup: bool) {
|
||||||
|
// The startup parameter will send uncapitalized keys but parameter status packets will send capitalized keys
|
||||||
|
if key == "timezone" {
|
||||||
|
key = "TimeZone".to_string();
|
||||||
|
} else if key == "datestyle" {
|
||||||
|
key = "DateStyle".to_string();
|
||||||
|
};
|
||||||
|
|
||||||
|
if TRACKED_PARAMETERS.contains(&key) || startup {
|
||||||
|
self.parameters.insert(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_from_hashmap(&mut self, parameters: &HashMap<String, String>, startup: bool) {
|
||||||
|
// iterate through each and call set_param
|
||||||
|
for (key, value) in parameters {
|
||||||
|
self.set_param(key.to_string(), value.to_string(), startup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gets the diff of the parameters
|
||||||
|
fn compare_params(&self, incoming_parameters: &ServerParameters) -> HashMap<String, String> {
|
||||||
|
let mut diff = HashMap::new();
|
||||||
|
|
||||||
|
// iterate through tracked parameters
|
||||||
|
for key in TRACKED_PARAMETERS.iter() {
|
||||||
|
if let Some(incoming_value) = incoming_parameters.parameters.get(key) {
|
||||||
|
if let Some(value) = self.parameters.get(key) {
|
||||||
|
if value != incoming_value {
|
||||||
|
diff.insert(key.to_string(), incoming_value.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diff
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_application_name(&self) -> &String {
|
||||||
|
// Can unwrap because we set it in the constructor
|
||||||
|
self.parameters.get("application_name").unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_parameter_message(key: &str, value: &str, buffer: &mut BytesMut) {
|
||||||
|
buffer.put_u8(b'S');
|
||||||
|
|
||||||
|
// 4 is len of i32, the plus for the null terminator
|
||||||
|
let len = 4 + key.len() + 1 + value.len() + 1;
|
||||||
|
|
||||||
|
buffer.put_i32(len as i32);
|
||||||
|
|
||||||
|
buffer.put_slice(key.as_bytes());
|
||||||
|
buffer.put_u8(0);
|
||||||
|
buffer.put_slice(value.as_bytes());
|
||||||
|
buffer.put_u8(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&ServerParameters> for BytesMut {
|
||||||
|
fn from(server_parameters: &ServerParameters) -> Self {
|
||||||
|
let mut bytes = BytesMut::new();
|
||||||
|
|
||||||
|
for (key, value) in &server_parameters.parameters {
|
||||||
|
ServerParameters::add_parameter_message(key, value, &mut bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pub fn compare
|
||||||
|
|
||||||
/// Server state.
|
/// Server state.
|
||||||
pub struct Server {
|
pub struct Server {
|
||||||
/// Server host, e.g. localhost,
|
/// Server host, e.g. localhost,
|
||||||
@@ -158,7 +276,7 @@ pub struct Server {
|
|||||||
buffer: BytesMut,
|
buffer: BytesMut,
|
||||||
|
|
||||||
/// Server information the server sent us over on startup.
|
/// Server information the server sent us over on startup.
|
||||||
server_info: BytesMut,
|
server_parameters: ServerParameters,
|
||||||
|
|
||||||
/// Backend id and secret key used for query cancellation.
|
/// Backend id and secret key used for query cancellation.
|
||||||
process_id: i32,
|
process_id: i32,
|
||||||
@@ -176,7 +294,7 @@ 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 DISCARD ALL before checkin
|
/// If server connection requires reset statements before checkin
|
||||||
cleanup_state: CleanupState,
|
cleanup_state: CleanupState,
|
||||||
|
|
||||||
/// Mapping of clients and servers used for query cancellation.
|
/// Mapping of clients and servers used for query cancellation.
|
||||||
@@ -202,13 +320,20 @@ pub struct Server {
|
|||||||
/// Should clean up dirty connections?
|
/// Should clean up dirty connections?
|
||||||
cleanup_connections: bool,
|
cleanup_connections: bool,
|
||||||
|
|
||||||
|
/// Log client parameter status changes
|
||||||
|
log_client_parameter_status_changes: bool,
|
||||||
|
|
||||||
/// Prepared statements
|
/// Prepared statements
|
||||||
prepared_statements: BTreeSet<String>,
|
prepared_statement_cache: Option<LruCache<String, ()>>,
|
||||||
|
|
||||||
|
/// Prepared statement being currently registered on the server.
|
||||||
|
registering_prepared_statement: VecDeque<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Server {
|
impl Server {
|
||||||
/// Pretend to be the Postgres client and connect to the server given host, port and credentials.
|
/// Pretend to be the Postgres client and connect to the server given host, port and credentials.
|
||||||
/// Perform the authentication and return the server in a ready for query state.
|
/// Perform the authentication and return the server in a ready for query state.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub async fn startup(
|
pub async fn startup(
|
||||||
address: &Address,
|
address: &Address,
|
||||||
user: &User,
|
user: &User,
|
||||||
@@ -217,6 +342,8 @@ impl Server {
|
|||||||
stats: Arc<ServerStats>,
|
stats: Arc<ServerStats>,
|
||||||
auth_hash: Arc<RwLock<Option<String>>>,
|
auth_hash: Arc<RwLock<Option<String>>>,
|
||||||
cleanup_connections: bool,
|
cleanup_connections: bool,
|
||||||
|
log_client_parameter_status_changes: bool,
|
||||||
|
prepared_statement_cache_size: usize,
|
||||||
) -> Result<Server, Error> {
|
) -> Result<Server, Error> {
|
||||||
let cached_resolver = CACHED_RESOLVER.load();
|
let cached_resolver = CACHED_RESOLVER.load();
|
||||||
let mut addr_set: Option<AddrSet> = None;
|
let mut addr_set: Option<AddrSet> = None;
|
||||||
@@ -316,10 +443,7 @@ impl Server {
|
|||||||
|
|
||||||
// Something else?
|
// Something else?
|
||||||
m => {
|
m => {
|
||||||
return Err(Error::SocketError(format!(
|
return Err(Error::SocketError(format!("Unknown message: {}", { m })));
|
||||||
"Unknown message: {}",
|
|
||||||
m as char
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -337,27 +461,22 @@ impl Server {
|
|||||||
None => &user.username,
|
None => &user.username,
|
||||||
};
|
};
|
||||||
|
|
||||||
let password = match user.server_password {
|
let password = match user.server_password.as_ref() {
|
||||||
Some(ref server_password) => Some(server_password),
|
Some(server_password) => Some(server_password),
|
||||||
None => match user.password {
|
None => user.password.as_ref(),
|
||||||
Some(ref password) => Some(password),
|
|
||||||
None => None,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
startup(&mut stream, username, database).await?;
|
startup(&mut stream, username, database).await?;
|
||||||
|
|
||||||
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);
|
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: Option<ScramSha256> = password.map(|password| ScramSha256::new(password));
|
||||||
Some(password) => Some(ScramSha256::new(password)),
|
|
||||||
None => None,
|
let mut server_parameters = ServerParameters::new();
|
||||||
};
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let code = match stream.read_u8().await {
|
let code = match stream.read_u8().await {
|
||||||
@@ -588,8 +707,7 @@ impl Server {
|
|||||||
|
|
||||||
// An error message will be present.
|
// An error message will be present.
|
||||||
_ => {
|
_ => {
|
||||||
// Read the error message without the terminating null character.
|
let mut error = vec![0u8; len as usize];
|
||||||
let mut error = vec![0u8; len as usize - 4 - 1];
|
|
||||||
|
|
||||||
match stream.read_exact(&mut error).await {
|
match stream.read_exact(&mut error).await {
|
||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
@@ -601,10 +719,14 @@ impl Server {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: the error message contains multiple fields; we can decode them and
|
let fields = match PgErrorMsg::parse(&error) {
|
||||||
// present a prettier message to the user.
|
Ok(f) => f,
|
||||||
// See: https://www.postgresql.org/docs/12/protocol-error-fields.html
|
Err(err) => {
|
||||||
error!("Server error: {}", String::from_utf8_lossy(&error));
|
return Err(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
trace!("error fields: {}", &fields);
|
||||||
|
error!("server error: {}: {}", fields.severity, fields.message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -613,9 +735,10 @@ impl Server {
|
|||||||
|
|
||||||
// ParameterStatus
|
// ParameterStatus
|
||||||
'S' => {
|
'S' => {
|
||||||
let mut param = vec![0u8; len as usize - 4];
|
let mut bytes = BytesMut::with_capacity(len as usize - 4);
|
||||||
|
bytes.resize(len as usize - mem::size_of::<i32>(), b'0');
|
||||||
|
|
||||||
match stream.read_exact(&mut param).await {
|
match stream.read_exact(&mut bytes[..]).await {
|
||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
return Err(Error::ServerStartupError(
|
return Err(Error::ServerStartupError(
|
||||||
@@ -625,12 +748,13 @@ impl Server {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let key = bytes.read_string().unwrap();
|
||||||
|
let value = bytes.read_string().unwrap();
|
||||||
|
|
||||||
// Save the parameter so we can pass it to the client later.
|
// Save the parameter so we can pass it to the client later.
|
||||||
// These can be server_encoding, client_encoding, server timezone, Postgres version,
|
// These can be server_encoding, client_encoding, server timezone, Postgres version,
|
||||||
// and many more interesting things we should know about the Postgres server we are talking to.
|
// and many more interesting things we should know about the Postgres server we are talking to.
|
||||||
server_info.put_u8(b'S');
|
server_parameters.set_param(key, value, true);
|
||||||
server_info.put_i32(len);
|
|
||||||
server_info.put_slice(¶m[..]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// BackendKeyData
|
// BackendKeyData
|
||||||
@@ -672,11 +796,11 @@ impl Server {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut server = Server {
|
let server = Server {
|
||||||
address: address.clone(),
|
address: address.clone(),
|
||||||
stream: BufStream::new(stream),
|
stream: BufStream::new(stream),
|
||||||
buffer: BytesMut::with_capacity(8196),
|
buffer: BytesMut::with_capacity(8196),
|
||||||
server_info,
|
server_parameters,
|
||||||
process_id,
|
process_id,
|
||||||
secret_key,
|
secret_key,
|
||||||
in_transaction: false,
|
in_transaction: false,
|
||||||
@@ -688,7 +812,7 @@ impl Server {
|
|||||||
addr_set,
|
addr_set,
|
||||||
connected_at: chrono::offset::Utc::now().naive_utc(),
|
connected_at: chrono::offset::Utc::now().naive_utc(),
|
||||||
stats,
|
stats,
|
||||||
application_name: String::new(),
|
application_name: "pgcat".to_string(),
|
||||||
last_activity: SystemTime::now(),
|
last_activity: SystemTime::now(),
|
||||||
mirror_manager: match address.mirrors.len() {
|
mirror_manager: match address.mirrors.len() {
|
||||||
0 => None,
|
0 => None,
|
||||||
@@ -699,11 +823,16 @@ impl Server {
|
|||||||
)),
|
)),
|
||||||
},
|
},
|
||||||
cleanup_connections,
|
cleanup_connections,
|
||||||
prepared_statements: BTreeSet::new(),
|
log_client_parameter_status_changes,
|
||||||
|
prepared_statement_cache: match prepared_statement_cache_size {
|
||||||
|
0 => None,
|
||||||
|
_ => Some(LruCache::new(
|
||||||
|
NonZeroUsize::new(prepared_statement_cache_size).unwrap(),
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
registering_prepared_statement: VecDeque::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
server.set_name("pgcat").await?;
|
|
||||||
|
|
||||||
return Ok(server);
|
return Ok(server);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -753,7 +882,7 @@ impl Server {
|
|||||||
self.mirror_send(messages);
|
self.mirror_send(messages);
|
||||||
self.stats().data_sent(messages.len());
|
self.stats().data_sent(messages.len());
|
||||||
|
|
||||||
match write_all_flush(&mut self.stream, &messages).await {
|
match write_all_flush(&mut self.stream, messages).await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
// Successfully sent to server
|
// Successfully sent to server
|
||||||
self.last_activity = SystemTime::now();
|
self.last_activity = SystemTime::now();
|
||||||
@@ -773,7 +902,10 @@ impl Server {
|
|||||||
/// 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.
|
||||||
pub async fn recv(&mut self) -> Result<BytesMut, Error> {
|
pub async fn recv(
|
||||||
|
&mut self,
|
||||||
|
mut client_server_parameters: Option<&mut ServerParameters>,
|
||||||
|
) -> Result<BytesMut, Error> {
|
||||||
loop {
|
loop {
|
||||||
let mut message = match read_message(&mut self.stream).await {
|
let mut message = match read_message(&mut self.stream).await {
|
||||||
Ok(message) => message,
|
Ok(message) => message,
|
||||||
@@ -828,7 +960,6 @@ impl Server {
|
|||||||
|
|
||||||
// There is no more data available from the server.
|
// There is no more data available from the server.
|
||||||
self.data_available = false;
|
self.data_available = false;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -837,6 +968,37 @@ impl Server {
|
|||||||
if self.in_copy_mode {
|
if self.in_copy_mode {
|
||||||
self.in_copy_mode = false;
|
self.in_copy_mode = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove the prepared statement from the cache, it has a syntax error or something else bad happened.
|
||||||
|
if let Some(prepared_stmt_name) =
|
||||||
|
self.registering_prepared_statement.pop_front()
|
||||||
|
{
|
||||||
|
if let Some(ref mut cache) = self.prepared_statement_cache {
|
||||||
|
if let Some(_removed) = cache.pop(&prepared_stmt_name) {
|
||||||
|
debug!(
|
||||||
|
"Removed {} from prepared statement cache",
|
||||||
|
prepared_stmt_name
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Shouldn't happen.
|
||||||
|
debug!("Prepared statement {} was not cached", prepared_stmt_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.prepared_statement_cache.is_some() {
|
||||||
|
let error_message = PgErrorMsg::parse(&message)?;
|
||||||
|
if error_message.message == "cached plan must not change result type" {
|
||||||
|
warn!("Server {:?} changed schema, dropping connection to clean up prepared statements", self.address);
|
||||||
|
// This will still result in an error to the client, but this server connection will drop all cached prepared statements
|
||||||
|
// so that any new queries will be re-prepared
|
||||||
|
// TODO: Other ideas to solve errors when there are DDL changes after a statement has been prepared
|
||||||
|
// - Recreate entire connection pool to force recreation of all server connections
|
||||||
|
// - Clear the ConnectionPool's statement cache so that new statement names are generated
|
||||||
|
// - Implement a retry (re-prepare) so the client doesn't see an error
|
||||||
|
self.cleanup_state.needs_cleanup_prepare = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CommandComplete
|
// CommandComplete
|
||||||
@@ -845,24 +1007,24 @@ impl Server {
|
|||||||
self.in_copy_mode = false;
|
self.in_copy_mode = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut command_tag = String::new();
|
match message.read_string() {
|
||||||
match message.reader().read_to_string(&mut command_tag) {
|
Ok(command) => {
|
||||||
Ok(_) => {
|
|
||||||
// Non-exhaustive list of commands that are likely to change session variables/resources
|
// 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
|
// 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
|
// from poisoning a transaction-mode pool by setting inappropriate session variables
|
||||||
match command_tag.as_str() {
|
match command.as_str() {
|
||||||
"SET\0" => {
|
"SET" => {
|
||||||
// We don't detect set statements in transactions
|
// We don't detect set statements in transactions
|
||||||
// No great way to differentiate between set and set local
|
// No great way to differentiate between set and set local
|
||||||
// As a result, we will miss cases when set statements are used in transactions
|
// As a result, we will miss cases when set statements are used in transactions
|
||||||
// This will reduce amount of discard statements sent
|
// This will reduce amount of reset statements sent
|
||||||
if !self.in_transaction {
|
if !self.in_transaction {
|
||||||
debug!("Server connection marked for clean up");
|
debug!("Server connection marked for clean up");
|
||||||
self.cleanup_state.needs_cleanup_set = true;
|
self.cleanup_state.needs_cleanup_set = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"PREPARE\0" => {
|
|
||||||
|
"PREPARE" => {
|
||||||
debug!("Server connection marked for clean up");
|
debug!("Server connection marked for clean up");
|
||||||
self.cleanup_state.needs_cleanup_prepare = true;
|
self.cleanup_state.needs_cleanup_prepare = true;
|
||||||
}
|
}
|
||||||
@@ -876,6 +1038,20 @@ impl Server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
'S' => {
|
||||||
|
let key = message.read_string().unwrap();
|
||||||
|
let value = message.read_string().unwrap();
|
||||||
|
|
||||||
|
if let Some(client_server_parameters) = client_server_parameters.as_mut() {
|
||||||
|
client_server_parameters.set_param(key.clone(), value.clone(), false);
|
||||||
|
if self.log_client_parameter_status_changes {
|
||||||
|
info!("Client parameter status change: {} = {}", key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.server_parameters.set_param(key, value, false);
|
||||||
|
}
|
||||||
|
|
||||||
// DataRow
|
// 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.
|
||||||
@@ -912,6 +1088,11 @@ impl Server {
|
|||||||
// Buffer until ReadyForQuery shows up, so don't exit the loop yet.
|
// Buffer until ReadyForQuery shows up, so don't exit the loop yet.
|
||||||
'c' => (),
|
'c' => (),
|
||||||
|
|
||||||
|
// Parse complete successfully
|
||||||
|
'1' => {
|
||||||
|
self.registering_prepared_statement.pop_front();
|
||||||
|
}
|
||||||
|
|
||||||
// Anything else, e.g. errors, notices, etc.
|
// Anything else, e.g. errors, notices, etc.
|
||||||
// Keep buffering until ReadyForQuery shows up.
|
// Keep buffering until ReadyForQuery shows up.
|
||||||
_ => (),
|
_ => (),
|
||||||
@@ -933,117 +1114,103 @@ impl Server {
|
|||||||
Ok(bytes)
|
Ok(bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add the prepared statement to being tracked by this server.
|
// Determines if the server already has a prepared statement with the given name
|
||||||
/// The client is processing data that will create a prepared statement on this server.
|
// Increments the prepared statement cache hit counter
|
||||||
pub fn will_prepare(&mut self, name: &str) {
|
pub fn has_prepared_statement(&mut self, name: &str) -> bool {
|
||||||
debug!("Will prepare `{}`", name);
|
let cache = match &mut self.prepared_statement_cache {
|
||||||
|
Some(cache) => cache,
|
||||||
|
None => return false,
|
||||||
|
};
|
||||||
|
|
||||||
self.prepared_statements.insert(name.to_string());
|
let has_it = cache.get(name).is_some();
|
||||||
self.stats.prepared_cache_add();
|
if has_it {
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if we should prepare a statement on the server.
|
|
||||||
pub fn should_prepare(&self, name: &str) -> bool {
|
|
||||||
let should_prepare = !self.prepared_statements.contains(name);
|
|
||||||
|
|
||||||
debug!("Should prepare `{}`: {}", name, should_prepare);
|
|
||||||
|
|
||||||
if should_prepare {
|
|
||||||
self.stats.prepared_cache_miss();
|
|
||||||
} else {
|
|
||||||
self.stats.prepared_cache_hit();
|
self.stats.prepared_cache_hit();
|
||||||
|
} else {
|
||||||
|
self.stats.prepared_cache_miss();
|
||||||
}
|
}
|
||||||
|
|
||||||
should_prepare
|
has_it
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a prepared statement on the server.
|
fn add_prepared_statement_to_cache(&mut self, name: &str) -> Option<String> {
|
||||||
pub async fn prepare(&mut self, parse: &Parse) -> Result<(), Error> {
|
let cache = match &mut self.prepared_statement_cache {
|
||||||
debug!("Preparing `{}`", parse.name);
|
Some(cache) => cache,
|
||||||
|
None => return None,
|
||||||
|
};
|
||||||
|
|
||||||
let bytes: BytesMut = parse.try_into()?;
|
|
||||||
self.send(&bytes).await?;
|
|
||||||
self.send(&flush()).await?;
|
|
||||||
|
|
||||||
// Read and discard ParseComplete (B)
|
|
||||||
match read_message(&mut self.stream).await {
|
|
||||||
Ok(_) => (),
|
|
||||||
Err(err) => {
|
|
||||||
self.bad = true;
|
|
||||||
return Err(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.prepared_statements.insert(parse.name.to_string());
|
|
||||||
self.stats.prepared_cache_add();
|
self.stats.prepared_cache_add();
|
||||||
|
|
||||||
debug!("Prepared `{}`", parse.name);
|
// If we evict something, we need to close it on the server
|
||||||
|
if let Some((evicted_name, _)) = cache.push(name.to_string(), ()) {
|
||||||
Ok(())
|
if evicted_name != name {
|
||||||
}
|
debug!(
|
||||||
|
"Evicted prepared statement {} from cache, replaced with {}",
|
||||||
/// Maintain adequate cache size on the server.
|
evicted_name, name
|
||||||
pub async fn maintain_cache(&mut self) -> Result<(), Error> {
|
);
|
||||||
debug!("Cache maintenance run");
|
return Some(evicted_name);
|
||||||
|
|
||||||
let max_cache_size = get_prepared_statements_cache_size();
|
|
||||||
let mut names = Vec::new();
|
|
||||||
|
|
||||||
while self.prepared_statements.len() >= max_cache_size {
|
|
||||||
// The prepared statmeents are alphanumerically sorted by the BTree.
|
|
||||||
// FIFO.
|
|
||||||
if let Some(name) = self.prepared_statements.pop_last() {
|
|
||||||
names.push(name);
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
if !names.is_empty() {
|
None
|
||||||
self.deallocate(names).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove the prepared statement from being tracked by this server.
|
fn remove_prepared_statement_from_cache(&mut self, name: &str) {
|
||||||
/// The client is processing data that will cause the server to close the prepared statement.
|
let cache = match &mut self.prepared_statement_cache {
|
||||||
pub fn will_close(&mut self, name: &str) {
|
Some(cache) => cache,
|
||||||
debug!("Will close `{}`", name);
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
self.prepared_statements.remove(name);
|
self.stats.prepared_cache_remove();
|
||||||
|
cache.pop(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Close a prepared statement on the server.
|
pub async fn register_prepared_statement(
|
||||||
pub async fn deallocate(&mut self, names: Vec<String>) -> Result<(), Error> {
|
&mut self,
|
||||||
for name in &names {
|
parse: &Parse,
|
||||||
debug!("Deallocating prepared statement `{}`", name);
|
should_send_parse_to_server: bool,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
if !self.has_prepared_statement(&parse.name) {
|
||||||
|
self.registering_prepared_statement
|
||||||
|
.push_back(parse.name.clone());
|
||||||
|
|
||||||
let close = Close::new(name);
|
let mut bytes = BytesMut::new();
|
||||||
let bytes: BytesMut = close.try_into()?;
|
|
||||||
|
|
||||||
self.send(&bytes).await?;
|
if should_send_parse_to_server {
|
||||||
}
|
let parse_bytes: BytesMut = parse.try_into()?;
|
||||||
|
bytes.extend_from_slice(&parse_bytes);
|
||||||
|
}
|
||||||
|
|
||||||
if !names.is_empty() {
|
// If we evict something, we need to close it on the server
|
||||||
self.send(&flush()).await?;
|
// We do this by adding it to the messages we're sending to the server before the sync
|
||||||
}
|
if let Some(evicted_name) = self.add_prepared_statement_to_cache(&parse.name) {
|
||||||
|
self.remove_prepared_statement_from_cache(&evicted_name);
|
||||||
// Read and discard CloseComplete (3)
|
let close_bytes: BytesMut = Close::new(&evicted_name).try_into()?;
|
||||||
for name in &names {
|
bytes.extend_from_slice(&close_bytes);
|
||||||
match read_message(&mut self.stream).await {
|
|
||||||
Ok(_) => {
|
|
||||||
self.prepared_statements.remove(name);
|
|
||||||
self.stats.prepared_cache_remove();
|
|
||||||
debug!("Closed `{}`", name);
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(err) => {
|
|
||||||
self.bad = true;
|
|
||||||
return Err(err);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
// If we have a parse or close we need to send to the server, send them and sync
|
||||||
|
if !bytes.is_empty() {
|
||||||
|
bytes.extend_from_slice(&sync());
|
||||||
|
|
||||||
|
self.send(&bytes).await?;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
self.recv(None).await?;
|
||||||
|
|
||||||
|
if !self.is_data_available() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// If it's not there, something went bad, I'm guessing bad syntax or permissions error
|
||||||
|
// on the server.
|
||||||
|
if !self.has_prepared_statement(&parse.name) {
|
||||||
|
Err(Error::PreparedStatementError)
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// If the server is still inside a transaction.
|
/// If the server is still inside a transaction.
|
||||||
@@ -1053,6 +1220,7 @@ impl Server {
|
|||||||
self.in_transaction
|
self.in_transaction
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Currently copying data from client to server or vice-versa.
|
||||||
pub fn in_copy_mode(&self) -> bool {
|
pub fn in_copy_mode(&self) -> bool {
|
||||||
self.in_copy_mode
|
self.in_copy_mode
|
||||||
}
|
}
|
||||||
@@ -1086,14 +1254,33 @@ impl Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get server startup information to forward it to the client.
|
/// Get server startup information to forward it to the client.
|
||||||
/// Not used at the moment.
|
pub fn server_parameters(&self) -> ServerParameters {
|
||||||
pub fn server_info(&self) -> BytesMut {
|
self.server_parameters.clone()
|
||||||
self.server_info.clone()
|
}
|
||||||
|
|
||||||
|
pub async fn sync_parameters(&mut self, parameters: &ServerParameters) -> Result<(), Error> {
|
||||||
|
let parameter_diff = self.server_parameters.compare_params(parameters);
|
||||||
|
|
||||||
|
if parameter_diff.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut query = String::from("");
|
||||||
|
|
||||||
|
for (key, value) in parameter_diff {
|
||||||
|
query.push_str(&format!("SET {} TO '{}';", key, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = self.query(&query).await;
|
||||||
|
|
||||||
|
self.cleanup_state.reset();
|
||||||
|
|
||||||
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Indicate that this server connection cannot be re-used and must be discarded.
|
/// Indicate that this server connection cannot be re-used and must be discarded.
|
||||||
pub fn mark_bad(&mut self) {
|
pub fn mark_bad(&mut self, reason: &str) {
|
||||||
error!("Server {:?} marked bad", self.address);
|
error!("Server {:?} marked bad, reason: {}", self.address, reason);
|
||||||
self.bad = true;
|
self.bad = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1122,7 +1309,7 @@ impl Server {
|
|||||||
self.send(&query).await?;
|
self.send(&query).await?;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let _ = self.recv().await?;
|
let _ = self.recv(None).await?;
|
||||||
|
|
||||||
if !self.data_available {
|
if !self.data_available {
|
||||||
break;
|
break;
|
||||||
@@ -1147,12 +1334,25 @@ impl Server {
|
|||||||
// Client disconnected but it performed session-altering operations such as
|
// Client disconnected but it performed session-altering operations such as
|
||||||
// SET statement_timeout to 1 or create a prepared statement. We clear that
|
// SET statement_timeout to 1 or create a prepared statement. We clear that
|
||||||
// to avoid leaking state between clients. For performance reasons we only
|
// 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
|
// send `RESET ALL` if we think the session is altered instead of just sending
|
||||||
// it before each checkin.
|
// it before each checkin.
|
||||||
if self.cleanup_state.needs_cleanup() && self.cleanup_connections {
|
if self.cleanup_state.needs_cleanup() && self.cleanup_connections {
|
||||||
info!(target: "pgcat::server::cleanup", "Server returned with session state altered, discarding state ({}) for application {}", self.cleanup_state, self.application_name);
|
info!(target: "pgcat::server::cleanup", "Server returned with session state altered, discarding state ({}) for application {}", self.cleanup_state, self.application_name);
|
||||||
self.query("DISCARD ALL").await?;
|
let mut reset_string = String::from("RESET ROLE;");
|
||||||
self.query("RESET ROLE").await?;
|
|
||||||
|
if self.cleanup_state.needs_cleanup_set {
|
||||||
|
reset_string.push_str("RESET ALL;");
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.cleanup_state.needs_cleanup_prepare {
|
||||||
|
reset_string.push_str("DEALLOCATE ALL;");
|
||||||
|
// Since we deallocated all prepared statements, we need to clear the cache
|
||||||
|
if let Some(cache) = &mut self.prepared_statement_cache {
|
||||||
|
cache.clear();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.query(&reset_string).await?;
|
||||||
self.cleanup_state.reset();
|
self.cleanup_state.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1163,24 +1363,6 @@ impl Server {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A shorthand for `SET application_name = $1`.
|
|
||||||
pub async fn set_name(&mut self, name: &str) -> Result<(), Error> {
|
|
||||||
if self.application_name != name {
|
|
||||||
self.application_name = name.to_string();
|
|
||||||
// We don't want `SET application_name` to mark the server connection
|
|
||||||
// as needing cleanup
|
|
||||||
let needs_cleanup_before = self.cleanup_state;
|
|
||||||
|
|
||||||
let result = Ok(self
|
|
||||||
.query(&format!("SET application_name = '{}'", name))
|
|
||||||
.await?);
|
|
||||||
self.cleanup_state = needs_cleanup_before;
|
|
||||||
result
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// get Server stats
|
/// get Server stats
|
||||||
pub fn stats(&self) -> Arc<ServerStats> {
|
pub fn stats(&self) -> Arc<ServerStats> {
|
||||||
self.stats.clone()
|
self.stats.clone()
|
||||||
@@ -1197,22 +1379,20 @@ impl Server {
|
|||||||
self.last_activity
|
self.last_activity
|
||||||
}
|
}
|
||||||
|
|
||||||
// Marks a connection as needing DISCARD ALL at checkin
|
// Marks a connection as needing cleanup at checkin
|
||||||
pub fn mark_dirty(&mut self) {
|
pub fn mark_dirty(&mut self) {
|
||||||
self.cleanup_state.set_true();
|
self.cleanup_state.set_true();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mirror_send(&mut self, bytes: &BytesMut) {
|
pub fn mirror_send(&mut self, bytes: &BytesMut) {
|
||||||
match self.mirror_manager.as_mut() {
|
if let Some(manager) = self.mirror_manager.as_mut() {
|
||||||
Some(manager) => manager.send(bytes),
|
manager.send(bytes)
|
||||||
None => (),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mirror_disconnect(&mut self) {
|
pub fn mirror_disconnect(&mut self) {
|
||||||
match self.mirror_manager.as_mut() {
|
if let Some(manager) = self.mirror_manager.as_mut() {
|
||||||
Some(manager) => manager.disconnect(),
|
manager.disconnect()
|
||||||
None => (),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1234,13 +1414,15 @@ impl Server {
|
|||||||
Arc::new(ServerStats::default()),
|
Arc::new(ServerStats::default()),
|
||||||
Arc::new(RwLock::new(None)),
|
Arc::new(RwLock::new(None)),
|
||||||
true,
|
true,
|
||||||
|
false,
|
||||||
|
0,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
debug!("Connected!, sending query.");
|
debug!("Connected!, sending query.");
|
||||||
server.send(&simple_query(query)).await?;
|
server.send(&simple_query(query)).await?;
|
||||||
let mut message = server.recv().await?;
|
let mut message = server.recv(None).await?;
|
||||||
|
|
||||||
Ok(parse_query_message(&mut message).await?)
|
parse_query_message(&mut message).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ pub enum ShardingFunction {
|
|||||||
Sha1,
|
Sha1,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToString for ShardingFunction {
|
impl std::fmt::Display for ShardingFunction {
|
||||||
fn to_string(&self) -> String {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match *self {
|
match self {
|
||||||
ShardingFunction::PgBigintHash => "pg_bigint_hash".to_string(),
|
ShardingFunction::PgBigintHash => write!(f, "pg_bigint_hash"),
|
||||||
ShardingFunction::Sha1 => "sha1".to_string(),
|
ShardingFunction::Sha1 => write!(f, "sha1"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,7 +64,7 @@ impl Sharder {
|
|||||||
fn sha1(&self, key: i64) -> usize {
|
fn sha1(&self, key: i64) -> usize {
|
||||||
let mut hasher = Sha1::new();
|
let mut hasher = Sha1::new();
|
||||||
|
|
||||||
hasher.update(&key.to_string().as_bytes());
|
hasher.update(key.to_string().as_bytes());
|
||||||
|
|
||||||
let result = hasher.finalize();
|
let result = hasher.finalize();
|
||||||
|
|
||||||
@@ -202,10 +202,10 @@ mod test {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_sha1_hash() {
|
fn test_sha1_hash() {
|
||||||
let sharder = Sharder::new(12, ShardingFunction::Sha1);
|
let sharder = Sharder::new(12, ShardingFunction::Sha1);
|
||||||
let ids = vec![
|
let ids = [
|
||||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
|
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
|
||||||
];
|
];
|
||||||
let shards = vec![
|
let shards = [
|
||||||
4, 7, 8, 3, 6, 0, 0, 10, 3, 11, 1, 7, 4, 4, 11, 2, 5, 0, 8, 3,
|
4, 7, 8, 3, 6, 0, 0, 10, 3, 11, 1, 7, 4, 4, 11, 2, 5, 0, 8, 3,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ pub struct ClientStats {
|
|||||||
/// Maximum time spent waiting for a connection from pool, measures in microseconds
|
/// Maximum time spent waiting for a connection from pool, measures in microseconds
|
||||||
pub max_wait_time: Arc<AtomicU64>,
|
pub max_wait_time: Arc<AtomicU64>,
|
||||||
|
|
||||||
|
// Time when the client started waiting for a connection from pool, measures in microseconds
|
||||||
|
// We use connect_time as the reference point for this value
|
||||||
|
// U64 can represent ~5850 centuries in microseconds, so we should be fine
|
||||||
|
pub wait_start_us: Arc<AtomicU64>,
|
||||||
|
|
||||||
/// Current state of the client
|
/// Current state of the client
|
||||||
pub state: Arc<AtomicClientState>,
|
pub state: Arc<AtomicClientState>,
|
||||||
|
|
||||||
@@ -64,6 +69,7 @@ impl Default for ClientStats {
|
|||||||
pool_name: String::new(),
|
pool_name: String::new(),
|
||||||
total_wait_time: Arc::new(AtomicU64::new(0)),
|
total_wait_time: Arc::new(AtomicU64::new(0)),
|
||||||
max_wait_time: Arc::new(AtomicU64::new(0)),
|
max_wait_time: Arc::new(AtomicU64::new(0)),
|
||||||
|
wait_start_us: Arc::new(AtomicU64::new(0)),
|
||||||
state: Arc::new(AtomicClientState::new(ClientState::Idle)),
|
state: Arc::new(AtomicClientState::new(ClientState::Idle)),
|
||||||
transaction_count: Arc::new(AtomicU64::new(0)),
|
transaction_count: Arc::new(AtomicU64::new(0)),
|
||||||
query_count: Arc::new(AtomicU64::new(0)),
|
query_count: Arc::new(AtomicU64::new(0)),
|
||||||
@@ -111,6 +117,9 @@ impl ClientStats {
|
|||||||
|
|
||||||
/// Reports a client is waiting for a connection
|
/// Reports a client is waiting for a connection
|
||||||
pub fn waiting(&self) {
|
pub fn waiting(&self) {
|
||||||
|
let wait_start = self.connect_time.elapsed().as_micros() as u64;
|
||||||
|
|
||||||
|
self.wait_start_us.store(wait_start, Ordering::Relaxed);
|
||||||
self.state.store(ClientState::Waiting, Ordering::Relaxed);
|
self.state.store(ClientState::Waiting, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,6 +131,13 @@ impl ClientStats {
|
|||||||
/// Reports a client has failed to obtain a connection from a connection pool
|
/// Reports a client has failed to obtain a connection from a connection pool
|
||||||
pub fn checkout_error(&self) {
|
pub fn checkout_error(&self) {
|
||||||
self.state.store(ClientState::Idle, Ordering::Relaxed);
|
self.state.store(ClientState::Idle, Ordering::Relaxed);
|
||||||
|
self.update_wait_times();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reports a client has succeeded in obtaining a connection from a connection pool
|
||||||
|
pub fn checkout_success(&self) {
|
||||||
|
self.state.store(ClientState::Active, Ordering::Relaxed);
|
||||||
|
self.update_wait_times();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reports a client has had the server assigned to it be banned
|
/// Reports a client has had the server assigned to it be banned
|
||||||
@@ -130,12 +146,26 @@ impl ClientStats {
|
|||||||
self.error_count.fetch_add(1, 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
|
fn update_wait_times(&self) {
|
||||||
pub fn checkout_time(&self, microseconds: u64) {
|
if self.wait_start_us.load(Ordering::Relaxed) == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let wait_time_us = self.get_current_wait_time_us();
|
||||||
self.total_wait_time
|
self.total_wait_time
|
||||||
.fetch_add(microseconds, Ordering::Relaxed);
|
.fetch_add(wait_time_us, Ordering::Relaxed);
|
||||||
self.max_wait_time
|
self.max_wait_time
|
||||||
.fetch_max(microseconds, Ordering::Relaxed);
|
.fetch_max(wait_time_us, Ordering::Relaxed);
|
||||||
|
self.wait_start_us.store(0, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_current_wait_time_us(&self) -> u64 {
|
||||||
|
let wait_start_us = self.wait_start_us.load(Ordering::Relaxed);
|
||||||
|
let microseconds_since_connection_epoch = self.connect_time.elapsed().as_micros() as u64;
|
||||||
|
if wait_start_us == 0 || microseconds_since_connection_epoch < wait_start_us {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
microseconds_since_connection_epoch - wait_start_us
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Report a query executed by a client against a server
|
/// Report a query executed by a client against a server
|
||||||
|
|||||||
@@ -64,8 +64,11 @@ impl PoolStats {
|
|||||||
ClientState::Idle => pool_stats.cl_idle += 1,
|
ClientState::Idle => pool_stats.cl_idle += 1,
|
||||||
ClientState::Waiting => pool_stats.cl_waiting += 1,
|
ClientState::Waiting => pool_stats.cl_waiting += 1,
|
||||||
}
|
}
|
||||||
let max_wait = client.max_wait_time.load(Ordering::Relaxed);
|
let wait_start_us = client.wait_start_us.load(Ordering::Relaxed);
|
||||||
pool_stats.maxwait = std::cmp::max(pool_stats.maxwait, max_wait);
|
if wait_start_us > 0 {
|
||||||
|
let wait_time_us = client.get_current_wait_time_us();
|
||||||
|
pool_stats.maxwait = std::cmp::max(pool_stats.maxwait, wait_time_us);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
None => debug!("Client from an obselete pool"),
|
None => debug!("Client from an obselete pool"),
|
||||||
}
|
}
|
||||||
@@ -86,11 +89,11 @@ impl PoolStats {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return map;
|
map
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_header() -> Vec<(&'static str, DataType)> {
|
pub fn generate_header() -> Vec<(&'static str, DataType)> {
|
||||||
return vec![
|
vec![
|
||||||
("database", DataType::Text),
|
("database", DataType::Text),
|
||||||
("user", DataType::Text),
|
("user", DataType::Text),
|
||||||
("pool_mode", DataType::Text),
|
("pool_mode", DataType::Text),
|
||||||
@@ -105,11 +108,11 @@ impl PoolStats {
|
|||||||
("sv_login", DataType::Numeric),
|
("sv_login", DataType::Numeric),
|
||||||
("maxwait", DataType::Numeric),
|
("maxwait", DataType::Numeric),
|
||||||
("maxwait_us", DataType::Numeric),
|
("maxwait_us", DataType::Numeric),
|
||||||
];
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_row(&self) -> Vec<String> {
|
pub fn generate_row(&self) -> Vec<String> {
|
||||||
return vec![
|
vec![
|
||||||
self.identifier.db.clone(),
|
self.identifier.db.clone(),
|
||||||
self.identifier.user.clone(),
|
self.identifier.user.clone(),
|
||||||
self.mode.to_string(),
|
self.mode.to_string(),
|
||||||
@@ -124,7 +127,7 @@ impl PoolStats {
|
|||||||
self.sv_login.to_string(),
|
self.sv_login.to_string(),
|
||||||
(self.maxwait / 1_000_000).to_string(),
|
(self.maxwait / 1_000_000).to_string(),
|
||||||
(self.maxwait % 1_000_000).to_string(),
|
(self.maxwait % 1_000_000).to_string(),
|
||||||
];
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ pub struct ServerStats {
|
|||||||
pub error_count: Arc<AtomicU64>,
|
pub error_count: Arc<AtomicU64>,
|
||||||
pub prepared_hit_count: Arc<AtomicU64>,
|
pub prepared_hit_count: Arc<AtomicU64>,
|
||||||
pub prepared_miss_count: Arc<AtomicU64>,
|
pub prepared_miss_count: Arc<AtomicU64>,
|
||||||
|
pub prepared_eviction_count: Arc<AtomicU64>,
|
||||||
pub prepared_cache_size: Arc<AtomicU64>,
|
pub prepared_cache_size: Arc<AtomicU64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +69,7 @@ impl Default for ServerStats {
|
|||||||
reporter: get_reporter(),
|
reporter: get_reporter(),
|
||||||
prepared_hit_count: Arc::new(AtomicU64::new(0)),
|
prepared_hit_count: Arc::new(AtomicU64::new(0)),
|
||||||
prepared_miss_count: Arc::new(AtomicU64::new(0)),
|
prepared_miss_count: Arc::new(AtomicU64::new(0)),
|
||||||
|
prepared_eviction_count: Arc::new(AtomicU64::new(0)),
|
||||||
prepared_cache_size: Arc::new(AtomicU64::new(0)),
|
prepared_cache_size: Arc::new(AtomicU64::new(0)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -221,6 +223,7 @@ impl ServerStats {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn prepared_cache_remove(&self) {
|
pub fn prepared_cache_remove(&self) {
|
||||||
|
self.prepared_eviction_count.fetch_add(1, Ordering::Relaxed);
|
||||||
self.prepared_cache_size.fetch_sub(1, Ordering::Relaxed);
|
self.prepared_cache_size.fetch_sub(1, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
34
start_test_env.sh
Executable file
34
start_test_env.sh
Executable file
@@ -0,0 +1,34 @@
|
|||||||
|
GREEN="\033[0;32m"
|
||||||
|
RED="\033[0;31m"
|
||||||
|
BLUE="\033[0;34m"
|
||||||
|
RESET="\033[0m"
|
||||||
|
|
||||||
|
|
||||||
|
cd tests/docker/
|
||||||
|
docker compose kill main || true
|
||||||
|
docker compose build main
|
||||||
|
docker compose down
|
||||||
|
docker compose up -d
|
||||||
|
# wait for the container to start
|
||||||
|
while ! docker compose exec main ls; do
|
||||||
|
echo "Waiting for test environment to start"
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
echo "==================================="
|
||||||
|
docker compose exec -e LOG_LEVEL=error -d main toxiproxy-server
|
||||||
|
docker compose exec --workdir /app main cargo build
|
||||||
|
docker compose exec -d --workdir /app main ./target/debug/pgcat ./.circleci/pgcat.toml
|
||||||
|
docker compose exec --workdir /app/tests/ruby main bundle install
|
||||||
|
docker compose exec --workdir /app/tests/python main pip3 install -r requirements.txt
|
||||||
|
echo "Interactive test environment ready"
|
||||||
|
echo "To run integration tests, you can use the following commands:"
|
||||||
|
echo -e " ${BLUE}Ruby: ${RED}cd /app/tests/ruby && bundle exec ruby tests.rb --format documentation${RESET}"
|
||||||
|
echo -e " ${BLUE}Python: ${RED}cd /app/ && pytest ${RESET}"
|
||||||
|
echo -e " ${BLUE}Rust: ${RED}cd /app/tests/rust && cargo run ${RESET}"
|
||||||
|
echo -e " ${BLUE}Go: ${RED}cd /app/tests/go && /usr/local/go/bin/go test${RESET}"
|
||||||
|
echo "the source code for tests are directly linked to the source code in the container so you can modify the code and run the tests again"
|
||||||
|
echo "You can rebuild PgCat from within the container by running"
|
||||||
|
echo -e " ${GREEN}cargo build${RESET}"
|
||||||
|
echo "and then run the tests again"
|
||||||
|
echo "==================================="
|
||||||
|
docker compose exec --workdir /app/tests main bash
|
||||||
@@ -1,8 +1,13 @@
|
|||||||
FROM rust:bullseye
|
FROM rust:bullseye
|
||||||
|
|
||||||
|
COPY --from=sclevine/yj /bin/yj /bin/yj
|
||||||
|
RUN /bin/yj -h
|
||||||
RUN apt-get update && apt-get install llvm-11 psmisc postgresql-contrib postgresql-client ruby ruby-dev libpq-dev python3 python3-pip lcov curl sudo iproute2 -y
|
RUN 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 cargo install cargo-binutils rustfilt
|
||||||
RUN rustup component add llvm-tools-preview
|
RUN rustup component add llvm-tools-preview
|
||||||
RUN sudo gem install bundler
|
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 && \
|
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
|
sudo dpkg -i toxiproxy-2.4.0.deb
|
||||||
|
RUN wget -O go1.21.3.linux-$(dpkg --print-architecture).tar.gz https://go.dev/dl/go1.21.3.linux-$(dpkg --print-architecture).tar.gz && \
|
||||||
|
sudo tar -C /usr/local -xzf go1.21.3.linux-$(dpkg --print-architecture).tar.gz && \
|
||||||
|
rm go1.21.3.linux-$(dpkg --print-architecture).tar.gz
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
version: "3"
|
|
||||||
services:
|
services:
|
||||||
pg1:
|
pg1:
|
||||||
image: postgres:14
|
image: postgres:14
|
||||||
@@ -48,6 +47,8 @@ services:
|
|||||||
main:
|
main:
|
||||||
build: .
|
build: .
|
||||||
command: ["bash", "/app/tests/docker/run.sh"]
|
command: ["bash", "/app/tests/docker/run.sh"]
|
||||||
|
environment:
|
||||||
|
- INTERACTIVE_TEST_ENVIRONMENT=true
|
||||||
volumes:
|
volumes:
|
||||||
- ../../:/app/
|
- ../../:/app/
|
||||||
- /app/target/
|
- /app/target/
|
||||||
|
|||||||
@@ -5,6 +5,38 @@ rm /app/*.profraw || true
|
|||||||
rm /app/pgcat.profdata || true
|
rm /app/pgcat.profdata || true
|
||||||
rm -rf /app/cov || true
|
rm -rf /app/cov || true
|
||||||
|
|
||||||
|
# Prepares the interactive test environment
|
||||||
|
#
|
||||||
|
if [ -n "$INTERACTIVE_TEST_ENVIRONMENT" ]; then
|
||||||
|
ports=(5432 7432 8432 9432 10432)
|
||||||
|
for port in "${ports[@]}"; do
|
||||||
|
is_it_up=0
|
||||||
|
attempts=0
|
||||||
|
while [ $is_it_up -eq 0 ]; do
|
||||||
|
PGPASSWORD=postgres psql -h 127.0.0.1 -p $port -U postgres -c '\q' > /dev/null 2>&1
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo "PostgreSQL on port $port is up."
|
||||||
|
is_it_up=1
|
||||||
|
else
|
||||||
|
attempts=$((attempts+1))
|
||||||
|
if [ $attempts -gt 10 ]; then
|
||||||
|
echo "PostgreSQL on port $port is down, giving up."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "PostgreSQL on port $port is down, waiting for it to start."
|
||||||
|
sleep 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
PGPASSWORD=postgres psql -e -h 127.0.0.1 -p 5432 -U postgres -f /app/tests/sharding/query_routing_setup.sql
|
||||||
|
PGPASSWORD=postgres psql -e -h 127.0.0.1 -p 7432 -U postgres -f /app/tests/sharding/query_routing_setup.sql
|
||||||
|
PGPASSWORD=postgres psql -e -h 127.0.0.1 -p 8432 -U postgres -f /app/tests/sharding/query_routing_setup.sql
|
||||||
|
PGPASSWORD=postgres psql -e -h 127.0.0.1 -p 9432 -U postgres -f /app/tests/sharding/query_routing_setup.sql
|
||||||
|
PGPASSWORD=postgres psql -e -h 127.0.0.1 -p 10432 -U postgres -f /app/tests/sharding/query_routing_setup.sql
|
||||||
|
sleep 100000000000000000
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
export LLVM_PROFILE_FILE="/app/pgcat-%m-%p.profraw"
|
export LLVM_PROFILE_FILE="/app/pgcat-%m-%p.profraw"
|
||||||
export RUSTC_BOOTSTRAP=1
|
export RUSTC_BOOTSTRAP=1
|
||||||
export CARGO_INCREMENTAL=0
|
export CARGO_INCREMENTAL=0
|
||||||
|
|||||||
5
tests/go/go.mod
Normal file
5
tests/go/go.mod
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module pgcat
|
||||||
|
|
||||||
|
go 1.21
|
||||||
|
|
||||||
|
require github.com/lib/pq v1.10.9
|
||||||
2
tests/go/go.sum
Normal file
2
tests/go/go.sum
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
162
tests/go/pgcat.toml
Normal file
162
tests/go/pgcat.toml
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
#
|
||||||
|
# PgCat config example.
|
||||||
|
#
|
||||||
|
|
||||||
|
#
|
||||||
|
# General pooler settings
|
||||||
|
[general]
|
||||||
|
# What IP to run on, 0.0.0.0 means accessible from everywhere.
|
||||||
|
host = "0.0.0.0"
|
||||||
|
|
||||||
|
# Port to run on, same as PgBouncer used in this example.
|
||||||
|
port = "${PORT}"
|
||||||
|
|
||||||
|
# Whether to enable prometheus exporter or not.
|
||||||
|
enable_prometheus_exporter = true
|
||||||
|
|
||||||
|
# Port at which prometheus exporter listens on.
|
||||||
|
prometheus_exporter_port = 9930
|
||||||
|
|
||||||
|
# How long to wait before aborting a server connection (ms).
|
||||||
|
connect_timeout = 1000
|
||||||
|
|
||||||
|
# How much time to give the health check query to return with a result (ms).
|
||||||
|
healthcheck_timeout = 1000
|
||||||
|
|
||||||
|
# How long to keep connection available for immediate re-use, without running a healthcheck query on it
|
||||||
|
healthcheck_delay = 30000
|
||||||
|
|
||||||
|
# How much time to give clients during shutdown before forcibly killing client connections (ms).
|
||||||
|
shutdown_timeout = 5000
|
||||||
|
|
||||||
|
# For how long to ban a server if it fails a health check (seconds).
|
||||||
|
ban_time = 60 # Seconds
|
||||||
|
|
||||||
|
# If we should log client connections
|
||||||
|
log_client_connections = false
|
||||||
|
|
||||||
|
# If we should log client disconnections
|
||||||
|
log_client_disconnections = false
|
||||||
|
|
||||||
|
# Reload config automatically if it changes.
|
||||||
|
autoreload = 15000
|
||||||
|
|
||||||
|
server_round_robin = false
|
||||||
|
|
||||||
|
# TLS
|
||||||
|
tls_certificate = "../../.circleci/server.cert"
|
||||||
|
tls_private_key = "../../.circleci/server.key"
|
||||||
|
|
||||||
|
# Credentials to access the virtual administrative database (pgbouncer or pgcat)
|
||||||
|
# Connecting to that database allows running commands like `SHOW POOLS`, `SHOW DATABASES`, etc..
|
||||||
|
admin_username = "admin_user"
|
||||||
|
admin_password = "admin_pass"
|
||||||
|
|
||||||
|
# pool
|
||||||
|
# configs are structured as pool.<pool_name>
|
||||||
|
# the pool_name is what clients use as database name when connecting
|
||||||
|
# For the example below a client can connect using "postgres://sharding_user:sharding_user@pgcat_host:pgcat_port/sharded_db"
|
||||||
|
[pools.sharded_db]
|
||||||
|
# Pool mode (see PgBouncer docs for more).
|
||||||
|
# session: one server connection per connected client
|
||||||
|
# transaction: one server connection per client transaction
|
||||||
|
pool_mode = "transaction"
|
||||||
|
|
||||||
|
# If the client doesn't specify, route traffic to
|
||||||
|
# this role by default.
|
||||||
|
#
|
||||||
|
# any: round-robin between primary and replicas,
|
||||||
|
# replica: round-robin between replicas only without touching the primary,
|
||||||
|
# primary: all queries go to the primary unless otherwise specified.
|
||||||
|
default_role = "any"
|
||||||
|
|
||||||
|
# Query parser. If enabled, we'll attempt to parse
|
||||||
|
# every incoming query to determine if it's a read or a write.
|
||||||
|
# If it's a read query, we'll direct it to a replica. Otherwise, if it's a write,
|
||||||
|
# we'll direct it to the primary.
|
||||||
|
query_parser_enabled = true
|
||||||
|
|
||||||
|
# If the query parser is enabled and this setting is enabled, we'll attempt to
|
||||||
|
# infer the role from the query itself.
|
||||||
|
query_parser_read_write_splitting = true
|
||||||
|
|
||||||
|
# If the query parser is enabled and this setting is enabled, the primary will be part of the pool of databases used for
|
||||||
|
# load balancing of read queries. Otherwise, the primary will only be used for write
|
||||||
|
# queries. The primary can always be explicitely selected with our custom protocol.
|
||||||
|
primary_reads_enabled = true
|
||||||
|
|
||||||
|
# So what if you wanted to implement a different hashing function,
|
||||||
|
# or you've already built one and you want this pooler to use it?
|
||||||
|
#
|
||||||
|
# Current options:
|
||||||
|
#
|
||||||
|
# pg_bigint_hash: PARTITION BY HASH (Postgres hashing function)
|
||||||
|
# sha1: A hashing function based on SHA1
|
||||||
|
#
|
||||||
|
sharding_function = "pg_bigint_hash"
|
||||||
|
|
||||||
|
# Prepared statements cache size.
|
||||||
|
prepared_statements_cache_size = 500
|
||||||
|
|
||||||
|
# Credentials for users that may connect to this cluster
|
||||||
|
[pools.sharded_db.users.0]
|
||||||
|
username = "sharding_user"
|
||||||
|
password = "sharding_user"
|
||||||
|
# Maximum number of server connections that can be established for this user
|
||||||
|
# The maximum number of connection from a single Pgcat process to any database in the cluster
|
||||||
|
# is the sum of pool_size across all users.
|
||||||
|
pool_size = 5
|
||||||
|
statement_timeout = 0
|
||||||
|
|
||||||
|
|
||||||
|
[pools.sharded_db.users.1]
|
||||||
|
username = "other_user"
|
||||||
|
password = "other_user"
|
||||||
|
pool_size = 21
|
||||||
|
statement_timeout = 30000
|
||||||
|
|
||||||
|
# Shard 0
|
||||||
|
[pools.sharded_db.shards.0]
|
||||||
|
# [ host, port, role ]
|
||||||
|
servers = [
|
||||||
|
[ "127.0.0.1", 5432, "primary" ],
|
||||||
|
[ "localhost", 5432, "replica" ]
|
||||||
|
]
|
||||||
|
# Database name (e.g. "postgres")
|
||||||
|
database = "shard0"
|
||||||
|
|
||||||
|
[pools.sharded_db.shards.1]
|
||||||
|
servers = [
|
||||||
|
[ "127.0.0.1", 5432, "primary" ],
|
||||||
|
[ "localhost", 5432, "replica" ],
|
||||||
|
]
|
||||||
|
database = "shard1"
|
||||||
|
|
||||||
|
[pools.sharded_db.shards.2]
|
||||||
|
servers = [
|
||||||
|
[ "127.0.0.1", 5432, "primary" ],
|
||||||
|
[ "localhost", 5432, "replica" ],
|
||||||
|
]
|
||||||
|
database = "shard2"
|
||||||
|
|
||||||
|
|
||||||
|
[pools.simple_db]
|
||||||
|
pool_mode = "session"
|
||||||
|
default_role = "primary"
|
||||||
|
query_parser_enabled = true
|
||||||
|
query_parser_read_write_splitting = true
|
||||||
|
primary_reads_enabled = true
|
||||||
|
sharding_function = "pg_bigint_hash"
|
||||||
|
|
||||||
|
[pools.simple_db.users.0]
|
||||||
|
username = "simple_user"
|
||||||
|
password = "simple_user"
|
||||||
|
pool_size = 5
|
||||||
|
statement_timeout = 30000
|
||||||
|
|
||||||
|
[pools.simple_db.shards.0]
|
||||||
|
servers = [
|
||||||
|
[ "127.0.0.1", 5432, "primary" ],
|
||||||
|
[ "localhost", 5432, "replica" ]
|
||||||
|
]
|
||||||
|
database = "some_db"
|
||||||
52
tests/go/prepared_test.go
Normal file
52
tests/go/prepared_test.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package pgcat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
_ "github.com/lib/pq"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test(t *testing.T) {
|
||||||
|
t.Cleanup(setup(t))
|
||||||
|
t.Run("Named parameterized prepared statement works", namedParameterizedPreparedStatement)
|
||||||
|
t.Run("Unnamed parameterized prepared statement works", unnamedParameterizedPreparedStatement)
|
||||||
|
}
|
||||||
|
|
||||||
|
func namedParameterizedPreparedStatement(t *testing.T) {
|
||||||
|
db, err := sql.Open("postgres", fmt.Sprintf("host=localhost port=%d database=sharded_db user=sharding_user password=sharding_user sslmode=disable", port))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not open connection: %+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stmt, err := db.Prepare("SELECT $1")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not prepare: %+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
rows, err := stmt.Query(1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not query: %+v", err)
|
||||||
|
}
|
||||||
|
_ = rows.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func unnamedParameterizedPreparedStatement(t *testing.T) {
|
||||||
|
db, err := sql.Open("postgres", fmt.Sprintf("host=localhost port=%d database=sharded_db user=sharding_user password=sharding_user sslmode=disable", port))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not open connection: %+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
// Under the hood QueryContext generates an unnamed parameterized prepared statement
|
||||||
|
rows, err := db.QueryContext(context.Background(), "SELECT $1", 1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not query: %+v", err)
|
||||||
|
}
|
||||||
|
_ = rows.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
81
tests/go/setup.go
Normal file
81
tests/go/setup.go
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
package pgcat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
_ "embed"
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed pgcat.toml
|
||||||
|
var pgcatCfg string
|
||||||
|
|
||||||
|
var port = rand.Intn(32760-20000) + 20000
|
||||||
|
|
||||||
|
func setup(t *testing.T) func() {
|
||||||
|
cfg, err := os.CreateTemp("/tmp", "pgcat_cfg_*.toml")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not create temp file: %+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pgcatCfg = strings.Replace(pgcatCfg, "\"${PORT}\"", fmt.Sprintf("%d", port), 1)
|
||||||
|
|
||||||
|
_, err = cfg.Write([]byte(pgcatCfg))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not write temp file: %+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
commandPath := "../../target/debug/pgcat"
|
||||||
|
if os.Getenv("CARGO_TARGET_DIR") != "" {
|
||||||
|
commandPath = os.Getenv("CARGO_TARGET_DIR") + "/debug/pgcat"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(commandPath, cfg.Name())
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
go func() {
|
||||||
|
err = cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("could not run pgcat: %+v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
deadline, cancelFunc := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second))
|
||||||
|
defer cancelFunc()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-deadline.Done():
|
||||||
|
break
|
||||||
|
case <-time.After(50 * time.Millisecond):
|
||||||
|
db, err := sql.Open("postgres", fmt.Sprintf("host=localhost port=%d database=pgcat user=admin_user password=admin_pass sslmode=disable", port))
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rows, err := db.QueryContext(deadline, "SHOW STATS")
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
_ = rows.Close()
|
||||||
|
_ = db.Close()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return func() {
|
||||||
|
err := cmd.Process.Signal(os.Interrupt)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not interrupt pgcat: %+v", err)
|
||||||
|
}
|
||||||
|
err = os.Remove(cfg.Name())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not remove temp file: %+v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,4 +36,4 @@ SELECT abalance FROM pgbench_accounts WHERE aid = :aid;
|
|||||||
SET SERVER ROLE TO 'replica';
|
SET SERVER ROLE TO 'replica';
|
||||||
|
|
||||||
-- Read load balancing
|
-- Read load balancing
|
||||||
SELECT abalance FROM pgbench_accounts WHERE aid = :aid;
|
SELECT abalance FROM pgbench_accounts WHERE aid = :aid;
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
|
pytest
|
||||||
psycopg2==2.9.3
|
psycopg2==2.9.3
|
||||||
psutil==5.9.1
|
psutil==5.9.1
|
||||||
|
|||||||
71
tests/python/test_auth.py
Normal file
71
tests/python/test_auth.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import utils
|
||||||
|
import signal
|
||||||
|
|
||||||
|
class TestTrustAuth:
|
||||||
|
@classmethod
|
||||||
|
def setup_method(cls):
|
||||||
|
config= """
|
||||||
|
[general]
|
||||||
|
host = "0.0.0.0"
|
||||||
|
port = 6432
|
||||||
|
admin_username = "admin_user"
|
||||||
|
admin_password = ""
|
||||||
|
admin_auth_type = "trust"
|
||||||
|
|
||||||
|
[pools.sharded_db.users.0]
|
||||||
|
username = "sharding_user"
|
||||||
|
password = "sharding_user"
|
||||||
|
auth_type = "trust"
|
||||||
|
pool_size = 10
|
||||||
|
min_pool_size = 1
|
||||||
|
pool_mode = "transaction"
|
||||||
|
|
||||||
|
[pools.sharded_db.shards.0]
|
||||||
|
servers = [
|
||||||
|
[ "127.0.0.1", 5432, "primary" ],
|
||||||
|
]
|
||||||
|
database = "shard0"
|
||||||
|
"""
|
||||||
|
utils.pgcat_generic_start(config)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def teardown_method(self):
|
||||||
|
utils.pg_cat_send_signal(signal.SIGTERM)
|
||||||
|
|
||||||
|
def test_admin_trust_auth(self):
|
||||||
|
conn, cur = utils.connect_db_trust(admin=True)
|
||||||
|
cur.execute("SHOW POOLS")
|
||||||
|
res = cur.fetchall()
|
||||||
|
print(res)
|
||||||
|
utils.cleanup_conn(conn, cur)
|
||||||
|
|
||||||
|
def test_normal_trust_auth(self):
|
||||||
|
conn, cur = utils.connect_db_trust(autocommit=False)
|
||||||
|
cur.execute("SELECT 1")
|
||||||
|
res = cur.fetchall()
|
||||||
|
print(res)
|
||||||
|
utils.cleanup_conn(conn, cur)
|
||||||
|
|
||||||
|
class TestMD5Auth:
|
||||||
|
@classmethod
|
||||||
|
def setup_method(cls):
|
||||||
|
utils.pgcat_start()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def teardown_method(self):
|
||||||
|
utils.pg_cat_send_signal(signal.SIGTERM)
|
||||||
|
|
||||||
|
def test_normal_db_access(self):
|
||||||
|
conn, cur = utils.connect_db(autocommit=False)
|
||||||
|
cur.execute("SELECT 1")
|
||||||
|
res = cur.fetchall()
|
||||||
|
print(res)
|
||||||
|
utils.cleanup_conn(conn, cur)
|
||||||
|
|
||||||
|
def test_admin_db_access(self):
|
||||||
|
conn, cur = utils.connect_db(admin=True)
|
||||||
|
|
||||||
|
cur.execute("SHOW POOLS")
|
||||||
|
res = cur.fetchall()
|
||||||
|
print(res)
|
||||||
|
utils.cleanup_conn(conn, cur)
|
||||||
@@ -1,84 +1,12 @@
|
|||||||
from typing import Tuple
|
|
||||||
import psycopg2
|
|
||||||
import psutil
|
|
||||||
import os
|
|
||||||
import signal
|
import signal
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
import psycopg2
|
||||||
|
import utils
|
||||||
|
|
||||||
SHUTDOWN_TIMEOUT = 5
|
SHUTDOWN_TIMEOUT = 5
|
||||||
|
|
||||||
PGCAT_HOST = "127.0.0.1"
|
|
||||||
PGCAT_PORT = "6432"
|
|
||||||
|
|
||||||
|
|
||||||
def pgcat_start():
|
|
||||||
pg_cat_send_signal(signal.SIGTERM)
|
|
||||||
os.system("./target/debug/pgcat .circleci/pgcat.toml &")
|
|
||||||
time.sleep(2)
|
|
||||||
|
|
||||||
|
|
||||||
def pg_cat_send_signal(signal: signal.Signals):
|
|
||||||
try:
|
|
||||||
for proc in psutil.process_iter(["pid", "name"]):
|
|
||||||
if "pgcat" == proc.name():
|
|
||||||
os.kill(proc.pid, signal)
|
|
||||||
except Exception as e:
|
|
||||||
# The process can be gone when we send this signal
|
|
||||||
print(e)
|
|
||||||
|
|
||||||
if signal == signal.SIGTERM:
|
|
||||||
# Returns 0 if pgcat process exists
|
|
||||||
time.sleep(2)
|
|
||||||
if not os.system('pgrep pgcat'):
|
|
||||||
raise Exception("pgcat not closed after SIGTERM")
|
|
||||||
|
|
||||||
|
|
||||||
def connect_db(
|
|
||||||
autocommit: bool = True,
|
|
||||||
admin: bool = False,
|
|
||||||
) -> Tuple[psycopg2.extensions.connection, psycopg2.extensions.cursor]:
|
|
||||||
|
|
||||||
if admin:
|
|
||||||
user = "admin_user"
|
|
||||||
password = "admin_pass"
|
|
||||||
db = "pgcat"
|
|
||||||
else:
|
|
||||||
user = "sharding_user"
|
|
||||||
password = "sharding_user"
|
|
||||||
db = "sharded_db"
|
|
||||||
|
|
||||||
conn = psycopg2.connect(
|
|
||||||
f"postgres://{user}:{password}@{PGCAT_HOST}:{PGCAT_PORT}/{db}?application_name=testing_pgcat",
|
|
||||||
connect_timeout=2,
|
|
||||||
)
|
|
||||||
conn.autocommit = autocommit
|
|
||||||
cur = conn.cursor()
|
|
||||||
|
|
||||||
return (conn, cur)
|
|
||||||
|
|
||||||
|
|
||||||
def cleanup_conn(conn: psycopg2.extensions.connection, cur: psycopg2.extensions.cursor):
|
|
||||||
cur.close()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
|
|
||||||
def test_normal_db_access():
|
|
||||||
pgcat_start()
|
|
||||||
conn, cur = connect_db(autocommit=False)
|
|
||||||
cur.execute("SELECT 1")
|
|
||||||
res = cur.fetchall()
|
|
||||||
print(res)
|
|
||||||
cleanup_conn(conn, cur)
|
|
||||||
|
|
||||||
|
|
||||||
def test_admin_db_access():
|
|
||||||
conn, cur = connect_db(admin=True)
|
|
||||||
|
|
||||||
cur.execute("SHOW POOLS")
|
|
||||||
res = cur.fetchall()
|
|
||||||
print(res)
|
|
||||||
cleanup_conn(conn, cur)
|
|
||||||
|
|
||||||
|
|
||||||
def test_shutdown_logic():
|
def test_shutdown_logic():
|
||||||
|
|
||||||
@@ -86,17 +14,17 @@ def test_shutdown_logic():
|
|||||||
# NO ACTIVE QUERIES SIGINT HANDLING
|
# NO ACTIVE QUERIES SIGINT HANDLING
|
||||||
|
|
||||||
# Start pgcat
|
# Start pgcat
|
||||||
pgcat_start()
|
utils.pgcat_start()
|
||||||
|
|
||||||
# Create client connection and send query (not in transaction)
|
# Create client connection and send query (not in transaction)
|
||||||
conn, cur = connect_db()
|
conn, cur = utils.connect_db()
|
||||||
|
|
||||||
cur.execute("BEGIN;")
|
cur.execute("BEGIN;")
|
||||||
cur.execute("SELECT 1;")
|
cur.execute("SELECT 1;")
|
||||||
cur.execute("COMMIT;")
|
cur.execute("COMMIT;")
|
||||||
|
|
||||||
# Send sigint to pgcat
|
# Send sigint to pgcat
|
||||||
pg_cat_send_signal(signal.SIGINT)
|
utils.pg_cat_send_signal(signal.SIGINT)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
# Check that any new queries fail after sigint since server should close with no active transactions
|
# Check that any new queries fail after sigint since server should close with no active transactions
|
||||||
@@ -108,18 +36,18 @@ def test_shutdown_logic():
|
|||||||
# Fail if query execution succeeded
|
# Fail if query execution succeeded
|
||||||
raise Exception("Server not closed after sigint")
|
raise Exception("Server not closed after sigint")
|
||||||
|
|
||||||
cleanup_conn(conn, cur)
|
utils.cleanup_conn(conn, cur)
|
||||||
pg_cat_send_signal(signal.SIGTERM)
|
utils.pg_cat_send_signal(signal.SIGTERM)
|
||||||
|
|
||||||
# - - - - - - - - - - - - - - - - - -
|
# - - - - - - - - - - - - - - - - - -
|
||||||
# NO ACTIVE QUERIES ADMIN SHUTDOWN COMMAND
|
# NO ACTIVE QUERIES ADMIN SHUTDOWN COMMAND
|
||||||
|
|
||||||
# Start pgcat
|
# Start pgcat
|
||||||
pgcat_start()
|
utils.pgcat_start()
|
||||||
|
|
||||||
# Create client connection and begin transaction
|
# Create client connection and begin transaction
|
||||||
conn, cur = connect_db()
|
conn, cur = utils.connect_db()
|
||||||
admin_conn, admin_cur = connect_db(admin=True)
|
admin_conn, admin_cur = utils.connect_db(admin=True)
|
||||||
|
|
||||||
cur.execute("BEGIN;")
|
cur.execute("BEGIN;")
|
||||||
cur.execute("SELECT 1;")
|
cur.execute("SELECT 1;")
|
||||||
@@ -138,24 +66,24 @@ def test_shutdown_logic():
|
|||||||
# Fail if query execution succeeded
|
# Fail if query execution succeeded
|
||||||
raise Exception("Server not closed after sigint")
|
raise Exception("Server not closed after sigint")
|
||||||
|
|
||||||
cleanup_conn(conn, cur)
|
utils.cleanup_conn(conn, cur)
|
||||||
cleanup_conn(admin_conn, admin_cur)
|
utils.cleanup_conn(admin_conn, admin_cur)
|
||||||
pg_cat_send_signal(signal.SIGTERM)
|
utils.pg_cat_send_signal(signal.SIGTERM)
|
||||||
|
|
||||||
# - - - - - - - - - - - - - - - - - -
|
# - - - - - - - - - - - - - - - - - -
|
||||||
# HANDLE TRANSACTION WITH SIGINT
|
# HANDLE TRANSACTION WITH SIGINT
|
||||||
|
|
||||||
# Start pgcat
|
# Start pgcat
|
||||||
pgcat_start()
|
utils.pgcat_start()
|
||||||
|
|
||||||
# Create client connection and begin transaction
|
# Create client connection and begin transaction
|
||||||
conn, cur = connect_db()
|
conn, cur = utils.connect_db()
|
||||||
|
|
||||||
cur.execute("BEGIN;")
|
cur.execute("BEGIN;")
|
||||||
cur.execute("SELECT 1;")
|
cur.execute("SELECT 1;")
|
||||||
|
|
||||||
# Send sigint to pgcat while still in transaction
|
# Send sigint to pgcat while still in transaction
|
||||||
pg_cat_send_signal(signal.SIGINT)
|
utils.pg_cat_send_signal(signal.SIGINT)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
# Check that any new queries succeed after sigint since server should still allow transaction to complete
|
# Check that any new queries succeed after sigint since server should still allow transaction to complete
|
||||||
@@ -165,18 +93,18 @@ def test_shutdown_logic():
|
|||||||
# Fail if query fails since server closed
|
# Fail if query fails since server closed
|
||||||
raise Exception("Server closed while in transaction", e.pgerror)
|
raise Exception("Server closed while in transaction", e.pgerror)
|
||||||
|
|
||||||
cleanup_conn(conn, cur)
|
utils.cleanup_conn(conn, cur)
|
||||||
pg_cat_send_signal(signal.SIGTERM)
|
utils.pg_cat_send_signal(signal.SIGTERM)
|
||||||
|
|
||||||
# - - - - - - - - - - - - - - - - - -
|
# - - - - - - - - - - - - - - - - - -
|
||||||
# HANDLE TRANSACTION WITH ADMIN SHUTDOWN COMMAND
|
# HANDLE TRANSACTION WITH ADMIN SHUTDOWN COMMAND
|
||||||
|
|
||||||
# Start pgcat
|
# Start pgcat
|
||||||
pgcat_start()
|
utils.pgcat_start()
|
||||||
|
|
||||||
# Create client connection and begin transaction
|
# Create client connection and begin transaction
|
||||||
conn, cur = connect_db()
|
conn, cur = utils.connect_db()
|
||||||
admin_conn, admin_cur = connect_db(admin=True)
|
admin_conn, admin_cur = utils.connect_db(admin=True)
|
||||||
|
|
||||||
cur.execute("BEGIN;")
|
cur.execute("BEGIN;")
|
||||||
cur.execute("SELECT 1;")
|
cur.execute("SELECT 1;")
|
||||||
@@ -194,30 +122,30 @@ def test_shutdown_logic():
|
|||||||
# Fail if query fails since server closed
|
# Fail if query fails since server closed
|
||||||
raise Exception("Server closed while in transaction", e.pgerror)
|
raise Exception("Server closed while in transaction", e.pgerror)
|
||||||
|
|
||||||
cleanup_conn(conn, cur)
|
utils.cleanup_conn(conn, cur)
|
||||||
cleanup_conn(admin_conn, admin_cur)
|
utils.cleanup_conn(admin_conn, admin_cur)
|
||||||
pg_cat_send_signal(signal.SIGTERM)
|
utils.pg_cat_send_signal(signal.SIGTERM)
|
||||||
|
|
||||||
# - - - - - - - - - - - - - - - - - -
|
# - - - - - - - - - - - - - - - - - -
|
||||||
# NO NEW NON-ADMIN CONNECTIONS DURING SHUTDOWN
|
# NO NEW NON-ADMIN CONNECTIONS DURING SHUTDOWN
|
||||||
# Start pgcat
|
# Start pgcat
|
||||||
pgcat_start()
|
utils.pgcat_start()
|
||||||
|
|
||||||
# Create client connection and begin transaction
|
# Create client connection and begin transaction
|
||||||
transaction_conn, transaction_cur = connect_db()
|
transaction_conn, transaction_cur = utils.connect_db()
|
||||||
|
|
||||||
transaction_cur.execute("BEGIN;")
|
transaction_cur.execute("BEGIN;")
|
||||||
transaction_cur.execute("SELECT 1;")
|
transaction_cur.execute("SELECT 1;")
|
||||||
|
|
||||||
# Send sigint to pgcat while still in transaction
|
# Send sigint to pgcat while still in transaction
|
||||||
pg_cat_send_signal(signal.SIGINT)
|
utils.pg_cat_send_signal(signal.SIGINT)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
start = time.perf_counter()
|
start = time.perf_counter()
|
||||||
try:
|
try:
|
||||||
conn, cur = connect_db()
|
conn, cur = utils.connect_db()
|
||||||
cur.execute("SELECT 1;")
|
cur.execute("SELECT 1;")
|
||||||
cleanup_conn(conn, cur)
|
utils.cleanup_conn(conn, cur)
|
||||||
except psycopg2.OperationalError as e:
|
except psycopg2.OperationalError as e:
|
||||||
time_taken = time.perf_counter() - start
|
time_taken = time.perf_counter() - start
|
||||||
if time_taken > 0.1:
|
if time_taken > 0.1:
|
||||||
@@ -227,49 +155,49 @@ def test_shutdown_logic():
|
|||||||
else:
|
else:
|
||||||
raise Exception("Able connect to database during shutdown")
|
raise Exception("Able connect to database during shutdown")
|
||||||
|
|
||||||
cleanup_conn(transaction_conn, transaction_cur)
|
utils.cleanup_conn(transaction_conn, transaction_cur)
|
||||||
pg_cat_send_signal(signal.SIGTERM)
|
utils.pg_cat_send_signal(signal.SIGTERM)
|
||||||
|
|
||||||
# - - - - - - - - - - - - - - - - - -
|
# - - - - - - - - - - - - - - - - - -
|
||||||
# ALLOW NEW ADMIN CONNECTIONS DURING SHUTDOWN
|
# ALLOW NEW ADMIN CONNECTIONS DURING SHUTDOWN
|
||||||
# Start pgcat
|
# Start pgcat
|
||||||
pgcat_start()
|
utils.pgcat_start()
|
||||||
|
|
||||||
# Create client connection and begin transaction
|
# Create client connection and begin transaction
|
||||||
transaction_conn, transaction_cur = connect_db()
|
transaction_conn, transaction_cur = utils.connect_db()
|
||||||
|
|
||||||
transaction_cur.execute("BEGIN;")
|
transaction_cur.execute("BEGIN;")
|
||||||
transaction_cur.execute("SELECT 1;")
|
transaction_cur.execute("SELECT 1;")
|
||||||
|
|
||||||
# Send sigint to pgcat while still in transaction
|
# Send sigint to pgcat while still in transaction
|
||||||
pg_cat_send_signal(signal.SIGINT)
|
utils.pg_cat_send_signal(signal.SIGINT)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
conn, cur = connect_db(admin=True)
|
conn, cur = utils.connect_db(admin=True)
|
||||||
cur.execute("SHOW DATABASES;")
|
cur.execute("SHOW DATABASES;")
|
||||||
cleanup_conn(conn, cur)
|
utils.cleanup_conn(conn, cur)
|
||||||
except psycopg2.OperationalError as e:
|
except psycopg2.OperationalError as e:
|
||||||
raise Exception(e)
|
raise Exception(e)
|
||||||
|
|
||||||
cleanup_conn(transaction_conn, transaction_cur)
|
utils.cleanup_conn(transaction_conn, transaction_cur)
|
||||||
pg_cat_send_signal(signal.SIGTERM)
|
utils.pg_cat_send_signal(signal.SIGTERM)
|
||||||
|
|
||||||
# - - - - - - - - - - - - - - - - - -
|
# - - - - - - - - - - - - - - - - - -
|
||||||
# ADMIN CONNECTIONS CONTINUING TO WORK AFTER SHUTDOWN
|
# ADMIN CONNECTIONS CONTINUING TO WORK AFTER SHUTDOWN
|
||||||
# Start pgcat
|
# Start pgcat
|
||||||
pgcat_start()
|
utils.pgcat_start()
|
||||||
|
|
||||||
# Create client connection and begin transaction
|
# Create client connection and begin transaction
|
||||||
transaction_conn, transaction_cur = connect_db()
|
transaction_conn, transaction_cur = utils.connect_db()
|
||||||
transaction_cur.execute("BEGIN;")
|
transaction_cur.execute("BEGIN;")
|
||||||
transaction_cur.execute("SELECT 1;")
|
transaction_cur.execute("SELECT 1;")
|
||||||
|
|
||||||
admin_conn, admin_cur = connect_db(admin=True)
|
admin_conn, admin_cur = utils.connect_db(admin=True)
|
||||||
admin_cur.execute("SHOW DATABASES;")
|
admin_cur.execute("SHOW DATABASES;")
|
||||||
|
|
||||||
# Send sigint to pgcat while still in transaction
|
# Send sigint to pgcat while still in transaction
|
||||||
pg_cat_send_signal(signal.SIGINT)
|
utils.pg_cat_send_signal(signal.SIGINT)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -277,24 +205,24 @@ def test_shutdown_logic():
|
|||||||
except psycopg2.OperationalError as e:
|
except psycopg2.OperationalError as e:
|
||||||
raise Exception("Could not execute admin command:", e)
|
raise Exception("Could not execute admin command:", e)
|
||||||
|
|
||||||
cleanup_conn(transaction_conn, transaction_cur)
|
utils.cleanup_conn(transaction_conn, transaction_cur)
|
||||||
cleanup_conn(admin_conn, admin_cur)
|
utils.cleanup_conn(admin_conn, admin_cur)
|
||||||
pg_cat_send_signal(signal.SIGTERM)
|
utils.pg_cat_send_signal(signal.SIGTERM)
|
||||||
|
|
||||||
# - - - - - - - - - - - - - - - - - -
|
# - - - - - - - - - - - - - - - - - -
|
||||||
# HANDLE SHUTDOWN TIMEOUT WITH SIGINT
|
# HANDLE SHUTDOWN TIMEOUT WITH SIGINT
|
||||||
|
|
||||||
# Start pgcat
|
# Start pgcat
|
||||||
pgcat_start()
|
utils.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 = utils.connect_db()
|
||||||
|
|
||||||
cur.execute("BEGIN;")
|
cur.execute("BEGIN;")
|
||||||
cur.execute("SELECT 1;")
|
cur.execute("SELECT 1;")
|
||||||
|
|
||||||
# Send sigint to pgcat while still in transaction
|
# Send sigint to pgcat while still in transaction
|
||||||
pg_cat_send_signal(signal.SIGINT)
|
utils.pg_cat_send_signal(signal.SIGINT)
|
||||||
|
|
||||||
# pgcat shutdown timeout is set to SHUTDOWN_TIMEOUT seconds, so we sleep for SHUTDOWN_TIMEOUT + 1 seconds
|
# pgcat shutdown timeout is set to SHUTDOWN_TIMEOUT seconds, so we sleep for SHUTDOWN_TIMEOUT + 1 seconds
|
||||||
time.sleep(SHUTDOWN_TIMEOUT + 1)
|
time.sleep(SHUTDOWN_TIMEOUT + 1)
|
||||||
@@ -308,12 +236,7 @@ def test_shutdown_logic():
|
|||||||
# Fail if query execution succeeded
|
# Fail if query execution succeeded
|
||||||
raise Exception("Server not closed after sigint and expected timeout")
|
raise Exception("Server not closed after sigint and expected timeout")
|
||||||
|
|
||||||
cleanup_conn(conn, cur)
|
utils.cleanup_conn(conn, cur)
|
||||||
pg_cat_send_signal(signal.SIGTERM)
|
utils.pg_cat_send_signal(signal.SIGTERM)
|
||||||
|
|
||||||
# - - - - - - - - - - - - - - - - - -
|
# - - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
|
|
||||||
test_normal_db_access()
|
|
||||||
test_admin_db_access()
|
|
||||||
test_shutdown_logic()
|
|
||||||
110
tests/python/utils.py
Normal file
110
tests/python/utils.py
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import time
|
||||||
|
from typing import Tuple
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
import psutil
|
||||||
|
import psycopg2
|
||||||
|
|
||||||
|
PGCAT_HOST = "127.0.0.1"
|
||||||
|
PGCAT_PORT = "6432"
|
||||||
|
|
||||||
|
|
||||||
|
def _pgcat_start(config_path: str):
|
||||||
|
pg_cat_send_signal(signal.SIGTERM)
|
||||||
|
os.system(f"./target/debug/pgcat {config_path} &")
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
|
||||||
|
def pgcat_start():
|
||||||
|
_pgcat_start(config_path='.circleci/pgcat.toml')
|
||||||
|
|
||||||
|
|
||||||
|
def pgcat_generic_start(config: str):
|
||||||
|
tmp = tempfile.NamedTemporaryFile()
|
||||||
|
with open(tmp.name, 'w') as f:
|
||||||
|
f.write(config)
|
||||||
|
_pgcat_start(config_path=tmp.name)
|
||||||
|
|
||||||
|
|
||||||
|
def glauth_send_signal(signal: signal.Signals):
|
||||||
|
try:
|
||||||
|
for proc in psutil.process_iter(["pid", "name"]):
|
||||||
|
if proc.name() == "glauth":
|
||||||
|
os.kill(proc.pid, signal)
|
||||||
|
except Exception as e:
|
||||||
|
# The process can be gone when we send this signal
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
if signal == signal.SIGTERM:
|
||||||
|
# Returns 0 if pgcat process exists
|
||||||
|
time.sleep(2)
|
||||||
|
if not os.system('pgrep glauth'):
|
||||||
|
raise Exception("glauth not closed after SIGTERM")
|
||||||
|
|
||||||
|
|
||||||
|
def pg_cat_send_signal(signal: signal.Signals):
|
||||||
|
try:
|
||||||
|
for proc in psutil.process_iter(["pid", "name"]):
|
||||||
|
if "pgcat" == proc.name():
|
||||||
|
os.kill(proc.pid, signal)
|
||||||
|
except Exception as e:
|
||||||
|
# The process can be gone when we send this signal
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
if signal == signal.SIGTERM:
|
||||||
|
# Returns 0 if pgcat process exists
|
||||||
|
time.sleep(2)
|
||||||
|
if not os.system('pgrep pgcat'):
|
||||||
|
raise Exception("pgcat not closed after SIGTERM")
|
||||||
|
|
||||||
|
|
||||||
|
def connect_db(
|
||||||
|
autocommit: bool = True,
|
||||||
|
admin: bool = False,
|
||||||
|
) -> Tuple[psycopg2.extensions.connection, psycopg2.extensions.cursor]:
|
||||||
|
|
||||||
|
if admin:
|
||||||
|
user = "admin_user"
|
||||||
|
password = "admin_pass"
|
||||||
|
db = "pgcat"
|
||||||
|
else:
|
||||||
|
user = "sharding_user"
|
||||||
|
password = "sharding_user"
|
||||||
|
db = "sharded_db"
|
||||||
|
|
||||||
|
conn = psycopg2.connect(
|
||||||
|
f"postgres://{user}:{password}@{PGCAT_HOST}:{PGCAT_PORT}/{db}?application_name=testing_pgcat",
|
||||||
|
connect_timeout=2,
|
||||||
|
)
|
||||||
|
conn.autocommit = autocommit
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
return (conn, cur)
|
||||||
|
|
||||||
|
def connect_db_trust(
|
||||||
|
autocommit: bool = True,
|
||||||
|
admin: bool = False,
|
||||||
|
) -> Tuple[psycopg2.extensions.connection, psycopg2.extensions.cursor]:
|
||||||
|
|
||||||
|
if admin:
|
||||||
|
user = "admin_user"
|
||||||
|
db = "pgcat"
|
||||||
|
else:
|
||||||
|
user = "sharding_user"
|
||||||
|
db = "sharded_db"
|
||||||
|
|
||||||
|
conn = psycopg2.connect(
|
||||||
|
f"postgres://{user}@{PGCAT_HOST}:{PGCAT_PORT}/{db}?application_name=testing_pgcat",
|
||||||
|
connect_timeout=2,
|
||||||
|
)
|
||||||
|
conn.autocommit = autocommit
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
return (conn, cur)
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup_conn(conn: psycopg2.extensions.connection, cur: psycopg2.extensions.cursor):
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
@@ -24,7 +24,8 @@ GEM
|
|||||||
pg (1.3.2)
|
pg (1.3.2)
|
||||||
rainbow (3.1.1)
|
rainbow (3.1.1)
|
||||||
regexp_parser (2.3.1)
|
regexp_parser (2.3.1)
|
||||||
rexml (3.2.5)
|
rexml (3.3.6)
|
||||||
|
strscan
|
||||||
rspec (3.11.0)
|
rspec (3.11.0)
|
||||||
rspec-core (~> 3.11.0)
|
rspec-core (~> 3.11.0)
|
||||||
rspec-expectations (~> 3.11.0)
|
rspec-expectations (~> 3.11.0)
|
||||||
@@ -50,6 +51,7 @@ GEM
|
|||||||
rubocop-ast (1.17.0)
|
rubocop-ast (1.17.0)
|
||||||
parser (>= 3.1.1.0)
|
parser (>= 3.1.1.0)
|
||||||
ruby-progressbar (1.11.0)
|
ruby-progressbar (1.11.0)
|
||||||
|
strscan (3.1.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)
|
toxiproxy (2.0.1)
|
||||||
|
|||||||
@@ -90,4 +90,49 @@ describe "Admin" do
|
|||||||
expect(results["pool_mode"]).to eq("transaction")
|
expect(results["pool_mode"]).to eq("transaction")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
[
|
||||||
|
"SHOW ME THE MONEY",
|
||||||
|
"SHOW ME THE WAY",
|
||||||
|
"SHOW UP",
|
||||||
|
"SHOWTIME",
|
||||||
|
"HAMMER TIME",
|
||||||
|
"SHOWN TO BE TRUE",
|
||||||
|
"SHOW ",
|
||||||
|
"SHOW ",
|
||||||
|
"SHOW 1",
|
||||||
|
";;;;;"
|
||||||
|
].each do |cmd|
|
||||||
|
describe "Bad command #{cmd}" do
|
||||||
|
it "does not panic and responds with PG::SystemError" do
|
||||||
|
admin_conn = PG::connect(processes.pgcat.admin_connection_string)
|
||||||
|
expect { admin_conn.async_exec(cmd) }.to raise_error(PG::SystemError).with_message(/Unsupported/)
|
||||||
|
admin_conn.close
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "PAUSE" do
|
||||||
|
it "pauses all pools" do
|
||||||
|
admin_conn = PG::connect(processes.pgcat.admin_connection_string)
|
||||||
|
results = admin_conn.async_exec("SHOW DATABASES").to_a
|
||||||
|
expect(results.map{ |r| r["paused"] }.uniq).to eq(["0"])
|
||||||
|
|
||||||
|
admin_conn.async_exec("PAUSE")
|
||||||
|
|
||||||
|
results = admin_conn.async_exec("SHOW DATABASES").to_a
|
||||||
|
expect(results.map{ |r| r["paused"] }.uniq).to eq(["1"])
|
||||||
|
|
||||||
|
admin_conn.async_exec("RESUME")
|
||||||
|
|
||||||
|
results = admin_conn.async_exec("SHOW DATABASES").to_a
|
||||||
|
expect(results.map{ |r| r["paused"] }.uniq).to eq(["0"])
|
||||||
|
end
|
||||||
|
|
||||||
|
it "handles errors" do
|
||||||
|
admin_conn = PG::connect(processes.pgcat.admin_connection_string)
|
||||||
|
expect { admin_conn.async_exec("PAUSE foo").to_a }.to raise_error(PG::SystemError)
|
||||||
|
expect { admin_conn.async_exec("PAUSE foo,bar").to_a }.to raise_error(PG::SystemError)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ describe "Auth Query" do
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
context 'and with cleartext passwords set' do
|
context 'and with cleartext passwords set' do
|
||||||
it 'it uses local passwords' do
|
it 'it uses local passwords' do
|
||||||
|
|||||||
@@ -33,18 +33,18 @@ module Helpers
|
|||||||
"0" => {
|
"0" => {
|
||||||
"database" => "shard0",
|
"database" => "shard0",
|
||||||
"servers" => [
|
"servers" => [
|
||||||
["localhost", primary.port.to_s, "primary"],
|
["localhost", primary.port.to_i, "primary"],
|
||||||
["localhost", replica.port.to_s, "replica"],
|
["localhost", replica.port.to_i, "replica"],
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"users" => { "0" => user.merge(config_user) }
|
"users" => { "0" => user.merge(config_user) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pgcat_cfg["general"]["port"] = pgcat.port
|
pgcat_cfg["general"]["port"] = pgcat.port.to_i
|
||||||
pgcat.update_config(pgcat_cfg)
|
pgcat.update_config(pgcat_cfg)
|
||||||
pgcat.start
|
pgcat.start
|
||||||
|
|
||||||
pgcat.wait_until_ready(
|
pgcat.wait_until_ready(
|
||||||
pgcat.connection_string(
|
pgcat.connection_string(
|
||||||
"sharded_db",
|
"sharded_db",
|
||||||
@@ -92,13 +92,13 @@ module Helpers
|
|||||||
"0" => {
|
"0" => {
|
||||||
"database" => database,
|
"database" => database,
|
||||||
"servers" => [
|
"servers" => [
|
||||||
["localhost", primary.port.to_s, "primary"],
|
["localhost", primary.port.to_i, "primary"],
|
||||||
["localhost", replica.port.to_s, "replica"],
|
["localhost", replica.port.to_i, "replica"],
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"users" => { "0" => user.merge(config_user) }
|
"users" => { "0" => user.merge(config_user) }
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
# Main proxy configs
|
# Main proxy configs
|
||||||
pgcat_cfg["pools"] = {
|
pgcat_cfg["pools"] = {
|
||||||
@@ -109,7 +109,7 @@ module Helpers
|
|||||||
pgcat_cfg["general"]["port"] = pgcat.port
|
pgcat_cfg["general"]["port"] = pgcat.port
|
||||||
pgcat.update_config(pgcat_cfg.deep_merge(extra_conf))
|
pgcat.update_config(pgcat_cfg.deep_merge(extra_conf))
|
||||||
pgcat.start
|
pgcat.start
|
||||||
|
|
||||||
pgcat.wait_until_ready(pgcat.connection_string("sharded_db0", pg_user['username'], pg_user['password']))
|
pgcat.wait_until_ready(pgcat.connection_string("sharded_db0", pg_user['username'], pg_user['password']))
|
||||||
|
|
||||||
OpenStruct.new.tap do |struct|
|
OpenStruct.new.tap do |struct|
|
||||||
|
|||||||
@@ -7,10 +7,24 @@ class PgInstance
|
|||||||
attr_reader :password
|
attr_reader :password
|
||||||
attr_reader :database_name
|
attr_reader :database_name
|
||||||
|
|
||||||
|
def self.mass_takedown(databases)
|
||||||
|
raise StandardError "block missing" unless block_given?
|
||||||
|
|
||||||
|
databases.each do |database|
|
||||||
|
database.toxiproxy.toxic(:limit_data, bytes: 1).toxics.each(&:save)
|
||||||
|
end
|
||||||
|
sleep 0.1
|
||||||
|
yield
|
||||||
|
ensure
|
||||||
|
databases.each do |database|
|
||||||
|
database.toxiproxy.toxics.each(&:destroy)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def initialize(port, username, password, database_name)
|
def initialize(port, username, password, database_name)
|
||||||
@original_port = port
|
@original_port = port.to_i
|
||||||
@toxiproxy_port = 10000 + port.to_i
|
@toxiproxy_port = 10000 + port.to_i
|
||||||
@port = @toxiproxy_port
|
@port = @toxiproxy_port.to_i
|
||||||
|
|
||||||
@username = username
|
@username = username
|
||||||
@password = password
|
@password = password
|
||||||
@@ -48,9 +62,9 @@ class PgInstance
|
|||||||
|
|
||||||
def take_down
|
def take_down
|
||||||
if block_given?
|
if block_given?
|
||||||
Toxiproxy[@toxiproxy_name].toxic(:limit_data, bytes: 5).apply { yield }
|
Toxiproxy[@toxiproxy_name].toxic(:limit_data, bytes: 1).apply { yield }
|
||||||
else
|
else
|
||||||
Toxiproxy[@toxiproxy_name].toxic(:limit_data, bytes: 5).toxics.each(&:save)
|
Toxiproxy[@toxiproxy_name].toxic(:limit_data, bytes: 1).toxics.each(&:save)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -89,6 +103,6 @@ class PgInstance
|
|||||||
end
|
end
|
||||||
|
|
||||||
def count_select_1_plus_2
|
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 }
|
with_connection { |c| c.async_exec("SELECT SUM(calls) FROM pg_stat_statements WHERE query LIKE '%SELECT $1 + $2%'")[0]["sum"].to_i }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -34,12 +34,13 @@ module Helpers
|
|||||||
"load_balancing_mode" => lb_mode,
|
"load_balancing_mode" => lb_mode,
|
||||||
"primary_reads_enabled" => true,
|
"primary_reads_enabled" => true,
|
||||||
"query_parser_enabled" => true,
|
"query_parser_enabled" => true,
|
||||||
|
"query_parser_read_write_splitting" => true,
|
||||||
"automatic_sharding_key" => "data.id",
|
"automatic_sharding_key" => "data.id",
|
||||||
"sharding_function" => "pg_bigint_hash",
|
"sharding_function" => "pg_bigint_hash",
|
||||||
"shards" => {
|
"shards" => {
|
||||||
"0" => { "database" => "shard0", "servers" => [["localhost", primary0.port.to_s, "primary"]] },
|
"0" => { "database" => "shard0", "servers" => [["localhost", primary0.port.to_i, "primary"]] },
|
||||||
"1" => { "database" => "shard1", "servers" => [["localhost", primary1.port.to_s, "primary"]] },
|
"1" => { "database" => "shard1", "servers" => [["localhost", primary1.port.to_i, "primary"]] },
|
||||||
"2" => { "database" => "shard2", "servers" => [["localhost", primary2.port.to_s, "primary"]] },
|
"2" => { "database" => "shard2", "servers" => [["localhost", primary2.port.to_i, "primary"]] },
|
||||||
},
|
},
|
||||||
"users" => { "0" => user },
|
"users" => { "0" => user },
|
||||||
"plugins" => {
|
"plugins" => {
|
||||||
@@ -99,7 +100,7 @@ module Helpers
|
|||||||
"0" => {
|
"0" => {
|
||||||
"database" => "shard0",
|
"database" => "shard0",
|
||||||
"servers" => [
|
"servers" => [
|
||||||
["localhost", primary.port.to_s, "primary"]
|
["localhost", primary.port.to_i, "primary"]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -145,10 +146,10 @@ module Helpers
|
|||||||
"0" => {
|
"0" => {
|
||||||
"database" => "shard0",
|
"database" => "shard0",
|
||||||
"servers" => [
|
"servers" => [
|
||||||
["localhost", primary.port.to_s, "primary"],
|
["localhost", primary.port.to_i, "primary"],
|
||||||
["localhost", replica0.port.to_s, "replica"],
|
["localhost", replica0.port.to_i, "replica"],
|
||||||
["localhost", replica1.port.to_s, "replica"],
|
["localhost", replica1.port.to_i, "replica"],
|
||||||
["localhost", replica2.port.to_s, "replica"]
|
["localhost", replica2.port.to_i, "replica"]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
require 'pg'
|
require 'pg'
|
||||||
require 'toml'
|
require 'json'
|
||||||
|
require 'tempfile'
|
||||||
require 'fileutils'
|
require 'fileutils'
|
||||||
require 'securerandom'
|
require 'securerandom'
|
||||||
|
|
||||||
|
class ConfigReloadFailed < StandardError; end
|
||||||
class PgcatProcess
|
class PgcatProcess
|
||||||
attr_reader :port
|
attr_reader :port
|
||||||
attr_reader :pid
|
attr_reader :pid
|
||||||
@@ -18,7 +20,7 @@ class PgcatProcess
|
|||||||
end
|
end
|
||||||
|
|
||||||
def initialize(log_level)
|
def initialize(log_level)
|
||||||
@env = {"RUST_LOG" => log_level}
|
@env = {}
|
||||||
@port = rand(20000..32760)
|
@port = rand(20000..32760)
|
||||||
@log_level = log_level
|
@log_level = log_level
|
||||||
@log_filename = "/tmp/pgcat_log_#{SecureRandom.urlsafe_base64}.log"
|
@log_filename = "/tmp/pgcat_log_#{SecureRandom.urlsafe_base64}.log"
|
||||||
@@ -30,7 +32,7 @@ class PgcatProcess
|
|||||||
'../../target/debug/pgcat'
|
'../../target/debug/pgcat'
|
||||||
end
|
end
|
||||||
|
|
||||||
@command = "#{command_path} #{@config_filename}"
|
@command = "#{command_path} #{@config_filename} --log-level #{@log_level}"
|
||||||
|
|
||||||
FileUtils.cp("../../pgcat.toml", @config_filename)
|
FileUtils.cp("../../pgcat.toml", @config_filename)
|
||||||
cfg = current_config
|
cfg = current_config
|
||||||
@@ -46,22 +48,34 @@ class PgcatProcess
|
|||||||
|
|
||||||
def update_config(config_hash)
|
def update_config(config_hash)
|
||||||
@original_config = current_config
|
@original_config = current_config
|
||||||
output_to_write = TOML::Generator.new(config_hash).body
|
Tempfile.create('json_out', '/tmp') do |f|
|
||||||
output_to_write = output_to_write.gsub(/,\s*["|'](\d+)["|']\s*,/, ',\1,')
|
f.write(config_hash.to_json)
|
||||||
output_to_write = output_to_write.gsub(/,\s*["|'](\d+)["|']\s*\]/, ',\1]')
|
f.flush
|
||||||
File.write(@config_filename, output_to_write)
|
`cat #{f.path} | yj -jt > #{@config_filename}`
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def current_config
|
def current_config
|
||||||
loadable_string = File.read(@config_filename)
|
JSON.parse(`cat #{@config_filename} | yj -tj`)
|
||||||
loadable_string = loadable_string.gsub(/,\s*(\d+)\s*,/, ', "\1",')
|
end
|
||||||
loadable_string = loadable_string.gsub(/,\s*(\d+)\s*\]/, ', "\1"]')
|
|
||||||
TOML.load(loadable_string)
|
def raw_config_file
|
||||||
|
File.read(@config_filename)
|
||||||
end
|
end
|
||||||
|
|
||||||
def reload_config
|
def reload_config
|
||||||
`kill -s HUP #{@pid}`
|
conn = PG.connect(admin_connection_string)
|
||||||
sleep 0.5
|
|
||||||
|
conn.async_exec("RELOAD")
|
||||||
|
rescue PG::ConnectionBad => e
|
||||||
|
errors = logs.split("Reloading config").last
|
||||||
|
errors = errors.gsub(/\e\[([;\d]+)?m/, '') # Remove color codes
|
||||||
|
errors = errors.
|
||||||
|
split("\n").select{|line| line.include?("ERROR") }.
|
||||||
|
map { |line| line.split("pgcat::config: ").last }
|
||||||
|
raise ConfigReloadFailed, errors.join("\n")
|
||||||
|
ensure
|
||||||
|
conn&.close
|
||||||
end
|
end
|
||||||
|
|
||||||
def start
|
def start
|
||||||
@@ -112,10 +126,16 @@ class PgcatProcess
|
|||||||
"postgresql://#{username}:#{password}@0.0.0.0:#{@port}/pgcat"
|
"postgresql://#{username}:#{password}@0.0.0.0:#{@port}/pgcat"
|
||||||
end
|
end
|
||||||
|
|
||||||
def connection_string(pool_name, username, password = nil)
|
def connection_string(pool_name, username, password = nil, parameters: {})
|
||||||
cfg = current_config
|
cfg = current_config
|
||||||
user_idx, user_obj = cfg["pools"][pool_name]["users"].detect { |k, user| user["username"] == username }
|
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}"
|
connection_string = "postgresql://#{username}:#{password || user_obj["password"]}@0.0.0.0:#{@port}/#{pool_name}"
|
||||||
|
|
||||||
|
# Add the additional parameters to the connection string
|
||||||
|
parameter_string = parameters.map { |key, value| "#{key}=#{value}" }.join("&")
|
||||||
|
connection_string += "?#{parameter_string}" unless parameter_string.empty?
|
||||||
|
|
||||||
|
connection_string
|
||||||
end
|
end
|
||||||
|
|
||||||
def example_connection_string
|
def example_connection_string
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ describe "Query Mirroing" do
|
|||||||
before do
|
before do
|
||||||
new_configs = processes.pgcat.current_config
|
new_configs = processes.pgcat.current_config
|
||||||
new_configs["pools"]["sharded_db"]["shards"]["0"]["mirrors"] = [
|
new_configs["pools"]["sharded_db"]["shards"]["0"]["mirrors"] = [
|
||||||
[mirror_host, mirror_pg.port.to_s, "0"],
|
[mirror_host, mirror_pg.port.to_i, 0],
|
||||||
[mirror_host, mirror_pg.port.to_s, "0"],
|
[mirror_host, mirror_pg.port.to_i, 0],
|
||||||
[mirror_host, mirror_pg.port.to_s, "0"],
|
[mirror_host, mirror_pg.port.to_i, 0],
|
||||||
]
|
]
|
||||||
processes.pgcat.update_config(new_configs)
|
processes.pgcat.update_config(new_configs)
|
||||||
processes.pgcat.reload_config
|
processes.pgcat.reload_config
|
||||||
@@ -31,7 +31,8 @@ describe "Query Mirroing" do
|
|||||||
runs.times { conn.async_exec("SELECT 1 + 2") }
|
runs.times { conn.async_exec("SELECT 1 + 2") }
|
||||||
sleep 0.5
|
sleep 0.5
|
||||||
expect(processes.all_databases.first.count_select_1_plus_2).to eq(runs)
|
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)
|
# Allow some slack in mirroring successes
|
||||||
|
expect(mirror_pg.count_select_1_plus_2).to be > ((runs - 5) * 3)
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when main server connection is closed" do
|
context "when main server connection is closed" do
|
||||||
@@ -42,9 +43,9 @@ describe "Query Mirroing" do
|
|||||||
new_configs = processes.pgcat.current_config
|
new_configs = processes.pgcat.current_config
|
||||||
new_configs["pools"]["sharded_db"]["idle_timeout"] = 5000 + i
|
new_configs["pools"]["sharded_db"]["idle_timeout"] = 5000 + i
|
||||||
new_configs["pools"]["sharded_db"]["shards"]["0"]["mirrors"] = [
|
new_configs["pools"]["sharded_db"]["shards"]["0"]["mirrors"] = [
|
||||||
[mirror_host, mirror_pg.port.to_s, "0"],
|
[mirror_host, mirror_pg.port.to_i, 0],
|
||||||
[mirror_host, mirror_pg.port.to_s, "0"],
|
[mirror_host, mirror_pg.port.to_i, 0],
|
||||||
[mirror_host, mirror_pg.port.to_s, "0"],
|
[mirror_host, mirror_pg.port.to_i, 0],
|
||||||
]
|
]
|
||||||
processes.pgcat.update_config(new_configs)
|
processes.pgcat.update_config(new_configs)
|
||||||
processes.pgcat.reload_config
|
processes.pgcat.reload_config
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ describe "Miscellaneous" do
|
|||||||
conn.close
|
conn.close
|
||||||
end
|
end
|
||||||
|
|
||||||
it "Does not send DISCARD ALL unless necessary" do
|
it "Does not send RESET ALL unless necessary" do
|
||||||
10.times do
|
10.times do
|
||||||
conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
|
conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
|
||||||
conn.async_exec("SET SERVER ROLE to 'primary'")
|
conn.async_exec("SET SERVER ROLE to 'primary'")
|
||||||
@@ -229,7 +229,7 @@ describe "Miscellaneous" do
|
|||||||
conn.close
|
conn.close
|
||||||
end
|
end
|
||||||
|
|
||||||
expect(processes.primary.count_query("DISCARD ALL")).to eq(0)
|
expect(processes.primary.count_query("RESET ALL")).to eq(0)
|
||||||
|
|
||||||
10.times do
|
10.times do
|
||||||
conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
|
conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
|
||||||
@@ -239,7 +239,7 @@ describe "Miscellaneous" do
|
|||||||
conn.close
|
conn.close
|
||||||
end
|
end
|
||||||
|
|
||||||
expect(processes.primary.count_query("DISCARD ALL")).to eq(10)
|
expect(processes.primary.count_query("RESET ALL")).to eq(10)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "Resets server roles correctly" do
|
it "Resets server roles correctly" do
|
||||||
@@ -252,7 +252,7 @@ describe "Miscellaneous" do
|
|||||||
end
|
end
|
||||||
|
|
||||||
expect(processes.primary.count_query("RESET ROLE")).to eq(10)
|
expect(processes.primary.count_query("RESET ROLE")).to eq(10)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "transaction mode" do
|
context "transaction mode" do
|
||||||
@@ -273,7 +273,7 @@ describe "Miscellaneous" do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it "Does not send DISCARD ALL unless necessary" do
|
it "Does not send RESET ALL unless necessary" do
|
||||||
10.times do
|
10.times do
|
||||||
conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
|
conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
|
||||||
conn.async_exec("SET SERVER ROLE to 'primary'")
|
conn.async_exec("SET SERVER ROLE to 'primary'")
|
||||||
@@ -282,7 +282,7 @@ describe "Miscellaneous" do
|
|||||||
conn.close
|
conn.close
|
||||||
end
|
end
|
||||||
|
|
||||||
expect(processes.primary.count_query("DISCARD ALL")).to eq(0)
|
expect(processes.primary.count_query("RESET ALL")).to eq(0)
|
||||||
|
|
||||||
10.times do
|
10.times do
|
||||||
conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
|
conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
|
||||||
@@ -292,8 +292,32 @@ describe "Miscellaneous" do
|
|||||||
conn.close
|
conn.close
|
||||||
end
|
end
|
||||||
|
|
||||||
expect(processes.primary.count_query("DISCARD ALL")).to eq(10)
|
expect(processes.primary.count_query("RESET ALL")).to eq(10)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "Respects tracked parameters on startup" do
|
||||||
|
conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user", parameters: { "application_name" => "my_pgcat_test" }))
|
||||||
|
|
||||||
|
expect(conn.async_exec("SHOW application_name")[0]["application_name"]).to eq("my_pgcat_test")
|
||||||
|
conn.close
|
||||||
|
end
|
||||||
|
|
||||||
|
it "Respect tracked parameter on set statemet" do
|
||||||
|
conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
|
||||||
|
|
||||||
|
conn.async_exec("SET application_name to 'my_pgcat_test'")
|
||||||
|
expect(conn.async_exec("SHOW application_name")[0]["application_name"]).to eq("my_pgcat_test")
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
it "Ignore untracked parameter on set statemet" do
|
||||||
|
conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
|
||||||
|
orignal_statement_timeout = conn.async_exec("SHOW statement_timeout")[0]["statement_timeout"]
|
||||||
|
|
||||||
|
conn.async_exec("SET statement_timeout to 1500")
|
||||||
|
expect(conn.async_exec("SHOW statement_timeout")[0]["statement_timeout"]).to eq(orignal_statement_timeout)
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context "transaction mode with transactions" do
|
context "transaction mode with transactions" do
|
||||||
@@ -307,7 +331,7 @@ describe "Miscellaneous" do
|
|||||||
conn.async_exec("COMMIT")
|
conn.async_exec("COMMIT")
|
||||||
conn.close
|
conn.close
|
||||||
end
|
end
|
||||||
expect(processes.primary.count_query("DISCARD ALL")).to eq(0)
|
expect(processes.primary.count_query("RESET ALL")).to eq(0)
|
||||||
|
|
||||||
10.times do
|
10.times do
|
||||||
conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
|
conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
|
||||||
@@ -317,7 +341,7 @@ describe "Miscellaneous" do
|
|||||||
conn.async_exec("COMMIT")
|
conn.async_exec("COMMIT")
|
||||||
conn.close
|
conn.close
|
||||||
end
|
end
|
||||||
expect(processes.primary.count_query("DISCARD ALL")).to eq(0)
|
expect(processes.primary.count_query("RESET ALL")).to eq(0)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -330,8 +354,7 @@ describe "Miscellaneous" do
|
|||||||
conn.async_exec("SET statement_timeout TO 1000")
|
conn.async_exec("SET statement_timeout TO 1000")
|
||||||
conn.close
|
conn.close
|
||||||
|
|
||||||
puts processes.pgcat.logs
|
expect(processes.primary.count_query("RESET ALL")).to eq(0)
|
||||||
expect(processes.primary.count_query("DISCARD ALL")).to eq(0)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it "will not clean up prepared statements" do
|
it "will not clean up prepared statements" do
|
||||||
@@ -341,8 +364,7 @@ describe "Miscellaneous" do
|
|||||||
|
|
||||||
conn.close
|
conn.close
|
||||||
|
|
||||||
puts processes.pgcat.logs
|
expect(processes.primary.count_query("RESET ALL")).to eq(0)
|
||||||
expect(processes.primary.count_query("DISCARD ALL")).to eq(0)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -352,10 +374,9 @@ describe "Miscellaneous" do
|
|||||||
before do
|
before do
|
||||||
current_configs = processes.pgcat.current_config
|
current_configs = processes.pgcat.current_config
|
||||||
correct_idle_client_transaction_timeout = current_configs["general"]["idle_client_in_transaction_timeout"]
|
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
|
current_configs["general"]["idle_client_in_transaction_timeout"] = 0
|
||||||
|
|
||||||
processes.pgcat.update_config(current_configs) # with timeout 0
|
processes.pgcat.update_config(current_configs) # with timeout 0
|
||||||
processes.pgcat.reload_config
|
processes.pgcat.reload_config
|
||||||
end
|
end
|
||||||
@@ -373,9 +394,9 @@ describe "Miscellaneous" do
|
|||||||
context "idle transaction timeout set to 500ms" do
|
context "idle transaction timeout set to 500ms" do
|
||||||
before do
|
before do
|
||||||
current_configs = processes.pgcat.current_config
|
current_configs = processes.pgcat.current_config
|
||||||
correct_idle_client_transaction_timeout = current_configs["general"]["idle_client_in_transaction_timeout"]
|
correct_idle_client_transaction_timeout = current_configs["general"]["idle_client_in_transaction_timeout"]
|
||||||
current_configs["general"]["idle_client_in_transaction_timeout"] = 500
|
current_configs["general"]["idle_client_in_transaction_timeout"] = 500
|
||||||
|
|
||||||
processes.pgcat.update_config(current_configs) # with timeout 500
|
processes.pgcat.update_config(current_configs) # with timeout 500
|
||||||
processes.pgcat.reload_config
|
processes.pgcat.reload_config
|
||||||
end
|
end
|
||||||
@@ -394,7 +415,7 @@ describe "Miscellaneous" do
|
|||||||
conn.async_exec("BEGIN")
|
conn.async_exec("BEGIN")
|
||||||
conn.async_exec("SELECT 1")
|
conn.async_exec("SELECT 1")
|
||||||
sleep(1) # above 500ms
|
sleep(1) # above 500ms
|
||||||
expect{ conn.async_exec("COMMIT") }.to raise_error(PG::SystemError, /idle transaction timeout/)
|
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.async_exec("SELECT 1") # should be able to send another query
|
||||||
conn.close
|
conn.close
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,29 +1,214 @@
|
|||||||
require_relative 'spec_helper'
|
require_relative 'spec_helper'
|
||||||
|
|
||||||
describe 'Prepared statements' do
|
describe 'Prepared statements' do
|
||||||
let(:processes) { Helpers::Pgcat.three_shard_setup('sharded_db', 5) }
|
let(:pool_size) { 5 }
|
||||||
|
let(:processes) { Helpers::Pgcat.single_instance_setup("sharded_db", pool_size) }
|
||||||
|
let(:prepared_statements_cache_size) { 100 }
|
||||||
|
let(:server_round_robin) { false }
|
||||||
|
|
||||||
context 'enabled' do
|
before do
|
||||||
it 'will work over the same connection' do
|
new_configs = processes.pgcat.current_config
|
||||||
|
new_configs["general"]["server_round_robin"] = server_round_robin
|
||||||
|
new_configs["pools"]["sharded_db"]["prepared_statements_cache_size"] = prepared_statements_cache_size
|
||||||
|
new_configs["pools"]["sharded_db"]["users"]["0"]["pool_size"] = pool_size
|
||||||
|
processes.pgcat.update_config(new_configs)
|
||||||
|
processes.pgcat.reload_config
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when trying prepared statements' do
|
||||||
|
it 'it allows unparameterized statements to succeed' do
|
||||||
|
conn1 = PG.connect(processes.pgcat.connection_string('sharded_db', 'sharding_user'))
|
||||||
|
conn2 = PG.connect(processes.pgcat.connection_string('sharded_db', 'sharding_user'))
|
||||||
|
|
||||||
|
prepared_query = "SELECT 1"
|
||||||
|
|
||||||
|
# prepare query on server 1 and client 1
|
||||||
|
conn1.prepare('statement1', prepared_query)
|
||||||
|
conn1.exec_prepared('statement1')
|
||||||
|
|
||||||
|
conn2.transaction do
|
||||||
|
# Claim server 1 with client 2
|
||||||
|
conn2.exec("SELECT 2")
|
||||||
|
|
||||||
|
# Client 1 now runs the prepared query, and it's automatically
|
||||||
|
# prepared on server 2
|
||||||
|
conn1.prepare('statement2', prepared_query)
|
||||||
|
conn1.exec_prepared('statement2')
|
||||||
|
|
||||||
|
# Client 2 now prepares the same query that was already
|
||||||
|
# prepared on server 1. And PgBouncer reuses that already
|
||||||
|
# prepared query for this different client.
|
||||||
|
conn2.prepare('statement3', prepared_query)
|
||||||
|
conn2.exec_prepared('statement3')
|
||||||
|
end
|
||||||
|
ensure
|
||||||
|
conn1.close if conn1
|
||||||
|
conn2.close if conn2
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'it allows parameterized statements to succeed' do
|
||||||
|
conn1 = PG.connect(processes.pgcat.connection_string('sharded_db', 'sharding_user'))
|
||||||
|
conn2 = PG.connect(processes.pgcat.connection_string('sharded_db', 'sharding_user'))
|
||||||
|
|
||||||
|
prepared_query = "SELECT $1"
|
||||||
|
|
||||||
|
# prepare query on server 1 and client 1
|
||||||
|
conn1.prepare('statement1', prepared_query)
|
||||||
|
conn1.exec_prepared('statement1', [1])
|
||||||
|
|
||||||
|
conn2.transaction do
|
||||||
|
# Claim server 1 with client 2
|
||||||
|
conn2.exec("SELECT 2")
|
||||||
|
|
||||||
|
# Client 1 now runs the prepared query, and it's automatically
|
||||||
|
# prepared on server 2
|
||||||
|
conn1.prepare('statement2', prepared_query)
|
||||||
|
conn1.exec_prepared('statement2', [1])
|
||||||
|
|
||||||
|
# Client 2 now prepares the same query that was already
|
||||||
|
# prepared on server 1. And PgBouncer reuses that already
|
||||||
|
# prepared query for this different client.
|
||||||
|
conn2.prepare('statement3', prepared_query)
|
||||||
|
conn2.exec_prepared('statement3', [1])
|
||||||
|
end
|
||||||
|
ensure
|
||||||
|
conn1.close if conn1
|
||||||
|
conn2.close if conn2
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when trying large packets' do
|
||||||
|
it "works with large parse" do
|
||||||
|
conn1 = PG.connect(processes.pgcat.connection_string('sharded_db', 'sharding_user'))
|
||||||
|
|
||||||
|
long_string = "1" * 4096 * 10
|
||||||
|
prepared_query = "SELECT '#{long_string}'"
|
||||||
|
|
||||||
|
|
||||||
|
# prepare query on server 1 and client 1
|
||||||
|
conn1.prepare('statement1', prepared_query)
|
||||||
|
result = conn1.exec_prepared('statement1')
|
||||||
|
|
||||||
|
# assert result matches long_string
|
||||||
|
expect(result.getvalue(0, 0)).to eq(long_string)
|
||||||
|
ensure
|
||||||
|
conn1.close if conn1
|
||||||
|
end
|
||||||
|
|
||||||
|
it "works with large bind" do
|
||||||
|
conn1 = PG.connect(processes.pgcat.connection_string('sharded_db', 'sharding_user'))
|
||||||
|
|
||||||
|
long_string = "1" * 4096 * 10
|
||||||
|
prepared_query = "SELECT $1::text"
|
||||||
|
|
||||||
|
# prepare query on server 1 and client 1
|
||||||
|
conn1.prepare('statement1', prepared_query)
|
||||||
|
result = conn1.exec_prepared('statement1', [long_string])
|
||||||
|
|
||||||
|
# assert result matches long_string
|
||||||
|
expect(result.getvalue(0, 0)).to eq(long_string)
|
||||||
|
ensure
|
||||||
|
conn1.close if conn1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when statement cache is smaller than set of unqiue statements' do
|
||||||
|
let(:prepared_statements_cache_size) { 1 }
|
||||||
|
let(:pool_size) { 1 }
|
||||||
|
|
||||||
|
it "evicts all but 1 statement from the server cache" do
|
||||||
|
conn = PG.connect(processes.pgcat.connection_string('sharded_db', 'sharding_user'))
|
||||||
|
|
||||||
|
5.times do |i|
|
||||||
|
prepared_query = "SELECT '#{i}'"
|
||||||
|
conn.prepare("statement#{i}", prepared_query)
|
||||||
|
result = conn.exec_prepared("statement#{i}")
|
||||||
|
expect(result.getvalue(0, 0)).to eq(i.to_s)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Check number of prepared statements (expected: 1)
|
||||||
|
n_statements = conn.exec("SELECT count(*) FROM pg_prepared_statements").getvalue(0, 0).to_i
|
||||||
|
expect(n_statements).to eq(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when statement cache is larger than set of unqiue statements' do
|
||||||
|
let(:pool_size) { 1 }
|
||||||
|
|
||||||
|
it "does not evict any of the statements from the cache" do
|
||||||
|
# cache size 5
|
||||||
|
conn = PG.connect(processes.pgcat.connection_string('sharded_db', 'sharding_user'))
|
||||||
|
|
||||||
|
5.times do |i|
|
||||||
|
prepared_query = "SELECT '#{i}'"
|
||||||
|
conn.prepare("statement#{i}", prepared_query)
|
||||||
|
result = conn.exec_prepared("statement#{i}")
|
||||||
|
expect(result.getvalue(0, 0)).to eq(i.to_s)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Check number of prepared statements (expected: 1)
|
||||||
|
n_statements = conn.exec("SELECT count(*) FROM pg_prepared_statements").getvalue(0, 0).to_i
|
||||||
|
expect(n_statements).to eq(5)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when preparing the same query' do
|
||||||
|
let(:prepared_statements_cache_size) { 5 }
|
||||||
|
let(:pool_size) { 5 }
|
||||||
|
|
||||||
|
it "reuses statement cache when there are different statement names on the same connection" do
|
||||||
conn = PG.connect(processes.pgcat.connection_string('sharded_db', 'sharding_user'))
|
conn = PG.connect(processes.pgcat.connection_string('sharded_db', 'sharding_user'))
|
||||||
|
|
||||||
10.times do |i|
|
10.times do |i|
|
||||||
statement_name = "statement_#{i}"
|
statement_name = "statement_#{i}"
|
||||||
conn.prepare(statement_name, 'SELECT $1::int')
|
conn.prepare(statement_name, 'SELECT $1::int')
|
||||||
conn.exec_prepared(statement_name, [1])
|
conn.exec_prepared(statement_name, [1])
|
||||||
conn.describe_prepared(statement_name)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Check number of prepared statements (expected: 1)
|
||||||
|
n_statements = conn.exec("SELECT count(*) FROM pg_prepared_statements").getvalue(0, 0).to_i
|
||||||
|
expect(n_statements).to eq(1)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'will work with new connections' do
|
it "reuses statement cache when there are different statement names on different connections" do
|
||||||
10.times do
|
10.times do |i|
|
||||||
conn = PG.connect(processes.pgcat.connection_string('sharded_db', 'sharding_user'))
|
conn = PG.connect(processes.pgcat.connection_string('sharded_db', 'sharding_user'))
|
||||||
|
statement_name = "statement_#{i}"
|
||||||
statement_name = 'statement1'
|
conn.prepare(statement_name, 'SELECT $1::int')
|
||||||
conn.prepare('statement1', 'SELECT $1::int')
|
conn.exec_prepared(statement_name, [1])
|
||||||
conn.exec_prepared('statement1', [1])
|
|
||||||
conn.describe_prepared('statement1')
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Check number of prepared statements (expected: 1)
|
||||||
|
conn = PG.connect(processes.pgcat.connection_string('sharded_db', 'sharding_user'))
|
||||||
|
n_statements = conn.exec("SELECT count(*) FROM pg_prepared_statements").getvalue(0, 0).to_i
|
||||||
|
expect(n_statements).to eq(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when reloading config' do
|
||||||
|
let(:pool_size) { 1 }
|
||||||
|
|
||||||
|
it "test_reload_config" do
|
||||||
|
conn = PG.connect(processes.pgcat.connection_string('sharded_db', 'sharding_user'))
|
||||||
|
|
||||||
|
# prepare query
|
||||||
|
conn.prepare('statement1', 'SELECT 1')
|
||||||
|
conn.exec_prepared('statement1')
|
||||||
|
|
||||||
|
# Reload config which triggers pool recreation
|
||||||
|
new_configs = processes.pgcat.current_config
|
||||||
|
new_configs["pools"]["sharded_db"]["prepared_statements_cache_size"] = prepared_statements_cache_size + 1
|
||||||
|
processes.pgcat.update_config(new_configs)
|
||||||
|
processes.pgcat.reload_config
|
||||||
|
|
||||||
|
# check that we're starting with no prepared statements on the server
|
||||||
|
conn_check = PG.connect(processes.pgcat.connection_string('sharded_db', 'sharding_user'))
|
||||||
|
n_statements = conn_check.exec("SELECT count(*) FROM pg_prepared_statements").getvalue(0, 0).to_i
|
||||||
|
expect(n_statements).to eq(0)
|
||||||
|
|
||||||
|
# still able to run prepared query
|
||||||
|
conn.exec_prepared('statement1')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ describe "Sharding" do
|
|||||||
|
|
||||||
before do
|
before do
|
||||||
conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
|
conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
|
||||||
|
|
||||||
# Setup the sharding data
|
# Setup the sharding data
|
||||||
3.times do |i|
|
3.times do |i|
|
||||||
conn.exec("SET SHARD TO '#{i}'")
|
conn.exec("SET SHARD TO '#{i}'")
|
||||||
conn.exec("DELETE FROM data WHERE id > 0")
|
|
||||||
|
conn.exec("DELETE FROM data WHERE id > 0") rescue nil
|
||||||
end
|
end
|
||||||
|
|
||||||
18.times do |i|
|
18.times do |i|
|
||||||
@@ -19,10 +19,11 @@ describe "Sharding" do
|
|||||||
conn.exec("SET SHARDING KEY TO '#{i}'")
|
conn.exec("SET SHARDING KEY TO '#{i}'")
|
||||||
conn.exec("INSERT INTO data (id, value) VALUES (#{i}, 'value_#{i}')")
|
conn.exec("INSERT INTO data (id, value) VALUES (#{i}, 'value_#{i}')")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
conn.close
|
||||||
end
|
end
|
||||||
|
|
||||||
after do
|
after do
|
||||||
|
|
||||||
processes.all_databases.map(&:reset)
|
processes.all_databases.map(&:reset)
|
||||||
processes.pgcat.shutdown
|
processes.pgcat.shutdown
|
||||||
end
|
end
|
||||||
@@ -48,4 +49,148 @@ describe "Sharding" do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "no_shard_specified_behavior config" do
|
||||||
|
context "when default shard number is invalid" do
|
||||||
|
it "prevents config reload" do
|
||||||
|
admin_conn = PG::connect(processes.pgcat.admin_connection_string)
|
||||||
|
|
||||||
|
current_configs = processes.pgcat.current_config
|
||||||
|
current_configs["pools"]["sharded_db"]["default_shard"] = "shard_99"
|
||||||
|
|
||||||
|
processes.pgcat.update_config(current_configs)
|
||||||
|
|
||||||
|
expect { processes.pgcat.reload_config }.to raise_error(ConfigReloadFailed, /Invalid shard 99/)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "comment-based routing" do
|
||||||
|
context "when no configs are set" do
|
||||||
|
it "routes queries with a shard_id comment to the default shard" do
|
||||||
|
conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
|
||||||
|
10.times { conn.async_exec("/* shard_id: 2 */ SELECT 1 + 2") }
|
||||||
|
|
||||||
|
expect(processes.all_databases.map(&:count_select_1_plus_2)).to eq([10, 0, 0])
|
||||||
|
end
|
||||||
|
|
||||||
|
it "does not honor no_shard_specified_behavior directives" do
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
[
|
||||||
|
["shard_id_regex", "/\\* the_shard_id: (\\d+) \\*/", "/* the_shard_id: 1 */"],
|
||||||
|
["sharding_key_regex", "/\\* the_sharding_key: (\\d+) \\*/", "/* the_sharding_key: 3 */"],
|
||||||
|
].each do |config_name, config_value, comment_to_use|
|
||||||
|
context "when #{config_name} config is set" do
|
||||||
|
let(:no_shard_specified_behavior) { nil }
|
||||||
|
|
||||||
|
before do
|
||||||
|
admin_conn = PG::connect(processes.pgcat.admin_connection_string)
|
||||||
|
|
||||||
|
current_configs = processes.pgcat.current_config
|
||||||
|
current_configs["pools"]["sharded_db"][config_name] = config_value
|
||||||
|
if no_shard_specified_behavior
|
||||||
|
current_configs["pools"]["sharded_db"]["default_shard"] = no_shard_specified_behavior
|
||||||
|
else
|
||||||
|
current_configs["pools"]["sharded_db"].delete("default_shard")
|
||||||
|
end
|
||||||
|
|
||||||
|
processes.pgcat.update_config(current_configs)
|
||||||
|
processes.pgcat.reload_config
|
||||||
|
end
|
||||||
|
|
||||||
|
it "routes queries with a shard_id comment to the correct shard" do
|
||||||
|
conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
|
||||||
|
25.times { conn.async_exec("#{comment_to_use} SELECT 1 + 2") }
|
||||||
|
|
||||||
|
expect(processes.all_databases.map(&:count_select_1_plus_2)).to eq([0, 25, 0])
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when no_shard_specified_behavior config is set to random" do
|
||||||
|
let(:no_shard_specified_behavior) { "random" }
|
||||||
|
|
||||||
|
context "with no shard comment" do
|
||||||
|
it "sends queries to random shard" do
|
||||||
|
conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
|
||||||
|
25.times { conn.async_exec("SELECT 1 + 2") }
|
||||||
|
|
||||||
|
expect(processes.all_databases.map(&:count_select_1_plus_2).all?(&:positive?)).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with a shard comment" do
|
||||||
|
it "honors the comment" do
|
||||||
|
conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
|
||||||
|
25.times { conn.async_exec("#{comment_to_use} SELECT 1 + 2") }
|
||||||
|
|
||||||
|
expect(processes.all_databases.map(&:count_select_1_plus_2)).to eq([0, 25, 0])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when no_shard_specified_behavior config is set to random_healthy" do
|
||||||
|
let(:no_shard_specified_behavior) { "random_healthy" }
|
||||||
|
|
||||||
|
context "with no shard comment" do
|
||||||
|
it "sends queries to random healthy shard" do
|
||||||
|
|
||||||
|
good_databases = [processes.all_databases[0], processes.all_databases[2]]
|
||||||
|
bad_database = processes.all_databases[1]
|
||||||
|
conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
|
||||||
|
250.times { conn.async_exec("SELECT 99") }
|
||||||
|
bad_database.take_down do
|
||||||
|
250.times do
|
||||||
|
conn.async_exec("SELECT 99")
|
||||||
|
rescue PG::ConnectionBad => e
|
||||||
|
conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Routes traffic away from bad shard
|
||||||
|
25.times { conn.async_exec("SELECT 1 + 2") }
|
||||||
|
expect(good_databases.map(&:count_select_1_plus_2).all?(&:positive?)).to be true
|
||||||
|
expect(bad_database.count_select_1_plus_2).to eq(0)
|
||||||
|
|
||||||
|
# Routes traffic to the bad shard if the shard_id is specified
|
||||||
|
25.times { conn.async_exec("#{comment_to_use} SELECT 1 + 2") }
|
||||||
|
bad_database = processes.all_databases[1]
|
||||||
|
expect(bad_database.count_select_1_plus_2).to eq(25)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with a shard comment" do
|
||||||
|
it "honors the comment" do
|
||||||
|
conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
|
||||||
|
25.times { conn.async_exec("#{comment_to_use} SELECT 1 + 2") }
|
||||||
|
|
||||||
|
expect(processes.all_databases.map(&:count_select_1_plus_2)).to eq([0, 25, 0])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when no_shard_specified_behavior config is set to shard_x" do
|
||||||
|
let(:no_shard_specified_behavior) { "shard_2" }
|
||||||
|
|
||||||
|
context "with no shard comment" do
|
||||||
|
it "sends queries to the specified shard" do
|
||||||
|
conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
|
||||||
|
25.times { conn.async_exec("SELECT 1 + 2") }
|
||||||
|
|
||||||
|
expect(processes.all_databases.map(&:count_select_1_plus_2)).to eq([0, 0, 25])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with a shard comment" do
|
||||||
|
it "honors the comment" do
|
||||||
|
conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
|
||||||
|
25.times { conn.async_exec("#{comment_to_use} SELECT 1 + 2") }
|
||||||
|
|
||||||
|
expect(processes.all_databases.map(&:count_select_1_plus_2)).to eq([0, 25, 0])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -233,17 +233,19 @@ describe "Stats" do
|
|||||||
sleep(1.1) # Allow time for stats to update
|
sleep(1.1) # Allow time for stats to update
|
||||||
admin_conn = PG::connect(processes.pgcat.admin_connection_string)
|
admin_conn = PG::connect(processes.pgcat.admin_connection_string)
|
||||||
results = admin_conn.async_exec("SHOW POOLS")[0]
|
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|
|
|
||||||
|
%w[cl_idle cl_cancel_req sv_idle sv_used sv_tested sv_login].each do |s|
|
||||||
raise StandardError, "Field #{s} was expected to be 0 but found to be #{results[s]}" if results[s] != "0"
|
raise StandardError, "Field #{s} was expected to be 0 but found to be #{results[s]}" if results[s] != "0"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
expect(results["maxwait"]).to eq("1")
|
||||||
expect(results["cl_waiting"]).to eq("2")
|
expect(results["cl_waiting"]).to eq("2")
|
||||||
expect(results["cl_active"]).to eq("2")
|
expect(results["cl_active"]).to eq("2")
|
||||||
expect(results["sv_active"]).to eq("2")
|
expect(results["sv_active"]).to eq("2")
|
||||||
|
|
||||||
sleep(2.5) # Allow time for stats to update
|
sleep(2.5) # Allow time for stats to update
|
||||||
results = admin_conn.async_exec("SHOW POOLS")[0]
|
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|
|
%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"
|
raise StandardError, "Field #{s} was expected to be 0 but found to be #{results[s]}" if results[s] != "0"
|
||||||
end
|
end
|
||||||
expect(results["cl_idle"]).to eq("4")
|
expect(results["cl_idle"]).to eq("4")
|
||||||
@@ -255,22 +257,23 @@ describe "Stats" do
|
|||||||
|
|
||||||
it "show correct max_wait" do
|
it "show correct max_wait" do
|
||||||
threads = []
|
threads = []
|
||||||
|
admin_conn = PG::connect(processes.pgcat.admin_connection_string)
|
||||||
connections = Array.new(4) { PG::connect("#{pgcat_conn_str}?application_name=one_query") }
|
connections = Array.new(4) { PG::connect("#{pgcat_conn_str}?application_name=one_query") }
|
||||||
connections.each do |c|
|
connections.each do |c|
|
||||||
threads << Thread.new { c.async_exec("SELECT pg_sleep(1.5)") rescue nil }
|
threads << Thread.new { c.async_exec("SELECT pg_sleep(1.5)") rescue nil }
|
||||||
end
|
end
|
||||||
|
sleep(1.1)
|
||||||
|
results = admin_conn.async_exec("SHOW POOLS")[0]
|
||||||
|
# Value is only reported when there are clients waiting
|
||||||
|
expect(results["maxwait"]).to eq("1")
|
||||||
|
expect(results["maxwait_us"].to_i).to be_within(20_000).of(100_000)
|
||||||
|
|
||||||
sleep(2.5) # Allow time for stats to update
|
sleep(2.5) # Allow time for stats to update
|
||||||
admin_conn = PG::connect(processes.pgcat.admin_connection_string)
|
|
||||||
results = admin_conn.async_exec("SHOW POOLS")[0]
|
|
||||||
|
|
||||||
expect(results["maxwait"]).to eq("1")
|
|
||||||
expect(results["maxwait_us"].to_i).to be_within(200_000).of(500_000)
|
|
||||||
connections.map(&:close)
|
|
||||||
|
|
||||||
sleep(4.5) # Allow time for stats to update
|
|
||||||
results = admin_conn.async_exec("SHOW POOLS")[0]
|
results = admin_conn.async_exec("SHOW POOLS")[0]
|
||||||
|
# no clients are waiting so value is 0
|
||||||
expect(results["maxwait"]).to eq("0")
|
expect(results["maxwait"]).to eq("0")
|
||||||
|
expect(results["maxwait_us"]).to eq("0")
|
||||||
|
connections.map(&:close)
|
||||||
|
|
||||||
threads.map(&:join)
|
threads.map(&:join)
|
||||||
end
|
end
|
||||||
@@ -329,6 +332,40 @@ describe "Stats" do
|
|||||||
admin_conn.close
|
admin_conn.close
|
||||||
connections.map(&:close)
|
connections.map(&:close)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "when client has waited for a server" do
|
||||||
|
let(:processes) { Helpers::Pgcat.single_instance_setup("sharded_db", 2) }
|
||||||
|
|
||||||
|
it "shows correct maxwait" do
|
||||||
|
threads = []
|
||||||
|
connections = Array.new(3) { |i| PG::connect("#{pgcat_conn_str}?application_name=app#{i}") }
|
||||||
|
connections.each do |c|
|
||||||
|
threads << Thread.new { c.async_exec("SELECT pg_sleep(1.5)") rescue nil }
|
||||||
|
end
|
||||||
|
|
||||||
|
sleep(2.5) # Allow time for stats to update
|
||||||
|
admin_conn = PG::connect(processes.pgcat.admin_connection_string)
|
||||||
|
results = admin_conn.async_exec("SHOW CLIENTS")
|
||||||
|
|
||||||
|
normal_client_results = results.reject { |r| r["database"] == "pgcat" }
|
||||||
|
|
||||||
|
non_waiting_clients = normal_client_results.select { |c| c["maxwait"] == "0" }
|
||||||
|
waiting_clients = normal_client_results.select { |c| c["maxwait"].to_i > 0 }
|
||||||
|
|
||||||
|
expect(non_waiting_clients.count).to eq(2)
|
||||||
|
non_waiting_clients.each do |client|
|
||||||
|
expect(client["maxwait_us"].to_i).to be_between(0, 50_000)
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(waiting_clients.count).to eq(1)
|
||||||
|
waiting_clients.each do |client|
|
||||||
|
expect(client["maxwait_us"].to_i).to be_within(200_000).of(500_000)
|
||||||
|
end
|
||||||
|
|
||||||
|
admin_conn.close
|
||||||
|
connections.map(&:close)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
682
tests/rust/Cargo.lock
generated
682
tests/rust/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -15,8 +15,13 @@ async fn test_prepared_statements() {
|
|||||||
for _ in 0..5 {
|
for _ in 0..5 {
|
||||||
let pool = pool.clone();
|
let pool = pool.clone();
|
||||||
let handle = tokio::task::spawn(async move {
|
let handle = tokio::task::spawn(async move {
|
||||||
for _ in 0..1000 {
|
for i in 0..1000 {
|
||||||
sqlx::query("SELECT 1").fetch_all(&pool).await.unwrap();
|
match sqlx::query(&format!("SELECT {:?}", i % 5)).fetch_all(&pool).await {
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(err) => {
|
||||||
|
panic!("prepared statement error: {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
40
utilities/deb.sh
Normal file
40
utilities/deb.sh
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Build an Ubuntu deb.
|
||||||
|
#
|
||||||
|
script_dir=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||||
|
deb_dir="/tmp/pgcat-build"
|
||||||
|
export PACKAGE_VERSION=${1:-"1.1.1"}
|
||||||
|
if [[ $(arch) == "x86_64" ]]; then
|
||||||
|
export ARCH=amd64
|
||||||
|
else
|
||||||
|
export ARCH=arm64
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "$script_dir/.."
|
||||||
|
cargo build --release
|
||||||
|
|
||||||
|
rm -rf "$deb_dir"
|
||||||
|
mkdir -p "$deb_dir/DEBIAN"
|
||||||
|
mkdir -p "$deb_dir/usr/bin"
|
||||||
|
mkdir -p "$deb_dir/etc/systemd/system"
|
||||||
|
|
||||||
|
cp target/release/pgcat "$deb_dir/usr/bin/pgcat"
|
||||||
|
chmod +x "$deb_dir/usr/bin/pgcat"
|
||||||
|
|
||||||
|
cp pgcat.toml "$deb_dir/etc/pgcat.example.toml"
|
||||||
|
cp pgcat.service "$deb_dir/etc/systemd/system/pgcat.service"
|
||||||
|
|
||||||
|
(cat control | envsubst) > "$deb_dir/DEBIAN/control"
|
||||||
|
cp postinst "$deb_dir/DEBIAN/postinst"
|
||||||
|
cp postrm "$deb_dir/DEBIAN/postrm"
|
||||||
|
cp prerm "$deb_dir/DEBIAN/prerm"
|
||||||
|
|
||||||
|
chmod +x ${deb_dir}/DEBIAN/post*
|
||||||
|
chmod +x ${deb_dir}/DEBIAN/pre*
|
||||||
|
|
||||||
|
dpkg-deb \
|
||||||
|
--root-owner-group \
|
||||||
|
-z1 \
|
||||||
|
--build "$deb_dir" \
|
||||||
|
pgcat-${PACKAGE_VERSION}-ubuntu22.04-${ARCH}.deb
|
||||||
Reference in New Issue
Block a user