mirror of
https://github.com/postgresml/pgcat.git
synced 2026-03-23 17:36:28 +00:00
Compare commits
1 Commits
main
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52119f2333 |
@@ -59,7 +59,6 @@ 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.
|
||||||
@@ -142,7 +141,6 @@ query_parser_enabled = true
|
|||||||
query_parser_read_write_splitting = 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,7 +26,6 @@ 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
|
||||||
|
|
||||||
@@ -107,7 +106,7 @@ cd ../..
|
|||||||
# These tests will start and stop the pgcat server so it will need to be restarted after the tests
|
# These tests will start and stop the pgcat server so it will need to be restarted after the tests
|
||||||
#
|
#
|
||||||
pip3 install -r tests/python/requirements.txt
|
pip3 install -r tests/python/requirements.txt
|
||||||
pytest || exit 1
|
python3 tests/python/tests.py || exit 1
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -178,6 +177,3 @@ killall pgcat -s SIGINT
|
|||||||
|
|
||||||
# Allow for graceful shutdown
|
# Allow for graceful shutdown
|
||||||
sleep 1
|
sleep 1
|
||||||
|
|
||||||
kill -9 $(pgrep toxiproxy)
|
|
||||||
sleep 1
|
|
||||||
|
|||||||
16
.github/workflows/build-and-push.yaml
vendored
16
.github/workflows/build-and-push.yaml
vendored
@@ -23,17 +23,14 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- 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@v3
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
- name: Determine tags
|
- name: Determine tags
|
||||||
id: metadata
|
id: metadata
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v4
|
||||||
with:
|
with:
|
||||||
images: ${{ env.registry }}/${{ env.image-name }}
|
images: ${{ env.registry }}/${{ env.image-name }}
|
||||||
tags: |
|
tags: |
|
||||||
@@ -45,18 +42,15 @@ 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@v3
|
uses: docker/login-action@v2.1.0
|
||||||
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@v6
|
uses: docker/build-push-action@v3
|
||||||
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 }}
|
||||||
|
|||||||
6
.github/workflows/chart-lint-test.yaml
vendored
6
.github/workflows/chart-lint-test.yaml
vendored
@@ -22,12 +22,12 @@ jobs:
|
|||||||
# Python is required because `ct lint` runs Yamale (https://github.com/23andMe/Yamale) and
|
# Python is required because `ct lint` runs Yamale (https://github.com/23andMe/Yamale) and
|
||||||
# yamllint (https://github.com/adrienverge/yamllint) which require Python
|
# yamllint (https://github.com/adrienverge/yamllint) which require Python
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v5.1.0
|
uses: actions/setup-python@v4.1.0
|
||||||
with:
|
with:
|
||||||
python-version: 3.7
|
python-version: 3.7
|
||||||
|
|
||||||
- name: Set up chart-testing
|
- name: Set up chart-testing
|
||||||
uses: helm/chart-testing-action@v2.2.1
|
uses: helm/chart-testing-action@v2.6.1
|
||||||
with:
|
with:
|
||||||
version: v3.5.1
|
version: v3.5.1
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ jobs:
|
|||||||
run: ct lint --config ct.yaml
|
run: ct lint --config ct.yaml
|
||||||
|
|
||||||
- name: Create kind cluster
|
- name: Create kind cluster
|
||||||
uses: helm/kind-action@v1.10.0
|
uses: helm/kind-action@v1.7.0
|
||||||
if: steps.list-changed.outputs.changed == 'true'
|
if: steps.list-changed.outputs.changed == 'true'
|
||||||
|
|
||||||
- name: Run chart-testing (install)
|
- name: Run chart-testing (install)
|
||||||
|
|||||||
2
.github/workflows/chart-release.yaml
vendored
2
.github/workflows/chart-release.yaml
vendored
@@ -32,7 +32,7 @@ jobs:
|
|||||||
version: v3.13.0
|
version: v3.13.0
|
||||||
|
|
||||||
- name: Run chart-releaser
|
- name: Run chart-releaser
|
||||||
uses: helm/chart-releaser-action@a917fd15b20e8b64b94d9158ad54cd6345335584 # v1.6.0
|
uses: helm/chart-releaser-action@be16258da8010256c6e82849661221415f031968 # v1.5.0
|
||||||
with:
|
with:
|
||||||
charts_dir: charts
|
charts_dir: charts
|
||||||
config: cr.yaml
|
config: cr.yaml
|
||||||
|
|||||||
15
.github/workflows/publish-deb-package.yml
vendored
15
.github/workflows/publish-deb-package.yml
vendored
@@ -1,9 +1,6 @@
|
|||||||
name: pgcat package (deb)
|
name: pgcat package (deb)
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- v*
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
packageVersion:
|
packageVersion:
|
||||||
@@ -19,14 +16,6 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- 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
|
- uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
toolchain: stable
|
toolchain: stable
|
||||||
@@ -50,10 +39,10 @@ jobs:
|
|||||||
export ARCH=arm64
|
export ARCH=arm64
|
||||||
fi
|
fi
|
||||||
|
|
||||||
bash utilities/deb.sh ${{ env.packageVersion }}
|
bash utilities/deb.sh ${{ inputs.packageVersion }}
|
||||||
|
|
||||||
deb-s3 upload \
|
deb-s3 upload \
|
||||||
--lock \
|
--lock \
|
||||||
--bucket apt.postgresml.org \
|
--bucket apt.postgresml.org \
|
||||||
pgcat-${{ env.packageVersion }}-ubuntu22.04-${ARCH}.deb \
|
pgcat-${{ inputs.packageVersion }}-ubuntu22.04-${ARCH}.deb \
|
||||||
--codename $(lsb_release -cs)
|
--codename $(lsb_release -cs)
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -10,6 +10,4 @@ lcov.info
|
|||||||
dev/.bash_history
|
dev/.bash_history
|
||||||
dev/cache
|
dev/cache
|
||||||
!dev/cache/.keepme
|
!dev/cache/.keepme
|
||||||
.venv
|
.venv
|
||||||
**/__pycache__
|
|
||||||
.bundle
|
|
||||||
78
CONFIG.md
78
CONFIG.md
@@ -36,11 +36,10 @@ Port at which prometheus exporter listens on.
|
|||||||
### connect_timeout
|
### connect_timeout
|
||||||
```
|
```
|
||||||
path: general.connect_timeout
|
path: general.connect_timeout
|
||||||
default: 1000 # milliseconds
|
default: 5000 # milliseconds
|
||||||
```
|
```
|
||||||
|
|
||||||
How long the client waits to obtain a server connection before aborting (ms).
|
How long to wait before aborting a server connection (ms).
|
||||||
This is similar to PgBouncer's `query_wait_timeout`.
|
|
||||||
|
|
||||||
### idle_timeout
|
### idle_timeout
|
||||||
```
|
```
|
||||||
@@ -298,19 +297,6 @@ Load balancing mode
|
|||||||
`random` selects the server at random
|
`random` selects the server at random
|
||||||
`loc` selects the server with the least outstanding busy connections
|
`loc` selects the server with the least outstanding busy connections
|
||||||
|
|
||||||
### checkout_failure_limit
|
|
||||||
```
|
|
||||||
path: pools.<pool_name>.checkout_failure_limit
|
|
||||||
default: 0 (disabled)
|
|
||||||
```
|
|
||||||
|
|
||||||
`Maximum number of checkout failures a client is allowed before it
|
|
||||||
gets disconnected. This is needed to prevent persistent client/server
|
|
||||||
imbalance in high availability setups where multiple PgCat instances are placed
|
|
||||||
behind a single load balancer. If for any reason a client lands on a PgCat instance that has
|
|
||||||
a large number of connected clients, it might get stuck in perpetual checkout failure loop especially
|
|
||||||
in session mode
|
|
||||||
`
|
|
||||||
### default_role
|
### default_role
|
||||||
```
|
```
|
||||||
path: pools.<pool_name>.default_role
|
path: pools.<pool_name>.default_role
|
||||||
@@ -322,45 +308,6 @@ 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.
|
||||||
|
|
||||||
### db_activity_based_routing
|
|
||||||
```
|
|
||||||
path: pools.<pool_name>.db_activity_based_routing
|
|
||||||
default: false
|
|
||||||
```
|
|
||||||
|
|
||||||
If enabled, PgCat will route queries to the primary if the queried table was recently written to.
|
|
||||||
Only relevant when `query_parser_enabled` *and* `query_parser_read_write_splitting` is enabled.
|
|
||||||
|
|
||||||
##### Considerations:
|
|
||||||
- *This feature is experimental and may not work as expected.*
|
|
||||||
- This feature only works when the same PgCat instance is used for both reads and writes to the database.
|
|
||||||
- This feature is not relevant when the primary is not part of the pool of databases used for load balancing of read queries.
|
|
||||||
- If more than one PgCat instance is used for HA purposes, this feature will not work as expected. A way to still make it work is by using sticky sessions.
|
|
||||||
|
|
||||||
### db_activity_based_ms_init_delay
|
|
||||||
```
|
|
||||||
path: pools.<pool_name>.db_activity_based_ms_init_delay
|
|
||||||
default: 100
|
|
||||||
```
|
|
||||||
|
|
||||||
The delay in milliseconds before the first activity-based routing check is performed.
|
|
||||||
|
|
||||||
### db_activity_ttl
|
|
||||||
```
|
|
||||||
path: pools.<pool_name>.db_activity_ttl
|
|
||||||
default: 900
|
|
||||||
```
|
|
||||||
|
|
||||||
The time in seconds after which a DB is considered inactive when no queries/updates are performed to it.
|
|
||||||
|
|
||||||
### table_mutation_cache_ms_ttl
|
|
||||||
```
|
|
||||||
path: pools.<pool_name>.table_mutation_cache_ms_ttl
|
|
||||||
default: 50
|
|
||||||
```
|
|
||||||
|
|
||||||
The time in milliseconds after a write to a table that all queries to that table will be routed to the primary.
|
|
||||||
|
|
||||||
### prepared_statements_cache_size
|
### prepared_statements_cache_size
|
||||||
```
|
```
|
||||||
path: general.prepared_statements_cache_size
|
path: general.prepared_statements_cache_size
|
||||||
@@ -515,18 +462,10 @@ path: pools.<pool_name>.users.<user_index>.pool_size
|
|||||||
default: 9
|
default: 9
|
||||||
```
|
```
|
||||||
|
|
||||||
Maximum number of server connections that can be established for this user.
|
Maximum number of server connections that can be established for this user
|
||||||
The maximum number of connection from a single Pgcat process to any database in the cluster
|
The maximum number of connection from a single Pgcat process to any database in the cluster
|
||||||
is the sum of pool_size across all users.
|
is the sum of pool_size across all users.
|
||||||
|
|
||||||
### min_pool_size
|
|
||||||
```
|
|
||||||
path: pools.<pool_name>.users.<user_index>.min_pool_size
|
|
||||||
default: 0
|
|
||||||
```
|
|
||||||
|
|
||||||
Minimum number of idle server connections to retain for this pool.
|
|
||||||
|
|
||||||
### statement_timeout
|
### statement_timeout
|
||||||
```
|
```
|
||||||
path: pools.<pool_name>.users.<user_index>.statement_timeout
|
path: pools.<pool_name>.users.<user_index>.statement_timeout
|
||||||
@@ -536,16 +475,6 @@ default: 0
|
|||||||
Maximum query duration. Dangerous, but protects against DBs that died in a non-obvious way.
|
Maximum query duration. Dangerous, but protects against DBs that died in a non-obvious way.
|
||||||
0 means it is disabled.
|
0 means it is disabled.
|
||||||
|
|
||||||
### connect_timeout
|
|
||||||
```
|
|
||||||
path: pools.<pool_name>.users.<user_index>.connect_timeout
|
|
||||||
default: <UNSET> # milliseconds
|
|
||||||
```
|
|
||||||
|
|
||||||
How long the client waits to obtain a server connection before aborting (ms).
|
|
||||||
This is similar to PgBouncer's `query_wait_timeout`.
|
|
||||||
If unset, uses the `connect_timeout` defined globally.
|
|
||||||
|
|
||||||
## `pools.<pool_name>.shards.<shard_index>` Section
|
## `pools.<pool_name>.shards.<shard_index>` Section
|
||||||
|
|
||||||
### servers
|
### servers
|
||||||
@@ -573,3 +502,4 @@ default: "shard0"
|
|||||||
```
|
```
|
||||||
|
|
||||||
Database name (e.g. "postgres")
|
Database name (e.g. "postgres")
|
||||||
|
|
||||||
|
|||||||
@@ -6,32 +6,6 @@ Thank you for contributing! Just a few tips here:
|
|||||||
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
|
||||||
|
|||||||
394
Cargo.lock
generated
394
Cargo.lock
generated
@@ -132,7 +132,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 2.0.26",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -143,15 +143,9 @@ checksum = "a564d521dd56509c4c47480d00b80ee55f7e385ae48db5744c67ad50c92d2ebf"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"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"
|
||||||
@@ -192,11 +186,12 @@ checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bb8"
|
name = "bb8"
|
||||||
version = "0.8.6"
|
version = "0.8.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d89aabfae550a5c44b43ab941844ffcd2e993cb6900b342debf59e9ea74acdb8"
|
checksum = "98b4b0f25f18bcdc3ac72bdb486ed0acf7e185221fd4dc985bc15db5800b0ba2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
"futures-channel",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -229,12 +224,6 @@ version = "3.13.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1"
|
checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bytecount"
|
|
||||||
version = "0.6.8"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder"
|
name = "byteorder"
|
||||||
version = "1.4.3"
|
version = "1.4.3"
|
||||||
@@ -247,37 +236,6 @@ version = "1.4.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
|
checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "camino"
|
|
||||||
version = "1.1.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3"
|
|
||||||
dependencies = [
|
|
||||||
"serde",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cargo-platform"
|
|
||||||
version = "0.1.8"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "24b1f0365a6c6bb4020cd05806fd0d33c44d38046b8bd7f0e40814b9763cabfc"
|
|
||||||
dependencies = [
|
|
||||||
"serde",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cargo_metadata"
|
|
||||||
version = "0.14.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4acbb09d9ee8e23699b9634375c72795d095bf268439da88562cf9b501f181fa"
|
|
||||||
dependencies = [
|
|
||||||
"camino",
|
|
||||||
"cargo-platform",
|
|
||||||
"semver",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.0.79"
|
version = "1.0.79"
|
||||||
@@ -337,7 +295,7 @@ dependencies = [
|
|||||||
"heck",
|
"heck",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 2.0.26",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -367,21 +325,6 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "crossbeam-channel"
|
|
||||||
version = "0.5.13"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2"
|
|
||||||
dependencies = [
|
|
||||||
"crossbeam-utils",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "crossbeam-utils"
|
|
||||||
version = "0.8.20"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crypto-common"
|
name = "crypto-common"
|
||||||
version = "0.1.6"
|
version = "0.1.6"
|
||||||
@@ -392,19 +335,6 @@ dependencies = [
|
|||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dashmap"
|
|
||||||
version = "5.5.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"hashbrown",
|
|
||||||
"lock_api",
|
|
||||||
"once_cell",
|
|
||||||
"parking_lot_core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "data-encoding"
|
name = "data-encoding"
|
||||||
version = "2.4.0"
|
version = "2.4.0"
|
||||||
@@ -467,15 +397,6 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "error-chain"
|
|
||||||
version = "0.12.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc"
|
|
||||||
dependencies = [
|
|
||||||
"version_check",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "exitcode"
|
name = "exitcode"
|
||||||
version = "1.1.2"
|
version = "1.1.2"
|
||||||
@@ -488,12 +409,6 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
|
checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "fastrand"
|
|
||||||
version = "2.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fnv"
|
name = "fnv"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
@@ -565,7 +480,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 2.0.26",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -625,31 +540,31 @@ version = "0.27.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e"
|
checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "glob"
|
|
||||||
version = "0.3.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h2"
|
name = "h2"
|
||||||
version = "0.4.6"
|
version = "0.3.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205"
|
checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atomic-waker",
|
|
||||||
"bytes",
|
"bytes",
|
||||||
"fnv",
|
"fnv",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
|
"futures-util",
|
||||||
"http",
|
"http",
|
||||||
"indexmap",
|
"indexmap 1.9.3",
|
||||||
"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"
|
||||||
@@ -694,9 +609,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "1.1.0"
|
version = "0.2.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
|
checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"fnv",
|
"fnv",
|
||||||
@@ -705,24 +620,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http-body"
|
name = "http-body"
|
||||||
version = "1.0.1"
|
version = "0.4.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
|
checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
|
||||||
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",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -740,12 +643,13 @@ checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper"
|
name = "hyper"
|
||||||
version = "1.4.1"
|
version = "0.14.27"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05"
|
checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"h2",
|
"h2",
|
||||||
"http",
|
"http",
|
||||||
@@ -754,26 +658,13 @@ dependencies = [
|
|||||||
"httpdate",
|
"httpdate",
|
||||||
"itoa",
|
"itoa",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"smallvec",
|
"socket2 0.4.9",
|
||||||
"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"
|
||||||
@@ -818,6 +709,16 @@ 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"
|
||||||
@@ -825,7 +726,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
|
checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown",
|
"hashbrown 0.14.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -947,7 +848,7 @@ version = "0.12.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1efa59af2ddfad1854ae27d75009d538d0998b4b2fd47083e743ac1a10e46c60"
|
checksum = "1efa59af2ddfad1854ae27d75009d538d0998b4b2fd47083e743ac1a10e46c60"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hashbrown",
|
"hashbrown 0.14.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1004,21 +905,6 @@ dependencies = [
|
|||||||
"autocfg",
|
"autocfg",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mini-moka"
|
|
||||||
version = "0.10.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c325dfab65f261f386debee8b0969da215b3fa0037e74c8a1234db7ba986d803"
|
|
||||||
dependencies = [
|
|
||||||
"crossbeam-channel",
|
|
||||||
"crossbeam-utils",
|
|
||||||
"dashmap",
|
|
||||||
"skeptic",
|
|
||||||
"smallvec",
|
|
||||||
"tagptr",
|
|
||||||
"triomphe",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
@@ -1093,9 +979,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.20.2"
|
version = "1.18.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
|
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "overload"
|
name = "overload"
|
||||||
@@ -1134,7 +1020,7 @@ checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pgcat"
|
name = "pgcat"
|
||||||
version = "1.3.0"
|
version = "1.1.2-dev4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -1148,15 +1034,12 @@ dependencies = [
|
|||||||
"fallible-iterator",
|
"fallible-iterator",
|
||||||
"futures",
|
"futures",
|
||||||
"hmac",
|
"hmac",
|
||||||
"http-body-util",
|
|
||||||
"hyper",
|
"hyper",
|
||||||
"hyper-util",
|
|
||||||
"itertools",
|
"itertools",
|
||||||
"jemallocator",
|
"jemallocator",
|
||||||
"log",
|
"log",
|
||||||
"lru",
|
"lru",
|
||||||
"md-5",
|
"md-5",
|
||||||
"mini-moka",
|
|
||||||
"nix",
|
"nix",
|
||||||
"num_cpus",
|
"num_cpus",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@@ -1171,7 +1054,6 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serial_test",
|
|
||||||
"sha-1",
|
"sha-1",
|
||||||
"sha2",
|
"sha2",
|
||||||
"socket2 0.4.9",
|
"socket2 0.4.9",
|
||||||
@@ -1217,7 +1099,7 @@ dependencies = [
|
|||||||
"phf_shared",
|
"phf_shared",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 2.0.26",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1246,7 +1128,7 @@ checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 2.0.26",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1287,24 +1169,13 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.89"
|
version = "1.0.66"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
|
checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pulldown-cmark"
|
|
||||||
version = "0.9.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 2.3.3",
|
|
||||||
"memchr",
|
|
||||||
"unicase",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quick-error"
|
name = "quick-error"
|
||||||
version = "1.2.3"
|
version = "1.2.3"
|
||||||
@@ -1313,9 +1184,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.37"
|
version = "1.0.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
|
checksum = "5fe8a65d69dd0808184ebb5f836ab526bb259db23c657efa38711b1072ee47f0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
@@ -1494,24 +1365,6 @@ version = "1.0.15"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
|
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "same-file"
|
|
||||||
version = "1.0.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
|
||||||
dependencies = [
|
|
||||||
"winapi-util",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "scc"
|
|
||||||
version = "2.2.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d8d25269dd3a12467afe2e510f69fb0b46b698e5afb296b59f2145259deaf8e8"
|
|
||||||
dependencies = [
|
|
||||||
"sdd",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@@ -1528,39 +1381,24 @@ dependencies = [
|
|||||||
"untrusted",
|
"untrusted",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "sdd"
|
|
||||||
version = "3.0.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "49c1eeaf4b6a87c7479688c6d52b9f1153cedd3c489300564f932b065c6eab95"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "semver"
|
|
||||||
version = "1.0.23"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
|
|
||||||
dependencies = [
|
|
||||||
"serde",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.214"
|
version = "1.0.171"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5"
|
checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.214"
|
version = "1.0.171"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766"
|
checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 2.0.26",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1583,31 +1421,6 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serial_test"
|
|
||||||
version = "3.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4b4b487fe2acf240a021cf57c6b2b4903b1e78ca0ecd862a71b71d2a51fed77d"
|
|
||||||
dependencies = [
|
|
||||||
"futures",
|
|
||||||
"log",
|
|
||||||
"once_cell",
|
|
||||||
"parking_lot",
|
|
||||||
"scc",
|
|
||||||
"serial_test_derive",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serial_test_derive"
|
|
||||||
version = "3.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "82fe9db325bcef1fbcde82e078a5cc4efdf787e96b3b9cf45b50b529f2083d67"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.87",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha-1"
|
name = "sha-1"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
@@ -1654,21 +1467,6 @@ version = "0.3.10"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de"
|
checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "skeptic"
|
|
||||||
version = "0.13.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "16d23b015676c90a0f01c197bfdc786c20342c73a0afdda9025adb0bc42940a8"
|
|
||||||
dependencies = [
|
|
||||||
"bytecount",
|
|
||||||
"cargo_metadata",
|
|
||||||
"error-chain",
|
|
||||||
"glob",
|
|
||||||
"pulldown-cmark",
|
|
||||||
"tempfile",
|
|
||||||
"walkdir",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.8"
|
version = "0.4.8"
|
||||||
@@ -1680,9 +1478,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
version = "1.13.2"
|
version = "1.11.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
@@ -1712,9 +1510,9 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlparser"
|
name = "sqlparser"
|
||||||
version = "0.52.0"
|
version = "0.34.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9a875d8cd437cc8a97e9aeaeea352ec9a19aea99c23e9effb17757291de80b08"
|
checksum = "37d3706eefb17039056234df6b566b0014f303f867f2656108334a55b8096f59"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"sqlparser_derive",
|
"sqlparser_derive",
|
||||||
@@ -1722,13 +1520,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlparser_derive"
|
name = "sqlparser_derive"
|
||||||
version = "0.2.2"
|
version = "0.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "01b2e185515564f15375f593fb966b5718bc624ba77fe49fa4616ad619690554"
|
checksum = "55fe75cb4a364c7f7ae06c7dbbc8d84bddd85d6cdf9975963c3935bc1991761e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 1.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1772,34 +1570,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.87"
|
version = "2.0.26"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
|
checksum = "45c3457aacde3c65315de5031ec191ce46604304d2446e803d71ade03308d970"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tagptr"
|
|
||||||
version = "0.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tempfile"
|
|
||||||
version = "3.8.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"fastrand",
|
|
||||||
"redox_syscall",
|
|
||||||
"rustix",
|
|
||||||
"windows-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.43"
|
version = "1.0.43"
|
||||||
@@ -1817,7 +1596,7 @@ checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 2.0.26",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1884,7 +1663,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 2.0.26",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1962,13 +1741,19 @@ 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",
|
"indexmap 2.0.0",
|
||||||
"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"
|
||||||
@@ -1989,7 +1774,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 2.0.26",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2044,12 +1829,6 @@ dependencies = [
|
|||||||
"tracing-serde",
|
"tracing-serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "triomphe"
|
|
||||||
version = "0.1.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "859eb650cfee7434994602c3a68b25d77ad9e68c8a6cd491616ef86661382eb3"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "trust-dns-proto"
|
name = "trust-dns-proto"
|
||||||
version = "0.22.0"
|
version = "0.22.0"
|
||||||
@@ -2107,12 +1886,6 @@ version = "1.16.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
|
checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicase"
|
|
||||||
version = "2.8.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-bidi"
|
name = "unicode-bidi"
|
||||||
version = "0.3.13"
|
version = "0.3.13"
|
||||||
@@ -2169,16 +1942,6 @@ version = "0.9.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "walkdir"
|
|
||||||
version = "2.5.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
|
||||||
dependencies = [
|
|
||||||
"same-file",
|
|
||||||
"winapi-util",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "want"
|
name = "want"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
@@ -2221,7 +1984,7 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 2.0.26",
|
||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2243,7 +2006,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 2.0.26",
|
||||||
"wasm-bindgen-backend",
|
"wasm-bindgen-backend",
|
||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
@@ -2295,15 +2058,6 @@ version = "0.4.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "winapi-util"
|
|
||||||
version = "0.1.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
|
||||||
dependencies = [
|
|
||||||
"windows-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi-x86_64-pc-windows-gnu"
|
name = "winapi-x86_64-pc-windows-gnu"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
|
|||||||
19
Cargo.toml
19
Cargo.toml
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "pgcat"
|
name = "pgcat"
|
||||||
version = "1.3.0"
|
version = "1.1.2-dev4"
|
||||||
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
|
||||||
@@ -8,7 +8,7 @@ edition = "2021"
|
|||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
md-5 = "0.10"
|
md-5 = "0.10"
|
||||||
bb8 = "=0.8.6"
|
bb8 = "0.8.1"
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
@@ -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.52", features = ["visitor"] }
|
sqlparser = {version = "0.34", 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,9 +29,7 @@ base64 = "0.21"
|
|||||||
stringprep = "0.1"
|
stringprep = "0.1"
|
||||||
tokio-rustls = "0.24"
|
tokio-rustls = "0.24"
|
||||||
rustls-pemfile = "1"
|
rustls-pemfile = "1"
|
||||||
http-body-util = "0.1.2"
|
hyper = { version = "0.14", features = ["full"] }
|
||||||
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"
|
||||||
@@ -49,16 +47,9 @@ 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 = [
|
tracing-subscriber = { version = "0.3.17", features = ["json", "env-filter", "std"]}
|
||||||
"json",
|
|
||||||
"env-filter",
|
|
||||||
"std",
|
|
||||||
] }
|
|
||||||
lru = "0.12.0"
|
lru = "0.12.0"
|
||||||
mini-moka = "0.10.3"
|
|
||||||
|
|
||||||
[target.'cfg(not(target_env = "msvc"))'.dependencies]
|
[target.'cfg(not(target_env = "msvc"))'.dependencies]
|
||||||
jemallocator = "0.5.0"
|
jemallocator = "0.5.0"
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
serial_test = "*"
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM rust:1.81.0-slim-bookworm AS builder
|
FROM rust:1-slim-bookworm AS builder
|
||||||
|
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y build-essential
|
apt-get install -y build-essential
|
||||||
@@ -19,4 +19,3 @@ 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,4 @@
|
|||||||
FROM cimg/rust:1.81.0
|
FROM cimg/rust:1.67.1
|
||||||
COPY --from=sclevine/yj /bin/yj /bin/yj
|
COPY --from=sclevine/yj /bin/yj /bin/yj
|
||||||
RUN /bin/yj -h
|
RUN /bin/yj -h
|
||||||
RUN sudo apt-get update && \
|
RUN sudo apt-get update && \
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ User.find_by_email("test@example.com")
|
|||||||
```sql
|
```sql
|
||||||
-- Grab a bunch of users from shard 1
|
-- Grab a bunch of users from shard 1
|
||||||
SET SHARD TO '1';
|
SET SHARD TO '1';
|
||||||
SELECT * FROM users LIMIT 10;
|
SELECT * FROM users LIMT 10;
|
||||||
|
|
||||||
-- Find by id
|
-- Find by id
|
||||||
SET SHARDING KEY TO '1234';
|
SET SHARDING KEY TO '1234';
|
||||||
@@ -268,8 +268,6 @@ 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.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ apiVersion: v2
|
|||||||
name: pgcat
|
name: pgcat
|
||||||
description: A Helm chart for PgCat a PostgreSQL pooler and proxy (like PgBouncer) with support for sharding, load balancing, failover and mirroring.
|
description: A Helm chart for PgCat a PostgreSQL pooler and proxy (like PgBouncer) with support for sharding, load balancing, failover and mirroring.
|
||||||
maintainers:
|
maintainers:
|
||||||
- name: PostgresML
|
- name: Wildcard
|
||||||
email: team@postgresml.org
|
email: support@w6d.io
|
||||||
appVersion: "1.3.0"
|
appVersion: "1.1.1"
|
||||||
version: 0.2.5
|
version: 0.1.0
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ stringData:
|
|||||||
connect_timeout = {{ .Values.configuration.general.connect_timeout }}
|
connect_timeout = {{ .Values.configuration.general.connect_timeout }}
|
||||||
idle_timeout = {{ .Values.configuration.general.idle_timeout | int }}
|
idle_timeout = {{ .Values.configuration.general.idle_timeout | int }}
|
||||||
server_lifetime = {{ .Values.configuration.general.server_lifetime | int }}
|
server_lifetime = {{ .Values.configuration.general.server_lifetime | int }}
|
||||||
server_tls = {{ .Values.configuration.general.server_tls }}
|
|
||||||
idle_client_in_transaction_timeout = {{ .Values.configuration.general.idle_client_in_transaction_timeout | int }}
|
idle_client_in_transaction_timeout = {{ .Values.configuration.general.idle_client_in_transaction_timeout | int }}
|
||||||
healthcheck_timeout = {{ .Values.configuration.general.healthcheck_timeout }}
|
healthcheck_timeout = {{ .Values.configuration.general.healthcheck_timeout }}
|
||||||
healthcheck_delay = {{ .Values.configuration.general.healthcheck_delay }}
|
healthcheck_delay = {{ .Values.configuration.general.healthcheck_delay }}
|
||||||
@@ -51,10 +50,6 @@ stringData:
|
|||||||
query_parser_enabled = {{ default true $pool.query_parser_enabled }}
|
query_parser_enabled = {{ default true $pool.query_parser_enabled }}
|
||||||
query_parser_read_write_splitting = {{ default true $pool.query_parser_read_write_splitting }}
|
query_parser_read_write_splitting = {{ default true $pool.query_parser_read_write_splitting }}
|
||||||
primary_reads_enabled = {{ default true $pool.primary_reads_enabled }}
|
primary_reads_enabled = {{ default true $pool.primary_reads_enabled }}
|
||||||
db_activity_based_routing = {{ default false $pool.db_activity_based_routing }}
|
|
||||||
db_activity_based_ms_init_delay = {{ default 100 $pool.db_activity_based_ms_init_delay }}
|
|
||||||
db_activity_ttl = {{ default 900 $pool.db_activity_ttl }}
|
|
||||||
table_mutation_cache_ttl = {{ default 50 $pool.table_mutation_cache_ttl }}
|
|
||||||
sharding_function = {{ default "pg_bigint_hash" $pool.sharding_function | quote }}
|
sharding_function = {{ default "pg_bigint_hash" $pool.sharding_function | quote }}
|
||||||
|
|
||||||
{{- range $index, $user := $pool.users }}
|
{{- range $index, $user := $pool.users }}
|
||||||
@@ -63,21 +58,11 @@ stringData:
|
|||||||
##
|
##
|
||||||
[pools.{{ $pool.name | quote }}.users.{{ $index }}]
|
[pools.{{ $pool.name | quote }}.users.{{ $index }}]
|
||||||
username = {{ $user.username | quote }}
|
username = {{ $user.username | quote }}
|
||||||
{{- if $user.password }}
|
|
||||||
password = {{ $user.password | quote }}
|
password = {{ $user.password | quote }}
|
||||||
{{- else if and $user.passwordSecret.name $user.passwordSecret.key }}
|
|
||||||
{{- $secret := (lookup "v1" "Secret" $.Release.Namespace $user.passwordSecret.name) }}
|
|
||||||
{{- if $secret }}
|
|
||||||
{{- $password := index $secret.data $user.passwordSecret.key | b64dec }}
|
|
||||||
password = {{ $password | quote }}
|
|
||||||
{{- end }}
|
|
||||||
{{- end }}
|
|
||||||
pool_size = {{ $user.pool_size }}
|
pool_size = {{ $user.pool_size }}
|
||||||
statement_timeout = {{ default 0 $user.statement_timeout }}
|
statement_timeout = {{ $user.statement_timeout }}
|
||||||
min_pool_size = {{ default 3 $user.min_pool_size }}
|
min_pool_size = 3
|
||||||
{{- if $user.server_lifetime }}
|
server_lifetime = 60000
|
||||||
server_lifetime = {{ $user.server_lifetime }}
|
|
||||||
{{- end }}
|
|
||||||
{{- if and $user.server_username $user.server_password }}
|
{{- if and $user.server_username $user.server_password }}
|
||||||
server_username = {{ $user.server_username | quote }}
|
server_username = {{ $user.server_username | quote }}
|
||||||
server_password = {{ $user.server_password | quote }}
|
server_password = {{ $user.server_password | quote }}
|
||||||
|
|||||||
@@ -170,16 +170,13 @@ configuration:
|
|||||||
connect_timeout: 5000
|
connect_timeout: 5000
|
||||||
|
|
||||||
# How long an idle connection with a server is left open (ms).
|
# How long an idle connection with a server is left open (ms).
|
||||||
idle_timeout: 30000 # milliseconds
|
idle_timeout: 30000 # milliseconds
|
||||||
|
|
||||||
# Max connection lifetime before it's closed, even if actively used.
|
# Max connection lifetime before it's closed, even if actively used.
|
||||||
server_lifetime: 86400000 # 24 hours
|
server_lifetime: 86400000 # 24 hours
|
||||||
|
|
||||||
# Whether to use TLS for server connections or not.
|
|
||||||
server_tls: false
|
|
||||||
|
|
||||||
# How long a client is allowed to be idle while in a transaction (ms).
|
# How long a client is allowed to be idle while in a transaction (ms).
|
||||||
idle_client_in_transaction_timeout: 0 # milliseconds
|
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).
|
# @param configuration.general.healthcheck_timeout How much time to give `SELECT 1` health check query to return with a result (ms).
|
||||||
healthcheck_timeout: 1000
|
healthcheck_timeout: 1000
|
||||||
@@ -243,15 +240,7 @@ configuration:
|
|||||||
## the pool_name is what clients use as database name when connecting
|
## the pool_name is what clients use as database name when connecting
|
||||||
## For the example below a client can connect using "postgres://sharding_user:sharding_user@pgcat_host:pgcat_port/sharded"
|
## For the example below a client can connect using "postgres://sharding_user:sharding_user@pgcat_host:pgcat_port/sharded"
|
||||||
## @param [object]
|
## @param [object]
|
||||||
pools:
|
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
|
# - ## default values
|
||||||
# ##
|
# ##
|
||||||
# ##
|
# ##
|
||||||
@@ -298,22 +287,6 @@ configuration:
|
|||||||
# ## @param configuration.poolsPostgres.query_parser_read_write_splitting
|
# ## @param configuration.poolsPostgres.query_parser_read_write_splitting
|
||||||
# query_parser_read_write_splitting: true
|
# query_parser_read_write_splitting: true
|
||||||
|
|
||||||
# ## Db activity based routing. If enabled, we'll route queries to the primary if the table was recently mutated.
|
|
||||||
# ## @param configuration.poolsPostgres.db_activity_based_routing
|
|
||||||
# db_activity_based_routing: false
|
|
||||||
|
|
||||||
# ## DB activity based init delay. How long to wait before starting to route queries to the primary after a table mutation.
|
|
||||||
# ## @param configuration.poolsPostgres.db_activity_based_ms_init_delay
|
|
||||||
# db_activity_based_ms_init_delay: 100
|
|
||||||
|
|
||||||
# ## DB activity TTL. How long before marking the DB as inactive after no mutations or queries.
|
|
||||||
# ## @param configuration.poolsPostgres.db_activity_ttl
|
|
||||||
# db_activity_ttl: 900
|
|
||||||
|
|
||||||
# ## Table mutation cache TTL. How long to keep track of table mutations.
|
|
||||||
# ## @param configuration.poolsPostgres.table_mutation_cache_ttl
|
|
||||||
# table_mutation_cache_ttl: 50
|
|
||||||
|
|
||||||
# ## 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.
|
||||||
@@ -334,9 +307,7 @@ configuration:
|
|||||||
# ## Credentials for users that may connect to this cluster
|
# ## Credentials for users that may connect to this cluster
|
||||||
# ## @param users [array]
|
# ## @param users [array]
|
||||||
# ## @param users[0].username Name of the env var (required)
|
# ## @param users[0].username Name of the env var (required)
|
||||||
# ## @param users[0].password Value for the env var (required) leave empty to use existing secret see passwordSecret.name and passwordSecret.key
|
# ## @param users[0].password Value for the env var (required)
|
||||||
# ## @param users[0].passwordSecret.name Name of the secret containing the password
|
|
||||||
# ## @param users[0].passwordSecret.key Key in the secret containing the password
|
|
||||||
# ## @param users[0].pool_size Maximum number of server connections that can be established for this user
|
# ## @param users[0].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.
|
# ## @param users[0].statement_timeout Maximum query duration. Dangerous, but protects against DBs that died in a non-obvious way.
|
||||||
# users: []
|
# users: []
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM rust:bullseye
|
FROM rust:1.70-bullseye
|
||||||
|
|
||||||
# Dependencies
|
# Dependencies
|
||||||
COPY --from=sclevine/yj /bin/yj /bin/yj
|
COPY --from=sclevine/yj /bin/yj /bin/yj
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,6 @@ RestartSec=1
|
|||||||
Environment=RUST_LOG=info
|
Environment=RUST_LOG=info
|
||||||
LimitNOFILE=65536
|
LimitNOFILE=65536
|
||||||
ExecStart=/usr/bin/pgcat /etc/pgcat.toml
|
ExecStart=/usr/bin/pgcat /etc/pgcat.toml
|
||||||
ExecReload=/bin/kill -SIGHUP $MAINPID
|
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ primary_reads_enabled = true
|
|||||||
# `random`: picks a shard at random
|
# `random`: picks a shard at random
|
||||||
# `random_healthy`: picks a shard at random favoring shards with the least number of recent errors
|
# `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
|
# `shard_<number>`: e.g. shard_0, shard_4, etc. picks a specific shard, everytime
|
||||||
# default_shard = "shard_0"
|
# 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?
|
||||||
|
|||||||
4
postinst
4
postinst
@@ -7,7 +7,3 @@ systemctl enable pgcat
|
|||||||
if ! id pgcat 2> /dev/null; then
|
if ! id pgcat 2> /dev/null; then
|
||||||
useradd -s /usr/bin/false pgcat
|
useradd -s /usr/bin/false pgcat
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -f /etc/pgcat.toml ]; then
|
|
||||||
systemctl start pgcat
|
|
||||||
fi
|
|
||||||
|
|||||||
14
src/admin.rs
14
src/admin.rs
@@ -55,12 +55,7 @@ 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
|
match query_parts[0].to_ascii_uppercase().as_str() {
|
||||||
.first()
|
|
||||||
.unwrap_or(&"")
|
|
||||||
.to_ascii_uppercase()
|
|
||||||
.as_str()
|
|
||||||
{
|
|
||||||
"BAN" => {
|
"BAN" => {
|
||||||
trace!("BAN");
|
trace!("BAN");
|
||||||
ban(stream, query_parts).await
|
ban(stream, query_parts).await
|
||||||
@@ -89,12 +84,7 @@ where
|
|||||||
trace!("SHUTDOWN");
|
trace!("SHUTDOWN");
|
||||||
shutdown(stream).await
|
shutdown(stream).await
|
||||||
}
|
}
|
||||||
"SHOW" => match query_parts
|
"SHOW" => match query_parts[1].to_ascii_uppercase().as_str() {
|
||||||
.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
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
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;
|
||||||
@@ -72,7 +71,6 @@ 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,
|
||||||
|
|||||||
340
src/client.rs
340
src/client.rs
@@ -14,9 +14,7 @@ use tokio::sync::mpsc::Sender;
|
|||||||
|
|
||||||
use crate::admin::{generate_server_parameters_for_admin, handle_admin};
|
use crate::admin::{generate_server_parameters_for_admin, handle_admin};
|
||||||
use crate::auth_passthrough::refetch_auth_hash;
|
use crate::auth_passthrough::refetch_auth_hash;
|
||||||
use crate::config::{
|
use crate::config::{get_config, get_idle_client_in_transaction_timeout, Address, PoolMode};
|
||||||
get_config, get_idle_client_in_transaction_timeout, Address, AuthType, PoolMode,
|
|
||||||
};
|
|
||||||
use crate::constants::*;
|
use crate::constants::*;
|
||||||
use crate::messages::*;
|
use crate::messages::*;
|
||||||
use crate::plugins::PluginOutput;
|
use crate::plugins::PluginOutput;
|
||||||
@@ -465,8 +463,8 @@ where
|
|||||||
.count()
|
.count()
|
||||||
== 1;
|
== 1;
|
||||||
|
|
||||||
|
// Kick any client that's not admin while we're in admin-only mode.
|
||||||
if !admin && admin_only {
|
if !admin && admin_only {
|
||||||
// Kick any client that's not admin while we're in admin-only mode.
|
|
||||||
debug!(
|
debug!(
|
||||||
"Rejecting non-admin connection to {} when in admin only mode",
|
"Rejecting non-admin connection to {} when in admin only mode",
|
||||||
pool_name
|
pool_name
|
||||||
@@ -483,76 +481,72 @@ where
|
|||||||
let process_id: i32 = rand::random();
|
let process_id: i32 = rand::random();
|
||||||
let secret_key: i32 = rand::random();
|
let secret_key: i32 = rand::random();
|
||||||
|
|
||||||
|
// Perform MD5 authentication.
|
||||||
|
// TODO: Add SASL support.
|
||||||
|
let salt = md5_challenge(&mut write).await?;
|
||||||
|
|
||||||
|
let code = match read.read_u8().await {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(_) => {
|
||||||
|
return Err(Error::ClientSocketError(
|
||||||
|
"password code".into(),
|
||||||
|
client_identifier,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// PasswordMessage
|
||||||
|
if code as char != 'p' {
|
||||||
|
return Err(Error::ProtocolSyncError(format!(
|
||||||
|
"Expected p, got {}",
|
||||||
|
code as char
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let len = match read.read_i32().await {
|
||||||
|
Ok(len) => len,
|
||||||
|
Err(_) => {
|
||||||
|
return Err(Error::ClientSocketError(
|
||||||
|
"password message length".into(),
|
||||||
|
client_identifier,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut password_response = vec![0u8; (len - 4) as usize];
|
||||||
|
|
||||||
|
match read.read_exact(&mut password_response).await {
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(_) => {
|
||||||
|
return Err(Error::ClientSocketError(
|
||||||
|
"password message".into(),
|
||||||
|
client_identifier,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let mut prepared_statements_enabled = false;
|
let mut prepared_statements_enabled = false;
|
||||||
|
|
||||||
// Authenticate admin user.
|
// Authenticate admin user.
|
||||||
let (transaction_mode, mut server_parameters) = if admin {
|
let (transaction_mode, mut server_parameters) = if admin {
|
||||||
let config = get_config();
|
let config = get_config();
|
||||||
// TODO: Add SASL support.
|
|
||||||
// Perform MD5 authentication.
|
|
||||||
match config.general.admin_auth_type {
|
|
||||||
AuthType::Trust => (),
|
|
||||||
AuthType::MD5 => {
|
|
||||||
let salt = md5_challenge(&mut write).await?;
|
|
||||||
|
|
||||||
let code = match read.read_u8().await {
|
// Compare server and client hashes.
|
||||||
Ok(p) => p,
|
let password_hash = md5_hash_password(
|
||||||
Err(_) => {
|
&config.general.admin_username,
|
||||||
return Err(Error::ClientSocketError(
|
&config.general.admin_password,
|
||||||
"password code".into(),
|
&salt,
|
||||||
client_identifier,
|
);
|
||||||
))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// PasswordMessage
|
if password_hash != password_response {
|
||||||
if code as char != 'p' {
|
let error = Error::ClientGeneralError("Invalid password".into(), client_identifier);
|
||||||
return Err(Error::ProtocolSyncError(format!(
|
|
||||||
"Expected p, got {}",
|
|
||||||
code as char
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
let len = match read.read_i32().await {
|
warn!("{}", error);
|
||||||
Ok(len) => len,
|
wrong_password(&mut write, username).await?;
|
||||||
Err(_) => {
|
|
||||||
return Err(Error::ClientSocketError(
|
|
||||||
"password message length".into(),
|
|
||||||
client_identifier,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut password_response = vec![0u8; (len - 4) as usize];
|
return Err(error);
|
||||||
|
|
||||||
match read.read_exact(&mut password_response).await {
|
|
||||||
Ok(_) => (),
|
|
||||||
Err(_) => {
|
|
||||||
return Err(Error::ClientSocketError(
|
|
||||||
"password message".into(),
|
|
||||||
client_identifier,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Compare server and client hashes.
|
|
||||||
let password_hash = md5_hash_password(
|
|
||||||
&config.general.admin_username,
|
|
||||||
&config.general.admin_password,
|
|
||||||
&salt,
|
|
||||||
);
|
|
||||||
|
|
||||||
if password_hash != password_response {
|
|
||||||
let error =
|
|
||||||
Error::ClientGeneralError("Invalid password".into(), client_identifier);
|
|
||||||
|
|
||||||
warn!("{}", error);
|
|
||||||
wrong_password(&mut write, username).await?;
|
|
||||||
|
|
||||||
return Err(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
(false, generate_server_parameters_for_admin())
|
(false, generate_server_parameters_for_admin())
|
||||||
}
|
}
|
||||||
// Authenticate normal user.
|
// Authenticate normal user.
|
||||||
@@ -579,143 +573,92 @@ where
|
|||||||
// Obtain the hash to compare, we give preference to that written in cleartext in config
|
// Obtain the hash to compare, we give preference to that written in cleartext in config
|
||||||
// if there is nothing set in cleartext and auth passthrough (auth_query) is configured, we use the hash obtained
|
// if there is nothing set in cleartext and auth passthrough (auth_query) is configured, we use the hash obtained
|
||||||
// when the pool was created. If there is no hash there, we try to fetch it one more time.
|
// when the pool was created. If there is no hash there, we try to fetch it one more time.
|
||||||
match pool.settings.user.auth_type {
|
let password_hash = if let Some(password) = &pool.settings.user.password {
|
||||||
AuthType::Trust => (),
|
Some(md5_hash_password(username, password, &salt))
|
||||||
AuthType::MD5 => {
|
} else {
|
||||||
// Perform MD5 authentication.
|
if !get_config().is_auth_query_configured() {
|
||||||
// TODO: Add SASL support.
|
wrong_password(&mut write, username).await?;
|
||||||
let salt = md5_challenge(&mut write).await?;
|
return Err(Error::ClientAuthImpossible(username.into()));
|
||||||
|
}
|
||||||
|
|
||||||
let code = match read.read_u8().await {
|
let mut hash = (*pool.auth_hash.read()).clone();
|
||||||
Ok(p) => p,
|
|
||||||
Err(_) => {
|
|
||||||
return Err(Error::ClientSocketError(
|
|
||||||
"password code".into(),
|
|
||||||
client_identifier,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// PasswordMessage
|
if hash.is_none() {
|
||||||
if code as char != 'p' {
|
warn!(
|
||||||
return Err(Error::ProtocolSyncError(format!(
|
"Query auth configured \
|
||||||
"Expected p, got {}",
|
but no hash password found \
|
||||||
code as char
|
for pool {}. Will try to refetch it.",
|
||||||
)));
|
pool_name
|
||||||
}
|
);
|
||||||
|
|
||||||
let len = match read.read_i32().await {
|
match refetch_auth_hash(&pool).await {
|
||||||
Ok(len) => len,
|
Ok(fetched_hash) => {
|
||||||
Err(_) => {
|
warn!("Password for {}, obtained. Updating.", client_identifier);
|
||||||
return Err(Error::ClientSocketError(
|
|
||||||
"password message length".into(),
|
|
||||||
client_identifier,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut password_response = vec![0u8; (len - 4) as usize];
|
|
||||||
|
|
||||||
match read.read_exact(&mut password_response).await {
|
|
||||||
Ok(_) => (),
|
|
||||||
Err(_) => {
|
|
||||||
return Err(Error::ClientSocketError(
|
|
||||||
"password message".into(),
|
|
||||||
client_identifier,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let password_hash = if let Some(password) = &pool.settings.user.password {
|
|
||||||
Some(md5_hash_password(username, password, &salt))
|
|
||||||
} else {
|
|
||||||
if !get_config().is_auth_query_configured() {
|
|
||||||
wrong_password(&mut write, username).await?;
|
|
||||||
return Err(Error::ClientAuthImpossible(username.into()));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut hash = (*pool.auth_hash.read()).clone();
|
|
||||||
|
|
||||||
if hash.is_none() {
|
|
||||||
warn!(
|
|
||||||
"Query auth configured \
|
|
||||||
but no hash password found \
|
|
||||||
for pool {}. Will try to refetch it.",
|
|
||||||
pool_name
|
|
||||||
);
|
|
||||||
|
|
||||||
match refetch_auth_hash(&pool).await {
|
|
||||||
Ok(fetched_hash) => {
|
|
||||||
warn!(
|
|
||||||
"Password for {}, obtained. Updating.",
|
|
||||||
client_identifier
|
|
||||||
);
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut pool_auth_hash = pool.auth_hash.write();
|
|
||||||
*pool_auth_hash = Some(fetched_hash.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
hash = Some(fetched_hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(err) => {
|
|
||||||
wrong_password(&mut write, username).await?;
|
|
||||||
|
|
||||||
return Err(Error::ClientAuthPassthroughError(
|
|
||||||
err.to_string(),
|
|
||||||
client_identifier,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(md5_hash_second_pass(&hash.unwrap(), &salt))
|
|
||||||
};
|
|
||||||
|
|
||||||
// Once we have the resulting hash, we compare with what the client gave us.
|
|
||||||
// If they do not match and auth query is set up, we try to refetch the hash one more time
|
|
||||||
// to see if the password has changed since the pool was created.
|
|
||||||
//
|
|
||||||
// @TODO: we could end up fetching again the same password twice (see above).
|
|
||||||
if password_hash.unwrap() != password_response {
|
|
||||||
warn!(
|
|
||||||
"Invalid password {}, will try to refetch it.",
|
|
||||||
client_identifier
|
|
||||||
);
|
|
||||||
|
|
||||||
let fetched_hash = match refetch_auth_hash(&pool).await {
|
|
||||||
Ok(fetched_hash) => fetched_hash,
|
|
||||||
Err(err) => {
|
|
||||||
wrong_password(&mut write, username).await?;
|
|
||||||
|
|
||||||
return Err(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let new_password_hash = md5_hash_second_pass(&fetched_hash, &salt);
|
|
||||||
|
|
||||||
// Ok password changed in server an auth is possible.
|
|
||||||
if new_password_hash == password_response {
|
|
||||||
warn!(
|
|
||||||
"Password for {}, changed in server. Updating.",
|
|
||||||
client_identifier
|
|
||||||
);
|
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut pool_auth_hash = pool.auth_hash.write();
|
let mut pool_auth_hash = pool.auth_hash.write();
|
||||||
*pool_auth_hash = Some(fetched_hash);
|
*pool_auth_hash = Some(fetched_hash.clone());
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
|
hash = Some(fetched_hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(err) => {
|
||||||
wrong_password(&mut write, username).await?;
|
wrong_password(&mut write, username).await?;
|
||||||
return Err(Error::ClientGeneralError(
|
|
||||||
"Invalid password".into(),
|
return Err(Error::ClientAuthPassthroughError(
|
||||||
|
err.to_string(),
|
||||||
client_identifier,
|
client_identifier,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(md5_hash_second_pass(&hash.unwrap(), &salt))
|
||||||
|
};
|
||||||
|
|
||||||
|
// Once we have the resulting hash, we compare with what the client gave us.
|
||||||
|
// If they do not match and auth query is set up, we try to refetch the hash one more time
|
||||||
|
// to see if the password has changed since the pool was created.
|
||||||
|
//
|
||||||
|
// @TODO: we could end up fetching again the same password twice (see above).
|
||||||
|
if password_hash.unwrap() != password_response {
|
||||||
|
warn!(
|
||||||
|
"Invalid password {}, will try to refetch it.",
|
||||||
|
client_identifier
|
||||||
|
);
|
||||||
|
|
||||||
|
let fetched_hash = match refetch_auth_hash(&pool).await {
|
||||||
|
Ok(fetched_hash) => fetched_hash,
|
||||||
|
Err(err) => {
|
||||||
|
wrong_password(&mut write, username).await?;
|
||||||
|
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let new_password_hash = md5_hash_second_pass(&fetched_hash, &salt);
|
||||||
|
|
||||||
|
// Ok password changed in server an auth is possible.
|
||||||
|
if new_password_hash == password_response {
|
||||||
|
warn!(
|
||||||
|
"Password for {}, changed in server. Updating.",
|
||||||
|
client_identifier
|
||||||
|
);
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut pool_auth_hash = pool.auth_hash.write();
|
||||||
|
*pool_auth_hash = Some(fetched_hash);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
wrong_password(&mut write, username).await?;
|
||||||
|
return Err(Error::ClientGeneralError(
|
||||||
|
"Invalid password".into(),
|
||||||
|
client_identifier,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let transaction_mode = pool.settings.pool_mode == PoolMode::Transaction;
|
let transaction_mode = pool.settings.pool_mode == PoolMode::Transaction;
|
||||||
prepared_statements_enabled =
|
prepared_statements_enabled =
|
||||||
transaction_mode && pool.prepared_statement_cache.is_some();
|
transaction_mode && pool.prepared_statement_cache.is_some();
|
||||||
@@ -859,8 +802,6 @@ where
|
|||||||
// e.g. primary, replica, which shard.
|
// e.g. primary, replica, which shard.
|
||||||
let mut query_router = QueryRouter::new();
|
let mut query_router = QueryRouter::new();
|
||||||
|
|
||||||
let mut checkout_failure_count: u64 = 0;
|
|
||||||
|
|
||||||
self.stats.register(self.stats.clone());
|
self.stats.register(self.stats.clone());
|
||||||
|
|
||||||
// Result returned by one of the plugins.
|
// Result returned by one of the plugins.
|
||||||
@@ -883,7 +824,6 @@ where
|
|||||||
};
|
};
|
||||||
|
|
||||||
query_router.update_pool_settings(&pool.settings);
|
query_router.update_pool_settings(&pool.settings);
|
||||||
query_router.set_default_role();
|
|
||||||
|
|
||||||
// Our custom protocol loop.
|
// Our custom protocol loop.
|
||||||
// We expect the client to either start a transaction with regular queries
|
// We expect the client to either start a transaction with regular queries
|
||||||
@@ -1110,25 +1050,7 @@ where
|
|||||||
query_router.role(),
|
query_router.role(),
|
||||||
err
|
err
|
||||||
);
|
);
|
||||||
checkout_failure_count += 1;
|
|
||||||
if let Some(limit) = pool.settings.checkout_failure_limit {
|
|
||||||
if checkout_failure_count >= limit {
|
|
||||||
error!(
|
|
||||||
"Checkout failure limit reached ({} / {}) - disconnecting client",
|
|
||||||
checkout_failure_count, limit
|
|
||||||
);
|
|
||||||
error_response_terminal(
|
|
||||||
&mut self.write,
|
|
||||||
&format!(
|
|
||||||
"checkout failure limit reached ({} / {})",
|
|
||||||
checkout_failure_count, limit
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
self.stats.disconnect();
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
157
src/config.rs
157
src/config.rs
@@ -38,12 +38,12 @@ pub enum Role {
|
|||||||
Mirror,
|
Mirror,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for Role {
|
impl ToString for Role {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn to_string(&self) -> String {
|
||||||
match self {
|
match *self {
|
||||||
Role::Primary => write!(f, "primary"),
|
Role::Primary => "primary".to_string(),
|
||||||
Role::Replica => write!(f, "replica"),
|
Role::Replica => "replica".to_string(),
|
||||||
Role::Mirror => write!(f, "mirror"),
|
Role::Mirror => "mirror".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -208,9 +208,6 @@ 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,
|
||||||
@@ -228,7 +225,6 @@ 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,
|
||||||
@@ -243,10 +239,6 @@ impl Default for User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl User {
|
impl User {
|
||||||
pub fn default_auth_type() -> AuthType {
|
|
||||||
AuthType::MD5
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate(&self) -> Result<(), Error> {
|
fn validate(&self) -> Result<(), Error> {
|
||||||
if let Some(min_pool_size) = self.min_pool_size {
|
if let Some(min_pool_size) = self.min_pool_size {
|
||||||
if min_pool_size > self.pool_size {
|
if min_pool_size > self.pool_size {
|
||||||
@@ -342,9 +334,6 @@ 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,
|
||||||
|
|
||||||
@@ -359,10 +348,6 @@ 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
|
||||||
}
|
}
|
||||||
@@ -471,7 +456,6 @@ impl Default for General {
|
|||||||
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,
|
validate_config: true,
|
||||||
auth_query: None,
|
auth_query: None,
|
||||||
auth_query_user: None,
|
auth_query_user: None,
|
||||||
@@ -492,20 +476,11 @@ pub enum PoolMode {
|
|||||||
Session,
|
Session,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Copy, Hash)]
|
impl ToString for PoolMode {
|
||||||
pub enum AuthType {
|
fn to_string(&self) -> String {
|
||||||
#[serde(alias = "trust", alias = "Trust")]
|
match *self {
|
||||||
Trust,
|
PoolMode::Transaction => "transaction".to_string(),
|
||||||
|
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"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -518,13 +493,12 @@ 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 {
|
||||||
impl std::fmt::Display for LoadBalancingMode {
|
fn to_string(&self) -> String {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
match *self {
|
||||||
match self {
|
LoadBalancingMode::Random => "random".to_string(),
|
||||||
LoadBalancingMode::Random => write!(f, "random"),
|
|
||||||
LoadBalancingMode::LeastOutstandingConnections => {
|
LoadBalancingMode::LeastOutstandingConnections => {
|
||||||
write!(f, "least_outstanding_connections")
|
"least_outstanding_connections".to_string()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -558,14 +532,6 @@ pub struct Pool {
|
|||||||
/// Close idle connections that have been opened for longer than this.
|
/// Close idle connections that have been opened for longer than this.
|
||||||
pub idle_timeout: Option<u64>,
|
pub idle_timeout: Option<u64>,
|
||||||
|
|
||||||
/// Maximum number of checkout failures a client is allowed before it
|
|
||||||
/// gets disconnected. This is needed to prevent persistent client/server
|
|
||||||
/// imbalance in high availability setups where multiple PgCat instances are placed
|
|
||||||
/// behind a single load balancer. If for any reason a client lands on a PgCat instance that has
|
|
||||||
/// a large number of connected clients, it might get stuck in perpetual checkout failure loop especially
|
|
||||||
/// in session mode
|
|
||||||
pub checkout_failure_limit: Option<u64>,
|
|
||||||
|
|
||||||
/// Close server connections that have been opened for longer than this.
|
/// Close server connections that have been opened for longer than this.
|
||||||
/// Only applied to idle connections. If the connection is actively used for
|
/// Only applied to idle connections. If the connection is actively used for
|
||||||
/// longer than this period, the pool will not interrupt it.
|
/// longer than this period, the pool will not interrupt it.
|
||||||
@@ -597,19 +563,6 @@ pub struct Pool {
|
|||||||
#[serde(default = "Pool::default_prepared_statements_cache_size")]
|
#[serde(default = "Pool::default_prepared_statements_cache_size")]
|
||||||
pub prepared_statements_cache_size: usize,
|
pub prepared_statements_cache_size: usize,
|
||||||
|
|
||||||
// Support for query routing based on database activity
|
|
||||||
#[serde(default = "Pool::default_db_activity_based_routing")]
|
|
||||||
pub db_activity_based_routing: bool,
|
|
||||||
|
|
||||||
#[serde(default = "Pool::default_db_activity_init_delay")]
|
|
||||||
pub db_activity_init_delay: u64,
|
|
||||||
|
|
||||||
#[serde(default = "Pool::default_db_activity_ttl")]
|
|
||||||
pub db_activity_ttl: u64,
|
|
||||||
|
|
||||||
#[serde(default = "Pool::default_table_mutation_cache_ms_ttl")]
|
|
||||||
pub table_mutation_cache_ms_ttl: u64,
|
|
||||||
|
|
||||||
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>,
|
||||||
@@ -663,25 +616,6 @@ impl Pool {
|
|||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn default_db_activity_based_routing() -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn default_db_activity_init_delay() -> u64 {
|
|
||||||
// 100 milliseconds
|
|
||||||
100
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn default_db_activity_ttl() -> u64 {
|
|
||||||
// 15 minutes
|
|
||||||
15 * 60
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn default_table_mutation_cache_ms_ttl() -> u64 {
|
|
||||||
// 50 milliseconds
|
|
||||||
50
|
|
||||||
}
|
|
||||||
|
|
||||||
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" => (),
|
||||||
@@ -764,23 +698,6 @@ impl Pool {
|
|||||||
user.validate()?;
|
user.validate()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.db_activity_based_routing {
|
|
||||||
if self.db_activity_init_delay == 0 {
|
|
||||||
error!("db_activity_init_delay must be greater than 0");
|
|
||||||
return Err(Error::BadConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.table_mutation_cache_ms_ttl == 0 {
|
|
||||||
error!("table_mutation_cache_ms_ttl must be greater than 0");
|
|
||||||
return Err(Error::BadConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.db_activity_ttl == 0 {
|
|
||||||
error!("db_activity_ttl must be greater than 0");
|
|
||||||
return Err(Error::BadConfig);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -790,7 +707,6 @@ 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(),
|
||||||
checkout_failure_limit: None,
|
|
||||||
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_max_length: None,
|
||||||
@@ -811,10 +727,6 @@ impl Default for Pool {
|
|||||||
cleanup_server_connections: true,
|
cleanup_server_connections: true,
|
||||||
log_client_parameter_status_changes: false,
|
log_client_parameter_status_changes: false,
|
||||||
prepared_statements_cache_size: Self::default_prepared_statements_cache_size(),
|
prepared_statements_cache_size: Self::default_prepared_statements_cache_size(),
|
||||||
db_activity_based_routing: Self::default_db_activity_based_routing(),
|
|
||||||
db_activity_init_delay: Self::default_db_activity_init_delay(),
|
|
||||||
db_activity_ttl: Self::default_db_activity_ttl(),
|
|
||||||
table_mutation_cache_ms_ttl: Self::default_table_mutation_cache_ms_ttl(),
|
|
||||||
plugins: None,
|
plugins: None,
|
||||||
shards: BTreeMap::from([(String::from("1"), Shard::default())]),
|
shards: BTreeMap::from([(String::from("1"), Shard::default())]),
|
||||||
users: BTreeMap::default(),
|
users: BTreeMap::default(),
|
||||||
@@ -1087,17 +999,15 @@ 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.clone_from(&self.general.auth_query);
|
pool.auth_query = self.general.auth_query.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
if pool.auth_query_user.is_none() {
|
if pool.auth_query_user.is_none() {
|
||||||
pool.auth_query_user
|
pool.auth_query_user = self.general.auth_query_user.clone();
|
||||||
.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
|
pool.auth_query_password = self.general.auth_query_password.clone();
|
||||||
.clone_from(&self.general.auth_query_password);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1245,7 +1155,7 @@ impl Config {
|
|||||||
"Default max server lifetime: {}ms",
|
"Default max server lifetime: {}ms",
|
||||||
self.general.server_lifetime
|
self.general.server_lifetime
|
||||||
);
|
);
|
||||||
info!("Server round robin: {}", self.general.server_round_robin);
|
info!("Sever 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);
|
||||||
@@ -1307,17 +1217,6 @@ impl Config {
|
|||||||
None => self.general.idle_timeout,
|
None => self.general.idle_timeout,
|
||||||
};
|
};
|
||||||
info!("[pool: {}] Idle timeout: {}ms", pool_name, idle_timeout);
|
info!("[pool: {}] Idle timeout: {}ms", pool_name, idle_timeout);
|
||||||
match pool_config.checkout_failure_limit {
|
|
||||||
Some(checkout_failure_limit) => {
|
|
||||||
info!(
|
|
||||||
"[pool: {}] Checkout failure limit: {}",
|
|
||||||
pool_name, checkout_failure_limit
|
|
||||||
);
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
info!("[pool: {}] Checkout failure limit: not set", pool_name);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
info!(
|
info!(
|
||||||
"[pool: {}] Sharding function: {}",
|
"[pool: {}] Sharding function: {}",
|
||||||
pool_name,
|
pool_name,
|
||||||
@@ -1362,22 +1261,6 @@ 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: {}] DB activity based routing: {}",
|
|
||||||
pool_name, pool_config.db_activity_based_routing
|
|
||||||
);
|
|
||||||
info!(
|
|
||||||
"[pool: {}] DB activity init delay: {}",
|
|
||||||
pool_name, pool_config.db_activity_init_delay
|
|
||||||
);
|
|
||||||
info!(
|
|
||||||
"[pool: {}] DB activity TTL: {}",
|
|
||||||
pool_name, pool_config.db_activity_ttl
|
|
||||||
);
|
|
||||||
info!(
|
|
||||||
"[pool: {}] Table mutation cache TTL: {}",
|
|
||||||
pool_name, pool_config.table_mutation_cache_ms_ttl
|
|
||||||
);
|
|
||||||
info!(
|
info!(
|
||||||
"[pool: {}] Log client parameter status changes: {}",
|
"[pool: {}] Log client parameter status changes: {}",
|
||||||
pool_name, pool_config.log_client_parameter_status_changes
|
pool_name, pool_config.log_client_parameter_status_changes
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use tracing_subscriber;
|
|||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
pub fn init(args: &Args) {
|
pub fn init(args: &Args) {
|
||||||
// Initialize a default filter, and then override the builtin default "warning" with our
|
// Iniitalize a default filter, and then override the builtin default "warning" with our
|
||||||
// commandline, (default: "info")
|
// commandline, (default: "info")
|
||||||
let filter = EnvFilter::from_default_env().add_directive(args.log_level.into());
|
let filter = EnvFilter::from_default_env().add_directive(args.log_level.into());
|
||||||
|
|
||||||
|
|||||||
@@ -733,10 +733,6 @@ 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 {
|
||||||
|
|||||||
32
src/pool.rs
32
src/pool.rs
@@ -152,14 +152,6 @@ pub struct PoolSettings {
|
|||||||
/// Random or LeastOutstandingConnections.
|
/// Random or LeastOutstandingConnections.
|
||||||
pub load_balancing_mode: LoadBalancingMode,
|
pub load_balancing_mode: LoadBalancingMode,
|
||||||
|
|
||||||
/// Maximum number of checkout failures a client is allowed before it
|
|
||||||
/// gets disconnected. This is needed to prevent persistent client/server
|
|
||||||
/// imbalance in high availability setups where multiple PgCat instances are placed
|
|
||||||
/// behind a single load balancer. If for any reason a client lands on a PgCat instance that has
|
|
||||||
/// a large number of connected clients, it might get stuck in perpetual checkout failure loop especially
|
|
||||||
/// in session mode
|
|
||||||
pub checkout_failure_limit: Option<u64>,
|
|
||||||
|
|
||||||
// Number of shards.
|
// Number of shards.
|
||||||
pub shards: usize,
|
pub shards: usize,
|
||||||
|
|
||||||
@@ -182,18 +174,6 @@ pub struct PoolSettings {
|
|||||||
// 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,
|
||||||
|
|
||||||
// Automatic primary/replica selection based on recent activity.
|
|
||||||
pub db_activity_based_routing: bool,
|
|
||||||
|
|
||||||
// DB activity init delay
|
|
||||||
pub db_activity_init_delay: u64,
|
|
||||||
|
|
||||||
// DB activity TTL
|
|
||||||
pub db_activity_ttl: u64,
|
|
||||||
|
|
||||||
// Table mutation cache TTL
|
|
||||||
pub table_mutation_cache_ms_ttl: u64,
|
|
||||||
|
|
||||||
// Sharding function.
|
// Sharding function.
|
||||||
pub sharding_function: ShardingFunction,
|
pub sharding_function: ShardingFunction,
|
||||||
|
|
||||||
@@ -235,7 +215,6 @@ impl Default for PoolSettings {
|
|||||||
PoolSettings {
|
PoolSettings {
|
||||||
pool_mode: PoolMode::Transaction,
|
pool_mode: PoolMode::Transaction,
|
||||||
load_balancing_mode: LoadBalancingMode::Random,
|
load_balancing_mode: LoadBalancingMode::Random,
|
||||||
checkout_failure_limit: None,
|
|
||||||
shards: 1,
|
shards: 1,
|
||||||
user: User::default(),
|
user: User::default(),
|
||||||
db: String::default(),
|
db: String::default(),
|
||||||
@@ -244,10 +223,6 @@ impl Default for PoolSettings {
|
|||||||
query_parser_max_length: None,
|
query_parser_max_length: None,
|
||||||
query_parser_read_write_splitting: false,
|
query_parser_read_write_splitting: false,
|
||||||
primary_reads_enabled: true,
|
primary_reads_enabled: true,
|
||||||
db_activity_based_routing: false,
|
|
||||||
db_activity_init_delay: 100,
|
|
||||||
db_activity_ttl: 15 * 60,
|
|
||||||
table_mutation_cache_ms_ttl: 50,
|
|
||||||
sharding_function: ShardingFunction::PgBigintHash,
|
sharding_function: ShardingFunction::PgBigintHash,
|
||||||
automatic_sharding_key: None,
|
automatic_sharding_key: None,
|
||||||
healthcheck_delay: General::default_healthcheck_delay(),
|
healthcheck_delay: General::default_healthcheck_delay(),
|
||||||
@@ -546,7 +521,6 @@ impl ConnectionPool {
|
|||||||
None => pool_config.pool_mode,
|
None => pool_config.pool_mode,
|
||||||
},
|
},
|
||||||
load_balancing_mode: pool_config.load_balancing_mode,
|
load_balancing_mode: pool_config.load_balancing_mode,
|
||||||
checkout_failure_limit: pool_config.checkout_failure_limit,
|
|
||||||
// shards: pool_config.shards.clone(),
|
// shards: pool_config.shards.clone(),
|
||||||
shards: shard_ids.len(),
|
shards: shard_ids.len(),
|
||||||
user: user.clone(),
|
user: user.clone(),
|
||||||
@@ -563,10 +537,6 @@ impl ConnectionPool {
|
|||||||
.query_parser_read_write_splitting,
|
.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,
|
||||||
db_activity_based_routing: pool_config.db_activity_based_routing,
|
|
||||||
db_activity_init_delay: pool_config.db_activity_init_delay,
|
|
||||||
db_activity_ttl: pool_config.db_activity_ttl,
|
|
||||||
table_mutation_cache_ms_ttl: pool_config.table_mutation_cache_ms_ttl,
|
|
||||||
automatic_sharding_key: pool_config.automatic_sharding_key.clone(),
|
automatic_sharding_key: pool_config.automatic_sharding_key.clone(),
|
||||||
healthcheck_delay: config.general.healthcheck_delay,
|
healthcheck_delay: config.general.healthcheck_delay,
|
||||||
healthcheck_timeout: config.general.healthcheck_timeout,
|
healthcheck_timeout: config.general.healthcheck_timeout,
|
||||||
@@ -843,7 +813,7 @@ impl ConnectionPool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
client_stats.checkout_error();
|
client_stats.checkout_success();
|
||||||
|
|
||||||
Err(Error::AllServersDown)
|
Err(Error::AllServersDown)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,23 @@
|
|||||||
use http_body_util::Full;
|
use hyper::service::{make_service_fn, service_fn};
|
||||||
use hyper::body;
|
use hyper::{Body, Method, Request, Response, Server, StatusCode};
|
||||||
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 tokio::net::TcpListener;
|
use std::sync::Arc;
|
||||||
|
|
||||||
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
|
||||||
@@ -138,46 +120,22 @@ 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: "counter",
|
ty: "gauge",
|
||||||
},
|
},
|
||||||
"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: "counter",
|
ty: "gauge",
|
||||||
},
|
},
|
||||||
"servers_transaction_count" => MetricHelpType {
|
"servers_transaction_count" => MetricHelpType {
|
||||||
help: "Number of transactions executed by server",
|
help: "Number of transactions executed by server",
|
||||||
ty: "counter",
|
ty: "gauge",
|
||||||
},
|
},
|
||||||
"servers_query_count" => MetricHelpType {
|
"servers_query_count" => MetricHelpType {
|
||||||
help: "Number of queries executed by server",
|
help: "Number of queries executed by server",
|
||||||
ty: "counter",
|
ty: "gauge",
|
||||||
},
|
},
|
||||||
"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 {
|
||||||
@@ -200,17 +158,18 @@ 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 mut sorted_labels: Vec<_> = self.labels.iter().collect();
|
let formatted_labels = self
|
||||||
sorted_labels.sort_by_key(|&(key, _)| key);
|
.labels
|
||||||
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,
|
||||||
"{name}{{{formatted_labels}}} {value}",
|
"# HELP {name} {help}\n# TYPE {name} {ty}\n{name}{{{formatted_labels}}} {value}\n",
|
||||||
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
|
||||||
)
|
)
|
||||||
@@ -244,9 +203,7 @@ 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)
|
||||||
}
|
}
|
||||||
@@ -261,9 +218,7 @@ 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)
|
||||||
}
|
}
|
||||||
@@ -274,9 +229,7 @@ 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)
|
||||||
}
|
}
|
||||||
@@ -288,20 +241,9 @@ 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(
|
async fn prometheus_stats(request: Request<Body>) -> Result<Response<Body>, hyper::http::Error> {
|
||||||
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();
|
||||||
@@ -309,7 +251,6 @@ async fn prometheus_stats(
|
|||||||
push_pool_stats(&mut lines);
|
push_pool_stats(&mut lines);
|
||||||
push_server_stats(&mut lines);
|
push_server_stats(&mut lines);
|
||||||
push_database_stats(&mut lines);
|
push_database_stats(&mut lines);
|
||||||
lines.push("".to_string()); // Ensure to end the stats with a line terminator as required by the specification.
|
|
||||||
|
|
||||||
Response::builder()
|
Response::builder()
|
||||||
.header("content-type", "text/plain; version=0.0.4")
|
.header("content-type", "text/plain; version=0.0.4")
|
||||||
@@ -323,7 +264,6 @@ async fn prometheus_stats(
|
|||||||
|
|
||||||
// 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) {
|
||||||
@@ -333,10 +273,7 @@ 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)
|
||||||
{
|
{
|
||||||
grouped_metrics
|
lines.push(prometheus_metric.to_string());
|
||||||
.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());
|
||||||
}
|
}
|
||||||
@@ -344,53 +281,33 @@ 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)
|
||||||
{
|
{
|
||||||
grouped_metrics
|
lines.push(prometheus_metric.to_string());
|
||||||
.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),
|
||||||
@@ -399,10 +316,7 @@ 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)
|
||||||
{
|
{
|
||||||
grouped_metrics
|
lines.push(prometheus_metric.to_string());
|
||||||
.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());
|
||||||
}
|
}
|
||||||
@@ -410,73 +324,45 @@ 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 prom_stats = HashMap::<String, ServerPrometheusStats>::new();
|
let mut server_stats_by_addresses = HashMap::<String, Arc<ServerStats>>::new();
|
||||||
for (_, stats) in server_stats {
|
for (_, stats) in server_stats {
|
||||||
let entry = prom_stats
|
server_stats_by_addresses.insert(stats.address_name(), 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) = prom_stats.get(&address.name()) {
|
if let Some(server_info) = server_stats_by_addresses.get(&address.name()) {
|
||||||
let metrics = [
|
let metrics = [
|
||||||
("bytes_received", server_info.bytes_received),
|
(
|
||||||
("bytes_sent", server_info.bytes_sent),
|
"bytes_received",
|
||||||
("transaction_count", server_info.transaction_count),
|
server_info.bytes_received.load(Ordering::Relaxed),
|
||||||
("query_count", server_info.query_count),
|
),
|
||||||
("error_count", server_info.error_count),
|
("bytes_sent", server_info.bytes_sent.load(Ordering::Relaxed)),
|
||||||
("idle_count", server_info.idle_count),
|
(
|
||||||
("active_count", server_info.active_count),
|
"transaction_count",
|
||||||
("login_count", server_info.login_count),
|
server_info.transaction_count.load(Ordering::Relaxed),
|
||||||
("tested_count", server_info.tested_count),
|
),
|
||||||
("is_banned", if pool.is_banned(address) { 1 } else { 0 }),
|
(
|
||||||
("is_paused", if pool.paused() { 1 } else { 0 }),
|
"query_count",
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
grouped_metrics
|
lines.push(prometheus_metric.to_string());
|
||||||
.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());
|
||||||
}
|
}
|
||||||
@@ -485,46 +371,17 @@ 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 listener = TcpListener::bind(http_addr);
|
let http_service_factory =
|
||||||
let listener = match listener.await {
|
make_service_fn(|_conn| async { Ok::<_, hyper::Error>(service_fn(prometheus_stats)) });
|
||||||
Ok(listener) => listener,
|
let server = Server::bind(&http_addr).serve(http_service_factory);
|
||||||
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
|
||||||
);
|
);
|
||||||
loop {
|
if let Err(e) = server.await {
|
||||||
let stream = match listener.accept().await {
|
error!("Failed to run HTTP server: {}.", e);
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
/// or implied query characteristics.
|
/// or implied query characteristics.
|
||||||
use bytes::{Buf, BytesMut};
|
use bytes::{Buf, BytesMut};
|
||||||
use log::{debug, error};
|
use log::{debug, error};
|
||||||
use mini_moka::sync::Cache;
|
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
use regex::{Regex, RegexSet};
|
use regex::{Regex, RegexSet};
|
||||||
use sqlparser::ast::Statement::{Delete, Insert, Query, StartTransaction, Update};
|
use sqlparser::ast::Statement::{Delete, Insert, Query, StartTransaction, Update};
|
||||||
@@ -12,7 +11,6 @@ use sqlparser::ast::{
|
|||||||
};
|
};
|
||||||
use sqlparser::dialect::PostgreSqlDialect;
|
use sqlparser::dialect::PostgreSqlDialect;
|
||||||
use sqlparser::parser::Parser;
|
use sqlparser::parser::Parser;
|
||||||
use std::sync::OnceLock;
|
|
||||||
|
|
||||||
use crate::config::Role;
|
use crate::config::Role;
|
||||||
use crate::errors::Error;
|
use crate::errors::Error;
|
||||||
@@ -23,7 +21,6 @@ use crate::sharding::Sharder;
|
|||||||
|
|
||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
use std::time::Duration;
|
|
||||||
use std::{cmp, mem};
|
use std::{cmp, mem};
|
||||||
|
|
||||||
/// Regexes used to parse custom commands.
|
/// Regexes used to parse custom commands.
|
||||||
@@ -69,18 +66,6 @@ static CUSTOM_SQL_REGEX_SET: OnceCell<RegexSet> = OnceCell::new();
|
|||||||
// Get the value inside the custom command.
|
// Get the value inside the custom command.
|
||||||
static CUSTOM_SQL_REGEX_LIST: OnceCell<Vec<Regex>> = OnceCell::new();
|
static CUSTOM_SQL_REGEX_LIST: OnceCell<Vec<Regex>> = OnceCell::new();
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
enum DatabaseActivityState {
|
|
||||||
Active,
|
|
||||||
Initializing,
|
|
||||||
}
|
|
||||||
|
|
||||||
// A moka cache for the databases
|
|
||||||
// the key is the database name and the value is the database activity state
|
|
||||||
static DATABASE_ACTIVITY_CACHE: OnceLock<Cache<String, DatabaseActivityState>> = OnceLock::new();
|
|
||||||
// A moka cache for the tables, the key is the db_table.
|
|
||||||
static TABLE_MUTATIONS_CACHE: OnceLock<Cache<String, bool>> = OnceLock::new();
|
|
||||||
|
|
||||||
/// The query router.
|
/// The query router.
|
||||||
pub struct QueryRouter {
|
pub struct QueryRouter {
|
||||||
/// Which shard we should be talking to right now.
|
/// Which shard we should be talking to right now.
|
||||||
@@ -102,12 +87,6 @@ pub struct QueryRouter {
|
|||||||
placeholders: Vec<i16>,
|
placeholders: Vec<i16>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ExtractedExprsAndTables<'a> {
|
|
||||||
exprs: Vec<Expr>,
|
|
||||||
table_names: Vec<Vec<Ident>>,
|
|
||||||
assignments_opt: Option<&'a Vec<Assignment>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl QueryRouter {
|
impl QueryRouter {
|
||||||
/// One-time initialization of regexes
|
/// One-time initialization of regexes
|
||||||
/// that parse our custom SQL protocol.
|
/// that parse our custom SQL protocol.
|
||||||
@@ -407,53 +386,6 @@ impl QueryRouter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Determines if a query is a mutation or not.
|
|
||||||
fn is_mutation_query(q: &sqlparser::ast::Query) -> bool {
|
|
||||||
use sqlparser::ast::*;
|
|
||||||
|
|
||||||
match q.body.as_ref() {
|
|
||||||
SetExpr::Insert(_) => true,
|
|
||||||
SetExpr::Update(_) => true,
|
|
||||||
SetExpr::Query(q) => Self::is_mutation_query(q),
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn database_activity_cache(&self) -> Cache<String, DatabaseActivityState> {
|
|
||||||
DATABASE_ACTIVITY_CACHE
|
|
||||||
.get_or_init(|| {
|
|
||||||
Cache::builder()
|
|
||||||
.time_to_idle(Duration::from_secs(self.pool_settings.db_activity_ttl))
|
|
||||||
.build()
|
|
||||||
})
|
|
||||||
.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check database activity state and reset it if necessary
|
|
||||||
fn database_activity_state(&self, db: &String) -> DatabaseActivityState {
|
|
||||||
let cache = self.database_activity_cache();
|
|
||||||
|
|
||||||
// Exists in cache
|
|
||||||
if cache.contains_key(db) {
|
|
||||||
return cache.get(db).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Not in cache
|
|
||||||
debug!("Adding database to cache: {}", db);
|
|
||||||
|
|
||||||
cache.insert(db.to_string(), DatabaseActivityState::Initializing);
|
|
||||||
|
|
||||||
// Set a timer to update the cache
|
|
||||||
let db = db.clone();
|
|
||||||
let db_activity_init_delay = self.pool_settings.db_activity_init_delay;
|
|
||||||
tokio::spawn(async move {
|
|
||||||
tokio::time::sleep(Duration::from_millis(db_activity_init_delay)).await;
|
|
||||||
cache.insert(db, DatabaseActivityState::Active);
|
|
||||||
});
|
|
||||||
|
|
||||||
DatabaseActivityState::Initializing
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Try to infer which server to connect to based on the contents of the query.
|
/// Try to infer which server to connect to based on the contents of the query.
|
||||||
pub fn infer(&mut self, ast: &Vec<sqlparser::ast::Statement>) -> Result<(), Error> {
|
pub fn infer(&mut self, ast: &Vec<sqlparser::ast::Statement>) -> Result<(), Error> {
|
||||||
if !self.pool_settings.query_parser_read_write_splitting {
|
if !self.pool_settings.query_parser_read_write_splitting {
|
||||||
@@ -468,23 +400,9 @@ impl QueryRouter {
|
|||||||
return Err(Error::QueryRouterParserError("empty query".into()));
|
return Err(Error::QueryRouterParserError("empty query".into()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut primary_set_based_on_activity = false;
|
|
||||||
let mut visited_write_statement = false;
|
let mut visited_write_statement = false;
|
||||||
let mut prev_inferred_shard = None;
|
let mut prev_inferred_shard = None;
|
||||||
|
|
||||||
if self.pool_settings.db_activity_based_routing {
|
|
||||||
let db = self.pool_settings.db.clone();
|
|
||||||
let state = self.database_activity_state(&db);
|
|
||||||
debug!("Database activity state: {:?}", state);
|
|
||||||
|
|
||||||
if let DatabaseActivityState::Initializing = state {
|
|
||||||
debug!("Database is initializing, going to primary");
|
|
||||||
|
|
||||||
self.active_role = Some(Role::Primary);
|
|
||||||
primary_set_based_on_activity = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for q in ast {
|
for q in ast {
|
||||||
match q {
|
match q {
|
||||||
// All transactions go to the primary, probably a write.
|
// All transactions go to the primary, probably a write.
|
||||||
@@ -495,22 +413,6 @@ impl QueryRouter {
|
|||||||
|
|
||||||
// Likely a read-only query
|
// Likely a read-only query
|
||||||
Query(query) => {
|
Query(query) => {
|
||||||
if primary_set_based_on_activity {
|
|
||||||
// If we already set the role based on activity, we don't need to do it again
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.pool_settings.db_activity_based_routing {
|
|
||||||
// Check if the tables in the query have been written to recently
|
|
||||||
if self.query_handles_tables_in_mutation_cache(query) {
|
|
||||||
debug!("Query handles tables in mutation cache, going to primary");
|
|
||||||
|
|
||||||
self.active_role = Some(Role::Primary);
|
|
||||||
primary_set_based_on_activity = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match &self.pool_settings.automatic_sharding_key {
|
match &self.pool_settings.automatic_sharding_key {
|
||||||
Some(_) => {
|
Some(_) => {
|
||||||
// TODO: if we have multiple queries in the same message,
|
// TODO: if we have multiple queries in the same message,
|
||||||
@@ -525,13 +427,8 @@ impl QueryRouter {
|
|||||||
None => (),
|
None => (),
|
||||||
};
|
};
|
||||||
|
|
||||||
let has_locks = !query.locks.is_empty();
|
// If we already visited a write statement, we should be going to the primary.
|
||||||
let has_mutation = Self::is_mutation_query(query);
|
if !visited_write_statement {
|
||||||
|
|
||||||
if has_locks || has_mutation {
|
|
||||||
self.active_role = Some(Role::Primary);
|
|
||||||
} else if !visited_write_statement {
|
|
||||||
// If we already visited a write statement, we should be going to the primary.
|
|
||||||
self.active_role = match self.primary_reads_enabled() {
|
self.active_role = match self.primary_reads_enabled() {
|
||||||
false => Some(Role::Replica), // If primary should not be receiving reads, use a replica.
|
false => Some(Role::Replica), // If primary should not be receiving reads, use a replica.
|
||||||
true => None, // Any server role is fine in this case.
|
true => None, // Any server role is fine in this case.
|
||||||
@@ -541,13 +438,6 @@ impl QueryRouter {
|
|||||||
|
|
||||||
// Likely a write
|
// Likely a write
|
||||||
_ => {
|
_ => {
|
||||||
debug!("Write statement found, going to primary");
|
|
||||||
|
|
||||||
if self.pool_settings.db_activity_based_routing {
|
|
||||||
// add all of the query tables to the mutation cache
|
|
||||||
self.update_mutation_cache_on_write(q);
|
|
||||||
}
|
|
||||||
|
|
||||||
match &self.pool_settings.automatic_sharding_key {
|
match &self.pool_settings.automatic_sharding_key {
|
||||||
Some(_) => {
|
Some(_) => {
|
||||||
// TODO: similar to the above, if we have multiple queries in the
|
// TODO: similar to the above, if we have multiple queries in the
|
||||||
@@ -590,69 +480,57 @@ impl QueryRouter {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn table_mutations_cache(&self) -> Cache<String, bool> {
|
fn infer_shard_on_write(&mut self, q: &Statement) -> Result<Option<usize>, Error> {
|
||||||
TABLE_MUTATIONS_CACHE
|
|
||||||
.get_or_init(|| {
|
|
||||||
Cache::builder()
|
|
||||||
.time_to_live(Duration::from_millis(
|
|
||||||
self.pool_settings.table_mutation_cache_ms_ttl,
|
|
||||||
))
|
|
||||||
.build()
|
|
||||||
})
|
|
||||||
.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn query_handles_tables_in_mutation_cache(&self, query: &sqlparser::ast::Query) -> bool {
|
|
||||||
let table_mutations_cache = self.table_mutations_cache();
|
|
||||||
debug!("Checking if query handles tables in mutation cache");
|
|
||||||
debug!("Table mutations cache: {:?}", table_mutations_cache);
|
|
||||||
|
|
||||||
for tables in self.table_names(query) {
|
|
||||||
for table in tables {
|
|
||||||
if table_mutations_cache.contains_key(&self.table_mutation_cache_key(table)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
false
|
|
||||||
}
|
|
||||||
fn extract_exprs_and_table_names<'a>(
|
|
||||||
&'a self,
|
|
||||||
q: &'a Statement,
|
|
||||||
) -> Option<ExtractedExprsAndTables<'a>> {
|
|
||||||
let mut exprs = Vec::new();
|
let mut exprs = Vec::new();
|
||||||
|
|
||||||
|
// Collect all table names from the query.
|
||||||
let mut table_names = Vec::new();
|
let mut table_names = Vec::new();
|
||||||
let mut assignments_opt = None;
|
|
||||||
|
|
||||||
match q {
|
match q {
|
||||||
Insert(i) => {
|
Insert {
|
||||||
|
or,
|
||||||
|
into: _,
|
||||||
|
table_name,
|
||||||
|
columns,
|
||||||
|
overwrite: _,
|
||||||
|
source,
|
||||||
|
partitioned,
|
||||||
|
after_columns,
|
||||||
|
table: _,
|
||||||
|
on: _,
|
||||||
|
returning: _,
|
||||||
|
} => {
|
||||||
// Not supported in postgres.
|
// Not supported in postgres.
|
||||||
assert!(i.or.is_none());
|
assert!(or.is_none());
|
||||||
assert!(i.partitioned.is_none());
|
assert!(partitioned.is_none());
|
||||||
assert!(i.after_columns.is_empty());
|
assert!(after_columns.is_empty());
|
||||||
|
|
||||||
Self::process_table(&i.table_name, &mut table_names);
|
Self::process_table(table_name, &mut table_names);
|
||||||
if let Some(source) = &i.source {
|
Self::process_query(source, &mut exprs, &mut table_names, &Some(columns));
|
||||||
Self::process_query(source, &mut exprs, &mut table_names, &Some(&i.columns));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Delete(d) => {
|
Delete {
|
||||||
if let Some(expr) = &d.selection {
|
tables,
|
||||||
|
from,
|
||||||
|
using,
|
||||||
|
selection,
|
||||||
|
returning: _,
|
||||||
|
} => {
|
||||||
|
if let Some(expr) = selection {
|
||||||
exprs.push(expr.clone());
|
exprs.push(expr.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Multi-tables delete are not supported in postgres.
|
// Multi tables delete are not supported in postgres.
|
||||||
assert!(d.tables.is_empty());
|
assert!(tables.is_empty());
|
||||||
|
|
||||||
if let Some(using_tbl_with_join) = &d.using {
|
Self::process_tables_with_join(from, &mut exprs, &mut table_names);
|
||||||
|
if let Some(using_tbl_with_join) = using {
|
||||||
Self::process_tables_with_join(
|
Self::process_tables_with_join(
|
||||||
using_tbl_with_join,
|
using_tbl_with_join,
|
||||||
&mut exprs,
|
&mut exprs,
|
||||||
&mut table_names,
|
&mut table_names,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Self::process_selection(&d.selection, &mut exprs);
|
Self::process_selection(selection, &mut exprs);
|
||||||
}
|
}
|
||||||
Update {
|
Update {
|
||||||
table,
|
table,
|
||||||
@@ -666,55 +544,14 @@ impl QueryRouter {
|
|||||||
Self::process_table_with_join(from_tbl, &mut exprs, &mut table_names);
|
Self::process_table_with_join(from_tbl, &mut exprs, &mut table_names);
|
||||||
}
|
}
|
||||||
Self::process_selection(selection, &mut exprs);
|
Self::process_selection(selection, &mut exprs);
|
||||||
|
|
||||||
assignments_opt = Some(assignments);
|
|
||||||
}
|
|
||||||
_ => return None,
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(ExtractedExprsAndTables {
|
|
||||||
exprs,
|
|
||||||
table_names,
|
|
||||||
assignments_opt,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn infer_shard_on_write(&mut self, q: &Statement) -> Result<Option<usize>, Error> {
|
|
||||||
if let Some(extracted) = self.extract_exprs_and_table_names(q) {
|
|
||||||
let exprs = extracted.exprs;
|
|
||||||
let table_names = extracted.table_names;
|
|
||||||
let assignments_opt = extracted.assignments_opt;
|
|
||||||
|
|
||||||
if let Some(assignments) = assignments_opt {
|
|
||||||
self.assignment_parser(assignments)?;
|
self.assignment_parser(assignments)?;
|
||||||
}
|
}
|
||||||
|
_ => {
|
||||||
Ok(self.infer_shard_from_exprs(exprs, table_names))
|
return Ok(None);
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_mutation_cache_on_write(&self, q: &Statement) {
|
|
||||||
if let Some(extracted) = self.extract_exprs_and_table_names(q) {
|
|
||||||
debug!("Updating mutation cache on write");
|
|
||||||
|
|
||||||
let table_names = extracted.table_names;
|
|
||||||
debug!("Table names in mutation query: {:?}", table_names);
|
|
||||||
let table_mutations_cache = self.table_mutations_cache();
|
|
||||||
for tables in table_names {
|
|
||||||
for table in tables {
|
|
||||||
table_mutations_cache.insert(self.table_mutation_cache_key(table), true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
// combines the database name and table name into a single string
|
Ok(self.infer_shard_from_exprs(exprs, table_names))
|
||||||
// to be used as the key in the table mutation cache
|
|
||||||
// e.g. "mydb.mytable"
|
|
||||||
fn table_mutation_cache_key(&self, table: Ident) -> String {
|
|
||||||
format!("{}.{}", self.pool_settings.db, table.value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process_query(
|
fn process_query(
|
||||||
@@ -963,13 +800,7 @@ impl QueryRouter {
|
|||||||
|
|
||||||
for a in assignments {
|
for a in assignments {
|
||||||
if sharding_key[0].value == "*"
|
if sharding_key[0].value == "*"
|
||||||
&& sharding_key[1].value
|
&& sharding_key[1].value == a.id.last().unwrap().value.to_lowercase()
|
||||||
== a.target
|
|
||||||
.to_string()
|
|
||||||
.split('.')
|
|
||||||
.last()
|
|
||||||
.unwrap()
|
|
||||||
.to_lowercase()
|
|
||||||
{
|
{
|
||||||
return Err(Error::QueryRouterParserError(
|
return Err(Error::QueryRouterParserError(
|
||||||
"Sharding key cannot be updated.".into(),
|
"Sharding key cannot be updated.".into(),
|
||||||
@@ -1102,18 +933,6 @@ impl QueryRouter {
|
|||||||
self.infer_shard_from_exprs(exprs, table_names)
|
self.infer_shard_from_exprs(exprs, table_names)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// get table names from query
|
|
||||||
fn table_names(&self, query: &sqlparser::ast::Query) -> Vec<Vec<Ident>> {
|
|
||||||
let mut exprs = Vec::new();
|
|
||||||
|
|
||||||
let mut table_names = Vec::new();
|
|
||||||
Self::process_query(query, &mut exprs, &mut table_names, &None);
|
|
||||||
|
|
||||||
debug!("Table names in query: {:?}", table_names);
|
|
||||||
|
|
||||||
table_names
|
|
||||||
}
|
|
||||||
|
|
||||||
fn infer_shard_from_exprs(
|
fn infer_shard_from_exprs(
|
||||||
&mut self,
|
&mut self,
|
||||||
exprs: Vec<Expr>,
|
exprs: Vec<Expr>,
|
||||||
@@ -1220,11 +1039,6 @@ impl QueryRouter {
|
|||||||
self.active_shard
|
self.active_shard
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set active_role as the default_role specified in the pool.
|
|
||||||
pub fn set_default_role(&mut self) {
|
|
||||||
self.active_role = self.pool_settings.default_role;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the current desired server role we should be talking to.
|
/// Get the current desired server role we should be talking to.
|
||||||
pub fn role(&self) -> Option<Role> {
|
pub fn role(&self) -> Option<Role> {
|
||||||
self.active_role
|
self.active_role
|
||||||
@@ -1281,7 +1095,6 @@ mod test {
|
|||||||
use crate::messages::simple_query;
|
use crate::messages::simple_query;
|
||||||
use crate::sharding::ShardingFunction;
|
use crate::sharding::ShardingFunction;
|
||||||
use bytes::BufMut;
|
use bytes::BufMut;
|
||||||
use serial_test::serial;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_defaults() {
|
fn test_defaults() {
|
||||||
@@ -1291,26 +1104,6 @@ mod test {
|
|||||||
assert_eq!(qr.role(), None);
|
assert_eq!(qr.role(), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_split_cte_queries() {
|
|
||||||
QueryRouter::setup();
|
|
||||||
let mut qr = QueryRouter::new();
|
|
||||||
qr.pool_settings.query_parser_read_write_splitting = true;
|
|
||||||
qr.pool_settings.query_parser_enabled = true;
|
|
||||||
|
|
||||||
let query = simple_query(
|
|
||||||
"WITH t AS (
|
|
||||||
SELECT id FROM users WHERE name ILIKE '%ja%'
|
|
||||||
)
|
|
||||||
UPDATE user_languages
|
|
||||||
SET settings = '{}'
|
|
||||||
FROM t WHERE t.id = user_id;",
|
|
||||||
);
|
|
||||||
let ast = qr.parse(&query).unwrap();
|
|
||||||
assert!(qr.infer(&ast).is_ok());
|
|
||||||
assert_eq!(qr.role(), Some(Role::Primary));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_infer_replica() {
|
fn test_infer_replica() {
|
||||||
QueryRouter::setup();
|
QueryRouter::setup();
|
||||||
@@ -1360,29 +1153,6 @@ mod test {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_select_for_update() {
|
|
||||||
QueryRouter::setup();
|
|
||||||
let mut qr = QueryRouter::new();
|
|
||||||
qr.pool_settings.query_parser_read_write_splitting = true;
|
|
||||||
|
|
||||||
let queries_in_primary_role = vec![
|
|
||||||
simple_query("BEGIN"), // Transaction start
|
|
||||||
simple_query("SELECT * FROM items WHERE id = 5 FOR UPDATE"),
|
|
||||||
simple_query("UPDATE items SET name = 'pumpkin' WHERE id = 5"),
|
|
||||||
];
|
|
||||||
|
|
||||||
for query in queries_in_primary_role {
|
|
||||||
assert!(qr.infer(&qr.parse(&query).unwrap()).is_ok());
|
|
||||||
assert_eq!(qr.role(), Some(Role::Primary));
|
|
||||||
}
|
|
||||||
|
|
||||||
// query without lock do not change role
|
|
||||||
let query = simple_query("SELECT * FROM items WHERE id = 5");
|
|
||||||
assert!(qr.infer(&qr.parse(&query).unwrap()).is_ok());
|
|
||||||
assert_eq!(qr.role(), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_infer_primary_reads_enabled() {
|
fn test_infer_primary_reads_enabled() {
|
||||||
QueryRouter::setup();
|
QueryRouter::setup();
|
||||||
@@ -1597,19 +1367,6 @@ mod test {
|
|||||||
assert!(!qr.query_parser_enabled());
|
assert!(!qr.query_parser_enabled());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_query_parser() {
|
|
||||||
QueryRouter::setup();
|
|
||||||
let mut qr = QueryRouter::new();
|
|
||||||
qr.pool_settings.query_parser_read_write_splitting = true;
|
|
||||||
|
|
||||||
let query = simple_query("SELECT req_tab_0.* FROM validation req_tab_0 WHERE array['http://www.w3.org/ns/shacl#ValidationResult'] && req_tab_0.type::text[] AND ( ( (req_tab_0.focusnode = 'DataSource_Credilogic_DataSourceAddress_144959227') ) )");
|
|
||||||
assert!(qr.infer(&qr.parse(&query).unwrap()).is_ok());
|
|
||||||
|
|
||||||
let query = simple_query("WITH EmployeeSalaries AS (SELECT Department, Salary FROM Employees) SELECT Department, AVG(Salary) AS AverageSalary FROM EmployeeSalaries GROUP BY Department;");
|
|
||||||
assert!(qr.infer(&qr.parse(&query).unwrap()).is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_update_from_pool_settings() {
|
fn test_update_from_pool_settings() {
|
||||||
QueryRouter::setup();
|
QueryRouter::setup();
|
||||||
@@ -1617,7 +1374,6 @@ mod test {
|
|||||||
let pool_settings = PoolSettings {
|
let pool_settings = PoolSettings {
|
||||||
pool_mode: PoolMode::Transaction,
|
pool_mode: PoolMode::Transaction,
|
||||||
load_balancing_mode: crate::config::LoadBalancingMode::Random,
|
load_balancing_mode: crate::config::LoadBalancingMode::Random,
|
||||||
checkout_failure_limit: None,
|
|
||||||
shards: 2,
|
shards: 2,
|
||||||
user: crate::config::User::default(),
|
user: crate::config::User::default(),
|
||||||
default_role: Some(Role::Replica),
|
default_role: Some(Role::Replica),
|
||||||
@@ -1638,10 +1394,6 @@ mod test {
|
|||||||
auth_query_password: None,
|
auth_query_password: None,
|
||||||
auth_query_user: None,
|
auth_query_user: None,
|
||||||
db: "test".to_string(),
|
db: "test".to_string(),
|
||||||
db_activity_based_routing: PoolSettings::default().db_activity_based_routing,
|
|
||||||
db_activity_init_delay: PoolSettings::default().db_activity_init_delay,
|
|
||||||
db_activity_ttl: PoolSettings::default().db_activity_ttl,
|
|
||||||
table_mutation_cache_ms_ttl: PoolSettings::default().table_mutation_cache_ms_ttl,
|
|
||||||
plugins: None,
|
plugins: None,
|
||||||
};
|
};
|
||||||
let mut qr = QueryRouter::new();
|
let mut qr = QueryRouter::new();
|
||||||
@@ -1700,7 +1452,6 @@ mod test {
|
|||||||
let pool_settings = PoolSettings {
|
let pool_settings = PoolSettings {
|
||||||
pool_mode: PoolMode::Transaction,
|
pool_mode: PoolMode::Transaction,
|
||||||
load_balancing_mode: crate::config::LoadBalancingMode::Random,
|
load_balancing_mode: crate::config::LoadBalancingMode::Random,
|
||||||
checkout_failure_limit: Some(10),
|
|
||||||
shards: 5,
|
shards: 5,
|
||||||
user: crate::config::User::default(),
|
user: crate::config::User::default(),
|
||||||
default_role: Some(Role::Replica),
|
default_role: Some(Role::Replica),
|
||||||
@@ -1721,10 +1472,6 @@ mod test {
|
|||||||
auth_query_password: None,
|
auth_query_password: None,
|
||||||
auth_query_user: None,
|
auth_query_user: None,
|
||||||
db: "test".to_string(),
|
db: "test".to_string(),
|
||||||
db_activity_based_routing: PoolSettings::default().db_activity_based_routing,
|
|
||||||
db_activity_init_delay: PoolSettings::default().db_activity_init_delay,
|
|
||||||
db_activity_ttl: PoolSettings::default().db_activity_ttl,
|
|
||||||
table_mutation_cache_ms_ttl: PoolSettings::default().table_mutation_cache_ms_ttl,
|
|
||||||
plugins: None,
|
plugins: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2140,150 +1887,4 @@ mod test {
|
|||||||
|
|
||||||
assert_eq!(res, Ok(PluginOutput::Allow));
|
assert_eq!(res, Ok(PluginOutput::Allow));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
#[serial]
|
|
||||||
async fn test_db_activity_based_routing_initializing_state() {
|
|
||||||
QueryRouter::setup();
|
|
||||||
let mut qr = QueryRouter::new();
|
|
||||||
qr.pool_settings.db_activity_based_routing = true;
|
|
||||||
qr.pool_settings.query_parser_read_write_splitting = true;
|
|
||||||
qr.pool_settings.query_parser_enabled = true;
|
|
||||||
qr.pool_settings.db = "test_table_mutation_cache".to_string();
|
|
||||||
|
|
||||||
qr.database_activity_cache()
|
|
||||||
.invalidate(&qr.pool_settings.db.clone());
|
|
||||||
|
|
||||||
let query = simple_query("SELECT * FROM some_table");
|
|
||||||
let ast = qr.parse(&query).unwrap();
|
|
||||||
|
|
||||||
// Initially, the database activity should be in the "Initializing" state
|
|
||||||
let state = qr.database_activity_state(&qr.pool_settings.db.clone());
|
|
||||||
assert_eq!(state, DatabaseActivityState::Initializing);
|
|
||||||
|
|
||||||
// Check that the router chooses the primary role due to "Initializing" state
|
|
||||||
assert!(qr.infer(&ast).is_ok());
|
|
||||||
assert_eq!(qr.role(), Some(Role::Primary));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
#[serial]
|
|
||||||
async fn test_db_activity_based_routing_active_state() {
|
|
||||||
QueryRouter::setup();
|
|
||||||
let mut qr = QueryRouter::new();
|
|
||||||
qr.pool_settings.db_activity_based_routing = true;
|
|
||||||
qr.pool_settings.query_parser_read_write_splitting = true;
|
|
||||||
qr.pool_settings.query_parser_enabled = true;
|
|
||||||
qr.pool_settings.db = "test_table_mutation_cache".to_string();
|
|
||||||
|
|
||||||
let db_name = qr.pool_settings.db.clone();
|
|
||||||
let cache = qr.database_activity_cache();
|
|
||||||
cache.insert(db_name.clone(), DatabaseActivityState::Active);
|
|
||||||
|
|
||||||
let query = simple_query("SELECT * FROM some_table");
|
|
||||||
let ast = qr.parse(&query).unwrap();
|
|
||||||
|
|
||||||
// Check that the router can choose a replica role when in "Active" state
|
|
||||||
assert!(qr.infer(&ast).is_ok());
|
|
||||||
assert_eq!(qr.role(), None); // Default should allow replica due to active state
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
#[serial]
|
|
||||||
async fn test_table_mutation_cache_on_write() {
|
|
||||||
QueryRouter::setup();
|
|
||||||
let mut qr = QueryRouter::new();
|
|
||||||
qr.pool_settings.db_activity_based_routing = true;
|
|
||||||
qr.pool_settings.table_mutation_cache_ms_ttl = 20_000; // 20 seconds in milliseconds
|
|
||||||
qr.pool_settings.query_parser_enabled = true;
|
|
||||||
qr.pool_settings.query_parser_read_write_splitting = true;
|
|
||||||
qr.pool_settings.db = "test_table_mutation_cache".to_string();
|
|
||||||
|
|
||||||
qr.database_activity_cache()
|
|
||||||
.invalidate(&qr.pool_settings.db.clone());
|
|
||||||
|
|
||||||
let query = simple_query("UPDATE some_table SET col1 = 'value' WHERE col2 = 1");
|
|
||||||
let ast = qr.parse(&query).unwrap();
|
|
||||||
|
|
||||||
// Simulate the mutation query which should populate the mutation cache
|
|
||||||
assert!(qr.infer(&ast).is_ok());
|
|
||||||
assert_eq!(qr.role(), Some(Role::Primary));
|
|
||||||
|
|
||||||
let table_cache_key = qr.table_mutation_cache_key(Ident::new("some_table"));
|
|
||||||
let cache = qr.table_mutations_cache();
|
|
||||||
|
|
||||||
// Ensure the table mutation cache contains the table with recent write
|
|
||||||
assert!(cache.contains_key(&table_cache_key));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
#[serial]
|
|
||||||
async fn test_db_activity_based_routing_multi_query() {
|
|
||||||
use super::*;
|
|
||||||
use crate::messages::simple_query;
|
|
||||||
use tokio::time::Duration;
|
|
||||||
|
|
||||||
QueryRouter::setup();
|
|
||||||
let mut qr = QueryRouter::new();
|
|
||||||
|
|
||||||
// Configure the pool settings for db_activity_based_routing
|
|
||||||
qr.pool_settings.query_parser_read_write_splitting = true;
|
|
||||||
qr.pool_settings.query_parser_enabled = true;
|
|
||||||
qr.pool_settings.db_activity_based_routing = true;
|
|
||||||
qr.pool_settings.db = "test_db_activity_routing".to_string();
|
|
||||||
|
|
||||||
qr.database_activity_cache()
|
|
||||||
.invalidate(&qr.pool_settings.db.clone());
|
|
||||||
|
|
||||||
// First query when database is initializing
|
|
||||||
let query = simple_query("SELECT * FROM test_table");
|
|
||||||
let ast = qr.parse(&query).unwrap();
|
|
||||||
assert!(qr.infer(&ast).is_ok());
|
|
||||||
// Should route to primary because database is initializing
|
|
||||||
assert_eq!(qr.role(), Some(Role::Primary));
|
|
||||||
|
|
||||||
// Wait for the initialization delay to pass
|
|
||||||
tokio::time::sleep(Duration::from_millis(
|
|
||||||
qr.pool_settings.db_activity_init_delay * 2,
|
|
||||||
))
|
|
||||||
.await;
|
|
||||||
|
|
||||||
// Next query after database is active
|
|
||||||
let query = simple_query("SELECT * FROM test_table");
|
|
||||||
let ast = qr.parse(&query).unwrap();
|
|
||||||
qr.active_role = None; // Reset the active_role
|
|
||||||
assert!(qr.infer(&ast).is_ok());
|
|
||||||
// Should route to replica because database is active and no recent mutations
|
|
||||||
assert_eq!(qr.role(), None);
|
|
||||||
|
|
||||||
// Simulate a write query to update the mutation cache
|
|
||||||
let query = simple_query("INSERT INTO test_table (id, name) VALUES (1, 'test')");
|
|
||||||
let ast = qr.parse(&query).unwrap();
|
|
||||||
qr.active_role = None; // Reset the active_role
|
|
||||||
assert!(qr.infer(&ast).is_ok());
|
|
||||||
// Should route to primary because it's a write operation
|
|
||||||
assert_eq!(qr.role(), Some(Role::Primary));
|
|
||||||
|
|
||||||
// Immediately run a read query on the same table
|
|
||||||
let query = simple_query("SELECT * FROM test_table WHERE id = 1");
|
|
||||||
let ast = qr.parse(&query).unwrap();
|
|
||||||
qr.active_role = None; // Reset the active_role
|
|
||||||
assert!(qr.infer(&ast).is_ok());
|
|
||||||
// Should route to primary because the table was recently mutated
|
|
||||||
assert_eq!(qr.role(), Some(Role::Primary));
|
|
||||||
|
|
||||||
// Wait for the mutation cache TTL to expire
|
|
||||||
tokio::time::sleep(Duration::from_millis(
|
|
||||||
qr.pool_settings.table_mutation_cache_ms_ttl * 2,
|
|
||||||
))
|
|
||||||
.await;
|
|
||||||
|
|
||||||
// Run the read query again after cache expiration
|
|
||||||
let query = simple_query("SELECT * FROM test_table WHERE id = 1");
|
|
||||||
let ast = qr.parse(&query).unwrap();
|
|
||||||
qr.active_role = None; // Reset the active_role
|
|
||||||
assert!(qr.infer(&ast).is_ok());
|
|
||||||
// Should route to replica because mutation cache has expired
|
|
||||||
assert_eq!(qr.role(), None);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ pub enum ShardingFunction {
|
|||||||
Sha1,
|
Sha1,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for ShardingFunction {
|
impl ToString for ShardingFunction {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn to_string(&self) -> String {
|
||||||
match self {
|
match *self {
|
||||||
ShardingFunction::PgBigintHash => write!(f, "pg_bigint_hash"),
|
ShardingFunction::PgBigintHash => "pg_bigint_hash".to_string(),
|
||||||
ShardingFunction::Sha1 => write!(f, "sha1"),
|
ShardingFunction::Sha1 => "sha1".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
GREEN="\033[0;32m"
|
|
||||||
RED="\033[0;31m"
|
|
||||||
BLUE="\033[0;34m"
|
|
||||||
RESET="\033[0m"
|
|
||||||
|
|
||||||
|
|
||||||
cd tests/docker/
|
|
||||||
docker compose kill main || true
|
|
||||||
docker compose build main
|
|
||||||
docker compose down
|
|
||||||
docker compose up -d
|
|
||||||
# wait for the container to start
|
|
||||||
while ! docker compose exec main ls; do
|
|
||||||
echo "Waiting for test environment to start"
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
echo "==================================="
|
|
||||||
docker compose exec -e LOG_LEVEL=error -d main toxiproxy-server
|
|
||||||
docker compose exec --workdir /app main cargo build
|
|
||||||
docker compose exec -d --workdir /app main ./target/debug/pgcat ./.circleci/pgcat.toml
|
|
||||||
docker compose exec --workdir /app/tests/ruby main bundle install
|
|
||||||
docker compose exec --workdir /app/tests/python main pip3 install -r requirements.txt
|
|
||||||
echo "Interactive test environment ready"
|
|
||||||
echo "To run integration tests, you can use the following commands:"
|
|
||||||
echo -e " ${BLUE}Ruby: ${RED}cd /app/tests/ruby && bundle exec ruby tests.rb --format documentation${RESET}"
|
|
||||||
echo -e " ${BLUE}Python: ${RED}cd /app/ && pytest ${RESET}"
|
|
||||||
echo -e " ${BLUE}Rust: ${RED}cd /app/tests/rust && cargo run ${RESET}"
|
|
||||||
echo -e " ${BLUE}Go: ${RED}cd /app/tests/go && /usr/local/go/bin/go test${RESET}"
|
|
||||||
echo "the source code for tests are directly linked to the source code in the container so you can modify the code and run the tests again"
|
|
||||||
echo "You can rebuild PgCat from within the container by running"
|
|
||||||
echo -e " ${GREEN}cargo build${RESET}"
|
|
||||||
echo "and then run the tests again"
|
|
||||||
echo "==================================="
|
|
||||||
docker compose exec --workdir /app/tests main bash
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM rust:1.81.0-slim-bookworm
|
FROM rust:bullseye
|
||||||
|
|
||||||
COPY --from=sclevine/yj /bin/yj /bin/yj
|
COPY --from=sclevine/yj /bin/yj /bin/yj
|
||||||
RUN /bin/yj -h
|
RUN /bin/yj -h
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
version: "3"
|
||||||
services:
|
services:
|
||||||
pg1:
|
pg1:
|
||||||
image: postgres:14
|
image: postgres:14
|
||||||
@@ -47,8 +48,6 @@ 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,38 +5,6 @@ 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
|
||||||
|
|||||||
@@ -1,3 +1,2 @@
|
|||||||
pytest
|
|
||||||
psycopg2==2.9.3
|
psycopg2==2.9.3
|
||||||
psutil==5.9.1
|
psutil==5.9.1
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
import utils
|
|
||||||
import signal
|
|
||||||
|
|
||||||
class TestTrustAuth:
|
|
||||||
@classmethod
|
|
||||||
def setup_method(cls):
|
|
||||||
config= """
|
|
||||||
[general]
|
|
||||||
host = "0.0.0.0"
|
|
||||||
port = 6432
|
|
||||||
admin_username = "admin_user"
|
|
||||||
admin_password = ""
|
|
||||||
admin_auth_type = "trust"
|
|
||||||
|
|
||||||
[pools.sharded_db.users.0]
|
|
||||||
username = "sharding_user"
|
|
||||||
password = "sharding_user"
|
|
||||||
auth_type = "trust"
|
|
||||||
pool_size = 10
|
|
||||||
min_pool_size = 1
|
|
||||||
pool_mode = "transaction"
|
|
||||||
|
|
||||||
[pools.sharded_db.shards.0]
|
|
||||||
servers = [
|
|
||||||
[ "127.0.0.1", 5432, "primary" ],
|
|
||||||
]
|
|
||||||
database = "shard0"
|
|
||||||
"""
|
|
||||||
utils.pgcat_generic_start(config)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def teardown_method(self):
|
|
||||||
utils.pg_cat_send_signal(signal.SIGTERM)
|
|
||||||
|
|
||||||
def test_admin_trust_auth(self):
|
|
||||||
conn, cur = utils.connect_db_trust(admin=True)
|
|
||||||
cur.execute("SHOW POOLS")
|
|
||||||
res = cur.fetchall()
|
|
||||||
print(res)
|
|
||||||
utils.cleanup_conn(conn, cur)
|
|
||||||
|
|
||||||
def test_normal_trust_auth(self):
|
|
||||||
conn, cur = utils.connect_db_trust(autocommit=False)
|
|
||||||
cur.execute("SELECT 1")
|
|
||||||
res = cur.fetchall()
|
|
||||||
print(res)
|
|
||||||
utils.cleanup_conn(conn, cur)
|
|
||||||
|
|
||||||
class TestMD5Auth:
|
|
||||||
@classmethod
|
|
||||||
def setup_method(cls):
|
|
||||||
utils.pgcat_start()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def teardown_method(self):
|
|
||||||
utils.pg_cat_send_signal(signal.SIGTERM)
|
|
||||||
|
|
||||||
def test_normal_db_access(self):
|
|
||||||
conn, cur = utils.connect_db(autocommit=False)
|
|
||||||
cur.execute("SELECT 1")
|
|
||||||
res = cur.fetchall()
|
|
||||||
print(res)
|
|
||||||
utils.cleanup_conn(conn, cur)
|
|
||||||
|
|
||||||
def test_admin_db_access(self):
|
|
||||||
conn, cur = utils.connect_db(admin=True)
|
|
||||||
|
|
||||||
cur.execute("SHOW POOLS")
|
|
||||||
res = cur.fetchall()
|
|
||||||
print(res)
|
|
||||||
utils.cleanup_conn(conn, cur)
|
|
||||||
@@ -1,12 +1,84 @@
|
|||||||
|
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():
|
||||||
|
|
||||||
@@ -14,17 +86,17 @@ def test_shutdown_logic():
|
|||||||
# NO ACTIVE QUERIES SIGINT HANDLING
|
# NO ACTIVE QUERIES SIGINT HANDLING
|
||||||
|
|
||||||
# Start pgcat
|
# Start pgcat
|
||||||
utils.pgcat_start()
|
pgcat_start()
|
||||||
|
|
||||||
# Create client connection and send query (not in transaction)
|
# Create client connection and send query (not in transaction)
|
||||||
conn, cur = utils.connect_db()
|
conn, cur = 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
|
||||||
utils.pg_cat_send_signal(signal.SIGINT)
|
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
|
||||||
@@ -36,18 +108,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")
|
||||||
|
|
||||||
utils.cleanup_conn(conn, cur)
|
cleanup_conn(conn, cur)
|
||||||
utils.pg_cat_send_signal(signal.SIGTERM)
|
pg_cat_send_signal(signal.SIGTERM)
|
||||||
|
|
||||||
# - - - - - - - - - - - - - - - - - -
|
# - - - - - - - - - - - - - - - - - -
|
||||||
# NO ACTIVE QUERIES ADMIN SHUTDOWN COMMAND
|
# NO ACTIVE QUERIES ADMIN SHUTDOWN COMMAND
|
||||||
|
|
||||||
# Start pgcat
|
# Start pgcat
|
||||||
utils.pgcat_start()
|
pgcat_start()
|
||||||
|
|
||||||
# Create client connection and begin transaction
|
# Create client connection and begin transaction
|
||||||
conn, cur = utils.connect_db()
|
conn, cur = connect_db()
|
||||||
admin_conn, admin_cur = utils.connect_db(admin=True)
|
admin_conn, admin_cur = connect_db(admin=True)
|
||||||
|
|
||||||
cur.execute("BEGIN;")
|
cur.execute("BEGIN;")
|
||||||
cur.execute("SELECT 1;")
|
cur.execute("SELECT 1;")
|
||||||
@@ -66,24 +138,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")
|
||||||
|
|
||||||
utils.cleanup_conn(conn, cur)
|
cleanup_conn(conn, cur)
|
||||||
utils.cleanup_conn(admin_conn, admin_cur)
|
cleanup_conn(admin_conn, admin_cur)
|
||||||
utils.pg_cat_send_signal(signal.SIGTERM)
|
pg_cat_send_signal(signal.SIGTERM)
|
||||||
|
|
||||||
# - - - - - - - - - - - - - - - - - -
|
# - - - - - - - - - - - - - - - - - -
|
||||||
# HANDLE TRANSACTION WITH SIGINT
|
# HANDLE TRANSACTION WITH SIGINT
|
||||||
|
|
||||||
# Start pgcat
|
# Start pgcat
|
||||||
utils.pgcat_start()
|
pgcat_start()
|
||||||
|
|
||||||
# Create client connection and begin transaction
|
# Create client connection and begin transaction
|
||||||
conn, cur = utils.connect_db()
|
conn, cur = 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
|
||||||
utils.pg_cat_send_signal(signal.SIGINT)
|
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
|
||||||
@@ -93,18 +165,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)
|
||||||
|
|
||||||
utils.cleanup_conn(conn, cur)
|
cleanup_conn(conn, cur)
|
||||||
utils.pg_cat_send_signal(signal.SIGTERM)
|
pg_cat_send_signal(signal.SIGTERM)
|
||||||
|
|
||||||
# - - - - - - - - - - - - - - - - - -
|
# - - - - - - - - - - - - - - - - - -
|
||||||
# HANDLE TRANSACTION WITH ADMIN SHUTDOWN COMMAND
|
# HANDLE TRANSACTION WITH ADMIN SHUTDOWN COMMAND
|
||||||
|
|
||||||
# Start pgcat
|
# Start pgcat
|
||||||
utils.pgcat_start()
|
pgcat_start()
|
||||||
|
|
||||||
# Create client connection and begin transaction
|
# Create client connection and begin transaction
|
||||||
conn, cur = utils.connect_db()
|
conn, cur = connect_db()
|
||||||
admin_conn, admin_cur = utils.connect_db(admin=True)
|
admin_conn, admin_cur = connect_db(admin=True)
|
||||||
|
|
||||||
cur.execute("BEGIN;")
|
cur.execute("BEGIN;")
|
||||||
cur.execute("SELECT 1;")
|
cur.execute("SELECT 1;")
|
||||||
@@ -122,30 +194,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)
|
||||||
|
|
||||||
utils.cleanup_conn(conn, cur)
|
cleanup_conn(conn, cur)
|
||||||
utils.cleanup_conn(admin_conn, admin_cur)
|
cleanup_conn(admin_conn, admin_cur)
|
||||||
utils.pg_cat_send_signal(signal.SIGTERM)
|
pg_cat_send_signal(signal.SIGTERM)
|
||||||
|
|
||||||
# - - - - - - - - - - - - - - - - - -
|
# - - - - - - - - - - - - - - - - - -
|
||||||
# NO NEW NON-ADMIN CONNECTIONS DURING SHUTDOWN
|
# NO NEW NON-ADMIN CONNECTIONS DURING SHUTDOWN
|
||||||
# Start pgcat
|
# Start pgcat
|
||||||
utils.pgcat_start()
|
pgcat_start()
|
||||||
|
|
||||||
# Create client connection and begin transaction
|
# Create client connection and begin transaction
|
||||||
transaction_conn, transaction_cur = utils.connect_db()
|
transaction_conn, transaction_cur = 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
|
||||||
utils.pg_cat_send_signal(signal.SIGINT)
|
pg_cat_send_signal(signal.SIGINT)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
start = time.perf_counter()
|
start = time.perf_counter()
|
||||||
try:
|
try:
|
||||||
conn, cur = utils.connect_db()
|
conn, cur = connect_db()
|
||||||
cur.execute("SELECT 1;")
|
cur.execute("SELECT 1;")
|
||||||
utils.cleanup_conn(conn, cur)
|
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:
|
||||||
@@ -155,49 +227,49 @@ def test_shutdown_logic():
|
|||||||
else:
|
else:
|
||||||
raise Exception("Able connect to database during shutdown")
|
raise Exception("Able connect to database during shutdown")
|
||||||
|
|
||||||
utils.cleanup_conn(transaction_conn, transaction_cur)
|
cleanup_conn(transaction_conn, transaction_cur)
|
||||||
utils.pg_cat_send_signal(signal.SIGTERM)
|
pg_cat_send_signal(signal.SIGTERM)
|
||||||
|
|
||||||
# - - - - - - - - - - - - - - - - - -
|
# - - - - - - - - - - - - - - - - - -
|
||||||
# ALLOW NEW ADMIN CONNECTIONS DURING SHUTDOWN
|
# ALLOW NEW ADMIN CONNECTIONS DURING SHUTDOWN
|
||||||
# Start pgcat
|
# Start pgcat
|
||||||
utils.pgcat_start()
|
pgcat_start()
|
||||||
|
|
||||||
# Create client connection and begin transaction
|
# Create client connection and begin transaction
|
||||||
transaction_conn, transaction_cur = utils.connect_db()
|
transaction_conn, transaction_cur = 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
|
||||||
utils.pg_cat_send_signal(signal.SIGINT)
|
pg_cat_send_signal(signal.SIGINT)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
conn, cur = utils.connect_db(admin=True)
|
conn, cur = connect_db(admin=True)
|
||||||
cur.execute("SHOW DATABASES;")
|
cur.execute("SHOW DATABASES;")
|
||||||
utils.cleanup_conn(conn, cur)
|
cleanup_conn(conn, cur)
|
||||||
except psycopg2.OperationalError as e:
|
except psycopg2.OperationalError as e:
|
||||||
raise Exception(e)
|
raise Exception(e)
|
||||||
|
|
||||||
utils.cleanup_conn(transaction_conn, transaction_cur)
|
cleanup_conn(transaction_conn, transaction_cur)
|
||||||
utils.pg_cat_send_signal(signal.SIGTERM)
|
pg_cat_send_signal(signal.SIGTERM)
|
||||||
|
|
||||||
# - - - - - - - - - - - - - - - - - -
|
# - - - - - - - - - - - - - - - - - -
|
||||||
# ADMIN CONNECTIONS CONTINUING TO WORK AFTER SHUTDOWN
|
# ADMIN CONNECTIONS CONTINUING TO WORK AFTER SHUTDOWN
|
||||||
# Start pgcat
|
# Start pgcat
|
||||||
utils.pgcat_start()
|
pgcat_start()
|
||||||
|
|
||||||
# Create client connection and begin transaction
|
# Create client connection and begin transaction
|
||||||
transaction_conn, transaction_cur = utils.connect_db()
|
transaction_conn, transaction_cur = connect_db()
|
||||||
transaction_cur.execute("BEGIN;")
|
transaction_cur.execute("BEGIN;")
|
||||||
transaction_cur.execute("SELECT 1;")
|
transaction_cur.execute("SELECT 1;")
|
||||||
|
|
||||||
admin_conn, admin_cur = utils.connect_db(admin=True)
|
admin_conn, admin_cur = 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
|
||||||
utils.pg_cat_send_signal(signal.SIGINT)
|
pg_cat_send_signal(signal.SIGINT)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -205,24 +277,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)
|
||||||
|
|
||||||
utils.cleanup_conn(transaction_conn, transaction_cur)
|
cleanup_conn(transaction_conn, transaction_cur)
|
||||||
utils.cleanup_conn(admin_conn, admin_cur)
|
cleanup_conn(admin_conn, admin_cur)
|
||||||
utils.pg_cat_send_signal(signal.SIGTERM)
|
pg_cat_send_signal(signal.SIGTERM)
|
||||||
|
|
||||||
# - - - - - - - - - - - - - - - - - -
|
# - - - - - - - - - - - - - - - - - -
|
||||||
# HANDLE SHUTDOWN TIMEOUT WITH SIGINT
|
# HANDLE SHUTDOWN TIMEOUT WITH SIGINT
|
||||||
|
|
||||||
# Start pgcat
|
# Start pgcat
|
||||||
utils.pgcat_start()
|
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 = utils.connect_db()
|
conn, cur = 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
|
||||||
utils.pg_cat_send_signal(signal.SIGINT)
|
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)
|
||||||
@@ -236,7 +308,12 @@ 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")
|
||||||
|
|
||||||
utils.cleanup_conn(conn, cur)
|
cleanup_conn(conn, cur)
|
||||||
utils.pg_cat_send_signal(signal.SIGTERM)
|
pg_cat_send_signal(signal.SIGTERM)
|
||||||
|
|
||||||
# - - - - - - - - - - - - - - - - - -
|
# - - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
|
|
||||||
|
test_normal_db_access()
|
||||||
|
test_admin_db_access()
|
||||||
|
test_shutdown_logic()
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
import os
|
|
||||||
import signal
|
|
||||||
import time
|
|
||||||
from typing import Tuple
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
import psutil
|
|
||||||
import psycopg2
|
|
||||||
|
|
||||||
PGCAT_HOST = "127.0.0.1"
|
|
||||||
PGCAT_PORT = "6432"
|
|
||||||
|
|
||||||
|
|
||||||
def _pgcat_start(config_path: str):
|
|
||||||
pg_cat_send_signal(signal.SIGTERM)
|
|
||||||
os.system(f"./target/debug/pgcat {config_path} &")
|
|
||||||
time.sleep(2)
|
|
||||||
|
|
||||||
|
|
||||||
def pgcat_start():
|
|
||||||
_pgcat_start(config_path='.circleci/pgcat.toml')
|
|
||||||
|
|
||||||
|
|
||||||
def pgcat_generic_start(config: str):
|
|
||||||
tmp = tempfile.NamedTemporaryFile()
|
|
||||||
with open(tmp.name, 'w') as f:
|
|
||||||
f.write(config)
|
|
||||||
_pgcat_start(config_path=tmp.name)
|
|
||||||
|
|
||||||
|
|
||||||
def glauth_send_signal(signal: signal.Signals):
|
|
||||||
try:
|
|
||||||
for proc in psutil.process_iter(["pid", "name"]):
|
|
||||||
if proc.name() == "glauth":
|
|
||||||
os.kill(proc.pid, signal)
|
|
||||||
except Exception as e:
|
|
||||||
# The process can be gone when we send this signal
|
|
||||||
print(e)
|
|
||||||
|
|
||||||
if signal == signal.SIGTERM:
|
|
||||||
# Returns 0 if pgcat process exists
|
|
||||||
time.sleep(2)
|
|
||||||
if not os.system('pgrep glauth'):
|
|
||||||
raise Exception("glauth not closed after SIGTERM")
|
|
||||||
|
|
||||||
|
|
||||||
def pg_cat_send_signal(signal: signal.Signals):
|
|
||||||
try:
|
|
||||||
for proc in psutil.process_iter(["pid", "name"]):
|
|
||||||
if "pgcat" == proc.name():
|
|
||||||
os.kill(proc.pid, signal)
|
|
||||||
except Exception as e:
|
|
||||||
# The process can be gone when we send this signal
|
|
||||||
print(e)
|
|
||||||
|
|
||||||
if signal == signal.SIGTERM:
|
|
||||||
# Returns 0 if pgcat process exists
|
|
||||||
time.sleep(2)
|
|
||||||
if not os.system('pgrep pgcat'):
|
|
||||||
raise Exception("pgcat not closed after SIGTERM")
|
|
||||||
|
|
||||||
|
|
||||||
def connect_db(
|
|
||||||
autocommit: bool = True,
|
|
||||||
admin: bool = False,
|
|
||||||
) -> Tuple[psycopg2.extensions.connection, psycopg2.extensions.cursor]:
|
|
||||||
|
|
||||||
if admin:
|
|
||||||
user = "admin_user"
|
|
||||||
password = "admin_pass"
|
|
||||||
db = "pgcat"
|
|
||||||
else:
|
|
||||||
user = "sharding_user"
|
|
||||||
password = "sharding_user"
|
|
||||||
db = "sharded_db"
|
|
||||||
|
|
||||||
conn = psycopg2.connect(
|
|
||||||
f"postgres://{user}:{password}@{PGCAT_HOST}:{PGCAT_PORT}/{db}?application_name=testing_pgcat",
|
|
||||||
connect_timeout=2,
|
|
||||||
)
|
|
||||||
conn.autocommit = autocommit
|
|
||||||
cur = conn.cursor()
|
|
||||||
|
|
||||||
return (conn, cur)
|
|
||||||
|
|
||||||
def connect_db_trust(
|
|
||||||
autocommit: bool = True,
|
|
||||||
admin: bool = False,
|
|
||||||
) -> Tuple[psycopg2.extensions.connection, psycopg2.extensions.cursor]:
|
|
||||||
|
|
||||||
if admin:
|
|
||||||
user = "admin_user"
|
|
||||||
db = "pgcat"
|
|
||||||
else:
|
|
||||||
user = "sharding_user"
|
|
||||||
db = "sharded_db"
|
|
||||||
|
|
||||||
conn = psycopg2.connect(
|
|
||||||
f"postgres://{user}@{PGCAT_HOST}:{PGCAT_PORT}/{db}?application_name=testing_pgcat",
|
|
||||||
connect_timeout=2,
|
|
||||||
)
|
|
||||||
conn.autocommit = autocommit
|
|
||||||
cur = conn.cursor()
|
|
||||||
|
|
||||||
return (conn, cur)
|
|
||||||
|
|
||||||
|
|
||||||
def cleanup_conn(conn: psycopg2.extensions.connection, cur: psycopg2.extensions.cursor):
|
|
||||||
cur.close()
|
|
||||||
conn.close()
|
|
||||||
@@ -1,33 +1,22 @@
|
|||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
activemodel (7.1.4)
|
activemodel (7.0.4.1)
|
||||||
activesupport (= 7.1.4)
|
activesupport (= 7.0.4.1)
|
||||||
activerecord (7.1.4)
|
activerecord (7.0.4.1)
|
||||||
activemodel (= 7.1.4)
|
activemodel (= 7.0.4.1)
|
||||||
activesupport (= 7.1.4)
|
activesupport (= 7.0.4.1)
|
||||||
timeout (>= 0.4.0)
|
activesupport (7.0.4.1)
|
||||||
activesupport (7.1.4)
|
|
||||||
base64
|
|
||||||
bigdecimal
|
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
connection_pool (>= 2.2.5)
|
|
||||||
drb
|
|
||||||
i18n (>= 1.6, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
minitest (>= 5.1)
|
minitest (>= 5.1)
|
||||||
mutex_m
|
|
||||||
tzinfo (~> 2.0)
|
tzinfo (~> 2.0)
|
||||||
ast (2.4.2)
|
ast (2.4.2)
|
||||||
base64 (0.2.0)
|
concurrent-ruby (1.1.10)
|
||||||
bigdecimal (3.1.8)
|
|
||||||
concurrent-ruby (1.3.4)
|
|
||||||
connection_pool (2.4.1)
|
|
||||||
diff-lcs (1.5.0)
|
diff-lcs (1.5.0)
|
||||||
drb (2.2.1)
|
i18n (1.12.0)
|
||||||
i18n (1.14.5)
|
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
minitest (5.25.1)
|
minitest (5.17.0)
|
||||||
mutex_m (0.2.0)
|
|
||||||
parallel (1.22.1)
|
parallel (1.22.1)
|
||||||
parser (3.1.2.0)
|
parser (3.1.2.0)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
@@ -35,8 +24,7 @@ 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.3.6)
|
rexml (3.2.5)
|
||||||
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)
|
||||||
@@ -62,12 +50,10 @@ 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)
|
|
||||||
timeout (0.4.1)
|
|
||||||
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)
|
||||||
tzinfo (2.0.6)
|
tzinfo (2.0.5)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
unicode-display_width (2.1.0)
|
unicode-display_width (2.1.0)
|
||||||
|
|
||||||
|
|||||||
@@ -91,27 +91,6 @@ describe "Admin" do
|
|||||||
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
|
describe "PAUSE" do
|
||||||
it "pauses all pools" do
|
it "pauses all pools" do
|
||||||
admin_conn = PG::connect(processes.pgcat.admin_connection_string)
|
admin_conn = PG::connect(processes.pgcat.admin_connection_string)
|
||||||
|
|||||||
@@ -56,41 +56,6 @@ describe "Random Load Balancing" do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when all replicas are down " do
|
|
||||||
let(:processes) { Helpers::Pgcat.single_shard_setup("sharded_db", 5, "transaction", "random", "debug", {"default_role" => "replica"}) }
|
|
||||||
|
|
||||||
it "unbans them automatically to prevent false positives in health checks that could make all replicas unavailable" do
|
|
||||||
conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
|
|
||||||
failed_count = 0
|
|
||||||
number_of_replicas = processes[:replicas].length
|
|
||||||
|
|
||||||
# Take down all replicas
|
|
||||||
processes[:replicas].each(&:take_down)
|
|
||||||
|
|
||||||
(number_of_replicas + 1).times do |n|
|
|
||||||
conn.async_exec("SELECT 1 + 2")
|
|
||||||
rescue
|
|
||||||
conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
|
|
||||||
failed_count += 1
|
|
||||||
end
|
|
||||||
|
|
||||||
expect(failed_count).to eq(number_of_replicas + 1)
|
|
||||||
failed_count = 0
|
|
||||||
|
|
||||||
# Ban_time is configured to 60 so this reset will only work
|
|
||||||
# if the replicas are unbanned automatically
|
|
||||||
processes[:replicas].each(&:reset)
|
|
||||||
|
|
||||||
number_of_replicas.times do
|
|
||||||
conn.async_exec("SELECT 1 + 2")
|
|
||||||
rescue
|
|
||||||
conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
|
|
||||||
failed_count += 1
|
|
||||||
end
|
|
||||||
expect(failed_count).to eq(0)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "Least Outstanding Queries Load Balancing" do
|
describe "Least Outstanding Queries Load Balancing" do
|
||||||
@@ -196,3 +161,4 @@ describe "Least Outstanding Queries Load Balancing" do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -188,102 +188,6 @@ describe "Miscellaneous" do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "Checkout failure limit" do
|
|
||||||
context "when no checkout failure limit is set" do
|
|
||||||
before do
|
|
||||||
new_configs = processes.pgcat.current_config
|
|
||||||
new_configs["general"]["connect_timeout"] = 200
|
|
||||||
new_configs["pools"]["sharded_db"]["users"]["0"]["pool_size"] = 1
|
|
||||||
processes.pgcat.update_config(new_configs)
|
|
||||||
processes.pgcat.reload_config
|
|
||||||
sleep 0.5
|
|
||||||
end
|
|
||||||
|
|
||||||
it "does not disconnect client" do
|
|
||||||
Array.new(5) do
|
|
||||||
Thread.new do
|
|
||||||
conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
|
|
||||||
for i in 0..4
|
|
||||||
begin
|
|
||||||
conn.async_exec("SELECT pg_sleep(0.5);")
|
|
||||||
expect(conn.status).to eq(PG::CONNECTION_OK)
|
|
||||||
rescue PG::SystemError
|
|
||||||
expect(conn.status).to eq(PG::CONNECTION_OK)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
conn.close
|
|
||||||
end
|
|
||||||
end.each(&:join)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context "when checkout failure limit is set high" do
|
|
||||||
before do
|
|
||||||
new_configs = processes.pgcat.current_config
|
|
||||||
new_configs["general"]["connect_timeout"] = 200
|
|
||||||
new_configs["pools"]["sharded_db"]["users"]["0"]["pool_size"] = 1
|
|
||||||
new_configs["pools"]["sharded_db"]["checkout_failure_limit"] = 10000
|
|
||||||
processes.pgcat.update_config(new_configs)
|
|
||||||
processes.pgcat.reload_config
|
|
||||||
sleep 0.5
|
|
||||||
end
|
|
||||||
|
|
||||||
it "does not disconnect client" do
|
|
||||||
Array.new(5) do
|
|
||||||
Thread.new do
|
|
||||||
conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
|
|
||||||
for i in 0..4
|
|
||||||
begin
|
|
||||||
conn.async_exec("SELECT pg_sleep(0.5);")
|
|
||||||
expect(conn.status).to eq(PG::CONNECTION_OK)
|
|
||||||
rescue PG::SystemError
|
|
||||||
expect(conn.status).to eq(PG::CONNECTION_OK)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
conn.close
|
|
||||||
end
|
|
||||||
end.each(&:join)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context "when checkout failure limit is set low" do
|
|
||||||
before do
|
|
||||||
new_configs = processes.pgcat.current_config
|
|
||||||
new_configs["general"]["connect_timeout"] = 200
|
|
||||||
new_configs["pools"]["sharded_db"]["users"]["0"]["pool_size"] = 1
|
|
||||||
new_configs["pools"]["sharded_db"]["checkout_failure_limit"] = 2
|
|
||||||
processes.pgcat.update_config(new_configs)
|
|
||||||
processes.pgcat.reload_config
|
|
||||||
sleep 0.5
|
|
||||||
end
|
|
||||||
|
|
||||||
it "disconnects client after reaching limit" do
|
|
||||||
Array.new(5) do
|
|
||||||
Thread.new do
|
|
||||||
conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
|
|
||||||
checkout_failure_count = 0
|
|
||||||
for i in 0..4
|
|
||||||
begin
|
|
||||||
conn.async_exec("SELECT pg_sleep(1);")
|
|
||||||
expect(conn.status).to eq(PG::CONNECTION_OK)
|
|
||||||
rescue PG::SystemError
|
|
||||||
checkout_failure_count += 1
|
|
||||||
expect(conn.status).to eq(PG::CONNECTION_OK)
|
|
||||||
rescue PG::ConnectionBad
|
|
||||||
expect(checkout_failure_count).to eq(2)
|
|
||||||
expect(conn.status).to eq(PG::CONNECTION_BAD)
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
conn.close
|
|
||||||
end
|
|
||||||
end.each(&:join)
|
|
||||||
puts processes.pgcat.logs
|
|
||||||
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "Server version reporting" do
|
describe "Server version reporting" do
|
||||||
it "reports correct version for normal and admin databases" do
|
it "reports correct version for normal and admin databases" do
|
||||||
server_conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
|
server_conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
|
||||||
|
|||||||
664
tests/rust/Cargo.lock
generated
664
tests/rust/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -15,11 +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 i in 0..1000 {
|
for _ in 0..1000 {
|
||||||
match sqlx::query(&format!("SELECT {:?}", i % 5)).fetch_all(&pool).await {
|
match sqlx::query("SELECT one").fetch_all(&pool).await {
|
||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
panic!("prepared statement error: {}", err);
|
if err.to_string().contains("prepared statement") {
|
||||||
|
panic!("prepared statement error: {}", err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user